@powerhousedao/switchboard 6.2.3-dev.5 → 6.2.3-dev.51

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 (143) hide show
  1. package/.tsbuild/test/attachments/byte-grants.test.d.ts +2 -0
  2. package/.tsbuild/test/attachments/byte-grants.test.d.ts.map +1 -0
  3. package/.tsbuild/test/attachments/byte-routes-read-gate.integration.test.d.ts +2 -0
  4. package/.tsbuild/test/attachments/byte-routes-read-gate.integration.test.d.ts.map +1 -0
  5. package/.tsbuild/test/attachments/reserve-metadata-parity.test.d.ts +2 -0
  6. package/.tsbuild/test/attachments/reserve-metadata-parity.test.d.ts.map +1 -0
  7. package/.tsbuild/test/attachments/url-signer.test.d.ts +2 -0
  8. package/.tsbuild/test/attachments/url-signer.test.d.ts.map +1 -0
  9. package/.tsbuild/test/boot-create-signature-policy.test.d.ts +2 -0
  10. package/.tsbuild/test/boot-create-signature-policy.test.d.ts.map +1 -0
  11. package/.tsbuild/test/boot-mcp-enabled.test.d.ts +2 -0
  12. package/.tsbuild/test/boot-mcp-enabled.test.d.ts.map +1 -0
  13. package/.tsbuild/test/boot-privacy.test.d.ts +2 -0
  14. package/.tsbuild/test/boot-privacy.test.d.ts.map +1 -0
  15. package/.tsbuild/test/boot-unsupported-stored-documents.test.d.ts +2 -0
  16. package/.tsbuild/test/boot-unsupported-stored-documents.test.d.ts.map +1 -0
  17. package/.tsbuild/test/boot-without-renown.test.d.ts +2 -0
  18. package/.tsbuild/test/boot-without-renown.test.d.ts.map +1 -0
  19. package/.tsbuild/test/boot-without-require-signatures.test.d.ts +2 -0
  20. package/.tsbuild/test/boot-without-require-signatures.test.d.ts.map +1 -0
  21. package/.tsbuild/test/boot-worker-trust-policy.test.d.ts +2 -0
  22. package/.tsbuild/test/boot-worker-trust-policy.test.d.ts.map +1 -0
  23. package/.tsbuild/test/boot-workflow-singleton-conflict.test.d.ts +2 -0
  24. package/.tsbuild/test/boot-workflow-singleton-conflict.test.d.ts.map +1 -0
  25. package/.tsbuild/test/boot-workflow-singleton-retry.test.d.ts +2 -0
  26. package/.tsbuild/test/boot-workflow-singleton-retry.test.d.ts.map +1 -0
  27. package/.tsbuild/test/builder-defaults.test.d.ts +2 -0
  28. package/.tsbuild/test/builder-defaults.test.d.ts.map +1 -0
  29. package/.tsbuild/test/create-signature-policy.test.d.ts +2 -0
  30. package/.tsbuild/test/create-signature-policy.test.d.ts.map +1 -0
  31. package/.tsbuild/test/durable-node-fs-pg16.test.d.ts +2 -0
  32. package/.tsbuild/test/durable-node-fs-pg16.test.d.ts.map +1 -0
  33. package/.tsbuild/test/fatal-shutdown.test.d.ts +2 -0
  34. package/.tsbuild/test/fatal-shutdown.test.d.ts.map +1 -0
  35. package/.tsbuild/test/mcp-flag.test.d.ts +2 -0
  36. package/.tsbuild/test/mcp-flag.test.d.ts.map +1 -0
  37. package/.tsbuild/test/observability-signals.test.d.ts +2 -0
  38. package/.tsbuild/test/observability-signals.test.d.ts.map +1 -0
  39. package/.tsbuild/test/pglite-preflight.test.d.ts +2 -0
  40. package/.tsbuild/test/pglite-preflight.test.d.ts.map +1 -0
  41. package/.tsbuild/test/renown-trust-policy.test.d.ts +2 -0
  42. package/.tsbuild/test/renown-trust-policy.test.d.ts.map +1 -0
  43. package/.tsbuild/test/snapshot-writer.d.ts +18 -0
  44. package/.tsbuild/test/snapshot-writer.d.ts.map +1 -0
  45. package/.tsbuild/test/unsupported-stored-documents.test.d.ts +2 -0
  46. package/.tsbuild/test/unsupported-stored-documents.test.d.ts.map +1 -0
  47. package/.tsbuild/test/workflow/harness.d.ts +56 -0
  48. package/.tsbuild/test/workflow/harness.d.ts.map +1 -0
  49. package/.tsbuild/test/workflow/oauth.test.d.ts +2 -0
  50. package/.tsbuild/test/workflow/oauth.test.d.ts.map +1 -0
  51. package/.tsbuild/test/workflow/resolvers.test.d.ts +2 -0
  52. package/.tsbuild/test/workflow/resolvers.test.d.ts.map +1 -0
  53. package/.tsbuild/test/workflow/webhook-dedupe.test.d.ts +2 -0
  54. package/.tsbuild/test/workflow/webhook-dedupe.test.d.ts.map +1 -0
  55. package/.tsbuild/test/workflow/webhook-signed.test.d.ts +2 -0
  56. package/.tsbuild/test/workflow/webhook-signed.test.d.ts.map +1 -0
  57. package/.tsbuild/test/workflow/webhook-trigger-test.test.d.ts +2 -0
  58. package/.tsbuild/test/workflow/webhook-trigger-test.test.d.ts.map +1 -0
  59. package/.tsbuild/test/workflow/webhook-unsigned.test.d.ts +2 -0
  60. package/.tsbuild/test/workflow/webhook-unsigned.test.d.ts.map +1 -0
  61. package/.tsbuild/test/workflow-reactor-access.test.d.ts +2 -0
  62. package/.tsbuild/test/workflow-reactor-access.test.d.ts.map +1 -0
  63. package/.tsbuild/test/workflow-reactor-enforcement.test.d.ts +2 -0
  64. package/.tsbuild/test/workflow-reactor-enforcement.test.d.ts.map +1 -0
  65. package/.tsbuild/test/workflow-read-gate.test.d.ts +2 -0
  66. package/.tsbuild/test/workflow-read-gate.test.d.ts.map +1 -0
  67. package/.tsbuild/test/workflow-runtime.test.d.ts +2 -0
  68. package/.tsbuild/test/workflow-runtime.test.d.ts.map +1 -0
  69. package/.tsbuild/tsconfig.tsbuildinfo +1 -1
  70. package/.tsbuild/tsdown.config.d.ts.map +1 -1
  71. package/.tsbuild/vitest.config.d.ts.map +1 -1
  72. package/Auth.md +94 -0
  73. package/CHANGELOG.md +541 -0
  74. package/README.md +35 -0
  75. package/dist/index.d.mts +2 -1
  76. package/dist/index.mjs +48 -49
  77. package/dist/index.mjs.map +1 -1
  78. package/dist/install-packages.d.mts +2 -1
  79. package/dist/install-packages.mjs +2 -2
  80. package/dist/install-packages.mjs.map +1 -1
  81. package/dist/migrate.d.mts +2 -1
  82. package/dist/migrate.mjs +4 -3
  83. package/dist/migrate.mjs.map +1 -1
  84. package/dist/server-J9fpd42v.mjs +3549 -0
  85. package/dist/server-J9fpd42v.mjs.map +1 -0
  86. package/dist/server.d.mts +155 -30
  87. package/dist/server.d.mts.map +1 -1
  88. package/dist/server.mjs +4 -5
  89. package/dist/{utils-Baw7rThP.mjs → utils-uaBlzyjS.mjs} +15 -9
  90. package/dist/utils-uaBlzyjS.mjs.map +1 -0
  91. package/dist/utils.d.mts +4 -5
  92. package/dist/utils.d.mts.map +1 -1
  93. package/dist/utils.mjs +3 -3
  94. package/package.json +22 -18
  95. package/test/attachment-reference-read-model.test.ts +6 -1
  96. package/test/attachments/auth.test.ts +85 -0
  97. package/test/attachments/byte-grants.test.ts +290 -0
  98. package/test/attachments/byte-routes-read-gate.integration.test.ts +314 -0
  99. package/test/attachments/download-target.test.ts +201 -32
  100. package/test/attachments/index.test.ts +34 -3
  101. package/test/attachments/reserve-metadata-parity.test.ts +96 -0
  102. package/test/attachments/routes-integration.test.ts +60 -4
  103. package/test/attachments/routes.test.ts +166 -23
  104. package/test/attachments/service-config.test.ts +13 -3
  105. package/test/attachments/url-signer.test.ts +105 -0
  106. package/test/boot-create-signature-policy.test.ts +108 -0
  107. package/test/boot-mcp-enabled.test.ts +121 -0
  108. package/test/boot-privacy.test.ts +243 -0
  109. package/test/boot-unsupported-stored-documents.test.ts +213 -0
  110. package/test/boot-without-renown.test.ts +81 -0
  111. package/test/boot-without-require-signatures.test.ts +74 -0
  112. package/test/boot-worker-trust-policy.test.ts +90 -0
  113. package/test/boot-workflow-singleton-conflict.test.ts +107 -0
  114. package/test/boot-workflow-singleton-retry.test.ts +133 -0
  115. package/test/builder-defaults.test.ts +80 -0
  116. package/test/create-signature-policy.test.ts +69 -0
  117. package/test/durable-node-fs-pg16.test.ts +80 -0
  118. package/test/fatal-shutdown.test.ts +165 -0
  119. package/test/mcp-flag.test.ts +56 -0
  120. package/test/observability-signals.test.ts +131 -0
  121. package/test/pglite-dialect.test.ts +217 -2
  122. package/test/pglite-preflight.test.ts +313 -0
  123. package/test/pglite-version.test.ts +9 -1
  124. package/test/renown-trust-policy.test.ts +315 -0
  125. package/test/snapshot-writer.ts +94 -0
  126. package/test/unsupported-stored-documents.test.ts +72 -0
  127. package/test/worker-pool.test.ts +27 -1
  128. package/test/workflow/harness.ts +289 -0
  129. package/test/workflow/oauth.test.ts +214 -0
  130. package/test/workflow/resolvers.test.ts +357 -0
  131. package/test/workflow/webhook-dedupe.test.ts +295 -0
  132. package/test/workflow/webhook-signed.test.ts +509 -0
  133. package/test/workflow/webhook-trigger-test.test.ts +82 -0
  134. package/test/workflow/webhook-unsigned.test.ts +345 -0
  135. package/test/workflow-reactor-access.test.ts +384 -0
  136. package/test/workflow-reactor-enforcement.test.ts +808 -0
  137. package/test/workflow-read-gate.test.ts +213 -0
  138. package/test/workflow-runtime.test.ts +1067 -0
  139. package/tsconfig.json +9 -3
  140. package/tsdown.config.ts +3 -1
  141. package/dist/server-CiKkVE6g.mjs +0 -1555
  142. package/dist/server-CiKkVE6g.mjs.map +0 -1
  143. package/dist/utils-Baw7rThP.mjs.map +0 -1
package/README.md CHANGED
@@ -110,6 +110,7 @@ pnpm add -g @powerhousedao/switchboard
110
110
  | `REDIS_TLS_URL` | Redis TLS connection URL | - |
111
111
  | `PYROSCOPE_SERVER_ADDRESS` | Pyroscope server address | - |
112
112
  | `FEATURE_REACTORV2_ENABLED` | Enable Reactor v2 subgraph feature | `false` |
113
+ | `MCP_ENABLED` | `false` keeps `/mcp` unmounted | `true` |
113
114
 
114
115
  See [Observability](#observability) below for Sentry and OpenTelemetry variables.
115
116
 
@@ -186,6 +187,40 @@ exits non-zero instead of waiting out the init timeout.
186
187
 
187
188
  Programmatically: `startSwitchboard({ projectionWorker: { enabled, dbPoolSize } })`.
188
189
 
190
+ ### Creation Signature Policy
191
+
192
+ | Variable | Description | Default |
193
+ | ------------------------- | ------------------------------------------------------------ | ------------- |
194
+ | `CREATE_SIGNATURE_POLICY` | What new documents are created as: `legacy` or `v2-required` | `v2-required` |
195
+
196
+ A v2-required document accepts only v2 action signatures and takes a
197
+ content-addressed id. Set `legacy` while any reactor that syncs with this one
198
+ predates v2-required documents. It only decides what new documents are born
199
+ as; existing documents keep their policy, and it does not change what the
200
+ switchboard accepts. Without a Renown identity the switchboard has no signer,
201
+ so it creates legacy documents and logs a warning. A default drive configured
202
+ with a fixed `id` is always created legacy.
203
+
204
+ ### Stored Documents This Build Does Not Run
205
+
206
+ | Variable | Description | Default |
207
+ | -------------------------------------- | -------------------------------------------------------------------------------------- | -------- |
208
+ | `REACTOR_UNSUPPORTED_STORED_DOCUMENTS` | Stored documents at protocol versions this build does not run: `refuse` or `read-only` | `refuse` |
209
+
210
+ Before it starts, the reactor reads the protocol versions its stored documents
211
+ were created with. If this build does not run one of them, for example after a
212
+ rollback, switchboard logs this and exits with code 1:
213
+
214
+ ```
215
+ Refusing to start: 12 stored document(s) require base-reducer 3, which this switchboard does not run. Either start a switchboard build that runs base-reducer 3, or set REACTOR_UNSUPPORTED_STORED_DOCUMENTS=read-only to start with those documents read-only.
216
+ ```
217
+
218
+ With `read-only` it starts and logs a warning. Every write into those documents
219
+ and every operation received for them is refused; the rest of the store works
220
+ as usual. The check runs once on the host, so it covers the executor worker
221
+ pool too. Any other value fails the boot. Programmatically:
222
+ `startSwitchboard({ unsupportedStoredDocuments: "read-only" })`.
223
+
189
224
  ### Reactor Enforcement Flags
190
225
 
191
226
  | Variable | Description | Default |
package/dist/index.d.mts CHANGED
@@ -1,2 +1,3 @@
1
1
  import { NodeSDK } from "@opentelemetry/sdk-node";
2
- import { MeterProvider } from "@opentelemetry/sdk-metrics";
2
+ import { MeterProvider } from "@opentelemetry/sdk-metrics";
3
+ export {};
package/dist/index.mjs CHANGED
@@ -1,8 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
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]="c6c10af1-f4cc-56a5-abf6-fa35cf0de956")}catch(e){}}();
4
- import { a as parseForcePgVersion, r as startSwitchboard } from "./server-CiKkVE6g.mjs";
5
- import "./utils-Baw7rThP.mjs";
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]="d30347ec-a382-56a4-b3ef-bdacc9d409fd")}catch(e){}}();
4
+ import { a as StoredDocumentsRefusedError, c as initProfilerFromEnv, i as startSwitchboard, l as parseForcePgVersion } from "./server-J9fpd42v.mjs";
6
5
  import { metrics } from "@opentelemetry/api";
7
6
  import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
8
7
  import { ExpressInstrumentation } from "@opentelemetry/instrumentation-express";
@@ -44,6 +43,44 @@ function createMeterProviderFromEnv(env) {
44
43
  return meterProvider;
45
44
  }
46
45
  //#endregion
46
+ //#region src/observability-signals.mts
47
+ /**
48
+ * Flushes telemetry on SIGINT/SIGTERM, then exits only when no other module
49
+ * has claimed a failing exit.
50
+ *
51
+ * Exit ownership, decided here on purpose:
52
+ *
53
+ * - Ordinary signal (process.exitCode unset or 0): this handler exits with
54
+ * the current exit code after the flush settles (the flush bounds itself,
55
+ * see shutdown() in observability.mts). When the reactor builder's drain
56
+ * handler is also attached it runs first (prependListener) and swaps
57
+ * process.exit for a recording shim before this handler's exit call, so
58
+ * the drain still finishes and performs the real exit; when the builder is
59
+ * not attached (startup, signalHandlers: false), this exit is what
60
+ * terminates the process, with code 0.
61
+ *
62
+ * - Fatal shutdown in progress (process.exitCode already non-zero):
63
+ * fatal-shutdown.mts sets exitCode = 1 and arms a 15s forced exit BEFORE
64
+ * raising SIGTERM, precisely so the builder's drain can flush the PGlite
65
+ * snapshots. Calling exit here would lower that code to 0 (supervisors
66
+ * would see a crash as success) and, whenever the builder's shim is not in
67
+ * place, cut the drain off at this handler's shorter flush budget. So this
68
+ * handler never exits once a non-zero code is set: the exit belongs to the
69
+ * builder's drain, or failing that to fatal-shutdown's 15s backstop, both
70
+ * of which preserve code 1.
71
+ */
72
+ function installObservabilitySignalHandlers(flush, proc = process) {
73
+ const onSignal = () => {
74
+ flush().finally(() => {
75
+ const code = proc.exitCode;
76
+ if (code !== void 0 && code !== 0) return;
77
+ proc.exit(code ?? 0);
78
+ });
79
+ };
80
+ proc.on("SIGINT", onSignal);
81
+ proc.on("SIGTERM", onSignal);
82
+ }
83
+ //#endregion
47
84
  //#region src/redact-webhook-path.ts
48
85
  const WEBHOOK_SEGMENT = "/webhooks/";
49
86
  /**
@@ -149,12 +186,7 @@ async function shutdown() {
149
186
  eventLoopInstrumentation?.stop();
150
187
  await Promise.race([Promise.all([meterProvider?.shutdown().catch(() => void 0), sdk?.shutdown().catch(() => void 0)]), new Promise((resolve) => setTimeout(resolve, 5e3))]);
151
188
  }
152
- process.on("SIGINT", () => {
153
- shutdown().finally(() => process.exit(0));
154
- });
155
- process.on("SIGTERM", () => {
156
- shutdown().finally(() => process.exit(0));
157
- });
189
+ installObservabilitySignalHandlers(shutdown);
158
190
  //#endregion
159
191
  //#region src/config.ts
160
192
  dotenv.config();
@@ -187,42 +219,6 @@ const config = {
187
219
  }
188
220
  };
189
221
  //#endregion
190
- //#region src/profiler.ts
191
- async function initProfilerFromEnv(env) {
192
- const { PYROSCOPE_SERVER_ADDRESS: serverAddress, PYROSCOPE_APPLICATION_NAME: appName, PYROSCOPE_USER: basicAuthUser, PYROSCOPE_PASSWORD: basicAuthPassword, PYROSCOPE_WALL_ENABLED: wallEnabled, PYROSCOPE_HEAP_ENABLED: heapEnabled } = env;
193
- return initProfiler({
194
- serverAddress,
195
- appName,
196
- basicAuthUser,
197
- basicAuthPassword,
198
- wall: {
199
- samplingDurationMs: 1e4,
200
- samplingIntervalMicros: 1e4,
201
- collectCpuTime: true
202
- },
203
- heap: {
204
- samplingIntervalBytes: 512 * 1024,
205
- stackDepth: 64
206
- }
207
- }, {
208
- wallEnabled: wallEnabled !== "false",
209
- heapEnabled: heapEnabled === "true"
210
- });
211
- }
212
- async function initProfiler(options, flags = {
213
- wallEnabled: true,
214
- heapEnabled: false
215
- }) {
216
- console.log("Initializing Pyroscope profiler at:", options?.serverAddress);
217
- console.log(" Wall profiling:", flags.wallEnabled ? "enabled" : "disabled");
218
- console.log(" Heap profiling:", flags.heapEnabled ? "enabled" : "disabled");
219
- const { default: Pyroscope } = await import("@pyroscope/nodejs");
220
- Pyroscope.init(options);
221
- if (flags.wallEnabled) Pyroscope.startWallProfiling();
222
- Pyroscope.startCpuProfiling();
223
- if (flags.heapEnabled) Pyroscope.startHeapProfiling();
224
- }
225
- //#endregion
226
222
  //#region src/index.mts
227
223
  const logger = childLogger(["switchboard"]);
228
224
  function ensureNodeVersion(minVersion = "24") {
@@ -246,11 +242,14 @@ try {
246
242
  await startSwitchboard({
247
243
  ...config,
248
244
  migratePglite: cliMigratePglite || config.migratePglite,
249
- forcePgVersion: config.forcePgVersion ?? void 0
245
+ forcePgVersion: config.forcePgVersion ?? void 0,
246
+ fatalErrorShutdown: true
250
247
  });
251
248
  } catch (e) {
252
- Sentry.captureException(e);
253
- logger.error("Switchboard failed to start: @error", e);
249
+ if (!StoredDocumentsRefusedError.isError(e)) {
250
+ Sentry.captureException(e);
251
+ logger.error("Switchboard failed to start: @error", e);
252
+ }
254
253
  await Sentry.flush(2e3).catch(() => void 0);
255
254
  process.exit(1);
256
255
  }
@@ -258,4 +257,4 @@ try {
258
257
  export {};
259
258
 
260
259
  //# sourceMappingURL=index.mjs.map
261
- //# debugId=c6c10af1-f4cc-56a5-abf6-fa35cf0de956
260
+ //# debugId=d30347ec-a382-56a4-b3ef-bdacc9d409fd
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","sources":["../src/metrics.ts","../src/redact-webhook-path.ts","../src/observability.mts","../src/config.ts","../src/profiler.ts","../src/index.mts"],"sourcesContent":["import { OTLPMetricExporter } from \"@opentelemetry/exporter-metrics-otlp-http\";\nimport { resourceFromAttributes } from \"@opentelemetry/resources\";\nimport {\n MeterProvider,\n PeriodicExportingMetricReader,\n} from \"@opentelemetry/sdk-metrics\";\nimport { childLogger } from \"document-model\";\n\nconst logger = childLogger([\"switchboard\", \"metrics\"]);\n\nexport function createMeterProviderFromEnv(env: {\n OTEL_EXPORTER_OTLP_ENDPOINT?: string;\n OTEL_METRIC_EXPORT_INTERVAL?: string;\n OTEL_SERVICE_NAME?: string;\n}): MeterProvider | undefined {\n const endpoint = env.OTEL_EXPORTER_OTLP_ENDPOINT;\n if (!endpoint) return undefined;\n\n const parsed = parseInt(env.OTEL_METRIC_EXPORT_INTERVAL ?? \"\", 10);\n const exportIntervalMillis =\n Number.isFinite(parsed) && parsed > 0 ? parsed : 5_000;\n\n const base = endpoint.replace(/\\/$/, \"\");\n const exporterUrl = base.endsWith(\"/v1/metrics\")\n ? base\n : `${base}/v1/metrics`;\n\n logger.info(`Initializing OpenTelemetry metrics exporter at: ${endpoint}`);\n const meterProvider = new MeterProvider({\n resource: resourceFromAttributes({\n \"service.name\": env.OTEL_SERVICE_NAME ?? \"switchboard\",\n }),\n readers: [\n new PeriodicExportingMetricReader({\n exporter: new OTLPMetricExporter({\n url: exporterUrl,\n }),\n exportIntervalMillis,\n exportTimeoutMillis: Math.max(exportIntervalMillis - 250, 1),\n }),\n ],\n });\n logger.info(`Metrics export enabled (interval: ${exportIntervalMillis}ms)`);\n return meterProvider;\n}\n","const WEBHOOK_SEGMENT = \"/webhooks/\";\n\n/**\n * Strips a webhook's delivery token out of a request path.\n *\n * The token is the endpoint's whole credential and it sits in the path, so it\n * must not reach a log line or a span attribute that ships off-box. The family\n * mounts under the host's base path, so the segment can sit at any depth.\n */\nexport function redactWebhookPath(url: string | undefined): string {\n if (!url) return \"\";\n // Dropped with the token: `http.route` is meant to be a template, and a\n // query string is another place a sender can put a credential.\n const path = url.split(\"?\")[0] ?? url;\n const at = path.indexOf(WEBHOOK_SEGMENT);\n if (at === -1) return url;\n // Nothing after the segment is nothing to redact.\n if (path.length === at + WEBHOOK_SEGMENT.length) return url;\n return `${path.slice(0, at)}${WEBHOOK_SEGMENT}[redacted]`;\n}\n","// Single observability bootstrap: Sentry + OpenTelemetry (tracing + metrics).\n//\n// MUST be imported as the very first module in apps/switchboard/src/index.mts.\n// OpenTelemetry instrumentations register require-time hooks at module load,\n// so http/express/pg/graphql must not be imported (transitively) before this\n// file runs.\n//\n// Replaces three legacy bootstrap sites:\n// - apps/switchboard/src/server.mts top-level Sentry.init\n// - apps/switchboard/src/metrics.ts standalone MeterProvider (still exported\n// here via createMeterProviderFromEnv so its tests keep passing)\n// - packages/reactor-api/src/tracing.ts side-effect NodeSDK\nimport { metrics } from \"@opentelemetry/api\";\nimport { OTLPTraceExporter } from \"@opentelemetry/exporter-trace-otlp-http\";\nimport { ExpressInstrumentation } from \"@opentelemetry/instrumentation-express\";\nimport { GraphQLInstrumentation } from \"@opentelemetry/instrumentation-graphql\";\nimport { HttpInstrumentation } from \"@opentelemetry/instrumentation-http\";\nimport { PgInstrumentation } from \"@opentelemetry/instrumentation-pg\";\nimport { UndiciInstrumentation } from \"@opentelemetry/instrumentation-undici\";\nimport { resourceFromAttributes } from \"@opentelemetry/resources\";\nimport type { MeterProvider } from \"@opentelemetry/sdk-metrics\";\nimport { NodeSDK } from \"@opentelemetry/sdk-node\";\nimport {\n BatchSpanProcessor,\n type SpanProcessor,\n} from \"@opentelemetry/sdk-trace-base\";\nimport {\n ATTR_SERVICE_NAME,\n ATTR_SERVICE_VERSION,\n} from \"@opentelemetry/semantic-conventions\";\nimport {\n registerEventLoopInstrumentation,\n type EventLoopInstrumentation,\n} from \"@powerhousedao/opentelemetry-instrumentation-reactor\";\nimport * as Sentry from \"@sentry/node\";\nimport { SentryPropagator, SentrySpanProcessor } from \"@sentry/opentelemetry\";\nimport { childLogger } from \"document-model\";\nimport type { IncomingMessage } from \"node:http\";\nimport { createMeterProviderFromEnv } from \"./metrics.js\";\nimport { redactWebhookPath } from \"./redact-webhook-path.js\";\n\nconst logger = childLogger([\"switchboard\", \"observability\"]);\n\nconst SERVICE_NAME = process.env.OTEL_SERVICE_NAME || \"switchboard\";\nconst SERVICE_VERSION = process.env.npm_package_version || \"unknown\";\nconst TENANT_ID = process.env.TENANT_ID || \"default\";\nconst DEPLOY_ENV = process.env.NODE_ENV || \"development\";\n\nconst TEMPO_ENDPOINT = process.env.TEMPO_ENDPOINT;\nconst SENTRY_DSN = process.env.SENTRY_DSN;\n\n// Whether to forward TRANSACTIONS (spans) to Sentry. Errors are always sent\n// when SENTRY_DSN is set — this flag only gates APM/tracing into Sentry.\n// Default on (back-compat). Set SENTRY_TRACING_ENABLED=false for an\n// \"errors-only\" deployment: errors still go to Sentry, traces still go to\n// Tempo, but no transactions hit Sentry (no Kafka/ClickHouse/nodestore\n// cost). This is the recommended mode for tenant workloads at scale —\n// Sentry's value there is error grouping; traces live in Tempo.\nconst SENTRY_TRACING_TO_SENTRY =\n Boolean(SENTRY_DSN) && process.env.SENTRY_TRACING_ENABLED !== \"false\";\n\nconst TRACING_REQUESTED =\n process.env.ENABLE_TRACING === \"true\" ||\n process.env.NODE_ENV === \"production\";\nconst HAS_TRACE_DESTINATION = Boolean(TEMPO_ENDPOINT) || Boolean(SENTRY_DSN);\nconst TRACING_ENABLED = TRACING_REQUESTED && HAS_TRACE_DESTINATION;\n\nif (TRACING_REQUESTED && !HAS_TRACE_DESTINATION) {\n logger.warn(\n \"Tracing was requested (NODE_ENV=production or ENABLE_TRACING=true) but \" +\n \"no destination is configured — instrumentation will not run. Set \" +\n \"TEMPO_ENDPOINT (e.g. http://tempo.monitoring.svc.cluster.local:4318/v1/traces) \" +\n \"to export OTLP spans, and/or SENTRY_DSN to forward spans to Sentry.\",\n );\n}\n\n// APM sampling for the Sentry-SDK-managed path (i.e. when TRACING is OFF and\n// @sentry/node runs its own bundled OTel — see skipOpenTelemetrySetup below).\n//\n// IMPORTANT: when TRACING_ENABLED, this value does NOT govern span volume.\n// Our NodeSDK (below) owns the pipeline and is constructed with no explicit\n// `sampler`, so @opentelemetry/sdk-node falls back to buildSamplerFromEnv()\n// and the REAL head-sampling knob is the standard OTEL_TRACES_SAMPLER /\n// OTEL_TRACES_SAMPLER_ARG env (set per-deploy in the k8s chart). That head\n// decision gates spans before any processor runs, so it bounds BOTH the\n// SentrySpanProcessor (→ Sentry transactions) and the Tempo OTLP export.\n// (Wiring @sentry/opentelemetry's SentrySampler here would let this rate\n// drive both backends and make DSC/sample_rand propagation spec-correct, but\n// that only matters for Sentry server-side dynamic sampling — a SaaS feature\n// our self-hosted install doesn't run — so it's intentionally deferred.)\nconst SENTRY_TRACES_SAMPLE_RATE = parseFloat(\n process.env.SENTRY_TRACES_SAMPLE_RATE ?? \"0.1\",\n);\n\nif (SENTRY_DSN) {\n logger.info(\"Initialized Sentry with env: @env\", process.env.SENTRY_ENV);\n Sentry.init({\n dsn: 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 // 0 in errors-only mode so even @sentry/node's bundled auto-OTel path\n // (used when TRACING_ENABLED is false) produces no transactions.\n tracesSampleRate: SENTRY_TRACING_TO_SENTRY ? SENTRY_TRACES_SAMPLE_RATE : 0,\n // When tracing is on, our NodeSDK below owns the OTel globals and Sentry\n // receives spans via SentrySpanProcessor. Skipping Sentry's bundled OTel\n // setup avoids two TracerProviders fighting over setGlobalTracerProvider.\n // When tracing is off, leave the flag unset so @sentry/node's default\n // auto-OTel still records HTTP transactions for the APM dashboard.\n skipOpenTelemetrySetup: TRACING_ENABLED,\n });\n}\n\nconst meterProvider: MeterProvider | undefined = createMeterProviderFromEnv({\n OTEL_EXPORTER_OTLP_ENDPOINT: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,\n OTEL_METRIC_EXPORT_INTERVAL: process.env.OTEL_METRIC_EXPORT_INTERVAL,\n OTEL_SERVICE_NAME: process.env.OTEL_SERVICE_NAME,\n});\nlet eventLoopInstrumentation: EventLoopInstrumentation | undefined;\nif (meterProvider) {\n // One-way door: must register before any code calls metrics.getMeter() —\n // most notably ReactorInstrumentation inside the reactor module.\n metrics.setGlobalMeterProvider(meterProvider);\n // Host event-loop and CPU gauges (reactor.host.eventloop.*,\n // reactor.host.cpu.utilization). The bench sweep identified the host event\n // loop, not the executor, as the binding constraint on write throughput\n // (bench/test/integration/BASELINE.md, Run 10) — these are the production\n // counterpart of the metrics that finding was made from. Must come after\n // setGlobalMeterProvider or the gauges bind to a no-op meter.\n eventLoopInstrumentation = registerEventLoopInstrumentation();\n}\n\nlet sdk: NodeSDK | undefined;\n\nif (TRACING_ENABLED) {\n logger.info(`Initializing OpenTelemetry tracing for ${SERVICE_NAME}`);\n if (TEMPO_ENDPOINT) logger.info(` Tempo endpoint: ${TEMPO_ENDPOINT}`);\n if (SENTRY_DSN) logger.info(` Sentry span forwarding: enabled`);\n logger.info(` Tenant: ${TENANT_ID}`);\n\n const resource = resourceFromAttributes({\n [ATTR_SERVICE_NAME]: SERVICE_NAME,\n [ATTR_SERVICE_VERSION]: SERVICE_VERSION,\n \"tenant.id\": TENANT_ID,\n \"deployment.environment\": DEPLOY_ENV,\n });\n\n const spanProcessors: SpanProcessor[] = [];\n if (TEMPO_ENDPOINT) {\n spanProcessors.push(\n new BatchSpanProcessor(new OTLPTraceExporter({ url: TEMPO_ENDPOINT })),\n );\n }\n if (SENTRY_TRACING_TO_SENTRY) {\n // Fan the same OTel spans into Sentry — same trace IDs as Tempo, so\n // Sentry transactions cross-link to traces in Grafana. Skipped in\n // errors-only mode (SENTRY_TRACING_ENABLED=false): spans still flow to\n // Tempo via the BatchSpanProcessor above, just not to Sentry.\n spanProcessors.push(new SentrySpanProcessor());\n }\n\n sdk = new NodeSDK({\n resource,\n spanProcessors,\n textMapPropagator: SENTRY_TRACING_TO_SENTRY\n ? new SentryPropagator()\n : undefined,\n instrumentations: [\n new HttpInstrumentation({\n ignoreIncomingRequestHook: (req) =>\n req.url === \"/health\" || req.url === \"/ready\",\n requireParentforIncomingSpans: false,\n requireParentforOutgoingSpans: false,\n requestHook: (span, request) => {\n span.setAttribute(\n \"http.route\",\n redactWebhookPath((request as IncomingMessage).url),\n );\n },\n responseHook: (span, response) => {\n if (response.statusCode) {\n span.setAttribute(\"http.status_code\", response.statusCode);\n }\n },\n }),\n new ExpressInstrumentation({\n requestHook: (span, info) => {\n if (info.route) span.setAttribute(\"http.route\", info.route);\n },\n }),\n // HttpInstrumentation only patches node:http/https; outbound global\n // fetch() goes through undici and is otherwise untraced — e.g. the\n // per-request Renown credential check in AuthService.verifyCredentialExists.\n new UndiciInstrumentation(),\n new GraphQLInstrumentation({ mergeItems: true, allowValues: true }),\n // requireParentSpan: only trace DB queries that run inside a request\n // (HTTP/GraphQL) span. Parentless queries — the management\n // switchboard's background polling loops (vetra-cloud-observability\n // reconcile @60s + clint pull-worker @15s, each writing\n // environment_pods / clint_runtime_endpoints) — would otherwise each\n // become a standalone root transaction. That volume scales O(tenant\n // count) and was ~70% of all Sentry transactions before this change.\n // Dropping it at the instrumentation layer (no span created at all) is\n // cheaper and cleaner than sampling it away downstream.\n new PgInstrumentation({\n enhancedDatabaseReporting: true,\n requireParentSpan: true,\n }),\n ],\n });\n sdk.start();\n if (\n SENTRY_TRACING_TO_SENTRY &&\n typeof Sentry.validateOpenTelemetrySetup === \"function\"\n ) {\n Sentry.validateOpenTelemetrySetup();\n }\n logger.info(\"OpenTelemetry tracing initialized\");\n}\n\nasync function shutdown() {\n // Detach the observable callback before the provider drains, so the final\n // export does not fire a gauge callback against a shutting-down meter.\n eventLoopInstrumentation?.stop();\n await Promise.race([\n Promise.all([\n meterProvider?.shutdown().catch(() => undefined),\n sdk?.shutdown().catch(() => undefined),\n ]),\n new Promise<void>((resolve) => setTimeout(resolve, 5_000)),\n ]);\n}\n\nprocess.on(\"SIGINT\", () => {\n void shutdown().finally(() => process.exit(0));\n});\nprocess.on(\"SIGTERM\", () => {\n void shutdown().finally(() => process.exit(0));\n});\n\nexport { meterProvider, sdk };\n","import dotenv from \"dotenv\";\ndotenv.config();\n\nimport { getConfig } from \"@powerhousedao/config/node\";\nimport { parseForcePgVersion } from \"./pglite-version.js\";\nimport type {\n SwitchboardDriveDocumentType,\n SwitchboardDriveInput,\n} from \"./types.js\";\nconst phConfig = getConfig();\nconst { switchboard } = phConfig;\ninterface Config {\n database: {\n url: string;\n };\n port: number;\n mcp: boolean;\n migratePglite: boolean;\n forcePgVersion: 16 | 17 | null;\n drive: SwitchboardDriveInput;\n}\n\nfunction parseDriveType(\n raw: string | undefined,\n): SwitchboardDriveDocumentType | undefined {\n if (!raw) return undefined;\n if (raw === \"powerhouse/document-drive\" || raw === \"powerhouse/reactor-drive\")\n return raw;\n throw new Error(\n `Invalid PH_DEFAULT_DRIVE_TYPE: ${raw}. Expected \"powerhouse/document-drive\" or \"powerhouse/reactor-drive\".`,\n );\n}\n\nexport const config: Config = {\n database: {\n // url: process.env.PH_SWITCHBOARD_DATABASE_URL ?? switchboard?.database?.url ?? \"dev.db\",\n url:\n process.env.PH_SWITCHBOARD_DATABASE_URL ??\n switchboard?.database?.url ??\n \"dev.db\",\n },\n port:\n process.env.PH_SWITCHBOARD_PORT &&\n !isNaN(Number(process.env.PH_SWITCHBOARD_PORT))\n ? Number(process.env.PH_SWITCHBOARD_PORT)\n : (switchboard?.port ?? 4001),\n mcp: true,\n migratePglite: process.env.PH_MIGRATE_PGLITE === \"true\",\n forcePgVersion: parseForcePgVersion(process.env.PH_FORCE_PG_VERSION),\n drive: {\n id: \"powerhouse\",\n slug: \"powerhouse\",\n documentType: parseDriveType(process.env.PH_DEFAULT_DRIVE_TYPE),\n global: {\n name: \"Powerhouse\",\n icon: \"https://ipfs.io/ipfs/QmcaTDBYn8X2psGaXe7iQ6qd8q6oqHLgxvMX9yXf7f9uP7\",\n },\n local: {\n availableOffline: true,\n listeners: [],\n sharingType: \"public\",\n triggers: [],\n },\n },\n};\n","import type { PyroscopeConfig } from \"@pyroscope/nodejs\";\n\nexport async function initProfilerFromEnv(env: typeof process.env) {\n const {\n PYROSCOPE_SERVER_ADDRESS: serverAddress,\n PYROSCOPE_APPLICATION_NAME: appName,\n PYROSCOPE_USER: basicAuthUser,\n PYROSCOPE_PASSWORD: basicAuthPassword,\n PYROSCOPE_WALL_ENABLED: wallEnabled,\n PYROSCOPE_HEAP_ENABLED: heapEnabled,\n } = env;\n\n const options: PyroscopeConfig = {\n serverAddress,\n appName,\n basicAuthUser,\n basicAuthPassword,\n // Wall profiling captures wall-clock time (includes async I/O waits)\n // This shows GraphQL resolvers even when waiting for database\n wall: {\n samplingDurationMs: 10000, // 10 second sampling windows\n samplingIntervalMicros: 10000, // 10ms sampling interval (100 samples/sec)\n collectCpuTime: true, // Also collect CPU time alongside wall time\n },\n // Heap profiling for memory allocation tracking\n heap: {\n samplingIntervalBytes: 512 * 1024, // Sample every 512KB allocated\n stackDepth: 64, // Capture deeper stacks for better context\n },\n };\n return initProfiler(options, {\n wallEnabled: wallEnabled !== \"false\",\n heapEnabled: heapEnabled === \"true\",\n });\n}\n\ninterface ProfilerFlags {\n wallEnabled?: boolean;\n heapEnabled?: boolean;\n}\n\nexport async function initProfiler(\n options?: PyroscopeConfig,\n flags: ProfilerFlags = { wallEnabled: true, heapEnabled: false },\n) {\n console.log(\"Initializing Pyroscope profiler at:\", options?.serverAddress);\n console.log(\" Wall profiling:\", flags.wallEnabled ? \"enabled\" : \"disabled\");\n console.log(\" Heap profiling:\", flags.heapEnabled ? \"enabled\" : \"disabled\");\n\n const { default: Pyroscope } = await import(\"@pyroscope/nodejs\");\n Pyroscope.init(options);\n\n // Start wall profiling (captures async I/O time - shows resolvers)\n if (flags.wallEnabled) {\n Pyroscope.startWallProfiling();\n }\n\n // Start CPU profiling (captures CPU-bound work)\n Pyroscope.startCpuProfiling();\n\n // Optionally start heap profiling (memory allocations)\n if (flags.heapEnabled) {\n Pyroscope.startHeapProfiling();\n }\n}\n","#!/usr/bin/env node\n// Observability MUST load before any module that imports http/express/pg/graphql\n// so OpenTelemetry's require-time hooks can patch them. It also owns Sentry\n// init and the SIGINT/SIGTERM flush.\nimport \"./observability.mjs\";\n\nimport * as Sentry from \"@sentry/node\";\nimport { childLogger } from \"document-model\";\nimport { config } from \"./config.js\";\nimport { initProfilerFromEnv } from \"./profiler.js\";\nimport { startSwitchboard } from \"./server.mjs\";\n\nconst logger = childLogger([\"switchboard\"]);\n\nfunction ensureNodeVersion(minVersion = \"24\") {\n const version = process.versions.node;\n if (!version) {\n return;\n }\n\n if (version < minVersion) {\n console.error(\n `Node version ${minVersion} or higher is required. Current version: ${version}`,\n );\n process.exit(1);\n }\n}\n// Ensure minimum Node.js version\nensureNodeVersion(\"24\");\n\n// Each subgraph registers its own SIGINT/SIGTERM listeners, and the count\n// scales with dynamically-loaded document models beyond the default cap of 10.\nprocess.setMaxListeners(0);\n\nif (process.env.PYROSCOPE_SERVER_ADDRESS) {\n try {\n await initProfilerFromEnv(process.env);\n } catch (e) {\n Sentry.captureException(e);\n logger.error(\"Error starting profiler: @error\", e);\n }\n}\n\nconst cliMigratePglite = process.argv.slice(2).includes(\"--migrate-pglite\");\n\n// A boot failure leaves no serving process behind, so it must exit non-zero:\n// logging and continuing hides a dead switchboard from the supervisor, which\n// would otherwise never restart it.\ntry {\n await startSwitchboard({\n ...config,\n migratePglite: cliMigratePglite || config.migratePglite,\n forcePgVersion: config.forcePgVersion ?? undefined,\n });\n} catch (e) {\n Sentry.captureException(e);\n logger.error(\"Switchboard failed to start: @error\", e);\n await Sentry.flush(2000).catch(() => undefined);\n process.exit(1);\n}\n"],"names":["logger","logger"],"mappings":"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","debug_id":"c6c10af1-f4cc-56a5-abf6-fa35cf0de956"}
1
+ {"version":3,"file":"index.mjs","sources":["../src/metrics.ts","../src/observability-signals.mts","../src/redact-webhook-path.ts","../src/observability.mts","../src/config.ts","../src/index.mts"],"sourcesContent":["import { OTLPMetricExporter } from \"@opentelemetry/exporter-metrics-otlp-http\";\nimport { resourceFromAttributes } from \"@opentelemetry/resources\";\nimport {\n MeterProvider,\n PeriodicExportingMetricReader,\n} from \"@opentelemetry/sdk-metrics\";\nimport { childLogger } from \"document-model\";\n\nconst logger = childLogger([\"switchboard\", \"metrics\"]);\n\nexport function createMeterProviderFromEnv(env: {\n OTEL_EXPORTER_OTLP_ENDPOINT?: string;\n OTEL_METRIC_EXPORT_INTERVAL?: string;\n OTEL_SERVICE_NAME?: string;\n}): MeterProvider | undefined {\n const endpoint = env.OTEL_EXPORTER_OTLP_ENDPOINT;\n if (!endpoint) return undefined;\n\n const parsed = parseInt(env.OTEL_METRIC_EXPORT_INTERVAL ?? \"\", 10);\n const exportIntervalMillis =\n Number.isFinite(parsed) && parsed > 0 ? parsed : 5_000;\n\n const base = endpoint.replace(/\\/$/, \"\");\n const exporterUrl = base.endsWith(\"/v1/metrics\")\n ? base\n : `${base}/v1/metrics`;\n\n logger.info(`Initializing OpenTelemetry metrics exporter at: ${endpoint}`);\n const meterProvider = new MeterProvider({\n resource: resourceFromAttributes({\n \"service.name\": env.OTEL_SERVICE_NAME ?? \"switchboard\",\n }),\n readers: [\n new PeriodicExportingMetricReader({\n exporter: new OTLPMetricExporter({\n url: exporterUrl,\n }),\n exportIntervalMillis,\n exportTimeoutMillis: Math.max(exportIntervalMillis - 250, 1),\n }),\n ],\n });\n logger.info(`Metrics export enabled (interval: ${exportIntervalMillis}ms)`);\n return meterProvider;\n}\n","// Signal handling for the observability bootstrap, split out of\n// observability.mts so tests can exercise it without importing the module\n// whose load-time side effects start Sentry and OpenTelemetry.\n\ntype SignalProcess = Pick<NodeJS.Process, \"on\" | \"exit\"> & {\n exitCode?: NodeJS.Process[\"exitCode\"];\n};\n\n/**\n * Flushes telemetry on SIGINT/SIGTERM, then exits only when no other module\n * has claimed a failing exit.\n *\n * Exit ownership, decided here on purpose:\n *\n * - Ordinary signal (process.exitCode unset or 0): this handler exits with\n * the current exit code after the flush settles (the flush bounds itself,\n * see shutdown() in observability.mts). When the reactor builder's drain\n * handler is also attached it runs first (prependListener) and swaps\n * process.exit for a recording shim before this handler's exit call, so\n * the drain still finishes and performs the real exit; when the builder is\n * not attached (startup, signalHandlers: false), this exit is what\n * terminates the process, with code 0.\n *\n * - Fatal shutdown in progress (process.exitCode already non-zero):\n * fatal-shutdown.mts sets exitCode = 1 and arms a 15s forced exit BEFORE\n * raising SIGTERM, precisely so the builder's drain can flush the PGlite\n * snapshots. Calling exit here would lower that code to 0 (supervisors\n * would see a crash as success) and, whenever the builder's shim is not in\n * place, cut the drain off at this handler's shorter flush budget. So this\n * handler never exits once a non-zero code is set: the exit belongs to the\n * builder's drain, or failing that to fatal-shutdown's 15s backstop, both\n * of which preserve code 1.\n */\nexport function installObservabilitySignalHandlers(\n flush: () => Promise<void>,\n proc: SignalProcess = process,\n): void {\n const onSignal = () => {\n void flush().finally(() => {\n const code = proc.exitCode;\n if (code !== undefined && code !== 0) {\n return;\n }\n proc.exit(code ?? 0);\n });\n };\n proc.on(\"SIGINT\", onSignal);\n proc.on(\"SIGTERM\", onSignal);\n}\n","const WEBHOOK_SEGMENT = \"/webhooks/\";\n\n/**\n * Strips a webhook's delivery token out of a request path.\n *\n * The token is the endpoint's whole credential and it sits in the path, so it\n * must not reach a log line or a span attribute that ships off-box. The family\n * mounts under the host's base path, so the segment can sit at any depth.\n */\nexport function redactWebhookPath(url: string | undefined): string {\n if (!url) return \"\";\n // Dropped with the token: `http.route` is meant to be a template, and a\n // query string is another place a sender can put a credential.\n const path = url.split(\"?\")[0] ?? url;\n const at = path.indexOf(WEBHOOK_SEGMENT);\n if (at === -1) return url;\n // Nothing after the segment is nothing to redact.\n if (path.length === at + WEBHOOK_SEGMENT.length) return url;\n return `${path.slice(0, at)}${WEBHOOK_SEGMENT}[redacted]`;\n}\n","// Single observability bootstrap: Sentry + OpenTelemetry (tracing + metrics).\n//\n// MUST be imported as the very first module in apps/switchboard/src/index.mts.\n// OpenTelemetry instrumentations register require-time hooks at module load,\n// so http/express/pg/graphql must not be imported (transitively) before this\n// file runs.\n//\n// Replaces three legacy bootstrap sites:\n// - apps/switchboard/src/server.mts top-level Sentry.init\n// - apps/switchboard/src/metrics.ts standalone MeterProvider (still exported\n// here via createMeterProviderFromEnv so its tests keep passing)\n// - packages/reactor-api/src/tracing.ts side-effect NodeSDK\nimport { metrics } from \"@opentelemetry/api\";\nimport { OTLPTraceExporter } from \"@opentelemetry/exporter-trace-otlp-http\";\nimport { ExpressInstrumentation } from \"@opentelemetry/instrumentation-express\";\nimport { GraphQLInstrumentation } from \"@opentelemetry/instrumentation-graphql\";\nimport { HttpInstrumentation } from \"@opentelemetry/instrumentation-http\";\nimport { PgInstrumentation } from \"@opentelemetry/instrumentation-pg\";\nimport { UndiciInstrumentation } from \"@opentelemetry/instrumentation-undici\";\nimport { resourceFromAttributes } from \"@opentelemetry/resources\";\nimport type { MeterProvider } from \"@opentelemetry/sdk-metrics\";\nimport { NodeSDK } from \"@opentelemetry/sdk-node\";\nimport {\n BatchSpanProcessor,\n type SpanProcessor,\n} from \"@opentelemetry/sdk-trace-base\";\nimport {\n ATTR_SERVICE_NAME,\n ATTR_SERVICE_VERSION,\n} from \"@opentelemetry/semantic-conventions\";\nimport {\n registerEventLoopInstrumentation,\n type EventLoopInstrumentation,\n} from \"@powerhousedao/opentelemetry-instrumentation-reactor\";\nimport * as Sentry from \"@sentry/node\";\nimport { SentryPropagator, SentrySpanProcessor } from \"@sentry/opentelemetry\";\nimport { childLogger } from \"document-model\";\nimport type { IncomingMessage } from \"node:http\";\nimport { createMeterProviderFromEnv } from \"./metrics.js\";\nimport { installObservabilitySignalHandlers } from \"./observability-signals.mjs\";\nimport { redactWebhookPath } from \"./redact-webhook-path.js\";\n\nconst logger = childLogger([\"switchboard\", \"observability\"]);\n\nconst SERVICE_NAME = process.env.OTEL_SERVICE_NAME || \"switchboard\";\nconst SERVICE_VERSION = process.env.npm_package_version || \"unknown\";\nconst TENANT_ID = process.env.TENANT_ID || \"default\";\nconst DEPLOY_ENV = process.env.NODE_ENV || \"development\";\n\nconst TEMPO_ENDPOINT = process.env.TEMPO_ENDPOINT;\nconst SENTRY_DSN = process.env.SENTRY_DSN;\n\n// Whether to forward TRANSACTIONS (spans) to Sentry. Errors are always sent\n// when SENTRY_DSN is set — this flag only gates APM/tracing into Sentry.\n// Default on (back-compat). Set SENTRY_TRACING_ENABLED=false for an\n// \"errors-only\" deployment: errors still go to Sentry, traces still go to\n// Tempo, but no transactions hit Sentry (no Kafka/ClickHouse/nodestore\n// cost). This is the recommended mode for tenant workloads at scale —\n// Sentry's value there is error grouping; traces live in Tempo.\nconst SENTRY_TRACING_TO_SENTRY =\n Boolean(SENTRY_DSN) && process.env.SENTRY_TRACING_ENABLED !== \"false\";\n\nconst TRACING_REQUESTED =\n process.env.ENABLE_TRACING === \"true\" ||\n process.env.NODE_ENV === \"production\";\nconst HAS_TRACE_DESTINATION = Boolean(TEMPO_ENDPOINT) || Boolean(SENTRY_DSN);\nconst TRACING_ENABLED = TRACING_REQUESTED && HAS_TRACE_DESTINATION;\n\nif (TRACING_REQUESTED && !HAS_TRACE_DESTINATION) {\n logger.warn(\n \"Tracing was requested (NODE_ENV=production or ENABLE_TRACING=true) but \" +\n \"no destination is configured — instrumentation will not run. Set \" +\n \"TEMPO_ENDPOINT (e.g. http://tempo.monitoring.svc.cluster.local:4318/v1/traces) \" +\n \"to export OTLP spans, and/or SENTRY_DSN to forward spans to Sentry.\",\n );\n}\n\n// APM sampling for the Sentry-SDK-managed path (i.e. when TRACING is OFF and\n// @sentry/node runs its own bundled OTel — see skipOpenTelemetrySetup below).\n//\n// IMPORTANT: when TRACING_ENABLED, this value does NOT govern span volume.\n// Our NodeSDK (below) owns the pipeline and is constructed with no explicit\n// `sampler`, so @opentelemetry/sdk-node falls back to buildSamplerFromEnv()\n// and the REAL head-sampling knob is the standard OTEL_TRACES_SAMPLER /\n// OTEL_TRACES_SAMPLER_ARG env (set per-deploy in the k8s chart). That head\n// decision gates spans before any processor runs, so it bounds BOTH the\n// SentrySpanProcessor (→ Sentry transactions) and the Tempo OTLP export.\n// (Wiring @sentry/opentelemetry's SentrySampler here would let this rate\n// drive both backends and make DSC/sample_rand propagation spec-correct, but\n// that only matters for Sentry server-side dynamic sampling — a SaaS feature\n// our self-hosted install doesn't run — so it's intentionally deferred.)\nconst SENTRY_TRACES_SAMPLE_RATE = parseFloat(\n process.env.SENTRY_TRACES_SAMPLE_RATE ?? \"0.1\",\n);\n\nif (SENTRY_DSN) {\n logger.info(\"Initialized Sentry with env: @env\", process.env.SENTRY_ENV);\n Sentry.init({\n dsn: 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 // 0 in errors-only mode so even @sentry/node's bundled auto-OTel path\n // (used when TRACING_ENABLED is false) produces no transactions.\n tracesSampleRate: SENTRY_TRACING_TO_SENTRY ? SENTRY_TRACES_SAMPLE_RATE : 0,\n // When tracing is on, our NodeSDK below owns the OTel globals and Sentry\n // receives spans via SentrySpanProcessor. Skipping Sentry's bundled OTel\n // setup avoids two TracerProviders fighting over setGlobalTracerProvider.\n // When tracing is off, leave the flag unset so @sentry/node's default\n // auto-OTel still records HTTP transactions for the APM dashboard.\n skipOpenTelemetrySetup: TRACING_ENABLED,\n });\n}\n\nconst meterProvider: MeterProvider | undefined = createMeterProviderFromEnv({\n OTEL_EXPORTER_OTLP_ENDPOINT: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,\n OTEL_METRIC_EXPORT_INTERVAL: process.env.OTEL_METRIC_EXPORT_INTERVAL,\n OTEL_SERVICE_NAME: process.env.OTEL_SERVICE_NAME,\n});\nlet eventLoopInstrumentation: EventLoopInstrumentation | undefined;\nif (meterProvider) {\n // One-way door: must register before any code calls metrics.getMeter() —\n // most notably ReactorInstrumentation inside the reactor module.\n metrics.setGlobalMeterProvider(meterProvider);\n // Host event-loop and CPU gauges (reactor.host.eventloop.*,\n // reactor.host.cpu.utilization). The bench sweep identified the host event\n // loop, not the executor, as the binding constraint on write throughput\n // (bench/test/integration/BASELINE.md, Run 10) — these are the production\n // counterpart of the metrics that finding was made from. Must come after\n // setGlobalMeterProvider or the gauges bind to a no-op meter.\n eventLoopInstrumentation = registerEventLoopInstrumentation();\n}\n\nlet sdk: NodeSDK | undefined;\n\nif (TRACING_ENABLED) {\n logger.info(`Initializing OpenTelemetry tracing for ${SERVICE_NAME}`);\n if (TEMPO_ENDPOINT) logger.info(` Tempo endpoint: ${TEMPO_ENDPOINT}`);\n if (SENTRY_DSN) logger.info(` Sentry span forwarding: enabled`);\n logger.info(` Tenant: ${TENANT_ID}`);\n\n const resource = resourceFromAttributes({\n [ATTR_SERVICE_NAME]: SERVICE_NAME,\n [ATTR_SERVICE_VERSION]: SERVICE_VERSION,\n \"tenant.id\": TENANT_ID,\n \"deployment.environment\": DEPLOY_ENV,\n });\n\n const spanProcessors: SpanProcessor[] = [];\n if (TEMPO_ENDPOINT) {\n spanProcessors.push(\n new BatchSpanProcessor(new OTLPTraceExporter({ url: TEMPO_ENDPOINT })),\n );\n }\n if (SENTRY_TRACING_TO_SENTRY) {\n // Fan the same OTel spans into Sentry — same trace IDs as Tempo, so\n // Sentry transactions cross-link to traces in Grafana. Skipped in\n // errors-only mode (SENTRY_TRACING_ENABLED=false): spans still flow to\n // Tempo via the BatchSpanProcessor above, just not to Sentry.\n spanProcessors.push(new SentrySpanProcessor());\n }\n\n sdk = new NodeSDK({\n resource,\n spanProcessors,\n textMapPropagator: SENTRY_TRACING_TO_SENTRY\n ? new SentryPropagator()\n : undefined,\n instrumentations: [\n new HttpInstrumentation({\n ignoreIncomingRequestHook: (req) =>\n req.url === \"/health\" || req.url === \"/ready\",\n requireParentforIncomingSpans: false,\n requireParentforOutgoingSpans: false,\n requestHook: (span, request) => {\n span.setAttribute(\n \"http.route\",\n redactWebhookPath((request as IncomingMessage).url),\n );\n },\n responseHook: (span, response) => {\n if (response.statusCode) {\n span.setAttribute(\"http.status_code\", response.statusCode);\n }\n },\n }),\n new ExpressInstrumentation({\n requestHook: (span, info) => {\n if (info.route) span.setAttribute(\"http.route\", info.route);\n },\n }),\n // HttpInstrumentation only patches node:http/https; outbound global\n // fetch() goes through undici and is otherwise untraced — e.g. the\n // per-request Renown credential check in AuthService.verifyCredentialExists.\n new UndiciInstrumentation(),\n new GraphQLInstrumentation({ mergeItems: true, allowValues: true }),\n // requireParentSpan: only trace DB queries that run inside a request\n // (HTTP/GraphQL) span. Parentless queries — the management\n // switchboard's background polling loops (vetra-cloud-observability\n // reconcile @60s + clint pull-worker @15s, each writing\n // environment_pods / clint_runtime_endpoints) — would otherwise each\n // become a standalone root transaction. That volume scales O(tenant\n // count) and was ~70% of all Sentry transactions before this change.\n // Dropping it at the instrumentation layer (no span created at all) is\n // cheaper and cleaner than sampling it away downstream.\n new PgInstrumentation({\n enhancedDatabaseReporting: true,\n requireParentSpan: true,\n }),\n ],\n });\n sdk.start();\n if (\n SENTRY_TRACING_TO_SENTRY &&\n typeof Sentry.validateOpenTelemetrySetup === \"function\"\n ) {\n Sentry.validateOpenTelemetrySetup();\n }\n logger.info(\"OpenTelemetry tracing initialized\");\n}\n\nasync function shutdown() {\n // Detach the observable callback before the provider drains, so the final\n // export does not fire a gauge callback against a shutting-down meter.\n eventLoopInstrumentation?.stop();\n await Promise.race([\n Promise.all([\n meterProvider?.shutdown().catch(() => undefined),\n sdk?.shutdown().catch(() => undefined),\n ]),\n new Promise<void>((resolve) => setTimeout(resolve, 5_000)),\n ]);\n}\n\n// Exit-code and drain coordination with fatal-shutdown.mts and the reactor\n// builder lives in observability-signals.mts; the earlier inline handlers\n// exited with a literal 0, which turned a fatal crash (exitCode = 1) into a\n// success in the supervisor's eyes and could cut the snapshot drain short.\ninstallObservabilitySignalHandlers(shutdown);\n\nexport { meterProvider, sdk };\n","import dotenv from \"dotenv\";\ndotenv.config();\n\nimport { getConfig } from \"@powerhousedao/config/node\";\nimport { parseForcePgVersion } from \"./pglite-version.js\";\nimport type {\n SwitchboardDriveDocumentType,\n SwitchboardDriveInput,\n} from \"./types.js\";\nconst phConfig = getConfig();\nconst { switchboard } = phConfig;\ninterface Config {\n database: {\n url: string;\n };\n port: number;\n mcp: boolean;\n migratePglite: boolean;\n forcePgVersion: 16 | 17 | null;\n drive: SwitchboardDriveInput;\n}\n\nfunction parseDriveType(\n raw: string | undefined,\n): SwitchboardDriveDocumentType | undefined {\n if (!raw) return undefined;\n if (raw === \"powerhouse/document-drive\" || raw === \"powerhouse/reactor-drive\")\n return raw;\n throw new Error(\n `Invalid PH_DEFAULT_DRIVE_TYPE: ${raw}. Expected \"powerhouse/document-drive\" or \"powerhouse/reactor-drive\".`,\n );\n}\n\nexport const config: Config = {\n database: {\n // url: process.env.PH_SWITCHBOARD_DATABASE_URL ?? switchboard?.database?.url ?? \"dev.db\",\n url:\n process.env.PH_SWITCHBOARD_DATABASE_URL ??\n switchboard?.database?.url ??\n \"dev.db\",\n },\n port:\n process.env.PH_SWITCHBOARD_PORT &&\n !isNaN(Number(process.env.PH_SWITCHBOARD_PORT))\n ? Number(process.env.PH_SWITCHBOARD_PORT)\n : (switchboard?.port ?? 4001),\n mcp: true,\n migratePglite: process.env.PH_MIGRATE_PGLITE === \"true\",\n forcePgVersion: parseForcePgVersion(process.env.PH_FORCE_PG_VERSION),\n drive: {\n id: \"powerhouse\",\n slug: \"powerhouse\",\n documentType: parseDriveType(process.env.PH_DEFAULT_DRIVE_TYPE),\n global: {\n name: \"Powerhouse\",\n icon: \"https://ipfs.io/ipfs/QmcaTDBYn8X2psGaXe7iQ6qd8q6oqHLgxvMX9yXf7f9uP7\",\n },\n local: {\n availableOffline: true,\n listeners: [],\n sharingType: \"public\",\n triggers: [],\n },\n },\n};\n","#!/usr/bin/env node\n// Observability MUST load before any module that imports http/express/pg/graphql\n// so OpenTelemetry's require-time hooks can patch them. It also owns Sentry\n// init and the SIGINT/SIGTERM flush.\nimport \"./observability.mjs\";\n\nimport * as Sentry from \"@sentry/node\";\nimport { childLogger } from \"document-model\";\nimport { config } from \"./config.js\";\nimport { initProfilerFromEnv } from \"./profiler.js\";\nimport { startSwitchboard } from \"./server.mjs\";\nimport { StoredDocumentsRefusedError } from \"./unsupported-stored-documents.mjs\";\n\nconst logger = childLogger([\"switchboard\"]);\n\nfunction ensureNodeVersion(minVersion = \"24\") {\n const version = process.versions.node;\n if (!version) {\n return;\n }\n\n if (version < minVersion) {\n console.error(\n `Node version ${minVersion} or higher is required. Current version: ${version}`,\n );\n process.exit(1);\n }\n}\n// Ensure minimum Node.js version\nensureNodeVersion(\"24\");\n\n// Each subgraph registers its own SIGINT/SIGTERM listeners, and the count\n// scales with dynamically-loaded document models beyond the default cap of 10.\nprocess.setMaxListeners(0);\n\nif (process.env.PYROSCOPE_SERVER_ADDRESS) {\n try {\n await initProfilerFromEnv(process.env);\n } catch (e) {\n Sentry.captureException(e);\n logger.error(\"Error starting profiler: @error\", e);\n }\n}\n\nconst cliMigratePglite = process.argv.slice(2).includes(\"--migrate-pglite\");\n\n// A boot failure leaves no serving process behind, so it must exit non-zero:\n// logging and continuing hides a dead switchboard from the supervisor, which\n// would otherwise never restart it.\ntry {\n await startSwitchboard({\n ...config,\n migratePglite: cliMigratePglite || config.migratePglite,\n forcePgVersion: config.forcePgVersion ?? undefined,\n fatalErrorShutdown: true,\n });\n} catch (e) {\n // startSwitchboard already reported a refusal to the operator.\n if (!StoredDocumentsRefusedError.isError(e)) {\n Sentry.captureException(e);\n logger.error(\"Switchboard failed to start: @error\", e);\n }\n await Sentry.flush(2000).catch(() => undefined);\n process.exit(1);\n}\n"],"names":["logger","logger"],"mappings":"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","debug_id":"d30347ec-a382-56a4-b3ef-bdacc9d409fd"}
@@ -1 +1,2 @@
1
- export { };
1
+ export {}
2
+ export {};
@@ -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]="50e38408-9102-5ba1-977a-69acc9cb0fe9")}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]="58dbe86a-e88c-57d1-9ab1-54f06a068229")}catch(e){}}();
3
3
  import path from "path";
4
4
  import { execSync } from "child_process";
5
5
  import fs from "fs";
@@ -31,4 +31,4 @@ try {
31
31
  export {};
32
32
 
33
33
  //# sourceMappingURL=install-packages.mjs.map
34
- //# debugId=50e38408-9102-5ba1-977a-69acc9cb0fe9
34
+ //# debugId=58dbe86a-e88c-57d1-9ab1-54f06a068229
@@ -1 +1 @@
1
- {"version":3,"file":"install-packages.mjs","sources":["../src/install-packages.mts"],"sourcesContent":["import { execSync } from \"child_process\";\nimport fs from \"fs\";\nimport path from \"path\";\n\n// Define interface for package.json\ninterface PackageJson {\n dependencies?: Record<string, string>;\n devDependencies?: Record<string, string>;\n}\n\n// Get the list of packages to install from the environment variable\nconst pkgs = process.env.PH_PACKAGES?.split(\",\") || [];\n\n// Skip if no packages to install\nif (pkgs.length === 0 || (pkgs.length === 1 && pkgs[0] === \"\")) {\n process.exit(0);\n}\n\ntry {\n // Read the package.json file to check existing dependencies\n const packageJsonPath = path.join(process.cwd(), \"package.json\");\n const packageJsonContent = fs.readFileSync(packageJsonPath, \"utf-8\");\n const packageJson = JSON.parse(packageJsonContent) as PackageJson;\n\n // Get all installed dependencies\n const installedDependencies: Record<string, string> = {\n ...(packageJson.dependencies || {}),\n ...(packageJson.devDependencies || {}),\n };\n\n for (const pkg of pkgs) {\n if (pkg === \"\") continue;\n\n // Check if the package is already installed\n if (installedDependencies[pkg]) {\n console.log(`> Package ${pkg} is already installed, skipping`);\n continue;\n }\n\n console.log(`> Installing ${pkg}`);\n execSync(`pnpm add ${pkg}@latest`, { stdio: \"inherit\" });\n }\n} catch (error) {\n console.error(\"Error in package installation:\", error);\n process.exit(1);\n}\n"],"names":[],"mappings":";;;;;;AAWA,MAAM,OAAO,QAAQ,IAAI,aAAa,MAAM,IAAI,IAAI,EAAE;AAGtD,IAAI,KAAK,WAAW,KAAM,KAAK,WAAW,KAAK,KAAK,OAAO,GACzD,SAAQ,KAAK,EAAE;AAGjB,IAAI;CAEF,MAAM,kBAAkB,KAAK,KAAK,QAAQ,KAAK,EAAE,eAAe;CAChE,MAAM,qBAAqB,GAAG,aAAa,iBAAiB,QAAQ;CACpE,MAAM,cAAc,KAAK,MAAM,mBAAmB;CAGlD,MAAM,wBAAgD;EACpD,GAAI,YAAY,gBAAgB,EAAE;EAClC,GAAI,YAAY,mBAAmB,EAAE;EACtC;AAED,MAAK,MAAM,OAAO,MAAM;AACtB,MAAI,QAAQ,GAAI;AAGhB,MAAI,sBAAsB,MAAM;AAC9B,WAAQ,IAAI,aAAa,IAAI,iCAAiC;AAC9D;;AAGF,UAAQ,IAAI,gBAAgB,MAAM;AAClC,WAAS,YAAY,IAAI,UAAU,EAAE,OAAO,WAAW,CAAC;;SAEnD,OAAO;AACd,SAAQ,MAAM,kCAAkC,MAAM;AACtD,SAAQ,KAAK,EAAE","debug_id":"50e38408-9102-5ba1-977a-69acc9cb0fe9"}
1
+ {"version":3,"file":"install-packages.mjs","sources":["../src/install-packages.mts"],"sourcesContent":["import { execSync } from \"child_process\";\nimport fs from \"fs\";\nimport path from \"path\";\n\n// Define interface for package.json\ninterface PackageJson {\n dependencies?: Record<string, string>;\n devDependencies?: Record<string, string>;\n}\n\n// Get the list of packages to install from the environment variable\nconst pkgs = process.env.PH_PACKAGES?.split(\",\") || [];\n\n// Skip if no packages to install\nif (pkgs.length === 0 || (pkgs.length === 1 && pkgs[0] === \"\")) {\n process.exit(0);\n}\n\ntry {\n // Read the package.json file to check existing dependencies\n const packageJsonPath = path.join(process.cwd(), \"package.json\");\n const packageJsonContent = fs.readFileSync(packageJsonPath, \"utf-8\");\n const packageJson = JSON.parse(packageJsonContent) as PackageJson;\n\n // Get all installed dependencies\n const installedDependencies: Record<string, string> = {\n ...(packageJson.dependencies || {}),\n ...(packageJson.devDependencies || {}),\n };\n\n for (const pkg of pkgs) {\n if (pkg === \"\") continue;\n\n // Check if the package is already installed\n if (installedDependencies[pkg]) {\n console.log(`> Package ${pkg} is already installed, skipping`);\n continue;\n }\n\n console.log(`> Installing ${pkg}`);\n execSync(`pnpm add ${pkg}@latest`, { stdio: \"inherit\" });\n }\n} catch (error) {\n console.error(\"Error in package installation:\", error);\n process.exit(1);\n}\n"],"names":[],"mappings":";;;;;;AAWA,MAAM,OAAO,QAAQ,IAAI,aAAa,MAAM,GAAG,KAAK,CAAC;AAGrD,IAAI,KAAK,WAAW,KAAM,KAAK,WAAW,KAAK,KAAK,OAAO,IACzD,QAAQ,KAAK,CAAC;AAGhB,IAAI;CAEF,MAAM,kBAAkB,KAAK,KAAK,QAAQ,IAAI,GAAG,cAAc;CAC/D,MAAM,qBAAqB,GAAG,aAAa,iBAAiB,OAAO;CACnE,MAAM,cAAc,KAAK,MAAM,kBAAkB;CAGjD,MAAM,wBAAgD;EACpD,GAAI,YAAY,gBAAgB,CAAC;EACjC,GAAI,YAAY,mBAAmB,CAAC;CACtC;CAEA,KAAK,MAAM,OAAO,MAAM;EACtB,IAAI,QAAQ,IAAI;EAGhB,IAAI,sBAAsB,MAAM;GAC9B,QAAQ,IAAI,aAAa,IAAI,gCAAgC;GAC7D;EACF;EAEA,QAAQ,IAAI,gBAAgB,KAAK;EACjC,SAAS,YAAY,IAAI,UAAU,EAAE,OAAO,UAAU,CAAC;CACzD;AACF,SAAS,OAAO;CACd,QAAQ,MAAM,kCAAkC,KAAK;CACrD,QAAQ,KAAK,CAAC;AAChB","debug_id":"58dbe86a-e88c-57d1-9ab1-54f06a068229"}
@@ -1 +1,2 @@
1
- export { };
1
+ export {}
2
+ export {};
package/dist/migrate.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
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]="faf76c26-cfd8-5dc4-94c3-936a42663059")}catch(e){}}();
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]="6af9cf0a-695b-5636-840b-192b4027e5cb")}catch(e){}}();
4
4
  import dotenv from "dotenv";
5
5
  import { getConfig } from "@powerhousedao/config/node";
6
6
  import { REACTOR_SCHEMA, getMigrationStatus, runMigrations } from "@powerhousedao/reactor";
@@ -22,7 +22,8 @@ async function main() {
22
22
  return;
23
23
  }
24
24
  console.log(`Database: ${dbPath}`);
25
- const db = new Kysely({ dialect: new PostgresDialect({ pool: new Pool({ connectionString: dbPath }) }) });
25
+ const pool = new Pool({ connectionString: dbPath });
26
+ const db = new Kysely({ dialect: new PostgresDialect({ pool }) });
26
27
  try {
27
28
  if (command === "status") {
28
29
  console.log("\nChecking migration status...");
@@ -76,4 +77,4 @@ main();
76
77
  export {};
77
78
 
78
79
  //# sourceMappingURL=migrate.mjs.map
79
- //# debugId=faf76c26-cfd8-5dc4-94c3-936a42663059
80
+ //# debugId=6af9cf0a-695b-5636-840b-192b4027e5cb
@@ -1 +1 @@
1
- {"version":3,"file":"migrate.mjs","sources":["../src/migrate.mts"],"sourcesContent":["#!/usr/bin/env node\nimport dotenv from \"dotenv\";\ndotenv.config();\n\nimport { Kysely, PostgresDialect } from \"kysely\";\nimport { Pool } from \"pg\";\nimport {\n runMigrations,\n getMigrationStatus,\n REACTOR_SCHEMA,\n} from \"@powerhousedao/reactor\";\nimport {\n getReactorDriveMigrationStatus,\n runReactorDriveMigrations,\n} from \"@powerhousedao/reactor-drive\";\nimport { getConfig } from \"@powerhousedao/config/node\";\n\nfunction isPostgresUrl(url: string): boolean {\n return url.startsWith(\"postgresql://\") || url.startsWith(\"postgres://\");\n}\n\nasync function main() {\n const command = process.argv[2];\n const config = getConfig();\n\n const dbPath =\n process.env.PH_SWITCHBOARD_DATABASE_URL ??\n process.env.PH_REACTOR_DATABASE_URL ??\n process.env.DATABASE_URL ??\n config.switchboard?.database?.url;\n\n if (!dbPath || !isPostgresUrl(dbPath)) {\n console.log(\"No PostgreSQL URL configured. Skipping migrations.\");\n console.log(\"(PGlite migrations are handled automatically on startup)\");\n return;\n }\n\n console.log(`Database: ${dbPath}`);\n\n const pool = new Pool({ connectionString: dbPath });\n\n const db = new Kysely<any>({\n dialect: new PostgresDialect({ pool }),\n });\n\n try {\n if (command === \"status\") {\n console.log(\"\\nChecking migration status...\");\n const migrations = await getMigrationStatus(db, REACTOR_SCHEMA);\n\n console.log(\"\\nReactor Migration Status:\");\n console.log(\"=========================\");\n\n for (const migration of migrations) {\n const status = migration.executedAt\n ? `[OK] Executed at ${migration.executedAt.toISOString()}`\n : \"[--] Pending\";\n console.log(`${status} - ${migration.name}`);\n }\n\n const driveMigrations = await getReactorDriveMigrationStatus(\n db,\n REACTOR_SCHEMA,\n );\n\n console.log(\"\\nReactor-Drive Migration Status:\");\n console.log(\"===============================\");\n\n for (const migration of driveMigrations) {\n const status = migration.executedAt\n ? `[OK] Executed at ${migration.executedAt.toISOString()}`\n : \"[--] Pending\";\n console.log(`${status} - ${migration.name}`);\n }\n } else {\n console.log(\"\\nRunning reactor migrations...\");\n const result = await runMigrations(db, REACTOR_SCHEMA);\n\n if (!result.success) {\n console.error(\"Migration failed:\", result.error?.message);\n process.exit(1);\n }\n\n if (result.migrationsExecuted.length === 0) {\n console.log(\"No reactor migrations to run - database is up to date\");\n } else {\n console.log(\n `Successfully executed ${result.migrationsExecuted.length} reactor migration(s):`,\n );\n for (const name of result.migrationsExecuted) {\n console.log(` - ${name}`);\n }\n }\n\n console.log(\"\\nRunning reactor-drive migrations...\");\n const driveResult = await runReactorDriveMigrations(db, REACTOR_SCHEMA);\n\n if (!driveResult.success) {\n console.error(\n \"Reactor-drive migration failed:\",\n driveResult.error?.message,\n );\n process.exit(1);\n }\n\n if (driveResult.migrationsExecuted.length === 0) {\n console.log(\n \"No reactor-drive migrations to run - database is up to date\",\n );\n } else {\n console.log(\n `Successfully executed ${driveResult.migrationsExecuted.length} reactor-drive migration(s):`,\n );\n for (const name of driveResult.migrationsExecuted) {\n console.log(` - ${name}`);\n }\n }\n }\n } catch (error) {\n console.error(\n \"Error:\",\n error instanceof Error ? error.message : String(error),\n );\n process.exit(1);\n } finally {\n await db.destroy();\n }\n}\n\nvoid main();\n"],"names":[],"mappings":";;;;;;;;;;AAEA,OAAO,QAAQ;AAef,SAAS,cAAc,KAAsB;AAC3C,QAAO,IAAI,WAAW,gBAAgB,IAAI,IAAI,WAAW,cAAc;;AAGzE,eAAe,OAAO;CACpB,MAAM,UAAU,QAAQ,KAAK;CAC7B,MAAM,SAAS,WAAW;CAE1B,MAAM,SACJ,QAAQ,IAAI,+BACZ,QAAQ,IAAI,2BACZ,QAAQ,IAAI,gBACZ,OAAO,aAAa,UAAU;AAEhC,KAAI,CAAC,UAAU,CAAC,cAAc,OAAO,EAAE;AACrC,UAAQ,IAAI,qDAAqD;AACjE,UAAQ,IAAI,2DAA2D;AACvE;;AAGF,SAAQ,IAAI,aAAa,SAAS;CAIlC,MAAM,KAAK,IAAI,OAAY,EACzB,SAAS,IAAI,gBAAgB,EAAE,MAHpB,IAAI,KAAK,EAAE,kBAAkB,QAAQ,CAAC,EAGZ,CAAC,EACvC,CAAC;AAEF,KAAI;AACF,MAAI,YAAY,UAAU;AACxB,WAAQ,IAAI,iCAAiC;GAC7C,MAAM,aAAa,MAAM,mBAAmB,IAAI,eAAe;AAE/D,WAAQ,IAAI,8BAA8B;AAC1C,WAAQ,IAAI,4BAA4B;AAExC,QAAK,MAAM,aAAa,YAAY;IAClC,MAAM,SAAS,UAAU,aACrB,oBAAoB,UAAU,WAAW,aAAa,KACtD;AACJ,YAAQ,IAAI,GAAG,OAAO,KAAK,UAAU,OAAO;;GAG9C,MAAM,kBAAkB,MAAM,+BAC5B,IACA,eACD;AAED,WAAQ,IAAI,oCAAoC;AAChD,WAAQ,IAAI,kCAAkC;AAE9C,QAAK,MAAM,aAAa,iBAAiB;IACvC,MAAM,SAAS,UAAU,aACrB,oBAAoB,UAAU,WAAW,aAAa,KACtD;AACJ,YAAQ,IAAI,GAAG,OAAO,KAAK,UAAU,OAAO;;SAEzC;AACL,WAAQ,IAAI,kCAAkC;GAC9C,MAAM,SAAS,MAAM,cAAc,IAAI,eAAe;AAEtD,OAAI,CAAC,OAAO,SAAS;AACnB,YAAQ,MAAM,qBAAqB,OAAO,OAAO,QAAQ;AACzD,YAAQ,KAAK,EAAE;;AAGjB,OAAI,OAAO,mBAAmB,WAAW,EACvC,SAAQ,IAAI,wDAAwD;QAC/D;AACL,YAAQ,IACN,yBAAyB,OAAO,mBAAmB,OAAO,wBAC3D;AACD,SAAK,MAAM,QAAQ,OAAO,mBACxB,SAAQ,IAAI,OAAO,OAAO;;AAI9B,WAAQ,IAAI,wCAAwC;GACpD,MAAM,cAAc,MAAM,0BAA0B,IAAI,eAAe;AAEvE,OAAI,CAAC,YAAY,SAAS;AACxB,YAAQ,MACN,mCACA,YAAY,OAAO,QACpB;AACD,YAAQ,KAAK,EAAE;;AAGjB,OAAI,YAAY,mBAAmB,WAAW,EAC5C,SAAQ,IACN,8DACD;QACI;AACL,YAAQ,IACN,yBAAyB,YAAY,mBAAmB,OAAO,8BAChE;AACD,SAAK,MAAM,QAAQ,YAAY,mBAC7B,SAAQ,IAAI,OAAO,OAAO;;;UAIzB,OAAO;AACd,UAAQ,MACN,UACA,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM,CACvD;AACD,UAAQ,KAAK,EAAE;WACP;AACR,QAAM,GAAG,SAAS;;;AAIjB,MAAM","debug_id":"faf76c26-cfd8-5dc4-94c3-936a42663059"}
1
+ {"version":3,"file":"migrate.mjs","sources":["../src/migrate.mts"],"sourcesContent":["#!/usr/bin/env node\nimport dotenv from \"dotenv\";\ndotenv.config();\n\nimport { Kysely, PostgresDialect } from \"kysely\";\nimport { Pool } from \"pg\";\nimport {\n runMigrations,\n getMigrationStatus,\n REACTOR_SCHEMA,\n} from \"@powerhousedao/reactor\";\nimport {\n getReactorDriveMigrationStatus,\n runReactorDriveMigrations,\n} from \"@powerhousedao/reactor-drive\";\nimport { getConfig } from \"@powerhousedao/config/node\";\n\nfunction isPostgresUrl(url: string): boolean {\n return url.startsWith(\"postgresql://\") || url.startsWith(\"postgres://\");\n}\n\nasync function main() {\n const command = process.argv[2];\n const config = getConfig();\n\n const dbPath =\n process.env.PH_SWITCHBOARD_DATABASE_URL ??\n process.env.PH_REACTOR_DATABASE_URL ??\n process.env.DATABASE_URL ??\n config.switchboard?.database?.url;\n\n if (!dbPath || !isPostgresUrl(dbPath)) {\n console.log(\"No PostgreSQL URL configured. Skipping migrations.\");\n console.log(\"(PGlite migrations are handled automatically on startup)\");\n return;\n }\n\n console.log(`Database: ${dbPath}`);\n\n const pool = new Pool({ connectionString: dbPath });\n\n const db = new Kysely<any>({\n dialect: new PostgresDialect({ pool }),\n });\n\n try {\n if (command === \"status\") {\n console.log(\"\\nChecking migration status...\");\n const migrations = await getMigrationStatus(db, REACTOR_SCHEMA);\n\n console.log(\"\\nReactor Migration Status:\");\n console.log(\"=========================\");\n\n for (const migration of migrations) {\n const status = migration.executedAt\n ? `[OK] Executed at ${migration.executedAt.toISOString()}`\n : \"[--] Pending\";\n console.log(`${status} - ${migration.name}`);\n }\n\n const driveMigrations = await getReactorDriveMigrationStatus(\n db,\n REACTOR_SCHEMA,\n );\n\n console.log(\"\\nReactor-Drive Migration Status:\");\n console.log(\"===============================\");\n\n for (const migration of driveMigrations) {\n const status = migration.executedAt\n ? `[OK] Executed at ${migration.executedAt.toISOString()}`\n : \"[--] Pending\";\n console.log(`${status} - ${migration.name}`);\n }\n } else {\n console.log(\"\\nRunning reactor migrations...\");\n const result = await runMigrations(db, REACTOR_SCHEMA);\n\n if (!result.success) {\n console.error(\"Migration failed:\", result.error?.message);\n process.exit(1);\n }\n\n if (result.migrationsExecuted.length === 0) {\n console.log(\"No reactor migrations to run - database is up to date\");\n } else {\n console.log(\n `Successfully executed ${result.migrationsExecuted.length} reactor migration(s):`,\n );\n for (const name of result.migrationsExecuted) {\n console.log(` - ${name}`);\n }\n }\n\n console.log(\"\\nRunning reactor-drive migrations...\");\n const driveResult = await runReactorDriveMigrations(db, REACTOR_SCHEMA);\n\n if (!driveResult.success) {\n console.error(\n \"Reactor-drive migration failed:\",\n driveResult.error?.message,\n );\n process.exit(1);\n }\n\n if (driveResult.migrationsExecuted.length === 0) {\n console.log(\n \"No reactor-drive migrations to run - database is up to date\",\n );\n } else {\n console.log(\n `Successfully executed ${driveResult.migrationsExecuted.length} reactor-drive migration(s):`,\n );\n for (const name of driveResult.migrationsExecuted) {\n console.log(` - ${name}`);\n }\n }\n }\n } catch (error) {\n console.error(\n \"Error:\",\n error instanceof Error ? error.message : String(error),\n );\n process.exit(1);\n } finally {\n await db.destroy();\n }\n}\n\nvoid main();\n"],"names":[],"mappings":";;;;;;;;;;AAEA,OAAO,OAAO;AAed,SAAS,cAAc,KAAsB;CAC3C,OAAO,IAAI,WAAW,eAAe,KAAK,IAAI,WAAW,aAAa;AACxE;AAEA,eAAe,OAAO;CACpB,MAAM,UAAU,QAAQ,KAAK;CAC7B,MAAM,SAAS,UAAU;CAEzB,MAAM,SACJ,QAAQ,IAAI,+BACZ,QAAQ,IAAI,2BACZ,QAAQ,IAAI,gBACZ,OAAO,aAAa,UAAU;CAEhC,IAAI,CAAC,UAAU,CAAC,cAAc,MAAM,GAAG;EACrC,QAAQ,IAAI,oDAAoD;EAChE,QAAQ,IAAI,0DAA0D;EACtE;CACF;CAEA,QAAQ,IAAI,aAAa,QAAQ;CAEjC,MAAM,OAAO,IAAI,KAAK,EAAE,kBAAkB,OAAO,CAAC;CAElD,MAAM,KAAK,IAAI,OAAY,EACzB,SAAS,IAAI,gBAAgB,EAAE,KAAK,CAAC,EACvC,CAAC;CAED,IAAI;EACF,IAAI,YAAY,UAAU;GACxB,QAAQ,IAAI,gCAAgC;GAC5C,MAAM,aAAa,MAAM,mBAAmB,IAAI,cAAc;GAE9D,QAAQ,IAAI,6BAA6B;GACzC,QAAQ,IAAI,2BAA2B;GAEvC,KAAK,MAAM,aAAa,YAAY;IAClC,MAAM,SAAS,UAAU,aACrB,oBAAoB,UAAU,WAAW,YAAY,MACrD;IACJ,QAAQ,IAAI,GAAG,OAAO,KAAK,UAAU,MAAM;GAC7C;GAEA,MAAM,kBAAkB,MAAM,+BAC5B,IACA,cACF;GAEA,QAAQ,IAAI,mCAAmC;GAC/C,QAAQ,IAAI,iCAAiC;GAE7C,KAAK,MAAM,aAAa,iBAAiB;IACvC,MAAM,SAAS,UAAU,aACrB,oBAAoB,UAAU,WAAW,YAAY,MACrD;IACJ,QAAQ,IAAI,GAAG,OAAO,KAAK,UAAU,MAAM;GAC7C;EACF,OAAO;GACL,QAAQ,IAAI,iCAAiC;GAC7C,MAAM,SAAS,MAAM,cAAc,IAAI,cAAc;GAErD,IAAI,CAAC,OAAO,SAAS;IACnB,QAAQ,MAAM,qBAAqB,OAAO,OAAO,OAAO;IACxD,QAAQ,KAAK,CAAC;GAChB;GAEA,IAAI,OAAO,mBAAmB,WAAW,GACvC,QAAQ,IAAI,uDAAuD;QAC9D;IACL,QAAQ,IACN,yBAAyB,OAAO,mBAAmB,OAAO,uBAC5D;IACA,KAAK,MAAM,QAAQ,OAAO,oBACxB,QAAQ,IAAI,OAAO,MAAM;GAE7B;GAEA,QAAQ,IAAI,uCAAuC;GACnD,MAAM,cAAc,MAAM,0BAA0B,IAAI,cAAc;GAEtE,IAAI,CAAC,YAAY,SAAS;IACxB,QAAQ,MACN,mCACA,YAAY,OAAO,OACrB;IACA,QAAQ,KAAK,CAAC;GAChB;GAEA,IAAI,YAAY,mBAAmB,WAAW,GAC5C,QAAQ,IACN,6DACF;QACK;IACL,QAAQ,IACN,yBAAyB,YAAY,mBAAmB,OAAO,6BACjE;IACA,KAAK,MAAM,QAAQ,YAAY,oBAC7B,QAAQ,IAAI,OAAO,MAAM;GAE7B;EACF;CACF,SAAS,OAAO;EACd,QAAQ,MACN,UACA,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CACvD;EACA,QAAQ,KAAK,CAAC;CAChB,UAAU;EACR,MAAM,GAAG,QAAQ;CACnB;AACF;AAEK,KAAK","debug_id":"6af9cf0a-695b-5636-840b-192b4027e5cb"}