Release Core v0.1.35 configuration package safeguards
Module Package Release / publish-packages (push) Successful in 13s

This commit is contained in:
2026-08-22 18:05:33 +02:00
parent aa91063211
commit 1c3ee9e8c7
8 changed files with 409 additions and 25 deletions
+23
View File
@@ -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:
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "govoplan-core" name = "govoplan-core"
version = "0.1.34" version = "0.1.35"
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()
@@ -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"}
+6 -6
View File
@@ -1,12 +1,12 @@
{ {
"name": "@govoplan/core-webui", "name": "@govoplan/core-webui",
"version": "0.1.34", "version": "0.1.35",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@govoplan/core-webui", "name": "@govoplan/core-webui",
"version": "0.1.34", "version": "0.1.35",
"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",
@@ -85,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"
}, },
@@ -120,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",
@@ -394,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",
+9 -9
View File
@@ -1,15 +1,15 @@
{ {
"name": "@govoplan/core-webui", "name": "@govoplan/core-webui",
"version": "0.1.34", "version": "0.1.35",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@govoplan/core-webui", "name": "@govoplan/core-webui",
"version": "0.1.34", "version": "0.1.35",
"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",
@@ -757,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",
@@ -773,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",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@govoplan/core-webui", "name": "@govoplan/core-webui",
"version": "0.1.34", "version": "0.1.35",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+3 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "@govoplan/core-webui", "name": "@govoplan/core-webui",
"version": "0.1.34", "version": "0.1.35",
"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",