@fabricorg/platform-host 2.0.1 → 4.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/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_SOURCE = /^(?:ui|sdui|api|agent|worker|system|[a-z][a-z0-9-]*(?:\.[a-z][a-z0-9-]*)+)$/;
35
+ function normalizeProvenance(input, options) {
36
+ if (!input) return void 0;
37
+ if (!INVOCATION_SOURCE.test(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) => {
@@ -26,9 +83,15 @@ function createGovernedActionHost(options) {
26
83
  for (const adapter of options.adapters ?? []) adapters.register(adapter);
27
84
  const now = options.now ?? (() => /* @__PURE__ */ new Date());
28
85
  const extractEvents = options.extractEvents ?? DEFAULT_EXTRACT_EVENTS;
29
- const actionResolver = options.resolveAction ?? platform.resolveAction;
86
+ const configuredActionResolver = options.resolveAction ?? options.registry?.resolveAction;
87
+ if (!configuredActionResolver) throw new Error("Platform Host requires an explicit module registry or resolveAction function.");
88
+ const actionResolver = configuredActionResolver;
89
+ const stateMachineResolver = options.registry?.resolveStateMachine ?? (() => void 0);
30
90
  const eventResultFields = options.eventResultFields ?? ["_events"];
31
91
  async function submitAction(input) {
92
+ if (input.executionReason && !["initial", "offline_replay"].includes(input.executionReason)) {
93
+ throw new Error(`Unsupported submitted execution reason: ${input.executionReason}`);
94
+ }
32
95
  const action = actionResolver(input.actionId);
33
96
  if (!action) throw new Error(`Unknown action: ${input.actionId}`);
34
97
  const authorizationInput = toAuthorizationInput(action, input);
@@ -39,8 +102,16 @@ function createGovernedActionHost(options) {
39
102
  throw new Error(`Actor ${input.actorId} is not authorized for action ${input.actionId}`);
40
103
  }
41
104
  const actionInvocationId = platform.createFabricId("act");
42
- const correlationId = input.correlationId ?? platform.createFabricId("corr");
105
+ if (input.correlationId && input.provenance?.correlationId && input.correlationId !== input.provenance.correlationId) throw new Error("Invocation provenance correlationId does not match submission correlationId.");
106
+ if (input.causationId && input.provenance?.causationId && input.causationId !== input.provenance.causationId) throw new Error("Invocation provenance causationId does not match submission causationId.");
107
+ const correlationId = input.correlationId ?? input.provenance?.correlationId ?? platform.createFabricId("corr");
43
108
  const durableParameters = options.redactActionParameters ? options.redactActionParameters(input.actionId, input.parameters) : input.parameters;
109
+ const parameterDigest = digestParameters(input.parameters);
110
+ if (input.authorizationBinding) validateAuthorizationBinding(input, parameterDigest, action.execution?.authorityMoment, now());
111
+ const durableProvenance = normalizeProvenance(
112
+ input.provenance,
113
+ options.provenance
114
+ );
44
115
  const runtimeEvidence = {
45
116
  governanceContractVersion: platform.FABRIC_GOVERNANCE_CONTRACT_VERSION,
46
117
  hostContractVersion: PLATFORM_HOST_CONTRACT_VERSION,
@@ -60,12 +131,41 @@ function createGovernedActionHost(options) {
60
131
  result: {},
61
132
  runtimeEvidence,
62
133
  correlationId,
63
- ...input.causationId ? { causationId: input.causationId } : {},
134
+ ...input.causationId ?? input.provenance?.causationId ? { causationId: input.causationId ?? input.provenance?.causationId } : {},
64
135
  ...input.idempotencyKey ? { idempotencyKey: input.idempotencyKey } : {},
136
+ ...input.idempotencyKey ? {
137
+ parameterDigest,
138
+ parameterDigestAlgorithm: PARAMETER_DIGEST_ALGORITHM,
139
+ idempotencyActorId: input.actorId,
140
+ ...input.authorizationBindingId ? { idempotencyAuthorizationBindingId: input.authorizationBindingId } : {}
141
+ } : {},
142
+ ...durableProvenance ? { provenance: durableProvenance } : {},
143
+ ...input.authorizationBinding ? { authorizationBinding: input.authorizationBinding } : {},
144
+ ...input.executionReason ? { executionReason: input.executionReason } : {},
65
145
  ...input.authorizationBindingId ? { authorizationBindingId: input.authorizationBindingId } : {}
66
146
  });
147
+ if (durableInvocation.id !== actionInvocationId && input.idempotencyKey) {
148
+ const conflict = idempotencyConflict(durableInvocation, {
149
+ actorId: input.actorId,
150
+ authorizationBindingId: input.authorizationBindingId,
151
+ actionVersion: action.version,
152
+ parameterDigest,
153
+ idempotencyKey: input.idempotencyKey
154
+ });
155
+ if (!durableInvocation.parameterDigest) {
156
+ options.idempotency?.onLegacyRecord?.(durableInvocation);
157
+ const enforceableLegacyConflict = conflict && { ...conflict, reasons: conflict.reasons.filter((reason) => reason !== "parameters") };
158
+ if (enforceableLegacyConflict && enforceableLegacyConflict.reasons.length) {
159
+ options.idempotency?.onConflict?.(enforceableLegacyConflict);
160
+ if ((options.idempotency?.conflictMode ?? "enforce") === "enforce") throw new IdempotencyConflictError(enforceableLegacyConflict);
161
+ }
162
+ } else if (conflict) {
163
+ options.idempotency?.onConflict?.(conflict);
164
+ if ((options.idempotency?.conflictMode ?? "enforce") === "enforce") throw new IdempotencyConflictError(conflict);
165
+ }
166
+ }
67
167
  const durableWorkflowId = `action-invocation-${durableInvocation.id}`;
68
- if (durableInvocation.id !== actionInvocationId && isTerminal(durableInvocation.status)) {
168
+ if (durableInvocation.id !== actionInvocationId) {
69
169
  return {
70
170
  actionInvocationId: durableInvocation.id,
71
171
  status: durableInvocation.status,
@@ -73,7 +173,8 @@ function createGovernedActionHost(options) {
73
173
  result: durableInvocation.result,
74
174
  ...durableInvocation.error ? { error: durableInvocation.error } : {},
75
175
  ...durableInvocation.hitlRoute ? { hitlRoute: durableInvocation.hitlRoute } : {},
76
- ...durableInvocation.hitlRiskTier ? { hitlRiskTier: durableInvocation.hitlRiskTier } : {}
176
+ ...durableInvocation.hitlRiskTier ? { hitlRiskTier: durableInvocation.hitlRiskTier } : {},
177
+ ...durableInvocation.authorizationReconciliation ? { reconciliation: durableInvocation.authorizationReconciliation } : {}
77
178
  };
78
179
  }
79
180
  if (options.dispatcher) {
@@ -221,7 +322,7 @@ function createGovernedActionHost(options) {
221
322
  }
222
323
  }
223
324
  const authorizationInput = toAuthorizationInput(action, invocation);
224
- const executionReason = executionReasonOverride ?? (invocation.attemptCount > 1 || resumingRunningInvocation && invocation.attemptCount === 0 ? "recovery" : "initial");
325
+ const executionReason = executionReasonOverride ?? (invocation.attemptCount > 1 || resumingRunningInvocation && invocation.attemptCount === 0 ? "recovery" : invocation.executionReason ?? "initial");
225
326
  if (!await options.authorization.checkEntitlement(authorizationInput)) {
226
327
  return fail(
227
328
  invocation,
@@ -229,7 +330,32 @@ function createGovernedActionHost(options) {
229
330
  `Module "${action.namespace}" is no longer enabled for tenant ${tenantId}`
230
331
  );
231
332
  }
232
- const executionAuthorized = options.authorization.authorizeExecution ? await options.authorization.authorizeExecution({
333
+ const authorityMoment = action.execution?.authorityMoment;
334
+ if (authorityMoment === "capture" && !invocation.authorizationBinding) {
335
+ const reconciliation = {
336
+ kind: "authorization_missing_capture_evidence",
337
+ governingMoment: authorityMoment,
338
+ executionReason,
339
+ ...invocation.provenance ? { provenance: invocation.provenance } : {},
340
+ message: `Action ${action.actionId} requires durable capture-time authorization evidence`
341
+ };
342
+ await options.store.updateActionInvocation(actionInvocationId, tenantId, spaceId, { status: "reconciliation_required", error: reconciliation.message, authorizationReconciliation: reconciliation });
343
+ return { actionInvocationId, status: "reconciliation_required", error: reconciliation.message, reconciliation };
344
+ }
345
+ const bindingExpired = authorityMoment !== "capture" && invocation.authorizationBinding?.expiresAt !== void 0 && Date.parse(invocation.authorizationBinding.expiresAt) <= now().getTime();
346
+ if (bindingExpired) {
347
+ const reconciliation = {
348
+ kind: "authorization_expired",
349
+ governingMoment: authorityMoment ?? "both",
350
+ executionReason,
351
+ ...invocation.authorizationBindingId ? { authorizationBindingId: invocation.authorizationBindingId } : {},
352
+ ...invocation.provenance ? { provenance: invocation.provenance } : {},
353
+ message: `Authorization binding for action ${action.actionId} expired before execution`
354
+ };
355
+ await options.store.updateActionInvocation(actionInvocationId, tenantId, spaceId, { status: "reconciliation_required", error: reconciliation.message, authorizationReconciliation: reconciliation });
356
+ return { actionInvocationId, status: "reconciliation_required", error: reconciliation.message, reconciliation };
357
+ }
358
+ const executionAuthorized = authorityMoment === "capture" ? invocation.authorizationBinding !== void 0 : options.authorization.authorizeExecution ? await options.authorization.authorizeExecution({
233
359
  ...authorizationInput,
234
360
  actionInvocationId,
235
361
  parameters: parsed.data,
@@ -237,11 +363,19 @@ function createGovernedActionHost(options) {
237
363
  executionReason
238
364
  }) : await options.authorization.authorize(authorizationInput);
239
365
  if (!executionAuthorized) {
240
- return fail(
241
- invocation,
242
- "failed",
243
- `Actor ${invocation.actorId} is not authorized to execute action ${action.actionId}`
244
- );
366
+ if (!action.execution) {
367
+ return fail(invocation, "failed", `Actor ${invocation.actorId} is not authorized to execute action ${action.actionId}`);
368
+ }
369
+ const reconciliation = {
370
+ kind: "authorization_denied",
371
+ governingMoment: action.execution?.authorityMoment ?? "both",
372
+ executionReason,
373
+ ...invocation.authorizationBindingId ? { authorizationBindingId: invocation.authorizationBindingId } : {},
374
+ ...invocation.provenance ? { provenance: invocation.provenance } : {},
375
+ message: `Actor ${invocation.actorId} is not authorized to execute action ${action.actionId}`
376
+ };
377
+ await options.store.updateActionInvocation(actionInvocationId, tenantId, spaceId, { status: "reconciliation_required", error: reconciliation.message, authorizationReconciliation: reconciliation });
378
+ return { actionInvocationId, status: "reconciliation_required", error: reconciliation.message, reconciliation };
245
379
  }
246
380
  const definitions = options.resolvePolicies ? await options.resolvePolicies({
247
381
  ...authorizationInput,
@@ -257,6 +391,7 @@ function createGovernedActionHost(options) {
257
391
  parameters: parsed.data,
258
392
  db: options.store.db,
259
393
  services: options.services,
394
+ resolveCodePolicy: options.registry?.resolvePolicy,
260
395
  mode: "execute",
261
396
  now: now()
262
397
  });
@@ -309,10 +444,11 @@ function createGovernedActionHost(options) {
309
444
  spaceId,
310
445
  binding.entityType,
311
446
  entityId
312
- ) ?? initialState(binding.entityType) : initialState(binding.entityType);
447
+ ) ?? initialState(stateMachineResolver(binding.entityType)) : initialState(stateMachineResolver(binding.entityType));
313
448
  const targetState = typeof binding.targetState === "function" ? binding.targetState(parsed.data) : binding.targetState;
314
449
  if (targetState !== "") {
315
- const transition = platform.validateTransition(
450
+ const transition = platform.validateStateMachineTransition(
451
+ stateMachineResolver(binding.entityType),
316
452
  binding.entityType,
317
453
  currentState,
318
454
  targetState,
@@ -340,6 +476,7 @@ function createGovernedActionHost(options) {
340
476
  actorType: invocation.actorType,
341
477
  correlationId: invocation.correlationId,
342
478
  ...invocation.causationId ? { causationId: invocation.causationId } : {},
479
+ ...invocation.provenance ? { provenance: invocation.provenance } : {},
343
480
  db,
344
481
  services: options.services
345
482
  },
@@ -694,7 +831,8 @@ function createGovernedActionHost(options) {
694
831
  occurredAt: timestamp,
695
832
  recordedAt: timestamp,
696
833
  correlationId: invocation.correlationId,
697
- ...invocation.causationId ? { causationId: invocation.causationId } : {}
834
+ ...invocation.causationId ? { causationId: invocation.causationId } : {},
835
+ ...invocation.provenance ? { provenance: { source: invocation.provenance.source, ...invocation.provenance.auditAttributes ? { auditAttributes: invocation.provenance.auditAttributes } : {} } } : {}
698
836
  };
699
837
  if (options.outbox) {
700
838
  const hostLifecycleEvent = (/* @__PURE__ */ new Set(["AdapterInvocationStarted", "AdapterInvocationSucceeded", "AdapterInvocationFailed", "ComplianceBlocked"])).has(envelope.eventType);
@@ -704,9 +842,11 @@ function createGovernedActionHost(options) {
704
842
  return;
705
843
  }
706
844
  const traceContext = options.outbox.traceContext?.(envelope);
845
+ const payloadClassification = options.outbox.classifyPayload(envelope);
707
846
  const metadata = {
708
847
  producerModuleVersion: options.outbox.producerModuleVersion(invocation.actionId, invocation.actionVersion),
709
- payloadClassification: options.outbox.classifyPayload(envelope),
848
+ payloadClassification,
849
+ includeProvenance: options.outbox.includeProvenance?.(envelope, payloadClassification) ?? payloadClassification !== "restricted",
710
850
  ...traceContext ? { traceContext } : {}
711
851
  };
712
852
  if (transaction) {
@@ -745,7 +885,8 @@ function actionResult(invocation) {
745
885
  result: invocation.result,
746
886
  ...invocation.error ? { error: invocation.error } : {},
747
887
  ...invocation.hitlRoute ? { hitlRoute: invocation.hitlRoute } : {},
748
- ...invocation.hitlRiskTier ? { hitlRiskTier: invocation.hitlRiskTier } : {}
888
+ ...invocation.hitlRiskTier ? { hitlRiskTier: invocation.hitlRiskTier } : {},
889
+ ...invocation.authorizationReconciliation ? { reconciliation: invocation.authorizationReconciliation } : {}
749
890
  };
750
891
  }
751
892
  function asApprovalStore(store) {
@@ -793,12 +934,11 @@ function declaredPolicies(policyIds) {
793
934
  codeEvaluatorPolicyId: policyId
794
935
  }));
795
936
  }
796
- function initialState(entityType) {
797
- const machine = platform.resolveStateMachine(entityType);
937
+ function initialState(machine) {
798
938
  return Object.values(machine?.states ?? {}).find((state) => state.stateClass === "initial")?.id ?? "none";
799
939
  }
800
940
  function isTerminal(status) {
801
- return ["completed", "failed", "blocked_by_policy", "validation_failed"].includes(status);
941
+ return ["completed", "failed", "blocked_by_policy", "reconciliation_required", "validation_failed"].includes(status);
802
942
  }
803
943
  function withoutPrivateHostFields(data, eventResultFields) {
804
944
  return Object.fromEntries(
@@ -815,6 +955,30 @@ function lifecycleId(prefix, invocationId, key) {
815
955
  const safeKey = key.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 96);
816
956
  return `${prefix}_${invocationId}_${safeKey}`;
817
957
  }
958
+ function validateAuthorizationBinding(input, parameterDigest, authorityMoment, currentTime = /* @__PURE__ */ new Date()) {
959
+ const binding = input.authorizationBinding;
960
+ if (binding.id !== input.authorizationBindingId && input.authorizationBindingId) throw new Error("Authorization binding ID does not match authorizationBindingId.");
961
+ if (binding.tenantId !== input.tenantId || binding.actorId !== input.actorId || binding.actionId !== input.actionId || binding.parameterDigest !== parameterDigest) {
962
+ throw new Error("Authorization binding does not match the submitted command identity.");
963
+ }
964
+ if (authorityMoment && binding.governingMoment !== authorityMoment) throw new Error("Authorization binding governingMoment does not match the action execution contract.");
965
+ const capturedAt = Date.parse(binding.capturedAt);
966
+ const expiresAt = binding.expiresAt === void 0 ? void 0 : Date.parse(binding.expiresAt);
967
+ if (!Number.isFinite(capturedAt) || expiresAt !== void 0 && !Number.isFinite(expiresAt)) {
968
+ throw new Error("Authorization binding timestamps must be valid ISO-8601 values.");
969
+ }
970
+ if (expiresAt !== void 0 && (expiresAt <= capturedAt || authorityMoment === "capture" && expiresAt <= currentTime.getTime())) {
971
+ throw new Error("Authorization binding must be unexpired at capture and expire after capturedAt.");
972
+ }
973
+ }
974
+ function idempotencyConflict(existing, incoming) {
975
+ const reasons = [];
976
+ if ((existing.idempotencyActorId ?? existing.actorId) !== incoming.actorId) reasons.push("actor");
977
+ if ((existing.idempotencyAuthorizationBindingId ?? existing.authorizationBindingId) !== incoming.authorizationBindingId) reasons.push("authority_binding");
978
+ if (existing.actionVersion !== incoming.actionVersion) reasons.push("action_version");
979
+ if (existing.parameterDigest !== incoming.parameterDigest) reasons.push("parameters");
980
+ return reasons.length ? { code: "IDEMPOTENCY_CONFLICT", idempotencyKey: incoming.idempotencyKey, existingInvocationId: existing.id, reasons } : void 0;
981
+ }
818
982
 
819
983
  // src/outbox.ts
820
984
  function toEnterpriseEventEnvelope(event, metadata) {
@@ -835,6 +999,7 @@ function toEnterpriseEventEnvelope(event, metadata) {
835
999
  producerModuleVersion: metadata.producerModuleVersion,
836
1000
  payloadClassification: metadata.payloadClassification,
837
1001
  ...metadata.traceContext ? { traceContext: { ...metadata.traceContext } } : {},
1002
+ ...metadata.includeProvenance && event.provenance ? { provenance: event.provenance } : {},
838
1003
  payload: event.payload
839
1004
  };
840
1005
  }
@@ -969,7 +1134,7 @@ var MemoryPlatformHostStore = class {
969
1134
  const record = await this.getActionInvocation(id, tenantId, spaceId);
970
1135
  if (!record) throw new Error(`ActionInvocation not found: ${id}`);
971
1136
  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") {
1137
+ 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
1138
  delete record.leaseOwner;
974
1139
  delete record.leaseExpiresAt;
975
1140
  }
@@ -1181,6 +1346,9 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
1181
1346
  actor_id text NOT NULL, actor_type text NOT NULL, status text NOT NULL,
1182
1347
  parameters jsonb NOT NULL, result jsonb NOT NULL DEFAULT '{}'::jsonb,
1183
1348
  correlation_id text NOT NULL, causation_id text, idempotency_key text,
1349
+ parameter_digest text, parameter_digest_algorithm text, idempotency_actor_id text,
1350
+ idempotency_authorization_binding_id text, invocation_provenance jsonb,
1351
+ authorization_binding jsonb, execution_reason text, authorization_reconciliation jsonb,
1184
1352
  authorization_binding_id text, error text,
1185
1353
  attempt_count integer NOT NULL DEFAULT 0, lease_owner text,
1186
1354
  lease_expires_at timestamptz, hitl_route text, hitl_risk_tier text,
@@ -1190,6 +1358,14 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
1190
1358
  );
1191
1359
  ALTER TABLE fabric_platform.action_invocations
1192
1360
  ADD COLUMN IF NOT EXISTS idempotency_key text,
1361
+ ADD COLUMN IF NOT EXISTS parameter_digest text,
1362
+ ADD COLUMN IF NOT EXISTS parameter_digest_algorithm text,
1363
+ ADD COLUMN IF NOT EXISTS idempotency_actor_id text,
1364
+ ADD COLUMN IF NOT EXISTS idempotency_authorization_binding_id text,
1365
+ ADD COLUMN IF NOT EXISTS invocation_provenance jsonb,
1366
+ ADD COLUMN IF NOT EXISTS authorization_binding jsonb,
1367
+ ADD COLUMN IF NOT EXISTS execution_reason text,
1368
+ ADD COLUMN IF NOT EXISTS authorization_reconciliation jsonb,
1193
1369
  ADD COLUMN IF NOT EXISTS authorization_binding_id text,
1194
1370
  ADD COLUMN IF NOT EXISTS attempt_count integer NOT NULL DEFAULT 0,
1195
1371
  ADD COLUMN IF NOT EXISTS lease_owner text,
@@ -1206,6 +1382,12 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
1206
1382
  ON fabric_platform.action_invocations
1207
1383
  (tenant_id, space_id, action_id, idempotency_key)
1208
1384
  WHERE idempotency_key IS NOT NULL;
1385
+ CREATE INDEX IF NOT EXISTS action_invocations_parameter_digest_idx
1386
+ ON fabric_platform.action_invocations (tenant_id, parameter_digest)
1387
+ WHERE parameter_digest IS NOT NULL;
1388
+ CREATE INDEX IF NOT EXISTS action_invocations_authority_binding_idx
1389
+ ON fabric_platform.action_invocations (tenant_id, authorization_binding_id)
1390
+ WHERE authorization_binding_id IS NOT NULL;
1209
1391
  CREATE INDEX IF NOT EXISTS action_invocations_worker_idx
1210
1392
  ON fabric_platform.action_invocations (status, lease_expires_at, created_at);
1211
1393
  CREATE TABLE IF NOT EXISTS fabric_platform.policy_evaluations (
@@ -1249,9 +1431,10 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
1249
1431
  actor_id text NOT NULL, actor_type text NOT NULL, action_invocation_id text,
1250
1432
  payload jsonb NOT NULL, sequence bigint NOT NULL,
1251
1433
  occurred_at timestamptz NOT NULL, recorded_at timestamptz NOT NULL,
1252
- correlation_id text NOT NULL, causation_id text,
1434
+ correlation_id text NOT NULL, causation_id text, provenance jsonb,
1253
1435
  UNIQUE (tenant_id, space_id, sequence)
1254
1436
  );
1437
+ ALTER TABLE fabric_platform.asset_events ADD COLUMN IF NOT EXISTS provenance jsonb;
1255
1438
  CREATE INDEX IF NOT EXISTS asset_events_subject_idx
1256
1439
  ON fabric_platform.asset_events
1257
1440
  (tenant_id, space_id, subject_type, subject_id, sequence);
@@ -1275,8 +1458,10 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
1275
1458
  `INSERT INTO fabric_platform.action_invocations
1276
1459
  (id,tenant_id,space_id,action_id,action_version,actor_id,actor_type,status,
1277
1460
  parameters,result,correlation_id,causation_id,idempotency_key,
1461
+ parameter_digest,parameter_digest_algorithm,idempotency_actor_id,
1462
+ idempotency_authorization_binding_id,invocation_provenance,authorization_binding,execution_reason,
1278
1463
  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)
1464
+ 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
1465
  ON CONFLICT (tenant_id,space_id,action_id,idempotency_key)
1281
1466
  WHERE idempotency_key IS NOT NULL DO UPDATE SET id=fabric_platform.action_invocations.id
1282
1467
  RETURNING *`,
@@ -1294,6 +1479,13 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
1294
1479
  input.correlationId,
1295
1480
  input.causationId ?? null,
1296
1481
  input.idempotencyKey ?? null,
1482
+ input.parameterDigest ?? null,
1483
+ input.parameterDigestAlgorithm ?? null,
1484
+ input.idempotencyActorId ?? null,
1485
+ input.idempotencyAuthorizationBindingId ?? null,
1486
+ input.provenance ? JSON.stringify(input.provenance) : null,
1487
+ input.authorizationBinding ? JSON.stringify(input.authorizationBinding) : null,
1488
+ input.executionReason ?? null,
1297
1489
  input.authorizationBindingId ?? null,
1298
1490
  input.error ?? null,
1299
1491
  input.runtimeEvidence ? JSON.stringify(input.runtimeEvidence) : null,
@@ -1315,9 +1507,10 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
1315
1507
  `UPDATE fabric_platform.action_invocations SET
1316
1508
  status=COALESCE($4,status), result=COALESCE($5::jsonb,result),
1317
1509
  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')
1510
+ authorization_reconciliation=CASE WHEN $8::boolean THEN $9::jsonb ELSE authorization_reconciliation END,
1511
+ lease_owner=CASE WHEN $4 IN ('waiting_for_approval','completed','failed','blocked_by_policy','reconciliation_required','validation_failed')
1319
1512
  THEN NULL ELSE lease_owner END,
1320
- lease_expires_at=CASE WHEN $4 IN ('waiting_for_approval','completed','failed','blocked_by_policy','validation_failed')
1513
+ lease_expires_at=CASE WHEN $4 IN ('waiting_for_approval','completed','failed','blocked_by_policy','reconciliation_required','validation_failed')
1321
1514
  THEN NULL ELSE lease_expires_at END,
1322
1515
  updated_at=now()
1323
1516
  WHERE id=$1 AND tenant_id=$2 AND space_id=$3`,
@@ -1328,7 +1521,9 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
1328
1521
  patch.status ?? null,
1329
1522
  patch.result === void 0 ? null : JSON.stringify(patch.result),
1330
1523
  Object.hasOwn(patch, "error"),
1331
- patch.error ?? null
1524
+ patch.error ?? null,
1525
+ Object.hasOwn(patch, "authorizationReconciliation"),
1526
+ patch.authorizationReconciliation ? JSON.stringify(patch.authorizationReconciliation) : null
1332
1527
  ]
1333
1528
  );
1334
1529
  }
@@ -1523,8 +1718,8 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
1523
1718
  `INSERT INTO fabric_platform.asset_events
1524
1719
  (id,tenant_id,space_id,event_type,event_schema_version,subject_type,subject_id,
1525
1720
  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)
1721
+ correlation_id,causation_id,provenance)
1722
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11::jsonb,$12,$13,$14,$15,$16,$17::jsonb)
1528
1723
  ON CONFLICT (id) DO NOTHING`,
1529
1724
  [
1530
1725
  event.id,
@@ -1542,7 +1737,8 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
1542
1737
  event.occurredAt,
1543
1738
  event.recordedAt,
1544
1739
  event.correlationId,
1545
- event.causationId ?? null
1740
+ event.causationId ?? null,
1741
+ event.provenance ? JSON.stringify(event.provenance) : null
1546
1742
  ]
1547
1743
  );
1548
1744
  }
@@ -1553,13 +1749,13 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
1553
1749
  INSERT INTO fabric_platform.asset_events
1554
1750
  (id,tenant_id,space_id,event_type,event_schema_version,subject_type,subject_id,
1555
1751
  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)
1752
+ correlation_id,causation_id,provenance)
1753
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11::jsonb,$12,$13,$14,$15,$16,$17::jsonb)
1558
1754
  ON CONFLICT (id) DO NOTHING RETURNING id
1559
1755
  )
1560
1756
  INSERT INTO fabric_platform.event_outbox
1561
1757
  (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
1758
+ SELECT $1,$2,$3,$18::jsonb,'pending',0,$14,$14 FROM inserted_event
1563
1759
  ON CONFLICT (id) DO NOTHING`,
1564
1760
  [
1565
1761
  event.id,
@@ -1578,6 +1774,7 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
1578
1774
  event.recordedAt,
1579
1775
  event.correlationId,
1580
1776
  event.causationId ?? null,
1777
+ event.provenance ? JSON.stringify(event.provenance) : null,
1581
1778
  JSON.stringify(envelope)
1582
1779
  ]
1583
1780
  );
@@ -1738,6 +1935,14 @@ function toActionRecord(row) {
1738
1935
  correlationId: String(row.correlation_id),
1739
1936
  ...row.causation_id ? { causationId: String(row.causation_id) } : {},
1740
1937
  ...row.idempotency_key ? { idempotencyKey: String(row.idempotency_key) } : {},
1938
+ ...row.parameter_digest ? { parameterDigest: String(row.parameter_digest) } : {},
1939
+ ...row.parameter_digest_algorithm ? { parameterDigestAlgorithm: String(row.parameter_digest_algorithm) } : {},
1940
+ ...row.idempotency_actor_id ? { idempotencyActorId: String(row.idempotency_actor_id) } : {},
1941
+ ...row.idempotency_authorization_binding_id ? { idempotencyAuthorizationBindingId: String(row.idempotency_authorization_binding_id) } : {},
1942
+ ...row.invocation_provenance ? { provenance: row.invocation_provenance } : {},
1943
+ ...row.authorization_binding ? { authorizationBinding: row.authorization_binding } : {},
1944
+ ...row.execution_reason ? { executionReason: String(row.execution_reason) } : {},
1945
+ ...row.authorization_reconciliation ? { authorizationReconciliation: row.authorization_reconciliation } : {},
1741
1946
  ...row.authorization_binding_id ? { authorizationBindingId: String(row.authorization_binding_id) } : {},
1742
1947
  attemptCount: Number(row.attempt_count ?? 0),
1743
1948
  ...row.lease_owner ? { leaseOwner: String(row.lease_owner) } : {},
@@ -1811,7 +2016,8 @@ function toEventRecord(row) {
1811
2016
  occurredAt: new Date(row.occurred_at),
1812
2017
  recordedAt: new Date(row.recorded_at),
1813
2018
  correlationId: String(row.correlation_id),
1814
- ...row.causation_id ? { causationId: String(row.causation_id) } : {}
2019
+ ...row.causation_id ? { causationId: String(row.causation_id) } : {},
2020
+ ...row.provenance ? { provenance: row.provenance } : {}
1815
2021
  };
1816
2022
  }
1817
2023
  function toOutboxRecord(row) {
@@ -1832,6 +2038,7 @@ function toOutboxRecord(row) {
1832
2038
  ...event.actionInvocationId ? { actionInvocationId: String(event.actionInvocationId) } : {},
1833
2039
  correlationId: String(event.correlationId),
1834
2040
  ...event.causationId ? { causationId: String(event.causationId) } : {},
2041
+ ...event.provenance ? { provenance: event.provenance } : {},
1835
2042
  occurredAt: new Date(event.occurredAt),
1836
2043
  recordedAt: new Date(event.recordedAt),
1837
2044
  producerModuleVersion: String(event.producerModuleVersion),
@@ -1918,12 +2125,16 @@ async function abortableDelay(milliseconds, signal) {
1918
2125
  });
1919
2126
  }
1920
2127
 
2128
+ exports.IdempotencyConflictError = IdempotencyConflictError;
1921
2129
  exports.MemoryPlatformHostStore = MemoryPlatformHostStore;
2130
+ exports.PARAMETER_DIGEST_ALGORITHM = PARAMETER_DIGEST_ALGORITHM;
1922
2131
  exports.PLATFORM_HOST_CONTRACT_VERSION = PLATFORM_HOST_CONTRACT_VERSION;
1923
2132
  exports.PostgresPlatformHostStore = PostgresPlatformHostStore;
2133
+ exports.canonicalJson = canonicalJson;
1924
2134
  exports.cloneOutboxRecord = cloneOutboxRecord;
1925
2135
  exports.createGovernedActionHost = createGovernedActionHost;
1926
2136
  exports.createStoreBackedActionDispatcher = createStoreBackedActionDispatcher;
2137
+ exports.digestParameters = digestParameters;
1927
2138
  exports.runOutboxRelayCycle = runOutboxRelayCycle;
1928
2139
  exports.runPlatformActionWorker = runPlatformActionWorker;
1929
2140
  exports.runPlatformActionWorkerCycle = runPlatformActionWorkerCycle;