@powerhousedao/switchboard 6.0.0-dev.207 → 6.0.0-dev.209

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,40 @@
1
+ ## 6.0.0-dev.209 (2026-04-30)
2
+
3
+ ### 🚀 Features
4
+
5
+ - **reactor-api:** added system subgraph which returns version and hash information ([248fc1e92](https://github.com/powerhouse-inc/powerhouse/commit/248fc1e92))
6
+ - **reactor-attachments:** switchboard implementation fixes ([3b320d01c](https://github.com/powerhouse-inc/powerhouse/commit/3b320d01c))
7
+ - initial switchboard endpoints and implementation ([01b20cede](https://github.com/powerhouse-inc/powerhouse/commit/01b20cede))
8
+
9
+ ### 🩹 Fixes
10
+
11
+ - so much linting that it kills my computer ([d6b6ff143](https://github.com/powerhouse-inc/powerhouse/commit/d6b6ff143))
12
+ - **reactor-attachments:** force octet-stream content-type for remote uploads ([fc45afccb](https://github.com/powerhouse-inc/powerhouse/commit/fc45afccb))
13
+
14
+ ### ❤️ Thank You
15
+
16
+ - Benjamin Jordan
17
+
18
+ ## 6.0.0-dev.208 (2026-04-29)
19
+
20
+ ### 🚀 Features
21
+
22
+ - first swing at a load test ([f7e0f4456](https://github.com/powerhouse-inc/powerhouse/commit/f7e0f4456))
23
+ - added observability profile ([957af0925](https://github.com/powerhouse-inc/powerhouse/commit/957af0925))
24
+ - metrics integration ([1ce0b5fdf](https://github.com/powerhouse-inc/powerhouse/commit/1ce0b5fdf))
25
+ - switchboard-lb M3 ([cc49638e0](https://github.com/powerhouse-inc/powerhouse/commit/cc49638e0))
26
+ - **reactor-api:** added attachment service creation to reactor-api ([f96e9806b](https://github.com/powerhouse-inc/powerhouse/commit/f96e9806b))
27
+ - **reactor-attachments:** initial storage implementation ([b82e0fc8c](https://github.com/powerhouse-inc/powerhouse/commit/b82e0fc8c))
28
+ - **reactor-attachments:** initial setup of package ([ac5bac96a](https://github.com/powerhouse-inc/powerhouse/commit/ac5bac96a))
29
+
30
+ ### 🩹 Fixes
31
+
32
+ - **reactor-attachments:** fix the tsdown config ([8485b54be](https://github.com/powerhouse-inc/powerhouse/commit/8485b54be))
33
+
34
+ ### ❤️ Thank You
35
+
36
+ - Benjamin Jordan
37
+
1
38
  ## 6.0.0-dev.207 (2026-04-29)
2
39
 
3
40
  This was a version bump only for @powerhousedao/switchboard to align it with other projects, there were no code changes.
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { n as startSwitchboard } from "./server-BMtyzhoR.mjs";
2
+ import { n as startSwitchboard } from "./server-8U7q7B7r.mjs";
3
3
  import "./utils-DFl0ezBT.mjs";
4
4
  import * as Sentry from "@sentry/node";
5
5
  import { childLogger } from "document-model";
@@ -19,9 +19,210 @@ import { PGliteDialect } from "kysely-pglite-dialect";
19
19
  import net from "node:net";
20
20
  import path from "path";
21
21
  import { Pool } from "pg";
22
+ import { AttachmentNotFound, InvalidAttachmentRef, ReservationNotFound } from "@powerhousedao/reactor-attachments";
23
+ import { Readable } from "node:stream";
22
24
  import { EnvVarProvider } from "@openfeature/env-var-provider";
23
25
  import { OpenFeature } from "@openfeature/server-sdk";
24
26
  import { DEFAULT_RENOWN_URL, NodeKeyStorage, RenownBuilder, RenownCryptoBuilder, createSignatureVerifier } from "@renown/sdk/node";
27
+ //#region src/attachments/auth.ts
28
+ /**
29
+ * Wrap a Node-style handler so that, when `authService` is provided and auth is
30
+ * enabled, the request must carry a verifiable Bearer token.
31
+ */
32
+ function requireAuth(authService, handler) {
33
+ if (!authService) return handler;
34
+ return async (req, res) => {
35
+ let result;
36
+ try {
37
+ result = await authService.verifyBearer(req.headers.authorization);
38
+ } catch {
39
+ res.statusCode = 500;
40
+ res.setHeader("Content-Type", "application/json");
41
+ res.end(JSON.stringify({ error: "Internal authentication error" }));
42
+ return;
43
+ }
44
+ if (result instanceof Response) {
45
+ const body = await result.text();
46
+ res.statusCode = result.status;
47
+ const contentType = result.headers.get("content-type");
48
+ if (contentType) res.setHeader("Content-Type", contentType);
49
+ res.end(body);
50
+ return;
51
+ }
52
+ if (result.auth_enabled && !result.user) {
53
+ res.statusCode = 401;
54
+ res.setHeader("Content-Type", "application/json");
55
+ res.end(JSON.stringify({ error: "Authentication required" }));
56
+ return;
57
+ }
58
+ await handler(req, res);
59
+ };
60
+ }
61
+ //#endregion
62
+ //#region src/attachments/mount-auth.ts
63
+ /**
64
+ * Mount a Node-style attachment route with `requireAuth` applied unconditionally.
65
+ * When `api.authService` is undefined (auth disabled), `requireAuth` returns the
66
+ * handler unchanged — that is the only way to opt out. To register a route
67
+ * without auth wrapping you must call `api.httpAdapter.mountNodeRoute` directly.
68
+ */
69
+ function mountAuthenticatedNodeRoute(api, method, path, handler) {
70
+ api.httpAdapter.mountNodeRoute(method, path, requireAuth(api.authService, handler));
71
+ }
72
+ //#endregion
73
+ //#region src/attachments/routes.ts
74
+ const logger$1 = childLogger(["switchboard", "attachments"]);
75
+ const HASH_PATTERN = /^[a-f0-9]{64}$/;
76
+ const CONTROL_CHARS = /[\x00-\x1f\x7f]/;
77
+ const MIME_TYPE_PATTERN = /^[!#$%&'*+\-.^_`|~\w]+\/[!#$%&'*+\-.^_`|~\w]+(?:\s*;\s*[!#$%&'*+\-.^_`|~\w]+=(?:[!#$%&'*+\-.^_`|~\w]+|"(?:[^"\\\r\n]|\\[^\r\n])*"))*$/;
78
+ const MAX_FILENAME_LEN = 255;
79
+ const MAX_MIMETYPE_LEN = 255;
80
+ function sendJson(res, status, body) {
81
+ res.statusCode = status;
82
+ res.setHeader("Content-Type", "application/json");
83
+ res.end(JSON.stringify(body));
84
+ }
85
+ function sendError(res, status, message) {
86
+ sendJson(res, status, { error: message });
87
+ }
88
+ function statusForError(err) {
89
+ if (err instanceof AttachmentNotFound) return 404;
90
+ if (err instanceof ReservationNotFound) return 404;
91
+ if (err instanceof InvalidAttachmentRef) return 400;
92
+ return 500;
93
+ }
94
+ function sendErrorFromException(res, err) {
95
+ const status = statusForError(err);
96
+ if (status >= 500) {
97
+ logger$1.error("Attachment route error: @error", err);
98
+ sendError(res, status, "Internal error");
99
+ return;
100
+ }
101
+ sendError(res, status, err instanceof Error ? err.message : String(err));
102
+ }
103
+ async function readJsonBody(req, body) {
104
+ if (body !== void 0 && body !== null && typeof body === "object") return body;
105
+ const chunks = [];
106
+ for await (const chunk of req) chunks.push(chunk);
107
+ if (chunks.length === 0) return void 0;
108
+ const text = Buffer.concat(chunks).toString("utf8");
109
+ if (text.length === 0) return void 0;
110
+ return JSON.parse(text);
111
+ }
112
+ function parseReserveOptions(input) {
113
+ if (input === null || typeof input !== "object") return null;
114
+ const obj = input;
115
+ if (typeof obj.mimeType !== "string" || obj.mimeType.length === 0 || obj.mimeType.length > MAX_MIMETYPE_LEN || !MIME_TYPE_PATTERN.test(obj.mimeType)) return null;
116
+ if (typeof obj.fileName !== "string" || obj.fileName.length === 0 || obj.fileName.length > MAX_FILENAME_LEN || CONTROL_CHARS.test(obj.fileName)) return null;
117
+ let extension = null;
118
+ if (typeof obj.extension === "string") {
119
+ if (obj.extension.length === 0 || /[\\/]/.test(obj.extension)) return null;
120
+ extension = obj.extension;
121
+ } else if (obj.extension !== void 0 && obj.extension !== null) return null;
122
+ return {
123
+ mimeType: obj.mimeType,
124
+ fileName: obj.fileName,
125
+ extension
126
+ };
127
+ }
128
+ function quoteFilename(name) {
129
+ return `"${name.replace(/[\\"]/g, "\\$&")}"`;
130
+ }
131
+ function buildContentDisposition(fileName) {
132
+ const ascii = fileName.replace(/[^\x20-\x21\x23-\x5b\x5d-\x7e]/g, "_");
133
+ const encoded = encodeURIComponent(fileName).replace(/['()*!]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);
134
+ return `attachment; filename=${quoteFilename(ascii)}; filename*=UTF-8''${encoded}`;
135
+ }
136
+ function makeReserveHandler(attachments) {
137
+ return async (req, res, body) => {
138
+ let parsed;
139
+ try {
140
+ parsed = await readJsonBody(req, body);
141
+ } catch {
142
+ sendError(res, 400, "Invalid JSON body");
143
+ return;
144
+ }
145
+ const opts = parseReserveOptions(parsed);
146
+ if (!opts) {
147
+ sendError(res, 400, "Body must be { mimeType: string (type/subtype), fileName: string (no control characters, max 255 chars), extension?: string|null }");
148
+ return;
149
+ }
150
+ try {
151
+ sendJson(res, 201, { reservationId: (await attachments.service.reserve(opts)).reservationId });
152
+ } catch (err) {
153
+ sendErrorFromException(res, err);
154
+ }
155
+ };
156
+ }
157
+ function makeUploadHandler(attachments) {
158
+ return async (req, res) => {
159
+ const reservationId = extractParam(req, "reservationId");
160
+ if (!reservationId) {
161
+ sendError(res, 400, "Missing reservationId");
162
+ return;
163
+ }
164
+ let reservation;
165
+ try {
166
+ reservation = await attachments.reservations.get(reservationId);
167
+ } catch (err) {
168
+ sendErrorFromException(res, err);
169
+ return;
170
+ }
171
+ const upload = attachments.uploadFactory.createUpload(reservation.reservationId, {
172
+ mimeType: reservation.mimeType,
173
+ fileName: reservation.fileName,
174
+ extension: reservation.extension
175
+ });
176
+ const webStream = Readable.toWeb(req);
177
+ try {
178
+ sendJson(res, 200, await upload.send(webStream));
179
+ } catch (err) {
180
+ sendErrorFromException(res, err);
181
+ }
182
+ };
183
+ }
184
+ function makeDownloadHandler(attachments) {
185
+ return async (req, res) => {
186
+ const hash = extractParam(req, "hash");
187
+ if (!hash || !HASH_PATTERN.test(hash)) {
188
+ sendError(res, 400, "Invalid attachment hash");
189
+ return;
190
+ }
191
+ const controller = new AbortController();
192
+ req.once("close", () => controller.abort());
193
+ let response;
194
+ try {
195
+ response = await attachments.store.get(hash, controller.signal);
196
+ } catch (err) {
197
+ sendErrorFromException(res, err);
198
+ return;
199
+ }
200
+ const { header, body } = response;
201
+ res.statusCode = 200;
202
+ res.setHeader("Content-Type", header.mimeType);
203
+ res.setHeader("Content-Length", String(header.sizeBytes));
204
+ res.setHeader("Content-Disposition", buildContentDisposition(header.fileName));
205
+ res.setHeader("X-Attachment-Metadata", JSON.stringify({
206
+ mimeType: header.mimeType,
207
+ fileName: header.fileName,
208
+ sizeBytes: header.sizeBytes,
209
+ extension: header.extension
210
+ }));
211
+ Readable.fromWeb(body).pipe(res);
212
+ };
213
+ }
214
+ function extractParam(req, name) {
215
+ return req.params?.[name];
216
+ }
217
+ //#endregion
218
+ //#region src/attachments/index.ts
219
+ function registerAttachmentRoutes(api) {
220
+ const { attachments } = api;
221
+ mountAuthenticatedNodeRoute(api, "POST", "/attachments/reservations", makeReserveHandler(attachments));
222
+ mountAuthenticatedNodeRoute(api, "PUT", "/attachments/reservations/:reservationId", makeUploadHandler(attachments));
223
+ mountAuthenticatedNodeRoute(api, "GET", "/attachments/:hash", makeDownloadHandler(attachments));
224
+ }
225
+ //#endregion
25
226
  //#region src/feature-flags.ts
26
227
  async function initFeatureFlags() {
27
228
  const provider = new EnvVarProvider();
@@ -195,6 +396,7 @@ async function initServer(serverPort, options, renown) {
195
396
  logger: apiLogger,
196
397
  enableDocumentModelSubgraphs: options.enableDocumentModelSubgraphs
197
398
  }, "switchboard");
399
+ registerAttachmentRoutes(api);
198
400
  if (process.env.SENTRY_DSN) api.httpAdapter.setupSentryErrorHandler(Sentry);
199
401
  const { client, graphqlManager, documentModelRegistry } = api;
200
402
  if (httpLoader) {
@@ -288,4 +490,4 @@ if (import.meta.main) await startSwitchboard();
288
490
  //#endregion
289
491
  export { startSwitchboard as n, isPortAvailable as t };
290
492
 
291
- //# sourceMappingURL=server-BMtyzhoR.mjs.map
493
+ //# sourceMappingURL=server-8U7q7B7r.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server-8U7q7B7r.mjs","names":["logger","vetraDocumentModels","documentModels","vetraProcessorFactory"],"sources":["../src/attachments/auth.ts","../src/attachments/mount-auth.ts","../src/attachments/routes.ts","../src/attachments/index.ts","../src/feature-flags.ts","../src/renown.ts","../src/server.mts"],"sourcesContent":["import type { AuthService } from \"@powerhousedao/reactor-api\";\nimport type { IncomingMessage, ServerResponse } from \"node:http\";\n\nexport type NodeHandler = (\n req: IncomingMessage,\n res: ServerResponse,\n) => Promise<void> | void;\n\n/**\n * Wrap a Node-style handler so that, when `authService` is provided and auth is\n * enabled, the request must carry a verifiable Bearer token.\n */\nexport function requireAuth(\n authService: AuthService | undefined,\n handler: NodeHandler,\n): NodeHandler {\n if (!authService) return handler;\n\n return async (req, res) => {\n let result;\n try {\n result = await authService.verifyBearer(req.headers.authorization);\n } catch {\n res.statusCode = 500;\n res.setHeader(\"Content-Type\", \"application/json\");\n res.end(JSON.stringify({ error: \"Internal authentication error\" }));\n return;\n }\n\n if (result instanceof Response) {\n const body = await result.text();\n res.statusCode = result.status;\n const contentType = result.headers.get(\"content-type\");\n if (contentType) res.setHeader(\"Content-Type\", contentType);\n res.end(body);\n return;\n }\n\n if (result.auth_enabled && !result.user) {\n res.statusCode = 401;\n res.setHeader(\"Content-Type\", \"application/json\");\n res.end(JSON.stringify({ error: \"Authentication required\" }));\n return;\n }\n\n await handler(req, res);\n };\n}\n","import type { API } from \"@powerhousedao/reactor-api\";\nimport { requireAuth, type NodeHandler } from \"./auth.js\";\n\nexport type HttpMethod = \"DELETE\" | \"GET\" | \"POST\" | \"PUT\";\n\n/**\n * Mount a Node-style attachment route with `requireAuth` applied unconditionally.\n * When `api.authService` is undefined (auth disabled), `requireAuth` returns the\n * handler unchanged — that is the only way to opt out. To register a route\n * without auth wrapping you must call `api.httpAdapter.mountNodeRoute` directly.\n */\nexport function mountAuthenticatedNodeRoute(\n api: Pick<API, \"httpAdapter\" | \"authService\">,\n method: HttpMethod,\n path: string,\n handler: NodeHandler,\n): void {\n api.httpAdapter.mountNodeRoute(\n method,\n path,\n requireAuth(api.authService, handler),\n );\n}\n","import {\n AttachmentNotFound,\n InvalidAttachmentRef,\n ReservationNotFound,\n type AttachmentBuildResult,\n type ReserveAttachmentOptions,\n} from \"@powerhousedao/reactor-attachments\";\nimport type { AttachmentHash } from \"@powerhousedao/reactor\";\nimport { childLogger } from \"document-model\";\nimport type { IncomingMessage, ServerResponse } from \"node:http\";\nimport { Readable } from \"node:stream\";\nimport type { ReadableStream as NodeReadableStream } from \"node:stream/web\";\n\nconst logger = childLogger([\"switchboard\", \"attachments\"]);\n\nconst HASH_PATTERN = /^[a-f0-9]{64}$/;\n// eslint-disable-next-line no-control-regex\nconst CONTROL_CHARS = /[\\x00-\\x1f\\x7f]/;\n// RFC 6838 token chars; allows optional `; param=value` pairs (token or quoted-string).\nconst MIME_TYPE_PATTERN =\n /^[!#$%&'*+\\-.^_`|~\\w]+\\/[!#$%&'*+\\-.^_`|~\\w]+(?:\\s*;\\s*[!#$%&'*+\\-.^_`|~\\w]+=(?:[!#$%&'*+\\-.^_`|~\\w]+|\"(?:[^\"\\\\\\r\\n]|\\\\[^\\r\\n])*\"))*$/;\nconst MAX_FILENAME_LEN = 255;\nconst MAX_MIMETYPE_LEN = 255;\n\nfunction sendJson(res: ServerResponse, status: number, body: unknown): void {\n res.statusCode = status;\n res.setHeader(\"Content-Type\", \"application/json\");\n res.end(JSON.stringify(body));\n}\n\nfunction sendError(res: ServerResponse, status: number, message: string): void {\n sendJson(res, status, { error: message });\n}\n\nfunction statusForError(err: unknown): number {\n if (err instanceof AttachmentNotFound) return 404;\n if (err instanceof ReservationNotFound) return 404;\n if (err instanceof InvalidAttachmentRef) return 400;\n return 500;\n}\n\nfunction sendErrorFromException(res: ServerResponse, err: unknown): void {\n const status = statusForError(err);\n if (status >= 500) {\n logger.error(\"Attachment route error: @error\", err);\n sendError(res, status, \"Internal error\");\n return;\n }\n sendError(res, status, err instanceof Error ? err.message : String(err));\n}\n\nasync function readJsonBody(\n req: IncomingMessage,\n body: unknown,\n): Promise<unknown> {\n // The Express body-parser may have already populated `body`. When that\n // happens we trust it; otherwise read the raw stream ourselves so this\n // module is independent of upstream middleware ordering.\n if (body !== undefined && body !== null && typeof body === \"object\") {\n return body;\n }\n const chunks: Buffer[] = [];\n for await (const chunk of req) {\n chunks.push(chunk as Buffer);\n }\n if (chunks.length === 0) return undefined;\n const text = Buffer.concat(chunks).toString(\"utf8\");\n if (text.length === 0) return undefined;\n return JSON.parse(text);\n}\n\nexport function parseReserveOptions(\n input: unknown,\n): ReserveAttachmentOptions | null {\n if (input === null || typeof input !== \"object\") return null;\n const obj = input as Record<string, unknown>;\n if (\n typeof obj.mimeType !== \"string\" ||\n obj.mimeType.length === 0 ||\n obj.mimeType.length > MAX_MIMETYPE_LEN ||\n !MIME_TYPE_PATTERN.test(obj.mimeType)\n ) {\n return null;\n }\n if (\n typeof obj.fileName !== \"string\" ||\n obj.fileName.length === 0 ||\n obj.fileName.length > MAX_FILENAME_LEN ||\n CONTROL_CHARS.test(obj.fileName)\n ) {\n return null;\n }\n let extension: string | null = null;\n if (typeof obj.extension === \"string\") {\n if (obj.extension.length === 0 || /[\\\\/]/.test(obj.extension)) return null;\n extension = obj.extension;\n } else if (obj.extension !== undefined && obj.extension !== null) {\n return null;\n }\n return {\n mimeType: obj.mimeType,\n fileName: obj.fileName,\n extension,\n };\n}\n\nexport function quoteFilename(name: string): string {\n // RFC 6266: quoted-string with internal \" and \\ escaped.\n return `\"${name.replace(/[\\\\\"]/g, \"\\\\$&\")}\"`;\n}\n\nexport function buildContentDisposition(fileName: string): string {\n // ASCII fallback: replace any byte outside printable ASCII (0x20-0x7e),\n // plus `\"` and `\\`, with `_`. Browsers fall back to this when they don't\n // grok `filename*=`; the modern parameter carries the real name.\n const ascii = fileName.replace(/[^\\x20-\\x21\\x23-\\x5b\\x5d-\\x7e]/g, \"_\");\n // RFC 5987: percent-encode UTF-8 bytes. encodeURIComponent leaves a few\n // chars that 5987 disallows in token; re-encode them.\n const encoded = encodeURIComponent(fileName).replace(\n /['()*!]/g,\n (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`,\n );\n return `attachment; filename=${quoteFilename(ascii)}; filename*=UTF-8''${encoded}`;\n}\n\nexport function makeReserveHandler(attachments: AttachmentBuildResult) {\n return async (\n req: IncomingMessage,\n res: ServerResponse,\n body?: unknown,\n ): Promise<void> => {\n let parsed: unknown;\n try {\n parsed = await readJsonBody(req, body);\n } catch {\n sendError(res, 400, \"Invalid JSON body\");\n return;\n }\n const opts = parseReserveOptions(parsed);\n if (!opts) {\n sendError(\n res,\n 400,\n \"Body must be { mimeType: string (type/subtype), fileName: string (no control characters, max 255 chars), extension?: string|null }\",\n );\n return;\n }\n try {\n const upload = await attachments.service.reserve(opts);\n sendJson(res, 201, { reservationId: upload.reservationId });\n } catch (err) {\n sendErrorFromException(res, err);\n }\n };\n}\n\nexport function makeUploadHandler(attachments: AttachmentBuildResult) {\n return async (req: IncomingMessage, res: ServerResponse): Promise<void> => {\n const reservationId = extractParam(req, \"reservationId\");\n if (!reservationId) {\n sendError(res, 400, \"Missing reservationId\");\n return;\n }\n\n let reservation;\n try {\n reservation = await attachments.reservations.get(reservationId);\n } catch (err) {\n sendErrorFromException(res, err);\n return;\n }\n\n const upload = attachments.uploadFactory.createUpload(\n reservation.reservationId,\n {\n mimeType: reservation.mimeType,\n fileName: reservation.fileName,\n extension: reservation.extension,\n },\n );\n\n const webStream = Readable.toWeb(\n req as Readable,\n ) as ReadableStream<Uint8Array>;\n\n try {\n const result = await upload.send(webStream);\n sendJson(res, 200, result);\n } catch (err) {\n sendErrorFromException(res, err);\n }\n };\n}\n\nexport function makeDownloadHandler(attachments: AttachmentBuildResult) {\n return async (req: IncomingMessage, res: ServerResponse): Promise<void> => {\n const hash = extractParam(req, \"hash\");\n if (!hash || !HASH_PATTERN.test(hash)) {\n sendError(res, 400, \"Invalid attachment hash\");\n return;\n }\n\n const controller = new AbortController();\n req.once(\"close\", () => controller.abort());\n\n let response;\n try {\n response = await attachments.store.get(\n hash as AttachmentHash,\n controller.signal,\n );\n } catch (err) {\n sendErrorFromException(res, err);\n return;\n }\n\n const { header, body } = response;\n res.statusCode = 200;\n res.setHeader(\"Content-Type\", header.mimeType);\n res.setHeader(\"Content-Length\", String(header.sizeBytes));\n res.setHeader(\n \"Content-Disposition\",\n buildContentDisposition(header.fileName),\n );\n res.setHeader(\n \"X-Attachment-Metadata\",\n JSON.stringify({\n mimeType: header.mimeType,\n fileName: header.fileName,\n sizeBytes: header.sizeBytes,\n extension: header.extension,\n }),\n );\n\n Readable.fromWeb(body as unknown as NodeReadableStream<Uint8Array>).pipe(\n res,\n );\n };\n}\n\nfunction extractParam(req: IncomingMessage, name: string): string | undefined {\n const expressParams = (\n req as IncomingMessage & {\n params?: Record<string, string>;\n }\n ).params;\n return expressParams?.[name];\n}\n","import type { API } from \"@powerhousedao/reactor-api\";\nimport { mountAuthenticatedNodeRoute } from \"./mount-auth.js\";\nimport {\n makeDownloadHandler,\n makeReserveHandler,\n makeUploadHandler,\n} from \"./routes.js\";\n\nexport function registerAttachmentRoutes(api: API): void {\n const { attachments } = api;\n\n mountAuthenticatedNodeRoute(\n api,\n \"POST\",\n \"/attachments/reservations\",\n makeReserveHandler(attachments),\n );\n\n mountAuthenticatedNodeRoute(\n api,\n \"PUT\",\n \"/attachments/reservations/:reservationId\",\n makeUploadHandler(attachments),\n );\n\n mountAuthenticatedNodeRoute(\n api,\n \"GET\",\n \"/attachments/:hash\",\n makeDownloadHandler(attachments),\n );\n}\n","import { EnvVarProvider } from \"@openfeature/env-var-provider\";\nimport { OpenFeature } from \"@openfeature/server-sdk\";\n\nexport async function initFeatureFlags() {\n // for now, we're only using env vars for feature flags\n const provider = new EnvVarProvider();\n\n await OpenFeature.setProviderAndWait(provider);\n\n return OpenFeature.getClient();\n}\n","import type { SignerConfig } from \"@powerhousedao/reactor\";\nimport {\n createSignatureVerifier,\n DEFAULT_RENOWN_URL,\n NodeKeyStorage,\n RenownBuilder,\n RenownCryptoBuilder,\n type IRenown,\n} from \"@renown/sdk/node\";\nimport { childLogger } from \"document-model\";\n\nconst logger = childLogger([\"switchboard\", \"renown\"]);\n\nexport interface RenownOptions {\n /** Path to the keypair file. Defaults to .ph/.keypair.json in cwd */\n keypairPath?: string;\n /** If true, won't generate a new keypair if none exists */\n requireExisting?: boolean;\n /** Base url of the Renown instance to use */\n baseUrl?: string;\n}\n\n/**\n * Initialize Renown for the Switchboard instance.\n * This allows Switchboard to authenticate with remote services\n * using the same identity established during `ph login`.\n */\nexport async function initRenown(\n options: RenownOptions = {},\n): Promise<IRenown | null> {\n const {\n keypairPath,\n requireExisting = false,\n baseUrl = DEFAULT_RENOWN_URL,\n } = options;\n\n const keyStorage = new NodeKeyStorage(keypairPath, {\n logger,\n });\n\n // Check if we have an existing keypair\n const existingKeyPair = await keyStorage.loadKeyPair();\n\n if (!existingKeyPair && requireExisting) {\n throw new Error(\n \"No existing keypair found and requireExisting is true. \" +\n 'Run \"ph login\" to create one.',\n );\n }\n\n if (!existingKeyPair) {\n logger.info(\"No existing keypair found. A new one will be generated.\");\n }\n\n const renownCrypto = await new RenownCryptoBuilder()\n .withKeyPairStorage(keyStorage)\n .build();\n\n const renown = await new RenownBuilder(\"switchboard\", {})\n .withCrypto(renownCrypto)\n .withBaseUrl(baseUrl)\n .build();\n\n logger.info(\"Switchboard identity initialized: @did\", renownCrypto.did);\n\n return renown;\n}\n\n/**\n * Get the signer config for the given renown instance.\n *\n * @param renown - The renown instance\n * @param requireSignature - If true, unsigned actions are rejected\n */\nexport function getRenownSignerConfig(\n renown: IRenown,\n requireSignature?: boolean,\n): SignerConfig {\n return {\n signer: renown.signer,\n verifier: createSignatureVerifier(requireSignature),\n };\n}\n","#!/usr/bin/env node\nimport { PGlite } from \"@electric-sql/pglite\";\nimport { metrics } from \"@opentelemetry/api\";\nimport { getConfig } from \"@powerhousedao/config/node\";\nimport { ReactorInstrumentation } from \"@powerhousedao/opentelemetry-instrumentation-reactor\";\nimport {\n ChannelScheme,\n EventBus,\n ReactorBuilder,\n ReactorClientBuilder,\n driveCollectionId,\n parseDriveUrl,\n type Database,\n} from \"@powerhousedao/reactor\";\nimport {\n HttpPackageLoader,\n ImportPackageLoader,\n PackageManagementService,\n PackagesSubgraph,\n getUniqueDocumentModels,\n initializeAndStartAPI,\n type IPackageLoader,\n} from \"@powerhousedao/reactor-api\";\nimport { httpsHooksPath } from \"@powerhousedao/reactor-api/https-hooks\";\nimport {\n VitePackageLoader,\n createViteLogger,\n startViteServer,\n} from \"@powerhousedao/reactor-api/vite\";\nimport { driveDocumentModelModule } from \"@powerhousedao/shared/document-drive\";\nimport type { DocumentModelModule } from \"@powerhousedao/shared/document-model\";\nimport { documentModels as vetraDocumentModels } from \"@powerhousedao/vetra\";\nimport { processorFactory as vetraProcessorFactory } from \"@powerhousedao/vetra/processors\";\nimport type { IRenown } from \"@renown/sdk/node\";\nimport * as Sentry from \"@sentry/node\";\nimport {\n childLogger,\n documentModelDocumentModelModule,\n setLogLevel,\n type ILogger,\n} from \"document-model\";\nimport dotenv from \"dotenv\";\nimport { Kysely, PostgresDialect } from \"kysely\";\nimport { PGliteDialect } from \"kysely-pglite-dialect\";\nimport net from \"node:net\";\nimport { register } from \"node:module\";\nimport path from \"path\";\nimport { Pool } from \"pg\";\nimport { registerAttachmentRoutes } from \"./attachments/index.js\";\nimport { initFeatureFlags } from \"./feature-flags.js\";\nimport { getRenownSignerConfig, initRenown } from \"./renown.js\";\nimport type { StartServerOptions, SwitchboardReactor } from \"./types.js\";\nimport { addDefaultDrive, isPostgresUrl } from \"./utils.mjs\";\n\nconst defaultLogger = childLogger([\"switchboard\"]);\n\nconst LogLevel = (process.env.LOG_LEVEL as ILogger[\"level\"] | \"\") || \"info\";\nsetLogLevel(LogLevel);\n\ndotenv.config();\n\n// Feature flag constants\nconst DOCUMENT_MODEL_SUBGRAPHS_ENABLED = \"DOCUMENT_MODEL_SUBGRAPHS_ENABLED\";\nconst DOCUMENT_MODEL_SUBGRAPHS_ENABLED_DEFAULT = true;\nconst REQUIRE_SIGNATURES = \"REQUIRE_SIGNATURES\";\nconst REQUIRE_SIGNATURES_DEFAULT = false;\n\nif (process.env.SENTRY_DSN) {\n defaultLogger.info(\n \"Initialized Sentry with env: @env\",\n process.env.SENTRY_ENV,\n );\n Sentry.init({\n dsn: process.env.SENTRY_DSN,\n environment: process.env.SENTRY_ENV,\n // Match the version tag uploaded by release-branch.yml so source maps\n // resolve. Populated by the CI (WORKSPACE_VERSION) or npm at runtime.\n release:\n process.env.SENTRY_RELEASE ||\n (process.env.npm_package_version\n ? `v${process.env.npm_package_version}`\n : undefined),\n });\n}\n\nconst DEFAULT_PORT = process.env.PORT ? Number(process.env.PORT) : 4001;\n\n// How many ports forward from the requested one we will try before giving up.\nconst PORT_FALLBACK_ATTEMPTS = 20;\n\n/**\n * Attempt to bind a throwaway TCP server to the given port. Resolves true if\n * the port is free, false if the OS reports it in use. Any other error is\n * surfaced so we don't silently mask real issues (permissions, bad host, …).\n */\nexport function isPortAvailable(port: number): Promise<boolean> {\n return new Promise((resolve, reject) => {\n const tester = net.createServer();\n tester.once(\"error\", (err: NodeJS.ErrnoException) => {\n if (err.code === \"EADDRINUSE\" || err.code === \"EACCES\") {\n resolve(false);\n } else {\n reject(err);\n }\n });\n tester.once(\"listening\", () => {\n tester.close(() => resolve(true));\n });\n // Bind on the unspecified IPv6 address so we detect collisions with both\n // IPv6 and IPv4 listeners (Node maps `::` to dual-stack on most systems).\n tester.listen({ port, host: \"::\" });\n });\n}\n\nasync function resolveServerPort(\n requested: number,\n strictPort: boolean,\n logger: ILogger,\n): Promise<number> {\n if (strictPort) return requested;\n for (let i = 0; i < PORT_FALLBACK_ATTEMPTS; i++) {\n const candidate = requested + i;\n if (await isPortAvailable(candidate)) {\n if (candidate !== requested) {\n logger.info(\n `Port ${requested} is in use. Falling back to port ${candidate}.`,\n );\n }\n return candidate;\n }\n }\n // Couldn't find a free port in the window; let the caller surface the\n // original EADDRINUSE when the real bind attempts runs.\n return requested;\n}\n\nasync function initServer(\n serverPort: number,\n options: StartServerOptions,\n renown: IRenown | null,\n) {\n // Register the global MeterProvider before ReactorInstrumentation is\n // constructed. setGlobalMeterProvider is a one-way door — once set it cannot\n // be unset — so this must happen before initializeClient calls\n // instrumentation.start() → createMetrics() → metrics.getMeter().\n if (options.meterProvider) {\n metrics.setGlobalMeterProvider(options.meterProvider);\n }\n\n const {\n dev,\n packages = [],\n remoteDrives = [],\n logger = defaultLogger,\n } = options;\n logger.level = LogLevel;\n const dbPath = options.dbPath ?? process.env.DATABASE_URL;\n\n // use postgres url for read model storage if available, otherwise use local PGlite path\n const readModelPath = dbPath || \".ph/read-storage\";\n\n // HTTP registry package loading\n const configPath =\n options.configFile ?? path.join(process.cwd(), \"powerhouse.config.json\");\n const config = getConfig(configPath);\n const registryUrl = process.env.PH_REGISTRY_URL ?? config.packageRegistryUrl;\n const registryPackages = process.env.PH_REGISTRY_PACKAGES;\n const dynamicModelLoading =\n options.dynamicModelLoading ?? process.env.DYNAMIC_MODEL_LOADING === \"true\";\n let httpLoader: HttpPackageLoader | undefined;\n\n if (registryUrl) {\n // Register HTTP/HTTPS module loader hooks for dynamic package imports\n register(httpsHooksPath, import.meta.url);\n httpLoader = new HttpPackageLoader({ registryUrl });\n registryPackages?.split(\",\").forEach((p) => {\n const name = p.trim();\n if (!packages.includes(name)) {\n packages.push(name);\n }\n });\n }\n\n const reactorLogger = logger.child([\"reactor\"]);\n const initializeClient = async (documentModels: DocumentModelModule[]) => {\n const eventBus = new EventBus();\n const builder = new ReactorBuilder()\n .withEventBus(eventBus)\n .withDocumentModels(\n getUniqueDocumentModels([\n documentModelDocumentModelModule,\n driveDocumentModelModule,\n ...vetraDocumentModels,\n ...documentModels,\n ]),\n )\n .withChannelScheme(ChannelScheme.SWITCHBOARD)\n .withSignalHandlers()\n .withLogger(reactorLogger);\n\n const maxSkipThreshold = parseInt(process.env.MAX_SKIP_THRESHOLD ?? \"\", 10);\n if (!isNaN(maxSkipThreshold) && maxSkipThreshold > 0) {\n builder.withExecutorConfig({ maxSkipThreshold });\n logger.info(`Reactor maxSkipThreshold set to ${maxSkipThreshold}`);\n }\n\n const reactorDbUrl = process.env.PH_REACTOR_DATABASE_URL;\n if (reactorDbUrl && isPostgresUrl(reactorDbUrl)) {\n const connectionString = reactorDbUrl.includes(\"?\")\n ? reactorDbUrl\n : `${reactorDbUrl}?sslmode=disable`;\n const pool = new Pool({ connectionString });\n const kysely = new Kysely<Database>({\n dialect: new PostgresDialect({ pool }),\n });\n builder.withKysely(kysely);\n logger.info(\"Using PostgreSQL for reactor storage\");\n } else {\n const pglitePath = \"./.ph/reactor-storage\";\n const pglite = new PGlite(pglitePath);\n const kysely = new Kysely<Database>({\n dialect: new PGliteDialect(pglite),\n });\n builder.withKysely(kysely);\n logger.info(\"Using PGlite for reactor storage\");\n }\n\n if (httpLoader && dynamicModelLoading) {\n builder.withDocumentModelLoader(httpLoader.documentModelLoader);\n }\n\n const clientBuilder = new ReactorClientBuilder().withReactorBuilder(\n builder,\n );\n\n if (renown) {\n const signerConfig = getRenownSignerConfig(\n renown,\n options.identity?.requireSignatures,\n );\n clientBuilder.withSigner(signerConfig);\n }\n\n const module = await clientBuilder.buildModule();\n\n if (module.reactorModule) {\n const instrumentation = new ReactorInstrumentation(module.reactorModule);\n instrumentation.start();\n reactorLogger.info(\"Reactor metrics instrumentation started\");\n }\n\n return module;\n };\n\n let defaultDriveUrl: undefined | string = undefined;\n\n // TODO get path from powerhouse config\n // start vite server if dev mode is enabled\n const basePath = process.cwd();\n const viteLogger = createViteLogger(logger);\n const vite = dev\n ? await startViteServer(process.cwd(), viteLogger)\n : undefined;\n\n // get paths to local document models\n if (!options.disableLocalPackages) {\n packages.push(basePath);\n }\n\n // create loaders\n const packageLoaders: IPackageLoader[] = [];\n if (vite) {\n packageLoaders.push(VitePackageLoader.build(vite));\n } else {\n packageLoaders.push(new ImportPackageLoader());\n }\n if (httpLoader) {\n packageLoaders.push(httpLoader);\n registryPackages?.split(\",\").forEach((p) => {\n const name = p.trim();\n if (!packages.includes(name)) {\n packages.push(name);\n }\n });\n }\n\n const apiLogger = logger.child([\"reactor-api\"]);\n const api = await initializeAndStartAPI(\n initializeClient,\n {\n port: serverPort,\n dbPath: readModelPath,\n https: options.https,\n packageLoaders: packageLoaders.length > 0 ? packageLoaders : undefined,\n packages: packages,\n processorConfig: options.processorConfig,\n processors: {\n \"@powerhousedao/vetra\": [vetraProcessorFactory],\n },\n configFile:\n options.configFile ??\n path.join(process.cwd(), \"powerhouse.config.json\"),\n mcp: options.mcp ?? true,\n logger: apiLogger,\n enableDocumentModelSubgraphs: options.enableDocumentModelSubgraphs,\n },\n \"switchboard\",\n );\n\n registerAttachmentRoutes(api);\n\n if (process.env.SENTRY_DSN) {\n // Register Sentry error handler after all routes are established.\n // The adapter calls the framework-specific Sentry setup internally.\n api.httpAdapter.setupSentryErrorHandler(Sentry);\n }\n\n const { client, graphqlManager, documentModelRegistry } = api;\n\n // Wire up dynamic package management if HTTP loader is configured\n if (httpLoader) {\n const packageManagementService = new PackageManagementService({\n defaultRegistryUrl: registryUrl,\n httpLoader,\n documentModelRegistry,\n });\n\n packageManagementService.setOnModelsChanged(() => {\n graphqlManager.regenerateDocumentModelSubgraphs().catch(logger.error);\n });\n\n const packagesSubgraph = new PackagesSubgraph({\n relationalDb: undefined as never,\n analyticsStore: undefined as never,\n reactorClient: client,\n graphqlManager,\n syncManager: api.syncManager,\n path: graphqlManager.getBasePath(),\n packageManagementService,\n });\n\n void graphqlManager\n .registerSubgraphInstance(packagesSubgraph, \"graphql\", false)\n .then(() => graphqlManager.updateRouter())\n .catch((error: unknown) => {\n logger.error(\"Failed to register packages subgraph: @error\", error);\n });\n }\n\n // Create default drive if provided\n if (options.drive) {\n if (!renown) {\n throw new Error(\"Cannot create default drive without Renown identity\");\n }\n\n defaultDriveUrl = await addDefaultDrive(client, options.drive, serverPort);\n }\n\n // add vite middleware after express app is initialized if applicable\n if (vite) {\n api.httpAdapter.mountRawMiddleware(vite.middlewares);\n }\n\n // Connect to remote drives AFTER packages are loaded\n if (remoteDrives.length > 0) {\n for (const remoteDriveUrl of remoteDrives) {\n let driveId: string | undefined;\n\n try {\n const { syncManager } = api;\n const parsed = parseDriveUrl(remoteDriveUrl);\n driveId = parsed.driveId;\n const remoteName = `remote-drive-${driveId}-${crypto.randomUUID()}`;\n await syncManager.add(remoteName, driveCollectionId(\"main\", driveId), {\n type: \"gql\",\n parameters: { url: parsed.graphqlEndpoint },\n });\n logger.debug(\"Remote drive @remoteDriveUrl synced\", remoteDriveUrl);\n } catch (error) {\n if (\n error instanceof Error &&\n error.message.includes(\"already exists\")\n ) {\n logger.debug(\n \"Remote drive already added: @remoteDriveUrl\",\n remoteDriveUrl,\n );\n driveId = remoteDriveUrl.split(\"/\").pop();\n } else {\n logger.error(\n \"Failed to connect to remote drive @remoteDriveUrl: @error\",\n remoteDriveUrl,\n error,\n );\n }\n } finally {\n // Construct local URL once in finally block\n if (!defaultDriveUrl && driveId) {\n const protocol = options.https ? \"https\" : \"http\";\n defaultDriveUrl = `${protocol}://localhost:${serverPort}/d/${driveId}`;\n }\n }\n }\n }\n\n return {\n defaultDriveUrl,\n api,\n reactor: client,\n renown,\n port: serverPort,\n };\n}\n\nexport const startSwitchboard = async (\n options: StartServerOptions = {},\n): Promise<SwitchboardReactor> => {\n const requestedPort = options.port ?? DEFAULT_PORT;\n const logger = options.logger ?? defaultLogger;\n const serverPort = await resolveServerPort(\n requestedPort,\n options.strictPort ?? false,\n logger,\n );\n\n // Initialize feature flags\n const featureFlags = await initFeatureFlags();\n\n const enableDocumentModelSubgraphs = await featureFlags.getBooleanValue(\n DOCUMENT_MODEL_SUBGRAPHS_ENABLED,\n options.enableDocumentModelSubgraphs ??\n DOCUMENT_MODEL_SUBGRAPHS_ENABLED_DEFAULT,\n );\n\n options.enableDocumentModelSubgraphs = enableDocumentModelSubgraphs;\n\n const requireSignatures =\n options.identity?.requireSignatures ??\n (await featureFlags.getBooleanValue(\n REQUIRE_SIGNATURES,\n REQUIRE_SIGNATURES_DEFAULT,\n ));\n options.identity = { ...options.identity, requireSignatures };\n\n logger.info(\n \"Feature flags: @flags\",\n JSON.stringify(\n {\n DOCUMENT_MODEL_SUBGRAPHS_ENABLED: enableDocumentModelSubgraphs,\n REQUIRE_SIGNATURES: requireSignatures,\n },\n null,\n 2,\n ),\n );\n\n // Initialize Renown if identity options are provided or keypair exists\n let renown: IRenown | null = null;\n try {\n renown = await initRenown(options.identity);\n } catch (e) {\n logger.warn(\"Failed to initialize ConnectCrypto: @error\", e);\n if (options.identity?.requireExisting) {\n throw new Error(\n 'Identity required but failed to initialize. Run \"ph login\" first.',\n );\n }\n }\n\n try {\n return await initServer(serverPort, options, renown);\n } catch (e) {\n Sentry.captureException(e);\n logger.error(\"App crashed: @error\", e);\n throw e;\n }\n};\n\nexport * from \"./types.js\";\n\nif (import.meta.main) {\n await startSwitchboard();\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.mts","names":[],"sources":["../src/types.ts","../src/server.mts"],"mappings":";;;;;;;KAMY,cAAA;EACV,IAAA;EACA,cAAA;EACA,WAAA;AAAA;AAAA,KAGU,eAAA;EALV,+DAOA,WAAA;EALA;;;AAGF;EAOE,eAAA;EAGA,OAAA,WARA;EAWA,iBAAA;AAAA;AAAA,KAGU,kBAAA;EACV,UAAA;EACA,IAAA;EAFU;;;;;EAQV,UAAA;EACA,GAAA;EACA,MAAA;EACA,KAAA,GAAQ,UAAA;EACR,QAAA;EACA,YAAA;EACA,KAAA;IAEM,OAAA;IACA,QAAA;EAAA;EAIN,IAAA;IACE,OAAA;IACA,MAAA;IACA,KAAA;IACA,MAAA;EAAA;EARI;;;;;EAeN,QAAA,GAAW,eAAA;EACX,GAAA;EACA,eAAA,GAAkB,GAAA;EAClB,oBAAA;EACA,4BAAA;EAFkB;;;;;EAQlB,mBAAA;EACA,MAAA,GAAS,OAAA;EAOO;;;AAGlB;;;EAHE,aAAA,GAAgB,aAAA;AAAA;AAAA,KAGN,kBAAA;EACV,eAAA;EACA,OAAA,EAAS,cAAA,EAED;EAAR,MAAA,EAAQ,OAAA;EAKJ;;;;EAAJ,IAAA;AAAA;;;;;;;;iBCIc,eAAA,CAAgB,IAAA,WAAe,OAAA;AAAA,cA6TlC,gBAAA,GACX,OAAA,GAAS,kBAAA,KACR,OAAA,CAAQ,kBAAA"}
1
+ {"version":3,"file":"server.d.mts","names":[],"sources":["../src/types.ts","../src/server.mts"],"mappings":";;;;;;;KAMY,cAAA;EACV,IAAA;EACA,cAAA;EACA,WAAA;AAAA;AAAA,KAGU,eAAA;EALV,+DAOA,WAAA;EALA;;;AAGF;EAOE,eAAA;EAGA,OAAA,WARA;EAWA,iBAAA;AAAA;AAAA,KAGU,kBAAA;EACV,UAAA;EACA,IAAA;EAFU;;;;;EAQV,UAAA;EACA,GAAA;EACA,MAAA;EACA,KAAA,GAAQ,UAAA;EACR,QAAA;EACA,YAAA;EACA,KAAA;IAEM,OAAA;IACA,QAAA;EAAA;EAIN,IAAA;IACE,OAAA;IACA,MAAA;IACA,KAAA;IACA,MAAA;EAAA;EARI;;;;;EAeN,QAAA,GAAW,eAAA;EACX,GAAA;EACA,eAAA,GAAkB,GAAA;EAClB,oBAAA;EACA,4BAAA;EAFkB;;;;;EAQlB,mBAAA;EACA,MAAA,GAAS,OAAA;EAOO;;;AAGlB;;;EAHE,aAAA,GAAgB,aAAA;AAAA;AAAA,KAGN,kBAAA;EACV,eAAA;EACA,OAAA,EAAS,cAAA,EAED;EAAR,MAAA,EAAQ,OAAA;EAKJ;;;;EAAJ,IAAA;AAAA;;;;;;;;iBCKc,eAAA,CAAgB,IAAA,WAAe,OAAA;AAAA,cA+TlC,gBAAA,GACX,OAAA,GAAS,kBAAA,KACR,OAAA,CAAQ,kBAAA"}
package/dist/server.mjs CHANGED
@@ -1,4 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import { n as startSwitchboard, t as isPortAvailable } from "./server-BMtyzhoR.mjs";
2
+ import { n as startSwitchboard, t as isPortAvailable } from "./server-8U7q7B7r.mjs";
3
3
  import "./utils-DFl0ezBT.mjs";
4
4
  export { isPortAvailable, startSwitchboard };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@powerhousedao/switchboard",
3
3
  "type": "module",
4
- "version": "6.0.0-dev.207",
4
+ "version": "6.0.0-dev.209",
5
5
  "main": "dist/index.mjs",
6
6
  "exports": {
7
7
  ".": {
@@ -47,14 +47,15 @@
47
47
  "kysely-pglite-dialect": "1.2.0",
48
48
  "pg": "8.18.0",
49
49
  "vite": "8.0.8",
50
- "@powerhousedao/config": "6.0.0-dev.207",
51
- "@powerhousedao/opentelemetry-instrumentation-reactor": "6.0.0-dev.207",
52
- "@powerhousedao/reactor": "6.0.0-dev.207",
53
- "@powerhousedao/shared": "6.0.0-dev.207",
54
- "@powerhousedao/vetra": "6.0.0-dev.207",
55
- "@renown/sdk": "6.0.0-dev.207",
56
- "@powerhousedao/reactor-api": "6.0.0-dev.207",
57
- "document-model": "6.0.0-dev.207"
50
+ "@powerhousedao/config": "6.0.0-dev.209",
51
+ "@powerhousedao/shared": "6.0.0-dev.209",
52
+ "@powerhousedao/reactor": "6.0.0-dev.209",
53
+ "@powerhousedao/vetra": "6.0.0-dev.209",
54
+ "@powerhousedao/reactor-api": "6.0.0-dev.209",
55
+ "@powerhousedao/reactor-attachments": "6.0.0-dev.209",
56
+ "@renown/sdk": "6.0.0-dev.209",
57
+ "document-model": "6.0.0-dev.209",
58
+ "@powerhousedao/opentelemetry-instrumentation-reactor": "6.0.0-dev.209"
58
59
  },
59
60
  "devDependencies": {
60
61
  "@types/express": "^4.17.25",
@@ -0,0 +1,219 @@
1
+ import type { AuthService } from "@powerhousedao/reactor-api";
2
+ import type { IncomingMessage, ServerResponse } from "node:http";
3
+ import { describe, expect, it, vi } from "vitest";
4
+ import { requireAuth, type NodeHandler } from "../../src/attachments/auth.js";
5
+
6
+ type CapturedRes = ServerResponse & {
7
+ _headers: Record<string, string>;
8
+ _body: string;
9
+ _ended: boolean;
10
+ };
11
+
12
+ function makeReq(opts: {
13
+ method?: string;
14
+ url?: string;
15
+ headers?: Record<string, string>;
16
+ }): IncomingMessage {
17
+ return {
18
+ method: opts.method ?? "POST",
19
+ url: opts.url ?? "/attachments/reservations/abc",
20
+ headers: opts.headers ?? {},
21
+ } as unknown as IncomingMessage;
22
+ }
23
+
24
+ function makeRes(): CapturedRes {
25
+ const headers: Record<string, string> = {};
26
+ let body = "";
27
+ let ended = false;
28
+ const res = {
29
+ statusCode: 200,
30
+ setHeader(name: string, value: string | number | readonly string[]) {
31
+ headers[name.toLowerCase()] = String(value);
32
+ },
33
+ getHeader(name: string) {
34
+ return headers[name.toLowerCase()];
35
+ },
36
+ end(chunk?: string | Buffer) {
37
+ if (chunk !== undefined) {
38
+ body += typeof chunk === "string" ? chunk : chunk.toString("utf8");
39
+ }
40
+ ended = true;
41
+ },
42
+ } as unknown as CapturedRes;
43
+ Object.defineProperty(res, "_headers", { get: () => headers });
44
+ Object.defineProperty(res, "_body", { get: () => body });
45
+ Object.defineProperty(res, "_ended", { get: () => ended });
46
+ return res;
47
+ }
48
+
49
+ function makeAuthService(
50
+ impl: (authorization: string | undefined) => Promise<unknown>,
51
+ ): {
52
+ service: AuthService;
53
+ spy: ReturnType<typeof vi.fn>;
54
+ } {
55
+ const spy = vi.fn(
56
+ impl as (authorization: string | undefined) => Promise<unknown>,
57
+ );
58
+ const service = { verifyBearer: spy } as unknown as AuthService;
59
+ return { service, spy };
60
+ }
61
+
62
+ describe("requireAuth", () => {
63
+ it("returns the original handler unchanged when authService is undefined", async () => {
64
+ const handler: NodeHandler = vi.fn();
65
+ const wrapped = requireAuth(undefined, handler);
66
+ expect(wrapped).toBe(handler);
67
+ });
68
+
69
+ it("invokes the handler when authService is undefined (auth disabled path)", async () => {
70
+ const handler = vi.fn<NodeHandler>();
71
+ const wrapped = requireAuth(undefined, handler);
72
+ const req = makeReq({});
73
+ const res = makeRes();
74
+ await wrapped(req, res);
75
+ expect(handler).toHaveBeenCalledTimes(1);
76
+ expect(handler).toHaveBeenCalledWith(req, res);
77
+ });
78
+
79
+ it("returns 401 with { error: 'Authentication required' } when Authorization header is missing", async () => {
80
+ const { service, spy } = makeAuthService(async () => ({
81
+ user: undefined,
82
+ admins: [],
83
+ auth_enabled: true,
84
+ }));
85
+ const handler = vi.fn<NodeHandler>();
86
+ const wrapped = requireAuth(service, handler);
87
+
88
+ const res = makeRes();
89
+ await wrapped(makeReq({ headers: {} }), res);
90
+
91
+ expect(res.statusCode).toBe(401);
92
+ expect(res._headers["content-type"]).toBe("application/json");
93
+ expect(JSON.parse(res._body)).toEqual({ error: "Authentication required" });
94
+ expect(handler).not.toHaveBeenCalled();
95
+ expect(spy).toHaveBeenCalledTimes(1);
96
+ });
97
+
98
+ it("forwards a Response from AuthService (status, content-type, body) for an invalid bearer token", async () => {
99
+ const { service } = makeAuthService(
100
+ async () =>
101
+ new Response(JSON.stringify({ error: "Verification failed" }), {
102
+ status: 401,
103
+ headers: { "content-type": "application/json" },
104
+ }),
105
+ );
106
+ const handler = vi.fn<NodeHandler>();
107
+ const wrapped = requireAuth(service, handler);
108
+
109
+ const res = makeRes();
110
+ await wrapped(
111
+ makeReq({ headers: { authorization: "Bearer bad-token" } }),
112
+ res,
113
+ );
114
+
115
+ expect(res.statusCode).toBe(401);
116
+ expect(res._headers["content-type"]).toBe("application/json");
117
+ expect(JSON.parse(res._body)).toEqual({ error: "Verification failed" });
118
+ expect(handler).not.toHaveBeenCalled();
119
+ });
120
+
121
+ it("forwards a Response with a non-JSON content-type verbatim", async () => {
122
+ const { service } = makeAuthService(
123
+ async () =>
124
+ new Response("nope", {
125
+ status: 403,
126
+ headers: { "content-type": "text/plain" },
127
+ }),
128
+ );
129
+ const handler = vi.fn<NodeHandler>();
130
+ const wrapped = requireAuth(service, handler);
131
+
132
+ const res = makeRes();
133
+ await wrapped(
134
+ makeReq({ headers: { authorization: "Bearer bad-token" } }),
135
+ res,
136
+ );
137
+
138
+ expect(res.statusCode).toBe(403);
139
+ expect(res._headers["content-type"]).toBe("text/plain");
140
+ expect(res._body).toBe("nope");
141
+ expect(handler).not.toHaveBeenCalled();
142
+ });
143
+
144
+ it("invokes the handler and leaves the response untouched on a valid bearer token", async () => {
145
+ const { service } = makeAuthService(async () => ({
146
+ user: { address: "0x123", chainId: 1, networkId: "mainnet" },
147
+ admins: [],
148
+ auth_enabled: true,
149
+ }));
150
+ const handler = vi.fn<NodeHandler>();
151
+ const wrapped = requireAuth(service, handler);
152
+
153
+ const req = makeReq({
154
+ headers: { authorization: "Bearer good-token" },
155
+ });
156
+ const res = makeRes();
157
+ await wrapped(req, res);
158
+
159
+ expect(handler).toHaveBeenCalledTimes(1);
160
+ expect(handler).toHaveBeenCalledWith(req, res);
161
+ expect(res.statusCode).toBe(200);
162
+ expect(res._body).toBe("");
163
+ expect(res._ended).toBe(false);
164
+ expect(res._headers["content-type"]).toBeUndefined();
165
+ });
166
+
167
+ it("returns 500 with a sanitized body when AuthService throws", async () => {
168
+ const { service } = makeAuthService(async () => {
169
+ throw new Error("transient Renown failure: secret-internal-detail");
170
+ });
171
+ const handler = vi.fn<NodeHandler>();
172
+ const wrapped = requireAuth(service, handler);
173
+
174
+ const res = makeRes();
175
+ await wrapped(
176
+ makeReq({ headers: { authorization: "Bearer good-token" } }),
177
+ res,
178
+ );
179
+
180
+ expect(res.statusCode).toBe(500);
181
+ expect(res._headers["content-type"]).toBe("application/json");
182
+ const parsed = JSON.parse(res._body) as { error: string };
183
+ expect(parsed.error).toBe("Internal authentication error");
184
+ expect(res._body).not.toContain("secret-internal-detail");
185
+ expect(res._body).not.toContain("Renown");
186
+ expect(handler).not.toHaveBeenCalled();
187
+ });
188
+
189
+ it("calls authService.verifyBearer with the incoming authorization header", async () => {
190
+ const { service, spy } = makeAuthService(async () => ({
191
+ user: { address: "0x1", chainId: 1, networkId: "mainnet" },
192
+ admins: [],
193
+ auth_enabled: true,
194
+ }));
195
+ const wrapped = requireAuth(service, vi.fn());
196
+
197
+ await wrapped(
198
+ makeReq({ headers: { authorization: "Bearer t" } }),
199
+ makeRes(),
200
+ );
201
+
202
+ expect(spy).toHaveBeenCalledTimes(1);
203
+ expect(spy).toHaveBeenCalledWith("Bearer t");
204
+ });
205
+
206
+ it("calls verifyBearer with undefined when no authorization header is present", async () => {
207
+ const { service, spy } = makeAuthService(async () => ({
208
+ user: undefined,
209
+ admins: [],
210
+ auth_enabled: true,
211
+ }));
212
+ const wrapped = requireAuth(service, vi.fn());
213
+
214
+ await wrapped(makeReq({ headers: {} }), makeRes());
215
+
216
+ expect(spy).toHaveBeenCalledTimes(1);
217
+ expect(spy).toHaveBeenCalledWith(undefined);
218
+ });
219
+ });