@fabricorg/platform-host 4.0.0 → 5.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 +27 -0
- package/dist/index.cjs +41 -21
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +41 -21
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# @fabricorg/platform-host
|
|
2
2
|
|
|
3
|
+
## 5.0.0
|
|
4
|
+
|
|
5
|
+
### Major Changes
|
|
6
|
+
|
|
7
|
+
- c504d0a: Enforce every field of `ActionExecutionContract` or hand it to an explicit owner.
|
|
8
|
+
|
|
9
|
+
- `connectivity`: an action declaring `online-required` is refused when submitted with
|
|
10
|
+
`executionReason: "offline_replay"`. What a client offers offline is a composition decision; what
|
|
11
|
+
the Host accepts is not.
|
|
12
|
+
- `consistency`: an action declaring `provisional-until-reconciled` marks the events it emits and the
|
|
13
|
+
submission result, so bus consumers and reconciliation flows can tell a settled fact from a pending
|
|
14
|
+
one without a lookup.
|
|
15
|
+
- `sensitivity`: an action declaring `restricted` refuses a submission carrying provenance audit
|
|
16
|
+
attributes instead of durably recording them.
|
|
17
|
+
- `completion`: `immediate` combined with `kind: "saga"` is rejected at registry build.
|
|
18
|
+
|
|
19
|
+
`consistency` is persisted by the PostgreSQL store (additive nullable column, applied by
|
|
20
|
+
`ensureSchema()`) and carried through `toEnterpriseEventEnvelope()` to the bus, so a provisional fact
|
|
21
|
+
stays marked as provisional after replay and after publication. Every result path is decorated, so an
|
|
22
|
+
idempotent replay reports the same consistency as the original submission.
|
|
23
|
+
|
|
24
|
+
Applications that previously submitted offline replays of online-required actions, or audit
|
|
25
|
+
attributes on restricted actions, now receive an explicit error where both were silent.
|
|
26
|
+
|
|
27
|
+
At release, raise the `@fabricorg/platform` peer floor to the version carrying
|
|
28
|
+
`AssetEventEnvelope.consistency`.
|
|
29
|
+
|
|
3
30
|
## 4.0.0
|
|
4
31
|
|
|
5
32
|
### Major Changes
|
package/dist/index.cjs
CHANGED
|
@@ -94,6 +94,12 @@ function createGovernedActionHost(options) {
|
|
|
94
94
|
}
|
|
95
95
|
const action = actionResolver(input.actionId);
|
|
96
96
|
if (!action) throw new Error(`Unknown action: ${input.actionId}`);
|
|
97
|
+
if (action.execution?.connectivity === "online-required" && input.executionReason === "offline_replay") {
|
|
98
|
+
throw new Error(`Action ${input.actionId} is online-required and cannot be submitted as an offline replay.`);
|
|
99
|
+
}
|
|
100
|
+
if (action.execution?.sensitivity === "restricted" && Object.keys(input.provenance?.auditAttributes ?? {}).length > 0) {
|
|
101
|
+
throw new Error(`Action ${input.actionId} declares restricted sensitivity; provenance audit attributes must not be durably recorded.`);
|
|
102
|
+
}
|
|
97
103
|
const authorizationInput = toAuthorizationInput(action, input);
|
|
98
104
|
if (!await options.authorization.checkEntitlement(authorizationInput)) {
|
|
99
105
|
throw new Error(`Module "${action.namespace}" is not enabled for tenant ${input.tenantId}`);
|
|
@@ -166,7 +172,7 @@ function createGovernedActionHost(options) {
|
|
|
166
172
|
}
|
|
167
173
|
const durableWorkflowId = `action-invocation-${durableInvocation.id}`;
|
|
168
174
|
if (durableInvocation.id !== actionInvocationId) {
|
|
169
|
-
return {
|
|
175
|
+
return withConsistency({
|
|
170
176
|
actionInvocationId: durableInvocation.id,
|
|
171
177
|
status: durableInvocation.status,
|
|
172
178
|
workflowId: durableWorkflowId,
|
|
@@ -175,7 +181,7 @@ function createGovernedActionHost(options) {
|
|
|
175
181
|
...durableInvocation.hitlRoute ? { hitlRoute: durableInvocation.hitlRoute } : {},
|
|
176
182
|
...durableInvocation.hitlRiskTier ? { hitlRiskTier: durableInvocation.hitlRiskTier } : {},
|
|
177
183
|
...durableInvocation.authorizationReconciliation ? { reconciliation: durableInvocation.authorizationReconciliation } : {}
|
|
178
|
-
};
|
|
184
|
+
}, input.actionId);
|
|
179
185
|
}
|
|
180
186
|
if (options.dispatcher) {
|
|
181
187
|
try {
|
|
@@ -185,14 +191,14 @@ function createGovernedActionHost(options) {
|
|
|
185
191
|
spaceId: input.spaceId,
|
|
186
192
|
workflowId: durableWorkflowId
|
|
187
193
|
});
|
|
188
|
-
return {
|
|
194
|
+
return withConsistency({
|
|
189
195
|
actionInvocationId: durableInvocation.id,
|
|
190
196
|
status: durableInvocation.status,
|
|
191
197
|
workflowId: dispatched.workflowId,
|
|
192
198
|
...dispatched.runId ? { runId: dispatched.runId } : {},
|
|
193
199
|
...durableInvocation.hitlRoute ? { hitlRoute: durableInvocation.hitlRoute } : {},
|
|
194
200
|
...durableInvocation.hitlRiskTier ? { hitlRiskTier: durableInvocation.hitlRiskTier } : {}
|
|
195
|
-
};
|
|
201
|
+
}, input.actionId);
|
|
196
202
|
} catch (error) {
|
|
197
203
|
const message = errorMessage(error);
|
|
198
204
|
await options.store.updateActionInvocation(
|
|
@@ -209,7 +215,7 @@ function createGovernedActionHost(options) {
|
|
|
209
215
|
input.tenantId,
|
|
210
216
|
input.spaceId
|
|
211
217
|
);
|
|
212
|
-
return { ...executed, workflowId: durableWorkflowId };
|
|
218
|
+
return { ...withConsistency(executed, input.actionId), workflowId: durableWorkflowId };
|
|
213
219
|
}
|
|
214
220
|
async function executeInvocation(actionInvocationId, tenantId, spaceId, executionOptions = {}, executionReasonOverride) {
|
|
215
221
|
const loadedInvocation = await options.store.getActionInvocation(
|
|
@@ -223,7 +229,7 @@ function createGovernedActionHost(options) {
|
|
|
223
229
|
const resumingRunningInvocation = loadedInvocation.status === "running";
|
|
224
230
|
let invocation = loadedInvocation;
|
|
225
231
|
if (isTerminal(invocation.status) || invocation.status === "waiting_for_approval") {
|
|
226
|
-
return actionResult(invocation);
|
|
232
|
+
return withConsistency(actionResult(invocation), invocation.actionId);
|
|
227
233
|
}
|
|
228
234
|
if (invocation.status === "running" && invocation.leaseOwner && executionOptions.leaseOwner !== invocation.leaseOwner) {
|
|
229
235
|
return {
|
|
@@ -340,7 +346,7 @@ function createGovernedActionHost(options) {
|
|
|
340
346
|
message: `Action ${action.actionId} requires durable capture-time authorization evidence`
|
|
341
347
|
};
|
|
342
348
|
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 };
|
|
349
|
+
return withConsistency({ actionInvocationId, status: "reconciliation_required", error: reconciliation.message, reconciliation }, action.actionId);
|
|
344
350
|
}
|
|
345
351
|
const bindingExpired = authorityMoment !== "capture" && invocation.authorizationBinding?.expiresAt !== void 0 && Date.parse(invocation.authorizationBinding.expiresAt) <= now().getTime();
|
|
346
352
|
if (bindingExpired) {
|
|
@@ -353,7 +359,7 @@ function createGovernedActionHost(options) {
|
|
|
353
359
|
message: `Authorization binding for action ${action.actionId} expired before execution`
|
|
354
360
|
};
|
|
355
361
|
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 };
|
|
362
|
+
return withConsistency({ actionInvocationId, status: "reconciliation_required", error: reconciliation.message, reconciliation }, action.actionId);
|
|
357
363
|
}
|
|
358
364
|
const executionAuthorized = authorityMoment === "capture" ? invocation.authorizationBinding !== void 0 : options.authorization.authorizeExecution ? await options.authorization.authorizeExecution({
|
|
359
365
|
...authorizationInput,
|
|
@@ -375,7 +381,7 @@ function createGovernedActionHost(options) {
|
|
|
375
381
|
message: `Actor ${invocation.actorId} is not authorized to execute action ${action.actionId}`
|
|
376
382
|
};
|
|
377
383
|
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 };
|
|
384
|
+
return withConsistency({ actionInvocationId, status: "reconciliation_required", error: reconciliation.message, reconciliation }, action.actionId);
|
|
379
385
|
}
|
|
380
386
|
const definitions = options.resolvePolicies ? await options.resolvePolicies({
|
|
381
387
|
...authorizationInput,
|
|
@@ -717,7 +723,7 @@ function createGovernedActionHost(options) {
|
|
|
717
723
|
spaceId
|
|
718
724
|
);
|
|
719
725
|
if (!invocation) throw new Error(`ActionInvocation not found: ${actionInvocationId}`);
|
|
720
|
-
if (isTerminal(invocation.status)) return actionResult(invocation);
|
|
726
|
+
if (isTerminal(invocation.status)) return withConsistency(actionResult(invocation), invocation.actionId);
|
|
721
727
|
if (invocation.status !== "waiting_for_approval") {
|
|
722
728
|
return {
|
|
723
729
|
...actionResult(invocation),
|
|
@@ -767,9 +773,9 @@ function createGovernedActionHost(options) {
|
|
|
767
773
|
const transitioned = transition.invocation ?? await options.store.getActionInvocation(actionInvocationId, tenantId, spaceId);
|
|
768
774
|
if (!transition.applied || !transitioned) {
|
|
769
775
|
if (!transitioned) throw new Error(`ActionInvocation not found: ${actionInvocationId}`);
|
|
770
|
-
return actionResult(transitioned);
|
|
776
|
+
return withConsistency(actionResult(transitioned), transitioned.actionId);
|
|
771
777
|
}
|
|
772
|
-
if (!decision.approved) return actionResult(transitioned);
|
|
778
|
+
if (!decision.approved) return withConsistency(actionResult(transitioned), transitioned.actionId);
|
|
773
779
|
return executeInvocation(
|
|
774
780
|
actionInvocationId,
|
|
775
781
|
tenantId,
|
|
@@ -810,6 +816,13 @@ function createGovernedActionHost(options) {
|
|
|
810
816
|
spaceId
|
|
811
817
|
});
|
|
812
818
|
}
|
|
819
|
+
function declaredConsistency(actionId) {
|
|
820
|
+
return actionResolver(actionId)?.execution?.consistency;
|
|
821
|
+
}
|
|
822
|
+
function withConsistency(result, actionId) {
|
|
823
|
+
const consistency = declaredConsistency(actionId);
|
|
824
|
+
return consistency ? { ...result, consistency } : result;
|
|
825
|
+
}
|
|
813
826
|
async function appendEvent(invocation, event, deduplicationKey, defaultEventSchemaVersion = 1, transaction) {
|
|
814
827
|
const timestamp = now();
|
|
815
828
|
const envelope = {
|
|
@@ -832,7 +845,8 @@ function createGovernedActionHost(options) {
|
|
|
832
845
|
recordedAt: timestamp,
|
|
833
846
|
correlationId: invocation.correlationId,
|
|
834
847
|
...invocation.causationId ? { causationId: invocation.causationId } : {},
|
|
835
|
-
...invocation.provenance ? { provenance: { source: invocation.provenance.source, ...invocation.provenance.auditAttributes ? { auditAttributes: invocation.provenance.auditAttributes } : {} } } : {}
|
|
848
|
+
...invocation.provenance ? { provenance: { source: invocation.provenance.source, ...invocation.provenance.auditAttributes ? { auditAttributes: invocation.provenance.auditAttributes } : {} } } : {},
|
|
849
|
+
...declaredConsistency(invocation.actionId) === "provisional-until-reconciled" ? { consistency: "provisional-until-reconciled" } : {}
|
|
836
850
|
};
|
|
837
851
|
if (options.outbox) {
|
|
838
852
|
const hostLifecycleEvent = (/* @__PURE__ */ new Set(["AdapterInvocationStarted", "AdapterInvocationSucceeded", "AdapterInvocationFailed", "ComplianceBlocked"])).has(envelope.eventType);
|
|
@@ -1000,6 +1014,8 @@ function toEnterpriseEventEnvelope(event, metadata) {
|
|
|
1000
1014
|
payloadClassification: metadata.payloadClassification,
|
|
1001
1015
|
...metadata.traceContext ? { traceContext: { ...metadata.traceContext } } : {},
|
|
1002
1016
|
...metadata.includeProvenance && event.provenance ? { provenance: event.provenance } : {},
|
|
1017
|
+
// A provisional fact must stay marked as provisional once it leaves the platform.
|
|
1018
|
+
...event.consistency ? { consistency: event.consistency } : {},
|
|
1003
1019
|
payload: event.payload
|
|
1004
1020
|
};
|
|
1005
1021
|
}
|
|
@@ -1431,10 +1447,11 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
|
|
|
1431
1447
|
actor_id text NOT NULL, actor_type text NOT NULL, action_invocation_id text,
|
|
1432
1448
|
payload jsonb NOT NULL, sequence bigint NOT NULL,
|
|
1433
1449
|
occurred_at timestamptz NOT NULL, recorded_at timestamptz NOT NULL,
|
|
1434
|
-
correlation_id text NOT NULL, causation_id text, provenance jsonb,
|
|
1450
|
+
correlation_id text NOT NULL, causation_id text, provenance jsonb, consistency text,
|
|
1435
1451
|
UNIQUE (tenant_id, space_id, sequence)
|
|
1436
1452
|
);
|
|
1437
1453
|
ALTER TABLE fabric_platform.asset_events ADD COLUMN IF NOT EXISTS provenance jsonb;
|
|
1454
|
+
ALTER TABLE fabric_platform.asset_events ADD COLUMN IF NOT EXISTS consistency text;
|
|
1438
1455
|
CREATE INDEX IF NOT EXISTS asset_events_subject_idx
|
|
1439
1456
|
ON fabric_platform.asset_events
|
|
1440
1457
|
(tenant_id, space_id, subject_type, subject_id, sequence);
|
|
@@ -1718,8 +1735,8 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
|
|
|
1718
1735
|
`INSERT INTO fabric_platform.asset_events
|
|
1719
1736
|
(id,tenant_id,space_id,event_type,event_schema_version,subject_type,subject_id,
|
|
1720
1737
|
actor_id,actor_type,action_invocation_id,payload,sequence,occurred_at,recorded_at,
|
|
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)
|
|
1738
|
+
correlation_id,causation_id,provenance,consistency)
|
|
1739
|
+
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11::jsonb,$12,$13,$14,$15,$16,$17::jsonb,$18)
|
|
1723
1740
|
ON CONFLICT (id) DO NOTHING`,
|
|
1724
1741
|
[
|
|
1725
1742
|
event.id,
|
|
@@ -1738,7 +1755,8 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
|
|
|
1738
1755
|
event.recordedAt,
|
|
1739
1756
|
event.correlationId,
|
|
1740
1757
|
event.causationId ?? null,
|
|
1741
|
-
event.provenance ? JSON.stringify(event.provenance) : null
|
|
1758
|
+
event.provenance ? JSON.stringify(event.provenance) : null,
|
|
1759
|
+
event.consistency ?? null
|
|
1742
1760
|
]
|
|
1743
1761
|
);
|
|
1744
1762
|
}
|
|
@@ -1749,8 +1767,8 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
|
|
|
1749
1767
|
INSERT INTO fabric_platform.asset_events
|
|
1750
1768
|
(id,tenant_id,space_id,event_type,event_schema_version,subject_type,subject_id,
|
|
1751
1769
|
actor_id,actor_type,action_invocation_id,payload,sequence,occurred_at,recorded_at,
|
|
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)
|
|
1770
|
+
correlation_id,causation_id,provenance,consistency)
|
|
1771
|
+
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11::jsonb,$12,$13,$14,$15,$16,$17::jsonb,$19)
|
|
1754
1772
|
ON CONFLICT (id) DO NOTHING RETURNING id
|
|
1755
1773
|
)
|
|
1756
1774
|
INSERT INTO fabric_platform.event_outbox
|
|
@@ -1775,7 +1793,8 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
|
|
|
1775
1793
|
event.correlationId,
|
|
1776
1794
|
event.causationId ?? null,
|
|
1777
1795
|
event.provenance ? JSON.stringify(event.provenance) : null,
|
|
1778
|
-
JSON.stringify(envelope)
|
|
1796
|
+
JSON.stringify(envelope),
|
|
1797
|
+
event.consistency ?? null
|
|
1779
1798
|
]
|
|
1780
1799
|
);
|
|
1781
1800
|
}
|
|
@@ -2017,7 +2036,8 @@ function toEventRecord(row) {
|
|
|
2017
2036
|
recordedAt: new Date(row.recorded_at),
|
|
2018
2037
|
correlationId: String(row.correlation_id),
|
|
2019
2038
|
...row.causation_id ? { causationId: String(row.causation_id) } : {},
|
|
2020
|
-
...row.provenance ? { provenance: row.provenance } : {}
|
|
2039
|
+
...row.provenance ? { provenance: row.provenance } : {},
|
|
2040
|
+
...row.consistency ? { consistency: row.consistency } : {}
|
|
2021
2041
|
};
|
|
2022
2042
|
}
|
|
2023
2043
|
function toOutboxRecord(row) {
|