@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/CHANGELOG.md +35 -8
- package/MIGRATION-2-IDEMPOTENCY.md +32 -0
- package/README.md +36 -7
- package/dist/index.cjs +295 -31
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +94 -8
- package/dist/index.d.ts +94 -8
- package/dist/index.js +292 -32
- 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_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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
241
|
-
invocation,
|
|
242
|
-
|
|
243
|
-
|
|
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
|
|
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
|
|
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:
|
|
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,$
|
|
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
|
-
|
|
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,$
|
|
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;
|