@powerhousedao/switchboard 6.2.3-dev.22 → 6.2.3-dev.24

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 (46) hide show
  1. package/.tsbuild/test/boot-create-signature-policy.test.d.ts +2 -0
  2. package/.tsbuild/test/boot-create-signature-policy.test.d.ts.map +1 -0
  3. package/.tsbuild/test/boot-without-renown.test.d.ts +2 -0
  4. package/.tsbuild/test/boot-without-renown.test.d.ts.map +1 -0
  5. package/.tsbuild/test/boot-without-require-signatures.test.d.ts +2 -0
  6. package/.tsbuild/test/boot-without-require-signatures.test.d.ts.map +1 -0
  7. package/.tsbuild/test/boot-worker-trust-policy.test.d.ts +2 -0
  8. package/.tsbuild/test/boot-worker-trust-policy.test.d.ts.map +1 -0
  9. package/.tsbuild/test/builder-defaults.test.d.ts +2 -0
  10. package/.tsbuild/test/builder-defaults.test.d.ts.map +1 -0
  11. package/.tsbuild/test/create-signature-policy.test.d.ts +2 -0
  12. package/.tsbuild/test/create-signature-policy.test.d.ts.map +1 -0
  13. package/.tsbuild/test/fatal-shutdown.test.d.ts +2 -0
  14. package/.tsbuild/test/fatal-shutdown.test.d.ts.map +1 -0
  15. package/.tsbuild/test/renown-trust-policy.test.d.ts +2 -0
  16. package/.tsbuild/test/renown-trust-policy.test.d.ts.map +1 -0
  17. package/.tsbuild/tsconfig.tsbuildinfo +1 -1
  18. package/Auth.md +41 -0
  19. package/CHANGELOG.md +33 -0
  20. package/README.md +14 -0
  21. package/dist/index.mjs +6 -5
  22. package/dist/index.mjs.map +1 -1
  23. package/dist/{server-C6VtW18k.mjs → server-BFbus-at.mjs} +225 -33
  24. package/dist/server-BFbus-at.mjs.map +1 -0
  25. package/dist/server.d.mts +19 -5
  26. package/dist/server.d.mts.map +1 -1
  27. package/dist/server.mjs +4 -4
  28. package/dist/{utils-Baw7rThP.mjs → utils-BBHt2kdo.mjs} +15 -9
  29. package/dist/utils-BBHt2kdo.mjs.map +1 -0
  30. package/dist/utils.d.mts.map +1 -1
  31. package/dist/utils.mjs +3 -3
  32. package/package.json +16 -16
  33. package/test/attachments/auth.test.ts +85 -0
  34. package/test/attachments/index.test.ts +34 -3
  35. package/test/attachments/routes-integration.test.ts +40 -0
  36. package/test/attachments/routes.test.ts +126 -2
  37. package/test/boot-create-signature-policy.test.ts +108 -0
  38. package/test/boot-without-renown.test.ts +81 -0
  39. package/test/boot-without-require-signatures.test.ts +74 -0
  40. package/test/boot-worker-trust-policy.test.ts +90 -0
  41. package/test/builder-defaults.test.ts +80 -0
  42. package/test/create-signature-policy.test.ts +69 -0
  43. package/test/fatal-shutdown.test.ts +102 -0
  44. package/test/renown-trust-policy.test.ts +315 -0
  45. package/dist/server-C6VtW18k.mjs.map +0 -1
  46. package/dist/utils-Baw7rThP.mjs.map +0 -1
@@ -1,14 +1,14 @@
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]="75db5742-026a-585f-b76c-c4e4981bb421")}catch(e){}}();
3
- import { n as addDefaultReactorDrive, r as isPostgresUrl, t as addDefaultDrive } from "./utils-Baw7rThP.mjs";
4
- import { register } from "node:module";
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]="dd6695ea-7590-5100-84c4-edefa353d65e")}catch(e){}}();
3
+ import { n as addDefaultReactorDrive, r as isPostgresUrl, t as addDefaultDrive } from "./utils-BBHt2kdo.mjs";
4
+ import { createRequire, register } from "node:module";
5
5
  import { ReactorInstrumentation } from "@powerhousedao/opentelemetry-instrumentation-reactor";
6
6
  import * as Sentry from "@sentry/node";
7
7
  import { childLogger, documentModelDocumentModelModule, setLogLevel } from "document-model";
8
8
  import dotenv from "dotenv";
9
9
  import { getConfig } from "@powerhousedao/config/node";
10
10
  import { existsSync, promises, realpathSync } from "node:fs";
11
- import path from "node:path";
11
+ import path, { resolve } from "node:path";
12
12
  import { AtomicNodeFs } from "@powerhousedao/pglite-fs";
13
13
  import { ChannelScheme, DriveCollectionId, EventBus, REACTOR_SCHEMA, ReactorBuilder, ReactorClientBuilder, createHybridProjectionCoordinatorFactory, instrumentPgPool, parseDriveUrl, supportsLiveReadModelRegistration } from "@powerhousedao/reactor";
14
14
  import { AuthorizationPolicy, BaseSubgraph, ForbiddenError, HttpPackageLoader, ImportPackageLoader, PGLITE_UTC_PARSERS, PackageManagementService, PackagesSubgraph, createCanonicalDocumentIdResolver, getUniqueDocumentModels, initializeAndStartAPI, resolveRenownConfig } from "@powerhousedao/reactor-api";
@@ -16,7 +16,7 @@ import { httpsHooksPath } from "@powerhousedao/reactor-api/https-hooks";
16
16
  import { AttachmentAlreadyExists, AttachmentNotFound, AttachmentPending, AttachmentReferenceReadModel, AttachmentSchemaCompiler, HashMismatch, InvalidAttachmentRef, ReservationNotFound, SizeMismatch, UploadTooLarge, createRef, createRemoteAttachmentService, parseAttachmentDownloadTarget, parseRef } from "@powerhousedao/reactor-attachments";
17
17
  import { createAttachmentClient } from "@powerhousedao/reactor-attachments/client";
18
18
  import { DriveNodeView, NodeProcessor, ReactorDriveClient, createReactorDriveResolvers, reactorDriveDocumentModelModule, reactorDriveSubgraphTypeDefs } from "@powerhousedao/reactor-drive";
19
- import { DEFAULT_RENOWN_URL, NodeKeyStorage, RENOWN_READ_MODEL_SUBGRAPH, RenownBuilder, RenownCryptoBuilder, createLocalCredentialVerifier, createSignatureVerifier } from "@renown/sdk/node";
19
+ import { DEFAULT_KEYPAIR_PATH, DEFAULT_RENOWN_URL, NodeKeyStorage, RENOWN_READ_MODEL_SUBGRAPH, RenownBuilder, RenownCryptoBuilder, createLocalCredentialVerifier, createRenownTrustPolicy, resolveSwitchboardEndpoint } from "@renown/sdk/node";
20
20
  import { Kysely, PostgresDialect } from "kysely";
21
21
  import net from "node:net";
22
22
  import path$1 from "path";
@@ -24,6 +24,7 @@ import { Pool } from "pg";
24
24
  import { Readable } from "node:stream";
25
25
  import { ReactorGroupV1 } from "@powerhousedao/reactor-group";
26
26
  import { driveDocumentModelModule } from "@powerhousedao/shared/document-drive";
27
+ import { DEFAULT_SIGNATURE_POLICY, isSignaturePolicy } from "@powerhousedao/shared/document-model";
27
28
  import { readFile } from "node:fs/promises";
28
29
  import os from "node:os";
29
30
  import { fileURLToPath } from "node:url";
@@ -77,7 +78,15 @@ const ANONYMOUS_ACTOR = {
77
78
  * enabled, the request must carry a verifiable Bearer token. The handler always
78
79
  * receives an actor context: the verified bearer user when auth is enabled, or
79
80
  * the anonymous context when it is disabled. With `allowAnonymous`, a missing
80
- * bearer yields an anonymous actor with `authEnabled: true` instead of a 401.
81
+ * bearer yields an anonymous actor with `authEnabled: true` instead of a 401 —
82
+ * unless `requireAuthenticatedCaller` says the deployment refuses anonymous
83
+ * callers outright, which overrides it.
84
+ *
85
+ * With no `authService` there is no bearer to read and every caller is
86
+ * anonymous, so nothing is refused here. That combination cannot carry the
87
+ * floor: `assertRequireAuthenticatedCallerAllowed` refuses to boot with
88
+ * `REQUIRE_AUTHENTICATED_CALLER` set and no identity resolution, and identity
89
+ * resolution is what builds the service.
81
90
  */
82
91
  function requireAuth(authService, handler, options) {
83
92
  if (!authService) return (req, res, body) => handler(req, res, body, ANONYMOUS_ACTOR);
@@ -99,11 +108,13 @@ function requireAuth(authService, handler, options) {
99
108
  res.end(body);
100
109
  return;
101
110
  }
102
- if (result.auth_enabled && !result.user && !options?.allowAnonymous) {
103
- res.statusCode = 401;
104
- res.setHeader("Content-Type", "application/json");
105
- res.end(JSON.stringify({ error: "Authentication required" }));
106
- return;
111
+ if (!result.user) {
112
+ if (options?.requireAuthenticatedCaller === true || result.auth_enabled && !options?.allowAnonymous) {
113
+ res.statusCode = 401;
114
+ res.setHeader("Content-Type", "application/json");
115
+ res.end(JSON.stringify({ error: "Authentication required" }));
116
+ return;
117
+ }
107
118
  }
108
119
  await handler(req, res, body, {
109
120
  user: result.user,
@@ -121,10 +132,18 @@ function requireAuth(authService, handler, options) {
121
132
  * wrapping you must call `api.httpAdapter.mountNodeRoute` directly.
122
133
  *
123
134
  * `allowAnonymous` is reserved for routes whose handlers make a per-document
124
- * authorization decision themselves; identity-only routes must not use it.
135
+ * authorization decision themselves; identity-only routes must not use it. It
136
+ * is also not the last word: a deployment that refuses anonymous callers
137
+ * (`REQUIRE_AUTHENTICATED_CALLER`) refuses them here too, because these routes
138
+ * are mounted on the HTTP adapter and never reach the fetch chain that carries
139
+ * that floor for GraphQL. The flag is read from the API, so every route
140
+ * mounted through here inherits it without each one remembering to.
125
141
  */
126
142
  function mountAuthenticatedNodeRoute(api, method, path, handler, options) {
127
- api.httpAdapter.mountNodeRoute(method, path, requireAuth(api.authService, handler, options));
143
+ api.httpAdapter.mountNodeRoute(method, path, requireAuth(api.authService, handler, {
144
+ ...options,
145
+ requireAuthenticatedCaller: api.requireAuthenticatedCaller
146
+ }));
128
147
  }
129
148
  //#endregion
130
149
  //#region src/attachments/routes.ts
@@ -299,7 +318,7 @@ function makeDownloadHandler(attachments) {
299
318
  if (err instanceof AttachmentPending) {
300
319
  res.statusCode = 202;
301
320
  res.setHeader("Retry-After", String(RETRY_AFTER_SECONDS));
302
- res.setHeader("Attachment-Pending", JSON.stringify({
321
+ res.setHeader("Attachment-Pending", buildPendingHeader({
303
322
  expiresAtUtc: err.expiresAtUtc,
304
323
  ...err.metadata ?? {}
305
324
  }));
@@ -318,8 +337,14 @@ function makeDownloadHandler(attachments) {
318
337
  Readable.fromWeb(body).pipe(res);
319
338
  };
320
339
  }
340
+ function toAsciiJson(value) {
341
+ return JSON.stringify(value).replace(/[\u007f-\uffff]/g, (char) => `\\u${char.charCodeAt(0).toString(16).padStart(4, "0")}`);
342
+ }
343
+ function buildPendingHeader(pending) {
344
+ return toAsciiJson(pending);
345
+ }
321
346
  function buildMetadataHeader(header) {
322
- return JSON.stringify({
347
+ return toAsciiJson({
323
348
  mimeType: header.mimeType,
324
349
  fileName: header.fileName,
325
350
  sizeBytes: header.sizeBytes,
@@ -346,7 +371,7 @@ function makeStatHandler(attachments) {
346
371
  if (header.status === "pending") {
347
372
  res.statusCode = 202;
348
373
  res.setHeader("Retry-After", String(RETRY_AFTER_SECONDS));
349
- res.setHeader("Attachment-Pending", JSON.stringify({
374
+ res.setHeader("Attachment-Pending", buildPendingHeader({
350
375
  expiresAtUtc: header.expiresAtUtc,
351
376
  mimeType: header.mimeType,
352
377
  fileName: header.fileName,
@@ -587,6 +612,43 @@ async function registerAttachmentReferenceReadModelOnModule(clientModule, attach
587
612
  return { status: "available" };
588
613
  }
589
614
  //#endregion
615
+ //#region src/fatal-shutdown.mts
616
+ const FORCED_EXIT_MS = 15e3;
617
+ const installed = /* @__PURE__ */ new WeakSet();
618
+ /**
619
+ * Sends an uncaught exception or unhandled rejection through the SIGTERM
620
+ * shutdown that `withSignalHandlers()` installs, so the reactor and read-model
621
+ * PGlite stores write their snapshots before the process exits with code 1.
622
+ * Node's default is to exit at once, which drops every write AtomicNodeFs has
623
+ * not flushed yet. A shutdown that hangs is cut off after FORCED_EXIT_MS.
624
+ *
625
+ * A rejection is fatal only when this is its sole listener: Node exits on an
626
+ * unhandled rejection only when nothing listens for it, and Sentry's
627
+ * listener deliberately keeps the process running.
628
+ */
629
+ function installFatalErrorShutdown(logger, proc = process) {
630
+ if (installed.has(proc)) return;
631
+ installed.add(proc);
632
+ const realExit = proc.exit.bind(proc);
633
+ let shuttingDown = false;
634
+ const onFatal = (kind, err) => {
635
+ logger.error(`${kind}: @error`, err);
636
+ if (shuttingDown) return;
637
+ shuttingDown = true;
638
+ proc.exitCode = 1;
639
+ setTimeout(() => {
640
+ logger.error("Shutdown after fatal error timed out; exiting");
641
+ realExit(1);
642
+ }, FORCED_EXIT_MS).unref();
643
+ proc.kill(proc.pid, "SIGTERM");
644
+ };
645
+ proc.on("uncaughtException", (err) => onFatal("Uncaught exception", err));
646
+ proc.on("unhandledRejection", (reason) => {
647
+ if (proc.listenerCount("unhandledRejection") > 1) return;
648
+ onFatal("Unhandled rejection", reason);
649
+ });
650
+ }
651
+ //#endregion
590
652
  //#region src/builder-defaults.mts
591
653
  /**
592
654
  * Apply switchboard's standard configuration to a reactor + client builder
@@ -620,7 +682,13 @@ function applySwitchboardReactorDefaults(reactorBuilder, clientBuilder, options
620
682
  });
621
683
  if (options.documentModelLoader) reactorBuilder.withDocumentModelLoader(options.documentModelLoader);
622
684
  if (options.logger) reactorBuilder.withLogger(options.logger);
623
- if (options.signer) clientBuilder.withSigner(options.signer);
685
+ if (options.signer) {
686
+ clientBuilder.withSigner(options.signer);
687
+ reactorBuilder.withSigner(options.signer.signer, options.signer.workerSigner);
688
+ }
689
+ if (options.createSignaturePolicy) clientBuilder.withCreateSignaturePolicy(options.createSignaturePolicy);
690
+ const trust = options.trustPolicy ?? options.signer;
691
+ if (trust?.trustPolicy) reactorBuilder.withTrustPolicy(trust.trustPolicy, trust.workerTrustPolicy);
624
692
  }
625
693
  //#endregion
626
694
  //#region src/worker-pool.mts
@@ -1057,8 +1125,10 @@ const schema = gql`
1057
1125
  description: String!
1058
1126
  "action | trigger"
1059
1127
  kind: String!
1060
- "Triggers only: POLLING | WEBHOOK | APP_WEBHOOK"
1128
+ "Triggers only: POLLING | WEBHOOK | APP_WEBHOOK | MANUAL"
1061
1129
  strategy: String
1130
+ "Why this block cannot run on this reactor; null when it can."
1131
+ unsupported: String
1062
1132
  }
1063
1133
 
1064
1134
  type BlockSearchResult {
@@ -1090,6 +1160,10 @@ const schema = gql`
1090
1160
  type ConnectionCheckResult {
1091
1161
  ok: Boolean!
1092
1162
  detail: String
1163
+ """
1164
+ From auth.getConnectionIdentifier when the check passes; otherwise the
1165
+ label the connection already holds.
1166
+ """
1093
1167
  accountLabel: String
1094
1168
  }
1095
1169
 
@@ -1196,8 +1270,9 @@ const schema = gql`
1196
1270
  """
1197
1271
  deleteSecret(ref: String!): Boolean!
1198
1272
  """
1199
- Runs the piece's app.checkConnection against the connection's
1200
- credentials and records the outcome on the connection document.
1273
+ Runs the piece's auth.validate against the connection's credentials,
1274
+ then auth.getConnectionIdentifier for the account label, and records the
1275
+ outcome on the connection document.
1201
1276
  """
1202
1277
  checkConnection(connectionId: String!): ConnectionCheckResult!
1203
1278
  }
@@ -1495,6 +1570,26 @@ function resolveReactorFeatureFlags(env) {
1495
1570
  };
1496
1571
  }
1497
1572
  //#endregion
1573
+ //#region src/create-signature-policy.mts
1574
+ /**
1575
+ * What new documents are created as, from CREATE_SIGNATURE_POLICY. Without a
1576
+ * signer this switchboard cannot write to a v2-required document, so it falls
1577
+ * back to legacy and says so.
1578
+ */
1579
+ function resolveCreateSignaturePolicy(env, options) {
1580
+ const raw = env.CREATE_SIGNATURE_POLICY?.trim();
1581
+ let policy = DEFAULT_SIGNATURE_POLICY;
1582
+ if (raw) {
1583
+ if (!isSignaturePolicy(raw)) throw new Error(`CREATE_SIGNATURE_POLICY must be "legacy" or "v2-required", got "${raw}"`);
1584
+ policy = raw;
1585
+ }
1586
+ if (policy === "v2-required" && !options.hasSigner) {
1587
+ options.logger?.warn("No signer is configured, so new documents are created legacy: a v2-required document refuses the unsigned operations this switchboard would write. Set up an identity (ph login) to create v2-required documents.");
1588
+ return "legacy";
1589
+ }
1590
+ return policy;
1591
+ }
1592
+ //#endregion
1498
1593
  //#region src/renown.ts
1499
1594
  const logger = childLogger(["switchboard", "renown"]);
1500
1595
  /**
@@ -1517,12 +1612,94 @@ async function initRenown(options = {}) {
1517
1612
  * Get the signer config for the given renown instance.
1518
1613
  *
1519
1614
  * @param renown - The renown instance
1520
- * @param requireSignature - If true, unsigned actions are rejected
1615
+ * @param keypairPath - Where `initRenown` loaded the key from
1521
1616
  */
1522
- function getRenownSignerConfig(renown, requireSignature) {
1617
+ function getRenownSignerConfig(renown, keypairPath) {
1523
1618
  return {
1524
1619
  signer: renown.signer,
1525
- verifier: createSignatureVerifier(requireSignature)
1620
+ workerSigner: getRenownWorkerSignerSpec(renown, keypairPath)
1621
+ };
1622
+ }
1623
+ /**
1624
+ * What a pooled executor worker imports to sign as this switchboard: it
1625
+ * reloads the key `initRenown` stored, and takes the user known at boot.
1626
+ */
1627
+ function getRenownWorkerSignerSpec(renown, keypairPath) {
1628
+ const { signer } = renown;
1629
+ const user = signer.user;
1630
+ return {
1631
+ module: {
1632
+ filePath: createRequire(import.meta.url).resolve("@renown/sdk/node"),
1633
+ exportName: "createNodeRenownSigner"
1634
+ },
1635
+ initArgs: {
1636
+ appName: signer.app?.name ?? "switchboard",
1637
+ keypairPath: resolve(keypairPath ?? DEFAULT_KEYPAIR_PATH),
1638
+ ...signer.app?.key ? { did: signer.app.key } : {},
1639
+ ...user ? { user: {
1640
+ address: user.address,
1641
+ networkId: user.networkId,
1642
+ chainId: user.chainId
1643
+ } } : {}
1644
+ }
1645
+ };
1646
+ }
1647
+ /**
1648
+ * Admits a key as a signer for an address a Renown credential binds it to; a
1649
+ * `self` source has no worker spec. In process, the switchboard's own key is
1650
+ * matched against its current user; the worker spec carries the user known now.
1651
+ */
1652
+ async function getRenownTrustPolicyConfig(trustSource, renown) {
1653
+ const ownSigner = renown?.signer;
1654
+ if (trustSource.source === "self") return { trustPolicy: createRenownTrustPolicy({
1655
+ switchboard: trustSource.request,
1656
+ ownSigner
1657
+ }) };
1658
+ const switchboardUrl = await resolveSwitchboardEndpoint({
1659
+ switchboardUrl: trustSource.switchboardUrl,
1660
+ baseUrl: trustSource.renownUrl
1661
+ });
1662
+ const options = {
1663
+ ...switchboardUrl ? { switchboard: switchboardUrl } : {},
1664
+ ...trustSource.renownUrl ? { renownUrl: trustSource.renownUrl } : {}
1665
+ };
1666
+ const self = ownIdentity(renown);
1667
+ return {
1668
+ trustPolicy: createRenownTrustPolicy({
1669
+ ...options,
1670
+ ownSigner
1671
+ }),
1672
+ workerTrustPolicy: {
1673
+ module: {
1674
+ filePath: createRequire(import.meta.url).resolve("@renown/sdk/node"),
1675
+ exportName: "createRenownTrustPolicy"
1676
+ },
1677
+ initArgs: {
1678
+ ...options,
1679
+ ...self ? { self } : {}
1680
+ }
1681
+ }
1682
+ };
1683
+ }
1684
+ /**
1685
+ * Fails the boot when pooled workers would refuse every signed write the
1686
+ * in-process executor accepts: a `self` Renown source gives workers no trust
1687
+ * policy spec, so under authEnforcement they apply the default.
1688
+ */
1689
+ function assertWorkerTrustPolicy(config) {
1690
+ if (config.workers > 0 && config.authEnforcement && config.renownSource === "self") throw new Error("The executor worker pool (REACTOR_WORKERS) cannot verify signers when REACTOR_AUTH_ENFORCEMENT is on and Renown credentials are read from this switchboard (\"self\" via auth.renown.source or RENOWN_SOURCE): pooled workers have no trust policy and would refuse every signed write. Set RENOWN_SOURCE=remote, or disable the worker pool (REACTOR_WORKERS=0).");
1691
+ }
1692
+ function ownIdentity(renown) {
1693
+ const key = renown?.signer.app?.key;
1694
+ if (!key) return;
1695
+ const user = renown.signer.user;
1696
+ return {
1697
+ key,
1698
+ user: {
1699
+ address: user?.address ?? "",
1700
+ networkId: user?.networkId ?? "",
1701
+ chainId: user?.chainId ?? 0
1702
+ }
1526
1703
  };
1527
1704
  }
1528
1705
  //#endregion
@@ -1533,8 +1710,6 @@ setLogLevel(LogLevel);
1533
1710
  dotenv.config();
1534
1711
  const DOCUMENT_MODEL_SUBGRAPHS_ENABLED = "DOCUMENT_MODEL_SUBGRAPHS_ENABLED";
1535
1712
  const DOCUMENT_MODEL_SUBGRAPHS_ENABLED_DEFAULT = true;
1536
- const REQUIRE_SIGNATURES = "REQUIRE_SIGNATURES";
1537
- const REQUIRE_SIGNATURES_DEFAULT = false;
1538
1713
  const DEFAULT_PORT = process.env.PORT ? Number(process.env.PORT) : 4001;
1539
1714
  const PORT_FALLBACK_ATTEMPTS = 20;
1540
1715
  const PGLITE_FLUSH_INTERVAL_MS = (() => {
@@ -1688,6 +1863,11 @@ async function initServer(serverPort, options, renown, renownConfig) {
1688
1863
  }
1689
1864
  if (workerPool) {
1690
1865
  if (dev) throw new Error("The executor worker pool (REACTOR_WORKERS) is not supported in dev mode: Vite-loaded document models cannot cross a worker-thread boundary");
1866
+ assertWorkerTrustPolicy({
1867
+ workers: workerPool.numWorkers,
1868
+ authEnforcement: resolveReactorFeatureFlags(process.env).flags.authEnforcement === true,
1869
+ renownSource: renownConfig.source
1870
+ });
1691
1871
  if (!reactorDbUrl || !isPostgresUrl(reactorDbUrl)) throw new Error("The executor worker pool (REACTOR_WORKERS) requires a Postgres reactor database — set PH_REACTOR_DATABASE_URL or PH_SWITCHBOARD_DATABASE_URL. PGlite cannot be shared across worker threads.");
1692
1872
  }
1693
1873
  let projectionWorker = resolveProjectionWorkerOptions(options.projectionWorker, process.env);
@@ -1729,6 +1909,7 @@ async function initServer(serverPort, options, renown, renownConfig) {
1729
1909
  if (!packages.includes("@powerhousedao/workflow")) packages.push(WORKFLOW_PACKAGE_NAME);
1730
1910
  }
1731
1911
  let ownedReactorModule;
1912
+ let localRenownRequest;
1732
1913
  const initializeClient = async (documentModels, { attachmentReferenceWriter, upgradeManifests }) => {
1733
1914
  if (options.reactor) {
1734
1915
  const attachmentReferenceProjection = await registerAttachmentReferenceReadModelOnModule(options.reactor, attachmentReferenceWriter);
@@ -1768,7 +1949,19 @@ async function initServer(serverPort, options, renown, renownConfig) {
1768
1949
  } : void 0,
1769
1950
  documentModelLoader: httpLoader && dynamicModelLoading ? httpLoader.documentModelLoader : void 0,
1770
1951
  logger: reactorLogger,
1771
- signer: renown ? getRenownSignerConfig(renown, options.identity?.requireSignatures) : void 0
1952
+ signer: renown ? getRenownSignerConfig(renown, options.identity?.keypairPath) : void 0,
1953
+ createSignaturePolicy: resolveCreateSignaturePolicy(process.env, {
1954
+ hasSigner: renown !== null,
1955
+ logger
1956
+ }),
1957
+ trustPolicy: reactorFeatureFlags.authEnforcement ? await getRenownTrustPolicyConfig(renownConfig.source === "self" ? {
1958
+ source: "self",
1959
+ request: (query, variables) => localRenownRequest ? localRenownRequest(query, variables) : Promise.reject(/* @__PURE__ */ new Error("The local renown read model is not bound yet"))
1960
+ } : {
1961
+ source: "remote",
1962
+ renownUrl: renownConfig.url,
1963
+ switchboardUrl: renownConfig.switchboardUrl
1964
+ }, renown) : void 0
1772
1965
  });
1773
1966
  if (workerPool) {
1774
1967
  if (!reactorDbUrl) throw new Error("unreachable: worker pool enabled without a reactor database URL");
@@ -1823,6 +2016,7 @@ async function initServer(serverPort, options, renown, renownConfig) {
1823
2016
  readModel: driveNodeView
1824
2017
  });
1825
2018
  ownedReactorModule = module;
2019
+ if (options.fatalErrorShutdown) installFatalErrorShutdown(logger);
1826
2020
  return {
1827
2021
  module,
1828
2022
  reactorDriveClient,
@@ -1905,7 +2099,8 @@ async function initServer(serverPort, options, renown, renownConfig) {
1905
2099
  await abortBoot(api);
1906
2100
  throw new Error(`Renown credential verification is set to "self" (auth.renown.source or RENOWN_SOURCE) but no loaded package serves the "${RENOWN_READ_MODEL_SUBGRAPH}" subgraph. Install one (@powerhousedao/renown-package) or set RENOWN_SOURCE=remote.`);
1907
2101
  }
1908
- localCredentialCheck = createLocalCredentialVerifier((query, variables) => manager.executeSubgraphQuery(RENOWN_READ_MODEL_SUBGRAPH, query, variables), { onError: (error) => logger.error("Renown read model query failed: @error", error) });
2102
+ localRenownRequest = (query, variables) => manager.executeSubgraphQuery(RENOWN_READ_MODEL_SUBGRAPH, query, variables);
2103
+ localCredentialCheck = createLocalCredentialVerifier(localRenownRequest, { onError: (error) => logger.error("Renown read model query failed: @error", error) });
1909
2104
  logger.info("Renown credentials will be verified against this switchboard's own renown read model");
1910
2105
  }
1911
2106
  registerAttachmentRoutes(api);
@@ -2063,7 +2258,6 @@ const startSwitchboard = async (options = {}) => {
2063
2258
  const featureFlags = await initFeatureFlags();
2064
2259
  const enableDocumentModelSubgraphs = await featureFlags.getBooleanValue(DOCUMENT_MODEL_SUBGRAPHS_ENABLED, options.enableDocumentModelSubgraphs ?? DOCUMENT_MODEL_SUBGRAPHS_ENABLED_DEFAULT);
2065
2260
  options.enableDocumentModelSubgraphs = enableDocumentModelSubgraphs;
2066
- const requireSignatures = options.identity?.requireSignatures ?? await featureFlags.getBooleanValue(REQUIRE_SIGNATURES, REQUIRE_SIGNATURES_DEFAULT);
2067
2261
  const configPathForFlags = resolveConfigPath(options.configFile);
2068
2262
  const workflowsEnabled = await resolveWorkflowsEnabled({
2069
2263
  featureFlags,
@@ -2074,12 +2268,10 @@ const startSwitchboard = async (options = {}) => {
2074
2268
  const renownConfig = resolveRenownConfig(getConfig(resolveConfigPath(options.configFile)).auth?.renown, process.env, logger);
2075
2269
  options.identity = {
2076
2270
  ...options.identity,
2077
- requireSignatures,
2078
2271
  baseUrl: options.identity?.baseUrl ?? renownConfig.url
2079
2272
  };
2080
2273
  logger.info("Feature flags: @flags", JSON.stringify({
2081
2274
  DOCUMENT_MODEL_SUBGRAPHS_ENABLED: enableDocumentModelSubgraphs,
2082
- REQUIRE_SIGNATURES: requireSignatures,
2083
2275
  PH_WORKFLOWS_ENABLED: workflowsEnabled
2084
2276
  }, null, 2));
2085
2277
  let renown = null;
@@ -2097,9 +2289,9 @@ const startSwitchboard = async (options = {}) => {
2097
2289
  throw e;
2098
2290
  }
2099
2291
  };
2100
- if (import.meta.main) await startSwitchboard();
2292
+ if (import.meta.main) await startSwitchboard({ fatalErrorShutdown: true });
2101
2293
  //#endregion
2102
2294
  export { parseForcePgVersion as a, applySwitchboardReactorDefaults as i, isPortAvailable as n, startSwitchboard as r, deriveAttachmentServiceConfig as t };
2103
2295
 
2104
- //# sourceMappingURL=server-C6VtW18k.mjs.map
2105
- //# debugId=75db5742-026a-585f-b76c-c4e4981bb421
2296
+ //# sourceMappingURL=server-BFbus-at.mjs.map
2297
+ //# debugId=dd6695ea-7590-5100-84c4-edefa353d65e