From 29a20ca7c71b7c3f4e93200294e4943050ff8c66 Mon Sep 17 00:00:00 2001 From: Volodymyr Vreshch Date: Mon, 21 Sep 2026 01:26:26 +0200 Subject: [PATCH] fix: keep the root barrel edge-safe, resolve node:http at call time --- package-lock.json | 4 +-- package.json | 2 +- src/serve-health.ts | 5 +++- test/unit/edge-safe-barrel.test.ts | 44 ++++++++++++++++++++++++++++++ 4 files changed, 51 insertions(+), 4 deletions(-) create mode 100644 test/unit/edge-safe-barrel.test.ts diff --git a/package-lock.json b/package-lock.json index 4d2d021..ff61989 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@agentage/observability", - "version": "1.2.0", + "version": "1.2.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@agentage/observability", - "version": "1.2.0", + "version": "1.2.1", "license": "MIT", "dependencies": { "@opentelemetry/exporter-trace-otlp-http": "^0.222.0", diff --git a/package.json b/package.json index ed698fa..f5270b0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@agentage/observability", - "version": "1.2.0", + "version": "1.2.1", "description": "Shared observability kit for agentage services: OTLP trace bootstrap (node --import), pino logger preset with trace correlation, one-call error capture, and the estate /health envelope.", "type": "module", "license": "MIT", diff --git a/src/serve-health.ts b/src/serve-health.ts index 32ed8fa..1c199b1 100644 --- a/src/serve-health.ts +++ b/src/serve-health.ts @@ -1,4 +1,4 @@ -import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http'; +import type { IncomingMessage, Server, ServerResponse } from 'node:http'; import { healthResponse, type ChecksInput, type HealthSourceOptions } from './health.js'; /** The two paths the estate probes: the container HEALTHCHECK and the edge-routed one. */ @@ -59,6 +59,9 @@ export function serveHealth( send(res, response, await response.text(), method === 'HEAD'); }; + // Resolved at call time, not import time: the root barrel must stay loadable on the + // Next edge runtime, where `node:http` is absent (instrumentation.ts is bundled there). + const { createServer } = process.getBuiltinModule('node:http'); const server = createServer((req, res) => { // A throwing check must answer 500, not kill the worker it is reporting on. void handle(req, res).catch(() => { diff --git a/test/unit/edge-safe-barrel.test.ts b/test/unit/edge-safe-barrel.test.ts new file mode 100644 index 0000000..5315538 --- /dev/null +++ b/test/unit/edge-safe-barrel.test.ts @@ -0,0 +1,44 @@ +import { readFileSync } from 'node:fs'; +import { dirname, join, normalize } from 'node:path'; +import { describe, expect, it } from 'vitest'; + +// Next bundles instrumentation.ts for the edge runtime, where only a few node: builtins +// exist. Anything the root barrel reaches must therefore load without the rest of them. +const EDGE_BUILTINS = new Set(['node:async_hooks', 'node:events']); +const SRC = join(__dirname, '..', '..', 'src'); + +const staticImports = (file: string): string[] => + [ + ...readFileSync(file, 'utf8').matchAll( + /^(?:import|export)\s+(?!type\s)[^'"]*from\s+['"]([^'"]+)['"]/gm + ), + ].map((m) => m[1]); + +const reachable = (entry: string): Map => { + const seen = new Map(); + const queue = [entry]; + while (queue.length > 0) { + const file = queue.shift()!; + if (seen.has(file)) continue; + const specs = staticImports(file); + seen.set(file, specs); + for (const spec of specs) { + if (spec.startsWith('.')) + queue.push(normalize(join(dirname(file), spec.replace(/\.js$/, '.ts')))); + } + } + return seen; +}; + +describe('root barrel is edge-safe', () => { + it('reaches no node: builtin the Next edge runtime lacks', () => { + const offenders: string[] = []; + for (const [file, specs] of reachable(join(SRC, 'index.ts'))) { + for (const spec of specs) { + if (spec.startsWith('node:') && !EDGE_BUILTINS.has(spec)) + offenders.push(`${file} -> ${spec}`); + } + } + expect(offenders).toEqual([]); + }); +});