@powerhousedao/switchboard 6.2.2-dev.5 → 6.2.2-dev.50

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]="c23ecfb9-9882-5c12-9555-3c40f7167555")}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,17 @@ 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";
13
+ import { ChannelScheme, DriveCollectionId, EventBus, REACTOR_SCHEMA, ReactorBuilder, ReactorClientBuilder, parseDriveUrl, supportsLiveReadModelRegistration } from "@powerhousedao/reactor";
14
14
  import { HttpPackageLoader, ImportPackageLoader, PackageManagementService, PackagesSubgraph, getUniqueDocumentModels, initializeAndStartAPI } 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
18
  import { Kysely, PostgresDialect } from "kysely";
19
19
  import net from "node:net";
20
20
  import path$1 from "path";
21
21
  import { Pool } from "pg";
22
22
  import { Readable } from "node:stream";
23
+ import { ReactorGroupV1 } from "@powerhousedao/reactor-group";
23
24
  import { driveDocumentModelModule } from "@powerhousedao/shared/document-drive";
24
25
  import { readFile } from "node:fs/promises";
25
26
  import os from "node:os";
@@ -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
  /**
@@ -911,13 +1135,17 @@ async function initServer(serverPort, options, renown) {
911
1135
  });
912
1136
  }
913
1137
  const reactorLogger = logger.child(["reactor"]);
914
- const initializeClient = async (documentModels) => {
1138
+ const initializeClient = async (documentModels, { attachmentReferenceWriter, upgradeManifests }) => {
915
1139
  if (options.reactor) {
1140
+ const attachmentReferenceProjection = await registerAttachmentReferenceReadModelOnModule(options.reactor, attachmentReferenceWriter);
916
1141
  if (options.reactor.reactorModule) {
917
1142
  new ReactorInstrumentation(options.reactor.reactorModule).start();
918
1143
  reactorLogger.info("Reactor metrics instrumentation started (using caller-provided reactor)");
919
1144
  }
920
- return { module: options.reactor };
1145
+ return {
1146
+ module: options.reactor,
1147
+ attachmentReferenceProjection
1148
+ };
921
1149
  }
922
1150
  const baseKysely = await createReactorKysely({
923
1151
  reactorDbUrl,
@@ -930,13 +1158,18 @@ async function initServer(serverPort, options, renown) {
930
1158
  const maxSkipThreshold = parseInt(process.env.MAX_SKIP_THRESHOLD ?? "", 10);
931
1159
  const hasSkipThreshold = !isNaN(maxSkipThreshold) && maxSkipThreshold > 0;
932
1160
  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");
1161
+ const { flags: reactorFeatureFlags, enabled: enabledFeatureFlags } = resolveReactorFeatureFlags(process.env);
1162
+ if (enabledFeatureFlags.length > 0) logger.info(`Reactor feature flags enabled: ${enabledFeatureFlags.join(", ")}`);
934
1163
  const reactorBuilder = new ReactorBuilder().withEventBus(new EventBus()).withKysely(baseKysely);
935
1164
  const clientBuilder = new ReactorClientBuilder().withReactorBuilder(reactorBuilder);
936
1165
  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
1166
  applySwitchboardReactorDefaults(reactorBuilder, clientBuilder, {
938
1167
  documentModels: [...documentModels, ...vetraDocumentModels],
939
- executorConfig: hasSkipThreshold ? { maxSkipThreshold } : void 0,
1168
+ upgradeManifests,
1169
+ executorConfig: hasSkipThreshold || enabledFeatureFlags.length > 0 ? {
1170
+ ...hasSkipThreshold ? { maxSkipThreshold } : {},
1171
+ ...enabledFeatureFlags.length > 0 ? { featureFlags: reactorFeatureFlags } : {}
1172
+ } : void 0,
940
1173
  documentModelLoader: httpLoader && dynamicModelLoading ? httpLoader.documentModelLoader : void 0,
941
1174
  logger: reactorLogger,
942
1175
  signer: renown ? getRenownSignerConfig(renown, options.identity?.requireSignatures) : void 0
@@ -955,6 +1188,10 @@ async function initServer(serverPort, options, renown) {
955
1188
  await nodeProcessor.init();
956
1189
  return nodeProcessor;
957
1190
  });
1191
+ registerAttachmentReferenceReadModel(reactorBuilder, {
1192
+ baseKysely,
1193
+ attachmentReferenceWriter
1194
+ });
958
1195
  reactorBuilder.withShutdownHook(async () => {
959
1196
  if (apiRef.current) await apiRef.current.dispose();
960
1197
  });
@@ -969,7 +1206,8 @@ async function initServer(serverPort, options, renown) {
969
1206
  reactorDriveClient: new ReactorDriveClient({
970
1207
  reactor: module.client,
971
1208
  readModel: driveNodeView
972
- })
1209
+ }),
1210
+ attachmentReferenceProjection: { status: "available" }
973
1211
  };
974
1212
  };
975
1213
  let defaultDriveUrl = void 0;
@@ -1100,6 +1338,7 @@ async function initServer(serverPort, options, renown) {
1100
1338
  api,
1101
1339
  reactor: client,
1102
1340
  attachmentService,
1341
+ attachmentReferenceProjection: api.attachmentReferenceProjection,
1103
1342
  renown,
1104
1343
  port: serverPort,
1105
1344
  shutdown: () => api.dispose()
@@ -1157,5 +1396,5 @@ if (import.meta.main) await startSwitchboard();
1157
1396
  //#endregion
1158
1397
  export { parseForcePgVersion as a, applySwitchboardReactorDefaults as i, isPortAvailable as n, startSwitchboard as r, deriveAttachmentServiceConfig as t };
1159
1398
 
1160
- //# sourceMappingURL=server-Cgud_ONM.mjs.map
1161
- //# debugId=5eb0a978-689a-51dd-978a-cd3d7fe5df4b
1399
+ //# sourceMappingURL=server-By1-k-B6.mjs.map
1400
+ //# debugId=c23ecfb9-9882-5c12-9555-3c40f7167555