theokit 0.72.1 → 0.73.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/dist/{actions-virtual-module-EAKYEMGF.js → actions-virtual-module-QCDKQ6QD.js} +4 -4
- package/dist/adapters/security-headers.d.ts +27 -11
- package/dist/adapters/security-headers.js.map +1 -1
- package/dist/{agent-VJTZ4DQ7.js → agent-OWJ5A7W5.js} +2 -2
- package/dist/{app-typed-client-LHH77QDB.js → app-typed-client-2PTK2SJH.js} +4 -4
- package/dist/{aws-lambda-7AZNJGWU.js → aws-lambda-OP2L5RKZ.js} +3 -3
- package/dist/{build-23QF5FNQ.js → build-CVP2XU5M.js} +5 -5
- package/dist/{bun-NKVHOFSJ.js → bun-CZXHPK2W.js} +4 -4
- package/dist/{chunk-V5SSTT3C.js → chunk-C43AHOHI.js} +1 -1
- package/dist/chunk-C43AHOHI.js.map +1 -0
- package/dist/{chunk-XLSJWKSP.js → chunk-D3M7LPHY.js} +12 -6
- package/dist/{chunk-XLSJWKSP.js.map → chunk-D3M7LPHY.js.map} +1 -1
- package/dist/{chunk-E2RC367T.js → chunk-EDBQM6RB.js} +3 -3
- package/dist/{chunk-QD5X56I3.js → chunk-FNJVSFEC.js} +1 -1
- package/dist/chunk-FNJVSFEC.js.map +1 -0
- package/dist/{chunk-DKX7LF5Q.js → chunk-HDAILYVM.js} +2 -2
- package/dist/{chunk-RE2WE7CD.js → chunk-LOOOB46A.js} +2 -2
- package/dist/{chunk-L6UHMWXU.js → chunk-M3DT6EMK.js} +3 -3
- package/dist/chunk-M3DT6EMK.js.map +1 -0
- package/dist/{chunk-PEL3MJLM.js → chunk-MX63AWEK.js} +1 -1
- package/dist/{chunk-PEL3MJLM.js.map → chunk-MX63AWEK.js.map} +1 -1
- package/dist/{chunk-IB5VIXLR.js → chunk-R36YNWBU.js} +2 -2
- package/dist/{chunk-C5CAL3DX.js → chunk-SEGD3JUC.js} +1 -1
- package/dist/chunk-SEGD3JUC.js.map +1 -0
- package/dist/{chunk-4EQEJ4PH.js → chunk-WRHT5NAJ.js} +20 -14
- package/dist/{chunk-4EQEJ4PH.js.map → chunk-WRHT5NAJ.js.map} +1 -1
- package/dist/cli/index.js +6 -6
- package/dist/client/index.d.ts +18 -1
- package/dist/client/index.js +15 -1
- package/dist/client/index.js.map +1 -1
- package/dist/{cloudflare-4M4XVNUH.js → cloudflare-2SVRRVV5.js} +3 -3
- package/dist/{deno-deploy-IVACZ62D.js → deno-deploy-OK4QFJIZ.js} +4 -4
- package/dist/{dev-JDJWMRLH.js → dev-B2IZEVAD.js} +6 -6
- package/dist/index.js +1 -1
- package/dist/{internal-api-VDYOXOT7.js → internal-api-5Y3MLZH4.js} +4 -4
- package/dist/{mcp-WJNCTM7M.js → mcp-GJQIREYC.js} +2 -2
- package/dist/{netlify-3HEWV375.js → netlify-IAC53FVZ.js} +3 -3
- package/dist/{observability-bootstrap-FP6LMMJE.js → observability-bootstrap-DOWRMADT.js} +2 -2
- package/dist/{preview-BARW75PN.js → preview-XBP622HM.js} +3 -3
- package/dist/{registry-SDBODRU6.js → registry-L6JG75LY.js} +7 -7
- package/dist/{server-boundary-AO6EELRF.js → server-boundary-IFIYNGTA.js} +4 -4
- package/dist/{start-4JTYXQQJ.js → start-JGNBTTG2.js} +7 -7
- package/dist/{vercel-5PMYEEKF.js → vercel-EHAOKPF7.js} +3 -3
- package/dist/vite-plugin/index.js +1 -1
- package/dist/{vite-plugin-3ON45MQE.js → vite-plugin-54XIRMKT.js} +6 -6
- package/package.json +2 -2
- package/dist/chunk-C5CAL3DX.js.map +0 -1
- package/dist/chunk-L6UHMWXU.js.map +0 -1
- package/dist/chunk-QD5X56I3.js.map +0 -1
- package/dist/chunk-V5SSTT3C.js.map +0 -1
- /package/dist/{actions-virtual-module-EAKYEMGF.js.map → actions-virtual-module-QCDKQ6QD.js.map} +0 -0
- /package/dist/{agent-VJTZ4DQ7.js.map → agent-OWJ5A7W5.js.map} +0 -0
- /package/dist/{app-typed-client-LHH77QDB.js.map → app-typed-client-2PTK2SJH.js.map} +0 -0
- /package/dist/{aws-lambda-7AZNJGWU.js.map → aws-lambda-OP2L5RKZ.js.map} +0 -0
- /package/dist/{build-23QF5FNQ.js.map → build-CVP2XU5M.js.map} +0 -0
- /package/dist/{bun-NKVHOFSJ.js.map → bun-CZXHPK2W.js.map} +0 -0
- /package/dist/{chunk-E2RC367T.js.map → chunk-EDBQM6RB.js.map} +0 -0
- /package/dist/{chunk-DKX7LF5Q.js.map → chunk-HDAILYVM.js.map} +0 -0
- /package/dist/{chunk-RE2WE7CD.js.map → chunk-LOOOB46A.js.map} +0 -0
- /package/dist/{chunk-IB5VIXLR.js.map → chunk-R36YNWBU.js.map} +0 -0
- /package/dist/{cloudflare-4M4XVNUH.js.map → cloudflare-2SVRRVV5.js.map} +0 -0
- /package/dist/{deno-deploy-IVACZ62D.js.map → deno-deploy-OK4QFJIZ.js.map} +0 -0
- /package/dist/{dev-JDJWMRLH.js.map → dev-B2IZEVAD.js.map} +0 -0
- /package/dist/{internal-api-VDYOXOT7.js.map → internal-api-5Y3MLZH4.js.map} +0 -0
- /package/dist/{mcp-WJNCTM7M.js.map → mcp-GJQIREYC.js.map} +0 -0
- /package/dist/{netlify-3HEWV375.js.map → netlify-IAC53FVZ.js.map} +0 -0
- /package/dist/{observability-bootstrap-FP6LMMJE.js.map → observability-bootstrap-DOWRMADT.js.map} +0 -0
- /package/dist/{preview-BARW75PN.js.map → preview-XBP622HM.js.map} +0 -0
- /package/dist/{registry-SDBODRU6.js.map → registry-L6JG75LY.js.map} +0 -0
- /package/dist/{server-boundary-AO6EELRF.js.map → server-boundary-IFIYNGTA.js.map} +0 -0
- /package/dist/{start-4JTYXQQJ.js.map → start-JGNBTTG2.js.map} +0 -0
- /package/dist/{vercel-5PMYEEKF.js.map → vercel-EHAOKPF7.js.map} +0 -0
- /package/dist/{vite-plugin-3ON45MQE.js.map → vite-plugin-54XIRMKT.js.map} +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/server/observability/trace-context-propagation.ts","../src/server/observability/span.ts","../src/server/observability/adapters/console.ts","../src/server/observability/adapters/noop.ts","../src/server/observability/otlp-serializer.ts","../src/server/observability/adapters/theo-cloud.ts","../src/server/observability/adapter-registry.ts","../src/server/http/trace-context.ts","../src/server/observability/request-trace.ts","../src/server/observability/middleware.ts","../src/server/observability-bootstrap.ts"],"sourcesContent":["// T5a.1a — Web Standards migration (leaf-first slice). Web Crypto's\n// `crypto.getRandomValues()` is available on globalThis in every supported\n// runtime per ADR-0028 (Node 22+, CF Workers, Bun, Deno, browsers). No\n// node:crypto import needed — Web Crypto returns a typed array filled with\n// CSPRNG bytes, which we hex-encode locally instead of relying on Buffer.\n\n/**\n * W3C Trace Context propagation helpers for non-HTTP carriers (job\n * leases, webhook reply headers, agent SSE frames). Works against any\n * `Headers`-shaped carrier — Web Standards only.\n *\n * The HTTP-side extractor `extractTraceId` in `../http/trace-context.ts`\n * is request-scoped and returns only the trace_id string. This module\n * exposes the full `TraceContext` shape (trace_id + span_id + flags)\n * because downstream jobs/webhooks need to propagate a NEW span_id\n * (child span) while keeping the same trace_id.\n *\n * Format spec (W3C Trace Context Level 2):\n * traceparent: version-trace_id-parent_id-trace_flags\n * - version = '00' (current)\n * - trace_id = 32 hex chars (128-bit)\n * - parent_id = 16 hex chars (64-bit) — \"span_id\" of the producer\n * - trace_flags = 2 hex chars (sampled bit)\n *\n * @see https://www.w3.org/TR/trace-context/\n */\n\nexport interface TraceContext {\n /** 32 hex chars. NEVER the reserved all-zeros value. */\n readonly trace_id: string\n /** 16 hex chars. NEVER the reserved all-zeros value. */\n readonly span_id: string\n /** 2 hex chars. Sampled bit + reserved. */\n readonly flags: string\n}\n\nconst TRACEPARENT_RE = /^00-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/\nconst ALL_ZEROS_TRACE = '00000000000000000000000000000000'\nconst ALL_ZEROS_SPAN = '0000000000000000'\n\n/**\n * Extract a TraceContext from a Web Headers object. Returns `null` for:\n * - missing header\n * - malformed (not matching the version-trace-span-flags regex)\n * - reserved all-zeros trace_id (W3C-invalid)\n * - reserved all-zeros span_id (W3C-invalid)\n *\n * NEVER throws — defensive read.\n */\nexport function extractTraceContext(headers: Headers): TraceContext | null {\n const value = headers.get('traceparent')\n if (!value) return null\n const m = TRACEPARENT_RE.exec(value)\n if (!m) return null\n const trace_id = m[1]\n const span_id = m[2]\n const flags = m[3]\n if (trace_id === ALL_ZEROS_TRACE) return null\n if (span_id === ALL_ZEROS_SPAN) return null\n return { trace_id, span_id, flags }\n}\n\n/**\n * Write a canonical traceparent into a Web Headers object. Always\n * succeeds (defensive write — the caller is responsible for passing\n * a valid TraceContext; passing malformed input is a programming bug).\n */\nexport function injectTraceContext(headers: Headers, ctx: TraceContext): void {\n headers.set('traceparent', `00-${ctx.trace_id}-${ctx.span_id}-${ctx.flags}`)\n}\n\n/**\n * Generate a fresh TraceContext (new trace_id + span_id, flags='01' for\n * sampled). Used when no upstream traceparent exists — e.g., when a\n * cron fires or a webhook arrives without an upstream tracer.\n */\nexport function generateNewTraceContext(): TraceContext {\n return {\n trace_id: newTraceId(),\n span_id: newSpanId(),\n flags: '01',\n }\n}\n\n/**\n * A fresh 128-bit trace id, 32 hex chars, never the reserved all-zeros.\n *\n * Exported next to its span-id sibling because span identity needs the two\n * halves separately: a child span mints only an id and inherits the trace, and\n * a caller that had to mint a whole `TraceContext` to get one span id would be\n * discarding a trace id it never used. `span.ts` is the consumer.\n */\nexport function newTraceId(): string {\n return randomHex(16) // 16 bytes = 32 hex chars\n}\n\n/** A fresh 64-bit span id, 16 hex chars, never the reserved all-zeros. */\nexport function newSpanId(): string {\n return randomHex(8) // 8 bytes = 16 hex chars\n}\n\nfunction randomHex(bytes: number): string {\n // crypto.getRandomValues is overwhelmingly unlikely to produce all-zeros,\n // but we guard anyway — the W3C spec rejects all-zeros and tests assert\n // this. Web Crypto API returns a Uint8Array; we hex-encode without Buffer\n // to stay runtime-agnostic (CF Workers / Bun / Deno have no Buffer global).\n for (;;) {\n const buf = new Uint8Array(bytes)\n globalThis.crypto.getRandomValues(buf)\n let hex = ''\n for (const b of buf) hex += b.toString(16).padStart(2, '0')\n if (!/^0+$/.test(hex)) return hex\n }\n}\n","/**\n * SpanHandle implementation — records timing + attributes.\n *\n * Used by console and theo-cloud adapters. Noop adapter uses NoopSpan.\n */\nimport type { SpanHandle, SpanAttributes, SpanContextInput } from './adapters/types.js'\nimport { newSpanId, newTraceId } from './trace-context-propagation.js'\n\nexport interface SpanData {\n name: string\n /**\n * The trace this span belongs to, and the span's own id within it.\n *\n * These used to not exist, and the OTLP serializer minted a `traceId` per span\n * at export time. Every export was well-formed and every span was an island: a\n * five-span agent run reached the collector as five unrelated single-span\n * traces, so \"read the run back from an exported trace\" had nothing to read\n * (usetheokit/theokit#368). Identity belongs to the span, decided when it\n * starts, not to the exporter, guessed when it leaves.\n */\n traceId: string\n spanId: string\n /** Absent on the root span of a trace. */\n parentSpanId?: string\n attributes: Record<string, string | number | boolean>\n status: 'ok' | 'error'\n statusMessage?: string\n startTimeMs: number\n endTimeMs?: number\n durationMs?: number\n}\n\nexport class SpanImpl implements SpanHandle {\n private readonly data: SpanData\n private ended = false\n\n constructor(name: string, attributes?: SpanAttributes, context?: SpanContextInput) {\n this.data = {\n name,\n traceId: context?.traceId ?? newTraceId(),\n spanId: context?.spanId ?? newSpanId(),\n attributes: {},\n status: 'ok',\n startTimeMs: Date.now(),\n }\n if (context?.parentSpanId !== undefined) this.data.parentSpanId = context.parentSpanId\n if (attributes) {\n for (const [k, v] of Object.entries(attributes)) {\n if (v !== undefined) this.data.attributes[k] = v\n }\n }\n }\n\n setAttribute(key: string, value: string | number | boolean): void {\n if (!this.ended) this.data.attributes[key] = value\n }\n\n setStatus(status: 'ok' | 'error', message?: string): void {\n if (!this.ended) {\n this.data.status = status\n this.data.statusMessage = message\n }\n }\n\n end(): void {\n if (this.ended) return // idempotent\n this.ended = true\n this.data.endTimeMs = Date.now()\n this.data.durationMs = this.data.endTimeMs - this.data.startTimeMs\n }\n\n /** Read-only access to span data (for adapters to export). */\n getData(): SpanData {\n return { ...this.data, attributes: { ...this.data.attributes } }\n }\n\n isEnded(): boolean {\n return this.ended\n }\n}\n\n/**\n * Noop span — used by NoopAdapter and post-shutdown fallback (EC-2).\n *\n * The three empty bodies are the Null Object, not forgetfulness (agent-builder#319): **doing\n * nothing** is the contracted behaviour. Filling them with a `void 0` or a log just to silence the\n * lint would trade a readable intention for noise — and a log here would run on the post-shutdown\n * path, which is precisely where there must be no effect at all.\n */\nexport class NoopSpan implements SpanHandle {\n // eslint-disable-next-line @typescript-eslint/no-empty-function -- Null Object, see above\n setAttribute(): void {}\n // eslint-disable-next-line @typescript-eslint/no-empty-function -- Null Object, see above\n setStatus(): void {}\n // eslint-disable-next-line @typescript-eslint/no-empty-function -- Null Object, see above\n end(): void {}\n}\n","/**\n * ConsoleObservabilityAdapter — dev-mode console output.\n *\n * Emits JSON-structured lines to a configurable writer (default: process.stderr).\n * Times each request from the middleware layer.\n */\nimport { SpanImpl, NoopSpan, type SpanData } from '../span.js'\n\nimport type { ObservabilityAdapter, SpanHandle, SpanAttributes, SpanContextInput } from './types.js'\n\ninterface ConsoleAdapterOptions {\n /** Writer function — defaults to process.stderr.write. */\n write?: (line: string) => void\n}\n\nexport class ConsoleObservabilityAdapter implements ObservabilityAdapter {\n readonly name = 'console'\n private write: (line: string) => void\n private isShutdown = false\n private spans: SpanImpl[] = []\n\n constructor(options: ConsoleAdapterOptions = {}) {\n this.write = options.write ?? ((line: string) => process.stderr.write(line + '\\n'))\n }\n\n startSpan(name: string, attributes?: SpanAttributes, context?: SpanContextInput): SpanHandle {\n if (this.isShutdown) return new NoopSpan()\n const span = new SpanImpl(name, attributes, context)\n this.spans.push(span)\n return {\n setAttribute: (k, v) => {\n span.setAttribute(k, v)\n },\n setStatus: (s, m) => {\n span.setStatus(s, m)\n },\n end: () => {\n span.end()\n this.emitSpan(span.getData())\n },\n }\n }\n\n counter(name: string, value: number, attributes?: SpanAttributes): void {\n if (this.isShutdown) return\n this.emit({\n type: 'counter',\n metric: name,\n value,\n attributes: attributes ?? {},\n timestamp: Date.now(),\n })\n }\n\n histogram(name: string, value: number, attributes?: SpanAttributes): void {\n if (this.isShutdown) return\n this.emit({\n type: 'histogram',\n metric: name,\n value,\n attributes: attributes ?? {},\n timestamp: Date.now(),\n })\n }\n\n log(\n level: 'debug' | 'info' | 'warn' | 'error',\n message: string,\n attributes?: SpanAttributes,\n ): void {\n if (this.isShutdown) return\n this.emit({ type: 'log', level, message, attributes: attributes ?? {}, timestamp: Date.now() })\n }\n\n // Not `async`: this adapter writes synchronously, so there is nothing to await.\n flush(): Promise<void> {\n return Promise.resolve()\n }\n\n shutdown(): Promise<void> {\n this.isShutdown = true\n return Promise.resolve()\n }\n\n private emitSpan(data: SpanData): void {\n this.emit({\n type: 'span',\n name: data.name,\n status: data.status,\n duration_ms: data.durationMs ?? 0,\n attributes: data.attributes,\n timestamp: data.startTimeMs,\n })\n }\n\n private emit(record: Record<string, unknown>): void {\n this.write(JSON.stringify(record))\n }\n}\n","/**\n * NoopObservabilityAdapter — silent fallback.\n *\n * All methods are no-ops. Never throws, never blocks.\n * Used as the default when no other adapter is configured.\n * EC-2: startSpan after shutdown returns a noop span (no crash).\n */\nimport { NoopSpan } from '../span.js'\n\nimport type { ObservabilityAdapter, SpanHandle, SpanAttributes } from './types.js'\n\nexport class NoopObservabilityAdapter implements ObservabilityAdapter {\n readonly name = 'noop'\n private isShutdown = false\n\n startSpan(_name: string, _attributes?: SpanAttributes): SpanHandle {\n return new NoopSpan()\n }\n\n // Null Object: discarding the call IS the behaviour. An empty body is the honest\n // implementation — a fabricated one would only hide that from the next reader.\n /* eslint-disable @typescript-eslint/no-empty-function -- no-op adapter by design */\n counter(_name: string, _value: number, _attributes?: SpanAttributes): void {}\n histogram(_name: string, _value: number, _attributes?: SpanAttributes): void {}\n log(\n _level: 'debug' | 'info' | 'warn' | 'error',\n _message: string,\n _attributes?: SpanAttributes,\n ): void {}\n /* eslint-enable @typescript-eslint/no-empty-function */\n\n // Not `async`: there is nothing to await, and declaring it so would claim otherwise.\n flush(): Promise<void> {\n return Promise.resolve()\n }\n\n shutdown(): Promise<void> {\n this.isShutdown = true\n return Promise.resolve()\n }\n}\n","/**\n * Lightweight OTLP JSON serializer — ~50 LoC, no @opentelemetry/* dependency.\n *\n * Per ADR D456: in-house serializer for the theo-cloud adapter.\n * Produces valid ExportTraceServiceRequest JSON (OTLP v1.0).\n * Pinned to OTLP JSON v1.0 (stable since 2023).\n */\nimport type { SpanData } from './span.js'\n\ninterface OtlpSpan {\n traceId: string\n spanId: string\n /** Omitted on the root span — OTLP reads an absent parent as \"this is the root\". */\n parentSpanId?: string\n name: string\n kind: number\n startTimeUnixNano: string\n endTimeUnixNano: string\n attributes: {\n key: string\n value: { stringValue?: string; intValue?: string; doubleValue?: number; boolValue?: boolean }\n }[]\n status: { code: number; message?: string }\n}\n\ninterface ExportTraceServiceRequest {\n resourceSpans: [\n {\n scopeSpans: [\n {\n scope: { name: string; version: string }\n spans: OtlpSpan[]\n },\n ]\n },\n ]\n}\n\ninterface OtlpAttributeValue {\n stringValue?: string\n intValue?: string\n doubleValue?: number\n boolValue?: boolean\n}\n\n/**\n * OTLP's `AnyValue`: one field filled in, the others absent.\n *\n * It was an inline nested ternary (agent-builder#319). Extracted with a `switch`, and not flattened\n * into a cleverer ternary, because OTLP's type list is open — `arrayValue`, `doubleValue` and\n * `kvlistValue` exist in the spec and are not emitted here yet. With the `switch`, each becomes a new\n * `case`; with the ternary, each would become one more level of nesting.\n */\nfunction paraValorOtlp(value: string | number | boolean): OtlpAttributeValue {\n switch (typeof value) {\n case 'string':\n return { stringValue: value }\n case 'number':\n // usetheokit/theokit#380 — every number used to go out as `intValue`, so\n // `cost.usd` reached the collector as `{\"intValue\":\"0.0031\"}`: a string\n // that is not an integer, in the field reserved for integers. A collector\n // may reject it, coerce it to 0, or keep the string; none of those is the\n // number, and cost is the one attribute that answers what a run cost.\n //\n // `Number.isInteger` and not a decimal-point test: `2.0` IS `2` in\n // JavaScript, and making the wire shape depend on how a literal was typed\n // rather than on the value would be a stranger rule than the bug.\n return Number.isInteger(value) ? { intValue: String(value) } : { doubleValue: value }\n default:\n return { boolValue: value }\n }\n}\n\n/** Convert SpanData[] to OTLP JSON bytes (Uint8Array). */\nexport function serializeSpansToOtlp(spans: SpanData[], serviceName = 'theokit'): Uint8Array {\n const otlpSpans: OtlpSpan[] = spans.map((s) => ({\n // #368 — read, never minted. This used to call `randomHex` for both, which\n // gave every span a trace of its own and made a multi-span run unreadable at\n // the collector. The ids now arrive on the span, decided when it started.\n traceId: s.traceId,\n spanId: s.spanId,\n ...(s.parentSpanId === undefined ? {} : { parentSpanId: s.parentSpanId }),\n name: s.name,\n kind: 2, // SPAN_KIND_SERVER\n startTimeUnixNano: String(s.startTimeMs * 1_000_000),\n endTimeUnixNano: String((s.endTimeMs ?? s.startTimeMs) * 1_000_000),\n attributes: Object.entries(s.attributes).map(([key, value]) => ({\n key,\n value: paraValorOtlp(value),\n })),\n status: { code: s.status === 'ok' ? 1 : 2, message: s.statusMessage },\n }))\n\n const request: ExportTraceServiceRequest = {\n resourceSpans: [\n {\n scopeSpans: [\n {\n scope: { name: serviceName, version: '1.0.0' },\n spans: otlpSpans,\n },\n ],\n },\n ],\n }\n\n return new TextEncoder().encode(JSON.stringify(request))\n}\n","/**\n * TheoCloudObservabilityAdapter — OTLP/HTTP batched export for TheoCloud.\n *\n * Zero-config via env vars (THEO_CLOUD_INGEST_URL, THEO_CLOUD_API_KEY).\n * Batches spans and flushes via native fetch() POST.\n *\n * EC-1: flush failure logs warning, does NOT throw, does NOT retry (KISS).\n * EC-2: startSpan after shutdown returns noop span.\n */\nimport { serializeSpansToOtlp } from '../otlp-serializer.js'\nimport { SpanImpl, NoopSpan, type SpanData } from '../span.js'\n\nimport type { ObservabilityAdapter, SpanHandle, SpanAttributes, SpanContextInput } from './types.js'\n\ninterface TheoCloudAdapterOptions {\n /** TheoCloud ingest endpoint URL. */\n ingestUrl: string\n /** TheoCloud API key for authentication. */\n token: string\n /** Flush interval in ms (default: 5000). */\n flushIntervalMs?: number\n /**\n * Most spans held before the oldest are dropped (default: 10 000).\n *\n * A collector that is unreachable does not make the spans stop arriving, and a\n * buffer with no ceiling turns a telemetry outage into an out-of-memory. The\n * drop is counted rather than silent: losing data is a real cost, losing it\n * without saying so is a worse one.\n */\n maxPendingSpans?: number\n /** Mock fetch for testing (never in production). */\n _mockFetch?: typeof globalThis.fetch\n}\n\nexport class TheoCloudObservabilityAdapter implements ObservabilityAdapter {\n readonly name = 'theo-cloud'\n private pendingSpans: SpanData[] = []\n private isShutdown = false\n private dropped = 0\n private readonly flushTimer: ReturnType<typeof setInterval>\n private readonly opts: Required<\n Pick<TheoCloudAdapterOptions, 'ingestUrl' | 'token' | 'flushIntervalMs' | 'maxPendingSpans'>\n > &\n TheoCloudAdapterOptions\n\n constructor(options: TheoCloudAdapterOptions) {\n this.opts = { flushIntervalMs: 5000, maxPendingSpans: 10_000, ...options }\n\n // `flushIntervalMs` was accepted and defaulted here and read nowhere — there\n // was no timer in the file. `shutdown()` was the only drain, and nothing\n // called it, so a long-running server exported nothing at all\n // (usetheokit/theokit#353).\n //\n // `unref()` is not optional: a telemetry exporter that pins the event loop\n // turns a clean process exit into a hang, which is worse than the defect it\n // was added to fix.\n this.flushTimer = setInterval(() => {\n void this.flush()\n }, this.opts.flushIntervalMs)\n this.flushTimer.unref()\n }\n\n /** How many spans were dropped because the buffer was full. */\n droppedSpanCount(): number {\n return this.dropped\n }\n\n /**\n * Whether the periodic flush is unref'd. A test seam: \"the timer does not hold\n * the process open\" is otherwise only observable by hanging.\n */\n hasUnrefdFlushTimer(): boolean {\n return !this.flushTimer.hasRef()\n }\n\n startSpan(name: string, attributes?: SpanAttributes, context?: SpanContextInput): SpanHandle {\n if (this.isShutdown) return new NoopSpan()\n const span = new SpanImpl(name, attributes, context)\n return {\n setAttribute: (k, v) => {\n span.setAttribute(k, v)\n },\n setStatus: (s, m) => {\n span.setStatus(s, m)\n },\n end: () => {\n span.end()\n if (this.pendingSpans.length >= this.opts.maxPendingSpans) {\n this.pendingSpans.shift()\n this.dropped++\n }\n this.pendingSpans.push(span.getData())\n },\n }\n }\n\n /* eslint-disable @typescript-eslint/no-empty-function -- metrics not shipped by this adapter yet; an empty body is honest, a fabricated one is not */\n counter(_name: string, _value: number, _attributes?: SpanAttributes): void {}\n histogram(_name: string, _value: number, _attributes?: SpanAttributes): void {}\n log(\n _level: 'debug' | 'info' | 'warn' | 'error',\n _message: string,\n _attributes?: SpanAttributes,\n ): void {}\n /* eslint-enable @typescript-eslint/no-empty-function */\n\n async flush(): Promise<void> {\n if (this.isShutdown || this.pendingSpans.length === 0) return\n\n const spans = this.pendingSpans.splice(0)\n const body = serializeSpansToOtlp(spans) as unknown as BodyInit\n const fetchFn = this.opts._mockFetch ?? globalThis.fetch\n\n try {\n await fetchFn(this.opts.ingestUrl, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n authorization: `Bearer ${this.opts.token}`,\n },\n body,\n })\n } catch (err) {\n // EC-1: log warning, don't throw, don't retry\n console.error(\n `[theokit:observability] flush failed: ${err instanceof Error ? err.message : 'unknown error'}`,\n )\n }\n }\n\n async shutdown(): Promise<void> {\n if (this.isShutdown) return\n // Cleared, not merely ignored: `isShutdown` would make later ticks no-ops,\n // and a live handle on a process that is trying to exit is the thing to\n // remove rather than to tolerate.\n clearInterval(this.flushTimer)\n await this.flush()\n this.isShutdown = true\n }\n}\n","/**\n * Adapter registry — resolves the active observability adapter.\n *\n * Per ADR D457 (v1.1) priority chain:\n * 1. Explicit config (theo.config.ts observability.provider) — ALWAYS wins\n * 2. THEO_CLOUD_INGEST_URL env → theo-cloud adapter\n * 3. NODE_ENV=development → console adapter\n * 4. Fallback → noop adapter\n */\nimport { ConsoleObservabilityAdapter } from './adapters/console.js'\nimport { NoopObservabilityAdapter } from './adapters/noop.js'\nimport { TheoCloudObservabilityAdapter } from './adapters/theo-cloud.js'\nimport type { ObservabilityAdapter } from './adapters/types.js'\n\ninterface ResolveAdapterOptions {\n env: Record<string, string | undefined>\n config?: {\n provider?: ObservabilityAdapter\n }\n}\n\n/**\n * Resolve the observability adapter from config + env.\n * Called once at boot — returns the active adapter for the process lifetime.\n */\nexport function resolveAdapter(options: ResolveAdapterOptions): ObservabilityAdapter {\n // 1. Explicit config ALWAYS wins (EC-4)\n if (options.config?.provider) {\n return options.config.provider\n }\n\n // 2. TheoCloud env vars → theo-cloud adapter\n const ingestUrl = options.env.THEO_CLOUD_INGEST_URL\n const apiKey = options.env.THEO_CLOUD_API_KEY\n if (ingestUrl && apiKey) {\n return new TheoCloudObservabilityAdapter({ ingestUrl, token: apiKey })\n }\n\n // 3. Development → console adapter\n if (options.env.NODE_ENV === 'development') {\n return new ConsoleObservabilityAdapter()\n }\n\n // 4. Fallback → noop\n return new NoopObservabilityAdapter()\n}\n","// T5a.1b — Web Crypto migration. randomUUID() moved to globalThis.crypto.\n// IncomingMessage stays as a type-only import (runtime-clean — TS erases at\n// build); full IncomingMessage→Request boundary migration deferred to a\n// later T5a.1c+ slice per ADR-0028 incremental leaf-first sequence.\nimport type { IncomingMessage } from 'node:http'\n\n/**\n * Phase 7 — Observability: traceId propagation (D7).\n *\n * Extract a stable identifier from incoming requests so a single value\n * correlates the client request, every server log line, the response\n * envelope, and any downstream span. Precedence:\n *\n * 1. `traceparent` (W3C Trace Context — `00-{32-hex}-{16-hex}-{flags}`)\n * 2. `x-request-id` (Heroku / GCP / generic proxy header)\n * 3. Generated UUID (fresh per request)\n *\n * UUIDs are accepted as trace identifiers by every major vendor that\n * does not enforce strict 32-hex (Datadog, Honeycomb, Sentry, Logflare,\n * Axiom, etc). We don't need ULIDs to ship this surface.\n */\n\nexport const TRACE_HEADER = 'x-trace-id'\nexport const TRACE_PARENT_HEADER = 'traceparent'\nconst REQUEST_ID_HEADER = 'x-request-id'\n\n// W3C Trace Context: 00-<trace-id 32 hex>-<span-id 16 hex>-<flags 2 hex>\nconst TRACEPARENT_RE = /^00-([0-9a-f]{32})-([0-9a-f]{16})-[0-9a-f]{2}$/\n\n/**\n * What a valid `traceparent` says: the trace to join, and the caller's span\n * inside it.\n *\n * Both halves are on the wire and only the first was ever read, so a span this\n * process opened for an incoming request became a SECOND root of the caller's\n * trace instead of a child of the caller's span — the trace correlated and the\n * waterfall lost its shape (usetheokit/theokit#385).\n */\nexport interface W3CTraceContext {\n /** The trace this request belongs to. 32 hex chars. */\n readonly traceId: string\n /**\n * The caller's span, which a span opened for this request hangs under.\n *\n * Absent when the caller sent the reserved all-zero parent id, which W3C\n * defines as \"no parent\" rather than as a span to point at.\n */\n readonly parentSpanId?: string\n}\n\n/**\n * Parse a W3C Trace Context `traceparent` header value into the trace it names\n * and the caller's span within it. `null` when the value is not a well-formed\n * `traceparent` or names the reserved all-zero trace.\n */\nexport function parseTraceparentContext(value: string): W3CTraceContext | null {\n if (!value) return null\n const m = TRACEPARENT_RE.exec(value)\n if (!m) return null\n const traceId = m[1]\n // W3C: trace-id of all zeroes is invalid by spec\n if (/^0+$/.test(traceId)) return null\n const parentSpanId = m[2]\n // Same rule one field over: an all-zero parent-id is the spec's way of saying\n // there is no parent, so it is dropped rather than exported as a span id no\n // backend can resolve.\n return /^0+$/.test(parentSpanId) ? { traceId } : { traceId, parentSpanId }\n}\n\n/**\n * Parse a W3C Trace Context `traceparent` header value. Returns the\n * 32-hex trace-id when valid (and not the reserved all-zeros), else\n * `null`.\n */\nexport function parseTraceparent(value: string): string | null {\n return parseTraceparentContext(value)?.traceId ?? null\n}\n\n/**\n * Shape a correlation id must have to be trusted: printable, unpunctuated beyond\n * the separators real id formats use, and bounded.\n *\n * `x-request-id` is chosen by the caller and ends up in the structured logs an\n * operator reads. Unvalidated, a newline in it splits one log line into two with\n * the second forged, and a megabyte of it is a megabyte per request through the\n * whole log pipeline (usetheokit/theokit#353). The character set covers what real\n * id formats use — UUID, ULID, hex, dotted and colon-separated ids — and excludes\n * whitespace and control characters, which no id format needs and every injection\n * does.\n *\n * 128 is comfortably above any of those formats and far below a payload.\n */\nconst REQUEST_ID_RE = /^[A-Za-z0-9_.:-]{1,128}$/\n\nfunction isTrustedRequestId(value: string): boolean {\n return REQUEST_ID_RE.test(value)\n}\n\n/**\n * Pick the first TRUSTED string value out of an IncomingMessage header. Node\n * collapses repeated headers into arrays; proxies sometimes do this for\n * `x-request-id`. Empty strings count as absent.\n *\n * \"First trusted\" rather than \"first non-empty\": taking the first value and\n * validating afterwards would let a proxy prepending a hostile value defeat a\n * good one sitting behind it.\n */\nfunction pickHeader(\n value: string | string[] | undefined,\n isTrusted: (candidate: string) => boolean = () => true,\n): string | null {\n if (Array.isArray(value)) {\n for (const v of value) {\n if (typeof v === 'string' && v.length > 0 && isTrusted(v)) return v\n }\n return null\n }\n if (typeof value === 'string' && value.length > 0 && isTrusted(value)) return value\n return null\n}\n\n/**\n * T5a.2 Phase C slice 1/2 — pure traceId resolution from pre-extracted\n * header values. Shared between IncomingMessage and Web Request wrappers.\n */\nfunction resolveTraceIdFromHeaders(traceparent: string | null, requestId: string | null): string {\n if (traceparent !== null) {\n const parsed = parseTraceparent(traceparent)\n if (parsed !== null) return parsed\n }\n if (requestId !== null) return requestId\n return globalThis.crypto.randomUUID()\n}\n\n/**\n * Resolve the request's traceId following the precedence above.\n */\nexport function extractTraceId(req: IncomingMessage): string {\n return resolveTraceIdFromHeaders(\n pickHeader(req.headers[TRACE_PARENT_HEADER]),\n pickHeader(req.headers[REQUEST_ID_HEADER], isTrustedRequestId),\n )\n}\n\n/**\n * T5a.2 Phase C slice 1/2 — Web-Standards-shaped traceId resolver.\n *\n * Mirror of `extractTraceId(req: IncomingMessage)` for the Web `Request`\n * shape. Same precedence (`traceparent` → `x-request-id` → generated\n * UUID). Uses `request.headers.get(name)` (native Web `Headers` API)\n * instead of the Node indexer.\n *\n * **Multi-value note:** Web `Headers` collapses repeated headers into a\n * single comma-separated string at parse. The IncomingMessage path's\n * `pickHeader` \"first non-empty value\" semantic is naturally satisfied\n * because there's no array to pick from on the Web side — `.get()`\n * returns the comma-joined value, which for `traceparent` / `x-request-id`\n * is treated as a single string anyway (both headers are conventionally\n * single-valued).\n */\n/**\n * The request's W3C trace context — the trace to join and the caller's span\n * within it — or `undefined` when the caller supplied no usable `traceparent`.\n *\n * Deliberately narrower than {@link extractTraceIdFromRequest}, which always\n * returns something and may return an `x-request-id` or a generated UUID. Those\n * are fine as a log correlation key and are NOT trace ids: OTLP wants 32 hex\n * characters, and a dashed UUID exported as a `traceId` is a malformed span.\n *\n * So a span-emitting caller asks this question instead — \"is there a real trace\n * to join?\" — and mints its own when the answer is no (usetheokit/theokit#368).\n *\n * It answers with the whole context rather than the trace id alone, because a\n * span opened for this request belongs in the caller's trace AND under the\n * caller's span. Returning only the first half is what made one request arrive\n * as a trace with two roots (usetheokit/theokit#385).\n */\nexport function extractW3CTraceContext(request: Request): W3CTraceContext | undefined {\n const traceparent = request.headers.get(TRACE_PARENT_HEADER)\n if (traceparent === null) return undefined\n return parseTraceparentContext(traceparent) ?? undefined\n}\n\nexport function extractTraceIdFromRequest(request: Request): string {\n const requestId = request.headers.get(REQUEST_ID_HEADER)\n return resolveTraceIdFromHeaders(\n request.headers.get(TRACE_PARENT_HEADER),\n // Same policy on both resolvers. A validation living on one side only is the\n // gap an attacker picks the other transport to reach.\n requestId !== null && isTrustedRequestId(requestId) ? requestId : null,\n )\n}\n","/**\n * One request, one trace — the side table both consumers read (usetheokit/theokit#404).\n *\n * ## The defect this exists to remove\n *\n * Two places decide what trace a request belongs to: the observability plugin, which opens the\n * `http.request` span, and `observeServedRun`, which opens `agent.run`. Both used to answer the\n * question the same way and *independently* — by reading the inbound `traceparent` header. That\n * agrees only while the header is there. A browser sends none, and neither does `curl` or an\n * uninstrumented `fetch`, so on the majority path each side took its own `?? newTraceId()` branch\n * and one request reached the collector as two disconnected traces, neither naming the other.\n *\n * Reading the same header is not sharing. This module is the sharing: the request's trace is\n * resolved ONCE, on first ask, and every later ask gets the same answer.\n *\n * ## Why a `WeakMap` keyed on the `Request`\n *\n * The two consumers already hold the same `Request` object — `serveThroughPluginLifecycle` builds\n * it once and hands that instance both to the hooks (as `PluginContext.request`) and to the\n * handler that calls `mountAgent`. So the request itself is already the shared thing, and a side\n * table keyed on it needs no new parameter threaded through `mountAgent`, no decoration contract,\n * and no signature change on either side.\n *\n * The two alternatives were weighed and rejected for reasons that outlive this comment:\n *\n * - `AsyncLocalStorage` is what an OTel Node SDK does, and it would work here — but it imports\n * `node:async_hooks`, and `server/` holds a no-`node:*` invariant precisely so the same code\n * serves the Web, Tauri and TUI targets (`rules/three-target-parity.md`). A `WeakMap` is plain\n * ECMAScript and runs unchanged on every one of them.\n * - Threading a resolved context through `mountAgent`'s options is explicit, and it puts a\n * telemetry concern in the signature of every route that might one day open a span. The entry\n * above is the same value, reachable without the parameter.\n *\n * Keying on the object also disposes of the entry: the record dies with the request, with no cap,\n * no eviction and no leak — unlike the plugin's own span map, which is keyed by `requestId` and\n * needs `evictUntilRoom` for exactly that reason.\n *\n * ## What this does NOT do\n *\n * It does not open a span, and it does not claim one was opened. `outermostSpanId` stays unset\n * until something actually emits the request's outermost span, so a run with no `http.request`\n * span in scope stays the root of its own trace rather than naming a parent this process never\n * emitted. A dangling parent reads as a span that was lost in transit, which is a worse report\n * than an honest root.\n */\nimport { extractW3CTraceContext } from '../http/trace-context.js'\n\nimport { newTraceId } from './trace-context-propagation.js'\n\nexport interface RequestTrace {\n /** The trace every span caused by this request belongs to. 32 hex chars. */\n readonly traceId: string\n /** The CALLER's span, when the caller sent a well-formed `traceparent`. */\n readonly parentSpanId?: string\n /**\n * The outermost span THIS process opened for the request, once one has been opened.\n *\n * Undefined means nothing opened one — no observability plugin on this path, or a run started\n * outside an HTTP turn. Consumers treat it as \"no local parent\", never as an id to point at.\n */\n outermostSpanId?: string\n}\n\nconst resolved = new WeakMap<Request, RequestTrace>()\n\n/**\n * The request's trace, resolved once and memoized on the request itself.\n *\n * Idempotent by construction: the first caller decides — from the inbound `traceparent` when there\n * is one, from a fresh mint when there is not — and every caller after it, on either side of the\n * request, is handed that same decision.\n */\nexport function requestTrace(request: Request): RequestTrace {\n const existing = resolved.get(request)\n if (existing !== undefined) return existing\n\n const trace = resolve(request)\n resolved.set(request, trace)\n return trace\n}\n\n/** Decide a request's trace from what it carries. Called once per request, by `requestTrace`. */\nfunction resolve(request: Request): RequestTrace {\n const inbound = extractW3CTraceContext(request)\n if (inbound === undefined) return { traceId: newTraceId() }\n if (inbound.parentSpanId === undefined) return { traceId: inbound.traceId }\n return { traceId: inbound.traceId, parentSpanId: inbound.parentSpanId }\n}\n\n/**\n * Record the span this process opened as the request's outermost one, so spans opened later in the\n * same request hang under it instead of beside it.\n *\n * First writer wins. A second outermost span for one request is a bug in the caller, and silently\n * re-pointing every later child at it would hide that bug behind a plausible waterfall.\n */\nexport function recordOutermostSpan(request: Request, spanId: string): void {\n const trace = requestTrace(request)\n trace.outermostSpanId ??= spanId\n}\n","/**\n * Auto-instrumentation plugin — one span per HTTP request.\n *\n * Per blueprint Pattern 2 (Hono middleware timing):\n * - onRequest: start span with method + path\n * - onResponse: end span with status + duration\n * - onError: set span status to error\n *\n * ## It used to be unregistrable, and nothing said so\n *\n * This returned `{ name, onRequest, onResponse, onError }` against a plugin\n * contract of `{ name, register }`, so the obvious wiring — putting it in\n * `config.plugins` — threw `InvalidPluginShapeError` at boot\n * (`../plugins/load-plugins.ts:20`). Its own context type was a second, narrower\n * invention: `request: { method, url }` where the real hook receives a Web\n * `Request` with an ABSOLUTE url, and `response?: { statusCode }` where the real\n * one is a `ServerResponse`. The tests passed because they called the hooks\n * directly with the invented shape, which verified the implementation and never\n * the contract (usetheokit/theokit#353).\n *\n * ## Span state is per-instance, and bounded\n *\n * The active-span map used to be a module-level `Map` with no cap and no TTL.\n * Two consequences, both real once anything registered this:\n *\n * - two plugin instances shared one map, so one app's response closed\n * another's span;\n * - a request whose `onResponse` never fires leaked its span forever. That is\n * not an edge case here: the SSE path is exactly where `onResponse` does not\n * fire on stream open, and an agent run is the longest-lived stream the\n * framework serves.\n *\n * The map now lives in the closure and is capped. An evicted span is ENDED with\n * an `span.abandoned` attribute rather than dropped, so it still reaches the\n * exporter carrying the reason it was cut — trading a memory leak for a silent\n * hole in the trace would be the worse bargain.\n */\nimport type { PluginContext, PluginErrorContext, TheoApp, TheoPlugin } from '../plugin-types.js'\n\nimport type { ObservabilityAdapter, SpanContextInput, SpanHandle } from './adapters/types.js'\nimport { recordOutermostSpan, requestTrace } from './request-trace.js'\nimport { newSpanId } from './trace-context-propagation.js'\n\n/**\n * How many requests may be in flight with an unclosed span before the oldest is\n * force-ended. Sized to be far above any real concurrent-request count on a\n * single Node process, so eviction signals a leak rather than back-pressure.\n */\nconst DEFAULT_MAX_ACTIVE_SPANS = 1024\n\nexport interface ObservabilityPluginOptions {\n /** Override the in-flight span cap. Mainly a test seam. */\n maxActiveSpans?: number\n}\n\nexport function createObservabilityPlugin(\n adapter: ObservabilityAdapter,\n options: ObservabilityPluginOptions = {},\n): TheoPlugin {\n const maxActiveSpans = options.maxActiveSpans ?? DEFAULT_MAX_ACTIVE_SPANS\n const activeSpans = new Map<string, SpanHandle>()\n\n /** Force-end the oldest spans until there is room for one more. */\n function evictUntilRoom(): void {\n while (activeSpans.size >= maxActiveSpans) {\n const oldest = activeSpans.keys().next()\n if (oldest.done === true) return\n const span = activeSpans.get(oldest.value)\n activeSpans.delete(oldest.value)\n if (span === undefined) continue\n span.setAttribute('span.abandoned', true)\n span.setStatus('error', 'span abandoned: onResponse never fired for this request')\n span.end()\n }\n }\n\n /**\n * Where the request's span sits: in the request's trace — the caller's when the caller sent a\n * `traceparent` (usetheokit/theokit#385), a freshly minted one otherwise — under the caller's\n * span when there is one.\n *\n * This is the OUTERMOST span of a request, which makes it precisely the caller\n * `startSpan`'s optional `context` was written for — and precisely the caller\n * that went on passing the two-argument form. The consequence was worse than\n * \"the HTTP span is a root\": once `mountAgent` learned to continue an incoming\n * trace, one request that ran an agent reached the collector as TWO\n * disconnected traces, and the caller's trace id was present on the HTTP span\n * as the `requestId` attribute rather than as its `traceId` — resolved,\n * carried, and written to a field no tracing backend correlates on.\n *\n * No `traceparent` (or a malformed one) mints a fresh trace, which is the\n * correct answer for a request nothing upstream traced — and it is minted by\n * `requestTrace`, ONCE, so the run joins that trace instead of minting a\n * second one of its own (usetheokit/theokit#404). The extraction there\n * deliberately refuses `x-request-id` and dashed UUIDs: those are fine\n * correlation keys and are not trace ids.\n */\n function requestSpanContext(request: Request): SpanContextInput {\n const trace = requestTrace(request)\n // Pinned rather than left to the adapter, because this id is what everything else the request\n // causes hangs under — `SpanContextInput.spanId` exists for exactly this caller. Recording it\n // is what makes the run a child instead of a second root (usetheokit/theokit#404).\n const spanId = newSpanId()\n recordOutermostSpan(request, spanId)\n return trace.parentSpanId === undefined\n ? { traceId: trace.traceId, spanId }\n : { traceId: trace.traceId, spanId, parentSpanId: trace.parentSpanId }\n }\n\n /** Take the span for a request, if one is still open. */\n function claim(requestId: string): SpanHandle | undefined {\n const span = activeSpans.get(requestId)\n if (span === undefined) return undefined\n activeSpans.delete(requestId)\n return span\n }\n\n return {\n name: 'theokit:observability',\n\n register(app: TheoApp): void {\n app.addHook('onRequest', (ctx: PluginContext) => {\n evictUntilRoom()\n const span = adapter.startSpan(\n 'http.request',\n {\n method: ctx.request.method,\n // `ctx.request.url` is absolute, because that is what a Web `Request`\n // carries (`plugin-types.ts` says so, at length, for this reason).\n path: new URL(ctx.request.url).pathname,\n requestId: ctx.requestId,\n },\n requestSpanContext(ctx.request),\n )\n activeSpans.set(ctx.requestId, span)\n })\n\n app.addHook('onResponse', (ctx: PluginContext) => {\n const span = claim(ctx.requestId)\n if (span === undefined) return\n\n const status = ctx.response.statusCode\n span.setAttribute('status', status)\n span.setStatus('ok')\n span.end()\n\n // Deliberately no `path` label: a counter is an aggregated series, and\n // a dynamic route would mint one series per id.\n adapter.counter('http.requests', 1, { method: ctx.request.method, status })\n })\n\n app.addHook('onError', (ctx: PluginErrorContext) => {\n const span = claim(ctx.requestId)\n if (span === undefined) return\n\n const message = ctx.error instanceof Error ? ctx.error.message : 'unknown error'\n span.setAttribute('error', true)\n span.setStatus('error', message)\n span.end()\n\n adapter.counter('http.errors', 1, { method: ctx.request.method })\n })\n },\n }\n}\n","/**\n * Build the observability plugin from `theo.config.ts > observability` + the\n * environment (#353).\n *\n * Until this existed, `createObservabilityPlugin` had no production caller and\n * `startSpan` was invoked in exactly one production file — the one nothing\n * called. Every adapter, the OTLP serializer and the span implementation were\n * tested, published and unreachable: the framework emitted no spans at all.\n *\n * ## When the plugin is wired, and one deliberate divergence\n *\n * `adapter-registry.ts` documents a four-step chain: explicit config, then\n * TheoCloud env vars, then `NODE_ENV=development` → console, then noop. This\n * function honours the first two and does NOT treat the third as an opt-in.\n *\n * The reason is that step 3 would turn telemetry on for every `theo dev` that\n * never asked for it, and the console adapter writes JSON lines to `stderr`\n * (`adapters/console.ts:23`) — the same stream `observability/logger.ts` already\n * writes a different JSON shape to. Two interleaved formats on one stream is a\n * downgrade for every developer, bought in exchange for telemetry nobody\n * requested.\n *\n * So: `observability: {}` in the config turns it on, and in dev that still\n * resolves to the console adapter exactly as the chain says. What changes is\n * that `NODE_ENV=development` alone no longer counts as asking.\n *\n * Returns `undefined` when nothing asked for telemetry, which preserves the\n * zero-plugin path for applications that configure none.\n */\nimport { observabilitySchema } from '../config/schemas/index.js'\n\nimport { resolveAdapter } from './observability/adapter-registry.js'\nimport type { ObservabilityAdapter } from './observability/adapters/types.js'\nimport { warnOnce } from './observability/logger.js'\nimport { createObservabilityPlugin } from './observability/middleware.js'\nimport type { TheoPlugin } from './plugin-types.js'\n\n/**\n * The adapter resolved at boot, for callers that are not the HTTP plugin.\n *\n * An agent run is the case that forced this: its spans are produced by\n * `observeAgentRun` from the wire chunk stream, far from the request hooks, and\n * it must be the SAME adapter — two independently resolved adapters mean two\n * exporters and two half-complete pictures of one run.\n *\n * `undefined` when nothing asked for telemetry, which is what keeps the\n * zero-cost path zero-cost.\n */\nlet activeAdapter: ObservabilityAdapter | undefined\n\nexport function getObservabilityAdapter(): ObservabilityAdapter | undefined {\n return activeAdapter\n}\n\n/** Test-only: forget the boot-resolved adapter. Production code must not call this. */\nexport function _resetObservabilityAdapter(): void {\n activeAdapter = undefined\n}\n\nexport function createObservabilityPluginFromConfig(\n observabilityConfig: unknown,\n env: Record<string, string | undefined>,\n): TheoPlugin | undefined {\n const parsed =\n observabilityConfig === undefined || observabilityConfig === null\n ? undefined\n : observabilitySchema.parse(observabilityConfig)\n\n if (parsed?.enabled === false) return undefined\n\n // An ingest URL plus a key is an explicit deployment decision, so it opts in\n // on its own — that is what makes the env half of the documented chain reachable\n // without a config file.\n const cloudConfigured = Boolean(env.THEO_CLOUD_INGEST_URL) && Boolean(env.THEO_CLOUD_API_KEY)\n if (parsed === undefined && !cloudConfigured) return undefined\n\n const adapter = resolveAdapter({\n env,\n config: { provider: parsed?.provider as ObservabilityAdapter | undefined },\n })\n\n // The registry never fails; it falls back to noop. Wiring a plugin whose every\n // hook is a no-op would cost a runner on the request path and buy nothing.\n if (adapter.name === 'noop') {\n // Silence here is the defect. An application that WROTE `observability: {}`\n // asked for telemetry, and returning quietly gives it a passing boot, no\n // spans, and nothing to search for — the config-validates-and-does-nothing\n // shape usetheokit/theokit#321 recorded for `rateLimit`.\n //\n // Only when it was asked for: an application that configured nothing is not\n // owed a warning about a thing it never requested.\n if (parsed !== undefined) {\n warnOnce('observability.no_exporter', {\n event: 'observability.no_exporter',\n message:\n 'observability is configured but no exporter resolved, so no spans will be recorded. ' +\n 'Set THEO_CLOUD_INGEST_URL and THEO_CLOUD_API_KEY, or pass observability.provider with ' +\n 'your own adapter. In development, NODE_ENV=development resolves the console exporter.',\n })\n }\n return undefined\n }\n\n activeAdapter = adapter\n return createObservabilityPlugin(adapter)\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/server/observability/trace-context-propagation.ts","../src/server/observability/span.ts","../src/server/observability/adapters/console.ts","../src/server/observability/adapters/noop.ts","../src/server/observability/otlp-serializer.ts","../src/server/observability/adapters/theo-cloud.ts","../src/server/observability/adapter-registry.ts","../src/server/http/trace-context.ts","../src/server/observability/request-trace.ts","../src/server/observability/middleware.ts","../src/server/observability-bootstrap.ts"],"sourcesContent":["// T5a.1a — Web Standards migration (leaf-first slice). Web Crypto's\n// `crypto.getRandomValues()` is available on globalThis in every supported\n// runtime per ADR-0028 (Node 22+, CF Workers, Bun, Deno, browsers). No\n// node:crypto import needed — Web Crypto returns a typed array filled with\n// CSPRNG bytes, which we hex-encode locally instead of relying on Buffer.\n\n/**\n * W3C Trace Context propagation helpers for non-HTTP carriers (job\n * leases, webhook reply headers, agent SSE frames). Works against any\n * `Headers`-shaped carrier — Web Standards only.\n *\n * The HTTP-side extractor `extractTraceId` in `../http/trace-context.ts`\n * is request-scoped and returns only the trace_id string. This module\n * exposes the full `TraceContext` shape (trace_id + span_id + flags)\n * because downstream jobs/webhooks need to propagate a NEW span_id\n * (child span) while keeping the same trace_id.\n *\n * Format spec (W3C Trace Context Level 2):\n * traceparent: version-trace_id-parent_id-trace_flags\n * - version = '00' (current)\n * - trace_id = 32 hex chars (128-bit)\n * - parent_id = 16 hex chars (64-bit) — \"span_id\" of the producer\n * - trace_flags = 2 hex chars (sampled bit)\n *\n * @see https://www.w3.org/TR/trace-context/\n */\n\nexport interface TraceContext {\n /** 32 hex chars. NEVER the reserved all-zeros value. */\n readonly trace_id: string\n /** 16 hex chars. NEVER the reserved all-zeros value. */\n readonly span_id: string\n /** 2 hex chars. Sampled bit + reserved. */\n readonly flags: string\n}\n\nconst TRACEPARENT_RE = /^00-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/\nconst ALL_ZEROS_TRACE = '00000000000000000000000000000000'\nconst ALL_ZEROS_SPAN = '0000000000000000'\n\n/**\n * Extract a TraceContext from a Web Headers object. Returns `null` for:\n * - missing header\n * - malformed (not matching the version-trace-span-flags regex)\n * - reserved all-zeros trace_id (W3C-invalid)\n * - reserved all-zeros span_id (W3C-invalid)\n *\n * NEVER throws — defensive read.\n */\nexport function extractTraceContext(headers: Headers): TraceContext | null {\n const value = headers.get('traceparent')\n if (!value) return null\n const m = TRACEPARENT_RE.exec(value)\n if (!m) return null\n const trace_id = m[1]\n const span_id = m[2]\n const flags = m[3]\n if (trace_id === ALL_ZEROS_TRACE) return null\n if (span_id === ALL_ZEROS_SPAN) return null\n return { trace_id, span_id, flags }\n}\n\n/**\n * Write a canonical traceparent into a Web Headers object. Always\n * succeeds (defensive write — the caller is responsible for passing\n * a valid TraceContext; passing malformed input is a programming bug).\n */\nexport function injectTraceContext(headers: Headers, ctx: TraceContext): void {\n headers.set('traceparent', `00-${ctx.trace_id}-${ctx.span_id}-${ctx.flags}`)\n}\n\n/**\n * Generate a fresh TraceContext (new trace_id + span_id, flags='01' for\n * sampled). Used when no upstream traceparent exists — e.g., when a\n * cron fires or a webhook arrives without an upstream tracer.\n */\nexport function generateNewTraceContext(): TraceContext {\n return {\n trace_id: newTraceId(),\n span_id: newSpanId(),\n flags: '01',\n }\n}\n\n/**\n * A fresh 128-bit trace id, 32 hex chars, never the reserved all-zeros.\n *\n * Exported next to its span-id sibling because span identity needs the two\n * halves separately: a child span mints only an id and inherits the trace, and\n * a caller that had to mint a whole `TraceContext` to get one span id would be\n * discarding a trace id it never used. `span.ts` is the consumer.\n */\nexport function newTraceId(): string {\n return randomHex(16) // 16 bytes = 32 hex chars\n}\n\n/** A fresh 64-bit span id, 16 hex chars, never the reserved all-zeros. */\nexport function newSpanId(): string {\n return randomHex(8) // 8 bytes = 16 hex chars\n}\n\nfunction randomHex(bytes: number): string {\n // crypto.getRandomValues is overwhelmingly unlikely to produce all-zeros,\n // but we guard anyway — the W3C spec rejects all-zeros and tests assert\n // this. Web Crypto API returns a Uint8Array; we hex-encode without Buffer\n // to stay runtime-agnostic (CF Workers / Bun / Deno have no Buffer global).\n for (;;) {\n const buf = new Uint8Array(bytes)\n globalThis.crypto.getRandomValues(buf)\n let hex = ''\n for (const b of buf) hex += b.toString(16).padStart(2, '0')\n if (!/^0+$/.test(hex)) return hex\n }\n}\n","/**\n * SpanHandle implementation — records timing + attributes.\n *\n * Used by console and theo-cloud adapters. Noop adapter uses NoopSpan.\n */\nimport type { SpanHandle, SpanAttributes, SpanContextInput } from './adapters/types.js'\nimport { newSpanId, newTraceId } from './trace-context-propagation.js'\n\nexport interface SpanData {\n name: string\n /**\n * The trace this span belongs to, and the span's own id within it.\n *\n * These used to not exist, and the OTLP serializer minted a `traceId` per span\n * at export time. Every export was well-formed and every span was an island: a\n * five-span agent run reached the collector as five unrelated single-span\n * traces, so \"read the run back from an exported trace\" had nothing to read\n * (usetheokit/theokit#368). Identity belongs to the span, decided when it\n * starts, not to the exporter, guessed when it leaves.\n */\n traceId: string\n spanId: string\n /** Absent on the root span of a trace. */\n parentSpanId?: string\n attributes: Record<string, string | number | boolean>\n status: 'ok' | 'error'\n statusMessage?: string\n startTimeMs: number\n endTimeMs?: number\n durationMs?: number\n}\n\nexport class SpanImpl implements SpanHandle {\n private readonly data: SpanData\n private ended = false\n\n constructor(name: string, attributes?: SpanAttributes, context?: SpanContextInput) {\n this.data = {\n name,\n traceId: context?.traceId ?? newTraceId(),\n spanId: context?.spanId ?? newSpanId(),\n attributes: {},\n status: 'ok',\n startTimeMs: Date.now(),\n }\n if (context?.parentSpanId !== undefined) this.data.parentSpanId = context.parentSpanId\n if (attributes) {\n for (const [k, v] of Object.entries(attributes)) {\n if (v !== undefined) this.data.attributes[k] = v\n }\n }\n }\n\n setAttribute(key: string, value: string | number | boolean): void {\n if (!this.ended) this.data.attributes[key] = value\n }\n\n setStatus(status: 'ok' | 'error', message?: string): void {\n if (!this.ended) {\n this.data.status = status\n this.data.statusMessage = message\n }\n }\n\n end(): void {\n if (this.ended) return // idempotent\n this.ended = true\n this.data.endTimeMs = Date.now()\n this.data.durationMs = this.data.endTimeMs - this.data.startTimeMs\n }\n\n /** Read-only access to span data (for adapters to export). */\n getData(): SpanData {\n return { ...this.data, attributes: { ...this.data.attributes } }\n }\n\n isEnded(): boolean {\n return this.ended\n }\n}\n\n/**\n * Noop span — used by NoopAdapter and post-shutdown fallback (EC-2).\n *\n * The three empty bodies are the Null Object, not forgetfulness (agent-builder#319): **doing\n * nothing** is the contracted behaviour. Filling them with a `void 0` or a log just to silence the\n * lint would trade a readable intention for noise — and a log here would run on the post-shutdown\n * path, which is precisely where there must be no effect at all.\n */\nexport class NoopSpan implements SpanHandle {\n // eslint-disable-next-line @typescript-eslint/no-empty-function -- Null Object, see above\n setAttribute(): void {}\n // eslint-disable-next-line @typescript-eslint/no-empty-function -- Null Object, see above\n setStatus(): void {}\n // eslint-disable-next-line @typescript-eslint/no-empty-function -- Null Object, see above\n end(): void {}\n}\n","/**\n * ConsoleObservabilityAdapter — dev-mode console output.\n *\n * Emits JSON-structured lines to a configurable writer (default: process.stderr).\n * Times each request from the middleware layer.\n */\nimport { SpanImpl, NoopSpan, type SpanData } from '../span.js'\n\nimport type { ObservabilityAdapter, SpanHandle, SpanAttributes, SpanContextInput } from './types.js'\n\ninterface ConsoleAdapterOptions {\n /** Writer function — defaults to process.stderr.write. */\n write?: (line: string) => void\n}\n\nexport class ConsoleObservabilityAdapter implements ObservabilityAdapter {\n readonly name = 'console'\n private write: (line: string) => void\n private isShutdown = false\n private spans: SpanImpl[] = []\n\n constructor(options: ConsoleAdapterOptions = {}) {\n this.write = options.write ?? ((line: string) => process.stderr.write(line + '\\n'))\n }\n\n startSpan(name: string, attributes?: SpanAttributes, context?: SpanContextInput): SpanHandle {\n if (this.isShutdown) return new NoopSpan()\n const span = new SpanImpl(name, attributes, context)\n this.spans.push(span)\n return {\n setAttribute: (k, v) => {\n span.setAttribute(k, v)\n },\n setStatus: (s, m) => {\n span.setStatus(s, m)\n },\n end: () => {\n span.end()\n this.emitSpan(span.getData())\n },\n }\n }\n\n counter(name: string, value: number, attributes?: SpanAttributes): void {\n if (this.isShutdown) return\n this.emit({\n type: 'counter',\n metric: name,\n value,\n attributes: attributes ?? {},\n timestamp: Date.now(),\n })\n }\n\n histogram(name: string, value: number, attributes?: SpanAttributes): void {\n if (this.isShutdown) return\n this.emit({\n type: 'histogram',\n metric: name,\n value,\n attributes: attributes ?? {},\n timestamp: Date.now(),\n })\n }\n\n log(\n level: 'debug' | 'info' | 'warn' | 'error',\n message: string,\n attributes?: SpanAttributes,\n ): void {\n if (this.isShutdown) return\n this.emit({ type: 'log', level, message, attributes: attributes ?? {}, timestamp: Date.now() })\n }\n\n // Not `async`: this adapter writes synchronously, so there is nothing to await.\n flush(): Promise<void> {\n return Promise.resolve()\n }\n\n shutdown(): Promise<void> {\n this.isShutdown = true\n return Promise.resolve()\n }\n\n private emitSpan(data: SpanData): void {\n this.emit({\n type: 'span',\n name: data.name,\n status: data.status,\n duration_ms: data.durationMs ?? 0,\n attributes: data.attributes,\n timestamp: data.startTimeMs,\n })\n }\n\n private emit(record: Record<string, unknown>): void {\n this.write(JSON.stringify(record))\n }\n}\n","/**\n * NoopObservabilityAdapter — silent fallback.\n *\n * All methods are no-ops. Never throws, never blocks.\n * Used as the default when no other adapter is configured.\n * EC-2: startSpan after shutdown returns a noop span (no crash).\n */\nimport { NoopSpan } from '../span.js'\n\nimport type { ObservabilityAdapter, SpanHandle, SpanAttributes } from './types.js'\n\nexport class NoopObservabilityAdapter implements ObservabilityAdapter {\n readonly name = 'noop'\n private isShutdown = false\n\n startSpan(_name: string, _attributes?: SpanAttributes): SpanHandle {\n return new NoopSpan()\n }\n\n // Null Object: discarding the call IS the behaviour. An empty body is the honest\n // implementation — a fabricated one would only hide that from the next reader.\n /* eslint-disable @typescript-eslint/no-empty-function -- no-op adapter by design */\n counter(_name: string, _value: number, _attributes?: SpanAttributes): void {}\n histogram(_name: string, _value: number, _attributes?: SpanAttributes): void {}\n log(\n _level: 'debug' | 'info' | 'warn' | 'error',\n _message: string,\n _attributes?: SpanAttributes,\n ): void {}\n /* eslint-enable @typescript-eslint/no-empty-function */\n\n // Not `async`: there is nothing to await, and declaring it so would claim otherwise.\n flush(): Promise<void> {\n return Promise.resolve()\n }\n\n shutdown(): Promise<void> {\n this.isShutdown = true\n return Promise.resolve()\n }\n}\n","/**\n * Lightweight OTLP JSON serializer — ~50 LoC, no @opentelemetry/* dependency.\n *\n * Per ADR D456: in-house serializer for the theo-cloud adapter.\n * Produces valid ExportTraceServiceRequest JSON (OTLP v1.0).\n * Pinned to OTLP JSON v1.0 (stable since 2023).\n */\nimport type { SpanData } from './span.js'\n\ninterface OtlpSpan {\n traceId: string\n spanId: string\n /** Omitted on the root span — OTLP reads an absent parent as \"this is the root\". */\n parentSpanId?: string\n name: string\n kind: number\n startTimeUnixNano: string\n endTimeUnixNano: string\n attributes: {\n key: string\n value: { stringValue?: string; intValue?: string; doubleValue?: number; boolValue?: boolean }\n }[]\n status: { code: number; message?: string }\n}\n\ninterface ExportTraceServiceRequest {\n resourceSpans: [\n {\n scopeSpans: [\n {\n scope: { name: string; version: string }\n spans: OtlpSpan[]\n },\n ]\n },\n ]\n}\n\ninterface OtlpAttributeValue {\n stringValue?: string\n intValue?: string\n doubleValue?: number\n boolValue?: boolean\n}\n\n/**\n * OTLP's `AnyValue`: one field filled in, the others absent.\n *\n * It was an inline nested ternary (agent-builder#319). Extracted with a `switch`, and not flattened\n * into a cleverer ternary, because OTLP's type list is open — `arrayValue`, `doubleValue` and\n * `kvlistValue` exist in the spec and are not emitted here yet. With the `switch`, each becomes a new\n * `case`; with the ternary, each would become one more level of nesting.\n */\nfunction paraValorOtlp(value: string | number | boolean): OtlpAttributeValue {\n switch (typeof value) {\n case 'string':\n return { stringValue: value }\n case 'number':\n // usetheokit/theokit#380 — every number used to go out as `intValue`, so\n // `cost.usd` reached the collector as `{\"intValue\":\"0.0031\"}`: a string\n // that is not an integer, in the field reserved for integers. A collector\n // may reject it, coerce it to 0, or keep the string; none of those is the\n // number, and cost is the one attribute that answers what a run cost.\n //\n // `Number.isInteger` and not a decimal-point test: `2.0` IS `2` in\n // JavaScript, and making the wire shape depend on how a literal was typed\n // rather than on the value would be a stranger rule than the bug.\n return Number.isInteger(value) ? { intValue: String(value) } : { doubleValue: value }\n default:\n return { boolValue: value }\n }\n}\n\n/** Convert SpanData[] to OTLP JSON bytes (Uint8Array). */\nexport function serializeSpansToOtlp(spans: SpanData[], serviceName = 'theokit'): Uint8Array {\n const otlpSpans: OtlpSpan[] = spans.map((s) => ({\n // #368 — read, never minted. This used to call `randomHex` for both, which\n // gave every span a trace of its own and made a multi-span run unreadable at\n // the collector. The ids now arrive on the span, decided when it started.\n traceId: s.traceId,\n spanId: s.spanId,\n ...(s.parentSpanId === undefined ? {} : { parentSpanId: s.parentSpanId }),\n name: s.name,\n kind: 2, // SPAN_KIND_SERVER\n startTimeUnixNano: String(s.startTimeMs * 1_000_000),\n endTimeUnixNano: String((s.endTimeMs ?? s.startTimeMs) * 1_000_000),\n attributes: Object.entries(s.attributes).map(([key, value]) => ({\n key,\n value: paraValorOtlp(value),\n })),\n status: { code: s.status === 'ok' ? 1 : 2, message: s.statusMessage },\n }))\n\n const request: ExportTraceServiceRequest = {\n resourceSpans: [\n {\n scopeSpans: [\n {\n scope: { name: serviceName, version: '1.0.0' },\n spans: otlpSpans,\n },\n ],\n },\n ],\n }\n\n return new TextEncoder().encode(JSON.stringify(request))\n}\n","/**\n * TheoCloudObservabilityAdapter — OTLP/HTTP batched export for TheoCloud.\n *\n * Zero-config via env vars (THEO_CLOUD_INGEST_URL, THEO_CLOUD_API_KEY).\n * Batches spans and flushes via native fetch() POST.\n *\n * EC-1: flush failure logs warning, does NOT throw, does NOT retry (KISS).\n * EC-2: startSpan after shutdown returns noop span.\n */\nimport { serializeSpansToOtlp } from '../otlp-serializer.js'\nimport { SpanImpl, NoopSpan, type SpanData } from '../span.js'\n\nimport type { ObservabilityAdapter, SpanHandle, SpanAttributes, SpanContextInput } from './types.js'\n\ninterface TheoCloudAdapterOptions {\n /** TheoCloud ingest endpoint URL. */\n ingestUrl: string\n /** TheoCloud API key for authentication. */\n token: string\n /** Flush interval in ms (default: 5000). */\n flushIntervalMs?: number\n /**\n * Most spans held before the oldest are dropped (default: 10 000).\n *\n * A collector that is unreachable does not make the spans stop arriving, and a\n * buffer with no ceiling turns a telemetry outage into an out-of-memory. The\n * drop is counted rather than silent: losing data is a real cost, losing it\n * without saying so is a worse one.\n */\n maxPendingSpans?: number\n /** Mock fetch for testing (never in production). */\n _mockFetch?: typeof globalThis.fetch\n}\n\nexport class TheoCloudObservabilityAdapter implements ObservabilityAdapter {\n readonly name = 'theo-cloud'\n private pendingSpans: SpanData[] = []\n private isShutdown = false\n private dropped = 0\n private readonly flushTimer: ReturnType<typeof setInterval>\n private readonly opts: Required<\n Pick<TheoCloudAdapterOptions, 'ingestUrl' | 'token' | 'flushIntervalMs' | 'maxPendingSpans'>\n > &\n TheoCloudAdapterOptions\n\n constructor(options: TheoCloudAdapterOptions) {\n this.opts = { flushIntervalMs: 5000, maxPendingSpans: 10_000, ...options }\n\n // `flushIntervalMs` was accepted and defaulted here and read nowhere — there\n // was no timer in the file. `shutdown()` was the only drain, and nothing\n // called it, so a long-running server exported nothing at all\n // (usetheokit/theokit#353).\n //\n // `unref()` is not optional: a telemetry exporter that pins the event loop\n // turns a clean process exit into a hang, which is worse than the defect it\n // was added to fix.\n this.flushTimer = setInterval(() => {\n void this.flush()\n }, this.opts.flushIntervalMs)\n this.flushTimer.unref()\n }\n\n /** How many spans were dropped because the buffer was full. */\n droppedSpanCount(): number {\n return this.dropped\n }\n\n /**\n * Whether the periodic flush is unref'd. A test seam: \"the timer does not hold\n * the process open\" is otherwise only observable by hanging.\n */\n hasUnrefdFlushTimer(): boolean {\n return !this.flushTimer.hasRef()\n }\n\n startSpan(name: string, attributes?: SpanAttributes, context?: SpanContextInput): SpanHandle {\n if (this.isShutdown) return new NoopSpan()\n const span = new SpanImpl(name, attributes, context)\n return {\n setAttribute: (k, v) => {\n span.setAttribute(k, v)\n },\n setStatus: (s, m) => {\n span.setStatus(s, m)\n },\n end: () => {\n span.end()\n if (this.pendingSpans.length >= this.opts.maxPendingSpans) {\n this.pendingSpans.shift()\n this.dropped++\n }\n this.pendingSpans.push(span.getData())\n },\n }\n }\n\n /* eslint-disable @typescript-eslint/no-empty-function -- metrics not shipped by this adapter yet; an empty body is honest, a fabricated one is not */\n counter(_name: string, _value: number, _attributes?: SpanAttributes): void {}\n histogram(_name: string, _value: number, _attributes?: SpanAttributes): void {}\n log(\n _level: 'debug' | 'info' | 'warn' | 'error',\n _message: string,\n _attributes?: SpanAttributes,\n ): void {}\n /* eslint-enable @typescript-eslint/no-empty-function */\n\n async flush(): Promise<void> {\n if (this.isShutdown || this.pendingSpans.length === 0) return\n\n const spans = this.pendingSpans.splice(0)\n const body = serializeSpansToOtlp(spans) as unknown as BodyInit\n const fetchFn = this.opts._mockFetch ?? globalThis.fetch\n\n try {\n await fetchFn(this.opts.ingestUrl, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n authorization: `Bearer ${this.opts.token}`,\n },\n body,\n })\n } catch (err) {\n // EC-1: log warning, don't throw, don't retry\n console.error(\n `[theokit:observability] flush failed: ${err instanceof Error ? err.message : 'unknown error'}`,\n )\n }\n }\n\n async shutdown(): Promise<void> {\n if (this.isShutdown) return\n // Cleared, not merely ignored: `isShutdown` would make later ticks no-ops,\n // and a live handle on a process that is trying to exit is the thing to\n // remove rather than to tolerate.\n clearInterval(this.flushTimer)\n await this.flush()\n this.isShutdown = true\n }\n}\n","/**\n * Adapter registry — resolves the active observability adapter.\n *\n * Per ADR D457 (v1.1) priority chain:\n * 1. Explicit config (theo.config.ts observability.provider) — ALWAYS wins\n * 2. THEO_CLOUD_INGEST_URL env → theo-cloud adapter\n * 3. NODE_ENV=development → console adapter\n * 4. Fallback → noop adapter\n */\nimport { ConsoleObservabilityAdapter } from './adapters/console.js'\nimport { NoopObservabilityAdapter } from './adapters/noop.js'\nimport { TheoCloudObservabilityAdapter } from './adapters/theo-cloud.js'\nimport type { ObservabilityAdapter } from './adapters/types.js'\n\ninterface ResolveAdapterOptions {\n env: Record<string, string | undefined>\n config?: {\n provider?: ObservabilityAdapter\n }\n}\n\n/**\n * Resolve the observability adapter from config + env.\n * Called once at boot — returns the active adapter for the process lifetime.\n */\nexport function resolveAdapter(options: ResolveAdapterOptions): ObservabilityAdapter {\n // 1. Explicit config ALWAYS wins (EC-4)\n if (options.config?.provider) {\n return options.config.provider\n }\n\n // 2. TheoCloud env vars → theo-cloud adapter\n const ingestUrl = options.env.THEO_CLOUD_INGEST_URL\n const apiKey = options.env.THEO_CLOUD_API_KEY\n if (ingestUrl && apiKey) {\n return new TheoCloudObservabilityAdapter({ ingestUrl, token: apiKey })\n }\n\n // 3. Development → console adapter\n if (options.env.NODE_ENV === 'development') {\n return new ConsoleObservabilityAdapter()\n }\n\n // 4. Fallback → noop\n return new NoopObservabilityAdapter()\n}\n","// T5a.1b — Web Crypto migration. randomUUID() moved to globalThis.crypto.\n// IncomingMessage stays as a type-only import (runtime-clean — TS erases at\n// build); full IncomingMessage→Request boundary migration deferred to a\n// later T5a.1c+ slice per ADR-0028 incremental leaf-first sequence.\nimport type { IncomingMessage } from 'node:http'\n\n/**\n * Phase 7 — Observability: traceId propagation (D7).\n *\n * Extract a stable identifier from incoming requests so a single value\n * correlates the client request, every server log line, the response\n * envelope, and any downstream span. Precedence:\n *\n * 1. `traceparent` (W3C Trace Context — `00-{32-hex}-{16-hex}-{flags}`)\n * 2. `x-request-id` (Heroku / GCP / generic proxy header)\n * 3. Generated UUID (fresh per request)\n *\n * UUIDs are accepted as trace identifiers by every major vendor that\n * does not enforce strict 32-hex (Datadog, Honeycomb, Sentry, Logflare,\n * Axiom, etc). We don't need ULIDs to ship this surface.\n */\n\nexport const TRACE_HEADER = 'x-trace-id'\nexport const TRACE_PARENT_HEADER = 'traceparent'\nconst REQUEST_ID_HEADER = 'x-request-id'\n\n// W3C Trace Context: 00-<trace-id 32 hex>-<span-id 16 hex>-<flags 2 hex>\nconst TRACEPARENT_RE = /^00-([0-9a-f]{32})-([0-9a-f]{16})-[0-9a-f]{2}$/\n\n/**\n * What a valid `traceparent` says: the trace to join, and the caller's span\n * inside it.\n *\n * Both halves are on the wire and only the first was ever read, so a span this\n * process opened for an incoming request became a SECOND root of the caller's\n * trace instead of a child of the caller's span — the trace correlated and the\n * waterfall lost its shape (usetheokit/theokit#385).\n */\nexport interface W3CTraceContext {\n /** The trace this request belongs to. 32 hex chars. */\n readonly traceId: string\n /**\n * The caller's span, which a span opened for this request hangs under.\n *\n * Absent when the caller sent the reserved all-zero parent id, which W3C\n * defines as \"no parent\" rather than as a span to point at.\n */\n readonly parentSpanId?: string\n}\n\n/**\n * Parse a W3C Trace Context `traceparent` header value into the trace it names\n * and the caller's span within it. `null` when the value is not a well-formed\n * `traceparent` or names the reserved all-zero trace.\n */\nexport function parseTraceparentContext(value: string): W3CTraceContext | null {\n if (!value) return null\n const m = TRACEPARENT_RE.exec(value)\n if (!m) return null\n const traceId = m[1]\n // W3C: trace-id of all zeroes is invalid by spec\n if (/^0+$/.test(traceId)) return null\n const parentSpanId = m[2]\n // Same rule one field over: an all-zero parent-id is the spec's way of saying\n // there is no parent, so it is dropped rather than exported as a span id no\n // backend can resolve.\n return /^0+$/.test(parentSpanId) ? { traceId } : { traceId, parentSpanId }\n}\n\n/**\n * Parse a W3C Trace Context `traceparent` header value. Returns the\n * 32-hex trace-id when valid (and not the reserved all-zeros), else\n * `null`.\n */\nexport function parseTraceparent(value: string): string | null {\n return parseTraceparentContext(value)?.traceId ?? null\n}\n\n/**\n * Shape a correlation id must have to be trusted: printable, unpunctuated beyond\n * the separators real id formats use, and bounded.\n *\n * `x-request-id` is chosen by the caller and ends up in the structured logs an\n * operator reads. Unvalidated, a newline in it splits one log line into two with\n * the second forged, and a megabyte of it is a megabyte per request through the\n * whole log pipeline (usetheokit/theokit#353). The character set covers what real\n * id formats use — UUID, ULID, hex, dotted and colon-separated ids — and excludes\n * whitespace and control characters, which no id format needs and every injection\n * does.\n *\n * 128 is comfortably above any of those formats and far below a payload.\n */\nconst REQUEST_ID_RE = /^[A-Za-z0-9_.:-]{1,128}$/\n\nfunction isTrustedRequestId(value: string): boolean {\n return REQUEST_ID_RE.test(value)\n}\n\n/**\n * Pick the first TRUSTED string value out of an IncomingMessage header. Node\n * collapses repeated headers into arrays; proxies sometimes do this for\n * `x-request-id`. Empty strings count as absent.\n *\n * \"First trusted\" rather than \"first non-empty\": taking the first value and\n * validating afterwards would let a proxy prepending a hostile value defeat a\n * good one sitting behind it.\n */\nfunction pickHeader(\n value: string | string[] | undefined,\n isTrusted: (candidate: string) => boolean = () => true,\n): string | null {\n if (Array.isArray(value)) {\n for (const v of value) {\n if (typeof v === 'string' && v.length > 0 && isTrusted(v)) return v\n }\n return null\n }\n if (typeof value === 'string' && value.length > 0 && isTrusted(value)) return value\n return null\n}\n\n/**\n * T5a.2 Phase C slice 1/2 — pure traceId resolution from pre-extracted\n * header values. Shared between IncomingMessage and Web Request wrappers.\n */\nfunction resolveTraceIdFromHeaders(traceparent: string | null, requestId: string | null): string {\n if (traceparent !== null) {\n const parsed = parseTraceparent(traceparent)\n if (parsed !== null) return parsed\n }\n if (requestId !== null) return requestId\n return globalThis.crypto.randomUUID()\n}\n\n/**\n * Resolve the request's traceId following the precedence above.\n */\nexport function extractTraceId(req: IncomingMessage): string {\n return resolveTraceIdFromHeaders(\n pickHeader(req.headers[TRACE_PARENT_HEADER]),\n pickHeader(req.headers[REQUEST_ID_HEADER], isTrustedRequestId),\n )\n}\n\n/**\n * T5a.2 Phase C slice 1/2 — Web-Standards-shaped traceId resolver.\n *\n * Mirror of `extractTraceId(req: IncomingMessage)` for the Web `Request`\n * shape. Same precedence (`traceparent` → `x-request-id` → generated\n * UUID). Uses `request.headers.get(name)` (native Web `Headers` API)\n * instead of the Node indexer.\n *\n * **Multi-value note:** Web `Headers` collapses repeated headers into a\n * single comma-separated string at parse. The IncomingMessage path's\n * `pickHeader` \"first non-empty value\" semantic is naturally satisfied\n * because there's no array to pick from on the Web side — `.get()`\n * returns the comma-joined value, which for `traceparent` / `x-request-id`\n * is treated as a single string anyway (both headers are conventionally\n * single-valued).\n */\n/**\n * The request's W3C trace context — the trace to join and the caller's span\n * within it — or `undefined` when the caller supplied no usable `traceparent`.\n *\n * Deliberately narrower than {@link extractTraceIdFromRequest}, which always\n * returns something and may return an `x-request-id` or a generated UUID. Those\n * are fine as a log correlation key and are NOT trace ids: OTLP wants 32 hex\n * characters, and a dashed UUID exported as a `traceId` is a malformed span.\n *\n * So a span-emitting caller asks this question instead — \"is there a real trace\n * to join?\" — and mints its own when the answer is no (usetheokit/theokit#368).\n *\n * It answers with the whole context rather than the trace id alone, because a\n * span opened for this request belongs in the caller's trace AND under the\n * caller's span. Returning only the first half is what made one request arrive\n * as a trace with two roots (usetheokit/theokit#385).\n */\nexport function extractW3CTraceContext(request: Request): W3CTraceContext | undefined {\n const traceparent = request.headers.get(TRACE_PARENT_HEADER)\n if (traceparent === null) return undefined\n return parseTraceparentContext(traceparent) ?? undefined\n}\n\nexport function extractTraceIdFromRequest(request: Request): string {\n const requestId = request.headers.get(REQUEST_ID_HEADER)\n return resolveTraceIdFromHeaders(\n request.headers.get(TRACE_PARENT_HEADER),\n // Same policy on both resolvers. A validation living on one side only is the\n // gap an attacker picks the other transport to reach.\n requestId !== null && isTrustedRequestId(requestId) ? requestId : null,\n )\n}\n","/**\n * One request, one trace — the side table both consumers read (usetheokit/theokit#404).\n *\n * ## The defect this exists to remove\n *\n * Two places decide what trace a request belongs to: the observability plugin, which opens the\n * `http.request` span, and `observeServedRun`, which opens `agent.run`. Both used to answer the\n * question the same way and *independently* — by reading the inbound `traceparent` header. That\n * agrees only while the header is there. A browser sends none, and neither does `curl` or an\n * uninstrumented `fetch`, so on the majority path each side took its own `?? newTraceId()` branch\n * and one request reached the collector as two disconnected traces, neither naming the other.\n *\n * Reading the same header is not sharing. This module is the sharing: the request's trace is\n * resolved ONCE, on first ask, and every later ask gets the same answer.\n *\n * ## Why a `WeakMap` keyed on the `Request`\n *\n * The two consumers already hold the same `Request` object — `serveThroughPluginLifecycle` builds\n * it once and hands that instance both to the hooks (as `PluginContext.request`) and to the\n * handler that calls `mountAgent`. So the request itself is already the shared thing, and a side\n * table keyed on it needs no new parameter threaded through `mountAgent`, no decoration contract,\n * and no signature change on either side.\n *\n * The two alternatives were weighed and rejected for reasons that outlive this comment:\n *\n * - `AsyncLocalStorage` is what an OTel Node SDK does, and it would work here — but it imports\n * `node:async_hooks`, and `server/` holds a no-`node:*` invariant precisely so the same code\n * serves the Web, Tauri and TUI targets (`docs/program/three-target-parity.md`). A `WeakMap` is plain\n * ECMAScript and runs unchanged on every one of them.\n * - Threading a resolved context through `mountAgent`'s options is explicit, and it puts a\n * telemetry concern in the signature of every route that might one day open a span. The entry\n * above is the same value, reachable without the parameter.\n *\n * Keying on the object also disposes of the entry: the record dies with the request, with no cap,\n * no eviction and no leak — unlike the plugin's own span map, which is keyed by `requestId` and\n * needs `evictUntilRoom` for exactly that reason.\n *\n * ## What this does NOT do\n *\n * It does not open a span, and it does not claim one was opened. `outermostSpanId` stays unset\n * until something actually emits the request's outermost span, so a run with no `http.request`\n * span in scope stays the root of its own trace rather than naming a parent this process never\n * emitted. A dangling parent reads as a span that was lost in transit, which is a worse report\n * than an honest root.\n */\nimport { extractW3CTraceContext } from '../http/trace-context.js'\n\nimport { newTraceId } from './trace-context-propagation.js'\n\nexport interface RequestTrace {\n /** The trace every span caused by this request belongs to. 32 hex chars. */\n readonly traceId: string\n /** The CALLER's span, when the caller sent a well-formed `traceparent`. */\n readonly parentSpanId?: string\n /**\n * The outermost span THIS process opened for the request, once one has been opened.\n *\n * Undefined means nothing opened one — no observability plugin on this path, or a run started\n * outside an HTTP turn. Consumers treat it as \"no local parent\", never as an id to point at.\n */\n outermostSpanId?: string\n}\n\nconst resolved = new WeakMap<Request, RequestTrace>()\n\n/**\n * The request's trace, resolved once and memoized on the request itself.\n *\n * Idempotent by construction: the first caller decides — from the inbound `traceparent` when there\n * is one, from a fresh mint when there is not — and every caller after it, on either side of the\n * request, is handed that same decision.\n */\nexport function requestTrace(request: Request): RequestTrace {\n const existing = resolved.get(request)\n if (existing !== undefined) return existing\n\n const trace = resolve(request)\n resolved.set(request, trace)\n return trace\n}\n\n/** Decide a request's trace from what it carries. Called once per request, by `requestTrace`. */\nfunction resolve(request: Request): RequestTrace {\n const inbound = extractW3CTraceContext(request)\n if (inbound === undefined) return { traceId: newTraceId() }\n if (inbound.parentSpanId === undefined) return { traceId: inbound.traceId }\n return { traceId: inbound.traceId, parentSpanId: inbound.parentSpanId }\n}\n\n/**\n * Record the span this process opened as the request's outermost one, so spans opened later in the\n * same request hang under it instead of beside it.\n *\n * First writer wins. A second outermost span for one request is a bug in the caller, and silently\n * re-pointing every later child at it would hide that bug behind a plausible waterfall.\n */\nexport function recordOutermostSpan(request: Request, spanId: string): void {\n const trace = requestTrace(request)\n trace.outermostSpanId ??= spanId\n}\n","/**\n * Auto-instrumentation plugin — one span per HTTP request.\n *\n * Per blueprint Pattern 2 (Hono middleware timing):\n * - onRequest: start span with method + path\n * - onResponse: end span with status + duration\n * - onError: set span status to error\n *\n * ## It used to be unregistrable, and nothing said so\n *\n * This returned `{ name, onRequest, onResponse, onError }` against a plugin\n * contract of `{ name, register }`, so the obvious wiring — putting it in\n * `config.plugins` — threw `InvalidPluginShapeError` at boot\n * (`../plugins/load-plugins.ts:20`). Its own context type was a second, narrower\n * invention: `request: { method, url }` where the real hook receives a Web\n * `Request` with an ABSOLUTE url, and `response?: { statusCode }` where the real\n * one is a `ServerResponse`. The tests passed because they called the hooks\n * directly with the invented shape, which verified the implementation and never\n * the contract (usetheokit/theokit#353).\n *\n * ## Span state is per-instance, and bounded\n *\n * The active-span map used to be a module-level `Map` with no cap and no TTL.\n * Two consequences, both real once anything registered this:\n *\n * - two plugin instances shared one map, so one app's response closed\n * another's span;\n * - a request whose `onResponse` never fires leaked its span forever. That is\n * not an edge case here: the SSE path is exactly where `onResponse` does not\n * fire on stream open, and an agent run is the longest-lived stream the\n * framework serves.\n *\n * The map now lives in the closure and is capped. An evicted span is ENDED with\n * an `span.abandoned` attribute rather than dropped, so it still reaches the\n * exporter carrying the reason it was cut — trading a memory leak for a silent\n * hole in the trace would be the worse bargain.\n */\nimport type { PluginContext, PluginErrorContext, TheoApp, TheoPlugin } from '../plugin-types.js'\n\nimport type { ObservabilityAdapter, SpanContextInput, SpanHandle } from './adapters/types.js'\nimport { recordOutermostSpan, requestTrace } from './request-trace.js'\nimport { newSpanId } from './trace-context-propagation.js'\n\n/**\n * How many requests may be in flight with an unclosed span before the oldest is\n * force-ended. Sized to be far above any real concurrent-request count on a\n * single Node process, so eviction signals a leak rather than back-pressure.\n */\nconst DEFAULT_MAX_ACTIVE_SPANS = 1024\n\nexport interface ObservabilityPluginOptions {\n /** Override the in-flight span cap. Mainly a test seam. */\n maxActiveSpans?: number\n}\n\nexport function createObservabilityPlugin(\n adapter: ObservabilityAdapter,\n options: ObservabilityPluginOptions = {},\n): TheoPlugin {\n const maxActiveSpans = options.maxActiveSpans ?? DEFAULT_MAX_ACTIVE_SPANS\n const activeSpans = new Map<string, SpanHandle>()\n\n /** Force-end the oldest spans until there is room for one more. */\n function evictUntilRoom(): void {\n while (activeSpans.size >= maxActiveSpans) {\n const oldest = activeSpans.keys().next()\n if (oldest.done === true) return\n const span = activeSpans.get(oldest.value)\n activeSpans.delete(oldest.value)\n if (span === undefined) continue\n span.setAttribute('span.abandoned', true)\n span.setStatus('error', 'span abandoned: onResponse never fired for this request')\n span.end()\n }\n }\n\n /**\n * Where the request's span sits: in the request's trace — the caller's when the caller sent a\n * `traceparent` (usetheokit/theokit#385), a freshly minted one otherwise — under the caller's\n * span when there is one.\n *\n * This is the OUTERMOST span of a request, which makes it precisely the caller\n * `startSpan`'s optional `context` was written for — and precisely the caller\n * that went on passing the two-argument form. The consequence was worse than\n * \"the HTTP span is a root\": once `mountAgent` learned to continue an incoming\n * trace, one request that ran an agent reached the collector as TWO\n * disconnected traces, and the caller's trace id was present on the HTTP span\n * as the `requestId` attribute rather than as its `traceId` — resolved,\n * carried, and written to a field no tracing backend correlates on.\n *\n * No `traceparent` (or a malformed one) mints a fresh trace, which is the\n * correct answer for a request nothing upstream traced — and it is minted by\n * `requestTrace`, ONCE, so the run joins that trace instead of minting a\n * second one of its own (usetheokit/theokit#404). The extraction there\n * deliberately refuses `x-request-id` and dashed UUIDs: those are fine\n * correlation keys and are not trace ids.\n */\n function requestSpanContext(request: Request): SpanContextInput {\n const trace = requestTrace(request)\n // Pinned rather than left to the adapter, because this id is what everything else the request\n // causes hangs under — `SpanContextInput.spanId` exists for exactly this caller. Recording it\n // is what makes the run a child instead of a second root (usetheokit/theokit#404).\n const spanId = newSpanId()\n recordOutermostSpan(request, spanId)\n return trace.parentSpanId === undefined\n ? { traceId: trace.traceId, spanId }\n : { traceId: trace.traceId, spanId, parentSpanId: trace.parentSpanId }\n }\n\n /** Take the span for a request, if one is still open. */\n function claim(requestId: string): SpanHandle | undefined {\n const span = activeSpans.get(requestId)\n if (span === undefined) return undefined\n activeSpans.delete(requestId)\n return span\n }\n\n return {\n name: 'theokit:observability',\n\n register(app: TheoApp): void {\n app.addHook('onRequest', (ctx: PluginContext) => {\n evictUntilRoom()\n const span = adapter.startSpan(\n 'http.request',\n {\n method: ctx.request.method,\n // `ctx.request.url` is absolute, because that is what a Web `Request`\n // carries (`plugin-types.ts` says so, at length, for this reason).\n path: new URL(ctx.request.url).pathname,\n requestId: ctx.requestId,\n },\n requestSpanContext(ctx.request),\n )\n activeSpans.set(ctx.requestId, span)\n })\n\n app.addHook('onResponse', (ctx: PluginContext) => {\n const span = claim(ctx.requestId)\n if (span === undefined) return\n\n const status = ctx.response.statusCode\n span.setAttribute('status', status)\n span.setStatus('ok')\n span.end()\n\n // Deliberately no `path` label: a counter is an aggregated series, and\n // a dynamic route would mint one series per id.\n adapter.counter('http.requests', 1, { method: ctx.request.method, status })\n })\n\n app.addHook('onError', (ctx: PluginErrorContext) => {\n const span = claim(ctx.requestId)\n if (span === undefined) return\n\n const message = ctx.error instanceof Error ? ctx.error.message : 'unknown error'\n span.setAttribute('error', true)\n span.setStatus('error', message)\n span.end()\n\n adapter.counter('http.errors', 1, { method: ctx.request.method })\n })\n },\n }\n}\n","/**\n * Build the observability plugin from `theo.config.ts > observability` + the\n * environment (#353).\n *\n * Until this existed, `createObservabilityPlugin` had no production caller and\n * `startSpan` was invoked in exactly one production file — the one nothing\n * called. Every adapter, the OTLP serializer and the span implementation were\n * tested, published and unreachable: the framework emitted no spans at all.\n *\n * ## When the plugin is wired, and one deliberate divergence\n *\n * `adapter-registry.ts` documents a four-step chain: explicit config, then\n * TheoCloud env vars, then `NODE_ENV=development` → console, then noop. This\n * function honours the first two and does NOT treat the third as an opt-in.\n *\n * The reason is that step 3 would turn telemetry on for every `theo dev` that\n * never asked for it, and the console adapter writes JSON lines to `stderr`\n * (`adapters/console.ts:23`) — the same stream `observability/logger.ts` already\n * writes a different JSON shape to. Two interleaved formats on one stream is a\n * downgrade for every developer, bought in exchange for telemetry nobody\n * requested.\n *\n * So: `observability: {}` in the config turns it on, and in dev that still\n * resolves to the console adapter exactly as the chain says. What changes is\n * that `NODE_ENV=development` alone no longer counts as asking.\n *\n * Returns `undefined` when nothing asked for telemetry, which preserves the\n * zero-plugin path for applications that configure none.\n */\nimport { observabilitySchema } from '../config/schemas/index.js'\n\nimport { resolveAdapter } from './observability/adapter-registry.js'\nimport type { ObservabilityAdapter } from './observability/adapters/types.js'\nimport { warnOnce } from './observability/logger.js'\nimport { createObservabilityPlugin } from './observability/middleware.js'\nimport type { TheoPlugin } from './plugin-types.js'\n\n/**\n * The adapter resolved at boot, for callers that are not the HTTP plugin.\n *\n * An agent run is the case that forced this: its spans are produced by\n * `observeAgentRun` from the wire chunk stream, far from the request hooks, and\n * it must be the SAME adapter — two independently resolved adapters mean two\n * exporters and two half-complete pictures of one run.\n *\n * `undefined` when nothing asked for telemetry, which is what keeps the\n * zero-cost path zero-cost.\n */\nlet activeAdapter: ObservabilityAdapter | undefined\n\nexport function getObservabilityAdapter(): ObservabilityAdapter | undefined {\n return activeAdapter\n}\n\n/** Test-only: forget the boot-resolved adapter. Production code must not call this. */\nexport function _resetObservabilityAdapter(): void {\n activeAdapter = undefined\n}\n\nexport function createObservabilityPluginFromConfig(\n observabilityConfig: unknown,\n env: Record<string, string | undefined>,\n): TheoPlugin | undefined {\n const parsed =\n observabilityConfig === undefined || observabilityConfig === null\n ? undefined\n : observabilitySchema.parse(observabilityConfig)\n\n if (parsed?.enabled === false) return undefined\n\n // An ingest URL plus a key is an explicit deployment decision, so it opts in\n // on its own — that is what makes the env half of the documented chain reachable\n // without a config file.\n const cloudConfigured = Boolean(env.THEO_CLOUD_INGEST_URL) && Boolean(env.THEO_CLOUD_API_KEY)\n if (parsed === undefined && !cloudConfigured) return undefined\n\n const adapter = resolveAdapter({\n env,\n config: { provider: parsed?.provider as ObservabilityAdapter | undefined },\n })\n\n // The registry never fails; it falls back to noop. Wiring a plugin whose every\n // hook is a no-op would cost a runner on the request path and buy nothing.\n if (adapter.name === 'noop') {\n // Silence here is the defect. An application that WROTE `observability: {}`\n // asked for telemetry, and returning quietly gives it a passing boot, no\n // spans, and nothing to search for — the config-validates-and-does-nothing\n // shape usetheokit/theokit#321 recorded for `rateLimit`.\n //\n // Only when it was asked for: an application that configured nothing is not\n // owed a warning about a thing it never requested.\n if (parsed !== undefined) {\n warnOnce('observability.no_exporter', {\n event: 'observability.no_exporter',\n message:\n 'observability is configured but no exporter resolved, so no spans will be recorded. ' +\n 'Set THEO_CLOUD_INGEST_URL and THEO_CLOUD_API_KEY, or pass observability.provider with ' +\n 'your own adapter. In development, NODE_ENV=development resolves the console exporter.',\n })\n }\n return undefined\n }\n\n activeAdapter = adapter\n return createObservabilityPlugin(adapter)\n}\n"],"mappings":"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","names":[]}
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
deployedCorsFragment,
|
|
5
5
|
deployedCsrfFragment,
|
|
6
6
|
securityHeadersDeclarations
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-SEGD3JUC.js";
|
|
8
8
|
|
|
9
9
|
// src/adapters/deployed-preamble.ts
|
|
10
10
|
function deployedEntryPreamble(runtimeConfig, agentsFragment, opts, host) {
|
|
@@ -24,4 +24,4 @@ function deployedEntryPreamble(runtimeConfig, agentsFragment, opts, host) {
|
|
|
24
24
|
export {
|
|
25
25
|
deployedEntryPreamble
|
|
26
26
|
};
|
|
27
|
-
//# sourceMappingURL=chunk-
|
|
27
|
+
//# sourceMappingURL=chunk-R36YNWBU.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/adapters/deployed-agents.ts","../src/adapters/deployed-cors.ts","../src/adapters/deployed-csrf.ts","../src/adapters/security-headers.ts","../src/adapters/deployed-rate-limit.ts","../src/adapters/deployed-runtime-config.ts","../src/adapters/deployed-trace.ts"],"sourcesContent":["/**\n * Serving an agent from a generated deploy entry (usetheokit/theokit#367).\n *\n * ## The gap this closes\n *\n * `grep -rc \"agent\" packages/theo/src/adapters/*.ts` used to return nothing across 14 files. The\n * notion did not exist in this layer at all: every generated entry routes `/api/` exclusively\n * through `scanServerRoutes` + `executeRoute`, and an agent is a DIFFERENT scan served by a\n * DIFFERENT function — `scanAgents` + `mountAgent`. So `/api/agents/chat` matched no file route and\n * fell into `notFoundResponse()` on every target.\n *\n * For a framework whose stated reason to exist is \"the agent is a file, delivered by the same\n * pipeline that serves the page\", that is the gap which contradicts the sentence: the agent was\n * delivered by no pipeline at all outside a machine running `theokit start`.\n *\n * ## Why baking, and why `../../`\n *\n * A Worker has no filesystem to scan and no path to `import()`, which is the same reason routes are\n * baked (#369). The agent modules are resolved on the build machine and emitted as static imports,\n * relative to the entry's directory — every target that can do this writes two levels below the\n * project root, so the arithmetic is the one `renderBakedRoutes` already does.\n *\n * ## Which targets can do it\n *\n * Only the ones whose output is BUNDLED from the project: `cloudflare` (wrangler), `bun` and\n * `deno-deploy` resolve the emitted import against the app's own source tree. `vercel`, `netlify`\n * and `aws-lambda` receive a standalone function directory that never sees the app's modules, so\n * an agent cannot travel there by this road at all. Same split as plugins (#425), same cause.\n *\n * ## The table is keyed by NAME\n *\n * The URL carries the name, the access policy is judged under the name, and the run's spans are\n * labelled with it (#406). Keying by file path would make the lookup depend on the server's\n * directory layout — which changes per deploy, and which is exactly the value #406 removed from\n * telemetry for the same reason.\n */\n\n/** One agent, as `scanAgents` reports it. Structural, so the adapters need no `server/` import. */\nexport interface DeployedAgent {\n /** Path relative to the project root — the specifier the static import uses. */\n readonly filePath: string\n /** The URL the agent answers on. */\n readonly agentPath: string\n /** The agent's NAME — what the URL carries, the policy is judged under, and spans are labelled. */\n readonly name: string\n}\n\nexport interface DeployedAgentsFragment {\n /** Top-level imports of the agent modules. */\n readonly imports: string[]\n /** Module-scope declarations — the name→module table. */\n readonly declarations: string[]\n /** The request-handler branch, to be emitted before the file-route table is consulted. */\n readonly branch: string[]\n /**\n * SI-020 — the condition a host ANDs into its non-API early-return guard, so an agent card path\n * is not handed to static assets before this fragment's branch is reached.\n *\n * Empty string when the fragment emits nothing, so a host that interpolates it unconditionally\n * never references a name that was not declared.\n */\n readonly hostBypass: string\n}\n\nconst EMPTY: DeployedAgentsFragment = { imports: [], declarations: [], branch: [], hostBypass: '' }\n\n/**\n * How the host entry names the things this branch has to use.\n *\n * The fragment is generated code injected into three entries that do NOT agree on their own\n * vocabulary — Bun reads `pathname`, Cloudflare reads `url.pathname`; Deno calls `notFound()`,\n * Cloudflare calls `notFoundResponse()`; Cloudflare wraps each branch in the security baseline\n * while Bun and Deno wrap once at the caller; Deno resolves npm packages with an `npm:` prefix.\n *\n * Naming those differences is what keeps this ONE fragment. The alternative — three near-identical\n * branches, one per adapter — is the copy that `serveThroughPluginLifecycle` was extracted to undo\n * after it had drifted five ways (#405).\n */\n/**\n * How the target REACHES its agent modules — and the two answers are genuinely different, not a\n * copy waiting to be merged.\n *\n * A Worker has no filesystem: its agents must be resolved on the build machine and emitted as\n * static imports, which is what `renderBakedRoutes` already does for routes there. Bun and Deno DO\n * have a filesystem and already scan their routes at request time; making them bake would couple an\n * agent's existence to a rebuild for no gain. The split mirrors the one routes already have on\n * exactly these targets, which is the argument for keeping it.\n */\nexport type DeployedAgentsSource =\n | {\n readonly kind: 'baked'\n /** Scanned on the build machine, emitted as static imports. */\n readonly agents: readonly DeployedAgent[]\n /**\n * B-185 — the app's `server/context.ts`, as a specifier relative to the emitted entry, or\n * `undefined` when the app has none. A Worker has no filesystem on which to find it, so it is\n * baked like the agent modules beside it (ADR 0014). `undefined` is the ordinary case for an\n * app that declares no context, and emitting an import of a file that is not there would\n * fail the BUILD rather than the request — which is why `planDeployedPlugins` takes the same\n * road for plugins.\n */\n readonly contextModule?: string\n }\n | {\n readonly kind: 'scan'\n /** Expression yielding the project root at runtime. */\n readonly projectRoot: string\n /** Expression yielding the module loader the entry already builds. */\n readonly loadModule: string\n /**\n * A statement that guarantees `loadModule` is usable, for a host that builds its loader\n * lazily. Deno's `loaderCache` is created on the route path, which runs AFTER this branch —\n * so without this the agent branch would call `null`.\n */\n readonly ensureLoader?: string\n /** The configured agents directory as a quoted literal. Absent ⇒ `scanAgents` defaults to `agents`. */\n readonly agentsDirLiteral?: string\n /**\n * B-185 — an expression yielding the app's `server/` directory, which this host already\n * declares (`bun.ts:95`, `deno-deploy.ts:69`) and already hands to `executeRoute`. A host\n * with a filesystem locates its own `context.ts`, so it needs no baking (ADR 0014, and the\n * split `deployed-agents.ts` already argues for routes two docblocks above).\n */\n readonly serverDir?: string\n }\n\nexport interface DeployedAgentsHost {\n /** Expression yielding the request path inside the handler. Default `url.pathname`. */\n readonly pathname?: string\n /** Call producing the 404 response. Default `notFoundResponse()`. */\n readonly notFound?: string\n /** Whether THIS branch applies the security baseline, or the caller already does. */\n readonly wrapSecurityHeaders?: boolean\n /** Prefix for bare package specifiers. Deno needs `npm:`; the others need nothing. */\n readonly importPrefix?: string\n /**\n * Expression yielding the entry's plugin runner, or `undefined` when the app declared no plugins\n * and the build emitted no module to bind.\n *\n * `deployedRuntimeConfigFragment` already declares `const THEO_PLUGIN_RUNNER` at module scope and\n * spreads `pluginRunner: await THEO_PLUGIN_RUNNER` into `executeRoute`; an adapter passes that\n * same expression here rather than building a second runner, because a runner rebuilt per request\n * re-runs every plugin's `register` -- which is where a plugin allocates the state its hooks read.\n */\n readonly pluginRunnerExpr?: string\n}\n\n/**\n * The two `AuxRouteDeps` fields a deployed entry must supply to match what dev already does.\n *\n * Both dev callers pass them — `vite-plugin/agent-middleware.ts:161,164` and\n * `cli/commands/start/handlers.ts:226,229` — and a first draft of this fragment passed neither,\n * on a quotation from `serve-aux-routes.ts:83-85` that names three routes (\"card, approvals,\n * stream\") while the dispatcher serves six. Two reviewers measured the same two consequences:\n *\n * - `csrfMode` unset defaults to `'strict'` (`serve-aux-routes.ts:379`), so an app declaring\n * `security: { csrf: 'off' }` got `off` on `POST /api/agents/<name>` and `strict` on its `/mcp`\n * sibling one line away — verbatim the divergence `deployed-csrf.ts`'s own header exists to\n * record, re-created on a route this slice had just made reachable.\n * - `resolveApiKey` unset makes the thread follow-up answer a permanent 501 on every deploy\n * target (`serve-aux-routes.ts:359-365`), naming a framework-internal parameter to the caller.\n *\n * Both values are already in scope: `CSRF_CONFIG` is spread into `mountAgent` in the same emitted\n * function, and `resolveProvider` is already on its import line.\n */\nconst AUX_DEPS_PARITY = [\n ` ...CSRF_CONFIG,`,\n ` resolveApiKey: (model, plugins) => resolveProvider(model, { plugins }).apiKey,`,\n] as const\n\n/** The prefix the agent convention owns. Matches `handlers.ts`'s own `tryServeAgent`. */\nconst AGENT_PREFIX = '/api/agents/'\n\n/**\n * The second shape the aux dispatcher owns. `matchGetAuxRoute` serves M15's\n * `GET /.well-known/<name>/agent-card.json`, and a guard admitting only {@link AGENT_PREFIX} left\n * five of the dispatcher's six families reachable on a deploy target and excluded the sixth — by\n * the guard, not by a decision, and silently.\n *\n * A prefix rather than the matcher's own `isAgentCardPath`: the guard exists to keep a url nobody\n * answers from paying for anything, and importing the matcher's predicate into every emitted entry\n * would buy a narrower pre-filter at the cost of a module the branch already declines without.\n */\nconst WELL_KNOWN_PREFIX = '/.well-known/'\n\n/**\n * What a deployed entry needs in order to serve the app's agents.\n *\n * @param agents - the agents scanned on the build machine. Empty emits nothing at all.\n */\n/**\n * The scan source every filesystem host with a lazily-created loader shares.\n *\n * Four adapters — `netlify`, `vercel`, `aws-lambda`, `deno-deploy` — emit an entry that resolves\n * `serverDir` itself, caches its loader in `loaderCache`, and creates that cache on first use.\n * They therefore hand this fragment the SAME six fields, and writing them out four times is the\n * copy `serveThroughPluginLifecycle` was extracted to undo after it had drifted five ways (#405).\n *\n * B-235 is what made it four: wiring three more targets duplicated the block three more times, and\n * the duplication gate on new code is what said so — 4.2% against a 3% ceiling. The gate was right\n * and the fix is the one it implies, not a threshold.\n *\n * `bun` is deliberately NOT a caller. It names its loader `loadModule` and creates it eagerly, so\n * it has no `ensureLoader` — a parameter to cover that difference would make this function a\n * switch over its callers, which is the shape that drifts.\n */\nexport function scannedFromLoaderCache(agentsDirLiteral?: string): DeployedAgentsSource {\n return {\n kind: 'scan',\n projectRoot: 'cwd',\n agentsDirLiteral,\n loadModule: 'loaderCache',\n serverDir: 'serverDir',\n ensureLoader: 'if (!loaderCache) loaderCache = createProductionLoader()',\n }\n}\n\n/**\n * The JSON 404 `vercel` and `aws-lambda` answer a routing miss with.\n *\n * An expression rather than a call, because the fragment interpolates `notFound` into a `return`\n * and neither host declares a helper for it.\n */\nexport const JSON_NOT_FOUND_RESPONSE = `new Response(JSON.stringify({ error: { code: 'NOT_FOUND' } }), { status: 404, headers: { 'content-type': 'application/json' } })`\n\nexport function deployedAgentsFragment(\n source: DeployedAgentsSource | undefined,\n host: DeployedAgentsHost = {},\n): DeployedAgentsFragment {\n if (source === undefined) return EMPTY\n if (source.kind === 'baked' && source.agents.length === 0) return EMPTY\n\n const pathname = host.pathname ?? 'url.pathname'\n const notFound = host.notFound ?? 'notFoundResponse()'\n const prefix = host.importPrefix ?? ''\n\n const resolution =\n source.kind === 'baked'\n ? bakedResolution(source.agents, source.contextModule, host.pluginRunnerExpr)\n : scannedResolution({ ...source, pluginRunnerExpr: host.pluginRunnerExpr })\n\n return {\n hostBypass: ` && !__theoIsAgentCardPath(${pathname})`,\n imports: [\n // `theokit/adapters/agent-mount`, not `theokit/server`: `mount-agent` is deliberately not on\n // the app-facing surface (ADR 0041), and a generated entry is not an app. See that module.\n // B-185 — the identity entry differs by host: a filesystem host LOCATES its own\n // `context.ts`, a Worker is handed the baked factory (ADR 0014). `createWebShim` is not here\n // because all three entries already import it at module level.\n `import { mountAgent, resolveProvider, matchAgentAuxRoute, serveMatchedAuxRoute${resolution.identityImport}${source.kind === 'scan' ? ', scanAgents' : ''} } from '${prefix}theokit/adapters/agent-mount'`,\n ...resolution.imports,\n ],\n declarations: [\n ...resolution.declarations,\n // SI-020 — the host's `/api/` guard decides between the API surface and static assets, and it\n // runs BEFORE this branch. Widening the branch guard alone left the `.well-known` arm as dead\n // code on all three targets: present in the source, unreachable in the emitted program. The\n // host consults this predicate, so the card path escapes the asset branch and NOTHING else\n // under `/.well-known/` does — a blanket prefix bypass would route `/.well-known/security.txt`\n // away from assets, trading an unreachable card for a broken namespace.\n //\n // Same shape as `agent-card-handler.ts`'s own WELL_KNOWN. That is a COPY, not a shared\n // definition, and saying otherwise was the claim an inventory judge falsified: changing the\n // handler to serve `/agent.json` left every card test green, because this guard and that\n // matcher are checked separately and never against each other. The drift that matters here —\n // this guard narrowing while the matcher still serves the path — is caught by\n // `a-deployed-worker-reaches-the-agent-card.test.ts`, which drives a real request. The other\n // direction is not, and is the honest limit of this line.\n String.raw`const __theoIsAgentCardPath = (p) => /^\\/\\.well-known\\/[^/]+\\/agent-card\\.json$/.test(p)`,\n // B-185 — one factory, two call sites: the aux branch on a hit, and the run handler below.\n // Declared rather than inlined twice so the mechanism ADR 0014 decides has a single home.\n // `async` because the entry's plugin runner is a promise: `createPluginRunnerFromConfig` is\n // async, so the runtime-config fragment declares `const THEO_PLUGIN_RUNNER = createPluginRunnerFromConfig(...)`\n // WITHOUT awaiting it, and every consumer awaits at the point of use. `await` is a reserved\n // word everywhere in an ES module, so a non-async factory carrying that binding does not\n // merely misbehave -- the entry does not parse, and `tests/unit/adapter-entry-parses.test.ts`\n // is where that is caught. It went red at HEAD before this line existed, on a first version\n // of this fix whose own test asserted `toContain('await THEO_PLUGIN_RUNNER')`: the presence\n // of the token that breaks the parse. That test file's header says why, in its own words --\n // `toContain` does not care whether the string is a program.\n //\n // Laziness is unaffected. The shim was always built eagerly inside this factory; what ADR-1\n // refuses is paying for a url nobody answers, and neither call site is reached by one.\n `async function __theoResolveSubject(request) {`,\n ...resolution.identity,\n ` return resolveSubject`,\n `}`,\n ],\n branch: [\n ` // #367 — the agent convention owns this prefix. It is answered BEFORE the file-route`,\n ` // table because an agent matches no file route: falling through is how a deployed`,\n ` // \\`/api/agents/<name>\\` used to 404 on every target.`,\n ` if (`,\n ` ${pathname}.startsWith(${JSON.stringify(AGENT_PREFIX)}) ||`,\n ` ${pathname}.startsWith(${JSON.stringify(WELL_KNOWN_PREFIX)})`,\n ` ) {`,\n ...resolution.auxPrelude,\n ` // B-185 — the aux dispatcher is asked FIRST, and declining costs nothing: the matcher`,\n ` // reads the url and the scanned nodes, never a module (ADR-1). A miss falls through to`,\n ` // the run handler exactly as before, so this branch owns the sub-paths and nothing more.`,\n ` const auxRoute = await matchAgentAuxRoute(request.method, ${pathname}, auxDeps)`,\n ` if (auxRoute !== null) {`,\n ` // B-185 — identity is resolved only now. Building it costs a web shim and, where the`,\n ` // app declares one, a call into its own \\`createContext\\`; a url this dispatcher`,\n ` // merely declined must pay for neither (resolve-agent-subject.ts:93-96).`,\n ` const auxResponse = await serveMatchedAuxRoute(auxRoute, request, {`,\n ` ...auxDeps,`,\n ` resolveSubject: await __theoResolveSubject(request),`,\n ` })`,\n host.wrapSecurityHeaders === true\n ? ` return withSecurityHeaders(auxResponse, SECURITY_HEADERS)`\n : ` return auxResponse`,\n ` }`,\n ` // B-185 — only the agent prefix has a run handler behind it. A \\`.well-known\\` url the`,\n ` // dispatcher declined must NOT slice a prefix it does not carry: the name would be`,\n ` // garbage and mountAgent would answer 500 for what is a routing miss.`,\n ` if (!${pathname}.startsWith(${JSON.stringify(AGENT_PREFIX)})) return ${notFound}`,\n ` const agentName = ${pathname}.slice(${String(AGENT_PREFIX.length)}).split('/')[0]`,\n ...resolution.lookup,\n ` // A name nobody scanned is a 404, exactly like any other unknown path. Handing`,\n ` // \\`undefined\\` to mountAgent would surface as a 500 for what is a routing miss.`,\n ` if (mod === undefined) return ${notFound}`,\n ` const agentResponse = await mountAgent(mod, request, (model, plugins) => resolveProvider(model, { plugins }).apiKey, {`,\n ` agentName,`,\n ` // B-185 — mount-agent.ts:121 has accepted this since #365 and 0 of 23 adapters passed`,\n ` // one, so agent-access.ts:146 judged every deployed policy against \\`subject: null\\`.`,\n ` // A run is not a decline: it is about to do real work, so the shim this builds is not`,\n ` // the cost ADR-1 refuses.`,\n ` resolveSubject: await __theoResolveSubject(request),`,\n ` ...CSRF_CONFIG,`,\n ` })`,\n host.wrapSecurityHeaders === true\n ? ` return withSecurityHeaders(agentResponse, SECURITY_HEADERS)`\n : ` return agentResponse`,\n ` }`,\n ],\n }\n}\n\ninterface AgentResolution {\n imports: string[]\n declarations: string[]\n /** Lines that must leave `mod` bound to the agent's module, or `undefined`. */\n lookup: string[]\n /**\n * B-185 — lines that must leave `auxDeps` bound to an `AuxRouteDeps`, plus the expression for\n * its `agents`. They run BEFORE {@link AgentResolution.lookup} so a declined aux route performs\n * no module load, which is what ADR-1 buys and what a later ordering would spend.\n */\n auxPrelude: string[]\n /**\n * B-185 — lines that must leave `resolveSubject` bound to a resolver, or to `undefined`. Emitted\n * only where a match has already happened, because building one costs a web shim and\n * `resolve-agent-subject.ts:93-96` states that a declined url must never run the application's\n * `createContext`.\n */\n identity: string[]\n /**\n * B-185 — what {@link AgentResolution.identity} imports, appended to the `agent-mount` line, or\n * `''` where identity is not resolved at all.\n *\n * It lives HERE, beside the lines that CALL it, because a first draft decided it in a separate\n * `identityEntryFor` that switched the same discriminated union a second time. The two could not\n * disagree then — the conditions were identical — which is what made it latent rather than live.\n * A later drift would emit an entry that CALLS a symbol it never imported, and nothing catches\n * that: the stub-coverage guard derives its requirement FROM the imports, so a missing import\n * shrinks the requirement instead of failing, and `node --check` is syntax rather than\n * resolution. It would surface as a `ReferenceError` inside a deployed Worker.\n */\n identityImport: string\n}\n\n/** No filesystem: every agent module is a static import decided on the build machine. */\nfunction bakedResolution(\n agents: readonly DeployedAgent[],\n contextModule: string | undefined,\n pluginRunnerExpr: string | undefined,\n): AgentResolution {\n const varOf = (index: number): string => `__theoAgent${String(index)}`\n return {\n imports: [\n ...agents.map((agent, index) => `import * as ${varOf(index)} from '../../${agent.filePath}'`),\n // B-185 — the app's context module, baked exactly like the agents above it (ADR 0014).\n ...(contextModule === undefined\n ? []\n : [\n // SI-022 — DYNAMIC, not a top-level static import. B-185 added this import; before it\n // a Worker did not load the app's `server/context.ts` at all. A module-scope throw\n // there — the commonest shape being a required-env-var check — then went from costing\n // nothing to taking the ENTIRE target down: the import fails, the module never\n // evaluates, and every route dies rather than only the one that wanted an identity.\n //\n // That is strictly worse than what `resolve-agent-subject.ts:69-72` promises. It says\n // a throwing `createContext` reaches the branch's own error handler and becomes a 500,\n // which is a failure scoped to the request that needed identity. A throw at IMPORT\n // time reaches no handler at all.\n //\n // A relative `import()` is statically analysable, so wrangler bundles the module\n // exactly as it bundled the static form — the reason ADR 0014 bakes it is unaffected.\n // What changes is WHEN it evaluates, and therefore what a failure costs. It is only\n // a real improvement if the import stays inside the thunk; see the factory below.\n `const __theoContext = () => import('../../${contextModule}')`,\n ]),\n ],\n declarations: [\n `// #367 — the app's agents, keyed by NAME because that is what the URL carries, what the`,\n `// access policy is judged under, and what the run's spans are labelled with (#406).`,\n `const agents = {`,\n ...agents.map((agent, index) => ` ${JSON.stringify(agent.name)}: ${varOf(index)},`),\n `}`,\n `// B-185 — the node list the aux dispatcher matches a url against. There is deliberately NO`,\n `// second table keyed by file path: \\`test_the_table_is_keyed_by_agent_name_not_by_file_path\\``,\n `// forbids one, and the reason it gives is the right one — a path key makes a lookup depend on`,\n `// the server's directory layout. The loader below reaches the module THROUGH this list, so the`,\n `// layout appears once, here, and the module table above stays keyed by the name the URL, the`,\n `// access policy and the run's spans all carry (#406).`,\n `const agentNodes = [`,\n ...agents.map(\n (agent) =>\n ` { filePath: ${JSON.stringify(agent.filePath)}, agentPath: ${JSON.stringify(agent.agentPath)}, name: ${JSON.stringify(agent.name)} },`,\n ),\n `]`,\n ],\n lookup: [\n ` const mod = Object.prototype.hasOwnProperty.call(agents, agentName)`,\n ` ? agents[agentName]`,\n ` : undefined`,\n ],\n identityImport: contextModule === undefined ? '' : ', createSubjectResolverFromFactory',\n identity:\n contextModule === undefined\n ? [\n ` // B-185 — this app declares no \\`server/context.ts\\`, so there is no factory to`,\n ` // bake and an anonymous caller is the honest answer. A policy that admits`,\n ` // nobody is the correct outcome, not an error.`,\n ` const resolveSubject = undefined`,\n ]\n : [\n ` // B-185 — a Worker has no filesystem to find \\`context.ts\\` on, so the module is baked`,\n ` // and its factory handed straight to the resolver (ADR 0014).`,\n ` //`,\n ` // This function has TWO callers and is lazy on purpose. Both build a shim, and that is`,\n ` // correct at both: the aux branch calls it only AFTER a match, and the run handler is`,\n ` // about to do real work. What neither pays for is the app's own \\`createContext\\` on a`,\n ` // url nobody answers — the resolver is a thunk, and \\`agent-access.ts:146\\` returns early`,\n ` // for an absent or public policy without ever invoking it.`,\n ` const { req: __theoReq, res: __theoRes } = createWebShim(request)`,\n ` const resolveSubject = createSubjectResolverFromFactory(`,\n // The import happens HERE, inside the factory, and `createSubjectResolverFromFactory`\n // calls the factory inside the thunk it returns (`resolve-agent-subject.ts:127-133`).\n // A first version of this fix awaited the import as an ARGUMENT, which evaluated it\n // eagerly inside `__theoResolveSubject` — both call sites await that before\n // `agent-access.ts:145` returns early for an absent or `'public'` policy. The blast\n // radius was then every agent request rather than the one that wanted an identity,\n // and the laziness invariant the lines above assert was broken. Found by the\n // inventory judge while the change was still in the working tree.\n ` async (__theoArgs) => (await __theoContext()).createContext(__theoArgs),`,\n ` __theoReq,`,\n ` __theoRes,`,\n ` ${pluginRunnerExpr ?? 'undefined'},`,\n ` )`,\n ],\n auxPrelude: [\n ` // B-185 — the aux dispatcher needs nodes carrying \\`filePath\\` and a loader keyed by it.`,\n ` // The loader resolves the path to a NAME through the node list, then reads the same`,\n ` // name-keyed table the url lookup uses: one map, one layout mention, no filesystem`,\n ` // (ADR-2). A linear find over a handful of agents is not worth a second table that`,\n ` // \\`test_the_table_is_keyed_by_agent_name_not_by_file_path\\` exists to forbid.`,\n ` const auxDeps = {`,\n ` agents: agentNodes,`,\n ` loadModule: async (filePath) => {`,\n ` const node = agentNodes.find((a) => a.filePath === filePath)`,\n ` return node === undefined ? undefined : agents[node.name]`,\n ` },`,\n ` baseUrl: url.origin,`,\n ...AUX_DEPS_PARITY,\n ` }`,\n ],\n }\n}\n\n/** A filesystem: scan once and load on demand, exactly as this entry already treats its routes. */\nfunction scannedResolution(source: {\n projectRoot: string\n loadModule: string\n ensureLoader?: string\n agentsDirLiteral?: string\n serverDir?: string\n pluginRunnerExpr?: string\n}): AgentResolution {\n // The configured directory as a second argument, or nothing. `scanAgents` defaults the name to\n // `agents`, which is right for a project that never set one and wrong for every project that did.\n const agentsDirArg = source.agentsDirLiteral === undefined ? '' : `, ${source.agentsDirLiteral}`\n\n return {\n imports: [],\n declarations: [\n `// #367 — scanned on first use and cached, the same shape this entry already gives routes.`,\n `// Baking would tie an agent's existence to a rebuild on a target that has a filesystem.`,\n `let agentsCache = null`,\n ],\n lookup: [\n ` const agentNode = agentsCache.find((a) => a.name === agentName)`,\n ` const mod = agentNode === undefined ? undefined : await ${source.loadModule}(agentNode.filePath)`,\n ],\n identityImport: source.serverDir === undefined ? '' : ', createAgentSubjectResolver',\n identity:\n source.serverDir === undefined\n ? [` const resolveSubject = undefined`]\n : [\n ` // B-185 — this host HAS a filesystem, so it locates its own \\`context.ts\\` and the`,\n ` // existing resolver works unchanged; nothing is baked (ADR 0014). Lazy for the reason`,\n ` // the baked branch gives: both callers build a shim, and neither runs the app's own`,\n ` // \\`createContext\\` for a url nobody answers.`,\n ` const { req: __theoReq, res: __theoRes } = createWebShim(request)`,\n ` const resolveSubject = createAgentSubjectResolver({`,\n ` req: __theoReq,`,\n ` res: __theoRes,`,\n ` loadModule: ${source.loadModule},`,\n ` serverDir: ${source.serverDir},`,\n ` pluginRunner: ${source.pluginRunnerExpr ?? 'undefined'},`,\n ` })`,\n ],\n auxPrelude: [\n ...(source.ensureLoader === undefined ? [] : [` ${source.ensureLoader}`]),\n ` // B-185 — hoisted above the lookup so a declined aux route loads no module. The scan`,\n ` // LISTS agent files; it imports none, so moving it here costs a declined request`,\n ` // nothing it was not already paying.`,\n ` if (!agentsCache) agentsCache = scanAgents(${source.projectRoot}${agentsDirArg})`,\n ` const auxDeps = {`,\n ` agents: agentsCache,`,\n ` loadModule: ${source.loadModule},`,\n ` baseUrl: url.origin,`,\n ...AUX_DEPS_PARITY,\n ` }`,\n ],\n }\n}\n","/**\n * The CORS configuration a deployed entry carries, baked at build time.\n *\n * ## The defect this closes\n *\n * `security.cors` reached exactly one consumer: Vite's `configureServer` hook. So an app that\n * worked cross-origin under `theokit dev` stopped working the moment anything else served it —\n * `theokit start` (fixed separately) and all six Web deploy targets (usetheokit/theokit#409). Same\n * config, same code, no error and no warning; the failure surfaces in a browser as a blocked fetch\n * on the deployed URL, three layers from the key that had quietly stopped being read.\n *\n * The pure half was written twice and called once: `createCorsWebHandler` — the Web mirror — had no\n * caller anywhere in the repository. Nothing here reimplements it.\n *\n * ## A callback origin is REFUSED, not silently dropped\n *\n * `corsSchema.origins` accepts `z.function(...)` alongside the string / RegExp / array shapes. A\n * deployed function has no `theo.config.ts` to read, and there is no literal for a closure — so a\n * build that baked only the serialisable shapes would produce an app whose CORS silently allowed\n * nothing, which is the exact class of failure this issue reports.\n *\n * `docs/program/three-target-parity.md` § 3 is explicit about the alternative: \"a target that cannot serve\n * a capability refuses by name. Silent degradation is the failure mode this rule exists to\n * prevent.\" So the build throws, naming the target, the key and the two ways forward.\n */\nimport type { TheoConfig } from '../config/schema.js'\n\ntype CorsConfig = NonNullable<NonNullable<TheoConfig['security']>['cors']>\n\n/**\n * The CORS slice of an adapter's build options.\n *\n * Narrow and named once, matching how `securityHeaders` and `DeployedCsrfOptions` already reach the\n * emitters: each renderer receives what it uses, and six signatures name one type instead of six\n * inline shapes that can drift.\n */\nexport interface DeployedCorsOptions {\n cors?: CorsConfig\n}\n\n/**\n * Thrown at BUILD time when the declared CORS cannot be carried to a deployed target.\n *\n * A build error is the point: the alternative is a deploy that looks configured and refuses every\n * cross-origin request, discovered by a browser rather than by the build.\n */\nexport class UnserializableCorsOriginError extends Error {\n constructor(target: string) {\n super(\n `security.cors.origins is a function, and the \\`${target}\\` target cannot carry it: a deployed ` +\n `function has no theo.config.ts to read, and a callback cannot be written into the emitted ` +\n `entry. Replace it with the origin, a RegExp, or an array of either — all of which travel — ` +\n `or build for \\`node\\` and run \\`theokit start\\`, which evaluates the callback at runtime.`,\n )\n this.name = 'UnserializableCorsOriginError'\n }\n}\n\n/** Source text for one origin matcher: a RegExp as a literal, anything else as JSON. */\nfunction renderOrigin(origin: unknown, target: string): string {\n if (typeof origin === 'function') throw new UnserializableCorsOriginError(target)\n if (origin instanceof RegExp) return String(origin)\n if (Array.isArray(origin)) return `[${origin.map((o) => renderOrigin(o, target)).join(', ')}]`\n return JSON.stringify(origin)\n}\n\n/**\n * Source text for the CORS config, or `undefined` when the app declared none.\n *\n * RegExp entries are emitted as regex literals for the reason `deployed-csrf.ts` gives at length:\n * `JSON.stringify` renders a RegExp as `{}`, and `matchesOrigin` checks `instanceof RegExp`, so a\n * JSON-rendered origin would sit in the emitted file looking configured and matching nothing.\n *\n * @throws UnserializableCorsOriginError when `origins` is a callback\n */\nexport function renderDeployedCorsLiteral(cors: CorsConfig | undefined, target: string): string {\n if (cors === undefined) return 'undefined'\n\n const parts = [`origins: ${renderOrigin(cors.origins, target)}`]\n if (cors.methods !== undefined) parts.push(`methods: ${JSON.stringify(cors.methods)}`)\n if (cors.allowedHeaders !== undefined)\n parts.push(`allowedHeaders: ${JSON.stringify(cors.allowedHeaders)}`)\n if (cors.exposedHeaders !== undefined)\n parts.push(`exposedHeaders: ${JSON.stringify(cors.exposedHeaders)}`)\n // Both carry schema defaults, so they are always present and always emitted — unlike the\n // optional fields above, whose absence is a real answer the handler already has one for.\n parts.push(\n `credentials: ${JSON.stringify(cors.credentials)}`,\n `maxAge: ${JSON.stringify(cors.maxAge)}`,\n )\n\n return `{ ${parts.join(', ')} }`\n}\n\n/**\n * The lines that declare the CORS handler in a generated entry.\n *\n * `null` when nothing was declared, which is what \"no cors block\" meant before and still means: no\n * headers, not permissive ones.\n */\nexport function deployedCorsFragment(cors: CorsConfig | undefined, target: string): string[] {\n return [\n `// #409 — the CORS the app declared, carried as a literal because a deployed function has no`,\n `// theo.config.ts to read. \\`null\\` when the app declared none: no headers, not permissive ones.`,\n `const CORS_CONFIG = ${renderDeployedCorsLiteral(cors, target)}`,\n `const CORS_HANDLER = CORS_CONFIG === undefined ? null : createCorsWebHandler(CORS_CONFIG)`,\n ``,\n `/** Answer a preflight before routing — an OPTIONS the router handles never gets a CORS answer. */`,\n `function corsPreflight(request) {`,\n ` return CORS_HANDLER === null ? null : CORS_HANDLER.handlePreflightRequest(request)`,\n `}`,\n ``,\n `/** Put the headers on whatever the app answered, including its 404s — a browser reads a 404`,\n ` * without them as a CORS failure rather than as the 404 it is. */`,\n `function withCors(request, response) {`,\n ` if (CORS_HANDLER !== null) CORS_HANDLER.applyCorsHeaders(request, response.headers)`,\n ` return response`,\n `}`,\n ]\n}\n","/**\n * The CSRF configuration a deployed entry carries, baked at build time.\n *\n * ## The defect this closes\n *\n * The six Web-standards adapter entries built `executeRoute`'s context from an eight-field\n * literal, and neither `csrfMode` nor `disallowed` was among the eight (usetheokit/theokit#410).\n * `executeRoute` defaults an absent mode to `'strict'`, so an app declaring\n * `security: { csrf: 'off' }` — or `'warn'` — got `'strict'` on every deploy target: a `POST` that\n * works under `theokit dev` and `theokit start` answers `403 CSRF_INVALID` on Vercel, naming a\n * mechanism the operator had switched off. The config still validated and the build still\n * succeeded; the behaviour simply changed.\n *\n * The deployed function has no `theo.config.ts` to read, which is why the value is carried as a\n * literal — the same shape `security.headers` already uses (`renderSecurityHeadersConfigLiteral`).\n *\n * ## Why this is not `JSON.stringify`\n *\n * `disallowed.routes` accepts RegExp entries (`config/schemas/security.ts`), and `JSON.stringify`\n * renders a RegExp as `{}`. That is not a formatting problem: `matchDisallowed` checks\n * `p instanceof RegExp`, so a `{}` matches nothing while reading, in the emitted file, as a rule\n * that is present and configured. Reaching for JSON here would reproduce this issue's own defect —\n * configuration that survives validation and quietly stops applying — one layer further down.\n */\nimport type { TheoConfig } from '../config/schema.js'\n\ntype SecurityConfig = NonNullable<TheoConfig['security']>\n\n/**\n * The two slices of `security` that reach `executeRoute`'s context.\n *\n * Narrow rather than the whole block, matching how `securityHeaders` is already passed: each\n * renderer receives what it uses and nothing else, so a headers change cannot reach the CSRF\n * literal and vice versa.\n */\nexport interface DeployedCsrfOptions {\n csrf?: SecurityConfig['csrf']\n disallowed?: SecurityConfig['disallowed']\n}\n\n/**\n * Source text for one route pattern.\n *\n * A RegExp is emitted as a regex literal so it arrives as a RegExp; a string goes through\n * `JSON.stringify`, which is the correct escaper for a JS string literal (quotes, backslashes,\n * control characters, line separators).\n */\nfunction renderRoutePattern(pattern: string | RegExp): string {\n return pattern instanceof RegExp ? String(pattern) : JSON.stringify(pattern)\n}\n\n/**\n * Source text for the CSRF slice of `executeRoute`'s context.\n *\n * Absent values are OMITTED rather than defaulted. `executeRoute` already defaults an absent\n * `csrfMode` to `'strict'`, and writing `'strict'` here would put that default in a second place\n * where the two can disagree — which is the class of drift the whole issue is about.\n *\n * @param security - the declared csrf slices, or `undefined` when the app declared no security block\n */\nexport function renderDeployedCsrfLiteral(security: DeployedCsrfOptions | undefined): string {\n if (security === undefined) return '{}'\n\n const parts: string[] = []\n if (security.csrf !== undefined) parts.push(`csrfMode: ${JSON.stringify(security.csrf)}`)\n\n const { disallowed } = security\n if (disallowed !== undefined) {\n const routes = disallowed.routes.map(renderRoutePattern).join(', ')\n parts.push(\n `disallowed: { routes: [${routes}], behavior: ${JSON.stringify(disallowed.behavior)} }`,\n )\n }\n\n // `{}` and not `{ }` when nothing was declared: the emitted file is read by people, and the\n // two-space version reads as though something was meant to be there.\n return parts.length === 0 ? '{}' : `{ ${parts.join(', ')} }`\n}\n\n/**\n * The lines that declare `CSRF_CONFIG` in a generated entry.\n *\n * One function rather than the same six lines pasted into each of the six adapters: that\n * duplication is how the eight-field context literal came to be wrong in six places at once, and\n * repeating the fix in the same shape would leave the next field with the same six places to be\n * forgotten in. It also keeps the two largest emitters under the `max-lines-per-function` ceiling,\n * which `vercel.ts` already extracts fragments to respect.\n *\n * @param opts - the declared csrf slices, passed straight through from the adapter's build options\n * @param home - what the target has instead of a config file, for the comment's second sentence\n */\nexport function deployedCsrfFragment(\n opts: DeployedCsrfOptions,\n home = 'a deployed function',\n): string[] {\n return [\n `// #410 — the CSRF mode and per-route escalation the app declared. Carried as a`,\n `// literal for the same reason as the headers above: ${home} has no theo.config.ts`,\n `// to read. Absent keys stay absent so executeRoute's own default ('strict') applies,`,\n `// rather than this file becoming a second place it can drift.`,\n `const CSRF_CONFIG = ${renderDeployedCsrfLiteral(opts)}`,\n ]\n}\n","/**\n * The security headers a deployed target puts on its responses.\n *\n * `theokit start` applies the configured baseline to every response it writes\n * (`cli/commands/start/request-handler.ts`). None of the six Web-standards\n * deploy adapters applied any, so the same page carried a CSP,\n * `X-Frame-Options`, HSTS and `nosniff` under `theokit start` and none of them\n * once deployed (usetheokit/theokit#410, GHSA-87qq-fgcr-384x).\n *\n * This module is the seam that closes that half of the gap. It has two halves\n * and they live together on purpose: the code that writes the literal into a\n * generated entry and the code that reads it at request time have to agree on\n * one shape, and a shape stated in two files drifts.\n *\n * - **Build time** — {@link renderSecurityHeadersConfigLiteral} turns\n * `security.headers` into a JSON literal the adapter inlines. A deployed\n * runtime has no `theo.config.ts` to read, so the configuration travels as\n * data.\n * - **Request time** — the generated entry calls {@link buildSecurityHeaders}\n * on that literal and hands every response to {@link withSecurityHeaders}.\n * The same function `theokit start` calls, on the same input, so the two\n * cannot disagree about what the configuration means.\n *\n * ## The per-request nonce, and where it stops\n *\n * `buildSecurityHeaders` accepts a per-request `nonce` and substitutes it into\n * `script-src`. A nonce cannot survive a build-time literal — it is minted per\n * response — so a target reaches one only if it renders the HTML at request\n * time and can put the same value on the script tags it emits.\n *\n * Two paths do, and this docblock named one of them until 2026-09-24:\n *\n * - **Cloudflare with `ssrStreaming: true`** — the worker calls\n * `renderStreamingWeb(request, { nonce })`, and that renderer threads the\n * value into `renderToReadableStream` and into the hydration script\n * (`router/entry-server.ts`).\n * - **node** — `theokit start` mints one per request\n * (`cli/commands/start/request-handler.ts:272`) and stamps it onto the\n * inline scripts in the head. The `node` deploy adapter serves through that\n * same handler, which is what its own comment says\n * (`adapters/node.ts:24`).\n *\n * The six Web-standards targets that DO declare a limit here — aws-lambda, bun,\n * deno-deploy, netlify, vercel, and Cloudflare without `ssrStreaming` — serve\n * HTML written at build time, or no HTML at all, and carry a **nonce-less\n * CSP**: the same answer `buildSecurityHeaders` already gives a prerendered\n * route (EC-4), for the same reason — a nonce in the header with no nonce on\n * the tag blocks every inline script.\n *\n * `node` declares no `mintsNonce` at all, because it never calls\n * {@link describeDeployedSecurityHeaders}. That is a gap in what its build\n * PRINTS, not a wrong declaration, and closing it is its own change rather than\n * a line in the item that corrected this paragraph.\n *\n * That asymmetry is real and is not smoothed over. It is stated in the emitted\n * entry, printed by the build through\n * {@link describeDeployedSecurityHeaders}, and written down in\n * `docs/surfaces/build-adapters.md`.\n */\nimport { generateNonce } from '../core/contracts/nonce.js'\nimport type { SecurityHeadersConfig } from '../core/contracts/security-headers.js'\nimport { buildSecurityHeaders } from '../core/contracts/security-headers.js'\n\n/**\n * Re-exported so a generated entry has ONE import for the whole concern.\n *\n * Reaching it through `theokit/server/security` would work and would also drag\n * that barrel's CSRF surface into a Worker bundle, for a function that is forty\n * lines of string concatenation. It is defined in `core/contracts/`, which is\n * the module every target may import from — `adapters → server` is not an edge\n * in the DAG, and the header policy was never server code.\n */\nexport { buildSecurityHeaders }\n\n/**\n * Re-exported for the same reason, and so the streamed Cloudflare worker mints\n * its nonce with the identical primitive `theokit start` uses\n * (`cli/commands/start/request-handler.ts`) rather than a second, hand-rolled\n * one. It is already runtime-portable: Web Crypto first, with a named error\n * when the runtime has none.\n */\nexport { generateNonce }\n\n/**\n * The `security.headers` block, as a literal a generated entry can carry.\n *\n * `{}` when the app declares none — which is not the same as \"no headers\".\n * `buildSecurityHeaders({})` returns the full default baseline, and `{}` is\n * exactly what `theokit start` passes when `security.headers` is absent\n * (`cli/commands/start/index.ts`). An app with no security block gets the same\n * baseline deployed as it gets locally.\n */\n/**\n * The two declarations every deploy entry emits to carry the security baseline.\n *\n * Four adapters wrote these same two lines — `netlify`, `vercel`, `aws-lambda`, `deno-deploy` —\n * each under a comment naming its own host's static-asset boundary. The COMMENT differs and is\n * meant to; the declarations do not.\n *\n * Extracted after SonarCloud's duplication gate failed a PR at 4.0% against a 3% ceiling and named\n * this as the largest repeated block among the changed files. The gate was measuring something\n * real: a literal renderer and a builder call, copied four times, is four places to update when\n * either changes.\n *\n * Returns the lines rather than a joined string, because every caller splices them into an array\n * of emitted lines.\n */\nexport function securityHeadersDeclarations(config: SecurityHeadersConfig | undefined): string[] {\n return [\n `const SECURITY_HEADERS_CONFIG = ${renderSecurityHeadersConfigLiteral(config)}`,\n `const SECURITY_HEADERS = buildSecurityHeaders(SECURITY_HEADERS_CONFIG, { production: true })`,\n ]\n}\n\nexport function renderSecurityHeadersConfigLiteral(\n headers: SecurityHeadersConfig | undefined,\n): string {\n return JSON.stringify(headers ?? {})\n}\n\n/**\n * Put the headers on a response, without overruling the handler.\n *\n * `theokit start` sets the baseline BEFORE the route handler runs, so a handler\n * can override it with `res.setHeader` (last write wins, Node convention). On a\n * Web target the response arrives already built, so the equivalent of \"the\n * handler wins\" is to skip a header the response already carries: a route that\n * set its own `Content-Security-Policy` keeps it.\n *\n * Mutates and returns the same `Response` rather than constructing a\n * replacement, because these responses are handed to the runtime while their\n * body is still being written (#382) and re-wrapping the stream is exactly the\n * second buffering point that change removed.\n *\n * Mutation is safe for every response these entries produce: the Fetch spec\n * gives a locally constructed `Response` the `response` header guard, which\n * permits `set` for every name used here. The `immutable` guard belongs to\n * responses that came back from `fetch()`, and none of the six emitted handlers\n * returns one — each builds its response from `createWebShim`, from a `new\n * Response(...)`, or from the SSR renderer. The WebSocket upgrade, which is the\n * one response an adapter gets from its runtime rather than building, is\n * deliberately not routed through here.\n */\nexport function withSecurityHeaders(response: Response, headers: Record<string, string>): Response {\n for (const [key, value] of Object.entries(headers)) {\n if (!response.headers.has(key)) response.headers.set(key, value)\n }\n return response\n}\n\nexport interface DeployedSecurityHeaderLimits {\n target: string\n /** The app's `security.headers` block, or undefined when it declares none. */\n securityHeaders: SecurityHeadersConfig | undefined\n /**\n * Does the emitted handler render HTML at request time and mint a CSP nonce\n * for it? Among the targets that CALL this function, true only for Cloudflare\n * with `ssrStreaming: true`. `node` also mints one and is not among them —\n * see the note in this module's header docblock.\n */\n mintsNonce: boolean\n /**\n * Is the HTML document served by a platform static host rather than by the\n * handler this build emits? True wherever the emitted handler answers\n * `/api/*` and returns 404 for everything else.\n */\n /**\n * Who puts the security headers on the HTML DOCUMENT — a different question from who serves it,\n * and the two used to be collapsed into one boolean (usetheokit/theokit#412).\n *\n * - `handler` — this target's own handler returns the document, so it carries the same baseline\n * every API response carries. No caveat.\n * - `platform-configured` — the platform's static host serves the document, AND this build emits\n * the configuration that puts the headers on it (`.vercel/output/config.json`, `netlify.toml`).\n * Still worth stating, because nothing here has seen a deployed response.\n * - `platform-unmanaged` — the platform serves it and this build owns no artifact that could\n * configure it. This is the real remaining gap, and it stays named.\n *\n * The two-value version reported `platform-configured` targets with the same message as\n * `platform-unmanaged` ones, telling an operator to go and do work the build had already done —\n * and a stale limitation reads exactly like a current one.\n */\n documentHeaders: 'handler' | 'platform-configured' | 'platform-unmanaged'\n}\n\n/**\n * What the build tells the operator, once, per target.\n *\n * Silent degradation is the failure mode `docs/program/three-target-parity.md` exists\n * to prevent. Two things degrade quietly here and both are named rather than\n * discovered in production: a CSP that refuses inline scripts on a deploy while\n * allowing them locally, and an HTML document that never passes through the\n * handler these headers are attached to.\n *\n * The header names are read from the map the entry will actually carry, not\n * from a list written next to it. A configuration that switches HSTS or the CSP\n * off would otherwise be announced as sending them.\n */\nexport function describeDeployedSecurityHeaders(limits: DeployedSecurityHeaderLimits): string {\n const headers = buildSecurityHeaders(limits.securityHeaders ?? {}, { production: true })\n const names = Object.keys(headers)\n if (names.length === 0) {\n return ` ! \\`${limits.target}\\` sends no security headers: the configuration switched every one of them off.`\n }\n\n const lines = [\n ` ✓ security headers on every response \\`${limits.target}\\` returns: ${names.join(', ')}.`,\n ]\n const sendsCsp = names.some((name) => name.startsWith('Content-Security-Policy'))\n if (sendsCsp && !limits.mintsNonce) {\n lines.push(\n ` - The CSP carries no nonce: this target serves HTML written at build time,`,\n ` so there is no per-request value to put on a script tag. An inline`,\n ` <script> is refused by \\`script-src 'self'\\` here, while the same page`,\n ` under \\`theokit start\\` gets a nonce and runs it. Move inline scripts to`,\n ` \\`<script src=\"...\">\\`, or set \\`security.headers.cspMode: 'report-only'\\``,\n ` while you migrate.`,\n )\n }\n if (limits.documentHeaders === 'platform-configured') {\n lines.push(\n ` - The HTML document is served by the platform's static host, and reaches`,\n ` the browser with these headers through config this build emits. That`,\n ` path is not verified by a deploy from here — the values come from the`,\n ` same function the handler uses, but no response has been read back.`,\n )\n }\n if (limits.documentHeaders === 'platform-unmanaged') {\n lines.push(\n ` - The HTML document does NOT pass through this handler — the platform's`,\n ` static host serves it — so these headers reach \\`/api/*\\` responses and`,\n ` not the page. This build emits no artifact that could configure it, so`,\n ` set the document's headers on the platform (usetheokit/theokit#412).`,\n )\n }\n return lines.join('\\n')\n}\n","/**\n * The rate limit a deployed entry carries, baked at build time.\n *\n * ## What #508 asked for, and what this answers\n *\n * `theokit build` refuses the six Web-standards targets when `theo.config.ts` declares a\n * `rateLimit` (`UnenforceableRateLimitError`). That refusal is honest and it is not enforcement.\n * The issue names three things standing between the refusal and a working limit: per-runtime\n * caller-address resolution, a refusal that survives where the address cannot be resolved, and\n * storage that outlives one invocation.\n *\n * For `bun` all three already have answers, which is why it is the target that moves first:\n *\n * - **Address** — `Bun.serve`'s handler is `fetch(request, server)`, and `server.requestIP(request)`\n * returns the peer address without depending on a proxy header a client could set.\n * - **Storage** — `Bun.serve` is a long-lived process. `createRateLimiterWeb`'s default\n * `InMemoryStore` therefore behaves exactly as it does under `theokit start`, which is the\n * deployment `node` already ships.\n * - **Refusal** — see below: every shape whose key cannot be resolved in a deployed entry throws\n * at BUILD time rather than degrading.\n *\n * ## Why the other five are not here\n *\n * Cloudflare, AWS Lambda, Netlify and Vercel are per-invocation runtimes: an in-process counter\n * does not survive between requests, so a limiter built on one forgets. A limit that forgets is a\n * limit that does not limit — the same class of failure as the shared bucket, reached by a\n * different road. Deno Deploy evicts isolates for the same reason. Those need an external counter,\n * which is a storage design and not a wiring change, so they keep refusing by name.\n *\n * Being explicit about that boundary is the point. The failure this whole area exists to prevent is\n * a config that reads as protection while protecting nothing.\n */\nimport type { TheoConfig } from '../config/schema.js'\n\ntype RateLimitConfig = NonNullable<TheoConfig['rateLimit']>\n\n/** The rate-limit slice of an adapter's build options, named once like `DeployedCorsOptions`. */\nexport interface DeployedRateLimitOptions {\n rateLimit?: RateLimitConfig\n}\n\n/**\n * Thrown at BUILD time when a declared rate limit cannot be carried into a deployed entry.\n *\n * Distinct from {@link UnenforceableRateLimitError}, and the distinction is not cosmetic: that one\n * means *this target enforces no limit at all*, this one means *this target enforces limits, and\n * cannot carry THIS one*. An operator reading the first looks for another target; an operator\n * reading the second changes the key.\n */\nexport class UnserialisableRateLimitError extends Error {\n override readonly name = 'UnserialisableRateLimitError'\n constructor(\n readonly target: string,\n readonly reason: string,\n ways: readonly string[],\n ) {\n super(\n [\n `Refusing to build for \\`${target}\\`: theo.config.ts declares a rate limit this target cannot carry.`,\n ``,\n ` ${reason}`,\n ``,\n ` Ways forward:`,\n ...ways.map((w) => ` • ${w}`),\n ``,\n ` This refuses rather than dropping the key, because a rate limit that silently does not`,\n ` apply looks exactly like one that does (usetheokit/theokit#461, #508).`,\n ].join('\\n'),\n )\n }\n}\n\n/** The base shape every accepted config narrows to: a window, a ceiling, and an IP key. */\ninterface BakeableRateLimit {\n windowMs: number\n max: number\n /**\n * How many proxies sit in front of the app, for the forwarded-header path. Dropped before the\n * entry until B-027: the schema declares it (`config/schemas/rate-limit.ts:36`) and the generated\n * code could not honour it, so a deployment behind a proxy keyed every visitor on the proxy.\n */\n trustProxy: boolean | number\n /**\n * B-257 — the durable counter the entry constructs, or `undefined` for the in-process default.\n *\n * A reference rather than an instance: this is baked into a generated file, so it must survive\n * being written as source. `factory` is the one field that cannot be escaped — see\n * `bakeableStore` below.\n */\n store?: BakeableStore\n}\n\n/** A durable store named by `theo.config.ts`, in the form a generated entry can construct. */\ninterface BakeableStore {\n /** The specifier the entry imports from. Written through `JSON.stringify`. */\n module: string\n /** The exported name it constructs. VALIDATED, never escaped — it becomes a bare identifier. */\n factory: string\n /** Constructor options. Written through `JSON.stringify`. */\n options?: Record<string, string | number | boolean>\n}\n\n/**\n * A JavaScript identifier, and nothing else.\n *\n * `factory` becomes a bare identifier in `import { <factory> } from …`, where `JSON.stringify` would\n * emit `import { \"x\" }` and not parse. So it cannot be escaped — only validated. Without this, a\n * `theo.config.ts` carrying `factory: \"x } from 'evil'; //\"` writes arbitrary code into every\n * generated entry, and that file usually arrives with the clone.\n */\nconst IDENTIFIER = /^[A-Za-z_$][\\w$]*$/\n\n/**\n * The words that are well-formed identifiers and cannot be BOUND.\n *\n * `IDENTIFIER` answers \"could this be a name?\"; the emitter needs \"can this be THIS name?\", and the\n * two differ on exactly this set. `factory` lands as a bare binding in\n * `import { <factory> } from '<module>'`, where `import { default }` is a SyntaxError — so a config\n * naming one produces a generated entry that does not parse, and the deploy fails pointing at the\n * emitted file rather than at the line that caused it.\n *\n * `default` is the one a real config reaches by accident, because `export default createStore` is\n * the ordinary shape of the module being named. The rest are here because the cost of the list is\n * one comparison and the cost of an omission is a deploy that fails somewhere else.\n *\n * Reserved words only. `defaultStore` is a legal binding, and a substring match would refuse it —\n * an over-correction that breaks working configs to protect against a shape they do not have.\n */\nconst NOT_BINDABLE = new Set([\n 'await',\n 'break',\n 'case',\n 'catch',\n 'class',\n 'const',\n 'continue',\n 'debugger',\n 'default',\n 'delete',\n 'do',\n 'else',\n 'enum',\n 'export',\n 'extends',\n 'false',\n 'finally',\n 'for',\n 'function',\n 'if',\n 'import',\n 'in',\n 'instanceof',\n 'new',\n 'null',\n 'return',\n 'super',\n 'switch',\n 'this',\n 'throw',\n 'true',\n 'try',\n 'typeof',\n 'var',\n 'void',\n 'while',\n 'with',\n 'yield',\n])\n\n/**\n * Refuse a `store` a generated entry could not construct — or could construct into a hole.\n *\n * Extracted from `bakeableRateLimit` because it is its own responsibility and because inlining it\n * took that function to a cyclomatic complexity of 18 against a ceiling of 15. The three refusals\n * are one question asked of three fields, which is the shape SRP asks for.\n *\n * The third is the one that cannot be solved by escaping. `module` and `options` are written through\n * `JSON.stringify`, as `trustProxy` already is at the emit site. `factory` lands as a BARE\n * IDENTIFIER in `import { <factory> } from …`, where a quoted string does not parse — so it is\n * validated, and a config that fails the check is refused by name rather than interpolated raw.\n */\nfunction assertBakeableStore(store: unknown, target: string): void {\n if (store === undefined) return\n\n if (!isStoreShaped(store)) {\n throw new UnserialisableRateLimitError(\n target,\n '`security.rateLimit.store` must be an object naming a module and a factory.',\n [\n \"declare `store: { module: '@upstash/redis', factory: 'Redis' }`\",\n 'omit `store` and let the build refuse this target, which is honest about not limiting',\n ],\n )\n }\n\n const { module: mod, factory } = store as Record<string, unknown>\n\n if (typeof mod !== 'string' || mod.length === 0) {\n throw new UnserialisableRateLimitError(\n target,\n '`security.rateLimit.store.module` must be the specifier the entry imports from.',\n [\"declare `module: '@upstash/redis'`\", 'omit `store`'],\n )\n }\n\n if (typeof factory !== 'string' || !IDENTIFIER.test(factory) || NOT_BINDABLE.has(factory)) {\n throw new UnserialisableRateLimitError(\n target,\n '`security.rateLimit.store.factory` must be a plain identifier — it is written into the ' +\n 'generated entry as `import { <factory> }`, where a quoted string would not parse, so it ' +\n 'is validated rather than escaped. Anything else would put arbitrary source in every ' +\n 'deployment this config builds.',\n [\n \"name the export directly: `factory: 'Redis'`\",\n 'omit `store` and let the build refuse this target',\n ],\n )\n }\n\n // `options` is typed `Record<string, string | number | boolean>` and the type is erased before\n // this runs, so the declaration protects nobody: the value arrives from a config file. Three\n // measured escapes, and the third is why this is a refusal rather than a comment —\n // `JSON.stringify` throws a raw TypeError naming JSON on a BigInt and on a cycle, and on a\n // FUNCTION it throws nothing and omits the key. The option was then simply absent from the\n // emitted entry, with no diagnostic: a store configured and not configured, which is the same\n // silence this whole feature exists to remove.\n const { options } = store as Record<string, unknown>\n if (options !== undefined) {\n if (typeof options !== 'object' || options === null || Array.isArray(options)) {\n throw new UnserialisableRateLimitError(\n target,\n '`security.rateLimit.store.options` must be an object of scalar values.',\n [\n \"declare `options: { url: 'redis://…', retries: 3 }`\",\n 'omit `options` and configure the store inside the factory',\n ],\n )\n }\n\n const offending = Object.entries(options as Record<string, unknown>).find(\n ([, v]) => typeof v !== 'string' && typeof v !== 'number' && typeof v !== 'boolean',\n )\n if (offending !== undefined) {\n throw new UnserialisableRateLimitError(\n target,\n `\\`security.rateLimit.store.options.${offending[0]}\\` must be a string, number or boolean.`,\n [\n 'declare scalars only — strings, numbers and booleans',\n 'move anything else into the factory itself, which the generated entry calls at runtime',\n ],\n )\n }\n }\n}\n\n/**\n * The ONE predicate both sides read — the call emitter here and `assertBakeableStore`'s first\n * refusal. They were two, differing on arrays: this one accepted `store: []` while the refusal\n * rejected it, so the declaration threw while the call side would still have emitted `await`. The\n * build refused first, so no bad entry reached disk — and the pair exists to agree BY CONSTRUCTION\n * rather than because one check happens to run earlier.\n */\nfunction isStoreShaped(store: unknown): boolean {\n return typeof store === 'object' && store !== null && !Array.isArray(store)\n}\n\n/**\n * Narrow a declared rate limit to what a deployed entry can actually enforce, or refuse by name.\n *\n * Accepts the base `{ windowMs, max }` and the explicit `keyBy: 'ip'`, which is what the address\n * resolution below can key. Everything else throws:\n *\n * - a **function** `keyBy` has no literal — the same wall `deployed-cors.ts` hits on a callback\n * origin, and refused for the same reason;\n * - `keyBy: 'session' | 'user'` needs a session the deployed entry does not resolve at the point\n * the limit runs, which is before routing;\n * - `routes` needs the matched route, decided after this check. Refusing it is honest today and is\n * the natural next slice.\n */\nfunction bakeableRateLimit(\n rateLimit: RateLimitConfig | undefined,\n target: string,\n): BakeableRateLimit | undefined {\n if (rateLimit === undefined) return undefined\n const cfg = rateLimit as Record<string, unknown>\n\n if (typeof cfg.keyBy === 'function') {\n throw new UnserialisableRateLimitError(\n target,\n '`security.rateLimit.keyBy` is a function, and a deployed entry has no literal for a closure.',\n [\n \"use `keyBy: 'ip'` (the default), which this target resolves from the connection\",\n 'build for `node` and run `theokit start`, which can call the function',\n ],\n )\n }\n if (cfg.keyBy === 'session' || cfg.keyBy === 'user') {\n throw new UnserialisableRateLimitError(\n target,\n `\\`security.rateLimit.keyBy: '${cfg.keyBy}'\\` needs a resolved session, and the limit runs before routing.`,\n [\n \"use `keyBy: 'ip'`, which is resolvable at that point\",\n 'build for `node` and run `theokit start`',\n ],\n )\n }\n assertBakeableStore(cfg.store, target)\n\n if (cfg.routes !== undefined) {\n throw new UnserialisableRateLimitError(\n target,\n '`security.rateLimit.routes` needs the matched route, which is decided after the limit runs here.',\n [\n 'declare a single global limit (`windowMs` + `max`)',\n 'build for `node` and run `theokit start`, which applies per-route limits',\n ],\n )\n }\n\n const windowMs = cfg.windowMs\n const max = cfg.max\n if (typeof windowMs !== 'number' || typeof max !== 'number') return undefined\n // `?? false` and not `|| false`: `trustProxy: 0` is a number and falsy, and it means the same to\n // the resolver as `false` — but emitting `false` where the operator wrote `0` makes the generated\n // entry disagree with the config a reader compares it against.\n const trustProxy = cfg.trustProxy\n return {\n windowMs,\n max,\n trustProxy:\n typeof trustProxy === 'boolean' || typeof trustProxy === 'number' ? trustProxy : false,\n // Validated above — `module` and `options` are escaped at emit time, `factory` was checked\n // against IDENTIFIER because it cannot be.\n store: cfg.store as BakeableStore | undefined,\n }\n}\n\n/**\n * The generated declarations and helper for a target that keys on the connection's peer address.\n *\n * `addressExpression` is the per-runtime half the issue calls out — the one thing that genuinely\n * differs between targets. Bun passes `server`; a future Deno slice would pass its\n * `ServeHandlerInfo`. Everything else here is shared.\n *\n * Returns `[]` when nothing was declared, so an app without a limit emits no limiter at all rather\n * than an inert one.\n */\nexport function deployedRateLimitFragment(\n rateLimit: RateLimitConfig | undefined,\n target: string,\n addressExpression: string,\n /**\n * The parameter list `callerAddress` is DECLARED with, because the runtime source differs per\n * target and only `bun` binds these two names (`bun.ts:158`). `cloudflare.ts:452` binds\n * `(request, env, ctx)`, `vercel.ts:39` `(nodeReq, nodeRes)`, `netlify.ts:84` `(request, context)`,\n * `deno-deploy.ts:90` `(request)` and `aws-lambda.ts:152` `(event)`. An expression naming\n * `context`, `info`, `event` or `nodeReq` inside a function declared `(request, server)` is an\n * unbound identifier, and the entry throws on the first limited request.\n */\n params = 'request, server',\n /**\n * The specifier prefix this target needs — `'npm:'` on Deno Deploy, empty everywhere else.\n *\n * B-257: this function now emits its OWN import for the durable limiter, rather than relying on\n * each adapter to remember one. The previous shape had THREE sides to keep in step — the\n * declaration here, the call in `rateLimitCheckFragment`, and a per-adapter import line — and the\n * third was not joined: six adapters imported `createRateLimiterWeb` and none imported\n * `createDurableRateLimiterWeb`, so a store-carrying entry referenced a free variable and would\n * have thrown `ReferenceError` while evaluating its module body. Not at the first limited request:\n * at LOAD, taking down every route including those declaring no limit.\n *\n * `cloudflare.ts:370-373` documents that exact defect from B-027, one symbol earlier. Owning the\n * import here is what stops a fourth recurrence.\n */\n importPrefix = '',\n): string[] {\n const baked = bakeableRateLimit(rateLimit, target)\n if (baked === undefined) return []\n return [\n `// #508 — the limit the app declared, carried as a literal because a deployed entry has no`,\n `// theo.config.ts to read.`,\n `//`,\n `// WHERE THIS COUNTER LIVES, and it is not the same answer per target. On a long-lived server`,\n `// (\\`bun\\`) the process outlives a request and the count holds. On a per-invocation or`,\n `// per-isolate runtime — Cloudflare, AWS Lambda, Netlify, Vercel, Deno Deploy — it does not:`,\n `// the limit is PER INSTANCE, and a caller spread across instances gets that many budgets.`,\n `// The address is resolved correctly either way; the counting is what B-257 is about, and`,\n `// \\`theokit build\\` refuses a declared limit on those five until it is.`,\n // B-262 — ONE builder, one signature, always async. The emitter used to branch here:\n // `createRateLimiterWeb` returns a value and `createDurableRateLimiterWeb` returns a Promise, so\n // the CALL SITE had to know which it got and emit `await` or not. A call site whose shape changes\n // with the config is what produced B-257's missing `await`, and `buildRateLimiter` removes the\n // decision rather than documenting it.\n `import { buildRateLimiter } from '${importPrefix}theokit/server/rate-limit'`,\n ...(baked.store === undefined\n ? [\n `const RATE_LIMIT = buildRateLimiter({ windowMs: ${baked.windowMs}, max: ${baked.max} }, undefined)`,\n ]\n : [\n // B-257 — a durable counter, named by the app. `module` and the options are JSON literals;\n // `factory` was validated against IDENTIFIER at bake time because it lands here as a bare\n // identifier and no escape can make that safe.\n //\n // The limiter's own import is emitted HERE rather than by each adapter. See `importPrefix`.\n `import { ${baked.store.factory} } from ${JSON.stringify(baked.store.module)}`,\n `const RATE_LIMIT_STORE = new ${baked.store.factory}(${JSON.stringify(baked.store.options ?? {})})`,\n `const RATE_LIMIT = buildRateLimiter(`,\n ` { windowMs: ${baked.windowMs}, max: ${baked.max} },`,\n ` RATE_LIMIT_STORE,`,\n `)`,\n ]),\n ``,\n `// How many proxies the deployment declared in front of it. \\`client-ip.ts\\` reads a forwarded`,\n `// header only when this says one wrote it: the header is whatever the client typed, so`,\n `// trusting it unasked lets anyone rotate a forged value past the limiter with one \\`curl -H\\`.`,\n `const TRUST_PROXY = ${JSON.stringify(baked.trustProxy)}`,\n ``,\n `/**`,\n ` * The caller's address, from the connection rather than from a header.`,\n ` *`,\n ` * A header a client can set is a key a client can choose, which makes the bucket theirs to`,\n ` * split. An address this runtime cannot resolve returns \\`undefined\\`, and the caller answers`,\n ` * 503 rather than keying on a constant: one shared bucket is a budget the first caller each`,\n ` * window exhausts for everyone, which is worse than no limiting at all.`,\n ` */`,\n `const addr = (v) => (typeof v === 'string' && v.trim() !== '' ? v.trim() : undefined)`,\n `function callerAddress(${params}) {`,\n ` return addr(${addressExpression})`,\n `}`,\n ``,\n `/** The 503 a caller this runtime cannot name gets, instead of everyone's shared bucket. */`,\n `function unnamedCaller() {`,\n ` return new Response(`,\n ` JSON.stringify({ error: { code: 'CALLER_UNRESOLVED', message: ${JSON.stringify(\n `${target} could not resolve the caller's address, and a rate limit keyed on a constant is a denial of service`,\n )} } }),`,\n ` { status: 503, headers: { 'Content-Type': 'application/json' } },`,\n ` )`,\n `}`,\n ``,\n `/** The 429 a limited caller gets, carrying the limiter's own headers. */`,\n `function rateLimited(result) {`,\n ` return new Response(JSON.stringify({ error: { code: 'RATE_LIMITED', message: 'Too many requests' } }), {`,\n ` status: 429,`,\n ` headers: { 'Content-Type': 'application/json', ...result.headers },`,\n ` })`,\n `}`,\n ]\n}\n\n/**\n * The check itself, as generated source, placed inside the runtime's request handler.\n *\n * Extracted for the reason `bun.ts` extracts its other fragments: the emitter is one array literal,\n * so every line the entry gains counts against `max-lines-per-function`, and this one pushed it\n * past the ceiling.\n *\n * Runs AFTER the CORS preflight and BEFORE routing: a limited caller should not reach a handler,\n * and a browser still needs its CORS answer to read the 429 as a 429 rather than as a network\n * failure.\n */\nexport function rateLimitCheckFragment(\n rateLimit: RateLimitConfig | undefined,\n indent: string,\n /** The arguments `callerAddress` is CALLED with — see `deployedRateLimitFragment`'s `params`. */\n args = 'request, server',\n /** How this target answers a caller over its budget. Only `bun` can use the default. */\n refuse = 'return withCors(request, withSecurityHeaders(rateLimited(limit), SECURITY_HEADERS))',\n /** How it answers a caller it could not name. Same shape, different body. */\n refuseUnnamed = 'return withCors(request, withSecurityHeaders(unnamedCaller(), SECURITY_HEADERS))',\n): string[] {\n if (rateLimit === undefined) return []\n return [\n `${indent}const caller = callerAddress(${args})`,\n `${indent}if (caller === undefined) {`,\n `${indent} ${refuseUnnamed}`,\n `${indent}}`,\n // B-257 — the call side of the pair. `deployedRateLimitFragment` emits the DECLARATION; this\n // emits the CALL, and the two branch on the same fact or the entry names a symbol it never\n // declared. A durable limiter returns a promise; the sync facade does not.\n // B-262 — `await` unconditionally. `buildRateLimiter` is always async, so this line no longer\n // asks what the config declared. The conditional it replaces is where B-257's defect lived:\n // a Promise read for `limited` is always `undefined`, always falsy, and every request passes.\n `${indent}const limit = await RATE_LIMIT(caller)`,\n `${indent}if (limit.limited) {`,\n `${indent} ${refuse}`,\n `${indent}}`,\n ]\n}\n","/**\n * The configuration a deployed entry could not apply (usetheokit/theokit#425).\n *\n * ## Why this is not another literal renderer\n *\n * `deployed-csrf.ts` and `deployed-cors.ts` bake their values into the emitted source, which works\n * because `csrf` is an enum and `disallowed` is a `{ routes, behavior }` object — plain data, and a\n * deployed function has no `theo.config.ts` to read.\n *\n * The two concerns left over from #410 turn out to be different from each other, and the difference\n * is the whole design:\n *\n * - **`serialization` is plain data too.** The config field is `z.enum(['json', 'superjson'])`\n * (`config/schema.ts:147`) — a selector, not a transformer. `resolveTransformer` turns it into the\n * functions, and it already ships from `theokit/server`. So this half is a literal like the rest,\n * and the deployed entry resolves it exactly the way `theokit start` does\n * (`cli/commands/start/index.ts:108`), from the same string, through the same function.\n * - **`plugins` genuinely carries functions.** A plugin is constructed in `theo.config.ts` and there\n * is no literal for a closure, so this half needs the entry to import a module instead.\n *\n * ## Static import, not `import()` in the request path\n *\n * The tempting shortcut is `await import('../../theo.config.js')` inside the handler. It trades a\n * silent failure for a louder one on targets with no filesystem, and it moves configuration\n * resolution into every request on the targets that do have one.\n *\n * What this emits instead is a TOP-LEVEL import of a module the build already resolved and wrote\n * beside the entry — the same shape `renderBakedRoutes` uses for route modules (#369): decide on\n * the build machine, emit a static specifier. It is evaluated once at module load, the target's\n * bundler can see through it, and a plugin that needs an API the target lacks fails the build\n * rather than the first request.\n *\n * ## Why each half is optional\n *\n * An app that declares neither concern must produce the entry it produced before this existed.\n * Importing a module the build did not emit fails at load, and spreading an empty object costs an\n * allocation per request for nothing. So an empty request renders to nothing at all, and the\n * caller's `executeRoute` literal is unchanged.\n *\n * The halves are independent on purpose: an app that only picks `superjson` must not be made to\n * carry a plugins module, and an app with plugins and default JSON must not gain a transformer\n * lookup. Coupling them would have made the common case pay for the rare one.\n */\n\n/**\n * The option every Web-standards adapter grows to carry non-serialisable configuration.\n *\n * Composed into each adapter's option type the way `DeployedCsrfOptions` already is, so the six\n * targets cannot drift into six spellings of the same field.\n */\n/**\n * The project's `serverDir`, carried into every generated entrypoint (#RFC server-layout).\n *\n * Separate from the other option groups because it is not a runtime feature toggle: it is the\n * project's own layout, and an adapter that hardcodes `'server'` agrees with the default by\n * COINCIDENCE. The moment a project sets the option — the entire point of it existing — the\n * generated entrypoint resolves a directory that is not there, and only after deploy: the build\n * succeeds, the bundle is written, and routes 404 in production with nothing naming the cause.\n */\nexport interface DeployedServerDirOptions {\n /** Project-relative server directory. Absent ⇒ the schema default, `server`. */\n serverDir?: string\n}\n\n/**\n * The server directory as a quoted TypeScript literal, ready to interpolate into generated source.\n *\n * `JSON.stringify` rather than wrapping in quotes by hand: the value reaches this from user config,\n * so a directory containing a quote or a backslash would otherwise emit a syntax error into\n * somebody else's build — a worse failure than the one this fixes.\n */\nexport function serverDirLiteral(opts: DeployedServerDirOptions): string {\n return JSON.stringify(opts.serverDir ?? 'server')\n}\n\n/**\n * The agents directory, same shape and same reason as `serverDir` above.\n *\n * `scanAgents(projectRoot, agentsDirName = 'agents')` takes the name as its SECOND parameter\n * (`server/scan/agent-scan.ts:56`). Nine call sites pass the configured value; the generated\n * deploy entry was the only one that did not, so the default won and a project whose agents live\n * under the documented `core/agents` (`config/schema.ts:66`) served none of them — 404 per agent,\n * after deploy, with nothing naming the cause. Measured 2026-09-21 by executing `buildBun`\n * against a config carrying both directories and reading what it emitted.\n */\nexport interface DeployedAgentsDirOptions {\n /** Project-relative agents directory. Absent ⇒ the schema default, `agents`. */\n agentsDir?: string\n}\n\nexport function agentsDirLiteral(opts: DeployedAgentsDirOptions): string {\n return JSON.stringify(opts.agentsDir ?? 'agents')\n}\n\nexport interface DeployedRuntimeConfigOptions {\n /**\n * Specifier of the plugins module the build wrote beside the entry, or `undefined` when the app\n * declares no plugins and the build wrote none.\n */\n runtimeConfigModule?: string\n /**\n * The app's `serialization` selector, carried as a literal.\n *\n * `'json'` and `undefined` both mean the default, and neither emits anything: `executeRoute`\n * already falls back to `JSON.stringify`, and the `x-theo-transformer` header is deliberately\n * absent for the default so a client is told only when there is something to be told.\n */\n serialization?: 'json' | 'superjson'\n}\n\n/** The three places an entry has to grow to carry non-serialisable configuration. */\nexport interface DeployedRuntimeConfigFragment {\n /** Top-level imports. Empty when the build emitted no config module. */\n readonly imports: string[]\n /** Module-scope declarations — evaluated once, at load. Empty when there is nothing to carry. */\n readonly declarations: string[]\n /**\n * Spread into the entry's `executeRoute({ … })` literal, inside an async function.\n *\n * Empty string when there is nothing to carry, so the call site keeps the exact shape it had.\n */\n readonly executeRouteSpread: string\n}\n\nconst EMPTY: DeployedRuntimeConfigFragment = {\n imports: [],\n declarations: [],\n executeRouteSpread: '',\n}\n\n/**\n * What a deployed entry needs in order to apply `config.plugins` and `config.serialization`.\n *\n * @param moduleSpecifier - specifier of the runtime-config module the build emitted beside the\n * entry, or `undefined` when the app declared neither concern and the build emitted none.\n */\nexport function deployedRuntimeConfigFragment(\n options: DeployedRuntimeConfigOptions | undefined,\n): DeployedRuntimeConfigFragment {\n const pluginsModule = options?.runtimeConfigModule\n // 'json' is the default and emits nothing — see `serialization` above.\n const serialization = options?.serialization === 'superjson' ? 'superjson' : undefined\n if (pluginsModule === undefined && serialization === undefined) return EMPTY\n\n const imports: string[] = []\n const declarations: string[] = []\n const spread: string[] = []\n\n if (pluginsModule !== undefined) {\n imports.push(\n `import { createPluginRunnerFromConfig } from 'theokit/server'`,\n `// #425 — the app's own plugins, resolved on the build machine and written beside this entry.`,\n `// A closure has no literal, so this is an import rather than a baked value.`,\n `import theoRuntimeConfig from '${pluginsModule}'`,\n )\n declarations.push(\n `// Built ONCE, at module load. A runner rebuilt per request would re-run every plugin's`,\n `// \\`register\\`, which is where a plugin allocates the state its hooks then read.`,\n `const THEO_PLUGIN_RUNNER = createPluginRunnerFromConfig(theoRuntimeConfig.plugins)`,\n )\n // Awaited, not passed along: `createPluginRunnerFromConfig` is async because `register` is, and\n // a pending promise handed to `executeRoute` is a truthy object with none of the runner's\n // methods — every hook would silently not fire, which is this issue's own defect one layer in.\n spread.push(`pluginRunner: await THEO_PLUGIN_RUNNER`)\n }\n\n if (serialization !== undefined) {\n imports.push(`import { resolveTransformer } from 'theokit/server'`)\n declarations.push(\n `// #425 — a literal, because \\`config.serialization\\` is a SELECTOR and not a transformer.`,\n `// Same string, same function \\`theokit start\\` calls, so the deployed response and the local`,\n `// one cannot disagree about what the app asked for — including the \\`x-theo-transformer\\``,\n `// header, whose absence is what made this a data bug rather than a formatting one.`,\n `const THEO_TRANSFORMER = resolveTransformer('${serialization}')`,\n )\n spread.push(`transformer: THEO_TRANSFORMER`)\n }\n\n return { imports, declarations, executeRouteSpread: `${spread.join(', ')},` }\n}\n","/**\n * The request id a deployed entry uses, and the header it echoes back.\n *\n * ## The defect this closes\n *\n * Every generated entry minted a fresh `randomUUID()` per request and set no correlation header\n * at all on a success path (usetheokit/theokit#410). Both Node paths do the opposite: they resolve\n * an incoming `traceparent` / `x-request-id` through `extractTraceId` and echo the result under\n * both `x-request-id` and `x-trace-id` (`cli/commands/start/request-handler.ts`,\n * `vite-plugin/api-middleware.ts`).\n *\n * The consequence is that a trace crossing into a deployed function starts over. The caller's id\n * is discarded, and the response carries nothing to correlate against — so a request that fails in\n * production cannot be tied to the client that made it, which is the one situation the id exists\n * for.\n *\n * ## Why `setHeader` before the handler, rather than wrapping the response\n *\n * It is what the Node path does, and the shim reproduces Node's semantics exactly: `writeHead`\n * MERGES into the header map rather than replacing it (`web-shim.ts`), so a header set here\n * survives the handler's own `writeHead` and a handler that sets its own id still wins. Wrapping\n * the finished `Response` instead would have to mutate a response whose body is already streaming\n * (#382), and would miss the branches that return before the shim is built.\n */\n\n/**\n * Lines that resolve the request id and echo it, as generated source.\n *\n * @param requestVar - the name the entry gave the Web `Request` in scope\n * @param indent - leading whitespace, so the emitted file stays readable\n */\nexport function deployedTraceFragment(requestVar: string, indent: string): string[] {\n return [\n `${indent}// #410 — honour the caller's trace id instead of minting a new one, and echo it.`,\n `${indent}// \\`extractTraceIdFromRequest\\` validates the caller-controlled \\`x-request-id\\``,\n `${indent}// before trusting it, and falls back to a fresh UUID when neither header is present.`,\n `${indent}const requestId = extractTraceIdFromRequest(${requestVar})`,\n `${indent}res.setHeader('x-request-id', requestId)`,\n `${indent}res.setHeader(TRACE_HEADER, requestId)`,\n ]\n}\n"],"mappings":"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","names":["agentsDirLiteral","EMPTY"]}
|