@powerhousedao/switchboard 6.2.2-dev.16 → 6.2.2-dev.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.tsbuild/test/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/tsconfig.tsbuildinfo +1 -1
- package/CHANGELOG.md +10 -0
- package/dist/index.mjs +1 -1
- package/dist/{server-Cgud_ONM.mjs → server-ChK0GLG8.mjs} +228 -20
- package/dist/server-ChK0GLG8.mjs.map +1 -0
- package/dist/server.d.mts +3 -1
- package/dist/server.d.mts.map +1 -1
- package/dist/server.mjs +3 -3
- package/package.json +12 -12
- 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 +445 -0
- package/test/attachments/index.test.ts +37 -2
- package/test/attachments/routes-integration.test.ts +98 -1
- package/dist/server-Cgud_ONM.mjs.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
## 6.2.2-dev.17 (2026-07-26)
|
|
2
|
+
|
|
3
|
+
### 🚀 Features
|
|
4
|
+
|
|
5
|
+
- **attachments:** S3-compatible storage, direct transfers, and document-derived authorization ([#2884](https://github.com/powerhouse-inc/powerhouse/pull/2884))
|
|
6
|
+
|
|
7
|
+
### ❤️ Thank You
|
|
8
|
+
|
|
9
|
+
- Guillermo Puente Sandoval @gpuente
|
|
10
|
+
|
|
1
11
|
## 6.2.2-dev.16 (2026-07-25)
|
|
2
12
|
|
|
3
13
|
### 🩹 Fixes
|
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
!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]="5a65c537-fadb-5705-95c0-4cec44d7b4d2")}catch(e){}}();
|
|
4
|
-
import { a as parseForcePgVersion, r as startSwitchboard } from "./server-
|
|
4
|
+
import { a as parseForcePgVersion, r as startSwitchboard } from "./server-ChK0GLG8.mjs";
|
|
5
5
|
import "./utils-Baw7rThP.mjs";
|
|
6
6
|
import { metrics } from "@opentelemetry/api";
|
|
7
7
|
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
|
|
@@ -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]="
|
|
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]="b1867ed0-059f-587c-9a5f-677da3666de7")}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,10 +10,10 @@ 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";
|
|
@@ -64,13 +64,20 @@ async function loadPgDump(major) {
|
|
|
64
64
|
}
|
|
65
65
|
//#endregion
|
|
66
66
|
//#region src/attachments/auth.ts
|
|
67
|
+
const ANONYMOUS_ACTOR = {
|
|
68
|
+
user: void 0,
|
|
69
|
+
authEnabled: false
|
|
70
|
+
};
|
|
67
71
|
/**
|
|
68
72
|
* Wrap a Node-style handler so that, when `authService` is provided and auth is
|
|
69
|
-
* enabled, the request must carry a verifiable Bearer token.
|
|
73
|
+
* enabled, the request must carry a verifiable Bearer token. The handler always
|
|
74
|
+
* receives an actor context: the verified bearer user when auth is enabled, or
|
|
75
|
+
* the anonymous context when it is disabled. With `allowAnonymous`, a missing
|
|
76
|
+
* bearer yields an anonymous actor with `authEnabled: true` instead of a 401.
|
|
70
77
|
*/
|
|
71
|
-
function requireAuth(authService, handler) {
|
|
72
|
-
if (!authService) return handler;
|
|
73
|
-
return async (req, res) => {
|
|
78
|
+
function requireAuth(authService, handler, options) {
|
|
79
|
+
if (!authService) return (req, res, body) => handler(req, res, body, ANONYMOUS_ACTOR);
|
|
80
|
+
return async (req, res, body) => {
|
|
74
81
|
let result;
|
|
75
82
|
try {
|
|
76
83
|
result = await authService.verifyBearer(req.headers.authorization);
|
|
@@ -88,25 +95,32 @@ function requireAuth(authService, handler) {
|
|
|
88
95
|
res.end(body);
|
|
89
96
|
return;
|
|
90
97
|
}
|
|
91
|
-
if (result.auth_enabled && !result.user) {
|
|
98
|
+
if (result.auth_enabled && !result.user && !options?.allowAnonymous) {
|
|
92
99
|
res.statusCode = 401;
|
|
93
100
|
res.setHeader("Content-Type", "application/json");
|
|
94
101
|
res.end(JSON.stringify({ error: "Authentication required" }));
|
|
95
102
|
return;
|
|
96
103
|
}
|
|
97
|
-
await handler(req, res
|
|
104
|
+
await handler(req, res, body, {
|
|
105
|
+
user: result.user,
|
|
106
|
+
authEnabled: result.auth_enabled
|
|
107
|
+
});
|
|
98
108
|
};
|
|
99
109
|
}
|
|
100
110
|
//#endregion
|
|
101
111
|
//#region src/attachments/mount-auth.ts
|
|
102
112
|
/**
|
|
103
113
|
* Mount a Node-style attachment route with `requireAuth` applied unconditionally.
|
|
104
|
-
* When `api.authService` is undefined (auth disabled),
|
|
105
|
-
*
|
|
106
|
-
*
|
|
114
|
+
* When `api.authService` is undefined (auth disabled), the handler still runs
|
|
115
|
+
* through `requireAuth` and receives the anonymous actor context — that is the
|
|
116
|
+
* only way to opt out of bearer verification. To register a route without auth
|
|
117
|
+
* wrapping you must call `api.httpAdapter.mountNodeRoute` directly.
|
|
118
|
+
*
|
|
119
|
+
* `allowAnonymous` is reserved for routes whose handlers make a per-document
|
|
120
|
+
* authorization decision themselves; identity-only routes must not use it.
|
|
107
121
|
*/
|
|
108
|
-
function mountAuthenticatedNodeRoute(api, method, path, handler) {
|
|
109
|
-
api.httpAdapter.mountNodeRoute(method, path, requireAuth(api.authService, handler));
|
|
122
|
+
function mountAuthenticatedNodeRoute(api, method, path, handler, options) {
|
|
123
|
+
api.httpAdapter.mountNodeRoute(method, path, requireAuth(api.authService, handler, options));
|
|
110
124
|
}
|
|
111
125
|
//#endregion
|
|
112
126
|
//#region src/attachments/routes.ts
|
|
@@ -216,7 +230,8 @@ function makeReserveHandler(attachments) {
|
|
|
216
230
|
sendJson(res, 201, {
|
|
217
231
|
reservationId: upload.reservationId,
|
|
218
232
|
ref: upload.ref,
|
|
219
|
-
expiresAtUtc: upload.expiresAtUtc
|
|
233
|
+
expiresAtUtc: upload.expiresAtUtc,
|
|
234
|
+
...upload.uploadTarget ? { uploadTarget: upload.uploadTarget } : {}
|
|
220
235
|
});
|
|
221
236
|
};
|
|
222
237
|
}
|
|
@@ -227,6 +242,10 @@ function makeUploadHandler(attachments) {
|
|
|
227
242
|
sendError(res, 400, "Missing reservationId");
|
|
228
243
|
return;
|
|
229
244
|
}
|
|
245
|
+
if (attachments.backend?.kind === "s3") {
|
|
246
|
+
sendError(res, 405, "Use the reservation uploadTarget for S3 uploads");
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
230
249
|
let reservation;
|
|
231
250
|
try {
|
|
232
251
|
reservation = await attachments.reservations.get(reservationId);
|
|
@@ -373,6 +392,144 @@ function makeDeleteReservationHandler(attachments) {
|
|
|
373
392
|
function extractParam(req, name) {
|
|
374
393
|
return req.params?.[name];
|
|
375
394
|
}
|
|
395
|
+
const MAX_DOCUMENT_ID_LEN = 512;
|
|
396
|
+
const ATTACHMENT_NOT_FOUND_BODY = { error: "Attachment not found" };
|
|
397
|
+
const MAX_DOWNLOAD_TARGET_TTL_SECONDS = 10080 * 60;
|
|
398
|
+
/**
|
|
399
|
+
* Returns the single `documentId` query value, or null when it is missing,
|
|
400
|
+
* duplicated, blank, or oversized. Validation happens before authorization so
|
|
401
|
+
* malformed requests never reach the access service.
|
|
402
|
+
*/
|
|
403
|
+
function extractSingleDocumentId(req) {
|
|
404
|
+
if (!req.url) return null;
|
|
405
|
+
let url;
|
|
406
|
+
try {
|
|
407
|
+
url = new URL(req.url, "http://switchboard.invalid");
|
|
408
|
+
} catch {
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
const values = url.searchParams.getAll("documentId");
|
|
412
|
+
if (values.length !== 1) return null;
|
|
413
|
+
const value = values[0];
|
|
414
|
+
if (value.trim().length === 0 || value.length > MAX_DOCUMENT_ID_LEN) return null;
|
|
415
|
+
return value;
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Returns the requested target TTL in seconds: undefined when absent,
|
|
419
|
+
* "invalid" when malformed (duplicated, non-integer, or non-positive), and
|
|
420
|
+
* otherwise the value clamped to the presigning ceiling.
|
|
421
|
+
*/
|
|
422
|
+
function extractExpiresIn(req) {
|
|
423
|
+
if (!req.url) return void 0;
|
|
424
|
+
let url;
|
|
425
|
+
try {
|
|
426
|
+
url = new URL(req.url, "http://switchboard.invalid");
|
|
427
|
+
} catch {
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
const values = url.searchParams.getAll("expiresIn");
|
|
431
|
+
if (values.length === 0) return void 0;
|
|
432
|
+
if (values.length > 1) return "invalid";
|
|
433
|
+
const parsed = Number(values[0]);
|
|
434
|
+
if (!Number.isInteger(parsed) || parsed <= 0) return "invalid";
|
|
435
|
+
return Math.min(parsed, MAX_DOWNLOAD_TARGET_TTL_SECONDS);
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Base URL of this Switchboard as seen by the caller, used to build
|
|
439
|
+
* filesystem `switchboard` download targets that point back at the existing
|
|
440
|
+
* authenticated byte route.
|
|
441
|
+
*/
|
|
442
|
+
function requestBaseUrl(req) {
|
|
443
|
+
const forwardedProto = req.headers["x-forwarded-proto"];
|
|
444
|
+
const proto = (Array.isArray(forwardedProto) ? forwardedProto[0] : forwardedProto)?.split(",")[0]?.trim() || (req.socket.encrypted ? "https" : "http");
|
|
445
|
+
const forwardedHost = req.headers["x-forwarded-host"];
|
|
446
|
+
const host = (Array.isArray(forwardedHost) ? forwardedHost[0] : forwardedHost) ?? req.headers.host;
|
|
447
|
+
if (!host) return null;
|
|
448
|
+
return `${proto}://${host}`;
|
|
449
|
+
}
|
|
450
|
+
function makeDownloadTargetHandler(attachments, attachmentAccess) {
|
|
451
|
+
return async (req, res, _body, actor) => {
|
|
452
|
+
res.setHeader("Cache-Control", "no-store");
|
|
453
|
+
const hash = extractParam(req, "hash");
|
|
454
|
+
if (!hash || !HASH_PATTERN.test(hash)) {
|
|
455
|
+
sendError(res, 400, "Invalid attachment hash");
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
const documentId = extractSingleDocumentId(req);
|
|
459
|
+
if (documentId === null) {
|
|
460
|
+
sendError(res, 400, "documentId is required exactly once as a non-empty query parameter");
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
const expiresIn = extractExpiresIn(req);
|
|
464
|
+
if (expiresIn === "invalid") {
|
|
465
|
+
sendError(res, 400, "expiresIn must be a single positive integer number of seconds");
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
const canonicalHash = hash.toLowerCase();
|
|
469
|
+
let decision;
|
|
470
|
+
try {
|
|
471
|
+
decision = await attachmentAccess.canReadAttachment({
|
|
472
|
+
documentId,
|
|
473
|
+
attachmentRef: createRef(canonicalHash),
|
|
474
|
+
userAddress: actor?.user?.address
|
|
475
|
+
});
|
|
476
|
+
} catch (err) {
|
|
477
|
+
logger$1.error("Attachment access decision failed: @error", err);
|
|
478
|
+
sendError(res, 500, "Internal error");
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
if (decision.kind === "projection-unavailable") {
|
|
482
|
+
sendError(res, 503, "Attachment downloads are temporarily unavailable");
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
if (decision.kind === "denied") {
|
|
486
|
+
sendJson(res, 404, ATTACHMENT_NOT_FOUND_BODY);
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
let header;
|
|
490
|
+
try {
|
|
491
|
+
header = await attachments.store.stat(canonicalHash);
|
|
492
|
+
} catch (err) {
|
|
493
|
+
if (err instanceof AttachmentNotFound) {
|
|
494
|
+
sendJson(res, 404, ATTACHMENT_NOT_FOUND_BODY);
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
logger$1.error("Attachment metadata lookup failed: @error", err);
|
|
498
|
+
sendError(res, 500, "Internal error");
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
if (header.status !== "available") {
|
|
502
|
+
sendJson(res, 404, ATTACHMENT_NOT_FOUND_BODY);
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
let target;
|
|
506
|
+
if (attachments.backend && attachments.backend.kind !== "filesystem") try {
|
|
507
|
+
target = await attachments.backend.prepareDownloadTarget(canonicalHash, expiresIn);
|
|
508
|
+
} catch {
|
|
509
|
+
sendError(res, 502, "Attachment download target unavailable");
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
else {
|
|
513
|
+
const base = requestBaseUrl(req);
|
|
514
|
+
if (!base) {
|
|
515
|
+
sendError(res, 500, "Internal error");
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
try {
|
|
519
|
+
target = parseAttachmentDownloadTarget({
|
|
520
|
+
kind: "switchboard",
|
|
521
|
+
method: "GET",
|
|
522
|
+
url: `${base}/attachments/${canonicalHash}`,
|
|
523
|
+
headers: {}
|
|
524
|
+
});
|
|
525
|
+
} catch {
|
|
526
|
+
sendError(res, 500, "Internal error");
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
sendJson(res, 200, target);
|
|
531
|
+
};
|
|
532
|
+
}
|
|
376
533
|
//#endregion
|
|
377
534
|
//#region src/attachments/index.ts
|
|
378
535
|
function registerAttachmentRoutes(api) {
|
|
@@ -382,9 +539,50 @@ function registerAttachmentRoutes(api) {
|
|
|
382
539
|
mountAuthenticatedNodeRoute(api, "DELETE", "/attachments/reservations/:reservationId", makeDeleteReservationHandler(attachments));
|
|
383
540
|
mountAuthenticatedNodeRoute(api, "PUT", "/attachments/reservations/:reservationId", makeUploadHandler(attachments));
|
|
384
541
|
mountAuthenticatedNodeRoute(api, "HEAD", "/attachments/:hash", makeStatHandler(attachments));
|
|
542
|
+
mountAuthenticatedNodeRoute(api, "GET", "/attachments/:hash/download-target", makeDownloadTargetHandler(attachments, api.attachmentAccess), { allowAnonymous: true });
|
|
385
543
|
mountAuthenticatedNodeRoute(api, "GET", "/attachments/:hash", makeDownloadHandler(attachments));
|
|
386
544
|
}
|
|
387
545
|
//#endregion
|
|
546
|
+
//#region src/attachment-reference-read-model.mts
|
|
547
|
+
const attachmentSchemaCompiler = new AttachmentSchemaCompiler();
|
|
548
|
+
function createAttachmentReferenceReadModel(baseKysely, dependencies, attachmentReferenceWriter) {
|
|
549
|
+
return new AttachmentReferenceReadModel(baseKysely.withSchema(REACTOR_SCHEMA), dependencies.operationIndex, dependencies.writeCache, dependencies.processorManagerConsistencyTracker, dependencies.documentModelRegistry, attachmentSchemaCompiler, attachmentReferenceWriter);
|
|
550
|
+
}
|
|
551
|
+
function registerAttachmentReferenceReadModel(reactorBuilder, registration) {
|
|
552
|
+
reactorBuilder.withReadModelFactory(async ({ documentModelRegistry, operationIndex, writeCache, processorManagerConsistencyTracker }) => {
|
|
553
|
+
const readModel = createAttachmentReferenceReadModel(registration.baseKysely, {
|
|
554
|
+
operationIndex,
|
|
555
|
+
writeCache,
|
|
556
|
+
processorManagerConsistencyTracker,
|
|
557
|
+
documentModelRegistry
|
|
558
|
+
}, registration.attachmentReferenceWriter);
|
|
559
|
+
await readModel.init();
|
|
560
|
+
return readModel;
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
async function registerAttachmentReferenceReadModelOnModule(clientModule, attachmentReferenceWriter) {
|
|
564
|
+
const reactorModule = clientModule.reactorModule;
|
|
565
|
+
if (!reactorModule) return {
|
|
566
|
+
status: "unavailable",
|
|
567
|
+
reason: "in-process-reactor-module-unavailable"
|
|
568
|
+
};
|
|
569
|
+
const coordinator = reactorModule.readModelCoordinator;
|
|
570
|
+
if (!supportsLiveReadModelRegistration(coordinator)) return {
|
|
571
|
+
status: "unavailable",
|
|
572
|
+
reason: "live-read-model-registration-unsupported"
|
|
573
|
+
};
|
|
574
|
+
const readModel = createAttachmentReferenceReadModel(reactorModule.database, {
|
|
575
|
+
operationIndex: reactorModule.operationIndex,
|
|
576
|
+
writeCache: reactorModule.writeCache,
|
|
577
|
+
processorManagerConsistencyTracker: reactorModule.processorManagerConsistencyTracker,
|
|
578
|
+
documentModelRegistry: reactorModule.documentModelRegistry
|
|
579
|
+
}, attachmentReferenceWriter);
|
|
580
|
+
await readModel.init();
|
|
581
|
+
coordinator.addReadModel(readModel, "pre_ready");
|
|
582
|
+
await readModel.init();
|
|
583
|
+
return { status: "available" };
|
|
584
|
+
}
|
|
585
|
+
//#endregion
|
|
388
586
|
//#region src/builder-defaults.mts
|
|
389
587
|
/**
|
|
390
588
|
* Apply switchboard's standard configuration to a reactor + client builder
|
|
@@ -911,13 +1109,17 @@ async function initServer(serverPort, options, renown) {
|
|
|
911
1109
|
});
|
|
912
1110
|
}
|
|
913
1111
|
const reactorLogger = logger.child(["reactor"]);
|
|
914
|
-
const initializeClient = async (documentModels) => {
|
|
1112
|
+
const initializeClient = async (documentModels, { attachmentReferenceWriter }) => {
|
|
915
1113
|
if (options.reactor) {
|
|
1114
|
+
const attachmentReferenceProjection = await registerAttachmentReferenceReadModelOnModule(options.reactor, attachmentReferenceWriter);
|
|
916
1115
|
if (options.reactor.reactorModule) {
|
|
917
1116
|
new ReactorInstrumentation(options.reactor.reactorModule).start();
|
|
918
1117
|
reactorLogger.info("Reactor metrics instrumentation started (using caller-provided reactor)");
|
|
919
1118
|
}
|
|
920
|
-
return {
|
|
1119
|
+
return {
|
|
1120
|
+
module: options.reactor,
|
|
1121
|
+
attachmentReferenceProjection
|
|
1122
|
+
};
|
|
921
1123
|
}
|
|
922
1124
|
const baseKysely = await createReactorKysely({
|
|
923
1125
|
reactorDbUrl,
|
|
@@ -955,6 +1157,10 @@ async function initServer(serverPort, options, renown) {
|
|
|
955
1157
|
await nodeProcessor.init();
|
|
956
1158
|
return nodeProcessor;
|
|
957
1159
|
});
|
|
1160
|
+
registerAttachmentReferenceReadModel(reactorBuilder, {
|
|
1161
|
+
baseKysely,
|
|
1162
|
+
attachmentReferenceWriter
|
|
1163
|
+
});
|
|
958
1164
|
reactorBuilder.withShutdownHook(async () => {
|
|
959
1165
|
if (apiRef.current) await apiRef.current.dispose();
|
|
960
1166
|
});
|
|
@@ -969,7 +1175,8 @@ async function initServer(serverPort, options, renown) {
|
|
|
969
1175
|
reactorDriveClient: new ReactorDriveClient({
|
|
970
1176
|
reactor: module.client,
|
|
971
1177
|
readModel: driveNodeView
|
|
972
|
-
})
|
|
1178
|
+
}),
|
|
1179
|
+
attachmentReferenceProjection: { status: "available" }
|
|
973
1180
|
};
|
|
974
1181
|
};
|
|
975
1182
|
let defaultDriveUrl = void 0;
|
|
@@ -1100,6 +1307,7 @@ async function initServer(serverPort, options, renown) {
|
|
|
1100
1307
|
api,
|
|
1101
1308
|
reactor: client,
|
|
1102
1309
|
attachmentService,
|
|
1310
|
+
attachmentReferenceProjection: api.attachmentReferenceProjection,
|
|
1103
1311
|
renown,
|
|
1104
1312
|
port: serverPort,
|
|
1105
1313
|
shutdown: () => api.dispose()
|
|
@@ -1157,5 +1365,5 @@ if (import.meta.main) await startSwitchboard();
|
|
|
1157
1365
|
//#endregion
|
|
1158
1366
|
export { parseForcePgVersion as a, applySwitchboardReactorDefaults as i, isPortAvailable as n, startSwitchboard as r, deriveAttachmentServiceConfig as t };
|
|
1159
1367
|
|
|
1160
|
-
//# sourceMappingURL=server-
|
|
1161
|
-
//# debugId=
|
|
1368
|
+
//# sourceMappingURL=server-ChK0GLG8.mjs.map
|
|
1369
|
+
//# debugId=b1867ed0-059f-587c-9a5f-677da3666de7
|