@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.
- package/.tsbuild/test/boot-create-signature-policy.test.d.ts +2 -0
- package/.tsbuild/test/boot-create-signature-policy.test.d.ts.map +1 -0
- package/.tsbuild/test/boot-without-renown.test.d.ts +2 -0
- package/.tsbuild/test/boot-without-renown.test.d.ts.map +1 -0
- package/.tsbuild/test/boot-without-require-signatures.test.d.ts +2 -0
- package/.tsbuild/test/boot-without-require-signatures.test.d.ts.map +1 -0
- package/.tsbuild/test/boot-worker-trust-policy.test.d.ts +2 -0
- package/.tsbuild/test/boot-worker-trust-policy.test.d.ts.map +1 -0
- package/.tsbuild/test/builder-defaults.test.d.ts +2 -0
- package/.tsbuild/test/builder-defaults.test.d.ts.map +1 -0
- package/.tsbuild/test/create-signature-policy.test.d.ts +2 -0
- package/.tsbuild/test/create-signature-policy.test.d.ts.map +1 -0
- package/.tsbuild/test/renown-trust-policy.test.d.ts +2 -0
- package/.tsbuild/test/renown-trust-policy.test.d.ts.map +1 -0
- package/.tsbuild/tsconfig.tsbuildinfo +1 -1
- package/Auth.md +41 -0
- package/CHANGELOG.md +20 -0
- package/README.md +14 -0
- package/dist/index.mjs +2 -2
- package/dist/{server-DVotaGIp.mjs → server-BFbus-at.mjs} +177 -29
- package/dist/server-BFbus-at.mjs.map +1 -0
- package/dist/server.d.mts +11 -5
- package/dist/server.d.mts.map +1 -1
- package/dist/server.mjs +4 -4
- package/dist/{utils-Baw7rThP.mjs → utils-BBHt2kdo.mjs} +15 -9
- package/dist/utils-BBHt2kdo.mjs.map +1 -0
- package/dist/utils.d.mts.map +1 -1
- package/dist/utils.mjs +3 -3
- package/package.json +16 -16
- package/test/attachments/auth.test.ts +85 -0
- package/test/attachments/index.test.ts +34 -3
- package/test/boot-create-signature-policy.test.ts +108 -0
- package/test/boot-without-renown.test.ts +81 -0
- package/test/boot-without-require-signatures.test.ts +74 -0
- package/test/boot-worker-trust-policy.test.ts +90 -0
- package/test/builder-defaults.test.ts +80 -0
- package/test/create-signature-policy.test.ts +69 -0
- package/test/renown-trust-policy.test.ts +315 -0
- package/dist/server-DVotaGIp.mjs.map +0 -1
- 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-
|
|
5
|
-
import "./utils-
|
|
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]="
|
|
3
|
-
import { n as addDefaultReactorDrive, r as isPostgresUrl, t as addDefaultDrive } from "./utils-
|
|
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,
|
|
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 (
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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,
|
|
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)
|
|
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
|
|
1243
|
-
|
|
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
|
|
1615
|
+
* @param keypairPath - Where `initRenown` loaded the key from
|
|
1564
1616
|
*/
|
|
1565
|
-
function getRenownSignerConfig(renown,
|
|
1617
|
+
function getRenownSignerConfig(renown, keypairPath) {
|
|
1566
1618
|
return {
|
|
1567
1619
|
signer: renown.signer,
|
|
1568
|
-
|
|
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?.
|
|
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
|
-
|
|
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-
|
|
2149
|
-
//# debugId=
|
|
2296
|
+
//# sourceMappingURL=server-BFbus-at.mjs.map
|
|
2297
|
+
//# debugId=dd6695ea-7590-5100-84c4-edefa353d65e
|