@fabricorg/platform-host 2.0.1 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # @fabricorg/platform-host
2
2
 
3
+ ## 3.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - Deliver Phase 1 correctness and contracts: payload-bound idempotency with staged enforcement,
8
+ capture/replay authority evidence, bounded invocation provenance, capability execution and view usage
9
+ semantics, event-schema compatibility classification, and deterministic conformance plans.
10
+ - Default payload-bound conflicts to enforcement in Host 3 and return existing nonterminal invocations
11
+ without redispatch; dispatcher deployments must use the documented store-backed recovery worker.
12
+
13
+ ### Patch Changes
14
+
15
+ - Updated dependencies
16
+ - @fabricorg/platform@0.12.0
17
+
3
18
  ## 2.0.1
4
19
 
5
20
  ### Patch Changes
@@ -0,0 +1,32 @@
1
+ # Payload-bound idempotency rollout
2
+
3
+ `ensureSchema()` adds nullable parameter-digest, actor/binding, provenance, authority, and reconciliation
4
+ columns. Existing rows remain valid. New idempotent submissions always record
5
+ `fabric-canonical-json-sha256-v1`.
6
+
7
+ 1. Before adopting Host 3's enforcement default, deploy with `conflictMode: "audit-only"` and capture `onConflict`/`onLegacyRecord` metrics.
8
+ 2. Soak until modified-payload key reuse has been removed from callers.
9
+ 3. Opt selected applications into `conflictMode: "enforce"`; the same key with different command
10
+ identity now throws `IDEMPOTENCY_CONFLICT`.
11
+ 4. Remove the override after upgrading to Host 3; enforcement is its default.
12
+
13
+ Rollback to audit-only is configuration-only. Do not drop digest columns during rollback; they are
14
+ additive evidence and permit enforcement to resume without rebuilding history. Legacy null-digest rows
15
+ continue old-key payload recovery because their original payload identity cannot be reconstructed safely.
16
+ Host 3 still rejects actor, authority-binding, and action-version conflicts for those rows; review these
17
+ identities during the audit-only soak before enabling enforcement.
18
+
19
+ The additive `reconciliation_required` action status is terminal in Phase 1 and requires an operator-
20
+ owned reconciliation path; exhaustive `ActionStatus` consumers must handle it. Custom stores must
21
+ persist the widened invocation fields, including `parameterDigest`, actor/authority binding, provenance,
22
+ and `authorizationReconciliation`. A store that drops them cannot claim payload-bound enforcement or
23
+ durable reconciliation evidence.
24
+
25
+ An action-version change is part of command identity. A retry using a key first recorded under a prior
26
+ action version is reported in audit-only mode and rejected in enforce mode; drain or retain the prior
27
+ action version for the retry window before deploying a breaking action revision.
28
+
29
+ Duplicate submission now returns an existing nonterminal invocation without redispatching it, which
30
+ prevents concurrent callers from driving the same mutation twice. Production asynchronous deployments
31
+ must run the store-backed worker/recovery loop; a dispatcher-only deployment cannot use client retry as
32
+ recovery for a crash between durable creation and dispatch.
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  The canonical host for the `@fabricorg/platform` mutation pipeline.
4
4
 
5
5
  ```bash
6
- pnpm add @fabricorg/platform@^0.11.0 @fabricorg/platform-host@^2.0.0
6
+ pnpm add @fabricorg/platform@^0.12.0 @fabricorg/platform-host@^3.0.0
7
7
  ```
8
8
 
9
9
  ## AI agent integration boundary
@@ -15,7 +15,7 @@ registration, and submit stable idempotent commands. The gateway derives tenant
15
15
  calls `submitAction()`; an agent must never call handlers, adapters, workflow internals, or database
16
16
  writes directly.
17
17
 
18
- See the [Platform Host 2.x agent integration
18
+ See the [Platform Host 3.x agent integration
19
19
  guide](https://platform.fabric.pro/docs/platform/reference/platform-host) for application wiring,
20
20
  execution-time authorization, the PostgreSQL transaction binder, recovery semantics, and the external
21
21
  gateway contract.
@@ -45,6 +45,12 @@ bounded batches with an atomic lease and `FOR UPDATE SKIP LOCKED`, and an expire
45
45
  recoverable after interruption. Pass a stable `idempotencyKey` to `submitAction()` so retries resolve
46
46
  to the original tenant-scoped invocation instead of creating another mutation.
47
47
 
48
+ New rows bind that key to the action version, actor/authority binding, and a versioned canonical
49
+ parameter digest. Host 3 defaults to `enforce`, which throws `IdempotencyConflictError`; configure
50
+ `idempotency.conflictMode` as `audit-only` for a preflight soak. Legacy null-digest rows skip only the
51
+ parameter comparison and call `onLegacyRecord`; actor, authority-binding, and action-version mismatches
52
+ still reject in enforce mode. See `MIGRATION-2-IDEMPOTENCY.md`.
53
+
48
54
  Lifecycle records use deterministic checkpoint ids. Recovered idempotent actions do not repeat an
49
55
  adapter that already reached `succeeded`, and event, policy, and adapter writes are append-safe. A
50
56
  stale action that was not declared idempotent fails terminally for manual reconciliation instead of
@@ -58,6 +64,20 @@ schema-parsed durable parameters, canonical invocation, opaque `authorizationBin
58
64
  `executionReason` of `initial`, `approval_resume`, or `recovery` immediately before policies and
59
65
  mutation code run.
60
66
 
67
+ `offline_replay` is the extensible evidence value for disconnected commands. Actions with execution
68
+ semantics can declare whether capture, execution, or both govern authority. A denied governed replay
69
+ returns structured `reconciliation_required` evidence.
70
+
71
+ For `execution` and `both`, the Host rejects an expired `AuthorizationBinding` before calling mutation
72
+ code and returns `authorization_expired` reconciliation evidence. `resourceScope` remains an opaque,
73
+ audit-safe scope identifier; the application-owned `authorizeExecution` implementation resolves and
74
+ checks it against current resource state.
75
+
76
+ `InvocationProvenance` carries namespaced trace and audit attributes. Durable audit attributes fail
77
+ closed when no allowlist is configured, pass through `redactAuditAttributes`, and remain bounded.
78
+ Trace attributes go only to the configured trace callback. Restricted outbox payloads omit provenance
79
+ by default; `outbox.includeProvenance` is the explicit classification-aware override.
80
+
61
81
  ```ts
62
82
  const host = createGovernedActionHost({
63
83
  store,
@@ -178,7 +198,7 @@ explain which contract and provider adapter governed a mutation after dependenci
178
198
  const host = createGovernedActionHost({
179
199
  // ...
180
200
  runtimeEvidence: {
181
- hostPackageVersion: "2.0.1",
201
+ hostPackageVersion: "3.0.0",
182
202
  policyRulesetVersion: "gtm-rules.v8",
183
203
  providerBridge: { name: "@fabric-harness/databricks", version: "1" },
184
204
  },
package/dist/index.cjs CHANGED
@@ -1,11 +1,68 @@
1
1
  'use strict';
2
2
 
3
3
  var platform = require('@fabricorg/platform');
4
+ var crypto = require('crypto');
4
5
 
5
6
  // src/host.ts
6
7
 
7
8
  // src/types.ts
8
9
  var PLATFORM_HOST_CONTRACT_VERSION = 2;
10
+ var IdempotencyConflictError = class extends Error {
11
+ constructor(conflict) {
12
+ super(`Idempotency key "${conflict.idempotencyKey}" conflicts with invocation ${conflict.existingInvocationId}: ${conflict.reasons.join(", ")}`);
13
+ this.conflict = conflict;
14
+ this.name = "IdempotencyConflictError";
15
+ }
16
+ conflict;
17
+ code = "IDEMPOTENCY_CONFLICT";
18
+ };
19
+ var PARAMETER_DIGEST_ALGORITHM = "fabric-canonical-json-sha256-v1";
20
+ function canonicalJson(value) {
21
+ return JSON.stringify(sort(value));
22
+ }
23
+ function sort(value) {
24
+ if (Array.isArray(value)) return value.map(sort);
25
+ if (value && typeof value === "object") {
26
+ return Object.fromEntries(Object.entries(value).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([key, child]) => [key, sort(child)]));
27
+ }
28
+ return value;
29
+ }
30
+ function digestParameters(parameters) {
31
+ return crypto.createHash("sha256").update(canonicalJson(parameters)).digest("hex");
32
+ }
33
+ var ATTRIBUTE_NAME = /^[a-z][a-z0-9-]*(?:\.[A-Za-z][A-Za-z0-9_-]*)+$/;
34
+ var INVOCATION_SOURCES = /* @__PURE__ */ new Set(["sdui", "api", "agent", "worker", "system"]);
35
+ function normalizeProvenance(input, options) {
36
+ if (!input) return void 0;
37
+ if (!INVOCATION_SOURCES.has(input.source)) throw new Error(`Invalid invocation provenance source: ${input.source}`);
38
+ const maxAttributes = options?.maxAttributes ?? 16;
39
+ const maxValueLength = options?.maxValueLength ?? 256;
40
+ const allowlist = new Set(options?.auditAttributeAllowlist ?? []);
41
+ const validate = (attributes, durable) => {
42
+ if (!attributes) return void 0;
43
+ const entries = Object.entries(attributes);
44
+ if (entries.length > maxAttributes) throw new Error(`Invocation provenance exceeds ${maxAttributes} attributes.`);
45
+ for (const [key, value] of entries) {
46
+ if (!ATTRIBUTE_NAME.test(key)) throw new Error(`Invalid provenance attribute name: ${key}`);
47
+ if (value.length > maxValueLength) throw new Error(`Invocation provenance attribute "${key}" exceeds ${maxValueLength} characters.`);
48
+ if (durable && !allowlist.has(key)) throw new Error(`Audit provenance attribute "${key}" is not allowlisted.`);
49
+ }
50
+ return Object.fromEntries(entries);
51
+ };
52
+ validate(input.traceAttributes, false);
53
+ const redactedAuditAttributes = input.auditAttributes ? options?.redactAuditAttributes?.({ ...input.auditAttributes }) ?? input.auditAttributes : void 0;
54
+ const auditAttributes = validate(redactedAuditAttributes, true);
55
+ options?.onTrace?.({
56
+ ...input,
57
+ ...auditAttributes ? { auditAttributes } : { auditAttributes: void 0 }
58
+ });
59
+ return {
60
+ source: input.source,
61
+ correlationId: input.correlationId,
62
+ ...input.causationId ? { causationId: input.causationId } : {},
63
+ ...auditAttributes ? { auditAttributes } : {}
64
+ };
65
+ }
9
66
 
10
67
  // src/host.ts
11
68
  var DEFAULT_EXTRACT_EVENTS = (data) => {
@@ -29,6 +86,9 @@ function createGovernedActionHost(options) {
29
86
  const actionResolver = options.resolveAction ?? platform.resolveAction;
30
87
  const eventResultFields = options.eventResultFields ?? ["_events"];
31
88
  async function submitAction(input) {
89
+ if (input.executionReason && !["initial", "offline_replay"].includes(input.executionReason)) {
90
+ throw new Error(`Unsupported submitted execution reason: ${input.executionReason}`);
91
+ }
32
92
  const action = actionResolver(input.actionId);
33
93
  if (!action) throw new Error(`Unknown action: ${input.actionId}`);
34
94
  const authorizationInput = toAuthorizationInput(action, input);
@@ -39,8 +99,16 @@ function createGovernedActionHost(options) {
39
99
  throw new Error(`Actor ${input.actorId} is not authorized for action ${input.actionId}`);
40
100
  }
41
101
  const actionInvocationId = platform.createFabricId("act");
42
- const correlationId = input.correlationId ?? platform.createFabricId("corr");
102
+ if (input.correlationId && input.provenance?.correlationId && input.correlationId !== input.provenance.correlationId) throw new Error("Invocation provenance correlationId does not match submission correlationId.");
103
+ if (input.causationId && input.provenance?.causationId && input.causationId !== input.provenance.causationId) throw new Error("Invocation provenance causationId does not match submission causationId.");
104
+ const correlationId = input.correlationId ?? input.provenance?.correlationId ?? platform.createFabricId("corr");
43
105
  const durableParameters = options.redactActionParameters ? options.redactActionParameters(input.actionId, input.parameters) : input.parameters;
106
+ const parameterDigest = digestParameters(input.parameters);
107
+ if (input.authorizationBinding) validateAuthorizationBinding(input, parameterDigest, action.execution?.authorityMoment, now());
108
+ const durableProvenance = normalizeProvenance(
109
+ input.provenance,
110
+ options.provenance
111
+ );
44
112
  const runtimeEvidence = {
45
113
  governanceContractVersion: platform.FABRIC_GOVERNANCE_CONTRACT_VERSION,
46
114
  hostContractVersion: PLATFORM_HOST_CONTRACT_VERSION,
@@ -60,12 +128,41 @@ function createGovernedActionHost(options) {
60
128
  result: {},
61
129
  runtimeEvidence,
62
130
  correlationId,
63
- ...input.causationId ? { causationId: input.causationId } : {},
131
+ ...input.causationId ?? input.provenance?.causationId ? { causationId: input.causationId ?? input.provenance?.causationId } : {},
64
132
  ...input.idempotencyKey ? { idempotencyKey: input.idempotencyKey } : {},
133
+ ...input.idempotencyKey ? {
134
+ parameterDigest,
135
+ parameterDigestAlgorithm: PARAMETER_DIGEST_ALGORITHM,
136
+ idempotencyActorId: input.actorId,
137
+ ...input.authorizationBindingId ? { idempotencyAuthorizationBindingId: input.authorizationBindingId } : {}
138
+ } : {},
139
+ ...durableProvenance ? { provenance: durableProvenance } : {},
140
+ ...input.authorizationBinding ? { authorizationBinding: input.authorizationBinding } : {},
141
+ ...input.executionReason ? { executionReason: input.executionReason } : {},
65
142
  ...input.authorizationBindingId ? { authorizationBindingId: input.authorizationBindingId } : {}
66
143
  });
144
+ if (durableInvocation.id !== actionInvocationId && input.idempotencyKey) {
145
+ const conflict = idempotencyConflict(durableInvocation, {
146
+ actorId: input.actorId,
147
+ authorizationBindingId: input.authorizationBindingId,
148
+ actionVersion: action.version,
149
+ parameterDigest,
150
+ idempotencyKey: input.idempotencyKey
151
+ });
152
+ if (!durableInvocation.parameterDigest) {
153
+ options.idempotency?.onLegacyRecord?.(durableInvocation);
154
+ const enforceableLegacyConflict = conflict && { ...conflict, reasons: conflict.reasons.filter((reason) => reason !== "parameters") };
155
+ if (enforceableLegacyConflict && enforceableLegacyConflict.reasons.length) {
156
+ options.idempotency?.onConflict?.(enforceableLegacyConflict);
157
+ if ((options.idempotency?.conflictMode ?? "enforce") === "enforce") throw new IdempotencyConflictError(enforceableLegacyConflict);
158
+ }
159
+ } else if (conflict) {
160
+ options.idempotency?.onConflict?.(conflict);
161
+ if ((options.idempotency?.conflictMode ?? "enforce") === "enforce") throw new IdempotencyConflictError(conflict);
162
+ }
163
+ }
67
164
  const durableWorkflowId = `action-invocation-${durableInvocation.id}`;
68
- if (durableInvocation.id !== actionInvocationId && isTerminal(durableInvocation.status)) {
165
+ if (durableInvocation.id !== actionInvocationId) {
69
166
  return {
70
167
  actionInvocationId: durableInvocation.id,
71
168
  status: durableInvocation.status,
@@ -73,7 +170,8 @@ function createGovernedActionHost(options) {
73
170
  result: durableInvocation.result,
74
171
  ...durableInvocation.error ? { error: durableInvocation.error } : {},
75
172
  ...durableInvocation.hitlRoute ? { hitlRoute: durableInvocation.hitlRoute } : {},
76
- ...durableInvocation.hitlRiskTier ? { hitlRiskTier: durableInvocation.hitlRiskTier } : {}
173
+ ...durableInvocation.hitlRiskTier ? { hitlRiskTier: durableInvocation.hitlRiskTier } : {},
174
+ ...durableInvocation.authorizationReconciliation ? { reconciliation: durableInvocation.authorizationReconciliation } : {}
77
175
  };
78
176
  }
79
177
  if (options.dispatcher) {
@@ -221,7 +319,7 @@ function createGovernedActionHost(options) {
221
319
  }
222
320
  }
223
321
  const authorizationInput = toAuthorizationInput(action, invocation);
224
- const executionReason = executionReasonOverride ?? (invocation.attemptCount > 1 || resumingRunningInvocation && invocation.attemptCount === 0 ? "recovery" : "initial");
322
+ const executionReason = executionReasonOverride ?? (invocation.attemptCount > 1 || resumingRunningInvocation && invocation.attemptCount === 0 ? "recovery" : invocation.executionReason ?? "initial");
225
323
  if (!await options.authorization.checkEntitlement(authorizationInput)) {
226
324
  return fail(
227
325
  invocation,
@@ -229,7 +327,32 @@ function createGovernedActionHost(options) {
229
327
  `Module "${action.namespace}" is no longer enabled for tenant ${tenantId}`
230
328
  );
231
329
  }
232
- const executionAuthorized = options.authorization.authorizeExecution ? await options.authorization.authorizeExecution({
330
+ const authorityMoment = action.execution?.authorityMoment;
331
+ if (authorityMoment === "capture" && !invocation.authorizationBinding) {
332
+ const reconciliation = {
333
+ kind: "authorization_missing_capture_evidence",
334
+ governingMoment: authorityMoment,
335
+ executionReason,
336
+ ...invocation.provenance ? { provenance: invocation.provenance } : {},
337
+ message: `Action ${action.actionId} requires durable capture-time authorization evidence`
338
+ };
339
+ await options.store.updateActionInvocation(actionInvocationId, tenantId, spaceId, { status: "reconciliation_required", error: reconciliation.message, authorizationReconciliation: reconciliation });
340
+ return { actionInvocationId, status: "reconciliation_required", error: reconciliation.message, reconciliation };
341
+ }
342
+ const bindingExpired = authorityMoment !== "capture" && invocation.authorizationBinding?.expiresAt !== void 0 && Date.parse(invocation.authorizationBinding.expiresAt) <= now().getTime();
343
+ if (bindingExpired) {
344
+ const reconciliation = {
345
+ kind: "authorization_expired",
346
+ governingMoment: authorityMoment ?? "both",
347
+ executionReason,
348
+ ...invocation.authorizationBindingId ? { authorizationBindingId: invocation.authorizationBindingId } : {},
349
+ ...invocation.provenance ? { provenance: invocation.provenance } : {},
350
+ message: `Authorization binding for action ${action.actionId} expired before execution`
351
+ };
352
+ await options.store.updateActionInvocation(actionInvocationId, tenantId, spaceId, { status: "reconciliation_required", error: reconciliation.message, authorizationReconciliation: reconciliation });
353
+ return { actionInvocationId, status: "reconciliation_required", error: reconciliation.message, reconciliation };
354
+ }
355
+ const executionAuthorized = authorityMoment === "capture" ? invocation.authorizationBinding !== void 0 : options.authorization.authorizeExecution ? await options.authorization.authorizeExecution({
233
356
  ...authorizationInput,
234
357
  actionInvocationId,
235
358
  parameters: parsed.data,
@@ -237,11 +360,19 @@ function createGovernedActionHost(options) {
237
360
  executionReason
238
361
  }) : await options.authorization.authorize(authorizationInput);
239
362
  if (!executionAuthorized) {
240
- return fail(
241
- invocation,
242
- "failed",
243
- `Actor ${invocation.actorId} is not authorized to execute action ${action.actionId}`
244
- );
363
+ if (!action.execution) {
364
+ return fail(invocation, "failed", `Actor ${invocation.actorId} is not authorized to execute action ${action.actionId}`);
365
+ }
366
+ const reconciliation = {
367
+ kind: "authorization_denied",
368
+ governingMoment: action.execution?.authorityMoment ?? "both",
369
+ executionReason,
370
+ ...invocation.authorizationBindingId ? { authorizationBindingId: invocation.authorizationBindingId } : {},
371
+ ...invocation.provenance ? { provenance: invocation.provenance } : {},
372
+ message: `Actor ${invocation.actorId} is not authorized to execute action ${action.actionId}`
373
+ };
374
+ await options.store.updateActionInvocation(actionInvocationId, tenantId, spaceId, { status: "reconciliation_required", error: reconciliation.message, authorizationReconciliation: reconciliation });
375
+ return { actionInvocationId, status: "reconciliation_required", error: reconciliation.message, reconciliation };
245
376
  }
246
377
  const definitions = options.resolvePolicies ? await options.resolvePolicies({
247
378
  ...authorizationInput,
@@ -340,6 +471,7 @@ function createGovernedActionHost(options) {
340
471
  actorType: invocation.actorType,
341
472
  correlationId: invocation.correlationId,
342
473
  ...invocation.causationId ? { causationId: invocation.causationId } : {},
474
+ ...invocation.provenance ? { provenance: invocation.provenance } : {},
343
475
  db,
344
476
  services: options.services
345
477
  },
@@ -694,7 +826,8 @@ function createGovernedActionHost(options) {
694
826
  occurredAt: timestamp,
695
827
  recordedAt: timestamp,
696
828
  correlationId: invocation.correlationId,
697
- ...invocation.causationId ? { causationId: invocation.causationId } : {}
829
+ ...invocation.causationId ? { causationId: invocation.causationId } : {},
830
+ ...invocation.provenance ? { provenance: { source: invocation.provenance.source, ...invocation.provenance.auditAttributes ? { auditAttributes: invocation.provenance.auditAttributes } : {} } } : {}
698
831
  };
699
832
  if (options.outbox) {
700
833
  const hostLifecycleEvent = (/* @__PURE__ */ new Set(["AdapterInvocationStarted", "AdapterInvocationSucceeded", "AdapterInvocationFailed", "ComplianceBlocked"])).has(envelope.eventType);
@@ -704,9 +837,11 @@ function createGovernedActionHost(options) {
704
837
  return;
705
838
  }
706
839
  const traceContext = options.outbox.traceContext?.(envelope);
840
+ const payloadClassification = options.outbox.classifyPayload(envelope);
707
841
  const metadata = {
708
842
  producerModuleVersion: options.outbox.producerModuleVersion(invocation.actionId, invocation.actionVersion),
709
- payloadClassification: options.outbox.classifyPayload(envelope),
843
+ payloadClassification,
844
+ includeProvenance: options.outbox.includeProvenance?.(envelope, payloadClassification) ?? payloadClassification !== "restricted",
710
845
  ...traceContext ? { traceContext } : {}
711
846
  };
712
847
  if (transaction) {
@@ -745,7 +880,8 @@ function actionResult(invocation) {
745
880
  result: invocation.result,
746
881
  ...invocation.error ? { error: invocation.error } : {},
747
882
  ...invocation.hitlRoute ? { hitlRoute: invocation.hitlRoute } : {},
748
- ...invocation.hitlRiskTier ? { hitlRiskTier: invocation.hitlRiskTier } : {}
883
+ ...invocation.hitlRiskTier ? { hitlRiskTier: invocation.hitlRiskTier } : {},
884
+ ...invocation.authorizationReconciliation ? { reconciliation: invocation.authorizationReconciliation } : {}
749
885
  };
750
886
  }
751
887
  function asApprovalStore(store) {
@@ -798,7 +934,7 @@ function initialState(entityType) {
798
934
  return Object.values(machine?.states ?? {}).find((state) => state.stateClass === "initial")?.id ?? "none";
799
935
  }
800
936
  function isTerminal(status) {
801
- return ["completed", "failed", "blocked_by_policy", "validation_failed"].includes(status);
937
+ return ["completed", "failed", "blocked_by_policy", "reconciliation_required", "validation_failed"].includes(status);
802
938
  }
803
939
  function withoutPrivateHostFields(data, eventResultFields) {
804
940
  return Object.fromEntries(
@@ -815,6 +951,30 @@ function lifecycleId(prefix, invocationId, key) {
815
951
  const safeKey = key.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 96);
816
952
  return `${prefix}_${invocationId}_${safeKey}`;
817
953
  }
954
+ function validateAuthorizationBinding(input, parameterDigest, authorityMoment, currentTime = /* @__PURE__ */ new Date()) {
955
+ const binding = input.authorizationBinding;
956
+ if (binding.id !== input.authorizationBindingId && input.authorizationBindingId) throw new Error("Authorization binding ID does not match authorizationBindingId.");
957
+ if (binding.tenantId !== input.tenantId || binding.actorId !== input.actorId || binding.actionId !== input.actionId || binding.parameterDigest !== parameterDigest) {
958
+ throw new Error("Authorization binding does not match the submitted command identity.");
959
+ }
960
+ if (authorityMoment && binding.governingMoment !== authorityMoment) throw new Error("Authorization binding governingMoment does not match the action execution contract.");
961
+ const capturedAt = Date.parse(binding.capturedAt);
962
+ const expiresAt = binding.expiresAt === void 0 ? void 0 : Date.parse(binding.expiresAt);
963
+ if (!Number.isFinite(capturedAt) || expiresAt !== void 0 && !Number.isFinite(expiresAt)) {
964
+ throw new Error("Authorization binding timestamps must be valid ISO-8601 values.");
965
+ }
966
+ if (expiresAt !== void 0 && (expiresAt <= capturedAt || authorityMoment === "capture" && expiresAt <= currentTime.getTime())) {
967
+ throw new Error("Authorization binding must be unexpired at capture and expire after capturedAt.");
968
+ }
969
+ }
970
+ function idempotencyConflict(existing, incoming) {
971
+ const reasons = [];
972
+ if ((existing.idempotencyActorId ?? existing.actorId) !== incoming.actorId) reasons.push("actor");
973
+ if ((existing.idempotencyAuthorizationBindingId ?? existing.authorizationBindingId) !== incoming.authorizationBindingId) reasons.push("authority_binding");
974
+ if (existing.actionVersion !== incoming.actionVersion) reasons.push("action_version");
975
+ if (existing.parameterDigest !== incoming.parameterDigest) reasons.push("parameters");
976
+ return reasons.length ? { code: "IDEMPOTENCY_CONFLICT", idempotencyKey: incoming.idempotencyKey, existingInvocationId: existing.id, reasons } : void 0;
977
+ }
818
978
 
819
979
  // src/outbox.ts
820
980
  function toEnterpriseEventEnvelope(event, metadata) {
@@ -835,6 +995,7 @@ function toEnterpriseEventEnvelope(event, metadata) {
835
995
  producerModuleVersion: metadata.producerModuleVersion,
836
996
  payloadClassification: metadata.payloadClassification,
837
997
  ...metadata.traceContext ? { traceContext: { ...metadata.traceContext } } : {},
998
+ ...metadata.includeProvenance && event.provenance ? { provenance: event.provenance } : {},
838
999
  payload: event.payload
839
1000
  };
840
1001
  }
@@ -969,7 +1130,7 @@ var MemoryPlatformHostStore = class {
969
1130
  const record = await this.getActionInvocation(id, tenantId, spaceId);
970
1131
  if (!record) throw new Error(`ActionInvocation not found: ${id}`);
971
1132
  Object.assign(record, patch, { updatedAt: /* @__PURE__ */ new Date() });
972
- if (patch.status === "waiting_for_approval" || patch.status === "completed" || patch.status === "failed" || patch.status === "blocked_by_policy" || patch.status === "validation_failed") {
1133
+ if (patch.status === "waiting_for_approval" || patch.status === "completed" || patch.status === "failed" || patch.status === "blocked_by_policy" || patch.status === "reconciliation_required" || patch.status === "validation_failed") {
973
1134
  delete record.leaseOwner;
974
1135
  delete record.leaseExpiresAt;
975
1136
  }
@@ -1181,6 +1342,9 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
1181
1342
  actor_id text NOT NULL, actor_type text NOT NULL, status text NOT NULL,
1182
1343
  parameters jsonb NOT NULL, result jsonb NOT NULL DEFAULT '{}'::jsonb,
1183
1344
  correlation_id text NOT NULL, causation_id text, idempotency_key text,
1345
+ parameter_digest text, parameter_digest_algorithm text, idempotency_actor_id text,
1346
+ idempotency_authorization_binding_id text, invocation_provenance jsonb,
1347
+ authorization_binding jsonb, execution_reason text, authorization_reconciliation jsonb,
1184
1348
  authorization_binding_id text, error text,
1185
1349
  attempt_count integer NOT NULL DEFAULT 0, lease_owner text,
1186
1350
  lease_expires_at timestamptz, hitl_route text, hitl_risk_tier text,
@@ -1190,6 +1354,14 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
1190
1354
  );
1191
1355
  ALTER TABLE fabric_platform.action_invocations
1192
1356
  ADD COLUMN IF NOT EXISTS idempotency_key text,
1357
+ ADD COLUMN IF NOT EXISTS parameter_digest text,
1358
+ ADD COLUMN IF NOT EXISTS parameter_digest_algorithm text,
1359
+ ADD COLUMN IF NOT EXISTS idempotency_actor_id text,
1360
+ ADD COLUMN IF NOT EXISTS idempotency_authorization_binding_id text,
1361
+ ADD COLUMN IF NOT EXISTS invocation_provenance jsonb,
1362
+ ADD COLUMN IF NOT EXISTS authorization_binding jsonb,
1363
+ ADD COLUMN IF NOT EXISTS execution_reason text,
1364
+ ADD COLUMN IF NOT EXISTS authorization_reconciliation jsonb,
1193
1365
  ADD COLUMN IF NOT EXISTS authorization_binding_id text,
1194
1366
  ADD COLUMN IF NOT EXISTS attempt_count integer NOT NULL DEFAULT 0,
1195
1367
  ADD COLUMN IF NOT EXISTS lease_owner text,
@@ -1206,6 +1378,12 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
1206
1378
  ON fabric_platform.action_invocations
1207
1379
  (tenant_id, space_id, action_id, idempotency_key)
1208
1380
  WHERE idempotency_key IS NOT NULL;
1381
+ CREATE INDEX IF NOT EXISTS action_invocations_parameter_digest_idx
1382
+ ON fabric_platform.action_invocations (tenant_id, parameter_digest)
1383
+ WHERE parameter_digest IS NOT NULL;
1384
+ CREATE INDEX IF NOT EXISTS action_invocations_authority_binding_idx
1385
+ ON fabric_platform.action_invocations (tenant_id, authorization_binding_id)
1386
+ WHERE authorization_binding_id IS NOT NULL;
1209
1387
  CREATE INDEX IF NOT EXISTS action_invocations_worker_idx
1210
1388
  ON fabric_platform.action_invocations (status, lease_expires_at, created_at);
1211
1389
  CREATE TABLE IF NOT EXISTS fabric_platform.policy_evaluations (
@@ -1249,9 +1427,10 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
1249
1427
  actor_id text NOT NULL, actor_type text NOT NULL, action_invocation_id text,
1250
1428
  payload jsonb NOT NULL, sequence bigint NOT NULL,
1251
1429
  occurred_at timestamptz NOT NULL, recorded_at timestamptz NOT NULL,
1252
- correlation_id text NOT NULL, causation_id text,
1430
+ correlation_id text NOT NULL, causation_id text, provenance jsonb,
1253
1431
  UNIQUE (tenant_id, space_id, sequence)
1254
1432
  );
1433
+ ALTER TABLE fabric_platform.asset_events ADD COLUMN IF NOT EXISTS provenance jsonb;
1255
1434
  CREATE INDEX IF NOT EXISTS asset_events_subject_idx
1256
1435
  ON fabric_platform.asset_events
1257
1436
  (tenant_id, space_id, subject_type, subject_id, sequence);
@@ -1275,8 +1454,10 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
1275
1454
  `INSERT INTO fabric_platform.action_invocations
1276
1455
  (id,tenant_id,space_id,action_id,action_version,actor_id,actor_type,status,
1277
1456
  parameters,result,correlation_id,causation_id,idempotency_key,
1457
+ parameter_digest,parameter_digest_algorithm,idempotency_actor_id,
1458
+ idempotency_authorization_binding_id,invocation_provenance,authorization_binding,execution_reason,
1278
1459
  authorization_binding_id,error,runtime_evidence,created_at,updated_at)
1279
- VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9::jsonb,$10::jsonb,$11,$12,$13,$14,$15,$16::jsonb,$17,$17)
1460
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9::jsonb,$10::jsonb,$11,$12,$13,$14,$15,$16,$17,$18::jsonb,$19::jsonb,$20,$21,$22,$23::jsonb,$24,$24)
1280
1461
  ON CONFLICT (tenant_id,space_id,action_id,idempotency_key)
1281
1462
  WHERE idempotency_key IS NOT NULL DO UPDATE SET id=fabric_platform.action_invocations.id
1282
1463
  RETURNING *`,
@@ -1294,6 +1475,13 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
1294
1475
  input.correlationId,
1295
1476
  input.causationId ?? null,
1296
1477
  input.idempotencyKey ?? null,
1478
+ input.parameterDigest ?? null,
1479
+ input.parameterDigestAlgorithm ?? null,
1480
+ input.idempotencyActorId ?? null,
1481
+ input.idempotencyAuthorizationBindingId ?? null,
1482
+ input.provenance ? JSON.stringify(input.provenance) : null,
1483
+ input.authorizationBinding ? JSON.stringify(input.authorizationBinding) : null,
1484
+ input.executionReason ?? null,
1297
1485
  input.authorizationBindingId ?? null,
1298
1486
  input.error ?? null,
1299
1487
  input.runtimeEvidence ? JSON.stringify(input.runtimeEvidence) : null,
@@ -1315,9 +1503,10 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
1315
1503
  `UPDATE fabric_platform.action_invocations SET
1316
1504
  status=COALESCE($4,status), result=COALESCE($5::jsonb,result),
1317
1505
  error=CASE WHEN $6::boolean THEN $7 ELSE error END,
1318
- lease_owner=CASE WHEN $4 IN ('waiting_for_approval','completed','failed','blocked_by_policy','validation_failed')
1506
+ authorization_reconciliation=CASE WHEN $8::boolean THEN $9::jsonb ELSE authorization_reconciliation END,
1507
+ lease_owner=CASE WHEN $4 IN ('waiting_for_approval','completed','failed','blocked_by_policy','reconciliation_required','validation_failed')
1319
1508
  THEN NULL ELSE lease_owner END,
1320
- lease_expires_at=CASE WHEN $4 IN ('waiting_for_approval','completed','failed','blocked_by_policy','validation_failed')
1509
+ lease_expires_at=CASE WHEN $4 IN ('waiting_for_approval','completed','failed','blocked_by_policy','reconciliation_required','validation_failed')
1321
1510
  THEN NULL ELSE lease_expires_at END,
1322
1511
  updated_at=now()
1323
1512
  WHERE id=$1 AND tenant_id=$2 AND space_id=$3`,
@@ -1328,7 +1517,9 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
1328
1517
  patch.status ?? null,
1329
1518
  patch.result === void 0 ? null : JSON.stringify(patch.result),
1330
1519
  Object.hasOwn(patch, "error"),
1331
- patch.error ?? null
1520
+ patch.error ?? null,
1521
+ Object.hasOwn(patch, "authorizationReconciliation"),
1522
+ patch.authorizationReconciliation ? JSON.stringify(patch.authorizationReconciliation) : null
1332
1523
  ]
1333
1524
  );
1334
1525
  }
@@ -1523,8 +1714,8 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
1523
1714
  `INSERT INTO fabric_platform.asset_events
1524
1715
  (id,tenant_id,space_id,event_type,event_schema_version,subject_type,subject_id,
1525
1716
  actor_id,actor_type,action_invocation_id,payload,sequence,occurred_at,recorded_at,
1526
- correlation_id,causation_id)
1527
- VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11::jsonb,$12,$13,$14,$15,$16)
1717
+ correlation_id,causation_id,provenance)
1718
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11::jsonb,$12,$13,$14,$15,$16,$17::jsonb)
1528
1719
  ON CONFLICT (id) DO NOTHING`,
1529
1720
  [
1530
1721
  event.id,
@@ -1542,7 +1733,8 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
1542
1733
  event.occurredAt,
1543
1734
  event.recordedAt,
1544
1735
  event.correlationId,
1545
- event.causationId ?? null
1736
+ event.causationId ?? null,
1737
+ event.provenance ? JSON.stringify(event.provenance) : null
1546
1738
  ]
1547
1739
  );
1548
1740
  }
@@ -1553,13 +1745,13 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
1553
1745
  INSERT INTO fabric_platform.asset_events
1554
1746
  (id,tenant_id,space_id,event_type,event_schema_version,subject_type,subject_id,
1555
1747
  actor_id,actor_type,action_invocation_id,payload,sequence,occurred_at,recorded_at,
1556
- correlation_id,causation_id)
1557
- VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11::jsonb,$12,$13,$14,$15,$16)
1748
+ correlation_id,causation_id,provenance)
1749
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11::jsonb,$12,$13,$14,$15,$16,$17::jsonb)
1558
1750
  ON CONFLICT (id) DO NOTHING RETURNING id
1559
1751
  )
1560
1752
  INSERT INTO fabric_platform.event_outbox
1561
1753
  (id,tenant_id,space_id,event,status,attempt_count,available_at,created_at)
1562
- SELECT $1,$2,$3,$17::jsonb,'pending',0,$14,$14 FROM inserted_event
1754
+ SELECT $1,$2,$3,$18::jsonb,'pending',0,$14,$14 FROM inserted_event
1563
1755
  ON CONFLICT (id) DO NOTHING`,
1564
1756
  [
1565
1757
  event.id,
@@ -1578,6 +1770,7 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
1578
1770
  event.recordedAt,
1579
1771
  event.correlationId,
1580
1772
  event.causationId ?? null,
1773
+ event.provenance ? JSON.stringify(event.provenance) : null,
1581
1774
  JSON.stringify(envelope)
1582
1775
  ]
1583
1776
  );
@@ -1738,6 +1931,14 @@ function toActionRecord(row) {
1738
1931
  correlationId: String(row.correlation_id),
1739
1932
  ...row.causation_id ? { causationId: String(row.causation_id) } : {},
1740
1933
  ...row.idempotency_key ? { idempotencyKey: String(row.idempotency_key) } : {},
1934
+ ...row.parameter_digest ? { parameterDigest: String(row.parameter_digest) } : {},
1935
+ ...row.parameter_digest_algorithm ? { parameterDigestAlgorithm: String(row.parameter_digest_algorithm) } : {},
1936
+ ...row.idempotency_actor_id ? { idempotencyActorId: String(row.idempotency_actor_id) } : {},
1937
+ ...row.idempotency_authorization_binding_id ? { idempotencyAuthorizationBindingId: String(row.idempotency_authorization_binding_id) } : {},
1938
+ ...row.invocation_provenance ? { provenance: row.invocation_provenance } : {},
1939
+ ...row.authorization_binding ? { authorizationBinding: row.authorization_binding } : {},
1940
+ ...row.execution_reason ? { executionReason: String(row.execution_reason) } : {},
1941
+ ...row.authorization_reconciliation ? { authorizationReconciliation: row.authorization_reconciliation } : {},
1741
1942
  ...row.authorization_binding_id ? { authorizationBindingId: String(row.authorization_binding_id) } : {},
1742
1943
  attemptCount: Number(row.attempt_count ?? 0),
1743
1944
  ...row.lease_owner ? { leaseOwner: String(row.lease_owner) } : {},
@@ -1811,7 +2012,8 @@ function toEventRecord(row) {
1811
2012
  occurredAt: new Date(row.occurred_at),
1812
2013
  recordedAt: new Date(row.recorded_at),
1813
2014
  correlationId: String(row.correlation_id),
1814
- ...row.causation_id ? { causationId: String(row.causation_id) } : {}
2015
+ ...row.causation_id ? { causationId: String(row.causation_id) } : {},
2016
+ ...row.provenance ? { provenance: row.provenance } : {}
1815
2017
  };
1816
2018
  }
1817
2019
  function toOutboxRecord(row) {
@@ -1832,6 +2034,7 @@ function toOutboxRecord(row) {
1832
2034
  ...event.actionInvocationId ? { actionInvocationId: String(event.actionInvocationId) } : {},
1833
2035
  correlationId: String(event.correlationId),
1834
2036
  ...event.causationId ? { causationId: String(event.causationId) } : {},
2037
+ ...event.provenance ? { provenance: event.provenance } : {},
1835
2038
  occurredAt: new Date(event.occurredAt),
1836
2039
  recordedAt: new Date(event.recordedAt),
1837
2040
  producerModuleVersion: String(event.producerModuleVersion),
@@ -1918,12 +2121,16 @@ async function abortableDelay(milliseconds, signal) {
1918
2121
  });
1919
2122
  }
1920
2123
 
2124
+ exports.IdempotencyConflictError = IdempotencyConflictError;
1921
2125
  exports.MemoryPlatformHostStore = MemoryPlatformHostStore;
2126
+ exports.PARAMETER_DIGEST_ALGORITHM = PARAMETER_DIGEST_ALGORITHM;
1922
2127
  exports.PLATFORM_HOST_CONTRACT_VERSION = PLATFORM_HOST_CONTRACT_VERSION;
1923
2128
  exports.PostgresPlatformHostStore = PostgresPlatformHostStore;
2129
+ exports.canonicalJson = canonicalJson;
1924
2130
  exports.cloneOutboxRecord = cloneOutboxRecord;
1925
2131
  exports.createGovernedActionHost = createGovernedActionHost;
1926
2132
  exports.createStoreBackedActionDispatcher = createStoreBackedActionDispatcher;
2133
+ exports.digestParameters = digestParameters;
1927
2134
  exports.runOutboxRelayCycle = runOutboxRelayCycle;
1928
2135
  exports.runPlatformActionWorker = runPlatformActionWorker;
1929
2136
  exports.runPlatformActionWorkerCycle = runPlatformActionWorkerCycle;