@powerhousedao/switchboard 6.2.2-dev.4 → 6.2.2-dev.40

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/CHANGELOG.md CHANGED
@@ -1,3 +1,228 @@
1
+ ## 6.2.2-dev.40 (2026-08-08)
2
+
3
+ This was a version bump only for @powerhousedao/switchboard to align it with other projects, there were no code changes.
4
+
5
+ ## 6.2.2-dev.39 (2026-08-07)
6
+
7
+ This was a version bump only for @powerhousedao/switchboard to align it with other projects, there were no code changes.
8
+
9
+ ## 6.2.2-dev.38 (2026-08-06)
10
+
11
+ ### 🚀 Features
12
+
13
+ - added recipe e2e tests to the suite ([88cd1a55c](https://github.com/powerhouse-inc/powerhouse/commit/88cd1a55c))
14
+ - auth phase 4 ([28aa9ad6a](https://github.com/powerhouse-inc/powerhouse/commit/28aa9ad6a))
15
+
16
+ ### 🩹 Fixes
17
+
18
+ - stage llm files from pre-commit hook ([0af2ab705](https://github.com/powerhouse-inc/powerhouse/commit/0af2ab705))
19
+ - **reactor:** point preflight:auth at a real store ([c1315a9f9](https://github.com/powerhouse-inc/powerhouse/commit/c1315a9f9))
20
+ - **reactor:** filter initialState and keep auth in a scoped subscription read ([77fee70b4](https://github.com/powerhouse-inc/powerhouse/commit/77fee70b4))
21
+ - **reactor:** degrade the poll query against a remote on the older schema ([0c3aefe65](https://github.com/powerhouse-inc/powerhouse/commit/0c3aefe65))
22
+ - **shared:** stop a state snapshot from installing or replacing a policy ([f947e562e](https://github.com/powerhouse-inc/powerhouse/commit/f947e562e))
23
+ - **shared:** make auth-administration retention a reachability rule ([3db42daac](https://github.com/powerhouse-inc/powerhouse/commit/3db42daac))
24
+
25
+ ### ❤️ Thank You
26
+
27
+ - Benjamin Jordan
28
+ - Claude Fable 5
29
+
30
+ ## 6.2.2-dev.37 (2026-08-05)
31
+
32
+ This was a version bump only for @powerhousedao/switchboard to align it with other projects, there were no code changes.
33
+
34
+ ## 6.2.2-dev.36 (2026-08-04)
35
+
36
+ This was a version bump only for @powerhousedao/switchboard to align it with other projects, there were no code changes.
37
+
38
+ ## 6.2.2-dev.35 (2026-08-03)
39
+
40
+ This was a version bump only for @powerhousedao/switchboard to align it with other projects, there were no code changes.
41
+
42
+ ## 6.2.2-dev.34 (2026-08-02)
43
+
44
+ This was a version bump only for @powerhousedao/switchboard to align it with other projects, there were no code changes.
45
+
46
+ ## 6.2.2-dev.33 (2026-08-01)
47
+
48
+ ### 🩹 Fixes
49
+
50
+ - **reactor:** fixing a few defects with document scope reshuffle specifics ([7546b9ff2](https://github.com/powerhouse-inc/powerhouse/commit/7546b9ff2))
51
+
52
+ ### ❤️ Thank You
53
+
54
+ - Benjamin Jordan
55
+
56
+ ## 6.2.2-dev.32 (2026-07-31)
57
+
58
+ ### 🚀 Features
59
+
60
+ - **reactor:** feature flag guards and spec updates to make plan for guarded reshuffle edge cases ([20b0fd6be](https://github.com/powerhouse-inc/powerhouse/commit/20b0fd6be))
61
+
62
+ ### 🩹 Fixes
63
+
64
+ - **reactor:** fix convergence in the case we deny an operation that has a skip ([21cbb3ecb](https://github.com/powerhouse-inc/powerhouse/commit/21cbb3ecb))
65
+ - auth-scope spec updates, assert ordered streams, and fix some busted default protocol versions ([97c5a37b5](https://github.com/powerhouse-inc/powerhouse/commit/97c5a37b5))
66
+
67
+ ### ❤️ Thank You
68
+
69
+ - Benjamin Jordan
70
+
71
+ ## 6.2.2-dev.31 (2026-07-31)
72
+
73
+ This was a version bump only for @powerhousedao/switchboard to align it with other projects, there were no code changes.
74
+
75
+ ## 6.2.2-dev.30 (2026-07-31)
76
+
77
+ This was a version bump only for @powerhousedao/switchboard to align it with other projects, there were no code changes.
78
+
79
+ ## 6.2.2-dev.29 (2026-07-31)
80
+
81
+ This was a version bump only for @powerhousedao/switchboard to align it with other projects, there were no code changes.
82
+
83
+ ## 6.2.2-dev.28 (2026-07-30)
84
+
85
+ ### 🚀 Features
86
+
87
+ - **reactor-browser:** add IReactorBrowserClient and a light GraphQL client ([#2893](https://github.com/powerhouse-inc/powerhouse/pull/2893))
88
+ - **reactor:** plugging in the first decision model ([27e9b2772](https://github.com/powerhouse-inc/powerhouse/commit/27e9b2772))
89
+
90
+ ### ❤️ Thank You
91
+
92
+ - Benjamin Jordan
93
+ - Guillermo Puente Sandoval @gpuente
94
+
95
+ ## 6.2.2-dev.27 (2026-07-30)
96
+
97
+ ### 🩹 Fixes
98
+
99
+ - **connect:** pin @privy-io/react-auth to 3.35.0 ([909352ce3](https://github.com/powerhouse-inc/powerhouse/commit/909352ce3))
100
+
101
+ ### ❤️ Thank You
102
+
103
+ - acaldas
104
+
105
+ ## 6.2.2-dev.26 (2026-07-30)
106
+
107
+ This was a version bump only for @powerhousedao/switchboard to align it with other projects, there were no code changes.
108
+
109
+ ## 6.2.2-dev.25 (2026-07-29)
110
+
111
+ This was a version bump only for @powerhousedao/switchboard to align it with other projects, there were no code changes.
112
+
113
+ ## 6.2.2-dev.24 (2026-07-29)
114
+
115
+ ### 🩹 Fixes
116
+
117
+ - **reactor:** fix an issue exacerbated by auth where snapshots were ordered by insertion not revision ([4aa32fe1b](https://github.com/powerhouse-inc/powerhouse/commit/4aa32fe1b))
118
+
119
+ ### ❤️ Thank You
120
+
121
+ - Benjamin Jordan
122
+
123
+ ## 6.2.2-dev.23 (2026-07-28)
124
+
125
+ ### 🩹 Fixes
126
+
127
+ - **deps:** pin qr to 0.5.5 so RainbowKit can render the WalletConnect QR ([5343f9178](https://github.com/powerhouse-inc/powerhouse/commit/5343f9178))
128
+
129
+ ### ❤️ Thank You
130
+
131
+ - acaldas
132
+
133
+ ## 6.2.2-dev.22 (2026-07-28)
134
+
135
+ This was a version bump only for @powerhousedao/switchboard to align it with other projects, there were no code changes.
136
+
137
+ ## 6.2.2-dev.21 (2026-07-27)
138
+
139
+ This was a version bump only for @powerhousedao/switchboard to align it with other projects, there were no code changes.
140
+
141
+ ## 6.2.2-dev.20 (2026-07-27)
142
+
143
+ This was a version bump only for @powerhousedao/switchboard to align it with other projects, there were no code changes.
144
+
145
+ ## 6.2.2-dev.19 (2026-07-27)
146
+
147
+ This was a version bump only for @powerhousedao/switchboard to align it with other projects, there were no code changes.
148
+
149
+ ## 6.2.2-dev.18 (2026-07-27)
150
+
151
+ This was a version bump only for @powerhousedao/switchboard to align it with other projects, there were no code changes.
152
+
153
+ ## 6.2.2-dev.17 (2026-07-26)
154
+
155
+ ### 🚀 Features
156
+
157
+ - **attachments:** S3-compatible storage, direct transfers, and document-derived authorization ([#2884](https://github.com/powerhouse-inc/powerhouse/pull/2884))
158
+
159
+ ### ❤️ Thank You
160
+
161
+ - Guillermo Puente Sandoval @gpuente
162
+
163
+ ## 6.2.2-dev.16 (2026-07-25)
164
+
165
+ ### 🩹 Fixes
166
+
167
+ - code review fixes ([318cbefa9](https://github.com/powerhouse-inc/powerhouse/commit/318cbefa9))
168
+ - version 0 is reserved and only 1 and on is available for setting ([193ac4c56](https://github.com/powerhouse-inc/powerhouse/commit/193ac4c56))
169
+ - resolve a contradiction in group membership ([39845a65c](https://github.com/powerhouse-inc/powerhouse/commit/39845a65c))
170
+ - grant evaluation needs to ignore features that do not exist yet ([0c1d06396](https://github.com/powerhouse-inc/powerhouse/commit/0c1d06396))
171
+
172
+ ### ❤️ Thank You
173
+
174
+ - Benjamin Jordan
175
+
176
+ ## 6.2.2-dev.15 (2026-07-24)
177
+
178
+ This was a version bump only for @powerhousedao/switchboard to align it with other projects, there were no code changes.
179
+
180
+ ## 6.2.2-dev.14 (2026-07-23)
181
+
182
+ This was a version bump only for @powerhousedao/switchboard to align it with other projects, there were no code changes.
183
+
184
+ ## 6.2.2-dev.13 (2026-07-23)
185
+
186
+ This was a version bump only for @powerhousedao/switchboard to align it with other projects, there were no code changes.
187
+
188
+ ## 6.2.2-dev.12 (2026-07-23)
189
+
190
+ This was a version bump only for @powerhousedao/switchboard to align it with other projects, there were no code changes.
191
+
192
+ ## 6.2.2-dev.11 (2026-07-22)
193
+
194
+ ### 🚀 Features
195
+
196
+ - **connect:** dynamic packages can contribute to the PWA configuration ([da867e21d](https://github.com/powerhouse-inc/powerhouse/commit/da867e21d))
197
+
198
+ ### ❤️ Thank You
199
+
200
+ - Yasiel Cabrera @YasielCabrera
201
+
202
+ ## 6.2.2-dev.10 (2026-07-21)
203
+
204
+ This was a version bump only for @powerhousedao/switchboard to align it with other projects, there were no code changes.
205
+
206
+ ## 6.2.2-dev.9 (2026-07-20)
207
+
208
+ This was a version bump only for @powerhousedao/switchboard to align it with other projects, there were no code changes.
209
+
210
+ ## 6.2.2-dev.8 (2026-07-19)
211
+
212
+ This was a version bump only for @powerhousedao/switchboard to align it with other projects, there were no code changes.
213
+
214
+ ## 6.2.2-dev.7 (2026-07-18)
215
+
216
+ This was a version bump only for @powerhousedao/switchboard to align it with other projects, there were no code changes.
217
+
218
+ ## 6.2.2-dev.6 (2026-07-17)
219
+
220
+ This was a version bump only for @powerhousedao/switchboard to align it with other projects, there were no code changes.
221
+
222
+ ## 6.2.2-dev.5 (2026-07-17)
223
+
224
+ This was a version bump only for @powerhousedao/switchboard to align it with other projects, there were no code changes.
225
+
1
226
  ## 6.2.2-dev.4 (2026-07-17)
2
227
 
3
228
  ### 🚀 Features
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-Cgud_ONM.mjs";
4
+ import { a as parseForcePgVersion, r as startSwitchboard } from "./server-BLXBKOSF.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]="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]="ca6cb919-a51c-59fe-9716-c135c0b04465")}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), `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.
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
@@ -410,7 +608,10 @@ function applySwitchboardReactorDefaults(reactorBuilder, clientBuilder, options
410
608
  const scheme = options.channelScheme === void 0 ? ChannelScheme.SWITCHBOARD : options.channelScheme;
411
609
  if (scheme !== false) reactorBuilder.withChannelScheme(scheme);
412
610
  if (options.signalHandlers !== false) reactorBuilder.withSignalHandlers();
413
- if (options.executorConfig?.maxSkipThreshold !== void 0) reactorBuilder.withExecutorConfig({ maxSkipThreshold: options.executorConfig.maxSkipThreshold });
611
+ if (options.executorConfig?.maxSkipThreshold !== void 0 || options.executorConfig?.featureFlags !== void 0) reactorBuilder.withExecutorConfig({
612
+ ...options.executorConfig.maxSkipThreshold !== void 0 ? { maxSkipThreshold: options.executorConfig.maxSkipThreshold } : {},
613
+ ...options.executorConfig.featureFlags !== void 0 ? { featureFlags: options.executorConfig.featureFlags } : {}
614
+ });
414
615
  if (options.documentModelLoader) reactorBuilder.withDocumentModelLoader(options.documentModelLoader);
415
616
  if (options.logger) reactorBuilder.withLogger(options.logger);
416
617
  if (options.signer) clientBuilder.withSigner(options.signer);
@@ -911,13 +1112,17 @@ async function initServer(serverPort, options, renown) {
911
1112
  });
912
1113
  }
913
1114
  const reactorLogger = logger.child(["reactor"]);
914
- const initializeClient = async (documentModels) => {
1115
+ const initializeClient = async (documentModels, { attachmentReferenceWriter }) => {
915
1116
  if (options.reactor) {
1117
+ const attachmentReferenceProjection = await registerAttachmentReferenceReadModelOnModule(options.reactor, attachmentReferenceWriter);
916
1118
  if (options.reactor.reactorModule) {
917
1119
  new ReactorInstrumentation(options.reactor.reactorModule).start();
918
1120
  reactorLogger.info("Reactor metrics instrumentation started (using caller-provided reactor)");
919
1121
  }
920
- return { module: options.reactor };
1122
+ return {
1123
+ module: options.reactor,
1124
+ attachmentReferenceProjection
1125
+ };
921
1126
  }
922
1127
  const baseKysely = await createReactorKysely({
923
1128
  reactorDbUrl,
@@ -930,13 +1135,22 @@ async function initServer(serverPort, options, renown) {
930
1135
  const maxSkipThreshold = parseInt(process.env.MAX_SKIP_THRESHOLD ?? "", 10);
931
1136
  const hasSkipThreshold = !isNaN(maxSkipThreshold) && maxSkipThreshold > 0;
932
1137
  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");
1138
+ const documentDecisions = process.env.REACTOR_DOCUMENT_DECISIONS === "true";
1139
+ if (documentDecisions) logger.info("Reactor document decisions enabled");
1140
+ const authEnforcement = process.env.REACTOR_AUTH_ENFORCEMENT === "true";
1141
+ if (authEnforcement) logger.info("Reactor auth enforcement enabled");
934
1142
  const reactorBuilder = new ReactorBuilder().withEventBus(new EventBus()).withKysely(baseKysely);
935
1143
  const clientBuilder = new ReactorClientBuilder().withReactorBuilder(reactorBuilder);
936
1144
  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
1145
  applySwitchboardReactorDefaults(reactorBuilder, clientBuilder, {
938
1146
  documentModels: [...documentModels, ...vetraDocumentModels],
939
- executorConfig: hasSkipThreshold ? { maxSkipThreshold } : void 0,
1147
+ executorConfig: hasSkipThreshold || documentDecisions || authEnforcement ? {
1148
+ ...hasSkipThreshold ? { maxSkipThreshold } : {},
1149
+ ...documentDecisions || authEnforcement ? { featureFlags: {
1150
+ ...documentDecisions ? { documentDecisions: true } : {},
1151
+ ...authEnforcement ? { authEnforcement: true } : {}
1152
+ } } : {}
1153
+ } : void 0,
940
1154
  documentModelLoader: httpLoader && dynamicModelLoading ? httpLoader.documentModelLoader : void 0,
941
1155
  logger: reactorLogger,
942
1156
  signer: renown ? getRenownSignerConfig(renown, options.identity?.requireSignatures) : void 0
@@ -955,6 +1169,10 @@ async function initServer(serverPort, options, renown) {
955
1169
  await nodeProcessor.init();
956
1170
  return nodeProcessor;
957
1171
  });
1172
+ registerAttachmentReferenceReadModel(reactorBuilder, {
1173
+ baseKysely,
1174
+ attachmentReferenceWriter
1175
+ });
958
1176
  reactorBuilder.withShutdownHook(async () => {
959
1177
  if (apiRef.current) await apiRef.current.dispose();
960
1178
  });
@@ -969,7 +1187,8 @@ async function initServer(serverPort, options, renown) {
969
1187
  reactorDriveClient: new ReactorDriveClient({
970
1188
  reactor: module.client,
971
1189
  readModel: driveNodeView
972
- })
1190
+ }),
1191
+ attachmentReferenceProjection: { status: "available" }
973
1192
  };
974
1193
  };
975
1194
  let defaultDriveUrl = void 0;
@@ -1100,6 +1319,7 @@ async function initServer(serverPort, options, renown) {
1100
1319
  api,
1101
1320
  reactor: client,
1102
1321
  attachmentService,
1322
+ attachmentReferenceProjection: api.attachmentReferenceProjection,
1103
1323
  renown,
1104
1324
  port: serverPort,
1105
1325
  shutdown: () => api.dispose()
@@ -1157,5 +1377,5 @@ if (import.meta.main) await startSwitchboard();
1157
1377
  //#endregion
1158
1378
  export { parseForcePgVersion as a, applySwitchboardReactorDefaults as i, isPortAvailable as n, startSwitchboard as r, deriveAttachmentServiceConfig as t };
1159
1379
 
1160
- //# sourceMappingURL=server-Cgud_ONM.mjs.map
1161
- //# debugId=5eb0a978-689a-51dd-978a-cd3d7fe5df4b
1380
+ //# sourceMappingURL=server-BLXBKOSF.mjs.map
1381
+ //# debugId=ca6cb919-a51c-59fe-9716-c135c0b04465