@powerhousedao/switchboard 6.2.3-dev.23 → 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 (40) 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/renown-trust-policy.test.d.ts +2 -0
  14. package/.tsbuild/test/renown-trust-policy.test.d.ts.map +1 -0
  15. package/.tsbuild/tsconfig.tsbuildinfo +1 -1
  16. package/Auth.md +41 -0
  17. package/CHANGELOG.md +20 -0
  18. package/README.md +14 -0
  19. package/dist/index.mjs +2 -2
  20. package/dist/{server-DVotaGIp.mjs → server-BFbus-at.mjs} +177 -29
  21. package/dist/server-BFbus-at.mjs.map +1 -0
  22. package/dist/server.d.mts +11 -5
  23. package/dist/server.d.mts.map +1 -1
  24. package/dist/server.mjs +4 -4
  25. package/dist/{utils-Baw7rThP.mjs → utils-BBHt2kdo.mjs} +15 -9
  26. package/dist/utils-BBHt2kdo.mjs.map +1 -0
  27. package/dist/utils.d.mts.map +1 -1
  28. package/dist/utils.mjs +3 -3
  29. package/package.json +16 -16
  30. package/test/attachments/auth.test.ts +85 -0
  31. package/test/attachments/index.test.ts +34 -3
  32. package/test/boot-create-signature-policy.test.ts +108 -0
  33. package/test/boot-without-renown.test.ts +81 -0
  34. package/test/boot-without-require-signatures.test.ts +74 -0
  35. package/test/boot-worker-trust-policy.test.ts +90 -0
  36. package/test/builder-defaults.test.ts +80 -0
  37. package/test/create-signature-policy.test.ts +69 -0
  38. package/test/renown-trust-policy.test.ts +315 -0
  39. package/dist/server-DVotaGIp.mjs.map +0 -1
  40. package/dist/utils-Baw7rThP.mjs.map +0 -1
package/Auth.md CHANGED
@@ -221,6 +221,47 @@ It requires a caller to be resolvable at all, so it refuses to boot without
221
221
  resolution off, no bearer is ever read and it would reject every caller,
222
222
  including authenticated ones.
223
223
 
224
+ It also covers the attachment routes, which are mounted on the HTTP adapter and
225
+ never pass the GraphQL fetch chain. Their own 401 keys on `AUTH_ENABLED`, so
226
+ without this switch a deployment running `OPEN` serves them to anyone; with it,
227
+ every attachment route refuses a caller it cannot name — `download-target`
228
+ included, even though that route decides per document on its own.
229
+
230
+ ##### Serving one path anonymously
231
+
232
+ Some products have a flow that runs *before* sign-in: previewing an invitation
233
+ from the code in its e-mail, for instance, so the screen can say who the
234
+ invitation is for. That flow has no caller by definition, and the floor above
235
+ would answer it a `401`.
236
+
237
+ ```bash
238
+ export REQUIRE_AUTHENTICATED_CALLER=true
239
+ export REQUIRE_AUTHENTICATED_CALLER_EXEMPT_PATHS=/graphql/public
240
+ ```
241
+
242
+ Comma-separated, so several paths are one variable:
243
+
244
+ ```bash
245
+ export REQUIRE_AUTHENTICATED_CALLER_EXEMPT_PATHS=/graphql/public,/graphql/invites
246
+ ```
247
+
248
+ Each entry is matched against the request's **pathname, in full** — never as a
249
+ prefix, so `/graphql/public` does not also exempt `/graphql/public-admin`. A
250
+ trailing slash on either side is ignored; a leading slash is required, and a
251
+ path without one is refused at boot rather than left as an exemption that
252
+ silently never applies. Configuring exemptions while the floor is off is
253
+ refused too: it would describe a protection the server is not applying.
254
+
255
+ The exempt paths are named in the boot log, because the one question worth
256
+ asking about this floor is what is still open.
257
+
258
+ Every entry is a hole in it. Only ever name a path that serves operations which
259
+ are safe without a caller, and prefer a route mounted for exactly that purpose
260
+ over exempting one that also serves something else. Note what this switch does
261
+ **not** reach, in either configuration: routes registered directly on the HTTP
262
+ adapter (`/health`, `/ready`, `/explorer`, `/d/:drive`) and package HTTP routes,
263
+ which declare their own `auth` per route.
264
+
224
265
  #### Configuration File Method
225
266
 
226
267
  ```json
package/CHANGELOG.md CHANGED
@@ -1,3 +1,23 @@
1
+ ## 6.2.3-dev.24 (2026-09-25)
2
+
3
+ ### 🚀 Features
4
+
5
+ - **reactor-api:** exempt named paths from the authenticated-caller floor, and carry it to the attachment routes ([#3108](https://github.com/powerhouse-inc/powerhouse/pull/3108))
6
+ - **reactor:** action signature integrity ([#3088](https://github.com/powerhouse-inc/powerhouse/pull/3088), [#2894](https://github.com/powerhouse-inc/powerhouse/issues/2894), [#7](https://github.com/powerhouse-inc/powerhouse/issues/7))
7
+ - **reactor-workflow:** refuse the auth and trigger features the engine can't run ([7ce03d28f](https://github.com/powerhouse-inc/powerhouse/commit/7ce03d28f))
8
+ - type-safe piece authoring, upstream connection hooks, and a ph build that typechecks first ([#3082](https://github.com/powerhouse-inc/powerhouse/pull/3082))
9
+
10
+ ### 🩹 Fixes
11
+
12
+ - **workflow:** build after powerhouse-vetra-packages so their tsc --build runs never overlap ([#3101](https://github.com/powerhouse-inc/powerhouse/pull/3101))
13
+
14
+ ### ❤️ Thank You
15
+
16
+ - acaldas
17
+ - Benjamin Jordan
18
+ - Claude Fable 5.1
19
+ - Guillermo Puente Sandoval @gpuente
20
+
1
21
  ## 6.2.3-dev.23 (2026-09-24)
2
22
 
3
23
  ### 🩹 Fixes
package/README.md CHANGED
@@ -186,6 +186,20 @@ exits non-zero instead of waiting out the init timeout.
186
186
 
187
187
  Programmatically: `startSwitchboard({ projectionWorker: { enabled, dbPoolSize } })`.
188
188
 
189
+ ### Creation Signature Policy
190
+
191
+ | Variable | Description | Default |
192
+ | ------------------------- | ------------------------------------------------------------ | ------------- |
193
+ | `CREATE_SIGNATURE_POLICY` | What new documents are created as: `legacy` or `v2-required` | `v2-required` |
194
+
195
+ A v2-required document accepts only v2 action signatures and takes a
196
+ content-addressed id. Set `legacy` while any reactor that syncs with this one
197
+ predates v2-required documents. It only decides what new documents are born
198
+ as; existing documents keep their policy, and it does not change what the
199
+ switchboard accepts. Without a Renown identity the switchboard has no signer,
200
+ so it creates legacy documents and logs a warning. A default drive configured
201
+ with a fixed `id` is always created legacy.
202
+
189
203
  ### Reactor Enforcement Flags
190
204
 
191
205
  | Variable | Description | Default |
package/dist/index.mjs CHANGED
@@ -1,8 +1,8 @@
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]="2ce93cee-94b7-54c2-9688-4127efd686af")}catch(e){}}();
4
- import { a as parseForcePgVersion, r as startSwitchboard } from "./server-DVotaGIp.mjs";
5
- import "./utils-Baw7rThP.mjs";
4
+ import { a as parseForcePgVersion, r as startSwitchboard } from "./server-BFbus-at.mjs";
5
+ import "./utils-BBHt2kdo.mjs";
6
6
  import { metrics } from "@opentelemetry/api";
7
7
  import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
8
8
  import { ExpressInstrumentation } from "@opentelemetry/instrumentation-express";
@@ -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]="f230ad22-7960-58f9-8fa7-97e55fa64a8d")}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
@@ -663,7 +682,13 @@ function applySwitchboardReactorDefaults(reactorBuilder, clientBuilder, options
663
682
  });
664
683
  if (options.documentModelLoader) reactorBuilder.withDocumentModelLoader(options.documentModelLoader);
665
684
  if (options.logger) reactorBuilder.withLogger(options.logger);
666
- 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);
667
692
  }
668
693
  //#endregion
669
694
  //#region src/worker-pool.mts
@@ -1100,8 +1125,10 @@ const schema = gql`
1100
1125
  description: String!
1101
1126
  "action | trigger"
1102
1127
  kind: String!
1103
- "Triggers only: POLLING | WEBHOOK | APP_WEBHOOK"
1128
+ "Triggers only: POLLING | WEBHOOK | APP_WEBHOOK | MANUAL"
1104
1129
  strategy: String
1130
+ "Why this block cannot run on this reactor; null when it can."
1131
+ unsupported: String
1105
1132
  }
1106
1133
 
1107
1134
  type BlockSearchResult {
@@ -1133,6 +1160,10 @@ const schema = gql`
1133
1160
  type ConnectionCheckResult {
1134
1161
  ok: Boolean!
1135
1162
  detail: String
1163
+ """
1164
+ From auth.getConnectionIdentifier when the check passes; otherwise the
1165
+ label the connection already holds.
1166
+ """
1136
1167
  accountLabel: String
1137
1168
  }
1138
1169
 
@@ -1239,8 +1270,9 @@ const schema = gql`
1239
1270
  """
1240
1271
  deleteSecret(ref: String!): Boolean!
1241
1272
  """
1242
- Runs the piece's app.checkConnection against the connection's
1243
- 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.
1244
1276
  """
1245
1277
  checkConnection(connectionId: String!): ConnectionCheckResult!
1246
1278
  }
@@ -1538,6 +1570,26 @@ function resolveReactorFeatureFlags(env) {
1538
1570
  };
1539
1571
  }
1540
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
1541
1593
  //#region src/renown.ts
1542
1594
  const logger = childLogger(["switchboard", "renown"]);
1543
1595
  /**
@@ -1560,12 +1612,94 @@ async function initRenown(options = {}) {
1560
1612
  * Get the signer config for the given renown instance.
1561
1613
  *
1562
1614
  * @param renown - The renown instance
1563
- * @param requireSignature - If true, unsigned actions are rejected
1615
+ * @param keypairPath - Where `initRenown` loaded the key from
1564
1616
  */
1565
- function getRenownSignerConfig(renown, requireSignature) {
1617
+ function getRenownSignerConfig(renown, keypairPath) {
1566
1618
  return {
1567
1619
  signer: renown.signer,
1568
- 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
+ }
1569
1703
  };
1570
1704
  }
1571
1705
  //#endregion
@@ -1576,8 +1710,6 @@ setLogLevel(LogLevel);
1576
1710
  dotenv.config();
1577
1711
  const DOCUMENT_MODEL_SUBGRAPHS_ENABLED = "DOCUMENT_MODEL_SUBGRAPHS_ENABLED";
1578
1712
  const DOCUMENT_MODEL_SUBGRAPHS_ENABLED_DEFAULT = true;
1579
- const REQUIRE_SIGNATURES = "REQUIRE_SIGNATURES";
1580
- const REQUIRE_SIGNATURES_DEFAULT = false;
1581
1713
  const DEFAULT_PORT = process.env.PORT ? Number(process.env.PORT) : 4001;
1582
1714
  const PORT_FALLBACK_ATTEMPTS = 20;
1583
1715
  const PGLITE_FLUSH_INTERVAL_MS = (() => {
@@ -1731,6 +1863,11 @@ async function initServer(serverPort, options, renown, renownConfig) {
1731
1863
  }
1732
1864
  if (workerPool) {
1733
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
+ });
1734
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.");
1735
1872
  }
1736
1873
  let projectionWorker = resolveProjectionWorkerOptions(options.projectionWorker, process.env);
@@ -1772,6 +1909,7 @@ async function initServer(serverPort, options, renown, renownConfig) {
1772
1909
  if (!packages.includes("@powerhousedao/workflow")) packages.push(WORKFLOW_PACKAGE_NAME);
1773
1910
  }
1774
1911
  let ownedReactorModule;
1912
+ let localRenownRequest;
1775
1913
  const initializeClient = async (documentModels, { attachmentReferenceWriter, upgradeManifests }) => {
1776
1914
  if (options.reactor) {
1777
1915
  const attachmentReferenceProjection = await registerAttachmentReferenceReadModelOnModule(options.reactor, attachmentReferenceWriter);
@@ -1811,7 +1949,19 @@ async function initServer(serverPort, options, renown, renownConfig) {
1811
1949
  } : void 0,
1812
1950
  documentModelLoader: httpLoader && dynamicModelLoading ? httpLoader.documentModelLoader : void 0,
1813
1951
  logger: reactorLogger,
1814
- 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
1815
1965
  });
1816
1966
  if (workerPool) {
1817
1967
  if (!reactorDbUrl) throw new Error("unreachable: worker pool enabled without a reactor database URL");
@@ -1949,7 +2099,8 @@ async function initServer(serverPort, options, renown, renownConfig) {
1949
2099
  await abortBoot(api);
1950
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.`);
1951
2101
  }
1952
- 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) });
1953
2104
  logger.info("Renown credentials will be verified against this switchboard's own renown read model");
1954
2105
  }
1955
2106
  registerAttachmentRoutes(api);
@@ -2107,7 +2258,6 @@ const startSwitchboard = async (options = {}) => {
2107
2258
  const featureFlags = await initFeatureFlags();
2108
2259
  const enableDocumentModelSubgraphs = await featureFlags.getBooleanValue(DOCUMENT_MODEL_SUBGRAPHS_ENABLED, options.enableDocumentModelSubgraphs ?? DOCUMENT_MODEL_SUBGRAPHS_ENABLED_DEFAULT);
2109
2260
  options.enableDocumentModelSubgraphs = enableDocumentModelSubgraphs;
2110
- const requireSignatures = options.identity?.requireSignatures ?? await featureFlags.getBooleanValue(REQUIRE_SIGNATURES, REQUIRE_SIGNATURES_DEFAULT);
2111
2261
  const configPathForFlags = resolveConfigPath(options.configFile);
2112
2262
  const workflowsEnabled = await resolveWorkflowsEnabled({
2113
2263
  featureFlags,
@@ -2118,12 +2268,10 @@ const startSwitchboard = async (options = {}) => {
2118
2268
  const renownConfig = resolveRenownConfig(getConfig(resolveConfigPath(options.configFile)).auth?.renown, process.env, logger);
2119
2269
  options.identity = {
2120
2270
  ...options.identity,
2121
- requireSignatures,
2122
2271
  baseUrl: options.identity?.baseUrl ?? renownConfig.url
2123
2272
  };
2124
2273
  logger.info("Feature flags: @flags", JSON.stringify({
2125
2274
  DOCUMENT_MODEL_SUBGRAPHS_ENABLED: enableDocumentModelSubgraphs,
2126
- REQUIRE_SIGNATURES: requireSignatures,
2127
2275
  PH_WORKFLOWS_ENABLED: workflowsEnabled
2128
2276
  }, null, 2));
2129
2277
  let renown = null;
@@ -2145,5 +2293,5 @@ if (import.meta.main) await startSwitchboard({ fatalErrorShutdown: true });
2145
2293
  //#endregion
2146
2294
  export { parseForcePgVersion as a, applySwitchboardReactorDefaults as i, isPortAvailable as n, startSwitchboard as r, deriveAttachmentServiceConfig as t };
2147
2295
 
2148
- //# sourceMappingURL=server-DVotaGIp.mjs.map
2149
- //# debugId=f230ad22-7960-58f9-8fa7-97e55fa64a8d
2296
+ //# sourceMappingURL=server-BFbus-at.mjs.map
2297
+ //# debugId=dd6695ea-7590-5100-84c4-edefa353d65e