@copilotkit/aimock 1.37.4 → 1.38.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.
Files changed (153) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +22 -0
  4. package/README.md +27 -9
  5. package/dist/agui-recorder.cjs +6 -0
  6. package/dist/agui-recorder.cjs.map +1 -1
  7. package/dist/agui-recorder.d.cts.map +1 -1
  8. package/dist/agui-recorder.d.ts.map +1 -1
  9. package/dist/agui-recorder.js +6 -0
  10. package/dist/agui-recorder.js.map +1 -1
  11. package/dist/agui-types.d.ts.map +1 -1
  12. package/dist/api-key-auth.cjs +166 -0
  13. package/dist/api-key-auth.cjs.map +1 -0
  14. package/dist/api-key-auth.d.cts +17 -0
  15. package/dist/api-key-auth.d.cts.map +1 -0
  16. package/dist/api-key-auth.d.ts +17 -0
  17. package/dist/api-key-auth.d.ts.map +1 -0
  18. package/dist/api-key-auth.js +158 -0
  19. package/dist/api-key-auth.js.map +1 -0
  20. package/dist/cli.cjs +4 -1
  21. package/dist/cli.cjs.map +1 -1
  22. package/dist/cli.js +4 -1
  23. package/dist/cli.js.map +1 -1
  24. package/dist/config-loader.cjs +4 -2
  25. package/dist/config-loader.cjs.map +1 -1
  26. package/dist/config-loader.d.cts +2 -1
  27. package/dist/config-loader.d.cts.map +1 -1
  28. package/dist/config-loader.d.ts +2 -1
  29. package/dist/config-loader.d.ts.map +1 -1
  30. package/dist/config-loader.js +5 -3
  31. package/dist/config-loader.js.map +1 -1
  32. package/dist/fal-audio.cjs +2 -0
  33. package/dist/fal-audio.cjs.map +1 -1
  34. package/dist/fal-audio.d.cts.map +1 -1
  35. package/dist/fal-audio.d.ts.map +1 -1
  36. package/dist/fal-audio.js +2 -0
  37. package/dist/fal-audio.js.map +1 -1
  38. package/dist/fal.cjs +4 -1
  39. package/dist/fal.cjs.map +1 -1
  40. package/dist/fal.d.cts.map +1 -1
  41. package/dist/fal.d.ts.map +1 -1
  42. package/dist/fal.js +4 -1
  43. package/dist/fal.js.map +1 -1
  44. package/dist/fixture-loader.cjs +0 -0
  45. package/dist/fixture-loader.cjs.map +1 -1
  46. package/dist/fixture-loader.d.cts +3 -1
  47. package/dist/fixture-loader.d.cts.map +1 -1
  48. package/dist/fixture-loader.d.ts +3 -1
  49. package/dist/fixture-loader.d.ts.map +1 -1
  50. package/dist/fixture-loader.js +0 -0
  51. package/dist/fixture-loader.js.map +1 -1
  52. package/dist/grok-video.cjs +9 -2
  53. package/dist/grok-video.cjs.map +1 -1
  54. package/dist/grok-video.js +10 -3
  55. package/dist/grok-video.js.map +1 -1
  56. package/dist/helpers.cjs +16 -22
  57. package/dist/helpers.cjs.map +1 -1
  58. package/dist/helpers.d.cts.map +1 -1
  59. package/dist/helpers.d.ts.map +1 -1
  60. package/dist/helpers.js +16 -21
  61. package/dist/helpers.js.map +1 -1
  62. package/dist/index.d.cts +2 -2
  63. package/dist/index.d.ts +2 -2
  64. package/dist/llmock.cjs +14 -3
  65. package/dist/llmock.cjs.map +1 -1
  66. package/dist/llmock.d.cts +4 -2
  67. package/dist/llmock.d.cts.map +1 -1
  68. package/dist/llmock.d.ts +4 -2
  69. package/dist/llmock.d.ts.map +1 -1
  70. package/dist/llmock.js +15 -5
  71. package/dist/llmock.js.map +1 -1
  72. package/dist/openrouter-chat.cjs +333 -0
  73. package/dist/openrouter-chat.cjs.map +1 -0
  74. package/dist/openrouter-chat.js +325 -0
  75. package/dist/openrouter-chat.js.map +1 -0
  76. package/dist/openrouter-video.cjs +24 -7
  77. package/dist/openrouter-video.cjs.map +1 -1
  78. package/dist/openrouter-video.d.cts.map +1 -1
  79. package/dist/openrouter-video.d.ts.map +1 -1
  80. package/dist/openrouter-video.js +25 -8
  81. package/dist/openrouter-video.js.map +1 -1
  82. package/dist/provider-auth.cjs +20 -0
  83. package/dist/provider-auth.cjs.map +1 -1
  84. package/dist/provider-auth.js +20 -1
  85. package/dist/provider-auth.js.map +1 -1
  86. package/dist/recorder.cjs +50 -8
  87. package/dist/recorder.cjs.map +1 -1
  88. package/dist/recorder.d.cts.map +1 -1
  89. package/dist/recorder.d.ts.map +1 -1
  90. package/dist/recorder.js +50 -10
  91. package/dist/recorder.js.map +1 -1
  92. package/dist/responses.cjs +200 -89
  93. package/dist/responses.cjs.map +1 -1
  94. package/dist/responses.d.cts +18 -3
  95. package/dist/responses.d.cts.map +1 -1
  96. package/dist/responses.d.ts +18 -3
  97. package/dist/responses.d.ts.map +1 -1
  98. package/dist/responses.js +199 -90
  99. package/dist/responses.js.map +1 -1
  100. package/dist/server.cjs +156 -66
  101. package/dist/server.cjs.map +1 -1
  102. package/dist/server.d.cts +1 -1
  103. package/dist/server.d.cts.map +1 -1
  104. package/dist/server.d.ts +1 -1
  105. package/dist/server.d.ts.map +1 -1
  106. package/dist/server.js +157 -68
  107. package/dist/server.js.map +1 -1
  108. package/dist/sse-writer.cjs +3 -0
  109. package/dist/sse-writer.cjs.map +1 -1
  110. package/dist/sse-writer.d.cts +8 -0
  111. package/dist/sse-writer.d.cts.map +1 -1
  112. package/dist/sse-writer.d.ts +8 -0
  113. package/dist/sse-writer.d.ts.map +1 -1
  114. package/dist/sse-writer.js +3 -0
  115. package/dist/sse-writer.js.map +1 -1
  116. package/dist/stream-collapse.cjs +26 -0
  117. package/dist/stream-collapse.cjs.map +1 -1
  118. package/dist/stream-collapse.d.cts +7 -0
  119. package/dist/stream-collapse.d.cts.map +1 -1
  120. package/dist/stream-collapse.d.ts +7 -0
  121. package/dist/stream-collapse.d.ts.map +1 -1
  122. package/dist/stream-collapse.js +26 -0
  123. package/dist/stream-collapse.js.map +1 -1
  124. package/dist/transcription.cjs +49 -2
  125. package/dist/transcription.cjs.map +1 -1
  126. package/dist/transcription.d.cts.map +1 -1
  127. package/dist/transcription.d.ts.map +1 -1
  128. package/dist/transcription.js +50 -3
  129. package/dist/transcription.js.map +1 -1
  130. package/dist/types.d.cts +187 -5
  131. package/dist/types.d.cts.map +1 -1
  132. package/dist/types.d.ts +187 -5
  133. package/dist/types.d.ts.map +1 -1
  134. package/dist/veo-video.cjs +9 -2
  135. package/dist/veo-video.cjs.map +1 -1
  136. package/dist/veo-video.js +10 -3
  137. package/dist/veo-video.js.map +1 -1
  138. package/dist/ws-realtime.cjs +306 -57
  139. package/dist/ws-realtime.cjs.map +1 -1
  140. package/dist/ws-realtime.d.cts +1 -0
  141. package/dist/ws-realtime.d.cts.map +1 -1
  142. package/dist/ws-realtime.d.ts +1 -0
  143. package/dist/ws-realtime.d.ts.map +1 -1
  144. package/dist/ws-realtime.js +307 -58
  145. package/dist/ws-realtime.js.map +1 -1
  146. package/dist/ws-responses.cjs +11 -6
  147. package/dist/ws-responses.cjs.map +1 -1
  148. package/dist/ws-responses.d.cts.map +1 -1
  149. package/dist/ws-responses.d.ts.map +1 -1
  150. package/dist/ws-responses.js +12 -7
  151. package/dist/ws-responses.js.map +1 -1
  152. package/package.json +1 -1
  153. package/skills/write-fixtures/SKILL.md +53 -19
@@ -9,7 +9,7 @@
9
9
  "source": {
10
10
  "source": "npm",
11
11
  "package": "@copilotkit/aimock",
12
- "version": "^1.35.1"
12
+ "version": "^1.38.0"
13
13
  },
14
14
  "description": "Fixture authoring skill for @copilotkit/aimock — LLM, multimedia (image/TTS/transcription/video), MCP, A2A, AG-UI, vector, embeddings, structured output, sequential responses, streaming physics, record/replay, agent loop patterns, and debugging"
15
15
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aimock",
3
- "version": "1.35.1",
3
+ "version": "1.38.0",
4
4
  "description": "Fixture authoring guidance for @copilotkit/aimock — LLM, multimedia, MCP, A2A, AG-UI, vector, and service mocking",
5
5
  "author": {
6
6
  "name": "CopilotKit"
package/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # @copilotkit/aimock
2
2
 
3
+ ## [Unreleased]
4
+
5
+ ## [1.38.0] - 2026-08-03
6
+
7
+ ### Added
8
+
9
+ - OpenAI transcription replay for `gpt-transcribe` HTTP streams and `gpt-live-transcribe` Realtime sessions. Recorded responses retain transcript languages and usage metadata; replay supports progressive transcript events, timing controls, strict fixture matching, and interruption behavior.
10
+ - Opt-in inbound API-key validation for HTTP, control, mount, and WebSocket boundaries. Validated test credentials are never forwarded by the generic record/proxy path; auth-enabled proxying requires a configured provider credential.
11
+ - API-key egress isolation is request-scoped across generic, AG-UI, fal, and video record paths. Authorization schemes are case-insensitive, and OpenRouter off-origin unsigned URLs never receive a configured provider credential.
12
+ - `aimock-pytest` `0.5.2` is released by the Release workflow after npm `@copilotkit/aimock` `1.38.0`; the PyPI job verifies the pinned npm package is available before building a wheel.
13
+ - **Reasoning `encrypted_content` on the Responses API.** aimock now synthesizes an opaque (base64) `reasoning.encrypted_content` blob on the TERMINAL reasoning item (`response.output_item.done` and non-streaming `output[]`). Emitted only when the request wants it: `include: ["reasoning.encrypted_content"]` OR a stateless request (`store: false` / ZDR). Stored / opted-out replays stay byte-identical. A request carrying the EXPLICIT `include: ["reasoning.encrypted_content"]` opt-in, against a reasoning-CAPABLE model, now gets a reasoning item even when the fixture declares NO `reasoning` summary — a `summary: []` item that exists purely to carry the blob, matching what real OpenAI returns when summaries were never requested. This is the shape a fixture recorded from the real stateless agent-framework flow has, so gating the item on a declared summary starved the exact case the feature exists to serve. The synthesized item leads `output[]` / takes `output_index` 0 and shifts the rest, emits `output_item.added` → `output_item.done` with no summary-text events (there is no summary part to describe), and is suppressed for non-reasoning models (gpt-4o etc.), which have no reasoning channel at all. Note the two gates differ in width on purpose: `store: false` still attaches the blob to a reasoning item that a declared summary already produces, but it does NOT synthesize an item where the fixture declares none — creating an output item that did not previously exist is a bigger behavior change than adding a field to one already on the wire, so it takes the gate that is directly observable in the request. `agent-framework-openai` >= 1.11.0 sends `include` and never `store`, so the narrower gate costs the feature nothing. This lets `agent-framework-openai` >= 1.11.0 replay reasoning-paired tool calls on a stateless reasoning + multi-tool chain (related: microsoft/agent-framework#7233). Applies to both the HTTP and WebSocket Responses transports. Known limitations: the blob is withheld from the in-progress `added` item, which is aimock's own simplification rather than upstream parity — real OpenAI populates `added` too (recorded captures carry a shorter blob there beside an empty summary, re-encrypted by `done`), but the field is `anyOf: [string, null]` and non-required, so omitting it is contract-legal and no known consumer requires it; and aimock skips inbound reasoning items, so it cannot reproduce OpenAI's `invalid_encrypted_content` rejection of a mismatched blob/id.
14
+ - **OpenRouter chat / LLM router simulation.** Requests whose original path starts with `/api/v1/` (point any OpenAI SDK at a `baseURL` ending `/api/v1`) are detected as OpenRouter and shaped to match real OpenRouter bytes: a `gen-` id prefix (a fixture `id` override still wins), a top-level `provider` (default = the winning model slug's author, fixture-overridable via `provider`), both `finish_reason` and `native_finish_reason` on every choice/delta, an always-present `system_fingerprint` and `service_tier` (null by default), `message.reasoning`, and a rich `usage` with a **fixture-scriptable** `cost` + `cost_details` (emitted only when a fixture supplies a cost — never fabricated), plus `is_byok` / `prompt_tokens_details` / `completion_tokens_details` when overridden. Callers on the plain OpenAI `/v1/...` base are byte-for-byte unchanged.
15
+ - **`models[]` fallback (router failover) simulation.** When the request body carries `models: [...]`, aimock walks `[model, ...models]` in order and serves the first fixture returning a NON-error response; a `429`/`503` error fixture on a candidate simulates a runtime provider failure and falls through to the next. The winning slug is echoed back as the top-level `model`, so a test asserts failover by reading `response.model`. (Deliberate non-goal: an unknown/invalid model is a fixture miss, not OpenRouter's up-front invalid-model 400.)
16
+ - **OpenRouter discovery endpoints** `GET /api/v1/models` (catalog synthesized from loaded chat fixtures), `GET /api/v1/key`, and `GET /api/v1/credits`, matching the real response shapes.
17
+ - **OpenRouter error envelope** `{ error: { message, code } }` (numeric `code` == HTTP status, optional free-form `metadata`) for OpenRouter requests; the OpenAI error shape is retained for `/v1/...`.
18
+ - **Opt-in `: OPENROUTER PROCESSING` SSE keepalive** (fixture option `openRouterProcessing`, default off): one comment line emitted before the first data frame, matching real OpenRouter streams.
19
+ - OpenRouter request extensions (`provider`, `models`, `route`, `reasoning`, `plugins`, `prediction`, `usage`) and attribution headers (`HTTP-Referer`, `X-OpenRouter-Title`, legacy `X-Title`) are accepted and journaled, never required or rejected.
20
+
21
+ ### Fixed
22
+
23
+ - **`GET /__aimock/journal` no longer leaks accepted inbound credentials.** Journal header redaction is now derived from the same registry that decides which headers are accepted as credentials, so the two can no longer drift apart. Previously only `authorization`, `x-api-key` and `api-key` were redacted while `x-goog-api-key` and `xi-api-key` were accepted as credentials and journaled **in plaintext** — so a real Gemini / Veo / ElevenLabs key sent on those headers appeared verbatim in the journal, which is unauthenticated unless inbound API-key validation is enabled. Affects 1.37.4 and earlier. A test asserts every accepted header redacts, so adding a new accepted header without redacting it now fails.
24
+
3
25
  ## [1.37.4] - 2026-07-20
4
26
 
5
27
  ### Fixed
package/README.md CHANGED
@@ -34,14 +34,14 @@ await mock.stop();
34
34
 
35
35
  aimock mocks everything your AI app talks to:
36
36
 
37
- | Tool | What it mocks | Docs |
38
- | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- |
39
- | **LLMock** | OpenAI (Chat/Responses/Realtime GA+Beta), Claude, Gemini (REST/Live/Interactions/Embeddings), Bedrock, Azure, Vertex AI, Ollama (chat/embeddings), Cohere (chat/embed), ElevenLabs TTS | [Providers](https://aimock.copilotkit.dev/docs) |
40
- | **MCPMock** | MCP tools, resources, prompts with session management | [MCP](https://aimock.copilotkit.dev/mcp-mock) |
41
- | **A2AMock** | Agent-to-agent protocol with SSE streaming | [A2A](https://aimock.copilotkit.dev/a2a-mock) |
42
- | **AGUIMock** | AG-UI agent-to-UI event streams for frontend testing | [AG-UI](https://aimock.copilotkit.dev/agui-mock) |
43
- | **VectorMock** | Pinecone, Qdrant, ChromaDB compatible endpoints | [Vector](https://aimock.copilotkit.dev/vector-mock) |
44
- | **Services** | Tavily search, Cohere rerank, OpenAI moderation, ElevenLabs TTS | [Services](https://aimock.copilotkit.dev/services) |
37
+ | Tool | What it mocks | Docs |
38
+ | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- |
39
+ | **LLMock** | OpenAI (Chat/Responses/Realtime GA+Beta), Claude, Gemini (REST/Live/Interactions/Embeddings), Bedrock, Azure, Vertex AI, Ollama (chat/embeddings), Cohere (chat/embed), OpenRouter (chat/router), ElevenLabs TTS | [Providers](https://aimock.copilotkit.dev/docs) |
40
+ | **MCPMock** | MCP tools, resources, prompts with session management | [MCP](https://aimock.copilotkit.dev/mcp-mock) |
41
+ | **A2AMock** | Agent-to-agent protocol with SSE streaming | [A2A](https://aimock.copilotkit.dev/a2a-mock) |
42
+ | **AGUIMock** | AG-UI agent-to-UI event streams for frontend testing | [AG-UI](https://aimock.copilotkit.dev/agui-mock) |
43
+ | **VectorMock** | Pinecone, Qdrant, ChromaDB compatible endpoints | [Vector](https://aimock.copilotkit.dev/vector-mock) |
44
+ | **Services** | Tavily search, Cohere rerank, OpenAI moderation, ElevenLabs TTS | [Services](https://aimock.copilotkit.dev/services) |
45
45
 
46
46
  Run them all on one port with `npx @copilotkit/aimock --config aimock.json`, or use the programmatic API to compose exactly what you need.
47
47
 
@@ -50,7 +50,7 @@ Run them all on one port with `npx @copilotkit/aimock --config aimock.json`, or
50
50
  - **[Record & Replay](https://aimock.copilotkit.dev/record-replay)** — Proxy real APIs, save as fixtures, replay deterministically forever
51
51
  - **Timing-aware recording and replay** — Recorded fixtures capture per-frame arrival timestamps; replay uses recorded timings for approximate timing reproduction based on recorded TTFT and inter-frame cadence (replay chunk count may differ from recording — TTFT and average pace are preserved, not per-token fidelity) with configurable `--replay-speed` multiplier
52
52
  - **[Multi-turn Conversations](https://aimock.copilotkit.dev/multi-turn)** — Record and replay multi-turn traces with tool rounds; match distinct turns via `turnIndex`, `hasToolResult`, `toolCallId`, `toolResultContains` (gate on the tool-result payload), `sequenceIndex`, `systemMessage` (gate on host-supplied agent context), or custom predicates
53
- - **[12 providers across 14 API surfaces](https://aimock.copilotkit.dev/docs)** — OpenAI Chat, OpenAI Responses, OpenAI Realtime (GA + Beta shim), Claude, Gemini (REST + embedContent), Gemini Live, Gemini Interactions, Azure, Bedrock, Vertex AI, Ollama (chat + embeddings), Cohere (chat + embed), ElevenLabs TTS — full streaming support
53
+ - **[13 providers across 15 API surfaces](https://aimock.copilotkit.dev/docs)** — OpenAI Chat, OpenAI Responses, OpenAI Realtime (GA + Beta shim), Claude, Gemini REST, Gemini embedContent, Gemini Live, Gemini Interactions, Azure, Bedrock, Vertex AI, Ollama (chat + embeddings), Cohere (chat + embed), OpenRouter (chat + router failover), ElevenLabs TTS — full streaming support
54
54
  - **Multimedia APIs** — [image generation](https://aimock.copilotkit.dev/images) (DALL-E, Imagen), [image editing](https://aimock.copilotkit.dev/images) (/v1/images/edits), [text-to-speech](https://aimock.copilotkit.dev/speech) (OpenAI + ElevenLabs), [audio transcription](https://aimock.copilotkit.dev/transcription), [audio translation](https://aimock.copilotkit.dev/transcription) (/v1/audio/translations), [video generation](https://aimock.copilotkit.dev/video), [OpenRouter video generation](https://aimock.copilotkit.dev/openrouter-video) (/api/v1/videos with async job lifecycle), [Google Veo video generation](https://aimock.copilotkit.dev/veo-video) (:predictLongRunning + /v1beta/operations async lifecycle), [Grok Imagine video generation](https://aimock.copilotkit.dev/grok-video) (/v1/videos/generations with async job lifecycle), [fal.ai](https://aimock.copilotkit.dev/fal-ai) (image / video / audio with queue lifecycle)
55
55
  - **[MCP](https://aimock.copilotkit.dev/mcp-mock) / [A2A](https://aimock.copilotkit.dev/a2a-mock) / [AG-UI](https://aimock.copilotkit.dev/agui-mock) / [Vector](https://aimock.copilotkit.dev/vector-mock)** — Mock every protocol your AI agents use
56
56
  - **[Chaos Testing](https://aimock.copilotkit.dev/chaos-testing)** — 500 errors, malformed JSON, mid-stream disconnects at any probability
@@ -126,6 +126,24 @@ Private and link-local addresses (loopback, RFC1918, CGNAT, cloud metadata, ULA,
126
126
 
127
127
  On replay, `turnIndex` is a non-fatal disambiguator, not a hard reject gate: a content-matching fixture is served even when its scripted `turnIndex` differs from the request's assistant-message count. This kills false "no fixture matched" misses for multi-bubble agent runs (multi-step agents emit several assistant bubbles per logical turn). When a served fixture diverges from its scripted `turnIndex`, the match diagnostic carries `turnIndexRelaxed: true` and aimock logs a one-shot warning (at the `warn` log level — silent by default). To restore the legacy strict behavior where a defined `turnIndex` must equal the assistant count exactly, set `AIMOCK_STRICT_TURN_INDEX=1`. The record path is always strict regardless of this flag.
128
128
 
129
+ ## API-key validation
130
+
131
+ By default aimock accepts all requests. Opt into inbound test-client validation with a programmatic option, top-level `aimock.json` field, or environment-only key list:
132
+
133
+ ```ts
134
+ await createServer(fixtures, { auth: { apiKeys: ["test-key"] } });
135
+ ```
136
+
137
+ ```json
138
+ { "auth": { "apiKeys": ["test-key"] } }
139
+ ```
140
+
141
+ ```bash
142
+ AIMOCK_API_KEYS=test-key,rotated-key npx @copilotkit/aimock --config aimock.json
143
+ ```
144
+
145
+ Use `Authorization: Bearer <key>`, `Authorization: Key <key>`, `x-api-key`, `x-goog-api-key`, `api-key`, or `xi-api-key`. Every supplied credential must resolve to one configured key; mismatches return `401` with an OpenAI-compatible authentication error. HTTP routes, control APIs, mounts, and WebSocket upgrades are protected. Genuine CORS preflights plus `GET /health`, `GET /ready`, and `GET /metrics` remain public. This is inbound test access control, distinct from `record.providerKeys`; when enabled, proxying strips test credentials and requires a configured static provider credential before egress.
146
+
129
147
  ### aimock-owned upstream keys — `AIMOCK_PROVIDER_*_KEY`
130
148
 
131
149
  In record or `--proxy-only` mode, aimock forwards the caller's auth header to the real provider unchanged. If your tests can only send a dummy placeholder key (e.g. an SDK that refuses to start without a non-empty API key), aimock can inject its own configured upstream key on a fixture-miss passthrough so the proxied call actually authenticates. Each provider has an independent env var, and the key is applied with the provider-correct wire scheme:
@@ -1,4 +1,5 @@
1
1
  const require_runtime = require('./_virtual/_rolldown/runtime.cjs');
2
+ const require_api_key_auth = require('./api-key-auth.cjs');
2
3
  const require_agui_handler = require('./agui-handler.cjs');
3
4
  let node_http = require("node:http");
4
5
  node_http = require_runtime.__toESM(node_http);
@@ -63,6 +64,11 @@ function resolveAGUIRecordBufferCap(configured) {
63
64
  * or `false` if no upstream is configured.
64
65
  */
65
66
  async function proxyAndRecordAGUI(req, res, input, fixtures, config, logger) {
67
+ if (require_api_key_auth.isAuthenticatedRequest(req)) {
68
+ res.writeHead(502, { "Content-Type": "application/json" });
69
+ res.end(JSON.stringify({ error: "No configured provider credential" }));
70
+ return 502;
71
+ }
66
72
  if (!config.upstream) {
67
73
  logger.warn("No upstream URL configured for AG-UI recording — cannot proxy");
68
74
  return false;
@@ -1 +1 @@
1
- {"version":3,"file":"agui-recorder.cjs","names":["https","http","getLastMessageIfToolResult","extractLastUserMessage","crypto","path"],"sources":["../src/agui-recorder.ts"],"sourcesContent":["import * as http from \"node:http\";\nimport * as https from \"node:https\";\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport * as crypto from \"node:crypto\";\nimport type {\n AGUIFixture,\n AGUIFixtureMatch,\n AGUIRecordConfig,\n AGUIEvent,\n AGUIRunAgentInput,\n} from \"./agui-types.js\";\nimport { extractLastUserMessage, getLastMessageIfToolResult } from \"./agui-handler.js\";\nimport type { Logger } from \"./logger.js\";\n\n/**\n * Sentinel `match.message` value written to disk when the request had no\n * extractable user text. Keeps the on-disk fixture serializable (predicate\n * matchers aren't) but won't match any real user input on replay.\n */\nexport const NO_USER_MESSAGE_SENTINEL = \"__NO_USER_MESSAGE__\";\n\n/**\n * Default ceiling (bytes) for the in-memory AG-UI record buffer. The recorder\n * tees every upstream SSE chunk straight to the client AND buffers a copy so it\n * can `Buffer.concat(chunks).toString()` + parse the SSE events into a fixture\n * once the stream ends. With no cap, a large upstream response (amplified since\n * the real-key fixture-miss passthrough landed) builds a string past V8's\n * ~512 MiB max string length and throws `RangeError: Invalid string length`.\n * 64 MiB mirrors the generic proxy path's `DEFAULT_MAX_PROXY_BUFFER_BYTES` and\n * is generous for any real agent turn. Overridable via\n * `AGUIRecordConfig.maxRecordBufferBytes`.\n */\nexport const DEFAULT_AGUI_RECORD_BUFFER_BYTES = 64 * 1024 * 1024; // 64 MiB\n\n/**\n * Absolute hard ceiling (bytes) for the AG-UI record buffer, independent of the\n * configurable `maxRecordBufferBytes`, mirroring the generic proxy path's\n * `PROXY_BUFFER_HARD_CEILING`. 256 MiB of bytes can never decode to more than\n * 256 Mi UTF-16 code units, comfortably below V8's 2^29 - 1 string-length limit,\n * so the eventual `Buffer.concat(chunks).toString()` can never throw.\n */\nexport const AGUI_RECORD_BUFFER_HARD_CEILING = 256 * 1024 * 1024; // 256 MiB\n\n/**\n * Test-only override of the effective AG-UI record-buffer ceiling. Lets the cap\n * suite exercise the over-cap truncation path with a small body instead of\n * streaming hundreds of MB. `undefined` (the default) uses the configured /\n * default cap. NEVER set from production code.\n */\nlet aguiRecordBufferCeilingOverride: number | undefined;\n\n/** @internal test-only — see {@link aguiRecordBufferCeilingOverride}. */\nexport function setAGUIRecordBufferCeilingForTests(value: number | undefined): void {\n aguiRecordBufferCeilingOverride = value;\n}\n\n/**\n * Resolve the effective AG-UI record-buffer byte cap: honor a test-only\n * override, else the configured `maxRecordBufferBytes` (clamped to the hard\n * ceiling), else the default. Non-finite / non-positive configured values fall\n * back to the default.\n */\nfunction resolveAGUIRecordBufferCap(configured: number | undefined): number {\n if (aguiRecordBufferCeilingOverride !== undefined) return aguiRecordBufferCeilingOverride;\n if (configured == null || !Number.isFinite(configured) || configured <= 0) {\n return DEFAULT_AGUI_RECORD_BUFFER_BYTES;\n }\n return Math.min(configured, AGUI_RECORD_BUFFER_HARD_CEILING);\n}\n\n/**\n * Proxy an unmatched AG-UI request to a real upstream agent, record the\n * SSE event stream as a fixture on disk and in memory, and relay the\n * response back to the original client in real time.\n *\n * Returns the HTTP status code written to the client if the request was proxied,\n * or `false` if no upstream is configured.\n */\nexport async function proxyAndRecordAGUI(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n input: AGUIRunAgentInput,\n fixtures: AGUIFixture[],\n config: AGUIRecordConfig,\n logger: Logger,\n): Promise<number | false> {\n if (!config.upstream) {\n logger.warn(\"No upstream URL configured for AG-UI recording — cannot proxy\");\n return false;\n }\n\n let target: URL;\n try {\n target = new URL(config.upstream);\n } catch (err) {\n const detail = err instanceof Error ? err.message : String(err);\n logger.error(`Invalid upstream AG-UI URL: ${config.upstream} — ${detail}`);\n res.writeHead(502, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Invalid upstream AG-UI URL\" }));\n return 502;\n }\n\n logger.warn(`NO AG-UI FIXTURE MATCH — proxying to ${config.upstream}`);\n\n // Build upstream request headers\n const forwardHeaders: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n Accept: \"text/event-stream\",\n };\n // Forward auth headers if present\n const authorization = req.headers[\"authorization\"];\n if (authorization) {\n forwardHeaders[\"Authorization\"] = Array.isArray(authorization)\n ? authorization.join(\", \")\n : authorization;\n }\n const apiKey = req.headers[\"x-api-key\"];\n if (apiKey) {\n forwardHeaders[\"x-api-key\"] = Array.isArray(apiKey) ? apiKey.join(\", \") : apiKey;\n }\n\n const requestBody = JSON.stringify(input);\n\n let status: number;\n try {\n status = await teeUpstreamStream(\n target,\n forwardHeaders,\n requestBody,\n res,\n input,\n fixtures,\n config,\n logger,\n );\n } catch (err) {\n const msg = err instanceof Error ? err.message : \"Unknown proxy error\";\n logger.error(`AG-UI proxy request failed: ${msg}`);\n if (!res.headersSent) {\n res.writeHead(502, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Upstream AG-UI agent unreachable\" }));\n } else if (!res.writableEnded) {\n res.end();\n }\n status = 502;\n }\n\n return status;\n}\n\n// ---------------------------------------------------------------------------\n// Internal: tee the upstream SSE stream to the client and buffer for recording\n// ---------------------------------------------------------------------------\n\nfunction teeUpstreamStream(\n target: URL,\n headers: Record<string, string>,\n body: string,\n clientRes: http.ServerResponse,\n input: AGUIRunAgentInput,\n fixtures: AGUIFixture[],\n config: AGUIRecordConfig,\n logger: Logger,\n): Promise<number> {\n return new Promise((resolve, reject) => {\n const transport = target.protocol === \"https:\" ? https : http;\n const UPSTREAM_TIMEOUT_MS = 30_000;\n\n const upstreamReq = transport.request(\n target,\n {\n method: \"POST\",\n timeout: UPSTREAM_TIMEOUT_MS,\n headers: {\n ...headers,\n \"Content-Length\": Buffer.byteLength(body).toString(),\n },\n },\n (upstreamRes) => {\n const upstreamStatus = upstreamRes.statusCode ?? 200;\n\n // Normalize status codes: aimock acts as a gateway, so upstream\n // provider details (429, 503, etc.) should not leak.\n // Successes → 200, errors → 502 (Bad Gateway).\n const clientStatus = upstreamStatus >= 200 && upstreamStatus < 300 ? 200 : 502;\n\n // Set appropriate headers on the client response.\n if (!clientRes.headersSent) {\n if (clientStatus === 200) {\n clientRes.writeHead(200, {\n \"Content-Type\": \"text/event-stream\",\n \"Cache-Control\": \"no-cache\",\n Connection: \"keep-alive\",\n });\n } else {\n const ct = upstreamRes.headers[\"content-type\"] || \"application/json\";\n clientRes.writeHead(502, { \"Content-Type\": ct });\n }\n }\n\n const chunks: Buffer[] = [];\n let clientWriteFailed = false;\n // Bound the in-memory record buffer so a single huge upstream response\n // cannot build a string past V8's ~512 MiB limit\n // (RangeError: Invalid string length) when we `Buffer.concat`+stringify\n // it below. The client relay above is INDEPENDENT of this buffer, so\n // capping it never truncates what the client receives — the cap means\n // \"don't journal\", not \"don't answer\" (mirrors the generic proxy path).\n const recordBufferCap = resolveAGUIRecordBufferCap(config.maxRecordBufferBytes);\n let bufferedBytes = 0;\n let recordTruncated = false;\n\n upstreamRes.on(\"data\", (chunk: Buffer) => {\n // Relay to client in real time\n try {\n clientRes.write(chunk);\n } catch (err) {\n if (!clientWriteFailed) {\n clientWriteFailed = true;\n logger?.warn(\n \"Client write failed during proxy relay:\",\n err instanceof Error ? err.message : String(err),\n );\n }\n }\n // Buffer for fixture construction, bounded by the record-buffer cap.\n // Once the cap is crossed we stop accumulating and free the buffer so\n // heap growth is bounded and the end-handler skips concat/stringify.\n if (recordTruncated) return;\n if (bufferedBytes + chunk.length > recordBufferCap) {\n recordTruncated = true;\n chunks.length = 0;\n bufferedBytes = 0;\n logger?.warn(\n `AG-UI upstream response exceeded the ${recordBufferCap}-byte record buffer cap — relaying full body to client, but skipping fixture recording to bound memory`,\n );\n return;\n }\n chunks.push(chunk);\n bufferedBytes += chunk.length;\n });\n\n let settled = false;\n\n upstreamRes.on(\"error\", (err) => {\n if (settled) return;\n settled = true;\n try {\n if (!clientRes.headersSent) {\n clientRes.writeHead(502, { \"Content-Type\": \"application/json\" });\n clientRes.end(JSON.stringify({ error: \"Upstream AG-UI agent unreachable\" }));\n } else if (!clientRes.writableEnded) {\n clientRes.end();\n }\n } catch (writeErr) {\n logger.warn(\n \"Failed to write error response to client:\",\n writeErr instanceof Error ? writeErr.message : String(writeErr),\n );\n }\n reject(err);\n });\n\n upstreamRes.on(\"end\", () => {\n if (settled) return;\n settled = true;\n\n // Don't record fixtures for non-2xx upstream responses\n if (clientStatus !== 200) {\n try {\n if (!clientRes.writableEnded) clientRes.end();\n } catch (writeErr) {\n logger.warn(\n \"Failed to end client response:\",\n writeErr instanceof Error ? writeErr.message : String(writeErr),\n );\n }\n resolve(clientStatus);\n return;\n }\n try {\n if (!clientRes.writableEnded) clientRes.end();\n } catch (writeErr) {\n logger.warn(\n \"Failed to end client response:\",\n writeErr instanceof Error ? writeErr.message : String(writeErr),\n );\n }\n\n // Record buffer cap tripped: the buffered copy is partial (and was\n // freed), so we cannot faithfully build a fixture. The client already\n // received every byte via the live tee above, so just skip recording\n // — never stringify the (now-empty) buffer, never persist a truncated\n // fixture. Mirrors the generic proxy path's over-cap \"skip recording,\n // still answer the client\" behavior.\n if (recordTruncated) {\n logger.warn(\n \"AG-UI record buffer cap exceeded — response relayed to client, recording skipped\",\n );\n resolve(clientStatus);\n return;\n }\n\n // Parse buffered SSE events\n const buffered = Buffer.concat(chunks).toString();\n const events = parseSSEEvents(buffered, logger);\n\n // Build fixture — three-way match priority:\n // 1. Tool-result continuation (HITL): match by toolCallId\n // 2. User message: match by last user message content\n // 3. Fallback predicate: no user message present\n let match: AGUIFixtureMatch;\n const lastToolResult = getLastMessageIfToolResult(input);\n if (lastToolResult?.toolCallId) {\n match = { toolCallId: lastToolResult.toolCallId };\n logger.info(`Recorded AG-UI fixture keyed on toolCallId=${lastToolResult.toolCallId}`);\n } else {\n const message = extractLastUserMessage(input);\n if (message) {\n match = { message };\n } else {\n match = {\n predicate: (inp: AGUIRunAgentInput) =>\n !inp.messages?.length || !inp.messages.some((m) => m.role === \"user\"),\n };\n logger.warn(\n \"Recorded AG-UI fixture has no user message — available in-memory only (predicate fixtures cannot be persisted to disk)\",\n );\n }\n }\n const fixture: AGUIFixture = { match, events };\n\n if (!config.proxyOnly) {\n // Register in memory first (always available even if disk write fails)\n fixtures.push(fixture);\n\n // Predicate fixtures (no user message, no toolCallId) cannot be\n // meaningfully serialized — the sentinel becomes a literal string\n // match that never matches real requests. Keep in-memory only.\n if (fixture.match.predicate) {\n logger.warn(\n \"Skipping disk write for predicate fixture — in-memory only (cannot be persisted)\",\n );\n } else {\n const serializableFixture = {\n match: fixture.match,\n events: fixture.events,\n ...(fixture.delayMs !== undefined ? { delayMs: fixture.delayMs } : {}),\n };\n\n const fixturePath = config.fixturePath ?? \"./fixtures/agui-recorded\";\n const timestamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n const filename = `agui-${timestamp}-${crypto.randomUUID().slice(0, 8)}.json`;\n const filepath = path.join(fixturePath, filename);\n\n try {\n fs.mkdirSync(fixturePath, { recursive: true });\n fs.writeFileSync(\n filepath,\n JSON.stringify({ fixtures: [serializableFixture] }, null, 2),\n \"utf-8\",\n );\n logger.warn(`AG-UI response recorded → ${filepath}`);\n } catch (err) {\n const msg = err instanceof Error ? err.message : \"Unknown filesystem error\";\n logger.error(\n `Failed to save AG-UI fixture to disk: ${msg} (fixture retained in memory)`,\n );\n }\n }\n } else {\n logger.info(\"Proxied AG-UI request (proxy-only mode)\");\n }\n\n resolve(clientStatus);\n });\n },\n );\n\n upstreamReq.on(\"timeout\", () => {\n upstreamReq.destroy(\n new Error(`Upstream AG-UI request timed out after ${UPSTREAM_TIMEOUT_MS / 1000}s`),\n );\n });\n\n upstreamReq.on(\"error\", (err) => {\n try {\n if (!clientRes.headersSent) {\n clientRes.writeHead(502, { \"Content-Type\": \"application/json\" });\n clientRes.end(JSON.stringify({ error: \"Upstream AG-UI agent unreachable\" }));\n } else if (!clientRes.writableEnded) {\n clientRes.end();\n }\n } catch (writeErr) {\n logger.warn(\n \"Failed to write error response to client:\",\n writeErr instanceof Error ? writeErr.message : String(writeErr),\n );\n }\n reject(err);\n });\n\n upstreamReq.write(body);\n upstreamReq.end();\n });\n}\n\n/**\n * Parse SSE data lines from buffered stream text.\n */\nfunction parseSSEEvents(text: string, logger?: Logger): AGUIEvent[] {\n const events: AGUIEvent[] = [];\n const blocks = text.split(\"\\n\\n\");\n for (const block of blocks) {\n const lines = block.split(\"\\n\");\n for (const line of lines) {\n if (line.startsWith(\"data:\")) {\n const payload = line.startsWith(\"data: \") ? line.slice(6) : line.slice(5);\n try {\n const parsed = JSON.parse(payload) as AGUIEvent;\n events.push(parsed);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n const warning = `Skipping unparseable SSE data line (${msg}): ${payload.slice(0, 200)}`;\n if (logger) logger.warn(warning);\n else console.warn(warning);\n }\n }\n }\n }\n return events;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"agui-recorder.cjs","names":["isAuthenticatedRequest","https","http","getLastMessageIfToolResult","extractLastUserMessage","crypto","path"],"sources":["../src/agui-recorder.ts"],"sourcesContent":["import * as http from \"node:http\";\nimport * as https from \"node:https\";\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport * as crypto from \"node:crypto\";\nimport type {\n AGUIFixture,\n AGUIFixtureMatch,\n AGUIRecordConfig,\n AGUIEvent,\n AGUIRunAgentInput,\n} from \"./agui-types.js\";\nimport { extractLastUserMessage, getLastMessageIfToolResult } from \"./agui-handler.js\";\nimport type { Logger } from \"./logger.js\";\nimport { isAuthenticatedRequest } from \"./api-key-auth.js\";\n\n/**\n * Sentinel `match.message` value written to disk when the request had no\n * extractable user text. Keeps the on-disk fixture serializable (predicate\n * matchers aren't) but won't match any real user input on replay.\n */\nexport const NO_USER_MESSAGE_SENTINEL = \"__NO_USER_MESSAGE__\";\n\n/**\n * Default ceiling (bytes) for the in-memory AG-UI record buffer. The recorder\n * tees every upstream SSE chunk straight to the client AND buffers a copy so it\n * can `Buffer.concat(chunks).toString()` + parse the SSE events into a fixture\n * once the stream ends. With no cap, a large upstream response (amplified since\n * the real-key fixture-miss passthrough landed) builds a string past V8's\n * ~512 MiB max string length and throws `RangeError: Invalid string length`.\n * 64 MiB mirrors the generic proxy path's `DEFAULT_MAX_PROXY_BUFFER_BYTES` and\n * is generous for any real agent turn. Overridable via\n * `AGUIRecordConfig.maxRecordBufferBytes`.\n */\nexport const DEFAULT_AGUI_RECORD_BUFFER_BYTES = 64 * 1024 * 1024; // 64 MiB\n\n/**\n * Absolute hard ceiling (bytes) for the AG-UI record buffer, independent of the\n * configurable `maxRecordBufferBytes`, mirroring the generic proxy path's\n * `PROXY_BUFFER_HARD_CEILING`. 256 MiB of bytes can never decode to more than\n * 256 Mi UTF-16 code units, comfortably below V8's 2^29 - 1 string-length limit,\n * so the eventual `Buffer.concat(chunks).toString()` can never throw.\n */\nexport const AGUI_RECORD_BUFFER_HARD_CEILING = 256 * 1024 * 1024; // 256 MiB\n\n/**\n * Test-only override of the effective AG-UI record-buffer ceiling. Lets the cap\n * suite exercise the over-cap truncation path with a small body instead of\n * streaming hundreds of MB. `undefined` (the default) uses the configured /\n * default cap. NEVER set from production code.\n */\nlet aguiRecordBufferCeilingOverride: number | undefined;\n\n/** @internal test-only — see {@link aguiRecordBufferCeilingOverride}. */\nexport function setAGUIRecordBufferCeilingForTests(value: number | undefined): void {\n aguiRecordBufferCeilingOverride = value;\n}\n\n/**\n * Resolve the effective AG-UI record-buffer byte cap: honor a test-only\n * override, else the configured `maxRecordBufferBytes` (clamped to the hard\n * ceiling), else the default. Non-finite / non-positive configured values fall\n * back to the default.\n */\nfunction resolveAGUIRecordBufferCap(configured: number | undefined): number {\n if (aguiRecordBufferCeilingOverride !== undefined) return aguiRecordBufferCeilingOverride;\n if (configured == null || !Number.isFinite(configured) || configured <= 0) {\n return DEFAULT_AGUI_RECORD_BUFFER_BYTES;\n }\n return Math.min(configured, AGUI_RECORD_BUFFER_HARD_CEILING);\n}\n\n/**\n * Proxy an unmatched AG-UI request to a real upstream agent, record the\n * SSE event stream as a fixture on disk and in memory, and relay the\n * response back to the original client in real time.\n *\n * Returns the HTTP status code written to the client if the request was proxied,\n * or `false` if no upstream is configured.\n */\nexport async function proxyAndRecordAGUI(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n input: AGUIRunAgentInput,\n fixtures: AGUIFixture[],\n config: AGUIRecordConfig,\n logger: Logger,\n): Promise<number | false> {\n // AG-UI has no provider-specific static credential contract. An inbound\n // test key must never become an implicit upstream credential, so fail before\n // opening an upstream connection.\n if (isAuthenticatedRequest(req)) {\n res.writeHead(502, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"No configured provider credential\" }));\n return 502;\n }\n if (!config.upstream) {\n logger.warn(\"No upstream URL configured for AG-UI recording — cannot proxy\");\n return false;\n }\n\n let target: URL;\n try {\n target = new URL(config.upstream);\n } catch (err) {\n const detail = err instanceof Error ? err.message : String(err);\n logger.error(`Invalid upstream AG-UI URL: ${config.upstream} — ${detail}`);\n res.writeHead(502, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Invalid upstream AG-UI URL\" }));\n return 502;\n }\n\n logger.warn(`NO AG-UI FIXTURE MATCH — proxying to ${config.upstream}`);\n\n // Build upstream request headers\n const forwardHeaders: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n Accept: \"text/event-stream\",\n };\n // Forward auth headers if present\n const authorization = req.headers[\"authorization\"];\n if (authorization) {\n forwardHeaders[\"Authorization\"] = Array.isArray(authorization)\n ? authorization.join(\", \")\n : authorization;\n }\n const apiKey = req.headers[\"x-api-key\"];\n if (apiKey) {\n forwardHeaders[\"x-api-key\"] = Array.isArray(apiKey) ? apiKey.join(\", \") : apiKey;\n }\n\n const requestBody = JSON.stringify(input);\n\n let status: number;\n try {\n status = await teeUpstreamStream(\n target,\n forwardHeaders,\n requestBody,\n res,\n input,\n fixtures,\n config,\n logger,\n );\n } catch (err) {\n const msg = err instanceof Error ? err.message : \"Unknown proxy error\";\n logger.error(`AG-UI proxy request failed: ${msg}`);\n if (!res.headersSent) {\n res.writeHead(502, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Upstream AG-UI agent unreachable\" }));\n } else if (!res.writableEnded) {\n res.end();\n }\n status = 502;\n }\n\n return status;\n}\n\n// ---------------------------------------------------------------------------\n// Internal: tee the upstream SSE stream to the client and buffer for recording\n// ---------------------------------------------------------------------------\n\nfunction teeUpstreamStream(\n target: URL,\n headers: Record<string, string>,\n body: string,\n clientRes: http.ServerResponse,\n input: AGUIRunAgentInput,\n fixtures: AGUIFixture[],\n config: AGUIRecordConfig,\n logger: Logger,\n): Promise<number> {\n return new Promise((resolve, reject) => {\n const transport = target.protocol === \"https:\" ? https : http;\n const UPSTREAM_TIMEOUT_MS = 30_000;\n\n const upstreamReq = transport.request(\n target,\n {\n method: \"POST\",\n timeout: UPSTREAM_TIMEOUT_MS,\n headers: {\n ...headers,\n \"Content-Length\": Buffer.byteLength(body).toString(),\n },\n },\n (upstreamRes) => {\n const upstreamStatus = upstreamRes.statusCode ?? 200;\n\n // Normalize status codes: aimock acts as a gateway, so upstream\n // provider details (429, 503, etc.) should not leak.\n // Successes → 200, errors → 502 (Bad Gateway).\n const clientStatus = upstreamStatus >= 200 && upstreamStatus < 300 ? 200 : 502;\n\n // Set appropriate headers on the client response.\n if (!clientRes.headersSent) {\n if (clientStatus === 200) {\n clientRes.writeHead(200, {\n \"Content-Type\": \"text/event-stream\",\n \"Cache-Control\": \"no-cache\",\n Connection: \"keep-alive\",\n });\n } else {\n const ct = upstreamRes.headers[\"content-type\"] || \"application/json\";\n clientRes.writeHead(502, { \"Content-Type\": ct });\n }\n }\n\n const chunks: Buffer[] = [];\n let clientWriteFailed = false;\n // Bound the in-memory record buffer so a single huge upstream response\n // cannot build a string past V8's ~512 MiB limit\n // (RangeError: Invalid string length) when we `Buffer.concat`+stringify\n // it below. The client relay above is INDEPENDENT of this buffer, so\n // capping it never truncates what the client receives — the cap means\n // \"don't journal\", not \"don't answer\" (mirrors the generic proxy path).\n const recordBufferCap = resolveAGUIRecordBufferCap(config.maxRecordBufferBytes);\n let bufferedBytes = 0;\n let recordTruncated = false;\n\n upstreamRes.on(\"data\", (chunk: Buffer) => {\n // Relay to client in real time\n try {\n clientRes.write(chunk);\n } catch (err) {\n if (!clientWriteFailed) {\n clientWriteFailed = true;\n logger?.warn(\n \"Client write failed during proxy relay:\",\n err instanceof Error ? err.message : String(err),\n );\n }\n }\n // Buffer for fixture construction, bounded by the record-buffer cap.\n // Once the cap is crossed we stop accumulating and free the buffer so\n // heap growth is bounded and the end-handler skips concat/stringify.\n if (recordTruncated) return;\n if (bufferedBytes + chunk.length > recordBufferCap) {\n recordTruncated = true;\n chunks.length = 0;\n bufferedBytes = 0;\n logger?.warn(\n `AG-UI upstream response exceeded the ${recordBufferCap}-byte record buffer cap — relaying full body to client, but skipping fixture recording to bound memory`,\n );\n return;\n }\n chunks.push(chunk);\n bufferedBytes += chunk.length;\n });\n\n let settled = false;\n\n upstreamRes.on(\"error\", (err) => {\n if (settled) return;\n settled = true;\n try {\n if (!clientRes.headersSent) {\n clientRes.writeHead(502, { \"Content-Type\": \"application/json\" });\n clientRes.end(JSON.stringify({ error: \"Upstream AG-UI agent unreachable\" }));\n } else if (!clientRes.writableEnded) {\n clientRes.end();\n }\n } catch (writeErr) {\n logger.warn(\n \"Failed to write error response to client:\",\n writeErr instanceof Error ? writeErr.message : String(writeErr),\n );\n }\n reject(err);\n });\n\n upstreamRes.on(\"end\", () => {\n if (settled) return;\n settled = true;\n\n // Don't record fixtures for non-2xx upstream responses\n if (clientStatus !== 200) {\n try {\n if (!clientRes.writableEnded) clientRes.end();\n } catch (writeErr) {\n logger.warn(\n \"Failed to end client response:\",\n writeErr instanceof Error ? writeErr.message : String(writeErr),\n );\n }\n resolve(clientStatus);\n return;\n }\n try {\n if (!clientRes.writableEnded) clientRes.end();\n } catch (writeErr) {\n logger.warn(\n \"Failed to end client response:\",\n writeErr instanceof Error ? writeErr.message : String(writeErr),\n );\n }\n\n // Record buffer cap tripped: the buffered copy is partial (and was\n // freed), so we cannot faithfully build a fixture. The client already\n // received every byte via the live tee above, so just skip recording\n // — never stringify the (now-empty) buffer, never persist a truncated\n // fixture. Mirrors the generic proxy path's over-cap \"skip recording,\n // still answer the client\" behavior.\n if (recordTruncated) {\n logger.warn(\n \"AG-UI record buffer cap exceeded — response relayed to client, recording skipped\",\n );\n resolve(clientStatus);\n return;\n }\n\n // Parse buffered SSE events\n const buffered = Buffer.concat(chunks).toString();\n const events = parseSSEEvents(buffered, logger);\n\n // Build fixture — three-way match priority:\n // 1. Tool-result continuation (HITL): match by toolCallId\n // 2. User message: match by last user message content\n // 3. Fallback predicate: no user message present\n let match: AGUIFixtureMatch;\n const lastToolResult = getLastMessageIfToolResult(input);\n if (lastToolResult?.toolCallId) {\n match = { toolCallId: lastToolResult.toolCallId };\n logger.info(`Recorded AG-UI fixture keyed on toolCallId=${lastToolResult.toolCallId}`);\n } else {\n const message = extractLastUserMessage(input);\n if (message) {\n match = { message };\n } else {\n match = {\n predicate: (inp: AGUIRunAgentInput) =>\n !inp.messages?.length || !inp.messages.some((m) => m.role === \"user\"),\n };\n logger.warn(\n \"Recorded AG-UI fixture has no user message — available in-memory only (predicate fixtures cannot be persisted to disk)\",\n );\n }\n }\n const fixture: AGUIFixture = { match, events };\n\n if (!config.proxyOnly) {\n // Register in memory first (always available even if disk write fails)\n fixtures.push(fixture);\n\n // Predicate fixtures (no user message, no toolCallId) cannot be\n // meaningfully serialized — the sentinel becomes a literal string\n // match that never matches real requests. Keep in-memory only.\n if (fixture.match.predicate) {\n logger.warn(\n \"Skipping disk write for predicate fixture — in-memory only (cannot be persisted)\",\n );\n } else {\n const serializableFixture = {\n match: fixture.match,\n events: fixture.events,\n ...(fixture.delayMs !== undefined ? { delayMs: fixture.delayMs } : {}),\n };\n\n const fixturePath = config.fixturePath ?? \"./fixtures/agui-recorded\";\n const timestamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n const filename = `agui-${timestamp}-${crypto.randomUUID().slice(0, 8)}.json`;\n const filepath = path.join(fixturePath, filename);\n\n try {\n fs.mkdirSync(fixturePath, { recursive: true });\n fs.writeFileSync(\n filepath,\n JSON.stringify({ fixtures: [serializableFixture] }, null, 2),\n \"utf-8\",\n );\n logger.warn(`AG-UI response recorded → ${filepath}`);\n } catch (err) {\n const msg = err instanceof Error ? err.message : \"Unknown filesystem error\";\n logger.error(\n `Failed to save AG-UI fixture to disk: ${msg} (fixture retained in memory)`,\n );\n }\n }\n } else {\n logger.info(\"Proxied AG-UI request (proxy-only mode)\");\n }\n\n resolve(clientStatus);\n });\n },\n );\n\n upstreamReq.on(\"timeout\", () => {\n upstreamReq.destroy(\n new Error(`Upstream AG-UI request timed out after ${UPSTREAM_TIMEOUT_MS / 1000}s`),\n );\n });\n\n upstreamReq.on(\"error\", (err) => {\n try {\n if (!clientRes.headersSent) {\n clientRes.writeHead(502, { \"Content-Type\": \"application/json\" });\n clientRes.end(JSON.stringify({ error: \"Upstream AG-UI agent unreachable\" }));\n } else if (!clientRes.writableEnded) {\n clientRes.end();\n }\n } catch (writeErr) {\n logger.warn(\n \"Failed to write error response to client:\",\n writeErr instanceof Error ? writeErr.message : String(writeErr),\n );\n }\n reject(err);\n });\n\n upstreamReq.write(body);\n upstreamReq.end();\n });\n}\n\n/**\n * Parse SSE data lines from buffered stream text.\n */\nfunction parseSSEEvents(text: string, logger?: Logger): AGUIEvent[] {\n const events: AGUIEvent[] = [];\n const blocks = text.split(\"\\n\\n\");\n for (const block of blocks) {\n const lines = block.split(\"\\n\");\n for (const line of lines) {\n if (line.startsWith(\"data:\")) {\n const payload = line.startsWith(\"data: \") ? line.slice(6) : line.slice(5);\n try {\n const parsed = JSON.parse(payload) as AGUIEvent;\n events.push(parsed);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n const warning = `Skipping unparseable SSE data line (${msg}): ${payload.slice(0, 200)}`;\n if (logger) logger.warn(warning);\n else console.warn(warning);\n }\n }\n }\n }\n return events;\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"agui-recorder.d.cts","names":[],"sources":["../src/agui-recorder.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;;;;;;cAiCa;;;;;;;;cASA;;iBAWG,kCAAA;;;;;;;;;iBA0BM,kBAAA,MACf,MAAA,CAAK,sBACL,MAAA,CAAK,uBACH,6BACG,uBACF,0BACA,SACP"}
1
+ {"version":3,"file":"agui-recorder.d.cts","names":[],"sources":["../src/agui-recorder.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;;;;;;cAkCa;;;;;;;;cASA;;iBAWG,kCAAA;;;;;;;;;iBA0BM,kBAAA,MACf,MAAA,CAAK,sBACL,MAAA,CAAK,uBACH,6BACG,uBACF,0BACA,SACP"}
@@ -1 +1 @@
1
- {"version":3,"file":"agui-recorder.d.ts","names":[],"sources":["../src/agui-recorder.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;;;;;;cAiCa;;;;;;;;cASA;;iBAWG,kCAAA;;;;;;;;;iBA0BM,kBAAA,MACf,MAAA,CAAK,sBACL,MAAA,CAAK,uBACH,6BACG,uBACF,0BACA,SACP"}
1
+ {"version":3,"file":"agui-recorder.d.ts","names":[],"sources":["../src/agui-recorder.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;;;;;;cAkCa;;;;;;;;cASA;;iBAWG,kCAAA;;;;;;;;;iBA0BM,kBAAA,MACf,MAAA,CAAK,sBACL,MAAA,CAAK,uBACH,6BACG,uBACF,0BACA,SACP"}
@@ -1,3 +1,4 @@
1
+ import { isAuthenticatedRequest } from "./api-key-auth.js";
1
2
  import { extractLastUserMessage, getLastMessageIfToolResult } from "./agui-handler.js";
2
3
  import * as http$1 from "node:http";
3
4
  import * as crypto$1 from "node:crypto";
@@ -57,6 +58,11 @@ function resolveAGUIRecordBufferCap(configured) {
57
58
  * or `false` if no upstream is configured.
58
59
  */
59
60
  async function proxyAndRecordAGUI(req, res, input, fixtures, config, logger) {
61
+ if (isAuthenticatedRequest(req)) {
62
+ res.writeHead(502, { "Content-Type": "application/json" });
63
+ res.end(JSON.stringify({ error: "No configured provider credential" }));
64
+ return 502;
65
+ }
60
66
  if (!config.upstream) {
61
67
  logger.warn("No upstream URL configured for AG-UI recording — cannot proxy");
62
68
  return false;
@@ -1 +1 @@
1
- {"version":3,"file":"agui-recorder.js","names":["http","crypto"],"sources":["../src/agui-recorder.ts"],"sourcesContent":["import * as http from \"node:http\";\nimport * as https from \"node:https\";\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport * as crypto from \"node:crypto\";\nimport type {\n AGUIFixture,\n AGUIFixtureMatch,\n AGUIRecordConfig,\n AGUIEvent,\n AGUIRunAgentInput,\n} from \"./agui-types.js\";\nimport { extractLastUserMessage, getLastMessageIfToolResult } from \"./agui-handler.js\";\nimport type { Logger } from \"./logger.js\";\n\n/**\n * Sentinel `match.message` value written to disk when the request had no\n * extractable user text. Keeps the on-disk fixture serializable (predicate\n * matchers aren't) but won't match any real user input on replay.\n */\nexport const NO_USER_MESSAGE_SENTINEL = \"__NO_USER_MESSAGE__\";\n\n/**\n * Default ceiling (bytes) for the in-memory AG-UI record buffer. The recorder\n * tees every upstream SSE chunk straight to the client AND buffers a copy so it\n * can `Buffer.concat(chunks).toString()` + parse the SSE events into a fixture\n * once the stream ends. With no cap, a large upstream response (amplified since\n * the real-key fixture-miss passthrough landed) builds a string past V8's\n * ~512 MiB max string length and throws `RangeError: Invalid string length`.\n * 64 MiB mirrors the generic proxy path's `DEFAULT_MAX_PROXY_BUFFER_BYTES` and\n * is generous for any real agent turn. Overridable via\n * `AGUIRecordConfig.maxRecordBufferBytes`.\n */\nexport const DEFAULT_AGUI_RECORD_BUFFER_BYTES = 64 * 1024 * 1024; // 64 MiB\n\n/**\n * Absolute hard ceiling (bytes) for the AG-UI record buffer, independent of the\n * configurable `maxRecordBufferBytes`, mirroring the generic proxy path's\n * `PROXY_BUFFER_HARD_CEILING`. 256 MiB of bytes can never decode to more than\n * 256 Mi UTF-16 code units, comfortably below V8's 2^29 - 1 string-length limit,\n * so the eventual `Buffer.concat(chunks).toString()` can never throw.\n */\nexport const AGUI_RECORD_BUFFER_HARD_CEILING = 256 * 1024 * 1024; // 256 MiB\n\n/**\n * Test-only override of the effective AG-UI record-buffer ceiling. Lets the cap\n * suite exercise the over-cap truncation path with a small body instead of\n * streaming hundreds of MB. `undefined` (the default) uses the configured /\n * default cap. NEVER set from production code.\n */\nlet aguiRecordBufferCeilingOverride: number | undefined;\n\n/** @internal test-only — see {@link aguiRecordBufferCeilingOverride}. */\nexport function setAGUIRecordBufferCeilingForTests(value: number | undefined): void {\n aguiRecordBufferCeilingOverride = value;\n}\n\n/**\n * Resolve the effective AG-UI record-buffer byte cap: honor a test-only\n * override, else the configured `maxRecordBufferBytes` (clamped to the hard\n * ceiling), else the default. Non-finite / non-positive configured values fall\n * back to the default.\n */\nfunction resolveAGUIRecordBufferCap(configured: number | undefined): number {\n if (aguiRecordBufferCeilingOverride !== undefined) return aguiRecordBufferCeilingOverride;\n if (configured == null || !Number.isFinite(configured) || configured <= 0) {\n return DEFAULT_AGUI_RECORD_BUFFER_BYTES;\n }\n return Math.min(configured, AGUI_RECORD_BUFFER_HARD_CEILING);\n}\n\n/**\n * Proxy an unmatched AG-UI request to a real upstream agent, record the\n * SSE event stream as a fixture on disk and in memory, and relay the\n * response back to the original client in real time.\n *\n * Returns the HTTP status code written to the client if the request was proxied,\n * or `false` if no upstream is configured.\n */\nexport async function proxyAndRecordAGUI(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n input: AGUIRunAgentInput,\n fixtures: AGUIFixture[],\n config: AGUIRecordConfig,\n logger: Logger,\n): Promise<number | false> {\n if (!config.upstream) {\n logger.warn(\"No upstream URL configured for AG-UI recording — cannot proxy\");\n return false;\n }\n\n let target: URL;\n try {\n target = new URL(config.upstream);\n } catch (err) {\n const detail = err instanceof Error ? err.message : String(err);\n logger.error(`Invalid upstream AG-UI URL: ${config.upstream} — ${detail}`);\n res.writeHead(502, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Invalid upstream AG-UI URL\" }));\n return 502;\n }\n\n logger.warn(`NO AG-UI FIXTURE MATCH — proxying to ${config.upstream}`);\n\n // Build upstream request headers\n const forwardHeaders: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n Accept: \"text/event-stream\",\n };\n // Forward auth headers if present\n const authorization = req.headers[\"authorization\"];\n if (authorization) {\n forwardHeaders[\"Authorization\"] = Array.isArray(authorization)\n ? authorization.join(\", \")\n : authorization;\n }\n const apiKey = req.headers[\"x-api-key\"];\n if (apiKey) {\n forwardHeaders[\"x-api-key\"] = Array.isArray(apiKey) ? apiKey.join(\", \") : apiKey;\n }\n\n const requestBody = JSON.stringify(input);\n\n let status: number;\n try {\n status = await teeUpstreamStream(\n target,\n forwardHeaders,\n requestBody,\n res,\n input,\n fixtures,\n config,\n logger,\n );\n } catch (err) {\n const msg = err instanceof Error ? err.message : \"Unknown proxy error\";\n logger.error(`AG-UI proxy request failed: ${msg}`);\n if (!res.headersSent) {\n res.writeHead(502, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Upstream AG-UI agent unreachable\" }));\n } else if (!res.writableEnded) {\n res.end();\n }\n status = 502;\n }\n\n return status;\n}\n\n// ---------------------------------------------------------------------------\n// Internal: tee the upstream SSE stream to the client and buffer for recording\n// ---------------------------------------------------------------------------\n\nfunction teeUpstreamStream(\n target: URL,\n headers: Record<string, string>,\n body: string,\n clientRes: http.ServerResponse,\n input: AGUIRunAgentInput,\n fixtures: AGUIFixture[],\n config: AGUIRecordConfig,\n logger: Logger,\n): Promise<number> {\n return new Promise((resolve, reject) => {\n const transport = target.protocol === \"https:\" ? https : http;\n const UPSTREAM_TIMEOUT_MS = 30_000;\n\n const upstreamReq = transport.request(\n target,\n {\n method: \"POST\",\n timeout: UPSTREAM_TIMEOUT_MS,\n headers: {\n ...headers,\n \"Content-Length\": Buffer.byteLength(body).toString(),\n },\n },\n (upstreamRes) => {\n const upstreamStatus = upstreamRes.statusCode ?? 200;\n\n // Normalize status codes: aimock acts as a gateway, so upstream\n // provider details (429, 503, etc.) should not leak.\n // Successes → 200, errors → 502 (Bad Gateway).\n const clientStatus = upstreamStatus >= 200 && upstreamStatus < 300 ? 200 : 502;\n\n // Set appropriate headers on the client response.\n if (!clientRes.headersSent) {\n if (clientStatus === 200) {\n clientRes.writeHead(200, {\n \"Content-Type\": \"text/event-stream\",\n \"Cache-Control\": \"no-cache\",\n Connection: \"keep-alive\",\n });\n } else {\n const ct = upstreamRes.headers[\"content-type\"] || \"application/json\";\n clientRes.writeHead(502, { \"Content-Type\": ct });\n }\n }\n\n const chunks: Buffer[] = [];\n let clientWriteFailed = false;\n // Bound the in-memory record buffer so a single huge upstream response\n // cannot build a string past V8's ~512 MiB limit\n // (RangeError: Invalid string length) when we `Buffer.concat`+stringify\n // it below. The client relay above is INDEPENDENT of this buffer, so\n // capping it never truncates what the client receives — the cap means\n // \"don't journal\", not \"don't answer\" (mirrors the generic proxy path).\n const recordBufferCap = resolveAGUIRecordBufferCap(config.maxRecordBufferBytes);\n let bufferedBytes = 0;\n let recordTruncated = false;\n\n upstreamRes.on(\"data\", (chunk: Buffer) => {\n // Relay to client in real time\n try {\n clientRes.write(chunk);\n } catch (err) {\n if (!clientWriteFailed) {\n clientWriteFailed = true;\n logger?.warn(\n \"Client write failed during proxy relay:\",\n err instanceof Error ? err.message : String(err),\n );\n }\n }\n // Buffer for fixture construction, bounded by the record-buffer cap.\n // Once the cap is crossed we stop accumulating and free the buffer so\n // heap growth is bounded and the end-handler skips concat/stringify.\n if (recordTruncated) return;\n if (bufferedBytes + chunk.length > recordBufferCap) {\n recordTruncated = true;\n chunks.length = 0;\n bufferedBytes = 0;\n logger?.warn(\n `AG-UI upstream response exceeded the ${recordBufferCap}-byte record buffer cap — relaying full body to client, but skipping fixture recording to bound memory`,\n );\n return;\n }\n chunks.push(chunk);\n bufferedBytes += chunk.length;\n });\n\n let settled = false;\n\n upstreamRes.on(\"error\", (err) => {\n if (settled) return;\n settled = true;\n try {\n if (!clientRes.headersSent) {\n clientRes.writeHead(502, { \"Content-Type\": \"application/json\" });\n clientRes.end(JSON.stringify({ error: \"Upstream AG-UI agent unreachable\" }));\n } else if (!clientRes.writableEnded) {\n clientRes.end();\n }\n } catch (writeErr) {\n logger.warn(\n \"Failed to write error response to client:\",\n writeErr instanceof Error ? writeErr.message : String(writeErr),\n );\n }\n reject(err);\n });\n\n upstreamRes.on(\"end\", () => {\n if (settled) return;\n settled = true;\n\n // Don't record fixtures for non-2xx upstream responses\n if (clientStatus !== 200) {\n try {\n if (!clientRes.writableEnded) clientRes.end();\n } catch (writeErr) {\n logger.warn(\n \"Failed to end client response:\",\n writeErr instanceof Error ? writeErr.message : String(writeErr),\n );\n }\n resolve(clientStatus);\n return;\n }\n try {\n if (!clientRes.writableEnded) clientRes.end();\n } catch (writeErr) {\n logger.warn(\n \"Failed to end client response:\",\n writeErr instanceof Error ? writeErr.message : String(writeErr),\n );\n }\n\n // Record buffer cap tripped: the buffered copy is partial (and was\n // freed), so we cannot faithfully build a fixture. The client already\n // received every byte via the live tee above, so just skip recording\n // — never stringify the (now-empty) buffer, never persist a truncated\n // fixture. Mirrors the generic proxy path's over-cap \"skip recording,\n // still answer the client\" behavior.\n if (recordTruncated) {\n logger.warn(\n \"AG-UI record buffer cap exceeded — response relayed to client, recording skipped\",\n );\n resolve(clientStatus);\n return;\n }\n\n // Parse buffered SSE events\n const buffered = Buffer.concat(chunks).toString();\n const events = parseSSEEvents(buffered, logger);\n\n // Build fixture — three-way match priority:\n // 1. Tool-result continuation (HITL): match by toolCallId\n // 2. User message: match by last user message content\n // 3. Fallback predicate: no user message present\n let match: AGUIFixtureMatch;\n const lastToolResult = getLastMessageIfToolResult(input);\n if (lastToolResult?.toolCallId) {\n match = { toolCallId: lastToolResult.toolCallId };\n logger.info(`Recorded AG-UI fixture keyed on toolCallId=${lastToolResult.toolCallId}`);\n } else {\n const message = extractLastUserMessage(input);\n if (message) {\n match = { message };\n } else {\n match = {\n predicate: (inp: AGUIRunAgentInput) =>\n !inp.messages?.length || !inp.messages.some((m) => m.role === \"user\"),\n };\n logger.warn(\n \"Recorded AG-UI fixture has no user message — available in-memory only (predicate fixtures cannot be persisted to disk)\",\n );\n }\n }\n const fixture: AGUIFixture = { match, events };\n\n if (!config.proxyOnly) {\n // Register in memory first (always available even if disk write fails)\n fixtures.push(fixture);\n\n // Predicate fixtures (no user message, no toolCallId) cannot be\n // meaningfully serialized — the sentinel becomes a literal string\n // match that never matches real requests. Keep in-memory only.\n if (fixture.match.predicate) {\n logger.warn(\n \"Skipping disk write for predicate fixture — in-memory only (cannot be persisted)\",\n );\n } else {\n const serializableFixture = {\n match: fixture.match,\n events: fixture.events,\n ...(fixture.delayMs !== undefined ? { delayMs: fixture.delayMs } : {}),\n };\n\n const fixturePath = config.fixturePath ?? \"./fixtures/agui-recorded\";\n const timestamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n const filename = `agui-${timestamp}-${crypto.randomUUID().slice(0, 8)}.json`;\n const filepath = path.join(fixturePath, filename);\n\n try {\n fs.mkdirSync(fixturePath, { recursive: true });\n fs.writeFileSync(\n filepath,\n JSON.stringify({ fixtures: [serializableFixture] }, null, 2),\n \"utf-8\",\n );\n logger.warn(`AG-UI response recorded → ${filepath}`);\n } catch (err) {\n const msg = err instanceof Error ? err.message : \"Unknown filesystem error\";\n logger.error(\n `Failed to save AG-UI fixture to disk: ${msg} (fixture retained in memory)`,\n );\n }\n }\n } else {\n logger.info(\"Proxied AG-UI request (proxy-only mode)\");\n }\n\n resolve(clientStatus);\n });\n },\n );\n\n upstreamReq.on(\"timeout\", () => {\n upstreamReq.destroy(\n new Error(`Upstream AG-UI request timed out after ${UPSTREAM_TIMEOUT_MS / 1000}s`),\n );\n });\n\n upstreamReq.on(\"error\", (err) => {\n try {\n if (!clientRes.headersSent) {\n clientRes.writeHead(502, { \"Content-Type\": \"application/json\" });\n clientRes.end(JSON.stringify({ error: \"Upstream AG-UI agent unreachable\" }));\n } else if (!clientRes.writableEnded) {\n clientRes.end();\n }\n } catch (writeErr) {\n logger.warn(\n \"Failed to write error response to client:\",\n writeErr instanceof Error ? writeErr.message : String(writeErr),\n );\n }\n reject(err);\n });\n\n upstreamReq.write(body);\n upstreamReq.end();\n });\n}\n\n/**\n * Parse SSE data lines from buffered stream text.\n */\nfunction parseSSEEvents(text: string, logger?: Logger): AGUIEvent[] {\n const events: AGUIEvent[] = [];\n const blocks = text.split(\"\\n\\n\");\n for (const block of blocks) {\n const lines = block.split(\"\\n\");\n for (const line of lines) {\n if (line.startsWith(\"data:\")) {\n const payload = line.startsWith(\"data: \") ? line.slice(6) : line.slice(5);\n try {\n const parsed = JSON.parse(payload) as AGUIEvent;\n events.push(parsed);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n const warning = `Skipping unparseable SSE data line (${msg}): ${payload.slice(0, 200)}`;\n if (logger) logger.warn(warning);\n else console.warn(warning);\n }\n }\n }\n }\n return events;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"agui-recorder.js","names":["http","crypto"],"sources":["../src/agui-recorder.ts"],"sourcesContent":["import * as http from \"node:http\";\nimport * as https from \"node:https\";\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport * as crypto from \"node:crypto\";\nimport type {\n AGUIFixture,\n AGUIFixtureMatch,\n AGUIRecordConfig,\n AGUIEvent,\n AGUIRunAgentInput,\n} from \"./agui-types.js\";\nimport { extractLastUserMessage, getLastMessageIfToolResult } from \"./agui-handler.js\";\nimport type { Logger } from \"./logger.js\";\nimport { isAuthenticatedRequest } from \"./api-key-auth.js\";\n\n/**\n * Sentinel `match.message` value written to disk when the request had no\n * extractable user text. Keeps the on-disk fixture serializable (predicate\n * matchers aren't) but won't match any real user input on replay.\n */\nexport const NO_USER_MESSAGE_SENTINEL = \"__NO_USER_MESSAGE__\";\n\n/**\n * Default ceiling (bytes) for the in-memory AG-UI record buffer. The recorder\n * tees every upstream SSE chunk straight to the client AND buffers a copy so it\n * can `Buffer.concat(chunks).toString()` + parse the SSE events into a fixture\n * once the stream ends. With no cap, a large upstream response (amplified since\n * the real-key fixture-miss passthrough landed) builds a string past V8's\n * ~512 MiB max string length and throws `RangeError: Invalid string length`.\n * 64 MiB mirrors the generic proxy path's `DEFAULT_MAX_PROXY_BUFFER_BYTES` and\n * is generous for any real agent turn. Overridable via\n * `AGUIRecordConfig.maxRecordBufferBytes`.\n */\nexport const DEFAULT_AGUI_RECORD_BUFFER_BYTES = 64 * 1024 * 1024; // 64 MiB\n\n/**\n * Absolute hard ceiling (bytes) for the AG-UI record buffer, independent of the\n * configurable `maxRecordBufferBytes`, mirroring the generic proxy path's\n * `PROXY_BUFFER_HARD_CEILING`. 256 MiB of bytes can never decode to more than\n * 256 Mi UTF-16 code units, comfortably below V8's 2^29 - 1 string-length limit,\n * so the eventual `Buffer.concat(chunks).toString()` can never throw.\n */\nexport const AGUI_RECORD_BUFFER_HARD_CEILING = 256 * 1024 * 1024; // 256 MiB\n\n/**\n * Test-only override of the effective AG-UI record-buffer ceiling. Lets the cap\n * suite exercise the over-cap truncation path with a small body instead of\n * streaming hundreds of MB. `undefined` (the default) uses the configured /\n * default cap. NEVER set from production code.\n */\nlet aguiRecordBufferCeilingOverride: number | undefined;\n\n/** @internal test-only — see {@link aguiRecordBufferCeilingOverride}. */\nexport function setAGUIRecordBufferCeilingForTests(value: number | undefined): void {\n aguiRecordBufferCeilingOverride = value;\n}\n\n/**\n * Resolve the effective AG-UI record-buffer byte cap: honor a test-only\n * override, else the configured `maxRecordBufferBytes` (clamped to the hard\n * ceiling), else the default. Non-finite / non-positive configured values fall\n * back to the default.\n */\nfunction resolveAGUIRecordBufferCap(configured: number | undefined): number {\n if (aguiRecordBufferCeilingOverride !== undefined) return aguiRecordBufferCeilingOverride;\n if (configured == null || !Number.isFinite(configured) || configured <= 0) {\n return DEFAULT_AGUI_RECORD_BUFFER_BYTES;\n }\n return Math.min(configured, AGUI_RECORD_BUFFER_HARD_CEILING);\n}\n\n/**\n * Proxy an unmatched AG-UI request to a real upstream agent, record the\n * SSE event stream as a fixture on disk and in memory, and relay the\n * response back to the original client in real time.\n *\n * Returns the HTTP status code written to the client if the request was proxied,\n * or `false` if no upstream is configured.\n */\nexport async function proxyAndRecordAGUI(\n req: http.IncomingMessage,\n res: http.ServerResponse,\n input: AGUIRunAgentInput,\n fixtures: AGUIFixture[],\n config: AGUIRecordConfig,\n logger: Logger,\n): Promise<number | false> {\n // AG-UI has no provider-specific static credential contract. An inbound\n // test key must never become an implicit upstream credential, so fail before\n // opening an upstream connection.\n if (isAuthenticatedRequest(req)) {\n res.writeHead(502, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"No configured provider credential\" }));\n return 502;\n }\n if (!config.upstream) {\n logger.warn(\"No upstream URL configured for AG-UI recording — cannot proxy\");\n return false;\n }\n\n let target: URL;\n try {\n target = new URL(config.upstream);\n } catch (err) {\n const detail = err instanceof Error ? err.message : String(err);\n logger.error(`Invalid upstream AG-UI URL: ${config.upstream} — ${detail}`);\n res.writeHead(502, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Invalid upstream AG-UI URL\" }));\n return 502;\n }\n\n logger.warn(`NO AG-UI FIXTURE MATCH — proxying to ${config.upstream}`);\n\n // Build upstream request headers\n const forwardHeaders: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n Accept: \"text/event-stream\",\n };\n // Forward auth headers if present\n const authorization = req.headers[\"authorization\"];\n if (authorization) {\n forwardHeaders[\"Authorization\"] = Array.isArray(authorization)\n ? authorization.join(\", \")\n : authorization;\n }\n const apiKey = req.headers[\"x-api-key\"];\n if (apiKey) {\n forwardHeaders[\"x-api-key\"] = Array.isArray(apiKey) ? apiKey.join(\", \") : apiKey;\n }\n\n const requestBody = JSON.stringify(input);\n\n let status: number;\n try {\n status = await teeUpstreamStream(\n target,\n forwardHeaders,\n requestBody,\n res,\n input,\n fixtures,\n config,\n logger,\n );\n } catch (err) {\n const msg = err instanceof Error ? err.message : \"Unknown proxy error\";\n logger.error(`AG-UI proxy request failed: ${msg}`);\n if (!res.headersSent) {\n res.writeHead(502, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ error: \"Upstream AG-UI agent unreachable\" }));\n } else if (!res.writableEnded) {\n res.end();\n }\n status = 502;\n }\n\n return status;\n}\n\n// ---------------------------------------------------------------------------\n// Internal: tee the upstream SSE stream to the client and buffer for recording\n// ---------------------------------------------------------------------------\n\nfunction teeUpstreamStream(\n target: URL,\n headers: Record<string, string>,\n body: string,\n clientRes: http.ServerResponse,\n input: AGUIRunAgentInput,\n fixtures: AGUIFixture[],\n config: AGUIRecordConfig,\n logger: Logger,\n): Promise<number> {\n return new Promise((resolve, reject) => {\n const transport = target.protocol === \"https:\" ? https : http;\n const UPSTREAM_TIMEOUT_MS = 30_000;\n\n const upstreamReq = transport.request(\n target,\n {\n method: \"POST\",\n timeout: UPSTREAM_TIMEOUT_MS,\n headers: {\n ...headers,\n \"Content-Length\": Buffer.byteLength(body).toString(),\n },\n },\n (upstreamRes) => {\n const upstreamStatus = upstreamRes.statusCode ?? 200;\n\n // Normalize status codes: aimock acts as a gateway, so upstream\n // provider details (429, 503, etc.) should not leak.\n // Successes → 200, errors → 502 (Bad Gateway).\n const clientStatus = upstreamStatus >= 200 && upstreamStatus < 300 ? 200 : 502;\n\n // Set appropriate headers on the client response.\n if (!clientRes.headersSent) {\n if (clientStatus === 200) {\n clientRes.writeHead(200, {\n \"Content-Type\": \"text/event-stream\",\n \"Cache-Control\": \"no-cache\",\n Connection: \"keep-alive\",\n });\n } else {\n const ct = upstreamRes.headers[\"content-type\"] || \"application/json\";\n clientRes.writeHead(502, { \"Content-Type\": ct });\n }\n }\n\n const chunks: Buffer[] = [];\n let clientWriteFailed = false;\n // Bound the in-memory record buffer so a single huge upstream response\n // cannot build a string past V8's ~512 MiB limit\n // (RangeError: Invalid string length) when we `Buffer.concat`+stringify\n // it below. The client relay above is INDEPENDENT of this buffer, so\n // capping it never truncates what the client receives — the cap means\n // \"don't journal\", not \"don't answer\" (mirrors the generic proxy path).\n const recordBufferCap = resolveAGUIRecordBufferCap(config.maxRecordBufferBytes);\n let bufferedBytes = 0;\n let recordTruncated = false;\n\n upstreamRes.on(\"data\", (chunk: Buffer) => {\n // Relay to client in real time\n try {\n clientRes.write(chunk);\n } catch (err) {\n if (!clientWriteFailed) {\n clientWriteFailed = true;\n logger?.warn(\n \"Client write failed during proxy relay:\",\n err instanceof Error ? err.message : String(err),\n );\n }\n }\n // Buffer for fixture construction, bounded by the record-buffer cap.\n // Once the cap is crossed we stop accumulating and free the buffer so\n // heap growth is bounded and the end-handler skips concat/stringify.\n if (recordTruncated) return;\n if (bufferedBytes + chunk.length > recordBufferCap) {\n recordTruncated = true;\n chunks.length = 0;\n bufferedBytes = 0;\n logger?.warn(\n `AG-UI upstream response exceeded the ${recordBufferCap}-byte record buffer cap — relaying full body to client, but skipping fixture recording to bound memory`,\n );\n return;\n }\n chunks.push(chunk);\n bufferedBytes += chunk.length;\n });\n\n let settled = false;\n\n upstreamRes.on(\"error\", (err) => {\n if (settled) return;\n settled = true;\n try {\n if (!clientRes.headersSent) {\n clientRes.writeHead(502, { \"Content-Type\": \"application/json\" });\n clientRes.end(JSON.stringify({ error: \"Upstream AG-UI agent unreachable\" }));\n } else if (!clientRes.writableEnded) {\n clientRes.end();\n }\n } catch (writeErr) {\n logger.warn(\n \"Failed to write error response to client:\",\n writeErr instanceof Error ? writeErr.message : String(writeErr),\n );\n }\n reject(err);\n });\n\n upstreamRes.on(\"end\", () => {\n if (settled) return;\n settled = true;\n\n // Don't record fixtures for non-2xx upstream responses\n if (clientStatus !== 200) {\n try {\n if (!clientRes.writableEnded) clientRes.end();\n } catch (writeErr) {\n logger.warn(\n \"Failed to end client response:\",\n writeErr instanceof Error ? writeErr.message : String(writeErr),\n );\n }\n resolve(clientStatus);\n return;\n }\n try {\n if (!clientRes.writableEnded) clientRes.end();\n } catch (writeErr) {\n logger.warn(\n \"Failed to end client response:\",\n writeErr instanceof Error ? writeErr.message : String(writeErr),\n );\n }\n\n // Record buffer cap tripped: the buffered copy is partial (and was\n // freed), so we cannot faithfully build a fixture. The client already\n // received every byte via the live tee above, so just skip recording\n // — never stringify the (now-empty) buffer, never persist a truncated\n // fixture. Mirrors the generic proxy path's over-cap \"skip recording,\n // still answer the client\" behavior.\n if (recordTruncated) {\n logger.warn(\n \"AG-UI record buffer cap exceeded — response relayed to client, recording skipped\",\n );\n resolve(clientStatus);\n return;\n }\n\n // Parse buffered SSE events\n const buffered = Buffer.concat(chunks).toString();\n const events = parseSSEEvents(buffered, logger);\n\n // Build fixture — three-way match priority:\n // 1. Tool-result continuation (HITL): match by toolCallId\n // 2. User message: match by last user message content\n // 3. Fallback predicate: no user message present\n let match: AGUIFixtureMatch;\n const lastToolResult = getLastMessageIfToolResult(input);\n if (lastToolResult?.toolCallId) {\n match = { toolCallId: lastToolResult.toolCallId };\n logger.info(`Recorded AG-UI fixture keyed on toolCallId=${lastToolResult.toolCallId}`);\n } else {\n const message = extractLastUserMessage(input);\n if (message) {\n match = { message };\n } else {\n match = {\n predicate: (inp: AGUIRunAgentInput) =>\n !inp.messages?.length || !inp.messages.some((m) => m.role === \"user\"),\n };\n logger.warn(\n \"Recorded AG-UI fixture has no user message — available in-memory only (predicate fixtures cannot be persisted to disk)\",\n );\n }\n }\n const fixture: AGUIFixture = { match, events };\n\n if (!config.proxyOnly) {\n // Register in memory first (always available even if disk write fails)\n fixtures.push(fixture);\n\n // Predicate fixtures (no user message, no toolCallId) cannot be\n // meaningfully serialized — the sentinel becomes a literal string\n // match that never matches real requests. Keep in-memory only.\n if (fixture.match.predicate) {\n logger.warn(\n \"Skipping disk write for predicate fixture — in-memory only (cannot be persisted)\",\n );\n } else {\n const serializableFixture = {\n match: fixture.match,\n events: fixture.events,\n ...(fixture.delayMs !== undefined ? { delayMs: fixture.delayMs } : {}),\n };\n\n const fixturePath = config.fixturePath ?? \"./fixtures/agui-recorded\";\n const timestamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n const filename = `agui-${timestamp}-${crypto.randomUUID().slice(0, 8)}.json`;\n const filepath = path.join(fixturePath, filename);\n\n try {\n fs.mkdirSync(fixturePath, { recursive: true });\n fs.writeFileSync(\n filepath,\n JSON.stringify({ fixtures: [serializableFixture] }, null, 2),\n \"utf-8\",\n );\n logger.warn(`AG-UI response recorded → ${filepath}`);\n } catch (err) {\n const msg = err instanceof Error ? err.message : \"Unknown filesystem error\";\n logger.error(\n `Failed to save AG-UI fixture to disk: ${msg} (fixture retained in memory)`,\n );\n }\n }\n } else {\n logger.info(\"Proxied AG-UI request (proxy-only mode)\");\n }\n\n resolve(clientStatus);\n });\n },\n );\n\n upstreamReq.on(\"timeout\", () => {\n upstreamReq.destroy(\n new Error(`Upstream AG-UI request timed out after ${UPSTREAM_TIMEOUT_MS / 1000}s`),\n );\n });\n\n upstreamReq.on(\"error\", (err) => {\n try {\n if (!clientRes.headersSent) {\n clientRes.writeHead(502, { \"Content-Type\": \"application/json\" });\n clientRes.end(JSON.stringify({ error: \"Upstream AG-UI agent unreachable\" }));\n } else if (!clientRes.writableEnded) {\n clientRes.end();\n }\n } catch (writeErr) {\n logger.warn(\n \"Failed to write error response to client:\",\n writeErr instanceof Error ? writeErr.message : String(writeErr),\n );\n }\n reject(err);\n });\n\n upstreamReq.write(body);\n upstreamReq.end();\n });\n}\n\n/**\n * Parse SSE data lines from buffered stream text.\n */\nfunction parseSSEEvents(text: string, logger?: Logger): AGUIEvent[] {\n const events: AGUIEvent[] = [];\n const blocks = text.split(\"\\n\\n\");\n for (const block of blocks) {\n const lines = block.split(\"\\n\");\n for (const line of lines) {\n if (line.startsWith(\"data:\")) {\n const payload = line.startsWith(\"data: \") ? line.slice(6) : line.slice(5);\n try {\n const parsed = JSON.parse(payload) as AGUIEvent;\n events.push(parsed);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n const warning = `Skipping unparseable SSE data line (${msg}): ${payload.slice(0, 200)}`;\n if (logger) logger.warn(warning);\n else console.warn(warning);\n }\n }\n }\n }\n return events;\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"agui-types.d.ts","names":[],"sources":["../src/agui-types.ts"],"sourcesContent":[],"mappings":"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"}
1
+ {"version":3,"file":"agui-types.d.ts","names":[],"sources":["../src/agui-types.ts"],"sourcesContent":[],"mappings":"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"}
@@ -0,0 +1,166 @@
1
+ const require_runtime = require('./_virtual/_rolldown/runtime.cjs');
2
+ let node_crypto = require("node:crypto");
3
+
4
+ //#region src/api-key-auth.ts
5
+ const INBOUND_API_KEY_HEADERS = [
6
+ "authorization",
7
+ "x-api-key",
8
+ "x-goog-api-key",
9
+ "api-key",
10
+ "xi-api-key"
11
+ ];
12
+ const RECOGNIZED_HEADERS = new Set(INBOUND_API_KEY_HEADERS);
13
+ const API_KEY_ERROR_BODY = JSON.stringify({ error: {
14
+ message: "Invalid API key",
15
+ type: "authentication_error",
16
+ code: "invalid_api_key"
17
+ } });
18
+ const DISABLED_POLICY = {
19
+ enabled: false,
20
+ keys: []
21
+ };
22
+ const authenticatedRequests = /* @__PURE__ */ new WeakSet();
23
+ const OWS_EDGES = /^[ \t]+|[ \t]+$/g;
24
+ function fail(label, suffix) {
25
+ throw new Error(`${label}${suffix}`);
26
+ }
27
+ function hasControl(value) {
28
+ for (let index = 0; index < value.length; index += 1) {
29
+ const code = value.charCodeAt(index);
30
+ if (code <= 31 || code === 127) return true;
31
+ }
32
+ return false;
33
+ }
34
+ function normalizeKey(value, label) {
35
+ if (typeof value !== "string") fail(label, " must be a string");
36
+ const normalized = value.replace(OWS_EDGES, "");
37
+ if (normalized.length === 0) fail(label, " must not be empty");
38
+ if (hasControl(normalized)) fail(label, " must not contain control characters");
39
+ return normalized;
40
+ }
41
+ /** Parse a public auth source once and construct its private server policy. */
42
+ function resolveInboundAuth(source) {
43
+ if (source.value === void 0) return { policy: DISABLED_POLICY };
44
+ if (source.value === null || typeof source.value !== "object" || Array.isArray(source.value)) fail(source.label, " must be an object");
45
+ const apiKeys = source.value.apiKeys;
46
+ if (!Array.isArray(apiKeys)) fail(`${source.label}.apiKeys`, " must be an array");
47
+ if (apiKeys.length === 0) fail(`${source.label}.apiKeys`, " must not be empty");
48
+ const keys = apiKeys.map((value, index) => normalizeKey(value, `${source.label}.apiKeys[${index}]`));
49
+ const seen = /* @__PURE__ */ new Set();
50
+ for (const key of keys) {
51
+ if (seen.has(key)) fail(source.label, " must not contain duplicate API keys");
52
+ seen.add(key);
53
+ }
54
+ return {
55
+ publicConfig: { apiKeys: keys },
56
+ policy: {
57
+ enabled: true,
58
+ keys: keys.map((key) => Buffer.from(key, "utf8"))
59
+ }
60
+ };
61
+ }
62
+ /** Select configuration without parsing it, so an environment override bypasses malformed JSON auth. */
63
+ function selectInboundAuthSource(configAuth, env = process.env) {
64
+ if (Object.prototype.hasOwnProperty.call(env, "AIMOCK_API_KEYS")) return {
65
+ value: { apiKeys: (env.AIMOCK_API_KEYS ?? "").split(",") },
66
+ label: "AIMOCK_API_KEYS"
67
+ };
68
+ return {
69
+ value: configAuth,
70
+ label: "aimock.json.auth"
71
+ };
72
+ }
73
+ function isRecognizedApiKeyHeader(name) {
74
+ return RECOGNIZED_HEADERS.has(name.toLowerCase());
75
+ }
76
+ function rawCandidates(req) {
77
+ const candidates = [];
78
+ for (let index = 0; index < req.rawHeaders.length; index += 2) {
79
+ const name = req.rawHeaders[index];
80
+ const value = req.rawHeaders[index + 1] ?? "";
81
+ const lower = name.toLowerCase();
82
+ if (!isRecognizedApiKeyHeader(lower)) continue;
83
+ if (hasControl(value)) return { reason: "invalid" };
84
+ if (lower === "authorization") {
85
+ const match = /^(?:Bearer|Key)[ \t]+(.+)$/i.exec(value.replace(OWS_EDGES, ""));
86
+ if (!match) return { reason: "invalid" };
87
+ const candidate = match[1].replace(OWS_EDGES, "");
88
+ if (candidate.length === 0 || hasControl(candidate)) return { reason: "invalid" };
89
+ candidates.push(candidate);
90
+ } else {
91
+ const candidate = value.replace(OWS_EDGES, "");
92
+ if (candidate.length === 0) return { reason: "invalid" };
93
+ candidates.push(candidate);
94
+ }
95
+ }
96
+ return candidates.length > 0 ? { candidates } : { reason: "missing" };
97
+ }
98
+ function matchingIndex(candidate, keys) {
99
+ const encoded = Buffer.from(candidate, "utf8");
100
+ for (let index = 0; index < keys.length; index += 1) {
101
+ const key = keys[index];
102
+ if (encoded.length === key.length && (0, node_crypto.timingSafeEqual)(encoded, key)) return index;
103
+ }
104
+ }
105
+ function validateRequestApiKey(req, policy) {
106
+ if (!policy.enabled) return { ok: true };
107
+ const extracted = rawCandidates(req);
108
+ if (!extracted.candidates) return {
109
+ ok: false,
110
+ reason: extracted.reason
111
+ };
112
+ let resolved;
113
+ for (const candidate of extracted.candidates) {
114
+ const index = matchingIndex(candidate, policy.keys);
115
+ if (index === void 0) return {
116
+ ok: false,
117
+ reason: "invalid"
118
+ };
119
+ if (resolved !== void 0 && resolved !== index) return {
120
+ ok: false,
121
+ reason: "conflict"
122
+ };
123
+ resolved = index;
124
+ }
125
+ return { ok: true };
126
+ }
127
+ /** @internal Opaque egress marker; it deliberately carries no key material. */
128
+ function markAuthenticatedRequest(req, policy) {
129
+ if (policy.enabled) authenticatedRequests.add(req);
130
+ }
131
+ /** @internal Used by egress paths to select safe credential handling. */
132
+ function isAuthenticatedRequest(req) {
133
+ return authenticatedRequests.has(req);
134
+ }
135
+ function writeApiKeyHttpRejection(res, setCorsHeaders) {
136
+ setCorsHeaders(res);
137
+ res.writeHead(401, {
138
+ "Content-Type": "application/json",
139
+ "Content-Length": Buffer.byteLength(API_KEY_ERROR_BODY),
140
+ "WWW-Authenticate": "Bearer realm=\"aimock\""
141
+ });
142
+ res.end(API_KEY_ERROR_BODY);
143
+ }
144
+ function writeApiKeyUpgradeRejection(socket) {
145
+ socket.write([
146
+ "HTTP/1.1 401 Unauthorized",
147
+ "Content-Type: application/json",
148
+ `Content-Length: ${Buffer.byteLength(API_KEY_ERROR_BODY)}`,
149
+ "WWW-Authenticate: Bearer realm=\"aimock\"",
150
+ "Connection: close",
151
+ "",
152
+ API_KEY_ERROR_BODY
153
+ ].join("\r\n"));
154
+ socket.destroy();
155
+ }
156
+
157
+ //#endregion
158
+ exports.isAuthenticatedRequest = isAuthenticatedRequest;
159
+ exports.isRecognizedApiKeyHeader = isRecognizedApiKeyHeader;
160
+ exports.markAuthenticatedRequest = markAuthenticatedRequest;
161
+ exports.resolveInboundAuth = resolveInboundAuth;
162
+ exports.selectInboundAuthSource = selectInboundAuthSource;
163
+ exports.validateRequestApiKey = validateRequestApiKey;
164
+ exports.writeApiKeyHttpRejection = writeApiKeyHttpRejection;
165
+ exports.writeApiKeyUpgradeRejection = writeApiKeyUpgradeRejection;
166
+ //# sourceMappingURL=api-key-auth.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api-key-auth.cjs","names":[],"sources":["../src/api-key-auth.ts"],"sourcesContent":["import { timingSafeEqual } from \"node:crypto\";\nimport type * as http from \"node:http\";\nimport type * as net from \"node:net\";\nimport type { ApiKeyAuthConfig } from \"./types.js\";\n\nexport const INBOUND_API_KEY_HEADERS = [\n \"authorization\",\n \"x-api-key\",\n \"x-goog-api-key\",\n \"api-key\",\n \"xi-api-key\",\n] as const;\n\nconst RECOGNIZED_HEADERS = new Set<string>(INBOUND_API_KEY_HEADERS);\n\nexport const API_KEY_ERROR_BODY = JSON.stringify({\n error: {\n message: \"Invalid API key\",\n type: \"authentication_error\",\n code: \"invalid_api_key\",\n },\n});\n\n/** @internal */\nexport interface ApiKeyPolicy {\n readonly enabled: boolean;\n readonly keys: readonly Buffer[];\n}\n\n/** @internal */\nexport interface ResolvedInboundAuth {\n readonly publicConfig?: ApiKeyAuthConfig;\n readonly policy: ApiKeyPolicy;\n}\n\nexport interface AuthResult {\n readonly ok: boolean;\n readonly reason?: \"missing\" | \"invalid\" | \"conflict\";\n}\n\nconst DISABLED_POLICY: ApiKeyPolicy = { enabled: false, keys: [] };\nconst authenticatedRequests = new WeakSet<http.IncomingMessage>();\nconst OWS_EDGES = /^[ \\t]+|[ \\t]+$/g;\n\nfunction fail(label: string, suffix: string): never {\n throw new Error(`${label}${suffix}`);\n}\n\nfunction hasControl(value: string): boolean {\n for (let index = 0; index < value.length; index += 1) {\n const code = value.charCodeAt(index);\n if (code <= 0x1f || code === 0x7f) return true;\n }\n return false;\n}\n\nfunction normalizeKey(value: unknown, label: string): string {\n if (typeof value !== \"string\") fail(label, \" must be a string\");\n const normalized = value.replace(OWS_EDGES, \"\");\n if (normalized.length === 0) fail(label, \" must not be empty\");\n if (hasControl(normalized)) fail(label, \" must not contain control characters\");\n return normalized;\n}\n\n/** Parse a public auth source once and construct its private server policy. */\nexport function resolveInboundAuth(source: { value: unknown; label: string }): ResolvedInboundAuth {\n if (source.value === undefined) return { policy: DISABLED_POLICY };\n if (source.value === null || typeof source.value !== \"object\" || Array.isArray(source.value)) {\n fail(source.label, \" must be an object\");\n }\n const apiKeys = (source.value as { apiKeys?: unknown }).apiKeys;\n if (!Array.isArray(apiKeys)) fail(`${source.label}.apiKeys`, \" must be an array\");\n if (apiKeys.length === 0) fail(`${source.label}.apiKeys`, \" must not be empty\");\n const keys = apiKeys.map((value, index) =>\n normalizeKey(value, `${source.label}.apiKeys[${index}]`),\n );\n const seen = new Set<string>();\n for (const key of keys) {\n if (seen.has(key)) fail(source.label, \" must not contain duplicate API keys\");\n seen.add(key);\n }\n return {\n publicConfig: { apiKeys: keys },\n policy: { enabled: true, keys: keys.map((key) => Buffer.from(key, \"utf8\")) },\n };\n}\n\n/** Select configuration without parsing it, so an environment override bypasses malformed JSON auth. */\nexport function selectInboundAuthSource(\n configAuth: unknown,\n env: NodeJS.ProcessEnv = process.env,\n): { value: unknown; label: string } {\n if (Object.prototype.hasOwnProperty.call(env, \"AIMOCK_API_KEYS\")) {\n return { value: { apiKeys: (env.AIMOCK_API_KEYS ?? \"\").split(\",\") }, label: \"AIMOCK_API_KEYS\" };\n }\n return { value: configAuth, label: \"aimock.json.auth\" };\n}\n\nexport function isRecognizedApiKeyHeader(name: string): boolean {\n return RECOGNIZED_HEADERS.has(name.toLowerCase());\n}\n\nfunction rawCandidates(req: http.IncomingMessage): {\n candidates?: string[];\n reason?: \"missing\" | \"invalid\";\n} {\n const candidates: string[] = [];\n for (let index = 0; index < req.rawHeaders.length; index += 2) {\n const name = req.rawHeaders[index];\n const value = req.rawHeaders[index + 1] ?? \"\";\n const lower = name.toLowerCase();\n if (!isRecognizedApiKeyHeader(lower)) continue;\n if (hasControl(value)) return { reason: \"invalid\" };\n if (lower === \"authorization\") {\n const match = /^(?:Bearer|Key)[ \\t]+(.+)$/i.exec(value.replace(OWS_EDGES, \"\"));\n if (!match) return { reason: \"invalid\" };\n const candidate = match[1].replace(OWS_EDGES, \"\");\n if (candidate.length === 0 || hasControl(candidate)) return { reason: \"invalid\" };\n candidates.push(candidate);\n } else {\n const candidate = value.replace(OWS_EDGES, \"\");\n if (candidate.length === 0) return { reason: \"invalid\" };\n candidates.push(candidate);\n }\n }\n return candidates.length > 0 ? { candidates } : { reason: \"missing\" };\n}\n\nfunction matchingIndex(candidate: string, keys: readonly Buffer[]): number | undefined {\n const encoded = Buffer.from(candidate, \"utf8\");\n for (let index = 0; index < keys.length; index += 1) {\n const key = keys[index];\n if (encoded.length === key.length && timingSafeEqual(encoded, key)) return index;\n }\n return undefined;\n}\n\nexport function validateRequestApiKey(req: http.IncomingMessage, policy: ApiKeyPolicy): AuthResult {\n if (!policy.enabled) return { ok: true };\n const extracted = rawCandidates(req);\n if (!extracted.candidates) return { ok: false, reason: extracted.reason };\n let resolved: number | undefined;\n for (const candidate of extracted.candidates) {\n const index = matchingIndex(candidate, policy.keys);\n if (index === undefined) return { ok: false, reason: \"invalid\" };\n if (resolved !== undefined && resolved !== index) return { ok: false, reason: \"conflict\" };\n resolved = index;\n }\n return { ok: true };\n}\n\n/** @internal Opaque egress marker; it deliberately carries no key material. */\nexport function markAuthenticatedRequest(req: http.IncomingMessage, policy: ApiKeyPolicy): void {\n if (policy.enabled) authenticatedRequests.add(req);\n}\n\n/** @internal Used by egress paths to select safe credential handling. */\nexport function isAuthenticatedRequest(req: http.IncomingMessage): boolean {\n return authenticatedRequests.has(req);\n}\n\nexport function writeApiKeyHttpRejection(\n res: http.ServerResponse,\n setCorsHeaders: (res: http.ServerResponse) => void,\n): void {\n setCorsHeaders(res);\n res.writeHead(401, {\n \"Content-Type\": \"application/json\",\n \"Content-Length\": Buffer.byteLength(API_KEY_ERROR_BODY),\n \"WWW-Authenticate\": 'Bearer realm=\"aimock\"',\n });\n res.end(API_KEY_ERROR_BODY);\n}\n\nexport function writeApiKeyUpgradeRejection(socket: net.Socket): void {\n socket.write(\n [\n \"HTTP/1.1 401 Unauthorized\",\n \"Content-Type: application/json\",\n `Content-Length: ${Buffer.byteLength(API_KEY_ERROR_BODY)}`,\n 'WWW-Authenticate: Bearer realm=\"aimock\"',\n \"Connection: close\",\n \"\",\n API_KEY_ERROR_BODY,\n ].join(\"\\r\\n\"),\n );\n socket.destroy();\n}\n"],"mappings":"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"}