@fabricorg/platform-host 0.4.0 → 0.4.2

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,17 @@
1
1
  # @fabricorg/platform-host
2
2
 
3
+ ## 0.4.2 — 2026-07-20
4
+
5
+ - Reject handler-emitted domain events absent from the action's `emitsEvents` declaration.
6
+
7
+ ## 0.4.1 — 2026-07-20
8
+
9
+ - Add runtime-local action resolution without process-wide registry mutation.
10
+ - Pass the injected host clock to policy evaluators.
11
+ - Treat empty state-machine targets as no-op transitions.
12
+ - Default domain events to the action schema version while preserving explicit event versions.
13
+ - Omit custom extracted-event carrier fields from durable invocation results.
14
+
3
15
  ## 0.4.0 — 2026-07-19
4
16
 
5
17
  - 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,13 @@ 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. Emitted domain events must appear in the action's `emitsEvents` declaration. The
26
+ injected `now` clock is also passed to ordinary policy evaluation.
27
+
21
28
  Production polling workers can use `createStoreBackedActionDispatcher()` plus
22
29
  `runPlatformActionWorker()`. The pending invocation row is the durable queue item; workers claim
23
30
  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;
@@ -261,9 +266,19 @@ function createGovernedActionHost(options) {
261
266
  }
262
267
  data = handlerResult.data ?? {};
263
268
  domainEvents = extractEvents(data);
269
+ const undeclaredEvent = domainEvents.find(
270
+ (event) => !action.emitsEvents.includes(event.eventType)
271
+ );
272
+ if (undeclaredEvent) {
273
+ return fail(
274
+ invocation,
275
+ "failed",
276
+ `${action.actionId} emitted undeclared event type ${undeclaredEvent.eventType}`
277
+ );
278
+ }
264
279
  if (action.eventPhase !== "after_adapters") {
265
280
  for (const [index, event] of domainEvents.entries()) {
266
- await appendEvent(invocation, event, `domain:${index}`);
281
+ await appendEvent(invocation, event, `domain:${index}`, action.version);
267
282
  }
268
283
  }
269
284
  } catch (error) {
@@ -382,10 +397,10 @@ function createGovernedActionHost(options) {
382
397
  }
383
398
  if (action.eventPhase === "after_adapters") {
384
399
  for (const [index, event] of domainEvents.entries()) {
385
- await appendEvent(invocation, event, `domain:${index}`);
400
+ await appendEvent(invocation, event, `domain:${index}`, action.version);
386
401
  }
387
402
  }
388
- const result = withoutPrivateHostFields(data);
403
+ const result = withoutPrivateHostFields(data, eventResultFields);
389
404
  await options.store.updateActionInvocation(actionInvocationId, tenantId, spaceId, {
390
405
  status: "completed",
391
406
  result
@@ -416,7 +431,7 @@ function createGovernedActionHost(options) {
416
431
  error: `Invocation is not waiting for approval (status: ${invocation.status})`
417
432
  };
418
433
  }
419
- const action = platform.resolveAction(invocation.actionId);
434
+ const action = actionResolver(invocation.actionId);
420
435
  if (!action) return fail(invocation, "failed", `Unknown action: ${invocation.actionId}`);
421
436
  const approvalAuthorizationInput = toAuthorizationInput(action, {
422
437
  tenantId,
@@ -464,14 +479,14 @@ function createGovernedActionHost(options) {
464
479
  if (!decision.approved) return actionResult(transitioned);
465
480
  return executeInvocation(actionInvocationId, tenantId, spaceId, { leaseOwner });
466
481
  }
467
- async function appendEvent(invocation, event, deduplicationKey) {
482
+ async function appendEvent(invocation, event, deduplicationKey, defaultEventSchemaVersion = 1) {
468
483
  const timestamp = now();
469
484
  const envelope = {
470
485
  id: lifecycleId("evt", invocation.id, deduplicationKey),
471
486
  tenantId: invocation.tenantId,
472
487
  spaceId: invocation.spaceId,
473
488
  eventType: event.eventType,
474
- eventSchemaVersion: event.eventSchemaVersion ?? 1,
489
+ eventSchemaVersion: event.eventSchemaVersion ?? defaultEventSchemaVersion,
475
490
  subjectType: event.subjectType,
476
491
  subjectId: event.subjectId,
477
492
  actorId: invocation.actorId,
@@ -556,9 +571,10 @@ function initialState(entityType) {
556
571
  function isTerminal(status) {
557
572
  return ["completed", "failed", "blocked_by_policy", "validation_failed"].includes(status);
558
573
  }
559
- function withoutPrivateHostFields(data) {
560
- const { _events: _ignored, ...result } = data;
561
- return result;
574
+ function withoutPrivateHostFields(data, eventResultFields) {
575
+ return Object.fromEntries(
576
+ Object.entries(data).filter(([key]) => !eventResultFields.includes(key))
577
+ );
562
578
  }
563
579
  function errorMessage(error) {
564
580
  return error instanceof Error ? error.message : String(error);