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