@fabricorg/platform-host 1.0.0 → 2.0.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,38 @@
1
1
  # @fabricorg/platform-host
2
2
 
3
+ ## 2.0.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Close final uplift audit gaps: align Host 2.x documentation and migration evidence, surface every
8
+ compiler/generator API to LLM consumers, make memory outbox egress directly usable, sanitize relay
9
+ failures, validate complete archetype state maps, support recursive JSON Schema generation, reject
10
+ cross-platform path traversal, synchronize artifact version stamps, and include package licenses.
11
+ - Require explicit domain snapshot/restore support before memory stores advertise atomic outbox
12
+ transactions, buffer Host lifecycle commits until success, fail missing transaction capabilities
13
+ before handlers run, sanitize relay failures, and exclude sensitive Host lifecycle events from bus
14
+ egress by default.
15
+ - Prevalidate buffered memory transactions so failed commits cannot append partial event or outbox rows.
16
+ - Qualify result-validation rollback documentation by the configured domain transaction provider.
17
+ - Updated dependencies
18
+ - @fabricorg/platform@0.11.1
19
+
20
+ ## 2.0.0
21
+
22
+ ### Major Changes
23
+
24
+ - Require `@fabricorg/platform@^0.11.0` and its manifest v2/result-schema contract. Applications
25
+ upgrading from Host 1.x must upgrade Platform in the same deployment and regenerate manifest consumers.
26
+ - Add the optional durable event outbox, PostgreSQL and memory adapters, and at-least-once relay.
27
+ - Add manifest v2 typed entity contracts, v1 reader normalization, optional action result schemas, and
28
+ Host enforcement of result validation before events and adapters.
29
+
30
+ ### Patch Changes
31
+
32
+ - Updated dependencies
33
+ - Updated dependencies
34
+ - @fabricorg/platform@0.11.0
35
+
3
36
  ## 1.0.0
4
37
 
5
38
  ### Minor Changes
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.9.0 @fabricorg/platform-host@^0.7.0
6
+ pnpm add @fabricorg/platform@^0.11.0 @fabricorg/platform-host@^2.0.0
7
7
  ```
8
8
 
9
9
  ## AI agent integration boundary
@@ -15,7 +15,7 @@ registration, and submit stable idempotent commands. The gateway derives tenant
15
15
  calls `submitAction()`; an agent must never call handlers, adapters, workflow internals, or database
16
16
  writes directly.
17
17
 
18
- See the [Platform Host 0.7 agent integration
18
+ See the [Platform Host 2.x agent integration
19
19
  guide](https://platform.fabric.pro/docs/platform/reference/platform-host) for application wiring,
20
20
  execution-time authorization, the PostgreSQL transaction binder, recovery semantics, and the external
21
21
  gateway contract.
@@ -100,6 +100,13 @@ application knows how its `TDb` is rebound to the transaction's SQL client.
100
100
  Stores without this additive capability retain the legacy boundary for compatibility and must not
101
101
  claim atomic domain-write/event persistence.
102
102
 
103
+ Actions can optionally declare `resultSchema`. The Host validates public handler result data after
104
+ the handler and before canonical event append or adapter execution. Invalid results fail the invocation,
105
+ emit no domain/completion event, invoke no adapter, and persist only a sanitized validation failure.
106
+ Domain writes roll back only when the configured store transaction provider includes those writes.
107
+ This validates the handler result contract; it does not observe arbitrary writes through an
108
+ application-owned `TDb`.
109
+
103
110
  ## Agent HITL
104
111
 
105
112
  Hosts can inject a vertical-owned `hitlEvaluator`. It runs only for `actorType: "agent"`, after schema
@@ -171,7 +178,7 @@ explain which contract and provider adapter governed a mutation after dependenci
171
178
  const host = createGovernedActionHost({
172
179
  // ...
173
180
  runtimeEvidence: {
174
- hostPackageVersion: "0.7.0",
181
+ hostPackageVersion: "2.0.1",
175
182
  policyRulesetVersion: "gtm-rules.v8",
176
183
  providerBridge: { name: "@fabric-harness/databricks", version: "1" },
177
184
  },
@@ -181,3 +188,20 @@ const host = createGovernedActionHost({
181
188
  `MemoryPlatformHostStore` is for tests and local demos. Production control planes use
182
189
  `PostgresPlatformHostStore` with Databricks Lakebase (or standard Postgres), call
183
190
  `ensureSchema()` at startup, and hydrate projections from `listEvents()`.
191
+
192
+ ## Durable outbox egress
193
+
194
+ Configure `outbox` on `createGovernedActionHost()` only with an `OutboxPlatformHostStore`.
195
+ The Host then uses `appendEventWithOutbox` so the canonical event and delivery record share
196
+ the proven event transaction. `runOutboxRelayCycle()` leases and publishes records with retry
197
+ and dead-letter handling. Delivery is at least once: consumers deduplicate on immutable
198
+ `eventId`. Payload classification is metadata, not a redaction mechanism; event payloads must
199
+ already be audit-safe.
200
+
201
+ Adapter and compliance lifecycle events remain canonical but are excluded from bus egress by default.
202
+ Use `shouldPublish` to opt them in only when the application has made their payloads safe for that bus.
203
+
204
+ For deterministic local tests, `MemoryPlatformHostStore` enables governed Host outbox egress only
205
+ when its constructor receives a `MemoryPlatformHostTransactionProvider` with domain `snapshot` and
206
+ `restore` functions. Without that explicit rollback seam, `transactionalOutbox` is false and Host
207
+ construction rejects outbox configuration. PostgreSQL remains the production implementation.
package/dist/index.cjs CHANGED
@@ -16,6 +16,12 @@ var RecoverableFinalizationError = class extends Error {
16
16
  name = "RecoverableFinalizationError";
17
17
  };
18
18
  function createGovernedActionHost(options) {
19
+ if (options.outbox) {
20
+ const outboxStore = asOutboxStore(options.store);
21
+ if (!outboxStore || !asAtomicMutationStore(options.store) || outboxStore.transactionalOutbox !== true) {
22
+ throw new Error("Outbox egress requires a store with transactionalOutbox and transactionWithEvents capabilities.");
23
+ }
24
+ }
19
25
  const adapters = new platform.AdapterRegistry();
20
26
  for (const adapter of options.adapters ?? []) adapters.register(adapter);
21
27
  const now = options.now ?? (() => /* @__PURE__ */ new Date());
@@ -322,6 +328,9 @@ function createGovernedActionHost(options) {
322
328
  let domainEvents = [];
323
329
  try {
324
330
  const runHandler = async (db, transaction) => {
331
+ if (options.outbox && !transaction?.appendEventWithOutbox) {
332
+ throw new Error("Outbox egress requires transactionWithEvents to provide appendEventWithOutbox before handler execution.");
333
+ }
325
334
  const handlerResult = action.handler ? await action.handler(
326
335
  {
327
336
  actionInvocationId,
@@ -340,6 +349,13 @@ function createGovernedActionHost(options) {
340
349
  throw new Error(handlerResult.error ?? "Action handler failed");
341
350
  }
342
351
  const handlerData = handlerResult.data ?? {};
352
+ if (action.resultSchema) {
353
+ const publicResult = withoutPrivateHostFields(handlerData, eventResultFields);
354
+ const parsedResult = action.resultSchema.safeParse(publicResult);
355
+ if (!parsedResult.success) {
356
+ throw new Error(`Action result validation failed at ${parsedResult.error.issues.map((issue) => issue.path.map(String).join(".") || "result").join(", ")}`);
357
+ }
358
+ }
343
359
  const events = extractEvents(handlerData);
344
360
  const undeclaredEvent = events.find(
345
361
  (event) => !action.emitsEvents.includes(event.eventType)
@@ -680,7 +696,30 @@ function createGovernedActionHost(options) {
680
696
  correlationId: invocation.correlationId,
681
697
  ...invocation.causationId ? { causationId: invocation.causationId } : {}
682
698
  };
683
- await (transaction ?? options.store).appendEvent(envelope);
699
+ if (options.outbox) {
700
+ const hostLifecycleEvent = (/* @__PURE__ */ new Set(["AdapterInvocationStarted", "AdapterInvocationSucceeded", "AdapterInvocationFailed", "ComplianceBlocked"])).has(envelope.eventType);
701
+ const shouldPublish = options.outbox.shouldPublish?.(envelope) ?? !hostLifecycleEvent;
702
+ if (!shouldPublish) {
703
+ await (transaction ?? options.store).appendEvent(envelope);
704
+ return;
705
+ }
706
+ const traceContext = options.outbox.traceContext?.(envelope);
707
+ const metadata = {
708
+ producerModuleVersion: options.outbox.producerModuleVersion(invocation.actionId, invocation.actionVersion),
709
+ payloadClassification: options.outbox.classifyPayload(envelope),
710
+ ...traceContext ? { traceContext } : {}
711
+ };
712
+ if (transaction) {
713
+ if (!transaction.appendEventWithOutbox) throw new Error("Outbox egress requires an atomic transaction with appendEventWithOutbox.");
714
+ await transaction.appendEventWithOutbox(envelope, metadata);
715
+ } else {
716
+ const outboxStore = asOutboxStore(options.store);
717
+ if (!outboxStore) throw new Error("Outbox egress requires an outbox-capable Platform Host store.");
718
+ await outboxStore.appendEventWithOutbox(envelope, metadata);
719
+ }
720
+ } else {
721
+ await (transaction ?? options.store).appendEvent(envelope);
722
+ }
684
723
  }
685
724
  async function fail(invocation, status, error) {
686
725
  await options.store.updateActionInvocation(
@@ -717,6 +756,10 @@ function asAtomicMutationStore(store) {
717
756
  const candidate = store;
718
757
  return typeof candidate.transactionWithEvents === "function" ? store : void 0;
719
758
  }
759
+ function asOutboxStore(store) {
760
+ const candidate = store;
761
+ return typeof candidate.appendEventWithOutbox === "function" && typeof candidate.claimOutbox === "function" ? store : void 0;
762
+ }
720
763
  function asGovernanceStore(store) {
721
764
  const candidate = store;
722
765
  return typeof candidate.recordMutationGovernance === "function" && typeof candidate.appendPolicyObligations === "function" && typeof candidate.appendExecutionAttestation === "function" ? store : void 0;
@@ -773,12 +816,83 @@ function lifecycleId(prefix, invocationId, key) {
773
816
  return `${prefix}_${invocationId}_${safeKey}`;
774
817
  }
775
818
 
819
+ // src/outbox.ts
820
+ function toEnterpriseEventEnvelope(event, metadata) {
821
+ return {
822
+ eventId: event.id,
823
+ eventType: event.eventType,
824
+ eventSchemaVersion: event.eventSchemaVersion,
825
+ tenantId: event.tenantId,
826
+ spaceId: event.spaceId,
827
+ subjectType: event.subjectType,
828
+ subjectId: event.subjectId,
829
+ sequence: event.sequence,
830
+ ...event.actionInvocationId ? { actionInvocationId: event.actionInvocationId } : {},
831
+ correlationId: event.correlationId,
832
+ ...event.causationId ? { causationId: event.causationId } : {},
833
+ occurredAt: event.occurredAt,
834
+ recordedAt: event.recordedAt,
835
+ producerModuleVersion: metadata.producerModuleVersion,
836
+ payloadClassification: metadata.payloadClassification,
837
+ ...metadata.traceContext ? { traceContext: { ...metadata.traceContext } } : {},
838
+ payload: event.payload
839
+ };
840
+ }
841
+ async function runOutboxRelayCycle(options) {
842
+ const now = options.now ?? (() => /* @__PURE__ */ new Date());
843
+ const maxAttempts = options.maxAttempts ?? 10;
844
+ const records = await options.store.claimOutbox({
845
+ workerId: options.workerId,
846
+ leaseDurationMs: options.leaseDurationMs ?? 3e4,
847
+ limit: options.batchSize ?? 100,
848
+ now: now()
849
+ });
850
+ const result = { claimed: records.length, published: 0, failed: 0, deadLettered: 0 };
851
+ for (const record of records) {
852
+ try {
853
+ await options.publisher.publish(record.event);
854
+ try {
855
+ await options.store.markOutboxPublished(record.id, options.workerId, now());
856
+ } catch {
857
+ result.failed += 1;
858
+ continue;
859
+ }
860
+ result.published += 1;
861
+ } catch {
862
+ const deadLetter = record.attemptCount >= maxAttempts;
863
+ try {
864
+ await options.store.markOutboxFailed({
865
+ id: record.id,
866
+ workerId: options.workerId,
867
+ error: "Event publisher failed",
868
+ availableAt: new Date(now().getTime() + (options.retryDelayMs?.(record.attemptCount) ?? 1e3)),
869
+ deadLetter
870
+ });
871
+ } catch {
872
+ }
873
+ result.failed += 1;
874
+ if (deadLetter) result.deadLettered += 1;
875
+ }
876
+ }
877
+ return result;
878
+ }
879
+ function cloneOutboxRecord(record) {
880
+ return {
881
+ ...record,
882
+ event: { ...record.event, ...record.event.traceContext ? { traceContext: { ...record.event.traceContext } } : {} }
883
+ };
884
+ }
885
+
776
886
  // src/memory-store.ts
777
887
  var MemoryPlatformHostStore = class {
778
- constructor(db) {
888
+ constructor(db, transactionProvider) {
779
889
  this.db = db;
890
+ this.transactionalOutbox = transactionProvider !== void 0;
891
+ if (transactionProvider) this.transactionWithEvents = (run) => this.runTransactionWithEvents(run, transactionProvider);
780
892
  }
781
893
  db;
894
+ transactionalOutbox;
895
+ transactionTail = Promise.resolve();
782
896
  invocations = [];
783
897
  policyEvaluations = [];
784
898
  adapterInvocations = [];
@@ -786,9 +900,54 @@ var MemoryPlatformHostStore = class {
786
900
  policyObligations = [];
787
901
  executionAttestations = [];
788
902
  externalReconciliations = [];
903
+ outbox = [];
789
904
  async transaction(run) {
790
905
  return run(this.db);
791
906
  }
907
+ async runTransactionWithEvents(run, transactionProvider) {
908
+ let release;
909
+ const previous = this.transactionTail;
910
+ this.transactionTail = new Promise((resolve) => {
911
+ release = resolve;
912
+ });
913
+ await previous;
914
+ let domainSnapshot;
915
+ let snapshotCreated = false;
916
+ try {
917
+ domainSnapshot = transactionProvider.snapshot(this.db);
918
+ snapshotCreated = true;
919
+ const pendingEvents = [];
920
+ const pendingUpdates = [];
921
+ const appendPending = async (event, metadata) => {
922
+ if (this.events.some((candidate) => candidate.id === event.id) || pendingEvents.some((candidate) => candidate.event.id === event.id)) return;
923
+ pendingEvents.push({ event, ...metadata ? { metadata } : {} });
924
+ };
925
+ const result = await run({
926
+ db: this.db,
927
+ appendEvent: (event) => appendPending(event),
928
+ appendEventWithOutbox: (event, metadata) => appendPending(event, metadata),
929
+ nextEventSequence: async (tenantId, spaceId) => (await this.listEvents(tenantId, spaceId)).length + pendingEvents.filter((candidate) => candidate.event.tenantId === tenantId && candidate.event.spaceId === spaceId).length + 1,
930
+ listEvents: async (tenantId, spaceId) => [...await this.listEvents(tenantId, spaceId), ...pendingEvents.map((candidate) => candidate.event).filter((event) => event.tenantId === tenantId && event.spaceId === spaceId)],
931
+ updateActionInvocation: async (id, tenantId, spaceId, patch) => {
932
+ pendingUpdates.push({ id, tenantId, spaceId, patch });
933
+ }
934
+ });
935
+ for (const update of pendingUpdates) {
936
+ if (!await this.getActionInvocation(update.id, update.tenantId, update.spaceId)) throw new Error(`ActionInvocation not found: ${update.id}`);
937
+ }
938
+ for (const pending of pendingEvents) {
939
+ if (pending.metadata) await this.appendEventWithOutbox(pending.event, pending.metadata);
940
+ else await this.appendEvent(pending.event);
941
+ }
942
+ for (const update of pendingUpdates) await this.updateActionInvocation(update.id, update.tenantId, update.spaceId, update.patch);
943
+ return result;
944
+ } catch (error) {
945
+ if (snapshotCreated) transactionProvider.restore(this.db, domainSnapshot);
946
+ throw error;
947
+ } finally {
948
+ release();
949
+ }
950
+ }
792
951
  async createActionInvocation(input) {
793
952
  if (input.idempotencyKey) {
794
953
  const existing = this.invocations.find(
@@ -904,6 +1063,53 @@ var MemoryPlatformHostStore = class {
904
1063
  if (this.events.some((candidate) => candidate.id === event.id)) return;
905
1064
  this.events.push(event);
906
1065
  }
1066
+ async appendEventWithOutbox(event, metadata) {
1067
+ if (this.events.some((candidate) => candidate.id === event.id)) return;
1068
+ const createdAt = new Date(event.recordedAt);
1069
+ this.events.push(event);
1070
+ this.outbox.push({
1071
+ id: event.id,
1072
+ tenantId: event.tenantId,
1073
+ spaceId: event.spaceId,
1074
+ event: toEnterpriseEventEnvelope(event, metadata),
1075
+ status: "pending",
1076
+ attemptCount: 0,
1077
+ availableAt: createdAt,
1078
+ createdAt
1079
+ });
1080
+ }
1081
+ async claimOutbox(input) {
1082
+ const current = input.now ?? /* @__PURE__ */ new Date();
1083
+ return this.outbox.filter((record) => record.status === "pending" && record.availableAt <= current && (!record.leaseExpiresAt || record.leaseExpiresAt <= current) && (!input.tenantId || record.tenantId === input.tenantId) && (!input.spaceId || record.spaceId === input.spaceId)).sort((left, right) => left.createdAt.getTime() - right.createdAt.getTime() || left.id.localeCompare(right.id)).slice(0, Math.max(1, Math.min(input.limit ?? 100, 1e3))).map((record) => {
1084
+ record.leaseOwner = input.workerId;
1085
+ record.leaseExpiresAt = new Date(current.getTime() + input.leaseDurationMs);
1086
+ record.attemptCount += 1;
1087
+ return cloneOutboxRecord(record);
1088
+ });
1089
+ }
1090
+ async markOutboxPublished(id, workerId, publishedAt) {
1091
+ const record = this.requireLeasedOutbox(id, workerId);
1092
+ record.status = "published";
1093
+ record.publishedAt = publishedAt;
1094
+ delete record.leaseOwner;
1095
+ delete record.leaseExpiresAt;
1096
+ }
1097
+ async markOutboxFailed(input) {
1098
+ const record = this.requireLeasedOutbox(input.id, input.workerId);
1099
+ record.status = input.deadLetter ? "dead_letter" : "pending";
1100
+ record.lastError = input.error;
1101
+ record.availableAt = input.availableAt;
1102
+ delete record.leaseOwner;
1103
+ delete record.leaseExpiresAt;
1104
+ }
1105
+ async listOutbox(input = {}) {
1106
+ return this.outbox.filter((record) => (!input.tenantId || record.tenantId === input.tenantId) && (!input.spaceId || record.spaceId === input.spaceId) && (!input.statuses?.length || input.statuses.includes(record.status))).map(cloneOutboxRecord);
1107
+ }
1108
+ requireLeasedOutbox(id, workerId) {
1109
+ const record = this.outbox.find((candidate) => candidate.id === id);
1110
+ if (!record || record.leaseOwner !== workerId) throw new Error(`Outbox record ${id} is not leased by ${workerId}.`);
1111
+ return record;
1112
+ }
907
1113
  async nextEventSequence(tenantId, spaceId) {
908
1114
  return this.events.filter((event) => event.tenantId === tenantId && event.spaceId === spaceId).length + 1;
909
1115
  }
@@ -947,12 +1153,14 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
947
1153
  constructor(db, sql, transactionProvider) {
948
1154
  this.db = db;
949
1155
  this.sql = sql;
1156
+ this.transactionalOutbox = transactionProvider !== void 0;
950
1157
  if (transactionProvider) {
951
1158
  this.transactionWithEvents = async (run) => transactionProvider.run(async ({ db: db2, sql: sql2 }) => {
952
1159
  const scoped = new _PostgresPlatformHostStore(db2, sql2);
953
1160
  return run({
954
1161
  db: db2,
955
1162
  appendEvent: (event) => scoped.appendEvent(event),
1163
+ appendEventWithOutbox: (event, metadata) => scoped.appendEventWithOutbox(event, metadata),
956
1164
  nextEventSequence: (tenantId, spaceId) => scoped.nextEventSequence(tenantId, spaceId),
957
1165
  listEvents: (tenantId, spaceId) => scoped.listEvents(tenantId, spaceId),
958
1166
  updateActionInvocation: (id, tenantId, spaceId, patch) => scoped.updateActionInvocation(id, tenantId, spaceId, patch)
@@ -963,6 +1171,7 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
963
1171
  db;
964
1172
  sql;
965
1173
  transactionWithEvents;
1174
+ transactionalOutbox;
966
1175
  async ensureSchema() {
967
1176
  await this.sql.query(`
968
1177
  CREATE SCHEMA IF NOT EXISTS fabric_platform;
@@ -1046,6 +1255,15 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
1046
1255
  CREATE INDEX IF NOT EXISTS asset_events_subject_idx
1047
1256
  ON fabric_platform.asset_events
1048
1257
  (tenant_id, space_id, subject_type, subject_id, sequence);
1258
+ CREATE TABLE IF NOT EXISTS fabric_platform.event_outbox (
1259
+ id text PRIMARY KEY, tenant_id text NOT NULL, space_id text NOT NULL,
1260
+ event jsonb NOT NULL, status text NOT NULL DEFAULT 'pending',
1261
+ attempt_count integer NOT NULL DEFAULT 0, available_at timestamptz NOT NULL,
1262
+ lease_owner text, lease_expires_at timestamptz, last_error text,
1263
+ created_at timestamptz NOT NULL, published_at timestamptz
1264
+ );
1265
+ CREATE INDEX IF NOT EXISTS event_outbox_claim_idx
1266
+ ON fabric_platform.event_outbox (status, available_at, lease_expires_at, created_at);
1049
1267
  `);
1050
1268
  }
1051
1269
  async transaction(run) {
@@ -1328,6 +1546,86 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
1328
1546
  ]
1329
1547
  );
1330
1548
  }
1549
+ async appendEventWithOutbox(event, metadata) {
1550
+ const envelope = toEnterpriseEventEnvelope(event, metadata);
1551
+ await this.sql.query(
1552
+ `WITH inserted_event AS (
1553
+ INSERT INTO fabric_platform.asset_events
1554
+ (id,tenant_id,space_id,event_type,event_schema_version,subject_type,subject_id,
1555
+ actor_id,actor_type,action_invocation_id,payload,sequence,occurred_at,recorded_at,
1556
+ correlation_id,causation_id)
1557
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11::jsonb,$12,$13,$14,$15,$16)
1558
+ ON CONFLICT (id) DO NOTHING RETURNING id
1559
+ )
1560
+ INSERT INTO fabric_platform.event_outbox
1561
+ (id,tenant_id,space_id,event,status,attempt_count,available_at,created_at)
1562
+ SELECT $1,$2,$3,$17::jsonb,'pending',0,$14,$14 FROM inserted_event
1563
+ ON CONFLICT (id) DO NOTHING`,
1564
+ [
1565
+ event.id,
1566
+ event.tenantId,
1567
+ event.spaceId,
1568
+ event.eventType,
1569
+ event.eventSchemaVersion,
1570
+ event.subjectType,
1571
+ event.subjectId,
1572
+ event.actorId,
1573
+ event.actorType,
1574
+ event.actionInvocationId ?? null,
1575
+ JSON.stringify(event.payload),
1576
+ event.sequence,
1577
+ event.occurredAt,
1578
+ event.recordedAt,
1579
+ event.correlationId,
1580
+ event.causationId ?? null,
1581
+ JSON.stringify(envelope)
1582
+ ]
1583
+ );
1584
+ }
1585
+ async claimOutbox(input) {
1586
+ const current = input.now ?? /* @__PURE__ */ new Date();
1587
+ const leaseExpiresAt = new Date(current.getTime() + input.leaseDurationMs);
1588
+ const result = await this.sql.query(
1589
+ `WITH claimable AS (
1590
+ SELECT id FROM fabric_platform.event_outbox
1591
+ WHERE status='pending' AND available_at <= $1
1592
+ AND (lease_expires_at IS NULL OR lease_expires_at <= $1)
1593
+ AND ($2::text IS NULL OR tenant_id=$2)
1594
+ AND ($3::text IS NULL OR space_id=$3)
1595
+ ORDER BY created_at,id FOR UPDATE SKIP LOCKED LIMIT $4
1596
+ )
1597
+ UPDATE fabric_platform.event_outbox AS item
1598
+ SET lease_owner=$5, lease_expires_at=$6, attempt_count=attempt_count+1
1599
+ FROM claimable WHERE item.id=claimable.id RETURNING item.*`,
1600
+ [current, input.tenantId ?? null, input.spaceId ?? null, Math.max(1, Math.min(input.limit ?? 100, 1e3)), input.workerId, leaseExpiresAt]
1601
+ );
1602
+ return result.rows.map(toOutboxRecord);
1603
+ }
1604
+ async markOutboxPublished(id, workerId, publishedAt) {
1605
+ const result = await this.sql.query(
1606
+ `UPDATE fabric_platform.event_outbox SET status='published',published_at=$3,
1607
+ lease_owner=NULL,lease_expires_at=NULL WHERE id=$1 AND lease_owner=$2 RETURNING id`,
1608
+ [id, workerId, publishedAt]
1609
+ );
1610
+ if (result.rows.length === 0) throw new Error(`Outbox record ${id} is not leased by ${workerId}.`);
1611
+ }
1612
+ async markOutboxFailed(input) {
1613
+ const result = await this.sql.query(
1614
+ `UPDATE fabric_platform.event_outbox SET status=$3,last_error=$4,available_at=$5,
1615
+ lease_owner=NULL,lease_expires_at=NULL WHERE id=$1 AND lease_owner=$2 RETURNING id`,
1616
+ [input.id, input.workerId, input.deadLetter ? "dead_letter" : "pending", input.error, input.availableAt]
1617
+ );
1618
+ if (result.rows.length === 0) throw new Error(`Outbox record ${input.id} is not leased by ${input.workerId}.`);
1619
+ }
1620
+ async listOutbox(input = {}) {
1621
+ const result = await this.sql.query(
1622
+ `SELECT * FROM fabric_platform.event_outbox
1623
+ WHERE ($1::text IS NULL OR tenant_id=$1) AND ($2::text IS NULL OR space_id=$2)
1624
+ AND ($3::text[] IS NULL OR status=ANY($3::text[])) ORDER BY created_at,id`,
1625
+ [input.tenantId ?? null, input.spaceId ?? null, input.statuses?.length ? input.statuses : null]
1626
+ );
1627
+ return result.rows.map(toOutboxRecord);
1628
+ }
1331
1629
  async nextEventSequence(tenantId, spaceId) {
1332
1630
  const result = await this.sql.query(
1333
1631
  `INSERT INTO fabric_platform.event_sequences (tenant_id,space_id,next_sequence)
@@ -1516,6 +1814,41 @@ function toEventRecord(row) {
1516
1814
  ...row.causation_id ? { causationId: String(row.causation_id) } : {}
1517
1815
  };
1518
1816
  }
1817
+ function toOutboxRecord(row) {
1818
+ const event = row.event;
1819
+ return {
1820
+ id: String(row.id),
1821
+ tenantId: String(row.tenant_id),
1822
+ spaceId: String(row.space_id),
1823
+ event: {
1824
+ eventId: String(event.eventId),
1825
+ eventType: String(event.eventType),
1826
+ eventSchemaVersion: Number(event.eventSchemaVersion),
1827
+ tenantId: String(event.tenantId),
1828
+ spaceId: String(event.spaceId),
1829
+ subjectType: String(event.subjectType),
1830
+ subjectId: String(event.subjectId),
1831
+ sequence: Number(event.sequence),
1832
+ ...event.actionInvocationId ? { actionInvocationId: String(event.actionInvocationId) } : {},
1833
+ correlationId: String(event.correlationId),
1834
+ ...event.causationId ? { causationId: String(event.causationId) } : {},
1835
+ occurredAt: new Date(event.occurredAt),
1836
+ recordedAt: new Date(event.recordedAt),
1837
+ producerModuleVersion: String(event.producerModuleVersion),
1838
+ payloadClassification: String(event.payloadClassification),
1839
+ ...event.traceContext ? { traceContext: event.traceContext } : {},
1840
+ payload: event.payload
1841
+ },
1842
+ status: String(row.status),
1843
+ attemptCount: Number(row.attempt_count),
1844
+ availableAt: new Date(row.available_at),
1845
+ ...row.lease_owner ? { leaseOwner: String(row.lease_owner) } : {},
1846
+ ...row.lease_expires_at ? { leaseExpiresAt: new Date(row.lease_expires_at) } : {},
1847
+ ...row.last_error ? { lastError: String(row.last_error) } : {},
1848
+ createdAt: new Date(row.created_at),
1849
+ ...row.published_at ? { publishedAt: new Date(row.published_at) } : {}
1850
+ };
1851
+ }
1519
1852
 
1520
1853
  // src/worker.ts
1521
1854
  var DEFAULT_BATCH_SIZE = 10;
@@ -1588,9 +1921,12 @@ async function abortableDelay(milliseconds, signal) {
1588
1921
  exports.MemoryPlatformHostStore = MemoryPlatformHostStore;
1589
1922
  exports.PLATFORM_HOST_CONTRACT_VERSION = PLATFORM_HOST_CONTRACT_VERSION;
1590
1923
  exports.PostgresPlatformHostStore = PostgresPlatformHostStore;
1924
+ exports.cloneOutboxRecord = cloneOutboxRecord;
1591
1925
  exports.createGovernedActionHost = createGovernedActionHost;
1592
1926
  exports.createStoreBackedActionDispatcher = createStoreBackedActionDispatcher;
1927
+ exports.runOutboxRelayCycle = runOutboxRelayCycle;
1593
1928
  exports.runPlatformActionWorker = runPlatformActionWorker;
1594
1929
  exports.runPlatformActionWorkerCycle = runPlatformActionWorkerCycle;
1930
+ exports.toEnterpriseEventEnvelope = toEnterpriseEventEnvelope;
1595
1931
  //# sourceMappingURL=index.cjs.map
1596
1932
  //# sourceMappingURL=index.cjs.map