@powerhousedao/switchboard 6.0.0-dev.24 → 6.0.0-dev.241

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.
Files changed (81) hide show
  1. package/.env +5 -1
  2. package/Auth.md +45 -27
  3. package/CHANGELOG.md +1878 -5
  4. package/README.md +55 -12
  5. package/dist/index.d.mts +2 -0
  6. package/dist/index.mjs +218 -0
  7. package/dist/index.mjs.map +1 -0
  8. package/dist/install-packages.d.mts +1 -0
  9. package/dist/install-packages.mjs +34 -0
  10. package/dist/install-packages.mjs.map +1 -0
  11. package/dist/migrate.d.mts +1 -0
  12. package/dist/migrate.mjs +60 -0
  13. package/dist/migrate.mjs.map +1 -0
  14. package/dist/server-DPxV64Dh.mjs +755 -0
  15. package/dist/server-DPxV64Dh.mjs.map +1 -0
  16. package/dist/server.d.mts +105 -0
  17. package/dist/server.d.mts.map +1 -0
  18. package/dist/server.mjs +6 -0
  19. package/dist/utils-DFl0ezBT.mjs +47 -0
  20. package/dist/utils-DFl0ezBT.mjs.map +1 -0
  21. package/dist/utils.d.mts +9 -0
  22. package/dist/utils.d.mts.map +1 -0
  23. package/dist/utils.mjs +4 -0
  24. package/package.json +66 -39
  25. package/test/attachments/auth.test.ts +219 -0
  26. package/test/attachments/index.test.ts +119 -0
  27. package/test/attachments/routes-integration.test.ts +103 -0
  28. package/test/attachments/routes.test.ts +864 -0
  29. package/test/metrics.test.ts +202 -0
  30. package/test/pglite-dialect.test.ts +40 -0
  31. package/test/pglite-version.test.ts +37 -0
  32. package/tsconfig.json +12 -3
  33. package/tsdown.config.ts +16 -0
  34. package/vitest.config.ts +11 -0
  35. package/Dockerfile +0 -86
  36. package/dist/src/clients/redis.d.ts +0 -5
  37. package/dist/src/clients/redis.d.ts.map +0 -1
  38. package/dist/src/clients/redis.js +0 -48
  39. package/dist/src/clients/redis.js.map +0 -1
  40. package/dist/src/config.d.ts +0 -12
  41. package/dist/src/config.d.ts.map +0 -1
  42. package/dist/src/config.js +0 -33
  43. package/dist/src/config.js.map +0 -1
  44. package/dist/src/connect-crypto.d.ts +0 -41
  45. package/dist/src/connect-crypto.d.ts.map +0 -1
  46. package/dist/src/connect-crypto.js +0 -127
  47. package/dist/src/connect-crypto.js.map +0 -1
  48. package/dist/src/feature-flags.d.ts +0 -2
  49. package/dist/src/feature-flags.d.ts.map +0 -1
  50. package/dist/src/feature-flags.js +0 -9
  51. package/dist/src/feature-flags.js.map +0 -1
  52. package/dist/src/index.d.ts +0 -3
  53. package/dist/src/index.d.ts.map +0 -1
  54. package/dist/src/index.js +0 -21
  55. package/dist/src/index.js.map +0 -1
  56. package/dist/src/install-packages.d.ts +0 -2
  57. package/dist/src/install-packages.d.ts.map +0 -1
  58. package/dist/src/install-packages.js +0 -36
  59. package/dist/src/install-packages.js.map +0 -1
  60. package/dist/src/migrate.d.ts +0 -3
  61. package/dist/src/migrate.d.ts.map +0 -1
  62. package/dist/src/migrate.js +0 -65
  63. package/dist/src/migrate.js.map +0 -1
  64. package/dist/src/profiler.d.ts +0 -9
  65. package/dist/src/profiler.d.ts.map +0 -1
  66. package/dist/src/profiler.js +0 -43
  67. package/dist/src/profiler.js.map +0 -1
  68. package/dist/src/server.d.ts +0 -6
  69. package/dist/src/server.d.ts.map +0 -1
  70. package/dist/src/server.js +0 -304
  71. package/dist/src/server.js.map +0 -1
  72. package/dist/src/types.d.ts +0 -64
  73. package/dist/src/types.d.ts.map +0 -1
  74. package/dist/src/types.js +0 -2
  75. package/dist/src/types.js.map +0 -1
  76. package/dist/src/utils.d.ts +0 -6
  77. package/dist/src/utils.d.ts.map +0 -1
  78. package/dist/src/utils.js +0 -92
  79. package/dist/src/utils.js.map +0 -1
  80. package/dist/tsconfig.tsbuildinfo +0 -1
  81. package/entrypoint.sh +0 -17
@@ -0,0 +1,755 @@
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]="243850c6-8159-53a4-a4f4-c310a075551a")}catch(e){}}();
3
+ import { n as isPostgresUrl, t as addDefaultDrive } from "./utils-DFl0ezBT.mjs";
4
+ import { register } from "node:module";
5
+ import * as Sentry from "@sentry/node";
6
+ import { childLogger, documentModelDocumentModelModule, setLogLevel } from "document-model";
7
+ import dotenv from "dotenv";
8
+ import { getConfig } from "@powerhousedao/config/node";
9
+ import { promises } from "node:fs";
10
+ import path from "node:path";
11
+ import { ReactorInstrumentation } from "@powerhousedao/opentelemetry-instrumentation-reactor";
12
+ import { ChannelScheme, EventBus, ReactorBuilder, ReactorClientBuilder, driveCollectionId, parseDriveUrl } from "@powerhousedao/reactor";
13
+ import { HttpPackageLoader, ImportPackageLoader, PackageManagementService, PackagesSubgraph, getUniqueDocumentModels, initializeAndStartAPI } from "@powerhousedao/reactor-api";
14
+ import { httpsHooksPath } from "@powerhousedao/reactor-api/https-hooks";
15
+ import { VitePackageLoader, createViteLogger, startViteServer } from "@powerhousedao/reactor-api/vite";
16
+ import { driveDocumentModelModule } from "@powerhousedao/shared/document-drive";
17
+ import { documentModels } from "@powerhousedao/vetra";
18
+ import { processorFactory } from "@powerhousedao/vetra/processors";
19
+ import { Kysely, PostgresDialect } from "kysely";
20
+ import { PGliteDialect } from "kysely-pglite-dialect";
21
+ import net from "node:net";
22
+ import path$1 from "path";
23
+ import { Pool } from "pg";
24
+ import { AttachmentNotFound, InvalidAttachmentRef, ReservationNotFound } from "@powerhousedao/reactor-attachments";
25
+ import { Readable } from "node:stream";
26
+ import { EnvVarProvider } from "@openfeature/env-var-provider";
27
+ import { OpenFeature } from "@openfeature/server-sdk";
28
+ import { DEFAULT_RENOWN_URL, NodeKeyStorage, RenownBuilder, RenownCryptoBuilder, createSignatureVerifier } from "@renown/sdk/node";
29
+ //#region src/pglite-version.ts
30
+ const SUPPORTED_PG_MAJORS = [16, 17];
31
+ async function readPgVersionFile(dataDir) {
32
+ try {
33
+ const raw = await promises.readFile(path.join(dataDir, "PG_VERSION"), "utf8");
34
+ const major = parseInt(raw.trim(), 10);
35
+ return Number.isFinite(major) ? major : null;
36
+ } catch {
37
+ return null;
38
+ }
39
+ }
40
+ function isSupportedMajor(major) {
41
+ return SUPPORTED_PG_MAJORS.includes(major);
42
+ }
43
+ /**
44
+ * Parses the `PH_FORCE_PG_VERSION` env var. Returns the validated major, or
45
+ * `null` when the var is unset/empty. Throws on any value that is not a
46
+ * supported major — invalid configuration must fail before the server starts
47
+ * touching disk.
48
+ */
49
+ function parseForcePgVersion(raw) {
50
+ if (raw === void 0 || raw.trim() === "") return null;
51
+ const parsed = Number(raw);
52
+ if (Number.isInteger(parsed) && isSupportedMajor(parsed)) return parsed;
53
+ throw new Error(`PH_FORCE_PG_VERSION must be one of: ${SUPPORTED_PG_MAJORS.join(", ")} (got: ${raw})`);
54
+ }
55
+ async function loadPGliteModule(major) {
56
+ if (major === 16) return await import("pglite-legacy-02");
57
+ return import("@electric-sql/pglite");
58
+ }
59
+ async function loadPgDump(major) {
60
+ if (major === 16) return (await import("pglite-tools-legacy-02/pg_dump")).pgDump;
61
+ return (await import("@electric-sql/pglite-tools/pg_dump")).pgDump;
62
+ }
63
+ //#endregion
64
+ //#region src/pglite-dialect.ts
65
+ var ClosablePGliteDialect = class extends PGliteDialect {
66
+ #pglite;
67
+ constructor(pglite) {
68
+ super(pglite);
69
+ this.#pglite = pglite;
70
+ }
71
+ createDriver() {
72
+ const driver = super.createDriver();
73
+ const pglite = this.#pglite;
74
+ const innerDestroy = driver.destroy.bind(driver);
75
+ driver.destroy = async () => {
76
+ await innerDestroy();
77
+ if (!pglite.closed) await pglite.close();
78
+ };
79
+ return driver;
80
+ }
81
+ };
82
+ //#endregion
83
+ //#region src/attachments/auth.ts
84
+ /**
85
+ * Wrap a Node-style handler so that, when `authService` is provided and auth is
86
+ * enabled, the request must carry a verifiable Bearer token.
87
+ */
88
+ function requireAuth(authService, handler) {
89
+ if (!authService) return handler;
90
+ return async (req, res) => {
91
+ let result;
92
+ try {
93
+ result = await authService.verifyBearer(req.headers.authorization);
94
+ } catch {
95
+ res.statusCode = 500;
96
+ res.setHeader("Content-Type", "application/json");
97
+ res.end(JSON.stringify({ error: "Internal authentication error" }));
98
+ return;
99
+ }
100
+ if (result instanceof Response) {
101
+ const body = await result.text();
102
+ res.statusCode = result.status;
103
+ const contentType = result.headers.get("content-type");
104
+ if (contentType) res.setHeader("Content-Type", contentType);
105
+ res.end(body);
106
+ return;
107
+ }
108
+ if (result.auth_enabled && !result.user) {
109
+ res.statusCode = 401;
110
+ res.setHeader("Content-Type", "application/json");
111
+ res.end(JSON.stringify({ error: "Authentication required" }));
112
+ return;
113
+ }
114
+ await handler(req, res);
115
+ };
116
+ }
117
+ //#endregion
118
+ //#region src/attachments/mount-auth.ts
119
+ /**
120
+ * Mount a Node-style attachment route with `requireAuth` applied unconditionally.
121
+ * When `api.authService` is undefined (auth disabled), `requireAuth` returns the
122
+ * handler unchanged — that is the only way to opt out. To register a route
123
+ * without auth wrapping you must call `api.httpAdapter.mountNodeRoute` directly.
124
+ */
125
+ function mountAuthenticatedNodeRoute(api, method, path, handler) {
126
+ api.httpAdapter.mountNodeRoute(method, path, requireAuth(api.authService, handler));
127
+ }
128
+ //#endregion
129
+ //#region src/attachments/routes.ts
130
+ const logger$1 = childLogger(["switchboard", "attachments"]);
131
+ const HASH_PATTERN = /^[a-f0-9]{64}$/i;
132
+ const CONTROL_CHARS = /[\x00-\x1f\x7f]/;
133
+ const MIME_TYPE_PATTERN = /^[!#$%&'*+\-.^_`|~\w]+\/[!#$%&'*+\-.^_`|~\w]+(?:\s*;\s*[!#$%&'*+\-.^_`|~\w]+=(?:[!#$%&'*+\-.^_`|~\w]+|"(?:[^"\\\r\n]|\\[^\r\n])*"))*$/;
134
+ const MAX_FILENAME_LEN = 255;
135
+ const MAX_MIMETYPE_LEN = 255;
136
+ function sendJson(res, status, body) {
137
+ res.statusCode = status;
138
+ res.setHeader("Content-Type", "application/json");
139
+ res.end(JSON.stringify(body));
140
+ }
141
+ function sendError(res, status, message) {
142
+ sendJson(res, status, { error: message });
143
+ }
144
+ function statusForError(err) {
145
+ if (err instanceof AttachmentNotFound) return 404;
146
+ if (err instanceof ReservationNotFound) return 404;
147
+ if (err instanceof InvalidAttachmentRef) return 400;
148
+ return 500;
149
+ }
150
+ function sendErrorFromException(res, err) {
151
+ const status = statusForError(err);
152
+ if (status >= 500) {
153
+ logger$1.error("Attachment route error: @error", err);
154
+ sendError(res, status, "Internal error");
155
+ return;
156
+ }
157
+ sendError(res, status, err instanceof Error ? err.message : String(err));
158
+ }
159
+ async function readJsonBody(req, body) {
160
+ if (body !== void 0 && body !== null && typeof body === "object") return body;
161
+ const chunks = [];
162
+ for await (const chunk of req) chunks.push(chunk);
163
+ if (chunks.length === 0) return void 0;
164
+ const text = Buffer.concat(chunks).toString("utf8");
165
+ if (text.length === 0) return void 0;
166
+ return JSON.parse(text);
167
+ }
168
+ function parseReserveOptions(input) {
169
+ if (input === null || typeof input !== "object") return null;
170
+ const obj = input;
171
+ if (typeof obj.mimeType !== "string" || obj.mimeType.length === 0 || obj.mimeType.length > MAX_MIMETYPE_LEN || !MIME_TYPE_PATTERN.test(obj.mimeType)) return null;
172
+ if (typeof obj.fileName !== "string" || obj.fileName.length === 0 || obj.fileName.length > MAX_FILENAME_LEN || CONTROL_CHARS.test(obj.fileName)) return null;
173
+ let extension = null;
174
+ if (typeof obj.extension === "string") {
175
+ if (obj.extension.length === 0 || /[\\/]/.test(obj.extension)) return null;
176
+ extension = obj.extension;
177
+ } else if (obj.extension !== void 0 && obj.extension !== null) return null;
178
+ return {
179
+ mimeType: obj.mimeType,
180
+ fileName: obj.fileName,
181
+ extension
182
+ };
183
+ }
184
+ function quoteFilename(name) {
185
+ return `"${name.replace(/[\\"]/g, "\\$&")}"`;
186
+ }
187
+ function buildContentDisposition(fileName) {
188
+ const ascii = fileName.replace(/[^\x20-\x21\x23-\x5b\x5d-\x7e]/g, "_");
189
+ const encoded = encodeURIComponent(fileName).replace(/['()*!]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);
190
+ return `attachment; filename=${quoteFilename(ascii)}; filename*=UTF-8''${encoded}`;
191
+ }
192
+ function makeReserveHandler(attachments) {
193
+ return async (req, res, body) => {
194
+ let parsed;
195
+ try {
196
+ parsed = await readJsonBody(req, body);
197
+ } catch {
198
+ sendError(res, 400, "Invalid JSON body");
199
+ return;
200
+ }
201
+ const opts = parseReserveOptions(parsed);
202
+ if (!opts) {
203
+ sendError(res, 400, "Body must be { mimeType: string (type/subtype), fileName: string (no control characters, max 255 chars), extension?: string|null }");
204
+ return;
205
+ }
206
+ try {
207
+ sendJson(res, 201, { reservationId: (await attachments.service.reserve(opts)).reservationId });
208
+ } catch (err) {
209
+ sendErrorFromException(res, err);
210
+ }
211
+ };
212
+ }
213
+ function makeUploadHandler(attachments) {
214
+ return async (req, res) => {
215
+ const reservationId = extractParam(req, "reservationId");
216
+ if (!reservationId) {
217
+ sendError(res, 400, "Missing reservationId");
218
+ return;
219
+ }
220
+ let reservation;
221
+ try {
222
+ reservation = await attachments.reservations.get(reservationId);
223
+ } catch (err) {
224
+ sendErrorFromException(res, err);
225
+ return;
226
+ }
227
+ const upload = attachments.uploadFactory.createUpload(reservation.reservationId, {
228
+ mimeType: reservation.mimeType,
229
+ fileName: reservation.fileName,
230
+ extension: reservation.extension
231
+ });
232
+ const webStream = Readable.toWeb(req);
233
+ try {
234
+ sendJson(res, 200, await upload.send(webStream));
235
+ } catch (err) {
236
+ sendErrorFromException(res, err);
237
+ }
238
+ };
239
+ }
240
+ function makeDownloadHandler(attachments) {
241
+ return async (req, res) => {
242
+ const hash = extractParam(req, "hash");
243
+ if (!hash || !HASH_PATTERN.test(hash)) {
244
+ sendError(res, 400, "Invalid attachment hash");
245
+ return;
246
+ }
247
+ const controller = new AbortController();
248
+ req.once("close", () => controller.abort());
249
+ const canonicalHash = hash.toLowerCase();
250
+ let response;
251
+ try {
252
+ response = await attachments.store.get(canonicalHash, controller.signal);
253
+ } catch (err) {
254
+ sendErrorFromException(res, err);
255
+ return;
256
+ }
257
+ const { header, body } = response;
258
+ res.statusCode = 200;
259
+ res.setHeader("Content-Type", header.mimeType);
260
+ res.setHeader("Content-Length", String(header.sizeBytes));
261
+ res.setHeader("Content-Disposition", buildContentDisposition(header.fileName));
262
+ res.setHeader("Attachment-Metadata", buildMetadataHeader(header));
263
+ Readable.fromWeb(body).pipe(res);
264
+ };
265
+ }
266
+ function buildMetadataHeader(header) {
267
+ return JSON.stringify({
268
+ mimeType: header.mimeType,
269
+ fileName: header.fileName,
270
+ sizeBytes: header.sizeBytes,
271
+ extension: header.extension,
272
+ createdAtUtc: header.createdAtUtc,
273
+ lastAccessedAtUtc: header.lastAccessedAtUtc
274
+ });
275
+ }
276
+ function makeStatHandler(attachments) {
277
+ return async (req, res) => {
278
+ const hash = extractParam(req, "hash");
279
+ if (!hash || !HASH_PATTERN.test(hash)) {
280
+ sendError(res, 400, "Invalid attachment hash");
281
+ return;
282
+ }
283
+ const canonicalHash = hash.toLowerCase();
284
+ let header;
285
+ try {
286
+ header = await attachments.store.stat(canonicalHash);
287
+ } catch (err) {
288
+ sendErrorFromException(res, err);
289
+ return;
290
+ }
291
+ res.statusCode = 200;
292
+ res.setHeader("Content-Type", header.mimeType);
293
+ res.setHeader("Content-Length", String(header.sizeBytes));
294
+ res.setHeader("Content-Disposition", buildContentDisposition(header.fileName));
295
+ res.setHeader("Attachment-Metadata", buildMetadataHeader(header));
296
+ res.end();
297
+ };
298
+ }
299
+ function makeGetReservationHandler(attachments) {
300
+ return async (req, res) => {
301
+ const reservationId = extractParam(req, "reservationId");
302
+ if (!reservationId) {
303
+ sendError(res, 400, "Missing reservationId");
304
+ return;
305
+ }
306
+ try {
307
+ sendJson(res, 200, await attachments.reservations.get(reservationId));
308
+ } catch (err) {
309
+ sendErrorFromException(res, err);
310
+ }
311
+ };
312
+ }
313
+ function makeDeleteReservationHandler(attachments) {
314
+ return async (req, res) => {
315
+ const reservationId = extractParam(req, "reservationId");
316
+ if (!reservationId) {
317
+ sendError(res, 400, "Missing reservationId");
318
+ return;
319
+ }
320
+ try {
321
+ await attachments.reservations.delete(reservationId);
322
+ res.statusCode = 204;
323
+ res.end();
324
+ } catch (err) {
325
+ sendErrorFromException(res, err);
326
+ }
327
+ };
328
+ }
329
+ function extractParam(req, name) {
330
+ return req.params?.[name];
331
+ }
332
+ //#endregion
333
+ //#region src/attachments/index.ts
334
+ function registerAttachmentRoutes(api) {
335
+ const { attachments } = api;
336
+ mountAuthenticatedNodeRoute(api, "POST", "/attachments/reservations", makeReserveHandler(attachments));
337
+ mountAuthenticatedNodeRoute(api, "GET", "/attachments/reservations/:reservationId", makeGetReservationHandler(attachments));
338
+ mountAuthenticatedNodeRoute(api, "DELETE", "/attachments/reservations/:reservationId", makeDeleteReservationHandler(attachments));
339
+ mountAuthenticatedNodeRoute(api, "PUT", "/attachments/reservations/:reservationId", makeUploadHandler(attachments));
340
+ mountAuthenticatedNodeRoute(api, "HEAD", "/attachments/:hash", makeStatHandler(attachments));
341
+ mountAuthenticatedNodeRoute(api, "GET", "/attachments/:hash", makeDownloadHandler(attachments));
342
+ }
343
+ //#endregion
344
+ //#region src/feature-flags.ts
345
+ async function initFeatureFlags() {
346
+ const provider = new EnvVarProvider();
347
+ await OpenFeature.setProviderAndWait(provider);
348
+ return OpenFeature.getClient();
349
+ }
350
+ //#endregion
351
+ //#region src/pglite-migration.ts
352
+ function backupPath(dataDir, major) {
353
+ return `${dataDir}.backup-pg${major}-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`;
354
+ }
355
+ async function pathExists(p) {
356
+ try {
357
+ await promises.stat(p);
358
+ return true;
359
+ } catch {
360
+ return false;
361
+ }
362
+ }
363
+ function logRestoreFailure(dataDir, sql, err, logger) {
364
+ const errObj = err;
365
+ const position = typeof errObj.position === "string" ? parseInt(errObj.position, 10) : typeof errObj.position === "number" ? errObj.position : NaN;
366
+ logger.error(`[pglite-migration] Restore failed for ${dataDir}: code=${errObj.code ?? ""} severity=${errObj.severity ?? ""} message=${errObj.message ?? ""} sqlLength=${sql.length}`);
367
+ if (Number.isFinite(position) && position > 0) {
368
+ const zeroBased = position - 1;
369
+ const start = Math.max(0, zeroBased - 200);
370
+ const end = Math.min(sql.length, zeroBased + 200);
371
+ const before = sql.slice(start, zeroBased);
372
+ const at = sql.slice(zeroBased, zeroBased + 1);
373
+ const after = sql.slice(zeroBased + 1, end);
374
+ logger.error(`[pglite-migration] SQL context around position ${position}:\n${before}»${at}«${after}`);
375
+ } else logger.error(`[pglite-migration] No position info. First 2000 chars of dump:\n${sql.slice(0, 2e3)}`);
376
+ }
377
+ /**
378
+ * Migrate a filesystem PGLite data directory from a legacy PG major to the
379
+ * current one. Renames the existing dir to a timestamped backup, dumps via the
380
+ * matching legacy `pg_dump`, restores into a fresh current-version PGLite at
381
+ * the original path. On failure, the original dir is restored from the backup.
382
+ *
383
+ * No-op when the dir is missing or already at the current major.
384
+ */
385
+ async function migratePgliteDir(dataDir, logger) {
386
+ const major = await readPgVersionFile(dataDir);
387
+ if (major === null) {
388
+ logger.info(`[pglite-migration] No PG_VERSION at ${dataDir}; skipping migration`);
389
+ return;
390
+ }
391
+ if (major === 17) return;
392
+ if (!isSupportedMajor(major)) throw new Error(`Unsupported legacy PGlite data dir: PG_VERSION=${major} for ${dataDir}`);
393
+ const backupDir = backupPath(dataDir, major);
394
+ logger.info(`[pglite-migration] Migrating ${dataDir} from PG${major} to PG17; backup: ${backupDir}`);
395
+ await promises.rename(dataDir, backupDir);
396
+ let sql;
397
+ try {
398
+ const [legacyMod, pgDump] = await Promise.all([loadPGliteModule(major), loadPgDump(major)]);
399
+ const LegacyPGlite = legacyMod.PGlite;
400
+ const pg = new LegacyPGlite(backupDir);
401
+ try {
402
+ await pg.waitReady;
403
+ sql = await (await pgDump({ pg })).text();
404
+ } finally {
405
+ await pg.close();
406
+ }
407
+ } catch (err) {
408
+ await rollback(dataDir, backupDir, err, logger);
409
+ throw err;
410
+ }
411
+ try {
412
+ const CurrentPGlite = (await loadPGliteModule(17)).PGlite;
413
+ const pg = new CurrentPGlite(dataDir, { relaxedDurability: false });
414
+ try {
415
+ await pg.waitReady;
416
+ try {
417
+ await pg.exec("SET standard_conforming_strings = off;");
418
+ } catch (gucErr) {
419
+ logger.warn(`[pglite-migration] Could not force standard_conforming_strings=off: ${String(gucErr)}`);
420
+ }
421
+ try {
422
+ await pg.exec(sql);
423
+ } catch (execErr) {
424
+ logRestoreFailure(dataDir, sql, execErr, logger);
425
+ throw execErr;
426
+ }
427
+ } finally {
428
+ await pg.close();
429
+ }
430
+ } catch (err) {
431
+ await rollback(dataDir, backupDir, err, logger);
432
+ throw err;
433
+ }
434
+ logger.info(`[pglite-migration] Migration of ${dataDir} complete. Backup retained at ${backupDir}; remove it manually once you have verified the upgrade.`);
435
+ }
436
+ async function rollback(dataDir, backupDir, originalError, logger) {
437
+ try {
438
+ if (await pathExists(dataDir)) await promises.rm(dataDir, {
439
+ recursive: true,
440
+ force: true
441
+ });
442
+ if (await pathExists(backupDir)) await promises.rename(backupDir, dataDir);
443
+ } catch (rollbackErr) {
444
+ logger.error(`[pglite-migration] Migration AND rollback failed for ${dataDir}. Original error: ${String(originalError)}; rollback error: ${String(rollbackErr)}; backup may still exist at ${backupDir}.`);
445
+ return;
446
+ }
447
+ logger.error(`[pglite-migration] Migration failed for ${dataDir}; rolled back from ${backupDir}. Original error: ${String(originalError)}`);
448
+ }
449
+ //#endregion
450
+ //#region src/renown.ts
451
+ const logger = childLogger(["switchboard", "renown"]);
452
+ /**
453
+ * Initialize Renown for the Switchboard instance.
454
+ * This allows Switchboard to authenticate with remote services
455
+ * using the same identity established during `ph login`.
456
+ */
457
+ async function initRenown(options = {}) {
458
+ const { keypairPath, requireExisting = false, baseUrl = DEFAULT_RENOWN_URL } = options;
459
+ const keyStorage = new NodeKeyStorage(keypairPath, { logger });
460
+ const existingKeyPair = await keyStorage.loadKeyPair();
461
+ if (!existingKeyPair && requireExisting) throw new Error("No existing keypair found and requireExisting is true. Run \"ph login\" to create one.");
462
+ if (!existingKeyPair) logger.info("No existing keypair found. A new one will be generated.");
463
+ const renownCrypto = await new RenownCryptoBuilder().withKeyPairStorage(keyStorage).build();
464
+ const renown = await new RenownBuilder("switchboard", {}).withCrypto(renownCrypto).withBaseUrl(baseUrl).build();
465
+ logger.info("Switchboard identity initialized: @did", renownCrypto.did);
466
+ return renown;
467
+ }
468
+ /**
469
+ * Get the signer config for the given renown instance.
470
+ *
471
+ * @param renown - The renown instance
472
+ * @param requireSignature - If true, unsigned actions are rejected
473
+ */
474
+ function getRenownSignerConfig(renown, requireSignature) {
475
+ return {
476
+ signer: renown.signer,
477
+ verifier: createSignatureVerifier(requireSignature)
478
+ };
479
+ }
480
+ //#endregion
481
+ //#region src/server.mts
482
+ const defaultLogger = childLogger(["switchboard"]);
483
+ const LogLevel = process.env.LOG_LEVEL || "info";
484
+ setLogLevel(LogLevel);
485
+ dotenv.config();
486
+ const DOCUMENT_MODEL_SUBGRAPHS_ENABLED = "DOCUMENT_MODEL_SUBGRAPHS_ENABLED";
487
+ const DOCUMENT_MODEL_SUBGRAPHS_ENABLED_DEFAULT = true;
488
+ const REQUIRE_SIGNATURES = "REQUIRE_SIGNATURES";
489
+ const REQUIRE_SIGNATURES_DEFAULT = false;
490
+ const DEFAULT_PORT = process.env.PORT ? Number(process.env.PORT) : 4001;
491
+ const PORT_FALLBACK_ATTEMPTS = 20;
492
+ /**
493
+ * Attempt to bind a throwaway TCP server to the given port. Resolves true if
494
+ * the port is free, false if the OS reports it in use. Any other error is
495
+ * surfaced so we don't silently mask real issues (permissions, bad host, …).
496
+ */
497
+ function isPortAvailable(port) {
498
+ return new Promise((resolve, reject) => {
499
+ const tester = net.createServer();
500
+ tester.once("error", (err) => {
501
+ if (err.code === "EADDRINUSE" || err.code === "EACCES") resolve(false);
502
+ else reject(err);
503
+ });
504
+ tester.once("listening", () => {
505
+ tester.close(() => resolve(true));
506
+ });
507
+ tester.listen({
508
+ port,
509
+ host: "::"
510
+ });
511
+ });
512
+ }
513
+ async function resolveServerPort(requested, strictPort, logger) {
514
+ if (strictPort) return requested;
515
+ for (let i = 0; i < PORT_FALLBACK_ATTEMPTS; i++) {
516
+ const candidate = requested + i;
517
+ if (await isPortAvailable(candidate)) {
518
+ if (candidate !== requested) logger.info(`Port ${requested} is in use. Falling back to port ${candidate}.`);
519
+ return candidate;
520
+ }
521
+ }
522
+ return requested;
523
+ }
524
+ async function initServer(serverPort, options, renown) {
525
+ const { dev, packages = [], remoteDrives = [], logger = defaultLogger } = options;
526
+ logger.level = LogLevel;
527
+ const dbPath = options.dbPath ?? process.env.DATABASE_URL ?? process.env.PH_SWITCHBOARD_DATABASE_URL;
528
+ const readModelPath = dbPath || ".ph/read-storage";
529
+ const reactorDbUrl = process.env.PH_REACTOR_DATABASE_URL ?? process.env.PH_SWITCHBOARD_DATABASE_URL;
530
+ const reactorPgliteDir = !reactorDbUrl || !isPostgresUrl(reactorDbUrl) ? "./.ph/reactor-storage" : null;
531
+ const readModelPgliteDir = !dbPath || !isPostgresUrl(dbPath) ? readModelPath : null;
532
+ const pgliteDirs = [reactorPgliteDir, readModelPgliteDir].filter((d) => d !== null);
533
+ const detectedMajors = /* @__PURE__ */ new Map();
534
+ if (options.forcePgVersion !== void 0 && pgliteDirs.length > 0) {
535
+ if (options.migratePglite) logger.warn("PH_FORCE_PG_VERSION is set; ignoring --migrate-pglite/PH_MIGRATE_PGLITE because the data dirs will be wiped.");
536
+ logger.warn(`PH_FORCE_PG_VERSION=${options.forcePgVersion} set; wiping PGLite data dirs and re-initializing at PG${options.forcePgVersion}.`);
537
+ for (const dir of pgliteDirs) {
538
+ await promises.rm(dir, {
539
+ recursive: true,
540
+ force: true
541
+ });
542
+ logger.info(`Wiped PGLite data dir ${dir}`);
543
+ }
544
+ } else if (options.forcePgVersion === void 0) {
545
+ for (const dir of pgliteDirs) {
546
+ const major = await readPgVersionFile(dir);
547
+ if (major !== null) detectedMajors.set(dir, major);
548
+ }
549
+ if (options.migratePglite) for (const [dir, major] of detectedMajors) {
550
+ if (major === 17) continue;
551
+ await migratePgliteDir(dir, logger);
552
+ const after = await readPgVersionFile(dir);
553
+ if (after !== null) detectedMajors.set(dir, after);
554
+ }
555
+ else for (const [dir, major] of detectedMajors) {
556
+ if (major === 17) continue;
557
+ logger.warn(`PGLite data dir at ${dir} was created with PG${major} but Switchboard ships PG17. Running on legacy PGLite. Re-start with --migrate-pglite (or PH_MIGRATE_PGLITE=true) to upgrade.`);
558
+ }
559
+ }
560
+ function resolvePgliteMajorForDir(dir) {
561
+ if (options.forcePgVersion !== void 0) return options.forcePgVersion;
562
+ const detected = detectedMajors.get(dir);
563
+ if (detected === void 0) return 17;
564
+ if (!isSupportedMajor(detected)) throw new Error(`Unsupported PGLite data dir at ${dir}: PG_VERSION=${detected}`);
565
+ return detected;
566
+ }
567
+ const reactorPgliteMajor = reactorPgliteDir ? resolvePgliteMajorForDir(reactorPgliteDir) : null;
568
+ const readModelPgliteMajor = readModelPgliteDir ? resolvePgliteMajorForDir(readModelPgliteDir) : null;
569
+ const apiRef = { current: void 0 };
570
+ const config = getConfig(options.configFile ?? path$1.join(process.cwd(), "powerhouse.config.json"));
571
+ const registryUrl = process.env.PH_REGISTRY_URL ?? config.packageRegistryUrl;
572
+ const registryPackages = process.env.PH_REGISTRY_PACKAGES;
573
+ const dynamicModelLoading = options.dynamicModelLoading ?? process.env.DYNAMIC_MODEL_LOADING === "true";
574
+ let httpLoader;
575
+ if (registryUrl) {
576
+ register(httpsHooksPath, import.meta.url);
577
+ httpLoader = new HttpPackageLoader({ registryUrl });
578
+ registryPackages?.split(",").forEach((p) => {
579
+ const name = p.trim();
580
+ if (!packages.includes(name)) packages.push(name);
581
+ });
582
+ }
583
+ const reactorLogger = logger.child(["reactor"]);
584
+ const initializeClient = async (documentModels$1) => {
585
+ const eventBus = new EventBus();
586
+ const builder = new ReactorBuilder().withEventBus(eventBus).withDocumentModels(getUniqueDocumentModels([
587
+ documentModelDocumentModelModule,
588
+ driveDocumentModelModule,
589
+ ...documentModels,
590
+ ...documentModels$1
591
+ ])).withChannelScheme(ChannelScheme.SWITCHBOARD).withSignalHandlers().withLogger(reactorLogger);
592
+ const maxSkipThreshold = parseInt(process.env.MAX_SKIP_THRESHOLD ?? "", 10);
593
+ if (!isNaN(maxSkipThreshold) && maxSkipThreshold > 0) {
594
+ builder.withExecutorConfig({ maxSkipThreshold });
595
+ logger.info(`Reactor maxSkipThreshold set to ${maxSkipThreshold}`);
596
+ }
597
+ if (reactorDbUrl && isPostgresUrl(reactorDbUrl)) {
598
+ const kysely = new Kysely({ dialect: new PostgresDialect({ pool: new Pool({ connectionString: reactorDbUrl.includes("?") ? reactorDbUrl : `${reactorDbUrl}?sslmode=disable` }) }) });
599
+ builder.withKysely(kysely);
600
+ logger.info("Using PostgreSQL for reactor storage");
601
+ } else {
602
+ if (!reactorPgliteDir || reactorPgliteMajor === null) throw new Error("Reactor PGLite directory not resolved");
603
+ const { PGlite } = await loadPGliteModule(reactorPgliteMajor);
604
+ const kysely = new Kysely({ dialect: new ClosablePGliteDialect(new PGlite(reactorPgliteDir)) });
605
+ builder.withKysely(kysely);
606
+ logger.info(`Using PGlite (PG${reactorPgliteMajor}) for reactor storage at ${reactorPgliteDir}`);
607
+ }
608
+ builder.withShutdownHook(async () => {
609
+ if (apiRef.current) await apiRef.current.dispose();
610
+ });
611
+ if (httpLoader && dynamicModelLoading) builder.withDocumentModelLoader(httpLoader.documentModelLoader);
612
+ const clientBuilder = new ReactorClientBuilder().withReactorBuilder(builder);
613
+ if (renown) {
614
+ const signerConfig = getRenownSignerConfig(renown, options.identity?.requireSignatures);
615
+ clientBuilder.withSigner(signerConfig);
616
+ }
617
+ const module = await clientBuilder.buildModule();
618
+ if (module.reactorModule) {
619
+ new ReactorInstrumentation(module.reactorModule).start();
620
+ reactorLogger.info("Reactor metrics instrumentation started");
621
+ }
622
+ return module;
623
+ };
624
+ let defaultDriveUrl = void 0;
625
+ const basePath = process.cwd();
626
+ const viteLogger = createViteLogger(logger);
627
+ const vite = dev ? await startViteServer(process.cwd(), viteLogger) : void 0;
628
+ if (!options.disableLocalPackages) packages.push(basePath);
629
+ const packageLoaders = [];
630
+ if (vite) packageLoaders.push(VitePackageLoader.build(vite));
631
+ else packageLoaders.push(new ImportPackageLoader());
632
+ if (httpLoader) {
633
+ packageLoaders.push(httpLoader);
634
+ registryPackages?.split(",").forEach((p) => {
635
+ const name = p.trim();
636
+ if (!packages.includes(name)) packages.push(name);
637
+ });
638
+ }
639
+ const apiLogger = logger.child(["reactor-api"]);
640
+ let pgliteFactory;
641
+ if (readModelPgliteDir && readModelPgliteMajor !== null) {
642
+ const { PGlite: ReadModelPGlite } = await loadPGliteModule(readModelPgliteMajor);
643
+ pgliteFactory = (connectionString) => new ReadModelPGlite(connectionString ?? readModelPgliteDir);
644
+ }
645
+ const api = await initializeAndStartAPI(initializeClient, {
646
+ port: serverPort,
647
+ dbPath: readModelPath,
648
+ pgliteFactory,
649
+ https: options.https,
650
+ packageLoaders: packageLoaders.length > 0 ? packageLoaders : void 0,
651
+ packages,
652
+ processorConfig: options.processorConfig,
653
+ processors: { "@powerhousedao/vetra": [processorFactory] },
654
+ configFile: options.configFile ?? path$1.join(process.cwd(), "powerhouse.config.json"),
655
+ mcp: options.mcp ?? true,
656
+ logger: apiLogger,
657
+ enableDocumentModelSubgraphs: options.enableDocumentModelSubgraphs
658
+ }, "switchboard");
659
+ apiRef.current = api;
660
+ registerAttachmentRoutes(api);
661
+ if (process.env.SENTRY_DSN) api.httpAdapter.setupSentryErrorHandler(Sentry);
662
+ const { client, graphqlManager, documentModelRegistry } = api;
663
+ if (httpLoader) {
664
+ const packageManagementService = new PackageManagementService({
665
+ defaultRegistryUrl: registryUrl,
666
+ httpLoader,
667
+ documentModelRegistry
668
+ });
669
+ packageManagementService.setOnModelsChanged(() => {
670
+ graphqlManager.regenerateDocumentModelSubgraphs().catch(logger.error);
671
+ });
672
+ const packagesSubgraph = new PackagesSubgraph({
673
+ relationalDb: void 0,
674
+ analyticsStore: void 0,
675
+ reactorClient: client,
676
+ graphqlManager,
677
+ syncManager: api.syncManager,
678
+ path: graphqlManager.getBasePath(),
679
+ packageManagementService
680
+ });
681
+ graphqlManager.registerSubgraphInstance(packagesSubgraph, "graphql", false).then(() => graphqlManager.updateRouter()).catch((error) => {
682
+ logger.error("Failed to register packages subgraph: @error", error);
683
+ });
684
+ }
685
+ if (options.drive) {
686
+ if (!renown) throw new Error("Cannot create default drive without Renown identity");
687
+ defaultDriveUrl = await addDefaultDrive(client, options.drive, serverPort);
688
+ }
689
+ if (vite) api.httpAdapter.mountRawMiddleware(vite.middlewares);
690
+ if (remoteDrives.length > 0) for (const remoteDriveUrl of remoteDrives) {
691
+ let driveId;
692
+ try {
693
+ const { syncManager } = api;
694
+ const parsed = parseDriveUrl(remoteDriveUrl);
695
+ driveId = parsed.driveId;
696
+ const remoteName = `remote-drive-${driveId}-${crypto.randomUUID()}`;
697
+ await syncManager.add(remoteName, driveCollectionId("main", driveId), {
698
+ type: "gql",
699
+ parameters: { url: parsed.graphqlEndpoint }
700
+ });
701
+ logger.debug("Remote drive @remoteDriveUrl synced", remoteDriveUrl);
702
+ } catch (error) {
703
+ if (error instanceof Error && error.message.includes("already exists")) {
704
+ logger.debug("Remote drive already added: @remoteDriveUrl", remoteDriveUrl);
705
+ driveId = remoteDriveUrl.split("/").pop();
706
+ } else logger.error("Failed to connect to remote drive @remoteDriveUrl: @error", remoteDriveUrl, error);
707
+ } finally {
708
+ if (!defaultDriveUrl && driveId) defaultDriveUrl = `${options.https ? "https" : "http"}://localhost:${serverPort}/d/${driveId}`;
709
+ }
710
+ }
711
+ return {
712
+ defaultDriveUrl,
713
+ api,
714
+ reactor: client,
715
+ renown,
716
+ port: serverPort
717
+ };
718
+ }
719
+ const startSwitchboard = async (options = {}) => {
720
+ const requestedPort = options.port ?? DEFAULT_PORT;
721
+ const logger = options.logger ?? defaultLogger;
722
+ const serverPort = await resolveServerPort(requestedPort, options.strictPort ?? false, logger);
723
+ const featureFlags = await initFeatureFlags();
724
+ const enableDocumentModelSubgraphs = await featureFlags.getBooleanValue(DOCUMENT_MODEL_SUBGRAPHS_ENABLED, options.enableDocumentModelSubgraphs ?? DOCUMENT_MODEL_SUBGRAPHS_ENABLED_DEFAULT);
725
+ options.enableDocumentModelSubgraphs = enableDocumentModelSubgraphs;
726
+ const requireSignatures = options.identity?.requireSignatures ?? await featureFlags.getBooleanValue(REQUIRE_SIGNATURES, REQUIRE_SIGNATURES_DEFAULT);
727
+ options.identity = {
728
+ ...options.identity,
729
+ requireSignatures
730
+ };
731
+ logger.info("Feature flags: @flags", JSON.stringify({
732
+ DOCUMENT_MODEL_SUBGRAPHS_ENABLED: enableDocumentModelSubgraphs,
733
+ REQUIRE_SIGNATURES: requireSignatures
734
+ }, null, 2));
735
+ let renown = null;
736
+ try {
737
+ renown = await initRenown(options.identity);
738
+ } catch (e) {
739
+ logger.warn("Failed to initialize ConnectCrypto: @error", e);
740
+ if (options.identity?.requireExisting) throw new Error("Identity required but failed to initialize. Run \"ph login\" first.");
741
+ }
742
+ try {
743
+ return await initServer(serverPort, options, renown);
744
+ } catch (e) {
745
+ Sentry.captureException(e);
746
+ logger.error("App crashed: @error", e);
747
+ throw e;
748
+ }
749
+ };
750
+ if (import.meta.main) await startSwitchboard();
751
+ //#endregion
752
+ export { startSwitchboard as n, parseForcePgVersion as r, isPortAvailable as t };
753
+
754
+ //# sourceMappingURL=server-DPxV64Dh.mjs.map
755
+ //# debugId=243850c6-8159-53a4-a4f4-c310a075551a