@fabricorg/platform 1.11.0 → 1.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,70 @@
1
1
  # @fabricorg/platform
2
2
 
3
+ ## 1.13.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 749c507: Add host-owned staged descendant invocations. Actions declare an allowlist and return typed child
8
+ requests; Platform Host validates and redacts them atomically, releases them only after parent
9
+ completion, and dispatches them through fresh entitlement, authorization, admission, runtime
10
+ evidence, policy, and HITL decisions.
11
+
12
+ PostgreSQL migration 11 adds the staged-invocation ledger and caller-unforgeable child-origin unique
13
+ index. Child identity is a Host-only parent-invocation/stage-key origin. Child creation is
14
+ atomically fenced on the stage's pending status, lease owner, lease token, and unexpired lease;
15
+ PostgreSQL locks that stage row `FOR SHARE` while the fenced insert is in flight.
16
+
17
+ Each attempt settles a stage through one sequence. It first looks the child up by origin: a child
18
+ created by an earlier attempt is handed to the dispatcher again with its stable workflow ID while it
19
+ remains pending, without pre-submission checks or repeated governance, even after a deploy changes
20
+ the child's schema. Otherwise Host submits the child. When that submission did not itself return
21
+ the child (a failed check, a refusal, an error, or a timeout), Host rotates the lease token through
22
+ the store operation `rotateStagedInvocationLease` before an authoritative lookup, so the attempt's
23
+ own timed-out submission can no longer create the child. A stage is rejected only when that lookup
24
+ finds no child, and a dead-lettered stage records the child ID whenever the attempt found or created
25
+ a child. A stage becomes dispatched only after confirmed handoff, when no dispatcher is configured,
26
+ or when the child has already left pending. A failed child lookup settles the stage for retry and
27
+ does not dead-letter it. Settlements are compare-and-set on the lease owner and current token and
28
+ keep a child ID recorded earlier. A store error on one stage is reported through the sweep's
29
+ `onError` option (the Platform worker passes its own) and does not stop the rest of the batch.
30
+ Handoff failures retry after a constant one-second delay by default; callers provide `retryDelayMs`
31
+ for backoff. Public entitlement and authorization refusals now expose `SubmissionRefusalError` with
32
+ stable `code: "ACTION_SUBMISSION_REFUSED"`. The child inherits the parent's actor type: a
33
+ natural-person parent therefore does not enter agent HITL, while an agent parent follows the normal
34
+ agent approval route.
35
+
36
+ ## 1.12.0
37
+
38
+ ### Minor Changes
39
+
40
+ - 71d306f: Add optional action-level finalization for atomic, immediate after_adapters actions, including non-idempotent actions. Export JSON finalization types, registration validation, a manifest protocol descriptor and defineFinalizingAction to guard handlers against older hosts.
41
+
42
+ Host requires atomic transactions, invocation locking and transaction-scoped lease fencing before application execution. On normal success the hook consumes stored redacted outputs and commits transaction-local domain writes, declared events, outbox and the merged result together. Hook or validation failure rolls back finalization and settles reconciliation_required when durable provider effects exist, or failed when there are none, with redacted error text. Lease loss escapes without a stale-worker verdict. Handler preparation writes remain separately committed.
43
+
44
+ If entry to or COMMIT of the final success transaction (after adapters) fails with an unknown outcome, Host raises RecoverableFinalizationError and records no verdict. An uncommitted invocation stays running for recovery; a committed completed invocation retains its stored verdict and result for re-execution.
45
+
46
+ Explicit undefined on the optional hook return fields events, resultPatch and eventSchemaVersion is treated as omitted. Payloads and result-patch values retain strict JSON validation and reject undefined.
47
+
48
+ Failed, ambiguous, degraded and accepted/handoff outcomes do not invoke the hook. Existing actions retain their lifecycle.
49
+
50
+ Before adapters, normalize hook parameters and public handler data through a JSON round-trip, stripping private handler carriers. Dates become ISO strings and undefined object properties drop; serialization failures such as cycles or BigInt fail before provider execution. Handler events are normalized separately. Hook event schema versions default to the action version when omitted.
51
+
52
+ Add optional Host eventContracts name validation for events returned by the finalize hook. It checks eventType and subjectType membership only; handler events, payload schemas and schema-version compatibility are outside this option's scope. Recovery refusal also preserves ambiguity evidence for in-flight adapter rows alongside succeeded effects.
53
+
54
+ - d5e7caa: Add optional adapter-step deadlineMs durations overriding the Host-wide duration,
55
+ including when the Host duration is unset or zero. Action and module registration, manifest export,
56
+ and manifest normalization reject step durations that are not positive finite integers.
57
+ Host validates resolved steps before handler or adapter execution. Invalid
58
+ durations fail a first execution without invoking adapters; retries with recorded
59
+ applied effects or unknown adapter outcomes require reconciliation instead.
60
+ Manifests preserve valid step durations.
61
+
62
+ Correct adapterDeadlineMs documentation: it is a duration starting just before
63
+ step retry execution, not an epoch timestamp. Host-wide behavior is unchanged.
64
+ Budgets span retries; expiry prevents a subsequent attempt and signals cooperative
65
+ cancellation of an in-flight call. An aborted call that throws retains the existing
66
+ ambiguous-outcome reconciliation semantics.
67
+
3
68
  ## 1.11.0
4
69
 
5
70
  ### Minor Changes
package/README.md CHANGED
@@ -88,6 +88,21 @@ const registry = createModuleRegistry([myModule]);
88
88
 
89
89
  The platform never auto-loads a module. The host app composes an instance-scoped registry and injects it into its hosts. Experience roles, slots, action intents, and display rendering live in `@fabricorg/experience-contracts`, carried through the namespaced `fabric.experience/v1` extension.
90
90
 
91
+ ## Staged descendant invocations
92
+
93
+ An action may declare `stagesInvocations` as the child action IDs it is allowed to stage. Its
94
+ handler returns requests through `ActionResult.stagedInvocations`; an `after_adapters` finalizer may
95
+ add requests through `ActionFinalizationResult.stagedInvocations`. Each request contains only a
96
+ stable key, an action ID, and parameters. Actor, tenant, space, correlation, causation,
97
+ authorization, provenance, and child identity remain Host-owned.
98
+
99
+ Module registration rejects unknown targets, self-targets, capture-authority targets, and targets
100
+ in another module unless that module is a declared dependency. A staged child is not delegated
101
+ under the parent's authorization decision: dispatch submits it through the current registered
102
+ action, entitlement, authorization, admission, runtime-evidence, and dispatcher pipeline. See the
103
+ [`@fabricorg/platform-host` staging contract](../platform-host/README.md#staged-descendant-invocations)
104
+ for transaction, recovery, release, and limit semantics.
105
+
91
106
  ## Exporting a module manifest
92
107
 
93
108
  Downstream tooling — semantics exporters, docs generators, tool generators — often needs a module's ontology as data rather than live code. `@fabricorg/platform/manifest` exports a stable, JSON-serializable, versioned description of any `FabricModule`:
@@ -101,7 +116,7 @@ const json = JSON.stringify(manifest, null, 2);
101
116
 
102
117
  `exportModuleManifest` is a pure function of the module object: it does not touch the registries and the module does not need to be registered first. Declaration order is preserved everywhere, so the same module always produces byte-identical JSON. Function fields (`handler`, policy `evaluate`, transition `guard`, dynamic `targetState`, adapter `getInput`) are never serialized — the manifest contains definitions only, never raw parameter values, secrets, or runtime data. Manifest v2 object registrations may include `PortableJsonSchema`, qualified relations, namespaced semantic annotations, and enforcement-owned invariants. Actions whose validators expose `toJSONSchema()` include `parameterSchema` and optional `resultSchema` definitions. `normalizeModuleManifest` and `normalizeModuleManifestBundle` let v2 readers accept v1 input without promising down-conversion.
103
118
 
104
- An entity schema does not validate arbitrary writes through `TDb`. Persisted-record validation requires an explicit Host/database adapter. When an action declares `resultSchema`, Platform Host validates the public result after the handler and before events or adapters; failure rolls back domain writes only when the Host store has a transaction provider that includes them.
119
+ An entity schema does not validate arbitrary writes through `TDb`. Persisted-record validation requires an explicit Host/database adapter. For actions without `finalize`, when an action declares `resultSchema`, Platform Host validates the public result after the handler and before events or adapters; failure rolls back domain writes only when the Host store has a transaction provider that includes them.
105
120
 
106
121
  An action may also declare `admitSubmission`, an async check that decides whether a submission may be recorded at all. It receives the trusted `tenantId`, `spaceId`, `actorId` and `actorType`, the submitted parameters and a `signal` (`ActionAdmissionSignal`, a structural subset of `AbortSignal`; Host passes a real one), and answers `{ admitted: true }` or `{ admitted: false, code }` with a fixed code. `@fabricorg/platform-host` 7.3.0 and later runs it after authorizing the submitter and before writing anything for the submission, refuses on a throw, timeout or any other answer, and passes the admission time to the handler as `ActionContext.admittedAt`. An older host ignores the field, so a handler that relies on it should refuse without `ctx.admittedAt`. Registration rejects an `admitSubmission` that is not a function.
107
122
 
@@ -141,6 +156,26 @@ Every `invokeAction(id, params)` call goes through:
141
156
  state-guard, adapter, or finalization failure semantics; see the
142
157
  [Host boundary and rollback contract](../platform-host/README.md#atomic-mutation-unit-of-work).
143
158
 
159
+ Provider-dependent records can use the optional action-level `finalize` hook through
160
+ `defineFinalizingAction`. It requires an atomic, immediate `after_adapters` action;
161
+ idempotency is not required. Before adapters, Host normalizes the hook's parameters and
162
+ public handler data with `JSON.parse(JSON.stringify(value))`, then validates the normalized
163
+ JSON: Dates become ISO strings, undefined object properties drop, class instances use their
164
+ JSON representation, and non-finite numbers become null. Serialization failures such as
165
+ cycles or BigInt fail before adapter execution. Host strips private `eventResultFields`
166
+ from hook handler data; handler events are normalized separately. The hook receives copies
167
+ of those saved inputs and ordered succeeded/skipped stored redacted adapter outputs, plus
168
+ transaction-bound `db` without services. Hook event schema versions default to
169
+ `action.version` when omitted.
170
+ It returns declared events and/or a shallow result patch. The manifest exports only
171
+ `finalization: { protocol: "atomic-after-adapters-v1" }`, not the callback.
172
+ The helper refuses older hosts before handler code runs. See the
173
+ [Host finalization contract](../platform-host/README.md#action-level-finalization) for
174
+ transaction requirements, validation, rollback and reconciliation semantics.
175
+ With a compatible durable preparation checkpoint, Host recovery reuses recorded outputs
176
+ and replays finalization without rerunning the handler. Missing or incompatible evidence
177
+ retains effect-aware refusal; see the Host finalization contract for rollout and limits.
178
+
144
179
  4. **Adapters** — for each `actionDef.adapterSteps`, the registered implementation runs via
145
180
  `executeWithAdapterRetry` (idempotent retry, exponential backoff, and an optional per-adapter
146
181
  circuit breaker; see [Adapter circuit breakers](#adapter-circuit-breakers)). Adapters may
@@ -534,6 +569,16 @@ Lifecycle evidence copies the supplied metadata's checksums; its options do not
534
569
  previously generated metadata. This option does not change `FieldRedactor`, `hash`,
535
570
  `tokenize`, `stableChecksum`, or packet-level checksum algorithms.
536
571
 
572
+ ## Adapter step deadlines
573
+
574
+ Adapter steps may declare `deadlineMs`, a positive finite integer duration in
575
+ milliseconds. Platform Host uses it instead of its `adapterDeadlineMs` duration for
576
+ that step. The budget begins just before retry execution and spans attempts and
577
+ retry delays. Action and module registration, manifest normalization and export,
578
+ and Host execution validate step durations. Host rejects invalid resolved steps
579
+ before handler or adapter execution. Manifest export preserves valid durations.
580
+ See the Platform Host README for cooperative cancellation and failure semantics.
581
+
537
582
  ## License
538
583
 
539
584
  MIT
@@ -14,6 +14,33 @@ function isActorType(value) {
14
14
  function assertActorType(value) {
15
15
  if (!isActorType(value)) throw new Error(`Invalid actorType: ${value}`);
16
16
  }
17
+ /** Validate declarations at registration and at dynamically resolved execution boundaries. */
18
+ function assertActionFinalization(action) {
19
+ if (action.finalize === void 0) return;
20
+ if (typeof action.finalize !== "function") throw new Error("Action finalization hook must be a function.");
21
+ if (action.eventPhase !== "after_adapters") throw new Error("Action finalization requires eventPhase after_adapters.");
22
+ if (action.kind !== void 0 && action.kind !== "atomic") throw new Error("Action finalization requires an atomic action.");
23
+ if (action.execution?.completion !== void 0 && action.execution.completion !== "immediate") throw new Error("Action finalization requires immediate completion.");
24
+ }
25
+ /** Refuse older hosts before application handler code executes. Use for distributed finalizing actions. */
26
+ function defineFinalizingAction(action) {
27
+ assertActionFinalization(action);
28
+ const handler = action.handler;
29
+ return {
30
+ ...action,
31
+ handler: async (ctx, params) => {
32
+ if (ctx.finalizationProtocol !== "atomic-after-adapters-v1") throw new Error("Host does not support atomic-after-adapters-v1 finalization.");
33
+ return handler ? handler(ctx, params) : {
34
+ success: true,
35
+ data: {}
36
+ };
37
+ }
38
+ };
39
+ }
40
+ /** Validate optional step execution budgets before accepting or executing an action. */
41
+ function assertAdapterStepDeadlines(action) {
42
+ for (const step of action.adapterSteps ?? []) if (step.deadlineMs !== void 0 && (!Number.isInteger(step.deadlineMs) || step.deadlineMs <= 0)) throw new Error(`Action "${action.actionId}" adapter step deadlineMs must be a positive finite integer.`);
43
+ }
17
44
  function assertActionId(actionId) {
18
45
  if (!/^[a-z][a-z0-9-]*\.[a-z][a-z0-9_]*$/.test(actionId)) throw new Error(`Invalid action ID: ${actionId}`);
19
46
  }
@@ -24,6 +51,8 @@ function createActionRegistry(definitions = []) {
24
51
  if (actions.has(definition.actionId)) throw new Error(`Action "${definition.actionId}" is already registered`);
25
52
  if (definition.mutatesDomain && definition.emitsEvents.length === 0) throw new Error(`Action "${definition.actionId}" mutates domain but emits no events. Every mutating action must emit at least one AssetEvent.`);
26
53
  if (definition.admitSubmission !== void 0 && typeof definition.admitSubmission !== "function") throw new Error(`Action "${definition.actionId}" declares admitSubmission that is not a function.`);
54
+ assertAdapterStepDeadlines(definition);
55
+ assertActionFinalization(definition);
27
56
  actions.set(definition.actionId, definition);
28
57
  };
29
58
  for (const definition of definitions) register(definition);
@@ -39,9 +68,12 @@ function createActionRegistry(definitions = []) {
39
68
  }
40
69
  //#endregion
41
70
  exports.ACTOR_TYPES = ACTOR_TYPES;
71
+ exports.assertActionFinalization = assertActionFinalization;
42
72
  exports.assertActionId = assertActionId;
43
73
  exports.assertActorType = assertActorType;
74
+ exports.assertAdapterStepDeadlines = assertAdapterStepDeadlines;
44
75
  exports.createActionRegistry = createActionRegistry;
76
+ exports.defineFinalizingAction = defineFinalizingAction;
45
77
  exports.isActorType = isActorType;
46
78
 
47
79
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":[],"sources":["../../actions/index.ts"],"sourcesContent":["import type { SurfaceDeclaration } from \"../discovery\";\nexport type ActionId = `${string}.${string}`;\n\nexport interface SchemaParseIssue {\n\tpath: PropertyKey[];\n\tmessage: string;\n}\n\nexport type SchemaParseResult<T = unknown> =\n\t| { success: true; data: T }\n\t| { success: false; error: { issues: SchemaParseIssue[] } };\n\nexport interface SchemaValidator<T = unknown> {\n\tparse(input: unknown): T;\n\tsafeParse(input: unknown): SchemaParseResult<T>;\n}\n\nexport type ActorType =\n\t| \"natural_person\"\n\t| \"agent\"\n\t| \"system\"\n\t| \"service_account\"\n\t| \"external_system\"\n\t| \"integration\";\n\nexport const ACTOR_TYPES: readonly ActorType[] = [\n\t\"natural_person\",\n\t\"agent\",\n\t\"system\",\n\t\"service_account\",\n\t\"external_system\",\n\t\"integration\",\n] as const;\n\nexport function isActorType(value: string): value is ActorType {\n\treturn (ACTOR_TYPES as readonly string[]).includes(value);\n}\n\nexport function assertActorType(value: string): asserts value is ActorType {\n\tif (!isActorType(value)) {\n\t\tthrow new Error(`Invalid actorType: ${value}`);\n\t}\n}\n\nexport type ActionStatus =\n\t| \"pending\"\n\t| \"running\"\n\t| \"completed\"\n\t| \"failed\"\n\t| \"blocked_by_policy\"\n\t| \"waiting_for_approval\"\n\t| \"reconciliation_required\"\n\t| \"validation_failed\";\n\nexport interface FabricRuntimeServices {\n\treadonly [serviceName: string]: unknown;\n}\n\nexport interface ActionContext<TDb = unknown> {\n\tactionInvocationId: string;\n\ttenantId: string;\n\tspaceId: string;\n\tactorId: string;\n\tactorType: ActorType;\n\tcorrelationId: string;\n\tcausationId?: string;\n\tprovenance?: { source: string; auditAttributes?: Record<string, string> };\n\tdb: TDb;\n\tservices?: FabricRuntimeServices;\n\t/**\n\t * When the host admitted this invocation's submission through the\n\t * action's `admitSubmission` check, read from the durable invocation\n\t * record. Absent when the action declares no check, or when the runtime\n\t * that built this context does not run declared admission checks (a host\n\t * older than the one that introduced them). A handler whose action\n\t * declares `admitSubmission` can refuse to run without it.\n\t */\n\tadmittedAt?: Date;\n}\n\n/**\n * Trusted facts a host gives an action's `admitSubmission` check. Scope and\n * actor come from the host's authenticated submission, never from the\n * parameters.\n */\nexport interface ActionAdmissionInput {\n\treadonly actionId: ActionId;\n\treadonly tenantId: string;\n\treadonly spaceId: string;\n\treadonly actorId: string;\n\treadonly actorType: ActorType;\n\t/**\n\t * The submitted parameters, unvalidated, as a copy made for this check.\n\t * `@fabricorg/platform-host` records its own copy of the same value, taken\n\t * before the check ran, and only if the check admits it; changing this\n\t * copy changes nothing the host records.\n\t */\n\treadonly parameters: Readonly<Record<string, unknown>>;\n\t/**\n\t * Aborted when the host's admission deadline passes. Hosts pass an\n\t * `AbortSignal`. `@fabricorg/platform-host` refuses an answer that settles\n\t * at or after the deadline, even one that settles before the abort.\n\t */\n\treadonly signal: ActionAdmissionSignal;\n}\n\n/**\n * The part of an `AbortSignal` an admission check can rely on. Declared\n * structurally so this module compiles without DOM or Node type libraries; a\n * host passes a real `AbortSignal`, which satisfies it.\n */\nexport interface ActionAdmissionSignal {\n\treadonly aborted: boolean;\n\taddEventListener(type: \"abort\", listener: () => void, options?: { once?: boolean }): void;\n\tremoveEventListener(type: \"abort\", listener: () => void): void;\n}\n\n/**\n * An admission check's answer. Only `{ admitted: true }` admits. A refusal's\n * `code` must be a fixed code chosen by the action (lower-case first letter,\n * then letters, digits, `_`, `.`, `:` or `-`, at most 128 characters), never\n * a submitted value: the host puts it in the error it throws.\n */\nexport type ActionAdmissionDecision =\n\t| { readonly admitted: true }\n\t| { readonly admitted: false; readonly code: string };\n\n/**\n * Decides whether a submission may be recorded at all. A host that supports\n * it runs the check after authorizing the submitter and before it writes\n * anything durable for the submission; a refusal, a throw, a timeout or any\n * answer other than `{ admitted: true }` refuses the submission.\n */\nexport type ActionAdmission = (input: ActionAdmissionInput) => Promise<ActionAdmissionDecision>;\n\nexport interface ActionResult {\n\tsuccess: boolean;\n\tdata?: Record<string, unknown>;\n\terror?: string;\n}\n\nexport type ActionHandler<TDb = unknown> = (\n\tctx: ActionContext<TDb>,\n\tparams: unknown,\n) => Promise<ActionResult>;\n\nexport interface AdapterStepRetryPolicy {\n\tidempotent: boolean;\n\tmaxAttempts?: number;\n\tinitialDelayMs?: number;\n\tbackoffMultiplier?: number;\n\tmaxDelayMs?: number;\n}\n\nexport interface AdapterStep {\n\tadapterType: string;\n\toperation: string;\n\tretryPolicy?: AdapterStepRetryPolicy;\n\tgetInput: (\n\t\tparams: unknown,\n\t\thandlerResult: Record<string, unknown>,\n\t) => Record<string, unknown> | undefined;\n}\n\nexport interface ActionStateMachineBinding {\n\tentityType: string;\n\ttargetState: string | ((params: unknown) => string);\n\tgetEntityId: (params: unknown) => string | undefined;\n}\n\nexport interface ActionDefinition<TDb = unknown> extends SurfaceDeclaration {\n\tactionId: ActionId;\n\tnamespace: string;\n\tversion: number;\n\tschema: SchemaValidator;\n\t/**\n\t * Optional check a host runs on every submission before it records\n\t * anything durable for it (invocation, idempotency key, events, errors).\n\t * Use it when parameters must be validated against trusted context, for\n\t * example a secret reference the tenant's secret store must hold, before\n\t * they may be persisted. `@fabricorg/platform-host` runs it from the\n\t * release that introduced it; an older host ignores it, so a handler that\n\t * relies on it should require `ActionContext.admittedAt`.\n\t */\n\tadmitSubmission?: ActionAdmission;\n\t/** Optional validation contract for the public handler result data. */\n\tresultSchema?: SchemaValidator;\n\thandler?: ActionHandler<TDb>;\n\trequiredRoles?: string[];\n\trequiredPermissions?: string[];\n\tpolicies?: `${string}.v${number}`[];\n\temitsEvents: string[];\n\tidempotent: boolean;\n\tmutatesDomain: boolean;\n\t/** Portable execution semantics used by hosts, gateways, and generated capability contracts. */\n\texecution?: ActionExecutionContract;\n\tstateMachine?: ActionStateMachineBinding;\n\tadapterSteps?: AdapterStep[];\n\t/** Completion actions can attest only after all external adapter work succeeds. */\n\teventPhase?: \"before_adapters\" | \"after_adapters\";\n\t/**\n\t * If \"saga\", the action is a multi-step orchestration. The worker dispatches\n\t * to a registered saga implementation instead of running `handler` directly.\n\t */\n\tkind?: \"atomic\" | \"saga\";\n}\n\nexport type ActionConnectivity = \"online-required\" | \"offline-capturable\";\nexport type AuthorityMoment = \"capture\" | \"execution\" | \"both\";\nexport type ActionIdempotencyStance = \"required\" | \"supported\" | \"prohibited\";\nexport type ActionCompletionMode = \"immediate\" | \"accepted\" | \"long-running\";\nexport type ActionConsistency = \"authoritative-now\" | \"provisional-until-reconciled\";\nexport type ActionDataSensitivity = \"public\" | \"internal\" | \"confidential\" | \"restricted\";\n\nexport interface ActionExecutionContract {\n\tconnectivity: ActionConnectivity;\n\tauthorityMoment: AuthorityMoment;\n\tidempotency: ActionIdempotencyStance;\n\t/**\n\t * `immediate`: the invocation settles within the host's execution.\n\t * `accepted` and `long-running`: an adapter may hand the work to an\n\t * external system and the invocation stays running until that system\n\t * completes it by reference through the host's completion ingress.\n\t * Declare `completionDeadlineMs` on a `long-running` action so a callback\n\t * that never comes is noticed rather than waited on forever.\n\t */\n\tcompletion: ActionCompletionMode;\n\t/**\n\t * How long an accepted handoff may wait for its completion before the host\n\t * moves the invocation to reconciliation. Without it, nothing sweeps the\n\t * handoff. Refused on `immediate`, since nothing external completes it.\n\t */\n\tcompletionDeadlineMs?: number;\n\tconsistency: ActionConsistency;\n\tsensitivity?: ActionDataSensitivity;\n}\n\nexport interface SagaContext {\n\tactionInvocationId: string;\n\ttenantId: string;\n\tspaceId: string;\n\tparameters: unknown;\n\tsleep?: (delayMs: number) => Promise<void>;\n\tquery?: <T = unknown>(model: string, operation: string, args: unknown) => Promise<T>;\n\t/**\n\t * Wait for a named external signal carrying a payload of `T`, with a timeout.\n\t *\n\t * Resolves with the payload of the first signal received, or `null` if the timeout\n\t * fires first. Subsequent distinct signals with the same name received before\n\t * another `awaitSignal` call are buffered in FIFO order so none is lost.\n\t *\n\t * A signal may carry a stable signal ID that the sender keeps the same on each redelivery\n\t * (for example a webhook delivery ID or an approval decision ID). A runtime drops\n\t * a signal whose ID it has already received for this saga, so a sender retrying\n\t * after a timeout does not wake the saga twice. Signals without an ID are not\n\t * deduplicated. `@fabricorg/platform-temporal` provides `createSagaSignalInbox()`\n\t * for Temporal runtimes.\n\t *\n\t * Intended for agent-driven and webhook-driven coordination: replace polling loops\n\t * with push-based wakeups while preserving the saga's deterministic replay\n\t * semantics (the runtime is responsible for capturing signals into workflow history).\n\t *\n\t * Optional. Runtimes that cannot implement push-based signaling (e.g. an in-memory\n\t * test harness) may omit this; sagas should treat absence as a fallback condition\n\t * and use `sleep` + `query` polling instead.\n\t *\n\t * Signal *senders* are runtime-specific (e.g. a Temporal `WorkflowClient.signal()`\n\t * call from an action handler). Apps that need to send signals from action\n\t * handlers should expose a coordination service via `ActionContext.services`\n\t * — the platform does not prescribe the sender API.\n\t *\n\t * @example\n\t * const payload = await ctx.awaitSignal?.<{ result: Record<string, unknown> }>(\n\t * \"userInputProcessed\",\n\t * 5 * 60 * 1000,\n\t * );\n\t * if (payload) { /* handle pushed result *\\/ }\n\t * else { /* timeout — fall back to deterministic path *\\/ }\n\t */\n\tawaitSignal?: <T = unknown>(signalName: string, timeoutMs: number) => Promise<T | null>;\n}\n\nexport interface SagaChildActionInput {\n\tsuffix: string;\n\tactionId: string;\n\tparameters: Record<string, unknown>;\n}\n\nexport type SagaRunChild = (input: SagaChildActionInput) => Promise<Record<string, unknown>>;\n\nexport interface SagaCompletionEvent {\n\teventType: string;\n\tsubjectType: string;\n\tsubjectId: string;\n\tpayload: unknown;\n\teventSchemaVersion?: number;\n}\n\nexport interface SagaCompletion {\n\tresultData: Record<string, unknown>;\n\tevent?: SagaCompletionEvent;\n}\n\nexport type SagaImplementation = (\n\tctx: SagaContext,\n\trunChild: SagaRunChild,\n) => Promise<SagaCompletion>;\n\nexport function assertActionId(actionId: string): asserts actionId is ActionId {\n\tif (!/^[a-z][a-z0-9-]*\\.[a-z][a-z0-9_]*$/.test(actionId)) {\n\t\tthrow new Error(`Invalid action ID: ${actionId}`);\n\t}\n}\n\nexport interface ActionRegistry<TDb = unknown> {\n\tregister(definition: ActionDefinition<TDb>): void;\n\tregisterIfMissing(definition: ActionDefinition<TDb>): void;\n\tresolve(actionId: ActionId): ActionDefinition<TDb> | undefined;\n\thas(actionId: string): actionId is ActionId;\n\tids(): ActionId[];\n}\n\nexport function createActionRegistry<TDb = unknown>(definitions: readonly ActionDefinition<TDb>[] = []): ActionRegistry<TDb> {\n\tconst actions = new Map<ActionId, ActionDefinition<TDb>>();\n\tconst register = (definition: ActionDefinition<TDb>): void => {\n\t\tassertActionId(definition.actionId);\n\t\tif (actions.has(definition.actionId)) throw new Error(`Action \"${definition.actionId}\" is already registered`);\n\t\tif (definition.mutatesDomain && definition.emitsEvents.length === 0) {\n\t\t\tthrow new Error(`Action \"${definition.actionId}\" mutates domain but emits no events. Every mutating action must emit at least one AssetEvent.`);\n\t\t}\n\t\tif (definition.admitSubmission !== undefined && typeof definition.admitSubmission !== \"function\") {\n\t\t\tthrow new Error(`Action \"${definition.actionId}\" declares admitSubmission that is not a function.`);\n\t\t}\n\t\tactions.set(definition.actionId, definition);\n\t};\n\tfor (const definition of definitions) register(definition);\n\treturn {\n\t\tregister,\n\t\tregisterIfMissing(definition) {\n\t\t\tif (!actions.has(definition.actionId)) register(definition);\n\t\t},\n\t\tresolve: (actionId) => actions.get(actionId),\n\t\thas: (actionId): actionId is ActionId => actions.has(actionId as ActionId),\n\t\tids: () => Array.from(actions.keys()),\n\t};\n}\n"],"mappings":";;AAyBA,MAAa,cAAoC;CAChD;CACA;CACA;CACA;CACA;CACA;AACD;AAEA,SAAgB,YAAY,OAAmC;CAC9D,OAAQ,YAAkC,SAAS,KAAK;AACzD;AAEA,SAAgB,gBAAgB,OAA2C;CAC1E,IAAI,CAAC,YAAY,KAAK,GACrB,MAAM,IAAI,MAAM,sBAAsB,OAAO;AAE/C;AA0QA,SAAgB,eAAe,UAAgD;CAC9E,IAAI,CAAC,qCAAqC,KAAK,QAAQ,GACtD,MAAM,IAAI,MAAM,sBAAsB,UAAU;AAElD;AAUA,SAAgB,qBAAoC,cAAgD,CAAC,GAAwB;CAC5H,MAAM,0BAAU,IAAI,IAAqC;CACzD,MAAM,YAAY,eAA4C;EAC7D,eAAe,WAAW,QAAQ;EAClC,IAAI,QAAQ,IAAI,WAAW,QAAQ,GAAG,MAAM,IAAI,MAAM,WAAW,WAAW,SAAS,wBAAwB;EAC7G,IAAI,WAAW,iBAAiB,WAAW,YAAY,WAAW,GACjE,MAAM,IAAI,MAAM,WAAW,WAAW,SAAS,+FAA+F;EAE/I,IAAI,WAAW,oBAAoB,KAAA,KAAa,OAAO,WAAW,oBAAoB,YACrF,MAAM,IAAI,MAAM,WAAW,WAAW,SAAS,mDAAmD;EAEnG,QAAQ,IAAI,WAAW,UAAU,UAAU;CAC5C;CACA,KAAK,MAAM,cAAc,aAAa,SAAS,UAAU;CACzD,OAAO;EACN;EACA,kBAAkB,YAAY;GAC7B,IAAI,CAAC,QAAQ,IAAI,WAAW,QAAQ,GAAG,SAAS,UAAU;EAC3D;EACA,UAAU,aAAa,QAAQ,IAAI,QAAQ;EAC3C,MAAM,aAAmC,QAAQ,IAAI,QAAoB;EACzE,WAAW,MAAM,KAAK,QAAQ,KAAK,CAAC;CACrC;AACD"}
1
+ {"version":3,"file":"index.cjs","names":[],"sources":["../../actions/index.ts"],"sourcesContent":["import type { SurfaceDeclaration } from \"../discovery\";\nexport type ActionId = `${string}.${string}`;\n\nexport interface SchemaParseIssue {\n\tpath: PropertyKey[];\n\tmessage: string;\n}\n\nexport type SchemaParseResult<T = unknown> =\n\t| { success: true; data: T }\n\t| { success: false; error: { issues: SchemaParseIssue[] } };\n\nexport interface SchemaValidator<T = unknown> {\n\tparse(input: unknown): T;\n\tsafeParse(input: unknown): SchemaParseResult<T>;\n}\n\nexport type ActorType =\n\t| \"natural_person\"\n\t| \"agent\"\n\t| \"system\"\n\t| \"service_account\"\n\t| \"external_system\"\n\t| \"integration\";\n\nexport const ACTOR_TYPES: readonly ActorType[] = [\n\t\"natural_person\",\n\t\"agent\",\n\t\"system\",\n\t\"service_account\",\n\t\"external_system\",\n\t\"integration\",\n] as const;\n\nexport function isActorType(value: string): value is ActorType {\n\treturn (ACTOR_TYPES as readonly string[]).includes(value);\n}\n\nexport function assertActorType(value: string): asserts value is ActorType {\n\tif (!isActorType(value)) {\n\t\tthrow new Error(`Invalid actorType: ${value}`);\n\t}\n}\n\nexport type ActionStatus =\n\t| \"pending\"\n\t| \"running\"\n\t| \"completed\"\n\t| \"failed\"\n\t| \"blocked_by_policy\"\n\t| \"waiting_for_approval\"\n\t| \"reconciliation_required\"\n\t| \"validation_failed\";\n\nexport interface FabricRuntimeServices {\n\treadonly [serviceName: string]: unknown;\n}\n\nexport interface ActionContext<TDb = unknown> {\n\tactionInvocationId: string;\n\ttenantId: string;\n\tspaceId: string;\n\tactorId: string;\n\tactorType: ActorType;\n\tcorrelationId: string;\n\tcausationId?: string;\n\tprovenance?: { source: string; auditAttributes?: Record<string, string> };\n\tdb: TDb;\n\tservices?: FabricRuntimeServices;\n\tfinalizationProtocol?: \"atomic-after-adapters-v1\";\n\t/**\n\t * When the host admitted this invocation's submission through the\n\t * action's `admitSubmission` check, read from the durable invocation\n\t * record. Absent when the action declares no check, or when the runtime\n\t * that built this context does not run declared admission checks (a host\n\t * older than the one that introduced them). A handler whose action\n\t * declares `admitSubmission` can refuse to run without it.\n\t */\n\tadmittedAt?: Date;\n}\n\n/**\n * Trusted facts a host gives an action's `admitSubmission` check. Scope and\n * actor come from the host's authenticated submission, never from the\n * parameters.\n */\nexport interface ActionAdmissionInput {\n\treadonly actionId: ActionId;\n\treadonly tenantId: string;\n\treadonly spaceId: string;\n\treadonly actorId: string;\n\treadonly actorType: ActorType;\n\t/**\n\t * The submitted parameters, unvalidated, as a copy made for this check.\n\t * `@fabricorg/platform-host` records its own copy of the same value, taken\n\t * before the check ran, and only if the check admits it; changing this\n\t * copy changes nothing the host records.\n\t */\n\treadonly parameters: Readonly<Record<string, unknown>>;\n\t/**\n\t * Aborted when the host's admission deadline passes. Hosts pass an\n\t * `AbortSignal`. `@fabricorg/platform-host` refuses an answer that settles\n\t * at or after the deadline, even one that settles before the abort.\n\t */\n\treadonly signal: ActionAdmissionSignal;\n}\n\n/**\n * The part of an `AbortSignal` an admission check can rely on. Declared\n * structurally so this module compiles without DOM or Node type libraries; a\n * host passes a real `AbortSignal`, which satisfies it.\n */\nexport interface ActionAdmissionSignal {\n\treadonly aborted: boolean;\n\taddEventListener(type: \"abort\", listener: () => void, options?: { once?: boolean }): void;\n\tremoveEventListener(type: \"abort\", listener: () => void): void;\n}\n\n/**\n * An admission check's answer. Only `{ admitted: true }` admits. A refusal's\n * `code` must be a fixed code chosen by the action (lower-case first letter,\n * then letters, digits, `_`, `.`, `:` or `-`, at most 128 characters), never\n * a submitted value: the host puts it in the error it throws.\n */\nexport type ActionAdmissionDecision =\n\t| { readonly admitted: true }\n\t| { readonly admitted: false; readonly code: string };\n\n/**\n * Decides whether a submission may be recorded at all. A host that supports\n * it runs the check after authorizing the submitter and before it writes\n * anything durable for the submission; a refusal, a throw, a timeout or any\n * answer other than `{ admitted: true }` refuses the submission.\n */\nexport type ActionAdmission = (input: ActionAdmissionInput) => Promise<ActionAdmissionDecision>;\n\n/** A descendant invocation the Host durably stages with its parent mutation. */\nexport interface StagedInvocationRequest {\n\t/** Stable identity within one parent invocation. */\n\treadonly key: string;\n\treadonly actionId: ActionId;\n\treadonly parameters: Record<string, unknown>;\n}\n\nexport interface ActionResult {\n\tsuccess: boolean;\n\tdata?: Record<string, unknown>;\n\terror?: string;\n\t/** Host-owned descendants released only after this invocation completes. */\n\tstagedInvocations?: readonly StagedInvocationRequest[];\n}\n\nexport type ActionHandler<TDb = unknown> = (\n\tctx: ActionContext<TDb>,\n\tparams: unknown,\n) => Promise<ActionResult>;\n\nexport type FinalizationJson = null | boolean | number | string | readonly FinalizationJson[] | { readonly [key: string]: FinalizationJson };\nexport type FinalizationRecord = { readonly [key: string]: FinalizationJson };\nexport type ActionFinalizationContext<TDb = unknown> = Pick<ActionContext<TDb>,\n\t\"actionInvocationId\" | \"tenantId\" | \"spaceId\" | \"actorId\" | \"actorType\" |\n\t\"correlationId\" | \"causationId\" | \"provenance\" | \"db\">;\nexport type ActionFinalizationStep = {\n\treadonly stepIndex: number;\n\treadonly adapterType: string;\n\treadonly operation: string;\n} & ({ readonly status: \"succeeded\"; readonly adapterInvocationId: string; readonly output: FinalizationRecord } |\n\t{ readonly status: \"skipped\" });\nexport interface ActionFinalizationInput {\n\treadonly params: FinalizationJson;\n\treadonly handlerData: FinalizationRecord;\n\treadonly adapterOutputs: readonly ActionFinalizationStep[];\n}\nexport interface ActionFinalizationEvent {\n\treadonly eventType: string;\n\treadonly subjectType: string;\n\treadonly subjectId: string;\n\treadonly payload: FinalizationJson;\n\treadonly eventSchemaVersion?: number;\n}\nexport interface ActionFinalizationResult {\n\treadonly events?: readonly ActionFinalizationEvent[];\n\treadonly resultPatch?: FinalizationRecord;\n\t/** Additional descendants staged atomically with finalization and completion. */\n\treadonly stagedInvocations?: readonly StagedInvocationRequest[];\n}\nexport type ActionFinalizer<TDb = unknown> = (\n\tctx: ActionFinalizationContext<TDb>, input: ActionFinalizationInput,\n) => Promise<ActionFinalizationResult>;\n\n/** Validate declarations at registration and at dynamically resolved execution boundaries. */\nexport function assertActionFinalization<TDb>(action: Pick<ActionDefinition<TDb>, \"actionId\" | \"finalize\" | \"eventPhase\" | \"kind\" | \"execution\">): void {\n\tif (action.finalize === undefined) return;\n\tif (typeof action.finalize !== \"function\") throw new Error(\"Action finalization hook must be a function.\");\n\tif (action.eventPhase !== \"after_adapters\") throw new Error(\"Action finalization requires eventPhase after_adapters.\");\n\tif (action.kind !== undefined && action.kind !== \"atomic\") throw new Error(\"Action finalization requires an atomic action.\");\n\tif (action.execution?.completion !== undefined && action.execution.completion !== \"immediate\") throw new Error(\"Action finalization requires immediate completion.\");\n}\n\n/** Refuse older hosts before application handler code executes. Use for distributed finalizing actions. */\nexport function defineFinalizingAction<TDb>(action: ActionDefinition<TDb> & { finalize: ActionFinalizer<TDb> }): ActionDefinition<TDb> {\n\tassertActionFinalization(action);\n\tconst handler = action.handler;\n\treturn { ...action, handler: async (ctx, params) => {\n\t\tif (ctx.finalizationProtocol !== \"atomic-after-adapters-v1\") throw new Error(\"Host does not support atomic-after-adapters-v1 finalization.\");\n\t\treturn handler ? handler(ctx, params) : { success: true, data: {} };\n\t} };\n}\n\nexport interface AdapterStepRetryPolicy {\n\tidempotent: boolean;\n\tmaxAttempts?: number;\n\tinitialDelayMs?: number;\n\tbackoffMultiplier?: number;\n\tmaxDelayMs?: number;\n}\n\nexport interface AdapterStep {\n\tadapterType: string;\n\toperation: string;\n\tretryPolicy?: AdapterStepRetryPolicy;\n\t/** Positive finite integer duration in milliseconds; overrides Host adapterDeadlineMs for this step. */\n\tdeadlineMs?: number;\n\tgetInput: (\n\t\tparams: unknown,\n\t\thandlerResult: Record<string, unknown>,\n\t) => Record<string, unknown> | undefined;\n}\n\n/** Validate optional step execution budgets before accepting or executing an action. */\nexport function assertAdapterStepDeadlines(action: { actionId: string; adapterSteps?: readonly Pick<AdapterStep, \"deadlineMs\">[] }): void {\n\tfor (const step of action.adapterSteps ?? []) {\n\t\tif (step.deadlineMs !== undefined && (!Number.isInteger(step.deadlineMs) || step.deadlineMs <= 0)) {\n\t\t\tthrow new Error(`Action \"${action.actionId}\" adapter step deadlineMs must be a positive finite integer.`);\n\t\t}\n\t}\n}\n\nexport interface ActionStateMachineBinding {\n\tentityType: string;\n\ttargetState: string | ((params: unknown) => string);\n\tgetEntityId: (params: unknown) => string | undefined;\n}\n\nexport interface ActionDefinition<TDb = unknown> extends SurfaceDeclaration {\n\tactionId: ActionId;\n\tnamespace: string;\n\tversion: number;\n\tschema: SchemaValidator;\n\t/**\n\t * Optional check a host runs on every submission before it records\n\t * anything durable for it (invocation, idempotency key, events, errors).\n\t * Use it when parameters must be validated against trusted context, for\n\t * example a secret reference the tenant's secret store must hold, before\n\t * they may be persisted. `@fabricorg/platform-host` runs it from the\n\t * release that introduced it; an older host ignores it, so a handler that\n\t * relies on it should require `ActionContext.admittedAt`.\n\t */\n\tadmitSubmission?: ActionAdmission;\n\t/** Optional validation contract for the public handler result data. */\n\tresultSchema?: SchemaValidator;\n\thandler?: ActionHandler<TDb>;\n\tfinalize?: ActionFinalizer<TDb>;\n\trequiredRoles?: string[];\n\trequiredPermissions?: string[];\n\tpolicies?: `${string}.v${number}`[];\n\temitsEvents: string[];\n\tidempotent: boolean;\n\tmutatesDomain: boolean;\n\t/** Portable execution semantics used by hosts, gateways, and generated capability contracts. */\n\texecution?: ActionExecutionContract;\n\tstateMachine?: ActionStateMachineBinding;\n\tadapterSteps?: AdapterStep[];\n\t/** Child action IDs this action is permitted to stage for post-completion dispatch. */\n\tstagesInvocations?: readonly ActionId[];\n\t/** Completion actions can attest only after all external adapter work succeeds. */\n\teventPhase?: \"before_adapters\" | \"after_adapters\";\n\t/**\n\t * If \"saga\", the action is a multi-step orchestration. The worker dispatches\n\t * to a registered saga implementation instead of running `handler` directly.\n\t */\n\tkind?: \"atomic\" | \"saga\";\n}\n\nexport type ActionConnectivity = \"online-required\" | \"offline-capturable\";\nexport type AuthorityMoment = \"capture\" | \"execution\" | \"both\";\nexport type ActionIdempotencyStance = \"required\" | \"supported\" | \"prohibited\";\nexport type ActionCompletionMode = \"immediate\" | \"accepted\" | \"long-running\";\nexport type ActionConsistency = \"authoritative-now\" | \"provisional-until-reconciled\";\nexport type ActionDataSensitivity = \"public\" | \"internal\" | \"confidential\" | \"restricted\";\n\nexport interface ActionExecutionContract {\n\tconnectivity: ActionConnectivity;\n\tauthorityMoment: AuthorityMoment;\n\tidempotency: ActionIdempotencyStance;\n\t/**\n\t * `immediate`: the invocation settles within the host's execution.\n\t * `accepted` and `long-running`: an adapter may hand the work to an\n\t * external system and the invocation stays running until that system\n\t * completes it by reference through the host's completion ingress.\n\t * Declare `completionDeadlineMs` on a `long-running` action so a callback\n\t * that never comes is noticed rather than waited on forever.\n\t */\n\tcompletion: ActionCompletionMode;\n\t/**\n\t * How long an accepted handoff may wait for its completion before the host\n\t * moves the invocation to reconciliation. Without it, nothing sweeps the\n\t * handoff. Refused on `immediate`, since nothing external completes it.\n\t */\n\tcompletionDeadlineMs?: number;\n\tconsistency: ActionConsistency;\n\tsensitivity?: ActionDataSensitivity;\n}\n\nexport interface SagaContext {\n\tactionInvocationId: string;\n\ttenantId: string;\n\tspaceId: string;\n\tparameters: unknown;\n\tsleep?: (delayMs: number) => Promise<void>;\n\tquery?: <T = unknown>(model: string, operation: string, args: unknown) => Promise<T>;\n\t/**\n\t * Wait for a named external signal carrying a payload of `T`, with a timeout.\n\t *\n\t * Resolves with the payload of the first signal received, or `null` if the timeout\n\t * fires first. Subsequent distinct signals with the same name received before\n\t * another `awaitSignal` call are buffered in FIFO order so none is lost.\n\t *\n\t * A signal may carry a stable signal ID that the sender keeps the same on each redelivery\n\t * (for example a webhook delivery ID or an approval decision ID). A runtime drops\n\t * a signal whose ID it has already received for this saga, so a sender retrying\n\t * after a timeout does not wake the saga twice. Signals without an ID are not\n\t * deduplicated. `@fabricorg/platform-temporal` provides `createSagaSignalInbox()`\n\t * for Temporal runtimes.\n\t *\n\t * Intended for agent-driven and webhook-driven coordination: replace polling loops\n\t * with push-based wakeups while preserving the saga's deterministic replay\n\t * semantics (the runtime is responsible for capturing signals into workflow history).\n\t *\n\t * Optional. Runtimes that cannot implement push-based signaling (e.g. an in-memory\n\t * test harness) may omit this; sagas should treat absence as a fallback condition\n\t * and use `sleep` + `query` polling instead.\n\t *\n\t * Signal *senders* are runtime-specific (e.g. a Temporal `WorkflowClient.signal()`\n\t * call from an action handler). Apps that need to send signals from action\n\t * handlers should expose a coordination service via `ActionContext.services`\n\t * — the platform does not prescribe the sender API.\n\t *\n\t * @example\n\t * const payload = await ctx.awaitSignal?.<{ result: Record<string, unknown> }>(\n\t * \"userInputProcessed\",\n\t * 5 * 60 * 1000,\n\t * );\n\t * if (payload) { /* handle pushed result *\\/ }\n\t * else { /* timeout — fall back to deterministic path *\\/ }\n\t */\n\tawaitSignal?: <T = unknown>(signalName: string, timeoutMs: number) => Promise<T | null>;\n}\n\nexport interface SagaChildActionInput {\n\tsuffix: string;\n\tactionId: string;\n\tparameters: Record<string, unknown>;\n}\n\nexport type SagaRunChild = (input: SagaChildActionInput) => Promise<Record<string, unknown>>;\n\nexport interface SagaCompletionEvent {\n\teventType: string;\n\tsubjectType: string;\n\tsubjectId: string;\n\tpayload: unknown;\n\teventSchemaVersion?: number;\n}\n\nexport interface SagaCompletion {\n\tresultData: Record<string, unknown>;\n\tevent?: SagaCompletionEvent;\n}\n\nexport type SagaImplementation = (\n\tctx: SagaContext,\n\trunChild: SagaRunChild,\n) => Promise<SagaCompletion>;\n\nexport function assertActionId(actionId: string): asserts actionId is ActionId {\n\tif (!/^[a-z][a-z0-9-]*\\.[a-z][a-z0-9_]*$/.test(actionId)) {\n\t\tthrow new Error(`Invalid action ID: ${actionId}`);\n\t}\n}\n\nexport interface ActionRegistry<TDb = unknown> {\n\tregister(definition: ActionDefinition<TDb>): void;\n\tregisterIfMissing(definition: ActionDefinition<TDb>): void;\n\tresolve(actionId: ActionId): ActionDefinition<TDb> | undefined;\n\thas(actionId: string): actionId is ActionId;\n\tids(): ActionId[];\n}\n\nexport function createActionRegistry<TDb = unknown>(definitions: readonly ActionDefinition<TDb>[] = []): ActionRegistry<TDb> {\n\tconst actions = new Map<ActionId, ActionDefinition<TDb>>();\n\tconst register = (definition: ActionDefinition<TDb>): void => {\n\t\tassertActionId(definition.actionId);\n\t\tif (actions.has(definition.actionId)) throw new Error(`Action \"${definition.actionId}\" is already registered`);\n\t\tif (definition.mutatesDomain && definition.emitsEvents.length === 0) {\n\t\t\tthrow new Error(`Action \"${definition.actionId}\" mutates domain but emits no events. Every mutating action must emit at least one AssetEvent.`);\n\t\t}\n\t\tif (definition.admitSubmission !== undefined && typeof definition.admitSubmission !== \"function\") {\n\t\t\tthrow new Error(`Action \"${definition.actionId}\" declares admitSubmission that is not a function.`);\n\t\t}\n\t\tassertAdapterStepDeadlines(definition);\n\t\tassertActionFinalization(definition);\n\t\tactions.set(definition.actionId, definition);\n\t};\n\tfor (const definition of definitions) register(definition);\n\treturn {\n\t\tregister,\n\t\tregisterIfMissing(definition) {\n\t\t\tif (!actions.has(definition.actionId)) register(definition);\n\t\t},\n\t\tresolve: (actionId) => actions.get(actionId),\n\t\thas: (actionId): actionId is ActionId => actions.has(actionId as ActionId),\n\t\tids: () => Array.from(actions.keys()),\n\t};\n}\n"],"mappings":";;AAyBA,MAAa,cAAoC;CAChD;CACA;CACA;CACA;CACA;CACA;AACD;AAEA,SAAgB,YAAY,OAAmC;CAC9D,OAAQ,YAAkC,SAAS,KAAK;AACzD;AAEA,SAAgB,gBAAgB,OAA2C;CAC1E,IAAI,CAAC,YAAY,KAAK,GACrB,MAAM,IAAI,MAAM,sBAAsB,OAAO;AAE/C;;AAqJA,SAAgB,yBAA8B,QAA0G;CACvJ,IAAI,OAAO,aAAa,KAAA,GAAW;CACnC,IAAI,OAAO,OAAO,aAAa,YAAY,MAAM,IAAI,MAAM,8CAA8C;CACzG,IAAI,OAAO,eAAe,kBAAkB,MAAM,IAAI,MAAM,yDAAyD;CACrH,IAAI,OAAO,SAAS,KAAA,KAAa,OAAO,SAAS,UAAU,MAAM,IAAI,MAAM,gDAAgD;CAC3H,IAAI,OAAO,WAAW,eAAe,KAAA,KAAa,OAAO,UAAU,eAAe,aAAa,MAAM,IAAI,MAAM,oDAAoD;AACpK;;AAGA,SAAgB,uBAA4B,QAA2F;CACtI,yBAAyB,MAAM;CAC/B,MAAM,UAAU,OAAO;CACvB,OAAO;EAAE,GAAG;EAAQ,SAAS,OAAO,KAAK,WAAW;GACnD,IAAI,IAAI,yBAAyB,4BAA4B,MAAM,IAAI,MAAM,8DAA8D;GAC3I,OAAO,UAAU,QAAQ,KAAK,MAAM,IAAI;IAAE,SAAS;IAAM,MAAM,CAAC;GAAE;EACnE;CAAE;AACH;;AAuBA,SAAgB,2BAA2B,QAA+F;CACzI,KAAK,MAAM,QAAQ,OAAO,gBAAgB,CAAC,GAC1C,IAAI,KAAK,eAAe,KAAA,MAAc,CAAC,OAAO,UAAU,KAAK,UAAU,KAAK,KAAK,cAAc,IAC9F,MAAM,IAAI,MAAM,WAAW,OAAO,SAAS,6DAA6D;AAG3G;AAqJA,SAAgB,eAAe,UAAgD;CAC9E,IAAI,CAAC,qCAAqC,KAAK,QAAQ,GACtD,MAAM,IAAI,MAAM,sBAAsB,UAAU;AAElD;AAUA,SAAgB,qBAAoC,cAAgD,CAAC,GAAwB;CAC5H,MAAM,0BAAU,IAAI,IAAqC;CACzD,MAAM,YAAY,eAA4C;EAC7D,eAAe,WAAW,QAAQ;EAClC,IAAI,QAAQ,IAAI,WAAW,QAAQ,GAAG,MAAM,IAAI,MAAM,WAAW,WAAW,SAAS,wBAAwB;EAC7G,IAAI,WAAW,iBAAiB,WAAW,YAAY,WAAW,GACjE,MAAM,IAAI,MAAM,WAAW,WAAW,SAAS,+FAA+F;EAE/I,IAAI,WAAW,oBAAoB,KAAA,KAAa,OAAO,WAAW,oBAAoB,YACrF,MAAM,IAAI,MAAM,WAAW,WAAW,SAAS,mDAAmD;EAEnG,2BAA2B,UAAU;EACrC,yBAAyB,UAAU;EACnC,QAAQ,IAAI,WAAW,UAAU,UAAU;CAC5C;CACA,KAAK,MAAM,cAAc,aAAa,SAAS,UAAU;CACzD,OAAO;EACN;EACA,kBAAkB,YAAY;GAC7B,IAAI,CAAC,QAAQ,IAAI,WAAW,QAAQ,GAAG,SAAS,UAAU;EAC3D;EACA,UAAU,aAAa,QAAQ,IAAI,QAAQ;EAC3C,MAAM,aAAmC,QAAQ,IAAI,QAAoB;EACzE,WAAW,MAAM,KAAK,QAAQ,KAAK,CAAC;CACrC;AACD"}
@@ -40,6 +40,7 @@ export interface ActionContext<TDb = unknown> {
40
40
  };
41
41
  db: TDb;
42
42
  services?: FabricRuntimeServices;
43
+ finalizationProtocol?: "atomic-after-adapters-v1";
43
44
  /**
44
45
  * When the host admitted this invocation's submission through the
45
46
  * action's `admitSubmission` check, read from the durable invocation
@@ -106,12 +107,64 @@ export type ActionAdmissionDecision = {
106
107
  * answer other than `{ admitted: true }` refuses the submission.
107
108
  */
108
109
  export type ActionAdmission = (input: ActionAdmissionInput) => Promise<ActionAdmissionDecision>;
110
+ /** A descendant invocation the Host durably stages with its parent mutation. */
111
+ export interface StagedInvocationRequest {
112
+ /** Stable identity within one parent invocation. */
113
+ readonly key: string;
114
+ readonly actionId: ActionId;
115
+ readonly parameters: Record<string, unknown>;
116
+ }
109
117
  export interface ActionResult {
110
118
  success: boolean;
111
119
  data?: Record<string, unknown>;
112
120
  error?: string;
121
+ /** Host-owned descendants released only after this invocation completes. */
122
+ stagedInvocations?: readonly StagedInvocationRequest[];
113
123
  }
114
124
  export type ActionHandler<TDb = unknown> = (ctx: ActionContext<TDb>, params: unknown) => Promise<ActionResult>;
125
+ export type FinalizationJson = null | boolean | number | string | readonly FinalizationJson[] | {
126
+ readonly [key: string]: FinalizationJson;
127
+ };
128
+ export type FinalizationRecord = {
129
+ readonly [key: string]: FinalizationJson;
130
+ };
131
+ export type ActionFinalizationContext<TDb = unknown> = Pick<ActionContext<TDb>, "actionInvocationId" | "tenantId" | "spaceId" | "actorId" | "actorType" | "correlationId" | "causationId" | "provenance" | "db">;
132
+ export type ActionFinalizationStep = {
133
+ readonly stepIndex: number;
134
+ readonly adapterType: string;
135
+ readonly operation: string;
136
+ } & ({
137
+ readonly status: "succeeded";
138
+ readonly adapterInvocationId: string;
139
+ readonly output: FinalizationRecord;
140
+ } | {
141
+ readonly status: "skipped";
142
+ });
143
+ export interface ActionFinalizationInput {
144
+ readonly params: FinalizationJson;
145
+ readonly handlerData: FinalizationRecord;
146
+ readonly adapterOutputs: readonly ActionFinalizationStep[];
147
+ }
148
+ export interface ActionFinalizationEvent {
149
+ readonly eventType: string;
150
+ readonly subjectType: string;
151
+ readonly subjectId: string;
152
+ readonly payload: FinalizationJson;
153
+ readonly eventSchemaVersion?: number;
154
+ }
155
+ export interface ActionFinalizationResult {
156
+ readonly events?: readonly ActionFinalizationEvent[];
157
+ readonly resultPatch?: FinalizationRecord;
158
+ /** Additional descendants staged atomically with finalization and completion. */
159
+ readonly stagedInvocations?: readonly StagedInvocationRequest[];
160
+ }
161
+ export type ActionFinalizer<TDb = unknown> = (ctx: ActionFinalizationContext<TDb>, input: ActionFinalizationInput) => Promise<ActionFinalizationResult>;
162
+ /** Validate declarations at registration and at dynamically resolved execution boundaries. */
163
+ export declare function assertActionFinalization<TDb>(action: Pick<ActionDefinition<TDb>, "actionId" | "finalize" | "eventPhase" | "kind" | "execution">): void;
164
+ /** Refuse older hosts before application handler code executes. Use for distributed finalizing actions. */
165
+ export declare function defineFinalizingAction<TDb>(action: ActionDefinition<TDb> & {
166
+ finalize: ActionFinalizer<TDb>;
167
+ }): ActionDefinition<TDb>;
115
168
  export interface AdapterStepRetryPolicy {
116
169
  idempotent: boolean;
117
170
  maxAttempts?: number;
@@ -123,8 +176,15 @@ export interface AdapterStep {
123
176
  adapterType: string;
124
177
  operation: string;
125
178
  retryPolicy?: AdapterStepRetryPolicy;
179
+ /** Positive finite integer duration in milliseconds; overrides Host adapterDeadlineMs for this step. */
180
+ deadlineMs?: number;
126
181
  getInput: (params: unknown, handlerResult: Record<string, unknown>) => Record<string, unknown> | undefined;
127
182
  }
183
+ /** Validate optional step execution budgets before accepting or executing an action. */
184
+ export declare function assertAdapterStepDeadlines(action: {
185
+ actionId: string;
186
+ adapterSteps?: readonly Pick<AdapterStep, "deadlineMs">[];
187
+ }): void;
128
188
  export interface ActionStateMachineBinding {
129
189
  entityType: string;
130
190
  targetState: string | ((params: unknown) => string);
@@ -148,6 +208,7 @@ export interface ActionDefinition<TDb = unknown> extends SurfaceDeclaration {
148
208
  /** Optional validation contract for the public handler result data. */
149
209
  resultSchema?: SchemaValidator;
150
210
  handler?: ActionHandler<TDb>;
211
+ finalize?: ActionFinalizer<TDb>;
151
212
  requiredRoles?: string[];
152
213
  requiredPermissions?: string[];
153
214
  policies?: `${string}.v${number}`[];
@@ -158,6 +219,8 @@ export interface ActionDefinition<TDb = unknown> extends SurfaceDeclaration {
158
219
  execution?: ActionExecutionContract;
159
220
  stateMachine?: ActionStateMachineBinding;
160
221
  adapterSteps?: AdapterStep[];
222
+ /** Child action IDs this action is permitted to stage for post-completion dispatch. */
223
+ stagesInvocations?: readonly ActionId[];
161
224
  /** Completion actions can attest only after all external adapter work succeeds. */
162
225
  eventPhase?: "before_adapters" | "after_adapters";
163
226
  /**
@@ -40,6 +40,7 @@ export interface ActionContext<TDb = unknown> {
40
40
  };
41
41
  db: TDb;
42
42
  services?: FabricRuntimeServices;
43
+ finalizationProtocol?: "atomic-after-adapters-v1";
43
44
  /**
44
45
  * When the host admitted this invocation's submission through the
45
46
  * action's `admitSubmission` check, read from the durable invocation
@@ -106,12 +107,64 @@ export type ActionAdmissionDecision = {
106
107
  * answer other than `{ admitted: true }` refuses the submission.
107
108
  */
108
109
  export type ActionAdmission = (input: ActionAdmissionInput) => Promise<ActionAdmissionDecision>;
110
+ /** A descendant invocation the Host durably stages with its parent mutation. */
111
+ export interface StagedInvocationRequest {
112
+ /** Stable identity within one parent invocation. */
113
+ readonly key: string;
114
+ readonly actionId: ActionId;
115
+ readonly parameters: Record<string, unknown>;
116
+ }
109
117
  export interface ActionResult {
110
118
  success: boolean;
111
119
  data?: Record<string, unknown>;
112
120
  error?: string;
121
+ /** Host-owned descendants released only after this invocation completes. */
122
+ stagedInvocations?: readonly StagedInvocationRequest[];
113
123
  }
114
124
  export type ActionHandler<TDb = unknown> = (ctx: ActionContext<TDb>, params: unknown) => Promise<ActionResult>;
125
+ export type FinalizationJson = null | boolean | number | string | readonly FinalizationJson[] | {
126
+ readonly [key: string]: FinalizationJson;
127
+ };
128
+ export type FinalizationRecord = {
129
+ readonly [key: string]: FinalizationJson;
130
+ };
131
+ export type ActionFinalizationContext<TDb = unknown> = Pick<ActionContext<TDb>, "actionInvocationId" | "tenantId" | "spaceId" | "actorId" | "actorType" | "correlationId" | "causationId" | "provenance" | "db">;
132
+ export type ActionFinalizationStep = {
133
+ readonly stepIndex: number;
134
+ readonly adapterType: string;
135
+ readonly operation: string;
136
+ } & ({
137
+ readonly status: "succeeded";
138
+ readonly adapterInvocationId: string;
139
+ readonly output: FinalizationRecord;
140
+ } | {
141
+ readonly status: "skipped";
142
+ });
143
+ export interface ActionFinalizationInput {
144
+ readonly params: FinalizationJson;
145
+ readonly handlerData: FinalizationRecord;
146
+ readonly adapterOutputs: readonly ActionFinalizationStep[];
147
+ }
148
+ export interface ActionFinalizationEvent {
149
+ readonly eventType: string;
150
+ readonly subjectType: string;
151
+ readonly subjectId: string;
152
+ readonly payload: FinalizationJson;
153
+ readonly eventSchemaVersion?: number;
154
+ }
155
+ export interface ActionFinalizationResult {
156
+ readonly events?: readonly ActionFinalizationEvent[];
157
+ readonly resultPatch?: FinalizationRecord;
158
+ /** Additional descendants staged atomically with finalization and completion. */
159
+ readonly stagedInvocations?: readonly StagedInvocationRequest[];
160
+ }
161
+ export type ActionFinalizer<TDb = unknown> = (ctx: ActionFinalizationContext<TDb>, input: ActionFinalizationInput) => Promise<ActionFinalizationResult>;
162
+ /** Validate declarations at registration and at dynamically resolved execution boundaries. */
163
+ export declare function assertActionFinalization<TDb>(action: Pick<ActionDefinition<TDb>, "actionId" | "finalize" | "eventPhase" | "kind" | "execution">): void;
164
+ /** Refuse older hosts before application handler code executes. Use for distributed finalizing actions. */
165
+ export declare function defineFinalizingAction<TDb>(action: ActionDefinition<TDb> & {
166
+ finalize: ActionFinalizer<TDb>;
167
+ }): ActionDefinition<TDb>;
115
168
  export interface AdapterStepRetryPolicy {
116
169
  idempotent: boolean;
117
170
  maxAttempts?: number;
@@ -123,8 +176,15 @@ export interface AdapterStep {
123
176
  adapterType: string;
124
177
  operation: string;
125
178
  retryPolicy?: AdapterStepRetryPolicy;
179
+ /** Positive finite integer duration in milliseconds; overrides Host adapterDeadlineMs for this step. */
180
+ deadlineMs?: number;
126
181
  getInput: (params: unknown, handlerResult: Record<string, unknown>) => Record<string, unknown> | undefined;
127
182
  }
183
+ /** Validate optional step execution budgets before accepting or executing an action. */
184
+ export declare function assertAdapterStepDeadlines(action: {
185
+ actionId: string;
186
+ adapterSteps?: readonly Pick<AdapterStep, "deadlineMs">[];
187
+ }): void;
128
188
  export interface ActionStateMachineBinding {
129
189
  entityType: string;
130
190
  targetState: string | ((params: unknown) => string);
@@ -148,6 +208,7 @@ export interface ActionDefinition<TDb = unknown> extends SurfaceDeclaration {
148
208
  /** Optional validation contract for the public handler result data. */
149
209
  resultSchema?: SchemaValidator;
150
210
  handler?: ActionHandler<TDb>;
211
+ finalize?: ActionFinalizer<TDb>;
151
212
  requiredRoles?: string[];
152
213
  requiredPermissions?: string[];
153
214
  policies?: `${string}.v${number}`[];
@@ -158,6 +219,8 @@ export interface ActionDefinition<TDb = unknown> extends SurfaceDeclaration {
158
219
  execution?: ActionExecutionContract;
159
220
  stateMachine?: ActionStateMachineBinding;
160
221
  adapterSteps?: AdapterStep[];
222
+ /** Child action IDs this action is permitted to stage for post-completion dispatch. */
223
+ stagesInvocations?: readonly ActionId[];
161
224
  /** Completion actions can attest only after all external adapter work succeeds. */
162
225
  eventPhase?: "before_adapters" | "after_adapters";
163
226
  /**
@@ -13,6 +13,33 @@ function isActorType(value) {
13
13
  function assertActorType(value) {
14
14
  if (!isActorType(value)) throw new Error(`Invalid actorType: ${value}`);
15
15
  }
16
+ /** Validate declarations at registration and at dynamically resolved execution boundaries. */
17
+ function assertActionFinalization(action) {
18
+ if (action.finalize === void 0) return;
19
+ if (typeof action.finalize !== "function") throw new Error("Action finalization hook must be a function.");
20
+ if (action.eventPhase !== "after_adapters") throw new Error("Action finalization requires eventPhase after_adapters.");
21
+ if (action.kind !== void 0 && action.kind !== "atomic") throw new Error("Action finalization requires an atomic action.");
22
+ if (action.execution?.completion !== void 0 && action.execution.completion !== "immediate") throw new Error("Action finalization requires immediate completion.");
23
+ }
24
+ /** Refuse older hosts before application handler code executes. Use for distributed finalizing actions. */
25
+ function defineFinalizingAction(action) {
26
+ assertActionFinalization(action);
27
+ const handler = action.handler;
28
+ return {
29
+ ...action,
30
+ handler: async (ctx, params) => {
31
+ if (ctx.finalizationProtocol !== "atomic-after-adapters-v1") throw new Error("Host does not support atomic-after-adapters-v1 finalization.");
32
+ return handler ? handler(ctx, params) : {
33
+ success: true,
34
+ data: {}
35
+ };
36
+ }
37
+ };
38
+ }
39
+ /** Validate optional step execution budgets before accepting or executing an action. */
40
+ function assertAdapterStepDeadlines(action) {
41
+ for (const step of action.adapterSteps ?? []) if (step.deadlineMs !== void 0 && (!Number.isInteger(step.deadlineMs) || step.deadlineMs <= 0)) throw new Error(`Action "${action.actionId}" adapter step deadlineMs must be a positive finite integer.`);
42
+ }
16
43
  function assertActionId(actionId) {
17
44
  if (!/^[a-z][a-z0-9-]*\.[a-z][a-z0-9_]*$/.test(actionId)) throw new Error(`Invalid action ID: ${actionId}`);
18
45
  }
@@ -23,6 +50,8 @@ function createActionRegistry(definitions = []) {
23
50
  if (actions.has(definition.actionId)) throw new Error(`Action "${definition.actionId}" is already registered`);
24
51
  if (definition.mutatesDomain && definition.emitsEvents.length === 0) throw new Error(`Action "${definition.actionId}" mutates domain but emits no events. Every mutating action must emit at least one AssetEvent.`);
25
52
  if (definition.admitSubmission !== void 0 && typeof definition.admitSubmission !== "function") throw new Error(`Action "${definition.actionId}" declares admitSubmission that is not a function.`);
53
+ assertAdapterStepDeadlines(definition);
54
+ assertActionFinalization(definition);
26
55
  actions.set(definition.actionId, definition);
27
56
  };
28
57
  for (const definition of definitions) register(definition);
@@ -37,6 +66,6 @@ function createActionRegistry(definitions = []) {
37
66
  };
38
67
  }
39
68
  //#endregion
40
- export { ACTOR_TYPES, assertActionId, assertActorType, createActionRegistry, isActorType };
69
+ export { ACTOR_TYPES, assertActionFinalization, assertActionId, assertActorType, assertAdapterStepDeadlines, createActionRegistry, defineFinalizingAction, isActorType };
41
70
 
42
71
  //# sourceMappingURL=index.js.map