@fabricorg/platform-host 0.4.0 → 0.4.1

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,13 @@
1
1
  # @fabricorg/platform-host
2
2
 
3
+ ## 0.4.1 — 2026-07-20
4
+
5
+ - Add runtime-local action resolution without process-wide registry mutation.
6
+ - Pass the injected host clock to policy evaluators.
7
+ - Treat empty state-machine targets as no-op transitions.
8
+ - Default domain events to the action schema version while preserving explicit event versions.
9
+ - Omit custom extracted-event carrier fields from durable invocation results.
10
+
3
11
  ## 0.4.0 — 2026-07-19
4
12
 
5
13
  - Add an optional agent-only HITL evaluator between schema validation and ordinary policies.
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  The canonical host for the `@fabricorg/platform` mutation pipeline.
4
4
 
5
5
  ```bash
6
- pnpm add @fabricorg/platform@^0.7.0 @fabricorg/platform-host@^0.4.0
6
+ pnpm add @fabricorg/platform@^0.7.0 @fabricorg/platform-host@^0.4.1
7
7
  ```
8
8
 
9
9
  Vertical packages register `FabricModule` definitions. Host applications provide tenant authorization,
@@ -18,6 +18,12 @@ Actor → ActionInvocation → Schema → Agent HITL → PolicyEvaluation → St
18
18
  worker entry point. With no dispatcher the host executes inline, which is intended for tests and local
19
19
  development only.
20
20
 
21
+ Applications that assemble action catalogs per runtime can provide `resolveAction` instead of
22
+ mutating the process-wide platform registry. A custom `extractEvents` implementation can pair with
23
+ `eventResultFields` so its event carrier is removed from the durable invocation result. Domain events
24
+ without an explicit `eventSchemaVersion` inherit the action version, while host lifecycle events stay
25
+ at version 1. The injected `now` clock is also passed to ordinary policy evaluation.
26
+
21
27
  Production polling workers can use `createStoreBackedActionDispatcher()` plus
22
28
  `runPlatformActionWorker()`. The pending invocation row is the durable queue item; workers claim
23
29
  bounded batches with an atomic lease and `FOR UPDATE SKIP LOCKED`, and an expired `running` lease is
package/dist/index.cjs CHANGED
@@ -12,8 +12,10 @@ function createGovernedActionHost(options) {
12
12
  for (const adapter of options.adapters ?? []) adapters.register(adapter);
13
13
  const now = options.now ?? (() => /* @__PURE__ */ new Date());
14
14
  const extractEvents = options.extractEvents ?? DEFAULT_EXTRACT_EVENTS;
15
+ const actionResolver = options.resolveAction ?? platform.resolveAction;
16
+ const eventResultFields = options.eventResultFields ?? ["_events"];
15
17
  async function submitAction(input) {
16
- const action = platform.resolveAction(input.actionId);
18
+ const action = actionResolver(input.actionId);
17
19
  if (!action) throw new Error(`Unknown action: ${input.actionId}`);
18
20
  const authorizationInput = toAuthorizationInput(action, input);
19
21
  if (!await options.authorization.checkEntitlement(authorizationInput)) {
@@ -105,7 +107,7 @@ function createGovernedActionHost(options) {
105
107
  error: `Invocation is leased by ${invocation.leaseOwner}`
106
108
  };
107
109
  }
108
- const action = platform.resolveAction(invocation.actionId);
110
+ const action = actionResolver(invocation.actionId);
109
111
  if (!action) {
110
112
  return fail(invocation, "failed", `Unknown action: ${invocation.actionId}`);
111
113
  }
@@ -193,7 +195,8 @@ function createGovernedActionHost(options) {
193
195
  parameters: parsed.data,
194
196
  db: options.store.db,
195
197
  services: options.services,
196
- mode: "execute"
198
+ mode: "execute",
199
+ now: now()
197
200
  });
198
201
  for (const outcome of outcomes) {
199
202
  await options.store.appendPolicyEvaluation({
@@ -226,15 +229,17 @@ function createGovernedActionHost(options) {
226
229
  entityId
227
230
  ) ?? initialState(binding.entityType) : initialState(binding.entityType);
228
231
  const targetState = typeof binding.targetState === "function" ? binding.targetState(parsed.data) : binding.targetState;
229
- const transition = platform.validateTransition(
230
- binding.entityType,
231
- currentState,
232
- targetState,
233
- action.actionId
234
- );
235
- const replayingAppliedTransition = action.idempotent && currentState === targetState;
236
- if (!transition.valid && !replayingAppliedTransition) {
237
- return fail(invocation, "failed", transition.error ?? "Invalid state transition");
232
+ if (targetState !== "") {
233
+ const transition = platform.validateTransition(
234
+ binding.entityType,
235
+ currentState,
236
+ targetState,
237
+ action.actionId
238
+ );
239
+ const replayingAppliedTransition = action.idempotent && currentState === targetState;
240
+ if (!transition.valid && !replayingAppliedTransition) {
241
+ return fail(invocation, "failed", transition.error ?? "Invalid state transition");
242
+ }
238
243
  }
239
244
  }
240
245
  let data;
@@ -263,7 +268,7 @@ function createGovernedActionHost(options) {
263
268
  domainEvents = extractEvents(data);
264
269
  if (action.eventPhase !== "after_adapters") {
265
270
  for (const [index, event] of domainEvents.entries()) {
266
- await appendEvent(invocation, event, `domain:${index}`);
271
+ await appendEvent(invocation, event, `domain:${index}`, action.version);
267
272
  }
268
273
  }
269
274
  } catch (error) {
@@ -382,10 +387,10 @@ function createGovernedActionHost(options) {
382
387
  }
383
388
  if (action.eventPhase === "after_adapters") {
384
389
  for (const [index, event] of domainEvents.entries()) {
385
- await appendEvent(invocation, event, `domain:${index}`);
390
+ await appendEvent(invocation, event, `domain:${index}`, action.version);
386
391
  }
387
392
  }
388
- const result = withoutPrivateHostFields(data);
393
+ const result = withoutPrivateHostFields(data, eventResultFields);
389
394
  await options.store.updateActionInvocation(actionInvocationId, tenantId, spaceId, {
390
395
  status: "completed",
391
396
  result
@@ -416,7 +421,7 @@ function createGovernedActionHost(options) {
416
421
  error: `Invocation is not waiting for approval (status: ${invocation.status})`
417
422
  };
418
423
  }
419
- const action = platform.resolveAction(invocation.actionId);
424
+ const action = actionResolver(invocation.actionId);
420
425
  if (!action) return fail(invocation, "failed", `Unknown action: ${invocation.actionId}`);
421
426
  const approvalAuthorizationInput = toAuthorizationInput(action, {
422
427
  tenantId,
@@ -464,14 +469,14 @@ function createGovernedActionHost(options) {
464
469
  if (!decision.approved) return actionResult(transitioned);
465
470
  return executeInvocation(actionInvocationId, tenantId, spaceId, { leaseOwner });
466
471
  }
467
- async function appendEvent(invocation, event, deduplicationKey) {
472
+ async function appendEvent(invocation, event, deduplicationKey, defaultEventSchemaVersion = 1) {
468
473
  const timestamp = now();
469
474
  const envelope = {
470
475
  id: lifecycleId("evt", invocation.id, deduplicationKey),
471
476
  tenantId: invocation.tenantId,
472
477
  spaceId: invocation.spaceId,
473
478
  eventType: event.eventType,
474
- eventSchemaVersion: event.eventSchemaVersion ?? 1,
479
+ eventSchemaVersion: event.eventSchemaVersion ?? defaultEventSchemaVersion,
475
480
  subjectType: event.subjectType,
476
481
  subjectId: event.subjectId,
477
482
  actorId: invocation.actorId,
@@ -556,9 +561,10 @@ function initialState(entityType) {
556
561
  function isTerminal(status) {
557
562
  return ["completed", "failed", "blocked_by_policy", "validation_failed"].includes(status);
558
563
  }
559
- function withoutPrivateHostFields(data) {
560
- const { _events: _ignored, ...result } = data;
561
- return result;
564
+ function withoutPrivateHostFields(data, eventResultFields) {
565
+ return Object.fromEntries(
566
+ Object.entries(data).filter(([key]) => !eventResultFields.includes(key))
567
+ );
562
568
  }
563
569
  function errorMessage(error) {
564
570
  return error instanceof Error ? error.message : String(error);