@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.
- 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/fatal-shutdown.test.d.ts +2 -0
- package/.tsbuild/test/fatal-shutdown.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 +33 -0
- package/README.md +14 -0
- package/dist/index.mjs +6 -5
- package/dist/index.mjs.map +1 -1
- package/dist/{server-C6VtW18k.mjs → server-BFbus-at.mjs} +225 -33
- package/dist/server-BFbus-at.mjs.map +1 -0
- package/dist/server.d.mts +19 -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/attachments/routes-integration.test.ts +40 -0
- package/test/attachments/routes.test.ts +126 -2
- 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/fatal-shutdown.test.ts +102 -0
- package/test/renown-trust-policy.test.ts +315 -0
- package/dist/server-C6VtW18k.mjs.map +0 -1
- 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]="
|
|
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
|
|
@@ -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",
|
|
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
|
|
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",
|
|
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)
|
|
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
|
|
1200
|
-
|
|
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
|
|
1615
|
+
* @param keypairPath - Where `initRenown` loaded the key from
|
|
1521
1616
|
*/
|
|
1522
|
-
function getRenownSignerConfig(renown,
|
|
1617
|
+
function getRenownSignerConfig(renown, keypairPath) {
|
|
1523
1618
|
return {
|
|
1524
1619
|
signer: renown.signer,
|
|
1525
|
-
|
|
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?.
|
|
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
|
-
|
|
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-
|
|
2105
|
-
//# debugId=
|
|
2296
|
+
//# sourceMappingURL=server-BFbus-at.mjs.map
|
|
2297
|
+
//# debugId=dd6695ea-7590-5100-84c4-edefa353d65e
|