@copilotkit/aimock 1.43.0 → 1.44.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 (290) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +15 -0
  4. package/README.md +8 -2
  5. package/dist/agui-handler.d.ts.map +1 -1
  6. package/dist/agui-types.d.ts.map +1 -1
  7. package/dist/bedrock-converse.cjs +32 -4
  8. package/dist/bedrock-converse.cjs.map +1 -1
  9. package/dist/bedrock-converse.d.cts.map +1 -1
  10. package/dist/bedrock-converse.d.ts.map +1 -1
  11. package/dist/bedrock-converse.js +32 -4
  12. package/dist/bedrock-converse.js.map +1 -1
  13. package/dist/chaos.cjs +8 -5
  14. package/dist/chaos.cjs.map +1 -1
  15. package/dist/chaos.d.cts.map +1 -1
  16. package/dist/chaos.d.ts.map +1 -1
  17. package/dist/chaos.js +9 -6
  18. package/dist/chaos.js.map +1 -1
  19. package/dist/cli.cjs +89 -7
  20. package/dist/cli.cjs.map +1 -1
  21. package/dist/cli.js +91 -9
  22. package/dist/cli.js.map +1 -1
  23. package/dist/cohere.cjs +27 -1
  24. package/dist/cohere.cjs.map +1 -1
  25. package/dist/cohere.js +27 -1
  26. package/dist/cohere.js.map +1 -1
  27. package/dist/config-loader.cjs +1 -1
  28. package/dist/config-loader.d.ts.map +1 -1
  29. package/dist/config-loader.js +1 -1
  30. package/dist/constants.cjs +8 -0
  31. package/dist/constants.cjs.map +1 -1
  32. package/dist/constants.d.cts +6 -2
  33. package/dist/constants.d.ts +6 -2
  34. package/dist/constants.js +8 -1
  35. package/dist/constants.js.map +1 -1
  36. package/dist/echo-text.cjs +65 -0
  37. package/dist/echo-text.cjs.map +1 -0
  38. package/dist/echo-text.d.cts +22 -0
  39. package/dist/echo-text.d.cts.map +1 -0
  40. package/dist/echo-text.d.ts +22 -0
  41. package/dist/echo-text.d.ts.map +1 -0
  42. package/dist/echo-text.js +65 -0
  43. package/dist/echo-text.js.map +1 -0
  44. package/dist/embeddings.cjs +18 -1
  45. package/dist/embeddings.cjs.map +1 -1
  46. package/dist/embeddings.d.cts +1 -1
  47. package/dist/embeddings.d.cts.map +1 -1
  48. package/dist/embeddings.d.ts +1 -1
  49. package/dist/embeddings.d.ts.map +1 -1
  50. package/dist/embeddings.js +18 -1
  51. package/dist/embeddings.js.map +1 -1
  52. package/dist/files.cjs +9 -11
  53. package/dist/files.cjs.map +1 -1
  54. package/dist/files.d.cts.map +1 -1
  55. package/dist/files.d.ts.map +1 -1
  56. package/dist/files.js +10 -13
  57. package/dist/files.js.map +1 -1
  58. package/dist/fine-tuning.cjs +1 -19
  59. package/dist/fine-tuning.cjs.map +1 -1
  60. package/dist/fine-tuning.d.cts.map +1 -1
  61. package/dist/fine-tuning.d.ts.map +1 -1
  62. package/dist/fine-tuning.js +2 -20
  63. package/dist/fine-tuning.js.map +1 -1
  64. package/dist/fixture-loader-services.cjs +116 -0
  65. package/dist/fixture-loader-services.cjs.map +1 -0
  66. package/dist/fixture-loader-services.d.cts +48 -0
  67. package/dist/fixture-loader-services.d.cts.map +1 -0
  68. package/dist/fixture-loader-services.d.ts +48 -0
  69. package/dist/fixture-loader-services.d.ts.map +1 -0
  70. package/dist/fixture-loader-services.js +114 -0
  71. package/dist/fixture-loader-services.js.map +1 -0
  72. package/dist/fixture-loader.cjs +172 -11
  73. package/dist/fixture-loader.cjs.map +1 -1
  74. package/dist/fixture-loader.d.cts +95 -1
  75. package/dist/fixture-loader.d.cts.map +1 -1
  76. package/dist/fixture-loader.d.ts +95 -1
  77. package/dist/fixture-loader.d.ts.map +1 -1
  78. package/dist/fixture-loader.js +168 -12
  79. package/dist/fixture-loader.js.map +1 -1
  80. package/dist/gemini-embeddings.cjs +42 -2
  81. package/dist/gemini-embeddings.cjs.map +1 -1
  82. package/dist/gemini-embeddings.js +43 -3
  83. package/dist/gemini-embeddings.js.map +1 -1
  84. package/dist/gemini-interactions.cjs +38 -0
  85. package/dist/gemini-interactions.cjs.map +1 -1
  86. package/dist/gemini-interactions.d.cts.map +1 -1
  87. package/dist/gemini-interactions.d.ts.map +1 -1
  88. package/dist/gemini-interactions.js +38 -0
  89. package/dist/gemini-interactions.js.map +1 -1
  90. package/dist/gemini.cjs +8 -1
  91. package/dist/gemini.cjs.map +1 -1
  92. package/dist/gemini.d.cts.map +1 -1
  93. package/dist/gemini.d.ts.map +1 -1
  94. package/dist/gemini.js +8 -1
  95. package/dist/gemini.js.map +1 -1
  96. package/dist/helpers.cjs +276 -5
  97. package/dist/helpers.cjs.map +1 -1
  98. package/dist/helpers.d.cts.map +1 -1
  99. package/dist/helpers.d.ts.map +1 -1
  100. package/dist/helpers.js +275 -7
  101. package/dist/helpers.js.map +1 -1
  102. package/dist/images.cjs +3 -2
  103. package/dist/images.cjs.map +1 -1
  104. package/dist/images.js +3 -2
  105. package/dist/images.js.map +1 -1
  106. package/dist/index.cjs +30 -1
  107. package/dist/index.d.cts +8 -3
  108. package/dist/index.d.ts +8 -3
  109. package/dist/index.js +9 -3
  110. package/dist/jest.cjs +15 -10
  111. package/dist/jest.cjs.map +1 -1
  112. package/dist/jest.d.cts +4 -2
  113. package/dist/jest.d.cts.map +1 -1
  114. package/dist/jest.d.ts +4 -2
  115. package/dist/jest.d.ts.map +1 -1
  116. package/dist/jest.js +16 -11
  117. package/dist/jest.js.map +1 -1
  118. package/dist/journal.cjs +20 -0
  119. package/dist/journal.cjs.map +1 -1
  120. package/dist/journal.d.cts +16 -0
  121. package/dist/journal.d.cts.map +1 -1
  122. package/dist/journal.d.ts +16 -0
  123. package/dist/journal.d.ts.map +1 -1
  124. package/dist/journal.js +20 -0
  125. package/dist/journal.js.map +1 -1
  126. package/dist/llmock.cjs +82 -10
  127. package/dist/llmock.cjs.map +1 -1
  128. package/dist/llmock.d.cts +35 -0
  129. package/dist/llmock.d.cts.map +1 -1
  130. package/dist/llmock.d.ts +35 -0
  131. package/dist/llmock.d.ts.map +1 -1
  132. package/dist/llmock.js +83 -11
  133. package/dist/llmock.js.map +1 -1
  134. package/dist/mcp-fakes-mount.cjs +216 -0
  135. package/dist/mcp-fakes-mount.cjs.map +1 -0
  136. package/dist/mcp-fakes-mount.js +210 -0
  137. package/dist/mcp-fakes-mount.js.map +1 -0
  138. package/dist/mcp-fakes.cjs +1619 -0
  139. package/dist/mcp-fakes.cjs.map +1 -0
  140. package/dist/mcp-fakes.d.cts +101 -0
  141. package/dist/mcp-fakes.d.cts.map +1 -0
  142. package/dist/mcp-fakes.d.ts +101 -0
  143. package/dist/mcp-fakes.d.ts.map +1 -0
  144. package/dist/mcp-fakes.js +1614 -0
  145. package/dist/mcp-fakes.js.map +1 -0
  146. package/dist/mcp-handler.cjs +331 -17
  147. package/dist/mcp-handler.cjs.map +1 -1
  148. package/dist/mcp-handler.js +330 -18
  149. package/dist/mcp-handler.js.map +1 -1
  150. package/dist/mcp-mock.cjs +217 -49
  151. package/dist/mcp-mock.cjs.map +1 -1
  152. package/dist/mcp-mock.d.cts +66 -1
  153. package/dist/mcp-mock.d.cts.map +1 -1
  154. package/dist/mcp-mock.d.ts +66 -1
  155. package/dist/mcp-mock.d.ts.map +1 -1
  156. package/dist/mcp-mock.js +218 -50
  157. package/dist/mcp-mock.js.map +1 -1
  158. package/dist/mcp-stub.cjs +5 -1
  159. package/dist/mcp-stub.d.cts +3 -1
  160. package/dist/mcp-stub.d.ts +3 -1
  161. package/dist/mcp-stub.js +3 -1
  162. package/dist/mcp-types.d.cts +6 -0
  163. package/dist/mcp-types.d.ts +6 -0
  164. package/dist/message-text.cjs +192 -0
  165. package/dist/message-text.cjs.map +1 -0
  166. package/dist/message-text.d.cts +31 -0
  167. package/dist/message-text.d.cts.map +1 -0
  168. package/dist/message-text.d.ts +31 -0
  169. package/dist/message-text.d.ts.map +1 -0
  170. package/dist/message-text.js +184 -0
  171. package/dist/message-text.js.map +1 -0
  172. package/dist/messages.cjs +38 -0
  173. package/dist/messages.cjs.map +1 -1
  174. package/dist/messages.js +38 -0
  175. package/dist/messages.js.map +1 -1
  176. package/dist/metrics.cjs +46 -66
  177. package/dist/metrics.cjs.map +1 -1
  178. package/dist/metrics.d.cts +14 -6
  179. package/dist/metrics.d.cts.map +1 -1
  180. package/dist/metrics.d.ts +14 -6
  181. package/dist/metrics.d.ts.map +1 -1
  182. package/dist/metrics.js +32 -40
  183. package/dist/metrics.js.map +1 -1
  184. package/dist/ollama.cjs +5 -1
  185. package/dist/ollama.cjs.map +1 -1
  186. package/dist/ollama.d.cts.map +1 -1
  187. package/dist/ollama.d.ts.map +1 -1
  188. package/dist/ollama.js +5 -1
  189. package/dist/ollama.js.map +1 -1
  190. package/dist/openapi.cjs +3405 -0
  191. package/dist/openapi.cjs.map +1 -0
  192. package/dist/openapi.d.cts +30 -0
  193. package/dist/openapi.d.cts.map +1 -0
  194. package/dist/openapi.d.ts +30 -0
  195. package/dist/openapi.d.ts.map +1 -0
  196. package/dist/openapi.js +3404 -0
  197. package/dist/openapi.js.map +1 -0
  198. package/dist/openrouter-video.cjs +4 -4
  199. package/dist/openrouter-video.cjs.map +1 -1
  200. package/dist/openrouter-video.d.cts.map +1 -1
  201. package/dist/openrouter-video.d.ts.map +1 -1
  202. package/dist/openrouter-video.js +4 -4
  203. package/dist/openrouter-video.js.map +1 -1
  204. package/dist/recorder.cjs +9 -1
  205. package/dist/recorder.cjs.map +1 -1
  206. package/dist/recorder.d.cts.map +1 -1
  207. package/dist/recorder.d.ts.map +1 -1
  208. package/dist/recorder.js +9 -1
  209. package/dist/recorder.js.map +1 -1
  210. package/dist/responses.cjs +43 -4
  211. package/dist/responses.cjs.map +1 -1
  212. package/dist/responses.d.cts.map +1 -1
  213. package/dist/responses.d.ts.map +1 -1
  214. package/dist/responses.js +43 -4
  215. package/dist/responses.js.map +1 -1
  216. package/dist/route-registry.cjs +951 -0
  217. package/dist/route-registry.cjs.map +1 -0
  218. package/dist/route-registry.d.cts +33 -0
  219. package/dist/route-registry.d.cts.map +1 -0
  220. package/dist/route-registry.d.ts +33 -0
  221. package/dist/route-registry.d.ts.map +1 -0
  222. package/dist/route-registry.js +866 -0
  223. package/dist/route-registry.js.map +1 -0
  224. package/dist/router.cjs +1 -1
  225. package/dist/router.cjs.map +1 -1
  226. package/dist/router.js +1 -1
  227. package/dist/router.js.map +1 -1
  228. package/dist/server.cjs +590 -191
  229. package/dist/server.cjs.map +1 -1
  230. package/dist/server.d.cts +4 -1
  231. package/dist/server.d.cts.map +1 -1
  232. package/dist/server.d.ts +4 -1
  233. package/dist/server.d.ts.map +1 -1
  234. package/dist/server.js +498 -99
  235. package/dist/server.js.map +1 -1
  236. package/dist/speech.cjs +4 -2
  237. package/dist/speech.cjs.map +1 -1
  238. package/dist/speech.js +4 -2
  239. package/dist/speech.js.map +1 -1
  240. package/dist/suite.cjs +1 -1
  241. package/dist/suite.js +1 -1
  242. package/dist/transcription.cjs +2 -2
  243. package/dist/transcription.cjs.map +1 -1
  244. package/dist/transcription.d.cts.map +1 -1
  245. package/dist/transcription.d.ts.map +1 -1
  246. package/dist/transcription.js +2 -2
  247. package/dist/transcription.js.map +1 -1
  248. package/dist/types.cjs +18 -0
  249. package/dist/types.cjs.map +1 -0
  250. package/dist/types.d.cts +202 -2
  251. package/dist/types.d.cts.map +1 -1
  252. package/dist/types.d.ts +202 -2
  253. package/dist/types.d.ts.map +1 -1
  254. package/dist/types.js +17 -0
  255. package/dist/types.js.map +1 -0
  256. package/dist/validate-cli.cjs +89 -31
  257. package/dist/validate-cli.cjs.map +1 -1
  258. package/dist/validate-cli.js +91 -33
  259. package/dist/validate-cli.js.map +1 -1
  260. package/dist/vector-stores.cjs +1180 -0
  261. package/dist/vector-stores.cjs.map +1 -0
  262. package/dist/vector-stores.d.cts +106 -0
  263. package/dist/vector-stores.d.cts.map +1 -0
  264. package/dist/vector-stores.d.ts +106 -0
  265. package/dist/vector-stores.d.ts.map +1 -0
  266. package/dist/vector-stores.js +1164 -0
  267. package/dist/vector-stores.js.map +1 -0
  268. package/dist/vector-types.d.cts.map +1 -1
  269. package/dist/vector-types.d.ts.map +1 -1
  270. package/dist/video.cjs +19 -0
  271. package/dist/video.cjs.map +1 -1
  272. package/dist/video.d.cts.map +1 -1
  273. package/dist/video.d.ts.map +1 -1
  274. package/dist/video.js +19 -0
  275. package/dist/video.js.map +1 -1
  276. package/dist/vitest.cjs +15 -10
  277. package/dist/vitest.cjs.map +1 -1
  278. package/dist/vitest.d.cts +4 -2
  279. package/dist/vitest.d.cts.map +1 -1
  280. package/dist/vitest.d.ts +4 -2
  281. package/dist/vitest.d.ts.map +1 -1
  282. package/dist/vitest.js +16 -11
  283. package/dist/vitest.js.map +1 -1
  284. package/dist/ws-gemini-live.d.ts +2 -2
  285. package/dist/ws-realtime.d.ts +2 -2
  286. package/dist/ws-responses.cjs +14 -2
  287. package/dist/ws-responses.cjs.map +1 -1
  288. package/dist/ws-responses.js +14 -2
  289. package/dist/ws-responses.js.map +1 -1
  290. package/package.json +4 -1
package/dist/helpers.js CHANGED
@@ -1,4 +1,6 @@
1
- import { DEFAULT_TEST_ID } from "./constants.js";
1
+ import { DEFAULT_TEST_ID, MCP_FAKES_ECHO_LIMIT } from "./constants.js";
2
+ import { cutText } from "./echo-text.js";
3
+ import { build, fixed, msg, quote } from "./message-text.js";
2
4
  import { isReasoningModel } from "./model-utils.js";
3
5
  import { isRecognizedApiKeyHeader } from "./api-key-auth.js";
4
6
  import { createHash, randomBytes } from "node:crypto";
@@ -25,6 +27,25 @@ function resolveStrictMode(serverDefault, rawHeaders) {
25
27
  }
26
28
  return serverDefault ?? false;
27
29
  }
30
+ const STRICT_INTEGER_RE = /^\d+$/;
31
+ /**
32
+ * Strict decimal-integer grammar shared by the query/list parsers that grew
33
+ * their own local copy on separate branches (files `limit`, fine-tuning
34
+ * `limit`, chaos integer fields): an unsigned digit run and nothing else — no
35
+ * sign, no exponent, no radix prefix, no surrounding whitespace (callers that
36
+ * accept padded input trim before calling).
37
+ *
38
+ * Deliberately narrower than `Number()`, which reads `0x10` as 16, `1e3` as
39
+ * 1000 and strips whitespace and a leading `+`. Returns the value, or `null`
40
+ * when the text is not a safe integer literal. Range checks and error
41
+ * messages stay with the callers — this is the grammar gate only, so sharing
42
+ * it cannot change any surface's accepted range or its 400 text.
43
+ */
44
+ function parseStrictIntegerText(text) {
45
+ if (!STRICT_INTEGER_RE.test(text)) return null;
46
+ const n = Number(text);
47
+ return Number.isSafeInteger(n) ? n : null;
48
+ }
28
49
  /**
29
50
  * Would a fixture miss on this request have been forwarded upstream? This is
30
51
  * the ONE rule behind the no-fixture chaos journal `source` label: "proxy"
@@ -1070,11 +1091,14 @@ function matchesPattern(text, pattern) {
1070
1091
  return result;
1071
1092
  }
1072
1093
  /**
1073
- * The ONE definition of "which test does this traffic belong to": the
1074
- * `X-Test-Id` header wins, then `?testId=` in the query string, then
1075
- * `DEFAULT_TEST_ID`. Every per-test axis (fixture match-counts, the journal
1076
- * filter, chaos scoping) resolves through this, so a caller that tags one way
1077
- * can never land in two different scopes.
1094
+ * Which test LLM and other HTTP-route traffic belongs to: the `X-Test-Id`
1095
+ * header (raw, not percent-decoded) wins, then `?testId=` in the query string
1096
+ * (read through `URLSearchParams`, so percent-decoded and with `+` as a
1097
+ * space), then `DEFAULT_TEST_ID`. Every per-test axis of those routes
1098
+ * (fixture match-counts, the journal filter, chaos scoping) resolves through
1099
+ * this. MCP mounts resolve the test id with {@link resolveMcpIdentity}
1100
+ * instead, which percent-decodes the header, so `X-Test-Id: a%20b` is `a%20b`
1101
+ * here and `a b` there. Both read a `+` in the query as a space.
1078
1102
  */
1079
1103
  function resolveTestId(headers, url) {
1080
1104
  const headerValue = headers["x-test-id"];
@@ -1098,6 +1122,237 @@ function getContext(req) {
1098
1122
  } else if (typeof headerValue === "string" && headerValue) return headerValue;
1099
1123
  }
1100
1124
  /**
1125
+ * Decode one MCP identity value: an `X-Test-Id` / `X-AIMock-Context` header
1126
+ * value, and also each query name and (through {@link decodeMcpQueryValue})
1127
+ * each `?testId=` / `?context=` / `?undeclared=` query value. Clients send `encodeURIComponent(id)` because
1128
+ * `fetch` rejects characters such as `›` in a header. A value that is not
1129
+ * valid percent-encoding (a raw `applies 50% discount`) is used as-is, with
1130
+ * `fellBack: true` so the caller can log the fallback. Only that `URIError`
1131
+ * is caught; any other throw propagates. MCP requests only: LLM routes read
1132
+ * the header raw and the query through `URLSearchParams` (see
1133
+ * {@link resolveTestId}).
1134
+ */
1135
+ function decodeMcpHeaderValue(raw) {
1136
+ try {
1137
+ return {
1138
+ value: decodeURIComponent(raw),
1139
+ fellBack: false
1140
+ };
1141
+ } catch (err) {
1142
+ if (!(err instanceof URIError)) throw err;
1143
+ return {
1144
+ value: raw,
1145
+ fellBack: true
1146
+ };
1147
+ }
1148
+ }
1149
+ /**
1150
+ * Decode one MCP query value (`?testId=`, `?context=`, `?undeclared=`) the way
1151
+ * `URLSearchParams` reads it (spec I1): a `+` is a space, then the value is
1152
+ * percent-decoded by the {@link decodeMcpHeaderValue} rule. So a query built
1153
+ * with `URL.searchParams.set(...)` (a space sent as `+`) and one built with
1154
+ * `encodeURIComponent` both resolve to the id itself. A value that is not
1155
+ * valid percent-encoding is used as-is (its `+` still a space), with
1156
+ * `fellBack: true`.
1157
+ */
1158
+ function decodeMcpQueryValue(raw) {
1159
+ return decodeMcpHeaderValue(raw.replace(/\+/g, " "));
1160
+ }
1161
+ /**
1162
+ * Every value of the header `name`, one per header line: `[]` when absent.
1163
+ * Node joins a repeated header into one comma-joined `req.headers` string, so
1164
+ * the values come from `req.headersDistinct` (one element per line) when the
1165
+ * request has it. A caller without it (a hand-built request) is read from
1166
+ * `req.headers`: one element for a string, each element for an array.
1167
+ */
1168
+ function headerValues(req, name) {
1169
+ const distinct = req.headersDistinct?.[name];
1170
+ if (distinct !== void 0) return distinct;
1171
+ const value = req.headers[name];
1172
+ if (value === void 0) return [];
1173
+ return Array.isArray(value) ? value : [value];
1174
+ }
1175
+ /**
1176
+ * Every pair of a query string, in order, each name decoded by the
1177
+ * `decodeMcpHeaderValue` rule. A bare `name` with no `=` counts as an
1178
+ * occurrence with an empty value, so `?testId&testId=t1` has two. Each value
1179
+ * is left raw for the caller to decode with {@link decodeMcpQueryValue}.
1180
+ * A name that is not valid percent-encoding (`test%Id`) gets `name: null`: it
1181
+ * matches no field, and the caller counts it.
1182
+ */
1183
+ function queryPairs(query) {
1184
+ if (query === "") return [];
1185
+ return query.split("&").map((pair) => {
1186
+ const eq = pair.indexOf("=");
1187
+ const key = decodeMcpHeaderValue(eq === -1 ? pair : pair.slice(0, eq));
1188
+ return {
1189
+ name: key.fellBack ? null : key.value,
1190
+ value: eq === -1 ? "" : pair.slice(eq + 1)
1191
+ };
1192
+ });
1193
+ }
1194
+ /**
1195
+ * A request value as a message part, the way the message builder quotes every
1196
+ * user string: JSON-quoted (quotes and control characters escaped), then cut
1197
+ * once as JSON text, so the whole echo, quotes, escapes and the
1198
+ * `… (<n> more chars)` count included, fits in `MCP_FAKES_ECHO_LIMIT` and the
1199
+ * cut never splits a JSON escape or a surrogate pair.
1200
+ */
1201
+ function echoIdentityValue(value) {
1202
+ return quote(value, MCP_FAKES_ECHO_LIMIT);
1203
+ }
1204
+ const MCP_IDENTITY_INPUTS = {
1205
+ testId: {
1206
+ header: "x-test-id",
1207
+ query: "testId",
1208
+ label: "X-Test-Id"
1209
+ },
1210
+ context: {
1211
+ header: "x-aimock-context",
1212
+ query: "context",
1213
+ label: "X-AIMock-Context"
1214
+ },
1215
+ undeclared: {
1216
+ header: "x-aimock-mcp-undeclared",
1217
+ query: "undeclared",
1218
+ label: "X-AIMock-MCP-Undeclared"
1219
+ }
1220
+ };
1221
+ /**
1222
+ * Resolve the identity (test id, context, undeclared override) of one MCP request.
1223
+ * Each field resolves on its own, first of: the header, the query parameter,
1224
+ * the session value bound at `initialize`. A field sent more than once on
1225
+ * either path (more than one header line, read from `headersDistinct` because
1226
+ * Node joins repeated lines into one `headers` string; a header array from a
1227
+ * hand-built request; a repeated query name) is an error
1228
+ * (`MCP_DUPLICATE_IDENTITY`), even when the other path also carries it. For
1229
+ * the test id and the context, a header value is decoded with
1230
+ * `decodeURIComponent` (raw value on failure, with `fellBack` set) and a query
1231
+ * value the way `URLSearchParams` reads it (spec I1): a `+` is a space, then
1232
+ * the same `decodeURIComponent` rule. So `encodeURIComponent` output (what
1233
+ * clients send) and a query built with `URL.searchParams` both decode to the
1234
+ * id itself, while a raw `+` is a `+` in the header and a space in the query.
1235
+ * When both paths carry a test id or a context, the header is used and the
1236
+ * query value is ignored, even if the two differ, but an ignored query value
1237
+ * that does not decode still sets `fellBack`. The undeclared header is not
1238
+ * percent-decoded; its query value is, by the same query rule. Both are
1239
+ * trimmed and compared case-insensitively. `supplied` says whether the
1240
+ * request itself carried a non-empty header or query value for the field,
1241
+ * never `resolveTestId`'s result (which returns `"__default__"` when nothing
1242
+ * is sent). Blank values: an empty value is not supplied and falls through;
1243
+ * it is never rejected. A whitespace-only value is not supplied for the
1244
+ * undeclared override (it is trimmed), but is a real value for the test id
1245
+ * and the context, which match exactly and are never trimmed (Node trims a
1246
+ * header value, so only the query or a hand-built request can carry one). A
1247
+ * lone query name with no `=` carries no value. An empty session value counts
1248
+ * as none, and a missing `req.url` as `/`. A field with no value anywhere is
1249
+ * `null`. An unrecognized undeclared override on the header or the query is
1250
+ * an error (`ok: false`), not a fall-through: both are checked before the
1251
+ * header one is used, so a valid header does not hide a mistyped query value.
1252
+ * Bad percent-encoding is never silent: a test id or context value that does
1253
+ * not decode sets `fellBack`, on either path, used or ignored; an undeclared
1254
+ * query value that does not decode is that error, with the message saying
1255
+ * so; and a query name that does not decode matches no field and is counted
1256
+ * in `undecodedQueryNames`. `resolveTestId` / `getContext` stay unchanged for
1257
+ * LLM routes.
1258
+ */
1259
+ function resolveMcpIdentity(req, session) {
1260
+ const url = req.url ?? "/";
1261
+ const qIdx = url.indexOf("?");
1262
+ const pairs = queryPairs(qIdx === -1 ? "" : url.slice(qIdx + 1));
1263
+ const raw = {};
1264
+ for (const field of [
1265
+ "testId",
1266
+ "context",
1267
+ "undeclared"
1268
+ ]) {
1269
+ const input = MCP_IDENTITY_INPUTS[field];
1270
+ const sent = [["header", headerValues(req, input.header)], ["query", pairs.filter((p) => p.name === input.query).map((p) => p.value)]];
1271
+ for (const [source, values] of sent) if (values.length > 1) {
1272
+ const label = source === "header" ? msg`${fixed(input.label)} header` : msg`?${fixed(input.query)}= query parameter`;
1273
+ return {
1274
+ ok: false,
1275
+ error: {
1276
+ code: "MCP_DUPLICATE_IDENTITY",
1277
+ status: 400,
1278
+ field,
1279
+ source,
1280
+ count: values.length,
1281
+ message: build(msg`Duplicate ${label}: ${values.length} values sent, expected one`)
1282
+ }
1283
+ };
1284
+ }
1285
+ raw[field] = {
1286
+ header: sent[0][1][0],
1287
+ query: sent[1][1][0]
1288
+ };
1289
+ }
1290
+ const fromRequest = (field) => {
1291
+ const { header, query: q } = raw[field];
1292
+ const fromQuery = q ? decodeMcpQueryValue(q) : null;
1293
+ if (header) {
1294
+ const used = decodeMcpHeaderValue(header);
1295
+ return {
1296
+ value: used.value,
1297
+ fellBack: used.fellBack || (fromQuery?.fellBack ?? false)
1298
+ };
1299
+ }
1300
+ return fromQuery ?? {
1301
+ value: null,
1302
+ fellBack: false
1303
+ };
1304
+ };
1305
+ const testId = fromRequest("testId");
1306
+ const context = fromRequest("context");
1307
+ let undeclared = null;
1308
+ for (const source of ["header", "query"]) {
1309
+ const sent = raw.undeclared[source];
1310
+ if (sent === void 0) continue;
1311
+ const decoded = source === "header" ? {
1312
+ value: sent,
1313
+ fellBack: false
1314
+ } : decodeMcpQueryValue(sent);
1315
+ if (decoded.value.trim() === "") continue;
1316
+ const parsed = parseMcpUndeclared(decoded.value);
1317
+ if (parsed === null) {
1318
+ const echoed = echoIdentityValue(sent);
1319
+ const label = source === "header" ? msg`X-AIMock-MCP-Undeclared header` : msg`?undeclared= query parameter`;
1320
+ const note = decoded.fellBack ? msg` (not valid percent-encoding)` : msg``;
1321
+ return {
1322
+ ok: false,
1323
+ error: {
1324
+ code: "MCP_INVALID_UNDECLARED",
1325
+ status: 400,
1326
+ field: "undeclared",
1327
+ source,
1328
+ raw: cutText(sent),
1329
+ message: build(msg`Invalid ${label} value ${echoed}${note}: expected allow or deny`)
1330
+ }
1331
+ };
1332
+ }
1333
+ undeclared ??= parsed;
1334
+ }
1335
+ const undecodedQueryNames = pairs.filter((p) => p.name === null).length;
1336
+ return {
1337
+ ok: true,
1338
+ identity: {
1339
+ testId: testId.value ?? (session?.testId || null),
1340
+ context: context.value ?? (session?.context || null),
1341
+ undeclared: undeclared ?? session?.undeclared ?? null
1342
+ },
1343
+ supplied: {
1344
+ testId: testId.value !== null,
1345
+ context: context.value !== null,
1346
+ undeclared: undeclared !== null
1347
+ },
1348
+ fellBack: {
1349
+ testId: testId.fellBack,
1350
+ context: context.fellBack
1351
+ },
1352
+ ...undecodedQueryNames > 0 ? { undecodedQueryNames } : {}
1353
+ };
1354
+ }
1355
+ /**
1101
1356
  * Convert a test ID (e.g. Playwright titlePath) into a filesystem-safe slug
1102
1357
  * suitable for use as a directory name in snapshot-style recording.
1103
1358
  */
@@ -1328,7 +1583,20 @@ function describeMatch(match, index) {
1328
1583
  const cut = head.lastIndexOf(", ");
1329
1584
  return `${cut > 0 ? head.slice(0, cut) : head}, … }`;
1330
1585
  }
1586
+ /**
1587
+ * Parse an `X-AIMock-MCP-Undeclared` header or `?undeclared=` value.
1588
+ * Modeled on {@link resolveStrictMode}: case-insensitive, surrounding
1589
+ * whitespace trimmed. Returns `null` when unrecognised; unlike the strict
1590
+ * header, {@link resolveMcpIdentity} turns that into an error rather than a
1591
+ * fall-through, because falling through would turn a mistyped `deny` into
1592
+ * the permissive default.
1593
+ */
1594
+ function parseMcpUndeclared(raw) {
1595
+ const val = raw.trim().toLowerCase();
1596
+ if (val === "allow" || val === "deny") return val;
1597
+ return null;
1598
+ }
1331
1599
 
1332
1600
  //#endregion
1333
- export { FORMAT_TO_CONTENT_TYPE, MAX_EMBEDDING_DIMENSIONS, RequestBodyTooLargeError, buildContentWithToolCallsChunks, buildContentWithToolCallsCompletion, buildEmbeddingResponse, buildTextChunks, buildTextCompletion, buildToolCallChunks, buildToolCallCompletion, buildUsageChunk, describeMatch, extractOverrides, flattenHeaders, formatToMime, generateDeterministicEmbedding, generateId, generateMessageId, generateToolCallId, generateToolUseId, getContext, getTestId, hasMintedRequestId, isAudioResponse, isContentWithToolCallsResponse, isEmbeddingResponse, isErrorResponse, isImageResponse, isJSONResponse, isJsonObject, isLiveResponse, isResponseFactory, isTextResponse, isToolCallResponse, isTranscriptionResponse, isVideoResponse, markMintedRequestId, matchesPattern, normalizeEmbeddingInput, normalizeTextInput, readBody, readBodyBufferBounded, resolveFixtureBlockOutcome, resolveFixtureBlocks, resolveReasoningArtifactsForModel, resolveReasoningForModel, resolveRequestId, resolveResponse, resolveStrictMode, resolveTestId, resolveUsage, serializeErrorResponse, slugifyContext, slugifyTestId, strictNoMatchLogLine, strictNoMatchMessage, strictOverrideField, validateChatMessages, validateEmbeddingDimensions, validateToolsField, wouldProxyMiss };
1601
+ export { FORMAT_TO_CONTENT_TYPE, MAX_EMBEDDING_DIMENSIONS, RequestBodyTooLargeError, buildContentWithToolCallsChunks, buildContentWithToolCallsCompletion, buildEmbeddingResponse, buildTextChunks, buildTextCompletion, buildToolCallChunks, buildToolCallCompletion, buildUsageChunk, decodeMcpHeaderValue, describeMatch, extractOverrides, flattenHeaders, formatToMime, generateDeterministicEmbedding, generateId, generateMessageId, generateToolCallId, generateToolUseId, getContext, getTestId, hasMintedRequestId, isAudioResponse, isContentWithToolCallsResponse, isEmbeddingResponse, isErrorResponse, isImageResponse, isJSONResponse, isJsonObject, isLiveResponse, isResponseFactory, isTextResponse, isToolCallResponse, isTranscriptionResponse, isVideoResponse, markMintedRequestId, matchesPattern, normalizeEmbeddingInput, normalizeTextInput, parseStrictIntegerText, readBody, readBodyBufferBounded, resolveFixtureBlockOutcome, resolveFixtureBlocks, resolveMcpIdentity, resolveReasoningArtifactsForModel, resolveReasoningForModel, resolveRequestId, resolveResponse, resolveStrictMode, resolveTestId, resolveUsage, serializeErrorResponse, slugifyContext, slugifyTestId, strictNoMatchLogLine, strictNoMatchMessage, strictOverrideField, validateChatMessages, validateEmbeddingDimensions, validateToolsField, wouldProxyMiss };
1334
1602
  //# sourceMappingURL=helpers.js.map