Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
562d278f60 | ||
|
|
c6f6faf64f | ||
|
|
1c3ee9e8c7 | ||
|
|
aa91063211 | ||
|
|
fa2d5d40dd | ||
|
|
6ccef162f6 | ||
|
|
48dac139a5 |
@@ -157,6 +157,16 @@ The initial implementation includes provider-neutral orchestration helpers:
|
|||||||
- `apply_configuration_package(...)`
|
- `apply_configuration_package(...)`
|
||||||
- `export_configuration_package(...)`
|
- `export_configuration_package(...)`
|
||||||
|
|
||||||
|
Portable fragments may bind deployment-specific operator input without placing
|
||||||
|
that value in the signed reusable definition. A payload value of
|
||||||
|
`{"$data": "requirement_key"}` references a key declared in the manifest's
|
||||||
|
`data_requirements`. Preflight fails before invoking the owning provider when a
|
||||||
|
reference is malformed, undeclared, or unresolved. Once supplied, Core replaces
|
||||||
|
the reference in memory and passes only the resolved fragment to the provider.
|
||||||
|
This mechanism is for deployment bindings and wording, not plaintext secrets:
|
||||||
|
credential-envelope or environment references remain the normal portable
|
||||||
|
boundary.
|
||||||
|
|
||||||
The first concrete provider is `govoplan_access.backend.configuration_provider`.
|
The first concrete provider is `govoplan_access.backend.configuration_provider`.
|
||||||
It supports access-owned `roles`, `groups`, and `group_role_assignments`
|
It supports access-owned `roles`, `groups`, and `group_role_assignments`
|
||||||
fragments and applies them idempotently. Mail and Files also register providers
|
fragments and applies them idempotently. Mail and Files also register providers
|
||||||
@@ -212,6 +222,14 @@ The admin wizard backend starts with these routes:
|
|||||||
10. Store import provenance, package version, supplied non-secret metadata, and
|
10. Store import provenance, package version, supplied non-secret metadata, and
|
||||||
audit events.
|
audit events.
|
||||||
|
|
||||||
|
Provider applies may commit independently. Core therefore stops at the first
|
||||||
|
apply or health blocker and reports an explicit rollback state. A blocked
|
||||||
|
preflight or a no-op needs no recovery; a successful multi-provider mutation
|
||||||
|
retains the reviewed pre-apply database snapshot as its generic rollback path;
|
||||||
|
a later-provider failure is reported as a partial apply that requires snapshot
|
||||||
|
recovery or an explicitly supported module-owned compensation. The generic
|
||||||
|
wizard never claims atomic cross-module undo.
|
||||||
|
|
||||||
The wizard should display everything necessary and nothing unnecessary. Generic
|
The wizard should display everything necessary and nothing unnecessary. Generic
|
||||||
sections should cover package trust, dependency plan, required data, conflicts,
|
sections should cover package trust, dependency plan, required data, conflicts,
|
||||||
review, and result. Module-specific fields should appear only when the selected
|
review, and result. Module-specific fields should appear only when the selected
|
||||||
@@ -262,6 +280,11 @@ Exported packages should record provenance: source GovOPlaN version, module
|
|||||||
versions, exporter identity, timestamp, selected scope, redactions, and
|
versions, exporter identity, timestamp, selected scope, redactions, and
|
||||||
validation status.
|
validation status.
|
||||||
|
|
||||||
|
The orchestrator emits this provenance independently of provider payloads and
|
||||||
|
lists secret requirement keys as redacted without serializing their supplied
|
||||||
|
values. Providers still own the deeper rule that credentials, tokens, and
|
||||||
|
decrypted envelope contents must never appear in exported fragments.
|
||||||
|
|
||||||
## Catalogs And Trust
|
## Catalogs And Trust
|
||||||
|
|
||||||
Configuration catalogs should follow the existing module package catalog model:
|
Configuration catalogs should follow the existing module package catalog model:
|
||||||
|
|||||||
@@ -18,6 +18,27 @@ The platform inventory recognizes both inline locale objects and generated
|
|||||||
catalogs declared as `const de` / `const en`. Its strict mode requires both
|
catalogs declared as `const de` / `const en`. Its strict mode requires both
|
||||||
locales and reports `de` explicitly as the reference locale.
|
locales and reports `de` explicitly as the reference locale.
|
||||||
|
|
||||||
|
## Structured Documentation Localization
|
||||||
|
|
||||||
|
`DocumentationTopic.translations` continues to own localized title, summary,
|
||||||
|
and body prose. Topics whose metadata contains rendered prose opt into the
|
||||||
|
separate `structured_translation_version="1"` contract and provide a complete
|
||||||
|
same-shape value for each translated metadata key in
|
||||||
|
`structured_translations`. Version 1 covers workflow prerequisites, steps,
|
||||||
|
outcome, result and verification; reference fields; limitations, constraints,
|
||||||
|
consequences and consequence classes; and the other rendered explanation
|
||||||
|
fields declared by Core.
|
||||||
|
|
||||||
|
The registry rejects an unversioned translation, an unsupported contract
|
||||||
|
version, missing structured keys, changed object keys or list lengths, empty
|
||||||
|
translated strings, and changed non-text values. Stable field IDs, routes,
|
||||||
|
permission scopes, and other technical leaves therefore remain structurally
|
||||||
|
bound to the source metadata. The Docs module overlays only a validated locale
|
||||||
|
at response time and reports the selected structured locale separately from the
|
||||||
|
title/body locale. Missing structured translations fall back to source content
|
||||||
|
and remain visible in public coverage until the owning module adopts the
|
||||||
|
contract.
|
||||||
|
|
||||||
## Help Resolution
|
## Help Resolution
|
||||||
|
|
||||||
Every focusable field and action receives a stable derived F1 identity from the
|
Every focusable field and action receives a stable derived F1 identity from the
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "govoplan-core"
|
name = "govoplan-core"
|
||||||
version = "0.1.30"
|
version = "0.1.37"
|
||||||
description = "Reusable GovOPlaN platform core, access, tenancy, and RBAC components."
|
description = "Reusable GovOPlaN platform core, access, tenancy, and RBAC components."
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ from __future__ import annotations
|
|||||||
import base64
|
import base64
|
||||||
from collections.abc import Mapping, Sequence
|
from collections.abc import Mapping, Sequence
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
|
from datetime import UTC, datetime
|
||||||
|
from importlib.metadata import PackageNotFoundError, version as package_version
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
@@ -38,6 +40,12 @@ CONFIGURATION_PROVIDER_CAPABILITY = "configuration.provider"
|
|||||||
|
|
||||||
DiagnosticSeverity = Literal["blocker", "warning", "info"]
|
DiagnosticSeverity = Literal["blocker", "warning", "info"]
|
||||||
PlanAction = Literal["create", "update", "bind", "skip", "blocked", "noop"]
|
PlanAction = Literal["create", "update", "bind", "skip", "blocked", "noop"]
|
||||||
|
ConfigurationRollbackStatus = Literal[
|
||||||
|
"blocked_before_apply",
|
||||||
|
"not_required",
|
||||||
|
"database_restore_required",
|
||||||
|
"partial_apply_requires_recovery",
|
||||||
|
]
|
||||||
ConfigurationPackageClass = Literal[
|
ConfigurationPackageClass = Literal[
|
||||||
"reference",
|
"reference",
|
||||||
"product",
|
"product",
|
||||||
@@ -461,6 +469,21 @@ class ConfigurationApplyResult:
|
|||||||
diagnostics: tuple[ConfigurationDiagnostic, ...] = ()
|
diagnostics: tuple[ConfigurationDiagnostic, ...] = ()
|
||||||
created_refs: Mapping[str, str] = field(default_factory=dict)
|
created_refs: Mapping[str, str] = field(default_factory=dict)
|
||||||
updated_refs: Mapping[str, str] = field(default_factory=dict)
|
updated_refs: Mapping[str, str] = field(default_factory=dict)
|
||||||
|
rollback: "ConfigurationRollbackState | None" = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class ConfigurationRollbackState:
|
||||||
|
status: ConfigurationRollbackStatus
|
||||||
|
summary: str
|
||||||
|
recovery_action: str | None = None
|
||||||
|
|
||||||
|
def to_dict(self) -> dict[str, object]:
|
||||||
|
return {
|
||||||
|
"status": self.status,
|
||||||
|
"summary": self.summary,
|
||||||
|
"recovery_action": self.recovery_action,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, slots=True)
|
@dataclass(frozen=True, slots=True)
|
||||||
@@ -471,11 +494,40 @@ class ConfigurationExportSelection:
|
|||||||
object_refs: tuple[str, ...] = ()
|
object_refs: tuple[str, ...] = ()
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class ConfigurationExportProvenance:
|
||||||
|
exported_at: str
|
||||||
|
source_core_version: str
|
||||||
|
module_versions: Mapping[str, str]
|
||||||
|
tenant_id: str | None
|
||||||
|
exporter_id: str | None
|
||||||
|
scopes: tuple[str, ...] = ()
|
||||||
|
module_ids: tuple[str, ...] = ()
|
||||||
|
object_refs: tuple[str, ...] = ()
|
||||||
|
redacted_secret_keys: tuple[str, ...] = ()
|
||||||
|
|
||||||
|
def to_dict(self) -> dict[str, object]:
|
||||||
|
return {
|
||||||
|
"exported_at": self.exported_at,
|
||||||
|
"source_core_version": self.source_core_version,
|
||||||
|
"module_versions": dict(self.module_versions),
|
||||||
|
"tenant_id": self.tenant_id,
|
||||||
|
"exporter_id": self.exporter_id,
|
||||||
|
"selection": {
|
||||||
|
"scopes": list(self.scopes),
|
||||||
|
"module_ids": list(self.module_ids),
|
||||||
|
"object_refs": list(self.object_refs),
|
||||||
|
},
|
||||||
|
"redacted_secret_keys": list(self.redacted_secret_keys),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, slots=True)
|
@dataclass(frozen=True, slots=True)
|
||||||
class ConfigurationExportResult:
|
class ConfigurationExportResult:
|
||||||
fragments: tuple[ConfigurationPackageFragment, ...] = ()
|
fragments: tuple[ConfigurationPackageFragment, ...] = ()
|
||||||
data_requirements: tuple[ConfigurationRequiredData, ...] = ()
|
data_requirements: tuple[ConfigurationRequiredData, ...] = ()
|
||||||
diagnostics: tuple[ConfigurationDiagnostic, ...] = ()
|
diagnostics: tuple[ConfigurationDiagnostic, ...] = ()
|
||||||
|
provenance: ConfigurationExportProvenance | None = None
|
||||||
|
|
||||||
|
|
||||||
@runtime_checkable
|
@runtime_checkable
|
||||||
@@ -508,6 +560,7 @@ def dry_run_configuration_package(
|
|||||||
diagnostics: list[ConfigurationDiagnostic] = []
|
diagnostics: list[ConfigurationDiagnostic] = []
|
||||||
required_data: list[ConfigurationRequiredData] = []
|
required_data: list[ConfigurationRequiredData] = []
|
||||||
plan: list[ConfigurationPlanItem] = []
|
plan: list[ConfigurationPlanItem] = []
|
||||||
|
declared_data: dict[str, ConfigurationRequiredData] = {}
|
||||||
|
|
||||||
diagnostics.extend(_module_requirement_diagnostics(manifest, context))
|
diagnostics.extend(_module_requirement_diagnostics(manifest, context))
|
||||||
diagnostics.extend(_capability_requirement_diagnostics(manifest, context))
|
diagnostics.extend(_capability_requirement_diagnostics(manifest, context))
|
||||||
@@ -515,6 +568,7 @@ def dry_run_configuration_package(
|
|||||||
for item in manifest.data_requirements:
|
for item in manifest.data_requirements:
|
||||||
requirement = ConfigurationRequiredData.from_mapping(item)
|
requirement = ConfigurationRequiredData.from_mapping(item)
|
||||||
required_data.append(requirement)
|
required_data.append(requirement)
|
||||||
|
declared_data[requirement.key] = requirement
|
||||||
if requirement.required and requirement.key not in context.supplied_data:
|
if requirement.required and requirement.key not in context.supplied_data:
|
||||||
diagnostics.append(ConfigurationDiagnostic(
|
diagnostics.append(ConfigurationDiagnostic(
|
||||||
severity="blocker",
|
severity="blocker",
|
||||||
@@ -525,6 +579,25 @@ def dry_run_configuration_package(
|
|||||||
))
|
))
|
||||||
|
|
||||||
for fragment in manifest.fragments:
|
for fragment in manifest.fragments:
|
||||||
|
data_ref_diagnostics = _fragment_data_reference_diagnostics(
|
||||||
|
fragment,
|
||||||
|
declared_data=declared_data,
|
||||||
|
supplied_data=context.supplied_data,
|
||||||
|
)
|
||||||
|
if data_ref_diagnostics:
|
||||||
|
diagnostics.extend(data_ref_diagnostics)
|
||||||
|
plan.append(ConfigurationPlanItem(
|
||||||
|
action="blocked",
|
||||||
|
module_id=fragment.module_id,
|
||||||
|
fragment_type=fragment.fragment_type,
|
||||||
|
fragment_id=fragment.fragment_id,
|
||||||
|
summary="Fragment needs declared deployment data before provider preflight.",
|
||||||
|
))
|
||||||
|
continue
|
||||||
|
resolved_fragment = _resolve_fragment_data_references(
|
||||||
|
fragment,
|
||||||
|
context.supplied_data,
|
||||||
|
)
|
||||||
provider = provider_map.get(fragment.module_id)
|
provider = provider_map.get(fragment.module_id)
|
||||||
if provider is None:
|
if provider is None:
|
||||||
diagnostics.append(ConfigurationDiagnostic(
|
diagnostics.append(ConfigurationDiagnostic(
|
||||||
@@ -550,7 +623,7 @@ def dry_run_configuration_package(
|
|||||||
plan.append(ConfigurationPlanItem(action="blocked", module_id=fragment.module_id, fragment_type=fragment.fragment_type, fragment_id=fragment.fragment_id, summary="Fragment type is unsupported."))
|
plan.append(ConfigurationPlanItem(action="blocked", module_id=fragment.module_id, fragment_type=fragment.fragment_type, fragment_id=fragment.fragment_id, summary="Fragment type is unsupported."))
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
result = provider.preflight(fragment, context)
|
result = provider.preflight(resolved_fragment, context)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
diagnostics.append(ConfigurationDiagnostic(
|
diagnostics.append(ConfigurationDiagnostic(
|
||||||
severity="blocker",
|
severity="blocker",
|
||||||
@@ -605,19 +678,41 @@ def apply_configuration_package(
|
|||||||
preflight = dry_run_configuration_package(manifest, providers, apply_context)
|
preflight = dry_run_configuration_package(manifest, providers, apply_context)
|
||||||
blockers = [item for item in preflight.diagnostics if item.severity == "blocker"]
|
blockers = [item for item in preflight.diagnostics if item.severity == "blocker"]
|
||||||
if blockers:
|
if blockers:
|
||||||
return ConfigurationApplyResult(diagnostics=tuple(blockers))
|
return ConfigurationApplyResult(
|
||||||
|
diagnostics=tuple(blockers),
|
||||||
|
rollback=ConfigurationRollbackState(
|
||||||
|
status="blocked_before_apply",
|
||||||
|
summary="No provider changes were attempted because package preflight is blocked.",
|
||||||
|
),
|
||||||
|
)
|
||||||
provider_map = _configuration_provider_map(providers)
|
provider_map = _configuration_provider_map(providers)
|
||||||
diagnostics: list[ConfigurationDiagnostic] = list(preflight.diagnostics)
|
diagnostics: list[ConfigurationDiagnostic] = list(preflight.diagnostics)
|
||||||
created_refs: dict[str, str] = {}
|
created_refs: dict[str, str] = {}
|
||||||
updated_refs: dict[str, str] = {}
|
updated_refs: dict[str, str] = {}
|
||||||
|
stopped_after_blocker = False
|
||||||
for fragment in manifest.fragments:
|
for fragment in manifest.fragments:
|
||||||
provider = provider_map[fragment.module_id]
|
provider = provider_map[fragment.module_id]
|
||||||
|
resolved_fragment = _resolve_fragment_data_references(
|
||||||
|
fragment,
|
||||||
|
apply_context.supplied_data,
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
result = provider.apply(fragment, apply_context.supplied_data, apply_context)
|
result = provider.apply(
|
||||||
|
resolved_fragment,
|
||||||
|
apply_context.supplied_data,
|
||||||
|
apply_context,
|
||||||
|
)
|
||||||
diagnostics.extend(result.diagnostics)
|
diagnostics.extend(result.diagnostics)
|
||||||
created_refs.update(result.created_refs)
|
created_refs.update(result.created_refs)
|
||||||
updated_refs.update(result.updated_refs)
|
updated_refs.update(result.updated_refs)
|
||||||
diagnostics.extend(provider.health(result, apply_context))
|
health_diagnostics = provider.health(result, apply_context)
|
||||||
|
diagnostics.extend(health_diagnostics)
|
||||||
|
if any(
|
||||||
|
item.severity == "blocker"
|
||||||
|
for item in (*result.diagnostics, *health_diagnostics)
|
||||||
|
):
|
||||||
|
stopped_after_blocker = True
|
||||||
|
break
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
diagnostics.append(ConfigurationDiagnostic(
|
diagnostics.append(ConfigurationDiagnostic(
|
||||||
severity="blocker",
|
severity="blocker",
|
||||||
@@ -627,10 +722,36 @@ def apply_configuration_package(
|
|||||||
object_ref=fragment.fragment_id or fragment.fragment_type,
|
object_ref=fragment.fragment_id or fragment.fragment_type,
|
||||||
resolution="Stop the import, keep previous configuration, and inspect provider logs.",
|
resolution="Stop the import, keep previous configuration, and inspect provider logs.",
|
||||||
))
|
))
|
||||||
|
stopped_after_blocker = True
|
||||||
|
break
|
||||||
|
changed = bool(created_refs or updated_refs)
|
||||||
|
if stopped_after_blocker and changed:
|
||||||
|
rollback = ConfigurationRollbackState(
|
||||||
|
status="partial_apply_requires_recovery",
|
||||||
|
summary="At least one provider committed changes before a later provider blocked the package.",
|
||||||
|
recovery_action="Restore the reviewed pre-apply database snapshot or use module-owned compensation where explicitly supported.",
|
||||||
|
)
|
||||||
|
elif stopped_after_blocker:
|
||||||
|
rollback = ConfigurationRollbackState(
|
||||||
|
status="blocked_before_apply",
|
||||||
|
summary="The first provider blocked before any configuration reference was created or updated.",
|
||||||
|
)
|
||||||
|
elif changed:
|
||||||
|
rollback = ConfigurationRollbackState(
|
||||||
|
status="database_restore_required",
|
||||||
|
summary="The package changed provider-owned configuration; generic cross-module compensation is not available.",
|
||||||
|
recovery_action="Retain the pre-apply database snapshot until verification is complete; restore it if the package must be rolled back.",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
rollback = ConfigurationRollbackState(
|
||||||
|
status="not_required",
|
||||||
|
summary="All package fragments were no-ops, so no rollback action is required.",
|
||||||
|
)
|
||||||
return ConfigurationApplyResult(
|
return ConfigurationApplyResult(
|
||||||
diagnostics=tuple(_dedupe_diagnostics(diagnostics)),
|
diagnostics=tuple(_dedupe_diagnostics(diagnostics)),
|
||||||
created_refs=created_refs,
|
created_refs=created_refs,
|
||||||
updated_refs=updated_refs,
|
updated_refs=updated_refs,
|
||||||
|
rollback=rollback,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -669,10 +790,29 @@ def export_configuration_package(
|
|||||||
fragments.extend(result.fragments)
|
fragments.extend(result.fragments)
|
||||||
data_requirements.extend(result.data_requirements)
|
data_requirements.extend(result.data_requirements)
|
||||||
diagnostics.extend(result.diagnostics)
|
diagnostics.extend(result.diagnostics)
|
||||||
|
deduped_required_data = tuple(_dedupe_required_data(data_requirements))
|
||||||
|
provenance = ConfigurationExportProvenance(
|
||||||
|
exported_at=datetime.now(UTC).isoformat(),
|
||||||
|
source_core_version=_installed_core_version(),
|
||||||
|
module_versions={
|
||||||
|
module_id: context.installed_modules[module_id]
|
||||||
|
for module_id in sorted(set(module_ids))
|
||||||
|
if module_id in context.installed_modules
|
||||||
|
},
|
||||||
|
tenant_id=selection.tenant_id,
|
||||||
|
exporter_id=context.operator_user_id,
|
||||||
|
scopes=selection.scopes,
|
||||||
|
module_ids=tuple(module_ids),
|
||||||
|
object_refs=selection.object_refs,
|
||||||
|
redacted_secret_keys=tuple(
|
||||||
|
sorted(item.key for item in deduped_required_data if item.secret)
|
||||||
|
),
|
||||||
|
)
|
||||||
return ConfigurationExportResult(
|
return ConfigurationExportResult(
|
||||||
fragments=tuple(fragments),
|
fragments=tuple(fragments),
|
||||||
data_requirements=tuple(_dedupe_required_data(data_requirements)),
|
data_requirements=deduped_required_data,
|
||||||
diagnostics=tuple(_dedupe_diagnostics(diagnostics)),
|
diagnostics=tuple(_dedupe_diagnostics(diagnostics)),
|
||||||
|
provenance=provenance,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -1283,6 +1423,103 @@ def _dedupe_required_data(items: Sequence[ConfigurationRequiredData]) -> list[Co
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _fragment_data_reference_diagnostics(
|
||||||
|
fragment: ConfigurationPackageFragment,
|
||||||
|
*,
|
||||||
|
declared_data: Mapping[str, ConfigurationRequiredData],
|
||||||
|
supplied_data: Mapping[str, Any],
|
||||||
|
) -> list[ConfigurationDiagnostic]:
|
||||||
|
references: set[str] = set()
|
||||||
|
invalid = _collect_fragment_data_references(fragment.payload, references)
|
||||||
|
diagnostics: list[ConfigurationDiagnostic] = []
|
||||||
|
object_ref = fragment.fragment_id or fragment.fragment_type
|
||||||
|
if invalid:
|
||||||
|
diagnostics.append(ConfigurationDiagnostic(
|
||||||
|
severity="blocker",
|
||||||
|
code="fragment_data_reference_invalid",
|
||||||
|
message="Configuration fragment data references must be objects containing only a non-empty $data key.",
|
||||||
|
module_id=fragment.module_id,
|
||||||
|
object_ref=object_ref,
|
||||||
|
resolution="Replace malformed references with {\"$data\": \"declared_requirement_key\"}.",
|
||||||
|
))
|
||||||
|
for key in sorted(references - set(declared_data)):
|
||||||
|
diagnostics.append(ConfigurationDiagnostic(
|
||||||
|
severity="blocker",
|
||||||
|
code="fragment_data_reference_undeclared",
|
||||||
|
message=f"Configuration fragment references undeclared operator data {key!r}.",
|
||||||
|
module_id=fragment.module_id,
|
||||||
|
object_ref=key,
|
||||||
|
resolution="Declare the key in package data_requirements before using it in a fragment.",
|
||||||
|
))
|
||||||
|
for key in sorted(references & set(declared_data)):
|
||||||
|
if key in supplied_data:
|
||||||
|
continue
|
||||||
|
diagnostics.append(ConfigurationDiagnostic(
|
||||||
|
severity="blocker",
|
||||||
|
code="fragment_data_reference_missing",
|
||||||
|
message=f"Configuration fragment needs operator data {declared_data[key].label!r} before provider preflight.",
|
||||||
|
module_id=fragment.module_id,
|
||||||
|
object_ref=key,
|
||||||
|
resolution="Provide the value in the generated configuration package form.",
|
||||||
|
))
|
||||||
|
return diagnostics
|
||||||
|
|
||||||
|
|
||||||
|
def _collect_fragment_data_references(value: object, references: set[str]) -> bool:
|
||||||
|
invalid = False
|
||||||
|
if isinstance(value, Mapping):
|
||||||
|
if "$data" in value:
|
||||||
|
key = value.get("$data")
|
||||||
|
if len(value) != 1 or not isinstance(key, str) or not key.strip():
|
||||||
|
return True
|
||||||
|
references.add(key.strip())
|
||||||
|
return False
|
||||||
|
for item in value.values():
|
||||||
|
invalid = _collect_fragment_data_references(item, references) or invalid
|
||||||
|
elif isinstance(value, Sequence) and not isinstance(value, (str, bytes)):
|
||||||
|
for item in value:
|
||||||
|
invalid = _collect_fragment_data_references(item, references) or invalid
|
||||||
|
return invalid
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_fragment_data_references(
|
||||||
|
fragment: ConfigurationPackageFragment,
|
||||||
|
supplied_data: Mapping[str, Any],
|
||||||
|
) -> ConfigurationPackageFragment:
|
||||||
|
payload = _resolve_data_reference_value(fragment.payload, supplied_data)
|
||||||
|
if not isinstance(payload, Mapping):
|
||||||
|
raise ValueError("Resolved configuration fragment payload must remain an object.")
|
||||||
|
return ConfigurationPackageFragment(
|
||||||
|
module_id=fragment.module_id,
|
||||||
|
fragment_type=fragment.fragment_type,
|
||||||
|
fragment_id=fragment.fragment_id,
|
||||||
|
payload=payload,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_data_reference_value(value: object, supplied_data: Mapping[str, Any]) -> object:
|
||||||
|
if isinstance(value, Mapping):
|
||||||
|
if set(value) == {"$data"}:
|
||||||
|
key = value.get("$data")
|
||||||
|
if not isinstance(key, str) or key not in supplied_data:
|
||||||
|
raise ValueError("Configuration fragment contains an unresolved $data reference.")
|
||||||
|
return supplied_data[key]
|
||||||
|
return {
|
||||||
|
str(key): _resolve_data_reference_value(item, supplied_data)
|
||||||
|
for key, item in value.items()
|
||||||
|
}
|
||||||
|
if isinstance(value, Sequence) and not isinstance(value, (str, bytes)):
|
||||||
|
return [_resolve_data_reference_value(item, supplied_data) for item in value]
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _installed_core_version() -> str:
|
||||||
|
try:
|
||||||
|
return package_version("govoplan-core")
|
||||||
|
except PackageNotFoundError:
|
||||||
|
return "workspace"
|
||||||
|
|
||||||
|
|
||||||
def _catalog_source(path: Path | str | None) -> Path | str | None:
|
def _catalog_source(path: Path | str | None) -> Path | str | None:
|
||||||
if path is not None:
|
if path is not None:
|
||||||
return path if isinstance(path, str) and _is_http_url(path) else Path(path).expanduser()
|
return path if isinstance(path, str) and _is_http_url(path) else Path(path).expanduser()
|
||||||
|
|||||||
@@ -101,6 +101,8 @@ class DatasourceGovernance:
|
|||||||
transfer_agreement_ref: str | None = None
|
transfer_agreement_ref: str | None = None
|
||||||
freshness_policy: Mapping[str, object] = field(default_factory=dict)
|
freshness_policy: Mapping[str, object] = field(default_factory=dict)
|
||||||
quality_policy: Mapping[str, object] = field(default_factory=dict)
|
quality_policy: Mapping[str, object] = field(default_factory=dict)
|
||||||
|
approval_policy: Mapping[str, object] = field(default_factory=dict)
|
||||||
|
retention_policy: Mapping[str, object] = field(default_factory=dict)
|
||||||
known_limits: tuple[str, ...] = ()
|
known_limits: tuple[str, ...] = ()
|
||||||
correction_procedure_ref: str | None = None
|
correction_procedure_ref: str | None = None
|
||||||
affected_refs: tuple[str, ...] = ()
|
affected_refs: tuple[str, ...] = ()
|
||||||
@@ -179,6 +181,8 @@ class DatasourceGovernance:
|
|||||||
),
|
),
|
||||||
freshness_policy=_governance_mapping(source.get("freshness_policy")),
|
freshness_policy=_governance_mapping(source.get("freshness_policy")),
|
||||||
quality_policy=_governance_mapping(source.get("quality_policy")),
|
quality_policy=_governance_mapping(source.get("quality_policy")),
|
||||||
|
approval_policy=_governance_mapping(source.get("approval_policy")),
|
||||||
|
retention_policy=_governance_mapping(source.get("retention_policy")),
|
||||||
known_limits=_governance_texts(source.get("known_limits")),
|
known_limits=_governance_texts(source.get("known_limits")),
|
||||||
correction_procedure_ref=_optional_governance_text(
|
correction_procedure_ref=_optional_governance_text(
|
||||||
source.get("correction_procedure_ref")
|
source.get("correction_procedure_ref")
|
||||||
@@ -210,6 +214,8 @@ class DatasourceGovernance:
|
|||||||
"transfer_agreement_ref": self.transfer_agreement_ref,
|
"transfer_agreement_ref": self.transfer_agreement_ref,
|
||||||
"freshness_policy": dict(self.freshness_policy),
|
"freshness_policy": dict(self.freshness_policy),
|
||||||
"quality_policy": dict(self.quality_policy),
|
"quality_policy": dict(self.quality_policy),
|
||||||
|
"approval_policy": dict(self.approval_policy),
|
||||||
|
"retention_policy": dict(self.retention_policy),
|
||||||
"known_limits": list(self.known_limits),
|
"known_limits": list(self.known_limits),
|
||||||
"correction_procedure_ref": self.correction_procedure_ref,
|
"correction_procedure_ref": self.correction_procedure_ref,
|
||||||
"affected_refs": list(self.affected_refs),
|
"affected_refs": list(self.affected_refs),
|
||||||
@@ -289,6 +295,8 @@ class DatasourceMaterialization:
|
|||||||
frozen_label: str | None = None
|
frozen_label: str | None = None
|
||||||
source_timestamp: datetime | None = None
|
source_timestamp: datetime | None = None
|
||||||
created_at: datetime | None = None
|
created_at: datetime | None = None
|
||||||
|
disposed_at: datetime | None = None
|
||||||
|
disposition: Mapping[str, object] = field(default_factory=dict)
|
||||||
provenance: Mapping[str, object] = field(default_factory=dict)
|
provenance: Mapping[str, object] = field(default_factory=dict)
|
||||||
metadata: Mapping[str, object] = field(default_factory=dict)
|
metadata: Mapping[str, object] = field(default_factory=dict)
|
||||||
governance: DatasourceGovernance = field(default_factory=DatasourceGovernance)
|
governance: DatasourceGovernance = field(default_factory=DatasourceGovernance)
|
||||||
@@ -309,6 +317,7 @@ class DatasourceStage:
|
|||||||
row_count: int | None = None
|
row_count: int | None = None
|
||||||
byte_count: int | None = None
|
byte_count: int | None = None
|
||||||
validation: Mapping[str, object] = field(default_factory=dict)
|
validation: Mapping[str, object] = field(default_factory=dict)
|
||||||
|
approval: Mapping[str, object] = field(default_factory=dict)
|
||||||
created_at: datetime | None = None
|
created_at: datetime | None = None
|
||||||
promoted_at: datetime | None = None
|
promoted_at: datetime | None = None
|
||||||
promoted_materialization_ref: str | None = None
|
promoted_materialization_ref: str | None = None
|
||||||
|
|||||||
@@ -289,6 +289,30 @@ DocumentationSourceState = Literal["configured", "disabled", "unavailable"]
|
|||||||
CapabilityStability = Literal["experimental", "stable", "deprecated"]
|
CapabilityStability = Literal["experimental", "stable", "deprecated"]
|
||||||
|
|
||||||
|
|
||||||
|
DOCUMENTATION_STRUCTURED_TRANSLATION_VERSION = "1"
|
||||||
|
DOCUMENTATION_LOCALIZABLE_METADATA_KEYS = frozenset(
|
||||||
|
{
|
||||||
|
"admin_explanation",
|
||||||
|
"consequence_classes",
|
||||||
|
"consequences",
|
||||||
|
"constraints",
|
||||||
|
"current_configuration",
|
||||||
|
"fields",
|
||||||
|
"limitations",
|
||||||
|
"operational_consequences",
|
||||||
|
"outcome",
|
||||||
|
"prerequisites",
|
||||||
|
"privacy_notes",
|
||||||
|
"purpose",
|
||||||
|
"result",
|
||||||
|
"steps",
|
||||||
|
"user_explanation",
|
||||||
|
"verification",
|
||||||
|
"when_used",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, slots=True)
|
@dataclass(frozen=True, slots=True)
|
||||||
class DocumentationLink:
|
class DocumentationLink:
|
||||||
label: str
|
label: str
|
||||||
@@ -324,12 +348,142 @@ class DocumentationTopic:
|
|||||||
configuration_keys: tuple[str, ...] = ()
|
configuration_keys: tuple[str, ...] = ()
|
||||||
i18n_key: str | None = None
|
i18n_key: str | None = None
|
||||||
translations: Mapping[str, Mapping[str, str]] = field(default_factory=dict)
|
translations: Mapping[str, Mapping[str, str]] = field(default_factory=dict)
|
||||||
|
structured_translation_version: str | None = None
|
||||||
|
structured_translations: Mapping[str, Mapping[str, Any]] = field(
|
||||||
|
default_factory=dict
|
||||||
|
)
|
||||||
source_module_id: str | None = None
|
source_module_id: str | None = None
|
||||||
version_min: str | None = None
|
version_min: str | None = None
|
||||||
version_max_exclusive: str | None = None
|
version_max_exclusive: str | None = None
|
||||||
metadata: Mapping[str, Any] = field(default_factory=dict)
|
metadata: Mapping[str, Any] = field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
|
def localizable_documentation_metadata_keys(
|
||||||
|
topic: DocumentationTopic,
|
||||||
|
) -> tuple[str, ...]:
|
||||||
|
"""Return structured metadata keys whose values are public prose."""
|
||||||
|
|
||||||
|
return tuple(
|
||||||
|
sorted(DOCUMENTATION_LOCALIZABLE_METADATA_KEYS.intersection(topic.metadata))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def localized_documentation_metadata(
|
||||||
|
topic: DocumentationTopic,
|
||||||
|
locale: str,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Overlay one validated structured translation onto source metadata."""
|
||||||
|
|
||||||
|
localized = dict(topic.metadata)
|
||||||
|
translation = topic.structured_translations.get(locale)
|
||||||
|
if translation:
|
||||||
|
localized.update(translation)
|
||||||
|
return localized
|
||||||
|
|
||||||
|
|
||||||
|
def documentation_structured_translation_issues(
|
||||||
|
topic: DocumentationTopic,
|
||||||
|
) -> tuple[str, ...]:
|
||||||
|
"""Validate the opt-in, versioned structured-documentation translation."""
|
||||||
|
|
||||||
|
version = topic.structured_translation_version
|
||||||
|
translations = topic.structured_translations
|
||||||
|
if version is None:
|
||||||
|
if translations:
|
||||||
|
return (
|
||||||
|
"structured_translations require structured_translation_version",
|
||||||
|
)
|
||||||
|
return ()
|
||||||
|
if version != DOCUMENTATION_STRUCTURED_TRANSLATION_VERSION:
|
||||||
|
return (
|
||||||
|
"unsupported structured_translation_version "
|
||||||
|
f"{version!r}; expected {DOCUMENTATION_STRUCTURED_TRANSLATION_VERSION!r}",
|
||||||
|
)
|
||||||
|
|
||||||
|
localizable_keys = set(localizable_documentation_metadata_keys(topic))
|
||||||
|
issues: list[str] = []
|
||||||
|
for locale, translation in translations.items():
|
||||||
|
if not locale.strip():
|
||||||
|
issues.append("structured translation locale must not be empty")
|
||||||
|
continue
|
||||||
|
translated_keys = set(translation)
|
||||||
|
for key in sorted(translated_keys - localizable_keys):
|
||||||
|
issues.append(
|
||||||
|
f"structured translation {locale!r} contains non-localizable or missing metadata key {key!r}"
|
||||||
|
)
|
||||||
|
for key in sorted(localizable_keys - translated_keys):
|
||||||
|
issues.append(
|
||||||
|
f"structured translation {locale!r} is missing metadata key {key!r}"
|
||||||
|
)
|
||||||
|
for key in sorted(localizable_keys & translated_keys):
|
||||||
|
issues.extend(
|
||||||
|
_structured_translation_shape_issues(
|
||||||
|
topic.metadata[key],
|
||||||
|
translation[key],
|
||||||
|
path=f"{locale}.{key}",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return tuple(issues)
|
||||||
|
|
||||||
|
|
||||||
|
def _structured_translation_shape_issues(
|
||||||
|
source: object,
|
||||||
|
translated: object,
|
||||||
|
*,
|
||||||
|
path: str,
|
||||||
|
) -> tuple[str, ...]:
|
||||||
|
if isinstance(source, str):
|
||||||
|
if not isinstance(translated, str) or not translated.strip():
|
||||||
|
return (f"structured translation {path} must be a non-empty string",)
|
||||||
|
return ()
|
||||||
|
if isinstance(source, Mapping):
|
||||||
|
if not isinstance(translated, Mapping):
|
||||||
|
return (f"structured translation {path} must preserve object shape",)
|
||||||
|
issues: list[str] = []
|
||||||
|
source_keys = {str(key) for key in source}
|
||||||
|
translated_keys = {str(key) for key in translated}
|
||||||
|
if source_keys != translated_keys:
|
||||||
|
issues.append(
|
||||||
|
f"structured translation {path} must preserve object keys"
|
||||||
|
)
|
||||||
|
return tuple(issues)
|
||||||
|
for key, value in source.items():
|
||||||
|
issues.extend(
|
||||||
|
_structured_translation_shape_issues(
|
||||||
|
value,
|
||||||
|
translated[key],
|
||||||
|
path=f"{path}.{key}",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return tuple(issues)
|
||||||
|
if isinstance(source, Sequence) and not isinstance(
|
||||||
|
source, (str, bytes, bytearray)
|
||||||
|
):
|
||||||
|
if not isinstance(translated, Sequence) or isinstance(
|
||||||
|
translated, (str, bytes, bytearray)
|
||||||
|
):
|
||||||
|
return (f"structured translation {path} must preserve list shape",)
|
||||||
|
if len(source) != len(translated):
|
||||||
|
return (f"structured translation {path} must preserve list length",)
|
||||||
|
issues: list[str] = []
|
||||||
|
for index, (source_item, translated_item) in enumerate(
|
||||||
|
zip(source, translated, strict=True)
|
||||||
|
):
|
||||||
|
issues.extend(
|
||||||
|
_structured_translation_shape_issues(
|
||||||
|
source_item,
|
||||||
|
translated_item,
|
||||||
|
path=f"{path}[{index}]",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return tuple(issues)
|
||||||
|
if translated != source:
|
||||||
|
return (
|
||||||
|
f"structured translation {path} must preserve non-text value {source!r}",
|
||||||
|
)
|
||||||
|
return ()
|
||||||
|
|
||||||
|
|
||||||
def user_workflow_scope_condition_issues(topic: DocumentationTopic) -> tuple[str, ...]:
|
def user_workflow_scope_condition_issues(topic: DocumentationTopic) -> tuple[str, ...]:
|
||||||
"""Return fail-closed authoring issues for a user-facing workflow topic.
|
"""Return fail-closed authoring issues for a user-facing workflow topic.
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ from govoplan_core.core.modules import (
|
|||||||
TenantSummaryBatchProvider,
|
TenantSummaryBatchProvider,
|
||||||
TenantSummaryProvider,
|
TenantSummaryProvider,
|
||||||
user_workflow_scope_condition_issues,
|
user_workflow_scope_condition_issues,
|
||||||
|
documentation_structured_translation_issues,
|
||||||
)
|
)
|
||||||
from govoplan_core.core.module_entitlements import (
|
from govoplan_core.core.module_entitlements import (
|
||||||
TenantModuleEntitlementResolver,
|
TenantModuleEntitlementResolver,
|
||||||
@@ -962,6 +963,10 @@ def _validate_manifest_shape(manifest: ModuleManifest) -> None:
|
|||||||
raise RegistryError(
|
raise RegistryError(
|
||||||
f"Module {manifest.id!r} documentation topic {topic.id!r}: {issue}"
|
f"Module {manifest.id!r} documentation topic {topic.id!r}: {issue}"
|
||||||
)
|
)
|
||||||
|
for issue in documentation_structured_translation_issues(topic):
|
||||||
|
raise RegistryError(
|
||||||
|
f"Module {manifest.id!r} documentation topic {topic.id!r}: {issue}"
|
||||||
|
)
|
||||||
_validate_documentation_extensions(manifest)
|
_validate_documentation_extensions(manifest)
|
||||||
_validate_architecture_declarations(manifest)
|
_validate_architecture_declarations(manifest)
|
||||||
_validate_workflow_definition_contributions(manifest)
|
_validate_workflow_definition_contributions(manifest)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
import urllib.request
|
import urllib.request
|
||||||
|
from collections.abc import Iterable
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Mapping
|
from typing import Mapping
|
||||||
|
|
||||||
@@ -12,6 +13,12 @@ from govoplan_core.security.outbound_http import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
MAX_OUTBOUND_HTTP_REQUEST_BODY_BYTES = 1_000_000
|
||||||
|
_STANDARD_REDIRECT_SENSITIVE_HEADERS = frozenset(
|
||||||
|
{"authorization", "proxy-authorization", "cookie", "cookie2"}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, slots=True)
|
@dataclass(frozen=True, slots=True)
|
||||||
class HttpFetchResponse:
|
class HttpFetchResponse:
|
||||||
status: int
|
status: int
|
||||||
@@ -46,15 +53,27 @@ def fetch_http(
|
|||||||
label: str = "URL",
|
label: str = "URL",
|
||||||
method: str = "GET",
|
method: str = "GET",
|
||||||
headers: Mapping[str, str] | None = None,
|
headers: Mapping[str, str] | None = None,
|
||||||
|
body: bytes | None = None,
|
||||||
max_bytes: int | None = None,
|
max_bytes: int | None = None,
|
||||||
|
redirect_sensitive_headers: Iterable[str] = (),
|
||||||
) -> HttpFetchResponse:
|
) -> HttpFetchResponse:
|
||||||
|
if body is not None and len(body) > MAX_OUTBOUND_HTTP_REQUEST_BODY_BYTES:
|
||||||
|
raise ValueError(
|
||||||
|
"Outbound HTTP request body exceeds the 1000000-byte safety limit."
|
||||||
|
)
|
||||||
validated_url = validate_outbound_http_url(url, label=label)
|
validated_url = validate_outbound_http_url(url, label=label)
|
||||||
request = urllib.request.Request( # noqa: S310 - URL is restricted to validated HTTP(S).
|
request = urllib.request.Request( # noqa: S310 - URL is restricted to validated HTTP(S).
|
||||||
validated_url,
|
validated_url,
|
||||||
|
data=body,
|
||||||
headers=dict(headers or {}),
|
headers=dict(headers or {}),
|
||||||
method=method,
|
method=method,
|
||||||
)
|
)
|
||||||
opener = build_outbound_http_opener(_PolicyRedirectHandler(label=label))
|
opener = build_outbound_http_opener(
|
||||||
|
_PolicyRedirectHandler(
|
||||||
|
label=label,
|
||||||
|
sensitive_headers=redirect_sensitive_headers,
|
||||||
|
)
|
||||||
|
)
|
||||||
with opener.open(request, timeout=timeout) as response: # noqa: S310 - URL and every redirect are policy-validated. # nosec B310 # nosemgrep: python.lang.security.audit.dynamic-urllib-use-detected.dynamic-urllib-use-detected
|
with opener.open(request, timeout=timeout) as response: # noqa: S310 - URL and every redirect are policy-validated. # nosec B310 # nosemgrep: python.lang.security.audit.dynamic-urllib-use-detected.dynamic-urllib-use-detected
|
||||||
response_headers = dict(response.headers.items())
|
response_headers = dict(response.headers.items())
|
||||||
return HttpFetchResponse(
|
return HttpFetchResponse(
|
||||||
@@ -76,16 +95,35 @@ def fetch_http_text(
|
|||||||
label: str = "URL",
|
label: str = "URL",
|
||||||
method: str = "GET",
|
method: str = "GET",
|
||||||
headers: Mapping[str, str] | None = None,
|
headers: Mapping[str, str] | None = None,
|
||||||
|
body: bytes | None = None,
|
||||||
encoding: str = "utf-8",
|
encoding: str = "utf-8",
|
||||||
max_bytes: int | None = None,
|
max_bytes: int | None = None,
|
||||||
|
redirect_sensitive_headers: Iterable[str] = (),
|
||||||
) -> str:
|
) -> str:
|
||||||
return fetch_http(url, timeout=timeout, label=label, method=method, headers=headers, max_bytes=max_bytes).text(encoding)
|
return fetch_http(
|
||||||
|
url,
|
||||||
|
timeout=timeout,
|
||||||
|
label=label,
|
||||||
|
method=method,
|
||||||
|
headers=headers,
|
||||||
|
body=body,
|
||||||
|
max_bytes=max_bytes,
|
||||||
|
redirect_sensitive_headers=redirect_sensitive_headers,
|
||||||
|
).text(encoding)
|
||||||
|
|
||||||
|
|
||||||
class _PolicyRedirectHandler(urllib.request.HTTPRedirectHandler):
|
class _PolicyRedirectHandler(urllib.request.HTTPRedirectHandler):
|
||||||
def __init__(self, *, label: str) -> None:
|
def __init__(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
label: str,
|
||||||
|
sensitive_headers: Iterable[str] = (),
|
||||||
|
) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._label = label
|
self._label = label
|
||||||
|
self._sensitive_headers = _STANDARD_REDIRECT_SENSITIVE_HEADERS | {
|
||||||
|
value.strip().lower() for value in sensitive_headers if value.strip()
|
||||||
|
}
|
||||||
|
|
||||||
def redirect_request(self, req, fp, code, msg, headers, newurl): # type: ignore[no-untyped-def]
|
def redirect_request(self, req, fp, code, msg, headers, newurl): # type: ignore[no-untyped-def]
|
||||||
candidate = validate_outbound_http_url(newurl, label=f"{self._label} redirect")
|
candidate = validate_outbound_http_url(newurl, label=f"{self._label} redirect")
|
||||||
@@ -95,8 +133,9 @@ class _PolicyRedirectHandler(urllib.request.HTTPRedirectHandler):
|
|||||||
return None
|
return None
|
||||||
new_request = super().redirect_request(req, fp, code, msg, headers, candidate)
|
new_request = super().redirect_request(req, fp, code, msg, headers, candidate)
|
||||||
if new_request is not None and _http_origin(previous) != _http_origin(redirected):
|
if new_request is not None and _http_origin(previous) != _http_origin(redirected):
|
||||||
for header in ("Authorization", "Proxy-Authorization", "Cookie", "Cookie2"):
|
for header in tuple(new_request.headers) + tuple(new_request.unredirected_hdrs):
|
||||||
new_request.remove_header(header)
|
if header.lower() in self._sensitive_headers:
|
||||||
|
new_request.remove_header(header)
|
||||||
return new_request
|
return new_request
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,13 +3,22 @@ from __future__ import annotations
|
|||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from govoplan_core.core.configuration_packages import (
|
from govoplan_core.core.configuration_packages import (
|
||||||
|
ConfigurationApplyResult,
|
||||||
|
ConfigurationExportResult,
|
||||||
|
ConfigurationExportSelection,
|
||||||
ConfigurationModuleRequirement,
|
ConfigurationModuleRequirement,
|
||||||
|
ConfigurationPackageFragment,
|
||||||
ConfigurationPackageEvidence,
|
ConfigurationPackageEvidence,
|
||||||
ConfigurationPackageManifest,
|
ConfigurationPackageManifest,
|
||||||
ConfigurationPackageParent,
|
ConfigurationPackageParent,
|
||||||
|
ConfigurationPlanItem,
|
||||||
ConfigurationPreflightContext,
|
ConfigurationPreflightContext,
|
||||||
|
ConfigurationPreflightResult,
|
||||||
ConfigurationProviderExpectation,
|
ConfigurationProviderExpectation,
|
||||||
|
ConfigurationRequiredData,
|
||||||
|
apply_configuration_package,
|
||||||
dry_run_configuration_package,
|
dry_run_configuration_package,
|
||||||
|
export_configuration_package,
|
||||||
validate_configuration_package_derivation,
|
validate_configuration_package_derivation,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -27,6 +36,121 @@ def _evidence(*kinds: str) -> tuple[ConfigurationPackageEvidence, ...]:
|
|||||||
|
|
||||||
|
|
||||||
class ConfigurationPackageArchitectureTests(unittest.TestCase):
|
class ConfigurationPackageArchitectureTests(unittest.TestCase):
|
||||||
|
def test_deployment_data_references_are_declared_resolved_and_never_exported(self) -> None:
|
||||||
|
class Provider:
|
||||||
|
module_id = "forms"
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.preflight_payloads: list[dict[str, object]] = []
|
||||||
|
|
||||||
|
def describe(self):
|
||||||
|
from govoplan_core.core.configuration_packages import ConfigurationProviderDescription
|
||||||
|
|
||||||
|
return ConfigurationProviderDescription(
|
||||||
|
module_id=self.module_id,
|
||||||
|
fragment_types=("definition",),
|
||||||
|
)
|
||||||
|
|
||||||
|
def preflight(self, fragment, context):
|
||||||
|
del context
|
||||||
|
self.preflight_payloads.append(dict(fragment.payload))
|
||||||
|
return ConfigurationPreflightResult(plan=(ConfigurationPlanItem(
|
||||||
|
action="create",
|
||||||
|
module_id=self.module_id,
|
||||||
|
fragment_type=fragment.fragment_type,
|
||||||
|
fragment_id=fragment.fragment_id,
|
||||||
|
),))
|
||||||
|
|
||||||
|
def apply(self, fragment, supplied_data, context):
|
||||||
|
del supplied_data, context
|
||||||
|
return ConfigurationApplyResult(
|
||||||
|
created_refs={fragment.fragment_id or "definition": "form:resident-parking"}
|
||||||
|
)
|
||||||
|
|
||||||
|
def export(self, selection, context):
|
||||||
|
del selection, context
|
||||||
|
return ConfigurationExportResult(
|
||||||
|
fragments=(ConfigurationPackageFragment(
|
||||||
|
module_id=self.module_id,
|
||||||
|
fragment_type="definition",
|
||||||
|
payload={"name": "Resident parking permit"},
|
||||||
|
),),
|
||||||
|
data_requirements=(ConfigurationRequiredData(
|
||||||
|
key="payment_credential_ref",
|
||||||
|
label="Payment credential reference",
|
||||||
|
secret=True,
|
||||||
|
),),
|
||||||
|
)
|
||||||
|
|
||||||
|
def health(self, import_result, context):
|
||||||
|
del import_result, context
|
||||||
|
return ()
|
||||||
|
|
||||||
|
provider = Provider()
|
||||||
|
package = ConfigurationPackageManifest(
|
||||||
|
package_id="product.resident-parking",
|
||||||
|
name="Resident parking permit",
|
||||||
|
version="1.0.0",
|
||||||
|
required_modules=(ConfigurationModuleRequirement("forms"),),
|
||||||
|
data_requirements=({
|
||||||
|
"key": "service_name",
|
||||||
|
"label": "Public service name",
|
||||||
|
},),
|
||||||
|
fragments=(ConfigurationPackageFragment(
|
||||||
|
module_id="forms",
|
||||||
|
fragment_type="definition",
|
||||||
|
fragment_id="resident-parking",
|
||||||
|
payload={
|
||||||
|
"definition": {
|
||||||
|
"title": {"$data": "service_name"},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),),
|
||||||
|
)
|
||||||
|
missing_context = ConfigurationPreflightContext(
|
||||||
|
installed_modules={"forms": "0.1.0"},
|
||||||
|
)
|
||||||
|
|
||||||
|
missing = dry_run_configuration_package(package, (provider,), missing_context)
|
||||||
|
|
||||||
|
self.assertEqual([], provider.preflight_payloads)
|
||||||
|
self.assertIn(
|
||||||
|
"fragment_data_reference_missing",
|
||||||
|
{item.code for item in missing.diagnostics},
|
||||||
|
)
|
||||||
|
|
||||||
|
ready_context = ConfigurationPreflightContext(
|
||||||
|
installed_modules={"forms": "0.1.0"},
|
||||||
|
supplied_data={"service_name": "Anwohnerparkausweis"},
|
||||||
|
operator_user_id="operator-1",
|
||||||
|
)
|
||||||
|
ready = dry_run_configuration_package(package, (provider,), ready_context)
|
||||||
|
applied = apply_configuration_package(package, (provider,), ready_context)
|
||||||
|
exported = export_configuration_package(
|
||||||
|
(provider,),
|
||||||
|
ConfigurationExportSelection(
|
||||||
|
tenant_id="tenant-1",
|
||||||
|
module_ids=("forms",),
|
||||||
|
),
|
||||||
|
ready_context,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertFalse(any(item.severity == "blocker" for item in ready.diagnostics))
|
||||||
|
self.assertEqual(
|
||||||
|
"Anwohnerparkausweis",
|
||||||
|
provider.preflight_payloads[-1]["definition"]["title"], # type: ignore[index]
|
||||||
|
)
|
||||||
|
self.assertIsNotNone(applied.rollback)
|
||||||
|
assert applied.rollback is not None
|
||||||
|
self.assertEqual("database_restore_required", applied.rollback.status)
|
||||||
|
self.assertIsNotNone(exported.provenance)
|
||||||
|
assert exported.provenance is not None
|
||||||
|
self.assertEqual("operator-1", exported.provenance.exporter_id)
|
||||||
|
self.assertEqual(
|
||||||
|
("payment_credential_ref",),
|
||||||
|
exported.provenance.redacted_secret_keys,
|
||||||
|
)
|
||||||
|
|
||||||
def test_legacy_package_defaults_to_product_and_round_trips(self) -> None:
|
def test_legacy_package_defaults_to_product_and_round_trips(self) -> None:
|
||||||
package = ConfigurationPackageManifest.from_mapping(
|
package = ConfigurationPackageManifest.from_mapping(
|
||||||
{"package_id": "example", "name": "Example", "version": "1.0.0"}
|
{"package_id": "example", "name": "Example", "version": "1.0.0"}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ from govoplan_core.core.datasources import (
|
|||||||
DatasourceArtifactBackendProvider,
|
DatasourceArtifactBackendProvider,
|
||||||
DatasourceDescriptor,
|
DatasourceDescriptor,
|
||||||
DatasourceField,
|
DatasourceField,
|
||||||
|
DatasourceGovernance,
|
||||||
DatasourceLifecycleProvider,
|
DatasourceLifecycleProvider,
|
||||||
DatasourceMaterialization,
|
DatasourceMaterialization,
|
||||||
DatasourceOrigin,
|
DatasourceOrigin,
|
||||||
@@ -212,6 +213,55 @@ class DatasourceContractTests(unittest.TestCase):
|
|||||||
self.assertEqual("upload", descriptor.kind)
|
self.assertEqual("upload", descriptor.kind)
|
||||||
self.assertEqual("tabular", descriptor.shape)
|
self.assertEqual("tabular", descriptor.shape)
|
||||||
|
|
||||||
|
def test_lifecycle_governance_round_trips_without_provider_specific_types(self) -> None:
|
||||||
|
governance = DatasourceGovernance.from_mapping(
|
||||||
|
{
|
||||||
|
"approval_policy": {
|
||||||
|
"version": "approval-v2",
|
||||||
|
"required": True,
|
||||||
|
"required_approvals": 2,
|
||||||
|
},
|
||||||
|
"retention_policy": {
|
||||||
|
"version": "retention-v3",
|
||||||
|
"enabled": True,
|
||||||
|
"stage_days": 30,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual("approval-v2", governance.approval_policy["version"])
|
||||||
|
self.assertEqual(30, governance.retention_policy["stage_days"])
|
||||||
|
self.assertEqual(
|
||||||
|
governance.approval_policy,
|
||||||
|
governance.to_dict()["approval_policy"],
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
governance.retention_policy,
|
||||||
|
governance.to_dict()["retention_policy"],
|
||||||
|
)
|
||||||
|
|
||||||
|
stage = DatasourceStage(
|
||||||
|
ref="stage:governed",
|
||||||
|
name="Governed stage",
|
||||||
|
source_name="governed",
|
||||||
|
kind="upload",
|
||||||
|
mode="static",
|
||||||
|
shape="tabular",
|
||||||
|
state="awaiting_approval",
|
||||||
|
approval={"status": "pending", "policy_version": "approval-v2"},
|
||||||
|
)
|
||||||
|
materialization = DatasourceMaterialization(
|
||||||
|
ref="materialization:disposed",
|
||||||
|
datasource_ref="datasource:governed",
|
||||||
|
revision=1,
|
||||||
|
state="disposed",
|
||||||
|
fingerprint="abc123",
|
||||||
|
disposition={"reason": "retention_policy", "policy_version": "retention-v3"},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual("pending", stage.approval["status"])
|
||||||
|
self.assertEqual("retention_policy", materialization.disposition["reason"])
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from govoplan_core.core.modules import (
|
|||||||
DocumentationSourceDefinition,
|
DocumentationSourceDefinition,
|
||||||
DocumentationTopic,
|
DocumentationTopic,
|
||||||
ModuleManifest,
|
ModuleManifest,
|
||||||
|
localized_documentation_metadata,
|
||||||
user_workflow_scope_condition_issues,
|
user_workflow_scope_condition_issues,
|
||||||
)
|
)
|
||||||
from govoplan_core.core.registry import PlatformRegistry, RegistryError
|
from govoplan_core.core.registry import PlatformRegistry, RegistryError
|
||||||
@@ -83,6 +84,58 @@ class DocumentationTopicContractTests(unittest.TestCase):
|
|||||||
self.assertEqual(user_workflow_scope_condition_issues(user_reference), ())
|
self.assertEqual(user_workflow_scope_condition_issues(user_reference), ())
|
||||||
registry_for(scoped, admin_workflow, user_reference).validate()
|
registry_for(scoped, admin_workflow, user_reference).validate()
|
||||||
|
|
||||||
|
def test_versioned_structured_translation_preserves_metadata_shape(self) -> None:
|
||||||
|
topic = DocumentationTopic(
|
||||||
|
id="example.workflow.localized",
|
||||||
|
title="Run task",
|
||||||
|
summary="Run the task.",
|
||||||
|
metadata={
|
||||||
|
"kind": "workflow",
|
||||||
|
"steps": ["Review", "Execute"],
|
||||||
|
"verification": "Confirm the result.",
|
||||||
|
},
|
||||||
|
structured_translation_version="1",
|
||||||
|
structured_translations={
|
||||||
|
"de": {
|
||||||
|
"steps": ["Prüfen", "Ausführen"],
|
||||||
|
"verification": "Das Ergebnis bestätigen.",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
registry_for(topic).validate()
|
||||||
|
self.assertEqual(
|
||||||
|
["Prüfen", "Ausführen"],
|
||||||
|
localized_documentation_metadata(topic, "de")["steps"],
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
"workflow", localized_documentation_metadata(topic, "de")["kind"]
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_structured_translation_requires_version_and_complete_shape(self) -> None:
|
||||||
|
missing_version = DocumentationTopic(
|
||||||
|
id="example.localized.missing-version",
|
||||||
|
title="Localized",
|
||||||
|
summary="Invalid contract.",
|
||||||
|
metadata={"limitations": ["One", "Two"]},
|
||||||
|
structured_translations={"de": {"limitations": ["Eins", "Zwei"]}},
|
||||||
|
)
|
||||||
|
with self.assertRaisesRegex(
|
||||||
|
RegistryError, "require structured_translation_version"
|
||||||
|
):
|
||||||
|
registry_for(missing_version).validate()
|
||||||
|
|
||||||
|
incomplete_shape = DocumentationTopic(
|
||||||
|
id="example.localized.incomplete",
|
||||||
|
title="Localized",
|
||||||
|
summary="Invalid shape.",
|
||||||
|
metadata={"limitations": ["One", "Two"]},
|
||||||
|
structured_translation_version="1",
|
||||||
|
structured_translations={"de": {"limitations": ["Eins"]}},
|
||||||
|
)
|
||||||
|
with self.assertRaisesRegex(RegistryError, "preserve list length"):
|
||||||
|
registry_for(incomplete_shape).validate()
|
||||||
|
|
||||||
def test_documentation_configuration_and_source_extensions_are_validated(self) -> None:
|
def test_documentation_configuration_and_source_extensions_are_validated(self) -> None:
|
||||||
resolver = lambda _context, keys: { # noqa: E731
|
resolver = lambda _context, keys: { # noqa: E731
|
||||||
key: DocumentationConfigurationDecision(key=key, state="enabled")
|
key: DocumentationConfigurationDecision(key=key, state="enabled")
|
||||||
|
|||||||
@@ -2,9 +2,14 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import io
|
import io
|
||||||
import unittest
|
import unittest
|
||||||
from unittest.mock import patch
|
from unittest.mock import Mock, patch
|
||||||
|
|
||||||
from govoplan_core.security.http_fetch import _PolicyRedirectHandler, is_http_url, validate_http_url
|
from govoplan_core.security.http_fetch import (
|
||||||
|
_PolicyRedirectHandler,
|
||||||
|
fetch_http,
|
||||||
|
is_http_url,
|
||||||
|
validate_http_url,
|
||||||
|
)
|
||||||
from govoplan_core.security.outbound_http import (
|
from govoplan_core.security.outbound_http import (
|
||||||
DEFAULT_FILE_TRANSFER_BYTES,
|
DEFAULT_FILE_TRANSFER_BYTES,
|
||||||
DEFAULT_STRUCTURED_RESPONSE_BYTES,
|
DEFAULT_STRUCTURED_RESPONSE_BYTES,
|
||||||
@@ -21,6 +26,51 @@ from govoplan_core.security.outbound_http import (
|
|||||||
|
|
||||||
|
|
||||||
class HttpFetchTests(unittest.TestCase):
|
class HttpFetchTests(unittest.TestCase):
|
||||||
|
def test_fetch_http_forwards_a_bounded_request_body(self) -> None:
|
||||||
|
class Response(io.BytesIO):
|
||||||
|
status = 200
|
||||||
|
headers = {"Content-Type": "application/json"}
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, *_args):
|
||||||
|
return False
|
||||||
|
|
||||||
|
opener = Mock()
|
||||||
|
opener.open.return_value = Response(b"{}")
|
||||||
|
with patch(
|
||||||
|
"govoplan_core.security.http_fetch.validate_outbound_http_url",
|
||||||
|
return_value="https://wiki.example.test/api.php",
|
||||||
|
), patch(
|
||||||
|
"govoplan_core.security.http_fetch.build_outbound_http_opener",
|
||||||
|
return_value=opener,
|
||||||
|
):
|
||||||
|
response = fetch_http(
|
||||||
|
"https://wiki.example.test/api.php",
|
||||||
|
method="POST",
|
||||||
|
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
||||||
|
body=b"action=edit",
|
||||||
|
max_bytes=1024,
|
||||||
|
)
|
||||||
|
|
||||||
|
request = opener.open.call_args.args[0]
|
||||||
|
self.assertEqual("POST", request.get_method())
|
||||||
|
self.assertEqual(b"action=edit", request.data)
|
||||||
|
self.assertEqual(b"{}", response.body)
|
||||||
|
|
||||||
|
def test_fetch_http_rejects_an_oversized_request_body_before_transport(self) -> None:
|
||||||
|
with patch(
|
||||||
|
"govoplan_core.security.http_fetch.validate_outbound_http_url"
|
||||||
|
) as validate:
|
||||||
|
with self.assertRaisesRegex(ValueError, "request body exceeds"):
|
||||||
|
fetch_http(
|
||||||
|
"https://wiki.example.test/api.php",
|
||||||
|
method="POST",
|
||||||
|
body=b"x" * 1_000_001,
|
||||||
|
)
|
||||||
|
validate.assert_not_called()
|
||||||
|
|
||||||
def test_validate_http_url_accepts_absolute_http_urls_without_credentials(self) -> None:
|
def test_validate_http_url_accepts_absolute_http_urls_without_credentials(self) -> None:
|
||||||
self.assertEqual("https://example.test/catalog.json", validate_http_url("https://example.test/catalog.json"))
|
self.assertEqual("https://example.test/catalog.json", validate_http_url("https://example.test/catalog.json"))
|
||||||
self.assertTrue(is_http_url("http://example.test/catalog.json"))
|
self.assertTrue(is_http_url("http://example.test/catalog.json"))
|
||||||
@@ -189,9 +239,17 @@ class HttpFetchTests(unittest.TestCase):
|
|||||||
|
|
||||||
request = urllib.request.Request(
|
request = urllib.request.Request(
|
||||||
"https://catalog.example.test/releases",
|
"https://catalog.example.test/releases",
|
||||||
headers={"Authorization": "Bearer secret", "X-Request-ID": "request-1"},
|
headers={
|
||||||
|
"Authorization": "Bearer secret",
|
||||||
|
"Cookie": "session=secret",
|
||||||
|
"X-OTRS-Header-Password": "secret",
|
||||||
|
"X-Request-ID": "request-1",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
handler = _PolicyRedirectHandler(
|
||||||
|
label="Catalog URL",
|
||||||
|
sensitive_headers=("X-OTRS-Header-Password",),
|
||||||
)
|
)
|
||||||
handler = _PolicyRedirectHandler(label="Catalog URL")
|
|
||||||
with patch.dict("os.environ", {"APP_ENV": "test"}), patch(
|
with patch.dict("os.environ", {"APP_ENV": "test"}), patch(
|
||||||
"govoplan_core.security.outbound_http.socket.getaddrinfo",
|
"govoplan_core.security.outbound_http.socket.getaddrinfo",
|
||||||
return_value=[(2, 1, 6, "", ("127.0.0.1", 443))],
|
return_value=[(2, 1, 6, "", ("127.0.0.1", 443))],
|
||||||
@@ -215,9 +273,38 @@ class HttpFetchTests(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertIsNotNone(redirected)
|
self.assertIsNotNone(redirected)
|
||||||
self.assertIsNone(redirected.get_header("Authorization"))
|
self.assertIsNone(redirected.get_header("Authorization"))
|
||||||
|
self.assertIsNone(redirected.get_header("Cookie"))
|
||||||
|
self.assertIsNone(redirected.get_header("X-otrs-header-password"))
|
||||||
self.assertEqual("request-1", redirected.get_header("X-request-id"))
|
self.assertEqual("request-1", redirected.get_header("X-request-id"))
|
||||||
self.assertIsNone(downgrade)
|
self.assertIsNone(downgrade)
|
||||||
|
|
||||||
|
def test_core_redirects_preserve_caller_sensitive_headers_on_the_same_origin(self) -> None:
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
request = urllib.request.Request(
|
||||||
|
"https://desk.example.test/original",
|
||||||
|
headers={"X-OTRS-Header-SessionID": "secret"},
|
||||||
|
)
|
||||||
|
handler = _PolicyRedirectHandler(
|
||||||
|
label="Service-desk URL",
|
||||||
|
sensitive_headers=("X-OTRS-Header-SessionID",),
|
||||||
|
)
|
||||||
|
with patch.dict("os.environ", {"APP_ENV": "test"}), patch(
|
||||||
|
"govoplan_core.security.outbound_http.socket.getaddrinfo",
|
||||||
|
return_value=[(2, 1, 6, "", ("127.0.0.1", 443))],
|
||||||
|
):
|
||||||
|
redirected = handler.redirect_request(
|
||||||
|
request,
|
||||||
|
None,
|
||||||
|
302,
|
||||||
|
"Found",
|
||||||
|
{},
|
||||||
|
"https://desk.example.test/final",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIsNotNone(redirected)
|
||||||
|
self.assertEqual("secret", redirected.get_header("X-otrs-header-sessionid"))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Generated
+29
-8
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@govoplan/core-webui",
|
"name": "@govoplan/core-webui",
|
||||||
"version": "0.1.30",
|
"version": "0.1.37",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@govoplan/core-webui",
|
"name": "@govoplan/core-webui",
|
||||||
"version": "0.1.30",
|
"version": "0.1.37",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@govoplan/access-webui": "file:../../govoplan-access/webui",
|
"@govoplan/access-webui": "file:../../govoplan-access/webui",
|
||||||
"@govoplan/addresses-webui": "file:../../govoplan-addresses/webui",
|
"@govoplan/addresses-webui": "file:../../govoplan-addresses/webui",
|
||||||
@@ -52,6 +52,7 @@
|
|||||||
"@govoplan/tickets-webui": "file:../../govoplan-tickets/webui",
|
"@govoplan/tickets-webui": "file:../../govoplan-tickets/webui",
|
||||||
"@govoplan/views-webui": "file:../../govoplan-views/webui",
|
"@govoplan/views-webui": "file:../../govoplan-views/webui",
|
||||||
"@govoplan/voting-webui": "file:../../govoplan-voting/webui",
|
"@govoplan/voting-webui": "file:../../govoplan-voting/webui",
|
||||||
|
"@govoplan/wiki-webui": "file:../../govoplan-wiki/webui",
|
||||||
"@govoplan/workflow-webui": "file:../../govoplan-workflow/webui",
|
"@govoplan/workflow-webui": "file:../../govoplan-workflow/webui",
|
||||||
"@tiptap/core": "^3.29.2",
|
"@tiptap/core": "^3.29.2",
|
||||||
"@tiptap/extension-image": "^3.29.2",
|
"@tiptap/extension-image": "^3.29.2",
|
||||||
@@ -84,7 +85,7 @@
|
|||||||
},
|
},
|
||||||
"../../govoplan-access/webui": {
|
"../../govoplan-access/webui": {
|
||||||
"name": "@govoplan/access-webui",
|
"name": "@govoplan/access-webui",
|
||||||
"version": "0.1.19",
|
"version": "0.1.20",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5.7.2"
|
"typescript": "^5.7.2"
|
||||||
},
|
},
|
||||||
@@ -119,12 +120,12 @@
|
|||||||
},
|
},
|
||||||
"../../govoplan-admin/webui": {
|
"../../govoplan-admin/webui": {
|
||||||
"name": "@govoplan/admin-webui",
|
"name": "@govoplan/admin-webui",
|
||||||
"version": "0.1.18",
|
"version": "0.1.19",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5.7.2"
|
"typescript": "^5.7.2"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@govoplan/core-webui": "^0.1.18",
|
"@govoplan/core-webui": "^0.1.35",
|
||||||
"lucide-react": "^1.23.0",
|
"lucide-react": "^1.23.0",
|
||||||
"react": ">=19.2.7 <20",
|
"react": ">=19.2.7 <20",
|
||||||
"react-dom": ">=19.2.7 <20",
|
"react-dom": ">=19.2.7 <20",
|
||||||
@@ -242,7 +243,7 @@
|
|||||||
},
|
},
|
||||||
"../../govoplan-connectors/webui": {
|
"../../govoplan-connectors/webui": {
|
||||||
"name": "@govoplan/connectors-webui",
|
"name": "@govoplan/connectors-webui",
|
||||||
"version": "0.1.20",
|
"version": "0.1.22",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@govoplan/core-webui": "^0.1.18",
|
"@govoplan/core-webui": "^0.1.18",
|
||||||
"react": ">=19.2.7 <20",
|
"react": ">=19.2.7 <20",
|
||||||
@@ -393,7 +394,7 @@
|
|||||||
},
|
},
|
||||||
"../../govoplan-forms/webui": {
|
"../../govoplan-forms/webui": {
|
||||||
"name": "@govoplan/forms-webui",
|
"name": "@govoplan/forms-webui",
|
||||||
"version": "0.1.19",
|
"version": "0.1.20",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@govoplan/core-webui": "^0.1.18",
|
"@govoplan/core-webui": "^0.1.18",
|
||||||
"lucide-react": "^1.23.0",
|
"lucide-react": "^1.23.0",
|
||||||
@@ -474,7 +475,7 @@
|
|||||||
},
|
},
|
||||||
"../../govoplan-mail/webui": {
|
"../../govoplan-mail/webui": {
|
||||||
"name": "@govoplan/mail-webui",
|
"name": "@govoplan/mail-webui",
|
||||||
"version": "0.1.21",
|
"version": "0.1.22",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5.7.2"
|
"typescript": "^5.7.2"
|
||||||
},
|
},
|
||||||
@@ -822,6 +823,22 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"../../govoplan-wiki/webui": {
|
||||||
|
"name": "@govoplan/wiki-webui",
|
||||||
|
"version": "0.1.20",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@govoplan/core-webui": "^0.1.31",
|
||||||
|
"lucide-react": "^1.23.0",
|
||||||
|
"react": ">=19.2.7 <20",
|
||||||
|
"react-dom": ">=19.2.7 <20",
|
||||||
|
"react-router": ">=8.3.0 <9"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@govoplan/core-webui": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"../../govoplan-workflow/webui": {
|
"../../govoplan-workflow/webui": {
|
||||||
"name": "@govoplan/workflow-webui",
|
"name": "@govoplan/workflow-webui",
|
||||||
"version": "0.1.21",
|
"version": "0.1.21",
|
||||||
@@ -1768,6 +1785,10 @@
|
|||||||
"resolved": "../../govoplan-voting/webui",
|
"resolved": "../../govoplan-voting/webui",
|
||||||
"link": true
|
"link": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@govoplan/wiki-webui": {
|
||||||
|
"resolved": "../../govoplan-wiki/webui",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
"node_modules/@govoplan/workflow-webui": {
|
"node_modules/@govoplan/workflow-webui": {
|
||||||
"resolved": "../../govoplan-workflow/webui",
|
"resolved": "../../govoplan-workflow/webui",
|
||||||
"link": true
|
"link": true
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
{
|
{
|
||||||
"name": "@govoplan/core-webui",
|
"name": "@govoplan/core-webui",
|
||||||
"version": "0.1.30",
|
"version": "0.1.37",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@govoplan/core-webui",
|
"name": "@govoplan/core-webui",
|
||||||
"version": "0.1.30",
|
"version": "0.1.37",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@govoplan/access-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-access.git#v0.1.19",
|
"@govoplan/access-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-access.git#v0.1.20",
|
||||||
"@govoplan/admin-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-admin.git#v0.1.18",
|
"@govoplan/admin-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-admin.git#v0.1.19",
|
||||||
"@govoplan/audit-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-audit.git#v0.1.18",
|
"@govoplan/audit-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-audit.git#v0.1.18",
|
||||||
"@govoplan/calendar-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-calendar.git#v0.1.18",
|
"@govoplan/calendar-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-calendar.git#v0.1.18",
|
||||||
"@govoplan/campaign-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-campaign.git#v0.1.22",
|
"@govoplan/campaign-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-campaign.git#v0.1.22",
|
||||||
@@ -24,6 +24,7 @@
|
|||||||
"@govoplan/organizations-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-organizations.git#v0.1.18",
|
"@govoplan/organizations-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-organizations.git#v0.1.18",
|
||||||
"@govoplan/policy-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-policy.git#v0.1.18",
|
"@govoplan/policy-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-policy.git#v0.1.18",
|
||||||
"@govoplan/tickets-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-tickets.git#v0.1.20",
|
"@govoplan/tickets-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-tickets.git#v0.1.20",
|
||||||
|
"@govoplan/wiki-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-wiki.git#v0.1.20",
|
||||||
"@tiptap/core": "^3.29.2",
|
"@tiptap/core": "^3.29.2",
|
||||||
"@tiptap/extension-image": "^3.29.2",
|
"@tiptap/extension-image": "^3.29.2",
|
||||||
"@tiptap/pm": "^3.29.2",
|
"@tiptap/pm": "^3.29.2",
|
||||||
@@ -756,8 +757,8 @@
|
|||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"node_modules/@govoplan/access-webui": {
|
"node_modules/@govoplan/access-webui": {
|
||||||
"version": "0.1.19",
|
"version": "0.1.20",
|
||||||
"resolved": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-access.git#2d1b1e356ecb8726219d4a502db78e61f435a88f",
|
"resolved": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-access.git#206873b62a9c77ac5715f9ce7d6bc16d156efa74",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@govoplan/core-webui": "^0.1.18",
|
"@govoplan/core-webui": "^0.1.18",
|
||||||
"lucide-react": "^1.23.0",
|
"lucide-react": "^1.23.0",
|
||||||
@@ -772,10 +773,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@govoplan/admin-webui": {
|
"node_modules/@govoplan/admin-webui": {
|
||||||
"version": "0.1.18",
|
"version": "0.1.19",
|
||||||
"resolved": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-admin.git#218f94fa23a2b1386ac89c001d9d69d155934ef0",
|
"resolved": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-admin.git#ed424c729cd0c1c7a1cbec387a793db0819c91fe",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@govoplan/core-webui": "^0.1.18",
|
"@govoplan/core-webui": "^0.1.35",
|
||||||
"lucide-react": "^1.23.0",
|
"lucide-react": "^1.23.0",
|
||||||
"react": ">=19.2.7 <20",
|
"react": ">=19.2.7 <20",
|
||||||
"react-dom": ">=19.2.7 <20",
|
"react-dom": ">=19.2.7 <20",
|
||||||
@@ -1032,6 +1033,22 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@govoplan/wiki-webui": {
|
||||||
|
"version": "0.1.20",
|
||||||
|
"resolved": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-wiki.git#66c91351c9eb693ace606c9b69dd5cd804d7531b",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@govoplan/core-webui": "^0.1.31",
|
||||||
|
"lucide-react": "^1.23.0",
|
||||||
|
"react": ">=19.2.7 <20",
|
||||||
|
"react-dom": ">=19.2.7 <20",
|
||||||
|
"react-router": ">=8.3.0 <9"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@govoplan/core-webui": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@jridgewell/gen-mapping": {
|
"node_modules/@jridgewell/gen-mapping": {
|
||||||
"version": "0.3.13",
|
"version": "0.3.13",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||||
|
|||||||
+2
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@govoplan/core-webui",
|
"name": "@govoplan/core-webui",
|
||||||
"version": "0.1.30",
|
"version": "0.1.37",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
@@ -102,6 +102,7 @@
|
|||||||
"@govoplan/tickets-webui": "file:../../govoplan-tickets/webui",
|
"@govoplan/tickets-webui": "file:../../govoplan-tickets/webui",
|
||||||
"@govoplan/views-webui": "file:../../govoplan-views/webui",
|
"@govoplan/views-webui": "file:../../govoplan-views/webui",
|
||||||
"@govoplan/voting-webui": "file:../../govoplan-voting/webui",
|
"@govoplan/voting-webui": "file:../../govoplan-voting/webui",
|
||||||
|
"@govoplan/wiki-webui": "file:../../govoplan-wiki/webui",
|
||||||
"@govoplan/workflow-webui": "file:../../govoplan-workflow/webui",
|
"@govoplan/workflow-webui": "file:../../govoplan-workflow/webui",
|
||||||
"@tiptap/core": "^3.29.2",
|
"@tiptap/core": "^3.29.2",
|
||||||
"@tiptap/extension-image": "^3.29.2",
|
"@tiptap/extension-image": "^3.29.2",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@govoplan/core-webui",
|
"name": "@govoplan/core-webui",
|
||||||
"version": "0.1.30",
|
"version": "0.1.37",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
@@ -26,8 +26,8 @@
|
|||||||
"preview": "vite preview --host 127.0.0.1 --port 4173"
|
"preview": "vite preview --host 127.0.0.1 --port 4173"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@govoplan/access-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-access.git#v0.1.19",
|
"@govoplan/access-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-access.git#v0.1.20",
|
||||||
"@govoplan/admin-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-admin.git#v0.1.18",
|
"@govoplan/admin-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-admin.git#v0.1.19",
|
||||||
"@govoplan/audit-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-audit.git#v0.1.18",
|
"@govoplan/audit-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-audit.git#v0.1.18",
|
||||||
"@govoplan/calendar-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-calendar.git#v0.1.18",
|
"@govoplan/calendar-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-calendar.git#v0.1.18",
|
||||||
"@govoplan/cases-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-cases.git#v0.1.20",
|
"@govoplan/cases-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-cases.git#v0.1.20",
|
||||||
@@ -42,6 +42,7 @@
|
|||||||
"@govoplan/ops-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-ops.git#v0.1.18",
|
"@govoplan/ops-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-ops.git#v0.1.18",
|
||||||
"@govoplan/policy-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-policy.git#v0.1.18",
|
"@govoplan/policy-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-policy.git#v0.1.18",
|
||||||
"@govoplan/tickets-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-tickets.git#v0.1.20",
|
"@govoplan/tickets-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-tickets.git#v0.1.20",
|
||||||
|
"@govoplan/wiki-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-wiki.git#v0.1.20",
|
||||||
"@tiptap/core": "^3.29.2",
|
"@tiptap/core": "^3.29.2",
|
||||||
"@tiptap/extension-image": "^3.29.2",
|
"@tiptap/extension-image": "^3.29.2",
|
||||||
"@tiptap/pm": "^3.29.2",
|
"@tiptap/pm": "^3.29.2",
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ const packageByModule = {
|
|||||||
tickets: "@govoplan/tickets-webui",
|
tickets: "@govoplan/tickets-webui",
|
||||||
views: "@govoplan/views-webui",
|
views: "@govoplan/views-webui",
|
||||||
voting: "@govoplan/voting-webui",
|
voting: "@govoplan/voting-webui",
|
||||||
|
wiki: "@govoplan/wiki-webui",
|
||||||
workflow: "@govoplan/workflow-webui"
|
workflow: "@govoplan/workflow-webui"
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -79,6 +80,8 @@ const cases = [
|
|||||||
{ name: "forms-runtime", modules: ["forms", "forms_runtime"] },
|
{ name: "forms-runtime", modules: ["forms", "forms_runtime"] },
|
||||||
{ name: "tickets-only", modules: ["tickets"] },
|
{ name: "tickets-only", modules: ["tickets"] },
|
||||||
{ name: "tickets-with-helpdesk-and-cases", modules: ["tickets", "helpdesk", "cases"] },
|
{ name: "tickets-with-helpdesk-and-cases", modules: ["tickets", "helpdesk", "cases"] },
|
||||||
|
{ name: "wiki-only", modules: ["wiki"] },
|
||||||
|
{ name: "wiki-with-files-search", modules: ["wiki", "files", "search"] },
|
||||||
{ name: "mail-only", modules: ["mail"] },
|
{ name: "mail-only", modules: ["mail"] },
|
||||||
{ name: "notifications-only", modules: ["notifications"] },
|
{ name: "notifications-only", modules: ["notifications"] },
|
||||||
{ name: "organizations-only", modules: ["organizations"] },
|
{ name: "organizations-only", modules: ["organizations"] },
|
||||||
@@ -110,7 +113,7 @@ const cases = [
|
|||||||
{ name: "tasks-only", modules: ["access", "tasks"] },
|
{ name: "tasks-only", modules: ["access", "tasks"] },
|
||||||
{ name: "tasks-with-contributors", modules: ["access", "approvals", "postbox", "workflow", "dashboard", "tasks"] },
|
{ name: "tasks-with-contributors", modules: ["access", "approvals", "postbox", "workflow", "dashboard", "tasks"] },
|
||||||
{ name: "voting-only", modules: ["access", "voting"] },
|
{ name: "voting-only", modules: ["access", "voting"] },
|
||||||
{ name: "full-product", modules: ["access", "tenancy", "admin", "addresses", "approvals", "policy", "audit", "dashboard", "datasources", "dataflow", "dist_lists", "templates", "workflow", "views", "organizations", "idm", "identity", "identity_trust", "encryption", "cases", "committee", "connectors", "campaigns", "files", "forms", "forms_runtime", "helpdesk", "mail", "notifications", "docs", "ops", "payments", "calendar", "scheduling", "portal", "postbox", "projects", "quick_access", "reporting", "records", "risk_compliance", "search", "tasks", "tickets", "voting"] }
|
{ name: "full-product", modules: ["access", "tenancy", "admin", "addresses", "approvals", "policy", "audit", "dashboard", "datasources", "dataflow", "dist_lists", "templates", "workflow", "views", "organizations", "idm", "identity", "identity_trust", "encryption", "cases", "committee", "connectors", "campaigns", "files", "forms", "forms_runtime", "helpdesk", "mail", "notifications", "docs", "ops", "payments", "calendar", "scheduling", "portal", "postbox", "projects", "quick_access", "reporting", "records", "risk_compliance", "search", "tasks", "tickets", "voting", "wiki"] }
|
||||||
];
|
];
|
||||||
|
|
||||||
const npmExec = process.env.npm_execpath;
|
const npmExec = process.env.npm_execpath;
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export type MailProfilePatternRules = Partial<Record<MailProfilePatternKey, stri
|
|||||||
|
|
||||||
export type MailConnectionTestResponse = {
|
export type MailConnectionTestResponse = {
|
||||||
ok: boolean;
|
ok: boolean;
|
||||||
protocol: "smtp" | "imap";
|
protocol: "smtp" | "imap" | "jmap" | "pop3";
|
||||||
host?: string | null;
|
host?: string | null;
|
||||||
port?: number | null;
|
port?: number | null;
|
||||||
security?: MailSecurity | string | null;
|
security?: MailSecurity | string | null;
|
||||||
@@ -52,7 +52,7 @@ export type MailImapFolderResponse = {
|
|||||||
|
|
||||||
export type MailImapFolderListResponse = {
|
export type MailImapFolderListResponse = {
|
||||||
ok: boolean;
|
ok: boolean;
|
||||||
protocol: "imap";
|
protocol: "imap" | "jmap";
|
||||||
host?: string | null;
|
host?: string | null;
|
||||||
port?: number | null;
|
port?: number | null;
|
||||||
security?: MailSecurity | string | null;
|
security?: MailSecurity | string | null;
|
||||||
@@ -69,6 +69,7 @@ export type MailImapFolderListResponse = {
|
|||||||
export const mailProfilePatternKeys = [
|
export const mailProfilePatternKeys = [
|
||||||
"smtp_hosts",
|
"smtp_hosts",
|
||||||
"imap_hosts",
|
"imap_hosts",
|
||||||
|
"jmap_hosts",
|
||||||
"envelope_senders",
|
"envelope_senders",
|
||||||
"from_headers",
|
"from_headers",
|
||||||
"recipient_domains"
|
"recipient_domains"
|
||||||
@@ -82,11 +83,13 @@ export const mailProfilePolicyLimitKeys = [
|
|||||||
"imap_credentials.inherit",
|
"imap_credentials.inherit",
|
||||||
"whitelist.smtp_hosts",
|
"whitelist.smtp_hosts",
|
||||||
"whitelist.imap_hosts",
|
"whitelist.imap_hosts",
|
||||||
|
"whitelist.jmap_hosts",
|
||||||
"whitelist.envelope_senders",
|
"whitelist.envelope_senders",
|
||||||
"whitelist.from_headers",
|
"whitelist.from_headers",
|
||||||
"whitelist.recipient_domains",
|
"whitelist.recipient_domains",
|
||||||
"blacklist.smtp_hosts",
|
"blacklist.smtp_hosts",
|
||||||
"blacklist.imap_hosts",
|
"blacklist.imap_hosts",
|
||||||
|
"blacklist.jmap_hosts",
|
||||||
"blacklist.envelope_senders",
|
"blacklist.envelope_senders",
|
||||||
"blacklist.from_headers",
|
"blacklist.from_headers",
|
||||||
"blacklist.recipient_domains"
|
"blacklist.recipient_domains"
|
||||||
|
|||||||
+13
-3
@@ -847,6 +847,16 @@ export type MailImapTransportSettings = MailTransportSettings & {
|
|||||||
folder_mappings?: MailImapFolderMappings | null;
|
folder_mappings?: MailImapFolderMappings | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type MailJmapTransportSettings = {
|
||||||
|
session_url: string;
|
||||||
|
account_id?: string | null;
|
||||||
|
auth_scheme?: "bearer" | "basic";
|
||||||
|
timeout_seconds?: number | null;
|
||||||
|
max_response_bytes?: number | null;
|
||||||
|
max_body_value_bytes?: number | null;
|
||||||
|
allowed_api_origins?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
export type MailServerProfileCredentials = {
|
export type MailServerProfileCredentials = {
|
||||||
smtp?: MailTransportCredentials | null;
|
smtp?: MailTransportCredentials | null;
|
||||||
imap?: MailTransportCredentials | null;
|
imap?: MailTransportCredentials | null;
|
||||||
@@ -883,9 +893,9 @@ export type MailServerEndpoint = {
|
|||||||
id: string;
|
id: string;
|
||||||
profile_id: string;
|
profile_id: string;
|
||||||
tenant_id?: string | null;
|
tenant_id?: string | null;
|
||||||
protocol: "smtp" | "imap";
|
protocol: "smtp" | "imap" | "jmap" | "pop3";
|
||||||
name: string;
|
name: string;
|
||||||
config: MailTransportSettings | MailImapTransportSettings;
|
config: MailTransportSettings | MailImapTransportSettings | MailJmapTransportSettings;
|
||||||
scope_type: MailProfileScope;
|
scope_type: MailProfileScope;
|
||||||
scope_id?: string | null;
|
scope_id?: string | null;
|
||||||
inherit_to_lower_scopes: boolean;
|
inherit_to_lower_scopes: boolean;
|
||||||
@@ -922,7 +932,7 @@ export type MailCredentialPolicy = {
|
|||||||
allow_override?: boolean | null;
|
allow_override?: boolean | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type MailProfilePatternKey = "smtp_hosts" | "imap_hosts" | "envelope_senders" | "from_headers" | "recipient_domains";
|
export type MailProfilePatternKey = "smtp_hosts" | "imap_hosts" | "jmap_hosts" | "envelope_senders" | "from_headers" | "recipient_domains";
|
||||||
|
|
||||||
export type MailProfilePolicy = {
|
export type MailProfilePolicy = {
|
||||||
allowed_profile_ids?: string[] | null;
|
allowed_profile_ids?: string[] | null;
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ const defaultWebModulePackages = [
|
|||||||
"@govoplan/tickets-webui",
|
"@govoplan/tickets-webui",
|
||||||
"@govoplan/views-webui",
|
"@govoplan/views-webui",
|
||||||
"@govoplan/voting-webui",
|
"@govoplan/voting-webui",
|
||||||
|
"@govoplan/wiki-webui",
|
||||||
"@govoplan/workflow-webui"
|
"@govoplan/workflow-webui"
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -289,6 +290,7 @@ export default defineConfig({
|
|||||||
fileURLToPath(new URL('../../govoplan-tickets/webui', import.meta.url)),
|
fileURLToPath(new URL('../../govoplan-tickets/webui', import.meta.url)),
|
||||||
fileURLToPath(new URL('../../govoplan-views/webui', import.meta.url)),
|
fileURLToPath(new URL('../../govoplan-views/webui', import.meta.url)),
|
||||||
fileURLToPath(new URL('../../govoplan-voting/webui', import.meta.url)),
|
fileURLToPath(new URL('../../govoplan-voting/webui', import.meta.url)),
|
||||||
|
fileURLToPath(new URL('../../govoplan-wiki/webui', import.meta.url)),
|
||||||
fileURLToPath(new URL('../../govoplan-workflow/webui', import.meta.url))
|
fileURLToPath(new URL('../../govoplan-workflow/webui', import.meta.url))
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user