@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.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,
@@ -328,6 +459,9 @@ function createGovernedActionHost(options) {
328
459
  let domainEvents = [];
329
460
  try {
330
461
  const runHandler = async (db, transaction) => {
462
+ if (options.outbox && !transaction?.appendEventWithOutbox) {
463
+ throw new Error("Outbox egress requires transactionWithEvents to provide appendEventWithOutbox before handler execution.");
464
+ }
331
465
  const handlerResult = action.handler ? await action.handler(
332
466
  {
333
467
  actionInvocationId,
@@ -337,6 +471,7 @@ function createGovernedActionHost(options) {
337
471
  actorType: invocation.actorType,
338
472
  correlationId: invocation.correlationId,
339
473
  ...invocation.causationId ? { causationId: invocation.causationId } : {},
474
+ ...invocation.provenance ? { provenance: invocation.provenance } : {},
340
475
  db,
341
476
  services: options.services
342
477
  },
@@ -691,13 +826,22 @@ function createGovernedActionHost(options) {
691
826
  occurredAt: timestamp,
692
827
  recordedAt: timestamp,
693
828
  correlationId: invocation.correlationId,
694
- ...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 } : {} } } : {}
695
831
  };
696
832
  if (options.outbox) {
833
+ const hostLifecycleEvent = (/* @__PURE__ */ new Set(["AdapterInvocationStarted", "AdapterInvocationSucceeded", "AdapterInvocationFailed", "ComplianceBlocked"])).has(envelope.eventType);
834
+ const shouldPublish = options.outbox.shouldPublish?.(envelope) ?? !hostLifecycleEvent;
835
+ if (!shouldPublish) {
836
+ await (transaction ?? options.store).appendEvent(envelope);
837
+ return;
838
+ }
697
839
  const traceContext = options.outbox.traceContext?.(envelope);
840
+ const payloadClassification = options.outbox.classifyPayload(envelope);
698
841
  const metadata = {
699
842
  producerModuleVersion: options.outbox.producerModuleVersion(invocation.actionId, invocation.actionVersion),
700
- payloadClassification: options.outbox.classifyPayload(envelope),
843
+ payloadClassification,
844
+ includeProvenance: options.outbox.includeProvenance?.(envelope, payloadClassification) ?? payloadClassification !== "restricted",
701
845
  ...traceContext ? { traceContext } : {}
702
846
  };
703
847
  if (transaction) {
@@ -736,7 +880,8 @@ function actionResult(invocation) {
736
880
  result: invocation.result,
737
881
  ...invocation.error ? { error: invocation.error } : {},
738
882
  ...invocation.hitlRoute ? { hitlRoute: invocation.hitlRoute } : {},
739
- ...invocation.hitlRiskTier ? { hitlRiskTier: invocation.hitlRiskTier } : {}
883
+ ...invocation.hitlRiskTier ? { hitlRiskTier: invocation.hitlRiskTier } : {},
884
+ ...invocation.authorizationReconciliation ? { reconciliation: invocation.authorizationReconciliation } : {}
740
885
  };
741
886
  }
742
887
  function asApprovalStore(store) {
@@ -789,7 +934,7 @@ function initialState(entityType) {
789
934
  return Object.values(machine?.states ?? {}).find((state) => state.stateClass === "initial")?.id ?? "none";
790
935
  }
791
936
  function isTerminal(status) {
792
- return ["completed", "failed", "blocked_by_policy", "validation_failed"].includes(status);
937
+ return ["completed", "failed", "blocked_by_policy", "reconciliation_required", "validation_failed"].includes(status);
793
938
  }
794
939
  function withoutPrivateHostFields(data, eventResultFields) {
795
940
  return Object.fromEntries(
@@ -806,6 +951,30 @@ function lifecycleId(prefix, invocationId, key) {
806
951
  const safeKey = key.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 96);
807
952
  return `${prefix}_${invocationId}_${safeKey}`;
808
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
+ }
809
978
 
810
979
  // src/outbox.ts
811
980
  function toEnterpriseEventEnvelope(event, metadata) {
@@ -826,6 +995,7 @@ function toEnterpriseEventEnvelope(event, metadata) {
826
995
  producerModuleVersion: metadata.producerModuleVersion,
827
996
  payloadClassification: metadata.payloadClassification,
828
997
  ...metadata.traceContext ? { traceContext: { ...metadata.traceContext } } : {},
998
+ ...metadata.includeProvenance && event.provenance ? { provenance: event.provenance } : {},
829
999
  payload: event.payload
830
1000
  };
831
1001
  }
@@ -849,13 +1019,13 @@ async function runOutboxRelayCycle(options) {
849
1019
  continue;
850
1020
  }
851
1021
  result.published += 1;
852
- } catch (error) {
1022
+ } catch {
853
1023
  const deadLetter = record.attemptCount >= maxAttempts;
854
1024
  try {
855
1025
  await options.store.markOutboxFailed({
856
1026
  id: record.id,
857
1027
  workerId: options.workerId,
858
- error: error instanceof Error ? error.message : String(error),
1028
+ error: "Event publisher failed",
859
1029
  availableAt: new Date(now().getTime() + (options.retryDelayMs?.(record.attemptCount) ?? 1e3)),
860
1030
  deadLetter
861
1031
  });
@@ -876,10 +1046,14 @@ function cloneOutboxRecord(record) {
876
1046
 
877
1047
  // src/memory-store.ts
878
1048
  var MemoryPlatformHostStore = class {
879
- constructor(db) {
1049
+ constructor(db, transactionProvider) {
880
1050
  this.db = db;
1051
+ this.transactionalOutbox = transactionProvider !== void 0;
1052
+ if (transactionProvider) this.transactionWithEvents = (run) => this.runTransactionWithEvents(run, transactionProvider);
881
1053
  }
882
1054
  db;
1055
+ transactionalOutbox;
1056
+ transactionTail = Promise.resolve();
883
1057
  invocations = [];
884
1058
  policyEvaluations = [];
885
1059
  adapterInvocations = [];
@@ -891,6 +1065,50 @@ var MemoryPlatformHostStore = class {
891
1065
  async transaction(run) {
892
1066
  return run(this.db);
893
1067
  }
1068
+ async runTransactionWithEvents(run, transactionProvider) {
1069
+ let release;
1070
+ const previous = this.transactionTail;
1071
+ this.transactionTail = new Promise((resolve) => {
1072
+ release = resolve;
1073
+ });
1074
+ await previous;
1075
+ let domainSnapshot;
1076
+ let snapshotCreated = false;
1077
+ try {
1078
+ domainSnapshot = transactionProvider.snapshot(this.db);
1079
+ snapshotCreated = true;
1080
+ const pendingEvents = [];
1081
+ const pendingUpdates = [];
1082
+ const appendPending = async (event, metadata) => {
1083
+ if (this.events.some((candidate) => candidate.id === event.id) || pendingEvents.some((candidate) => candidate.event.id === event.id)) return;
1084
+ pendingEvents.push({ event, ...metadata ? { metadata } : {} });
1085
+ };
1086
+ const result = await run({
1087
+ db: this.db,
1088
+ appendEvent: (event) => appendPending(event),
1089
+ appendEventWithOutbox: (event, metadata) => appendPending(event, metadata),
1090
+ nextEventSequence: async (tenantId, spaceId) => (await this.listEvents(tenantId, spaceId)).length + pendingEvents.filter((candidate) => candidate.event.tenantId === tenantId && candidate.event.spaceId === spaceId).length + 1,
1091
+ listEvents: async (tenantId, spaceId) => [...await this.listEvents(tenantId, spaceId), ...pendingEvents.map((candidate) => candidate.event).filter((event) => event.tenantId === tenantId && event.spaceId === spaceId)],
1092
+ updateActionInvocation: async (id, tenantId, spaceId, patch) => {
1093
+ pendingUpdates.push({ id, tenantId, spaceId, patch });
1094
+ }
1095
+ });
1096
+ for (const update of pendingUpdates) {
1097
+ if (!await this.getActionInvocation(update.id, update.tenantId, update.spaceId)) throw new Error(`ActionInvocation not found: ${update.id}`);
1098
+ }
1099
+ for (const pending of pendingEvents) {
1100
+ if (pending.metadata) await this.appendEventWithOutbox(pending.event, pending.metadata);
1101
+ else await this.appendEvent(pending.event);
1102
+ }
1103
+ for (const update of pendingUpdates) await this.updateActionInvocation(update.id, update.tenantId, update.spaceId, update.patch);
1104
+ return result;
1105
+ } catch (error) {
1106
+ if (snapshotCreated) transactionProvider.restore(this.db, domainSnapshot);
1107
+ throw error;
1108
+ } finally {
1109
+ release();
1110
+ }
1111
+ }
894
1112
  async createActionInvocation(input) {
895
1113
  if (input.idempotencyKey) {
896
1114
  const existing = this.invocations.find(
@@ -912,7 +1130,7 @@ var MemoryPlatformHostStore = class {
912
1130
  const record = await this.getActionInvocation(id, tenantId, spaceId);
913
1131
  if (!record) throw new Error(`ActionInvocation not found: ${id}`);
914
1132
  Object.assign(record, patch, { updatedAt: /* @__PURE__ */ new Date() });
915
- 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") {
916
1134
  delete record.leaseOwner;
917
1135
  delete record.leaseExpiresAt;
918
1136
  }
@@ -1124,6 +1342,9 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
1124
1342
  actor_id text NOT NULL, actor_type text NOT NULL, status text NOT NULL,
1125
1343
  parameters jsonb NOT NULL, result jsonb NOT NULL DEFAULT '{}'::jsonb,
1126
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,
1127
1348
  authorization_binding_id text, error text,
1128
1349
  attempt_count integer NOT NULL DEFAULT 0, lease_owner text,
1129
1350
  lease_expires_at timestamptz, hitl_route text, hitl_risk_tier text,
@@ -1133,6 +1354,14 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
1133
1354
  );
1134
1355
  ALTER TABLE fabric_platform.action_invocations
1135
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,
1136
1365
  ADD COLUMN IF NOT EXISTS authorization_binding_id text,
1137
1366
  ADD COLUMN IF NOT EXISTS attempt_count integer NOT NULL DEFAULT 0,
1138
1367
  ADD COLUMN IF NOT EXISTS lease_owner text,
@@ -1149,6 +1378,12 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
1149
1378
  ON fabric_platform.action_invocations
1150
1379
  (tenant_id, space_id, action_id, idempotency_key)
1151
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;
1152
1387
  CREATE INDEX IF NOT EXISTS action_invocations_worker_idx
1153
1388
  ON fabric_platform.action_invocations (status, lease_expires_at, created_at);
1154
1389
  CREATE TABLE IF NOT EXISTS fabric_platform.policy_evaluations (
@@ -1192,9 +1427,10 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
1192
1427
  actor_id text NOT NULL, actor_type text NOT NULL, action_invocation_id text,
1193
1428
  payload jsonb NOT NULL, sequence bigint NOT NULL,
1194
1429
  occurred_at timestamptz NOT NULL, recorded_at timestamptz NOT NULL,
1195
- correlation_id text NOT NULL, causation_id text,
1430
+ correlation_id text NOT NULL, causation_id text, provenance jsonb,
1196
1431
  UNIQUE (tenant_id, space_id, sequence)
1197
1432
  );
1433
+ ALTER TABLE fabric_platform.asset_events ADD COLUMN IF NOT EXISTS provenance jsonb;
1198
1434
  CREATE INDEX IF NOT EXISTS asset_events_subject_idx
1199
1435
  ON fabric_platform.asset_events
1200
1436
  (tenant_id, space_id, subject_type, subject_id, sequence);
@@ -1218,8 +1454,10 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
1218
1454
  `INSERT INTO fabric_platform.action_invocations
1219
1455
  (id,tenant_id,space_id,action_id,action_version,actor_id,actor_type,status,
1220
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,
1221
1459
  authorization_binding_id,error,runtime_evidence,created_at,updated_at)
1222
- 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)
1223
1461
  ON CONFLICT (tenant_id,space_id,action_id,idempotency_key)
1224
1462
  WHERE idempotency_key IS NOT NULL DO UPDATE SET id=fabric_platform.action_invocations.id
1225
1463
  RETURNING *`,
@@ -1237,6 +1475,13 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
1237
1475
  input.correlationId,
1238
1476
  input.causationId ?? null,
1239
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,
1240
1485
  input.authorizationBindingId ?? null,
1241
1486
  input.error ?? null,
1242
1487
  input.runtimeEvidence ? JSON.stringify(input.runtimeEvidence) : null,
@@ -1258,9 +1503,10 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
1258
1503
  `UPDATE fabric_platform.action_invocations SET
1259
1504
  status=COALESCE($4,status), result=COALESCE($5::jsonb,result),
1260
1505
  error=CASE WHEN $6::boolean THEN $7 ELSE error END,
1261
- 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')
1262
1508
  THEN NULL ELSE lease_owner END,
1263
- 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')
1264
1510
  THEN NULL ELSE lease_expires_at END,
1265
1511
  updated_at=now()
1266
1512
  WHERE id=$1 AND tenant_id=$2 AND space_id=$3`,
@@ -1271,7 +1517,9 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
1271
1517
  patch.status ?? null,
1272
1518
  patch.result === void 0 ? null : JSON.stringify(patch.result),
1273
1519
  Object.hasOwn(patch, "error"),
1274
- patch.error ?? null
1520
+ patch.error ?? null,
1521
+ Object.hasOwn(patch, "authorizationReconciliation"),
1522
+ patch.authorizationReconciliation ? JSON.stringify(patch.authorizationReconciliation) : null
1275
1523
  ]
1276
1524
  );
1277
1525
  }
@@ -1466,8 +1714,8 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
1466
1714
  `INSERT INTO fabric_platform.asset_events
1467
1715
  (id,tenant_id,space_id,event_type,event_schema_version,subject_type,subject_id,
1468
1716
  actor_id,actor_type,action_invocation_id,payload,sequence,occurred_at,recorded_at,
1469
- correlation_id,causation_id)
1470
- 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)
1471
1719
  ON CONFLICT (id) DO NOTHING`,
1472
1720
  [
1473
1721
  event.id,
@@ -1485,7 +1733,8 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
1485
1733
  event.occurredAt,
1486
1734
  event.recordedAt,
1487
1735
  event.correlationId,
1488
- event.causationId ?? null
1736
+ event.causationId ?? null,
1737
+ event.provenance ? JSON.stringify(event.provenance) : null
1489
1738
  ]
1490
1739
  );
1491
1740
  }
@@ -1496,13 +1745,13 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
1496
1745
  INSERT INTO fabric_platform.asset_events
1497
1746
  (id,tenant_id,space_id,event_type,event_schema_version,subject_type,subject_id,
1498
1747
  actor_id,actor_type,action_invocation_id,payload,sequence,occurred_at,recorded_at,
1499
- correlation_id,causation_id)
1500
- 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)
1501
1750
  ON CONFLICT (id) DO NOTHING RETURNING id
1502
1751
  )
1503
1752
  INSERT INTO fabric_platform.event_outbox
1504
1753
  (id,tenant_id,space_id,event,status,attempt_count,available_at,created_at)
1505
- 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
1506
1755
  ON CONFLICT (id) DO NOTHING`,
1507
1756
  [
1508
1757
  event.id,
@@ -1521,6 +1770,7 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
1521
1770
  event.recordedAt,
1522
1771
  event.correlationId,
1523
1772
  event.causationId ?? null,
1773
+ event.provenance ? JSON.stringify(event.provenance) : null,
1524
1774
  JSON.stringify(envelope)
1525
1775
  ]
1526
1776
  );
@@ -1681,6 +1931,14 @@ function toActionRecord(row) {
1681
1931
  correlationId: String(row.correlation_id),
1682
1932
  ...row.causation_id ? { causationId: String(row.causation_id) } : {},
1683
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 } : {},
1684
1942
  ...row.authorization_binding_id ? { authorizationBindingId: String(row.authorization_binding_id) } : {},
1685
1943
  attemptCount: Number(row.attempt_count ?? 0),
1686
1944
  ...row.lease_owner ? { leaseOwner: String(row.lease_owner) } : {},
@@ -1754,7 +2012,8 @@ function toEventRecord(row) {
1754
2012
  occurredAt: new Date(row.occurred_at),
1755
2013
  recordedAt: new Date(row.recorded_at),
1756
2014
  correlationId: String(row.correlation_id),
1757
- ...row.causation_id ? { causationId: String(row.causation_id) } : {}
2015
+ ...row.causation_id ? { causationId: String(row.causation_id) } : {},
2016
+ ...row.provenance ? { provenance: row.provenance } : {}
1758
2017
  };
1759
2018
  }
1760
2019
  function toOutboxRecord(row) {
@@ -1775,6 +2034,7 @@ function toOutboxRecord(row) {
1775
2034
  ...event.actionInvocationId ? { actionInvocationId: String(event.actionInvocationId) } : {},
1776
2035
  correlationId: String(event.correlationId),
1777
2036
  ...event.causationId ? { causationId: String(event.causationId) } : {},
2037
+ ...event.provenance ? { provenance: event.provenance } : {},
1778
2038
  occurredAt: new Date(event.occurredAt),
1779
2039
  recordedAt: new Date(event.recordedAt),
1780
2040
  producerModuleVersion: String(event.producerModuleVersion),
@@ -1861,12 +2121,16 @@ async function abortableDelay(milliseconds, signal) {
1861
2121
  });
1862
2122
  }
1863
2123
 
2124
+ exports.IdempotencyConflictError = IdempotencyConflictError;
1864
2125
  exports.MemoryPlatformHostStore = MemoryPlatformHostStore;
2126
+ exports.PARAMETER_DIGEST_ALGORITHM = PARAMETER_DIGEST_ALGORITHM;
1865
2127
  exports.PLATFORM_HOST_CONTRACT_VERSION = PLATFORM_HOST_CONTRACT_VERSION;
1866
2128
  exports.PostgresPlatformHostStore = PostgresPlatformHostStore;
2129
+ exports.canonicalJson = canonicalJson;
1867
2130
  exports.cloneOutboxRecord = cloneOutboxRecord;
1868
2131
  exports.createGovernedActionHost = createGovernedActionHost;
1869
2132
  exports.createStoreBackedActionDispatcher = createStoreBackedActionDispatcher;
2133
+ exports.digestParameters = digestParameters;
1870
2134
  exports.runOutboxRelayCycle = runOutboxRelayCycle;
1871
2135
  exports.runPlatformActionWorker = runPlatformActionWorker;
1872
2136
  exports.runPlatformActionWorkerCycle = runPlatformActionWorkerCycle;