@powerhousedao/switchboard 6.2.3-dev.44 → 6.2.3-dev.46

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.
Files changed (33) hide show
  1. package/.tsbuild/test/durable-node-fs-pg16.test.d.ts +2 -0
  2. package/.tsbuild/test/durable-node-fs-pg16.test.d.ts.map +1 -0
  3. package/.tsbuild/test/pglite-preflight.test.d.ts +2 -0
  4. package/.tsbuild/test/pglite-preflight.test.d.ts.map +1 -0
  5. package/.tsbuild/test/snapshot-writer.d.ts +18 -0
  6. package/.tsbuild/test/snapshot-writer.d.ts.map +1 -0
  7. package/.tsbuild/test/workflow-reactor-access.test.d.ts +2 -0
  8. package/.tsbuild/test/workflow-reactor-access.test.d.ts.map +1 -0
  9. package/.tsbuild/test/workflow-reactor-enforcement.test.d.ts +2 -0
  10. package/.tsbuild/test/workflow-reactor-enforcement.test.d.ts.map +1 -0
  11. package/.tsbuild/tsconfig.tsbuildinfo +1 -1
  12. package/CHANGELOG.md +46 -0
  13. package/dist/index.mjs +1 -1
  14. package/dist/{server-Bk_IZzCn.mjs → server-Dvw8_NeM.mjs} +256 -77
  15. package/dist/server-Dvw8_NeM.mjs.map +1 -0
  16. package/dist/server.d.mts +3 -1
  17. package/dist/server.d.mts.map +1 -1
  18. package/dist/server.mjs +3 -3
  19. package/package.json +15 -16
  20. package/test/boot-unsupported-stored-documents.test.ts +5 -5
  21. package/test/durable-node-fs-pg16.test.ts +80 -0
  22. package/test/pglite-dialect.test.ts +28 -0
  23. package/test/pglite-preflight.test.ts +313 -0
  24. package/test/pglite-version.test.ts +9 -1
  25. package/test/snapshot-writer.ts +94 -0
  26. package/test/worker-pool.test.ts +27 -1
  27. package/test/workflow/resolvers.test.ts +26 -0
  28. package/test/workflow-reactor-access.test.ts +384 -0
  29. package/test/workflow-reactor-enforcement.test.ts +805 -0
  30. package/test/workflow-read-gate.test.ts +125 -60
  31. package/test/workflow-runtime.test.ts +158 -169
  32. package/tsconfig.json +0 -3
  33. package/dist/server-Bk_IZzCn.mjs.map +0 -1
package/CHANGELOG.md CHANGED
@@ -1,3 +1,49 @@
1
+ ## 6.2.3-dev.46 (2026-10-07)
2
+
3
+ ### 🚀 Features
4
+
5
+ - **connect:** load local project models in the reactor worker ([6e2929ce60](https://github.com/powerhouse-inc/powerhouse/commit/6e2929ce60))
6
+
7
+ ### 🩹 Fixes
8
+
9
+ - **builder-tools:** make the worker bundle survive consumer projects ([6361a66b6d](https://github.com/powerhouse-inc/powerhouse/commit/6361a66b6d))
10
+ - **builder-tools:** assert the vendor-dir mode only where modes exist ([f47c32a020](https://github.com/powerhouse-inc/powerhouse/commit/f47c32a020))
11
+ - **switchboard:** never let observability lower a fatal exit code ([0498342d22](https://github.com/powerhouse-inc/powerhouse/commit/0498342d22))
12
+ - **switchboard:** survive a logger that fails during fatal shutdown ([ecc8975282](https://github.com/powerhouse-inc/powerhouse/commit/ecc8975282))
13
+
14
+ ### ❤️ Thank You
15
+
16
+ - Claude Fable 5
17
+ - Wouter Kampmann
18
+
19
+ ## 6.2.3-dev.45 (2026-10-06)
20
+
21
+ ### 🚀 Features
22
+
23
+ - ⚠️ **reactor-workflow:** let any workflow piece read and write documents through the reactor ([fcccf0dfca](https://github.com/powerhouse-inc/powerhouse/commit/fcccf0dfca))
24
+ - **switchboard:** open PGlite stores over the durable NodeFS ([b5126c3056](https://github.com/powerhouse-inc/powerhouse/commit/b5126c3056))
25
+
26
+ ### 🩹 Fixes
27
+
28
+ - **pglite:** bound the old-dir removal after conversion ([0aef46f0ca](https://github.com/powerhouse-inc/powerhouse/commit/0aef46f0ca))
29
+ - **pglite:** open files read-write for the tree sync ([ce0fa23af5](https://github.com/powerhouse-inc/powerhouse/commit/ce0fa23af5))
30
+ - **switchboard:** resolve the legacy pg_dump wasm path on Windows ([c35148183a](https://github.com/powerhouse-inc/powerhouse/commit/c35148183a))
31
+ - **switchboard:** dump legacy PG16 stores from an in-memory copy ([8084fa5b1e](https://github.com/powerhouse-inc/powerhouse/commit/8084fa5b1e))
32
+
33
+ ### 🔥 Performance
34
+
35
+ - **pglite:** sync the tree once after initdb, conversion and migration ([9ec8caf970](https://github.com/powerhouse-inc/powerhouse/commit/9ec8caf970))
36
+
37
+ ### ⚠️ Breaking Changes
38
+
39
+ - **reactor-workflow:** let any workflow piece read and write documents through the reactor ([fcccf0dfca](https://github.com/powerhouse-inc/powerhouse/commit/fcccf0dfca))
40
+
41
+ ### ❤️ Thank You
42
+
43
+ - acaldas
44
+ - Benjamin Jordan
45
+ - Claude Fable 5.1
46
+
1
47
  ## 6.2.3-dev.44 (2026-10-06)
2
48
 
3
49
  ### 🩹 Fixes
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="1fbb2406-730e-5d3e-a4cf-ac7dea3cedd5")}catch(e){}}();
4
- import { i as StoredDocumentsRefusedError, r as startSwitchboard, s as parseForcePgVersion } from "./server-Bk_IZzCn.mjs";
4
+ import { i as StoredDocumentsRefusedError, r as startSwitchboard, s as parseForcePgVersion } from "./server-Dvw8_NeM.mjs";
5
5
  import { metrics } from "@opentelemetry/api";
6
6
  import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
7
7
  import { ExpressInstrumentation } from "@opentelemetry/instrumentation-express";
@@ -1,5 +1,5 @@
1
1
 
2
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="d5e0ce19-ffcf-5425-add6-a7c67ab79534")}catch(e){}}();
2
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="b9c02424-1406-5d63-abbd-6c32caa53205")}catch(e){}}();
3
3
  import { n as addDefaultReactorDrive, r as isPostgresUrl, t as addDefaultDrive } from "./utils-uaBlzyjS.mjs";
4
4
  import { createRequire, register } from "node:module";
5
5
  import { ReactorInstrumentation } from "@powerhousedao/opentelemetry-instrumentation-reactor";
@@ -9,7 +9,7 @@ import dotenv from "dotenv";
9
9
  import { getConfig } from "@powerhousedao/config/node";
10
10
  import { existsSync, promises, realpathSync } from "node:fs";
11
11
  import path, { resolve } from "node:path";
12
- import { AtomicNodeFs } from "@powerhousedao/pglite-fs";
12
+ import { createDurableNodeFs, preparePgliteDataDir, removeStalePgliteFiles, resolvePgliteFsync, syncTree } from "@powerhousedao/reactor-api/pglite-node";
13
13
  import { ChannelScheme, DriveCollectionId, EventBus, REACTOR_SCHEMA, ReactorBuilder, ReactorClientBuilder, UnsupportedStoredProtocolError, createHybridProjectionCoordinatorFactory, instrumentPgPool, parseDriveUrl, supportsLiveReadModelRegistration } from "@powerhousedao/reactor";
14
14
  import { AuthorizationPolicy, BaseSubgraph, ForbiddenError, HttpPackageLoader, ImportPackageLoader, PGLITE_UTC_PARSERS, PackageManagementService, PackagesSubgraph, PrivacyPermissionAdapter, callerSubject, createCanonicalDocumentIdResolver, getUniqueDocumentModels, initializeAndStartAPI, resolveRenownConfig } from "@powerhousedao/reactor-api";
15
15
  import { httpsHooksPath } from "@powerhousedao/reactor-api/https-hooks";
@@ -29,7 +29,7 @@ import { driveDocumentModelModule } from "@powerhousedao/shared/document-drive";
29
29
  import { DEFAULT_SIGNATURE_POLICY, isSignaturePolicy } from "@powerhousedao/shared/document-model";
30
30
  import { readFile } from "node:fs/promises";
31
31
  import os from "node:os";
32
- import { fileURLToPath } from "node:url";
32
+ import { fileURLToPath, pathToFileURL } from "node:url";
33
33
  import { EnvVarProvider } from "@openfeature/env-var-provider";
34
34
  import { OpenFeature } from "@openfeature/server-sdk";
35
35
  import { GraphQLError, Kind } from "graphql";
@@ -65,6 +65,19 @@ async function loadPGliteModule(major) {
65
65
  if (major === 16) return await import("pglite-legacy-02");
66
66
  return import("@electric-sql/pglite");
67
67
  }
68
+ /** The stock NodeFS class of the PGlite module that opens `major` dirs. */
69
+ async function loadNodeFsClass(major) {
70
+ if (major === 16) return (await import("pglite-legacy-02/nodefs")).NodeFS;
71
+ return (await import("@electric-sql/pglite/nodefs")).NodeFS;
72
+ }
73
+ /** Opens a converted dir over stock NodeFS so the conversion can check it. */
74
+ async function openForVerify(major, dataDir) {
75
+ if (!isSupportedMajor(major)) throw new Error(`Cannot verify PGlite data dir ${dataDir}: PG_VERSION=${major} is not supported (expected one of ${SUPPORTED_PG_MAJORS.join(", ")})`);
76
+ const [{ PGlite }, NodeFS] = await Promise.all([loadPGliteModule(major), loadNodeFsClass(major)]);
77
+ const pg = new PGlite({ fs: new NodeFS(dataDir) });
78
+ await pg.waitReady;
79
+ return pg;
80
+ }
68
81
  async function loadPgDump(major) {
69
82
  if (major === 16) return (await import("pglite-tools-legacy-02/pg_dump")).pgDump;
70
83
  return (await import("@electric-sql/pglite-tools/pg_dump")).pgDump;
@@ -798,9 +811,11 @@ const fatalHandlers = /* @__PURE__ */ new WeakMap();
798
811
  /**
799
812
  * Sends an uncaught exception or unhandled rejection through the SIGTERM
800
813
  * shutdown that `withSignalHandlers()` installs, so the reactor and read-model
801
- * PGlite stores write their snapshots before the process exits with code 1.
802
- * Node's default is to exit at once, which drops every write AtomicNodeFs has
803
- * not flushed yet. A shutdown that hangs is cut off after FORCED_EXIT_MS.
814
+ * PGlite stores close with a shutdown checkpoint before the process exits
815
+ * with code 1. Node's default is to exit at once, which leaves both stores
816
+ * to WAL recovery on the next open. A store whose wasm runtime aborted
817
+ * (ENOSPC) rejects its close; the builder logs that and still exits. A
818
+ * shutdown that hangs is cut off after FORCED_EXIT_MS.
804
819
  *
805
820
  * A rejection is fatal only when this is its sole listener: Node exits on an
806
821
  * unhandled rejection only when nothing listens for it, and Sentry's
@@ -1048,6 +1063,20 @@ const BASE_MODEL_SPECIFIERS = [
1048
1063
  "@powerhousedao/reactor-drive",
1049
1064
  "@powerhousedao/reactor-group/document-models"
1050
1065
  ];
1066
+ function isModelModule(value) {
1067
+ const candidate = value;
1068
+ return typeof candidate?.reducer === "function" && typeof candidate.documentModel?.global?.id === "string";
1069
+ }
1070
+ async function keepImportableSources(sources, logger) {
1071
+ const kept = [];
1072
+ for (const source of sources) try {
1073
+ const exports = await import(pathToFileURL(source.filePath).href);
1074
+ if (Object.values(exports).some(isModelModule)) kept.push(source);
1075
+ } catch (error) {
1076
+ logger.warn(`Worker model sources: "${source.filePath}" does not import, so workflow pieces will not see its models: ${String(error)}`);
1077
+ }
1078
+ return kept;
1079
+ }
1051
1080
  /**
1052
1081
  * Resolves base models and configured package identifiers to `{ filePath }`
1053
1082
  * sources for the reactor builder. Resolution to absolute paths happens here
@@ -1362,6 +1391,7 @@ function toStepRecord(row) {
1362
1391
  output: parseJson(row.output),
1363
1392
  port: row.port,
1364
1393
  error: row.error,
1394
+ errorName: row.error_name,
1365
1395
  startedAt: row.started_at,
1366
1396
  endedAt: row.ended_at,
1367
1397
  pieceVersion: row.piece_version,
@@ -1380,6 +1410,7 @@ function toRunRecord(row, steps) {
1380
1410
  triggerPayload: parseJson(row.trigger_payload),
1381
1411
  status: row.status,
1382
1412
  error: row.error,
1413
+ errorName: row.error_name,
1383
1414
  startedAt: row.started_at,
1384
1415
  endedAt: row.ended_at,
1385
1416
  rerunOf: row.rerun_of,
@@ -1388,6 +1419,14 @@ function toRunRecord(row, steps) {
1388
1419
  steps: steps.map(toStepRecord)
1389
1420
  };
1390
1421
  }
1422
+ const UNKNOWN_ACCESS = {
1423
+ authEnforcement: true,
1424
+ authConditions: false,
1425
+ identity: null
1426
+ };
1427
+ function signedIn(ctx) {
1428
+ return Boolean(ctx.user?.address);
1429
+ }
1391
1430
  function isAbsolute(url) {
1392
1431
  return /^https?:\/\//i.test(url);
1393
1432
  }
@@ -1416,13 +1455,21 @@ function returnUrlFor(requested, ctx) {
1416
1455
  if (!url || !origin || url.origin !== origin) throw new GraphQLError("returnUrl must be on the requesting page's origin");
1417
1456
  return url.href;
1418
1457
  }
1419
- const getResolvers = (runtime, authorizationService, oauth) => {
1458
+ const getResolvers = (runtime, authorizationService, oauth, access = UNKNOWN_ACCESS, assertCanRead) => {
1420
1459
  return {
1421
1460
  Query: { workflowRuntime: () => ({}) },
1422
1461
  WorkflowRuntimeQueries: {
1423
1462
  health: () => "ok",
1463
+ authEnforcement: () => access.authEnforcement,
1464
+ reactorIdentity: (_parent, _args, ctx) => signedIn(ctx) ? access.identity : null,
1465
+ authConditions: (_parent, _args, ctx) => signedIn(ctx) ? access.authConditions : null,
1466
+ reactorAccessDenial: async (_parent, args, ctx) => {
1467
+ if (!assertCanRead) throw new GraphQLError("No read check configured");
1468
+ await assertCanRead(args.workflowId, ctx);
1469
+ return runtime.reactorAccessDenial(args.workflowId) ?? null;
1470
+ },
1424
1471
  blockDescriptor: (_parent, args) => runtime.blockDescriptor(blockRef(args.block)),
1425
- blockOptions: (_parent, args, ctx) => runtime.blockOptions(blockRef(args.block), args.propName, args.input, args.connectionId ?? void 0, ctx, args.searchValue ?? void 0),
1472
+ blockOptions: (_parent, args, ctx) => runtime.blockOptions(blockRef(args.block), args.propName, args.input, args.connectionId ?? void 0, ctx, args.searchValue ?? void 0, args.reactorConnectionId ?? void 0),
1426
1473
  pieceCatalog: () => runtime.pieceCatalog(),
1427
1474
  pieceActions: (_parent, args) => runtime.pieceActions(args.packageName, args.version ?? void 0),
1428
1475
  pieceTriggers: (_parent, args) => runtime.pieceTriggers(args.packageName, args.version ?? void 0),
@@ -1533,6 +1580,28 @@ const schema = gql`
1533
1580
  type WorkflowRuntimeQueries {
1534
1581
  health: String!
1535
1582
  """
1583
+ Whether this reactor enforces document auth (REACTOR_AUTH_ENFORCEMENT).
1584
+ On, a workflow needs a signed publish, and the host grants, to reach
1585
+ documents. Answered to any caller.
1586
+ """
1587
+ authEnforcement: Boolean!
1588
+ """
1589
+ Who this host signs as: grant it on each document a step writes. Null for
1590
+ a caller who is not signed in, or a host with no signer.
1591
+ """
1592
+ reactorIdentity: ReactorIdentity
1593
+ """
1594
+ REACTOR_AUTH_CONDITIONS. True: grant the host by key, with
1595
+ { match: subject.key == key }; false: by address. Null for a caller who
1596
+ is not signed in.
1597
+ """
1598
+ authConditions: Boolean
1599
+ """
1600
+ Why the workflow's last publish or enable got no reactor access, naming
1601
+ the publisher and the connection; null when the check passed.
1602
+ """
1603
+ reactorAccessDenial(workflowId: String!): String
1604
+ """
1536
1605
  Persisted runs, newest first. Scope them to one workflow, or to every
1537
1606
  workflow a drive holds; workflowId wins when both are given.
1538
1607
  """
@@ -1573,6 +1642,8 @@ const schema = gql`
1573
1642
  connectionId: String
1574
1643
  "What the author typed, for a DROPDOWN declared with refreshOnSearch."
1575
1644
  searchValue: String
1645
+ "The step's reactor connection; its filter narrows what the resolver reads."
1646
+ reactorConnectionId: String
1576
1647
  ): Unknown
1577
1648
  """
1578
1649
  Every piece this reactor offers: the core piece, its package pieces, and
@@ -1750,6 +1821,13 @@ const schema = gql`
1750
1821
  accountLabel: String
1751
1822
  }
1752
1823
 
1824
+ type ReactorIdentity {
1825
+ "The address the host signs as; null without a signed-in identity."
1826
+ address: String
1827
+ "The did:key that signs the host's operations."
1828
+ key: String!
1829
+ }
1830
+
1753
1831
  type ConnectionCheckResult {
1754
1832
  ok: Boolean!
1755
1833
  detail: String
@@ -1811,6 +1889,8 @@ const schema = gql`
1811
1889
  output: Unknown
1812
1890
  port: String
1813
1891
  error: String
1892
+ "The thrown error's name, e.g. ReactorAccessDeniedError."
1893
+ errorName: String
1814
1894
  startedAt: String
1815
1895
  endedAt: String
1816
1896
  "The piece version that ran; null for an unresolved block."
@@ -1832,6 +1912,7 @@ const schema = gql`
1832
1912
  triggerPayload: Unknown
1833
1913
  status: String!
1834
1914
  error: String
1915
+ errorName: String
1835
1916
  "When the run began executing; a PENDING run's is when it was journaled."
1836
1917
  startedAt: String!
1837
1918
  endedAt: String
@@ -1869,12 +1950,14 @@ const schema = gql`
1869
1950
  output: Unknown
1870
1951
  port: String
1871
1952
  error: String
1953
+ errorName: String
1872
1954
  }
1873
1955
 
1874
1956
  type WorkflowRunPayload {
1875
1957
  runId: String
1876
1958
  status: String!
1877
1959
  error: String
1960
+ errorName: String
1878
1961
  steps: [WorkflowStepRun!]!
1879
1962
  }
1880
1963
 
@@ -1885,6 +1968,7 @@ const schema = gql`
1885
1968
  status: String!
1886
1969
  output: Unknown
1887
1970
  error: String
1971
+ errorName: String
1888
1972
  durationMs: Int!
1889
1973
  }
1890
1974
 
@@ -1969,11 +2053,11 @@ const schema = gql`
1969
2053
  //#region src/workflow/subgraph.ts
1970
2054
  /** The runtime's read/write surface. The runtime itself is composed by the
1971
2055
  * host, so the subgraph only serves what it is handed. */
1972
- function createWorkflowRuntimeSubgraph(runtime, oauth) {
2056
+ function createWorkflowRuntimeSubgraph(runtime, oauth, access) {
1973
2057
  return class WorkflowRuntimeSubgraph extends BaseSubgraph {
1974
2058
  name = "workflow-runtime";
1975
2059
  typeDefs = schema;
1976
- resolvers = getResolvers(runtime, this.authorizationService, oauth);
2060
+ resolvers = getResolvers(runtime, this.authorizationService, oauth, access, (documentId, ctx) => this.assertCanRead(documentId, ctx));
1977
2061
  additionalContextFields = {};
1978
2062
  };
1979
2063
  }
@@ -1999,6 +2083,12 @@ async function assertWorkflowPackageLoadable(load = () => import("@powerhousedao
1999
2083
  throw new Error(`Workflows are enabled but ${WORKFLOW_PACKAGE_NAME} could not be loaded`, { cause: error });
2000
2084
  }
2001
2085
  }
2086
+ function modelManifestSource(builder) {
2087
+ return {
2088
+ modelManifest: () => builder.getResolvedModelManifest() ?? [],
2089
+ modelEntries: (documentType) => builder.getImportableEntries(documentType)
2090
+ };
2091
+ }
2002
2092
  function readAssertion(authorizationService, reactorClient) {
2003
2093
  const resolveCanonical = createCanonicalDocumentIdResolver(reactorClient);
2004
2094
  const hostCanRead = async (identifier, ctx) => {
@@ -2040,6 +2130,37 @@ function writeAssertion(authorizationService, reactorClient) {
2040
2130
  if (!await authorizationService.canWrite(documentId, ctx.user?.address)) throw new ForbiddenError("to write this document");
2041
2131
  };
2042
2132
  }
2133
+ /** What the reactor enforces and who it signs as; unknown counts as enforced. */
2134
+ function reactorAccessOf(clientModule) {
2135
+ const flags = clientModule?.reactorModule?.featureFlags;
2136
+ const signer = clientModule?.signer;
2137
+ const key = signer?.app?.key;
2138
+ const identity = key ? {
2139
+ address: signer.user?.address ?? null,
2140
+ key
2141
+ } : null;
2142
+ return {
2143
+ authEnforcement: flags ? flags.authEnforcement : true,
2144
+ authConditions: flags?.authConditions === true,
2145
+ identity
2146
+ };
2147
+ }
2148
+ /** Whom a run's created documents also grant: the host, as §8 grants it. */
2149
+ function hostPrincipalOf(access) {
2150
+ const identity = access.identity;
2151
+ if (!identity) return void 0;
2152
+ if (access.authConditions) return { match: { eq: [{ attr: "subject.key" }, { lit: identity.key }] } };
2153
+ return identity.address ? { address: identity.address } : void 0;
2154
+ }
2155
+ /** Who the host signs as, so a publish it signs makes no run user. */
2156
+ function hostIdentityOf(access) {
2157
+ const identity = access.identity;
2158
+ if (!identity) return void 0;
2159
+ return identity.address ? {
2160
+ address: identity.address,
2161
+ key: identity.key
2162
+ } : { key: identity.key };
2163
+ }
2043
2164
  /** A step reads any well-formed attachment, as it reads any document: a run
2044
2165
  * carries no caller to check against. */
2045
2166
  function canReadAttachmentRef(_documentId, ref) {
@@ -2082,6 +2203,8 @@ async function composeWorkflowRuntime(deps) {
2082
2203
  }
2083
2204
  engine.setPieceRegistryUrl(deps.pieceRegistryUrl);
2084
2205
  if (deps.pieces) bindPackagePieces(engine.packagePieces, deps.pieces);
2206
+ const access = reactorAccessOf(deps.clientModule);
2207
+ const models = deps.models;
2085
2208
  const runtime = engine.createWorkflowRuntime({
2086
2209
  relationalDb: deps.relationalDb,
2087
2210
  secretsKeyFile: deps.secretsKeyFile,
@@ -2089,6 +2212,13 @@ async function composeWorkflowRuntime(deps) {
2089
2212
  assertCanRead: readAssertion(deps.authorizationService, deps.reactorClient),
2090
2213
  subjectOf: (caller) => callerSubject(caller.user),
2091
2214
  assertCanWrite: writeAssertion(deps.authorizationService, deps.reactorClient),
2215
+ ...deps.clientModule?.reactorModule ? { authEnforcement: access.authEnforcement } : {},
2216
+ hostPrincipal: hostPrincipalOf(access),
2217
+ hostIdentity: hostIdentityOf(access),
2218
+ ...models ? {
2219
+ modelManifest: () => models.modelManifest(),
2220
+ modelEntries: (documentType) => models.modelEntries(documentType)
2221
+ } : {},
2092
2222
  webhooks: deps.webhooks,
2093
2223
  attachments: deps.attachments,
2094
2224
  canReadAttachmentRef,
@@ -2100,7 +2230,7 @@ async function composeWorkflowRuntime(deps) {
2100
2230
  let stopped = false;
2101
2231
  const oauthCallback = deps.http ? registerOAuthCallback(deps.http, runtime) : void 0;
2102
2232
  return {
2103
- subgraph: createWorkflowRuntimeSubgraph(runtime, deps.http ? { callbackUrl: callbackUrlOf(deps.http) } : void 0),
2233
+ subgraph: createWorkflowRuntimeSubgraph(runtime, deps.http ? { callbackUrl: callbackUrlOf(deps.http) } : void 0, access),
2104
2234
  triggers,
2105
2235
  async start() {
2106
2236
  await runtime.registerWebhookEndpoint();
@@ -2129,7 +2259,12 @@ var ClosablePGliteDialect = class extends PGliteDialect {
2129
2259
  const innerDestroy = driver.destroy.bind(driver);
2130
2260
  driver.destroy = async () => {
2131
2261
  await innerDestroy();
2132
- if (!pglite.closed) await pglite.close();
2262
+ if (pglite.closed) return;
2263
+ try {
2264
+ await pglite.close();
2265
+ } catch (err) {
2266
+ if (!/PGlite aborted/.test(String(err))) throw err;
2267
+ }
2133
2268
  };
2134
2269
  return driver;
2135
2270
  }
@@ -2184,7 +2319,15 @@ async function migratePgliteDir(dataDir, logger) {
2184
2319
  try {
2185
2320
  const [legacyMod, pgDump] = await Promise.all([loadPGliteModule(major), loadPgDump(major)]);
2186
2321
  const LegacyPGlite = legacyMod.PGlite;
2187
- const pg = new LegacyPGlite(backupDir);
2322
+ const onDisk = new LegacyPGlite(backupDir);
2323
+ let tar;
2324
+ try {
2325
+ await onDisk.waitReady;
2326
+ tar = await onDisk.dumpDataDir("none");
2327
+ } finally {
2328
+ await onDisk.close();
2329
+ }
2330
+ const pg = new LegacyPGlite({ loadDataDir: tar });
2188
2331
  try {
2189
2332
  await pg.waitReady;
2190
2333
  sql = await (await pgDump({ pg })).text();
@@ -2214,6 +2357,7 @@ async function migratePgliteDir(dataDir, logger) {
2214
2357
  } finally {
2215
2358
  await pg.close();
2216
2359
  }
2360
+ syncTree(dataDir);
2217
2361
  } catch (err) {
2218
2362
  await rollback(dataDir, backupDir, err, logger);
2219
2363
  throw err;
@@ -2234,6 +2378,59 @@ async function rollback(dataDir, backupDir, originalError, logger) {
2234
2378
  logger.error(`[pglite-migration] Migration failed for ${dataDir}; rolled back from ${backupDir}. Original error: ${String(originalError)}`);
2235
2379
  }
2236
2380
  //#endregion
2381
+ //#region src/pglite-preflight.ts
2382
+ const RM_OPTIONS = {
2383
+ recursive: true,
2384
+ force: true,
2385
+ maxRetries: 10,
2386
+ retryDelay: 100
2387
+ };
2388
+ /**
2389
+ * Prepares every local PGlite dir before the first open and returns the
2390
+ * PG major detected on disk per dir. Conversion of an AtomicNodeFs snapshot
2391
+ * runs for every dir, before the major is read, so a pure-snapshot PG16 dir
2392
+ * is converted and then migrated.
2393
+ */
2394
+ async function runPglitePreflight(options) {
2395
+ const { dirs, logger } = options;
2396
+ const detectedMajors = /* @__PURE__ */ new Map();
2397
+ if (options.inMemory || dirs.length === 0) return detectedMajors;
2398
+ if (options.forcePgVersion !== void 0) {
2399
+ if (options.migratePglite) logger.warn("PH_FORCE_PG_VERSION is set; ignoring --migrate-pglite/PH_MIGRATE_PGLITE because the data dirs will be wiped.");
2400
+ logger.warn(`PH_FORCE_PG_VERSION=${options.forcePgVersion} set; wiping PGLite data dirs and re-initializing at PG${options.forcePgVersion}.`);
2401
+ for (const dir of dirs) {
2402
+ for (const p of [
2403
+ dir,
2404
+ `${dir}.converting`,
2405
+ `${dir}.old`
2406
+ ]) await promises.rm(p, RM_OPTIONS);
2407
+ logger.info(`Wiped PGLite data dir ${dir}`);
2408
+ }
2409
+ return detectedMajors;
2410
+ }
2411
+ const deps = {
2412
+ openForVerify: options.openForVerify ?? openForVerify,
2413
+ logger
2414
+ };
2415
+ for (const dir of dirs) {
2416
+ await preparePgliteDataDir(dir, deps);
2417
+ const major = await readPgVersionFile(dir);
2418
+ if (major !== null) detectedMajors.set(dir, major);
2419
+ }
2420
+ if (options.migratePglite) for (const [dir, major] of detectedMajors) {
2421
+ if (major === 17) continue;
2422
+ await migratePgliteDir(dir, logger);
2423
+ await removeStalePgliteFiles(dir, logger);
2424
+ const after = await readPgVersionFile(dir);
2425
+ if (after !== null) detectedMajors.set(dir, after);
2426
+ }
2427
+ else for (const [dir, major] of detectedMajors) {
2428
+ if (major === 17) continue;
2429
+ logger.warn(`PGLite data dir at ${dir} was created with PG${major} but Switchboard ships PG17. Running on legacy PGLite. Re-start with --migrate-pglite (or PH_MIGRATE_PGLITE=true) to upgrade.`);
2430
+ }
2431
+ return detectedMajors;
2432
+ }
2433
+ //#endregion
2237
2434
  //#region src/reactor-feature-flags.mts
2238
2435
  /**
2239
2436
  * Enforcement flags from the REACTOR_* env vars. Each flag requires the ones
@@ -2422,14 +2619,29 @@ const DOCUMENT_MODEL_SUBGRAPHS_ENABLED = "DOCUMENT_MODEL_SUBGRAPHS_ENABLED";
2422
2619
  const DOCUMENT_MODEL_SUBGRAPHS_ENABLED_DEFAULT = true;
2423
2620
  const DEFAULT_PORT = process.env.PORT ? Number(process.env.PORT) : 4001;
2424
2621
  const PORT_FALLBACK_ATTEMPTS = 20;
2425
- const PGLITE_FLUSH_INTERVAL_MS = (() => {
2426
- const raw = process.env.PGLITE_FLUSH_INTERVAL_MS;
2427
- if (raw === void 0) return 100;
2428
- const parsed = Number(raw);
2429
- return Number.isFinite(parsed) && parsed >= 0 ? parsed : 100;
2430
- })();
2431
- function onPgliteFlushError(err) {
2432
- triggerFatalShutdown("PGlite snapshot flush failed", err);
2622
+ let flushIntervalWarned = false;
2623
+ function warnIgnoredFlushInterval(logger) {
2624
+ if (flushIntervalWarned) return;
2625
+ if (process.env.PGLITE_FLUSH_INTERVAL_MS === void 0) return;
2626
+ flushIntervalWarned = true;
2627
+ logger.warn("PGLITE_FLUSH_INTERVAL_MS is set but ignored: PGlite stores no longer snapshot on a timer.");
2628
+ }
2629
+ function onPgliteAbort(logger, what) {
2630
+ if (!triggerFatalShutdown("PGlite aborted", what)) logger.error("PGlite aborted: @error", what);
2631
+ }
2632
+ function pgliteFsyncFor(major, dir, logger) {
2633
+ const wanted = resolvePgliteFsync(process.env);
2634
+ if (wanted && major === 16) logger.warn(`PGlite data dir ${dir} is PG16 on pglite-legacy-02, which does not sync commits to disk; only checkpoints are synced until --migrate-pglite upgrades it.`);
2635
+ return wanted;
2636
+ }
2637
+ /** Preloads the module's NodeFS so the returned factory can be synchronous. */
2638
+ async function loadDurableFsFactory(major, logger) {
2639
+ const NodeFS = await loadNodeFsClass(major);
2640
+ return (dir) => createDurableNodeFs(NodeFS, dir, {
2641
+ fsync: pgliteFsyncFor(major, dir, logger),
2642
+ logger,
2643
+ onAbort: (what) => onPgliteAbort(logger, what)
2644
+ });
2433
2645
  }
2434
2646
  const PGLITE_IN_MEMORY = process.env.PH_PGLITE_IN_MEMORY === "1";
2435
2647
  /**
@@ -2476,7 +2688,7 @@ async function resolveServerPort(requested, strictPort, logger) {
2476
2688
  return requested;
2477
2689
  }
2478
2690
  async function createReactorKysely(opts) {
2479
- const { reactorDbUrl, reactorPgliteDir, reactorPgliteMajor, inMemory, flushIntervalMs, hostPoolSize, logger } = opts;
2691
+ const { reactorDbUrl, reactorPgliteDir, reactorPgliteMajor, inMemory, hostPoolSize, logger } = opts;
2480
2692
  if (reactorDbUrl && isPostgresUrl(reactorDbUrl)) {
2481
2693
  const connectionString = reactorDbUrl.includes("?") ? reactorDbUrl : `${reactorDbUrl}?sslmode=disable`;
2482
2694
  const poolSize = hostPoolSize();
@@ -2493,11 +2705,7 @@ async function createReactorKysely(opts) {
2493
2705
  }
2494
2706
  if (!reactorPgliteDir || reactorPgliteMajor === null) throw new Error("Reactor PGLite directory not resolved");
2495
2707
  const { PGlite } = await loadPGliteModule(reactorPgliteMajor);
2496
- const pglite = inMemory ? new PGlite() : new PGlite({ fs: new AtomicNodeFs(reactorPgliteDir, {
2497
- logger,
2498
- flushIntervalMs,
2499
- onFlushError: onPgliteFlushError
2500
- }) });
2708
+ const pglite = inMemory ? new PGlite() : new PGlite({ fs: (await loadDurableFsFactory(reactorPgliteMajor, logger))(reactorPgliteDir) });
2501
2709
  logger.info(inMemory ? `Using in-memory PGlite (PG${reactorPgliteMajor}) for reactor storage [PH_PGLITE_IN_MEMORY=1]` : `Using PGlite (PG${reactorPgliteMajor}) for reactor storage at ${reactorPgliteDir}`);
2502
2710
  return {
2503
2711
  kysely: new Kysely({ dialect: new ClosablePGliteDialect(pglite) }),
@@ -2525,42 +2733,14 @@ async function initServer(serverPort, options, renown, renownConfig) {
2525
2733
  const reactorPgliteDir = options.reactor ? null : !reactorDbUrl || !isPostgresUrl(reactorDbUrl) ? reactorPath : null;
2526
2734
  const readModelPgliteDir = !dbPath || !isPostgresUrl(dbPath) ? readModelPath : null;
2527
2735
  const pgliteDirs = [reactorPgliteDir, readModelPgliteDir].filter((d) => d !== null);
2528
- const detectedMajors = /* @__PURE__ */ new Map();
2529
- for (const dir of pgliteDirs) {
2530
- const lockPath = path$1.join(dir, "postmaster.pid");
2531
- try {
2532
- await promises.unlink(lockPath);
2533
- logger.warn(`Removed stale PGLite lockfile ${lockPath}`);
2534
- } catch (err) {
2535
- if (err.code !== "ENOENT") throw err;
2536
- }
2537
- }
2538
- if (options.forcePgVersion !== void 0 && pgliteDirs.length > 0) {
2539
- if (options.migratePglite) logger.warn("PH_FORCE_PG_VERSION is set; ignoring --migrate-pglite/PH_MIGRATE_PGLITE because the data dirs will be wiped.");
2540
- logger.warn(`PH_FORCE_PG_VERSION=${options.forcePgVersion} set; wiping PGLite data dirs and re-initializing at PG${options.forcePgVersion}.`);
2541
- for (const dir of pgliteDirs) {
2542
- await promises.rm(dir, {
2543
- recursive: true,
2544
- force: true
2545
- });
2546
- logger.info(`Wiped PGLite data dir ${dir}`);
2547
- }
2548
- } else if (options.forcePgVersion === void 0) {
2549
- for (const dir of pgliteDirs) {
2550
- const major = await readPgVersionFile(dir);
2551
- if (major !== null) detectedMajors.set(dir, major);
2552
- }
2553
- if (options.migratePglite) for (const [dir, major] of detectedMajors) {
2554
- if (major === 17) continue;
2555
- await migratePgliteDir(dir, logger);
2556
- const after = await readPgVersionFile(dir);
2557
- if (after !== null) detectedMajors.set(dir, after);
2558
- }
2559
- else for (const [dir, major] of detectedMajors) {
2560
- if (major === 17) continue;
2561
- logger.warn(`PGLite data dir at ${dir} was created with PG${major} but Switchboard ships PG17. Running on legacy PGLite. Re-start with --migrate-pglite (or PH_MIGRATE_PGLITE=true) to upgrade.`);
2562
- }
2563
- }
2736
+ warnIgnoredFlushInterval(logger);
2737
+ const detectedMajors = await runPglitePreflight({
2738
+ dirs: pgliteDirs,
2739
+ forcePgVersion: options.forcePgVersion,
2740
+ migratePglite: options.migratePglite,
2741
+ inMemory: PGLITE_IN_MEMORY,
2742
+ logger
2743
+ });
2564
2744
  function resolvePgliteMajorForDir(dir) {
2565
2745
  if (options.forcePgVersion !== void 0) return options.forcePgVersion;
2566
2746
  const detected = detectedMajors.get(dir);
@@ -2627,6 +2807,7 @@ async function initServer(serverPort, options, renown, renownConfig) {
2627
2807
  if (!packages.includes("@powerhousedao/workflow")) packages.push(WORKFLOW_PACKAGE_NAME);
2628
2808
  }
2629
2809
  let ownedReactorModule;
2810
+ let workerModels;
2630
2811
  let localRenownRequest;
2631
2812
  const initializeClient = async (documentModels, { attachmentReferenceWriter, upgradeManifests }) => {
2632
2813
  if (options.reactor) {
@@ -2645,7 +2826,6 @@ async function initServer(serverPort, options, renown, renownConfig) {
2645
2826
  reactorPgliteDir,
2646
2827
  reactorPgliteMajor,
2647
2828
  inMemory: PGLITE_IN_MEMORY,
2648
- flushIntervalMs: PGLITE_FLUSH_INTERVAL_MS,
2649
2829
  hostPoolSize: () => resolveHostPoolSize(process.env),
2650
2830
  logger
2651
2831
  });
@@ -2681,10 +2861,14 @@ async function initServer(serverPort, options, renown, renownConfig) {
2681
2861
  switchboardUrl: renownConfig.switchboardUrl
2682
2862
  }, renown) : void 0
2683
2863
  });
2864
+ if (workerPool || projectionWorker || workflowsEnabled) {
2865
+ const sources = await resolveWorkerModelSources(packages, reactorLogger, { resolveRemote: resolveRegistryModels });
2866
+ reactorBuilder.withDocumentModelSources(workerPool || projectionWorker ? sources : await keepImportableSources(sources, reactorLogger));
2867
+ }
2868
+ workerModels = modelManifestSource(reactorBuilder);
2684
2869
  if (workerPool) {
2685
2870
  if (!reactorDbUrl) throw new Error("unreachable: worker pool enabled without a reactor database URL");
2686
- const workerSources = await resolveWorkerModelSources(packages, reactorLogger, { resolveRemote: resolveRegistryModels });
2687
- reactorBuilder.withDocumentModelSources(workerSources).withWorkerPool({
2871
+ reactorBuilder.withWorkerPool({
2688
2872
  numWorkers: workerPool.numWorkers,
2689
2873
  db: buildWorkerDbConfig(reactorDbUrl, workerPool)
2690
2874
  });
@@ -2701,10 +2885,6 @@ async function initServer(serverPort, options, renown, renownConfig) {
2701
2885
  });
2702
2886
  if (projectionWorker) {
2703
2887
  if (!reactorDbUrl) throw new Error("unreachable: projection worker enabled without a reactor database URL");
2704
- if (!workerPool) {
2705
- const workerSources = await resolveWorkerModelSources(packages, reactorLogger, { resolveRemote: resolveRegistryModels });
2706
- reactorBuilder.withDocumentModelSources(workerSources);
2707
- }
2708
2888
  const db = {
2709
2889
  ...buildWorkerDbConfig(reactorDbUrl, {
2710
2890
  dbPoolSizePerWorker: projectionWorker.dbPoolSize,
@@ -2798,12 +2978,9 @@ async function initServer(serverPort, options, renown, renownConfig) {
2798
2978
  let pgliteFactory;
2799
2979
  if (readModelPgliteDir && readModelPgliteMajor !== null) {
2800
2980
  const { PGlite: ReadModelPGlite } = await loadPGliteModule(readModelPgliteMajor);
2981
+ const durableFs = await loadDurableFsFactory(readModelPgliteMajor, logger);
2801
2982
  pgliteFactory = PGLITE_IN_MEMORY ? () => new ReadModelPGlite({ parsers: PGLITE_UTC_PARSERS }) : (connectionString) => new ReadModelPGlite({
2802
- fs: new AtomicNodeFs(connectionString ?? readModelPgliteDir, {
2803
- logger,
2804
- flushIntervalMs: PGLITE_FLUSH_INTERVAL_MS,
2805
- onFlushError: onPgliteFlushError
2806
- }),
2983
+ fs: durableFs(connectionString ?? readModelPgliteDir),
2807
2984
  parsers: PGLITE_UTC_PARSERS
2808
2985
  });
2809
2986
  }
@@ -2852,6 +3029,7 @@ async function initServer(serverPort, options, renown, renownConfig) {
2852
3029
  authorizationService: api.authorizationService,
2853
3030
  pieces: api.packageManager,
2854
3031
  pieceRegistryUrl: registryUrl,
3032
+ models: workerModels,
2855
3033
  logger: logger.child(["workflow-runtime"])
2856
3034
  });
2857
3035
  const WorkflowRuntimeSubgraph = workflows.subgraph;
@@ -2989,6 +3167,7 @@ async function initServer(serverPort, options, renown, renownConfig) {
2989
3167
  attachmentReferenceProjection: api.attachmentReferenceProjection,
2990
3168
  workflowTriggers: workflows?.triggers,
2991
3169
  workflowsEnabled,
3170
+ modelManifest: () => workerModels?.modelManifest() ?? [],
2992
3171
  privacy: privacy ? { erasure: privacy.erasure } : void 0,
2993
3172
  mcpEnabled: options.mcp !== false,
2994
3173
  renown,
@@ -3061,5 +3240,5 @@ if (import.meta.main) await startSwitchboard({ fatalErrorShutdown: true });
3061
3240
  //#endregion
3062
3241
  export { resolveUnsupportedStoredDocuments as a, StoredDocumentsRefusedError as i, isPortAvailable as n, applySwitchboardReactorDefaults as o, startSwitchboard as r, parseForcePgVersion as s, deriveAttachmentServiceConfig as t };
3063
3242
 
3064
- //# sourceMappingURL=server-Bk_IZzCn.mjs.map
3065
- //# debugId=d5e0ce19-ffcf-5425-add6-a7c67ab79534
3243
+ //# sourceMappingURL=server-Dvw8_NeM.mjs.map
3244
+ //# debugId=b9c02424-1406-5d63-abbd-6c32caa53205