@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.
- 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/CHANGELOG.md +291 -0
- package/README.md +37 -0
- package/dist/index.mjs +1 -1
- package/dist/{server-Cgud_ONM.mjs → server-By1-k-B6.mjs} +263 -24
- package/dist/server-By1-k-B6.mjs.map +1 -0
- package/dist/server.d.mts +8 -4
- package/dist/server.d.mts.map +1 -1
- package/dist/server.mjs +3 -3
- package/package.json +13 -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/test/reactor-feature-flags.test.ts +61 -0
- package/tsconfig.json +3 -0
- package/dist/server-Cgud_ONM.mjs.map +0 -1
|
@@ -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]="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),
|
|
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);
|
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
1161
|
-
//# debugId=
|
|
1399
|
+
//# sourceMappingURL=server-By1-k-B6.mjs.map
|
|
1400
|
+
//# debugId=c23ecfb9-9882-5c12-9555-3c40f7167555
|