@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/CHANGELOG.md +29 -0
- package/MIGRATION-2-IDEMPOTENCY.md +32 -0
- package/README.md +26 -5
- package/dist/index.cjs +244 -33
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +86 -8
- package/dist/index.d.ts +86 -8
- package/dist/index.js +242 -35
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
package/dist/index.js
CHANGED
|
@@ -1,9 +1,66 @@
|
|
|
1
|
-
import { AdapterRegistry,
|
|
1
|
+
import { AdapterRegistry, createFabricId, assertGovernanceRuntimeEvidence, FABRIC_GOVERNANCE_CONTRACT_VERSION, assertMutationGovernanceContext, evaluatePolicyDefinitions, aggregatePolicyOutcomes, validateStateMachineTransition, executeWithAdapterRetry } 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_SOURCE = /^(?:ui|sdui|api|agent|worker|system|[a-z][a-z0-9-]*(?:\.[a-z][a-z0-9-]*)+)$/;
|
|
33
|
+
function normalizeProvenance(input, options) {
|
|
34
|
+
if (!input) return void 0;
|
|
35
|
+
if (!INVOCATION_SOURCE.test(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) => {
|
|
@@ -24,9 +81,15 @@ function createGovernedActionHost(options) {
|
|
|
24
81
|
for (const adapter of options.adapters ?? []) adapters.register(adapter);
|
|
25
82
|
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
26
83
|
const extractEvents = options.extractEvents ?? DEFAULT_EXTRACT_EVENTS;
|
|
27
|
-
const
|
|
84
|
+
const configuredActionResolver = options.resolveAction ?? options.registry?.resolveAction;
|
|
85
|
+
if (!configuredActionResolver) throw new Error("Platform Host requires an explicit module registry or resolveAction function.");
|
|
86
|
+
const actionResolver = configuredActionResolver;
|
|
87
|
+
const stateMachineResolver = options.registry?.resolveStateMachine ?? (() => void 0);
|
|
28
88
|
const eventResultFields = options.eventResultFields ?? ["_events"];
|
|
29
89
|
async function submitAction(input) {
|
|
90
|
+
if (input.executionReason && !["initial", "offline_replay"].includes(input.executionReason)) {
|
|
91
|
+
throw new Error(`Unsupported submitted execution reason: ${input.executionReason}`);
|
|
92
|
+
}
|
|
30
93
|
const action = actionResolver(input.actionId);
|
|
31
94
|
if (!action) throw new Error(`Unknown action: ${input.actionId}`);
|
|
32
95
|
const authorizationInput = toAuthorizationInput(action, input);
|
|
@@ -37,8 +100,16 @@ function createGovernedActionHost(options) {
|
|
|
37
100
|
throw new Error(`Actor ${input.actorId} is not authorized for action ${input.actionId}`);
|
|
38
101
|
}
|
|
39
102
|
const actionInvocationId = createFabricId("act");
|
|
40
|
-
|
|
103
|
+
if (input.correlationId && input.provenance?.correlationId && input.correlationId !== input.provenance.correlationId) throw new Error("Invocation provenance correlationId does not match submission correlationId.");
|
|
104
|
+
if (input.causationId && input.provenance?.causationId && input.causationId !== input.provenance.causationId) throw new Error("Invocation provenance causationId does not match submission causationId.");
|
|
105
|
+
const correlationId = input.correlationId ?? input.provenance?.correlationId ?? createFabricId("corr");
|
|
41
106
|
const durableParameters = options.redactActionParameters ? options.redactActionParameters(input.actionId, input.parameters) : input.parameters;
|
|
107
|
+
const parameterDigest = digestParameters(input.parameters);
|
|
108
|
+
if (input.authorizationBinding) validateAuthorizationBinding(input, parameterDigest, action.execution?.authorityMoment, now());
|
|
109
|
+
const durableProvenance = normalizeProvenance(
|
|
110
|
+
input.provenance,
|
|
111
|
+
options.provenance
|
|
112
|
+
);
|
|
42
113
|
const runtimeEvidence = {
|
|
43
114
|
governanceContractVersion: FABRIC_GOVERNANCE_CONTRACT_VERSION,
|
|
44
115
|
hostContractVersion: PLATFORM_HOST_CONTRACT_VERSION,
|
|
@@ -58,12 +129,41 @@ function createGovernedActionHost(options) {
|
|
|
58
129
|
result: {},
|
|
59
130
|
runtimeEvidence,
|
|
60
131
|
correlationId,
|
|
61
|
-
...input.causationId ? { causationId: input.causationId } : {},
|
|
132
|
+
...input.causationId ?? input.provenance?.causationId ? { causationId: input.causationId ?? input.provenance?.causationId } : {},
|
|
62
133
|
...input.idempotencyKey ? { idempotencyKey: input.idempotencyKey } : {},
|
|
134
|
+
...input.idempotencyKey ? {
|
|
135
|
+
parameterDigest,
|
|
136
|
+
parameterDigestAlgorithm: PARAMETER_DIGEST_ALGORITHM,
|
|
137
|
+
idempotencyActorId: input.actorId,
|
|
138
|
+
...input.authorizationBindingId ? { idempotencyAuthorizationBindingId: input.authorizationBindingId } : {}
|
|
139
|
+
} : {},
|
|
140
|
+
...durableProvenance ? { provenance: durableProvenance } : {},
|
|
141
|
+
...input.authorizationBinding ? { authorizationBinding: input.authorizationBinding } : {},
|
|
142
|
+
...input.executionReason ? { executionReason: input.executionReason } : {},
|
|
63
143
|
...input.authorizationBindingId ? { authorizationBindingId: input.authorizationBindingId } : {}
|
|
64
144
|
});
|
|
145
|
+
if (durableInvocation.id !== actionInvocationId && input.idempotencyKey) {
|
|
146
|
+
const conflict = idempotencyConflict(durableInvocation, {
|
|
147
|
+
actorId: input.actorId,
|
|
148
|
+
authorizationBindingId: input.authorizationBindingId,
|
|
149
|
+
actionVersion: action.version,
|
|
150
|
+
parameterDigest,
|
|
151
|
+
idempotencyKey: input.idempotencyKey
|
|
152
|
+
});
|
|
153
|
+
if (!durableInvocation.parameterDigest) {
|
|
154
|
+
options.idempotency?.onLegacyRecord?.(durableInvocation);
|
|
155
|
+
const enforceableLegacyConflict = conflict && { ...conflict, reasons: conflict.reasons.filter((reason) => reason !== "parameters") };
|
|
156
|
+
if (enforceableLegacyConflict && enforceableLegacyConflict.reasons.length) {
|
|
157
|
+
options.idempotency?.onConflict?.(enforceableLegacyConflict);
|
|
158
|
+
if ((options.idempotency?.conflictMode ?? "enforce") === "enforce") throw new IdempotencyConflictError(enforceableLegacyConflict);
|
|
159
|
+
}
|
|
160
|
+
} else if (conflict) {
|
|
161
|
+
options.idempotency?.onConflict?.(conflict);
|
|
162
|
+
if ((options.idempotency?.conflictMode ?? "enforce") === "enforce") throw new IdempotencyConflictError(conflict);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
65
165
|
const durableWorkflowId = `action-invocation-${durableInvocation.id}`;
|
|
66
|
-
if (durableInvocation.id !== actionInvocationId
|
|
166
|
+
if (durableInvocation.id !== actionInvocationId) {
|
|
67
167
|
return {
|
|
68
168
|
actionInvocationId: durableInvocation.id,
|
|
69
169
|
status: durableInvocation.status,
|
|
@@ -71,7 +171,8 @@ function createGovernedActionHost(options) {
|
|
|
71
171
|
result: durableInvocation.result,
|
|
72
172
|
...durableInvocation.error ? { error: durableInvocation.error } : {},
|
|
73
173
|
...durableInvocation.hitlRoute ? { hitlRoute: durableInvocation.hitlRoute } : {},
|
|
74
|
-
...durableInvocation.hitlRiskTier ? { hitlRiskTier: durableInvocation.hitlRiskTier } : {}
|
|
174
|
+
...durableInvocation.hitlRiskTier ? { hitlRiskTier: durableInvocation.hitlRiskTier } : {},
|
|
175
|
+
...durableInvocation.authorizationReconciliation ? { reconciliation: durableInvocation.authorizationReconciliation } : {}
|
|
75
176
|
};
|
|
76
177
|
}
|
|
77
178
|
if (options.dispatcher) {
|
|
@@ -219,7 +320,7 @@ function createGovernedActionHost(options) {
|
|
|
219
320
|
}
|
|
220
321
|
}
|
|
221
322
|
const authorizationInput = toAuthorizationInput(action, invocation);
|
|
222
|
-
const executionReason = executionReasonOverride ?? (invocation.attemptCount > 1 || resumingRunningInvocation && invocation.attemptCount === 0 ? "recovery" : "initial");
|
|
323
|
+
const executionReason = executionReasonOverride ?? (invocation.attemptCount > 1 || resumingRunningInvocation && invocation.attemptCount === 0 ? "recovery" : invocation.executionReason ?? "initial");
|
|
223
324
|
if (!await options.authorization.checkEntitlement(authorizationInput)) {
|
|
224
325
|
return fail(
|
|
225
326
|
invocation,
|
|
@@ -227,7 +328,32 @@ function createGovernedActionHost(options) {
|
|
|
227
328
|
`Module "${action.namespace}" is no longer enabled for tenant ${tenantId}`
|
|
228
329
|
);
|
|
229
330
|
}
|
|
230
|
-
const
|
|
331
|
+
const authorityMoment = action.execution?.authorityMoment;
|
|
332
|
+
if (authorityMoment === "capture" && !invocation.authorizationBinding) {
|
|
333
|
+
const reconciliation = {
|
|
334
|
+
kind: "authorization_missing_capture_evidence",
|
|
335
|
+
governingMoment: authorityMoment,
|
|
336
|
+
executionReason,
|
|
337
|
+
...invocation.provenance ? { provenance: invocation.provenance } : {},
|
|
338
|
+
message: `Action ${action.actionId} requires durable capture-time authorization evidence`
|
|
339
|
+
};
|
|
340
|
+
await options.store.updateActionInvocation(actionInvocationId, tenantId, spaceId, { status: "reconciliation_required", error: reconciliation.message, authorizationReconciliation: reconciliation });
|
|
341
|
+
return { actionInvocationId, status: "reconciliation_required", error: reconciliation.message, reconciliation };
|
|
342
|
+
}
|
|
343
|
+
const bindingExpired = authorityMoment !== "capture" && invocation.authorizationBinding?.expiresAt !== void 0 && Date.parse(invocation.authorizationBinding.expiresAt) <= now().getTime();
|
|
344
|
+
if (bindingExpired) {
|
|
345
|
+
const reconciliation = {
|
|
346
|
+
kind: "authorization_expired",
|
|
347
|
+
governingMoment: authorityMoment ?? "both",
|
|
348
|
+
executionReason,
|
|
349
|
+
...invocation.authorizationBindingId ? { authorizationBindingId: invocation.authorizationBindingId } : {},
|
|
350
|
+
...invocation.provenance ? { provenance: invocation.provenance } : {},
|
|
351
|
+
message: `Authorization binding for action ${action.actionId} expired before execution`
|
|
352
|
+
};
|
|
353
|
+
await options.store.updateActionInvocation(actionInvocationId, tenantId, spaceId, { status: "reconciliation_required", error: reconciliation.message, authorizationReconciliation: reconciliation });
|
|
354
|
+
return { actionInvocationId, status: "reconciliation_required", error: reconciliation.message, reconciliation };
|
|
355
|
+
}
|
|
356
|
+
const executionAuthorized = authorityMoment === "capture" ? invocation.authorizationBinding !== void 0 : options.authorization.authorizeExecution ? await options.authorization.authorizeExecution({
|
|
231
357
|
...authorizationInput,
|
|
232
358
|
actionInvocationId,
|
|
233
359
|
parameters: parsed.data,
|
|
@@ -235,11 +361,19 @@ function createGovernedActionHost(options) {
|
|
|
235
361
|
executionReason
|
|
236
362
|
}) : await options.authorization.authorize(authorizationInput);
|
|
237
363
|
if (!executionAuthorized) {
|
|
238
|
-
|
|
239
|
-
invocation,
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
364
|
+
if (!action.execution) {
|
|
365
|
+
return fail(invocation, "failed", `Actor ${invocation.actorId} is not authorized to execute action ${action.actionId}`);
|
|
366
|
+
}
|
|
367
|
+
const reconciliation = {
|
|
368
|
+
kind: "authorization_denied",
|
|
369
|
+
governingMoment: action.execution?.authorityMoment ?? "both",
|
|
370
|
+
executionReason,
|
|
371
|
+
...invocation.authorizationBindingId ? { authorizationBindingId: invocation.authorizationBindingId } : {},
|
|
372
|
+
...invocation.provenance ? { provenance: invocation.provenance } : {},
|
|
373
|
+
message: `Actor ${invocation.actorId} is not authorized to execute action ${action.actionId}`
|
|
374
|
+
};
|
|
375
|
+
await options.store.updateActionInvocation(actionInvocationId, tenantId, spaceId, { status: "reconciliation_required", error: reconciliation.message, authorizationReconciliation: reconciliation });
|
|
376
|
+
return { actionInvocationId, status: "reconciliation_required", error: reconciliation.message, reconciliation };
|
|
243
377
|
}
|
|
244
378
|
const definitions = options.resolvePolicies ? await options.resolvePolicies({
|
|
245
379
|
...authorizationInput,
|
|
@@ -255,6 +389,7 @@ function createGovernedActionHost(options) {
|
|
|
255
389
|
parameters: parsed.data,
|
|
256
390
|
db: options.store.db,
|
|
257
391
|
services: options.services,
|
|
392
|
+
resolveCodePolicy: options.registry?.resolvePolicy,
|
|
258
393
|
mode: "execute",
|
|
259
394
|
now: now()
|
|
260
395
|
});
|
|
@@ -307,10 +442,11 @@ function createGovernedActionHost(options) {
|
|
|
307
442
|
spaceId,
|
|
308
443
|
binding.entityType,
|
|
309
444
|
entityId
|
|
310
|
-
) ?? initialState(binding.entityType) : initialState(binding.entityType);
|
|
445
|
+
) ?? initialState(stateMachineResolver(binding.entityType)) : initialState(stateMachineResolver(binding.entityType));
|
|
311
446
|
const targetState = typeof binding.targetState === "function" ? binding.targetState(parsed.data) : binding.targetState;
|
|
312
447
|
if (targetState !== "") {
|
|
313
|
-
const transition =
|
|
448
|
+
const transition = validateStateMachineTransition(
|
|
449
|
+
stateMachineResolver(binding.entityType),
|
|
314
450
|
binding.entityType,
|
|
315
451
|
currentState,
|
|
316
452
|
targetState,
|
|
@@ -338,6 +474,7 @@ function createGovernedActionHost(options) {
|
|
|
338
474
|
actorType: invocation.actorType,
|
|
339
475
|
correlationId: invocation.correlationId,
|
|
340
476
|
...invocation.causationId ? { causationId: invocation.causationId } : {},
|
|
477
|
+
...invocation.provenance ? { provenance: invocation.provenance } : {},
|
|
341
478
|
db,
|
|
342
479
|
services: options.services
|
|
343
480
|
},
|
|
@@ -692,7 +829,8 @@ function createGovernedActionHost(options) {
|
|
|
692
829
|
occurredAt: timestamp,
|
|
693
830
|
recordedAt: timestamp,
|
|
694
831
|
correlationId: invocation.correlationId,
|
|
695
|
-
...invocation.causationId ? { causationId: invocation.causationId } : {}
|
|
832
|
+
...invocation.causationId ? { causationId: invocation.causationId } : {},
|
|
833
|
+
...invocation.provenance ? { provenance: { source: invocation.provenance.source, ...invocation.provenance.auditAttributes ? { auditAttributes: invocation.provenance.auditAttributes } : {} } } : {}
|
|
696
834
|
};
|
|
697
835
|
if (options.outbox) {
|
|
698
836
|
const hostLifecycleEvent = (/* @__PURE__ */ new Set(["AdapterInvocationStarted", "AdapterInvocationSucceeded", "AdapterInvocationFailed", "ComplianceBlocked"])).has(envelope.eventType);
|
|
@@ -702,9 +840,11 @@ function createGovernedActionHost(options) {
|
|
|
702
840
|
return;
|
|
703
841
|
}
|
|
704
842
|
const traceContext = options.outbox.traceContext?.(envelope);
|
|
843
|
+
const payloadClassification = options.outbox.classifyPayload(envelope);
|
|
705
844
|
const metadata = {
|
|
706
845
|
producerModuleVersion: options.outbox.producerModuleVersion(invocation.actionId, invocation.actionVersion),
|
|
707
|
-
payloadClassification
|
|
846
|
+
payloadClassification,
|
|
847
|
+
includeProvenance: options.outbox.includeProvenance?.(envelope, payloadClassification) ?? payloadClassification !== "restricted",
|
|
708
848
|
...traceContext ? { traceContext } : {}
|
|
709
849
|
};
|
|
710
850
|
if (transaction) {
|
|
@@ -743,7 +883,8 @@ function actionResult(invocation) {
|
|
|
743
883
|
result: invocation.result,
|
|
744
884
|
...invocation.error ? { error: invocation.error } : {},
|
|
745
885
|
...invocation.hitlRoute ? { hitlRoute: invocation.hitlRoute } : {},
|
|
746
|
-
...invocation.hitlRiskTier ? { hitlRiskTier: invocation.hitlRiskTier } : {}
|
|
886
|
+
...invocation.hitlRiskTier ? { hitlRiskTier: invocation.hitlRiskTier } : {},
|
|
887
|
+
...invocation.authorizationReconciliation ? { reconciliation: invocation.authorizationReconciliation } : {}
|
|
747
888
|
};
|
|
748
889
|
}
|
|
749
890
|
function asApprovalStore(store) {
|
|
@@ -791,12 +932,11 @@ function declaredPolicies(policyIds) {
|
|
|
791
932
|
codeEvaluatorPolicyId: policyId
|
|
792
933
|
}));
|
|
793
934
|
}
|
|
794
|
-
function initialState(
|
|
795
|
-
const machine = resolveStateMachine(entityType);
|
|
935
|
+
function initialState(machine) {
|
|
796
936
|
return Object.values(machine?.states ?? {}).find((state) => state.stateClass === "initial")?.id ?? "none";
|
|
797
937
|
}
|
|
798
938
|
function isTerminal(status) {
|
|
799
|
-
return ["completed", "failed", "blocked_by_policy", "validation_failed"].includes(status);
|
|
939
|
+
return ["completed", "failed", "blocked_by_policy", "reconciliation_required", "validation_failed"].includes(status);
|
|
800
940
|
}
|
|
801
941
|
function withoutPrivateHostFields(data, eventResultFields) {
|
|
802
942
|
return Object.fromEntries(
|
|
@@ -813,6 +953,30 @@ function lifecycleId(prefix, invocationId, key) {
|
|
|
813
953
|
const safeKey = key.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 96);
|
|
814
954
|
return `${prefix}_${invocationId}_${safeKey}`;
|
|
815
955
|
}
|
|
956
|
+
function validateAuthorizationBinding(input, parameterDigest, authorityMoment, currentTime = /* @__PURE__ */ new Date()) {
|
|
957
|
+
const binding = input.authorizationBinding;
|
|
958
|
+
if (binding.id !== input.authorizationBindingId && input.authorizationBindingId) throw new Error("Authorization binding ID does not match authorizationBindingId.");
|
|
959
|
+
if (binding.tenantId !== input.tenantId || binding.actorId !== input.actorId || binding.actionId !== input.actionId || binding.parameterDigest !== parameterDigest) {
|
|
960
|
+
throw new Error("Authorization binding does not match the submitted command identity.");
|
|
961
|
+
}
|
|
962
|
+
if (authorityMoment && binding.governingMoment !== authorityMoment) throw new Error("Authorization binding governingMoment does not match the action execution contract.");
|
|
963
|
+
const capturedAt = Date.parse(binding.capturedAt);
|
|
964
|
+
const expiresAt = binding.expiresAt === void 0 ? void 0 : Date.parse(binding.expiresAt);
|
|
965
|
+
if (!Number.isFinite(capturedAt) || expiresAt !== void 0 && !Number.isFinite(expiresAt)) {
|
|
966
|
+
throw new Error("Authorization binding timestamps must be valid ISO-8601 values.");
|
|
967
|
+
}
|
|
968
|
+
if (expiresAt !== void 0 && (expiresAt <= capturedAt || authorityMoment === "capture" && expiresAt <= currentTime.getTime())) {
|
|
969
|
+
throw new Error("Authorization binding must be unexpired at capture and expire after capturedAt.");
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
function idempotencyConflict(existing, incoming) {
|
|
973
|
+
const reasons = [];
|
|
974
|
+
if ((existing.idempotencyActorId ?? existing.actorId) !== incoming.actorId) reasons.push("actor");
|
|
975
|
+
if ((existing.idempotencyAuthorizationBindingId ?? existing.authorizationBindingId) !== incoming.authorizationBindingId) reasons.push("authority_binding");
|
|
976
|
+
if (existing.actionVersion !== incoming.actionVersion) reasons.push("action_version");
|
|
977
|
+
if (existing.parameterDigest !== incoming.parameterDigest) reasons.push("parameters");
|
|
978
|
+
return reasons.length ? { code: "IDEMPOTENCY_CONFLICT", idempotencyKey: incoming.idempotencyKey, existingInvocationId: existing.id, reasons } : void 0;
|
|
979
|
+
}
|
|
816
980
|
|
|
817
981
|
// src/outbox.ts
|
|
818
982
|
function toEnterpriseEventEnvelope(event, metadata) {
|
|
@@ -833,6 +997,7 @@ function toEnterpriseEventEnvelope(event, metadata) {
|
|
|
833
997
|
producerModuleVersion: metadata.producerModuleVersion,
|
|
834
998
|
payloadClassification: metadata.payloadClassification,
|
|
835
999
|
...metadata.traceContext ? { traceContext: { ...metadata.traceContext } } : {},
|
|
1000
|
+
...metadata.includeProvenance && event.provenance ? { provenance: event.provenance } : {},
|
|
836
1001
|
payload: event.payload
|
|
837
1002
|
};
|
|
838
1003
|
}
|
|
@@ -967,7 +1132,7 @@ var MemoryPlatformHostStore = class {
|
|
|
967
1132
|
const record = await this.getActionInvocation(id, tenantId, spaceId);
|
|
968
1133
|
if (!record) throw new Error(`ActionInvocation not found: ${id}`);
|
|
969
1134
|
Object.assign(record, patch, { updatedAt: /* @__PURE__ */ new Date() });
|
|
970
|
-
if (patch.status === "waiting_for_approval" || patch.status === "completed" || patch.status === "failed" || patch.status === "blocked_by_policy" || patch.status === "validation_failed") {
|
|
1135
|
+
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") {
|
|
971
1136
|
delete record.leaseOwner;
|
|
972
1137
|
delete record.leaseExpiresAt;
|
|
973
1138
|
}
|
|
@@ -1179,6 +1344,9 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
|
|
|
1179
1344
|
actor_id text NOT NULL, actor_type text NOT NULL, status text NOT NULL,
|
|
1180
1345
|
parameters jsonb NOT NULL, result jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
1181
1346
|
correlation_id text NOT NULL, causation_id text, idempotency_key text,
|
|
1347
|
+
parameter_digest text, parameter_digest_algorithm text, idempotency_actor_id text,
|
|
1348
|
+
idempotency_authorization_binding_id text, invocation_provenance jsonb,
|
|
1349
|
+
authorization_binding jsonb, execution_reason text, authorization_reconciliation jsonb,
|
|
1182
1350
|
authorization_binding_id text, error text,
|
|
1183
1351
|
attempt_count integer NOT NULL DEFAULT 0, lease_owner text,
|
|
1184
1352
|
lease_expires_at timestamptz, hitl_route text, hitl_risk_tier text,
|
|
@@ -1188,6 +1356,14 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
|
|
|
1188
1356
|
);
|
|
1189
1357
|
ALTER TABLE fabric_platform.action_invocations
|
|
1190
1358
|
ADD COLUMN IF NOT EXISTS idempotency_key text,
|
|
1359
|
+
ADD COLUMN IF NOT EXISTS parameter_digest text,
|
|
1360
|
+
ADD COLUMN IF NOT EXISTS parameter_digest_algorithm text,
|
|
1361
|
+
ADD COLUMN IF NOT EXISTS idempotency_actor_id text,
|
|
1362
|
+
ADD COLUMN IF NOT EXISTS idempotency_authorization_binding_id text,
|
|
1363
|
+
ADD COLUMN IF NOT EXISTS invocation_provenance jsonb,
|
|
1364
|
+
ADD COLUMN IF NOT EXISTS authorization_binding jsonb,
|
|
1365
|
+
ADD COLUMN IF NOT EXISTS execution_reason text,
|
|
1366
|
+
ADD COLUMN IF NOT EXISTS authorization_reconciliation jsonb,
|
|
1191
1367
|
ADD COLUMN IF NOT EXISTS authorization_binding_id text,
|
|
1192
1368
|
ADD COLUMN IF NOT EXISTS attempt_count integer NOT NULL DEFAULT 0,
|
|
1193
1369
|
ADD COLUMN IF NOT EXISTS lease_owner text,
|
|
@@ -1204,6 +1380,12 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
|
|
|
1204
1380
|
ON fabric_platform.action_invocations
|
|
1205
1381
|
(tenant_id, space_id, action_id, idempotency_key)
|
|
1206
1382
|
WHERE idempotency_key IS NOT NULL;
|
|
1383
|
+
CREATE INDEX IF NOT EXISTS action_invocations_parameter_digest_idx
|
|
1384
|
+
ON fabric_platform.action_invocations (tenant_id, parameter_digest)
|
|
1385
|
+
WHERE parameter_digest IS NOT NULL;
|
|
1386
|
+
CREATE INDEX IF NOT EXISTS action_invocations_authority_binding_idx
|
|
1387
|
+
ON fabric_platform.action_invocations (tenant_id, authorization_binding_id)
|
|
1388
|
+
WHERE authorization_binding_id IS NOT NULL;
|
|
1207
1389
|
CREATE INDEX IF NOT EXISTS action_invocations_worker_idx
|
|
1208
1390
|
ON fabric_platform.action_invocations (status, lease_expires_at, created_at);
|
|
1209
1391
|
CREATE TABLE IF NOT EXISTS fabric_platform.policy_evaluations (
|
|
@@ -1247,9 +1429,10 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
|
|
|
1247
1429
|
actor_id text NOT NULL, actor_type text NOT NULL, action_invocation_id text,
|
|
1248
1430
|
payload jsonb NOT NULL, sequence bigint NOT NULL,
|
|
1249
1431
|
occurred_at timestamptz NOT NULL, recorded_at timestamptz NOT NULL,
|
|
1250
|
-
correlation_id text NOT NULL, causation_id text,
|
|
1432
|
+
correlation_id text NOT NULL, causation_id text, provenance jsonb,
|
|
1251
1433
|
UNIQUE (tenant_id, space_id, sequence)
|
|
1252
1434
|
);
|
|
1435
|
+
ALTER TABLE fabric_platform.asset_events ADD COLUMN IF NOT EXISTS provenance jsonb;
|
|
1253
1436
|
CREATE INDEX IF NOT EXISTS asset_events_subject_idx
|
|
1254
1437
|
ON fabric_platform.asset_events
|
|
1255
1438
|
(tenant_id, space_id, subject_type, subject_id, sequence);
|
|
@@ -1273,8 +1456,10 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
|
|
|
1273
1456
|
`INSERT INTO fabric_platform.action_invocations
|
|
1274
1457
|
(id,tenant_id,space_id,action_id,action_version,actor_id,actor_type,status,
|
|
1275
1458
|
parameters,result,correlation_id,causation_id,idempotency_key,
|
|
1459
|
+
parameter_digest,parameter_digest_algorithm,idempotency_actor_id,
|
|
1460
|
+
idempotency_authorization_binding_id,invocation_provenance,authorization_binding,execution_reason,
|
|
1276
1461
|
authorization_binding_id,error,runtime_evidence,created_at,updated_at)
|
|
1277
|
-
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9::jsonb,$10::jsonb,$11,$12,$13,$14,$15,$16::jsonb,$
|
|
1462
|
+
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)
|
|
1278
1463
|
ON CONFLICT (tenant_id,space_id,action_id,idempotency_key)
|
|
1279
1464
|
WHERE idempotency_key IS NOT NULL DO UPDATE SET id=fabric_platform.action_invocations.id
|
|
1280
1465
|
RETURNING *`,
|
|
@@ -1292,6 +1477,13 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
|
|
|
1292
1477
|
input.correlationId,
|
|
1293
1478
|
input.causationId ?? null,
|
|
1294
1479
|
input.idempotencyKey ?? null,
|
|
1480
|
+
input.parameterDigest ?? null,
|
|
1481
|
+
input.parameterDigestAlgorithm ?? null,
|
|
1482
|
+
input.idempotencyActorId ?? null,
|
|
1483
|
+
input.idempotencyAuthorizationBindingId ?? null,
|
|
1484
|
+
input.provenance ? JSON.stringify(input.provenance) : null,
|
|
1485
|
+
input.authorizationBinding ? JSON.stringify(input.authorizationBinding) : null,
|
|
1486
|
+
input.executionReason ?? null,
|
|
1295
1487
|
input.authorizationBindingId ?? null,
|
|
1296
1488
|
input.error ?? null,
|
|
1297
1489
|
input.runtimeEvidence ? JSON.stringify(input.runtimeEvidence) : null,
|
|
@@ -1313,9 +1505,10 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
|
|
|
1313
1505
|
`UPDATE fabric_platform.action_invocations SET
|
|
1314
1506
|
status=COALESCE($4,status), result=COALESCE($5::jsonb,result),
|
|
1315
1507
|
error=CASE WHEN $6::boolean THEN $7 ELSE error END,
|
|
1316
|
-
|
|
1508
|
+
authorization_reconciliation=CASE WHEN $8::boolean THEN $9::jsonb ELSE authorization_reconciliation END,
|
|
1509
|
+
lease_owner=CASE WHEN $4 IN ('waiting_for_approval','completed','failed','blocked_by_policy','reconciliation_required','validation_failed')
|
|
1317
1510
|
THEN NULL ELSE lease_owner END,
|
|
1318
|
-
lease_expires_at=CASE WHEN $4 IN ('waiting_for_approval','completed','failed','blocked_by_policy','validation_failed')
|
|
1511
|
+
lease_expires_at=CASE WHEN $4 IN ('waiting_for_approval','completed','failed','blocked_by_policy','reconciliation_required','validation_failed')
|
|
1319
1512
|
THEN NULL ELSE lease_expires_at END,
|
|
1320
1513
|
updated_at=now()
|
|
1321
1514
|
WHERE id=$1 AND tenant_id=$2 AND space_id=$3`,
|
|
@@ -1326,7 +1519,9 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
|
|
|
1326
1519
|
patch.status ?? null,
|
|
1327
1520
|
patch.result === void 0 ? null : JSON.stringify(patch.result),
|
|
1328
1521
|
Object.hasOwn(patch, "error"),
|
|
1329
|
-
patch.error ?? null
|
|
1522
|
+
patch.error ?? null,
|
|
1523
|
+
Object.hasOwn(patch, "authorizationReconciliation"),
|
|
1524
|
+
patch.authorizationReconciliation ? JSON.stringify(patch.authorizationReconciliation) : null
|
|
1330
1525
|
]
|
|
1331
1526
|
);
|
|
1332
1527
|
}
|
|
@@ -1521,8 +1716,8 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
|
|
|
1521
1716
|
`INSERT INTO fabric_platform.asset_events
|
|
1522
1717
|
(id,tenant_id,space_id,event_type,event_schema_version,subject_type,subject_id,
|
|
1523
1718
|
actor_id,actor_type,action_invocation_id,payload,sequence,occurred_at,recorded_at,
|
|
1524
|
-
correlation_id,causation_id)
|
|
1525
|
-
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11::jsonb,$12,$13,$14,$15,$16)
|
|
1719
|
+
correlation_id,causation_id,provenance)
|
|
1720
|
+
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11::jsonb,$12,$13,$14,$15,$16,$17::jsonb)
|
|
1526
1721
|
ON CONFLICT (id) DO NOTHING`,
|
|
1527
1722
|
[
|
|
1528
1723
|
event.id,
|
|
@@ -1540,7 +1735,8 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
|
|
|
1540
1735
|
event.occurredAt,
|
|
1541
1736
|
event.recordedAt,
|
|
1542
1737
|
event.correlationId,
|
|
1543
|
-
event.causationId ?? null
|
|
1738
|
+
event.causationId ?? null,
|
|
1739
|
+
event.provenance ? JSON.stringify(event.provenance) : null
|
|
1544
1740
|
]
|
|
1545
1741
|
);
|
|
1546
1742
|
}
|
|
@@ -1551,13 +1747,13 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
|
|
|
1551
1747
|
INSERT INTO fabric_platform.asset_events
|
|
1552
1748
|
(id,tenant_id,space_id,event_type,event_schema_version,subject_type,subject_id,
|
|
1553
1749
|
actor_id,actor_type,action_invocation_id,payload,sequence,occurred_at,recorded_at,
|
|
1554
|
-
correlation_id,causation_id)
|
|
1555
|
-
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11::jsonb,$12,$13,$14,$15,$16)
|
|
1750
|
+
correlation_id,causation_id,provenance)
|
|
1751
|
+
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11::jsonb,$12,$13,$14,$15,$16,$17::jsonb)
|
|
1556
1752
|
ON CONFLICT (id) DO NOTHING RETURNING id
|
|
1557
1753
|
)
|
|
1558
1754
|
INSERT INTO fabric_platform.event_outbox
|
|
1559
1755
|
(id,tenant_id,space_id,event,status,attempt_count,available_at,created_at)
|
|
1560
|
-
SELECT $1,$2,$3,$
|
|
1756
|
+
SELECT $1,$2,$3,$18::jsonb,'pending',0,$14,$14 FROM inserted_event
|
|
1561
1757
|
ON CONFLICT (id) DO NOTHING`,
|
|
1562
1758
|
[
|
|
1563
1759
|
event.id,
|
|
@@ -1576,6 +1772,7 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
|
|
|
1576
1772
|
event.recordedAt,
|
|
1577
1773
|
event.correlationId,
|
|
1578
1774
|
event.causationId ?? null,
|
|
1775
|
+
event.provenance ? JSON.stringify(event.provenance) : null,
|
|
1579
1776
|
JSON.stringify(envelope)
|
|
1580
1777
|
]
|
|
1581
1778
|
);
|
|
@@ -1736,6 +1933,14 @@ function toActionRecord(row) {
|
|
|
1736
1933
|
correlationId: String(row.correlation_id),
|
|
1737
1934
|
...row.causation_id ? { causationId: String(row.causation_id) } : {},
|
|
1738
1935
|
...row.idempotency_key ? { idempotencyKey: String(row.idempotency_key) } : {},
|
|
1936
|
+
...row.parameter_digest ? { parameterDigest: String(row.parameter_digest) } : {},
|
|
1937
|
+
...row.parameter_digest_algorithm ? { parameterDigestAlgorithm: String(row.parameter_digest_algorithm) } : {},
|
|
1938
|
+
...row.idempotency_actor_id ? { idempotencyActorId: String(row.idempotency_actor_id) } : {},
|
|
1939
|
+
...row.idempotency_authorization_binding_id ? { idempotencyAuthorizationBindingId: String(row.idempotency_authorization_binding_id) } : {},
|
|
1940
|
+
...row.invocation_provenance ? { provenance: row.invocation_provenance } : {},
|
|
1941
|
+
...row.authorization_binding ? { authorizationBinding: row.authorization_binding } : {},
|
|
1942
|
+
...row.execution_reason ? { executionReason: String(row.execution_reason) } : {},
|
|
1943
|
+
...row.authorization_reconciliation ? { authorizationReconciliation: row.authorization_reconciliation } : {},
|
|
1739
1944
|
...row.authorization_binding_id ? { authorizationBindingId: String(row.authorization_binding_id) } : {},
|
|
1740
1945
|
attemptCount: Number(row.attempt_count ?? 0),
|
|
1741
1946
|
...row.lease_owner ? { leaseOwner: String(row.lease_owner) } : {},
|
|
@@ -1809,7 +2014,8 @@ function toEventRecord(row) {
|
|
|
1809
2014
|
occurredAt: new Date(row.occurred_at),
|
|
1810
2015
|
recordedAt: new Date(row.recorded_at),
|
|
1811
2016
|
correlationId: String(row.correlation_id),
|
|
1812
|
-
...row.causation_id ? { causationId: String(row.causation_id) } : {}
|
|
2017
|
+
...row.causation_id ? { causationId: String(row.causation_id) } : {},
|
|
2018
|
+
...row.provenance ? { provenance: row.provenance } : {}
|
|
1813
2019
|
};
|
|
1814
2020
|
}
|
|
1815
2021
|
function toOutboxRecord(row) {
|
|
@@ -1830,6 +2036,7 @@ function toOutboxRecord(row) {
|
|
|
1830
2036
|
...event.actionInvocationId ? { actionInvocationId: String(event.actionInvocationId) } : {},
|
|
1831
2037
|
correlationId: String(event.correlationId),
|
|
1832
2038
|
...event.causationId ? { causationId: String(event.causationId) } : {},
|
|
2039
|
+
...event.provenance ? { provenance: event.provenance } : {},
|
|
1833
2040
|
occurredAt: new Date(event.occurredAt),
|
|
1834
2041
|
recordedAt: new Date(event.recordedAt),
|
|
1835
2042
|
producerModuleVersion: String(event.producerModuleVersion),
|
|
@@ -1916,6 +2123,6 @@ async function abortableDelay(milliseconds, signal) {
|
|
|
1916
2123
|
});
|
|
1917
2124
|
}
|
|
1918
2125
|
|
|
1919
|
-
export { MemoryPlatformHostStore, PLATFORM_HOST_CONTRACT_VERSION, PostgresPlatformHostStore, cloneOutboxRecord, createGovernedActionHost, createStoreBackedActionDispatcher, runOutboxRelayCycle, runPlatformActionWorker, runPlatformActionWorkerCycle, toEnterpriseEventEnvelope };
|
|
2126
|
+
export { IdempotencyConflictError, MemoryPlatformHostStore, PARAMETER_DIGEST_ALGORITHM, PLATFORM_HOST_CONTRACT_VERSION, PostgresPlatformHostStore, canonicalJson, cloneOutboxRecord, createGovernedActionHost, createStoreBackedActionDispatcher, digestParameters, runOutboxRelayCycle, runPlatformActionWorker, runPlatformActionWorkerCycle, toEnterpriseEventEnvelope };
|
|
1920
2127
|
//# sourceMappingURL=index.js.map
|
|
1921
2128
|
//# sourceMappingURL=index.js.map
|