@powerhousedao/switchboard 6.2.2-dev.7 → 6.2.2-dev.70

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.
@@ -1,5 +1,5 @@
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]="5eb0a978-689a-51dd-978a-cd3d7fe5df4b")}catch(e){}}();
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]="837d5ec3-d43b-5b2e-bd46-c200dffdf8de")}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
5
  import * as Sentry from "@sentry/node";
@@ -10,16 +10,18 @@ import { existsSync, promises, realpathSync } from "node:fs";
10
10
  import path from "node:path";
11
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, 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), `requireAuth` returns the
105
- * handler unchanged that is the only way to opt out. To register a route
106
- * without auth wrapping you must call `api.httpAdapter.mountNodeRoute` directly.
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({ maxSkipThreshold: options.executorConfig.maxSkipThreshold });
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);
@@ -723,6 +927,26 @@ async function rollback(dataDir, backupDir, originalError, logger) {
723
927
  logger.error(`[pglite-migration] Migration failed for ${dataDir}; rolled back from ${backupDir}. Original error: ${String(originalError)}`);
724
928
  }
725
929
  //#endregion
930
+ //#region src/reactor-feature-flags.mts
931
+ /**
932
+ * Enforcement flags from the REACTOR_* env vars. Each flag requires the ones
933
+ * before it; the reactor rejects an inconsistent set rather than enforcing less
934
+ * than the operator asked for, so this reports what was asked without
935
+ * correcting it.
936
+ */
937
+ function resolveReactorFeatureFlags(env) {
938
+ const flags = {
939
+ documentDecisions: env.REACTOR_DOCUMENT_DECISIONS === "true",
940
+ authEnforcement: env.REACTOR_AUTH_ENFORCEMENT === "true",
941
+ authGroups: env.REACTOR_AUTH_GROUPS === "true",
942
+ authConditions: env.REACTOR_AUTH_CONDITIONS === "true"
943
+ };
944
+ return {
945
+ flags,
946
+ enabled: Object.entries(flags).filter(([, isEnabled]) => isEnabled).map(([name]) => name)
947
+ };
948
+ }
949
+ //#endregion
726
950
  //#region src/renown.ts
727
951
  const logger = childLogger(["switchboard", "renown"]);
728
952
  /**
@@ -793,6 +1017,10 @@ function isPortAvailable(port) {
793
1017
  });
794
1018
  });
795
1019
  }
1020
+ /** The powerhouse.config.json this run reads, defaulting to the cwd copy. */
1021
+ function resolveConfigPath(configFile) {
1022
+ return configFile ?? path$1.join(process.cwd(), "powerhouse.config.json");
1023
+ }
796
1024
  async function resolveServerPort(requested, strictPort, logger) {
797
1025
  if (strictPort) return requested;
798
1026
  for (let i = 0; i < PORT_FALLBACK_ATTEMPTS; i++) {
@@ -831,7 +1059,7 @@ function deriveAttachmentServiceConfig(options, serverPort, renown) {
831
1059
  }) : void 0 : void 0
832
1060
  };
833
1061
  }
834
- async function initServer(serverPort, options, renown) {
1062
+ async function initServer(serverPort, options, renown, renownConfig) {
835
1063
  const { dev, packages = [], remoteDrives = [], logger = defaultLogger } = options;
836
1064
  logger.level = LogLevel;
837
1065
  const dbPath = options.dbPath ?? process.env.DATABASE_URL ?? process.env.PH_SWITCHBOARD_DATABASE_URL;
@@ -897,7 +1125,8 @@ async function initServer(serverPort, options, renown) {
897
1125
  }
898
1126
  const apiRef = { current: void 0 };
899
1127
  let driveNodeView;
900
- const config = getConfig(options.configFile ?? path$1.join(process.cwd(), "powerhouse.config.json"));
1128
+ const configPath = resolveConfigPath(options.configFile);
1129
+ const config = getConfig(configPath);
901
1130
  const registryUrl = options.registryUrl ?? process.env.PH_REGISTRY_URL ?? config.packageRegistryUrl;
902
1131
  const registryPackages = process.env.PH_REGISTRY_PACKAGES;
903
1132
  const dynamicModelLoading = options.dynamicModelLoading ?? process.env.DYNAMIC_MODEL_LOADING === "true";
@@ -911,13 +1140,18 @@ async function initServer(serverPort, options, renown) {
911
1140
  });
912
1141
  }
913
1142
  const reactorLogger = logger.child(["reactor"]);
914
- const initializeClient = async (documentModels) => {
1143
+ let ownedReactorModule;
1144
+ const initializeClient = async (documentModels, { attachmentReferenceWriter, upgradeManifests }) => {
915
1145
  if (options.reactor) {
1146
+ const attachmentReferenceProjection = await registerAttachmentReferenceReadModelOnModule(options.reactor, attachmentReferenceWriter);
916
1147
  if (options.reactor.reactorModule) {
917
1148
  new ReactorInstrumentation(options.reactor.reactorModule).start();
918
1149
  reactorLogger.info("Reactor metrics instrumentation started (using caller-provided reactor)");
919
1150
  }
920
- return { module: options.reactor };
1151
+ return {
1152
+ module: options.reactor,
1153
+ attachmentReferenceProjection
1154
+ };
921
1155
  }
922
1156
  const baseKysely = await createReactorKysely({
923
1157
  reactorDbUrl,
@@ -930,13 +1164,18 @@ async function initServer(serverPort, options, renown) {
930
1164
  const maxSkipThreshold = parseInt(process.env.MAX_SKIP_THRESHOLD ?? "", 10);
931
1165
  const hasSkipThreshold = !isNaN(maxSkipThreshold) && maxSkipThreshold > 0;
932
1166
  if (hasSkipThreshold) logger.info(`Reactor maxSkipThreshold set to ${maxSkipThreshold}`);
933
- if (hasSkipThreshold && workerPool) logger.warn("MAX_SKIP_THRESHOLD is not forwarded to executor workers and has no effect in worker-pool mode");
934
- const reactorBuilder = new ReactorBuilder().withEventBus(new EventBus()).withKysely(baseKysely);
1167
+ const { flags: reactorFeatureFlags, enabled: enabledFeatureFlags } = resolveReactorFeatureFlags(process.env);
1168
+ if (enabledFeatureFlags.length > 0) logger.info(`Reactor feature flags enabled: ${enabledFeatureFlags.join(", ")}`);
1169
+ const reactorBuilder = new ReactorBuilder().withEventBus(new EventBus()).withKysely(baseKysely).withFeatures({ legacyProcessorIds: process.env.REACTOR_LEGACY_PROCESSOR_IDS !== "false" });
935
1170
  const clientBuilder = new ReactorClientBuilder().withReactorBuilder(reactorBuilder);
936
1171
  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
1172
  applySwitchboardReactorDefaults(reactorBuilder, clientBuilder, {
938
1173
  documentModels: [...documentModels, ...vetraDocumentModels],
939
- executorConfig: hasSkipThreshold ? { maxSkipThreshold } : void 0,
1174
+ upgradeManifests,
1175
+ executorConfig: hasSkipThreshold || enabledFeatureFlags.length > 0 ? {
1176
+ ...hasSkipThreshold ? { maxSkipThreshold } : {},
1177
+ ...enabledFeatureFlags.length > 0 ? { featureFlags: reactorFeatureFlags } : {}
1178
+ } : void 0,
940
1179
  documentModelLoader: httpLoader && dynamicModelLoading ? httpLoader.documentModelLoader : void 0,
941
1180
  logger: reactorLogger,
942
1181
  signer: renown ? getRenownSignerConfig(renown, options.identity?.requireSignatures) : void 0
@@ -955,6 +1194,10 @@ async function initServer(serverPort, options, renown) {
955
1194
  await nodeProcessor.init();
956
1195
  return nodeProcessor;
957
1196
  });
1197
+ registerAttachmentReferenceReadModel(reactorBuilder, {
1198
+ baseKysely,
1199
+ attachmentReferenceWriter
1200
+ });
958
1201
  reactorBuilder.withShutdownHook(async () => {
959
1202
  if (apiRef.current) await apiRef.current.dispose();
960
1203
  });
@@ -964,14 +1207,37 @@ async function initServer(serverPort, options, renown) {
964
1207
  reactorLogger.info("Reactor metrics instrumentation started");
965
1208
  }
966
1209
  driveNodeView = new DriveNodeView(baseKysely.withSchema(REACTOR_SCHEMA));
1210
+ const reactorDriveClient = new ReactorDriveClient({
1211
+ reactor: module.client,
1212
+ readModel: driveNodeView
1213
+ });
1214
+ ownedReactorModule = module;
967
1215
  return {
968
1216
  module,
969
- reactorDriveClient: new ReactorDriveClient({
970
- reactor: module.client,
971
- readModel: driveNodeView
972
- })
1217
+ reactorDriveClient,
1218
+ attachmentReferenceProjection: { status: "available" }
973
1219
  };
974
1220
  };
1221
+ const abortBoot = async (api) => {
1222
+ const owned = ownedReactorModule;
1223
+ if (owned) try {
1224
+ await owned.reactor.kill().completed;
1225
+ } catch (error) {
1226
+ logger.error("Aborting boot: reactor shutdown failed: @error", error);
1227
+ }
1228
+ try {
1229
+ await api.dispose();
1230
+ } catch (error) {
1231
+ logger.error("Aborting boot: api dispose failed: @error", error);
1232
+ }
1233
+ if (owned?.reactorModule) try {
1234
+ await owned.reactorModule.database.destroy();
1235
+ } catch (error) {
1236
+ logger.error("Aborting boot: database destroy failed: @error", error);
1237
+ }
1238
+ };
1239
+ let localCredentialCheck;
1240
+ 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
1241
  let defaultDriveUrl = void 0;
976
1242
  const basePath = process.cwd();
977
1243
  let vite;
@@ -997,10 +1263,13 @@ async function initServer(serverPort, options, renown) {
997
1263
  let pgliteFactory;
998
1264
  if (readModelPgliteDir && readModelPgliteMajor !== null) {
999
1265
  const { PGlite: ReadModelPGlite } = await loadPGliteModule(readModelPgliteMajor);
1000
- pgliteFactory = PGLITE_IN_MEMORY ? () => new ReadModelPGlite() : (connectionString) => new ReadModelPGlite({ fs: new AtomicNodeFs(connectionString ?? readModelPgliteDir, {
1001
- logger,
1002
- flushIntervalMs: PGLITE_FLUSH_INTERVAL_MS
1003
- }) });
1266
+ pgliteFactory = PGLITE_IN_MEMORY ? () => new ReadModelPGlite({ parsers: PGLITE_UTC_PARSERS }) : (connectionString) => new ReadModelPGlite({
1267
+ fs: new AtomicNodeFs(connectionString ?? readModelPgliteDir, {
1268
+ logger,
1269
+ flushIntervalMs: PGLITE_FLUSH_INTERVAL_MS
1270
+ }),
1271
+ parsers: PGLITE_UTC_PARSERS
1272
+ });
1004
1273
  }
1005
1274
  const api = await initializeAndStartAPI(initializeClient, {
1006
1275
  port: serverPort,
@@ -1011,12 +1280,23 @@ async function initServer(serverPort, options, renown) {
1011
1280
  packages,
1012
1281
  processorConfig: options.processorConfig,
1013
1282
  processors: vetraProcessorFactory ? { "@powerhousedao/vetra": [vetraProcessorFactory] } : {},
1014
- configFile: options.configFile ?? path$1.join(process.cwd(), "powerhouse.config.json"),
1283
+ configFile: configPath,
1015
1284
  mcp: options.mcp ?? true,
1016
1285
  logger: apiLogger,
1017
- enableDocumentModelSubgraphs: options.enableDocumentModelSubgraphs
1286
+ enableDocumentModelSubgraphs: options.enableDocumentModelSubgraphs,
1287
+ renown: renownConfig,
1288
+ verifyCredential
1018
1289
  }, "switchboard");
1019
1290
  apiRef.current = api;
1291
+ if (renownConfig.source === "self") {
1292
+ const { graphqlManager: manager } = api;
1293
+ if (!manager.hasSubgraphHandler(RENOWN_READ_MODEL_SUBGRAPH)) {
1294
+ await abortBoot(api);
1295
+ 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.`);
1296
+ }
1297
+ localCredentialCheck = createLocalCredentialVerifier((query, variables) => manager.executeSubgraphQuery(RENOWN_READ_MODEL_SUBGRAPH, query, variables), { onError: (error) => logger.error("Renown read model query failed: @error", error) });
1298
+ logger.info("Renown credentials will be verified against this switchboard's own renown read model");
1299
+ }
1020
1300
  registerAttachmentRoutes(api);
1021
1301
  const attachmentService = createRemoteAttachmentService(deriveAttachmentServiceConfig(options, serverPort, renown));
1022
1302
  if (process.env.SENTRY_DSN) api.httpAdapter.setupSentryErrorHandler(Sentry);
@@ -1100,6 +1380,7 @@ async function initServer(serverPort, options, renown) {
1100
1380
  api,
1101
1381
  reactor: client,
1102
1382
  attachmentService,
1383
+ attachmentReferenceProjection: api.attachmentReferenceProjection,
1103
1384
  renown,
1104
1385
  port: serverPort,
1105
1386
  shutdown: () => api.dispose()
@@ -1130,9 +1411,11 @@ const startSwitchboard = async (options = {}) => {
1130
1411
  const enableDocumentModelSubgraphs = await featureFlags.getBooleanValue(DOCUMENT_MODEL_SUBGRAPHS_ENABLED, options.enableDocumentModelSubgraphs ?? DOCUMENT_MODEL_SUBGRAPHS_ENABLED_DEFAULT);
1131
1412
  options.enableDocumentModelSubgraphs = enableDocumentModelSubgraphs;
1132
1413
  const requireSignatures = options.identity?.requireSignatures ?? await featureFlags.getBooleanValue(REQUIRE_SIGNATURES, REQUIRE_SIGNATURES_DEFAULT);
1414
+ const renownConfig = resolveRenownConfig(getConfig(resolveConfigPath(options.configFile)).auth?.renown, process.env, logger);
1133
1415
  options.identity = {
1134
1416
  ...options.identity,
1135
- requireSignatures
1417
+ requireSignatures,
1418
+ baseUrl: options.identity?.baseUrl ?? renownConfig.url
1136
1419
  };
1137
1420
  logger.info("Feature flags: @flags", JSON.stringify({
1138
1421
  DOCUMENT_MODEL_SUBGRAPHS_ENABLED: enableDocumentModelSubgraphs,
@@ -1146,7 +1429,7 @@ const startSwitchboard = async (options = {}) => {
1146
1429
  if (options.identity.requireExisting) throw new Error("Identity required but failed to initialize. Run \"ph login\" first.", { cause: e });
1147
1430
  }
1148
1431
  try {
1149
- return await initServer(serverPort, options, renown);
1432
+ return await initServer(serverPort, options, renown, renownConfig);
1150
1433
  } catch (e) {
1151
1434
  Sentry.captureException(e);
1152
1435
  logger.error("App crashed: @error", e);
@@ -1157,5 +1440,5 @@ if (import.meta.main) await startSwitchboard();
1157
1440
  //#endregion
1158
1441
  export { parseForcePgVersion as a, applySwitchboardReactorDefaults as i, isPortAvailable as n, startSwitchboard as r, deriveAttachmentServiceConfig as t };
1159
1442
 
1160
- //# sourceMappingURL=server-Cgud_ONM.mjs.map
1161
- //# debugId=5eb0a978-689a-51dd-978a-cd3d7fe5df4b
1443
+ //# sourceMappingURL=server-CQKVIxsA.mjs.map
1444
+ //# debugId=837d5ec3-d43b-5b2e-bd46-c200dffdf8de