Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ccef162f6 | ||
|
|
48dac139a5 | ||
|
|
a090e5af20 | ||
|
|
0c1358b862 | ||
|
|
a9035c4c3b | ||
|
|
137c7c005f | ||
|
|
8eeea968f2 | ||
|
|
0ca6568005 |
@@ -18,6 +18,7 @@ operator, and roadmap pages.
|
||||
| External references and integration maturity | `EXTERNAL_REFERENCES_AND_INTEGRATION_MATURITY.md` | Stable external identity and cumulative connector maturity; configured source authority is defined by the meta target architecture. |
|
||||
| Institutional context and governed references | `INSTITUTIONAL_CONTEXT_CONTRACT.md` | Shared temporal, actor/representation, institution, mandate, service, party, decision, evidence, legal-basis, information-governance, presentation, and geo DTO/provider contracts. |
|
||||
| Provider-neutral record filing | `RECORDS_FILING_CONTRACT.md` | Exact source-revision identity, current source authorization, idempotent filing, capability discovery, and ownership boundary. |
|
||||
| Ticket routing and Case escalation | `TICKET_INTEGRATION_CONTRACTS.md` | Optional fail-open routing, replay-safe Case handoff, authorization, evidence, and ownership boundaries. |
|
||||
| Temporal data read context | `TEMPORAL_DATA_CONTEXT.md` | Valid-time and recorded-time titlebar selection, HTTP/cache contract, security boundary, and module-adoption rule. |
|
||||
| Cross-module information governance adoption | `INFORMATION_GOVERNANCE_ADOPTION.md` | Manifest evidence and enforcement rules for temporal browsing, purpose-aware access, retention, and institutional context. |
|
||||
| Data-subject access and erasure requests | `DATA_SUBJECT_REQUESTS.md` | Provider-owned search and mutation, explicit coverage, governed export, retained evidence, permissions, and idempotent execution. |
|
||||
|
||||
@@ -14,6 +14,7 @@ consistent while each module still owns its domain rules.
|
||||
| Governance defaults | `govoplan-admin` plus `govoplan-access` materializer | admin settings, governance template routes, access materialization capability | System governance can block tenant-local groups, roles, and API keys. |
|
||||
| Delegation and ownership policy | access/campaign/mail/files modules | capability checks and owner-scoped APIs | Source provenance should use this contract when policies become externally explainable. |
|
||||
| Definition governance | `govoplan-policy` | capability `policy.definitionGovernance` | Resolves view, edit, run/start, reuse, derive, and automate for system, tenant, group, and user Dataflow/Workflow definitions. |
|
||||
| Function assignment governance | `govoplan-policy` | capability `policy.functionAssignmentGovernance` | Returns current review steps, delegation depth/validity ceilings, and explicit timed-escalation targets consumed by IDM. |
|
||||
|
||||
## Policy Decision
|
||||
|
||||
@@ -126,6 +127,22 @@ When the capability is absent, modules must not silently emulate cross-scope
|
||||
inheritance. Their conservative fallback is limited to local tenant
|
||||
definitions and disables reuse, derivation, and automation.
|
||||
|
||||
## Function Assignment Delegation And Escalation
|
||||
|
||||
`FunctionAssignmentGovernanceDecision` is the versioned cross-module contract
|
||||
for request/grant review. In addition to the required holder, authority, and
|
||||
recipient steps, it returns `delegation_allowed`,
|
||||
`maximum_delegation_depth`, `maximum_delegated_validity_days`, and typed
|
||||
`FunctionAssignmentEscalationRule` entries. Each escalation entry binds one
|
||||
review step to an exact target function and timeout.
|
||||
|
||||
The decision is a current ceiling, not durable authorization. IDM must recheck
|
||||
the complete assignment-source chain and all recorded decisions before final
|
||||
application. An elapsed timeout creates explicit state and evidence; it must
|
||||
never be interpreted as approval or as permission to silently substitute an
|
||||
approver. Missing providers, malformed rules, invalid chains, or tightened
|
||||
limits fail closed with an explainable reason.
|
||||
|
||||
## Bounded Impact-Subject Providers
|
||||
|
||||
Policy impact previews discover optional subject providers through capability
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
# Ticket Integration Capability Contracts
|
||||
|
||||
Core owns two narrow, optional contracts that let the Tickets module compose
|
||||
with policy and formal-procedure modules without importing either one. Tickets
|
||||
remains the authority for operational ticket identity, lifecycle, assignment,
|
||||
comments, links, and immutable history.
|
||||
|
||||
## Capability Names
|
||||
|
||||
- `tickets.routing` optionally supplies a `TicketRoutingProvider`.
|
||||
- `tickets.case_escalation` optionally supplies a
|
||||
`TicketCaseEscalationProvider`.
|
||||
|
||||
Both contracts are version 1 and are defined in
|
||||
`govoplan_core.core.tickets`. Registry helpers return `None` when a capability
|
||||
is absent or has the wrong shape, so optional-module absence is normal runtime
|
||||
state rather than a startup failure.
|
||||
|
||||
## Routing
|
||||
|
||||
Tickets sends a bounded, tenant-scoped `TicketRoutingRequest` containing the
|
||||
ticket reference, type, priority, title, receive time, optional queue hint, and
|
||||
non-secret attributes. The provider returns its identity and may return a queue
|
||||
reference, timezone-aware service target, human-readable explanation, and
|
||||
bounded metadata.
|
||||
|
||||
The provider is advisory. Tickets snapshots any returned queue and target into
|
||||
its own record and history. An absent provider, a no-match plan, or an absent
|
||||
queue must not prevent ticket intake; authorized staff can route manually.
|
||||
Providers must not persist a second ticket lifecycle.
|
||||
|
||||
## Case Escalation
|
||||
|
||||
Tickets sends a `TicketCaseEscalationCommand` with stable tenant, ticket, and
|
||||
display references, the requested Case type, actor-visible handoff note,
|
||||
timezone-aware occurrence time, and an idempotency key. The provider returns a
|
||||
stable Case identifier, number, bounded application-relative URL, replay flag,
|
||||
and bounded metadata.
|
||||
|
||||
Providers must:
|
||||
|
||||
- recheck tenant and Case-creation authorization;
|
||||
- reject an absent or inactive requested Case type;
|
||||
- make identical retries resolve the same Case;
|
||||
- preserve the Ticket reference in governed Case context; and
|
||||
- return only an application-relative path, never an untrusted external URL.
|
||||
|
||||
Tickets records the result and its own escalation evidence. Cases remains the
|
||||
authority for the formal procedure; Tickets remains the authority for the
|
||||
operational request. Creating a Case does not merge or silently close either
|
||||
lifecycle.
|
||||
|
||||
## Failure And Transaction Semantics
|
||||
|
||||
Capability calls receive the caller's active persistence session so a concrete
|
||||
provider can participate in the same unit of work. Authorization and validation
|
||||
errors fail the requested routing/escalation mutation explicitly. The caller
|
||||
must still apply its own permission checks, tenant boundary, replay protection,
|
||||
and immutable evidence rules.
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "govoplan-core"
|
||||
version = "0.1.24"
|
||||
version = "0.1.32"
|
||||
description = "Reusable GovOPlaN platform core, access, tenancy, and RBAC components."
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
from collections.abc import Callable, Iterable, Mapping
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from typing import Protocol, runtime_checkable
|
||||
from typing import Literal, Protocol, runtime_checkable
|
||||
|
||||
|
||||
CAPABILITY_CAMPAIGNS_MAIL_POLICY_CONTEXT = "campaigns.mailPolicyContext"
|
||||
@@ -12,6 +12,20 @@ CAPABILITY_CAMPAIGNS_POLICY_CONTEXT = "campaigns.policyContext"
|
||||
CAPABILITY_CAMPAIGNS_DELIVERY_TASKS = "campaigns.deliveryTasks"
|
||||
CAPABILITY_CAMPAIGNS_SCHEDULES = "campaigns.schedules"
|
||||
CAPABILITY_CAMPAIGNS_RETENTION = "campaigns.retention"
|
||||
CAPABILITY_CAMPAIGNS_WORK_ORCHESTRATION = "campaigns.workOrchestration"
|
||||
|
||||
CampaignWorkAssigneeKind = Literal[
|
||||
"account",
|
||||
"group",
|
||||
"organization_function",
|
||||
]
|
||||
CampaignWorkHandoffStatus = Literal[
|
||||
"open",
|
||||
"in_progress",
|
||||
"completed",
|
||||
"rejected",
|
||||
"cancelled",
|
||||
]
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
@@ -32,6 +46,88 @@ class CampaignPolicyContext:
|
||||
settings: Mapping[str, object] = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class CampaignWorkHandoffRequest:
|
||||
"""Typed request used by Workflow to open accountable Campaign work."""
|
||||
|
||||
tenant_id: str
|
||||
idempotency_key: str
|
||||
purpose: str
|
||||
assignee_kind: CampaignWorkAssigneeKind
|
||||
assignee_id: str
|
||||
campaign_id: str | None = None
|
||||
create_external_id: str | None = None
|
||||
create_name: str | None = None
|
||||
create_description: str | None = None
|
||||
expected_campaign_revision: int | None = None
|
||||
due_at: datetime | None = None
|
||||
mirror_to_tasks: bool = True
|
||||
correlation_id: str | None = None
|
||||
workflow_instance_id: str | None = None
|
||||
workflow_step_id: str | None = None
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
for value, label in (
|
||||
(self.tenant_id, "Campaign hand-off tenant"),
|
||||
(self.idempotency_key, "Campaign hand-off idempotency key"),
|
||||
(self.purpose, "Campaign hand-off purpose"),
|
||||
(self.assignee_id, "Campaign hand-off assignee"),
|
||||
):
|
||||
if not value.strip():
|
||||
raise ValueError(f"{label} is required")
|
||||
references_existing = bool(self.campaign_id and self.campaign_id.strip())
|
||||
creates_new = bool(
|
||||
self.create_external_id
|
||||
and self.create_external_id.strip()
|
||||
and self.create_name
|
||||
and self.create_name.strip()
|
||||
)
|
||||
if references_existing == creates_new:
|
||||
raise ValueError(
|
||||
"Campaign hand-offs must either reference one campaign or "
|
||||
"declare one new campaign."
|
||||
)
|
||||
if self.expected_campaign_revision is not None and (
|
||||
self.expected_campaign_revision < 1
|
||||
):
|
||||
raise ValueError("Expected Campaign revisions start at one")
|
||||
if self.due_at is not None and self.due_at.tzinfo is None:
|
||||
raise ValueError("Campaign hand-off due dates require a timezone")
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class CampaignWorkHandoffRef:
|
||||
"""Stable, revision-bearing reference returned to the Workflow instance."""
|
||||
|
||||
tenant_id: str
|
||||
campaign_id: str
|
||||
campaign_version_id: str
|
||||
campaign_revision: int
|
||||
assignment_id: str
|
||||
assignment_revision: int
|
||||
status: CampaignWorkHandoffStatus
|
||||
action_url: str
|
||||
campaign_ref: str
|
||||
assignment_ref: str
|
||||
event_type: str = "campaign.work.changed"
|
||||
replayed: bool = False
|
||||
optional_capabilities: Mapping[str, bool] = field(default_factory=dict)
|
||||
provenance: Mapping[str, object] = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class CampaignWorkHandoffInspection:
|
||||
"""Current authorization and revision check before Workflow continuation."""
|
||||
|
||||
allowed: bool
|
||||
status: CampaignWorkHandoffStatus | None = None
|
||||
assignment_revision: int | None = None
|
||||
action_url: str | None = None
|
||||
assignment_ref: str | None = None
|
||||
reason: str | None = None
|
||||
provenance: Mapping[str, object] = field(default_factory=dict)
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class CampaignMailPolicyContextProvider(Protocol):
|
||||
def get_campaign_mail_policy_context(
|
||||
@@ -132,3 +228,45 @@ class CampaignRetentionProvider(Protocol):
|
||||
policy_for_campaign_id: Callable[[str | None], object],
|
||||
) -> Mapping[str, Mapping[str, int]]:
|
||||
...
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class CampaignWorkOrchestrationProvider(Protocol):
|
||||
"""Optional Campaign boundary for durable Workflow-owned hand-offs."""
|
||||
|
||||
def prepare_handoff(
|
||||
self,
|
||||
session: object,
|
||||
principal: object,
|
||||
*,
|
||||
request: CampaignWorkHandoffRequest,
|
||||
) -> CampaignWorkHandoffRef:
|
||||
...
|
||||
|
||||
def inspect_handoff(
|
||||
self,
|
||||
session: object,
|
||||
principal: object,
|
||||
*,
|
||||
tenant_id: str,
|
||||
assignment_id: str,
|
||||
expected_revision: int | None = None,
|
||||
) -> CampaignWorkHandoffInspection:
|
||||
...
|
||||
|
||||
|
||||
def campaign_work_orchestration_provider(
|
||||
registry: object | None,
|
||||
) -> CampaignWorkOrchestrationProvider | None:
|
||||
if (
|
||||
registry is None
|
||||
or not hasattr(registry, "has_capability")
|
||||
or not registry.has_capability(CAPABILITY_CAMPAIGNS_WORK_ORCHESTRATION)
|
||||
):
|
||||
return None
|
||||
capability = registry.capability(CAPABILITY_CAMPAIGNS_WORK_ORCHESTRATION)
|
||||
return (
|
||||
capability
|
||||
if isinstance(capability, CampaignWorkOrchestrationProvider)
|
||||
else None
|
||||
)
|
||||
|
||||
@@ -41,10 +41,12 @@ ViewGovernanceAction = Literal[
|
||||
"workflow_activate",
|
||||
]
|
||||
FunctionAssignmentChangeKind = Literal["request", "grant"]
|
||||
FunctionAssignmentReviewStep = Literal["holder", "authority", "recipient"]
|
||||
FunctionAssignmentGovernanceAction = Literal[
|
||||
"submit",
|
||||
"approve_holder",
|
||||
"approve_authority",
|
||||
"approve_escalation",
|
||||
"accept_recipient",
|
||||
"request_changes",
|
||||
"respond",
|
||||
@@ -419,6 +421,20 @@ class FunctionAssignmentGovernanceRequest:
|
||||
context: Mapping[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class FunctionAssignmentEscalationRule:
|
||||
step: FunctionAssignmentReviewStep
|
||||
target_function_id: str
|
||||
timeout_hours: int
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"step": self.step,
|
||||
"target_function_id": self.target_function_id,
|
||||
"timeout_hours": self.timeout_hours,
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class FunctionAssignmentGovernanceDecision:
|
||||
allowed: bool
|
||||
@@ -431,6 +447,10 @@ class FunctionAssignmentGovernanceDecision:
|
||||
separation_of_duties: bool = True
|
||||
quorum: int = 1
|
||||
maximum_validity_days: int | None = None
|
||||
delegation_allowed: bool = False
|
||||
maximum_delegation_depth: int = 0
|
||||
maximum_delegated_validity_days: int | None = None
|
||||
escalation_rules: tuple[FunctionAssignmentEscalationRule, ...] = ()
|
||||
request_expiry_hours: int = 336
|
||||
source_path: tuple[PolicySourceStep, ...] = ()
|
||||
requirements: tuple[str, ...] = ()
|
||||
@@ -448,12 +468,24 @@ class FunctionAssignmentGovernanceDecision:
|
||||
"separation_of_duties": self.separation_of_duties,
|
||||
"quorum": self.quorum,
|
||||
"maximum_validity_days": self.maximum_validity_days,
|
||||
"delegation_allowed": self.delegation_allowed,
|
||||
"maximum_delegation_depth": self.maximum_delegation_depth,
|
||||
"maximum_delegated_validity_days": (
|
||||
self.maximum_delegated_validity_days
|
||||
),
|
||||
"escalation_rules": [rule.to_dict() for rule in self.escalation_rules],
|
||||
"request_expiry_hours": self.request_expiry_hours,
|
||||
"source_path": [step.to_dict() for step in self.source_path],
|
||||
"requirements": list(self.requirements),
|
||||
"details": dict(self.details),
|
||||
}
|
||||
|
||||
def escalation_rule(
|
||||
self,
|
||||
step: FunctionAssignmentReviewStep,
|
||||
) -> FunctionAssignmentEscalationRule | None:
|
||||
return next((rule for rule in self.escalation_rules if rule.step == step), None)
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class FunctionAssignmentGovernancePolicy(Protocol):
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from typing import Mapping, Protocol, runtime_checkable
|
||||
|
||||
|
||||
TICKET_INTEGRATION_CONTRACT_VERSION = "1"
|
||||
CAPABILITY_TICKET_ROUTING = "tickets.routing"
|
||||
CAPABILITY_TICKET_CASE_ESCALATION = "tickets.case_escalation"
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class TicketRoutingRequest:
|
||||
tenant_id: str
|
||||
ticket_id: str
|
||||
ticket_type: str
|
||||
priority: str
|
||||
title: str
|
||||
received_at: datetime
|
||||
queue_hint: str | None = None
|
||||
attributes: Mapping[str, object] = field(default_factory=dict)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
_required(self.tenant_id, "Ticket routing tenant", 255)
|
||||
_required(self.ticket_id, "Ticket routing ticket", 255)
|
||||
_required(self.ticket_type, "Ticket routing type", 80)
|
||||
_required(self.priority, "Ticket routing priority", 40)
|
||||
_required(self.title, "Ticket routing title", 500)
|
||||
_aware(self.received_at, "Ticket routing received_at")
|
||||
_optional(self.queue_hint, "Ticket routing queue hint", 255)
|
||||
if len(self.attributes) > 100:
|
||||
raise ValueError("Ticket routing attributes are limited to 100 entries.")
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class TicketRoutingPlan:
|
||||
provider_id: str
|
||||
queue_ref: str | None = None
|
||||
service_target_at: datetime | None = None
|
||||
explanation: str | None = None
|
||||
metadata: Mapping[str, object] = field(default_factory=dict)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
_required(self.provider_id, "Ticket routing provider", 200)
|
||||
_optional(self.queue_ref, "Ticket routing queue reference", 255)
|
||||
_optional(self.explanation, "Ticket routing explanation", 4_000)
|
||||
_aware(self.service_target_at, "Ticket routing service_target_at")
|
||||
if len(self.metadata) > 100:
|
||||
raise ValueError("Ticket routing metadata is limited to 100 entries.")
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class TicketRoutingProvider(Protocol):
|
||||
def route_ticket(
|
||||
self,
|
||||
session: object,
|
||||
principal: object,
|
||||
*,
|
||||
request: TicketRoutingRequest,
|
||||
) -> TicketRoutingPlan: ...
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class TicketCaseEscalationCommand:
|
||||
tenant_id: str
|
||||
ticket_id: str
|
||||
ticket_number: str
|
||||
title: str
|
||||
case_type_key: str
|
||||
occurred_at: datetime
|
||||
idempotency_key: str
|
||||
handoff_note: str | None = None
|
||||
metadata: Mapping[str, object] = field(default_factory=dict)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
_required(self.tenant_id, "Ticket escalation tenant", 255)
|
||||
_required(self.ticket_id, "Ticket escalation ticket", 255)
|
||||
_required(self.ticket_number, "Ticket escalation number", 255)
|
||||
_required(self.title, "Ticket escalation title", 500)
|
||||
_required(self.case_type_key, "Ticket escalation case type", 120)
|
||||
_required(self.idempotency_key, "Ticket escalation idempotency key", 255)
|
||||
_optional(self.handoff_note, "Ticket escalation handoff note", 10_000)
|
||||
_aware(self.occurred_at, "Ticket escalation occurred_at")
|
||||
if len(self.metadata) > 100:
|
||||
raise ValueError("Ticket escalation metadata is limited to 100 entries.")
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class TicketCaseEscalationResult:
|
||||
provider_id: str
|
||||
case_id: str
|
||||
case_number: str
|
||||
case_url: str
|
||||
replayed: bool = False
|
||||
metadata: Mapping[str, object] = field(default_factory=dict)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
_required(self.provider_id, "Ticket escalation provider", 200)
|
||||
_required(self.case_id, "Ticket escalation case", 255)
|
||||
_required(self.case_number, "Ticket escalation case number", 255)
|
||||
_relative_url(self.case_url)
|
||||
if len(self.metadata) > 100:
|
||||
raise ValueError("Ticket escalation metadata is limited to 100 entries.")
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class TicketCaseEscalationProvider(Protocol):
|
||||
def escalate_ticket(
|
||||
self,
|
||||
session: object,
|
||||
principal: object,
|
||||
*,
|
||||
command: TicketCaseEscalationCommand,
|
||||
) -> TicketCaseEscalationResult: ...
|
||||
|
||||
|
||||
def ticket_routing_provider(registry: object | None) -> TicketRoutingProvider | None:
|
||||
provider = _capability(registry, CAPABILITY_TICKET_ROUTING)
|
||||
return provider if isinstance(provider, TicketRoutingProvider) else None
|
||||
|
||||
|
||||
def ticket_case_escalation_provider(
|
||||
registry: object | None,
|
||||
) -> TicketCaseEscalationProvider | None:
|
||||
provider = _capability(registry, CAPABILITY_TICKET_CASE_ESCALATION)
|
||||
return provider if isinstance(provider, TicketCaseEscalationProvider) 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 _required(value: str, label: str, maximum: int) -> None:
|
||||
if not value.strip() or len(value) > maximum:
|
||||
raise ValueError(f"{label} must contain 1 to {maximum} characters.")
|
||||
|
||||
|
||||
def _optional(value: str | None, label: str, maximum: int) -> None:
|
||||
if value is not None and (not value.strip() or len(value) > maximum):
|
||||
raise ValueError(f"{label} must contain 1 to {maximum} characters when set.")
|
||||
|
||||
|
||||
def _aware(value: datetime | None, label: str) -> None:
|
||||
if value is not None and (value.tzinfo is None or value.utcoffset() is None):
|
||||
raise ValueError(f"{label} must include a timezone.")
|
||||
|
||||
|
||||
def _relative_url(value: str) -> None:
|
||||
if (
|
||||
not value.startswith("/")
|
||||
or value.startswith("//")
|
||||
or "\\" in value
|
||||
or len(value) > 1_500
|
||||
or any(ord(character) < 32 or ord(character) == 127 for character in value)
|
||||
):
|
||||
raise ValueError("Ticket escalation URLs must be bounded application-relative paths.")
|
||||
|
||||
|
||||
__all__ = [
|
||||
"CAPABILITY_TICKET_CASE_ESCALATION",
|
||||
"CAPABILITY_TICKET_ROUTING",
|
||||
"TICKET_INTEGRATION_CONTRACT_VERSION",
|
||||
"TicketCaseEscalationCommand",
|
||||
"TicketCaseEscalationProvider",
|
||||
"TicketCaseEscalationResult",
|
||||
"TicketRoutingPlan",
|
||||
"TicketRoutingProvider",
|
||||
"TicketRoutingRequest",
|
||||
"ticket_case_escalation_provider",
|
||||
"ticket_routing_provider",
|
||||
]
|
||||
@@ -12,6 +12,9 @@ from govoplan_core.security.outbound_http import (
|
||||
)
|
||||
|
||||
|
||||
MAX_OUTBOUND_HTTP_REQUEST_BODY_BYTES = 1_000_000
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class HttpFetchResponse:
|
||||
status: int
|
||||
@@ -46,11 +49,17 @@ def fetch_http(
|
||||
label: str = "URL",
|
||||
method: str = "GET",
|
||||
headers: Mapping[str, str] | None = None,
|
||||
body: bytes | None = None,
|
||||
max_bytes: int | None = None,
|
||||
) -> HttpFetchResponse:
|
||||
if body is not None and len(body) > MAX_OUTBOUND_HTTP_REQUEST_BODY_BYTES:
|
||||
raise ValueError(
|
||||
"Outbound HTTP request body exceeds the 1000000-byte safety limit."
|
||||
)
|
||||
validated_url = validate_outbound_http_url(url, label=label)
|
||||
request = urllib.request.Request( # noqa: S310 - URL is restricted to validated HTTP(S).
|
||||
validated_url,
|
||||
data=body,
|
||||
headers=dict(headers or {}),
|
||||
method=method,
|
||||
)
|
||||
@@ -76,10 +85,19 @@ def fetch_http_text(
|
||||
label: str = "URL",
|
||||
method: str = "GET",
|
||||
headers: Mapping[str, str] | None = None,
|
||||
body: bytes | None = None,
|
||||
encoding: str = "utf-8",
|
||||
max_bytes: int | None = None,
|
||||
) -> str:
|
||||
return fetch_http(url, timeout=timeout, label=label, method=method, headers=headers, max_bytes=max_bytes).text(encoding)
|
||||
return fetch_http(
|
||||
url,
|
||||
timeout=timeout,
|
||||
label=label,
|
||||
method=method,
|
||||
headers=headers,
|
||||
body=body,
|
||||
max_bytes=max_bytes,
|
||||
).text(encoding)
|
||||
|
||||
|
||||
class _PolicyRedirectHandler(urllib.request.HTTPRedirectHandler):
|
||||
|
||||
@@ -71,6 +71,7 @@ from govoplan_core.core.campaigns import (
|
||||
CAPABILITY_CAMPAIGNS_MAIL_POLICY_CONTEXT,
|
||||
CAPABILITY_CAMPAIGNS_POLICY_CONTEXT,
|
||||
CAPABILITY_CAMPAIGNS_RETENTION,
|
||||
CAPABILITY_CAMPAIGNS_WORK_ORCHESTRATION,
|
||||
CampaignAccessProvider,
|
||||
CampaignDeliveryTaskProvider,
|
||||
CampaignMailPolicyContext,
|
||||
@@ -78,6 +79,10 @@ from govoplan_core.core.campaigns import (
|
||||
CampaignPolicyContext,
|
||||
CampaignPolicyContextProvider,
|
||||
CampaignRetentionProvider,
|
||||
CampaignWorkHandoffInspection,
|
||||
CampaignWorkHandoffRef,
|
||||
CampaignWorkHandoffRequest,
|
||||
CampaignWorkOrchestrationProvider,
|
||||
)
|
||||
from govoplan_core.core.files import CAPABILITY_FILES_ACCESS, FileAccessProvider
|
||||
from govoplan_core.core.modules import ModuleContext, ModuleManifest
|
||||
@@ -464,6 +469,40 @@ class _FakeCampaignRetentionProvider:
|
||||
return {"raw_campaign_json": {"eligible": int(dry_run)}}
|
||||
|
||||
|
||||
class _FakeCampaignWorkOrchestrationProvider:
|
||||
def prepare_handoff(self, session: object, principal: object, *, request):
|
||||
del session, principal
|
||||
return CampaignWorkHandoffRef(
|
||||
tenant_id=request.tenant_id,
|
||||
campaign_id=request.campaign_id or "campaign-created",
|
||||
campaign_version_id="campaign-version-1",
|
||||
campaign_revision=1,
|
||||
assignment_id="assignment-1",
|
||||
assignment_revision=1,
|
||||
status="open",
|
||||
action_url="/campaigns/campaign-1/work?assignment=assignment-1",
|
||||
campaign_ref="campaign:campaign-1:version:campaign-version-1:r1",
|
||||
assignment_ref="campaign-work-assignment:assignment-1:r1",
|
||||
)
|
||||
|
||||
def inspect_handoff(
|
||||
self,
|
||||
session: object,
|
||||
principal: object,
|
||||
*,
|
||||
tenant_id: str,
|
||||
assignment_id: str,
|
||||
expected_revision: int | None = None,
|
||||
):
|
||||
del session, principal, tenant_id, assignment_id
|
||||
return CampaignWorkHandoffInspection(
|
||||
allowed=expected_revision in {None, 1},
|
||||
status="open",
|
||||
assignment_revision=1,
|
||||
assignment_ref="campaign-work-assignment:assignment-1:r1",
|
||||
)
|
||||
|
||||
|
||||
class _FakeSecretProvider:
|
||||
def __init__(self) -> None:
|
||||
self._values: dict[str, str] = {}
|
||||
@@ -528,6 +567,10 @@ class AccessContractTests(unittest.TestCase):
|
||||
self.assertEqual("campaigns.mailPolicyContext", CAPABILITY_CAMPAIGNS_MAIL_POLICY_CONTEXT)
|
||||
self.assertEqual("campaigns.policyContext", CAPABILITY_CAMPAIGNS_POLICY_CONTEXT)
|
||||
self.assertEqual("campaigns.retention", CAPABILITY_CAMPAIGNS_RETENTION)
|
||||
self.assertEqual(
|
||||
"campaigns.workOrchestration",
|
||||
CAPABILITY_CAMPAIGNS_WORK_ORCHESTRATION,
|
||||
)
|
||||
self.assertEqual("tenancy.tenantResolver", CAPABILITY_TENANCY_TENANT_RESOLVER)
|
||||
self.assertEqual("security.secretProvider", CAPABILITY_SECURITY_SECRET_PROVIDER)
|
||||
self.assertEqual("audit.sink", CAPABILITY_AUDIT_SINK)
|
||||
@@ -642,6 +685,10 @@ class AccessContractTests(unittest.TestCase):
|
||||
self.assertIsInstance(_FakeCampaignMailPolicyContextProvider(), CampaignMailPolicyContextProvider)
|
||||
self.assertIsInstance(_FakeCampaignPolicyContextProvider(), CampaignPolicyContextProvider)
|
||||
self.assertIsInstance(_FakeCampaignRetentionProvider(), CampaignRetentionProvider)
|
||||
self.assertIsInstance(
|
||||
_FakeCampaignWorkOrchestrationProvider(),
|
||||
CampaignWorkOrchestrationProvider,
|
||||
)
|
||||
self.assertIsInstance(_FakeSecretProvider(), SecretProvider)
|
||||
self.assertIsInstance(_FakeAuditSink(), AuditSink)
|
||||
self.assertIsInstance(_FakeAuditRecorder(), AuditRecorder)
|
||||
@@ -676,6 +723,37 @@ class AccessContractTests(unittest.TestCase):
|
||||
self.assertEqual({"job_id": "job-1", "status": "appended"}, delivery_provider.append_sent_for_job(object(), job_id="job-1"))
|
||||
self.assertEqual({"raw_campaign_json": {"eligible": 1}}, retention_provider.apply_retention(object(), dry_run=True, now=object(), policy_for_campaign_id=lambda campaign_id: object()))
|
||||
|
||||
def test_campaign_work_handoff_contract_requires_one_campaign_source(self) -> None:
|
||||
request = CampaignWorkHandoffRequest(
|
||||
tenant_id="tenant-1",
|
||||
campaign_id="campaign-1",
|
||||
idempotency_key="workflow-step-1",
|
||||
purpose="Review the campaign",
|
||||
assignee_kind="account",
|
||||
assignee_id="account-1",
|
||||
)
|
||||
provider = _FakeCampaignWorkOrchestrationProvider()
|
||||
|
||||
handoff = provider.prepare_handoff(object(), object(), request=request)
|
||||
inspection = provider.inspect_handoff(
|
||||
object(),
|
||||
object(),
|
||||
tenant_id="tenant-1",
|
||||
assignment_id=handoff.assignment_id,
|
||||
expected_revision=handoff.assignment_revision,
|
||||
)
|
||||
|
||||
self.assertEqual("campaign-1", handoff.campaign_id)
|
||||
self.assertTrue(inspection.allowed)
|
||||
with self.assertRaisesRegex(ValueError, "either reference one campaign"):
|
||||
CampaignWorkHandoffRequest(
|
||||
tenant_id="tenant-1",
|
||||
idempotency_key="workflow-step-2",
|
||||
purpose="Review",
|
||||
assignee_kind="account",
|
||||
assignee_id="account-1",
|
||||
)
|
||||
|
||||
def test_access_capabilities_register_and_resolve_through_platform_registry(self) -> None:
|
||||
directory = _FakeAccessDirectory()
|
||||
semantic_directory = _FakeAccessSemanticDirectory()
|
||||
|
||||
@@ -2,9 +2,14 @@ from __future__ import annotations
|
||||
|
||||
import io
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from govoplan_core.security.http_fetch import _PolicyRedirectHandler, is_http_url, validate_http_url
|
||||
from govoplan_core.security.http_fetch import (
|
||||
_PolicyRedirectHandler,
|
||||
fetch_http,
|
||||
is_http_url,
|
||||
validate_http_url,
|
||||
)
|
||||
from govoplan_core.security.outbound_http import (
|
||||
DEFAULT_FILE_TRANSFER_BYTES,
|
||||
DEFAULT_STRUCTURED_RESPONSE_BYTES,
|
||||
@@ -21,6 +26,51 @@ from govoplan_core.security.outbound_http import (
|
||||
|
||||
|
||||
class HttpFetchTests(unittest.TestCase):
|
||||
def test_fetch_http_forwards_a_bounded_request_body(self) -> None:
|
||||
class Response(io.BytesIO):
|
||||
status = 200
|
||||
headers = {"Content-Type": "application/json"}
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *_args):
|
||||
return False
|
||||
|
||||
opener = Mock()
|
||||
opener.open.return_value = Response(b"{}")
|
||||
with patch(
|
||||
"govoplan_core.security.http_fetch.validate_outbound_http_url",
|
||||
return_value="https://wiki.example.test/api.php",
|
||||
), patch(
|
||||
"govoplan_core.security.http_fetch.build_outbound_http_opener",
|
||||
return_value=opener,
|
||||
):
|
||||
response = fetch_http(
|
||||
"https://wiki.example.test/api.php",
|
||||
method="POST",
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
||||
body=b"action=edit",
|
||||
max_bytes=1024,
|
||||
)
|
||||
|
||||
request = opener.open.call_args.args[0]
|
||||
self.assertEqual("POST", request.get_method())
|
||||
self.assertEqual(b"action=edit", request.data)
|
||||
self.assertEqual(b"{}", response.body)
|
||||
|
||||
def test_fetch_http_rejects_an_oversized_request_body_before_transport(self) -> None:
|
||||
with patch(
|
||||
"govoplan_core.security.http_fetch.validate_outbound_http_url"
|
||||
) as validate:
|
||||
with self.assertRaisesRegex(ValueError, "request body exceeds"):
|
||||
fetch_http(
|
||||
"https://wiki.example.test/api.php",
|
||||
method="POST",
|
||||
body=b"x" * 1_000_001,
|
||||
)
|
||||
validate.assert_not_called()
|
||||
|
||||
def test_validate_http_url_accepts_absolute_http_urls_without_credentials(self) -> None:
|
||||
self.assertEqual("https://example.test/catalog.json", validate_http_url("https://example.test/catalog.json"))
|
||||
self.assertTrue(is_http_url("http://example.test/catalog.json"))
|
||||
|
||||
@@ -296,6 +296,9 @@ class ModuleSystemTests(unittest.TestCase):
|
||||
"approvals",
|
||||
"reporting",
|
||||
"search",
|
||||
"organizations",
|
||||
"idm",
|
||||
"tasks",
|
||||
),
|
||||
)
|
||||
self.assertEqual(manifests["dashboard"].dependencies, ())
|
||||
|
||||
@@ -10,6 +10,8 @@ from govoplan_core.core.configuration_safety import (
|
||||
ui_managed_configuration_fields_requiring_approval,
|
||||
)
|
||||
from govoplan_core.core.policy import (
|
||||
FunctionAssignmentEscalationRule,
|
||||
FunctionAssignmentGovernanceDecision,
|
||||
PolicyDecision,
|
||||
PolicySourceStep,
|
||||
parse_policy_source_path,
|
||||
@@ -19,6 +21,34 @@ from govoplan_core.core.policy import (
|
||||
|
||||
|
||||
class PolicyContractTests(unittest.TestCase):
|
||||
def test_function_assignment_policy_serializes_delegation_and_escalation(self) -> None:
|
||||
decision = FunctionAssignmentGovernanceDecision(
|
||||
allowed=True,
|
||||
delegation_allowed=True,
|
||||
maximum_delegation_depth=2,
|
||||
maximum_delegated_validity_days=30,
|
||||
escalation_rules=(
|
||||
FunctionAssignmentEscalationRule(
|
||||
step="authority",
|
||||
target_function_id="function-escalation",
|
||||
timeout_hours=48,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
payload = decision.to_dict()
|
||||
|
||||
self.assertEqual(2, payload["maximum_delegation_depth"])
|
||||
self.assertEqual(30, payload["maximum_delegated_validity_days"])
|
||||
self.assertEqual(
|
||||
"function-escalation",
|
||||
payload["escalation_rules"][0]["target_function_id"],
|
||||
)
|
||||
self.assertEqual(
|
||||
"function-escalation",
|
||||
decision.escalation_rule("authority").target_function_id,
|
||||
)
|
||||
|
||||
def test_policy_source_paths_are_stable_and_round_trip(self) -> None:
|
||||
self.assertEqual(policy_source_path("system"), "system")
|
||||
self.assertEqual(policy_source_path("tenant", "tenant-1"), "tenant:tenant-1")
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import UTC, datetime
|
||||
import unittest
|
||||
|
||||
from govoplan_core.core.tickets import (
|
||||
CAPABILITY_TICKET_CASE_ESCALATION,
|
||||
CAPABILITY_TICKET_ROUTING,
|
||||
TicketCaseEscalationCommand,
|
||||
TicketCaseEscalationResult,
|
||||
TicketRoutingPlan,
|
||||
TicketRoutingRequest,
|
||||
ticket_case_escalation_provider,
|
||||
ticket_routing_provider,
|
||||
)
|
||||
|
||||
|
||||
class _Provider:
|
||||
def route_ticket(self, session, principal, *, request):
|
||||
del session, principal, request
|
||||
return TicketRoutingPlan(provider_id="helpdesk", queue_ref="citizen-service")
|
||||
|
||||
def escalate_ticket(self, session, principal, *, command):
|
||||
del session, principal, command
|
||||
return TicketCaseEscalationResult(
|
||||
provider_id="cases",
|
||||
case_id="case-1",
|
||||
case_number="CASE-1",
|
||||
case_url="/cases/case-1",
|
||||
)
|
||||
|
||||
|
||||
class _Registry:
|
||||
def __init__(self, capabilities):
|
||||
self.capabilities = capabilities
|
||||
|
||||
def has_capability(self, name):
|
||||
return name in self.capabilities
|
||||
|
||||
def capability(self, name):
|
||||
return self.capabilities[name]
|
||||
|
||||
|
||||
class TicketContractTests(unittest.TestCase):
|
||||
def test_optional_providers_fail_open_when_absent(self) -> None:
|
||||
registry = _Registry({})
|
||||
self.assertIsNone(ticket_routing_provider(registry))
|
||||
self.assertIsNone(ticket_case_escalation_provider(registry))
|
||||
|
||||
def test_optional_providers_resolve_structurally(self) -> None:
|
||||
provider = _Provider()
|
||||
registry = _Registry(
|
||||
{
|
||||
CAPABILITY_TICKET_ROUTING: provider,
|
||||
CAPABILITY_TICKET_CASE_ESCALATION: provider,
|
||||
}
|
||||
)
|
||||
self.assertIs(provider, ticket_routing_provider(registry))
|
||||
self.assertIs(provider, ticket_case_escalation_provider(registry))
|
||||
|
||||
def test_commands_validate_tenant_time_and_relative_case_link(self) -> None:
|
||||
instant = datetime(2026, 8, 22, 9, 0, tzinfo=UTC)
|
||||
request = TicketRoutingRequest(
|
||||
tenant_id="tenant-1",
|
||||
ticket_id="ticket-1",
|
||||
ticket_type="request",
|
||||
priority="normal",
|
||||
title="Broken streetlight",
|
||||
received_at=instant,
|
||||
)
|
||||
self.assertEqual("ticket-1", request.ticket_id)
|
||||
|
||||
command = TicketCaseEscalationCommand(
|
||||
tenant_id="tenant-1",
|
||||
ticket_id="ticket-1",
|
||||
ticket_number="TKT-1",
|
||||
title="Broken streetlight",
|
||||
case_type_key="service-request",
|
||||
occurred_at=instant,
|
||||
idempotency_key="escalation-1",
|
||||
)
|
||||
self.assertEqual("service-request", command.case_type_key)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
TicketCaseEscalationResult(
|
||||
provider_id="cases",
|
||||
case_id="case-1",
|
||||
case_number="CASE-1",
|
||||
case_url="https://other.example/cases/1",
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Generated
+86
-22
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@govoplan/core-webui",
|
||||
"version": "0.1.24",
|
||||
"version": "0.1.32",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@govoplan/core-webui",
|
||||
"version": "0.1.24",
|
||||
"version": "0.1.32",
|
||||
"dependencies": {
|
||||
"@govoplan/access-webui": "file:../../govoplan-access/webui",
|
||||
"@govoplan/addresses-webui": "file:../../govoplan-addresses/webui",
|
||||
@@ -27,6 +27,7 @@
|
||||
"@govoplan/files-webui": "file:../../govoplan-files/webui",
|
||||
"@govoplan/forms-runtime-webui": "file:../../govoplan-forms-runtime/webui",
|
||||
"@govoplan/forms-webui": "file:../../govoplan-forms/webui",
|
||||
"@govoplan/helpdesk-webui": "file:../../govoplan-helpdesk/webui",
|
||||
"@govoplan/identity-trust-webui": "file:../../govoplan-identity-trust/webui",
|
||||
"@govoplan/identity-webui": "file:../../govoplan-identity/webui",
|
||||
"@govoplan/idm-webui": "file:../../govoplan-idm/webui",
|
||||
@@ -48,8 +49,10 @@
|
||||
"@govoplan/tasks-webui": "file:../../govoplan-tasks/webui",
|
||||
"@govoplan/templates-webui": "file:../../govoplan-templates/webui",
|
||||
"@govoplan/tenancy-webui": "file:../../govoplan-tenancy/webui",
|
||||
"@govoplan/tickets-webui": "file:../../govoplan-tickets/webui",
|
||||
"@govoplan/views-webui": "file:../../govoplan-views/webui",
|
||||
"@govoplan/voting-webui": "file:../../govoplan-voting/webui",
|
||||
"@govoplan/wiki-webui": "file:../../govoplan-wiki/webui",
|
||||
"@govoplan/workflow-webui": "file:../../govoplan-workflow/webui",
|
||||
"@tiptap/core": "^3.29.2",
|
||||
"@tiptap/extension-image": "^3.29.2",
|
||||
@@ -151,7 +154,7 @@
|
||||
},
|
||||
"../../govoplan-audit/webui": {
|
||||
"name": "@govoplan/audit-webui",
|
||||
"version": "0.1.18",
|
||||
"version": "0.1.19",
|
||||
"peerDependencies": {
|
||||
"@govoplan/core-webui": "^0.1.18",
|
||||
"lucide-react": "^1.23.0",
|
||||
@@ -186,7 +189,7 @@
|
||||
},
|
||||
"../../govoplan-campaign/webui": {
|
||||
"name": "@govoplan/campaign-webui",
|
||||
"version": "0.1.20",
|
||||
"version": "0.1.24",
|
||||
"dependencies": {
|
||||
"read-excel-file": "9.2.0"
|
||||
},
|
||||
@@ -208,9 +211,9 @@
|
||||
},
|
||||
"../../govoplan-cases/webui": {
|
||||
"name": "@govoplan/cases-webui",
|
||||
"version": "0.1.18",
|
||||
"version": "0.1.20",
|
||||
"peerDependencies": {
|
||||
"@govoplan/core-webui": "^0.1.18",
|
||||
"@govoplan/core-webui": "^0.1.30",
|
||||
"lucide-react": "^1.23.0",
|
||||
"react": ">=19.2.7 <20",
|
||||
"react-dom": ">=19.2.7 <20",
|
||||
@@ -240,7 +243,7 @@
|
||||
},
|
||||
"../../govoplan-connectors/webui": {
|
||||
"name": "@govoplan/connectors-webui",
|
||||
"version": "0.1.18",
|
||||
"version": "0.1.21",
|
||||
"peerDependencies": {
|
||||
"@govoplan/core-webui": "^0.1.18",
|
||||
"react": ">=19.2.7 <20",
|
||||
@@ -270,7 +273,7 @@
|
||||
},
|
||||
"../../govoplan-dataflow/webui": {
|
||||
"name": "@govoplan/dataflow-webui",
|
||||
"version": "0.1.18",
|
||||
"version": "0.1.20",
|
||||
"peerDependencies": {
|
||||
"@govoplan/core-webui": "^0.1.18",
|
||||
"@xyflow/react": "^12.11.2",
|
||||
@@ -322,7 +325,7 @@
|
||||
},
|
||||
"../../govoplan-docs/webui": {
|
||||
"name": "@govoplan/docs-webui",
|
||||
"version": "0.1.18",
|
||||
"version": "0.1.20",
|
||||
"peerDependencies": {
|
||||
"@govoplan/core-webui": "^0.1.18",
|
||||
"@vitejs/plugin-react": "^5.2.0",
|
||||
@@ -391,12 +394,29 @@
|
||||
},
|
||||
"../../govoplan-forms/webui": {
|
||||
"name": "@govoplan/forms-webui",
|
||||
"version": "0.1.18",
|
||||
"version": "0.1.19",
|
||||
"peerDependencies": {
|
||||
"@govoplan/core-webui": "^0.1.18",
|
||||
"lucide-react": "^1.23.0",
|
||||
"react": ">=19.2.7 <20",
|
||||
"react-dom": ">=19.2.7 <20"
|
||||
"react-dom": ">=19.2.7 <20",
|
||||
"react-router": ">=8.3.0 <9"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@govoplan/core-webui": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"../../govoplan-helpdesk/webui": {
|
||||
"name": "@govoplan/helpdesk-webui",
|
||||
"version": "0.1.20",
|
||||
"peerDependencies": {
|
||||
"@govoplan/core-webui": "^0.1.30",
|
||||
"lucide-react": "^1.23.0",
|
||||
"react": ">=19.2.7 <20",
|
||||
"react-dom": ">=19.2.7 <20",
|
||||
"react-router": ">=8.3.0 <9"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@govoplan/core-webui": {
|
||||
@@ -436,7 +456,7 @@
|
||||
},
|
||||
"../../govoplan-idm/webui": {
|
||||
"name": "@govoplan/idm-webui",
|
||||
"version": "0.1.19",
|
||||
"version": "0.1.20",
|
||||
"peerDependencies": {
|
||||
"@govoplan/core-webui": "^0.1.18",
|
||||
"@vitejs/plugin-react": "^5.2.0",
|
||||
@@ -455,7 +475,7 @@
|
||||
},
|
||||
"../../govoplan-mail/webui": {
|
||||
"name": "@govoplan/mail-webui",
|
||||
"version": "0.1.18",
|
||||
"version": "0.1.21",
|
||||
"devDependencies": {
|
||||
"typescript": "^5.7.2"
|
||||
},
|
||||
@@ -493,7 +513,7 @@
|
||||
},
|
||||
"../../govoplan-ops/webui": {
|
||||
"name": "@govoplan/ops-webui",
|
||||
"version": "0.1.18",
|
||||
"version": "0.1.19",
|
||||
"peerDependencies": {
|
||||
"@govoplan/core-webui": "^0.1.18",
|
||||
"@vitejs/plugin-react": "^5.2.0",
|
||||
@@ -547,7 +567,7 @@
|
||||
},
|
||||
"../../govoplan-policy/webui": {
|
||||
"name": "@govoplan/policy-webui",
|
||||
"version": "0.1.19",
|
||||
"version": "0.1.20",
|
||||
"peerDependencies": {
|
||||
"@govoplan/core-webui": "^0.1.18",
|
||||
"lucide-react": "^1.23.0",
|
||||
@@ -563,7 +583,7 @@
|
||||
},
|
||||
"../../govoplan-portal/webui": {
|
||||
"name": "@govoplan/portal-webui",
|
||||
"version": "0.1.18",
|
||||
"version": "0.1.19",
|
||||
"peerDependencies": {
|
||||
"@govoplan/core-webui": "^0.1.18",
|
||||
"lucide-react": "^1.23.0",
|
||||
@@ -579,7 +599,7 @@
|
||||
},
|
||||
"../../govoplan-postbox/webui": {
|
||||
"name": "@govoplan/postbox-webui",
|
||||
"version": "0.1.18",
|
||||
"version": "0.1.19",
|
||||
"peerDependencies": {
|
||||
"@govoplan/core-webui": "^0.1.18",
|
||||
"lucide-react": "^1.23.0",
|
||||
@@ -611,7 +631,7 @@
|
||||
},
|
||||
"../../govoplan-quick-access/webui": {
|
||||
"name": "@govoplan/quick-access-webui",
|
||||
"version": "0.1.18",
|
||||
"version": "0.1.19",
|
||||
"peerDependencies": {
|
||||
"@govoplan/core-webui": "^0.1.18",
|
||||
"lucide-react": "^1.23.0",
|
||||
@@ -675,7 +695,7 @@
|
||||
},
|
||||
"../../govoplan-scheduling/webui": {
|
||||
"name": "@govoplan/scheduling-webui",
|
||||
"version": "0.1.19",
|
||||
"version": "0.1.18",
|
||||
"peerDependencies": {
|
||||
"@govoplan/core-webui": "^0.1.18",
|
||||
"@vitejs/plugin-react": "^5.2.0",
|
||||
@@ -710,7 +730,7 @@
|
||||
},
|
||||
"../../govoplan-tasks/webui": {
|
||||
"name": "@govoplan/tasks-webui",
|
||||
"version": "0.1.19",
|
||||
"version": "0.1.20",
|
||||
"peerDependencies": {
|
||||
"@govoplan/core-webui": "^0.1.18",
|
||||
"lucide-react": "^1.23.0",
|
||||
@@ -756,9 +776,25 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"../../govoplan-tickets/webui": {
|
||||
"name": "@govoplan/tickets-webui",
|
||||
"version": "0.1.20",
|
||||
"peerDependencies": {
|
||||
"@govoplan/core-webui": "^0.1.30",
|
||||
"lucide-react": "^1.23.0",
|
||||
"react": ">=19.2.7 <20",
|
||||
"react-dom": ">=19.2.7 <20",
|
||||
"react-router": ">=8.3.0 <9"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@govoplan/core-webui": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"../../govoplan-views/webui": {
|
||||
"name": "@govoplan/views-webui",
|
||||
"version": "0.1.18",
|
||||
"version": "0.1.19",
|
||||
"peerDependencies": {
|
||||
"@govoplan/core-webui": "^0.1.18",
|
||||
"lucide-react": "^1.23.0",
|
||||
@@ -787,9 +823,25 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"../../govoplan-wiki/webui": {
|
||||
"name": "@govoplan/wiki-webui",
|
||||
"version": "0.1.20",
|
||||
"peerDependencies": {
|
||||
"@govoplan/core-webui": "^0.1.31",
|
||||
"lucide-react": "^1.23.0",
|
||||
"react": ">=19.2.7 <20",
|
||||
"react-dom": ">=19.2.7 <20",
|
||||
"react-router": ">=8.3.0 <9"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@govoplan/core-webui": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"../../govoplan-workflow/webui": {
|
||||
"name": "@govoplan/workflow-webui",
|
||||
"version": "0.1.18",
|
||||
"version": "0.1.21",
|
||||
"peerDependencies": {
|
||||
"@govoplan/core-webui": "^0.1.18",
|
||||
"@xyflow/react": "^12.11.2",
|
||||
@@ -1633,6 +1685,10 @@
|
||||
"resolved": "../../govoplan-forms/webui",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@govoplan/helpdesk-webui": {
|
||||
"resolved": "../../govoplan-helpdesk/webui",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@govoplan/identity-trust-webui": {
|
||||
"resolved": "../../govoplan-identity-trust/webui",
|
||||
"link": true
|
||||
@@ -1717,6 +1773,10 @@
|
||||
"resolved": "../../govoplan-tenancy/webui",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@govoplan/tickets-webui": {
|
||||
"resolved": "../../govoplan-tickets/webui",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@govoplan/views-webui": {
|
||||
"resolved": "../../govoplan-views/webui",
|
||||
"link": true
|
||||
@@ -1725,6 +1785,10 @@
|
||||
"resolved": "../../govoplan-voting/webui",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@govoplan/wiki-webui": {
|
||||
"resolved": "../../govoplan-wiki/webui",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@govoplan/workflow-webui": {
|
||||
"resolved": "../../govoplan-workflow/webui",
|
||||
"link": true
|
||||
|
||||
+472
-404
File diff suppressed because it is too large
Load Diff
+4
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@govoplan/core-webui",
|
||||
"version": "0.1.24",
|
||||
"version": "0.1.32",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
@@ -77,6 +77,7 @@
|
||||
"@govoplan/files-webui": "file:../../govoplan-files/webui",
|
||||
"@govoplan/forms-runtime-webui": "file:../../govoplan-forms-runtime/webui",
|
||||
"@govoplan/forms-webui": "file:../../govoplan-forms/webui",
|
||||
"@govoplan/helpdesk-webui": "file:../../govoplan-helpdesk/webui",
|
||||
"@govoplan/identity-trust-webui": "file:../../govoplan-identity-trust/webui",
|
||||
"@govoplan/identity-webui": "file:../../govoplan-identity/webui",
|
||||
"@govoplan/idm-webui": "file:../../govoplan-idm/webui",
|
||||
@@ -98,8 +99,10 @@
|
||||
"@govoplan/tasks-webui": "file:../../govoplan-tasks/webui",
|
||||
"@govoplan/templates-webui": "file:../../govoplan-templates/webui",
|
||||
"@govoplan/tenancy-webui": "file:../../govoplan-tenancy/webui",
|
||||
"@govoplan/tickets-webui": "file:../../govoplan-tickets/webui",
|
||||
"@govoplan/views-webui": "file:../../govoplan-views/webui",
|
||||
"@govoplan/voting-webui": "file:../../govoplan-voting/webui",
|
||||
"@govoplan/wiki-webui": "file:../../govoplan-wiki/webui",
|
||||
"@govoplan/workflow-webui": "file:../../govoplan-workflow/webui",
|
||||
"@tiptap/core": "^3.29.2",
|
||||
"@tiptap/extension-image": "^3.29.2",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@govoplan/core-webui",
|
||||
"version": "0.1.24",
|
||||
"version": "0.1.32",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
@@ -30,15 +30,19 @@
|
||||
"@govoplan/admin-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-admin.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/cases-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-cases.git#v0.1.20",
|
||||
"@govoplan/dashboard-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-dashboard.git#v0.1.18",
|
||||
"@govoplan/docs-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-docs.git#v0.1.18",
|
||||
"@govoplan/files-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-files.git#v0.1.20",
|
||||
"@govoplan/helpdesk-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-helpdesk.git#v0.1.20",
|
||||
"@govoplan/idm-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-idm.git#v0.1.19",
|
||||
"@govoplan/mail-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-mail.git#v0.1.18",
|
||||
"@govoplan/campaign-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-campaign.git#v0.1.20",
|
||||
"@govoplan/campaign-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-campaign.git#v0.1.22",
|
||||
"@govoplan/organizations-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-organizations.git#v0.1.18",
|
||||
"@govoplan/ops-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-ops.git#v0.1.18",
|
||||
"@govoplan/policy-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-policy.git#v0.1.18",
|
||||
"@govoplan/tickets-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-tickets.git#v0.1.20",
|
||||
"@govoplan/wiki-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-wiki.git#v0.1.20",
|
||||
"@tiptap/core": "^3.29.2",
|
||||
"@tiptap/extension-image": "^3.29.2",
|
||||
"@tiptap/pm": "^3.29.2",
|
||||
|
||||
@@ -22,6 +22,7 @@ const packageByModule = {
|
||||
files: "@govoplan/files-webui",
|
||||
forms: "@govoplan/forms-webui",
|
||||
forms_runtime: "@govoplan/forms-runtime-webui",
|
||||
helpdesk: "@govoplan/helpdesk-webui",
|
||||
idm: "@govoplan/idm-webui",
|
||||
identity: "@govoplan/identity-webui",
|
||||
mail: "@govoplan/mail-webui",
|
||||
@@ -42,8 +43,10 @@ const packageByModule = {
|
||||
tasks: "@govoplan/tasks-webui",
|
||||
tenancy: "@govoplan/tenancy-webui",
|
||||
templates: "@govoplan/templates-webui",
|
||||
tickets: "@govoplan/tickets-webui",
|
||||
views: "@govoplan/views-webui",
|
||||
voting: "@govoplan/voting-webui",
|
||||
wiki: "@govoplan/wiki-webui",
|
||||
workflow: "@govoplan/workflow-webui"
|
||||
};
|
||||
|
||||
@@ -75,6 +78,10 @@ const cases = [
|
||||
{ name: "files-only", modules: ["files"] },
|
||||
{ name: "forms-only", modules: ["forms"] },
|
||||
{ name: "forms-runtime", modules: ["forms", "forms_runtime"] },
|
||||
{ name: "tickets-only", modules: ["tickets"] },
|
||||
{ name: "tickets-with-helpdesk-and-cases", modules: ["tickets", "helpdesk", "cases"] },
|
||||
{ name: "wiki-only", modules: ["wiki"] },
|
||||
{ name: "wiki-with-files-search", modules: ["wiki", "files", "search"] },
|
||||
{ name: "mail-only", modules: ["mail"] },
|
||||
{ name: "notifications-only", modules: ["notifications"] },
|
||||
{ name: "organizations-only", modules: ["organizations"] },
|
||||
@@ -106,7 +113,7 @@ const cases = [
|
||||
{ name: "tasks-only", modules: ["access", "tasks"] },
|
||||
{ name: "tasks-with-contributors", modules: ["access", "approvals", "postbox", "workflow", "dashboard", "tasks"] },
|
||||
{ name: "voting-only", modules: ["access", "voting"] },
|
||||
{ name: "full-product", modules: ["access", "tenancy", "admin", "addresses", "approvals", "policy", "audit", "dashboard", "datasources", "dataflow", "dist_lists", "templates", "workflow", "views", "organizations", "idm", "identity", "identity_trust", "encryption", "cases", "committee", "connectors", "campaigns", "files", "forms", "forms_runtime", "mail", "notifications", "docs", "ops", "payments", "calendar", "scheduling", "portal", "postbox", "projects", "quick_access", "reporting", "records", "risk_compliance", "search", "tasks", "voting"] }
|
||||
{ name: "full-product", modules: ["access", "tenancy", "admin", "addresses", "approvals", "policy", "audit", "dashboard", "datasources", "dataflow", "dist_lists", "templates", "workflow", "views", "organizations", "idm", "identity", "identity_trust", "encryption", "cases", "committee", "connectors", "campaigns", "files", "forms", "forms_runtime", "helpdesk", "mail", "notifications", "docs", "ops", "payments", "calendar", "scheduling", "portal", "postbox", "projects", "quick_access", "reporting", "records", "risk_compliance", "search", "tasks", "tickets", "voting", "wiki"] }
|
||||
];
|
||||
|
||||
const npmExec = process.env.npm_execpath;
|
||||
|
||||
@@ -32,6 +32,7 @@ const defaultWebModulePackages = [
|
||||
"@govoplan/files-webui",
|
||||
"@govoplan/forms-webui",
|
||||
"@govoplan/forms-runtime-webui",
|
||||
"@govoplan/helpdesk-webui",
|
||||
"@govoplan/idm-webui",
|
||||
"@govoplan/identity-webui",
|
||||
"@govoplan/identity-trust-webui",
|
||||
@@ -52,8 +53,10 @@ const defaultWebModulePackages = [
|
||||
"@govoplan/search-webui",
|
||||
"@govoplan/tenancy-webui",
|
||||
"@govoplan/templates-webui",
|
||||
"@govoplan/tickets-webui",
|
||||
"@govoplan/views-webui",
|
||||
"@govoplan/voting-webui",
|
||||
"@govoplan/wiki-webui",
|
||||
"@govoplan/workflow-webui"
|
||||
];
|
||||
|
||||
@@ -266,6 +269,7 @@ export default defineConfig({
|
||||
fileURLToPath(new URL('../../govoplan-files/webui', import.meta.url)),
|
||||
fileURLToPath(new URL('../../govoplan-forms/webui', import.meta.url)),
|
||||
fileURLToPath(new URL('../../govoplan-forms-runtime/webui', import.meta.url)),
|
||||
fileURLToPath(new URL('../../govoplan-helpdesk/webui', import.meta.url)),
|
||||
fileURLToPath(new URL('../../govoplan-idm/webui', import.meta.url)),
|
||||
fileURLToPath(new URL('../../govoplan-identity/webui', import.meta.url)),
|
||||
fileURLToPath(new URL('../../govoplan-mail/webui', import.meta.url)),
|
||||
@@ -283,8 +287,10 @@ 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-templates/webui', import.meta.url)),
|
||||
fileURLToPath(new URL('../../govoplan-tickets/webui', import.meta.url)),
|
||||
fileURLToPath(new URL('../../govoplan-views/webui', import.meta.url)),
|
||||
fileURLToPath(new URL('../../govoplan-voting/webui', import.meta.url)),
|
||||
fileURLToPath(new URL('../../govoplan-wiki/webui', import.meta.url)),
|
||||
fileURLToPath(new URL('../../govoplan-workflow/webui', import.meta.url))
|
||||
]
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user