@astroscope/node 1.0.1 → 1.2.0

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/README.md CHANGED
@@ -15,12 +15,12 @@ Opinionated, cloud-friendly Node adapter for Astro: boot lifecycle, health probe
15
15
  - **Native mounts** — http-native handlers (`oidc-provider`, ACME) mounted on the adapter's server
16
16
  - **Build tweaks** — SSR sourcemaps, SSR effect stripping
17
17
  - **Dev restart machinery** — changes to the boot file or entry seams restart the dev server behind a holding page
18
+ - **HTTPS for development** — `SERVER_CERT_PATH` / `SERVER_KEY_PATH` serve TLS directly for local runs of the built server ([HTTPS](#https)); in production, terminate TLS at the ingress
18
19
 
19
20
  ## What it does NOT do — beware
20
21
 
21
22
  The adapter assumes a container behind a load balancer / reverse proxy (Kubernetes, Docker + ingress). Outside that setup, several defaults are wrong for you:
22
23
 
23
- - **No TLS.** Plain HTTP only — terminate TLS at the ingress.
24
24
  - **Opens `0.0.0.0:9090` in production** — the health probe server. Meant for the kubelet; do not expose it publicly.
25
25
  - **Opens `0.0.0.0:9464` in production** — the Prometheus metrics reader. Same: cluster-internal only.
26
26
  - **Trusts any `Host` / `X-Forwarded-Host`** — sets `security.allowedDomains: [{}]` (unless you set it yourself), because the reverse proxy is expected to control these headers. Without one, host header injection is possible.
@@ -102,7 +102,7 @@ export function register(ctx: InstrumentationContext) {
102
102
  }
103
103
  ```
104
104
 
105
- **`src/log.ts`** — pino logger *options* (a static object or a factory), never a logger instance. The platform constructs the logger itself, after instrumentation, and adds a mixin that stamps `trace_id`/`span_id`/`trace_flags` onto every entry when a span is active. This file runs after env loading and `src/config.ts`, so the options may safely read config.
105
+ **`src/log.ts`** — pino logger _options_ (a static object or a factory), never a logger instance. The platform constructs the logger itself, after instrumentation, and adds a mixin that stamps `trace_id`/`span_id`/`trace_flags` onto every entry when a span is active. This file runs after env loading and `src/config.ts`, so the options may safely read config.
106
106
 
107
107
  ```typescript
108
108
  // src/log.ts
@@ -235,14 +235,14 @@ node({
235
235
  // boot lifecycle; false disables it. Skipped automatically when no boot file exists.
236
236
  boot: {
237
237
  entry: 'src/boot.ts', // default: src/boot.ts or src/boot/index.ts
238
- watch: true, // dev: restart the dev server on boot-dependency changes
238
+ watch: true, // dev: restart the dev server on boot-dependency changes
239
239
  },
240
240
 
241
241
  // Kubernetes-style probes on a separate port. Enabled by default in
242
242
  // production (never active in dev); false disables.
243
243
  health: {
244
244
  host: '0.0.0.0', // falls back to HEALTH_HOST env, then 0.0.0.0 (kubelet probes hit the pod IP)
245
- port: 9090, // falls back to HEALTH_PORT env, then 9090
245
+ port: 9090, // falls back to HEALTH_PORT env, then 9090
246
246
  },
247
247
 
248
248
  // CSRF protection (origin check for POST/PUT/PATCH/DELETE with exclusions).
@@ -255,33 +255,47 @@ node({
255
255
  // false disables it (the log proxy keeps working).
256
256
  logging: {
257
257
  exclude: [{ prefix: '/internal/' }], // replaces RECOMMENDED_EXCLUDES + STATIC_EXCLUDES
258
- extended: false, // query/headers/client address (may capture sensitive data)
259
- dev: false, // also log requests in dev (astro narrates them already)
258
+ extended: false, // query/headers/client address (may capture sensitive data)
259
+ dev: false, // also log requests in dev (astro narrates them already)
260
260
  },
261
261
 
262
262
  // platform telemetry. Enabled by default in production, off in dev; false disables.
263
263
  telemetry: {
264
264
  exclude: [{ prefix: '/internal/' }], // replaces RECOMMENDED_EXCLUDES + STATIC_EXCLUDES
265
265
  prometheus: { host: '0.0.0.0', port: 9464 }, // false disables the reader
266
- dev: false, // start the SDK in dev too (once per process)
266
+ dev: false, // start the SDK in dev too (once per process)
267
267
  },
268
268
 
269
269
  bodySizeLimit: 1024 * 1024 * 1024, // request body limit in bytes
270
- shutdownTimeout: 10_000, // ms to wait for in-flight requests on shutdown
270
+ shutdownTimeout: 10_000, // ms to wait for in-flight requests on shutdown
271
271
  });
272
272
  ```
273
273
 
274
+ ## HTTPS
275
+
276
+ The built server serves plain HTTP — in production, TLS is expected to terminate at the ingress. For local runs of the built server (e.g. auth flows that require a secure origin), set `SERVER_CERT_PATH` and `SERVER_KEY_PATH` to serve HTTPS directly — the same contract as `@astrojs/node`:
277
+
278
+ ```sh
279
+ SERVER_CERT_PATH=./cert/tls.crt
280
+ SERVER_KEY_PATH=./cert/tls.key
281
+ ```
282
+
283
+ Both must be set — startup fails if only one is present. Since env files load before the server starts listening, the variables may live in `.env` (or the file pointed to by `CONFIG_PATH`) instead of the shell environment.
284
+
285
+ This only affects the built server. The dev server is Vite's — configure `vite.server.https` in `astro.config` for HTTPS in dev.
286
+
274
287
  ## Environment variables
275
288
 
276
- | Variable | Effect |
277
- |----------|--------|
278
- | `HOST` / `PORT` | Override the listen address at runtime |
279
- | `HEALTH_HOST` / `HEALTH_PORT` | Override the health probe address (when not set in options) |
280
- | `CONFIG_PATH` | Env file to load at startup (falls back to `./.env`) |
281
- | `OTEL_EXPORTER_PROMETHEUS_HOST` / `OTEL_EXPORTER_PROMETHEUS_PORT` | Override the Prometheus reader address |
282
- | `OTEL_SDK_DISABLED=true` | Disable the telemetry SDK entirely |
283
- | standard `OTEL_*` | Exporter/resource configuration (e.g. `OTEL_EXPORTER_OTLP_ENDPOINT`, `OTEL_SERVICE_NAME`) |
284
- | `ASTROSCOPE_NODE_AUTOSTART=disabled` | Build the entry without starting the server (exports `startServer()`) |
289
+ | Variable | Effect |
290
+ | ----------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
291
+ | `HOST` / `PORT` | Override the listen address at runtime |
292
+ | `SERVER_CERT_PATH` / `SERVER_KEY_PATH` | Serve HTTPS with the given certificate/key (see [HTTPS](#https)) |
293
+ | `HEALTH_HOST` / `HEALTH_PORT` | Override the health probe address (when not set in options) |
294
+ | `CONFIG_PATH` | Env file to load at startup (falls back to `./.env`) |
295
+ | `OTEL_EXPORTER_PROMETHEUS_HOST` / `OTEL_EXPORTER_PROMETHEUS_PORT` | Override the Prometheus reader address |
296
+ | `OTEL_SDK_DISABLED=true` | Disable the telemetry SDK entirely |
297
+ | standard `OTEL_*` | Exporter/resource configuration (e.g. `OTEL_EXPORTER_OTLP_ENDPOINT`, `OTEL_SERVICE_NAME`) |
298
+ | `ASTROSCOPE_NODE_AUTOSTART=disabled` | Build the entry without starting the server (exports `startServer()`) |
285
299
 
286
300
  ## Shutdown sequence
287
301
 
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { n as setBootContext } from "./context-Bkg-FnnQ.js";
2
- import { a as runShutdown, i as createRequestInstrumentation, o as runStartup, t as preparePlatform } from "./prepare-Be2GgRyf.js";
2
+ import { a as runShutdown, i as createRequestInstrumentation, o as runStartup, t as preparePlatform } from "./prepare-DQEf2Bnt.js";
3
3
  import { n as dispatchNativeMount, t as clearNativeMounts } from "./native-mount-hhwWdLtL.js";
4
4
  import { n as getCurrentGeneration, r as incrementGeneration, t as GEN_HEADER } from "./generation-Bp2IA0jf.js";
5
5
  import { i as STATIC_EXCLUDES, r as RECOMMENDED_EXCLUDES } from "./excludes-pE23EbmQ.js";
@@ -2,7 +2,7 @@ import { t as emit } from "./events-CgoM3Fvu.js";
2
2
  import { t as getLogStore } from "./store-BIUF4lqk.js";
3
3
  import { n as log, t as generateReqId } from "./log-CSJlKaxY.js";
4
4
  import fs from "node:fs";
5
- import { SpanKind, SpanStatusCode, ValueType, context, isSpanContextValid, metrics, propagation, trace } from "@opentelemetry/api";
5
+ import { ROOT_CONTEXT, SpanKind, SpanStatusCode, ValueType, context, isSpanContextValid, metrics, propagation, trace } from "@opentelemetry/api";
6
6
  import pino from "pino";
7
7
  import { createMatcher } from "@entwico/dash/match";
8
8
  //#region src/lifecycle/lifecycle.ts
@@ -141,9 +141,10 @@ function createRequestInstrumentation(config) {
141
141
  actionName: isAction ? pathname.slice(10).replace(/\/$/, "") : void 0
142
142
  };
143
143
  let span;
144
+ let firstByteSpan;
144
145
  let endActiveRequest;
145
146
  if (telemetry) {
146
- const parentContext = propagation.extract(context.active(), req.headers);
147
+ const parentContext = propagation.extract(ROOT_CONTEXT, req.headers);
147
148
  const contentLength = req.headers["content-length"];
148
149
  const clientIp = getClientIp(req);
149
150
  const host = req.headers["host"];
@@ -160,19 +161,28 @@ function createRequestInstrumentation(config) {
160
161
  ...clientIp && { "client.address": clientIp }
161
162
  }
162
163
  }, parentContext);
164
+ firstByteSpan = tracer.startSpan("response:first-byte", void 0, trace.setSpan(parentContext, span));
163
165
  endActiveRequest = recordHttpRequestStart(method);
164
166
  }
165
167
  let responseSize = 0;
166
168
  let firstByteTime;
167
169
  const originalWrite = res.write.bind(res);
168
170
  const originalEnd = res.end.bind(res);
171
+ const markFirstByte = () => {
172
+ if (firstByteTime !== void 0) return;
173
+ firstByteTime = performance.now();
174
+ if (firstByteSpan) {
175
+ firstByteSpan.setAttribute("http.response.status_code", res.statusCode);
176
+ firstByteSpan.end();
177
+ }
178
+ };
169
179
  res.write = ((chunk, ...rest) => {
170
- firstByteTime ??= performance.now();
180
+ markFirstByte();
171
181
  responseSize += chunkSize(chunk);
172
182
  return originalWrite(chunk, ...rest);
173
183
  });
174
184
  res.end = ((chunk, ...rest) => {
175
- firstByteTime ??= performance.now();
185
+ markFirstByte();
176
186
  responseSize += chunkSize(chunk);
177
187
  return originalEnd(chunk, ...rest);
178
188
  });
@@ -182,17 +192,26 @@ function createRequestInstrumentation(config) {
182
192
  finalized = true;
183
193
  const status = res.statusCode;
184
194
  const responseTime = performance.now() - startTime;
195
+ const ttfb = roundTime((firstByteTime ?? performance.now()) - startTime);
185
196
  if (requestLogger) requestLogger[status >= 500 ? "error" : status >= 400 ? "warn" : "info"]({
186
197
  res: { statusCode: status },
187
198
  responseTime: roundTime(responseTime),
188
- ttfb: roundTime((firstByteTime ?? performance.now()) - startTime),
199
+ ttfb,
189
200
  responseSize,
190
201
  ...record.route && { route: record.route },
191
202
  ...aborted && { aborted: true }
192
203
  }, aborted ? "request aborted" : "request completed");
204
+ if (firstByteSpan && firstByteTime === void 0) {
205
+ firstByteSpan.setStatus({
206
+ code: SpanStatusCode.ERROR,
207
+ message: "request aborted"
208
+ });
209
+ firstByteSpan.end();
210
+ }
193
211
  if (span) {
194
212
  span.setAttribute("http.response.status_code", status);
195
213
  span.setAttribute("http.response.body.size", responseSize);
214
+ span.setAttribute("ttfb", ttfb);
196
215
  if (aborted || status >= 400) span.setStatus({
197
216
  code: SpanStatusCode.ERROR,
198
217
  message: aborted ? "request aborted" : `HTTP ${status}`
@@ -387,4 +406,4 @@ async function preparePlatform(options) {
387
406
  //#endregion
388
407
  export { runShutdown as a, createRequestInstrumentation as i, shutdownTelemetry as n, runStartup as o, dumpEarlyLogs as r, preparePlatform as t };
389
408
 
390
- //# sourceMappingURL=prepare-Be2GgRyf.js.map
409
+ //# sourceMappingURL=prepare-DQEf2Bnt.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prepare-DQEf2Bnt.js","names":["LIB_NAME"],"sources":["../src/lifecycle/lifecycle.ts","../src/observability/telemetry/metrics.ts","../src/observability/instrument.ts","../src/observability/log/construct.ts","../src/observability/telemetry/sdk.ts","../src/platform/env.ts","../src/platform/prepare.ts"],"sourcesContent":["import { emit } from './events.js';\nimport type { BootContext } from './types.js';\n\nexport interface BootModule {\n onStartup?: ((context: BootContext) => Promise<void> | void) | undefined;\n onShutdown?: ((context: BootContext) => Promise<void> | void) | undefined;\n}\n\nexport async function runStartup(boot: BootModule, context: BootContext): Promise<void> {\n await emit('beforeOnStartup', context);\n await boot.onStartup?.(context);\n await emit('afterOnStartup', context);\n}\n\nexport async function runShutdown(boot: BootModule, context: BootContext): Promise<void> {\n try {\n await emit('beforeOnShutdown', context);\n await boot.onShutdown?.(context);\n } finally {\n await emit('afterOnShutdown', context);\n }\n}\n","import { type Histogram, type UpDownCounter, ValueType, metrics } from '@opentelemetry/api';\n\nconst LIB_NAME = '@astroscope/node';\n\n// lazy initialization so instruments bind to the SDK meter provider\nlet httpRequestDuration: Histogram | null = null;\nlet httpActiveRequests: UpDownCounter | null = null;\nlet actionDuration: Histogram | null = null;\n\nfunction getHttpRequestDuration(): Histogram {\n return (httpRequestDuration ??= metrics.getMeter(LIB_NAME).createHistogram('http.server.request.duration', {\n description: 'Duration of HTTP server requests',\n unit: 's',\n valueType: ValueType.DOUBLE,\n }));\n}\n\nfunction getHttpActiveRequests(): UpDownCounter {\n return (httpActiveRequests ??= metrics.getMeter(LIB_NAME).createUpDownCounter('http.server.active_requests', {\n description: 'Number of active HTTP server requests',\n unit: '{request}',\n valueType: ValueType.INT,\n }));\n}\n\nfunction getActionDuration(): Histogram {\n return (actionDuration ??= metrics.getMeter(LIB_NAME).createHistogram('astro.action.duration', {\n description: 'Duration of Astro action executions',\n unit: 's',\n valueType: ValueType.DOUBLE,\n }));\n}\n\n/**\n * Record the start of an HTTP request. Returns a function to call when the\n * request ends. Route is unknown at the native-handler level, so active\n * requests carry only the method.\n */\nexport function recordHttpRequestStart(method: string): () => void {\n getHttpActiveRequests().add(1, { 'http.request.method': method });\n\n return () => {\n getHttpActiveRequests().add(-1, { 'http.request.method': method });\n };\n}\n\nexport function recordHttpRequestDuration(\n attributes: { method: string; route: string | undefined; status: number },\n durationMs: number,\n): void {\n getHttpRequestDuration().record(durationMs / 1000, {\n 'http.request.method': attributes.method,\n 'http.route': attributes.route ?? '',\n 'http.response.status_code': attributes.status,\n });\n}\n\nexport function recordActionDuration(attributes: { name: string; status: number }, durationMs: number): void {\n getActionDuration().record(durationMs / 1000, {\n 'astro.action.name': attributes.name,\n 'http.response.status_code': attributes.status,\n });\n}\n","import type { IncomingMessage, ServerResponse } from 'node:http';\nimport { createMatcher } from '@entwico/dash/match';\nimport { ROOT_CONTEXT, SpanKind, SpanStatusCode, context, propagation, trace } from '@opentelemetry/api';\nimport type { Logger } from 'pino';\nimport type { ExcludePattern } from '../excludes/excludes.js';\nimport { generateReqId } from './log/index.js';\nimport { type RequestRecord, getLogStore } from './log/store.js';\nimport { recordActionDuration, recordHttpRequestDuration, recordHttpRequestStart } from './telemetry/metrics.js';\n\nconst LIB_NAME = '@astroscope/node';\nconst ACTIONS_PREFIX = '/_actions/';\nconst REQUEST_ID_PATTERN = /^[\\w.-]{1,64}$/;\n\nconst roundTime = (n: number) => Math.round(n * 100) / 100;\n\nexport interface RequestLoggingConfig {\n exclude: ExcludePattern[];\n extended: boolean;\n}\n\nexport interface RequestTelemetryConfig {\n exclude: ExcludePattern[];\n}\n\nexport interface RequestInstrumentationConfig {\n logging: RequestLoggingConfig | false;\n telemetry: RequestTelemetryConfig | false;\n}\n\nfunction getClientIp(req: IncomingMessage): string | undefined {\n const forwarded = req.headers['x-forwarded-for'];\n const first = Array.isArray(forwarded) ? forwarded[0] : forwarded;\n\n return (\n first?.split(',')[0]?.trim() ??\n (req.headers['x-real-ip'] as string | undefined) ??\n (req.headers['cf-connecting-ip'] as string | undefined)\n );\n}\n\nfunction resolveReqId(req: IncomingMessage): string {\n const incoming = req.headers['x-request-id'];\n const value = Array.isArray(incoming) ? incoming[0] : incoming;\n\n return value && REQUEST_ID_PATTERN.test(value) ? value : generateReqId();\n}\n\nfunction chunkSize(chunk: unknown): number {\n if (chunk == null) return 0;\n if (ArrayBuffer.isView(chunk)) return chunk.byteLength;\n if (typeof chunk === 'string') return Buffer.byteLength(chunk);\n\n return 0;\n}\n\n/**\n * Wraps the native request/response with logging and telemetry: a request\n * logger in async context (real status, response size, aborted-vs-completed\n * on `finish`/`close`), a SERVER span with propagation extraction, and\n * request metrics. Both concerns honor their own exclude patterns; when both\n * are excluded the request passes through untouched.\n */\nexport function createRequestInstrumentation(config: RequestInstrumentationConfig) {\n const tracer = trace.getTracer(LIB_NAME);\n const store = getLogStore();\n const loggingExcluded = config.logging ? createMatcher(config.logging.exclude) : () => true;\n const telemetryExcluded = config.telemetry ? createMatcher(config.telemetry.exclude) : () => true;\n\n return (req: IncomingMessage, res: ServerResponse, inner: () => void): void => {\n const url = req.url ?? '';\n const queryIndex = url.indexOf('?');\n const pathname = queryIndex === -1 ? url : url.slice(0, queryIndex);\n const method = req.method ?? 'GET';\n\n const logging = config.logging && !loggingExcluded(pathname) ? config.logging : false;\n const telemetry = config.telemetry && !telemetryExcluded(pathname) ? config.telemetry : false;\n\n if (!logging && !telemetry) {\n inner();\n\n return;\n }\n\n const startTime = performance.now();\n const isAction = pathname.startsWith(ACTIONS_PREFIX);\n\n let requestLogger: Logger | undefined;\n\n if (logging && store.root) {\n const reqId = resolveReqId(req);\n const reqData: Record<string, unknown> = { method, url: pathname };\n\n // extended logging includes potentially sensitive data\n if (logging.extended) {\n reqData['query'] = queryIndex === -1 ? '' : url.slice(queryIndex + 1);\n reqData['headers'] = req.headers;\n reqData['remoteAddress'] = getClientIp(req) ?? req.socket.remoteAddress;\n }\n\n requestLogger = store.root.child({ reqId, req: reqData });\n\n res.setHeader('x-request-id', reqId);\n }\n\n const record: RequestRecord = {\n logger: requestLogger,\n url,\n route: undefined,\n actionName: isAction ? pathname.slice(ACTIONS_PREFIX.length).replace(/\\/$/, '') : undefined,\n };\n\n let span: ReturnType<typeof tracer.startSpan> | undefined;\n let firstByteSpan: ReturnType<typeof tracer.startSpan> | undefined;\n let endActiveRequest: (() => void) | undefined;\n\n if (telemetry) {\n const parentContext = propagation.extract(ROOT_CONTEXT, req.headers);\n const contentLength = req.headers['content-length'];\n const clientIp = getClientIp(req);\n const host = req.headers['host'];\n\n span = tracer.startSpan(\n isAction ? `ACTION ${record.actionName}` : method,\n {\n kind: SpanKind.SERVER,\n attributes: {\n 'http.request.method': method,\n 'url.path': pathname,\n 'url.query': queryIndex === -1 ? '' : url.slice(queryIndex + 1),\n 'url.scheme': 'http',\n 'user_agent.original': req.headers['user-agent'] ?? '',\n ...(host && { 'server.address': host }),\n ...(contentLength && { 'http.request.body.size': parseInt(contentLength) }),\n ...(clientIp && { 'client.address': clientIp }),\n },\n },\n parentContext,\n );\n\n firstByteSpan = tracer.startSpan('response:first-byte', undefined, trace.setSpan(parentContext, span));\n\n endActiveRequest = recordHttpRequestStart(method);\n }\n\n let responseSize = 0;\n let firstByteTime: number | undefined;\n\n const originalWrite = res.write.bind(res);\n const originalEnd = res.end.bind(res);\n\n const markFirstByte = (): void => {\n if (firstByteTime !== undefined) return;\n\n firstByteTime = performance.now();\n\n if (firstByteSpan) {\n firstByteSpan.setAttribute('http.response.status_code', res.statusCode);\n firstByteSpan.end();\n }\n };\n\n res.write = ((chunk: unknown, ...rest: unknown[]) => {\n markFirstByte();\n responseSize += chunkSize(chunk);\n\n return (originalWrite as (...args: unknown[]) => boolean)(chunk, ...rest);\n }) as typeof res.write;\n\n res.end = ((chunk: unknown, ...rest: unknown[]) => {\n markFirstByte();\n responseSize += chunkSize(chunk);\n\n return (originalEnd as (...args: unknown[]) => ServerResponse)(chunk, ...rest);\n }) as typeof res.end;\n\n let finalized = false;\n\n const finalize = (aborted: boolean): void => {\n if (finalized) return;\n\n finalized = true;\n\n const status = res.statusCode;\n const responseTime = performance.now() - startTime;\n const ttfb = roundTime((firstByteTime ?? performance.now()) - startTime);\n\n if (requestLogger) {\n const level = status >= 500 ? 'error' : status >= 400 ? 'warn' : 'info';\n\n requestLogger[level](\n {\n res: { statusCode: status },\n responseTime: roundTime(responseTime),\n ttfb,\n responseSize,\n ...(record.route && { route: record.route }),\n ...(aborted && { aborted: true }),\n },\n aborted ? 'request aborted' : 'request completed',\n );\n }\n\n if (firstByteSpan && firstByteTime === undefined) {\n firstByteSpan.setStatus({ code: SpanStatusCode.ERROR, message: 'request aborted' });\n firstByteSpan.end();\n }\n\n if (span) {\n span.setAttribute('http.response.status_code', status);\n span.setAttribute('http.response.body.size', responseSize);\n span.setAttribute('ttfb', ttfb);\n\n if (aborted || status >= 400) {\n span.setStatus({ code: SpanStatusCode.ERROR, message: aborted ? 'request aborted' : `HTTP ${status}` });\n } else {\n span.setStatus({ code: SpanStatusCode.OK });\n }\n\n span.end();\n }\n\n if (telemetry) {\n endActiveRequest?.();\n recordHttpRequestDuration({ method, route: record.route, status }, responseTime);\n\n if (record.actionName) {\n recordActionDuration({ name: record.actionName, status }, responseTime);\n }\n }\n };\n\n res.once('finish', () => finalize(false));\n res.once('close', () => finalize(!res.writableFinished));\n\n const run = (): void => store.requestStorage.run(record, inner);\n\n if (span) {\n context.with(trace.setSpan(context.active(), span), run);\n } else {\n run();\n }\n };\n}\n","import { isSpanContextValid, trace } from '@opentelemetry/api';\nimport pino, { type Bindings, type Logger, type LoggerOptions } from 'pino';\nimport { getLogStore } from './store.js';\n\n/**\n * Contract of the `src/log.ts` entry seam: pino logger options, or a factory\n * producing them. Never a logger instance — the platform constructs the\n * logger itself (after instrumentation, so trace correlation works).\n */\nexport type LoggerOptionsFactory = LoggerOptions | ((ctx: { dev: boolean }) => LoggerOptions | Promise<LoggerOptions>);\n\n/**\n * Compose the user mixin (if any) with platform trace correlation: when a\n * span is active, every entry carries `trace_id` / `span_id` / `trace_flags`.\n */\nfunction composeMixin(userMixin: LoggerOptions['mixin']): NonNullable<LoggerOptions['mixin']> {\n return (mergeObject, level, logger) => {\n const user = userMixin ? userMixin(mergeObject, level, logger) : {};\n const spanContext = trace.getActiveSpan()?.spanContext();\n\n if (!spanContext || !isSpanContextValid(spanContext)) return user;\n\n return {\n ...user,\n trace_id: spanContext.traceId,\n span_id: spanContext.spanId,\n trace_flags: `0${spanContext.traceFlags.toString(16)}`,\n };\n };\n}\n\n/**\n * Construct the root logger from the app's options seam and replay any logs\n * buffered before construction (original timestamps kept as `bufferedTime`).\n * In dev this runs once per generation; the buffer only exists the first time.\n */\nexport async function constructRootLogger(\n factory: LoggerOptionsFactory | undefined,\n ctx: { dev: boolean },\n): Promise<Logger> {\n const store = getLogStore();\n const options = (typeof factory === 'function' ? await factory(ctx) : factory) ?? {};\n const root = pino({ level: 'info', ...options, mixin: composeMixin(options.mixin) });\n\n store.root = root;\n\n for (const entry of store.buffer.splice(0)) {\n const bindings = entry.bindings.length ? (Object.assign({}, ...entry.bindings) as Bindings) : {};\n const target = root.child({ ...bindings, bufferedTime: new Date(entry.time).toISOString() });\n\n (target[entry.level] as (...args: unknown[]) => void)(...entry.args);\n }\n\n if (store.dropped > 0) {\n root.warn({ dropped: store.dropped }, 'early log buffer overflowed, entries dropped');\n store.dropped = 0;\n }\n\n return root;\n}\n\n/**\n * Failure path for startups that die before the logger exists: dump the\n * buffered entries to the console so no phase is silent.\n */\nexport function dumpEarlyLogs(): void {\n const store = getLogStore();\n\n if (store.root) return;\n\n for (const entry of store.buffer.splice(0)) {\n const bindings = entry.bindings.length ? Object.assign({}, ...entry.bindings) : undefined;\n\n console.error(\n new Date(entry.time).toISOString(),\n entry.level.toUpperCase(),\n ...(bindings ? [bindings] : []),\n ...entry.args,\n );\n }\n\n if (store.dropped > 0) {\n console.error(`(${store.dropped} early log entries dropped)`);\n store.dropped = 0;\n }\n}\n","import { log } from '../log/index.js';\n\n/**\n * Platform-owned telemetry bundle: NodeSDK with undici (fetch) and node\n * runtime instrumentation, host metrics, and a Prometheus reader. Trace\n * exporters are driven by standard `OTEL_*` env vars; without any of them\n * traces stay off (no failing localhost OTLP exports).\n *\n * Guarded per process (dev restarts are in-process; a re-created NodeSDK\n * would double-register instrumentations and leak the Prometheus port).\n */\n\nconst TELEMETRY_KEY = Symbol.for('@astroscope/node/telemetry');\n\ninterface TelemetryHandle {\n shutdown: () => Promise<void>;\n}\n\nexport interface TelemetrySdkOptions {\n prometheus: { host?: string | undefined; port?: number | undefined } | false;\n}\n\nfunction getHandle(): TelemetryHandle | undefined {\n return (globalThis as Record<symbol, unknown>)[TELEMETRY_KEY] as TelemetryHandle | undefined;\n}\n\nfunction defaultEnv(key: string, value: string): void {\n if (!process.env[key]) process.env[key] = value;\n}\n\nexport async function startTelemetry(options: TelemetrySdkOptions): Promise<void> {\n const g = globalThis as Record<symbol, unknown>;\n\n if (g[TELEMETRY_KEY]) return;\n\n if (process.env['OTEL_SDK_DISABLED'] === 'true') {\n log.debug('telemetry disabled via OTEL_SDK_DISABLED');\n\n return;\n }\n\n // without an explicitly configured exporter target, exporting traces to the\n // default localhost OTLP endpoint would fail on every flush\n if (!process.env['OTEL_EXPORTER_OTLP_ENDPOINT'] && !process.env['OTEL_EXPORTER_OTLP_TRACES_ENDPOINT']) {\n defaultEnv('OTEL_TRACES_EXPORTER', 'none');\n }\n\n defaultEnv('OTEL_METRICS_EXPORTER', 'none');\n defaultEnv('OTEL_LOGS_EXPORTER', 'none');\n\n const [\n { NodeSDK },\n { UndiciInstrumentation },\n { RuntimeNodeInstrumentation },\n { PrometheusExporter },\n { HostMetrics },\n ] = await Promise.all([\n import('@opentelemetry/sdk-node'),\n import('@opentelemetry/instrumentation-undici'),\n import('@opentelemetry/instrumentation-runtime-node'),\n import('@opentelemetry/exporter-prometheus'),\n import('@opentelemetry/host-metrics'),\n ]);\n\n const prometheus = options.prometheus\n ? {\n host: process.env['OTEL_EXPORTER_PROMETHEUS_HOST'] ?? options.prometheus.host ?? '0.0.0.0',\n port: process.env['OTEL_EXPORTER_PROMETHEUS_PORT']\n ? Number(process.env['OTEL_EXPORTER_PROMETHEUS_PORT'])\n : (options.prometheus.port ?? 9464),\n }\n : false;\n\n const sdk = new NodeSDK({\n instrumentations: [new UndiciInstrumentation(), new RuntimeNodeInstrumentation()],\n ...(prometheus && { metricReaders: [new PrometheusExporter(prometheus)] }),\n });\n\n sdk.start();\n\n const hostMetrics = new HostMetrics();\n\n hostMetrics.start();\n\n g[TELEMETRY_KEY] = {\n shutdown: () => sdk.shutdown(),\n } satisfies TelemetryHandle;\n\n if (prometheus) {\n log.debug({ host: prometheus.host, port: prometheus.port }, 'prometheus metrics listening');\n }\n}\n\n/**\n * Flush and shut the SDK down. Prod-only (dev keeps the SDK for the process\n * lifetime across generations).\n */\nexport async function shutdownTelemetry(): Promise<void> {\n const handle = getHandle();\n\n if (!handle) return;\n\n delete (globalThis as Record<symbol, unknown>)[TELEMETRY_KEY];\n\n await handle.shutdown();\n}\n","import fs from 'node:fs';\nimport { log } from '../observability/log/index.js';\n\n/**\n * Platform env loading (position −1, before the config seam):\n * `CONFIG_PATH` → `./.env` → none. Existing process env vars win\n */\nexport function loadEnvFiles(): void {\n const configPath = process.env['CONFIG_PATH'];\n\n if (configPath) {\n process.loadEnvFile(configPath);\n\n log.debug({ path: configPath }, 'loaded env file from CONFIG_PATH');\n\n return;\n }\n\n if (fs.existsSync('.env')) {\n process.loadEnvFile('.env');\n log.debug({ path: '.env' }, 'loaded env file');\n\n return;\n }\n\n log.debug('no env file loaded');\n}\n","import { type LoggerOptionsFactory, constructRootLogger } from '../observability/log/construct.js';\nimport { type TelemetrySdkOptions, startTelemetry } from '../observability/telemetry/sdk.js';\nimport { loadEnvFiles } from './env.js';\n\nconst INSTRUMENTATION_KEY = Symbol.for('@astroscope/node/instrumentation');\n\nexport interface InstrumentationContext {\n dev: boolean;\n}\n\ninterface InstrumentationSeam {\n register?: ((ctx: InstrumentationContext) => void | Promise<void>) | undefined;\n}\n\ninterface LogSeam {\n default?: LoggerOptionsFactory | undefined;\n}\n\nexport interface PlatformSeams {\n /** `src/config.ts` — validation runs at import; a throw fails the startup */\n config?: (() => Promise<unknown>) | undefined;\n /** `src/instrumentation.ts` — extra instrumentation, once per process */\n instrumentation?: (() => Promise<InstrumentationSeam>) | undefined;\n /** `src/log.ts` — pino logger options (or a factory), never an instance */\n log?: (() => Promise<LogSeam>) | undefined;\n}\n\nexport interface PreparePlatformOptions {\n dev: boolean;\n telemetry: TelemetrySdkOptions | false;\n seams: PlatformSeams;\n}\n\n/**\n * The platform sequence in front of the boot lifecycle:\n * env → config → instrumentation (platform SDK + `register`, once per\n * process) → logger construction (after instrumentation, so entries carry\n * trace correlation). Prod runs it once in `startServer()`; dev re-runs it\n * per generation with the once-per-process parts guarded.\n */\nexport async function preparePlatform(options: PreparePlatformOptions): Promise<void> {\n loadEnvFiles();\n\n await options.seams.config?.();\n\n const g = globalThis as Record<symbol, unknown>;\n\n if (!g[INSTRUMENTATION_KEY]) {\n g[INSTRUMENTATION_KEY] = true;\n\n if (options.telemetry) {\n await startTelemetry(options.telemetry);\n }\n\n const instrumentation = await options.seams.instrumentation?.();\n\n await instrumentation?.register?.({ dev: options.dev });\n }\n\n const logSeam = await options.seams.log?.();\n\n await constructRootLogger(logSeam?.default, { dev: options.dev });\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","names":[],"sources":["../src/server/server.ts"],"mappings":";UAwDiB,YAAA;EACf,IAAA;EACA,IAAA;EACA,IAAA,IAAQ,OAAA;EACR,MAAA,IAAU,OAAO;AAAA;AAAA,iBAGG,WAAA,CAAY,SAAA;EAChC,IAAA;EACA,IAAA;AAAA,IACE,OAAO,CAAC,YAAA"}
1
+ {"version":3,"file":"server.d.ts","names":[],"sources":["../src/server/server.ts"],"mappings":";UA2EiB,YAAA;EACf,IAAA;EACA,IAAA;EACA,IAAA,IAAQ,OAAA;EACR,MAAA,IAAU,OAAO;AAAA;AAAA,iBAGG,WAAA,CAAY,SAAA;EAChC,IAAA;EACA,IAAA;AAAA,IACE,OAAO,CAAC,YAAA"}
package/dist/server.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { n as setBootContext } from "./context-Bkg-FnnQ.js";
2
- import { a as runShutdown, i as createRequestInstrumentation, n as shutdownTelemetry, o as runStartup, r as dumpEarlyLogs, t as preparePlatform } from "./prepare-Be2GgRyf.js";
2
+ import { a as runShutdown, i as createRequestInstrumentation, n as shutdownTelemetry, o as runStartup, r as dumpEarlyLogs, t as preparePlatform } from "./prepare-DQEf2Bnt.js";
3
3
  import { n as getRequestRecord } from "./store-BIUF4lqk.js";
4
4
  import { n as log } from "./log-CSJlKaxY.js";
5
5
  import { n as dispatchNativeMount, t as clearNativeMounts } from "./native-mount-hhwWdLtL.js";
@@ -10,6 +10,7 @@ import path from "node:path";
10
10
  import url from "node:url";
11
11
  import { SpanStatusCode, context, trace } from "@opentelemetry/api";
12
12
  import http from "node:http";
13
+ import https from "node:https";
13
14
  import { checks, probes, server } from "@entwico/health-probes";
14
15
  import { createApp } from "astro/app/entrypoint";
15
16
  import { setGetEnv } from "astro/env/setup";
@@ -281,6 +282,20 @@ async function warmupModules() {
281
282
  const results = await Promise.allSettled(loaders.map((load) => load()));
282
283
  for (const result of results) if (result.status === "rejected") log.error(result.reason instanceof Error ? { err: result.reason } : { reason: result.reason }, "warmup import failed");
283
284
  }
285
+ /**
286
+ * TLS tokens from `SERVER_CERT_PATH` / `SERVER_KEY_PATH` (same contract as
287
+ * `@astrojs/node`). Read after env loading, so the paths may come from `.env`.
288
+ */
289
+ function loadTlsOptions() {
290
+ const certPath = process.env["SERVER_CERT_PATH"];
291
+ const keyPath = process.env["SERVER_KEY_PATH"];
292
+ if (!certPath && !keyPath) return void 0;
293
+ if (!certPath || !keyPath) throw new Error("SERVER_CERT_PATH and SERVER_KEY_PATH must both be set to serve HTTPS");
294
+ return {
295
+ cert: fs.readFileSync(certPath),
296
+ key: fs.readFileSync(keyPath)
297
+ };
298
+ }
284
299
  async function startServer(overrides) {
285
300
  const startedAt = performance.now();
286
301
  const host = overrides?.host ?? process.env["HOST"] ?? runtimeOptions.host;
@@ -367,7 +382,13 @@ async function startServer(overrides) {
367
382
  logging: runtimeOptions.logging,
368
383
  telemetry: runtimeOptions.telemetry ? { exclude: runtimeOptions.telemetry.exclude } : false
369
384
  });
370
- const server$1 = http.createServer((req, res) => {
385
+ let tls;
386
+ try {
387
+ tls = loadTlsOptions();
388
+ } catch (err) {
389
+ await failStartup(err, "failed to load TLS options");
390
+ }
391
+ const listener = (req, res) => {
371
392
  try {
372
393
  decodeURI(req.url ?? "");
373
394
  } catch {
@@ -379,7 +400,8 @@ async function startServer(overrides) {
379
400
  if (dispatchNativeMount(req, res)) return;
380
401
  staticHandler(req, res, () => void appHandler(req, res));
381
402
  });
382
- });
403
+ };
404
+ const server$1 = tls ? https.createServer(tls, listener) : http.createServer(listener);
383
405
  try {
384
406
  await withLifecycleSpan("listen", startup.context, () => {
385
407
  return new Promise((resolve, reject) => {
@@ -396,6 +418,7 @@ async function startServer(overrides) {
396
418
  log.info({
397
419
  host,
398
420
  port,
421
+ ...tls && { https: true },
399
422
  health: !!health,
400
423
  bootMs,
401
424
  warmupMs,
@@ -1 +1 @@
1
- {"version":3,"file":"server.js","names":["healthServer","server"],"sources":["../src/observability/telemetry/lifecycle.ts","../src/server/client-dir.ts","../src/server/serve-app.ts","../src/server/serve-static.ts","../src/server/server.ts"],"sourcesContent":["import { type Context, type Span, SpanStatusCode, context, trace } from '@opentelemetry/api';\n\nconst LIB_NAME = '@astroscope/node';\n\n/**\n * Lifecycle spans (`startup` / `shutdown` with phase children). No-op when no\n * SDK is registered — `trace.getTracer` returns the no-op tracer.\n */\n\nexport function startLifecycleSpan(name: string, parent?: Context): { span: Span; context: Context } {\n const parentContext = parent ?? context.active();\n const span = trace.getTracer(LIB_NAME).startSpan(name, undefined, parentContext);\n\n return { span, context: trace.setSpan(parentContext, span) };\n}\n\nexport async function withLifecycleSpan<T>(name: string, parent: Context, fn: () => Promise<T> | T): Promise<T> {\n const { span, context: spanContext } = startLifecycleSpan(name, parent);\n\n try {\n const result = await context.with(spanContext, fn);\n\n span.setStatus({ code: SpanStatusCode.OK });\n\n return result;\n } catch (err) {\n span.setStatus({ code: SpanStatusCode.ERROR, message: err instanceof Error ? err.message : 'unknown error' });\n\n throw err;\n } finally {\n span.end();\n }\n}\n","import path from 'node:path';\nimport url from 'node:url';\n\n/**\n * Resolve the client directory at runtime relative to the built server entry.\n *\n * The build-time client/server URLs are only valid on the build machine; in a\n * container the deploy path differs. Walk up from `import.meta.url` of the\n * bundled server code until the server directory is found, then apply the\n * build-time server→client relative path.\n */\nexport function resolveClientDir(options: { client: string; server: string }, importMetaUrl: string): string {\n const clientPath = url.fileURLToPath(new URL(options.client));\n const serverPath = url.fileURLToPath(new URL(options.server));\n const rel = path.relative(serverPath, clientPath);\n const serverFolder = path.basename(serverPath);\n\n let serverEntryFolderURL = path.dirname(importMetaUrl);\n let previous = '';\n\n while (!serverEntryFolderURL.endsWith(serverFolder)) {\n if (serverEntryFolderURL === previous) {\n throw new Error(\n `[@astroscope/node] could not find the server directory \"${serverFolder}\" by walking up from \"${importMetaUrl}\"`,\n );\n }\n\n previous = serverEntryFolderURL;\n serverEntryFolderURL = path.dirname(serverEntryFolderURL);\n }\n\n const clientURL = new URL(rel.endsWith('/') ? rel : `${rel}/`, `${serverEntryFolderURL}/entry.mjs`);\n\n return url.fileURLToPath(clientURL);\n}\n","import { createReadStream } from 'node:fs';\nimport type { IncomingMessage, ServerResponse } from 'node:http';\nimport path from 'node:path';\nimport { Readable } from 'node:stream';\nimport type { BaseApp } from 'astro/app';\nimport { createRequestFromNodeRequest, writeResponse } from 'astro/app/node';\nimport { log } from '../observability/log/index.js';\nimport { getRequestRecord } from '../observability/log/store.js';\nimport type { RuntimeOptions } from '../types.js';\n\nasync function readFSErrorPage(client: string, status: number): Promise<Response | undefined> {\n const filePaths = [`${status}.html`, `${status}/index.html`];\n\n for (const filePath of filePaths) {\n const fullPath = path.join(client, filePath);\n let stream: ReturnType<typeof createReadStream> | undefined;\n\n try {\n stream = createReadStream(fullPath);\n\n await new Promise<void>((resolve, reject) => {\n stream!.once('open', () => resolve());\n stream!.once('error', reject);\n });\n\n return new Response(Readable.toWeb(stream) as ReadableStream, {\n headers: { 'Content-Type': 'text/html; charset=utf-8' },\n });\n } catch {\n stream?.destroy();\n }\n }\n\n return undefined;\n}\n\n/**\n * Render on-demand routes: node req → web Request → `app.render()` → node res.\n * Prerendered pages never reach this handler (the static handler serves them);\n * requests for them landing here render the 404 route.\n */\nexport function createAppHandler(app: BaseApp, options: RuntimeOptions, client: string) {\n process.on('unhandledRejection', (reason) => {\n const requestUrl = getRequestRecord()?.url;\n\n log.error(\n {\n ...(reason instanceof Error ? { err: reason } : { reason }),\n ...(requestUrl && { url: requestUrl }),\n },\n requestUrl ? 'unhandled rejection while rendering' : 'unhandled rejection',\n );\n });\n\n const prerenderedErrorPageFetch = async (url: string): Promise<Response> => {\n const { pathname } = new URL(url);\n\n for (const status of [404, 500]) {\n if (pathname.endsWith(`/${status}.html`) || pathname.endsWith(`/${status}/index.html`)) {\n const response = await readFSErrorPage(client, status);\n\n if (response) return response;\n }\n }\n\n return new Response(null, { status: 404 });\n };\n\n const bodySizeLimit =\n options.bodySizeLimit === 0 || options.bodySizeLimit === Number.POSITIVE_INFINITY\n ? undefined\n : options.bodySizeLimit;\n\n return async (req: IncomingMessage, res: ServerResponse): Promise<void> => {\n let request: Request;\n\n try {\n request = createRequestFromNodeRequest(req, {\n allowedDomains: app.getAllowedDomains?.() ?? [],\n ...(bodySizeLimit !== undefined && { bodySizeLimit }),\n port: options.port,\n });\n } catch (err) {\n log.error(err instanceof Error ? { err, url: req.url } : { reason: err, url: req.url }, 'could not render');\n\n res.statusCode = 500;\n res.end('Internal Server Error');\n\n return;\n }\n\n const routeData = app.match(request, true);\n\n const response =\n routeData && !(routeData.type === 'page' && routeData.prerender)\n ? await app.render(request, { addCookieHeader: true, routeData, prerenderedErrorPageFetch })\n : await app.render(request, { addCookieHeader: true, prerenderedErrorPageFetch });\n\n await writeResponse(response, res);\n };\n}\n","import fs from 'node:fs';\nimport type { IncomingMessage, ServerResponse } from 'node:http';\nimport path from 'node:path';\nimport type { BaseApp } from 'astro/app';\nimport send from 'send';\nimport { COMPRESSIBLE, MIME_TYPES } from './mime.js';\n\nconst VARIANTS = [\n { encoding: 'br', suffix: '.br' },\n { encoding: 'gzip', suffix: '.gz' },\n] as const;\n\nfunction negotiateVariant(\n req: IncomingMessage,\n client: string,\n pathname: string,\n): { pathname: string; encoding: string } | undefined {\n const accept = req.headers['accept-encoding'];\n\n if (typeof accept !== 'string') return undefined;\n\n for (const { encoding, suffix } of VARIANTS) {\n if (!accept.includes(encoding)) continue;\n\n if (fs.existsSync(path.join(client, `${pathname}${suffix}`))) {\n return { pathname: `${pathname}${suffix}`, encoding };\n }\n }\n\n return undefined;\n}\n\nfunction hasFileExtension(pathname: string): boolean {\n const last = pathname.split('/').pop();\n\n return !!last && last.includes('.');\n}\n\nfunction prependForwardSlash(pathname: string): string {\n return pathname.startsWith('/') ? pathname : `/${pathname}`;\n}\n\nfunction isDirectory(client: string, urlPath: string): boolean {\n const filePath = path.join(client, urlPath);\n const resolved = path.resolve(filePath);\n const resolvedClient = path.resolve(client);\n\n // path traversal guard\n if (resolved !== resolvedClient && !resolved.startsWith(resolvedClient + path.sep)) {\n return false;\n }\n\n try {\n return fs.lstatSync(filePath).isDirectory();\n } catch {\n return false;\n }\n}\n\n/**\n * Serve files from the client build directory, falling through to `ssr` when\n * no file matches. Handles trailing-slash redirects per the manifest config\n * and marks hashed assets as immutable.\n */\nexport function createStaticHandler(app: BaseApp, client: string) {\n return (req: IncomingMessage, res: ServerResponse, ssr: () => void): void => {\n if (!req.url) {\n ssr();\n\n return;\n }\n\n let fullUrl = req.url;\n\n if (fullUrl.includes('#')) {\n fullUrl = fullUrl.slice(0, fullUrl.indexOf('#'));\n }\n\n const [urlPath = '', urlQuery] = fullUrl.split('?');\n let fsPath = app.removeBase(urlPath);\n\n try {\n fsPath = decodeURI(fsPath);\n } catch {\n // fall through with the raw path; send() rejects malformed paths itself\n }\n\n const dir = isDirectory(client, fsPath);\n const hasSlash = urlPath.endsWith('/');\n let pathname = urlPath;\n\n switch (app.manifest.trailingSlash) {\n case 'never': {\n if (dir && urlPath !== '/' && hasSlash) {\n res.statusCode = 301;\n res.setHeader('Location', urlPath.slice(0, -1) + (urlQuery ? `?${urlQuery}` : ''));\n res.end();\n\n return;\n }\n\n if (dir && !hasSlash) {\n pathname = `${urlPath}/index.html`;\n }\n\n break;\n }\n case 'ignore': {\n if (dir && !hasSlash) {\n pathname = `${urlPath}/index.html`;\n }\n\n break;\n }\n case 'always': {\n if (!hasSlash && !hasFileExtension(urlPath) && !urlPath.startsWith('/_')) {\n res.statusCode = 301;\n res.setHeader('Location', `${urlPath}/${urlQuery ? `?${urlQuery}` : ''}`);\n res.end();\n\n return;\n }\n\n break;\n }\n }\n\n pathname = prependForwardSlash(app.removeBase(pathname));\n\n const normalizedPathname = path.posix.normalize(pathname);\n const compressible = COMPRESSIBLE.has(path.posix.extname(normalizedPathname));\n const variant = compressible ? negotiateVariant(req, client, normalizedPathname) : undefined;\n\n const stream = send(req, variant?.pathname ?? normalizedPathname, {\n root: client,\n dotfiles: normalizedPathname.startsWith('/.well-known/') ? 'allow' : 'deny',\n // with build.format 'file' or 'preserve', pages are output as `page.html`\n // instead of `page/index.html` — let send() try appending `.html`\n extensions: app.manifest.buildFormat === 'file' || app.manifest.buildFormat === 'preserve' ? ['html'] : [],\n });\n\n let forwardError = false;\n\n stream.on('error', (err: NodeJS.ErrnoException & { statusCode?: number }) => {\n if (forwardError) {\n const status = err.statusCode ?? 500;\n\n if (status >= 500) {\n console.error(err.toString());\n }\n\n res.writeHead(status);\n res.end(status >= 500 ? 'Internal server error' : '');\n\n return;\n }\n\n ssr();\n });\n\n stream.on('file', () => {\n forwardError = true;\n });\n\n // fires before the body and before conditional-GET handling, so these\n // headers also land on 304 responses\n stream.on('headers', (headersRes: ServerResponse) => {\n if (compressible) {\n headersRes.setHeader('Vary', 'Accept-Encoding');\n }\n\n if (variant) {\n headersRes.setHeader('Content-Encoding', variant.encoding);\n headersRes.setHeader(\n 'Content-Type',\n MIME_TYPES.get(path.posix.extname(normalizedPathname)) ?? 'application/octet-stream',\n );\n }\n\n if (normalizedPathname.startsWith(`/${app.manifest.assetsDir}/`)) {\n headersRes.setHeader('Cache-Control', 'public, max-age=31536000, immutable');\n }\n });\n\n stream.pipe(res);\n };\n}\n","import http from 'node:http';\nimport { checks, server as healthServer, probes } from '@entwico/health-probes';\nimport { SpanStatusCode } from '@opentelemetry/api';\nimport { createApp } from 'astro/app/entrypoint';\nimport { setGetEnv } from 'astro/env/setup';\n// @ts-expect-error virtual module provided by the integration\nimport { options } from 'virtual:@astroscope/node/config';\nimport { activateHealthChecks, deactivateHealthChecks } from '../health/store.js';\nimport { setBootContext } from '../lifecycle/context.js';\nimport { type BootModule, runShutdown, runStartup } from '../lifecycle/lifecycle.js';\nimport type { BootContext } from '../lifecycle/types.js';\nimport { createRequestInstrumentation } from '../observability/instrument.js';\nimport { dumpEarlyLogs } from '../observability/log/construct.js';\nimport { log } from '../observability/log/index.js';\nimport { startLifecycleSpan, withLifecycleSpan } from '../observability/telemetry/lifecycle.js';\nimport { shutdownTelemetry } from '../observability/telemetry/sdk.js';\nimport { preparePlatform } from '../platform/prepare.js';\nimport type { RuntimeOptions } from '../types.js';\nimport { resolveClientDir } from './client-dir.js';\nimport { clearNativeMounts, dispatchNativeMount } from './native-mount.js';\nimport { createAppHandler } from './serve-app.js';\nimport { createStaticHandler } from './serve-static.js';\n\nsetGetEnv((key) => process.env[key]);\n\nconst runtimeOptions = options as RuntimeOptions;\nconst app = createApp({ streaming: true });\n\nconst roundMs = (n: number) => Math.round(n * 100) / 100;\n\n/**\n * Pre-import every lazily loaded server module (pages, middleware, actions,\n * session driver) so the first request pays no import cost. Uses the\n * manifest's own loaders — exactly what the runtime calls per request.\n */\nasync function warmupModules(): Promise<void> {\n const loaders = [\n ...(app.manifest.pageMap?.values() ?? []),\n app.manifest.middleware,\n app.manifest.actions,\n app.manifest.sessionDriver,\n app.manifest.serverIslandMappings,\n ].filter((load) => load !== undefined);\n\n const results = await Promise.allSettled(loaders.map((load) => load()));\n\n for (const result of results) {\n if (result.status === 'rejected') {\n log.error(\n result.reason instanceof Error ? { err: result.reason } : { reason: result.reason },\n 'warmup import failed',\n );\n }\n }\n}\n\nexport interface ServerHandle {\n host: string;\n port: number;\n stop(): Promise<void>;\n closed(): Promise<void>;\n}\n\nexport async function startServer(overrides?: {\n host?: string | undefined;\n port?: number | undefined;\n}): Promise<ServerHandle> {\n const startedAt = performance.now();\n const host = overrides?.host ?? process.env['HOST'] ?? runtimeOptions.host;\n const port = overrides?.port ?? (process.env['PORT'] ? Number(process.env['PORT']) : runtimeOptions.port);\n const context: BootContext = { dev: false, host, port };\n const health = runtimeOptions.health;\n\n setBootContext(context);\n\n try {\n await preparePlatform({\n dev: false,\n telemetry: runtimeOptions.telemetry ? { prometheus: runtimeOptions.telemetry.prometheus } : false,\n seams: {\n config: () => import('virtual:@astroscope/node/config-entry'),\n instrumentation: () => import('virtual:@astroscope/node/instrumentation-entry'),\n log: () => import('virtual:@astroscope/node/log-entry'),\n },\n });\n } catch (err) {\n // the logger never came up — no silent phase, dump the buffer and die\n dumpEarlyLogs();\n console.error(err);\n process.exit(1);\n }\n\n if (health) {\n healthServer.start({ ...health, host: health.host ?? process.env['HEALTH_HOST'] ?? '0.0.0.0' });\n probes.live.enable();\n activateHealthChecks(checks);\n log.debug('health probes listening');\n }\n\n const startup = startLifecycleSpan('startup');\n\n // the boot module graph may read config at import time, so it must only be\n // evaluated after preparePlatform() has loaded env and config\n let bootModule: BootModule = {};\n let bootMs = 0;\n let warmupMs = 0;\n\n // starts in parallel with the boot startup, awaited before listen\n const warmupStartedAt = performance.now();\n const warmupSpan = startLifecycleSpan('warmup', startup.context);\n const warmup = warmupModules().then(() => {\n warmupMs = roundMs(performance.now() - warmupStartedAt);\n warmupSpan.span.end();\n });\n\n const shutdownLifecycle = async (\n shutdownContext?: ReturnType<typeof startLifecycleSpan>['context'],\n ): Promise<void> => {\n try {\n if (shutdownContext) {\n await withLifecycleSpan('onShutdown', shutdownContext, () => runShutdown(bootModule, context));\n } else {\n await runShutdown(bootModule, context);\n }\n } catch (err) {\n log.error(err instanceof Error ? { err } : { reason: err }, 'shutdown failed');\n }\n\n clearNativeMounts();\n\n if (health) {\n deactivateHealthChecks();\n await healthServer.stop();\n }\n };\n\n const failStartup = async (err: unknown, message: string): Promise<never> => {\n log.error(err instanceof Error ? { err } : { reason: err }, message);\n startup.span.setStatus({ code: SpanStatusCode.ERROR, message });\n startup.span.end();\n\n await shutdownLifecycle();\n await shutdownTelemetry();\n process.exit(1);\n };\n\n try {\n const bootStartedAt = performance.now();\n\n // @ts-expect-error virtual module provided by the integration\n bootModule = (await import('virtual:@astroscope/node/boot')) as BootModule;\n\n await withLifecycleSpan('boot', startup.context, () => runStartup(bootModule, context));\n\n bootMs = roundMs(performance.now() - bootStartedAt);\n } catch (err) {\n await failStartup(err, 'startup failed');\n }\n\n await warmup;\n\n if (health) probes.startup.enable();\n\n const client = resolveClientDir(runtimeOptions, import.meta.url);\n const appHandler = createAppHandler(app, runtimeOptions, client);\n const staticHandler = createStaticHandler(app, client);\n const instrument = createRequestInstrumentation({\n logging: runtimeOptions.logging,\n telemetry: runtimeOptions.telemetry ? { exclude: runtimeOptions.telemetry.exclude } : false,\n });\n\n const server = http.createServer((req, res) => {\n try {\n decodeURI(req.url ?? '');\n } catch {\n res.writeHead(400);\n res.end('Bad request.');\n\n return;\n }\n\n instrument(req, res, () => {\n if (dispatchNativeMount(req, res)) return;\n\n staticHandler(req, res, () => void appHandler(req, res));\n });\n });\n\n try {\n await withLifecycleSpan('listen', startup.context, () => {\n return new Promise<void>((resolve, reject) => {\n server.once('error', reject);\n server.listen(port, host, resolve);\n });\n });\n } catch (err) {\n await failStartup(err, `failed to listen on ${host}:${port}`);\n }\n\n if (health) probes.ready.enable();\n\n startup.span.setStatus({ code: SpanStatusCode.OK });\n startup.span.end();\n\n log.info(\n { host, port, health: !!health, bootMs, warmupMs, totalMs: roundMs(performance.now() - startedAt) },\n 'server ready',\n );\n\n let stopPromise: Promise<void> | undefined;\n let resolveClosed!: () => void;\n\n const closedPromise = new Promise<void>((resolve) => {\n resolveClosed = resolve;\n });\n\n const doStop = async (): Promise<void> => {\n if (health) probes.ready.disable();\n\n log.info('draining');\n\n const drainStartedAt = performance.now();\n const shutdown = startLifecycleSpan('shutdown');\n\n await withLifecycleSpan('drain', shutdown.context, async () => {\n const closed = new Promise<void>((resolve) => server.close(() => resolve()));\n\n server.closeIdleConnections();\n\n const forceTimer = setTimeout(() => server.closeAllConnections(), runtimeOptions.shutdownTimeout);\n\n await closed;\n\n clearTimeout(forceTimer);\n });\n\n const drainMs = roundMs(performance.now() - drainStartedAt);\n\n await shutdownLifecycle(shutdown.context);\n\n shutdown.span.end();\n\n log.info({ drainMs }, 'shutdown complete');\n\n await shutdownTelemetry();\n resolveClosed();\n };\n\n const stop = (): Promise<void> => (stopPromise ??= doStop());\n\n process.once('SIGTERM', () => void stop().then(() => process.exit(0)));\n process.once('SIGINT', () => void stop().then(() => process.exit(0)));\n\n return { host, port, stop, closed: () => closedPromise };\n}\n\nif (process.env['ASTROSCOPE_NODE_AUTOSTART'] !== 'disabled') {\n await startServer();\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"server.js","names":["healthServer","server"],"sources":["../src/observability/telemetry/lifecycle.ts","../src/server/client-dir.ts","../src/server/serve-app.ts","../src/server/serve-static.ts","../src/server/server.ts"],"sourcesContent":["import { type Context, type Span, SpanStatusCode, context, trace } from '@opentelemetry/api';\n\nconst LIB_NAME = '@astroscope/node';\n\n/**\n * Lifecycle spans (`startup` / `shutdown` with phase children). No-op when no\n * SDK is registered — `trace.getTracer` returns the no-op tracer.\n */\n\nexport function startLifecycleSpan(name: string, parent?: Context): { span: Span; context: Context } {\n const parentContext = parent ?? context.active();\n const span = trace.getTracer(LIB_NAME).startSpan(name, undefined, parentContext);\n\n return { span, context: trace.setSpan(parentContext, span) };\n}\n\nexport async function withLifecycleSpan<T>(name: string, parent: Context, fn: () => Promise<T> | T): Promise<T> {\n const { span, context: spanContext } = startLifecycleSpan(name, parent);\n\n try {\n const result = await context.with(spanContext, fn);\n\n span.setStatus({ code: SpanStatusCode.OK });\n\n return result;\n } catch (err) {\n span.setStatus({ code: SpanStatusCode.ERROR, message: err instanceof Error ? err.message : 'unknown error' });\n\n throw err;\n } finally {\n span.end();\n }\n}\n","import path from 'node:path';\nimport url from 'node:url';\n\n/**\n * Resolve the client directory at runtime relative to the built server entry.\n *\n * The build-time client/server URLs are only valid on the build machine; in a\n * container the deploy path differs. Walk up from `import.meta.url` of the\n * bundled server code until the server directory is found, then apply the\n * build-time server→client relative path.\n */\nexport function resolveClientDir(options: { client: string; server: string }, importMetaUrl: string): string {\n const clientPath = url.fileURLToPath(new URL(options.client));\n const serverPath = url.fileURLToPath(new URL(options.server));\n const rel = path.relative(serverPath, clientPath);\n const serverFolder = path.basename(serverPath);\n\n let serverEntryFolderURL = path.dirname(importMetaUrl);\n let previous = '';\n\n while (!serverEntryFolderURL.endsWith(serverFolder)) {\n if (serverEntryFolderURL === previous) {\n throw new Error(\n `[@astroscope/node] could not find the server directory \"${serverFolder}\" by walking up from \"${importMetaUrl}\"`,\n );\n }\n\n previous = serverEntryFolderURL;\n serverEntryFolderURL = path.dirname(serverEntryFolderURL);\n }\n\n const clientURL = new URL(rel.endsWith('/') ? rel : `${rel}/`, `${serverEntryFolderURL}/entry.mjs`);\n\n return url.fileURLToPath(clientURL);\n}\n","import { createReadStream } from 'node:fs';\nimport type { IncomingMessage, ServerResponse } from 'node:http';\nimport path from 'node:path';\nimport { Readable } from 'node:stream';\nimport type { BaseApp } from 'astro/app';\nimport { createRequestFromNodeRequest, writeResponse } from 'astro/app/node';\nimport { log } from '../observability/log/index.js';\nimport { getRequestRecord } from '../observability/log/store.js';\nimport type { RuntimeOptions } from '../types.js';\n\nasync function readFSErrorPage(client: string, status: number): Promise<Response | undefined> {\n const filePaths = [`${status}.html`, `${status}/index.html`];\n\n for (const filePath of filePaths) {\n const fullPath = path.join(client, filePath);\n let stream: ReturnType<typeof createReadStream> | undefined;\n\n try {\n stream = createReadStream(fullPath);\n\n await new Promise<void>((resolve, reject) => {\n stream!.once('open', () => resolve());\n stream!.once('error', reject);\n });\n\n return new Response(Readable.toWeb(stream) as ReadableStream, {\n headers: { 'Content-Type': 'text/html; charset=utf-8' },\n });\n } catch {\n stream?.destroy();\n }\n }\n\n return undefined;\n}\n\n/**\n * Render on-demand routes: node req → web Request → `app.render()` → node res.\n * Prerendered pages never reach this handler (the static handler serves them);\n * requests for them landing here render the 404 route.\n */\nexport function createAppHandler(app: BaseApp, options: RuntimeOptions, client: string) {\n process.on('unhandledRejection', (reason) => {\n const requestUrl = getRequestRecord()?.url;\n\n log.error(\n {\n ...(reason instanceof Error ? { err: reason } : { reason }),\n ...(requestUrl && { url: requestUrl }),\n },\n requestUrl ? 'unhandled rejection while rendering' : 'unhandled rejection',\n );\n });\n\n const prerenderedErrorPageFetch = async (url: string): Promise<Response> => {\n const { pathname } = new URL(url);\n\n for (const status of [404, 500]) {\n if (pathname.endsWith(`/${status}.html`) || pathname.endsWith(`/${status}/index.html`)) {\n const response = await readFSErrorPage(client, status);\n\n if (response) return response;\n }\n }\n\n return new Response(null, { status: 404 });\n };\n\n const bodySizeLimit =\n options.bodySizeLimit === 0 || options.bodySizeLimit === Number.POSITIVE_INFINITY\n ? undefined\n : options.bodySizeLimit;\n\n return async (req: IncomingMessage, res: ServerResponse): Promise<void> => {\n let request: Request;\n\n try {\n request = createRequestFromNodeRequest(req, {\n allowedDomains: app.getAllowedDomains?.() ?? [],\n ...(bodySizeLimit !== undefined && { bodySizeLimit }),\n port: options.port,\n });\n } catch (err) {\n log.error(err instanceof Error ? { err, url: req.url } : { reason: err, url: req.url }, 'could not render');\n\n res.statusCode = 500;\n res.end('Internal Server Error');\n\n return;\n }\n\n const routeData = app.match(request, true);\n\n const response =\n routeData && !(routeData.type === 'page' && routeData.prerender)\n ? await app.render(request, { addCookieHeader: true, routeData, prerenderedErrorPageFetch })\n : await app.render(request, { addCookieHeader: true, prerenderedErrorPageFetch });\n\n await writeResponse(response, res);\n };\n}\n","import fs from 'node:fs';\nimport type { IncomingMessage, ServerResponse } from 'node:http';\nimport path from 'node:path';\nimport type { BaseApp } from 'astro/app';\nimport send from 'send';\nimport { COMPRESSIBLE, MIME_TYPES } from './mime.js';\n\nconst VARIANTS = [\n { encoding: 'br', suffix: '.br' },\n { encoding: 'gzip', suffix: '.gz' },\n] as const;\n\nfunction negotiateVariant(\n req: IncomingMessage,\n client: string,\n pathname: string,\n): { pathname: string; encoding: string } | undefined {\n const accept = req.headers['accept-encoding'];\n\n if (typeof accept !== 'string') return undefined;\n\n for (const { encoding, suffix } of VARIANTS) {\n if (!accept.includes(encoding)) continue;\n\n if (fs.existsSync(path.join(client, `${pathname}${suffix}`))) {\n return { pathname: `${pathname}${suffix}`, encoding };\n }\n }\n\n return undefined;\n}\n\nfunction hasFileExtension(pathname: string): boolean {\n const last = pathname.split('/').pop();\n\n return !!last && last.includes('.');\n}\n\nfunction prependForwardSlash(pathname: string): string {\n return pathname.startsWith('/') ? pathname : `/${pathname}`;\n}\n\nfunction isDirectory(client: string, urlPath: string): boolean {\n const filePath = path.join(client, urlPath);\n const resolved = path.resolve(filePath);\n const resolvedClient = path.resolve(client);\n\n // path traversal guard\n if (resolved !== resolvedClient && !resolved.startsWith(resolvedClient + path.sep)) {\n return false;\n }\n\n try {\n return fs.lstatSync(filePath).isDirectory();\n } catch {\n return false;\n }\n}\n\n/**\n * Serve files from the client build directory, falling through to `ssr` when\n * no file matches. Handles trailing-slash redirects per the manifest config\n * and marks hashed assets as immutable.\n */\nexport function createStaticHandler(app: BaseApp, client: string) {\n return (req: IncomingMessage, res: ServerResponse, ssr: () => void): void => {\n if (!req.url) {\n ssr();\n\n return;\n }\n\n let fullUrl = req.url;\n\n if (fullUrl.includes('#')) {\n fullUrl = fullUrl.slice(0, fullUrl.indexOf('#'));\n }\n\n const [urlPath = '', urlQuery] = fullUrl.split('?');\n let fsPath = app.removeBase(urlPath);\n\n try {\n fsPath = decodeURI(fsPath);\n } catch {\n // fall through with the raw path; send() rejects malformed paths itself\n }\n\n const dir = isDirectory(client, fsPath);\n const hasSlash = urlPath.endsWith('/');\n let pathname = urlPath;\n\n switch (app.manifest.trailingSlash) {\n case 'never': {\n if (dir && urlPath !== '/' && hasSlash) {\n res.statusCode = 301;\n res.setHeader('Location', urlPath.slice(0, -1) + (urlQuery ? `?${urlQuery}` : ''));\n res.end();\n\n return;\n }\n\n if (dir && !hasSlash) {\n pathname = `${urlPath}/index.html`;\n }\n\n break;\n }\n case 'ignore': {\n if (dir && !hasSlash) {\n pathname = `${urlPath}/index.html`;\n }\n\n break;\n }\n case 'always': {\n if (!hasSlash && !hasFileExtension(urlPath) && !urlPath.startsWith('/_')) {\n res.statusCode = 301;\n res.setHeader('Location', `${urlPath}/${urlQuery ? `?${urlQuery}` : ''}`);\n res.end();\n\n return;\n }\n\n break;\n }\n }\n\n pathname = prependForwardSlash(app.removeBase(pathname));\n\n const normalizedPathname = path.posix.normalize(pathname);\n const compressible = COMPRESSIBLE.has(path.posix.extname(normalizedPathname));\n const variant = compressible ? negotiateVariant(req, client, normalizedPathname) : undefined;\n\n const stream = send(req, variant?.pathname ?? normalizedPathname, {\n root: client,\n dotfiles: normalizedPathname.startsWith('/.well-known/') ? 'allow' : 'deny',\n // with build.format 'file' or 'preserve', pages are output as `page.html`\n // instead of `page/index.html` — let send() try appending `.html`\n extensions: app.manifest.buildFormat === 'file' || app.manifest.buildFormat === 'preserve' ? ['html'] : [],\n });\n\n let forwardError = false;\n\n stream.on('error', (err: NodeJS.ErrnoException & { statusCode?: number }) => {\n if (forwardError) {\n const status = err.statusCode ?? 500;\n\n if (status >= 500) {\n console.error(err.toString());\n }\n\n res.writeHead(status);\n res.end(status >= 500 ? 'Internal server error' : '');\n\n return;\n }\n\n ssr();\n });\n\n stream.on('file', () => {\n forwardError = true;\n });\n\n // fires before the body and before conditional-GET handling, so these\n // headers also land on 304 responses\n stream.on('headers', (headersRes: ServerResponse) => {\n if (compressible) {\n headersRes.setHeader('Vary', 'Accept-Encoding');\n }\n\n if (variant) {\n headersRes.setHeader('Content-Encoding', variant.encoding);\n headersRes.setHeader(\n 'Content-Type',\n MIME_TYPES.get(path.posix.extname(normalizedPathname)) ?? 'application/octet-stream',\n );\n }\n\n if (normalizedPathname.startsWith(`/${app.manifest.assetsDir}/`)) {\n headersRes.setHeader('Cache-Control', 'public, max-age=31536000, immutable');\n }\n });\n\n stream.pipe(res);\n };\n}\n","import fs from 'node:fs';\nimport http from 'node:http';\nimport https from 'node:https';\nimport { checks, server as healthServer, probes } from '@entwico/health-probes';\nimport { SpanStatusCode } from '@opentelemetry/api';\nimport { createApp } from 'astro/app/entrypoint';\nimport { setGetEnv } from 'astro/env/setup';\n// @ts-expect-error virtual module provided by the integration\nimport { options } from 'virtual:@astroscope/node/config';\nimport { activateHealthChecks, deactivateHealthChecks } from '../health/store.js';\nimport { setBootContext } from '../lifecycle/context.js';\nimport { type BootModule, runShutdown, runStartup } from '../lifecycle/lifecycle.js';\nimport type { BootContext } from '../lifecycle/types.js';\nimport { createRequestInstrumentation } from '../observability/instrument.js';\nimport { dumpEarlyLogs } from '../observability/log/construct.js';\nimport { log } from '../observability/log/index.js';\nimport { startLifecycleSpan, withLifecycleSpan } from '../observability/telemetry/lifecycle.js';\nimport { shutdownTelemetry } from '../observability/telemetry/sdk.js';\nimport { preparePlatform } from '../platform/prepare.js';\nimport type { RuntimeOptions } from '../types.js';\nimport { resolveClientDir } from './client-dir.js';\nimport { clearNativeMounts, dispatchNativeMount } from './native-mount.js';\nimport { createAppHandler } from './serve-app.js';\nimport { createStaticHandler } from './serve-static.js';\n\nsetGetEnv((key) => process.env[key]);\n\nconst runtimeOptions = options as RuntimeOptions;\nconst app = createApp({ streaming: true });\n\nconst roundMs = (n: number) => Math.round(n * 100) / 100;\n\n/**\n * Pre-import every lazily loaded server module (pages, middleware, actions,\n * session driver) so the first request pays no import cost. Uses the\n * manifest's own loaders — exactly what the runtime calls per request.\n */\nasync function warmupModules(): Promise<void> {\n const loaders = [\n ...(app.manifest.pageMap?.values() ?? []),\n app.manifest.middleware,\n app.manifest.actions,\n app.manifest.sessionDriver,\n app.manifest.serverIslandMappings,\n ].filter((load) => load !== undefined);\n\n const results = await Promise.allSettled(loaders.map((load) => load()));\n\n for (const result of results) {\n if (result.status === 'rejected') {\n log.error(\n result.reason instanceof Error ? { err: result.reason } : { reason: result.reason },\n 'warmup import failed',\n );\n }\n }\n}\n\n/**\n * TLS tokens from `SERVER_CERT_PATH` / `SERVER_KEY_PATH` (same contract as\n * `@astrojs/node`). Read after env loading, so the paths may come from `.env`.\n */\nfunction loadTlsOptions(): { cert: Buffer; key: Buffer } | undefined {\n const certPath = process.env['SERVER_CERT_PATH'];\n const keyPath = process.env['SERVER_KEY_PATH'];\n\n if (!certPath && !keyPath) return undefined;\n\n if (!certPath || !keyPath) {\n throw new Error('SERVER_CERT_PATH and SERVER_KEY_PATH must both be set to serve HTTPS');\n }\n\n return { cert: fs.readFileSync(certPath), key: fs.readFileSync(keyPath) };\n}\n\nexport interface ServerHandle {\n host: string;\n port: number;\n stop(): Promise<void>;\n closed(): Promise<void>;\n}\n\nexport async function startServer(overrides?: {\n host?: string | undefined;\n port?: number | undefined;\n}): Promise<ServerHandle> {\n const startedAt = performance.now();\n const host = overrides?.host ?? process.env['HOST'] ?? runtimeOptions.host;\n const port = overrides?.port ?? (process.env['PORT'] ? Number(process.env['PORT']) : runtimeOptions.port);\n const context: BootContext = { dev: false, host, port };\n const health = runtimeOptions.health;\n\n setBootContext(context);\n\n try {\n await preparePlatform({\n dev: false,\n telemetry: runtimeOptions.telemetry ? { prometheus: runtimeOptions.telemetry.prometheus } : false,\n seams: {\n config: () => import('virtual:@astroscope/node/config-entry'),\n instrumentation: () => import('virtual:@astroscope/node/instrumentation-entry'),\n log: () => import('virtual:@astroscope/node/log-entry'),\n },\n });\n } catch (err) {\n // the logger never came up — no silent phase, dump the buffer and die\n dumpEarlyLogs();\n console.error(err);\n process.exit(1);\n }\n\n if (health) {\n healthServer.start({ ...health, host: health.host ?? process.env['HEALTH_HOST'] ?? '0.0.0.0' });\n probes.live.enable();\n activateHealthChecks(checks);\n log.debug('health probes listening');\n }\n\n const startup = startLifecycleSpan('startup');\n\n // the boot module graph may read config at import time, so it must only be\n // evaluated after preparePlatform() has loaded env and config\n let bootModule: BootModule = {};\n let bootMs = 0;\n let warmupMs = 0;\n\n // starts in parallel with the boot startup, awaited before listen\n const warmupStartedAt = performance.now();\n const warmupSpan = startLifecycleSpan('warmup', startup.context);\n const warmup = warmupModules().then(() => {\n warmupMs = roundMs(performance.now() - warmupStartedAt);\n warmupSpan.span.end();\n });\n\n const shutdownLifecycle = async (\n shutdownContext?: ReturnType<typeof startLifecycleSpan>['context'],\n ): Promise<void> => {\n try {\n if (shutdownContext) {\n await withLifecycleSpan('onShutdown', shutdownContext, () => runShutdown(bootModule, context));\n } else {\n await runShutdown(bootModule, context);\n }\n } catch (err) {\n log.error(err instanceof Error ? { err } : { reason: err }, 'shutdown failed');\n }\n\n clearNativeMounts();\n\n if (health) {\n deactivateHealthChecks();\n await healthServer.stop();\n }\n };\n\n const failStartup = async (err: unknown, message: string): Promise<never> => {\n log.error(err instanceof Error ? { err } : { reason: err }, message);\n startup.span.setStatus({ code: SpanStatusCode.ERROR, message });\n startup.span.end();\n\n await shutdownLifecycle();\n await shutdownTelemetry();\n process.exit(1);\n };\n\n try {\n const bootStartedAt = performance.now();\n\n // @ts-expect-error virtual module provided by the integration\n bootModule = (await import('virtual:@astroscope/node/boot')) as BootModule;\n\n await withLifecycleSpan('boot', startup.context, () => runStartup(bootModule, context));\n\n bootMs = roundMs(performance.now() - bootStartedAt);\n } catch (err) {\n await failStartup(err, 'startup failed');\n }\n\n await warmup;\n\n if (health) probes.startup.enable();\n\n const client = resolveClientDir(runtimeOptions, import.meta.url);\n const appHandler = createAppHandler(app, runtimeOptions, client);\n const staticHandler = createStaticHandler(app, client);\n const instrument = createRequestInstrumentation({\n logging: runtimeOptions.logging,\n telemetry: runtimeOptions.telemetry ? { exclude: runtimeOptions.telemetry.exclude } : false,\n });\n\n let tls: { cert: Buffer; key: Buffer } | undefined;\n\n try {\n tls = loadTlsOptions();\n } catch (err) {\n await failStartup(err, 'failed to load TLS options');\n }\n\n const listener: http.RequestListener = (req, res) => {\n try {\n decodeURI(req.url ?? '');\n } catch {\n res.writeHead(400);\n res.end('Bad request.');\n\n return;\n }\n\n instrument(req, res, () => {\n if (dispatchNativeMount(req, res)) return;\n\n staticHandler(req, res, () => void appHandler(req, res));\n });\n };\n\n const server = tls ? https.createServer(tls, listener) : http.createServer(listener);\n\n try {\n await withLifecycleSpan('listen', startup.context, () => {\n return new Promise<void>((resolve, reject) => {\n server.once('error', reject);\n server.listen(port, host, resolve);\n });\n });\n } catch (err) {\n await failStartup(err, `failed to listen on ${host}:${port}`);\n }\n\n if (health) probes.ready.enable();\n\n startup.span.setStatus({ code: SpanStatusCode.OK });\n startup.span.end();\n\n log.info(\n {\n host,\n port,\n ...(tls && { https: true }),\n health: !!health,\n bootMs,\n warmupMs,\n totalMs: roundMs(performance.now() - startedAt),\n },\n 'server ready',\n );\n\n let stopPromise: Promise<void> | undefined;\n let resolveClosed!: () => void;\n\n const closedPromise = new Promise<void>((resolve) => {\n resolveClosed = resolve;\n });\n\n const doStop = async (): Promise<void> => {\n if (health) probes.ready.disable();\n\n log.info('draining');\n\n const drainStartedAt = performance.now();\n const shutdown = startLifecycleSpan('shutdown');\n\n await withLifecycleSpan('drain', shutdown.context, async () => {\n const closed = new Promise<void>((resolve) => server.close(() => resolve()));\n\n server.closeIdleConnections();\n\n const forceTimer = setTimeout(() => server.closeAllConnections(), runtimeOptions.shutdownTimeout);\n\n await closed;\n\n clearTimeout(forceTimer);\n });\n\n const drainMs = roundMs(performance.now() - drainStartedAt);\n\n await shutdownLifecycle(shutdown.context);\n\n shutdown.span.end();\n\n log.info({ drainMs }, 'shutdown complete');\n\n await shutdownTelemetry();\n resolveClosed();\n };\n\n const stop = (): Promise<void> => (stopPromise ??= doStop());\n\n process.once('SIGTERM', () => void stop().then(() => process.exit(0)));\n process.once('SIGINT', () => void stop().then(() => process.exit(0)));\n\n return { host, port, stop, closed: () => closedPromise };\n}\n\nif (process.env['ASTROSCOPE_NODE_AUTOSTART'] !== 'disabled') {\n await startServer();\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astroscope/node",
3
- "version": "1.0.1",
3
+ "version": "1.2.0",
4
4
  "description": "Opinionated, cloud-friendly Node adapter for Astro: boot lifecycle, health probes, request logging, telemetry, CSRF and static serving run as plain code around server.listen()",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1 +0,0 @@
1
- {"version":3,"file":"prepare-Be2GgRyf.js","names":["LIB_NAME"],"sources":["../src/lifecycle/lifecycle.ts","../src/observability/telemetry/metrics.ts","../src/observability/instrument.ts","../src/observability/log/construct.ts","../src/observability/telemetry/sdk.ts","../src/platform/env.ts","../src/platform/prepare.ts"],"sourcesContent":["import { emit } from './events.js';\nimport type { BootContext } from './types.js';\n\nexport interface BootModule {\n onStartup?: ((context: BootContext) => Promise<void> | void) | undefined;\n onShutdown?: ((context: BootContext) => Promise<void> | void) | undefined;\n}\n\nexport async function runStartup(boot: BootModule, context: BootContext): Promise<void> {\n await emit('beforeOnStartup', context);\n await boot.onStartup?.(context);\n await emit('afterOnStartup', context);\n}\n\nexport async function runShutdown(boot: BootModule, context: BootContext): Promise<void> {\n try {\n await emit('beforeOnShutdown', context);\n await boot.onShutdown?.(context);\n } finally {\n await emit('afterOnShutdown', context);\n }\n}\n","import { type Histogram, type UpDownCounter, ValueType, metrics } from '@opentelemetry/api';\n\nconst LIB_NAME = '@astroscope/node';\n\n// lazy initialization so instruments bind to the SDK meter provider\nlet httpRequestDuration: Histogram | null = null;\nlet httpActiveRequests: UpDownCounter | null = null;\nlet actionDuration: Histogram | null = null;\n\nfunction getHttpRequestDuration(): Histogram {\n return (httpRequestDuration ??= metrics.getMeter(LIB_NAME).createHistogram('http.server.request.duration', {\n description: 'Duration of HTTP server requests',\n unit: 's',\n valueType: ValueType.DOUBLE,\n }));\n}\n\nfunction getHttpActiveRequests(): UpDownCounter {\n return (httpActiveRequests ??= metrics.getMeter(LIB_NAME).createUpDownCounter('http.server.active_requests', {\n description: 'Number of active HTTP server requests',\n unit: '{request}',\n valueType: ValueType.INT,\n }));\n}\n\nfunction getActionDuration(): Histogram {\n return (actionDuration ??= metrics.getMeter(LIB_NAME).createHistogram('astro.action.duration', {\n description: 'Duration of Astro action executions',\n unit: 's',\n valueType: ValueType.DOUBLE,\n }));\n}\n\n/**\n * Record the start of an HTTP request. Returns a function to call when the\n * request ends. Route is unknown at the native-handler level, so active\n * requests carry only the method.\n */\nexport function recordHttpRequestStart(method: string): () => void {\n getHttpActiveRequests().add(1, { 'http.request.method': method });\n\n return () => {\n getHttpActiveRequests().add(-1, { 'http.request.method': method });\n };\n}\n\nexport function recordHttpRequestDuration(\n attributes: { method: string; route: string | undefined; status: number },\n durationMs: number,\n): void {\n getHttpRequestDuration().record(durationMs / 1000, {\n 'http.request.method': attributes.method,\n 'http.route': attributes.route ?? '',\n 'http.response.status_code': attributes.status,\n });\n}\n\nexport function recordActionDuration(attributes: { name: string; status: number }, durationMs: number): void {\n getActionDuration().record(durationMs / 1000, {\n 'astro.action.name': attributes.name,\n 'http.response.status_code': attributes.status,\n });\n}\n","import type { IncomingMessage, ServerResponse } from 'node:http';\nimport { createMatcher } from '@entwico/dash/match';\nimport { SpanKind, SpanStatusCode, context, propagation, trace } from '@opentelemetry/api';\nimport type { Logger } from 'pino';\nimport type { ExcludePattern } from '../excludes/excludes.js';\nimport { generateReqId } from './log/index.js';\nimport { type RequestRecord, getLogStore } from './log/store.js';\nimport { recordActionDuration, recordHttpRequestDuration, recordHttpRequestStart } from './telemetry/metrics.js';\n\nconst LIB_NAME = '@astroscope/node';\nconst ACTIONS_PREFIX = '/_actions/';\nconst REQUEST_ID_PATTERN = /^[\\w.-]{1,64}$/;\n\nconst roundTime = (n: number) => Math.round(n * 100) / 100;\n\nexport interface RequestLoggingConfig {\n exclude: ExcludePattern[];\n extended: boolean;\n}\n\nexport interface RequestTelemetryConfig {\n exclude: ExcludePattern[];\n}\n\nexport interface RequestInstrumentationConfig {\n logging: RequestLoggingConfig | false;\n telemetry: RequestTelemetryConfig | false;\n}\n\nfunction getClientIp(req: IncomingMessage): string | undefined {\n const forwarded = req.headers['x-forwarded-for'];\n const first = Array.isArray(forwarded) ? forwarded[0] : forwarded;\n\n return (\n first?.split(',')[0]?.trim() ??\n (req.headers['x-real-ip'] as string | undefined) ??\n (req.headers['cf-connecting-ip'] as string | undefined)\n );\n}\n\nfunction resolveReqId(req: IncomingMessage): string {\n const incoming = req.headers['x-request-id'];\n const value = Array.isArray(incoming) ? incoming[0] : incoming;\n\n return value && REQUEST_ID_PATTERN.test(value) ? value : generateReqId();\n}\n\nfunction chunkSize(chunk: unknown): number {\n if (chunk == null) return 0;\n if (ArrayBuffer.isView(chunk)) return chunk.byteLength;\n if (typeof chunk === 'string') return Buffer.byteLength(chunk);\n\n return 0;\n}\n\n/**\n * Wraps the native request/response with logging and telemetry: a request\n * logger in async context (real status, response size, aborted-vs-completed\n * on `finish`/`close`), a SERVER span with propagation extraction, and\n * request metrics. Both concerns honor their own exclude patterns; when both\n * are excluded the request passes through untouched.\n */\nexport function createRequestInstrumentation(config: RequestInstrumentationConfig) {\n const tracer = trace.getTracer(LIB_NAME);\n const store = getLogStore();\n const loggingExcluded = config.logging ? createMatcher(config.logging.exclude) : () => true;\n const telemetryExcluded = config.telemetry ? createMatcher(config.telemetry.exclude) : () => true;\n\n return (req: IncomingMessage, res: ServerResponse, inner: () => void): void => {\n const url = req.url ?? '';\n const queryIndex = url.indexOf('?');\n const pathname = queryIndex === -1 ? url : url.slice(0, queryIndex);\n const method = req.method ?? 'GET';\n\n const logging = config.logging && !loggingExcluded(pathname) ? config.logging : false;\n const telemetry = config.telemetry && !telemetryExcluded(pathname) ? config.telemetry : false;\n\n if (!logging && !telemetry) {\n inner();\n\n return;\n }\n\n const startTime = performance.now();\n const isAction = pathname.startsWith(ACTIONS_PREFIX);\n\n let requestLogger: Logger | undefined;\n\n if (logging && store.root) {\n const reqId = resolveReqId(req);\n const reqData: Record<string, unknown> = { method, url: pathname };\n\n // extended logging includes potentially sensitive data\n if (logging.extended) {\n reqData['query'] = queryIndex === -1 ? '' : url.slice(queryIndex + 1);\n reqData['headers'] = req.headers;\n reqData['remoteAddress'] = getClientIp(req) ?? req.socket.remoteAddress;\n }\n\n requestLogger = store.root.child({ reqId, req: reqData });\n\n res.setHeader('x-request-id', reqId);\n }\n\n const record: RequestRecord = {\n logger: requestLogger,\n url,\n route: undefined,\n actionName: isAction ? pathname.slice(ACTIONS_PREFIX.length).replace(/\\/$/, '') : undefined,\n };\n\n let span: ReturnType<typeof tracer.startSpan> | undefined;\n let endActiveRequest: (() => void) | undefined;\n\n if (telemetry) {\n const parentContext = propagation.extract(context.active(), req.headers);\n const contentLength = req.headers['content-length'];\n const clientIp = getClientIp(req);\n const host = req.headers['host'];\n\n span = tracer.startSpan(\n isAction ? `ACTION ${record.actionName}` : method,\n {\n kind: SpanKind.SERVER,\n attributes: {\n 'http.request.method': method,\n 'url.path': pathname,\n 'url.query': queryIndex === -1 ? '' : url.slice(queryIndex + 1),\n 'url.scheme': 'http',\n 'user_agent.original': req.headers['user-agent'] ?? '',\n ...(host && { 'server.address': host }),\n ...(contentLength && { 'http.request.body.size': parseInt(contentLength) }),\n ...(clientIp && { 'client.address': clientIp }),\n },\n },\n parentContext,\n );\n\n endActiveRequest = recordHttpRequestStart(method);\n }\n\n let responseSize = 0;\n let firstByteTime: number | undefined;\n\n const originalWrite = res.write.bind(res);\n const originalEnd = res.end.bind(res);\n\n res.write = ((chunk: unknown, ...rest: unknown[]) => {\n firstByteTime ??= performance.now();\n responseSize += chunkSize(chunk);\n\n return (originalWrite as (...args: unknown[]) => boolean)(chunk, ...rest);\n }) as typeof res.write;\n\n res.end = ((chunk: unknown, ...rest: unknown[]) => {\n firstByteTime ??= performance.now();\n responseSize += chunkSize(chunk);\n\n return (originalEnd as (...args: unknown[]) => ServerResponse)(chunk, ...rest);\n }) as typeof res.end;\n\n let finalized = false;\n\n const finalize = (aborted: boolean): void => {\n if (finalized) return;\n\n finalized = true;\n\n const status = res.statusCode;\n const responseTime = performance.now() - startTime;\n\n if (requestLogger) {\n const level = status >= 500 ? 'error' : status >= 400 ? 'warn' : 'info';\n\n requestLogger[level](\n {\n res: { statusCode: status },\n responseTime: roundTime(responseTime),\n ttfb: roundTime((firstByteTime ?? performance.now()) - startTime),\n responseSize,\n ...(record.route && { route: record.route }),\n ...(aborted && { aborted: true }),\n },\n aborted ? 'request aborted' : 'request completed',\n );\n }\n\n if (span) {\n span.setAttribute('http.response.status_code', status);\n span.setAttribute('http.response.body.size', responseSize);\n\n if (aborted || status >= 400) {\n span.setStatus({ code: SpanStatusCode.ERROR, message: aborted ? 'request aborted' : `HTTP ${status}` });\n } else {\n span.setStatus({ code: SpanStatusCode.OK });\n }\n\n span.end();\n }\n\n if (telemetry) {\n endActiveRequest?.();\n recordHttpRequestDuration({ method, route: record.route, status }, responseTime);\n\n if (record.actionName) {\n recordActionDuration({ name: record.actionName, status }, responseTime);\n }\n }\n };\n\n res.once('finish', () => finalize(false));\n res.once('close', () => finalize(!res.writableFinished));\n\n const run = (): void => store.requestStorage.run(record, inner);\n\n if (span) {\n context.with(trace.setSpan(context.active(), span), run);\n } else {\n run();\n }\n };\n}\n","import { isSpanContextValid, trace } from '@opentelemetry/api';\nimport pino, { type Bindings, type Logger, type LoggerOptions } from 'pino';\nimport { getLogStore } from './store.js';\n\n/**\n * Contract of the `src/log.ts` entry seam: pino logger options, or a factory\n * producing them. Never a logger instance — the platform constructs the\n * logger itself (after instrumentation, so trace correlation works).\n */\nexport type LoggerOptionsFactory = LoggerOptions | ((ctx: { dev: boolean }) => LoggerOptions | Promise<LoggerOptions>);\n\n/**\n * Compose the user mixin (if any) with platform trace correlation: when a\n * span is active, every entry carries `trace_id` / `span_id` / `trace_flags`.\n */\nfunction composeMixin(userMixin: LoggerOptions['mixin']): NonNullable<LoggerOptions['mixin']> {\n return (mergeObject, level, logger) => {\n const user = userMixin ? userMixin(mergeObject, level, logger) : {};\n const spanContext = trace.getActiveSpan()?.spanContext();\n\n if (!spanContext || !isSpanContextValid(spanContext)) return user;\n\n return {\n ...user,\n trace_id: spanContext.traceId,\n span_id: spanContext.spanId,\n trace_flags: `0${spanContext.traceFlags.toString(16)}`,\n };\n };\n}\n\n/**\n * Construct the root logger from the app's options seam and replay any logs\n * buffered before construction (original timestamps kept as `bufferedTime`).\n * In dev this runs once per generation; the buffer only exists the first time.\n */\nexport async function constructRootLogger(\n factory: LoggerOptionsFactory | undefined,\n ctx: { dev: boolean },\n): Promise<Logger> {\n const store = getLogStore();\n const options = (typeof factory === 'function' ? await factory(ctx) : factory) ?? {};\n const root = pino({ level: 'info', ...options, mixin: composeMixin(options.mixin) });\n\n store.root = root;\n\n for (const entry of store.buffer.splice(0)) {\n const bindings = entry.bindings.length ? (Object.assign({}, ...entry.bindings) as Bindings) : {};\n const target = root.child({ ...bindings, bufferedTime: new Date(entry.time).toISOString() });\n\n (target[entry.level] as (...args: unknown[]) => void)(...entry.args);\n }\n\n if (store.dropped > 0) {\n root.warn({ dropped: store.dropped }, 'early log buffer overflowed, entries dropped');\n store.dropped = 0;\n }\n\n return root;\n}\n\n/**\n * Failure path for startups that die before the logger exists: dump the\n * buffered entries to the console so no phase is silent.\n */\nexport function dumpEarlyLogs(): void {\n const store = getLogStore();\n\n if (store.root) return;\n\n for (const entry of store.buffer.splice(0)) {\n const bindings = entry.bindings.length ? Object.assign({}, ...entry.bindings) : undefined;\n\n console.error(\n new Date(entry.time).toISOString(),\n entry.level.toUpperCase(),\n ...(bindings ? [bindings] : []),\n ...entry.args,\n );\n }\n\n if (store.dropped > 0) {\n console.error(`(${store.dropped} early log entries dropped)`);\n store.dropped = 0;\n }\n}\n","import { log } from '../log/index.js';\n\n/**\n * Platform-owned telemetry bundle: NodeSDK with undici (fetch) and node\n * runtime instrumentation, host metrics, and a Prometheus reader. Trace\n * exporters are driven by standard `OTEL_*` env vars; without any of them\n * traces stay off (no failing localhost OTLP exports).\n *\n * Guarded per process (dev restarts are in-process; a re-created NodeSDK\n * would double-register instrumentations and leak the Prometheus port).\n */\n\nconst TELEMETRY_KEY = Symbol.for('@astroscope/node/telemetry');\n\ninterface TelemetryHandle {\n shutdown: () => Promise<void>;\n}\n\nexport interface TelemetrySdkOptions {\n prometheus: { host?: string | undefined; port?: number | undefined } | false;\n}\n\nfunction getHandle(): TelemetryHandle | undefined {\n return (globalThis as Record<symbol, unknown>)[TELEMETRY_KEY] as TelemetryHandle | undefined;\n}\n\nfunction defaultEnv(key: string, value: string): void {\n if (!process.env[key]) process.env[key] = value;\n}\n\nexport async function startTelemetry(options: TelemetrySdkOptions): Promise<void> {\n const g = globalThis as Record<symbol, unknown>;\n\n if (g[TELEMETRY_KEY]) return;\n\n if (process.env['OTEL_SDK_DISABLED'] === 'true') {\n log.debug('telemetry disabled via OTEL_SDK_DISABLED');\n\n return;\n }\n\n // without an explicitly configured exporter target, exporting traces to the\n // default localhost OTLP endpoint would fail on every flush\n if (!process.env['OTEL_EXPORTER_OTLP_ENDPOINT'] && !process.env['OTEL_EXPORTER_OTLP_TRACES_ENDPOINT']) {\n defaultEnv('OTEL_TRACES_EXPORTER', 'none');\n }\n\n defaultEnv('OTEL_METRICS_EXPORTER', 'none');\n defaultEnv('OTEL_LOGS_EXPORTER', 'none');\n\n const [\n { NodeSDK },\n { UndiciInstrumentation },\n { RuntimeNodeInstrumentation },\n { PrometheusExporter },\n { HostMetrics },\n ] = await Promise.all([\n import('@opentelemetry/sdk-node'),\n import('@opentelemetry/instrumentation-undici'),\n import('@opentelemetry/instrumentation-runtime-node'),\n import('@opentelemetry/exporter-prometheus'),\n import('@opentelemetry/host-metrics'),\n ]);\n\n const prometheus = options.prometheus\n ? {\n host: process.env['OTEL_EXPORTER_PROMETHEUS_HOST'] ?? options.prometheus.host ?? '0.0.0.0',\n port: process.env['OTEL_EXPORTER_PROMETHEUS_PORT']\n ? Number(process.env['OTEL_EXPORTER_PROMETHEUS_PORT'])\n : (options.prometheus.port ?? 9464),\n }\n : false;\n\n const sdk = new NodeSDK({\n instrumentations: [new UndiciInstrumentation(), new RuntimeNodeInstrumentation()],\n ...(prometheus && { metricReaders: [new PrometheusExporter(prometheus)] }),\n });\n\n sdk.start();\n\n const hostMetrics = new HostMetrics();\n\n hostMetrics.start();\n\n g[TELEMETRY_KEY] = {\n shutdown: () => sdk.shutdown(),\n } satisfies TelemetryHandle;\n\n if (prometheus) {\n log.debug({ host: prometheus.host, port: prometheus.port }, 'prometheus metrics listening');\n }\n}\n\n/**\n * Flush and shut the SDK down. Prod-only (dev keeps the SDK for the process\n * lifetime across generations).\n */\nexport async function shutdownTelemetry(): Promise<void> {\n const handle = getHandle();\n\n if (!handle) return;\n\n delete (globalThis as Record<symbol, unknown>)[TELEMETRY_KEY];\n\n await handle.shutdown();\n}\n","import fs from 'node:fs';\nimport { log } from '../observability/log/index.js';\n\n/**\n * Platform env loading (position −1, before the config seam):\n * `CONFIG_PATH` → `./.env` → none. Existing process env vars win\n */\nexport function loadEnvFiles(): void {\n const configPath = process.env['CONFIG_PATH'];\n\n if (configPath) {\n process.loadEnvFile(configPath);\n\n log.debug({ path: configPath }, 'loaded env file from CONFIG_PATH');\n\n return;\n }\n\n if (fs.existsSync('.env')) {\n process.loadEnvFile('.env');\n log.debug({ path: '.env' }, 'loaded env file');\n\n return;\n }\n\n log.debug('no env file loaded');\n}\n","import { type LoggerOptionsFactory, constructRootLogger } from '../observability/log/construct.js';\nimport { type TelemetrySdkOptions, startTelemetry } from '../observability/telemetry/sdk.js';\nimport { loadEnvFiles } from './env.js';\n\nconst INSTRUMENTATION_KEY = Symbol.for('@astroscope/node/instrumentation');\n\nexport interface InstrumentationContext {\n dev: boolean;\n}\n\ninterface InstrumentationSeam {\n register?: ((ctx: InstrumentationContext) => void | Promise<void>) | undefined;\n}\n\ninterface LogSeam {\n default?: LoggerOptionsFactory | undefined;\n}\n\nexport interface PlatformSeams {\n /** `src/config.ts` — validation runs at import; a throw fails the startup */\n config?: (() => Promise<unknown>) | undefined;\n /** `src/instrumentation.ts` — extra instrumentation, once per process */\n instrumentation?: (() => Promise<InstrumentationSeam>) | undefined;\n /** `src/log.ts` — pino logger options (or a factory), never an instance */\n log?: (() => Promise<LogSeam>) | undefined;\n}\n\nexport interface PreparePlatformOptions {\n dev: boolean;\n telemetry: TelemetrySdkOptions | false;\n seams: PlatformSeams;\n}\n\n/**\n * The platform sequence in front of the boot lifecycle:\n * env → config → instrumentation (platform SDK + `register`, once per\n * process) → logger construction (after instrumentation, so entries carry\n * trace correlation). Prod runs it once in `startServer()`; dev re-runs it\n * per generation with the once-per-process parts guarded.\n */\nexport async function preparePlatform(options: PreparePlatformOptions): Promise<void> {\n loadEnvFiles();\n\n await options.seams.config?.();\n\n const g = globalThis as Record<symbol, unknown>;\n\n if (!g[INSTRUMENTATION_KEY]) {\n g[INSTRUMENTATION_KEY] = true;\n\n if (options.telemetry) {\n await startTelemetry(options.telemetry);\n }\n\n const instrumentation = await options.seams.instrumentation?.();\n\n await instrumentation?.register?.({ dev: options.dev });\n }\n\n const logSeam = await options.seams.log?.();\n\n await constructRootLogger(logSeam?.default, { dev: options.dev });\n}\n"],"mappings":";;;;;;;;AAQA,eAAsB,WAAW,MAAkB,SAAqC;CACtF,MAAM,KAAK,mBAAmB,OAAO;CACrC,MAAM,KAAK,YAAY,OAAO;CAC9B,MAAM,KAAK,kBAAkB,OAAO;AACtC;AAEA,eAAsB,YAAY,MAAkB,SAAqC;CACvF,IAAI;EACF,MAAM,KAAK,oBAAoB,OAAO;EACtC,MAAM,KAAK,aAAa,OAAO;CACjC,UAAU;EACR,MAAM,KAAK,mBAAmB,OAAO;CACvC;AACF;;;ACnBA,MAAMA,aAAW;AAGjB,IAAI,sBAAwC;AAC5C,IAAI,qBAA2C;AAC/C,IAAI,iBAAmC;AAEvC,SAAS,yBAAoC;CAC3C,OAAQ,wBAAwB,QAAQ,SAASA,UAAQ,CAAC,CAAC,gBAAgB,gCAAgC;EACzG,aAAa;EACb,MAAM;EACN,WAAW,UAAU;CACvB,CAAC;AACH;AAEA,SAAS,wBAAuC;CAC9C,OAAQ,uBAAuB,QAAQ,SAASA,UAAQ,CAAC,CAAC,oBAAoB,+BAA+B;EAC3G,aAAa;EACb,MAAM;EACN,WAAW,UAAU;CACvB,CAAC;AACH;AAEA,SAAS,oBAA+B;CACtC,OAAQ,mBAAmB,QAAQ,SAASA,UAAQ,CAAC,CAAC,gBAAgB,yBAAyB;EAC7F,aAAa;EACb,MAAM;EACN,WAAW,UAAU;CACvB,CAAC;AACH;;;;;;AAOA,SAAgB,uBAAuB,QAA4B;CACjE,sBAAsB,CAAC,CAAC,IAAI,GAAG,EAAE,uBAAuB,OAAO,CAAC;CAEhE,aAAa;EACX,sBAAsB,CAAC,CAAC,IAAI,IAAI,EAAE,uBAAuB,OAAO,CAAC;CACnE;AACF;AAEA,SAAgB,0BACd,YACA,YACM;CACN,uBAAuB,CAAC,CAAC,OAAO,aAAa,KAAM;EACjD,uBAAuB,WAAW;EAClC,cAAc,WAAW,SAAS;EAClC,6BAA6B,WAAW;CAC1C,CAAC;AACH;AAEA,SAAgB,qBAAqB,YAA8C,YAA0B;CAC3G,kBAAkB,CAAC,CAAC,OAAO,aAAa,KAAM;EAC5C,qBAAqB,WAAW;EAChC,6BAA6B,WAAW;CAC1C,CAAC;AACH;;;ACrDA,MAAM,WAAW;AACjB,MAAM,iBAAiB;AACvB,MAAM,qBAAqB;AAE3B,MAAM,aAAa,MAAc,KAAK,MAAM,IAAI,GAAG,IAAI;AAgBvD,SAAS,YAAY,KAA0C;CAC7D,MAAM,YAAY,IAAI,QAAQ;CAG9B,QAFc,MAAM,QAAQ,SAAS,IAAI,UAAU,KAAK,UAAA,EAG/C,MAAM,GAAG,CAAC,CAAC,EAAE,EAAE,KAAK,KAC1B,IAAI,QAAQ,gBACZ,IAAI,QAAQ;AAEjB;AAEA,SAAS,aAAa,KAA8B;CAClD,MAAM,WAAW,IAAI,QAAQ;CAC7B,MAAM,QAAQ,MAAM,QAAQ,QAAQ,IAAI,SAAS,KAAK;CAEtD,OAAO,SAAS,mBAAmB,KAAK,KAAK,IAAI,QAAQ,cAAc;AACzE;AAEA,SAAS,UAAU,OAAwB;CACzC,IAAI,SAAS,MAAM,OAAO;CAC1B,IAAI,YAAY,OAAO,KAAK,GAAG,OAAO,MAAM;CAC5C,IAAI,OAAO,UAAU,UAAU,OAAO,OAAO,WAAW,KAAK;CAE7D,OAAO;AACT;;;;;;;;AASA,SAAgB,6BAA6B,QAAsC;CACjF,MAAM,SAAS,MAAM,UAAU,QAAQ;CACvC,MAAM,QAAQ,YAAY;CAC1B,MAAM,kBAAkB,OAAO,UAAU,cAAc,OAAO,QAAQ,OAAO,UAAU;CACvF,MAAM,oBAAoB,OAAO,YAAY,cAAc,OAAO,UAAU,OAAO,UAAU;CAE7F,QAAQ,KAAsB,KAAqB,UAA4B;EAC7E,MAAM,MAAM,IAAI,OAAO;EACvB,MAAM,aAAa,IAAI,QAAQ,GAAG;EAClC,MAAM,WAAW,eAAe,KAAK,MAAM,IAAI,MAAM,GAAG,UAAU;EAClE,MAAM,SAAS,IAAI,UAAU;EAE7B,MAAM,UAAU,OAAO,WAAW,CAAC,gBAAgB,QAAQ,IAAI,OAAO,UAAU;EAChF,MAAM,YAAY,OAAO,aAAa,CAAC,kBAAkB,QAAQ,IAAI,OAAO,YAAY;EAExF,IAAI,CAAC,WAAW,CAAC,WAAW;GAC1B,MAAM;GAEN;EACF;EAEA,MAAM,YAAY,YAAY,IAAI;EAClC,MAAM,WAAW,SAAS,WAAW,cAAc;EAEnD,IAAI;EAEJ,IAAI,WAAW,MAAM,MAAM;GACzB,MAAM,QAAQ,aAAa,GAAG;GAC9B,MAAM,UAAmC;IAAE;IAAQ,KAAK;GAAS;GAGjE,IAAI,QAAQ,UAAU;IACpB,QAAQ,WAAW,eAAe,KAAK,KAAK,IAAI,MAAM,aAAa,CAAC;IACpE,QAAQ,aAAa,IAAI;IACzB,QAAQ,mBAAmB,YAAY,GAAG,KAAK,IAAI,OAAO;GAC5D;GAEA,gBAAgB,MAAM,KAAK,MAAM;IAAE;IAAO,KAAK;GAAQ,CAAC;GAExD,IAAI,UAAU,gBAAgB,KAAK;EACrC;EAEA,MAAM,SAAwB;GAC5B,QAAQ;GACR;GACA,OAAO,KAAA;GACP,YAAY,WAAW,SAAS,MAAM,EAAqB,CAAC,CAAC,QAAQ,OAAO,EAAE,IAAI,KAAA;EACpF;EAEA,IAAI;EACJ,IAAI;EAEJ,IAAI,WAAW;GACb,MAAM,gBAAgB,YAAY,QAAQ,QAAQ,OAAO,GAAG,IAAI,OAAO;GACvE,MAAM,gBAAgB,IAAI,QAAQ;GAClC,MAAM,WAAW,YAAY,GAAG;GAChC,MAAM,OAAO,IAAI,QAAQ;GAEzB,OAAO,OAAO,UACZ,WAAW,UAAU,OAAO,eAAe,QAC3C;IACE,MAAM,SAAS;IACf,YAAY;KACV,uBAAuB;KACvB,YAAY;KACZ,aAAa,eAAe,KAAK,KAAK,IAAI,MAAM,aAAa,CAAC;KAC9D,cAAc;KACd,uBAAuB,IAAI,QAAQ,iBAAiB;KACpD,GAAI,QAAQ,EAAE,kBAAkB,KAAK;KACrC,GAAI,iBAAiB,EAAE,0BAA0B,SAAS,aAAa,EAAE;KACzE,GAAI,YAAY,EAAE,kBAAkB,SAAS;IAC/C;GACF,GACA,aACF;GAEA,mBAAmB,uBAAuB,MAAM;EAClD;EAEA,IAAI,eAAe;EACnB,IAAI;EAEJ,MAAM,gBAAgB,IAAI,MAAM,KAAK,GAAG;EACxC,MAAM,cAAc,IAAI,IAAI,KAAK,GAAG;EAEpC,IAAI,UAAU,OAAgB,GAAG,SAAoB;GACnD,kBAAkB,YAAY,IAAI;GAClC,gBAAgB,UAAU,KAAK;GAE/B,OAAQ,cAAkD,OAAO,GAAG,IAAI;EAC1E;EAEA,IAAI,QAAQ,OAAgB,GAAG,SAAoB;GACjD,kBAAkB,YAAY,IAAI;GAClC,gBAAgB,UAAU,KAAK;GAE/B,OAAQ,YAAuD,OAAO,GAAG,IAAI;EAC/E;EAEA,IAAI,YAAY;EAEhB,MAAM,YAAY,YAA2B;GAC3C,IAAI,WAAW;GAEf,YAAY;GAEZ,MAAM,SAAS,IAAI;GACnB,MAAM,eAAe,YAAY,IAAI,IAAI;GAEzC,IAAI,eAGF,cAFc,UAAU,MAAM,UAAU,UAAU,MAAM,SAAS,OAE7C,CAClB;IACE,KAAK,EAAE,YAAY,OAAO;IAC1B,cAAc,UAAU,YAAY;IACpC,MAAM,WAAW,iBAAiB,YAAY,IAAI,KAAK,SAAS;IAChE;IACA,GAAI,OAAO,SAAS,EAAE,OAAO,OAAO,MAAM;IAC1C,GAAI,WAAW,EAAE,SAAS,KAAK;GACjC,GACA,UAAU,oBAAoB,mBAChC;GAGF,IAAI,MAAM;IACR,KAAK,aAAa,6BAA6B,MAAM;IACrD,KAAK,aAAa,2BAA2B,YAAY;IAEzD,IAAI,WAAW,UAAU,KACvB,KAAK,UAAU;KAAE,MAAM,eAAe;KAAO,SAAS,UAAU,oBAAoB,QAAQ;IAAS,CAAC;SAEtG,KAAK,UAAU,EAAE,MAAM,eAAe,GAAG,CAAC;IAG5C,KAAK,IAAI;GACX;GAEA,IAAI,WAAW;IACb,mBAAmB;IACnB,0BAA0B;KAAE;KAAQ,OAAO,OAAO;KAAO;IAAO,GAAG,YAAY;IAE/E,IAAI,OAAO,YACT,qBAAqB;KAAE,MAAM,OAAO;KAAY;IAAO,GAAG,YAAY;GAE1E;EACF;EAEA,IAAI,KAAK,gBAAgB,SAAS,KAAK,CAAC;EACxC,IAAI,KAAK,eAAe,SAAS,CAAC,IAAI,gBAAgB,CAAC;EAEvD,MAAM,YAAkB,MAAM,eAAe,IAAI,QAAQ,KAAK;EAE9D,IAAI,MACF,QAAQ,KAAK,MAAM,QAAQ,QAAQ,OAAO,GAAG,IAAI,GAAG,GAAG;OAEvD,IAAI;CAER;AACF;;;;;;;AC9MA,SAAS,aAAa,WAAwE;CAC5F,QAAQ,aAAa,OAAO,WAAW;EACrC,MAAM,OAAO,YAAY,UAAU,aAAa,OAAO,MAAM,IAAI,CAAC;EAClE,MAAM,cAAc,MAAM,cAAc,CAAC,EAAE,YAAY;EAEvD,IAAI,CAAC,eAAe,CAAC,mBAAmB,WAAW,GAAG,OAAO;EAE7D,OAAO;GACL,GAAG;GACH,UAAU,YAAY;GACtB,SAAS,YAAY;GACrB,aAAa,IAAI,YAAY,WAAW,SAAS,EAAE;EACrD;CACF;AACF;;;;;;AAOA,eAAsB,oBACpB,SACA,KACiB;CACjB,MAAM,QAAQ,YAAY;CAC1B,MAAM,WAAW,OAAO,YAAY,aAAa,MAAM,QAAQ,GAAG,IAAI,YAAY,CAAC;CACnF,MAAM,OAAO,KAAK;EAAE,OAAO;EAAQ,GAAG;EAAS,OAAO,aAAa,QAAQ,KAAK;CAAE,CAAC;CAEnF,MAAM,OAAO;CAEb,KAAK,MAAM,SAAS,MAAM,OAAO,OAAO,CAAC,GAAG;EAC1C,MAAM,WAAW,MAAM,SAAS,SAAU,OAAO,OAAO,CAAC,GAAG,GAAG,MAAM,QAAQ,IAAiB,CAAC;EAG/F,KAFoB,MAAM;GAAE,GAAG;GAAU,cAAc,IAAI,KAAK,MAAM,IAAI,CAAC,CAAC,YAAY;EAAE,CAEpF,CAAC,CAAC,MAAM,MAAM,CAAkC,GAAG,MAAM,IAAI;CACrE;CAEA,IAAI,MAAM,UAAU,GAAG;EACrB,KAAK,KAAK,EAAE,SAAS,MAAM,QAAQ,GAAG,8CAA8C;EACpF,MAAM,UAAU;CAClB;CAEA,OAAO;AACT;;;;;AAMA,SAAgB,gBAAsB;CACpC,MAAM,QAAQ,YAAY;CAE1B,IAAI,MAAM,MAAM;CAEhB,KAAK,MAAM,SAAS,MAAM,OAAO,OAAO,CAAC,GAAG;EAC1C,MAAM,WAAW,MAAM,SAAS,SAAS,OAAO,OAAO,CAAC,GAAG,GAAG,MAAM,QAAQ,IAAI,KAAA;EAEhF,QAAQ,MACN,IAAI,KAAK,MAAM,IAAI,CAAC,CAAC,YAAY,GACjC,MAAM,MAAM,YAAY,GACxB,GAAI,WAAW,CAAC,QAAQ,IAAI,CAAC,GAC7B,GAAG,MAAM,IACX;CACF;CAEA,IAAI,MAAM,UAAU,GAAG;EACrB,QAAQ,MAAM,IAAI,MAAM,QAAQ,4BAA4B;EAC5D,MAAM,UAAU;CAClB;AACF;;;;;;;;;;;;ACzEA,MAAM,gBAAgB,OAAO,IAAI,4BAA4B;AAU7D,SAAS,YAAyC;CAChD,OAAQ,WAAuC;AACjD;AAEA,SAAS,WAAW,KAAa,OAAqB;CACpD,IAAI,CAAC,QAAQ,IAAI,MAAM,QAAQ,IAAI,OAAO;AAC5C;AAEA,eAAsB,eAAe,SAA6C;CAChF,MAAM,IAAI;CAEV,IAAI,EAAE,gBAAgB;CAEtB,IAAI,QAAQ,IAAI,yBAAyB,QAAQ;EAC/C,IAAI,MAAM,0CAA0C;EAEpD;CACF;CAIA,IAAI,CAAC,QAAQ,IAAI,kCAAkC,CAAC,QAAQ,IAAI,uCAC9D,WAAW,wBAAwB,MAAM;CAG3C,WAAW,yBAAyB,MAAM;CAC1C,WAAW,sBAAsB,MAAM;CAEvC,MAAM,CACJ,EAAE,WACF,EAAE,yBACF,EAAE,8BACF,EAAE,sBACF,EAAE,iBACA,MAAM,QAAQ,IAAI;EACpB,OAAO;EACP,OAAO;EACP,OAAO;EACP,OAAO;EACP,OAAO;CACT,CAAC;CAED,MAAM,aAAa,QAAQ,aACvB;EACE,MAAM,QAAQ,IAAI,oCAAoC,QAAQ,WAAW,QAAQ;EACjF,MAAM,QAAQ,IAAI,mCACd,OAAO,QAAQ,IAAI,gCAAgC,IAClD,QAAQ,WAAW,QAAQ;CAClC,IACA;CAEJ,MAAM,MAAM,IAAI,QAAQ;EACtB,kBAAkB,CAAC,IAAI,sBAAsB,GAAG,IAAI,2BAA2B,CAAC;EAChF,GAAI,cAAc,EAAE,eAAe,CAAC,IAAI,mBAAmB,UAAU,CAAC,EAAE;CAC1E,CAAC;CAED,IAAI,MAAM;CAIV,IAFwB,YAEd,CAAC,CAAC,MAAM;CAElB,EAAE,iBAAiB,EACjB,gBAAgB,IAAI,SAAS,EAC/B;CAEA,IAAI,YACF,IAAI,MAAM;EAAE,MAAM,WAAW;EAAM,MAAM,WAAW;CAAK,GAAG,8BAA8B;AAE9F;;;;;AAMA,eAAsB,oBAAmC;CACvD,MAAM,SAAS,UAAU;CAEzB,IAAI,CAAC,QAAQ;CAEb,OAAQ,WAAuC;CAE/C,MAAM,OAAO,SAAS;AACxB;;;;;;;AClGA,SAAgB,eAAqB;CACnC,MAAM,aAAa,QAAQ,IAAI;CAE/B,IAAI,YAAY;EACd,QAAQ,YAAY,UAAU;EAE9B,IAAI,MAAM,EAAE,MAAM,WAAW,GAAG,kCAAkC;EAElE;CACF;CAEA,IAAI,GAAG,WAAW,MAAM,GAAG;EACzB,QAAQ,YAAY,MAAM;EAC1B,IAAI,MAAM,EAAE,MAAM,OAAO,GAAG,iBAAiB;EAE7C;CACF;CAEA,IAAI,MAAM,oBAAoB;AAChC;;;ACtBA,MAAM,sBAAsB,OAAO,IAAI,kCAAkC;;;;;;;;AAoCzE,eAAsB,gBAAgB,SAAgD;CACpF,aAAa;CAEb,MAAM,QAAQ,MAAM,SAAS;CAE7B,MAAM,IAAI;CAEV,IAAI,CAAC,EAAE,sBAAsB;EAC3B,EAAE,uBAAuB;EAEzB,IAAI,QAAQ,WACV,MAAM,eAAe,QAAQ,SAAS;EAKxC,OAAM,MAFwB,QAAQ,MAAM,kBAAkB,EAAA,EAEvC,WAAW,EAAE,KAAK,QAAQ,IAAI,CAAC;CACxD;CAIA,MAAM,qBAAoB,MAFJ,QAAQ,MAAM,MAAM,EAAA,EAEP,SAAS,EAAE,KAAK,QAAQ,IAAI,CAAC;AAClE"}