@powerhousedao/switchboard 6.1.0-dev.2 → 6.1.0-dev.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.tsbuild/test/attachments/auth.test.d.ts +2 -0
- package/.tsbuild/test/attachments/auth.test.d.ts.map +1 -0
- package/.tsbuild/test/attachments/index.test.d.ts +2 -0
- package/.tsbuild/test/attachments/index.test.d.ts.map +1 -0
- package/.tsbuild/test/attachments/routes-integration.test.d.ts +2 -0
- package/.tsbuild/test/attachments/routes-integration.test.d.ts.map +1 -0
- package/.tsbuild/test/attachments/routes.test.d.ts +2 -0
- package/.tsbuild/test/attachments/routes.test.d.ts.map +1 -0
- package/.tsbuild/test/attachments/service-config.test.d.ts +2 -0
- package/.tsbuild/test/attachments/service-config.test.d.ts.map +1 -0
- package/.tsbuild/test/metrics.test.d.ts +2 -0
- package/.tsbuild/test/metrics.test.d.ts.map +1 -0
- package/.tsbuild/test/pglite-dialect.test.d.ts +2 -0
- package/.tsbuild/test/pglite-dialect.test.d.ts.map +1 -0
- package/.tsbuild/test/pglite-version.test.d.ts +2 -0
- package/.tsbuild/test/pglite-version.test.d.ts.map +1 -0
- package/.tsbuild/tsconfig.tsbuildinfo +1 -0
- package/.tsbuild/tsdown.config.d.ts +3 -0
- package/.tsbuild/tsdown.config.d.ts.map +1 -0
- package/.tsbuild/vitest.config.d.ts +3 -0
- package/.tsbuild/vitest.config.d.ts.map +1 -0
- package/CHANGELOG.md +162 -0
- package/dist/index.mjs +7 -7
- package/dist/index.mjs.map +1 -1
- package/dist/{server-bMFA4VKj.mjs → server-DCeVXVeJ.mjs} +96 -13
- package/dist/server-DCeVXVeJ.mjs.map +1 -0
- package/dist/server.d.mts +15 -6
- package/dist/server.d.mts.map +1 -1
- package/dist/server.mjs +5 -5
- package/dist/{utils-BVNg1DRI.mjs → utils-Baw7rThP.mjs} +3 -4
- package/dist/utils-Baw7rThP.mjs.map +1 -0
- package/dist/utils.d.mts.map +1 -1
- package/dist/utils.mjs +3 -3
- package/package.json +25 -25
- package/test/attachments/auth.test.ts +36 -25
- package/test/attachments/index.test.ts +14 -10
- package/test/attachments/routes.test.ts +421 -0
- package/test/attachments/service-config.test.ts +170 -0
- package/dist/server-bMFA4VKj.mjs.map +0 -1
- package/dist/utils-BVNg1DRI.mjs.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tsdown.config.d.ts","sourceRoot":"","sources":["../tsdown.config.ts"],"names":[],"mappings":";AAEA,wBAaG"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vitest.config.d.ts","sourceRoot":"","sources":["../vitest.config.ts"],"names":[],"mappings":";AAEA,wBAQG"}
|
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,165 @@
|
|
|
1
|
+
## 6.1.0-dev.21 (2026-06-05)
|
|
2
|
+
|
|
3
|
+
This was a version bump only for @powerhousedao/switchboard to align it with other projects, there were no code changes.
|
|
4
|
+
|
|
5
|
+
## 6.1.0-dev.20 (2026-06-05)
|
|
6
|
+
|
|
7
|
+
This was a version bump only for @powerhousedao/switchboard to align it with other projects, there were no code changes.
|
|
8
|
+
|
|
9
|
+
## 6.1.0-dev.19 (2026-06-04)
|
|
10
|
+
|
|
11
|
+
This was a version bump only for @powerhousedao/switchboard to align it with other projects, there were no code changes.
|
|
12
|
+
|
|
13
|
+
## 6.1.0-dev.18 (2026-06-04)
|
|
14
|
+
|
|
15
|
+
### 🚀 Features
|
|
16
|
+
|
|
17
|
+
- **reactor-attachment:** added attachment client and refactored types so they do not pollute shared ([415dd9875](https://github.com/powerhouse-inc/powerhouse/commit/415dd9875))
|
|
18
|
+
- **reactor-attachments:** hash-first refs — client-supplied hash at reserve, pending status, and submit-before-upload support ([6a45c5de5](https://github.com/powerhouse-inc/powerhouse/commit/6a45c5de5))
|
|
19
|
+
|
|
20
|
+
### 🩹 Fixes
|
|
21
|
+
|
|
22
|
+
- merge fix ([2ac75f90c](https://github.com/powerhouse-inc/powerhouse/commit/2ac75f90c))
|
|
23
|
+
|
|
24
|
+
### ❤️ Thank You
|
|
25
|
+
|
|
26
|
+
- Benjamin Jordan
|
|
27
|
+
|
|
28
|
+
## 6.1.0-dev.17 (2026-06-03)
|
|
29
|
+
|
|
30
|
+
### 🩹 Fixes
|
|
31
|
+
|
|
32
|
+
- **connect:** preserve deep link until remote drives load ([e1ac2ef02](https://github.com/powerhouse-inc/powerhouse/commit/e1ac2ef02))
|
|
33
|
+
|
|
34
|
+
### ❤️ Thank You
|
|
35
|
+
|
|
36
|
+
- acaldas
|
|
37
|
+
|
|
38
|
+
## 6.1.0-dev.16 (2026-06-03)
|
|
39
|
+
|
|
40
|
+
### 🚀 Features
|
|
41
|
+
|
|
42
|
+
- **connect:** migrate runtime configuration to powerhouse.config.json ([#2624](https://github.com/powerhouse-inc/powerhouse/pull/2624))
|
|
43
|
+
|
|
44
|
+
### ❤️ Thank You
|
|
45
|
+
|
|
46
|
+
- Guillermo Puente Sandoval @gpuente
|
|
47
|
+
|
|
48
|
+
## 6.1.0-dev.15 (2026-06-02)
|
|
49
|
+
|
|
50
|
+
### 🚀 Features
|
|
51
|
+
|
|
52
|
+
- **renown:** add EIP-712 credential signing and signature verification ([069417a20](https://github.com/powerhouse-inc/powerhouse/commit/069417a20))
|
|
53
|
+
|
|
54
|
+
### ❤️ Thank You
|
|
55
|
+
|
|
56
|
+
- acaldas
|
|
57
|
+
|
|
58
|
+
## 6.1.0-dev.14 (2026-06-02)
|
|
59
|
+
|
|
60
|
+
### 🚀 Features
|
|
61
|
+
|
|
62
|
+
- add dark mode classes ([#2629](https://github.com/powerhouse-inc/powerhouse/pull/2629))
|
|
63
|
+
|
|
64
|
+
### ❤️ Thank You
|
|
65
|
+
|
|
66
|
+
- CallmeT-ty @CallmeT-ty
|
|
67
|
+
- Claude Sonnet 4.6
|
|
68
|
+
- Ryan Wolhuter @ryanwolhuter
|
|
69
|
+
|
|
70
|
+
## 6.1.0-dev.13 (2026-06-01)
|
|
71
|
+
|
|
72
|
+
This was a version bump only for @powerhousedao/switchboard to align it with other projects, there were no code changes.
|
|
73
|
+
|
|
74
|
+
## 6.1.0-dev.12 (2026-06-01)
|
|
75
|
+
|
|
76
|
+
This was a version bump only for @powerhousedao/switchboard to align it with other projects, there were no code changes.
|
|
77
|
+
|
|
78
|
+
## 6.1.0-dev.11 (2026-05-31)
|
|
79
|
+
|
|
80
|
+
This was a version bump only for @powerhousedao/switchboard to align it with other projects, there were no code changes.
|
|
81
|
+
|
|
82
|
+
## 6.1.0-dev.10 (2026-05-30)
|
|
83
|
+
|
|
84
|
+
### 🩹 Fixes
|
|
85
|
+
|
|
86
|
+
- fix issue from refactor ([d6c728c95](https://github.com/powerhouse-inc/powerhouse/commit/d6c728c95))
|
|
87
|
+
- **ci:** bump playwright to 1.60.0 to fix install hang on node 24.16+ ([#2669](https://github.com/powerhouse-inc/powerhouse/pull/2669))
|
|
88
|
+
- added a failing test for the set_name issue, which exposed the document-drive tests weren't running at all ([8221e66b9](https://github.com/powerhouse-inc/powerhouse/commit/8221e66b9))
|
|
89
|
+
|
|
90
|
+
### ❤️ Thank You
|
|
91
|
+
|
|
92
|
+
- Benjamin Jordan
|
|
93
|
+
- Guillermo Puente Sandoval @gpuente
|
|
94
|
+
|
|
95
|
+
## 6.1.0-dev.9 (2026-05-29)
|
|
96
|
+
|
|
97
|
+
This was a version bump only for @powerhousedao/switchboard to align it with other projects, there were no code changes.
|
|
98
|
+
|
|
99
|
+
## 6.1.0-dev.8 (2026-05-28)
|
|
100
|
+
|
|
101
|
+
This was a version bump only for @powerhousedao/switchboard to align it with other projects, there were no code changes.
|
|
102
|
+
|
|
103
|
+
## 6.1.0-dev.7 (2026-05-28)
|
|
104
|
+
|
|
105
|
+
### 🩹 Fixes
|
|
106
|
+
|
|
107
|
+
- **switchboard,reactor-bench,profiling:** migrate Sentry 9->10 and OpenTelemetry v1->v2 ([5e5c1ef9e](https://github.com/powerhouse-inc/powerhouse/commit/5e5c1ef9e))
|
|
108
|
+
|
|
109
|
+
### ❤️ Thank You
|
|
110
|
+
|
|
111
|
+
- acaldas
|
|
112
|
+
|
|
113
|
+
## 6.1.0-dev.6 (2026-05-28)
|
|
114
|
+
|
|
115
|
+
### 🚀 Features
|
|
116
|
+
|
|
117
|
+
- adding attachment service to the switchboard options ([a88e4cdc6](https://github.com/powerhouse-inc/powerhouse/commit/a88e4cdc6))
|
|
118
|
+
- mount OpenPanel with consent gating and user identification ([aa7c77b07](https://github.com/powerhouse-inc/powerhouse/commit/aa7c77b07))
|
|
119
|
+
- feat(openpanel): add event mappings, validation, and naming helpers ([556688064](https://github.com/powerhouse-inc/powerhouse/commit/556688064))
|
|
120
|
+
- **connect:** add OpenPanel Analytics configuration ([b39d072b2](https://github.com/powerhouse-inc/powerhouse/commit/b39d072b2))
|
|
121
|
+
|
|
122
|
+
### 🩹 Fixes
|
|
123
|
+
|
|
124
|
+
- **connect:** resolve @powerhousedao/connect/* aliases in vitest ([f7de787c6](https://github.com/powerhouse-inc/powerhouse/commit/f7de787c6))
|
|
125
|
+
- pnpm-lock file ([d5019827c](https://github.com/powerhouse-inc/powerhouse/commit/d5019827c))
|
|
126
|
+
|
|
127
|
+
### ❤️ Thank You
|
|
128
|
+
|
|
129
|
+
- Benjamin Jordan
|
|
130
|
+
- Claude Opus 4.7
|
|
131
|
+
|
|
132
|
+
## 6.1.0-dev.5 (2026-05-27)
|
|
133
|
+
|
|
134
|
+
### 🚀 Features
|
|
135
|
+
|
|
136
|
+
- **reactor:** benchmark for workers ([8ff51da2f](https://github.com/powerhouse-inc/powerhouse/commit/8ff51da2f))
|
|
137
|
+
- **reactor:** multi-worker integration test -- eep ([b8b9566cf](https://github.com/powerhouse-inc/powerhouse/commit/b8b9566cf))
|
|
138
|
+
- **reactor:** added workerhandle, parent-side ipc wrapper ([b1955e3a6](https://github.com/powerhouse-inc/powerhouse/commit/b1955e3a6))
|
|
139
|
+
- **reactor:** worker protocol ([c1bb0bd30](https://github.com/powerhouse-inc/powerhouse/commit/c1bb0bd30))
|
|
140
|
+
|
|
141
|
+
### 🩹 Fixes
|
|
142
|
+
|
|
143
|
+
- ci needs to run integration tests serially, not in parallel ([d97b73622](https://github.com/powerhouse-inc/powerhouse/commit/d97b73622))
|
|
144
|
+
- do not double run hub-spoke and make sure postgres is running in integration tests ([6a26a7377](https://github.com/powerhouse-inc/powerhouse/commit/6a26a7377))
|
|
145
|
+
- swap never bundle for reactor-api so that subgraphs are properly detected ([7ee0fdb5b](https://github.com/powerhouse-inc/powerhouse/commit/7ee0fdb5b))
|
|
146
|
+
- **reactor:** fix linting issues ([7e0a4af8d](https://github.com/powerhouse-inc/powerhouse/commit/7e0a4af8d))
|
|
147
|
+
- add hub-spoke test in test:integration ([5ab8c0b36](https://github.com/powerhouse-inc/powerhouse/commit/5ab8c0b36))
|
|
148
|
+
- **reactor:** a number of linter and import errors needed fixed due to node imports ([060babc30](https://github.com/powerhouse-inc/powerhouse/commit/060babc30))
|
|
149
|
+
- **reactor:** grr path resolution fixes, tailwind fixes, linter fixes ([0eb6ad89f](https://github.com/powerhouse-inc/powerhouse/commit/0eb6ad89f))
|
|
150
|
+
|
|
151
|
+
### ❤️ Thank You
|
|
152
|
+
|
|
153
|
+
- Benjamin Jordan
|
|
154
|
+
|
|
155
|
+
## 6.1.0-dev.4 (2026-05-26)
|
|
156
|
+
|
|
157
|
+
This was a version bump only for @powerhousedao/switchboard to align it with other projects, there were no code changes.
|
|
158
|
+
|
|
159
|
+
## 6.1.0-dev.3 (2026-05-25)
|
|
160
|
+
|
|
161
|
+
This was a version bump only for @powerhousedao/switchboard to align it with other projects, there were no code changes.
|
|
162
|
+
|
|
1
163
|
## 6.1.0-dev.2 (2026-05-24)
|
|
2
164
|
|
|
3
165
|
This was a version bump only for @powerhousedao/switchboard to align it with other projects, there were no code changes.
|
package/dist/index.mjs
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
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]="
|
|
4
|
-
import {
|
|
5
|
-
import "./utils-
|
|
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]="93967b4b-2ee5-5ba6-9c59-5152ee59080f")}catch(e){}}();
|
|
4
|
+
import { a as parseForcePgVersion, r as startSwitchboard } from "./server-DCeVXVeJ.mjs";
|
|
5
|
+
import "./utils-Baw7rThP.mjs";
|
|
6
6
|
import { metrics } from "@opentelemetry/api";
|
|
7
7
|
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
|
|
8
8
|
import { ExpressInstrumentation } from "@opentelemetry/instrumentation-express";
|
|
9
9
|
import { GraphQLInstrumentation } from "@opentelemetry/instrumentation-graphql";
|
|
10
10
|
import { HttpInstrumentation } from "@opentelemetry/instrumentation-http";
|
|
11
11
|
import { PgInstrumentation } from "@opentelemetry/instrumentation-pg";
|
|
12
|
-
import {
|
|
12
|
+
import { resourceFromAttributes } from "@opentelemetry/resources";
|
|
13
13
|
import { NodeSDK } from "@opentelemetry/sdk-node";
|
|
14
14
|
import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
|
|
15
15
|
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from "@opentelemetry/semantic-conventions";
|
|
@@ -31,7 +31,7 @@ function createMeterProviderFromEnv(env) {
|
|
|
31
31
|
const exporterUrl = base.endsWith("/v1/metrics") ? base : `${base}/v1/metrics`;
|
|
32
32
|
logger$2.info(`Initializing OpenTelemetry metrics exporter at: ${endpoint}`);
|
|
33
33
|
const meterProvider = new MeterProvider({
|
|
34
|
-
resource:
|
|
34
|
+
resource: resourceFromAttributes({ "service.name": env.OTEL_SERVICE_NAME ?? "switchboard" }),
|
|
35
35
|
readers: [new PeriodicExportingMetricReader({
|
|
36
36
|
exporter: new OTLPMetricExporter({ url: exporterUrl }),
|
|
37
37
|
exportIntervalMillis,
|
|
@@ -77,7 +77,7 @@ if (TRACING_ENABLED) {
|
|
|
77
77
|
if (TEMPO_ENDPOINT) logger$1.info(` Tempo endpoint: ${TEMPO_ENDPOINT}`);
|
|
78
78
|
if (SENTRY_DSN) logger$1.info(` Sentry span forwarding: enabled`);
|
|
79
79
|
logger$1.info(` Tenant: ${TENANT_ID}`);
|
|
80
|
-
const resource =
|
|
80
|
+
const resource = resourceFromAttributes({
|
|
81
81
|
[ATTR_SERVICE_NAME]: SERVICE_NAME,
|
|
82
82
|
[ATTR_SERVICE_VERSION]: SERVICE_VERSION,
|
|
83
83
|
"tenant.id": TENANT_ID,
|
|
@@ -221,4 +221,4 @@ startSwitchboard({
|
|
|
221
221
|
export {};
|
|
222
222
|
|
|
223
223
|
//# sourceMappingURL=index.mjs.map
|
|
224
|
-
//# debugId=
|
|
224
|
+
//# debugId=93967b4b-2ee5-5ba6-9c59-5152ee59080f
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","sources":["../src/metrics.ts","../src/observability.mts","../src/config.ts","../src/profiler.ts","../src/index.mts"],"sourcesContent":["import { OTLPMetricExporter } from \"@opentelemetry/exporter-metrics-otlp-http\";\nimport { Resource } 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: new Resource({\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","// 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 { Resource } 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 * 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\";\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\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// Default 10% APM sampling — Sentry's own production guidance; overridable\n// per-deploy. Only kicks in once tracesSampleRate * (sampler decision) lands.\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 tracesSampleRate: SENTRY_TRACES_SAMPLE_RATE,\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});\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}\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 = new Resource({\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_DSN) {\n // Fan the same OTel spans into Sentry — same trace IDs as Tempo, so\n // Sentry transactions cross-link to traces in Grafana.\n spanProcessors.push(new SentrySpanProcessor());\n }\n\n sdk = new NodeSDK({\n resource,\n spanProcessors,\n textMapPropagator: SENTRY_DSN ? new SentryPropagator() : 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 (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 new GraphQLInstrumentation({ mergeItems: true, allowValues: true }),\n new PgInstrumentation({ enhancedDatabaseReporting: true }),\n ],\n });\n sdk.start();\n if (SENTRY_DSN && typeof Sentry.validateOpenTelemetrySetup === \"function\") {\n Sentry.validateOpenTelemetrySetup();\n }\n logger.info(\"OpenTelemetry tracing initialized\");\n}\n\nasync function shutdown() {\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\nstartSwitchboard({\n ...config,\n migratePglite: cliMigratePglite || config.migratePglite,\n forcePgVersion: config.forcePgVersion ?? undefined,\n}).catch(console.error);\n"],"names":["logger","logger"],"mappings":"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","debug_id":"5f3b2177-d6bb-533b-b87e-e4eb03b10c87"}
|
|
1
|
+
{"version":3,"file":"index.mjs","sources":["../src/metrics.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","// 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 { 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 * 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\";\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\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// Default 10% APM sampling — Sentry's own production guidance; overridable\n// per-deploy. Only kicks in once tracesSampleRate * (sampler decision) lands.\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 tracesSampleRate: SENTRY_TRACES_SAMPLE_RATE,\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});\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}\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_DSN) {\n // Fan the same OTel spans into Sentry — same trace IDs as Tempo, so\n // Sentry transactions cross-link to traces in Grafana.\n spanProcessors.push(new SentrySpanProcessor());\n }\n\n sdk = new NodeSDK({\n resource,\n spanProcessors,\n textMapPropagator: SENTRY_DSN ? new SentryPropagator() : 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 (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 new GraphQLInstrumentation({ mergeItems: true, allowValues: true }),\n new PgInstrumentation({ enhancedDatabaseReporting: true }),\n ],\n });\n sdk.start();\n if (SENTRY_DSN && typeof Sentry.validateOpenTelemetrySetup === \"function\") {\n Sentry.validateOpenTelemetrySetup();\n }\n logger.info(\"OpenTelemetry tracing initialized\");\n}\n\nasync function shutdown() {\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\nstartSwitchboard({\n ...config,\n migratePglite: cliMigratePglite || config.migratePglite,\n forcePgVersion: config.forcePgVersion ?? undefined,\n}).catch(console.error);\n"],"names":["logger","logger"],"mappings":"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","debug_id":"93967b4b-2ee5-5ba6-9c59-5152ee59080f"}
|
|
@@ -1,6 +1,6 @@
|
|
|
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]="
|
|
3
|
-
import { n as addDefaultReactorDrive, r as isPostgresUrl, t as addDefaultDrive } from "./utils-
|
|
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]="a09b92a2-0cf7-5ee1-8c41-8fe19a32690f")}catch(e){}}();
|
|
3
|
+
import { n as addDefaultReactorDrive, r as isPostgresUrl, t as addDefaultDrive } from "./utils-Baw7rThP.mjs";
|
|
4
4
|
import { register } from "node:module";
|
|
5
5
|
import * as Sentry from "@sentry/node";
|
|
6
6
|
import { childLogger, documentModelDocumentModelModule, setLogLevel } from "document-model";
|
|
@@ -11,6 +11,7 @@ import path from "node:path";
|
|
|
11
11
|
import { ReactorInstrumentation } from "@powerhousedao/opentelemetry-instrumentation-reactor";
|
|
12
12
|
import { AtomicNodeFs } from "@powerhousedao/pglite-fs";
|
|
13
13
|
import { ChannelScheme, EventBus, REACTOR_SCHEMA, ReactorBuilder, ReactorClientBuilder, driveCollectionId, parseDriveUrl } from "@powerhousedao/reactor";
|
|
14
|
+
import { AttachmentAlreadyExists, AttachmentNotFound, AttachmentPending, HashMismatch, InvalidAttachmentRef, ReservationNotFound, SizeMismatch, UploadTooLarge, createRemoteAttachmentService } from "@powerhousedao/reactor-attachments";
|
|
14
15
|
import { HttpPackageLoader, ImportPackageLoader, PackageManagementService, PackagesSubgraph, getUniqueDocumentModels, initializeAndStartAPI } from "@powerhousedao/reactor-api";
|
|
15
16
|
import { httpsHooksPath } from "@powerhousedao/reactor-api/https-hooks";
|
|
16
17
|
import { VitePackageLoader, createViteLogger, startViteServer } from "@powerhousedao/reactor-api/vite";
|
|
@@ -22,7 +23,6 @@ import { Kysely, PostgresDialect } from "kysely";
|
|
|
22
23
|
import net from "node:net";
|
|
23
24
|
import path$1 from "path";
|
|
24
25
|
import { Pool } from "pg";
|
|
25
|
-
import { AttachmentNotFound, InvalidAttachmentRef, ReservationNotFound } from "@powerhousedao/reactor-attachments";
|
|
26
26
|
import { Readable } from "node:stream";
|
|
27
27
|
import { EnvVarProvider } from "@openfeature/env-var-provider";
|
|
28
28
|
import { OpenFeature } from "@openfeature/server-sdk";
|
|
@@ -139,6 +139,7 @@ function mountAuthenticatedNodeRoute(api, method, path, handler) {
|
|
|
139
139
|
//#endregion
|
|
140
140
|
//#region src/attachments/routes.ts
|
|
141
141
|
const logger$1 = childLogger(["switchboard", "attachments"]);
|
|
142
|
+
const RETRY_AFTER_SECONDS = 5;
|
|
142
143
|
const HASH_PATTERN = /^[a-f0-9]{64}$/i;
|
|
143
144
|
const CONTROL_CHARS = /[\x00-\x1f\x7f]/;
|
|
144
145
|
const MIME_TYPE_PATTERN = /^[!#$%&'*+\-.^_`|~\w]+\/[!#$%&'*+\-.^_`|~\w]+(?:\s*;\s*[!#$%&'*+\-.^_`|~\w]+=(?:[!#$%&'*+\-.^_`|~\w]+|"(?:[^"\\\r\n]|\\[^\r\n])*"))*$/;
|
|
@@ -156,6 +157,7 @@ function statusForError(err) {
|
|
|
156
157
|
if (err instanceof AttachmentNotFound) return 404;
|
|
157
158
|
if (err instanceof ReservationNotFound) return 404;
|
|
158
159
|
if (err instanceof InvalidAttachmentRef) return 400;
|
|
160
|
+
if (err instanceof UploadTooLarge) return 413;
|
|
159
161
|
return 500;
|
|
160
162
|
}
|
|
161
163
|
function sendErrorFromException(res, err) {
|
|
@@ -186,6 +188,17 @@ function parseReserveOptions(input) {
|
|
|
186
188
|
if (obj.extension.length === 0 || /[\\/]/.test(obj.extension)) return null;
|
|
187
189
|
extension = obj.extension;
|
|
188
190
|
} else if (obj.extension !== void 0 && obj.extension !== null) return null;
|
|
191
|
+
if (obj.clientHash !== void 0) {
|
|
192
|
+
if (typeof obj.clientHash !== "string" || !HASH_PATTERN.test(obj.clientHash)) return null;
|
|
193
|
+
if (typeof obj.sizeBytes !== "number" || !Number.isInteger(obj.sizeBytes) || obj.sizeBytes <= 0 || !Number.isSafeInteger(obj.sizeBytes)) return null;
|
|
194
|
+
return {
|
|
195
|
+
mimeType: obj.mimeType,
|
|
196
|
+
fileName: obj.fileName,
|
|
197
|
+
extension,
|
|
198
|
+
clientHash: obj.clientHash.toLowerCase(),
|
|
199
|
+
sizeBytes: obj.sizeBytes
|
|
200
|
+
};
|
|
201
|
+
}
|
|
189
202
|
return {
|
|
190
203
|
mimeType: obj.mimeType,
|
|
191
204
|
fileName: obj.fileName,
|
|
@@ -211,14 +224,28 @@ function makeReserveHandler(attachments) {
|
|
|
211
224
|
}
|
|
212
225
|
const opts = parseReserveOptions(parsed);
|
|
213
226
|
if (!opts) {
|
|
214
|
-
sendError(res, 400, "Body must be { mimeType: string (type/subtype), fileName: string (no control characters, max 255 chars), extension?: string|null }");
|
|
227
|
+
sendError(res, 400, "Body must be { mimeType: string (type/subtype), fileName: string (no control characters, max 255 chars), extension?: string|null, clientHash?: string (64 hex chars), sizeBytes?: number (required with clientHash) }");
|
|
215
228
|
return;
|
|
216
229
|
}
|
|
230
|
+
let upload;
|
|
217
231
|
try {
|
|
218
|
-
|
|
232
|
+
upload = await attachments.service.reserve(opts);
|
|
219
233
|
} catch (err) {
|
|
234
|
+
if (err instanceof AttachmentAlreadyExists) {
|
|
235
|
+
sendJson(res, 409, {
|
|
236
|
+
error: "already_exists",
|
|
237
|
+
ref: err.ref
|
|
238
|
+
});
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
220
241
|
sendErrorFromException(res, err);
|
|
242
|
+
return;
|
|
221
243
|
}
|
|
244
|
+
sendJson(res, 201, {
|
|
245
|
+
reservationId: upload.reservationId,
|
|
246
|
+
ref: upload.ref,
|
|
247
|
+
expiresAtUtc: upload.expiresAtUtc
|
|
248
|
+
});
|
|
222
249
|
};
|
|
223
250
|
}
|
|
224
251
|
function makeUploadHandler(attachments) {
|
|
@@ -235,15 +262,27 @@ function makeUploadHandler(attachments) {
|
|
|
235
262
|
sendErrorFromException(res, err);
|
|
236
263
|
return;
|
|
237
264
|
}
|
|
238
|
-
const upload = attachments.uploadFactory.createUpload(reservation
|
|
239
|
-
mimeType: reservation.mimeType,
|
|
240
|
-
fileName: reservation.fileName,
|
|
241
|
-
extension: reservation.extension
|
|
242
|
-
});
|
|
265
|
+
const upload = attachments.uploadFactory.createUpload(reservation);
|
|
243
266
|
const webStream = Readable.toWeb(req);
|
|
244
267
|
try {
|
|
245
268
|
sendJson(res, 200, await upload.send(webStream));
|
|
246
269
|
} catch (err) {
|
|
270
|
+
if (err instanceof HashMismatch) {
|
|
271
|
+
sendJson(res, 422, {
|
|
272
|
+
error: "hash_mismatch",
|
|
273
|
+
claimed: err.claimed,
|
|
274
|
+
actual: err.actual
|
|
275
|
+
});
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
if (err instanceof SizeMismatch) {
|
|
279
|
+
sendJson(res, 422, {
|
|
280
|
+
error: "size_mismatch",
|
|
281
|
+
declared: err.declared,
|
|
282
|
+
actual: err.actual
|
|
283
|
+
});
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
247
286
|
sendErrorFromException(res, err);
|
|
248
287
|
}
|
|
249
288
|
};
|
|
@@ -262,6 +301,16 @@ function makeDownloadHandler(attachments) {
|
|
|
262
301
|
try {
|
|
263
302
|
response = await attachments.store.get(canonicalHash, controller.signal);
|
|
264
303
|
} catch (err) {
|
|
304
|
+
if (err instanceof AttachmentPending) {
|
|
305
|
+
res.statusCode = 202;
|
|
306
|
+
res.setHeader("Retry-After", String(RETRY_AFTER_SECONDS));
|
|
307
|
+
res.setHeader("Attachment-Pending", JSON.stringify({
|
|
308
|
+
expiresAtUtc: err.expiresAtUtc,
|
|
309
|
+
...err.metadata ?? {}
|
|
310
|
+
}));
|
|
311
|
+
res.end();
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
265
314
|
sendErrorFromException(res, err);
|
|
266
315
|
return;
|
|
267
316
|
}
|
|
@@ -299,6 +348,18 @@ function makeStatHandler(attachments) {
|
|
|
299
348
|
sendErrorFromException(res, err);
|
|
300
349
|
return;
|
|
301
350
|
}
|
|
351
|
+
if (header.status === "pending") {
|
|
352
|
+
res.statusCode = 202;
|
|
353
|
+
res.setHeader("Retry-After", String(RETRY_AFTER_SECONDS));
|
|
354
|
+
res.setHeader("Attachment-Pending", JSON.stringify({
|
|
355
|
+
expiresAtUtc: header.expiresAtUtc,
|
|
356
|
+
mimeType: header.mimeType,
|
|
357
|
+
fileName: header.fileName,
|
|
358
|
+
sizeBytes: header.sizeBytes
|
|
359
|
+
}));
|
|
360
|
+
res.end();
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
302
363
|
res.statusCode = 200;
|
|
303
364
|
res.setHeader("Content-Type", header.mimeType);
|
|
304
365
|
res.setHeader("Content-Length", String(header.sizeBytes));
|
|
@@ -574,6 +635,17 @@ async function createReactorKysely(opts) {
|
|
|
574
635
|
logger.info(inMemory ? `Using in-memory PGlite (PG${reactorPgliteMajor}) for reactor storage [PH_PGLITE_IN_MEMORY=1]` : `Using PGlite (PG${reactorPgliteMajor}) for reactor storage at ${reactorPgliteDir}`);
|
|
575
636
|
return new Kysely({ dialect: new ClosablePGliteDialect(pglite) });
|
|
576
637
|
}
|
|
638
|
+
/** Derive the remote attachment service config for switchboard's own `/attachments/*` API. */
|
|
639
|
+
function deriveAttachmentServiceConfig(options, serverPort, renown) {
|
|
640
|
+
const protocol = options.https ? "https" : "http";
|
|
641
|
+
return {
|
|
642
|
+
remoteUrl: options.attachmentServiceUrl ?? process.env.PH_SWITCHBOARD_PUBLIC_URL ?? `${protocol}://localhost:${serverPort}`,
|
|
643
|
+
jwtHandler: renown ? async (url) => renown.user ? renown.getBearerToken({
|
|
644
|
+
expiresIn: 10,
|
|
645
|
+
aud: url
|
|
646
|
+
}) : void 0 : void 0
|
|
647
|
+
};
|
|
648
|
+
}
|
|
577
649
|
async function initServer(serverPort, options, renown) {
|
|
578
650
|
const { dev, packages = [], remoteDrives = [], logger = defaultLogger } = options;
|
|
579
651
|
logger.level = LogLevel;
|
|
@@ -585,6 +657,15 @@ async function initServer(serverPort, options, renown) {
|
|
|
585
657
|
const readModelPgliteDir = !dbPath || !isPostgresUrl(dbPath) ? readModelPath : null;
|
|
586
658
|
const pgliteDirs = [reactorPgliteDir, readModelPgliteDir].filter((d) => d !== null);
|
|
587
659
|
const detectedMajors = /* @__PURE__ */ new Map();
|
|
660
|
+
for (const dir of pgliteDirs) {
|
|
661
|
+
const lockPath = path$1.join(dir, "postmaster.pid");
|
|
662
|
+
try {
|
|
663
|
+
await promises.unlink(lockPath);
|
|
664
|
+
logger.warn(`Removed stale PGLite lockfile ${lockPath}`);
|
|
665
|
+
} catch (err) {
|
|
666
|
+
if (err.code !== "ENOENT") throw err;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
588
669
|
if (options.forcePgVersion !== void 0 && pgliteDirs.length > 0) {
|
|
589
670
|
if (options.migratePglite) logger.warn("PH_FORCE_PG_VERSION is set; ignoring --migrate-pglite/PH_MIGRATE_PGLITE because the data dirs will be wiped.");
|
|
590
671
|
logger.warn(`PH_FORCE_PG_VERSION=${options.forcePgVersion} set; wiping PGLite data dirs and re-initializing at PG${options.forcePgVersion}.`);
|
|
@@ -726,6 +807,7 @@ async function initServer(serverPort, options, renown) {
|
|
|
726
807
|
}, "switchboard");
|
|
727
808
|
apiRef.current = api;
|
|
728
809
|
registerAttachmentRoutes(api);
|
|
810
|
+
const attachmentService = createRemoteAttachmentService(deriveAttachmentServiceConfig(options, serverPort, renown));
|
|
729
811
|
if (process.env.SENTRY_DSN) api.httpAdapter.setupSentryErrorHandler(Sentry);
|
|
730
812
|
const { client, graphqlManager, documentModelRegistry } = api;
|
|
731
813
|
if (httpLoader) {
|
|
@@ -795,6 +877,7 @@ async function initServer(serverPort, options, renown) {
|
|
|
795
877
|
defaultDriveUrl,
|
|
796
878
|
api,
|
|
797
879
|
reactor: client,
|
|
880
|
+
attachmentService,
|
|
798
881
|
renown,
|
|
799
882
|
port: serverPort,
|
|
800
883
|
shutdown: () => api.dispose()
|
|
@@ -850,7 +933,7 @@ const startSwitchboard = async (options = {}) => {
|
|
|
850
933
|
};
|
|
851
934
|
if (import.meta.main) await startSwitchboard();
|
|
852
935
|
//#endregion
|
|
853
|
-
export { parseForcePgVersion as i,
|
|
936
|
+
export { parseForcePgVersion as a, applySwitchboardReactorDefaults as i, isPortAvailable as n, startSwitchboard as r, deriveAttachmentServiceConfig as t };
|
|
854
937
|
|
|
855
|
-
//# sourceMappingURL=server-
|
|
856
|
-
//# debugId=
|
|
938
|
+
//# sourceMappingURL=server-DCeVXVeJ.mjs.map
|
|
939
|
+
//# debugId=a09b92a2-0cf7-5ee1-8c41-8fe19a32690f
|