@powerhousedao/switchboard 6.2.3-dev.23 → 6.2.3-dev.25

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 (53) 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/.tsbuild/tsdown.config.d.ts.map +1 -1
  17. package/.tsbuild/vitest.config.d.ts.map +1 -1
  18. package/Auth.md +92 -0
  19. package/CHANGELOG.md +38 -0
  20. package/README.md +14 -0
  21. package/dist/index.d.mts +2 -1
  22. package/dist/index.mjs +4 -5
  23. package/dist/index.mjs.map +1 -1
  24. package/dist/install-packages.d.mts +2 -1
  25. package/dist/install-packages.mjs +2 -2
  26. package/dist/install-packages.mjs.map +1 -1
  27. package/dist/migrate.d.mts +2 -1
  28. package/dist/migrate.mjs +4 -3
  29. package/dist/migrate.mjs.map +1 -1
  30. package/dist/{server-DVotaGIp.mjs → server-C3WaP3kE.mjs} +229 -40
  31. package/dist/server-C3WaP3kE.mjs.map +1 -0
  32. package/dist/server.d.mts +48 -28
  33. package/dist/server.d.mts.map +1 -1
  34. package/dist/server.mjs +3 -4
  35. package/dist/{utils-Baw7rThP.mjs → utils-uaBlzyjS.mjs} +15 -9
  36. package/dist/utils-uaBlzyjS.mjs.map +1 -0
  37. package/dist/utils.d.mts +4 -5
  38. package/dist/utils.d.mts.map +1 -1
  39. package/dist/utils.mjs +3 -3
  40. package/package.json +17 -17
  41. package/test/attachments/auth.test.ts +85 -0
  42. package/test/attachments/download-target.test.ts +71 -8
  43. package/test/attachments/index.test.ts +34 -3
  44. package/test/boot-create-signature-policy.test.ts +108 -0
  45. package/test/boot-without-renown.test.ts +81 -0
  46. package/test/boot-without-require-signatures.test.ts +74 -0
  47. package/test/boot-worker-trust-policy.test.ts +90 -0
  48. package/test/builder-defaults.test.ts +80 -0
  49. package/test/create-signature-policy.test.ts +69 -0
  50. package/test/renown-trust-policy.test.ts +315 -0
  51. package/tsdown.config.ts +3 -1
  52. package/dist/server-DVotaGIp.mjs.map +0 -1
  53. 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]="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]="8174bb20-5463-598a-9c2d-44a8760037a5")}catch(e){}}();
3
+ import { n as addDefaultReactorDrive, r as isPostgresUrl, t as addDefaultDrive } from "./utils-uaBlzyjS.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
@@ -404,7 +423,37 @@ function extractParam(req, name) {
404
423
  }
405
424
  const MAX_DOCUMENT_ID_LEN = 512;
406
425
  const ATTACHMENT_NOT_FOUND_BODY = { error: "Attachment not found" };
407
- const MAX_DOWNLOAD_TARGET_TTL_SECONDS = 10080 * 60;
426
+ const MAX_DOWNLOAD_TARGET_TTL_SECONDS = 604800;
427
+ /**
428
+ * How long a download target may live, by default.
429
+ *
430
+ * The authorization behind a presigned URL is decided once, when it is issued,
431
+ * and the URL keeps working until it expires however the document's policy
432
+ * changes in between. A week of that is a revocation that does not revoke: an
433
+ * account deactivated, a company reassigned or a group left, and the bytes stay
434
+ * fetchable by anyone holding the link. Minutes are enough to open a file;
435
+ * days are only enough to lose control of it.
436
+ *
437
+ * A deployment that needs longer sets ATTACHMENT_DOWNLOAD_TARGET_MAX_TTL_SECONDS,
438
+ * still bounded by the signing ceiling above.
439
+ */
440
+ const DEFAULT_DOWNLOAD_TARGET_TTL_SECONDS = 300;
441
+ /**
442
+ * The ceiling in force, read per call so a deployment can set it without the
443
+ * value being frozen at import — which is also what lets a test cover it.
444
+ * Anything unparseable falls back to the default rather than to the maximum:
445
+ * a typo must not quietly widen the window it was meant to narrow.
446
+ */
447
+ function downloadTargetTtlCapSeconds() {
448
+ const configured = process.env.ATTACHMENT_DOWNLOAD_TARGET_MAX_TTL_SECONDS;
449
+ if (configured === void 0) return DEFAULT_DOWNLOAD_TARGET_TTL_SECONDS;
450
+ const parsed = Number(configured);
451
+ if (!Number.isInteger(parsed) || parsed <= 0) {
452
+ logger$1.warn(`Ignoring invalid ATTACHMENT_DOWNLOAD_TARGET_MAX_TTL_SECONDS="${configured}" (expected a positive integer number of seconds) — using ${DEFAULT_DOWNLOAD_TARGET_TTL_SECONDS}`);
453
+ return DEFAULT_DOWNLOAD_TARGET_TTL_SECONDS;
454
+ }
455
+ return Math.min(parsed, MAX_DOWNLOAD_TARGET_TTL_SECONDS);
456
+ }
408
457
  /**
409
458
  * Returns the single `documentId` query value, or null when it is missing,
410
459
  * duplicated, blank, or oversized. Validation happens before authorization so
@@ -425,24 +474,29 @@ function extractSingleDocumentId(req) {
425
474
  return value;
426
475
  }
427
476
  /**
428
- * Returns the requested target TTL in seconds: undefined when absent,
429
- * "invalid" when malformed (duplicated, non-integer, or non-positive), and
430
- * otherwise the value clamped to the presigning ceiling.
477
+ * Returns the target TTL in seconds, or "invalid" when the caller's own value
478
+ * is malformed (duplicated, non-integer, or non-positive).
479
+ *
480
+ * The cap applies whether or not the caller asked for a lifetime: omitting
481
+ * `expiresIn` used to fall through to whatever the storage backend was
482
+ * configured with, which would make the ceiling something a client opts out of
483
+ * by saying nothing.
431
484
  */
432
485
  function extractExpiresIn(req) {
433
- if (!req.url) return void 0;
486
+ const cap = downloadTargetTtlCapSeconds();
487
+ if (!req.url) return cap;
434
488
  let url;
435
489
  try {
436
490
  url = new URL(req.url, "http://switchboard.invalid");
437
491
  } catch {
438
- return;
492
+ return cap;
439
493
  }
440
494
  const values = url.searchParams.getAll("expiresIn");
441
- if (values.length === 0) return void 0;
495
+ if (values.length === 0) return cap;
442
496
  if (values.length > 1) return "invalid";
443
497
  const parsed = Number(values[0]);
444
498
  if (!Number.isInteger(parsed) || parsed <= 0) return "invalid";
445
- return Math.min(parsed, MAX_DOWNLOAD_TARGET_TTL_SECONDS);
499
+ return Math.min(parsed, cap);
446
500
  }
447
501
  /**
448
502
  * Base URL of this Switchboard as seen by the caller, used to build
@@ -481,7 +535,8 @@ function makeDownloadTargetHandler(attachments, attachmentAccess) {
481
535
  decision = await attachmentAccess.canReadAttachment({
482
536
  documentId,
483
537
  attachmentRef: createRef(canonicalHash),
484
- userAddress: actor?.user?.address
538
+ userAddress: actor?.user?.address,
539
+ appKey: actor?.user?.appKey
485
540
  });
486
541
  } catch (err) {
487
542
  logger$1.error("Attachment access decision failed: @error", err);
@@ -663,7 +718,13 @@ function applySwitchboardReactorDefaults(reactorBuilder, clientBuilder, options
663
718
  });
664
719
  if (options.documentModelLoader) reactorBuilder.withDocumentModelLoader(options.documentModelLoader);
665
720
  if (options.logger) reactorBuilder.withLogger(options.logger);
666
- if (options.signer) clientBuilder.withSigner(options.signer);
721
+ if (options.signer) {
722
+ clientBuilder.withSigner(options.signer);
723
+ reactorBuilder.withSigner(options.signer.signer, options.signer.workerSigner);
724
+ }
725
+ if (options.createSignaturePolicy) clientBuilder.withCreateSignaturePolicy(options.createSignaturePolicy);
726
+ const trust = options.trustPolicy ?? options.signer;
727
+ if (trust?.trustPolicy) reactorBuilder.withTrustPolicy(trust.trustPolicy, trust.workerTrustPolicy);
667
728
  }
668
729
  //#endregion
669
730
  //#region src/worker-pool.mts
@@ -932,7 +993,9 @@ function toStepRecord(row) {
932
993
  input: parseJson(row.input),
933
994
  output: parseJson(row.output),
934
995
  port: row.port,
935
- error: row.error
996
+ error: row.error,
997
+ startedAt: row.started_at,
998
+ endedAt: row.ended_at
936
999
  };
937
1000
  }
938
1001
  function toRunRecord(row, steps) {
@@ -1100,8 +1163,10 @@ const schema = gql`
1100
1163
  description: String!
1101
1164
  "action | trigger"
1102
1165
  kind: String!
1103
- "Triggers only: POLLING | WEBHOOK | APP_WEBHOOK"
1166
+ "Triggers only: POLLING | WEBHOOK | APP_WEBHOOK | MANUAL"
1104
1167
  strategy: String
1168
+ "Why this block cannot run on this reactor; null when it can."
1169
+ unsupported: String
1105
1170
  }
1106
1171
 
1107
1172
  type BlockSearchResult {
@@ -1133,6 +1198,10 @@ const schema = gql`
1133
1198
  type ConnectionCheckResult {
1134
1199
  ok: Boolean!
1135
1200
  detail: String
1201
+ """
1202
+ From auth.getConnectionIdentifier when the check passes; otherwise the
1203
+ label the connection already holds.
1204
+ """
1136
1205
  accountLabel: String
1137
1206
  }
1138
1207
 
@@ -1173,6 +1242,8 @@ const schema = gql`
1173
1242
  output: Unknown
1174
1243
  port: String
1175
1244
  error: String
1245
+ startedAt: String
1246
+ endedAt: String
1176
1247
  }
1177
1248
 
1178
1249
  type WorkflowRunRecord {
@@ -1239,8 +1310,9 @@ const schema = gql`
1239
1310
  """
1240
1311
  deleteSecret(ref: String!): Boolean!
1241
1312
  """
1242
- Runs the piece's app.checkConnection against the connection's
1243
- credentials and records the outcome on the connection document.
1313
+ Runs the piece's auth.validate against the connection's credentials,
1314
+ then auth.getConnectionIdentifier for the account label, and records the
1315
+ outcome on the connection document.
1244
1316
  """
1245
1317
  checkConnection(connectionId: String!): ConnectionCheckResult!
1246
1318
  }
@@ -1538,6 +1610,26 @@ function resolveReactorFeatureFlags(env) {
1538
1610
  };
1539
1611
  }
1540
1612
  //#endregion
1613
+ //#region src/create-signature-policy.mts
1614
+ /**
1615
+ * What new documents are created as, from CREATE_SIGNATURE_POLICY. Without a
1616
+ * signer this switchboard cannot write to a v2-required document, so it falls
1617
+ * back to legacy and says so.
1618
+ */
1619
+ function resolveCreateSignaturePolicy(env, options) {
1620
+ const raw = env.CREATE_SIGNATURE_POLICY?.trim();
1621
+ let policy = DEFAULT_SIGNATURE_POLICY;
1622
+ if (raw) {
1623
+ if (!isSignaturePolicy(raw)) throw new Error(`CREATE_SIGNATURE_POLICY must be "legacy" or "v2-required", got "${raw}"`);
1624
+ policy = raw;
1625
+ }
1626
+ if (policy === "v2-required" && !options.hasSigner) {
1627
+ 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.");
1628
+ return "legacy";
1629
+ }
1630
+ return policy;
1631
+ }
1632
+ //#endregion
1541
1633
  //#region src/renown.ts
1542
1634
  const logger = childLogger(["switchboard", "renown"]);
1543
1635
  /**
@@ -1560,12 +1652,94 @@ async function initRenown(options = {}) {
1560
1652
  * Get the signer config for the given renown instance.
1561
1653
  *
1562
1654
  * @param renown - The renown instance
1563
- * @param requireSignature - If true, unsigned actions are rejected
1655
+ * @param keypairPath - Where `initRenown` loaded the key from
1564
1656
  */
1565
- function getRenownSignerConfig(renown, requireSignature) {
1657
+ function getRenownSignerConfig(renown, keypairPath) {
1566
1658
  return {
1567
1659
  signer: renown.signer,
1568
- verifier: createSignatureVerifier(requireSignature)
1660
+ workerSigner: getRenownWorkerSignerSpec(renown, keypairPath)
1661
+ };
1662
+ }
1663
+ /**
1664
+ * What a pooled executor worker imports to sign as this switchboard: it
1665
+ * reloads the key `initRenown` stored, and takes the user known at boot.
1666
+ */
1667
+ function getRenownWorkerSignerSpec(renown, keypairPath) {
1668
+ const { signer } = renown;
1669
+ const user = signer.user;
1670
+ return {
1671
+ module: {
1672
+ filePath: createRequire(import.meta.url).resolve("@renown/sdk/node"),
1673
+ exportName: "createNodeRenownSigner"
1674
+ },
1675
+ initArgs: {
1676
+ appName: signer.app?.name ?? "switchboard",
1677
+ keypairPath: resolve(keypairPath ?? DEFAULT_KEYPAIR_PATH),
1678
+ ...signer.app?.key ? { did: signer.app.key } : {},
1679
+ ...user ? { user: {
1680
+ address: user.address,
1681
+ networkId: user.networkId,
1682
+ chainId: user.chainId
1683
+ } } : {}
1684
+ }
1685
+ };
1686
+ }
1687
+ /**
1688
+ * Admits a key as a signer for an address a Renown credential binds it to; a
1689
+ * `self` source has no worker spec. In process, the switchboard's own key is
1690
+ * matched against its current user; the worker spec carries the user known now.
1691
+ */
1692
+ async function getRenownTrustPolicyConfig(trustSource, renown) {
1693
+ const ownSigner = renown?.signer;
1694
+ if (trustSource.source === "self") return { trustPolicy: createRenownTrustPolicy({
1695
+ switchboard: trustSource.request,
1696
+ ownSigner
1697
+ }) };
1698
+ const switchboardUrl = await resolveSwitchboardEndpoint({
1699
+ switchboardUrl: trustSource.switchboardUrl,
1700
+ baseUrl: trustSource.renownUrl
1701
+ });
1702
+ const options = {
1703
+ ...switchboardUrl ? { switchboard: switchboardUrl } : {},
1704
+ ...trustSource.renownUrl ? { renownUrl: trustSource.renownUrl } : {}
1705
+ };
1706
+ const self = ownIdentity(renown);
1707
+ return {
1708
+ trustPolicy: createRenownTrustPolicy({
1709
+ ...options,
1710
+ ownSigner
1711
+ }),
1712
+ workerTrustPolicy: {
1713
+ module: {
1714
+ filePath: createRequire(import.meta.url).resolve("@renown/sdk/node"),
1715
+ exportName: "createRenownTrustPolicy"
1716
+ },
1717
+ initArgs: {
1718
+ ...options,
1719
+ ...self ? { self } : {}
1720
+ }
1721
+ }
1722
+ };
1723
+ }
1724
+ /**
1725
+ * Fails the boot when pooled workers would refuse every signed write the
1726
+ * in-process executor accepts: a `self` Renown source gives workers no trust
1727
+ * policy spec, so under authEnforcement they apply the default.
1728
+ */
1729
+ function assertWorkerTrustPolicy(config) {
1730
+ 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).");
1731
+ }
1732
+ function ownIdentity(renown) {
1733
+ const key = renown?.signer.app?.key;
1734
+ if (!key) return;
1735
+ const user = renown.signer.user;
1736
+ return {
1737
+ key,
1738
+ user: {
1739
+ address: user?.address ?? "",
1740
+ networkId: user?.networkId ?? "",
1741
+ chainId: user?.chainId ?? 0
1742
+ }
1569
1743
  };
1570
1744
  }
1571
1745
  //#endregion
@@ -1576,8 +1750,6 @@ setLogLevel(LogLevel);
1576
1750
  dotenv.config();
1577
1751
  const DOCUMENT_MODEL_SUBGRAPHS_ENABLED = "DOCUMENT_MODEL_SUBGRAPHS_ENABLED";
1578
1752
  const DOCUMENT_MODEL_SUBGRAPHS_ENABLED_DEFAULT = true;
1579
- const REQUIRE_SIGNATURES = "REQUIRE_SIGNATURES";
1580
- const REQUIRE_SIGNATURES_DEFAULT = false;
1581
1753
  const DEFAULT_PORT = process.env.PORT ? Number(process.env.PORT) : 4001;
1582
1754
  const PORT_FALLBACK_ATTEMPTS = 20;
1583
1755
  const PGLITE_FLUSH_INTERVAL_MS = (() => {
@@ -1731,6 +1903,11 @@ async function initServer(serverPort, options, renown, renownConfig) {
1731
1903
  }
1732
1904
  if (workerPool) {
1733
1905
  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");
1906
+ assertWorkerTrustPolicy({
1907
+ workers: workerPool.numWorkers,
1908
+ authEnforcement: resolveReactorFeatureFlags(process.env).flags.authEnforcement === true,
1909
+ renownSource: renownConfig.source
1910
+ });
1734
1911
  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
1912
  }
1736
1913
  let projectionWorker = resolveProjectionWorkerOptions(options.projectionWorker, process.env);
@@ -1772,6 +1949,7 @@ async function initServer(serverPort, options, renown, renownConfig) {
1772
1949
  if (!packages.includes("@powerhousedao/workflow")) packages.push(WORKFLOW_PACKAGE_NAME);
1773
1950
  }
1774
1951
  let ownedReactorModule;
1952
+ let localRenownRequest;
1775
1953
  const initializeClient = async (documentModels, { attachmentReferenceWriter, upgradeManifests }) => {
1776
1954
  if (options.reactor) {
1777
1955
  const attachmentReferenceProjection = await registerAttachmentReferenceReadModelOnModule(options.reactor, attachmentReferenceWriter);
@@ -1811,7 +1989,19 @@ async function initServer(serverPort, options, renown, renownConfig) {
1811
1989
  } : void 0,
1812
1990
  documentModelLoader: httpLoader && dynamicModelLoading ? httpLoader.documentModelLoader : void 0,
1813
1991
  logger: reactorLogger,
1814
- signer: renown ? getRenownSignerConfig(renown, options.identity?.requireSignatures) : void 0
1992
+ signer: renown ? getRenownSignerConfig(renown, options.identity?.keypairPath) : void 0,
1993
+ createSignaturePolicy: resolveCreateSignaturePolicy(process.env, {
1994
+ hasSigner: renown !== null,
1995
+ logger
1996
+ }),
1997
+ trustPolicy: reactorFeatureFlags.authEnforcement ? await getRenownTrustPolicyConfig(renownConfig.source === "self" ? {
1998
+ source: "self",
1999
+ request: (query, variables) => localRenownRequest ? localRenownRequest(query, variables) : Promise.reject(/* @__PURE__ */ new Error("The local renown read model is not bound yet"))
2000
+ } : {
2001
+ source: "remote",
2002
+ renownUrl: renownConfig.url,
2003
+ switchboardUrl: renownConfig.switchboardUrl
2004
+ }, renown) : void 0
1815
2005
  });
1816
2006
  if (workerPool) {
1817
2007
  if (!reactorDbUrl) throw new Error("unreachable: worker pool enabled without a reactor database URL");
@@ -1860,7 +2050,8 @@ async function initServer(serverPort, options, renown, renownConfig) {
1860
2050
  new ReactorInstrumentation(module.reactorModule).start();
1861
2051
  reactorLogger.info("Reactor metrics instrumentation started");
1862
2052
  }
1863
- driveNodeView = new DriveNodeView(baseKysely.withSchema(REACTOR_SCHEMA));
2053
+ const reactorDriveSchemaDb = baseKysely.withSchema(REACTOR_SCHEMA);
2054
+ driveNodeView = new DriveNodeView(reactorDriveSchemaDb);
1864
2055
  const reactorDriveClient = new ReactorDriveClient({
1865
2056
  reactor: module.client,
1866
2057
  readModel: driveNodeView
@@ -1949,7 +2140,8 @@ async function initServer(serverPort, options, renown, renownConfig) {
1949
2140
  await abortBoot(api);
1950
2141
  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
2142
  }
1952
- localCredentialCheck = createLocalCredentialVerifier((query, variables) => manager.executeSubgraphQuery(RENOWN_READ_MODEL_SUBGRAPH, query, variables), { onError: (error) => logger.error("Renown read model query failed: @error", error) });
2143
+ localRenownRequest = (query, variables) => manager.executeSubgraphQuery(RENOWN_READ_MODEL_SUBGRAPH, query, variables);
2144
+ localCredentialCheck = createLocalCredentialVerifier(localRenownRequest, { onError: (error) => logger.error("Renown read model query failed: @error", error) });
1953
2145
  logger.info("Renown credentials will be verified against this switchboard's own renown read model");
1954
2146
  }
1955
2147
  registerAttachmentRoutes(api);
@@ -2107,7 +2299,6 @@ const startSwitchboard = async (options = {}) => {
2107
2299
  const featureFlags = await initFeatureFlags();
2108
2300
  const enableDocumentModelSubgraphs = await featureFlags.getBooleanValue(DOCUMENT_MODEL_SUBGRAPHS_ENABLED, options.enableDocumentModelSubgraphs ?? DOCUMENT_MODEL_SUBGRAPHS_ENABLED_DEFAULT);
2109
2301
  options.enableDocumentModelSubgraphs = enableDocumentModelSubgraphs;
2110
- const requireSignatures = options.identity?.requireSignatures ?? await featureFlags.getBooleanValue(REQUIRE_SIGNATURES, REQUIRE_SIGNATURES_DEFAULT);
2111
2302
  const configPathForFlags = resolveConfigPath(options.configFile);
2112
2303
  const workflowsEnabled = await resolveWorkflowsEnabled({
2113
2304
  featureFlags,
@@ -2118,12 +2309,10 @@ const startSwitchboard = async (options = {}) => {
2118
2309
  const renownConfig = resolveRenownConfig(getConfig(resolveConfigPath(options.configFile)).auth?.renown, process.env, logger);
2119
2310
  options.identity = {
2120
2311
  ...options.identity,
2121
- requireSignatures,
2122
2312
  baseUrl: options.identity?.baseUrl ?? renownConfig.url
2123
2313
  };
2124
2314
  logger.info("Feature flags: @flags", JSON.stringify({
2125
2315
  DOCUMENT_MODEL_SUBGRAPHS_ENABLED: enableDocumentModelSubgraphs,
2126
- REQUIRE_SIGNATURES: requireSignatures,
2127
2316
  PH_WORKFLOWS_ENABLED: workflowsEnabled
2128
2317
  }, null, 2));
2129
2318
  let renown = null;
@@ -2145,5 +2334,5 @@ if (import.meta.main) await startSwitchboard({ fatalErrorShutdown: true });
2145
2334
  //#endregion
2146
2335
  export { parseForcePgVersion as a, applySwitchboardReactorDefaults as i, isPortAvailable as n, startSwitchboard as r, deriveAttachmentServiceConfig as t };
2147
2336
 
2148
- //# sourceMappingURL=server-DVotaGIp.mjs.map
2149
- //# debugId=f230ad22-7960-58f9-8fa7-97e55fa64a8d
2337
+ //# sourceMappingURL=server-C3WaP3kE.mjs.map
2338
+ //# debugId=8174bb20-5463-598a-9c2d-44a8760037a5