@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.
- 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/.tsbuild/tsdown.config.d.ts.map +1 -1
- package/.tsbuild/vitest.config.d.ts.map +1 -1
- package/Auth.md +92 -0
- package/CHANGELOG.md +38 -0
- package/README.md +14 -0
- package/dist/index.d.mts +2 -1
- package/dist/index.mjs +4 -5
- package/dist/index.mjs.map +1 -1
- package/dist/install-packages.d.mts +2 -1
- package/dist/install-packages.mjs +2 -2
- package/dist/install-packages.mjs.map +1 -1
- package/dist/migrate.d.mts +2 -1
- package/dist/migrate.mjs +4 -3
- package/dist/migrate.mjs.map +1 -1
- package/dist/{server-DVotaGIp.mjs → server-C3WaP3kE.mjs} +229 -40
- package/dist/server-C3WaP3kE.mjs.map +1 -0
- package/dist/server.d.mts +48 -28
- package/dist/server.d.mts.map +1 -1
- package/dist/server.mjs +3 -4
- package/dist/{utils-Baw7rThP.mjs → utils-uaBlzyjS.mjs} +15 -9
- package/dist/utils-uaBlzyjS.mjs.map +1 -0
- package/dist/utils.d.mts +4 -5
- package/dist/utils.d.mts.map +1 -1
- package/dist/utils.mjs +3 -3
- package/package.json +17 -17
- package/test/attachments/auth.test.ts +85 -0
- package/test/attachments/download-target.test.ts +71 -8
- 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/tsdown.config.ts +3 -1
- package/dist/server-DVotaGIp.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]="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,
|
|
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
|
|
@@ -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 =
|
|
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
|
|
429
|
-
*
|
|
430
|
-
*
|
|
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
|
-
|
|
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
|
|
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,
|
|
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)
|
|
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
|
|
1243
|
-
|
|
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
|
|
1655
|
+
* @param keypairPath - Where `initRenown` loaded the key from
|
|
1564
1656
|
*/
|
|
1565
|
-
function getRenownSignerConfig(renown,
|
|
1657
|
+
function getRenownSignerConfig(renown, keypairPath) {
|
|
1566
1658
|
return {
|
|
1567
1659
|
signer: renown.signer,
|
|
1568
|
-
|
|
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?.
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
2149
|
-
//# debugId=
|
|
2337
|
+
//# sourceMappingURL=server-C3WaP3kE.mjs.map
|
|
2338
|
+
//# debugId=8174bb20-5463-598a-9c2d-44a8760037a5
|