@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 +12 -0
- package/README.md +8 -1
- package/dist/index.cjs +37 -21
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +37 -21
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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.
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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 =
|
|
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 ??
|
|
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
|
-
|
|
561
|
-
|
|
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);
|