@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 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) {