@powerhousedao/switchboard 6.2.2-dev.9 → 6.2.2-staging.0
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/.gitignore +41 -0
- package/.npmignore +2 -0
- package/.tsbuild/test/attachment-reference-read-model.test.d.ts +2 -0
- package/.tsbuild/test/attachment-reference-read-model.test.d.ts.map +1 -0
- package/.tsbuild/test/attachments/download-target.test.d.ts +2 -0
- package/.tsbuild/test/attachments/download-target.test.d.ts.map +1 -0
- package/.tsbuild/test/reactor-feature-flags.test.d.ts +2 -0
- package/.tsbuild/test/reactor-feature-flags.test.d.ts.map +1 -0
- package/.tsbuild/tsconfig.tsbuildinfo +1 -1
- package/Auth.md +107 -1
- package/CHANGELOG.md +587 -0
- package/README.md +44 -2
- package/dist/index.mjs +10 -4
- package/dist/index.mjs.map +1 -1
- package/dist/{server-Cgud_ONM.mjs → server-DYraD11D.mjs} +364 -48
- package/dist/server-DYraD11D.mjs.map +1 -0
- package/dist/server.d.mts +9 -5
- package/dist/server.d.mts.map +1 -1
- package/dist/server.mjs +3 -3
- package/package.json +14 -13
- package/test/attachment-reference-read-model.test.ts +425 -0
- package/test/attachments/auth.test.ts +138 -8
- package/test/attachments/download-target.test.ts +450 -0
- package/test/attachments/index.test.ts +37 -2
- package/test/attachments/routes-integration.test.ts +98 -1
- package/test/reactor-feature-flags.test.ts +61 -0
- package/test/worker-pool.test.ts +27 -0
- package/tsconfig.json +3 -0
- package/dist/server-Cgud_ONM.mjs.map +0 -1
|
@@ -1,25 +1,27 @@
|
|
|
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]="
|
|
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]="65ab8387-7c36-5ebd-b971-913ce2e772b3")}catch(e){}}();
|
|
3
3
|
import { n as addDefaultReactorDrive, r as isPostgresUrl, t as addDefaultDrive } from "./utils-Baw7rThP.mjs";
|
|
4
4
|
import { register } from "node:module";
|
|
5
|
+
import { ReactorInstrumentation } from "@powerhousedao/opentelemetry-instrumentation-reactor";
|
|
5
6
|
import * as Sentry from "@sentry/node";
|
|
6
7
|
import { childLogger, documentModelDocumentModelModule, setLogLevel } from "document-model";
|
|
7
8
|
import dotenv from "dotenv";
|
|
8
9
|
import { getConfig } from "@powerhousedao/config/node";
|
|
9
10
|
import { existsSync, promises, realpathSync } from "node:fs";
|
|
10
11
|
import path from "node:path";
|
|
11
|
-
import { ReactorInstrumentation } from "@powerhousedao/opentelemetry-instrumentation-reactor";
|
|
12
12
|
import { AtomicNodeFs } from "@powerhousedao/pglite-fs";
|
|
13
|
-
import { ChannelScheme, DriveCollectionId, EventBus, REACTOR_SCHEMA, ReactorBuilder, ReactorClientBuilder, parseDriveUrl } from "@powerhousedao/reactor";
|
|
14
|
-
import { HttpPackageLoader, ImportPackageLoader, PackageManagementService, PackagesSubgraph, getUniqueDocumentModels, initializeAndStartAPI } from "@powerhousedao/reactor-api";
|
|
13
|
+
import { ChannelScheme, DriveCollectionId, EventBus, REACTOR_SCHEMA, ReactorBuilder, ReactorClientBuilder, instrumentPgPool, parseDriveUrl, supportsLiveReadModelRegistration } from "@powerhousedao/reactor";
|
|
14
|
+
import { HttpPackageLoader, ImportPackageLoader, PGLITE_UTC_PARSERS, PackageManagementService, PackagesSubgraph, getUniqueDocumentModels, initializeAndStartAPI, resolveRenownConfig } from "@powerhousedao/reactor-api";
|
|
15
15
|
import { httpsHooksPath } from "@powerhousedao/reactor-api/https-hooks";
|
|
16
|
-
import { AttachmentAlreadyExists, AttachmentNotFound, AttachmentPending, HashMismatch, InvalidAttachmentRef, ReservationNotFound, SizeMismatch, UploadTooLarge, createRemoteAttachmentService } from "@powerhousedao/reactor-attachments";
|
|
16
|
+
import { AttachmentAlreadyExists, AttachmentNotFound, AttachmentPending, AttachmentReferenceReadModel, AttachmentSchemaCompiler, HashMismatch, InvalidAttachmentRef, ReservationNotFound, SizeMismatch, UploadTooLarge, createRef, createRemoteAttachmentService, parseAttachmentDownloadTarget } from "@powerhousedao/reactor-attachments";
|
|
17
17
|
import { DriveNodeView, NodeProcessor, ReactorDriveClient, createReactorDriveResolvers, reactorDriveDocumentModelModule, reactorDriveSubgraphTypeDefs } from "@powerhousedao/reactor-drive";
|
|
18
|
+
import { DEFAULT_RENOWN_URL, NodeKeyStorage, RENOWN_READ_MODEL_SUBGRAPH, RenownBuilder, RenownCryptoBuilder, createLocalCredentialVerifier, createSignatureVerifier } from "@renown/sdk/node";
|
|
18
19
|
import { Kysely, PostgresDialect } from "kysely";
|
|
19
20
|
import net from "node:net";
|
|
20
21
|
import path$1 from "path";
|
|
21
22
|
import { Pool } from "pg";
|
|
22
23
|
import { Readable } from "node:stream";
|
|
24
|
+
import { ReactorGroupV1 } from "@powerhousedao/reactor-group";
|
|
23
25
|
import { driveDocumentModelModule } from "@powerhousedao/shared/document-drive";
|
|
24
26
|
import { readFile } from "node:fs/promises";
|
|
25
27
|
import os from "node:os";
|
|
@@ -27,7 +29,6 @@ import { fileURLToPath } from "node:url";
|
|
|
27
29
|
import { EnvVarProvider } from "@openfeature/env-var-provider";
|
|
28
30
|
import { OpenFeature } from "@openfeature/server-sdk";
|
|
29
31
|
import { PGliteDialect } from "kysely-pglite-dialect";
|
|
30
|
-
import { DEFAULT_RENOWN_URL, NodeKeyStorage, RenownBuilder, RenownCryptoBuilder, createSignatureVerifier } from "@renown/sdk/node";
|
|
31
32
|
//#region src/pglite-version.ts
|
|
32
33
|
const SUPPORTED_PG_MAJORS = [16, 17];
|
|
33
34
|
async function readPgVersionFile(dataDir) {
|
|
@@ -64,13 +65,20 @@ async function loadPgDump(major) {
|
|
|
64
65
|
}
|
|
65
66
|
//#endregion
|
|
66
67
|
//#region src/attachments/auth.ts
|
|
68
|
+
const ANONYMOUS_ACTOR = {
|
|
69
|
+
user: void 0,
|
|
70
|
+
authEnabled: false
|
|
71
|
+
};
|
|
67
72
|
/**
|
|
68
73
|
* Wrap a Node-style handler so that, when `authService` is provided and auth is
|
|
69
|
-
* enabled, the request must carry a verifiable Bearer token.
|
|
74
|
+
* enabled, the request must carry a verifiable Bearer token. The handler always
|
|
75
|
+
* receives an actor context: the verified bearer user when auth is enabled, or
|
|
76
|
+
* the anonymous context when it is disabled. With `allowAnonymous`, a missing
|
|
77
|
+
* bearer yields an anonymous actor with `authEnabled: true` instead of a 401.
|
|
70
78
|
*/
|
|
71
|
-
function requireAuth(authService, handler) {
|
|
72
|
-
if (!authService) return handler;
|
|
73
|
-
return async (req, res) => {
|
|
79
|
+
function requireAuth(authService, handler, options) {
|
|
80
|
+
if (!authService) return (req, res, body) => handler(req, res, body, ANONYMOUS_ACTOR);
|
|
81
|
+
return async (req, res, body) => {
|
|
74
82
|
let result;
|
|
75
83
|
try {
|
|
76
84
|
result = await authService.verifyBearer(req.headers.authorization);
|
|
@@ -88,25 +96,32 @@ function requireAuth(authService, handler) {
|
|
|
88
96
|
res.end(body);
|
|
89
97
|
return;
|
|
90
98
|
}
|
|
91
|
-
if (result.auth_enabled && !result.user) {
|
|
99
|
+
if (result.auth_enabled && !result.user && !options?.allowAnonymous) {
|
|
92
100
|
res.statusCode = 401;
|
|
93
101
|
res.setHeader("Content-Type", "application/json");
|
|
94
102
|
res.end(JSON.stringify({ error: "Authentication required" }));
|
|
95
103
|
return;
|
|
96
104
|
}
|
|
97
|
-
await handler(req, res
|
|
105
|
+
await handler(req, res, body, {
|
|
106
|
+
user: result.user,
|
|
107
|
+
authEnabled: result.auth_enabled
|
|
108
|
+
});
|
|
98
109
|
};
|
|
99
110
|
}
|
|
100
111
|
//#endregion
|
|
101
112
|
//#region src/attachments/mount-auth.ts
|
|
102
113
|
/**
|
|
103
114
|
* Mount a Node-style attachment route with `requireAuth` applied unconditionally.
|
|
104
|
-
* When `api.authService` is undefined (auth disabled),
|
|
105
|
-
*
|
|
106
|
-
*
|
|
115
|
+
* When `api.authService` is undefined (auth disabled), the handler still runs
|
|
116
|
+
* through `requireAuth` and receives the anonymous actor context — that is the
|
|
117
|
+
* only way to opt out of bearer verification. To register a route without auth
|
|
118
|
+
* wrapping you must call `api.httpAdapter.mountNodeRoute` directly.
|
|
119
|
+
*
|
|
120
|
+
* `allowAnonymous` is reserved for routes whose handlers make a per-document
|
|
121
|
+
* authorization decision themselves; identity-only routes must not use it.
|
|
107
122
|
*/
|
|
108
|
-
function mountAuthenticatedNodeRoute(api, method, path, handler) {
|
|
109
|
-
api.httpAdapter.mountNodeRoute(method, path, requireAuth(api.authService, handler));
|
|
123
|
+
function mountAuthenticatedNodeRoute(api, method, path, handler, options) {
|
|
124
|
+
api.httpAdapter.mountNodeRoute(method, path, requireAuth(api.authService, handler, options));
|
|
110
125
|
}
|
|
111
126
|
//#endregion
|
|
112
127
|
//#region src/attachments/routes.ts
|
|
@@ -216,7 +231,8 @@ function makeReserveHandler(attachments) {
|
|
|
216
231
|
sendJson(res, 201, {
|
|
217
232
|
reservationId: upload.reservationId,
|
|
218
233
|
ref: upload.ref,
|
|
219
|
-
expiresAtUtc: upload.expiresAtUtc
|
|
234
|
+
expiresAtUtc: upload.expiresAtUtc,
|
|
235
|
+
...upload.uploadTarget ? { uploadTarget: upload.uploadTarget } : {}
|
|
220
236
|
});
|
|
221
237
|
};
|
|
222
238
|
}
|
|
@@ -227,6 +243,10 @@ function makeUploadHandler(attachments) {
|
|
|
227
243
|
sendError(res, 400, "Missing reservationId");
|
|
228
244
|
return;
|
|
229
245
|
}
|
|
246
|
+
if (attachments.backend?.kind === "s3") {
|
|
247
|
+
sendError(res, 405, "Use the reservation uploadTarget for S3 uploads");
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
230
250
|
let reservation;
|
|
231
251
|
try {
|
|
232
252
|
reservation = await attachments.reservations.get(reservationId);
|
|
@@ -373,6 +393,144 @@ function makeDeleteReservationHandler(attachments) {
|
|
|
373
393
|
function extractParam(req, name) {
|
|
374
394
|
return req.params?.[name];
|
|
375
395
|
}
|
|
396
|
+
const MAX_DOCUMENT_ID_LEN = 512;
|
|
397
|
+
const ATTACHMENT_NOT_FOUND_BODY = { error: "Attachment not found" };
|
|
398
|
+
const MAX_DOWNLOAD_TARGET_TTL_SECONDS = 10080 * 60;
|
|
399
|
+
/**
|
|
400
|
+
* Returns the single `documentId` query value, or null when it is missing,
|
|
401
|
+
* duplicated, blank, or oversized. Validation happens before authorization so
|
|
402
|
+
* malformed requests never reach the access service.
|
|
403
|
+
*/
|
|
404
|
+
function extractSingleDocumentId(req) {
|
|
405
|
+
if (!req.url) return null;
|
|
406
|
+
let url;
|
|
407
|
+
try {
|
|
408
|
+
url = new URL(req.url, "http://switchboard.invalid");
|
|
409
|
+
} catch {
|
|
410
|
+
return null;
|
|
411
|
+
}
|
|
412
|
+
const values = url.searchParams.getAll("documentId");
|
|
413
|
+
if (values.length !== 1) return null;
|
|
414
|
+
const value = values[0];
|
|
415
|
+
if (value.trim().length === 0 || value.length > MAX_DOCUMENT_ID_LEN) return null;
|
|
416
|
+
return value;
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Returns the requested target TTL in seconds: undefined when absent,
|
|
420
|
+
* "invalid" when malformed (duplicated, non-integer, or non-positive), and
|
|
421
|
+
* otherwise the value clamped to the presigning ceiling.
|
|
422
|
+
*/
|
|
423
|
+
function extractExpiresIn(req) {
|
|
424
|
+
if (!req.url) return void 0;
|
|
425
|
+
let url;
|
|
426
|
+
try {
|
|
427
|
+
url = new URL(req.url, "http://switchboard.invalid");
|
|
428
|
+
} catch {
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
const values = url.searchParams.getAll("expiresIn");
|
|
432
|
+
if (values.length === 0) return void 0;
|
|
433
|
+
if (values.length > 1) return "invalid";
|
|
434
|
+
const parsed = Number(values[0]);
|
|
435
|
+
if (!Number.isInteger(parsed) || parsed <= 0) return "invalid";
|
|
436
|
+
return Math.min(parsed, MAX_DOWNLOAD_TARGET_TTL_SECONDS);
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Base URL of this Switchboard as seen by the caller, used to build
|
|
440
|
+
* filesystem `switchboard` download targets that point back at the existing
|
|
441
|
+
* authenticated byte route.
|
|
442
|
+
*/
|
|
443
|
+
function requestBaseUrl(req) {
|
|
444
|
+
const forwardedProto = req.headers["x-forwarded-proto"];
|
|
445
|
+
const proto = (Array.isArray(forwardedProto) ? forwardedProto[0] : forwardedProto)?.split(",")[0]?.trim() || (req.socket.encrypted ? "https" : "http");
|
|
446
|
+
const forwardedHost = req.headers["x-forwarded-host"];
|
|
447
|
+
const host = (Array.isArray(forwardedHost) ? forwardedHost[0] : forwardedHost) ?? req.headers.host;
|
|
448
|
+
if (!host) return null;
|
|
449
|
+
return `${proto}://${host}`;
|
|
450
|
+
}
|
|
451
|
+
function makeDownloadTargetHandler(attachments, attachmentAccess) {
|
|
452
|
+
return async (req, res, _body, actor) => {
|
|
453
|
+
res.setHeader("Cache-Control", "no-store");
|
|
454
|
+
const hash = extractParam(req, "hash");
|
|
455
|
+
if (!hash || !HASH_PATTERN.test(hash)) {
|
|
456
|
+
sendError(res, 400, "Invalid attachment hash");
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
const documentId = extractSingleDocumentId(req);
|
|
460
|
+
if (documentId === null) {
|
|
461
|
+
sendError(res, 400, "documentId is required exactly once as a non-empty query parameter");
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
const expiresIn = extractExpiresIn(req);
|
|
465
|
+
if (expiresIn === "invalid") {
|
|
466
|
+
sendError(res, 400, "expiresIn must be a single positive integer number of seconds");
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
const canonicalHash = hash.toLowerCase();
|
|
470
|
+
let decision;
|
|
471
|
+
try {
|
|
472
|
+
decision = await attachmentAccess.canReadAttachment({
|
|
473
|
+
documentId,
|
|
474
|
+
attachmentRef: createRef(canonicalHash),
|
|
475
|
+
userAddress: actor?.user?.address
|
|
476
|
+
});
|
|
477
|
+
} catch (err) {
|
|
478
|
+
logger$1.error("Attachment access decision failed: @error", err);
|
|
479
|
+
sendError(res, 500, "Internal error");
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
if (decision.kind === "projection-unavailable") {
|
|
483
|
+
sendError(res, 503, "Attachment downloads are temporarily unavailable");
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
if (decision.kind === "denied") {
|
|
487
|
+
sendJson(res, 404, ATTACHMENT_NOT_FOUND_BODY);
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
let header;
|
|
491
|
+
try {
|
|
492
|
+
header = await attachments.store.stat(canonicalHash);
|
|
493
|
+
} catch (err) {
|
|
494
|
+
if (err instanceof AttachmentNotFound) {
|
|
495
|
+
sendJson(res, 404, ATTACHMENT_NOT_FOUND_BODY);
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
logger$1.error("Attachment metadata lookup failed: @error", err);
|
|
499
|
+
sendError(res, 500, "Internal error");
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
if (header.status !== "available") {
|
|
503
|
+
sendJson(res, 404, ATTACHMENT_NOT_FOUND_BODY);
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
let target;
|
|
507
|
+
if (attachments.backend && attachments.backend.kind !== "filesystem") try {
|
|
508
|
+
target = await attachments.backend.prepareDownloadTarget(canonicalHash, expiresIn);
|
|
509
|
+
} catch {
|
|
510
|
+
sendError(res, 502, "Attachment download target unavailable");
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
else {
|
|
514
|
+
const base = requestBaseUrl(req);
|
|
515
|
+
if (!base) {
|
|
516
|
+
sendError(res, 500, "Internal error");
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
try {
|
|
520
|
+
target = parseAttachmentDownloadTarget({
|
|
521
|
+
kind: "switchboard",
|
|
522
|
+
method: "GET",
|
|
523
|
+
url: `${base}/attachments/${canonicalHash}`,
|
|
524
|
+
headers: {}
|
|
525
|
+
});
|
|
526
|
+
} catch {
|
|
527
|
+
sendError(res, 500, "Internal error");
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
sendJson(res, 200, target);
|
|
532
|
+
};
|
|
533
|
+
}
|
|
376
534
|
//#endregion
|
|
377
535
|
//#region src/attachments/index.ts
|
|
378
536
|
function registerAttachmentRoutes(api) {
|
|
@@ -382,9 +540,50 @@ function registerAttachmentRoutes(api) {
|
|
|
382
540
|
mountAuthenticatedNodeRoute(api, "DELETE", "/attachments/reservations/:reservationId", makeDeleteReservationHandler(attachments));
|
|
383
541
|
mountAuthenticatedNodeRoute(api, "PUT", "/attachments/reservations/:reservationId", makeUploadHandler(attachments));
|
|
384
542
|
mountAuthenticatedNodeRoute(api, "HEAD", "/attachments/:hash", makeStatHandler(attachments));
|
|
543
|
+
mountAuthenticatedNodeRoute(api, "GET", "/attachments/:hash/download-target", makeDownloadTargetHandler(attachments, api.attachmentAccess), { allowAnonymous: true });
|
|
385
544
|
mountAuthenticatedNodeRoute(api, "GET", "/attachments/:hash", makeDownloadHandler(attachments));
|
|
386
545
|
}
|
|
387
546
|
//#endregion
|
|
547
|
+
//#region src/attachment-reference-read-model.mts
|
|
548
|
+
const attachmentSchemaCompiler = new AttachmentSchemaCompiler();
|
|
549
|
+
function createAttachmentReferenceReadModel(baseKysely, dependencies, attachmentReferenceWriter) {
|
|
550
|
+
return new AttachmentReferenceReadModel(baseKysely.withSchema(REACTOR_SCHEMA), dependencies.operationIndex, dependencies.writeCache, dependencies.processorManagerConsistencyTracker, dependencies.documentModelRegistry, attachmentSchemaCompiler, attachmentReferenceWriter);
|
|
551
|
+
}
|
|
552
|
+
function registerAttachmentReferenceReadModel(reactorBuilder, registration) {
|
|
553
|
+
reactorBuilder.withReadModelFactory(async ({ documentModelRegistry, operationIndex, writeCache, processorManagerConsistencyTracker }) => {
|
|
554
|
+
const readModel = createAttachmentReferenceReadModel(registration.baseKysely, {
|
|
555
|
+
operationIndex,
|
|
556
|
+
writeCache,
|
|
557
|
+
processorManagerConsistencyTracker,
|
|
558
|
+
documentModelRegistry
|
|
559
|
+
}, registration.attachmentReferenceWriter);
|
|
560
|
+
await readModel.init();
|
|
561
|
+
return readModel;
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
async function registerAttachmentReferenceReadModelOnModule(clientModule, attachmentReferenceWriter) {
|
|
565
|
+
const reactorModule = clientModule.reactorModule;
|
|
566
|
+
if (!reactorModule) return {
|
|
567
|
+
status: "unavailable",
|
|
568
|
+
reason: "in-process-reactor-module-unavailable"
|
|
569
|
+
};
|
|
570
|
+
const coordinator = reactorModule.readModelCoordinator;
|
|
571
|
+
if (!supportsLiveReadModelRegistration(coordinator)) return {
|
|
572
|
+
status: "unavailable",
|
|
573
|
+
reason: "live-read-model-registration-unsupported"
|
|
574
|
+
};
|
|
575
|
+
const readModel = createAttachmentReferenceReadModel(reactorModule.database, {
|
|
576
|
+
operationIndex: reactorModule.operationIndex,
|
|
577
|
+
writeCache: reactorModule.writeCache,
|
|
578
|
+
processorManagerConsistencyTracker: reactorModule.processorManagerConsistencyTracker,
|
|
579
|
+
documentModelRegistry: reactorModule.documentModelRegistry
|
|
580
|
+
}, attachmentReferenceWriter);
|
|
581
|
+
await readModel.init();
|
|
582
|
+
coordinator.addReadModel(readModel, "pre_ready");
|
|
583
|
+
await readModel.init();
|
|
584
|
+
return { status: "available" };
|
|
585
|
+
}
|
|
586
|
+
//#endregion
|
|
388
587
|
//#region src/builder-defaults.mts
|
|
389
588
|
/**
|
|
390
589
|
* Apply switchboard's standard configuration to a reactor + client builder
|
|
@@ -400,17 +599,22 @@ function switchboardBaseDocumentModels() {
|
|
|
400
599
|
return [
|
|
401
600
|
documentModelDocumentModelModule,
|
|
402
601
|
driveDocumentModelModule,
|
|
403
|
-
reactorDriveDocumentModelModule
|
|
602
|
+
reactorDriveDocumentModelModule,
|
|
603
|
+
ReactorGroupV1
|
|
404
604
|
];
|
|
405
605
|
}
|
|
406
606
|
function applySwitchboardReactorDefaults(reactorBuilder, clientBuilder, options = {}) {
|
|
407
607
|
const baseModels = options.includeBaseModels !== false ? switchboardBaseDocumentModels() : [];
|
|
408
608
|
const extra = options.documentModels ?? [];
|
|
409
609
|
if (baseModels.length || extra.length) reactorBuilder.withDocumentModelSources(getUniqueDocumentModels(baseModels, extra));
|
|
610
|
+
if (options.upgradeManifests?.length) reactorBuilder.withUpgradeManifests(options.upgradeManifests);
|
|
410
611
|
const scheme = options.channelScheme === void 0 ? ChannelScheme.SWITCHBOARD : options.channelScheme;
|
|
411
612
|
if (scheme !== false) reactorBuilder.withChannelScheme(scheme);
|
|
412
613
|
if (options.signalHandlers !== false) reactorBuilder.withSignalHandlers();
|
|
413
|
-
if (options.executorConfig?.maxSkipThreshold !== void 0) reactorBuilder.withExecutorConfig({
|
|
614
|
+
if (options.executorConfig?.maxSkipThreshold !== void 0 || options.executorConfig?.featureFlags !== void 0) reactorBuilder.withExecutorConfig({
|
|
615
|
+
...options.executorConfig.maxSkipThreshold !== void 0 ? { maxSkipThreshold: options.executorConfig.maxSkipThreshold } : {},
|
|
616
|
+
...options.executorConfig.featureFlags !== void 0 ? { featureFlags: options.executorConfig.featureFlags } : {}
|
|
617
|
+
});
|
|
414
618
|
if (options.documentModelLoader) reactorBuilder.withDocumentModelLoader(options.documentModelLoader);
|
|
415
619
|
if (options.logger) reactorBuilder.withLogger(options.logger);
|
|
416
620
|
if (options.signer) clientBuilder.withSigner(options.signer);
|
|
@@ -419,6 +623,7 @@ function applySwitchboardReactorDefaults(reactorBuilder, clientBuilder, options
|
|
|
419
623
|
//#region src/worker-pool.mts
|
|
420
624
|
const DEFAULT_DB_POOL_SIZE_PER_WORKER = 2;
|
|
421
625
|
const DEFAULT_ACQUIRE_TIMEOUT_MS = 5e3;
|
|
626
|
+
const DEFAULT_DB_POOL_SIZE_HOST = 16;
|
|
422
627
|
const AUTO_RESERVED_CORES = 2;
|
|
423
628
|
const AUTO_WORKER_CAP = 8;
|
|
424
629
|
/** Worker count for "auto": always at least 1 — auto sizes the pool, it does not turn worker mode off. */
|
|
@@ -444,6 +649,23 @@ function resolveWorkerPoolOptions(input, env, availableCores = os.availableParal
|
|
|
444
649
|
};
|
|
445
650
|
}
|
|
446
651
|
/**
|
|
652
|
+
* Size of the reactor's own (host) Postgres pool. Unlike the worker pool there
|
|
653
|
+
* is no "disabled" state — the host pool is the only Postgres pool in the
|
|
654
|
+
* server path, backing both reactor storage and the read models that reach it
|
|
655
|
+
* via `withSchema` — so 0 is rejected rather than read as "off".
|
|
656
|
+
*
|
|
657
|
+
* The acquire timeout is deliberately not configurable here: the host pool
|
|
658
|
+
* waits indefinitely today, and making it finite converts saturation from a
|
|
659
|
+
* latency problem into a thrown error on the read-model path. That is a
|
|
660
|
+
* behavior change worth making separately, once the retry semantics of a
|
|
661
|
+
* rejected `pool.connect()` inside the read-model coordinator are settled.
|
|
662
|
+
*/
|
|
663
|
+
function resolveHostPoolSize(env) {
|
|
664
|
+
const poolSize = parseNonNegativeInt(env.REACTOR_DB_POOL_SIZE_HOST, "REACTOR_DB_POOL_SIZE_HOST") ?? DEFAULT_DB_POOL_SIZE_HOST;
|
|
665
|
+
if (poolSize < 1) throw new Error("REACTOR_DB_POOL_SIZE_HOST must be at least 1; the host pool cannot be disabled");
|
|
666
|
+
return poolSize;
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
447
669
|
* DbConfig for each worker's own pool, parsed from a postgres:// URL.
|
|
448
670
|
* Explicit host, database, and user are required — no pg-side defaults.
|
|
449
671
|
*/
|
|
@@ -723,6 +945,26 @@ async function rollback(dataDir, backupDir, originalError, logger) {
|
|
|
723
945
|
logger.error(`[pglite-migration] Migration failed for ${dataDir}; rolled back from ${backupDir}. Original error: ${String(originalError)}`);
|
|
724
946
|
}
|
|
725
947
|
//#endregion
|
|
948
|
+
//#region src/reactor-feature-flags.mts
|
|
949
|
+
/**
|
|
950
|
+
* Enforcement flags from the REACTOR_* env vars. Each flag requires the ones
|
|
951
|
+
* before it; the reactor rejects an inconsistent set rather than enforcing less
|
|
952
|
+
* than the operator asked for, so this reports what was asked without
|
|
953
|
+
* correcting it.
|
|
954
|
+
*/
|
|
955
|
+
function resolveReactorFeatureFlags(env) {
|
|
956
|
+
const flags = {
|
|
957
|
+
documentDecisions: env.REACTOR_DOCUMENT_DECISIONS === "true",
|
|
958
|
+
authEnforcement: env.REACTOR_AUTH_ENFORCEMENT === "true",
|
|
959
|
+
authGroups: env.REACTOR_AUTH_GROUPS === "true",
|
|
960
|
+
authConditions: env.REACTOR_AUTH_CONDITIONS === "true"
|
|
961
|
+
};
|
|
962
|
+
return {
|
|
963
|
+
flags,
|
|
964
|
+
enabled: Object.entries(flags).filter(([, isEnabled]) => isEnabled).map(([name]) => name)
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
//#endregion
|
|
726
968
|
//#region src/renown.ts
|
|
727
969
|
const logger = childLogger(["switchboard", "renown"]);
|
|
728
970
|
/**
|
|
@@ -793,6 +1035,10 @@ function isPortAvailable(port) {
|
|
|
793
1035
|
});
|
|
794
1036
|
});
|
|
795
1037
|
}
|
|
1038
|
+
/** The powerhouse.config.json this run reads, defaulting to the cwd copy. */
|
|
1039
|
+
function resolveConfigPath(configFile) {
|
|
1040
|
+
return configFile ?? path$1.join(process.cwd(), "powerhouse.config.json");
|
|
1041
|
+
}
|
|
796
1042
|
async function resolveServerPort(requested, strictPort, logger) {
|
|
797
1043
|
if (strictPort) return requested;
|
|
798
1044
|
for (let i = 0; i < PORT_FALLBACK_ATTEMPTS; i++) {
|
|
@@ -805,11 +1051,20 @@ async function resolveServerPort(requested, strictPort, logger) {
|
|
|
805
1051
|
return requested;
|
|
806
1052
|
}
|
|
807
1053
|
async function createReactorKysely(opts) {
|
|
808
|
-
const { reactorDbUrl, reactorPgliteDir, reactorPgliteMajor, inMemory, flushIntervalMs, logger } = opts;
|
|
1054
|
+
const { reactorDbUrl, reactorPgliteDir, reactorPgliteMajor, inMemory, flushIntervalMs, hostPoolSize, logger } = opts;
|
|
809
1055
|
if (reactorDbUrl && isPostgresUrl(reactorDbUrl)) {
|
|
810
|
-
const
|
|
811
|
-
|
|
812
|
-
|
|
1056
|
+
const connectionString = reactorDbUrl.includes("?") ? reactorDbUrl : `${reactorDbUrl}?sslmode=disable`;
|
|
1057
|
+
const poolSize = hostPoolSize();
|
|
1058
|
+
const pool = new Pool({
|
|
1059
|
+
connectionString,
|
|
1060
|
+
max: poolSize
|
|
1061
|
+
});
|
|
1062
|
+
const poolInstrumentation = instrumentPgPool(pool, "reactor-host");
|
|
1063
|
+
logger.info(`Using PostgreSQL for reactor storage (host pool max ${poolSize})`);
|
|
1064
|
+
return {
|
|
1065
|
+
kysely: new Kysely({ dialect: new PostgresDialect({ pool }) }),
|
|
1066
|
+
poolInstrumentation
|
|
1067
|
+
};
|
|
813
1068
|
}
|
|
814
1069
|
if (!reactorPgliteDir || reactorPgliteMajor === null) throw new Error("Reactor PGLite directory not resolved");
|
|
815
1070
|
const { PGlite } = await loadPGliteModule(reactorPgliteMajor);
|
|
@@ -818,7 +1073,10 @@ async function createReactorKysely(opts) {
|
|
|
818
1073
|
flushIntervalMs
|
|
819
1074
|
}) });
|
|
820
1075
|
logger.info(inMemory ? `Using in-memory PGlite (PG${reactorPgliteMajor}) for reactor storage [PH_PGLITE_IN_MEMORY=1]` : `Using PGlite (PG${reactorPgliteMajor}) for reactor storage at ${reactorPgliteDir}`);
|
|
821
|
-
return
|
|
1076
|
+
return {
|
|
1077
|
+
kysely: new Kysely({ dialect: new ClosablePGliteDialect(pglite) }),
|
|
1078
|
+
poolInstrumentation: void 0
|
|
1079
|
+
};
|
|
822
1080
|
}
|
|
823
1081
|
/** Derive the remote attachment service config for switchboard's own `/attachments/*` API. */
|
|
824
1082
|
function deriveAttachmentServiceConfig(options, serverPort, renown) {
|
|
@@ -831,7 +1089,7 @@ function deriveAttachmentServiceConfig(options, serverPort, renown) {
|
|
|
831
1089
|
}) : void 0 : void 0
|
|
832
1090
|
};
|
|
833
1091
|
}
|
|
834
|
-
async function initServer(serverPort, options, renown) {
|
|
1092
|
+
async function initServer(serverPort, options, renown, renownConfig) {
|
|
835
1093
|
const { dev, packages = [], remoteDrives = [], logger = defaultLogger } = options;
|
|
836
1094
|
logger.level = LogLevel;
|
|
837
1095
|
const dbPath = options.dbPath ?? process.env.DATABASE_URL ?? process.env.PH_SWITCHBOARD_DATABASE_URL;
|
|
@@ -897,7 +1155,8 @@ async function initServer(serverPort, options, renown) {
|
|
|
897
1155
|
}
|
|
898
1156
|
const apiRef = { current: void 0 };
|
|
899
1157
|
let driveNodeView;
|
|
900
|
-
const
|
|
1158
|
+
const configPath = resolveConfigPath(options.configFile);
|
|
1159
|
+
const config = getConfig(configPath);
|
|
901
1160
|
const registryUrl = options.registryUrl ?? process.env.PH_REGISTRY_URL ?? config.packageRegistryUrl;
|
|
902
1161
|
const registryPackages = process.env.PH_REGISTRY_PACKAGES;
|
|
903
1162
|
const dynamicModelLoading = options.dynamicModelLoading ?? process.env.DYNAMIC_MODEL_LOADING === "true";
|
|
@@ -911,32 +1170,44 @@ async function initServer(serverPort, options, renown) {
|
|
|
911
1170
|
});
|
|
912
1171
|
}
|
|
913
1172
|
const reactorLogger = logger.child(["reactor"]);
|
|
914
|
-
|
|
1173
|
+
let ownedReactorModule;
|
|
1174
|
+
const initializeClient = async (documentModels, { attachmentReferenceWriter, upgradeManifests }) => {
|
|
915
1175
|
if (options.reactor) {
|
|
1176
|
+
const attachmentReferenceProjection = await registerAttachmentReferenceReadModelOnModule(options.reactor, attachmentReferenceWriter);
|
|
916
1177
|
if (options.reactor.reactorModule) {
|
|
917
1178
|
new ReactorInstrumentation(options.reactor.reactorModule).start();
|
|
918
1179
|
reactorLogger.info("Reactor metrics instrumentation started (using caller-provided reactor)");
|
|
919
1180
|
}
|
|
920
|
-
return {
|
|
1181
|
+
return {
|
|
1182
|
+
module: options.reactor,
|
|
1183
|
+
attachmentReferenceProjection
|
|
1184
|
+
};
|
|
921
1185
|
}
|
|
922
|
-
const baseKysely = await createReactorKysely({
|
|
1186
|
+
const { kysely: baseKysely, poolInstrumentation } = await createReactorKysely({
|
|
923
1187
|
reactorDbUrl,
|
|
924
1188
|
reactorPgliteDir,
|
|
925
1189
|
reactorPgliteMajor,
|
|
926
1190
|
inMemory: PGLITE_IN_MEMORY,
|
|
927
1191
|
flushIntervalMs: PGLITE_FLUSH_INTERVAL_MS,
|
|
1192
|
+
hostPoolSize: () => resolveHostPoolSize(process.env),
|
|
928
1193
|
logger
|
|
929
1194
|
});
|
|
930
1195
|
const maxSkipThreshold = parseInt(process.env.MAX_SKIP_THRESHOLD ?? "", 10);
|
|
931
1196
|
const hasSkipThreshold = !isNaN(maxSkipThreshold) && maxSkipThreshold > 0;
|
|
932
1197
|
if (hasSkipThreshold) logger.info(`Reactor maxSkipThreshold set to ${maxSkipThreshold}`);
|
|
933
|
-
|
|
934
|
-
|
|
1198
|
+
const { flags: reactorFeatureFlags, enabled: enabledFeatureFlags } = resolveReactorFeatureFlags(process.env);
|
|
1199
|
+
if (enabledFeatureFlags.length > 0) logger.info(`Reactor feature flags enabled: ${enabledFeatureFlags.join(", ")}`);
|
|
1200
|
+
const reactorBuilder = new ReactorBuilder().withEventBus(new EventBus()).withKysely(baseKysely).withFeatures({ legacyProcessorIds: process.env.REACTOR_LEGACY_PROCESSOR_IDS !== "false" });
|
|
1201
|
+
if (poolInstrumentation) reactorBuilder.withInstrumentedPool(poolInstrumentation);
|
|
935
1202
|
const clientBuilder = new ReactorClientBuilder().withReactorBuilder(reactorBuilder);
|
|
936
1203
|
const vetraDocumentModels = dev ? Object.values(await import("@powerhousedao/vetra/document-models")).filter((m) => typeof m === "object" && m !== null && "documentModel" in m && "reducer" in m) : [];
|
|
937
1204
|
applySwitchboardReactorDefaults(reactorBuilder, clientBuilder, {
|
|
938
1205
|
documentModels: [...documentModels, ...vetraDocumentModels],
|
|
939
|
-
|
|
1206
|
+
upgradeManifests,
|
|
1207
|
+
executorConfig: hasSkipThreshold || enabledFeatureFlags.length > 0 ? {
|
|
1208
|
+
...hasSkipThreshold ? { maxSkipThreshold } : {},
|
|
1209
|
+
...enabledFeatureFlags.length > 0 ? { featureFlags: reactorFeatureFlags } : {}
|
|
1210
|
+
} : void 0,
|
|
940
1211
|
documentModelLoader: httpLoader && dynamicModelLoading ? httpLoader.documentModelLoader : void 0,
|
|
941
1212
|
logger: reactorLogger,
|
|
942
1213
|
signer: renown ? getRenownSignerConfig(renown, options.identity?.requireSignatures) : void 0
|
|
@@ -955,6 +1226,10 @@ async function initServer(serverPort, options, renown) {
|
|
|
955
1226
|
await nodeProcessor.init();
|
|
956
1227
|
return nodeProcessor;
|
|
957
1228
|
});
|
|
1229
|
+
registerAttachmentReferenceReadModel(reactorBuilder, {
|
|
1230
|
+
baseKysely,
|
|
1231
|
+
attachmentReferenceWriter
|
|
1232
|
+
});
|
|
958
1233
|
reactorBuilder.withShutdownHook(async () => {
|
|
959
1234
|
if (apiRef.current) await apiRef.current.dispose();
|
|
960
1235
|
});
|
|
@@ -964,14 +1239,37 @@ async function initServer(serverPort, options, renown) {
|
|
|
964
1239
|
reactorLogger.info("Reactor metrics instrumentation started");
|
|
965
1240
|
}
|
|
966
1241
|
driveNodeView = new DriveNodeView(baseKysely.withSchema(REACTOR_SCHEMA));
|
|
1242
|
+
const reactorDriveClient = new ReactorDriveClient({
|
|
1243
|
+
reactor: module.client,
|
|
1244
|
+
readModel: driveNodeView
|
|
1245
|
+
});
|
|
1246
|
+
ownedReactorModule = module;
|
|
967
1247
|
return {
|
|
968
1248
|
module,
|
|
969
|
-
reactorDriveClient
|
|
970
|
-
|
|
971
|
-
readModel: driveNodeView
|
|
972
|
-
})
|
|
1249
|
+
reactorDriveClient,
|
|
1250
|
+
attachmentReferenceProjection: { status: "available" }
|
|
973
1251
|
};
|
|
974
1252
|
};
|
|
1253
|
+
const abortBoot = async (api) => {
|
|
1254
|
+
const owned = ownedReactorModule;
|
|
1255
|
+
if (owned) try {
|
|
1256
|
+
await owned.reactor.kill().completed;
|
|
1257
|
+
} catch (error) {
|
|
1258
|
+
logger.error("Aborting boot: reactor shutdown failed: @error", error);
|
|
1259
|
+
}
|
|
1260
|
+
try {
|
|
1261
|
+
await api.dispose();
|
|
1262
|
+
} catch (error) {
|
|
1263
|
+
logger.error("Aborting boot: api dispose failed: @error", error);
|
|
1264
|
+
}
|
|
1265
|
+
if (owned?.reactorModule) try {
|
|
1266
|
+
await owned.reactorModule.database.destroy();
|
|
1267
|
+
} catch (error) {
|
|
1268
|
+
logger.error("Aborting boot: database destroy failed: @error", error);
|
|
1269
|
+
}
|
|
1270
|
+
};
|
|
1271
|
+
let localCredentialCheck;
|
|
1272
|
+
const verifyCredential = renownConfig.source === "self" ? (params) => localCredentialCheck ? localCredentialCheck(params) : Promise.reject(/* @__PURE__ */ new Error("The local renown read model is not bound yet")) : void 0;
|
|
975
1273
|
let defaultDriveUrl = void 0;
|
|
976
1274
|
const basePath = process.cwd();
|
|
977
1275
|
let vite;
|
|
@@ -997,10 +1295,13 @@ async function initServer(serverPort, options, renown) {
|
|
|
997
1295
|
let pgliteFactory;
|
|
998
1296
|
if (readModelPgliteDir && readModelPgliteMajor !== null) {
|
|
999
1297
|
const { PGlite: ReadModelPGlite } = await loadPGliteModule(readModelPgliteMajor);
|
|
1000
|
-
pgliteFactory = PGLITE_IN_MEMORY ? () => new ReadModelPGlite() : (connectionString) => new ReadModelPGlite({
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1298
|
+
pgliteFactory = PGLITE_IN_MEMORY ? () => new ReadModelPGlite({ parsers: PGLITE_UTC_PARSERS }) : (connectionString) => new ReadModelPGlite({
|
|
1299
|
+
fs: new AtomicNodeFs(connectionString ?? readModelPgliteDir, {
|
|
1300
|
+
logger,
|
|
1301
|
+
flushIntervalMs: PGLITE_FLUSH_INTERVAL_MS
|
|
1302
|
+
}),
|
|
1303
|
+
parsers: PGLITE_UTC_PARSERS
|
|
1304
|
+
});
|
|
1004
1305
|
}
|
|
1005
1306
|
const api = await initializeAndStartAPI(initializeClient, {
|
|
1006
1307
|
port: serverPort,
|
|
@@ -1011,12 +1312,23 @@ async function initServer(serverPort, options, renown) {
|
|
|
1011
1312
|
packages,
|
|
1012
1313
|
processorConfig: options.processorConfig,
|
|
1013
1314
|
processors: vetraProcessorFactory ? { "@powerhousedao/vetra": [vetraProcessorFactory] } : {},
|
|
1014
|
-
configFile:
|
|
1315
|
+
configFile: configPath,
|
|
1015
1316
|
mcp: options.mcp ?? true,
|
|
1016
1317
|
logger: apiLogger,
|
|
1017
|
-
enableDocumentModelSubgraphs: options.enableDocumentModelSubgraphs
|
|
1318
|
+
enableDocumentModelSubgraphs: options.enableDocumentModelSubgraphs,
|
|
1319
|
+
renown: renownConfig,
|
|
1320
|
+
verifyCredential
|
|
1018
1321
|
}, "switchboard");
|
|
1019
1322
|
apiRef.current = api;
|
|
1323
|
+
if (renownConfig.source === "self") {
|
|
1324
|
+
const { graphqlManager: manager } = api;
|
|
1325
|
+
if (!manager.hasSubgraphHandler(RENOWN_READ_MODEL_SUBGRAPH)) {
|
|
1326
|
+
await abortBoot(api);
|
|
1327
|
+
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.`);
|
|
1328
|
+
}
|
|
1329
|
+
localCredentialCheck = createLocalCredentialVerifier((query, variables) => manager.executeSubgraphQuery(RENOWN_READ_MODEL_SUBGRAPH, query, variables), { onError: (error) => logger.error("Renown read model query failed: @error", error) });
|
|
1330
|
+
logger.info("Renown credentials will be verified against this switchboard's own renown read model");
|
|
1331
|
+
}
|
|
1020
1332
|
registerAttachmentRoutes(api);
|
|
1021
1333
|
const attachmentService = createRemoteAttachmentService(deriveAttachmentServiceConfig(options, serverPort, renown));
|
|
1022
1334
|
if (process.env.SENTRY_DSN) api.httpAdapter.setupSentryErrorHandler(Sentry);
|
|
@@ -1026,7 +1338,8 @@ async function initServer(serverPort, options, renown) {
|
|
|
1026
1338
|
const packageManagementService = new PackageManagementService({
|
|
1027
1339
|
defaultRegistryUrl: registryUrl,
|
|
1028
1340
|
httpLoader,
|
|
1029
|
-
documentModelRegistry
|
|
1341
|
+
documentModelRegistry,
|
|
1342
|
+
packageManager: api.packageManager
|
|
1030
1343
|
});
|
|
1031
1344
|
packageManagementService.setOnModelsChanged(() => {
|
|
1032
1345
|
graphqlManager.regenerateDocumentModelSubgraphs().catch(logger.error);
|
|
@@ -1100,6 +1413,7 @@ async function initServer(serverPort, options, renown) {
|
|
|
1100
1413
|
api,
|
|
1101
1414
|
reactor: client,
|
|
1102
1415
|
attachmentService,
|
|
1416
|
+
attachmentReferenceProjection: api.attachmentReferenceProjection,
|
|
1103
1417
|
renown,
|
|
1104
1418
|
port: serverPort,
|
|
1105
1419
|
shutdown: () => api.dispose()
|
|
@@ -1130,9 +1444,11 @@ const startSwitchboard = async (options = {}) => {
|
|
|
1130
1444
|
const enableDocumentModelSubgraphs = await featureFlags.getBooleanValue(DOCUMENT_MODEL_SUBGRAPHS_ENABLED, options.enableDocumentModelSubgraphs ?? DOCUMENT_MODEL_SUBGRAPHS_ENABLED_DEFAULT);
|
|
1131
1445
|
options.enableDocumentModelSubgraphs = enableDocumentModelSubgraphs;
|
|
1132
1446
|
const requireSignatures = options.identity?.requireSignatures ?? await featureFlags.getBooleanValue(REQUIRE_SIGNATURES, REQUIRE_SIGNATURES_DEFAULT);
|
|
1447
|
+
const renownConfig = resolveRenownConfig(getConfig(resolveConfigPath(options.configFile)).auth?.renown, process.env, logger);
|
|
1133
1448
|
options.identity = {
|
|
1134
1449
|
...options.identity,
|
|
1135
|
-
requireSignatures
|
|
1450
|
+
requireSignatures,
|
|
1451
|
+
baseUrl: options.identity?.baseUrl ?? renownConfig.url
|
|
1136
1452
|
};
|
|
1137
1453
|
logger.info("Feature flags: @flags", JSON.stringify({
|
|
1138
1454
|
DOCUMENT_MODEL_SUBGRAPHS_ENABLED: enableDocumentModelSubgraphs,
|
|
@@ -1146,7 +1462,7 @@ const startSwitchboard = async (options = {}) => {
|
|
|
1146
1462
|
if (options.identity.requireExisting) throw new Error("Identity required but failed to initialize. Run \"ph login\" first.", { cause: e });
|
|
1147
1463
|
}
|
|
1148
1464
|
try {
|
|
1149
|
-
return await initServer(serverPort, options, renown);
|
|
1465
|
+
return await initServer(serverPort, options, renown, renownConfig);
|
|
1150
1466
|
} catch (e) {
|
|
1151
1467
|
Sentry.captureException(e);
|
|
1152
1468
|
logger.error("App crashed: @error", e);
|
|
@@ -1157,5 +1473,5 @@ if (import.meta.main) await startSwitchboard();
|
|
|
1157
1473
|
//#endregion
|
|
1158
1474
|
export { parseForcePgVersion as a, applySwitchboardReactorDefaults as i, isPortAvailable as n, startSwitchboard as r, deriveAttachmentServiceConfig as t };
|
|
1159
1475
|
|
|
1160
|
-
//# sourceMappingURL=server-
|
|
1161
|
-
//# debugId=
|
|
1476
|
+
//# sourceMappingURL=server-DYraD11D.mjs.map
|
|
1477
|
+
//# debugId=65ab8387-7c36-5ebd-b971-913ce2e772b3
|