Add governed institutional capability contracts
This commit is contained in:
@@ -53,7 +53,9 @@ from govoplan_core.core.postbox import (
|
||||
)
|
||||
from govoplan_core.core.workflows import (
|
||||
CAPABILITY_WORKFLOW_RUNTIME_WORKER,
|
||||
CAPABILITY_WORKFLOW_TRIGGER_DISPATCHER,
|
||||
WorkflowRuntimeWorker,
|
||||
WorkflowTriggerDispatcher,
|
||||
)
|
||||
from govoplan_core.core.registry import PlatformRegistry
|
||||
from govoplan_core.core.runtime import configure_runtime
|
||||
@@ -385,6 +387,18 @@ def _workflow_runtime_worker(
|
||||
return capability
|
||||
|
||||
|
||||
def _workflow_trigger_dispatcher(
|
||||
registry: PlatformRegistry | None = None,
|
||||
) -> WorkflowTriggerDispatcher | None:
|
||||
registry = registry or _platform_registry()
|
||||
if not registry.has_capability(CAPABILITY_WORKFLOW_TRIGGER_DISPATCHER):
|
||||
return None
|
||||
capability = registry.require_capability(CAPABILITY_WORKFLOW_TRIGGER_DISPATCHER)
|
||||
if not isinstance(capability, WorkflowTriggerDispatcher):
|
||||
raise RuntimeError("Workflow trigger dispatcher capability is invalid")
|
||||
return capability
|
||||
|
||||
|
||||
def _postbox_routing_provider(
|
||||
registry: PlatformRegistry | None = None,
|
||||
) -> PostboxRoutingProvider | None:
|
||||
@@ -764,7 +778,8 @@ def dispatch_platform_events(self, limit: int = 100):
|
||||
"observer_failed": 0,
|
||||
}
|
||||
dataflow_dispatcher = _dataflow_trigger_dispatcher(registry)
|
||||
consumers = ()
|
||||
workflow_dispatcher = _workflow_trigger_dispatcher(registry)
|
||||
consumers: list[DurableEventConsumer] = []
|
||||
if dataflow_dispatcher is not None:
|
||||
|
||||
def deliver_to_dataflow(
|
||||
@@ -776,19 +791,38 @@ def dispatch_platform_events(self, limit: int = 100):
|
||||
event=event,
|
||||
)
|
||||
|
||||
consumers = (
|
||||
consumers.append(
|
||||
DurableEventConsumer(
|
||||
consumer_id="dataflow.event-triggers.v1",
|
||||
event_types=frozenset({"*"}),
|
||||
classifications=frozenset({"public", "internal"}),
|
||||
handler=deliver_to_dataflow,
|
||||
),
|
||||
)
|
||||
)
|
||||
if workflow_dispatcher is not None:
|
||||
|
||||
def deliver_to_workflow(
|
||||
event: PlatformEvent,
|
||||
_delivery_key: str,
|
||||
) -> None:
|
||||
workflow_dispatcher.ingest_event(
|
||||
session,
|
||||
event=event,
|
||||
)
|
||||
|
||||
consumers.append(
|
||||
DurableEventConsumer(
|
||||
consumer_id="workflow.event-triggers.v1",
|
||||
event_types=frozenset({"*"}),
|
||||
classifications=frozenset({"public", "internal"}),
|
||||
handler=deliver_to_workflow,
|
||||
)
|
||||
)
|
||||
|
||||
result = dict(
|
||||
outbox.dispatch_pending(
|
||||
session,
|
||||
consumers=consumers,
|
||||
consumers=tuple(consumers),
|
||||
observer=publish_platform_event,
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from typing import Protocol, runtime_checkable
|
||||
|
||||
|
||||
CAPABILITY_APPROVAL_REQUESTS = "approvals.requests"
|
||||
|
||||
|
||||
class ApprovalCapabilityError(ValueError):
|
||||
"""Stable error raised by Approval capability implementations."""
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ApprovalActorSelector:
|
||||
kind: str
|
||||
value: str
|
||||
label: str | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ApprovalStepDefinition:
|
||||
key: str
|
||||
label: str
|
||||
selectors: tuple[ApprovalActorSelector, ...]
|
||||
required_approvals: int = 1
|
||||
rejection_policy: str = "fail_fast"
|
||||
due_at: datetime | None = None
|
||||
signature_required: bool = False
|
||||
forbidden_evidence_roles: tuple[str, ...] = ()
|
||||
metadata: Mapping[str, object] = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ApprovalRequestCreateCommand:
|
||||
title: str
|
||||
subject_module: str
|
||||
subject_type: str
|
||||
subject_id: str
|
||||
subject_version: str | None
|
||||
subject_digest: str
|
||||
steps: tuple[ApprovalStepDefinition, ...]
|
||||
description: str | None = None
|
||||
separation_of_duties: bool = True
|
||||
unique_actors_across_steps: bool = False
|
||||
expires_at: datetime | None = None
|
||||
policy_refs: tuple[str, ...] = ()
|
||||
evidence_actors: Mapping[str, tuple[str, ...]] = field(default_factory=dict)
|
||||
template_id: str | None = None
|
||||
template_revision: int | None = None
|
||||
metadata: Mapping[str, object] = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ApprovalTemplateCreateCommand:
|
||||
key: str
|
||||
title: str
|
||||
steps: tuple[ApprovalStepDefinition, ...]
|
||||
description: str | None = None
|
||||
separation_of_duties: bool = True
|
||||
unique_actors_across_steps: bool = False
|
||||
metadata: Mapping[str, object] = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ApprovalTemplateRef:
|
||||
id: str
|
||||
key: str
|
||||
revision: int
|
||||
state: str
|
||||
content_sha256: str
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ApprovalDecisionCommand:
|
||||
outcome: str
|
||||
reason: str
|
||||
expected_revision: int
|
||||
idempotency_key: str
|
||||
delegated_for_account_id: str | None = None
|
||||
signature_ref: Mapping[str, object] | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ApprovalRequestRef:
|
||||
id: str
|
||||
revision: int
|
||||
state: str
|
||||
current_step_key: str | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ApprovalDecisionReceipt:
|
||||
request_id: str
|
||||
revision: int
|
||||
state: str
|
||||
step_key: str
|
||||
outcome: str
|
||||
actor_id: str
|
||||
recorded_at: datetime
|
||||
receipt_sha256: str
|
||||
authority_provenance: Mapping[str, object] = field(default_factory=dict)
|
||||
replayed: bool = False
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ApprovalCheck:
|
||||
request_id: str
|
||||
revision: int
|
||||
state: str
|
||||
approved: bool
|
||||
subject_module: str
|
||||
subject_type: str
|
||||
subject_id: str
|
||||
subject_version: str | None
|
||||
subject_digest: str
|
||||
completed_at: datetime | None = None
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class ApprovalRequestProvider(Protocol):
|
||||
def create_template(
|
||||
self,
|
||||
session: object,
|
||||
principal: object,
|
||||
*,
|
||||
command: ApprovalTemplateCreateCommand,
|
||||
idempotency_key: str,
|
||||
) -> ApprovalTemplateRef: ...
|
||||
|
||||
def revise_template(
|
||||
self,
|
||||
session: object,
|
||||
principal: object,
|
||||
*,
|
||||
template_id: str,
|
||||
command: ApprovalTemplateCreateCommand,
|
||||
expected_revision: int,
|
||||
idempotency_key: str,
|
||||
) -> ApprovalTemplateRef: ...
|
||||
|
||||
def publish_template(
|
||||
self,
|
||||
session: object,
|
||||
principal: object,
|
||||
*,
|
||||
template_id: str,
|
||||
expected_revision: int,
|
||||
idempotency_key: str,
|
||||
) -> ApprovalTemplateRef: ...
|
||||
|
||||
def create_request(
|
||||
self,
|
||||
session: object,
|
||||
principal: object,
|
||||
*,
|
||||
command: ApprovalRequestCreateCommand,
|
||||
idempotency_key: str,
|
||||
) -> ApprovalRequestRef: ...
|
||||
|
||||
def get_request(
|
||||
self,
|
||||
session: object,
|
||||
principal: object,
|
||||
*,
|
||||
request_id: str,
|
||||
) -> Mapping[str, object] | None: ...
|
||||
|
||||
def decide(
|
||||
self,
|
||||
session: object,
|
||||
principal: object,
|
||||
*,
|
||||
request_id: str,
|
||||
command: ApprovalDecisionCommand,
|
||||
) -> ApprovalDecisionReceipt: ...
|
||||
|
||||
def check_approved(
|
||||
self,
|
||||
session: object,
|
||||
principal: object,
|
||||
*,
|
||||
request_id: str,
|
||||
subject_module: str,
|
||||
subject_type: str,
|
||||
subject_id: str,
|
||||
subject_version: str | None,
|
||||
subject_digest: str,
|
||||
) -> ApprovalCheck: ...
|
||||
|
||||
|
||||
__all__ = [
|
||||
"ApprovalActorSelector",
|
||||
"ApprovalCapabilityError",
|
||||
"ApprovalCheck",
|
||||
"ApprovalDecisionCommand",
|
||||
"ApprovalDecisionReceipt",
|
||||
"ApprovalRequestCreateCommand",
|
||||
"ApprovalRequestProvider",
|
||||
"ApprovalRequestRef",
|
||||
"ApprovalStepDefinition",
|
||||
"ApprovalTemplateCreateCommand",
|
||||
"ApprovalTemplateRef",
|
||||
"CAPABILITY_APPROVAL_REQUESTS",
|
||||
]
|
||||
@@ -0,0 +1,692 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from typing import Literal, Protocol, runtime_checkable
|
||||
|
||||
|
||||
CAPABILITY_ENCRYPTION_KEY_VAULT = "encryption.keyVault"
|
||||
CAPABILITY_ENCRYPTION_CONTENT_PROTECTION = "encryption.contentProtection"
|
||||
CAPABILITY_ENCRYPTION_RECOVERY = "encryption.recoveryCeremony"
|
||||
CAPABILITY_ENCRYPTION_DISABLE_PREFLIGHT = "encryption.disablePreflight"
|
||||
CAPABILITY_ENCRYPTION_KEY_MATERIAL_PROVIDER_PREFIX = "encryption.keyMaterialProvider."
|
||||
ENCRYPTION_CONTRACT_VERSION = "1"
|
||||
|
||||
ProtectionProfileKind = Literal[
|
||||
"server_envelope",
|
||||
"tenant_held",
|
||||
"end_to_end",
|
||||
]
|
||||
KeyLifecycleState = Literal[
|
||||
"active",
|
||||
"rotation_pending",
|
||||
"revoked",
|
||||
"destruction_scheduled",
|
||||
"destroyed",
|
||||
"unavailable",
|
||||
]
|
||||
ProtectionState = Literal[
|
||||
"active",
|
||||
"migration_pending",
|
||||
"migrating",
|
||||
"migrated",
|
||||
"decrypted",
|
||||
"exported",
|
||||
"destroyed",
|
||||
"unavailable",
|
||||
]
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class KeyMaterialProvisionRequest:
|
||||
tenant_id: str
|
||||
vault_id: str
|
||||
key_version: int
|
||||
algorithm_suite: str
|
||||
purpose: str
|
||||
idempotency_key: str
|
||||
provider_policy: Mapping[str, object] = field(default_factory=dict)
|
||||
import_reference: str | None = None
|
||||
contract_version: str = ENCRYPTION_CONTRACT_VERSION
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
_validate_contract(self.contract_version)
|
||||
if self.key_version < 1:
|
||||
raise ValueError("Key version must be positive")
|
||||
for label, value in (
|
||||
("tenant id", self.tenant_id),
|
||||
("vault id", self.vault_id),
|
||||
("algorithm suite", self.algorithm_suite),
|
||||
("purpose", self.purpose),
|
||||
("idempotency key", self.idempotency_key),
|
||||
):
|
||||
_require_text(value, label)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class KeyMaterialDescriptor:
|
||||
provider_id: str
|
||||
provider_key_ref: str
|
||||
algorithm_suite: str
|
||||
state: KeyLifecycleState
|
||||
created_at: datetime
|
||||
public_key_ref: str | None = None
|
||||
imported: bool = False
|
||||
exportable: bool = False
|
||||
provider_version: str | None = None
|
||||
provenance: Mapping[str, object] = field(default_factory=dict)
|
||||
contract_version: str = ENCRYPTION_CONTRACT_VERSION
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
_validate_contract(self.contract_version)
|
||||
for label, value in (
|
||||
("provider id", self.provider_id),
|
||||
("provider key reference", self.provider_key_ref),
|
||||
("algorithm suite", self.algorithm_suite),
|
||||
):
|
||||
_require_text(value, label)
|
||||
_reject_secret_mapping(self.provenance, "Provider provenance")
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class KeyVaultCreateRequest:
|
||||
tenant_id: str
|
||||
vault_id: str
|
||||
name: str
|
||||
provider_id: str
|
||||
purpose: str
|
||||
algorithm_suite: str
|
||||
scope_type: str
|
||||
scope_id: str | None
|
||||
policy_ref: str
|
||||
idempotency_key: str
|
||||
recovery_quorum: int = 2
|
||||
profile_kind: ProtectionProfileKind = "server_envelope"
|
||||
import_reference: str | None = None
|
||||
provider_policy: Mapping[str, object] = field(default_factory=dict)
|
||||
contract_version: str = ENCRYPTION_CONTRACT_VERSION
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
_validate_contract(self.contract_version)
|
||||
if self.recovery_quorum < 1:
|
||||
raise ValueError("Recovery quorum must be positive")
|
||||
for label, value in (
|
||||
("tenant id", self.tenant_id),
|
||||
("vault id", self.vault_id),
|
||||
("name", self.name),
|
||||
("provider id", self.provider_id),
|
||||
("purpose", self.purpose),
|
||||
("algorithm suite", self.algorithm_suite),
|
||||
("scope type", self.scope_type),
|
||||
("policy reference", self.policy_ref),
|
||||
("idempotency key", self.idempotency_key),
|
||||
):
|
||||
_require_text(value, label)
|
||||
_reject_secret_mapping(self.provider_policy, "Provider policy")
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class KeyVersionRef:
|
||||
tenant_id: str
|
||||
vault_id: str
|
||||
version: int
|
||||
provider_id: str
|
||||
provider_key_ref: str
|
||||
algorithm_suite: str
|
||||
state: KeyLifecycleState
|
||||
created_at: datetime
|
||||
activated_at: datetime | None = None
|
||||
revoked_at: datetime | None = None
|
||||
destruction_scheduled_at: datetime | None = None
|
||||
destroyed_at: datetime | None = None
|
||||
public_key_ref: str | None = None
|
||||
imported: bool = False
|
||||
exportable: bool = False
|
||||
provenance: Mapping[str, object] = field(default_factory=dict)
|
||||
contract_version: str = ENCRYPTION_CONTRACT_VERSION
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class KeyVaultRef:
|
||||
tenant_id: str
|
||||
vault_id: str
|
||||
name: str
|
||||
provider_id: str
|
||||
purpose: str
|
||||
profile_kind: ProtectionProfileKind
|
||||
scope_type: str
|
||||
scope_id: str | None
|
||||
policy_ref: str
|
||||
recovery_quorum: int
|
||||
state: str
|
||||
revision: int
|
||||
current_key: KeyVersionRef | None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
provenance: Mapping[str, object] = field(default_factory=dict)
|
||||
contract_version: str = ENCRYPTION_CONTRACT_VERSION
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class KeyRotationRequest:
|
||||
tenant_id: str
|
||||
vault_id: str
|
||||
expected_revision: int
|
||||
reason: str
|
||||
policy_decision_ref: str
|
||||
assurance_evidence_ref: str
|
||||
idempotency_key: str
|
||||
algorithm_suite: str | None = None
|
||||
provider_policy: Mapping[str, object] = field(default_factory=dict)
|
||||
contract_version: str = ENCRYPTION_CONTRACT_VERSION
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
_validate_contract(self.contract_version)
|
||||
if self.expected_revision < 1:
|
||||
raise ValueError("Expected revision must be positive")
|
||||
for label, value in (
|
||||
("tenant id", self.tenant_id),
|
||||
("vault id", self.vault_id),
|
||||
("reason", self.reason),
|
||||
("policy decision reference", self.policy_decision_ref),
|
||||
("assurance evidence reference", self.assurance_evidence_ref),
|
||||
("idempotency key", self.idempotency_key),
|
||||
):
|
||||
_require_text(value, label)
|
||||
_reject_secret_mapping(self.provider_policy, "Provider policy")
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class KeyLifecycleRequest:
|
||||
tenant_id: str
|
||||
vault_id: str
|
||||
key_version: int
|
||||
expected_revision: int
|
||||
reason: str
|
||||
policy_decision_ref: str
|
||||
assurance_evidence_ref: str
|
||||
idempotency_key: str
|
||||
effective_at: datetime | None = None
|
||||
contract_version: str = ENCRYPTION_CONTRACT_VERSION
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
_validate_contract(self.contract_version)
|
||||
if self.key_version < 1 or self.expected_revision < 1:
|
||||
raise ValueError("Key version and expected revision must be positive")
|
||||
for label, value in (
|
||||
("tenant id", self.tenant_id),
|
||||
("vault id", self.vault_id),
|
||||
("reason", self.reason),
|
||||
("policy decision reference", self.policy_decision_ref),
|
||||
("assurance evidence reference", self.assurance_evidence_ref),
|
||||
("idempotency key", self.idempotency_key),
|
||||
):
|
||||
_require_text(value, label)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ContentProtectionEnvelope:
|
||||
envelope_id: str
|
||||
tenant_id: str
|
||||
owner_module: str
|
||||
resource_type: str
|
||||
resource_id: str
|
||||
profile_kind: ProtectionProfileKind
|
||||
profile_id: str
|
||||
provider_id: str
|
||||
vault_id: str
|
||||
key_version: int
|
||||
algorithm_suite: str
|
||||
ciphertext_ref: str
|
||||
ciphertext_digest: str
|
||||
authenticated_context_digest: str
|
||||
state: ProtectionState
|
||||
created_at: datetime
|
||||
wrapped_key_refs: tuple[str, ...] = ()
|
||||
source_envelope_id: str | None = None
|
||||
migration_id: str | None = None
|
||||
metadata: Mapping[str, object] = field(default_factory=dict)
|
||||
contract_version: str = ENCRYPTION_CONTRACT_VERSION
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
_validate_contract(self.contract_version)
|
||||
if self.key_version < 1:
|
||||
raise ValueError("Envelope key version must be positive")
|
||||
for label, value in (
|
||||
("envelope id", self.envelope_id),
|
||||
("tenant id", self.tenant_id),
|
||||
("owner module", self.owner_module),
|
||||
("resource type", self.resource_type),
|
||||
("resource id", self.resource_id),
|
||||
("profile id", self.profile_id),
|
||||
("provider id", self.provider_id),
|
||||
("vault id", self.vault_id),
|
||||
("algorithm suite", self.algorithm_suite),
|
||||
("ciphertext reference", self.ciphertext_ref),
|
||||
("ciphertext digest", self.ciphertext_digest),
|
||||
("authenticated context digest", self.authenticated_context_digest),
|
||||
):
|
||||
_require_text(value, label)
|
||||
_reject_secret_mapping(self.metadata, "Protection metadata")
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ProtectionRegistrationRequest:
|
||||
envelope: ContentProtectionEnvelope
|
||||
idempotency_key: str
|
||||
policy_decision_ref: str
|
||||
contract_version: str = ENCRYPTION_CONTRACT_VERSION
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
_validate_contract(self.contract_version)
|
||||
_require_text(self.idempotency_key, "idempotency key")
|
||||
_require_text(self.policy_decision_ref, "policy decision reference")
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ProtectionMigrationRequest:
|
||||
tenant_id: str
|
||||
envelope_id: str
|
||||
target_provider_id: str
|
||||
target_vault_id: str
|
||||
target_key_version: int
|
||||
target_algorithm_suite: str
|
||||
mode: Literal["rewrap", "reencrypt", "decrypt", "export", "destroy"]
|
||||
policy_decision_ref: str
|
||||
assurance_evidence_ref: str
|
||||
idempotency_key: str
|
||||
contract_version: str = ENCRYPTION_CONTRACT_VERSION
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
_validate_contract(self.contract_version)
|
||||
if self.target_key_version < 1:
|
||||
raise ValueError("Target key version must be positive")
|
||||
for label, value in (
|
||||
("tenant id", self.tenant_id),
|
||||
("envelope id", self.envelope_id),
|
||||
("target provider id", self.target_provider_id),
|
||||
("target vault id", self.target_vault_id),
|
||||
("target algorithm suite", self.target_algorithm_suite),
|
||||
("policy decision reference", self.policy_decision_ref),
|
||||
("assurance evidence reference", self.assurance_evidence_ref),
|
||||
("idempotency key", self.idempotency_key),
|
||||
):
|
||||
_require_text(value, label)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ProtectionMigrationResult:
|
||||
migration_id: str
|
||||
state: Literal[
|
||||
"requested",
|
||||
"running",
|
||||
"succeeded",
|
||||
"rejected",
|
||||
"outcome_unknown",
|
||||
"reconciled",
|
||||
]
|
||||
source_envelope_id: str
|
||||
target_envelope: ContentProtectionEnvelope | None = None
|
||||
error: str | None = None
|
||||
evidence_refs: tuple[str, ...] = ()
|
||||
provenance: Mapping[str, object] = field(default_factory=dict)
|
||||
contract_version: str = ENCRYPTION_CONTRACT_VERSION
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class RecoveryRequest:
|
||||
tenant_id: str
|
||||
vault_id: str
|
||||
reason: str
|
||||
requested_scope: str
|
||||
policy_decision_ref: str
|
||||
assurance_evidence_ref: str
|
||||
idempotency_key: str
|
||||
expires_at: datetime
|
||||
contract_version: str = ENCRYPTION_CONTRACT_VERSION
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
_validate_contract(self.contract_version)
|
||||
for label, value in (
|
||||
("tenant id", self.tenant_id),
|
||||
("vault id", self.vault_id),
|
||||
("reason", self.reason),
|
||||
("requested scope", self.requested_scope),
|
||||
("policy decision reference", self.policy_decision_ref),
|
||||
("assurance evidence reference", self.assurance_evidence_ref),
|
||||
("idempotency key", self.idempotency_key),
|
||||
):
|
||||
_require_text(value, label)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class RecoveryApprovalRequest:
|
||||
tenant_id: str
|
||||
recovery_id: str
|
||||
decision: Literal["approve", "reject"]
|
||||
reason: str
|
||||
assurance_evidence_ref: str
|
||||
expected_revision: int
|
||||
idempotency_key: str
|
||||
contract_version: str = ENCRYPTION_CONTRACT_VERSION
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
_validate_contract(self.contract_version)
|
||||
if self.expected_revision < 1:
|
||||
raise ValueError("Expected revision must be positive")
|
||||
for label, value in (
|
||||
("tenant id", self.tenant_id),
|
||||
("recovery id", self.recovery_id),
|
||||
("reason", self.reason),
|
||||
("assurance evidence reference", self.assurance_evidence_ref),
|
||||
("idempotency key", self.idempotency_key),
|
||||
):
|
||||
_require_text(value, label)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class RecoveryRef:
|
||||
tenant_id: str
|
||||
recovery_id: str
|
||||
vault_id: str
|
||||
state: Literal["pending", "approved", "rejected", "expired", "executed"]
|
||||
requested_scope: str
|
||||
quorum: int
|
||||
approvals: int
|
||||
rejections: int
|
||||
revision: int
|
||||
expires_at: datetime
|
||||
policy_decision_ref: str
|
||||
execution_ref: str | None = None
|
||||
provenance: Mapping[str, object] = field(default_factory=dict)
|
||||
contract_version: str = ENCRYPTION_CONTRACT_VERSION
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class DisablePreflightReport:
|
||||
allowed: bool
|
||||
protected_count: int
|
||||
unresolved_count: int
|
||||
state_counts: Mapping[str, int]
|
||||
blocking_envelope_refs: tuple[str, ...]
|
||||
required_actions: tuple[str, ...]
|
||||
generated_at: datetime
|
||||
contract_version: str = ENCRYPTION_CONTRACT_VERSION
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class EncryptionKeyMaterialProvider(Protocol):
|
||||
def provision_key(
|
||||
self,
|
||||
session: object,
|
||||
principal: object,
|
||||
*,
|
||||
request: KeyMaterialProvisionRequest,
|
||||
) -> KeyMaterialDescriptor: ...
|
||||
|
||||
def schedule_key_destruction(
|
||||
self,
|
||||
session: object,
|
||||
principal: object,
|
||||
*,
|
||||
provider_key_ref: str,
|
||||
effective_at: datetime,
|
||||
idempotency_key: str,
|
||||
) -> KeyMaterialDescriptor: ...
|
||||
|
||||
def revoke_key(
|
||||
self,
|
||||
session: object,
|
||||
principal: object,
|
||||
*,
|
||||
provider_key_ref: str,
|
||||
reason: str,
|
||||
idempotency_key: str,
|
||||
) -> KeyMaterialDescriptor: ...
|
||||
|
||||
def key_status(
|
||||
self,
|
||||
session: object,
|
||||
*,
|
||||
provider_key_ref: str,
|
||||
) -> KeyMaterialDescriptor | None: ...
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class EncryptionKeyVault(Protocol):
|
||||
def create_vault(
|
||||
self,
|
||||
session: object,
|
||||
principal: object,
|
||||
*,
|
||||
request: KeyVaultCreateRequest,
|
||||
) -> KeyVaultRef: ...
|
||||
|
||||
def rotate_key(
|
||||
self,
|
||||
session: object,
|
||||
principal: object,
|
||||
*,
|
||||
request: KeyRotationRequest,
|
||||
) -> KeyVaultRef: ...
|
||||
|
||||
def revoke_key(
|
||||
self,
|
||||
session: object,
|
||||
principal: object,
|
||||
*,
|
||||
request: KeyLifecycleRequest,
|
||||
) -> KeyVaultRef: ...
|
||||
|
||||
def schedule_destruction(
|
||||
self,
|
||||
session: object,
|
||||
principal: object,
|
||||
*,
|
||||
request: KeyLifecycleRequest,
|
||||
) -> KeyVaultRef: ...
|
||||
|
||||
def get_vault(
|
||||
self,
|
||||
session: object,
|
||||
*,
|
||||
tenant_id: str,
|
||||
vault_id: str,
|
||||
) -> KeyVaultRef | None: ...
|
||||
|
||||
def reconcile_vault(
|
||||
self,
|
||||
session: object,
|
||||
principal: object,
|
||||
*,
|
||||
tenant_id: str,
|
||||
vault_id: str,
|
||||
) -> KeyVaultRef: ...
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class ContentProtectionRegistry(Protocol):
|
||||
def register_envelope(
|
||||
self,
|
||||
session: object,
|
||||
principal: object,
|
||||
*,
|
||||
request: ProtectionRegistrationRequest,
|
||||
) -> ContentProtectionEnvelope: ...
|
||||
|
||||
def request_migration(
|
||||
self,
|
||||
session: object,
|
||||
principal: object,
|
||||
*,
|
||||
request: ProtectionMigrationRequest,
|
||||
) -> ProtectionMigrationResult: ...
|
||||
|
||||
def reconcile_migration(
|
||||
self,
|
||||
session: object,
|
||||
principal: object,
|
||||
*,
|
||||
migration_id: str,
|
||||
) -> ProtectionMigrationResult: ...
|
||||
|
||||
def get_envelope(
|
||||
self,
|
||||
session: object,
|
||||
principal: object,
|
||||
*,
|
||||
tenant_id: str,
|
||||
envelope_id: str,
|
||||
) -> ContentProtectionEnvelope | None: ...
|
||||
|
||||
def record_migration_outcome(
|
||||
self,
|
||||
session: object,
|
||||
principal: object,
|
||||
*,
|
||||
migration_id: str,
|
||||
state: Literal["succeeded", "rejected", "outcome_unknown"],
|
||||
evidence_refs: tuple[str, ...],
|
||||
target_envelope: ContentProtectionEnvelope | None = None,
|
||||
error_code: str | None = None,
|
||||
) -> ProtectionMigrationResult: ...
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class EncryptionRecoveryCeremony(Protocol):
|
||||
def request_recovery(
|
||||
self,
|
||||
session: object,
|
||||
principal: object,
|
||||
*,
|
||||
request: RecoveryRequest,
|
||||
) -> RecoveryRef: ...
|
||||
|
||||
def decide_recovery(
|
||||
self,
|
||||
session: object,
|
||||
principal: object,
|
||||
*,
|
||||
request: RecoveryApprovalRequest,
|
||||
) -> RecoveryRef: ...
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class EncryptionDisablePreflight(Protocol):
|
||||
def assess_disable(
|
||||
self,
|
||||
session: object,
|
||||
*,
|
||||
tenant_id: str | None = None,
|
||||
limit: int = 100,
|
||||
) -> DisablePreflightReport: ...
|
||||
|
||||
|
||||
def key_material_provider(
|
||||
registry: object | None,
|
||||
provider_id: str,
|
||||
) -> EncryptionKeyMaterialProvider | None:
|
||||
capability = _capability(
|
||||
registry,
|
||||
f"{CAPABILITY_ENCRYPTION_KEY_MATERIAL_PROVIDER_PREFIX}{provider_id}",
|
||||
)
|
||||
return capability if isinstance(capability, EncryptionKeyMaterialProvider) else None
|
||||
|
||||
|
||||
def encryption_key_vault(registry: object | None) -> EncryptionKeyVault | None:
|
||||
value = _capability(registry, CAPABILITY_ENCRYPTION_KEY_VAULT)
|
||||
return value if isinstance(value, EncryptionKeyVault) else None
|
||||
|
||||
|
||||
def content_protection_registry(
|
||||
registry: object | None,
|
||||
) -> ContentProtectionRegistry | None:
|
||||
value = _capability(registry, CAPABILITY_ENCRYPTION_CONTENT_PROTECTION)
|
||||
return value if isinstance(value, ContentProtectionRegistry) else None
|
||||
|
||||
|
||||
def encryption_recovery_ceremony(
|
||||
registry: object | None,
|
||||
) -> EncryptionRecoveryCeremony | None:
|
||||
value = _capability(registry, CAPABILITY_ENCRYPTION_RECOVERY)
|
||||
return value if isinstance(value, EncryptionRecoveryCeremony) else None
|
||||
|
||||
|
||||
def encryption_disable_preflight(
|
||||
registry: object | None,
|
||||
) -> EncryptionDisablePreflight | None:
|
||||
value = _capability(registry, CAPABILITY_ENCRYPTION_DISABLE_PREFLIGHT)
|
||||
return value if isinstance(value, EncryptionDisablePreflight) else None
|
||||
|
||||
|
||||
def _capability(registry: object | None, name: str) -> object | None:
|
||||
if (
|
||||
registry is None
|
||||
or not hasattr(registry, "has_capability")
|
||||
or not hasattr(registry, "capability")
|
||||
or not registry.has_capability(name)
|
||||
):
|
||||
return None
|
||||
return registry.capability(name)
|
||||
|
||||
|
||||
def _validate_contract(value: str) -> None:
|
||||
if value != ENCRYPTION_CONTRACT_VERSION:
|
||||
raise ValueError("Unsupported encryption contract version")
|
||||
|
||||
|
||||
def _require_text(value: str, label: str) -> None:
|
||||
if not value.strip():
|
||||
raise ValueError(f"{label.capitalize()} is required")
|
||||
|
||||
|
||||
def _reject_secret_mapping(value: Mapping[str, object], label: str) -> None:
|
||||
forbidden = {
|
||||
"ciphertext",
|
||||
"credential",
|
||||
"key",
|
||||
"key_material",
|
||||
"password",
|
||||
"plaintext",
|
||||
"private_key",
|
||||
"secret",
|
||||
"token",
|
||||
}
|
||||
keys = {str(key).strip().casefold() for key in value}
|
||||
if forbidden & keys:
|
||||
raise ValueError(f"{label} must not contain secret material")
|
||||
|
||||
|
||||
__all__ = [
|
||||
"CAPABILITY_ENCRYPTION_CONTENT_PROTECTION",
|
||||
"CAPABILITY_ENCRYPTION_DISABLE_PREFLIGHT",
|
||||
"CAPABILITY_ENCRYPTION_KEY_MATERIAL_PROVIDER_PREFIX",
|
||||
"CAPABILITY_ENCRYPTION_KEY_VAULT",
|
||||
"CAPABILITY_ENCRYPTION_RECOVERY",
|
||||
"ContentProtectionEnvelope",
|
||||
"ContentProtectionRegistry",
|
||||
"DisablePreflightReport",
|
||||
"EncryptionDisablePreflight",
|
||||
"EncryptionKeyMaterialProvider",
|
||||
"EncryptionKeyVault",
|
||||
"EncryptionRecoveryCeremony",
|
||||
"KeyLifecycleRequest",
|
||||
"KeyMaterialDescriptor",
|
||||
"KeyMaterialProvisionRequest",
|
||||
"KeyRotationRequest",
|
||||
"KeyVaultCreateRequest",
|
||||
"KeyVaultRef",
|
||||
"KeyVersionRef",
|
||||
"ProtectionMigrationRequest",
|
||||
"ProtectionMigrationResult",
|
||||
"ProtectionRegistrationRequest",
|
||||
"RecoveryApprovalRequest",
|
||||
"RecoveryRef",
|
||||
"RecoveryRequest",
|
||||
"content_protection_registry",
|
||||
"encryption_disable_preflight",
|
||||
"encryption_key_vault",
|
||||
"encryption_recovery_ceremony",
|
||||
"key_material_provider",
|
||||
]
|
||||
@@ -0,0 +1,321 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from typing import Literal, Protocol, runtime_checkable
|
||||
|
||||
|
||||
CAPABILITY_IDENTITY_TRUST_DIRECTORY = "identity_trust.directory"
|
||||
CAPABILITY_IDENTITY_TRUST_ASSURANCE = "identity_trust.assurance"
|
||||
IDENTITY_TRUST_CONTRACT_VERSION = "1"
|
||||
|
||||
DeviceKeyPurpose = Literal["encryption", "signing", "encryption_and_signing"]
|
||||
DeviceKeyStatus = Literal["active", "revoked", "expired"]
|
||||
TrustSubjectKind = Literal[
|
||||
"identity",
|
||||
"account",
|
||||
"function",
|
||||
"postbox",
|
||||
"external_recipient",
|
||||
]
|
||||
|
||||
_PRIVATE_JWK_FIELDS = frozenset({"d", "p", "q", "dp", "dq", "qi", "oth", "k"})
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class DeviceKeyRegistration:
|
||||
tenant_id: str
|
||||
identity_id: str
|
||||
account_id: str
|
||||
device_id: str
|
||||
key_id: str
|
||||
algorithm: str
|
||||
public_jwk: Mapping[str, object]
|
||||
purpose: DeviceKeyPurpose = "encryption"
|
||||
assurance_level: str = "software"
|
||||
attestation_ref: str | None = None
|
||||
expires_at: datetime | None = None
|
||||
idempotency_key: str = ""
|
||||
contract_version: str = IDENTITY_TRUST_CONTRACT_VERSION
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
_validate_contract(self.contract_version)
|
||||
for label, value in (
|
||||
("tenant id", self.tenant_id),
|
||||
("identity id", self.identity_id),
|
||||
("account id", self.account_id),
|
||||
("device id", self.device_id),
|
||||
("key id", self.key_id),
|
||||
("algorithm", self.algorithm),
|
||||
("idempotency key", self.idempotency_key),
|
||||
):
|
||||
_require_text(value, label)
|
||||
if not self.public_jwk or _PRIVATE_JWK_FIELDS & set(self.public_jwk):
|
||||
raise ValueError("Only a bounded public JWK may be registered")
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class DeviceKeyRef:
|
||||
tenant_id: str
|
||||
identity_id: str
|
||||
account_id: str
|
||||
device_id: str
|
||||
key_id: str
|
||||
algorithm: str
|
||||
public_jwk: Mapping[str, object]
|
||||
purpose: DeviceKeyPurpose
|
||||
assurance_level: str
|
||||
status: DeviceKeyStatus
|
||||
epoch: int
|
||||
registered_at: datetime
|
||||
attestation_ref: str | None = None
|
||||
expires_at: datetime | None = None
|
||||
revoked_at: datetime | None = None
|
||||
revocation_reason: str | None = None
|
||||
provenance: Mapping[str, object] = field(default_factory=dict)
|
||||
contract_version: str = IDENTITY_TRUST_CONTRACT_VERSION
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class KeyEpochRotationRequest:
|
||||
tenant_id: str
|
||||
subject_kind: TrustSubjectKind
|
||||
subject_id: str
|
||||
reason: str
|
||||
access_decision_ref: str
|
||||
idempotency_key: str
|
||||
history_policy: str = "all_retained"
|
||||
previous_epoch: int | None = None
|
||||
contract_version: str = IDENTITY_TRUST_CONTRACT_VERSION
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
_validate_contract(self.contract_version)
|
||||
for label, value in (
|
||||
("tenant id", self.tenant_id),
|
||||
("subject id", self.subject_id),
|
||||
("reason", self.reason),
|
||||
("access decision reference", self.access_decision_ref),
|
||||
("idempotency key", self.idempotency_key),
|
||||
):
|
||||
_require_text(value, label)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class KeyEpochRef:
|
||||
tenant_id: str
|
||||
subject_kind: TrustSubjectKind
|
||||
subject_id: str
|
||||
epoch: int
|
||||
state: Literal["active", "superseded", "revoked"]
|
||||
history_policy: str
|
||||
effective_at: datetime
|
||||
previous_epoch: int | None = None
|
||||
reason: str | None = None
|
||||
access_decision_ref: str | None = None
|
||||
provenance: Mapping[str, object] = field(default_factory=dict)
|
||||
contract_version: str = IDENTITY_TRUST_CONTRACT_VERSION
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class KeyAccessRequest:
|
||||
tenant_id: str
|
||||
account_id: str
|
||||
device_key_id: str
|
||||
subject_kind: TrustSubjectKind
|
||||
subject_id: str
|
||||
key_epoch: int
|
||||
access_decision_ref: str
|
||||
purpose: str
|
||||
requested_at: datetime
|
||||
function_assignment_id: str | None = None
|
||||
delegation_id: str | None = None
|
||||
resource_ref: str | None = None
|
||||
contract_version: str = IDENTITY_TRUST_CONTRACT_VERSION
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
_validate_contract(self.contract_version)
|
||||
if self.key_epoch < 1:
|
||||
raise ValueError("Key epoch must be positive")
|
||||
for label, value in (
|
||||
("tenant id", self.tenant_id),
|
||||
("account id", self.account_id),
|
||||
("device key id", self.device_key_id),
|
||||
("subject id", self.subject_id),
|
||||
("access decision reference", self.access_decision_ref),
|
||||
("purpose", self.purpose),
|
||||
):
|
||||
_require_text(value, label)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class KeyAccessDecision:
|
||||
allowed: bool
|
||||
decision_ref: str
|
||||
reason: str
|
||||
device_key: DeviceKeyRef | None = None
|
||||
epoch: KeyEpochRef | None = None
|
||||
audit_event_ref: str | None = None
|
||||
requirements: tuple[str, ...] = ()
|
||||
provenance: Mapping[str, object] = field(default_factory=dict)
|
||||
contract_version: str = IDENTITY_TRUST_CONTRACT_VERSION
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class AssuranceCheckRequest:
|
||||
tenant_id: str
|
||||
account_id: str
|
||||
purpose: str
|
||||
minimum_level: str
|
||||
evidence_ref: str
|
||||
evaluated_at: datetime
|
||||
maximum_age_seconds: int = 300
|
||||
device_key_id: str | None = None
|
||||
contract_version: str = IDENTITY_TRUST_CONTRACT_VERSION
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
_validate_contract(self.contract_version)
|
||||
if self.maximum_age_seconds < 1:
|
||||
raise ValueError("Assurance maximum age must be positive")
|
||||
for label, value in (
|
||||
("tenant id", self.tenant_id),
|
||||
("account id", self.account_id),
|
||||
("purpose", self.purpose),
|
||||
("minimum level", self.minimum_level),
|
||||
("evidence reference", self.evidence_ref),
|
||||
):
|
||||
_require_text(value, label)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class AssuranceDecision:
|
||||
allowed: bool
|
||||
reason: str
|
||||
assurance_level: str | None = None
|
||||
evidence_ref: str | None = None
|
||||
verified_at: datetime | None = None
|
||||
expires_at: datetime | None = None
|
||||
provenance: Mapping[str, object] = field(default_factory=dict)
|
||||
contract_version: str = IDENTITY_TRUST_CONTRACT_VERSION
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class IdentityTrustDirectory(Protocol):
|
||||
def register_device_key(
|
||||
self,
|
||||
session: object,
|
||||
principal: object,
|
||||
*,
|
||||
request: DeviceKeyRegistration,
|
||||
) -> DeviceKeyRef: ...
|
||||
|
||||
def revoke_device_key(
|
||||
self,
|
||||
session: object,
|
||||
principal: object,
|
||||
*,
|
||||
tenant_id: str,
|
||||
key_id: str,
|
||||
expected_epoch: int,
|
||||
reason: str,
|
||||
) -> DeviceKeyRef: ...
|
||||
|
||||
def list_device_keys(
|
||||
self,
|
||||
session: object,
|
||||
principal: object,
|
||||
*,
|
||||
tenant_id: str,
|
||||
account_id: str,
|
||||
active_only: bool = True,
|
||||
) -> tuple[DeviceKeyRef, ...]: ...
|
||||
|
||||
def rotate_epoch(
|
||||
self,
|
||||
session: object,
|
||||
principal: object,
|
||||
*,
|
||||
request: KeyEpochRotationRequest,
|
||||
) -> KeyEpochRef: ...
|
||||
|
||||
def resolve_epoch(
|
||||
self,
|
||||
session: object,
|
||||
*,
|
||||
tenant_id: str,
|
||||
subject_kind: TrustSubjectKind,
|
||||
subject_id: str,
|
||||
epoch: int | None = None,
|
||||
) -> KeyEpochRef | None: ...
|
||||
|
||||
def decide_key_access(
|
||||
self,
|
||||
session: object,
|
||||
principal: object,
|
||||
*,
|
||||
request: KeyAccessRequest,
|
||||
) -> KeyAccessDecision: ...
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class IdentityTrustAssurance(Protocol):
|
||||
def verify_assurance(
|
||||
self,
|
||||
session: object,
|
||||
principal: object,
|
||||
*,
|
||||
request: AssuranceCheckRequest,
|
||||
) -> AssuranceDecision: ...
|
||||
|
||||
|
||||
def identity_trust_directory(
|
||||
registry: object | None,
|
||||
) -> IdentityTrustDirectory | None:
|
||||
capability = _capability(registry, CAPABILITY_IDENTITY_TRUST_DIRECTORY)
|
||||
return capability if isinstance(capability, IdentityTrustDirectory) else None
|
||||
|
||||
|
||||
def identity_trust_assurance(
|
||||
registry: object | None,
|
||||
) -> IdentityTrustAssurance | None:
|
||||
capability = _capability(registry, CAPABILITY_IDENTITY_TRUST_ASSURANCE)
|
||||
return capability if isinstance(capability, IdentityTrustAssurance) else None
|
||||
|
||||
|
||||
def _capability(registry: object | None, name: str) -> object | None:
|
||||
if (
|
||||
registry is None
|
||||
or not hasattr(registry, "has_capability")
|
||||
or not hasattr(registry, "capability")
|
||||
or not registry.has_capability(name)
|
||||
):
|
||||
return None
|
||||
return registry.capability(name)
|
||||
|
||||
|
||||
def _validate_contract(value: str) -> None:
|
||||
if value != IDENTITY_TRUST_CONTRACT_VERSION:
|
||||
raise ValueError("Unsupported identity-trust contract version")
|
||||
|
||||
|
||||
def _require_text(value: str, label: str) -> None:
|
||||
if not value.strip():
|
||||
raise ValueError(f"{label.capitalize()} is required")
|
||||
|
||||
|
||||
__all__ = [
|
||||
"AssuranceCheckRequest",
|
||||
"AssuranceDecision",
|
||||
"CAPABILITY_IDENTITY_TRUST_ASSURANCE",
|
||||
"CAPABILITY_IDENTITY_TRUST_DIRECTORY",
|
||||
"DeviceKeyRef",
|
||||
"DeviceKeyRegistration",
|
||||
"IdentityTrustAssurance",
|
||||
"IdentityTrustDirectory",
|
||||
"KeyAccessDecision",
|
||||
"KeyAccessRequest",
|
||||
"KeyEpochRef",
|
||||
"KeyEpochRotationRequest",
|
||||
"identity_trust_assurance",
|
||||
"identity_trust_directory",
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,223 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping, Sequence
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from typing import Protocol, runtime_checkable
|
||||
|
||||
|
||||
CAPABILITY_VOTING_BALLOTS = "voting.ballots"
|
||||
CAPABILITY_VOTING_PROVIDER_PREFIX = "voting.provider."
|
||||
|
||||
VOTING_ASSURANCE_RECORDED = "recorded"
|
||||
VOTING_ASSURANCE_CONFIDENTIAL = "confidential"
|
||||
VOTING_ASSURANCE_SECRET = "secret"
|
||||
VOTING_ASSURANCE_EXTERNAL_CERTIFIED = "external_certified"
|
||||
|
||||
|
||||
class VotingCapabilityError(ValueError):
|
||||
"""Stable error raised by Voting capability implementations."""
|
||||
|
||||
|
||||
def voting_provider_capability(provider_id: str) -> str:
|
||||
normalized = str(provider_id or "").strip().lower()
|
||||
if not normalized or any(
|
||||
character not in "abcdefghijklmnopqrstuvwxyz0123456789_.-"
|
||||
for character in normalized
|
||||
):
|
||||
raise VotingCapabilityError("Voting provider id is invalid.")
|
||||
return f"{CAPABILITY_VOTING_PROVIDER_PREFIX}{normalized}"
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class VotingOption:
|
||||
key: str
|
||||
label: str
|
||||
description: str | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class VotingElector:
|
||||
subject_id: str
|
||||
label: str | None = None
|
||||
weight: int = 1
|
||||
provenance: Mapping[str, object] = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class VotingBallotCreateCommand:
|
||||
title: str
|
||||
method: str
|
||||
assurance_profile: str
|
||||
options: tuple[VotingOption, ...]
|
||||
electorate: tuple[VotingElector, ...]
|
||||
description: str | None = None
|
||||
context_module: str | None = None
|
||||
context_resource_type: str | None = None
|
||||
context_resource_id: str | None = None
|
||||
quorum_weight: int = 0
|
||||
threshold_numerator: int = 1
|
||||
threshold_denominator: int = 2
|
||||
allow_replacement: bool = True
|
||||
opens_at: datetime | None = None
|
||||
closes_at: datetime | None = None
|
||||
provider_id: str | None = None
|
||||
provider_ballot_ref: str | None = None
|
||||
metadata: Mapping[str, object] = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class VotingCastCommand:
|
||||
selections: tuple[str, ...]
|
||||
idempotency_key: str
|
||||
elector_id: str | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class VotingBallotRef:
|
||||
id: str
|
||||
revision: int
|
||||
state: str
|
||||
assurance_profile: str
|
||||
definition_sha256: str | None = None
|
||||
electorate_sha256: str | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class VotingReceipt:
|
||||
ballot_id: str
|
||||
revision: int
|
||||
receipt_sha256: str
|
||||
cast_at: datetime
|
||||
replaced_previous: bool = False
|
||||
replayed: bool = False
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class VotingResult:
|
||||
ballot_id: str
|
||||
revision: int
|
||||
counts: Mapping[str, int]
|
||||
weighted_counts: Mapping[str, int]
|
||||
cast_count: int
|
||||
cast_weight: int
|
||||
eligible_count: int
|
||||
eligible_weight: int
|
||||
quorum_met: bool
|
||||
threshold_met: bool
|
||||
winning_options: tuple[str, ...]
|
||||
result_sha256: str
|
||||
evidence: tuple[Mapping[str, object], ...] = ()
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ExternalVotingFinalizationRequest:
|
||||
tenant_id: str
|
||||
ballot_id: str
|
||||
provider_ballot_ref: str
|
||||
definition_sha256: str
|
||||
electorate_sha256: str
|
||||
options: tuple[VotingOption, ...]
|
||||
eligible_count: int
|
||||
eligible_weight: int
|
||||
requested_at: datetime
|
||||
idempotency_key: str
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class ExternalVotingProvider(Protocol):
|
||||
"""Provider boundary for confidential, secret, or certified voting.
|
||||
|
||||
Providers return aggregate results and evidence only. Raw ballots and
|
||||
provider credentials must not cross this boundary.
|
||||
"""
|
||||
|
||||
def finalize_ballot(
|
||||
self,
|
||||
session: object,
|
||||
principal: object,
|
||||
*,
|
||||
request: ExternalVotingFinalizationRequest,
|
||||
) -> VotingResult: ...
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class VotingBallotProvider(Protocol):
|
||||
def create_ballot(
|
||||
self,
|
||||
session: object,
|
||||
principal: object,
|
||||
*,
|
||||
command: VotingBallotCreateCommand,
|
||||
idempotency_key: str,
|
||||
) -> VotingBallotRef: ...
|
||||
|
||||
def get_ballot(
|
||||
self,
|
||||
session: object,
|
||||
principal: object,
|
||||
*,
|
||||
ballot_id: str,
|
||||
) -> Mapping[str, object] | None: ...
|
||||
|
||||
def open_ballot(
|
||||
self,
|
||||
session: object,
|
||||
principal: object,
|
||||
*,
|
||||
ballot_id: str,
|
||||
expected_revision: int,
|
||||
idempotency_key: str,
|
||||
) -> VotingBallotRef: ...
|
||||
|
||||
def cast_ballot(
|
||||
self,
|
||||
session: object,
|
||||
principal: object,
|
||||
*,
|
||||
ballot_id: str,
|
||||
command: VotingCastCommand,
|
||||
) -> VotingReceipt: ...
|
||||
|
||||
def close_ballot(
|
||||
self,
|
||||
session: object,
|
||||
principal: object,
|
||||
*,
|
||||
ballot_id: str,
|
||||
expected_revision: int,
|
||||
idempotency_key: str,
|
||||
) -> VotingResult: ...
|
||||
|
||||
def certify_ballot(
|
||||
self,
|
||||
session: object,
|
||||
principal: object,
|
||||
*,
|
||||
ballot_id: str,
|
||||
expected_revision: int,
|
||||
evidence: Sequence[Mapping[str, object]],
|
||||
idempotency_key: str,
|
||||
) -> VotingBallotRef: ...
|
||||
|
||||
|
||||
__all__ = [
|
||||
"CAPABILITY_VOTING_BALLOTS",
|
||||
"CAPABILITY_VOTING_PROVIDER_PREFIX",
|
||||
"ExternalVotingFinalizationRequest",
|
||||
"ExternalVotingProvider",
|
||||
"VOTING_ASSURANCE_CONFIDENTIAL",
|
||||
"VOTING_ASSURANCE_EXTERNAL_CERTIFIED",
|
||||
"VOTING_ASSURANCE_RECORDED",
|
||||
"VOTING_ASSURANCE_SECRET",
|
||||
"VotingBallotCreateCommand",
|
||||
"VotingBallotProvider",
|
||||
"VotingBallotRef",
|
||||
"VotingCapabilityError",
|
||||
"VotingCastCommand",
|
||||
"VotingElector",
|
||||
"VotingOption",
|
||||
"VotingReceipt",
|
||||
"VotingResult",
|
||||
"voting_provider_capability",
|
||||
]
|
||||
@@ -7,10 +7,13 @@ import hashlib
|
||||
import json
|
||||
from typing import Protocol, runtime_checkable
|
||||
|
||||
from govoplan_core.core.events import PlatformEvent
|
||||
|
||||
|
||||
CAPABILITY_WORKFLOW_RUNTIME_WORKER = "workflow.runtimeWorker"
|
||||
CAPABILITY_WORKFLOW_DEFINITION_CONTRIBUTIONS = "workflow.definitionContributions"
|
||||
CAPABILITY_WORKFLOW_ORCHESTRATION = "workflow.orchestration"
|
||||
CAPABILITY_WORKFLOW_TRIGGER_DISPATCHER = "workflow.triggerDispatcher"
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
@@ -126,8 +129,27 @@ class WorkflowRuntimeWorker(Protocol):
|
||||
*,
|
||||
now: datetime | None = None,
|
||||
limit: int = 50,
|
||||
) -> Mapping[str, object]:
|
||||
...
|
||||
) -> Mapping[str, object]: ...
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class WorkflowTriggerDispatcher(Protocol):
|
||||
"""Durable schedule/event start and wait-resumption boundary."""
|
||||
|
||||
def dispatch_due(
|
||||
self,
|
||||
session: object,
|
||||
*,
|
||||
now: datetime | None = None,
|
||||
limit: int = 50,
|
||||
) -> Mapping[str, object]: ...
|
||||
|
||||
def ingest_event(
|
||||
self,
|
||||
session: object,
|
||||
*,
|
||||
event: PlatformEvent,
|
||||
) -> Mapping[str, object]: ...
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
@@ -137,8 +159,7 @@ class WorkflowDefinitionContributionProvider(Protocol):
|
||||
session: object,
|
||||
*,
|
||||
tenant_ids: Sequence[str] = (),
|
||||
) -> Mapping[str, object]:
|
||||
...
|
||||
) -> Mapping[str, object]: ...
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
@@ -180,11 +201,20 @@ def workflow_runtime_worker(
|
||||
):
|
||||
return None
|
||||
capability = registry.capability(CAPABILITY_WORKFLOW_RUNTIME_WORKER)
|
||||
return (
|
||||
capability
|
||||
if isinstance(capability, WorkflowRuntimeWorker)
|
||||
else None
|
||||
)
|
||||
return capability if isinstance(capability, WorkflowRuntimeWorker) else None
|
||||
|
||||
|
||||
def workflow_trigger_dispatcher(
|
||||
registry: object | None,
|
||||
) -> WorkflowTriggerDispatcher | None:
|
||||
if (
|
||||
registry is None
|
||||
or not hasattr(registry, "has_capability")
|
||||
or not registry.has_capability(CAPABILITY_WORKFLOW_TRIGGER_DISPATCHER)
|
||||
):
|
||||
return None
|
||||
capability = registry.capability(CAPABILITY_WORKFLOW_TRIGGER_DISPATCHER)
|
||||
return capability if isinstance(capability, WorkflowTriggerDispatcher) else None
|
||||
|
||||
|
||||
def workflow_definition_contribution_provider(
|
||||
@@ -193,14 +223,10 @@ def workflow_definition_contribution_provider(
|
||||
if (
|
||||
registry is None
|
||||
or not hasattr(registry, "has_capability")
|
||||
or not registry.has_capability(
|
||||
CAPABILITY_WORKFLOW_DEFINITION_CONTRIBUTIONS
|
||||
)
|
||||
or not registry.has_capability(CAPABILITY_WORKFLOW_DEFINITION_CONTRIBUTIONS)
|
||||
):
|
||||
return None
|
||||
capability = registry.capability(
|
||||
CAPABILITY_WORKFLOW_DEFINITION_CONTRIBUTIONS
|
||||
)
|
||||
capability = registry.capability(CAPABILITY_WORKFLOW_DEFINITION_CONTRIBUTIONS)
|
||||
return (
|
||||
capability
|
||||
if isinstance(capability, WorkflowDefinitionContributionProvider)
|
||||
@@ -225,15 +251,18 @@ __all__ = [
|
||||
"CAPABILITY_WORKFLOW_DEFINITION_CONTRIBUTIONS",
|
||||
"CAPABILITY_WORKFLOW_ORCHESTRATION",
|
||||
"CAPABILITY_WORKFLOW_RUNTIME_WORKER",
|
||||
"CAPABILITY_WORKFLOW_TRIGGER_DISPATCHER",
|
||||
"WorkflowCurrentStepResolution",
|
||||
"WorkflowDefinitionContribution",
|
||||
"WorkflowDefinitionContributionProvider",
|
||||
"WorkflowInstanceRef",
|
||||
"WorkflowOrchestrationProvider",
|
||||
"WorkflowRuntimeWorker",
|
||||
"WorkflowTriggerDispatcher",
|
||||
"WorkflowStandardStartRequest",
|
||||
"workflow_definition_contribution_hash",
|
||||
"workflow_definition_contribution_provider",
|
||||
"workflow_orchestration_provider",
|
||||
"workflow_runtime_worker",
|
||||
"workflow_trigger_dispatcher",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import UTC, datetime
|
||||
import unittest
|
||||
|
||||
from govoplan_core.core.encryption import (
|
||||
CAPABILITY_ENCRYPTION_KEY_MATERIAL_PROVIDER_PREFIX,
|
||||
ContentProtectionEnvelope,
|
||||
KeyMaterialDescriptor,
|
||||
KeyVaultCreateRequest,
|
||||
key_material_provider,
|
||||
)
|
||||
|
||||
|
||||
NOW = datetime(2026, 8, 1, 12, 0, tzinfo=UTC)
|
||||
|
||||
|
||||
class Provider:
|
||||
def provision_key(self, _session, _principal, *, request):
|
||||
return KeyMaterialDescriptor(
|
||||
provider_id="test",
|
||||
provider_key_ref=f"test:{request.vault_id}:{request.key_version}",
|
||||
algorithm_suite=request.algorithm_suite,
|
||||
state="active",
|
||||
created_at=NOW,
|
||||
)
|
||||
|
||||
def revoke_key(self, _session, _principal, **_kwargs):
|
||||
raise NotImplementedError
|
||||
|
||||
def schedule_key_destruction(self, _session, _principal, **_kwargs):
|
||||
raise NotImplementedError
|
||||
|
||||
def key_status(self, _session, **_kwargs):
|
||||
return None
|
||||
|
||||
|
||||
class Registry:
|
||||
def __init__(self, values):
|
||||
self.values = values
|
||||
|
||||
def has_capability(self, name: str) -> bool:
|
||||
return name in self.values
|
||||
|
||||
def capability(self, name: str):
|
||||
return self.values[name]
|
||||
|
||||
|
||||
class EncryptionContractTests(unittest.TestCase):
|
||||
def test_provider_resolution_is_optional_and_structural(self) -> None:
|
||||
name = f"{CAPABILITY_ENCRYPTION_KEY_MATERIAL_PROVIDER_PREFIX}test"
|
||||
provider = Provider()
|
||||
self.assertIs(
|
||||
provider, key_material_provider(Registry({name: provider}), "test")
|
||||
)
|
||||
self.assertIsNone(key_material_provider(Registry({}), "test"))
|
||||
|
||||
def test_secret_bearing_policy_and_envelope_metadata_are_rejected(self) -> None:
|
||||
with self.assertRaisesRegex(ValueError, "secret material"):
|
||||
KeyVaultCreateRequest(
|
||||
tenant_id="tenant-1",
|
||||
vault_id="vault-1",
|
||||
name="Vault",
|
||||
provider_id="test",
|
||||
purpose="content",
|
||||
algorithm_suite="AES-256-GCM",
|
||||
scope_type="tenant",
|
||||
scope_id="tenant-1",
|
||||
policy_ref="policy:1",
|
||||
idempotency_key="create-1",
|
||||
provider_policy={"password": "forbidden"},
|
||||
)
|
||||
with self.assertRaisesRegex(ValueError, "secret material"):
|
||||
ContentProtectionEnvelope(
|
||||
envelope_id="envelope-1",
|
||||
tenant_id="tenant-1",
|
||||
owner_module="files",
|
||||
resource_type="file",
|
||||
resource_id="file-1",
|
||||
profile_kind="server_envelope",
|
||||
profile_id="profile-1",
|
||||
provider_id="test",
|
||||
vault_id="vault-1",
|
||||
key_version=1,
|
||||
algorithm_suite="AES-256-GCM",
|
||||
ciphertext_ref="object://ciphertext/1",
|
||||
ciphertext_digest="sha256:ciphertext",
|
||||
authenticated_context_digest="sha256:context",
|
||||
state="active",
|
||||
created_at=NOW,
|
||||
metadata={"private_key": "forbidden"},
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -232,7 +232,7 @@ class ModuleSystemTests(unittest.TestCase):
|
||||
self.assertTrue(manifests["campaigns"].required_capabilities)
|
||||
self.assertEqual(
|
||||
manifests["campaigns"].optional_dependencies,
|
||||
("files", "mail", "notifications", "addresses", "postbox"),
|
||||
("files", "mail", "notifications", "addresses", "postbox", "approvals"),
|
||||
)
|
||||
self.assertEqual(manifests["dashboard"].dependencies, ())
|
||||
self.assertTrue(manifests["dashboard"].required_capabilities)
|
||||
@@ -3433,7 +3433,7 @@ finally:
|
||||
"version_max_exclusive": "0.3.0",
|
||||
}, modules["campaigns"]["requires_interfaces"])
|
||||
self.assertEqual(
|
||||
["files", "mail", "notifications", "addresses", "postbox"],
|
||||
["files", "mail", "notifications", "addresses", "postbox", "approvals"],
|
||||
modules["campaigns"]["optional_dependencies"],
|
||||
)
|
||||
self.assertEqual("requires_review", modules["files"]["migration_safety"])
|
||||
|
||||
@@ -42,6 +42,10 @@ class PlatformEventWorkerTests(unittest.TestCase):
|
||||
"govoplan_core.celery_app._dataflow_trigger_dispatcher",
|
||||
return_value=dataflow,
|
||||
),
|
||||
patch(
|
||||
"govoplan_core.celery_app._workflow_trigger_dispatcher",
|
||||
return_value=None,
|
||||
),
|
||||
patch(
|
||||
"govoplan_core.db.session.get_database",
|
||||
return_value=database,
|
||||
@@ -109,21 +113,15 @@ class PlatformEventWorkerTests(unittest.TestCase):
|
||||
def test_worker_routes_and_periodic_tasks_are_registered(self) -> None:
|
||||
self.assertEqual(
|
||||
{"queue": "events"},
|
||||
celery.conf.task_routes[
|
||||
"govoplan.events.dispatch_outbox"
|
||||
],
|
||||
celery.conf.task_routes["govoplan.events.dispatch_outbox"],
|
||||
)
|
||||
self.assertEqual(
|
||||
{"queue": "events"},
|
||||
celery.conf.task_routes[
|
||||
"govoplan.events.purge_outbox"
|
||||
],
|
||||
celery.conf.task_routes["govoplan.events.purge_outbox"],
|
||||
)
|
||||
self.assertEqual(
|
||||
"govoplan.events.purge_outbox",
|
||||
celery.conf.beat_schedule[
|
||||
"platform-event-retention-daily"
|
||||
]["task"],
|
||||
celery.conf.beat_schedule["platform-event-retention-daily"]["task"],
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -12,12 +12,15 @@ from govoplan_core.core.registry import PlatformRegistry
|
||||
from govoplan_core.core.workflows import (
|
||||
CAPABILITY_WORKFLOW_DEFINITION_CONTRIBUTIONS,
|
||||
CAPABILITY_WORKFLOW_RUNTIME_WORKER,
|
||||
CAPABILITY_WORKFLOW_TRIGGER_DISPATCHER,
|
||||
WorkflowDefinitionContribution,
|
||||
WorkflowDefinitionContributionProvider,
|
||||
WorkflowRuntimeWorker,
|
||||
WorkflowTriggerDispatcher,
|
||||
workflow_definition_contribution_hash,
|
||||
workflow_definition_contribution_provider,
|
||||
workflow_runtime_worker,
|
||||
workflow_trigger_dispatcher,
|
||||
)
|
||||
|
||||
|
||||
@@ -31,6 +34,14 @@ class _ContributionProvider:
|
||||
return {"created": 0, "tenant_ids": tuple(tenant_ids)}
|
||||
|
||||
|
||||
class _TriggerDispatcher:
|
||||
def dispatch_due(self, session, *, now=None, limit=50):
|
||||
return {"selected": 0}
|
||||
|
||||
def ingest_event(self, session, *, event):
|
||||
return {"queued": 0}
|
||||
|
||||
|
||||
class WorkflowRuntimeWorkerTests(unittest.TestCase):
|
||||
def test_contribution_contract_hash_and_provider_are_stable(self) -> None:
|
||||
contribution = WorkflowDefinitionContribution(
|
||||
@@ -77,9 +88,7 @@ class WorkflowRuntimeWorkerTests(unittest.TestCase):
|
||||
name="Workflow runtime test",
|
||||
version="test",
|
||||
capability_factories={
|
||||
CAPABILITY_WORKFLOW_RUNTIME_WORKER: (
|
||||
lambda _context: worker
|
||||
),
|
||||
CAPABILITY_WORKFLOW_RUNTIME_WORKER: (lambda _context: worker),
|
||||
},
|
||||
)
|
||||
)
|
||||
@@ -90,6 +99,29 @@ class WorkflowRuntimeWorkerTests(unittest.TestCase):
|
||||
self.assertIs(worker, workflow_runtime_worker(registry))
|
||||
self.assertIsNone(workflow_runtime_worker(PlatformRegistry()))
|
||||
|
||||
def test_trigger_dispatch_contract_is_runtime_checkable_and_resolved(self) -> None:
|
||||
dispatcher = _TriggerDispatcher()
|
||||
self.assertIsInstance(dispatcher, WorkflowTriggerDispatcher)
|
||||
registry = PlatformRegistry()
|
||||
registry.register(
|
||||
ModuleManifest(
|
||||
id="workflow_trigger_test",
|
||||
name="Workflow trigger test",
|
||||
version="test",
|
||||
capability_factories={
|
||||
CAPABILITY_WORKFLOW_TRIGGER_DISPATCHER: (
|
||||
lambda _context: dispatcher
|
||||
),
|
||||
},
|
||||
)
|
||||
)
|
||||
registry.configure_capability_context(
|
||||
ModuleContext(registry=registry, settings=object())
|
||||
)
|
||||
|
||||
self.assertIs(dispatcher, workflow_trigger_dispatcher(registry))
|
||||
self.assertIsNone(workflow_trigger_dispatcher(PlatformRegistry()))
|
||||
|
||||
def test_celery_task_commits_reconciliation(self) -> None:
|
||||
session = MagicMock()
|
||||
database = MagicMock()
|
||||
@@ -121,9 +153,7 @@ class WorkflowRuntimeWorkerTests(unittest.TestCase):
|
||||
{"queue": "workflow"},
|
||||
celery.conf.task_routes["govoplan.workflow.reconcile"],
|
||||
)
|
||||
schedule = celery.conf.beat_schedule[
|
||||
"workflow-reconcile-every-five-seconds"
|
||||
]
|
||||
schedule = celery.conf.beat_schedule["workflow-reconcile-every-five-seconds"]
|
||||
self.assertEqual("govoplan.workflow.reconcile", schedule["task"])
|
||||
self.assertEqual(5.0, schedule["schedule"])
|
||||
|
||||
|
||||
Generated
+40
@@ -11,6 +11,7 @@
|
||||
"@govoplan/access-webui": "file:../../govoplan-access/webui",
|
||||
"@govoplan/addresses-webui": "file:../../govoplan-addresses/webui",
|
||||
"@govoplan/admin-webui": "file:../../govoplan-admin/webui",
|
||||
"@govoplan/approvals-webui": "file:../../govoplan-approvals/webui",
|
||||
"@govoplan/audit-webui": "file:../../govoplan-audit/webui",
|
||||
"@govoplan/calendar-webui": "file:../../govoplan-calendar/webui",
|
||||
"@govoplan/campaign-webui": "file:../../govoplan-campaign/webui",
|
||||
@@ -39,6 +40,7 @@
|
||||
"@govoplan/search-webui": "file:../../govoplan-search/webui",
|
||||
"@govoplan/tenancy-webui": "file:../../govoplan-tenancy/webui",
|
||||
"@govoplan/views-webui": "file:../../govoplan-views/webui",
|
||||
"@govoplan/voting-webui": "file:../../govoplan-voting/webui",
|
||||
"@govoplan/workflow-webui": "file:../../govoplan-workflow/webui",
|
||||
"@tiptap/core": "^3.29.2",
|
||||
"@tiptap/extension-image": "^3.29.2",
|
||||
@@ -120,6 +122,21 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"../../govoplan-approvals/webui": {
|
||||
"name": "@govoplan/approvals-webui",
|
||||
"version": "0.1.14",
|
||||
"peerDependencies": {
|
||||
"@govoplan/core-webui": "^0.1.14",
|
||||
"lucide-react": "^1.23.0",
|
||||
"react": ">=19.2.7 <20",
|
||||
"react-dom": ">=19.2.7 <20"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@govoplan/core-webui": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"../../govoplan-audit/webui": {
|
||||
"name": "@govoplan/audit-webui",
|
||||
"version": "0.1.8",
|
||||
@@ -603,6 +620,21 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"../../govoplan-voting/webui": {
|
||||
"name": "@govoplan/voting-webui",
|
||||
"version": "0.1.14",
|
||||
"peerDependencies": {
|
||||
"@govoplan/core-webui": "^0.1.14",
|
||||
"lucide-react": "^1.23.0",
|
||||
"react": ">=19.2.7 <20",
|
||||
"react-dom": ">=19.2.7 <20"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@govoplan/core-webui": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"../../govoplan-workflow/webui": {
|
||||
"name": "@govoplan/workflow-webui",
|
||||
"version": "0.1.14",
|
||||
@@ -1385,6 +1417,10 @@
|
||||
"resolved": "../../govoplan-admin/webui",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@govoplan/approvals-webui": {
|
||||
"resolved": "../../govoplan-approvals/webui",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@govoplan/audit-webui": {
|
||||
"resolved": "../../govoplan-audit/webui",
|
||||
"link": true
|
||||
@@ -1497,6 +1533,10 @@
|
||||
"resolved": "../../govoplan-views/webui",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@govoplan/voting-webui": {
|
||||
"resolved": "../../govoplan-voting/webui",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@govoplan/workflow-webui": {
|
||||
"resolved": "../../govoplan-workflow/webui",
|
||||
"link": true
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@govoplan/access-webui": "file:../../govoplan-access/webui",
|
||||
"@govoplan/approvals-webui": "file:../../govoplan-approvals/webui",
|
||||
"@govoplan/addresses-webui": "file:../../govoplan-addresses/webui",
|
||||
"@govoplan/admin-webui": "file:../../govoplan-admin/webui",
|
||||
"@govoplan/audit-webui": "file:../../govoplan-audit/webui",
|
||||
@@ -79,6 +80,7 @@
|
||||
"@govoplan/search-webui": "file:../../govoplan-search/webui",
|
||||
"@govoplan/tenancy-webui": "file:../../govoplan-tenancy/webui",
|
||||
"@govoplan/views-webui": "file:../../govoplan-views/webui",
|
||||
"@govoplan/voting-webui": "file:../../govoplan-voting/webui",
|
||||
"@govoplan/workflow-webui": "file:../../govoplan-workflow/webui",
|
||||
"@tiptap/core": "^3.29.2",
|
||||
"@tiptap/extension-image": "^3.29.2",
|
||||
|
||||
@@ -3,6 +3,7 @@ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
|
||||
const packageByModule = {
|
||||
access: "@govoplan/access-webui",
|
||||
approvals: "@govoplan/approvals-webui",
|
||||
admin: "@govoplan/admin-webui",
|
||||
addresses: "@govoplan/addresses-webui",
|
||||
audit: "@govoplan/audit-webui",
|
||||
@@ -33,6 +34,7 @@ const packageByModule = {
|
||||
search: "@govoplan/search-webui",
|
||||
tenancy: "@govoplan/tenancy-webui",
|
||||
views: "@govoplan/views-webui",
|
||||
voting: "@govoplan/voting-webui",
|
||||
workflow: "@govoplan/workflow-webui"
|
||||
};
|
||||
|
||||
@@ -78,7 +80,9 @@ const cases = [
|
||||
{ name: "search-only", modules: ["search"] },
|
||||
{ name: "risk-compliance-only", modules: ["risk_compliance"] },
|
||||
{ name: "docs-and-ops", modules: ["access", "docs", "ops"] },
|
||||
{ name: "full-product", modules: ["access", "tenancy", "admin", "addresses", "policy", "audit", "dashboard", "datasources", "dataflow", "dist_lists", "workflow", "views", "organizations", "idm", "cases", "committee", "campaigns", "files", "forms", "forms_runtime", "mail", "notifications", "docs", "ops", "calendar", "scheduling", "portal", "postbox", "projects", "reporting", "risk_compliance", "search"] }
|
||||
{ name: "approvals-only", modules: ["access", "approvals"] },
|
||||
{ name: "voting-only", modules: ["access", "voting"] },
|
||||
{ name: "full-product", modules: ["access", "tenancy", "admin", "addresses", "approvals", "policy", "audit", "dashboard", "datasources", "dataflow", "dist_lists", "workflow", "views", "organizations", "idm", "cases", "committee", "campaigns", "files", "forms", "forms_runtime", "mail", "notifications", "docs", "ops", "calendar", "scheduling", "portal", "postbox", "projects", "reporting", "risk_compliance", "search", "voting"] }
|
||||
];
|
||||
|
||||
const npmExec = process.env.npm_execpath;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Activity, Bell, BookUser, BriefcaseBusiness, Building2, CalendarClock, CalendarDays, ClipboardPenLine, DatabaseZap, Folder, FolderKanban, Form, Gavel, Inbox, Landmark, LayoutDashboard, LayoutTemplate, ListTree, Mail, Mails, RadioTower, Shield, ShieldCheck, Users, Waypoints, Workflow as WorkflowIcon, type LucideIcon } from "lucide-react";
|
||||
import { Activity, Bell, BookUser, BriefcaseBusiness, Building2, CalendarClock, CalendarDays, ClipboardPenLine, DatabaseZap, Folder, FolderKanban, Form, Gavel, Inbox, Landmark, LayoutDashboard, LayoutTemplate, ListChecks, ListTree, Mail, Mails, RadioTower, Shield, ShieldCheck, Users, Vote, Waypoints, Workflow as WorkflowIcon, type LucideIcon } from "lucide-react";
|
||||
import installedWebModuleLoaderSource from "virtual:govoplan-installed-modules";
|
||||
import type { AuthInfo, DashboardWidgetContribution, DashboardWidgetsUiCapability, EffectiveViewProjection, PlatformModuleInfo, PlatformNavItem, PlatformPublicModuleInfo, PlatformViewSurface, PlatformWebModule } from "../types";
|
||||
import {
|
||||
@@ -75,6 +75,7 @@ const iconByName: Record<string, LucideIcon> = {
|
||||
landmark: Landmark,
|
||||
"layout-template": LayoutTemplate,
|
||||
"list-tree": ListTree,
|
||||
"list-checks": ListChecks,
|
||||
mail: Mail,
|
||||
notifications: Bell,
|
||||
organizations: Building2,
|
||||
@@ -84,6 +85,7 @@ const iconByName: Record<string, LucideIcon> = {
|
||||
"shield-check": ShieldCheck,
|
||||
templates: LayoutTemplate,
|
||||
users: Users,
|
||||
vote: Vote,
|
||||
waypoints: Waypoints,
|
||||
workflow: WorkflowIcon
|
||||
};
|
||||
|
||||
@@ -14,6 +14,7 @@ const resolvedInstalledModuleVirtualPrefix = `\0${installedModuleVirtualPrefix}`
|
||||
|
||||
const defaultWebModulePackages = [
|
||||
"@govoplan/access-webui",
|
||||
"@govoplan/approvals-webui",
|
||||
"@govoplan/admin-webui",
|
||||
"@govoplan/addresses-webui",
|
||||
"@govoplan/audit-webui",
|
||||
@@ -43,6 +44,7 @@ const defaultWebModulePackages = [
|
||||
"@govoplan/search-webui",
|
||||
"@govoplan/tenancy-webui",
|
||||
"@govoplan/views-webui",
|
||||
"@govoplan/voting-webui",
|
||||
"@govoplan/workflow-webui"
|
||||
];
|
||||
|
||||
@@ -224,6 +226,7 @@ export default defineConfig({
|
||||
allow: [
|
||||
fileURLToPath(new URL('.', import.meta.url)),
|
||||
fileURLToPath(new URL('../../govoplan-access/webui', import.meta.url)),
|
||||
fileURLToPath(new URL('../../govoplan-approvals/webui', import.meta.url)),
|
||||
fileURLToPath(new URL('../../govoplan-admin/webui', import.meta.url)),
|
||||
fileURLToPath(new URL('../../govoplan-addresses/webui', import.meta.url)),
|
||||
fileURLToPath(new URL('../../govoplan-audit/webui', import.meta.url)),
|
||||
@@ -250,6 +253,7 @@ export default defineConfig({
|
||||
fileURLToPath(new URL('../../govoplan-search/webui', import.meta.url)),
|
||||
fileURLToPath(new URL('../../govoplan-tenancy/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-workflow/webui', import.meta.url))
|
||||
]
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user