@fabricorg/platform 1.10.2 → 1.12.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 +44 -0
- package/README.md +76 -6
- package/dist/actions/index.cjs +32 -0
- package/dist/actions/index.cjs.map +1 -1
- package/dist/actions/index.d.cts +50 -0
- package/dist/actions/index.d.ts +50 -0
- package/dist/actions/index.js +30 -1
- package/dist/actions/index.js.map +1 -1
- package/dist/{engine-B4csEOEc.js → engine-CG-OKfsX.js} +12 -8
- package/dist/engine-CG-OKfsX.js.map +1 -0
- package/dist/{engine-BqScuhrT.cjs → engine-T6yBlzOE.cjs} +11 -7
- package/dist/engine-T6yBlzOE.cjs.map +1 -0
- package/dist/evidence/index.cjs +1 -1
- package/dist/evidence/index.d.cts +1 -1
- package/dist/evidence/index.d.ts +1 -1
- package/dist/evidence/index.js +1 -1
- package/dist/{evidence-BuCICb0c.js → evidence-Bf8dz-BP.js} +21 -10
- package/dist/evidence-Bf8dz-BP.js.map +1 -0
- package/dist/{evidence-Cq48eDvA.cjs → evidence-DPOImHJB.cjs} +21 -10
- package/dist/evidence-DPOImHJB.cjs.map +1 -0
- package/dist/governance/index.d.cts +1 -1
- package/dist/governance/index.d.ts +1 -1
- package/dist/{index-BnbsKZsT.d.cts → index-BIZIxaxe.d.cts} +16 -5
- package/dist/{index-BXpeFdk2.d.cts → index-Bc7UEt3o.d.cts} +9 -6
- package/dist/{index-BXpeFdk2.d.ts → index-Bc7UEt3o.d.ts} +9 -6
- package/dist/{index-z9-CYOIo.d.ts → index-BdW9UuCw.d.ts} +16 -5
- package/dist/index.cjs +5 -2
- package/dist/index.d.cts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +4 -4
- package/dist/manifest/index.cjs +13 -1
- package/dist/manifest/index.cjs.map +1 -1
- package/dist/manifest/index.d.cts +5 -0
- package/dist/manifest/index.d.ts +5 -0
- package/dist/manifest/index.js +13 -1
- package/dist/manifest/index.js.map +1 -1
- package/dist/modules/index.cjs +3 -0
- package/dist/modules/index.cjs.map +1 -1
- package/dist/modules/index.js +3 -0
- package/dist/modules/index.js.map +1 -1
- package/dist/privacy/index.cjs +9 -9
- package/dist/privacy/index.cjs.map +1 -1
- package/dist/privacy/index.d.cts +2 -2
- package/dist/privacy/index.d.ts +2 -2
- package/dist/privacy/index.js +9 -9
- package/dist/privacy/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/engine-B4csEOEc.js.map +0 -1
- package/dist/engine-BqScuhrT.cjs.map +0 -1
- package/dist/evidence-BuCICb0c.js.map +0 -1
- package/dist/evidence-Cq48eDvA.cjs.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,49 @@
|
|
|
1
1
|
# @fabricorg/platform
|
|
2
2
|
|
|
3
|
+
## 1.12.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 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.
|
|
8
|
+
|
|
9
|
+
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.
|
|
10
|
+
|
|
11
|
+
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.
|
|
12
|
+
|
|
13
|
+
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.
|
|
14
|
+
|
|
15
|
+
Failed, ambiguous, degraded and accepted/handoff outcomes do not invoke the hook. Existing actions retain their lifecycle.
|
|
16
|
+
|
|
17
|
+
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.
|
|
18
|
+
|
|
19
|
+
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.
|
|
20
|
+
|
|
21
|
+
- d5e7caa: Add optional adapter-step deadlineMs durations overriding the Host-wide duration,
|
|
22
|
+
including when the Host duration is unset or zero. Action and module registration, manifest export,
|
|
23
|
+
and manifest normalization reject step durations that are not positive finite integers.
|
|
24
|
+
Host validates resolved steps before handler or adapter execution. Invalid
|
|
25
|
+
durations fail a first execution without invoking adapters; retries with recorded
|
|
26
|
+
applied effects or unknown adapter outcomes require reconciliation instead.
|
|
27
|
+
Manifests preserve valid step durations.
|
|
28
|
+
|
|
29
|
+
Correct adapterDeadlineMs documentation: it is a duration starting just before
|
|
30
|
+
step retry execution, not an epoch timestamp. Host-wide behavior is unchanged.
|
|
31
|
+
Budgets span retries; expiry prevents a subsequent attempt and signals cooperative
|
|
32
|
+
cancellation of an in-flight call. An aborted call that throws retains the existing
|
|
33
|
+
ambiguous-outcome reconciliation semantics.
|
|
34
|
+
|
|
35
|
+
## 1.11.0
|
|
36
|
+
|
|
37
|
+
### Minor Changes
|
|
38
|
+
|
|
39
|
+
- a297fcd: Add optional `beforeChecksumKey` to AnonymizationEngine, the privacy wrappers, and evidence generation (issue #104). A supplied secret key produces HMAC-SHA256 field before-checksums; the default unkeyed SHA-256 output is unchanged.
|
|
40
|
+
|
|
41
|
+
Security note: an unkeyed checksum of a low-entropy value is reversible by enumeration, including after nullify or drop. Pass a secret key from application secret storage and retain it while checksum verification is needed. Keyed verification requires the key. Hash, tokenize, FieldRedactor and packet-level checksum algorithms are unchanged.
|
|
42
|
+
|
|
43
|
+
- 0a93df2: Add optional access-event reads to evidence packets (#101), so who-accessed records can live inside the redacted, checksummed payload. Without a reader the section is absent, leaving existing packets and checksums unchanged; an empty reader result includes an empty section. Schema version remains 1.
|
|
44
|
+
|
|
45
|
+
The new `access_event` reference type is an additive union member. Consumers with an exhaustive switch over `referenceType` must handle it. Reference metadata includes only string resource identifiers (`resourceType` and `resourceId`).
|
|
46
|
+
|
|
3
47
|
## 1.10.2
|
|
4
48
|
|
|
5
49
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -101,7 +101,7 @@ const json = JSON.stringify(manifest, null, 2);
|
|
|
101
101
|
|
|
102
102
|
`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
103
|
|
|
104
|
-
An entity schema does not validate arbitrary writes through `TDb`. Persisted-record validation requires an explicit Host/database adapter.
|
|
104
|
+
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
105
|
|
|
106
106
|
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
107
|
|
|
@@ -141,6 +141,26 @@ Every `invokeAction(id, params)` call goes through:
|
|
|
141
141
|
state-guard, adapter, or finalization failure semantics; see the
|
|
142
142
|
[Host boundary and rollback contract](../platform-host/README.md#atomic-mutation-unit-of-work).
|
|
143
143
|
|
|
144
|
+
Provider-dependent records can use the optional action-level `finalize` hook through
|
|
145
|
+
`defineFinalizingAction`. It requires an atomic, immediate `after_adapters` action;
|
|
146
|
+
idempotency is not required. Before adapters, Host normalizes the hook's parameters and
|
|
147
|
+
public handler data with `JSON.parse(JSON.stringify(value))`, then validates the normalized
|
|
148
|
+
JSON: Dates become ISO strings, undefined object properties drop, class instances use their
|
|
149
|
+
JSON representation, and non-finite numbers become null. Serialization failures such as
|
|
150
|
+
cycles or BigInt fail before adapter execution. Host strips private `eventResultFields`
|
|
151
|
+
from hook handler data; handler events are normalized separately. The hook receives copies
|
|
152
|
+
of those saved inputs and ordered succeeded/skipped stored redacted adapter outputs, plus
|
|
153
|
+
transaction-bound `db` without services. Hook event schema versions default to
|
|
154
|
+
`action.version` when omitted.
|
|
155
|
+
It returns declared events and/or a shallow result patch. The manifest exports only
|
|
156
|
+
`finalization: { protocol: "atomic-after-adapters-v1" }`, not the callback.
|
|
157
|
+
The helper refuses older hosts before handler code runs. See the
|
|
158
|
+
[Host finalization contract](../platform-host/README.md#action-level-finalization) for
|
|
159
|
+
transaction requirements, validation, rollback and reconciliation semantics.
|
|
160
|
+
With a compatible durable preparation checkpoint, Host recovery reuses recorded outputs
|
|
161
|
+
and replays finalization without rerunning the handler. Missing or incompatible evidence
|
|
162
|
+
retains effect-aware refusal; see the Host finalization contract for rollout and limits.
|
|
163
|
+
|
|
144
164
|
4. **Adapters** — for each `actionDef.adapterSteps`, the registered implementation runs via
|
|
145
165
|
`executeWithAdapterRetry` (idempotent retry, exponential backoff, and an optional per-adapter
|
|
146
166
|
circuit breaker; see [Adapter circuit breakers](#adapter-circuit-breakers)). Adapters may
|
|
@@ -319,11 +339,12 @@ import {
|
|
|
319
339
|
|
|
320
340
|
const port: EvidencePacketReadPort = {
|
|
321
341
|
listEvents: ({ tenantId, spaceId, subjects }) => store.eventsFor(tenantId, spaceId, subjects),
|
|
322
|
-
// Optional
|
|
342
|
+
// Optional readers below; access events are omitted when their reader is absent.
|
|
323
343
|
resolveRelatedSubjects: (subject, scope) => store.relatedSubjects(subject, scope),
|
|
324
344
|
listActionInvocations: (scope, ids) => store.actionInvocations(scope, ids),
|
|
325
345
|
listPolicyEvaluations: (scope, invocationIds) => store.policyEvaluations(scope, invocationIds),
|
|
326
346
|
listAdapterInvocations: (scope, invocationIds) => store.adapterInvocations(scope, invocationIds),
|
|
347
|
+
listAccessEvents: (scope, subjects) => store.accessEvents(scope, subjects),
|
|
327
348
|
};
|
|
328
349
|
|
|
329
350
|
const packet = await generateEvidencePacket(
|
|
@@ -338,8 +359,20 @@ await store.savePacket(packet);
|
|
|
338
359
|
verifyEvidencePacketChecksum(packet); // true until the payload is altered
|
|
339
360
|
```
|
|
340
361
|
|
|
362
|
+
The optional `listAccessEvents(scope, subjects)` reader receives the resolved subjects (primary
|
|
363
|
+
first, then related), even when there are no events or invocation ids. The generator omits
|
|
364
|
+
`payload.accessEvents` when the reader is absent, preserving existing payloads and checksums;
|
|
365
|
+
a reader returning `[]` includes `accessEvents: []`. The port selects the relevant access records;
|
|
366
|
+
the generator does not filter them by subject. Records require `id`, `tenantId` and `spaceId`
|
|
367
|
+
and pass through the same scope checks, JSON normalization, de-duplication, id sorting and
|
|
368
|
+
redaction as other sections. References use `access_event` with only string `resourceType`
|
|
369
|
+
and `resourceId` metadata from the redacted record. Schema version remains 1.
|
|
370
|
+
|
|
341
371
|
What the tests in `evidence/generator.test.ts` cover:
|
|
342
372
|
|
|
373
|
+
- **Access events.** Tests cover absent versus empty sections, the pinned legacy checksum,
|
|
374
|
+
copied scope and subjects, scope rejection, sorting, duplicate conflicts, redaction and
|
|
375
|
+
identifier/section preservation.
|
|
343
376
|
- **Scope fails closed.** A record from the port whose `tenantId` or `spaceId` differs from the context
|
|
344
377
|
throws `EvidencePacketGenerationError` with code `EVIDENCE_SCOPE_MISMATCH`, in every section. The
|
|
345
378
|
check runs on the record as returned and again after normalization. A dependent record for an action
|
|
@@ -399,10 +432,15 @@ What the tests in `evidence/generator.test.ts` cover:
|
|
|
399
432
|
stored as a key (`{ byEmail: { "jane@example.com": 1 } }`) is not redacted.
|
|
400
433
|
- Envelope fields copied from the context (`generatedBy`, `correlationId`, `causationId`) and from
|
|
401
434
|
the input (`policyVersion`) are not redacted.
|
|
402
|
-
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
435
|
+
- Supply `anonymization.beforeChecksumKey` (a non-empty string or `Uint8Array`) to use
|
|
436
|
+
HMAC-SHA256 for field `beforeChecksum` values. Without a key, the default remains unkeyed
|
|
437
|
+
SHA-256 of `JSON.stringify(value)`: low-entropy values can be recovered by enumeration,
|
|
438
|
+
including with `nullify` or `drop`. Supply a secret key from the application's secret store
|
|
439
|
+
and keep it stable while any party needs to verify checksums; keyed verification requires
|
|
440
|
+
the key. The generator passes it only to the engine, outside the packet context.
|
|
441
|
+
Existing `hash` output remains unsalted SHA-256, and the tenant, space, field path and policy
|
|
442
|
+
version used by `tokenize` are in the packet. Those outputs still permit candidate guessing;
|
|
443
|
+
use opaque references where appropriate.
|
|
406
444
|
- **Checksum.** `checksum` is the hex SHA-256 of `canonicalJson(payload)`, recorded with
|
|
407
445
|
`checksumAlgorithm: EVIDENCE_PACKET_CHECKSUM_ALGORITHM`. `verifyEvidencePacketChecksum` returns `false`
|
|
408
446
|
for a changed payload, a missing checksum or an unknown algorithm. The checksum covers the payload,
|
|
@@ -494,6 +532,38 @@ retain internal contracts for trusted assembly and worker integration; none impl
|
|
|
494
532
|
catalog disclosure. A usage contract or generated schema must not
|
|
495
533
|
be served as an audience catalog without the internal-surface and audience filters above.
|
|
496
534
|
|
|
535
|
+
## Privacy engine
|
|
536
|
+
|
|
537
|
+
The engine requires an application-owned field policy. Configure field before-checksums with
|
|
538
|
+
the optional second constructor argument, exported as `AnonymizationEngineOptions`:
|
|
539
|
+
|
|
540
|
+
```ts
|
|
541
|
+
const engine = new AnonymizationEngine(policy, { beforeChecksumKey: secretFromStore });
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
`beforeChecksumKey` accepts a non-empty string or `Uint8Array`; invalid or empty keys throw
|
|
545
|
+
`TypeError`. The engine stores the key privately and copies byte keys at construction.
|
|
546
|
+
With a key, `beforeChecksum` is lowercase hex HMAC-SHA256 of `JSON.stringify(value)`.
|
|
547
|
+
Without a key it retains the existing unkeyed SHA-256, which reveals low-entropy values by
|
|
548
|
+
enumeration even when the field is nullified or dropped. Use a secret from the application's
|
|
549
|
+
secret store, keep it stable while verification is needed, and provide it to authorized verifiers.
|
|
550
|
+
Verification of a keyed checksum requires the key.
|
|
551
|
+
|
|
552
|
+
The four standalone privacy wrappers accept the same options as a trailing argument.
|
|
553
|
+
Lifecycle evidence copies the supplied metadata's checksums; its options do not re-key
|
|
554
|
+
previously generated metadata. This option does not change `FieldRedactor`, `hash`,
|
|
555
|
+
`tokenize`, `stableChecksum`, or packet-level checksum algorithms.
|
|
556
|
+
|
|
557
|
+
## Adapter step deadlines
|
|
558
|
+
|
|
559
|
+
Adapter steps may declare `deadlineMs`, a positive finite integer duration in
|
|
560
|
+
milliseconds. Platform Host uses it instead of its `adapterDeadlineMs` duration for
|
|
561
|
+
that step. The budget begins just before retry execution and spans attempts and
|
|
562
|
+
retry delays. Action and module registration, manifest normalization and export,
|
|
563
|
+
and Host execution validate step durations. Host rejects invalid resolved steps
|
|
564
|
+
before handler or adapter execution. Manifest export preserves valid durations.
|
|
565
|
+
See the Platform Host README for cooperative cancellation and failure semantics.
|
|
566
|
+
|
|
497
567
|
## License
|
|
498
568
|
|
|
499
569
|
MIT
|
package/dist/actions/index.cjs
CHANGED
|
@@ -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\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 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}\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/** 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;;AAyIA,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;AAmJA,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"}
|
package/dist/actions/index.d.cts
CHANGED
|
@@ -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
|
|
@@ -112,6 +113,47 @@ export interface ActionResult {
|
|
|
112
113
|
error?: string;
|
|
113
114
|
}
|
|
114
115
|
export type ActionHandler<TDb = unknown> = (ctx: ActionContext<TDb>, params: unknown) => Promise<ActionResult>;
|
|
116
|
+
export type FinalizationJson = null | boolean | number | string | readonly FinalizationJson[] | {
|
|
117
|
+
readonly [key: string]: FinalizationJson;
|
|
118
|
+
};
|
|
119
|
+
export type FinalizationRecord = {
|
|
120
|
+
readonly [key: string]: FinalizationJson;
|
|
121
|
+
};
|
|
122
|
+
export type ActionFinalizationContext<TDb = unknown> = Pick<ActionContext<TDb>, "actionInvocationId" | "tenantId" | "spaceId" | "actorId" | "actorType" | "correlationId" | "causationId" | "provenance" | "db">;
|
|
123
|
+
export type ActionFinalizationStep = {
|
|
124
|
+
readonly stepIndex: number;
|
|
125
|
+
readonly adapterType: string;
|
|
126
|
+
readonly operation: string;
|
|
127
|
+
} & ({
|
|
128
|
+
readonly status: "succeeded";
|
|
129
|
+
readonly adapterInvocationId: string;
|
|
130
|
+
readonly output: FinalizationRecord;
|
|
131
|
+
} | {
|
|
132
|
+
readonly status: "skipped";
|
|
133
|
+
});
|
|
134
|
+
export interface ActionFinalizationInput {
|
|
135
|
+
readonly params: FinalizationJson;
|
|
136
|
+
readonly handlerData: FinalizationRecord;
|
|
137
|
+
readonly adapterOutputs: readonly ActionFinalizationStep[];
|
|
138
|
+
}
|
|
139
|
+
export interface ActionFinalizationEvent {
|
|
140
|
+
readonly eventType: string;
|
|
141
|
+
readonly subjectType: string;
|
|
142
|
+
readonly subjectId: string;
|
|
143
|
+
readonly payload: FinalizationJson;
|
|
144
|
+
readonly eventSchemaVersion?: number;
|
|
145
|
+
}
|
|
146
|
+
export interface ActionFinalizationResult {
|
|
147
|
+
readonly events?: readonly ActionFinalizationEvent[];
|
|
148
|
+
readonly resultPatch?: FinalizationRecord;
|
|
149
|
+
}
|
|
150
|
+
export type ActionFinalizer<TDb = unknown> = (ctx: ActionFinalizationContext<TDb>, input: ActionFinalizationInput) => Promise<ActionFinalizationResult>;
|
|
151
|
+
/** Validate declarations at registration and at dynamically resolved execution boundaries. */
|
|
152
|
+
export declare function assertActionFinalization<TDb>(action: Pick<ActionDefinition<TDb>, "actionId" | "finalize" | "eventPhase" | "kind" | "execution">): void;
|
|
153
|
+
/** Refuse older hosts before application handler code executes. Use for distributed finalizing actions. */
|
|
154
|
+
export declare function defineFinalizingAction<TDb>(action: ActionDefinition<TDb> & {
|
|
155
|
+
finalize: ActionFinalizer<TDb>;
|
|
156
|
+
}): ActionDefinition<TDb>;
|
|
115
157
|
export interface AdapterStepRetryPolicy {
|
|
116
158
|
idempotent: boolean;
|
|
117
159
|
maxAttempts?: number;
|
|
@@ -123,8 +165,15 @@ export interface AdapterStep {
|
|
|
123
165
|
adapterType: string;
|
|
124
166
|
operation: string;
|
|
125
167
|
retryPolicy?: AdapterStepRetryPolicy;
|
|
168
|
+
/** Positive finite integer duration in milliseconds; overrides Host adapterDeadlineMs for this step. */
|
|
169
|
+
deadlineMs?: number;
|
|
126
170
|
getInput: (params: unknown, handlerResult: Record<string, unknown>) => Record<string, unknown> | undefined;
|
|
127
171
|
}
|
|
172
|
+
/** Validate optional step execution budgets before accepting or executing an action. */
|
|
173
|
+
export declare function assertAdapterStepDeadlines(action: {
|
|
174
|
+
actionId: string;
|
|
175
|
+
adapterSteps?: readonly Pick<AdapterStep, "deadlineMs">[];
|
|
176
|
+
}): void;
|
|
128
177
|
export interface ActionStateMachineBinding {
|
|
129
178
|
entityType: string;
|
|
130
179
|
targetState: string | ((params: unknown) => string);
|
|
@@ -148,6 +197,7 @@ export interface ActionDefinition<TDb = unknown> extends SurfaceDeclaration {
|
|
|
148
197
|
/** Optional validation contract for the public handler result data. */
|
|
149
198
|
resultSchema?: SchemaValidator;
|
|
150
199
|
handler?: ActionHandler<TDb>;
|
|
200
|
+
finalize?: ActionFinalizer<TDb>;
|
|
151
201
|
requiredRoles?: string[];
|
|
152
202
|
requiredPermissions?: string[];
|
|
153
203
|
policies?: `${string}.v${number}`[];
|
package/dist/actions/index.d.ts
CHANGED
|
@@ -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
|
|
@@ -112,6 +113,47 @@ export interface ActionResult {
|
|
|
112
113
|
error?: string;
|
|
113
114
|
}
|
|
114
115
|
export type ActionHandler<TDb = unknown> = (ctx: ActionContext<TDb>, params: unknown) => Promise<ActionResult>;
|
|
116
|
+
export type FinalizationJson = null | boolean | number | string | readonly FinalizationJson[] | {
|
|
117
|
+
readonly [key: string]: FinalizationJson;
|
|
118
|
+
};
|
|
119
|
+
export type FinalizationRecord = {
|
|
120
|
+
readonly [key: string]: FinalizationJson;
|
|
121
|
+
};
|
|
122
|
+
export type ActionFinalizationContext<TDb = unknown> = Pick<ActionContext<TDb>, "actionInvocationId" | "tenantId" | "spaceId" | "actorId" | "actorType" | "correlationId" | "causationId" | "provenance" | "db">;
|
|
123
|
+
export type ActionFinalizationStep = {
|
|
124
|
+
readonly stepIndex: number;
|
|
125
|
+
readonly adapterType: string;
|
|
126
|
+
readonly operation: string;
|
|
127
|
+
} & ({
|
|
128
|
+
readonly status: "succeeded";
|
|
129
|
+
readonly adapterInvocationId: string;
|
|
130
|
+
readonly output: FinalizationRecord;
|
|
131
|
+
} | {
|
|
132
|
+
readonly status: "skipped";
|
|
133
|
+
});
|
|
134
|
+
export interface ActionFinalizationInput {
|
|
135
|
+
readonly params: FinalizationJson;
|
|
136
|
+
readonly handlerData: FinalizationRecord;
|
|
137
|
+
readonly adapterOutputs: readonly ActionFinalizationStep[];
|
|
138
|
+
}
|
|
139
|
+
export interface ActionFinalizationEvent {
|
|
140
|
+
readonly eventType: string;
|
|
141
|
+
readonly subjectType: string;
|
|
142
|
+
readonly subjectId: string;
|
|
143
|
+
readonly payload: FinalizationJson;
|
|
144
|
+
readonly eventSchemaVersion?: number;
|
|
145
|
+
}
|
|
146
|
+
export interface ActionFinalizationResult {
|
|
147
|
+
readonly events?: readonly ActionFinalizationEvent[];
|
|
148
|
+
readonly resultPatch?: FinalizationRecord;
|
|
149
|
+
}
|
|
150
|
+
export type ActionFinalizer<TDb = unknown> = (ctx: ActionFinalizationContext<TDb>, input: ActionFinalizationInput) => Promise<ActionFinalizationResult>;
|
|
151
|
+
/** Validate declarations at registration and at dynamically resolved execution boundaries. */
|
|
152
|
+
export declare function assertActionFinalization<TDb>(action: Pick<ActionDefinition<TDb>, "actionId" | "finalize" | "eventPhase" | "kind" | "execution">): void;
|
|
153
|
+
/** Refuse older hosts before application handler code executes. Use for distributed finalizing actions. */
|
|
154
|
+
export declare function defineFinalizingAction<TDb>(action: ActionDefinition<TDb> & {
|
|
155
|
+
finalize: ActionFinalizer<TDb>;
|
|
156
|
+
}): ActionDefinition<TDb>;
|
|
115
157
|
export interface AdapterStepRetryPolicy {
|
|
116
158
|
idempotent: boolean;
|
|
117
159
|
maxAttempts?: number;
|
|
@@ -123,8 +165,15 @@ export interface AdapterStep {
|
|
|
123
165
|
adapterType: string;
|
|
124
166
|
operation: string;
|
|
125
167
|
retryPolicy?: AdapterStepRetryPolicy;
|
|
168
|
+
/** Positive finite integer duration in milliseconds; overrides Host adapterDeadlineMs for this step. */
|
|
169
|
+
deadlineMs?: number;
|
|
126
170
|
getInput: (params: unknown, handlerResult: Record<string, unknown>) => Record<string, unknown> | undefined;
|
|
127
171
|
}
|
|
172
|
+
/** Validate optional step execution budgets before accepting or executing an action. */
|
|
173
|
+
export declare function assertAdapterStepDeadlines(action: {
|
|
174
|
+
actionId: string;
|
|
175
|
+
adapterSteps?: readonly Pick<AdapterStep, "deadlineMs">[];
|
|
176
|
+
}): void;
|
|
128
177
|
export interface ActionStateMachineBinding {
|
|
129
178
|
entityType: string;
|
|
130
179
|
targetState: string | ((params: unknown) => string);
|
|
@@ -148,6 +197,7 @@ export interface ActionDefinition<TDb = unknown> extends SurfaceDeclaration {
|
|
|
148
197
|
/** Optional validation contract for the public handler result data. */
|
|
149
198
|
resultSchema?: SchemaValidator;
|
|
150
199
|
handler?: ActionHandler<TDb>;
|
|
200
|
+
finalize?: ActionFinalizer<TDb>;
|
|
151
201
|
requiredRoles?: string[];
|
|
152
202
|
requiredPermissions?: string[];
|
|
153
203
|
policies?: `${string}.v${number}`[];
|
package/dist/actions/index.js
CHANGED
|
@@ -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
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","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.js","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\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 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}\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/** 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;;AAyIA,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;AAmJA,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"}
|