@fabricorg/platform-host 2.0.0 → 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/dist/index.js CHANGED
@@ -1,9 +1,66 @@
1
1
  import { AdapterRegistry, resolveAction, createFabricId, assertGovernanceRuntimeEvidence, FABRIC_GOVERNANCE_CONTRACT_VERSION, assertMutationGovernanceContext, evaluatePolicyDefinitions, aggregatePolicyOutcomes, validateTransition, executeWithAdapterRetry, resolveStateMachine } from '@fabricorg/platform';
2
+ import { createHash } from 'crypto';
2
3
 
3
4
  // src/host.ts
4
5
 
5
6
  // src/types.ts
6
7
  var PLATFORM_HOST_CONTRACT_VERSION = 2;
8
+ var IdempotencyConflictError = class extends Error {
9
+ constructor(conflict) {
10
+ super(`Idempotency key "${conflict.idempotencyKey}" conflicts with invocation ${conflict.existingInvocationId}: ${conflict.reasons.join(", ")}`);
11
+ this.conflict = conflict;
12
+ this.name = "IdempotencyConflictError";
13
+ }
14
+ conflict;
15
+ code = "IDEMPOTENCY_CONFLICT";
16
+ };
17
+ var PARAMETER_DIGEST_ALGORITHM = "fabric-canonical-json-sha256-v1";
18
+ function canonicalJson(value) {
19
+ return JSON.stringify(sort(value));
20
+ }
21
+ function sort(value) {
22
+ if (Array.isArray(value)) return value.map(sort);
23
+ if (value && typeof value === "object") {
24
+ return Object.fromEntries(Object.entries(value).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([key, child]) => [key, sort(child)]));
25
+ }
26
+ return value;
27
+ }
28
+ function digestParameters(parameters) {
29
+ return createHash("sha256").update(canonicalJson(parameters)).digest("hex");
30
+ }
31
+ var ATTRIBUTE_NAME = /^[a-z][a-z0-9-]*(?:\.[A-Za-z][A-Za-z0-9_-]*)+$/;
32
+ var INVOCATION_SOURCES = /* @__PURE__ */ new Set(["sdui", "api", "agent", "worker", "system"]);
33
+ function normalizeProvenance(input, options) {
34
+ if (!input) return void 0;
35
+ if (!INVOCATION_SOURCES.has(input.source)) throw new Error(`Invalid invocation provenance source: ${input.source}`);
36
+ const maxAttributes = options?.maxAttributes ?? 16;
37
+ const maxValueLength = options?.maxValueLength ?? 256;
38
+ const allowlist = new Set(options?.auditAttributeAllowlist ?? []);
39
+ const validate = (attributes, durable) => {
40
+ if (!attributes) return void 0;
41
+ const entries = Object.entries(attributes);
42
+ if (entries.length > maxAttributes) throw new Error(`Invocation provenance exceeds ${maxAttributes} attributes.`);
43
+ for (const [key, value] of entries) {
44
+ if (!ATTRIBUTE_NAME.test(key)) throw new Error(`Invalid provenance attribute name: ${key}`);
45
+ if (value.length > maxValueLength) throw new Error(`Invocation provenance attribute "${key}" exceeds ${maxValueLength} characters.`);
46
+ if (durable && !allowlist.has(key)) throw new Error(`Audit provenance attribute "${key}" is not allowlisted.`);
47
+ }
48
+ return Object.fromEntries(entries);
49
+ };
50
+ validate(input.traceAttributes, false);
51
+ const redactedAuditAttributes = input.auditAttributes ? options?.redactAuditAttributes?.({ ...input.auditAttributes }) ?? input.auditAttributes : void 0;
52
+ const auditAttributes = validate(redactedAuditAttributes, true);
53
+ options?.onTrace?.({
54
+ ...input,
55
+ ...auditAttributes ? { auditAttributes } : { auditAttributes: void 0 }
56
+ });
57
+ return {
58
+ source: input.source,
59
+ correlationId: input.correlationId,
60
+ ...input.causationId ? { causationId: input.causationId } : {},
61
+ ...auditAttributes ? { auditAttributes } : {}
62
+ };
63
+ }
7
64
 
8
65
  // src/host.ts
9
66
  var DEFAULT_EXTRACT_EVENTS = (data) => {
@@ -27,6 +84,9 @@ function createGovernedActionHost(options) {
27
84
  const actionResolver = options.resolveAction ?? resolveAction;
28
85
  const eventResultFields = options.eventResultFields ?? ["_events"];
29
86
  async function submitAction(input) {
87
+ if (input.executionReason && !["initial", "offline_replay"].includes(input.executionReason)) {
88
+ throw new Error(`Unsupported submitted execution reason: ${input.executionReason}`);
89
+ }
30
90
  const action = actionResolver(input.actionId);
31
91
  if (!action) throw new Error(`Unknown action: ${input.actionId}`);
32
92
  const authorizationInput = toAuthorizationInput(action, input);
@@ -37,8 +97,16 @@ function createGovernedActionHost(options) {
37
97
  throw new Error(`Actor ${input.actorId} is not authorized for action ${input.actionId}`);
38
98
  }
39
99
  const actionInvocationId = createFabricId("act");
40
- const correlationId = input.correlationId ?? createFabricId("corr");
100
+ if (input.correlationId && input.provenance?.correlationId && input.correlationId !== input.provenance.correlationId) throw new Error("Invocation provenance correlationId does not match submission correlationId.");
101
+ if (input.causationId && input.provenance?.causationId && input.causationId !== input.provenance.causationId) throw new Error("Invocation provenance causationId does not match submission causationId.");
102
+ const correlationId = input.correlationId ?? input.provenance?.correlationId ?? createFabricId("corr");
41
103
  const durableParameters = options.redactActionParameters ? options.redactActionParameters(input.actionId, input.parameters) : input.parameters;
104
+ const parameterDigest = digestParameters(input.parameters);
105
+ if (input.authorizationBinding) validateAuthorizationBinding(input, parameterDigest, action.execution?.authorityMoment, now());
106
+ const durableProvenance = normalizeProvenance(
107
+ input.provenance,
108
+ options.provenance
109
+ );
42
110
  const runtimeEvidence = {
43
111
  governanceContractVersion: FABRIC_GOVERNANCE_CONTRACT_VERSION,
44
112
  hostContractVersion: PLATFORM_HOST_CONTRACT_VERSION,
@@ -58,12 +126,41 @@ function createGovernedActionHost(options) {
58
126
  result: {},
59
127
  runtimeEvidence,
60
128
  correlationId,
61
- ...input.causationId ? { causationId: input.causationId } : {},
129
+ ...input.causationId ?? input.provenance?.causationId ? { causationId: input.causationId ?? input.provenance?.causationId } : {},
62
130
  ...input.idempotencyKey ? { idempotencyKey: input.idempotencyKey } : {},
131
+ ...input.idempotencyKey ? {
132
+ parameterDigest,
133
+ parameterDigestAlgorithm: PARAMETER_DIGEST_ALGORITHM,
134
+ idempotencyActorId: input.actorId,
135
+ ...input.authorizationBindingId ? { idempotencyAuthorizationBindingId: input.authorizationBindingId } : {}
136
+ } : {},
137
+ ...durableProvenance ? { provenance: durableProvenance } : {},
138
+ ...input.authorizationBinding ? { authorizationBinding: input.authorizationBinding } : {},
139
+ ...input.executionReason ? { executionReason: input.executionReason } : {},
63
140
  ...input.authorizationBindingId ? { authorizationBindingId: input.authorizationBindingId } : {}
64
141
  });
142
+ if (durableInvocation.id !== actionInvocationId && input.idempotencyKey) {
143
+ const conflict = idempotencyConflict(durableInvocation, {
144
+ actorId: input.actorId,
145
+ authorizationBindingId: input.authorizationBindingId,
146
+ actionVersion: action.version,
147
+ parameterDigest,
148
+ idempotencyKey: input.idempotencyKey
149
+ });
150
+ if (!durableInvocation.parameterDigest) {
151
+ options.idempotency?.onLegacyRecord?.(durableInvocation);
152
+ const enforceableLegacyConflict = conflict && { ...conflict, reasons: conflict.reasons.filter((reason) => reason !== "parameters") };
153
+ if (enforceableLegacyConflict && enforceableLegacyConflict.reasons.length) {
154
+ options.idempotency?.onConflict?.(enforceableLegacyConflict);
155
+ if ((options.idempotency?.conflictMode ?? "enforce") === "enforce") throw new IdempotencyConflictError(enforceableLegacyConflict);
156
+ }
157
+ } else if (conflict) {
158
+ options.idempotency?.onConflict?.(conflict);
159
+ if ((options.idempotency?.conflictMode ?? "enforce") === "enforce") throw new IdempotencyConflictError(conflict);
160
+ }
161
+ }
65
162
  const durableWorkflowId = `action-invocation-${durableInvocation.id}`;
66
- if (durableInvocation.id !== actionInvocationId && isTerminal(durableInvocation.status)) {
163
+ if (durableInvocation.id !== actionInvocationId) {
67
164
  return {
68
165
  actionInvocationId: durableInvocation.id,
69
166
  status: durableInvocation.status,
@@ -71,7 +168,8 @@ function createGovernedActionHost(options) {
71
168
  result: durableInvocation.result,
72
169
  ...durableInvocation.error ? { error: durableInvocation.error } : {},
73
170
  ...durableInvocation.hitlRoute ? { hitlRoute: durableInvocation.hitlRoute } : {},
74
- ...durableInvocation.hitlRiskTier ? { hitlRiskTier: durableInvocation.hitlRiskTier } : {}
171
+ ...durableInvocation.hitlRiskTier ? { hitlRiskTier: durableInvocation.hitlRiskTier } : {},
172
+ ...durableInvocation.authorizationReconciliation ? { reconciliation: durableInvocation.authorizationReconciliation } : {}
75
173
  };
76
174
  }
77
175
  if (options.dispatcher) {
@@ -219,7 +317,7 @@ function createGovernedActionHost(options) {
219
317
  }
220
318
  }
221
319
  const authorizationInput = toAuthorizationInput(action, invocation);
222
- const executionReason = executionReasonOverride ?? (invocation.attemptCount > 1 || resumingRunningInvocation && invocation.attemptCount === 0 ? "recovery" : "initial");
320
+ const executionReason = executionReasonOverride ?? (invocation.attemptCount > 1 || resumingRunningInvocation && invocation.attemptCount === 0 ? "recovery" : invocation.executionReason ?? "initial");
223
321
  if (!await options.authorization.checkEntitlement(authorizationInput)) {
224
322
  return fail(
225
323
  invocation,
@@ -227,7 +325,32 @@ function createGovernedActionHost(options) {
227
325
  `Module "${action.namespace}" is no longer enabled for tenant ${tenantId}`
228
326
  );
229
327
  }
230
- const executionAuthorized = options.authorization.authorizeExecution ? await options.authorization.authorizeExecution({
328
+ const authorityMoment = action.execution?.authorityMoment;
329
+ if (authorityMoment === "capture" && !invocation.authorizationBinding) {
330
+ const reconciliation = {
331
+ kind: "authorization_missing_capture_evidence",
332
+ governingMoment: authorityMoment,
333
+ executionReason,
334
+ ...invocation.provenance ? { provenance: invocation.provenance } : {},
335
+ message: `Action ${action.actionId} requires durable capture-time authorization evidence`
336
+ };
337
+ await options.store.updateActionInvocation(actionInvocationId, tenantId, spaceId, { status: "reconciliation_required", error: reconciliation.message, authorizationReconciliation: reconciliation });
338
+ return { actionInvocationId, status: "reconciliation_required", error: reconciliation.message, reconciliation };
339
+ }
340
+ const bindingExpired = authorityMoment !== "capture" && invocation.authorizationBinding?.expiresAt !== void 0 && Date.parse(invocation.authorizationBinding.expiresAt) <= now().getTime();
341
+ if (bindingExpired) {
342
+ const reconciliation = {
343
+ kind: "authorization_expired",
344
+ governingMoment: authorityMoment ?? "both",
345
+ executionReason,
346
+ ...invocation.authorizationBindingId ? { authorizationBindingId: invocation.authorizationBindingId } : {},
347
+ ...invocation.provenance ? { provenance: invocation.provenance } : {},
348
+ message: `Authorization binding for action ${action.actionId} expired before execution`
349
+ };
350
+ await options.store.updateActionInvocation(actionInvocationId, tenantId, spaceId, { status: "reconciliation_required", error: reconciliation.message, authorizationReconciliation: reconciliation });
351
+ return { actionInvocationId, status: "reconciliation_required", error: reconciliation.message, reconciliation };
352
+ }
353
+ const executionAuthorized = authorityMoment === "capture" ? invocation.authorizationBinding !== void 0 : options.authorization.authorizeExecution ? await options.authorization.authorizeExecution({
231
354
  ...authorizationInput,
232
355
  actionInvocationId,
233
356
  parameters: parsed.data,
@@ -235,11 +358,19 @@ function createGovernedActionHost(options) {
235
358
  executionReason
236
359
  }) : await options.authorization.authorize(authorizationInput);
237
360
  if (!executionAuthorized) {
238
- return fail(
239
- invocation,
240
- "failed",
241
- `Actor ${invocation.actorId} is not authorized to execute action ${action.actionId}`
242
- );
361
+ if (!action.execution) {
362
+ return fail(invocation, "failed", `Actor ${invocation.actorId} is not authorized to execute action ${action.actionId}`);
363
+ }
364
+ const reconciliation = {
365
+ kind: "authorization_denied",
366
+ governingMoment: action.execution?.authorityMoment ?? "both",
367
+ executionReason,
368
+ ...invocation.authorizationBindingId ? { authorizationBindingId: invocation.authorizationBindingId } : {},
369
+ ...invocation.provenance ? { provenance: invocation.provenance } : {},
370
+ message: `Actor ${invocation.actorId} is not authorized to execute action ${action.actionId}`
371
+ };
372
+ await options.store.updateActionInvocation(actionInvocationId, tenantId, spaceId, { status: "reconciliation_required", error: reconciliation.message, authorizationReconciliation: reconciliation });
373
+ return { actionInvocationId, status: "reconciliation_required", error: reconciliation.message, reconciliation };
243
374
  }
244
375
  const definitions = options.resolvePolicies ? await options.resolvePolicies({
245
376
  ...authorizationInput,
@@ -326,6 +457,9 @@ function createGovernedActionHost(options) {
326
457
  let domainEvents = [];
327
458
  try {
328
459
  const runHandler = async (db, transaction) => {
460
+ if (options.outbox && !transaction?.appendEventWithOutbox) {
461
+ throw new Error("Outbox egress requires transactionWithEvents to provide appendEventWithOutbox before handler execution.");
462
+ }
329
463
  const handlerResult = action.handler ? await action.handler(
330
464
  {
331
465
  actionInvocationId,
@@ -335,6 +469,7 @@ function createGovernedActionHost(options) {
335
469
  actorType: invocation.actorType,
336
470
  correlationId: invocation.correlationId,
337
471
  ...invocation.causationId ? { causationId: invocation.causationId } : {},
472
+ ...invocation.provenance ? { provenance: invocation.provenance } : {},
338
473
  db,
339
474
  services: options.services
340
475
  },
@@ -689,13 +824,22 @@ function createGovernedActionHost(options) {
689
824
  occurredAt: timestamp,
690
825
  recordedAt: timestamp,
691
826
  correlationId: invocation.correlationId,
692
- ...invocation.causationId ? { causationId: invocation.causationId } : {}
827
+ ...invocation.causationId ? { causationId: invocation.causationId } : {},
828
+ ...invocation.provenance ? { provenance: { source: invocation.provenance.source, ...invocation.provenance.auditAttributes ? { auditAttributes: invocation.provenance.auditAttributes } : {} } } : {}
693
829
  };
694
830
  if (options.outbox) {
831
+ const hostLifecycleEvent = (/* @__PURE__ */ new Set(["AdapterInvocationStarted", "AdapterInvocationSucceeded", "AdapterInvocationFailed", "ComplianceBlocked"])).has(envelope.eventType);
832
+ const shouldPublish = options.outbox.shouldPublish?.(envelope) ?? !hostLifecycleEvent;
833
+ if (!shouldPublish) {
834
+ await (transaction ?? options.store).appendEvent(envelope);
835
+ return;
836
+ }
695
837
  const traceContext = options.outbox.traceContext?.(envelope);
838
+ const payloadClassification = options.outbox.classifyPayload(envelope);
696
839
  const metadata = {
697
840
  producerModuleVersion: options.outbox.producerModuleVersion(invocation.actionId, invocation.actionVersion),
698
- payloadClassification: options.outbox.classifyPayload(envelope),
841
+ payloadClassification,
842
+ includeProvenance: options.outbox.includeProvenance?.(envelope, payloadClassification) ?? payloadClassification !== "restricted",
699
843
  ...traceContext ? { traceContext } : {}
700
844
  };
701
845
  if (transaction) {
@@ -734,7 +878,8 @@ function actionResult(invocation) {
734
878
  result: invocation.result,
735
879
  ...invocation.error ? { error: invocation.error } : {},
736
880
  ...invocation.hitlRoute ? { hitlRoute: invocation.hitlRoute } : {},
737
- ...invocation.hitlRiskTier ? { hitlRiskTier: invocation.hitlRiskTier } : {}
881
+ ...invocation.hitlRiskTier ? { hitlRiskTier: invocation.hitlRiskTier } : {},
882
+ ...invocation.authorizationReconciliation ? { reconciliation: invocation.authorizationReconciliation } : {}
738
883
  };
739
884
  }
740
885
  function asApprovalStore(store) {
@@ -787,7 +932,7 @@ function initialState(entityType) {
787
932
  return Object.values(machine?.states ?? {}).find((state) => state.stateClass === "initial")?.id ?? "none";
788
933
  }
789
934
  function isTerminal(status) {
790
- return ["completed", "failed", "blocked_by_policy", "validation_failed"].includes(status);
935
+ return ["completed", "failed", "blocked_by_policy", "reconciliation_required", "validation_failed"].includes(status);
791
936
  }
792
937
  function withoutPrivateHostFields(data, eventResultFields) {
793
938
  return Object.fromEntries(
@@ -804,6 +949,30 @@ function lifecycleId(prefix, invocationId, key) {
804
949
  const safeKey = key.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 96);
805
950
  return `${prefix}_${invocationId}_${safeKey}`;
806
951
  }
952
+ function validateAuthorizationBinding(input, parameterDigest, authorityMoment, currentTime = /* @__PURE__ */ new Date()) {
953
+ const binding = input.authorizationBinding;
954
+ if (binding.id !== input.authorizationBindingId && input.authorizationBindingId) throw new Error("Authorization binding ID does not match authorizationBindingId.");
955
+ if (binding.tenantId !== input.tenantId || binding.actorId !== input.actorId || binding.actionId !== input.actionId || binding.parameterDigest !== parameterDigest) {
956
+ throw new Error("Authorization binding does not match the submitted command identity.");
957
+ }
958
+ if (authorityMoment && binding.governingMoment !== authorityMoment) throw new Error("Authorization binding governingMoment does not match the action execution contract.");
959
+ const capturedAt = Date.parse(binding.capturedAt);
960
+ const expiresAt = binding.expiresAt === void 0 ? void 0 : Date.parse(binding.expiresAt);
961
+ if (!Number.isFinite(capturedAt) || expiresAt !== void 0 && !Number.isFinite(expiresAt)) {
962
+ throw new Error("Authorization binding timestamps must be valid ISO-8601 values.");
963
+ }
964
+ if (expiresAt !== void 0 && (expiresAt <= capturedAt || authorityMoment === "capture" && expiresAt <= currentTime.getTime())) {
965
+ throw new Error("Authorization binding must be unexpired at capture and expire after capturedAt.");
966
+ }
967
+ }
968
+ function idempotencyConflict(existing, incoming) {
969
+ const reasons = [];
970
+ if ((existing.idempotencyActorId ?? existing.actorId) !== incoming.actorId) reasons.push("actor");
971
+ if ((existing.idempotencyAuthorizationBindingId ?? existing.authorizationBindingId) !== incoming.authorizationBindingId) reasons.push("authority_binding");
972
+ if (existing.actionVersion !== incoming.actionVersion) reasons.push("action_version");
973
+ if (existing.parameterDigest !== incoming.parameterDigest) reasons.push("parameters");
974
+ return reasons.length ? { code: "IDEMPOTENCY_CONFLICT", idempotencyKey: incoming.idempotencyKey, existingInvocationId: existing.id, reasons } : void 0;
975
+ }
807
976
 
808
977
  // src/outbox.ts
809
978
  function toEnterpriseEventEnvelope(event, metadata) {
@@ -824,6 +993,7 @@ function toEnterpriseEventEnvelope(event, metadata) {
824
993
  producerModuleVersion: metadata.producerModuleVersion,
825
994
  payloadClassification: metadata.payloadClassification,
826
995
  ...metadata.traceContext ? { traceContext: { ...metadata.traceContext } } : {},
996
+ ...metadata.includeProvenance && event.provenance ? { provenance: event.provenance } : {},
827
997
  payload: event.payload
828
998
  };
829
999
  }
@@ -847,13 +1017,13 @@ async function runOutboxRelayCycle(options) {
847
1017
  continue;
848
1018
  }
849
1019
  result.published += 1;
850
- } catch (error) {
1020
+ } catch {
851
1021
  const deadLetter = record.attemptCount >= maxAttempts;
852
1022
  try {
853
1023
  await options.store.markOutboxFailed({
854
1024
  id: record.id,
855
1025
  workerId: options.workerId,
856
- error: error instanceof Error ? error.message : String(error),
1026
+ error: "Event publisher failed",
857
1027
  availableAt: new Date(now().getTime() + (options.retryDelayMs?.(record.attemptCount) ?? 1e3)),
858
1028
  deadLetter
859
1029
  });
@@ -874,10 +1044,14 @@ function cloneOutboxRecord(record) {
874
1044
 
875
1045
  // src/memory-store.ts
876
1046
  var MemoryPlatformHostStore = class {
877
- constructor(db) {
1047
+ constructor(db, transactionProvider) {
878
1048
  this.db = db;
1049
+ this.transactionalOutbox = transactionProvider !== void 0;
1050
+ if (transactionProvider) this.transactionWithEvents = (run) => this.runTransactionWithEvents(run, transactionProvider);
879
1051
  }
880
1052
  db;
1053
+ transactionalOutbox;
1054
+ transactionTail = Promise.resolve();
881
1055
  invocations = [];
882
1056
  policyEvaluations = [];
883
1057
  adapterInvocations = [];
@@ -889,6 +1063,50 @@ var MemoryPlatformHostStore = class {
889
1063
  async transaction(run) {
890
1064
  return run(this.db);
891
1065
  }
1066
+ async runTransactionWithEvents(run, transactionProvider) {
1067
+ let release;
1068
+ const previous = this.transactionTail;
1069
+ this.transactionTail = new Promise((resolve) => {
1070
+ release = resolve;
1071
+ });
1072
+ await previous;
1073
+ let domainSnapshot;
1074
+ let snapshotCreated = false;
1075
+ try {
1076
+ domainSnapshot = transactionProvider.snapshot(this.db);
1077
+ snapshotCreated = true;
1078
+ const pendingEvents = [];
1079
+ const pendingUpdates = [];
1080
+ const appendPending = async (event, metadata) => {
1081
+ if (this.events.some((candidate) => candidate.id === event.id) || pendingEvents.some((candidate) => candidate.event.id === event.id)) return;
1082
+ pendingEvents.push({ event, ...metadata ? { metadata } : {} });
1083
+ };
1084
+ const result = await run({
1085
+ db: this.db,
1086
+ appendEvent: (event) => appendPending(event),
1087
+ appendEventWithOutbox: (event, metadata) => appendPending(event, metadata),
1088
+ nextEventSequence: async (tenantId, spaceId) => (await this.listEvents(tenantId, spaceId)).length + pendingEvents.filter((candidate) => candidate.event.tenantId === tenantId && candidate.event.spaceId === spaceId).length + 1,
1089
+ listEvents: async (tenantId, spaceId) => [...await this.listEvents(tenantId, spaceId), ...pendingEvents.map((candidate) => candidate.event).filter((event) => event.tenantId === tenantId && event.spaceId === spaceId)],
1090
+ updateActionInvocation: async (id, tenantId, spaceId, patch) => {
1091
+ pendingUpdates.push({ id, tenantId, spaceId, patch });
1092
+ }
1093
+ });
1094
+ for (const update of pendingUpdates) {
1095
+ if (!await this.getActionInvocation(update.id, update.tenantId, update.spaceId)) throw new Error(`ActionInvocation not found: ${update.id}`);
1096
+ }
1097
+ for (const pending of pendingEvents) {
1098
+ if (pending.metadata) await this.appendEventWithOutbox(pending.event, pending.metadata);
1099
+ else await this.appendEvent(pending.event);
1100
+ }
1101
+ for (const update of pendingUpdates) await this.updateActionInvocation(update.id, update.tenantId, update.spaceId, update.patch);
1102
+ return result;
1103
+ } catch (error) {
1104
+ if (snapshotCreated) transactionProvider.restore(this.db, domainSnapshot);
1105
+ throw error;
1106
+ } finally {
1107
+ release();
1108
+ }
1109
+ }
892
1110
  async createActionInvocation(input) {
893
1111
  if (input.idempotencyKey) {
894
1112
  const existing = this.invocations.find(
@@ -910,7 +1128,7 @@ var MemoryPlatformHostStore = class {
910
1128
  const record = await this.getActionInvocation(id, tenantId, spaceId);
911
1129
  if (!record) throw new Error(`ActionInvocation not found: ${id}`);
912
1130
  Object.assign(record, patch, { updatedAt: /* @__PURE__ */ new Date() });
913
- if (patch.status === "waiting_for_approval" || patch.status === "completed" || patch.status === "failed" || patch.status === "blocked_by_policy" || patch.status === "validation_failed") {
1131
+ 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") {
914
1132
  delete record.leaseOwner;
915
1133
  delete record.leaseExpiresAt;
916
1134
  }
@@ -1122,6 +1340,9 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
1122
1340
  actor_id text NOT NULL, actor_type text NOT NULL, status text NOT NULL,
1123
1341
  parameters jsonb NOT NULL, result jsonb NOT NULL DEFAULT '{}'::jsonb,
1124
1342
  correlation_id text NOT NULL, causation_id text, idempotency_key text,
1343
+ parameter_digest text, parameter_digest_algorithm text, idempotency_actor_id text,
1344
+ idempotency_authorization_binding_id text, invocation_provenance jsonb,
1345
+ authorization_binding jsonb, execution_reason text, authorization_reconciliation jsonb,
1125
1346
  authorization_binding_id text, error text,
1126
1347
  attempt_count integer NOT NULL DEFAULT 0, lease_owner text,
1127
1348
  lease_expires_at timestamptz, hitl_route text, hitl_risk_tier text,
@@ -1131,6 +1352,14 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
1131
1352
  );
1132
1353
  ALTER TABLE fabric_platform.action_invocations
1133
1354
  ADD COLUMN IF NOT EXISTS idempotency_key text,
1355
+ ADD COLUMN IF NOT EXISTS parameter_digest text,
1356
+ ADD COLUMN IF NOT EXISTS parameter_digest_algorithm text,
1357
+ ADD COLUMN IF NOT EXISTS idempotency_actor_id text,
1358
+ ADD COLUMN IF NOT EXISTS idempotency_authorization_binding_id text,
1359
+ ADD COLUMN IF NOT EXISTS invocation_provenance jsonb,
1360
+ ADD COLUMN IF NOT EXISTS authorization_binding jsonb,
1361
+ ADD COLUMN IF NOT EXISTS execution_reason text,
1362
+ ADD COLUMN IF NOT EXISTS authorization_reconciliation jsonb,
1134
1363
  ADD COLUMN IF NOT EXISTS authorization_binding_id text,
1135
1364
  ADD COLUMN IF NOT EXISTS attempt_count integer NOT NULL DEFAULT 0,
1136
1365
  ADD COLUMN IF NOT EXISTS lease_owner text,
@@ -1147,6 +1376,12 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
1147
1376
  ON fabric_platform.action_invocations
1148
1377
  (tenant_id, space_id, action_id, idempotency_key)
1149
1378
  WHERE idempotency_key IS NOT NULL;
1379
+ CREATE INDEX IF NOT EXISTS action_invocations_parameter_digest_idx
1380
+ ON fabric_platform.action_invocations (tenant_id, parameter_digest)
1381
+ WHERE parameter_digest IS NOT NULL;
1382
+ CREATE INDEX IF NOT EXISTS action_invocations_authority_binding_idx
1383
+ ON fabric_platform.action_invocations (tenant_id, authorization_binding_id)
1384
+ WHERE authorization_binding_id IS NOT NULL;
1150
1385
  CREATE INDEX IF NOT EXISTS action_invocations_worker_idx
1151
1386
  ON fabric_platform.action_invocations (status, lease_expires_at, created_at);
1152
1387
  CREATE TABLE IF NOT EXISTS fabric_platform.policy_evaluations (
@@ -1190,9 +1425,10 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
1190
1425
  actor_id text NOT NULL, actor_type text NOT NULL, action_invocation_id text,
1191
1426
  payload jsonb NOT NULL, sequence bigint NOT NULL,
1192
1427
  occurred_at timestamptz NOT NULL, recorded_at timestamptz NOT NULL,
1193
- correlation_id text NOT NULL, causation_id text,
1428
+ correlation_id text NOT NULL, causation_id text, provenance jsonb,
1194
1429
  UNIQUE (tenant_id, space_id, sequence)
1195
1430
  );
1431
+ ALTER TABLE fabric_platform.asset_events ADD COLUMN IF NOT EXISTS provenance jsonb;
1196
1432
  CREATE INDEX IF NOT EXISTS asset_events_subject_idx
1197
1433
  ON fabric_platform.asset_events
1198
1434
  (tenant_id, space_id, subject_type, subject_id, sequence);
@@ -1216,8 +1452,10 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
1216
1452
  `INSERT INTO fabric_platform.action_invocations
1217
1453
  (id,tenant_id,space_id,action_id,action_version,actor_id,actor_type,status,
1218
1454
  parameters,result,correlation_id,causation_id,idempotency_key,
1455
+ parameter_digest,parameter_digest_algorithm,idempotency_actor_id,
1456
+ idempotency_authorization_binding_id,invocation_provenance,authorization_binding,execution_reason,
1219
1457
  authorization_binding_id,error,runtime_evidence,created_at,updated_at)
1220
- VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9::jsonb,$10::jsonb,$11,$12,$13,$14,$15,$16::jsonb,$17,$17)
1458
+ 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)
1221
1459
  ON CONFLICT (tenant_id,space_id,action_id,idempotency_key)
1222
1460
  WHERE idempotency_key IS NOT NULL DO UPDATE SET id=fabric_platform.action_invocations.id
1223
1461
  RETURNING *`,
@@ -1235,6 +1473,13 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
1235
1473
  input.correlationId,
1236
1474
  input.causationId ?? null,
1237
1475
  input.idempotencyKey ?? null,
1476
+ input.parameterDigest ?? null,
1477
+ input.parameterDigestAlgorithm ?? null,
1478
+ input.idempotencyActorId ?? null,
1479
+ input.idempotencyAuthorizationBindingId ?? null,
1480
+ input.provenance ? JSON.stringify(input.provenance) : null,
1481
+ input.authorizationBinding ? JSON.stringify(input.authorizationBinding) : null,
1482
+ input.executionReason ?? null,
1238
1483
  input.authorizationBindingId ?? null,
1239
1484
  input.error ?? null,
1240
1485
  input.runtimeEvidence ? JSON.stringify(input.runtimeEvidence) : null,
@@ -1256,9 +1501,10 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
1256
1501
  `UPDATE fabric_platform.action_invocations SET
1257
1502
  status=COALESCE($4,status), result=COALESCE($5::jsonb,result),
1258
1503
  error=CASE WHEN $6::boolean THEN $7 ELSE error END,
1259
- lease_owner=CASE WHEN $4 IN ('waiting_for_approval','completed','failed','blocked_by_policy','validation_failed')
1504
+ authorization_reconciliation=CASE WHEN $8::boolean THEN $9::jsonb ELSE authorization_reconciliation END,
1505
+ lease_owner=CASE WHEN $4 IN ('waiting_for_approval','completed','failed','blocked_by_policy','reconciliation_required','validation_failed')
1260
1506
  THEN NULL ELSE lease_owner END,
1261
- lease_expires_at=CASE WHEN $4 IN ('waiting_for_approval','completed','failed','blocked_by_policy','validation_failed')
1507
+ lease_expires_at=CASE WHEN $4 IN ('waiting_for_approval','completed','failed','blocked_by_policy','reconciliation_required','validation_failed')
1262
1508
  THEN NULL ELSE lease_expires_at END,
1263
1509
  updated_at=now()
1264
1510
  WHERE id=$1 AND tenant_id=$2 AND space_id=$3`,
@@ -1269,7 +1515,9 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
1269
1515
  patch.status ?? null,
1270
1516
  patch.result === void 0 ? null : JSON.stringify(patch.result),
1271
1517
  Object.hasOwn(patch, "error"),
1272
- patch.error ?? null
1518
+ patch.error ?? null,
1519
+ Object.hasOwn(patch, "authorizationReconciliation"),
1520
+ patch.authorizationReconciliation ? JSON.stringify(patch.authorizationReconciliation) : null
1273
1521
  ]
1274
1522
  );
1275
1523
  }
@@ -1464,8 +1712,8 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
1464
1712
  `INSERT INTO fabric_platform.asset_events
1465
1713
  (id,tenant_id,space_id,event_type,event_schema_version,subject_type,subject_id,
1466
1714
  actor_id,actor_type,action_invocation_id,payload,sequence,occurred_at,recorded_at,
1467
- correlation_id,causation_id)
1468
- VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11::jsonb,$12,$13,$14,$15,$16)
1715
+ correlation_id,causation_id,provenance)
1716
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11::jsonb,$12,$13,$14,$15,$16,$17::jsonb)
1469
1717
  ON CONFLICT (id) DO NOTHING`,
1470
1718
  [
1471
1719
  event.id,
@@ -1483,7 +1731,8 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
1483
1731
  event.occurredAt,
1484
1732
  event.recordedAt,
1485
1733
  event.correlationId,
1486
- event.causationId ?? null
1734
+ event.causationId ?? null,
1735
+ event.provenance ? JSON.stringify(event.provenance) : null
1487
1736
  ]
1488
1737
  );
1489
1738
  }
@@ -1494,13 +1743,13 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
1494
1743
  INSERT INTO fabric_platform.asset_events
1495
1744
  (id,tenant_id,space_id,event_type,event_schema_version,subject_type,subject_id,
1496
1745
  actor_id,actor_type,action_invocation_id,payload,sequence,occurred_at,recorded_at,
1497
- correlation_id,causation_id)
1498
- VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11::jsonb,$12,$13,$14,$15,$16)
1746
+ correlation_id,causation_id,provenance)
1747
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11::jsonb,$12,$13,$14,$15,$16,$17::jsonb)
1499
1748
  ON CONFLICT (id) DO NOTHING RETURNING id
1500
1749
  )
1501
1750
  INSERT INTO fabric_platform.event_outbox
1502
1751
  (id,tenant_id,space_id,event,status,attempt_count,available_at,created_at)
1503
- SELECT $1,$2,$3,$17::jsonb,'pending',0,$14,$14 FROM inserted_event
1752
+ SELECT $1,$2,$3,$18::jsonb,'pending',0,$14,$14 FROM inserted_event
1504
1753
  ON CONFLICT (id) DO NOTHING`,
1505
1754
  [
1506
1755
  event.id,
@@ -1519,6 +1768,7 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
1519
1768
  event.recordedAt,
1520
1769
  event.correlationId,
1521
1770
  event.causationId ?? null,
1771
+ event.provenance ? JSON.stringify(event.provenance) : null,
1522
1772
  JSON.stringify(envelope)
1523
1773
  ]
1524
1774
  );
@@ -1679,6 +1929,14 @@ function toActionRecord(row) {
1679
1929
  correlationId: String(row.correlation_id),
1680
1930
  ...row.causation_id ? { causationId: String(row.causation_id) } : {},
1681
1931
  ...row.idempotency_key ? { idempotencyKey: String(row.idempotency_key) } : {},
1932
+ ...row.parameter_digest ? { parameterDigest: String(row.parameter_digest) } : {},
1933
+ ...row.parameter_digest_algorithm ? { parameterDigestAlgorithm: String(row.parameter_digest_algorithm) } : {},
1934
+ ...row.idempotency_actor_id ? { idempotencyActorId: String(row.idempotency_actor_id) } : {},
1935
+ ...row.idempotency_authorization_binding_id ? { idempotencyAuthorizationBindingId: String(row.idempotency_authorization_binding_id) } : {},
1936
+ ...row.invocation_provenance ? { provenance: row.invocation_provenance } : {},
1937
+ ...row.authorization_binding ? { authorizationBinding: row.authorization_binding } : {},
1938
+ ...row.execution_reason ? { executionReason: String(row.execution_reason) } : {},
1939
+ ...row.authorization_reconciliation ? { authorizationReconciliation: row.authorization_reconciliation } : {},
1682
1940
  ...row.authorization_binding_id ? { authorizationBindingId: String(row.authorization_binding_id) } : {},
1683
1941
  attemptCount: Number(row.attempt_count ?? 0),
1684
1942
  ...row.lease_owner ? { leaseOwner: String(row.lease_owner) } : {},
@@ -1752,7 +2010,8 @@ function toEventRecord(row) {
1752
2010
  occurredAt: new Date(row.occurred_at),
1753
2011
  recordedAt: new Date(row.recorded_at),
1754
2012
  correlationId: String(row.correlation_id),
1755
- ...row.causation_id ? { causationId: String(row.causation_id) } : {}
2013
+ ...row.causation_id ? { causationId: String(row.causation_id) } : {},
2014
+ ...row.provenance ? { provenance: row.provenance } : {}
1756
2015
  };
1757
2016
  }
1758
2017
  function toOutboxRecord(row) {
@@ -1773,6 +2032,7 @@ function toOutboxRecord(row) {
1773
2032
  ...event.actionInvocationId ? { actionInvocationId: String(event.actionInvocationId) } : {},
1774
2033
  correlationId: String(event.correlationId),
1775
2034
  ...event.causationId ? { causationId: String(event.causationId) } : {},
2035
+ ...event.provenance ? { provenance: event.provenance } : {},
1776
2036
  occurredAt: new Date(event.occurredAt),
1777
2037
  recordedAt: new Date(event.recordedAt),
1778
2038
  producerModuleVersion: String(event.producerModuleVersion),
@@ -1859,6 +2119,6 @@ async function abortableDelay(milliseconds, signal) {
1859
2119
  });
1860
2120
  }
1861
2121
 
1862
- export { MemoryPlatformHostStore, PLATFORM_HOST_CONTRACT_VERSION, PostgresPlatformHostStore, cloneOutboxRecord, createGovernedActionHost, createStoreBackedActionDispatcher, runOutboxRelayCycle, runPlatformActionWorker, runPlatformActionWorkerCycle, toEnterpriseEventEnvelope };
2122
+ export { IdempotencyConflictError, MemoryPlatformHostStore, PARAMETER_DIGEST_ALGORITHM, PLATFORM_HOST_CONTRACT_VERSION, PostgresPlatformHostStore, canonicalJson, cloneOutboxRecord, createGovernedActionHost, createStoreBackedActionDispatcher, digestParameters, runOutboxRelayCycle, runPlatformActionWorker, runPlatformActionWorkerCycle, toEnterpriseEventEnvelope };
1863
2123
  //# sourceMappingURL=index.js.map
1864
2124
  //# sourceMappingURL=index.js.map