@copilotkit/aimock 1.44.0 → 1.45.1

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 (278) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +42 -0
  4. package/README.md +6 -0
  5. package/dist/agui-types.d.cts.map +1 -1
  6. package/dist/aimock-cli.cjs +31 -5
  7. package/dist/aimock-cli.cjs.map +1 -1
  8. package/dist/aimock-cli.d.cts.map +1 -1
  9. package/dist/aimock-cli.d.ts.map +1 -1
  10. package/dist/aimock-cli.js +31 -5
  11. package/dist/aimock-cli.js.map +1 -1
  12. package/dist/bedrock-converse.cjs +312 -39
  13. package/dist/bedrock-converse.cjs.map +1 -1
  14. package/dist/bedrock-converse.d.cts.map +1 -1
  15. package/dist/bedrock-converse.d.ts.map +1 -1
  16. package/dist/bedrock-converse.js +313 -40
  17. package/dist/bedrock-converse.js.map +1 -1
  18. package/dist/bedrock.cjs +195 -42
  19. package/dist/bedrock.cjs.map +1 -1
  20. package/dist/bedrock.d.cts.map +1 -1
  21. package/dist/bedrock.d.ts.map +1 -1
  22. package/dist/bedrock.js +196 -43
  23. package/dist/bedrock.js.map +1 -1
  24. package/dist/chaos.cjs +1 -0
  25. package/dist/chaos.js +1 -1
  26. package/dist/cli.cjs +152 -5
  27. package/dist/cli.cjs.map +1 -1
  28. package/dist/cli.js +155 -8
  29. package/dist/cli.js.map +1 -1
  30. package/dist/cohere.cjs +132 -71
  31. package/dist/cohere.cjs.map +1 -1
  32. package/dist/cohere.d.cts.map +1 -1
  33. package/dist/cohere.d.ts.map +1 -1
  34. package/dist/cohere.js +133 -72
  35. package/dist/cohere.js.map +1 -1
  36. package/dist/config-loader.cjs +62 -4
  37. package/dist/config-loader.cjs.map +1 -1
  38. package/dist/config-loader.d.cts +37 -4
  39. package/dist/config-loader.d.cts.map +1 -1
  40. package/dist/config-loader.d.ts +37 -4
  41. package/dist/config-loader.d.ts.map +1 -1
  42. package/dist/config-loader.js +62 -4
  43. package/dist/config-loader.js.map +1 -1
  44. package/dist/fakes-plugin.cjs +131 -0
  45. package/dist/fakes-plugin.cjs.map +1 -0
  46. package/dist/fakes-plugin.d.cts +23 -0
  47. package/dist/fakes-plugin.d.cts.map +1 -0
  48. package/dist/fakes-plugin.d.ts +23 -0
  49. package/dist/fakes-plugin.d.ts.map +1 -0
  50. package/dist/fakes-plugin.js +127 -0
  51. package/dist/fakes-plugin.js.map +1 -0
  52. package/dist/fixture-loader-services.cjs +1 -1
  53. package/dist/fixture-loader-services.cjs.map +1 -1
  54. package/dist/fixture-loader-services.js +1 -1
  55. package/dist/fixture-loader-services.js.map +1 -1
  56. package/dist/fixture-loader.cjs +389 -27
  57. package/dist/fixture-loader.cjs.map +1 -1
  58. package/dist/fixture-loader.d.cts +47 -3
  59. package/dist/fixture-loader.d.cts.map +1 -1
  60. package/dist/fixture-loader.d.ts +47 -3
  61. package/dist/fixture-loader.d.ts.map +1 -1
  62. package/dist/fixture-loader.js +384 -29
  63. package/dist/fixture-loader.js.map +1 -1
  64. package/dist/gemini-interactions.cjs +250 -52
  65. package/dist/gemini-interactions.cjs.map +1 -1
  66. package/dist/gemini-interactions.d.cts.map +1 -1
  67. package/dist/gemini-interactions.d.ts.map +1 -1
  68. package/dist/gemini-interactions.js +251 -53
  69. package/dist/gemini-interactions.js.map +1 -1
  70. package/dist/gemini.cjs +226 -37
  71. package/dist/gemini.cjs.map +1 -1
  72. package/dist/gemini.d.cts.map +1 -1
  73. package/dist/gemini.d.ts.map +1 -1
  74. package/dist/gemini.js +227 -38
  75. package/dist/gemini.js.map +1 -1
  76. package/dist/helpers.cjs +881 -7
  77. package/dist/helpers.cjs.map +1 -1
  78. package/dist/helpers.d.cts +85 -4
  79. package/dist/helpers.d.cts.map +1 -1
  80. package/dist/helpers.d.ts +85 -4
  81. package/dist/helpers.d.ts.map +1 -1
  82. package/dist/helpers.js +839 -8
  83. package/dist/helpers.js.map +1 -1
  84. package/dist/index.cjs +9 -0
  85. package/dist/index.d.cts +7 -5
  86. package/dist/index.d.ts +7 -5
  87. package/dist/index.js +4 -3
  88. package/dist/jest.cjs +31 -7
  89. package/dist/jest.cjs.map +1 -1
  90. package/dist/jest.d.cts +43 -3
  91. package/dist/jest.d.cts.map +1 -1
  92. package/dist/jest.d.ts +43 -3
  93. package/dist/jest.d.ts.map +1 -1
  94. package/dist/jest.js +33 -9
  95. package/dist/jest.js.map +1 -1
  96. package/dist/journal.cjs +135 -13
  97. package/dist/journal.cjs.map +1 -1
  98. package/dist/journal.d.cts +38 -4
  99. package/dist/journal.d.cts.map +1 -1
  100. package/dist/journal.d.ts +38 -4
  101. package/dist/journal.d.ts.map +1 -1
  102. package/dist/journal.js +135 -13
  103. package/dist/journal.js.map +1 -1
  104. package/dist/live-sanitize.cjs +4 -32
  105. package/dist/live-sanitize.cjs.map +1 -1
  106. package/dist/live-sanitize.js +4 -32
  107. package/dist/live-sanitize.js.map +1 -1
  108. package/dist/llmock.cjs +123 -30
  109. package/dist/llmock.cjs.map +1 -1
  110. package/dist/llmock.d.cts +16 -1
  111. package/dist/llmock.d.cts.map +1 -1
  112. package/dist/llmock.d.ts +16 -1
  113. package/dist/llmock.d.ts.map +1 -1
  114. package/dist/llmock.js +125 -32
  115. package/dist/llmock.js.map +1 -1
  116. package/dist/mcp-fakes-mount.cjs +1 -0
  117. package/dist/mcp-fakes-mount.cjs.map +1 -1
  118. package/dist/mcp-fakes-mount.js +1 -0
  119. package/dist/mcp-fakes-mount.js.map +1 -1
  120. package/dist/mcp-fakes-report.cjs +111 -0
  121. package/dist/mcp-fakes-report.cjs.map +1 -0
  122. package/dist/mcp-fakes-report.d.cts +40 -0
  123. package/dist/mcp-fakes-report.d.cts.map +1 -0
  124. package/dist/mcp-fakes-report.d.ts +40 -0
  125. package/dist/mcp-fakes-report.d.ts.map +1 -0
  126. package/dist/mcp-fakes-report.js +107 -0
  127. package/dist/mcp-fakes-report.js.map +1 -0
  128. package/dist/mcp-fakes.cjs +331 -8
  129. package/dist/mcp-fakes.cjs.map +1 -1
  130. package/dist/mcp-fakes.d.cts +406 -3
  131. package/dist/mcp-fakes.d.cts.map +1 -1
  132. package/dist/mcp-fakes.d.ts +406 -3
  133. package/dist/mcp-fakes.d.ts.map +1 -1
  134. package/dist/mcp-fakes.js +331 -8
  135. package/dist/mcp-fakes.js.map +1 -1
  136. package/dist/mcp-handler.cjs +216 -17
  137. package/dist/mcp-handler.cjs.map +1 -1
  138. package/dist/mcp-handler.d.cts +49 -0
  139. package/dist/mcp-handler.d.cts.map +1 -0
  140. package/dist/mcp-handler.d.ts +49 -0
  141. package/dist/mcp-handler.d.ts.map +1 -0
  142. package/dist/mcp-handler.js +214 -18
  143. package/dist/mcp-handler.js.map +1 -1
  144. package/dist/mcp-mock.cjs +112 -1
  145. package/dist/mcp-mock.cjs.map +1 -1
  146. package/dist/mcp-mock.d.cts +33 -2
  147. package/dist/mcp-mock.d.cts.map +1 -1
  148. package/dist/mcp-mock.d.ts +33 -2
  149. package/dist/mcp-mock.d.ts.map +1 -1
  150. package/dist/mcp-mock.js +112 -1
  151. package/dist/mcp-mock.js.map +1 -1
  152. package/dist/mcp-recorder.cjs +1224 -0
  153. package/dist/mcp-recorder.cjs.map +1 -0
  154. package/dist/mcp-recorder.d.cts +108 -0
  155. package/dist/mcp-recorder.d.cts.map +1 -0
  156. package/dist/mcp-recorder.d.ts +108 -0
  157. package/dist/mcp-recorder.d.ts.map +1 -0
  158. package/dist/mcp-recorder.js +1215 -0
  159. package/dist/mcp-recorder.js.map +1 -0
  160. package/dist/messages.cjs +362 -56
  161. package/dist/messages.cjs.map +1 -1
  162. package/dist/messages.d.cts.map +1 -1
  163. package/dist/messages.d.ts.map +1 -1
  164. package/dist/messages.js +363 -58
  165. package/dist/messages.js.map +1 -1
  166. package/dist/misbehavior.cjs +1523 -0
  167. package/dist/misbehavior.cjs.map +1 -0
  168. package/dist/misbehavior.d.cts +43 -0
  169. package/dist/misbehavior.d.cts.map +1 -0
  170. package/dist/misbehavior.d.ts +43 -0
  171. package/dist/misbehavior.d.ts.map +1 -0
  172. package/dist/misbehavior.js +1511 -0
  173. package/dist/misbehavior.js.map +1 -0
  174. package/dist/ollama.cjs +310 -112
  175. package/dist/ollama.cjs.map +1 -1
  176. package/dist/ollama.d.cts.map +1 -1
  177. package/dist/ollama.d.ts.map +1 -1
  178. package/dist/ollama.js +311 -113
  179. package/dist/ollama.js.map +1 -1
  180. package/dist/openrouter-video.cjs +30 -2
  181. package/dist/openrouter-video.cjs.map +1 -1
  182. package/dist/openrouter-video.d.cts.map +1 -1
  183. package/dist/openrouter-video.d.ts.map +1 -1
  184. package/dist/openrouter-video.js +30 -2
  185. package/dist/openrouter-video.js.map +1 -1
  186. package/dist/record-sanitize.cjs +231 -0
  187. package/dist/record-sanitize.cjs.map +1 -0
  188. package/dist/record-sanitize.js +224 -0
  189. package/dist/record-sanitize.js.map +1 -0
  190. package/dist/recorder.cjs +106 -5
  191. package/dist/recorder.cjs.map +1 -1
  192. package/dist/recorder.d.cts +2 -1
  193. package/dist/recorder.d.cts.map +1 -1
  194. package/dist/recorder.d.ts +2 -1
  195. package/dist/recorder.d.ts.map +1 -1
  196. package/dist/recorder.js +107 -7
  197. package/dist/recorder.js.map +1 -1
  198. package/dist/responses.cjs +667 -171
  199. package/dist/responses.cjs.map +1 -1
  200. package/dist/responses.d.cts +5 -13
  201. package/dist/responses.d.cts.map +1 -1
  202. package/dist/responses.d.ts +5 -13
  203. package/dist/responses.d.ts.map +1 -1
  204. package/dist/responses.js +665 -174
  205. package/dist/responses.js.map +1 -1
  206. package/dist/router.cjs +24 -2
  207. package/dist/router.cjs.map +1 -1
  208. package/dist/router.d.cts.map +1 -1
  209. package/dist/router.d.ts.map +1 -1
  210. package/dist/router.js +25 -4
  211. package/dist/router.js.map +1 -1
  212. package/dist/server.cjs +572 -76
  213. package/dist/server.cjs.map +1 -1
  214. package/dist/server.d.cts.map +1 -1
  215. package/dist/server.d.ts.map +1 -1
  216. package/dist/server.js +574 -78
  217. package/dist/server.js.map +1 -1
  218. package/dist/stream-collapse.cjs +236 -34
  219. package/dist/stream-collapse.cjs.map +1 -1
  220. package/dist/stream-collapse.d.cts +39 -4
  221. package/dist/stream-collapse.d.cts.map +1 -1
  222. package/dist/stream-collapse.d.ts +39 -4
  223. package/dist/stream-collapse.d.ts.map +1 -1
  224. package/dist/stream-collapse.js +235 -35
  225. package/dist/stream-collapse.js.map +1 -1
  226. package/dist/types.cjs.map +1 -1
  227. package/dist/types.d.cts +315 -4
  228. package/dist/types.d.cts.map +1 -1
  229. package/dist/types.d.ts +315 -4
  230. package/dist/types.d.ts.map +1 -1
  231. package/dist/types.js.map +1 -1
  232. package/dist/validate-cli.cjs +36 -4
  233. package/dist/validate-cli.cjs.map +1 -1
  234. package/dist/validate-cli.js +37 -5
  235. package/dist/validate-cli.js.map +1 -1
  236. package/dist/vector-types.d.ts.map +1 -1
  237. package/dist/version.cjs +45 -0
  238. package/dist/version.cjs.map +1 -0
  239. package/dist/version.js +44 -0
  240. package/dist/version.js.map +1 -0
  241. package/dist/vitest.cjs +66 -10
  242. package/dist/vitest.cjs.map +1 -1
  243. package/dist/vitest.d.cts +35 -4
  244. package/dist/vitest.d.cts.map +1 -1
  245. package/dist/vitest.d.ts +35 -4
  246. package/dist/vitest.d.ts.map +1 -1
  247. package/dist/vitest.js +67 -11
  248. package/dist/vitest.js.map +1 -1
  249. package/dist/watcher.cjs +6 -2
  250. package/dist/watcher.cjs.map +1 -1
  251. package/dist/watcher.js +6 -2
  252. package/dist/watcher.js.map +1 -1
  253. package/dist/ws-gemini-live.cjs +210 -101
  254. package/dist/ws-gemini-live.cjs.map +1 -1
  255. package/dist/ws-gemini-live.d.cts +9 -3
  256. package/dist/ws-gemini-live.d.cts.map +1 -1
  257. package/dist/ws-gemini-live.d.ts +9 -3
  258. package/dist/ws-gemini-live.d.ts.map +1 -1
  259. package/dist/ws-gemini-live.js +211 -102
  260. package/dist/ws-gemini-live.js.map +1 -1
  261. package/dist/ws-realtime.cjs +175 -62
  262. package/dist/ws-realtime.cjs.map +1 -1
  263. package/dist/ws-realtime.d.cts +8 -3
  264. package/dist/ws-realtime.d.cts.map +1 -1
  265. package/dist/ws-realtime.d.ts +8 -3
  266. package/dist/ws-realtime.d.ts.map +1 -1
  267. package/dist/ws-realtime.js +176 -63
  268. package/dist/ws-realtime.js.map +1 -1
  269. package/dist/ws-responses.cjs +109 -16
  270. package/dist/ws-responses.cjs.map +1 -1
  271. package/dist/ws-responses.d.cts +8 -12
  272. package/dist/ws-responses.d.cts.map +1 -1
  273. package/dist/ws-responses.d.ts +8 -12
  274. package/dist/ws-responses.d.ts.map +1 -1
  275. package/dist/ws-responses.js +111 -18
  276. package/dist/ws-responses.js.map +1 -1
  277. package/package.json +10 -2
  278. package/skills/write-fixtures/SKILL.md +179 -21
@@ -0,0 +1,231 @@
1
+
2
+ //#region src/record-sanitize.ts
3
+ /**
4
+ * Shared record sanitizer (D10, S1-S7).
5
+ *
6
+ * The credential-key and URL rules were moved here from `live-sanitize.ts`,
7
+ * which imports them back unchanged. `sanitizeRecording` applies the generic
8
+ * record rules to any JSON value (for example an `mcpFakes` recording).
9
+ */
10
+ const CREDENTIAL_KEY = /^(?:authorization|proxy-authorization|cookie|set-cookie|x-api-key|api[_-]?key|access[_-]?token|refresh[_-]?token|client[_-]?secret|password|secret|token|sec-websocket-protocol)$/i;
11
+ const REDACTED = "[REDACTED]";
12
+ const MIN_SECRET_LENGTH = 8;
13
+ /** Thrown when a recording cannot be made safe (S6). Never carries a value. */
14
+ var RecordUnsafeError = class extends Error {
15
+ constructor(reason) {
16
+ super(`unsafe recording: ${reason}`);
17
+ this.reason = reason;
18
+ this.name = "RecordUnsafeError";
19
+ }
20
+ };
21
+ function decodeUrlComponent(value) {
22
+ return new URLSearchParams(`value=${value.replace(/\+/g, "%2B").replace(/&/g, "%26")}`).get("value");
23
+ }
24
+ /**
25
+ * S3: scrub one absolute URL string; non-URLs unchanged. `onPathSecret` is
26
+ * called (and must throw) when the decoded path contains a secret.
27
+ */
28
+ function scrubUrl(value, hasSecret, onPathSecret) {
29
+ if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(value)) return value;
30
+ let url;
31
+ try {
32
+ url = new URL(value);
33
+ } catch {
34
+ return value;
35
+ }
36
+ if (hasSecret(decodeUrlComponent(url.pathname))) onPathSecret();
37
+ let changed = false;
38
+ if (url.username || url.password) {
39
+ url.username = "";
40
+ url.password = "";
41
+ changed = true;
42
+ }
43
+ for (const [key, val] of [...url.searchParams]) if (CREDENTIAL_KEY.test(key) || hasSecret(key) || hasSecret(val)) {
44
+ url.searchParams.delete(key);
45
+ changed = true;
46
+ }
47
+ const decodedHash = decodeUrlComponent(url.hash);
48
+ if (url.hash && (hasSecret(decodedHash) || /(?:token|secret|password|api[_-]?key)=/i.test(decodedHash))) {
49
+ url.hash = "";
50
+ changed = true;
51
+ }
52
+ return changed ? url.href : value;
53
+ }
54
+ /** S2 (d) start check: throws a plain Error naming the rule, never the value. */
55
+ function validateSecretValues(values) {
56
+ for (const value of values) {
57
+ if (typeof value !== "string") throw new Error("record.secretValues entries must be strings");
58
+ if (value.length < MIN_SECRET_LENGTH) throw new Error(`record.secretValues entry shorter than ${MIN_SECRET_LENGTH} characters`);
59
+ }
60
+ }
61
+ const API_KEY_HEADER = /(?:^|[-_])api[-_]?key$/i;
62
+ /** The value, the token after the scheme, and for `Basic` the decoded user and password. */
63
+ function credentialParts(value) {
64
+ const parts = [value];
65
+ const match = /^(\S+)\s+(\S.*)$/.exec(value.trim());
66
+ if (match) {
67
+ const token = match[2].trim();
68
+ parts.push(token);
69
+ if (match[1].toLowerCase() === "basic") {
70
+ const decoded = Buffer.from(token, "base64").toString("utf8");
71
+ const colon = decoded.indexOf(":");
72
+ if (colon >= 0) parts.push(decoded.slice(0, colon), decoded.slice(colon + 1));
73
+ }
74
+ }
75
+ return parts;
76
+ }
77
+ function urlParts(value) {
78
+ let url;
79
+ try {
80
+ url = new URL(value);
81
+ } catch {
82
+ return [];
83
+ }
84
+ const parts = [];
85
+ if (url.username) parts.push(url.username, decodeUrlComponent(url.username));
86
+ if (url.password) parts.push(url.password, decodeUrlComponent(url.password));
87
+ for (const val of url.searchParams.values()) parts.push(val);
88
+ return parts;
89
+ }
90
+ /** S2 (a)-(c): secrets from forwarded headers, upstreamAuth and the upstream URL; min length 8. */
91
+ function knownSecrets(input) {
92
+ const derived = [];
93
+ for (const [name, value] of Object.entries(input.headers)) {
94
+ if (typeof value !== "string") continue;
95
+ if (CREDENTIAL_KEY.test(name) || API_KEY_HEADER.test(name)) derived.push(...credentialParts(value));
96
+ }
97
+ if (input.upstreamAuth) derived.push(...credentialParts(input.upstreamAuth.value));
98
+ derived.push(...urlParts(input.upstreamUrl));
99
+ const secrets = new Set(derived.filter((s) => s.length >= MIN_SECRET_LENGTH));
100
+ for (const value of input.secretValues) if (value.length) secrets.add(value);
101
+ return [...secrets].sort((a, b) => b.length - a.length);
102
+ }
103
+ /** `%2b` for `%2B`: some encoders write lowercase hex. */
104
+ function lowerHex(value) {
105
+ return value.replace(/%[0-9A-F]{2}/g, (m) => m.toLowerCase());
106
+ }
107
+ /** `f(value)`, or nothing when it throws (a lone surrogate, a bad escape). */
108
+ function attempt(f, value) {
109
+ try {
110
+ return [f(value)];
111
+ } catch {
112
+ return [];
113
+ }
114
+ }
115
+ /**
116
+ * Every textual form a known secret can take in a recording (G2b B1-r2): the
117
+ * value as registered, its percent-decoded forms (it may have been
118
+ * registered encoded, as a URL query value is), and for each of those the
119
+ * `encodeURIComponent`, form (`URLSearchParams`, `+` for space) and
120
+ * `encodeURI` encodings, with uppercase or lowercase hex. A derived form
121
+ * shorter than `MIN_SECRET_LENGTH` is dropped; the value itself never is.
122
+ */
123
+ function secretForms(secret) {
124
+ const bases = [
125
+ secret,
126
+ ...attempt(decodeURIComponent, secret),
127
+ ...attempt((v) => decodeURIComponent(v.replace(/\+/g, " ")), secret)
128
+ ];
129
+ const forms = /* @__PURE__ */ new Set();
130
+ for (const base of bases) {
131
+ forms.add(base);
132
+ const encoded = [
133
+ ...attempt(encodeURIComponent, base),
134
+ ...attempt((v) => new URLSearchParams([["v", v]]).toString().slice(2), base),
135
+ ...attempt(encodeURI, base),
136
+ ...attempt((v) => encodeURIComponent(v).replace(/%20/g, "+"), base)
137
+ ];
138
+ for (const form of encoded) {
139
+ forms.add(form);
140
+ forms.add(lowerHex(form));
141
+ }
142
+ }
143
+ return [...forms].filter((form) => form === secret || form.length >= MIN_SECRET_LENGTH);
144
+ }
145
+ const TOKEN_PATTERNS = [
146
+ {
147
+ kind: "Anthropic API key",
148
+ pattern: /\bsk-ant-[A-Za-z0-9_-]{8,}/
149
+ },
150
+ {
151
+ kind: "OpenAI API key",
152
+ pattern: /\bsk-(?!ant-)[A-Za-z0-9_-]{16,}/
153
+ },
154
+ {
155
+ kind: "GitHub token",
156
+ pattern: /\b(?:ghp_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,})/
157
+ },
158
+ {
159
+ kind: "Slack token",
160
+ pattern: /\bxox[abp]-[A-Za-z0-9-]{10,}/
161
+ },
162
+ {
163
+ kind: "AWS access key",
164
+ pattern: /\bAKIA[0-9A-Z]{16}\b/
165
+ },
166
+ {
167
+ kind: "JWT",
168
+ pattern: /\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/
169
+ }
170
+ ];
171
+ function escapePointer(key) {
172
+ return key.replace(/~/g, "~0").replace(/\//g, "~1");
173
+ }
174
+ /**
175
+ * S2-S7 over one JSON value. `isCredentialBranch(pointer)` is called with the
176
+ * pointer of each object and says whether credential-shaped keys directly
177
+ * under it are removed (S4: `/recorded…`, any `…/_meta…`).
178
+ * Throws RecordUnsafeError (S6). The input is never mutated.
179
+ */
180
+ function sanitizeRecording(value, secrets, isCredentialBranch) {
181
+ const ordered = [...new Set(secrets.filter((s) => s.length).flatMap(secretForms))].sort((a, b) => b.length - a.length);
182
+ const hasSecret = (s) => ordered.some((secret) => s.includes(secret));
183
+ const jsonForms = ordered.map((secret) => JSON.stringify(secret).slice(1, -1));
184
+ const hasSecretInJson = (s) => hasSecret(s) || jsonForms.some((secret) => s.includes(secret));
185
+ const onPathSecret = () => {
186
+ throw new RecordUnsafeError("secret-in-path");
187
+ };
188
+ const warnings = [];
189
+ function visitString(input, pointer) {
190
+ const url = scrubUrl(input, hasSecret, onPathSecret);
191
+ if (url !== input) warnings.push(`${pointer}: removed credentials from a URL`);
192
+ let out = url;
193
+ for (const secret of ordered) out = out.split(secret).join(REDACTED);
194
+ if (out !== url) warnings.push(`${pointer}: redacted a known secret`);
195
+ for (const { kind, pattern } of TOKEN_PATTERNS) if (pattern.test(out)) warnings.push(`${pointer}: looks like a ${kind} token`);
196
+ return out;
197
+ }
198
+ function visit(node, pointer) {
199
+ if (typeof node === "string") return visitString(node, pointer);
200
+ if (Array.isArray(node)) return node.map((item, i) => visit(item, `${pointer}/${i}`));
201
+ if (!node || typeof node !== "object") return node;
202
+ const removeCredentials = isCredentialBranch(pointer);
203
+ const output = {};
204
+ for (const [key, item] of Object.entries(node)) {
205
+ if (removeCredentials && CREDENTIAL_KEY.test(key)) continue;
206
+ Object.defineProperty(output, key, {
207
+ value: visit(item, `${pointer}/${escapePointer(key)}`),
208
+ enumerable: true,
209
+ writable: true,
210
+ configurable: true
211
+ });
212
+ }
213
+ return output;
214
+ }
215
+ const cleaned = visit(value, "");
216
+ if (hasSecretInJson(JSON.stringify(cleaned) ?? "")) throw new RecordUnsafeError("secret-remains");
217
+ return {
218
+ value: cleaned,
219
+ warnings
220
+ };
221
+ }
222
+
223
+ //#endregion
224
+ exports.CREDENTIAL_KEY = CREDENTIAL_KEY;
225
+ exports.REDACTED = REDACTED;
226
+ exports.RecordUnsafeError = RecordUnsafeError;
227
+ exports.knownSecrets = knownSecrets;
228
+ exports.sanitizeRecording = sanitizeRecording;
229
+ exports.scrubUrl = scrubUrl;
230
+ exports.validateSecretValues = validateSecretValues;
231
+ //# sourceMappingURL=record-sanitize.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"record-sanitize.cjs","names":[],"sources":["../src/record-sanitize.ts"],"sourcesContent":["/**\n * Shared record sanitizer (D10, S1-S7).\n *\n * The credential-key and URL rules were moved here from `live-sanitize.ts`,\n * which imports them back unchanged. `sanitizeRecording` applies the generic\n * record rules to any JSON value (for example an `mcpFakes` recording).\n */\n\nexport const CREDENTIAL_KEY =\n /^(?:authorization|proxy-authorization|cookie|set-cookie|x-api-key|api[_-]?key|access[_-]?token|refresh[_-]?token|client[_-]?secret|password|secret|token|sec-websocket-protocol)$/i;\nexport const REDACTED = \"[REDACTED]\";\nexport const MIN_SECRET_LENGTH = 8;\n\n/** Thrown when a recording cannot be made safe (S6). Never carries a value. */\nexport class RecordUnsafeError extends Error {\n constructor(readonly reason: \"secret-in-path\" | \"secret-remains\") {\n super(`unsafe recording: ${reason}`);\n this.name = \"RecordUnsafeError\";\n }\n}\n\nexport function decodeUrlComponent(value: string): string {\n // Tolerate malformed escapes while exposing encoded secrets. Protect literal\n // form separators so the entire component remains one value.\n return new URLSearchParams(`value=${value.replace(/\\+/g, \"%2B\").replace(/&/g, \"%26\")}`).get(\n \"value\",\n )!;\n}\n\n/**\n * S3: scrub one absolute URL string; non-URLs unchanged. `onPathSecret` is\n * called (and must throw) when the decoded path contains a secret.\n */\nexport function scrubUrl(\n value: string,\n hasSecret: (s: string) => boolean,\n onPathSecret: () => never,\n): string {\n // Leave non-URLs and clean URLs byte-for-byte intact.\n if (!/^[a-z][a-z0-9+.-]*:\\/\\//i.test(value)) return value;\n let url: URL;\n try {\n url = new URL(value);\n } catch {\n return value;\n }\n // A path identifies the resource; changing it could change replay semantics.\n if (hasSecret(decodeUrlComponent(url.pathname))) onPathSecret();\n let changed = false;\n if (url.username || url.password) {\n url.username = \"\";\n url.password = \"\";\n changed = true;\n }\n for (const [key, val] of [...url.searchParams]) {\n if (CREDENTIAL_KEY.test(key) || hasSecret(key) || hasSecret(val)) {\n url.searchParams.delete(key);\n changed = true;\n }\n }\n const decodedHash = decodeUrlComponent(url.hash);\n if (\n url.hash &&\n (hasSecret(decodedHash) || /(?:token|secret|password|api[_-]?key)=/i.test(decodedHash))\n ) {\n url.hash = \"\";\n changed = true;\n }\n return changed ? url.href : value;\n}\n\n/** S2 (d) start check: throws a plain Error naming the rule, never the value. */\nexport function validateSecretValues(values: readonly string[]): void {\n for (const value of values) {\n if (typeof value !== \"string\") throw new Error(\"record.secretValues entries must be strings\");\n if (value.length < MIN_SECRET_LENGTH) {\n throw new Error(`record.secretValues entry shorter than ${MIN_SECRET_LENGTH} characters`);\n }\n }\n}\n\n// Recognized API-key header names beyond the credential-key regex\n// (for example `x-goog-api-key`).\nconst API_KEY_HEADER = /(?:^|[-_])api[-_]?key$/i;\n\n/** The value, the token after the scheme, and for `Basic` the decoded user and password. */\nfunction credentialParts(value: string): string[] {\n const parts = [value];\n const match = /^(\\S+)\\s+(\\S.*)$/.exec(value.trim());\n if (match) {\n const token = match[2].trim();\n parts.push(token);\n if (match[1].toLowerCase() === \"basic\") {\n const decoded = Buffer.from(token, \"base64\").toString(\"utf8\");\n const colon = decoded.indexOf(\":\");\n if (colon >= 0) parts.push(decoded.slice(0, colon), decoded.slice(colon + 1));\n }\n }\n return parts;\n}\n\nfunction urlParts(value: string): string[] {\n let url: URL;\n try {\n url = new URL(value);\n } catch {\n return [];\n }\n const parts: string[] = [];\n if (url.username) parts.push(url.username, decodeUrlComponent(url.username));\n if (url.password) parts.push(url.password, decodeUrlComponent(url.password));\n // Every query value; path segments are names, so they are not included (S2 c).\n for (const val of url.searchParams.values()) parts.push(val);\n return parts;\n}\n\n/** S2 (a)-(c): secrets from forwarded headers, upstreamAuth and the upstream URL; min length 8. */\nexport function knownSecrets(input: {\n headers: Record<string, string>;\n upstreamAuth?: { name: string; value: string };\n upstreamUrl: string;\n secretValues: readonly string[];\n}): string[] {\n const derived: string[] = [];\n for (const [name, value] of Object.entries(input.headers)) {\n if (typeof value !== \"string\") continue;\n if (CREDENTIAL_KEY.test(name) || API_KEY_HEADER.test(name)) {\n derived.push(...credentialParts(value));\n }\n }\n if (input.upstreamAuth) derived.push(...credentialParts(input.upstreamAuth.value));\n derived.push(...urlParts(input.upstreamUrl));\n const secrets = new Set(derived.filter((s) => s.length >= MIN_SECRET_LENGTH));\n // (d) is validated at start (`validateSecretValues`); keep every non-empty entry.\n for (const value of input.secretValues) if (value.length) secrets.add(value);\n return [...secrets].sort((a, b) => b.length - a.length);\n}\n\n/** `%2b` for `%2B`: some encoders write lowercase hex. */\nfunction lowerHex(value: string): string {\n return value.replace(/%[0-9A-F]{2}/g, (m) => m.toLowerCase());\n}\n\n/** `f(value)`, or nothing when it throws (a lone surrogate, a bad escape). */\nfunction attempt(f: (value: string) => string, value: string): string[] {\n try {\n return [f(value)];\n } catch {\n return [];\n }\n}\n\n/**\n * Every textual form a known secret can take in a recording (G2b B1-r2): the\n * value as registered, its percent-decoded forms (it may have been\n * registered encoded, as a URL query value is), and for each of those the\n * `encodeURIComponent`, form (`URLSearchParams`, `+` for space) and\n * `encodeURI` encodings, with uppercase or lowercase hex. A derived form\n * shorter than `MIN_SECRET_LENGTH` is dropped; the value itself never is.\n */\nexport function secretForms(secret: string): string[] {\n const bases = [\n secret,\n ...attempt(decodeURIComponent, secret),\n ...attempt((v) => decodeURIComponent(v.replace(/\\+/g, \" \")), secret),\n ];\n const forms = new Set<string>();\n for (const base of bases) {\n forms.add(base);\n const encoded = [\n ...attempt(encodeURIComponent, base),\n ...attempt((v) => new URLSearchParams([[\"v\", v]]).toString().slice(2), base),\n ...attempt(encodeURI, base),\n ...attempt((v) => encodeURIComponent(v).replace(/%20/g, \"+\"), base),\n ];\n for (const form of encoded) {\n forms.add(form);\n forms.add(lowerHex(form));\n }\n }\n return [...forms].filter((form) => form === secret || form.length >= MIN_SECRET_LENGTH);\n}\n\nexport interface SanitizeResult<T> {\n value: T;\n /** `_warnings` text: \"<pointer>: redacted a known secret\" / \"<pointer>: looks like a <kind> token\" */\n warnings: string[];\n}\n\n// S7: token-shaped strings are reported, never redacted. Order matters:\n// `sk-ant-` is checked before the generic `sk-`.\nconst TOKEN_PATTERNS: readonly { kind: string; pattern: RegExp }[] = [\n { kind: \"Anthropic API key\", pattern: /\\bsk-ant-[A-Za-z0-9_-]{8,}/ },\n { kind: \"OpenAI API key\", pattern: /\\bsk-(?!ant-)[A-Za-z0-9_-]{16,}/ },\n { kind: \"GitHub token\", pattern: /\\b(?:ghp_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,})/ },\n { kind: \"Slack token\", pattern: /\\bxox[abp]-[A-Za-z0-9-]{10,}/ },\n { kind: \"AWS access key\", pattern: /\\bAKIA[0-9A-Z]{16}\\b/ },\n { kind: \"JWT\", pattern: /\\beyJ[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+/ },\n];\n\nfunction escapePointer(key: string): string {\n return key.replace(/~/g, \"~0\").replace(/\\//g, \"~1\");\n}\n\n/**\n * S2-S7 over one JSON value. `isCredentialBranch(pointer)` is called with the\n * pointer of each object and says whether credential-shaped keys directly\n * under it are removed (S4: `/recorded…`, any `…/_meta…`).\n * Throws RecordUnsafeError (S6). The input is never mutated.\n */\nexport function sanitizeRecording<T>(\n value: T,\n secrets: readonly string[],\n isCredentialBranch: (pointer: string) => boolean,\n): SanitizeResult<T> {\n // G2b B1-r2: every form a secret can take (encoded, decoded) is matched.\n const ordered = [...new Set(secrets.filter((s) => s.length).flatMap(secretForms))].sort(\n (a, b) => b.length - a.length,\n );\n const hasSecret = (s: string) => ordered.some((secret) => s.includes(secret));\n // S6 runs over JSON text, where `\"`, `\\` and control characters are escaped.\n const jsonForms = ordered.map((secret) => JSON.stringify(secret).slice(1, -1));\n const hasSecretInJson = (s: string) =>\n hasSecret(s) || jsonForms.some((secret) => s.includes(secret));\n const onPathSecret = (): never => {\n throw new RecordUnsafeError(\"secret-in-path\");\n };\n const warnings: string[] = [];\n\n function visitString(input: string, pointer: string): string {\n const url = scrubUrl(input, hasSecret, onPathSecret);\n if (url !== input) warnings.push(`${pointer}: removed credentials from a URL`);\n let out = url;\n for (const secret of ordered) out = out.split(secret).join(REDACTED);\n if (out !== url) warnings.push(`${pointer}: redacted a known secret`);\n for (const { kind, pattern } of TOKEN_PATTERNS) {\n if (pattern.test(out)) warnings.push(`${pointer}: looks like a ${kind} token`);\n }\n return out;\n }\n\n function visit(node: unknown, pointer: string): unknown {\n if (typeof node === \"string\") return visitString(node, pointer);\n if (Array.isArray(node)) return node.map((item, i) => visit(item, `${pointer}/${i}`));\n if (!node || typeof node !== \"object\") return node;\n const removeCredentials = isCredentialBranch(pointer);\n const output: Record<string, unknown> = {};\n for (const [key, item] of Object.entries(node)) {\n if (removeCredentials && CREDENTIAL_KEY.test(key)) continue;\n // Own data property, so a `__proto__` key never rewires the prototype.\n Object.defineProperty(output, key, {\n value: visit(item, `${pointer}/${escapePointer(key)}`),\n enumerable: true,\n writable: true,\n configurable: true,\n });\n }\n return output;\n }\n\n const cleaned = visit(value, \"\") as T;\n // S6: fail closed if any known secret remains (for example in a key name).\n if (hasSecretInJson(JSON.stringify(cleaned) ?? \"\")) {\n throw new RecordUnsafeError(\"secret-remains\");\n }\n return { value: cleaned, warnings };\n}\n"],"mappings":";;;;;;;;;AAQA,MAAa,iBACX;AACF,MAAa,WAAW;AACxB,MAAa,oBAAoB;;AAGjC,IAAa,oBAAb,cAAuC,MAAM;CAC3C,YAAY,AAAS,QAA6C;AAChE,QAAM,qBAAqB,SAAS;EADjB;AAEnB,OAAK,OAAO;;;AAIhB,SAAgB,mBAAmB,OAAuB;AAGxD,QAAO,IAAI,gBAAgB,SAAS,MAAM,QAAQ,OAAO,MAAM,CAAC,QAAQ,MAAM,MAAM,GAAG,CAAC,IACtF,QACD;;;;;;AAOH,SAAgB,SACd,OACA,WACA,cACQ;AAER,KAAI,CAAC,2BAA2B,KAAK,MAAM,CAAE,QAAO;CACpD,IAAI;AACJ,KAAI;AACF,QAAM,IAAI,IAAI,MAAM;SACd;AACN,SAAO;;AAGT,KAAI,UAAU,mBAAmB,IAAI,SAAS,CAAC,CAAE,eAAc;CAC/D,IAAI,UAAU;AACd,KAAI,IAAI,YAAY,IAAI,UAAU;AAChC,MAAI,WAAW;AACf,MAAI,WAAW;AACf,YAAU;;AAEZ,MAAK,MAAM,CAAC,KAAK,QAAQ,CAAC,GAAG,IAAI,aAAa,CAC5C,KAAI,eAAe,KAAK,IAAI,IAAI,UAAU,IAAI,IAAI,UAAU,IAAI,EAAE;AAChE,MAAI,aAAa,OAAO,IAAI;AAC5B,YAAU;;CAGd,MAAM,cAAc,mBAAmB,IAAI,KAAK;AAChD,KACE,IAAI,SACH,UAAU,YAAY,IAAI,0CAA0C,KAAK,YAAY,GACtF;AACA,MAAI,OAAO;AACX,YAAU;;AAEZ,QAAO,UAAU,IAAI,OAAO;;;AAI9B,SAAgB,qBAAqB,QAAiC;AACpE,MAAK,MAAM,SAAS,QAAQ;AAC1B,MAAI,OAAO,UAAU,SAAU,OAAM,IAAI,MAAM,8CAA8C;AAC7F,MAAI,MAAM,SAAS,kBACjB,OAAM,IAAI,MAAM,0CAA0C,kBAAkB,aAAa;;;AAO/F,MAAM,iBAAiB;;AAGvB,SAAS,gBAAgB,OAAyB;CAChD,MAAM,QAAQ,CAAC,MAAM;CACrB,MAAM,QAAQ,mBAAmB,KAAK,MAAM,MAAM,CAAC;AACnD,KAAI,OAAO;EACT,MAAM,QAAQ,MAAM,GAAG,MAAM;AAC7B,QAAM,KAAK,MAAM;AACjB,MAAI,MAAM,GAAG,aAAa,KAAK,SAAS;GACtC,MAAM,UAAU,OAAO,KAAK,OAAO,SAAS,CAAC,SAAS,OAAO;GAC7D,MAAM,QAAQ,QAAQ,QAAQ,IAAI;AAClC,OAAI,SAAS,EAAG,OAAM,KAAK,QAAQ,MAAM,GAAG,MAAM,EAAE,QAAQ,MAAM,QAAQ,EAAE,CAAC;;;AAGjF,QAAO;;AAGT,SAAS,SAAS,OAAyB;CACzC,IAAI;AACJ,KAAI;AACF,QAAM,IAAI,IAAI,MAAM;SACd;AACN,SAAO,EAAE;;CAEX,MAAM,QAAkB,EAAE;AAC1B,KAAI,IAAI,SAAU,OAAM,KAAK,IAAI,UAAU,mBAAmB,IAAI,SAAS,CAAC;AAC5E,KAAI,IAAI,SAAU,OAAM,KAAK,IAAI,UAAU,mBAAmB,IAAI,SAAS,CAAC;AAE5E,MAAK,MAAM,OAAO,IAAI,aAAa,QAAQ,CAAE,OAAM,KAAK,IAAI;AAC5D,QAAO;;;AAIT,SAAgB,aAAa,OAKhB;CACX,MAAM,UAAoB,EAAE;AAC5B,MAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,MAAM,QAAQ,EAAE;AACzD,MAAI,OAAO,UAAU,SAAU;AAC/B,MAAI,eAAe,KAAK,KAAK,IAAI,eAAe,KAAK,KAAK,CACxD,SAAQ,KAAK,GAAG,gBAAgB,MAAM,CAAC;;AAG3C,KAAI,MAAM,aAAc,SAAQ,KAAK,GAAG,gBAAgB,MAAM,aAAa,MAAM,CAAC;AAClF,SAAQ,KAAK,GAAG,SAAS,MAAM,YAAY,CAAC;CAC5C,MAAM,UAAU,IAAI,IAAI,QAAQ,QAAQ,MAAM,EAAE,UAAU,kBAAkB,CAAC;AAE7E,MAAK,MAAM,SAAS,MAAM,aAAc,KAAI,MAAM,OAAQ,SAAQ,IAAI,MAAM;AAC5E,QAAO,CAAC,GAAG,QAAQ,CAAC,MAAM,GAAG,MAAM,EAAE,SAAS,EAAE,OAAO;;;AAIzD,SAAS,SAAS,OAAuB;AACvC,QAAO,MAAM,QAAQ,kBAAkB,MAAM,EAAE,aAAa,CAAC;;;AAI/D,SAAS,QAAQ,GAA8B,OAAyB;AACtE,KAAI;AACF,SAAO,CAAC,EAAE,MAAM,CAAC;SACX;AACN,SAAO,EAAE;;;;;;;;;;;AAYb,SAAgB,YAAY,QAA0B;CACpD,MAAM,QAAQ;EACZ;EACA,GAAG,QAAQ,oBAAoB,OAAO;EACtC,GAAG,SAAS,MAAM,mBAAmB,EAAE,QAAQ,OAAO,IAAI,CAAC,EAAE,OAAO;EACrE;CACD,MAAM,wBAAQ,IAAI,KAAa;AAC/B,MAAK,MAAM,QAAQ,OAAO;AACxB,QAAM,IAAI,KAAK;EACf,MAAM,UAAU;GACd,GAAG,QAAQ,oBAAoB,KAAK;GACpC,GAAG,SAAS,MAAM,IAAI,gBAAgB,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,MAAM,EAAE,EAAE,KAAK;GAC5E,GAAG,QAAQ,WAAW,KAAK;GAC3B,GAAG,SAAS,MAAM,mBAAmB,EAAE,CAAC,QAAQ,QAAQ,IAAI,EAAE,KAAK;GACpE;AACD,OAAK,MAAM,QAAQ,SAAS;AAC1B,SAAM,IAAI,KAAK;AACf,SAAM,IAAI,SAAS,KAAK,CAAC;;;AAG7B,QAAO,CAAC,GAAG,MAAM,CAAC,QAAQ,SAAS,SAAS,UAAU,KAAK,UAAU,kBAAkB;;AAWzF,MAAM,iBAA+D;CACnE;EAAE,MAAM;EAAqB,SAAS;EAA8B;CACpE;EAAE,MAAM;EAAkB,SAAS;EAAmC;CACtE;EAAE,MAAM;EAAgB,SAAS;EAA2D;CAC5F;EAAE,MAAM;EAAe,SAAS;EAAgC;CAChE;EAAE,MAAM;EAAkB,SAAS;EAAwB;CAC3D;EAAE,MAAM;EAAO,SAAS;EAAuD;CAChF;AAED,SAAS,cAAc,KAAqB;AAC1C,QAAO,IAAI,QAAQ,MAAM,KAAK,CAAC,QAAQ,OAAO,KAAK;;;;;;;;AASrD,SAAgB,kBACd,OACA,SACA,oBACmB;CAEnB,MAAM,UAAU,CAAC,GAAG,IAAI,IAAI,QAAQ,QAAQ,MAAM,EAAE,OAAO,CAAC,QAAQ,YAAY,CAAC,CAAC,CAAC,MAChF,GAAG,MAAM,EAAE,SAAS,EAAE,OACxB;CACD,MAAM,aAAa,MAAc,QAAQ,MAAM,WAAW,EAAE,SAAS,OAAO,CAAC;CAE7E,MAAM,YAAY,QAAQ,KAAK,WAAW,KAAK,UAAU,OAAO,CAAC,MAAM,GAAG,GAAG,CAAC;CAC9E,MAAM,mBAAmB,MACvB,UAAU,EAAE,IAAI,UAAU,MAAM,WAAW,EAAE,SAAS,OAAO,CAAC;CAChE,MAAM,qBAA4B;AAChC,QAAM,IAAI,kBAAkB,iBAAiB;;CAE/C,MAAM,WAAqB,EAAE;CAE7B,SAAS,YAAY,OAAe,SAAyB;EAC3D,MAAM,MAAM,SAAS,OAAO,WAAW,aAAa;AACpD,MAAI,QAAQ,MAAO,UAAS,KAAK,GAAG,QAAQ,kCAAkC;EAC9E,IAAI,MAAM;AACV,OAAK,MAAM,UAAU,QAAS,OAAM,IAAI,MAAM,OAAO,CAAC,KAAK,SAAS;AACpE,MAAI,QAAQ,IAAK,UAAS,KAAK,GAAG,QAAQ,2BAA2B;AACrE,OAAK,MAAM,EAAE,MAAM,aAAa,eAC9B,KAAI,QAAQ,KAAK,IAAI,CAAE,UAAS,KAAK,GAAG,QAAQ,iBAAiB,KAAK,QAAQ;AAEhF,SAAO;;CAGT,SAAS,MAAM,MAAe,SAA0B;AACtD,MAAI,OAAO,SAAS,SAAU,QAAO,YAAY,MAAM,QAAQ;AAC/D,MAAI,MAAM,QAAQ,KAAK,CAAE,QAAO,KAAK,KAAK,MAAM,MAAM,MAAM,MAAM,GAAG,QAAQ,GAAG,IAAI,CAAC;AACrF,MAAI,CAAC,QAAQ,OAAO,SAAS,SAAU,QAAO;EAC9C,MAAM,oBAAoB,mBAAmB,QAAQ;EACrD,MAAM,SAAkC,EAAE;AAC1C,OAAK,MAAM,CAAC,KAAK,SAAS,OAAO,QAAQ,KAAK,EAAE;AAC9C,OAAI,qBAAqB,eAAe,KAAK,IAAI,CAAE;AAEnD,UAAO,eAAe,QAAQ,KAAK;IACjC,OAAO,MAAM,MAAM,GAAG,QAAQ,GAAG,cAAc,IAAI,GAAG;IACtD,YAAY;IACZ,UAAU;IACV,cAAc;IACf,CAAC;;AAEJ,SAAO;;CAGT,MAAM,UAAU,MAAM,OAAO,GAAG;AAEhC,KAAI,gBAAgB,KAAK,UAAU,QAAQ,IAAI,GAAG,CAChD,OAAM,IAAI,kBAAkB,iBAAiB;AAE/C,QAAO;EAAE,OAAO;EAAS;EAAU"}
@@ -0,0 +1,224 @@
1
+ //#region src/record-sanitize.ts
2
+ /**
3
+ * Shared record sanitizer (D10, S1-S7).
4
+ *
5
+ * The credential-key and URL rules were moved here from `live-sanitize.ts`,
6
+ * which imports them back unchanged. `sanitizeRecording` applies the generic
7
+ * record rules to any JSON value (for example an `mcpFakes` recording).
8
+ */
9
+ const CREDENTIAL_KEY = /^(?:authorization|proxy-authorization|cookie|set-cookie|x-api-key|api[_-]?key|access[_-]?token|refresh[_-]?token|client[_-]?secret|password|secret|token|sec-websocket-protocol)$/i;
10
+ const REDACTED = "[REDACTED]";
11
+ const MIN_SECRET_LENGTH = 8;
12
+ /** Thrown when a recording cannot be made safe (S6). Never carries a value. */
13
+ var RecordUnsafeError = class extends Error {
14
+ constructor(reason) {
15
+ super(`unsafe recording: ${reason}`);
16
+ this.reason = reason;
17
+ this.name = "RecordUnsafeError";
18
+ }
19
+ };
20
+ function decodeUrlComponent(value) {
21
+ return new URLSearchParams(`value=${value.replace(/\+/g, "%2B").replace(/&/g, "%26")}`).get("value");
22
+ }
23
+ /**
24
+ * S3: scrub one absolute URL string; non-URLs unchanged. `onPathSecret` is
25
+ * called (and must throw) when the decoded path contains a secret.
26
+ */
27
+ function scrubUrl(value, hasSecret, onPathSecret) {
28
+ if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(value)) return value;
29
+ let url;
30
+ try {
31
+ url = new URL(value);
32
+ } catch {
33
+ return value;
34
+ }
35
+ if (hasSecret(decodeUrlComponent(url.pathname))) onPathSecret();
36
+ let changed = false;
37
+ if (url.username || url.password) {
38
+ url.username = "";
39
+ url.password = "";
40
+ changed = true;
41
+ }
42
+ for (const [key, val] of [...url.searchParams]) if (CREDENTIAL_KEY.test(key) || hasSecret(key) || hasSecret(val)) {
43
+ url.searchParams.delete(key);
44
+ changed = true;
45
+ }
46
+ const decodedHash = decodeUrlComponent(url.hash);
47
+ if (url.hash && (hasSecret(decodedHash) || /(?:token|secret|password|api[_-]?key)=/i.test(decodedHash))) {
48
+ url.hash = "";
49
+ changed = true;
50
+ }
51
+ return changed ? url.href : value;
52
+ }
53
+ /** S2 (d) start check: throws a plain Error naming the rule, never the value. */
54
+ function validateSecretValues(values) {
55
+ for (const value of values) {
56
+ if (typeof value !== "string") throw new Error("record.secretValues entries must be strings");
57
+ if (value.length < MIN_SECRET_LENGTH) throw new Error(`record.secretValues entry shorter than ${MIN_SECRET_LENGTH} characters`);
58
+ }
59
+ }
60
+ const API_KEY_HEADER = /(?:^|[-_])api[-_]?key$/i;
61
+ /** The value, the token after the scheme, and for `Basic` the decoded user and password. */
62
+ function credentialParts(value) {
63
+ const parts = [value];
64
+ const match = /^(\S+)\s+(\S.*)$/.exec(value.trim());
65
+ if (match) {
66
+ const token = match[2].trim();
67
+ parts.push(token);
68
+ if (match[1].toLowerCase() === "basic") {
69
+ const decoded = Buffer.from(token, "base64").toString("utf8");
70
+ const colon = decoded.indexOf(":");
71
+ if (colon >= 0) parts.push(decoded.slice(0, colon), decoded.slice(colon + 1));
72
+ }
73
+ }
74
+ return parts;
75
+ }
76
+ function urlParts(value) {
77
+ let url;
78
+ try {
79
+ url = new URL(value);
80
+ } catch {
81
+ return [];
82
+ }
83
+ const parts = [];
84
+ if (url.username) parts.push(url.username, decodeUrlComponent(url.username));
85
+ if (url.password) parts.push(url.password, decodeUrlComponent(url.password));
86
+ for (const val of url.searchParams.values()) parts.push(val);
87
+ return parts;
88
+ }
89
+ /** S2 (a)-(c): secrets from forwarded headers, upstreamAuth and the upstream URL; min length 8. */
90
+ function knownSecrets(input) {
91
+ const derived = [];
92
+ for (const [name, value] of Object.entries(input.headers)) {
93
+ if (typeof value !== "string") continue;
94
+ if (CREDENTIAL_KEY.test(name) || API_KEY_HEADER.test(name)) derived.push(...credentialParts(value));
95
+ }
96
+ if (input.upstreamAuth) derived.push(...credentialParts(input.upstreamAuth.value));
97
+ derived.push(...urlParts(input.upstreamUrl));
98
+ const secrets = new Set(derived.filter((s) => s.length >= MIN_SECRET_LENGTH));
99
+ for (const value of input.secretValues) if (value.length) secrets.add(value);
100
+ return [...secrets].sort((a, b) => b.length - a.length);
101
+ }
102
+ /** `%2b` for `%2B`: some encoders write lowercase hex. */
103
+ function lowerHex(value) {
104
+ return value.replace(/%[0-9A-F]{2}/g, (m) => m.toLowerCase());
105
+ }
106
+ /** `f(value)`, or nothing when it throws (a lone surrogate, a bad escape). */
107
+ function attempt(f, value) {
108
+ try {
109
+ return [f(value)];
110
+ } catch {
111
+ return [];
112
+ }
113
+ }
114
+ /**
115
+ * Every textual form a known secret can take in a recording (G2b B1-r2): the
116
+ * value as registered, its percent-decoded forms (it may have been
117
+ * registered encoded, as a URL query value is), and for each of those the
118
+ * `encodeURIComponent`, form (`URLSearchParams`, `+` for space) and
119
+ * `encodeURI` encodings, with uppercase or lowercase hex. A derived form
120
+ * shorter than `MIN_SECRET_LENGTH` is dropped; the value itself never is.
121
+ */
122
+ function secretForms(secret) {
123
+ const bases = [
124
+ secret,
125
+ ...attempt(decodeURIComponent, secret),
126
+ ...attempt((v) => decodeURIComponent(v.replace(/\+/g, " ")), secret)
127
+ ];
128
+ const forms = /* @__PURE__ */ new Set();
129
+ for (const base of bases) {
130
+ forms.add(base);
131
+ const encoded = [
132
+ ...attempt(encodeURIComponent, base),
133
+ ...attempt((v) => new URLSearchParams([["v", v]]).toString().slice(2), base),
134
+ ...attempt(encodeURI, base),
135
+ ...attempt((v) => encodeURIComponent(v).replace(/%20/g, "+"), base)
136
+ ];
137
+ for (const form of encoded) {
138
+ forms.add(form);
139
+ forms.add(lowerHex(form));
140
+ }
141
+ }
142
+ return [...forms].filter((form) => form === secret || form.length >= MIN_SECRET_LENGTH);
143
+ }
144
+ const TOKEN_PATTERNS = [
145
+ {
146
+ kind: "Anthropic API key",
147
+ pattern: /\bsk-ant-[A-Za-z0-9_-]{8,}/
148
+ },
149
+ {
150
+ kind: "OpenAI API key",
151
+ pattern: /\bsk-(?!ant-)[A-Za-z0-9_-]{16,}/
152
+ },
153
+ {
154
+ kind: "GitHub token",
155
+ pattern: /\b(?:ghp_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,})/
156
+ },
157
+ {
158
+ kind: "Slack token",
159
+ pattern: /\bxox[abp]-[A-Za-z0-9-]{10,}/
160
+ },
161
+ {
162
+ kind: "AWS access key",
163
+ pattern: /\bAKIA[0-9A-Z]{16}\b/
164
+ },
165
+ {
166
+ kind: "JWT",
167
+ pattern: /\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/
168
+ }
169
+ ];
170
+ function escapePointer(key) {
171
+ return key.replace(/~/g, "~0").replace(/\//g, "~1");
172
+ }
173
+ /**
174
+ * S2-S7 over one JSON value. `isCredentialBranch(pointer)` is called with the
175
+ * pointer of each object and says whether credential-shaped keys directly
176
+ * under it are removed (S4: `/recorded…`, any `…/_meta…`).
177
+ * Throws RecordUnsafeError (S6). The input is never mutated.
178
+ */
179
+ function sanitizeRecording(value, secrets, isCredentialBranch) {
180
+ const ordered = [...new Set(secrets.filter((s) => s.length).flatMap(secretForms))].sort((a, b) => b.length - a.length);
181
+ const hasSecret = (s) => ordered.some((secret) => s.includes(secret));
182
+ const jsonForms = ordered.map((secret) => JSON.stringify(secret).slice(1, -1));
183
+ const hasSecretInJson = (s) => hasSecret(s) || jsonForms.some((secret) => s.includes(secret));
184
+ const onPathSecret = () => {
185
+ throw new RecordUnsafeError("secret-in-path");
186
+ };
187
+ const warnings = [];
188
+ function visitString(input, pointer) {
189
+ const url = scrubUrl(input, hasSecret, onPathSecret);
190
+ if (url !== input) warnings.push(`${pointer}: removed credentials from a URL`);
191
+ let out = url;
192
+ for (const secret of ordered) out = out.split(secret).join(REDACTED);
193
+ if (out !== url) warnings.push(`${pointer}: redacted a known secret`);
194
+ for (const { kind, pattern } of TOKEN_PATTERNS) if (pattern.test(out)) warnings.push(`${pointer}: looks like a ${kind} token`);
195
+ return out;
196
+ }
197
+ function visit(node, pointer) {
198
+ if (typeof node === "string") return visitString(node, pointer);
199
+ if (Array.isArray(node)) return node.map((item, i) => visit(item, `${pointer}/${i}`));
200
+ if (!node || typeof node !== "object") return node;
201
+ const removeCredentials = isCredentialBranch(pointer);
202
+ const output = {};
203
+ for (const [key, item] of Object.entries(node)) {
204
+ if (removeCredentials && CREDENTIAL_KEY.test(key)) continue;
205
+ Object.defineProperty(output, key, {
206
+ value: visit(item, `${pointer}/${escapePointer(key)}`),
207
+ enumerable: true,
208
+ writable: true,
209
+ configurable: true
210
+ });
211
+ }
212
+ return output;
213
+ }
214
+ const cleaned = visit(value, "");
215
+ if (hasSecretInJson(JSON.stringify(cleaned) ?? "")) throw new RecordUnsafeError("secret-remains");
216
+ return {
217
+ value: cleaned,
218
+ warnings
219
+ };
220
+ }
221
+
222
+ //#endregion
223
+ export { CREDENTIAL_KEY, REDACTED, RecordUnsafeError, knownSecrets, sanitizeRecording, scrubUrl, validateSecretValues };
224
+ //# sourceMappingURL=record-sanitize.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"record-sanitize.js","names":[],"sources":["../src/record-sanitize.ts"],"sourcesContent":["/**\n * Shared record sanitizer (D10, S1-S7).\n *\n * The credential-key and URL rules were moved here from `live-sanitize.ts`,\n * which imports them back unchanged. `sanitizeRecording` applies the generic\n * record rules to any JSON value (for example an `mcpFakes` recording).\n */\n\nexport const CREDENTIAL_KEY =\n /^(?:authorization|proxy-authorization|cookie|set-cookie|x-api-key|api[_-]?key|access[_-]?token|refresh[_-]?token|client[_-]?secret|password|secret|token|sec-websocket-protocol)$/i;\nexport const REDACTED = \"[REDACTED]\";\nexport const MIN_SECRET_LENGTH = 8;\n\n/** Thrown when a recording cannot be made safe (S6). Never carries a value. */\nexport class RecordUnsafeError extends Error {\n constructor(readonly reason: \"secret-in-path\" | \"secret-remains\") {\n super(`unsafe recording: ${reason}`);\n this.name = \"RecordUnsafeError\";\n }\n}\n\nexport function decodeUrlComponent(value: string): string {\n // Tolerate malformed escapes while exposing encoded secrets. Protect literal\n // form separators so the entire component remains one value.\n return new URLSearchParams(`value=${value.replace(/\\+/g, \"%2B\").replace(/&/g, \"%26\")}`).get(\n \"value\",\n )!;\n}\n\n/**\n * S3: scrub one absolute URL string; non-URLs unchanged. `onPathSecret` is\n * called (and must throw) when the decoded path contains a secret.\n */\nexport function scrubUrl(\n value: string,\n hasSecret: (s: string) => boolean,\n onPathSecret: () => never,\n): string {\n // Leave non-URLs and clean URLs byte-for-byte intact.\n if (!/^[a-z][a-z0-9+.-]*:\\/\\//i.test(value)) return value;\n let url: URL;\n try {\n url = new URL(value);\n } catch {\n return value;\n }\n // A path identifies the resource; changing it could change replay semantics.\n if (hasSecret(decodeUrlComponent(url.pathname))) onPathSecret();\n let changed = false;\n if (url.username || url.password) {\n url.username = \"\";\n url.password = \"\";\n changed = true;\n }\n for (const [key, val] of [...url.searchParams]) {\n if (CREDENTIAL_KEY.test(key) || hasSecret(key) || hasSecret(val)) {\n url.searchParams.delete(key);\n changed = true;\n }\n }\n const decodedHash = decodeUrlComponent(url.hash);\n if (\n url.hash &&\n (hasSecret(decodedHash) || /(?:token|secret|password|api[_-]?key)=/i.test(decodedHash))\n ) {\n url.hash = \"\";\n changed = true;\n }\n return changed ? url.href : value;\n}\n\n/** S2 (d) start check: throws a plain Error naming the rule, never the value. */\nexport function validateSecretValues(values: readonly string[]): void {\n for (const value of values) {\n if (typeof value !== \"string\") throw new Error(\"record.secretValues entries must be strings\");\n if (value.length < MIN_SECRET_LENGTH) {\n throw new Error(`record.secretValues entry shorter than ${MIN_SECRET_LENGTH} characters`);\n }\n }\n}\n\n// Recognized API-key header names beyond the credential-key regex\n// (for example `x-goog-api-key`).\nconst API_KEY_HEADER = /(?:^|[-_])api[-_]?key$/i;\n\n/** The value, the token after the scheme, and for `Basic` the decoded user and password. */\nfunction credentialParts(value: string): string[] {\n const parts = [value];\n const match = /^(\\S+)\\s+(\\S.*)$/.exec(value.trim());\n if (match) {\n const token = match[2].trim();\n parts.push(token);\n if (match[1].toLowerCase() === \"basic\") {\n const decoded = Buffer.from(token, \"base64\").toString(\"utf8\");\n const colon = decoded.indexOf(\":\");\n if (colon >= 0) parts.push(decoded.slice(0, colon), decoded.slice(colon + 1));\n }\n }\n return parts;\n}\n\nfunction urlParts(value: string): string[] {\n let url: URL;\n try {\n url = new URL(value);\n } catch {\n return [];\n }\n const parts: string[] = [];\n if (url.username) parts.push(url.username, decodeUrlComponent(url.username));\n if (url.password) parts.push(url.password, decodeUrlComponent(url.password));\n // Every query value; path segments are names, so they are not included (S2 c).\n for (const val of url.searchParams.values()) parts.push(val);\n return parts;\n}\n\n/** S2 (a)-(c): secrets from forwarded headers, upstreamAuth and the upstream URL; min length 8. */\nexport function knownSecrets(input: {\n headers: Record<string, string>;\n upstreamAuth?: { name: string; value: string };\n upstreamUrl: string;\n secretValues: readonly string[];\n}): string[] {\n const derived: string[] = [];\n for (const [name, value] of Object.entries(input.headers)) {\n if (typeof value !== \"string\") continue;\n if (CREDENTIAL_KEY.test(name) || API_KEY_HEADER.test(name)) {\n derived.push(...credentialParts(value));\n }\n }\n if (input.upstreamAuth) derived.push(...credentialParts(input.upstreamAuth.value));\n derived.push(...urlParts(input.upstreamUrl));\n const secrets = new Set(derived.filter((s) => s.length >= MIN_SECRET_LENGTH));\n // (d) is validated at start (`validateSecretValues`); keep every non-empty entry.\n for (const value of input.secretValues) if (value.length) secrets.add(value);\n return [...secrets].sort((a, b) => b.length - a.length);\n}\n\n/** `%2b` for `%2B`: some encoders write lowercase hex. */\nfunction lowerHex(value: string): string {\n return value.replace(/%[0-9A-F]{2}/g, (m) => m.toLowerCase());\n}\n\n/** `f(value)`, or nothing when it throws (a lone surrogate, a bad escape). */\nfunction attempt(f: (value: string) => string, value: string): string[] {\n try {\n return [f(value)];\n } catch {\n return [];\n }\n}\n\n/**\n * Every textual form a known secret can take in a recording (G2b B1-r2): the\n * value as registered, its percent-decoded forms (it may have been\n * registered encoded, as a URL query value is), and for each of those the\n * `encodeURIComponent`, form (`URLSearchParams`, `+` for space) and\n * `encodeURI` encodings, with uppercase or lowercase hex. A derived form\n * shorter than `MIN_SECRET_LENGTH` is dropped; the value itself never is.\n */\nexport function secretForms(secret: string): string[] {\n const bases = [\n secret,\n ...attempt(decodeURIComponent, secret),\n ...attempt((v) => decodeURIComponent(v.replace(/\\+/g, \" \")), secret),\n ];\n const forms = new Set<string>();\n for (const base of bases) {\n forms.add(base);\n const encoded = [\n ...attempt(encodeURIComponent, base),\n ...attempt((v) => new URLSearchParams([[\"v\", v]]).toString().slice(2), base),\n ...attempt(encodeURI, base),\n ...attempt((v) => encodeURIComponent(v).replace(/%20/g, \"+\"), base),\n ];\n for (const form of encoded) {\n forms.add(form);\n forms.add(lowerHex(form));\n }\n }\n return [...forms].filter((form) => form === secret || form.length >= MIN_SECRET_LENGTH);\n}\n\nexport interface SanitizeResult<T> {\n value: T;\n /** `_warnings` text: \"<pointer>: redacted a known secret\" / \"<pointer>: looks like a <kind> token\" */\n warnings: string[];\n}\n\n// S7: token-shaped strings are reported, never redacted. Order matters:\n// `sk-ant-` is checked before the generic `sk-`.\nconst TOKEN_PATTERNS: readonly { kind: string; pattern: RegExp }[] = [\n { kind: \"Anthropic API key\", pattern: /\\bsk-ant-[A-Za-z0-9_-]{8,}/ },\n { kind: \"OpenAI API key\", pattern: /\\bsk-(?!ant-)[A-Za-z0-9_-]{16,}/ },\n { kind: \"GitHub token\", pattern: /\\b(?:ghp_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,})/ },\n { kind: \"Slack token\", pattern: /\\bxox[abp]-[A-Za-z0-9-]{10,}/ },\n { kind: \"AWS access key\", pattern: /\\bAKIA[0-9A-Z]{16}\\b/ },\n { kind: \"JWT\", pattern: /\\beyJ[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+/ },\n];\n\nfunction escapePointer(key: string): string {\n return key.replace(/~/g, \"~0\").replace(/\\//g, \"~1\");\n}\n\n/**\n * S2-S7 over one JSON value. `isCredentialBranch(pointer)` is called with the\n * pointer of each object and says whether credential-shaped keys directly\n * under it are removed (S4: `/recorded…`, any `…/_meta…`).\n * Throws RecordUnsafeError (S6). The input is never mutated.\n */\nexport function sanitizeRecording<T>(\n value: T,\n secrets: readonly string[],\n isCredentialBranch: (pointer: string) => boolean,\n): SanitizeResult<T> {\n // G2b B1-r2: every form a secret can take (encoded, decoded) is matched.\n const ordered = [...new Set(secrets.filter((s) => s.length).flatMap(secretForms))].sort(\n (a, b) => b.length - a.length,\n );\n const hasSecret = (s: string) => ordered.some((secret) => s.includes(secret));\n // S6 runs over JSON text, where `\"`, `\\` and control characters are escaped.\n const jsonForms = ordered.map((secret) => JSON.stringify(secret).slice(1, -1));\n const hasSecretInJson = (s: string) =>\n hasSecret(s) || jsonForms.some((secret) => s.includes(secret));\n const onPathSecret = (): never => {\n throw new RecordUnsafeError(\"secret-in-path\");\n };\n const warnings: string[] = [];\n\n function visitString(input: string, pointer: string): string {\n const url = scrubUrl(input, hasSecret, onPathSecret);\n if (url !== input) warnings.push(`${pointer}: removed credentials from a URL`);\n let out = url;\n for (const secret of ordered) out = out.split(secret).join(REDACTED);\n if (out !== url) warnings.push(`${pointer}: redacted a known secret`);\n for (const { kind, pattern } of TOKEN_PATTERNS) {\n if (pattern.test(out)) warnings.push(`${pointer}: looks like a ${kind} token`);\n }\n return out;\n }\n\n function visit(node: unknown, pointer: string): unknown {\n if (typeof node === \"string\") return visitString(node, pointer);\n if (Array.isArray(node)) return node.map((item, i) => visit(item, `${pointer}/${i}`));\n if (!node || typeof node !== \"object\") return node;\n const removeCredentials = isCredentialBranch(pointer);\n const output: Record<string, unknown> = {};\n for (const [key, item] of Object.entries(node)) {\n if (removeCredentials && CREDENTIAL_KEY.test(key)) continue;\n // Own data property, so a `__proto__` key never rewires the prototype.\n Object.defineProperty(output, key, {\n value: visit(item, `${pointer}/${escapePointer(key)}`),\n enumerable: true,\n writable: true,\n configurable: true,\n });\n }\n return output;\n }\n\n const cleaned = visit(value, \"\") as T;\n // S6: fail closed if any known secret remains (for example in a key name).\n if (hasSecretInJson(JSON.stringify(cleaned) ?? \"\")) {\n throw new RecordUnsafeError(\"secret-remains\");\n }\n return { value: cleaned, warnings };\n}\n"],"mappings":";;;;;;;;AAQA,MAAa,iBACX;AACF,MAAa,WAAW;AACxB,MAAa,oBAAoB;;AAGjC,IAAa,oBAAb,cAAuC,MAAM;CAC3C,YAAY,AAAS,QAA6C;AAChE,QAAM,qBAAqB,SAAS;EADjB;AAEnB,OAAK,OAAO;;;AAIhB,SAAgB,mBAAmB,OAAuB;AAGxD,QAAO,IAAI,gBAAgB,SAAS,MAAM,QAAQ,OAAO,MAAM,CAAC,QAAQ,MAAM,MAAM,GAAG,CAAC,IACtF,QACD;;;;;;AAOH,SAAgB,SACd,OACA,WACA,cACQ;AAER,KAAI,CAAC,2BAA2B,KAAK,MAAM,CAAE,QAAO;CACpD,IAAI;AACJ,KAAI;AACF,QAAM,IAAI,IAAI,MAAM;SACd;AACN,SAAO;;AAGT,KAAI,UAAU,mBAAmB,IAAI,SAAS,CAAC,CAAE,eAAc;CAC/D,IAAI,UAAU;AACd,KAAI,IAAI,YAAY,IAAI,UAAU;AAChC,MAAI,WAAW;AACf,MAAI,WAAW;AACf,YAAU;;AAEZ,MAAK,MAAM,CAAC,KAAK,QAAQ,CAAC,GAAG,IAAI,aAAa,CAC5C,KAAI,eAAe,KAAK,IAAI,IAAI,UAAU,IAAI,IAAI,UAAU,IAAI,EAAE;AAChE,MAAI,aAAa,OAAO,IAAI;AAC5B,YAAU;;CAGd,MAAM,cAAc,mBAAmB,IAAI,KAAK;AAChD,KACE,IAAI,SACH,UAAU,YAAY,IAAI,0CAA0C,KAAK,YAAY,GACtF;AACA,MAAI,OAAO;AACX,YAAU;;AAEZ,QAAO,UAAU,IAAI,OAAO;;;AAI9B,SAAgB,qBAAqB,QAAiC;AACpE,MAAK,MAAM,SAAS,QAAQ;AAC1B,MAAI,OAAO,UAAU,SAAU,OAAM,IAAI,MAAM,8CAA8C;AAC7F,MAAI,MAAM,SAAS,kBACjB,OAAM,IAAI,MAAM,0CAA0C,kBAAkB,aAAa;;;AAO/F,MAAM,iBAAiB;;AAGvB,SAAS,gBAAgB,OAAyB;CAChD,MAAM,QAAQ,CAAC,MAAM;CACrB,MAAM,QAAQ,mBAAmB,KAAK,MAAM,MAAM,CAAC;AACnD,KAAI,OAAO;EACT,MAAM,QAAQ,MAAM,GAAG,MAAM;AAC7B,QAAM,KAAK,MAAM;AACjB,MAAI,MAAM,GAAG,aAAa,KAAK,SAAS;GACtC,MAAM,UAAU,OAAO,KAAK,OAAO,SAAS,CAAC,SAAS,OAAO;GAC7D,MAAM,QAAQ,QAAQ,QAAQ,IAAI;AAClC,OAAI,SAAS,EAAG,OAAM,KAAK,QAAQ,MAAM,GAAG,MAAM,EAAE,QAAQ,MAAM,QAAQ,EAAE,CAAC;;;AAGjF,QAAO;;AAGT,SAAS,SAAS,OAAyB;CACzC,IAAI;AACJ,KAAI;AACF,QAAM,IAAI,IAAI,MAAM;SACd;AACN,SAAO,EAAE;;CAEX,MAAM,QAAkB,EAAE;AAC1B,KAAI,IAAI,SAAU,OAAM,KAAK,IAAI,UAAU,mBAAmB,IAAI,SAAS,CAAC;AAC5E,KAAI,IAAI,SAAU,OAAM,KAAK,IAAI,UAAU,mBAAmB,IAAI,SAAS,CAAC;AAE5E,MAAK,MAAM,OAAO,IAAI,aAAa,QAAQ,CAAE,OAAM,KAAK,IAAI;AAC5D,QAAO;;;AAIT,SAAgB,aAAa,OAKhB;CACX,MAAM,UAAoB,EAAE;AAC5B,MAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,MAAM,QAAQ,EAAE;AACzD,MAAI,OAAO,UAAU,SAAU;AAC/B,MAAI,eAAe,KAAK,KAAK,IAAI,eAAe,KAAK,KAAK,CACxD,SAAQ,KAAK,GAAG,gBAAgB,MAAM,CAAC;;AAG3C,KAAI,MAAM,aAAc,SAAQ,KAAK,GAAG,gBAAgB,MAAM,aAAa,MAAM,CAAC;AAClF,SAAQ,KAAK,GAAG,SAAS,MAAM,YAAY,CAAC;CAC5C,MAAM,UAAU,IAAI,IAAI,QAAQ,QAAQ,MAAM,EAAE,UAAU,kBAAkB,CAAC;AAE7E,MAAK,MAAM,SAAS,MAAM,aAAc,KAAI,MAAM,OAAQ,SAAQ,IAAI,MAAM;AAC5E,QAAO,CAAC,GAAG,QAAQ,CAAC,MAAM,GAAG,MAAM,EAAE,SAAS,EAAE,OAAO;;;AAIzD,SAAS,SAAS,OAAuB;AACvC,QAAO,MAAM,QAAQ,kBAAkB,MAAM,EAAE,aAAa,CAAC;;;AAI/D,SAAS,QAAQ,GAA8B,OAAyB;AACtE,KAAI;AACF,SAAO,CAAC,EAAE,MAAM,CAAC;SACX;AACN,SAAO,EAAE;;;;;;;;;;;AAYb,SAAgB,YAAY,QAA0B;CACpD,MAAM,QAAQ;EACZ;EACA,GAAG,QAAQ,oBAAoB,OAAO;EACtC,GAAG,SAAS,MAAM,mBAAmB,EAAE,QAAQ,OAAO,IAAI,CAAC,EAAE,OAAO;EACrE;CACD,MAAM,wBAAQ,IAAI,KAAa;AAC/B,MAAK,MAAM,QAAQ,OAAO;AACxB,QAAM,IAAI,KAAK;EACf,MAAM,UAAU;GACd,GAAG,QAAQ,oBAAoB,KAAK;GACpC,GAAG,SAAS,MAAM,IAAI,gBAAgB,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,MAAM,EAAE,EAAE,KAAK;GAC5E,GAAG,QAAQ,WAAW,KAAK;GAC3B,GAAG,SAAS,MAAM,mBAAmB,EAAE,CAAC,QAAQ,QAAQ,IAAI,EAAE,KAAK;GACpE;AACD,OAAK,MAAM,QAAQ,SAAS;AAC1B,SAAM,IAAI,KAAK;AACf,SAAM,IAAI,SAAS,KAAK,CAAC;;;AAG7B,QAAO,CAAC,GAAG,MAAM,CAAC,QAAQ,SAAS,SAAS,UAAU,KAAK,UAAU,kBAAkB;;AAWzF,MAAM,iBAA+D;CACnE;EAAE,MAAM;EAAqB,SAAS;EAA8B;CACpE;EAAE,MAAM;EAAkB,SAAS;EAAmC;CACtE;EAAE,MAAM;EAAgB,SAAS;EAA2D;CAC5F;EAAE,MAAM;EAAe,SAAS;EAAgC;CAChE;EAAE,MAAM;EAAkB,SAAS;EAAwB;CAC3D;EAAE,MAAM;EAAO,SAAS;EAAuD;CAChF;AAED,SAAS,cAAc,KAAqB;AAC1C,QAAO,IAAI,QAAQ,MAAM,KAAK,CAAC,QAAQ,OAAO,KAAK;;;;;;;;AASrD,SAAgB,kBACd,OACA,SACA,oBACmB;CAEnB,MAAM,UAAU,CAAC,GAAG,IAAI,IAAI,QAAQ,QAAQ,MAAM,EAAE,OAAO,CAAC,QAAQ,YAAY,CAAC,CAAC,CAAC,MAChF,GAAG,MAAM,EAAE,SAAS,EAAE,OACxB;CACD,MAAM,aAAa,MAAc,QAAQ,MAAM,WAAW,EAAE,SAAS,OAAO,CAAC;CAE7E,MAAM,YAAY,QAAQ,KAAK,WAAW,KAAK,UAAU,OAAO,CAAC,MAAM,GAAG,GAAG,CAAC;CAC9E,MAAM,mBAAmB,MACvB,UAAU,EAAE,IAAI,UAAU,MAAM,WAAW,EAAE,SAAS,OAAO,CAAC;CAChE,MAAM,qBAA4B;AAChC,QAAM,IAAI,kBAAkB,iBAAiB;;CAE/C,MAAM,WAAqB,EAAE;CAE7B,SAAS,YAAY,OAAe,SAAyB;EAC3D,MAAM,MAAM,SAAS,OAAO,WAAW,aAAa;AACpD,MAAI,QAAQ,MAAO,UAAS,KAAK,GAAG,QAAQ,kCAAkC;EAC9E,IAAI,MAAM;AACV,OAAK,MAAM,UAAU,QAAS,OAAM,IAAI,MAAM,OAAO,CAAC,KAAK,SAAS;AACpE,MAAI,QAAQ,IAAK,UAAS,KAAK,GAAG,QAAQ,2BAA2B;AACrE,OAAK,MAAM,EAAE,MAAM,aAAa,eAC9B,KAAI,QAAQ,KAAK,IAAI,CAAE,UAAS,KAAK,GAAG,QAAQ,iBAAiB,KAAK,QAAQ;AAEhF,SAAO;;CAGT,SAAS,MAAM,MAAe,SAA0B;AACtD,MAAI,OAAO,SAAS,SAAU,QAAO,YAAY,MAAM,QAAQ;AAC/D,MAAI,MAAM,QAAQ,KAAK,CAAE,QAAO,KAAK,KAAK,MAAM,MAAM,MAAM,MAAM,GAAG,QAAQ,GAAG,IAAI,CAAC;AACrF,MAAI,CAAC,QAAQ,OAAO,SAAS,SAAU,QAAO;EAC9C,MAAM,oBAAoB,mBAAmB,QAAQ;EACrD,MAAM,SAAkC,EAAE;AAC1C,OAAK,MAAM,CAAC,KAAK,SAAS,OAAO,QAAQ,KAAK,EAAE;AAC9C,OAAI,qBAAqB,eAAe,KAAK,IAAI,CAAE;AAEnD,UAAO,eAAe,QAAQ,KAAK;IACjC,OAAO,MAAM,MAAM,GAAG,QAAQ,GAAG,cAAc,IAAI,GAAG;IACtD,YAAY;IACZ,UAAU;IACV,cAAc;IACf,CAAC;;AAEJ,SAAO;;CAGT,MAAM,UAAU,MAAM,OAAO,GAAG;AAEhC,KAAI,gBAAgB,KAAK,UAAU,QAAQ,IAAI,GAAG,CAChD,OAAM,IAAI,kBAAkB,iBAAiB;AAE/C,QAAO;EAAE,OAAO;EAAS;EAAU"}