@copilotkit/aimock 1.44.0 → 1.45.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 (272) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +31 -0
  4. package/README.md +6 -0
  5. package/dist/agui-types.d.ts.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 +209 -16
  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 +207 -17
  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/record-sanitize.cjs +231 -0
  181. package/dist/record-sanitize.cjs.map +1 -0
  182. package/dist/record-sanitize.js +224 -0
  183. package/dist/record-sanitize.js.map +1 -0
  184. package/dist/recorder.cjs +106 -5
  185. package/dist/recorder.cjs.map +1 -1
  186. package/dist/recorder.d.cts +2 -1
  187. package/dist/recorder.d.cts.map +1 -1
  188. package/dist/recorder.d.ts +2 -1
  189. package/dist/recorder.d.ts.map +1 -1
  190. package/dist/recorder.js +107 -7
  191. package/dist/recorder.js.map +1 -1
  192. package/dist/responses.cjs +667 -171
  193. package/dist/responses.cjs.map +1 -1
  194. package/dist/responses.d.cts +5 -13
  195. package/dist/responses.d.cts.map +1 -1
  196. package/dist/responses.d.ts +5 -13
  197. package/dist/responses.d.ts.map +1 -1
  198. package/dist/responses.js +665 -174
  199. package/dist/responses.js.map +1 -1
  200. package/dist/router.cjs +24 -2
  201. package/dist/router.cjs.map +1 -1
  202. package/dist/router.d.cts.map +1 -1
  203. package/dist/router.d.ts.map +1 -1
  204. package/dist/router.js +25 -4
  205. package/dist/router.js.map +1 -1
  206. package/dist/server.cjs +572 -76
  207. package/dist/server.cjs.map +1 -1
  208. package/dist/server.d.cts.map +1 -1
  209. package/dist/server.d.ts.map +1 -1
  210. package/dist/server.js +574 -78
  211. package/dist/server.js.map +1 -1
  212. package/dist/stream-collapse.cjs +236 -34
  213. package/dist/stream-collapse.cjs.map +1 -1
  214. package/dist/stream-collapse.d.cts +39 -4
  215. package/dist/stream-collapse.d.cts.map +1 -1
  216. package/dist/stream-collapse.d.ts +39 -4
  217. package/dist/stream-collapse.d.ts.map +1 -1
  218. package/dist/stream-collapse.js +235 -35
  219. package/dist/stream-collapse.js.map +1 -1
  220. package/dist/types.cjs.map +1 -1
  221. package/dist/types.d.cts +315 -4
  222. package/dist/types.d.cts.map +1 -1
  223. package/dist/types.d.ts +315 -4
  224. package/dist/types.d.ts.map +1 -1
  225. package/dist/types.js.map +1 -1
  226. package/dist/validate-cli.cjs +36 -4
  227. package/dist/validate-cli.cjs.map +1 -1
  228. package/dist/validate-cli.js +37 -5
  229. package/dist/validate-cli.js.map +1 -1
  230. package/dist/vector-types.d.ts.map +1 -1
  231. package/dist/version.cjs +45 -0
  232. package/dist/version.cjs.map +1 -0
  233. package/dist/version.js +44 -0
  234. package/dist/version.js.map +1 -0
  235. package/dist/vitest.cjs +66 -10
  236. package/dist/vitest.cjs.map +1 -1
  237. package/dist/vitest.d.cts +35 -4
  238. package/dist/vitest.d.cts.map +1 -1
  239. package/dist/vitest.d.ts +35 -4
  240. package/dist/vitest.d.ts.map +1 -1
  241. package/dist/vitest.js +67 -11
  242. package/dist/vitest.js.map +1 -1
  243. package/dist/watcher.cjs +6 -2
  244. package/dist/watcher.cjs.map +1 -1
  245. package/dist/watcher.js +6 -2
  246. package/dist/watcher.js.map +1 -1
  247. package/dist/ws-gemini-live.cjs +210 -101
  248. package/dist/ws-gemini-live.cjs.map +1 -1
  249. package/dist/ws-gemini-live.d.cts +9 -3
  250. package/dist/ws-gemini-live.d.cts.map +1 -1
  251. package/dist/ws-gemini-live.d.ts +9 -3
  252. package/dist/ws-gemini-live.d.ts.map +1 -1
  253. package/dist/ws-gemini-live.js +211 -102
  254. package/dist/ws-gemini-live.js.map +1 -1
  255. package/dist/ws-realtime.cjs +175 -62
  256. package/dist/ws-realtime.cjs.map +1 -1
  257. package/dist/ws-realtime.d.cts +8 -3
  258. package/dist/ws-realtime.d.cts.map +1 -1
  259. package/dist/ws-realtime.d.ts +8 -3
  260. package/dist/ws-realtime.d.ts.map +1 -1
  261. package/dist/ws-realtime.js +176 -63
  262. package/dist/ws-realtime.js.map +1 -1
  263. package/dist/ws-responses.cjs +109 -16
  264. package/dist/ws-responses.cjs.map +1 -1
  265. package/dist/ws-responses.d.cts +8 -12
  266. package/dist/ws-responses.d.cts.map +1 -1
  267. package/dist/ws-responses.d.ts +8 -12
  268. package/dist/ws-responses.d.ts.map +1 -1
  269. package/dist/ws-responses.js +111 -18
  270. package/dist/ws-responses.js.map +1 -1
  271. package/package.json +10 -2
  272. package/skills/write-fixtures/SKILL.md +179 -21
package/dist/journal.js CHANGED
@@ -76,12 +76,43 @@ function systemMessageEqual(a, b) {
76
76
  * (ignoring sequenceIndex). Used to group sequenced fixtures.
77
77
  */
78
78
  function matchCriteriaEqual(a, b) {
79
- return fieldEqual(a.userMessage, b.userMessage) && systemMessageEqual(a.systemMessage, b.systemMessage) && fieldEqual(a.inputText, b.inputText) && fieldEqual(a.toolCallId, b.toolCallId) && fieldEqual(a.toolName, b.toolName) && fieldEqual(a.model, b.model) && fieldEqual(a.responseFormat, b.responseFormat) && fieldEqual(a.predicate, b.predicate) && fieldEqual(a.endpoint, b.endpoint) && fieldEqual(a.turnIndex, b.turnIndex) && fieldEqual(a.hasToolResult, b.hasToolResult) && fieldEqual(a.toolResultContains, b.toolResultContains) && fieldEqual(a.context, b.context);
79
+ return fieldEqual(a.userMessage, b.userMessage) && systemMessageEqual(a.systemMessage, b.systemMessage) && fieldEqual(a.inputText, b.inputText) && fieldEqual(a.toolCallId, b.toolCallId) && fieldEqual(a.toolName, b.toolName) && fieldEqual(a.toolNamespace, b.toolNamespace) && fieldEqual(a.model, b.model) && fieldEqual(a.responseFormat, b.responseFormat) && fieldEqual(a.predicate, b.predicate) && fieldEqual(a.endpoint, b.endpoint) && fieldEqual(a.turnIndex, b.turnIndex) && fieldEqual(a.hasToolResult, b.hasToolResult) && fieldEqual(a.toolResultContains, b.toolResultContains) && fieldEqual(a.context, b.context);
80
80
  }
81
+ /** Count-map keys resolve through `identity`, so every alias of a fixture finds its caller entry. */
82
+ var FixtureCountMap = class extends Map {
83
+ constructor(identity) {
84
+ super();
85
+ this.identity = identity;
86
+ }
87
+ get(key) {
88
+ return super.get(this.identity(key));
89
+ }
90
+ has(key) {
91
+ return super.has(this.identity(key));
92
+ }
93
+ set(key, value) {
94
+ return super.set(this.identity(key), value);
95
+ }
96
+ delete(key) {
97
+ return super.delete(this.identity(key));
98
+ }
99
+ };
81
100
  var Journal = class {
82
101
  entries = [];
83
102
  matchedFixtures = /* @__PURE__ */ new WeakMap();
103
+ /** The fixture a request was served from, where the entry names its caller fixture instead. */
104
+ servedFixtures = /* @__PURE__ */ new WeakMap();
84
105
  fixtureMatchCountsByTestId = /* @__PURE__ */ new Map();
106
+ /** An extended server's view of a fixture to the caller's fixture (configureFixtureViewSource). */
107
+ fixtureViewSource = (fixture) => fixture;
108
+ fixtureCountIdentity = (fixture) => fixture;
109
+ /** The key a fixture is counted under: the caller's object behind any view or copy. */
110
+ fixtureCountKey = (fixture) => this.fixtureCountIdentity(this.fixtureViewSource(fixture));
111
+ createFixtureCountMap = () => /* @__PURE__ */ new Map();
112
+ fixtureCountIdentityConfigured = false;
113
+ fixtureViewSourceConfigured = false;
114
+ counterTestIds = /* @__PURE__ */ new Set();
115
+ misbehaviorCountersByTestId = /* @__PURE__ */ new Map();
85
116
  maxEntries;
86
117
  fixtureCountsMaxTestIds;
87
118
  onAdd;
@@ -124,7 +155,16 @@ var Journal = class {
124
155
  ...entry,
125
156
  body: capBody(entry.body)
126
157
  };
127
- if (matchedFixture) this.matchedFixtures.set(full, matchedFixture);
158
+ const fixture = full.response.fixture;
159
+ const source = fixture ? this.fixtureCountKey(fixture) : fixture;
160
+ if (fixture && source !== fixture) {
161
+ full.response = {
162
+ ...full.response,
163
+ fixture: source
164
+ };
165
+ this.servedFixtures.set(full, fixture);
166
+ }
167
+ if (matchedFixture) this.matchedFixtures.set(full, this.fixtureCountKey(matchedFixture));
128
168
  this.entries.push(full);
129
169
  if (this.maxEntries > 0 && this.entries.length > this.maxEntries) this.entries.shift();
130
170
  this.onAdd?.(full);
@@ -151,7 +191,16 @@ var Journal = class {
151
191
  return this.entries.length > 0 ? this.entries[this.entries.length - 1] : null;
152
192
  }
153
193
  findByFixture(fixture) {
154
- return this.entries.filter((e) => (this.matchedFixtures.get(e) ?? e.response.fixture) === fixture);
194
+ const key = this.fixtureCountKey(fixture);
195
+ return this.entries.filter((e) => (this.matchedFixtures.get(e) ?? e.response.fixture) === key);
196
+ }
197
+ /**
198
+ * @internal The fixture `entry`'s request was served from: the extended
199
+ * view or the per-addition copy when the entry names the caller's fixture
200
+ * behind it, otherwise `entry.response.fixture`. The journal JSON shows it.
201
+ */
202
+ servedFixture(entry) {
203
+ return this.servedFixtures.get(entry) ?? entry.response.fixture;
155
204
  }
156
205
  /**
157
206
  * READ-ONLY accessor. Returns the existing count map for `testId`, or an
@@ -161,7 +210,7 @@ var Journal = class {
161
210
  * `getOrCreateFixtureMatchCountsForTest`.
162
211
  */
163
212
  getFixtureMatchCountsForTest(testId) {
164
- return this.fixtureMatchCountsByTestId.get(testId) ?? /* @__PURE__ */ new Map();
213
+ return this.fixtureMatchCountsByTestId.get(testId) ?? this.createFixtureCountMap();
165
214
  }
166
215
  /**
167
216
  * WRITE path: get the count map for `testId`, inserting a fresh empty Map
@@ -172,15 +221,83 @@ var Journal = class {
172
221
  getOrCreateFixtureMatchCountsForTest(testId) {
173
222
  let counts = this.fixtureMatchCountsByTestId.get(testId);
174
223
  if (!counts) {
175
- counts = /* @__PURE__ */ new Map();
224
+ counts = this.createFixtureCountMap();
176
225
  this.fixtureMatchCountsByTestId.set(testId, counts);
177
- if (this.fixtureCountsMaxTestIds > 0 && this.fixtureMatchCountsByTestId.size > this.fixtureCountsMaxTestIds) {
178
- const oldest = this.fixtureMatchCountsByTestId.keys().next().value;
179
- if (oldest !== void 0) this.fixtureMatchCountsByTestId.delete(oldest);
180
- }
226
+ this.admitCounterTestId(testId);
181
227
  }
182
228
  return counts;
183
229
  }
230
+ /** Match and fault mutations share one admission order and capacity. */
231
+ admitCounterTestId(testId) {
232
+ this.counterTestIds.add(testId);
233
+ if (this.fixtureCountsMaxTestIds > 0 && this.counterTestIds.size > this.fixtureCountsMaxTestIds) {
234
+ const oldest = this.counterTestIds.values().next().value;
235
+ if (oldest !== void 0) {
236
+ this.counterTestIds.delete(oldest);
237
+ this.fixtureMatchCountsByTestId.delete(oldest);
238
+ this.misbehaviorCountersByTestId.delete(oldest);
239
+ }
240
+ }
241
+ }
242
+ getOrCreateMisbehaviorCounter(key) {
243
+ let sources = this.misbehaviorCountersByTestId.get(key.testId);
244
+ if (!sources) {
245
+ sources = /* @__PURE__ */ new Map();
246
+ this.misbehaviorCountersByTestId.set(key.testId, sources);
247
+ this.admitCounterTestId(key.testId);
248
+ }
249
+ let entries = sources.get(key.sourceKey);
250
+ if (!entries) {
251
+ entries = /* @__PURE__ */ new Map();
252
+ sources.set(key.sourceKey, entries);
253
+ }
254
+ let counter = entries.get(key.entryIndex);
255
+ if (!counter) {
256
+ counter = {
257
+ ordinal: 0,
258
+ firings: 0
259
+ };
260
+ entries.set(key.entryIndex, counter);
261
+ }
262
+ return counter;
263
+ }
264
+ getFiringCount(key) {
265
+ return this.misbehaviorCountersByTestId.get(key.testId)?.get(key.sourceKey)?.get(key.entryIndex)?.firings ?? 0;
266
+ }
267
+ nextOrdinal(key) {
268
+ return this.getOrCreateMisbehaviorCounter(key).ordinal++;
269
+ }
270
+ recordFiring(key) {
271
+ this.getOrCreateMisbehaviorCounter(key).firings++;
272
+ }
273
+ clearMisbehaviorCounters(testId) {
274
+ if (testId !== void 0) {
275
+ this.misbehaviorCountersByTestId.delete(testId);
276
+ if (!this.fixtureMatchCountsByTestId.has(testId)) this.counterTestIds.delete(testId);
277
+ } else {
278
+ this.misbehaviorCountersByTestId.clear();
279
+ for (const id of this.counterTestIds) if (!this.fixtureMatchCountsByTestId.has(id)) this.counterTestIds.delete(id);
280
+ }
281
+ }
282
+ /** @internal LLMock configures caller count identity before serving requests. */
283
+ configureFixtureCountIdentity(identity) {
284
+ if (this.fixtureCountIdentityConfigured || this.fixtureMatchCountsByTestId.size > 0) throw new Error("Fixture count identity must be configured once before counting");
285
+ this.fixtureCountIdentity = identity;
286
+ this.createFixtureCountMap = () => new FixtureCountMap(this.fixtureCountKey);
287
+ this.fixtureCountIdentityConfigured = true;
288
+ }
289
+ /**
290
+ * @internal A server with `responsesTools: "extended"` matches each fixture
291
+ * through its extended view. `source` maps a view back to the caller's
292
+ * fixture, so counts, `findByFixture` and `JournalEntry.response.fixture`
293
+ * keep the caller's object. Configured once, before counting.
294
+ */
295
+ configureFixtureViewSource(source) {
296
+ if (this.fixtureViewSourceConfigured || this.fixtureMatchCountsByTestId.size > 0) throw new Error("Fixture view source must be configured once before counting");
297
+ this.fixtureViewSource = source;
298
+ this.createFixtureCountMap = () => new FixtureCountMap(this.fixtureCountKey);
299
+ this.fixtureViewSourceConfigured = true;
300
+ }
184
301
  getFixtureMatchCount(fixture, testId = DEFAULT_TEST_ID) {
185
302
  return this.getFixtureMatchCountsForTest(testId).get(fixture) ?? 0;
186
303
  }
@@ -188,14 +305,19 @@ var Journal = class {
188
305
  const counts = this.getOrCreateFixtureMatchCountsForTest(testId);
189
306
  counts.set(fixture, (counts.get(fixture) ?? 0) + 1);
190
307
  if (fixture.match.sequenceIndex !== void 0 && allFixtures) for (const sibling of allFixtures) {
191
- if (sibling === fixture) continue;
308
+ if (this.fixtureCountKey(sibling) === this.fixtureCountKey(fixture)) continue;
192
309
  if (sibling.match.sequenceIndex === void 0) continue;
193
310
  if (matchCriteriaEqual(fixture.match, sibling.match)) counts.set(sibling, (counts.get(sibling) ?? 0) + 1);
194
311
  }
195
312
  }
196
313
  clearMatchCounts(testId) {
197
- if (testId !== void 0) this.fixtureMatchCountsByTestId.delete(testId);
198
- else this.fixtureMatchCountsByTestId.clear();
314
+ if (testId !== void 0) {
315
+ this.fixtureMatchCountsByTestId.delete(testId);
316
+ this.clearMisbehaviorCounters(testId);
317
+ } else {
318
+ this.fixtureMatchCountsByTestId.clear();
319
+ this.clearMisbehaviorCounters();
320
+ }
199
321
  }
200
322
  /**
201
323
  * Clear ONLY the request journal entries, preserving fixture match-counts.
@@ -209,7 +331,7 @@ var Journal = class {
209
331
  }
210
332
  clear() {
211
333
  this.entries = [];
212
- this.fixtureMatchCountsByTestId.clear();
334
+ this.clearMatchCounts();
213
335
  }
214
336
  get size() {
215
337
  return this.entries.length;
@@ -1 +1 @@
1
- {"version":3,"file":"journal.js","names":[],"sources":["../src/journal.ts"],"sourcesContent":["import { generateId } from \"./helpers.js\";\nimport type {\n ChatCompletionRequest,\n Fixture,\n FixtureMatch,\n JournalBody,\n JournalEntry,\n} from \"./types.js\";\nimport { DEFAULT_TEST_ID } from \"./constants.js\";\nexport { DEFAULT_TEST_ID } from \"./constants.js\";\n\n/**\n * Maximum UTF-8 byte length of a serialized request body retained in a\n * journal entry. Bodies whose JSON serialization exceeds this byte size are\n * replaced with a truncation marker.\n *\n * Only the request `body` field is capped here. `headers` and `path` are\n * retained uncapped, but they are naturally bounded by Node's default ~16 KB\n * maximum HTTP header size, so the per-entry overhead beyond the body cap\n * remains small. Combined with the journal's maxEntries limit (default 1000),\n * the total `JSON.stringify(journal.getAll())` output stays well under V8's\n * ~512 MB string limit even at maximum capacity.\n */\nconst JOURNAL_BODY_CAP_BYTES = 64 * 1024; // 64 KB\n\n/**\n * If `body` serializes to more than JOURNAL_BODY_CAP_BYTES (in UTF-8 bytes),\n * replace it with a truncation marker. Returns the original body when within\n * the cap, or null when `body` is null.\n *\n * The gate uses Buffer.byteLength (UTF-8 bytes) rather than .length (UTF-16\n * code units) to match the constant name and to correctly cap multibyte\n * content (e.g. CJK/emoji) whose code-unit count falls under the threshold\n * but whose byte size does not.\n */\nfunction capBody(body: JournalBody | null): JournalBody | null {\n if (body === null) return null;\n const serialized = JSON.stringify(body);\n if (Buffer.byteLength(serialized, \"utf8\") <= JOURNAL_BODY_CAP_BYTES) return body;\n // The marker is not a chat request, and `JournalBody` no longer claims it is\n // — downstream consumers (e.g. GET /journal) treat the body as opaque JSON.\n return {\n __aimock_truncated: true,\n originalByteSize: Buffer.byteLength(serialized, \"utf8\"),\n note: \"body truncated by aimock journal cap (64 KB limit)\",\n };\n}\n\n/**\n * Narrow a journaled body to a chat-completion request.\n *\n * `JournalEntry.body` is a union (see {@link JournalBody}): most entries hold a\n * chat request, but the fine-tuning create payload and this module's own\n * truncation marker do not. Anything reading `messages`, `model` or the rest of\n * the chat shape off a journal entry — inside aimock or in a consumer's test\n * — goes through here, so an entry that is not a chat request fails as a\n * `false` it can assert on rather than as an undefined property three accesses\n * later.\n *\n * `messages` is the discriminator because it is the one required field no\n * other journaled shape carries.\n */\nexport function isChatCompletionBody(\n body: JournalBody | null | undefined,\n): body is ChatCompletionRequest {\n return (\n body !== null &&\n body !== undefined &&\n Array.isArray((body as { messages?: unknown }).messages) &&\n typeof (body as { model?: unknown }).model === \"string\"\n );\n}\n\n/**\n * Compare two field values, handling RegExp by source+flags rather than reference.\n */\nfunction fieldEqual(a: unknown, b: unknown): boolean {\n if (a instanceof RegExp && b instanceof RegExp)\n return a.source === b.source && a.flags === b.flags;\n return a === b;\n}\n\n/**\n * Compare two systemMessage values. Handles string, string[], and RegExp.\n * Both-undefined is treated as equal.\n */\nfunction systemMessageEqual(\n a: string | string[] | RegExp | undefined,\n b: string | string[] | RegExp | undefined,\n): boolean {\n if (a === undefined && b === undefined) return true;\n if (a === undefined || b === undefined) return false;\n if (typeof a === \"string\" && typeof b === \"string\") return a === b;\n if (Array.isArray(a) && Array.isArray(b))\n return a.length === b.length && a.every((v, i) => v === b[i]);\n if (a instanceof RegExp && b instanceof RegExp)\n return a.source === b.source && a.flags === b.flags;\n return false;\n}\n\n/**\n * Check whether two fixture match objects have the same criteria\n * (ignoring sequenceIndex). Used to group sequenced fixtures.\n */\nfunction matchCriteriaEqual(a: FixtureMatch, b: FixtureMatch): boolean {\n return (\n fieldEqual(a.userMessage, b.userMessage) &&\n systemMessageEqual(a.systemMessage, b.systemMessage) &&\n fieldEqual(a.inputText, b.inputText) &&\n fieldEqual(a.toolCallId, b.toolCallId) &&\n fieldEqual(a.toolName, b.toolName) &&\n fieldEqual(a.model, b.model) &&\n fieldEqual(a.responseFormat, b.responseFormat) &&\n fieldEqual(a.predicate, b.predicate) &&\n fieldEqual(a.endpoint, b.endpoint) &&\n fieldEqual(a.turnIndex, b.turnIndex) &&\n fieldEqual(a.hasToolResult, b.hasToolResult) &&\n fieldEqual(a.toolResultContains, b.toolResultContains) &&\n fieldEqual(a.context, b.context)\n );\n}\n\nexport interface JournalOptions {\n /**\n * Maximum number of entries to retain. When exceeded, oldest entries are\n * dropped FIFO. Set to 0 (or omit) for unbounded retention (the historical\n * default — suitable for short-lived test runs only). Negative values are\n * rejected at the CLI parse layer; programmatically they are treated as 0\n * (unbounded) for back-compat.\n *\n * Long-running servers (e.g. mock proxies in CI/demo environments) should\n * always set a finite cap: every request appends an entry holding the\n * request body + headers + fixture reference, and without a cap the\n * journal grows until the process OOMs.\n */\n maxEntries?: number;\n /**\n * Maximum number of unique testIds retained in the fixture match-count\n * map (`fixtureMatchCountsByTestId`). When exceeded, the oldest testId\n * (by first-insertion order) is evicted FIFO. Set to 0 (or omit) for\n * unbounded retention. Negative values are rejected at the CLI parse\n * layer; programmatically they are treated as 0 (unbounded) for\n * back-compat. Without a cap this map can grow over time in long-running\n * servers that see many unique testIds.\n */\n fixtureCountsMaxTestIds?: number;\n /**\n * Called with every entry {@link Journal.add} creates, right after it is\n * recorded. The hook exists so an embedder can attribute an entry to the\n * request that produced it WITHOUT touching each of the ~100 `journal.add`\n * call sites spread across the handler modules: the server binds the entry\n * to the in-flight `IncomingMessage` here, and its error arm then amends\n * that exact object rather than guessing from a caller-supplied header.\n *\n * The entry is already recorded when the hook runs, so a hook that throws\n * does not lose the write — but it does propagate out of `add` into the\n * caller's error path, so a hook must not throw.\n */\n onAdd?: (entry: JournalEntry) => void;\n}\n\nexport class Journal {\n private entries: JournalEntry[] = [];\n private readonly matchedFixtures = new WeakMap<JournalEntry, Fixture>();\n private readonly fixtureMatchCountsByTestId: Map<string, Map<Fixture, number>> = new Map();\n private readonly maxEntries: number;\n private readonly fixtureCountsMaxTestIds: number;\n private readonly onAdd?: (entry: JournalEntry) => void;\n\n constructor(options: JournalOptions = {}) {\n this.onAdd = options.onAdd;\n // Treat 0 or negative as \"unbounded\" to preserve prior behavior when\n // the option is omitted or explicitly disabled.\n const cap = options.maxEntries;\n this.maxEntries = cap !== undefined && cap > 0 ? cap : 0;\n const testIdCap = options.fixtureCountsMaxTestIds;\n this.fixtureCountsMaxTestIds = testIdCap !== undefined && testIdCap > 0 ? testIdCap : 0;\n }\n\n /**\n * The per-test-id cap on fixture match-count maps, for an MCP mount to pass\n * to `McpFakeStore.setMaxTestIds`. Always a non-negative safe integer, `0`\n * meaning unbounded, and never throws, whatever option was passed:\n * omitted, zero, negative or `NaN` is `0`; `Infinity` or a value above\n * `Number.MAX_SAFE_INTEGER` (never reached) is `0`; a fraction of 1 or more\n * is rounded down (`1.5` evicts at 2 ids, so it keeps 1). For these values\n * it is the number of test ids the journal keeps.\n *\n * An option between 0 and 1 is the exception. The journal keeps no test id\n * (each new count map is evicted as soon as it is made, so counts never\n * advance), and a store cap cannot say \"keep none\" because `0` is\n * unbounded. The getter returns `1`, the smallest bounded cap, so a store\n * given it keeps 1 test id where the journal keeps 0.\n */\n get fixtureCountsMaxTestIdsCap(): number {\n const cap = this.fixtureCountsMaxTestIds;\n if (!(cap > 0) || cap > Number.MAX_SAFE_INTEGER) return 0;\n return Math.max(1, Math.floor(cap));\n }\n\n /** Backwards-compatible accessor — returns the default (no testId) count map. */\n get fixtureMatchCounts(): Map<Fixture, number> {\n return this.getFixtureMatchCountsForTest(DEFAULT_TEST_ID);\n }\n\n /** @internal matchedFixture preserves identity when Live supplies safe diagnostics. */\n add(entry: Omit<JournalEntry, \"id\" | \"timestamp\">, matchedFixture?: Fixture): JournalEntry {\n const full: JournalEntry = {\n id: generateId(\"req\"),\n timestamp: Date.now(),\n ...entry,\n body: capBody(entry.body),\n };\n if (matchedFixture) this.matchedFixtures.set(full, matchedFixture);\n this.entries.push(full);\n // FIFO eviction when over capacity. Array.prototype.shift() is O(n)\n // regardless of how many we drop per add; we accept it at small caps\n // (default 1000) because the constant factor is tiny and this runs once\n // per request. For much larger caps, switch to a ring buffer for true\n // O(1) eviction.\n if (this.maxEntries > 0 && this.entries.length > this.maxEntries) {\n this.entries.shift();\n }\n this.onAdd?.(full);\n return full;\n }\n\n /**\n * Return every entry, or with `limit` the most recent `limit` entries.\n * `0` returns `[]` — exactly what `GET /__aimock/journal?limit=0` answers.\n * `slice(-0)` is `slice(0)`, so a `0` used to return the ENTIRE journal; it\n * is special-cased so the library and the HTTP route agree. This is a public\n * library API, so odd limits are tolerated rather than thrown on: a\n * non-finite limit (`NaN`, `Infinity`) means \"no limit\", a fractional limit\n * floors, and any limit at or below `0` asks for nothing.\n */\n getAll(opts?: { limit?: number }): JournalEntry[] {\n if (opts?.limit !== undefined && Number.isFinite(opts.limit)) {\n const limit = Math.floor(opts.limit);\n if (limit <= 0) return [];\n return this.entries.slice(-limit);\n }\n return this.entries.slice();\n }\n\n getLast(): JournalEntry | null {\n return this.entries.length > 0 ? this.entries[this.entries.length - 1] : null;\n }\n\n findByFixture(fixture: Fixture): JournalEntry[] {\n return this.entries.filter(\n (e) => (this.matchedFixtures.get(e) ?? e.response.fixture) === fixture,\n );\n }\n\n /**\n * READ-ONLY accessor. Returns the existing count map for `testId`, or an\n * empty transient Map if none exists. Does NOT insert into the cache and\n * does NOT trigger FIFO eviction — callers may read freely without\n * perturbing cache state. For the write path, see\n * `getOrCreateFixtureMatchCountsForTest`.\n */\n getFixtureMatchCountsForTest(testId: string): Map<Fixture, number> {\n return this.fixtureMatchCountsByTestId.get(testId) ?? new Map();\n }\n\n /**\n * WRITE path: get the count map for `testId`, inserting a fresh empty Map\n * if missing and running FIFO eviction when the testId cap is exceeded.\n * Only callers that intend to mutate the map (e.g. incrementing a count)\n * should use this.\n */\n private getOrCreateFixtureMatchCountsForTest(testId: string): Map<Fixture, number> {\n let counts = this.fixtureMatchCountsByTestId.get(testId);\n if (!counts) {\n counts = new Map();\n this.fixtureMatchCountsByTestId.set(testId, counts);\n // FIFO eviction when over capacity. JS Map preserves insertion order,\n // so the first key returned by keys() is the oldest. Same O(n) shift\n // caveat as `entries`: acceptable at small caps (createServer\n // default 500; a bare Journal is unbounded).\n if (\n this.fixtureCountsMaxTestIds > 0 &&\n this.fixtureMatchCountsByTestId.size > this.fixtureCountsMaxTestIds\n ) {\n const oldest = this.fixtureMatchCountsByTestId.keys().next().value;\n if (oldest !== undefined) {\n this.fixtureMatchCountsByTestId.delete(oldest);\n }\n }\n }\n return counts;\n }\n\n getFixtureMatchCount(fixture: Fixture, testId = DEFAULT_TEST_ID): number {\n return this.getFixtureMatchCountsForTest(testId).get(fixture) ?? 0;\n }\n\n incrementFixtureMatchCount(\n fixture: Fixture,\n allFixtures?: readonly Fixture[],\n testId = DEFAULT_TEST_ID,\n ): void {\n const counts = this.getOrCreateFixtureMatchCountsForTest(testId);\n counts.set(fixture, (counts.get(fixture) ?? 0) + 1);\n // When a sequenced fixture matches, also increment all siblings with matching criteria\n if (fixture.match.sequenceIndex !== undefined && allFixtures) {\n for (const sibling of allFixtures) {\n if (sibling === fixture) continue;\n if (sibling.match.sequenceIndex === undefined) continue;\n if (matchCriteriaEqual(fixture.match, sibling.match)) {\n counts.set(sibling, (counts.get(sibling) ?? 0) + 1);\n }\n }\n }\n }\n\n clearMatchCounts(testId?: string): void {\n if (testId !== undefined) {\n this.fixtureMatchCountsByTestId.delete(testId);\n } else {\n this.fixtureMatchCountsByTestId.clear();\n }\n }\n\n /**\n * Clear ONLY the request journal entries, preserving fixture match-counts.\n * Match-counts are fixture-matching/sequencing state, not journal data, so\n * clearing the journal must not silently rewind sequenced fixtures. Used by\n * `POST /__aimock/reset/journal`. For a full reset (entries + match-counts),\n * use `clear()` instead.\n */\n clearEntries(): void {\n this.entries = [];\n }\n\n clear(): void {\n this.entries = [];\n this.fixtureMatchCountsByTestId.clear();\n }\n\n get size(): number {\n return this.entries.length;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"journal.js","names":[],"sources":["../src/journal.ts"],"sourcesContent":["import { generateId } from \"./helpers.js\";\nimport type {\n ChatCompletionRequest,\n Fixture,\n FixtureMatch,\n JournalBody,\n JournalEntry,\n MisbehaviorCounterKey,\n MisbehaviorCounters,\n} from \"./types.js\";\nimport { DEFAULT_TEST_ID } from \"./constants.js\";\nexport { DEFAULT_TEST_ID } from \"./constants.js\";\n\n/**\n * Maximum UTF-8 byte length of a serialized request body retained in a\n * journal entry. Bodies whose JSON serialization exceeds this byte size are\n * replaced with a truncation marker.\n *\n * Only the request `body` field is capped here. `headers` and `path` are\n * retained uncapped, but they are naturally bounded by Node's default ~16 KB\n * maximum HTTP header size, so the per-entry overhead beyond the body cap\n * remains small. Combined with the journal's maxEntries limit (default 1000),\n * the total `JSON.stringify(journal.getAll())` output stays well under V8's\n * ~512 MB string limit even at maximum capacity.\n */\nconst JOURNAL_BODY_CAP_BYTES = 64 * 1024; // 64 KB\n\n/**\n * If `body` serializes to more than JOURNAL_BODY_CAP_BYTES (in UTF-8 bytes),\n * replace it with a truncation marker. Returns the original body when within\n * the cap, or null when `body` is null.\n *\n * The gate uses Buffer.byteLength (UTF-8 bytes) rather than .length (UTF-16\n * code units) to match the constant name and to correctly cap multibyte\n * content (e.g. CJK/emoji) whose code-unit count falls under the threshold\n * but whose byte size does not.\n */\nfunction capBody(body: JournalBody | null): JournalBody | null {\n if (body === null) return null;\n const serialized = JSON.stringify(body);\n if (Buffer.byteLength(serialized, \"utf8\") <= JOURNAL_BODY_CAP_BYTES) return body;\n // The marker is not a chat request, and `JournalBody` no longer claims it is\n // — downstream consumers (e.g. GET /journal) treat the body as opaque JSON.\n return {\n __aimock_truncated: true,\n originalByteSize: Buffer.byteLength(serialized, \"utf8\"),\n note: \"body truncated by aimock journal cap (64 KB limit)\",\n };\n}\n\n/**\n * Narrow a journaled body to a chat-completion request.\n *\n * `JournalEntry.body` is a union (see {@link JournalBody}): most entries hold a\n * chat request, but the fine-tuning create payload and this module's own\n * truncation marker do not. Anything reading `messages`, `model` or the rest of\n * the chat shape off a journal entry — inside aimock or in a consumer's test\n * — goes through here, so an entry that is not a chat request fails as a\n * `false` it can assert on rather than as an undefined property three accesses\n * later.\n *\n * `messages` is the discriminator because it is the one required field no\n * other journaled shape carries.\n */\nexport function isChatCompletionBody(\n body: JournalBody | null | undefined,\n): body is ChatCompletionRequest {\n return (\n body !== null &&\n body !== undefined &&\n Array.isArray((body as { messages?: unknown }).messages) &&\n typeof (body as { model?: unknown }).model === \"string\"\n );\n}\n\n/**\n * Compare two field values, handling RegExp by source+flags rather than reference.\n */\nfunction fieldEqual(a: unknown, b: unknown): boolean {\n if (a instanceof RegExp && b instanceof RegExp)\n return a.source === b.source && a.flags === b.flags;\n return a === b;\n}\n\n/**\n * Compare two systemMessage values. Handles string, string[], and RegExp.\n * Both-undefined is treated as equal.\n */\nfunction systemMessageEqual(\n a: string | string[] | RegExp | undefined,\n b: string | string[] | RegExp | undefined,\n): boolean {\n if (a === undefined && b === undefined) return true;\n if (a === undefined || b === undefined) return false;\n if (typeof a === \"string\" && typeof b === \"string\") return a === b;\n if (Array.isArray(a) && Array.isArray(b))\n return a.length === b.length && a.every((v, i) => v === b[i]);\n if (a instanceof RegExp && b instanceof RegExp)\n return a.source === b.source && a.flags === b.flags;\n return false;\n}\n\n/**\n * Check whether two fixture match objects have the same criteria\n * (ignoring sequenceIndex). Used to group sequenced fixtures.\n */\nfunction matchCriteriaEqual(a: FixtureMatch, b: FixtureMatch): boolean {\n return (\n fieldEqual(a.userMessage, b.userMessage) &&\n systemMessageEqual(a.systemMessage, b.systemMessage) &&\n fieldEqual(a.inputText, b.inputText) &&\n fieldEqual(a.toolCallId, b.toolCallId) &&\n fieldEqual(a.toolName, b.toolName) &&\n fieldEqual(a.toolNamespace, b.toolNamespace) &&\n fieldEqual(a.model, b.model) &&\n fieldEqual(a.responseFormat, b.responseFormat) &&\n fieldEqual(a.predicate, b.predicate) &&\n fieldEqual(a.endpoint, b.endpoint) &&\n fieldEqual(a.turnIndex, b.turnIndex) &&\n fieldEqual(a.hasToolResult, b.hasToolResult) &&\n fieldEqual(a.toolResultContains, b.toolResultContains) &&\n fieldEqual(a.context, b.context)\n );\n}\n\n/** Count-map keys resolve through `identity`, so every alias of a fixture finds its caller entry. */\nclass FixtureCountMap extends Map<Fixture, number> {\n constructor(private readonly identity: (fixture: Fixture) => Fixture) {\n super();\n }\n\n override get(key: Fixture): number | undefined {\n return super.get(this.identity(key));\n }\n\n override has(key: Fixture): boolean {\n return super.has(this.identity(key));\n }\n\n override set(key: Fixture, value: number): this {\n return super.set(this.identity(key), value);\n }\n\n override delete(key: Fixture): boolean {\n return super.delete(this.identity(key));\n }\n}\n\nexport interface JournalOptions {\n /**\n * Maximum number of entries to retain. When exceeded, oldest entries are\n * dropped FIFO. Set to 0 (or omit) for unbounded retention (the historical\n * default — suitable for short-lived test runs only). Negative values are\n * rejected at the CLI parse layer; programmatically they are treated as 0\n * (unbounded) for back-compat.\n *\n * Long-running servers (e.g. mock proxies in CI/demo environments) should\n * always set a finite cap: every request appends an entry holding the\n * request body + headers + fixture reference, and without a cap the\n * journal grows until the process OOMs.\n */\n maxEntries?: number;\n /**\n * Maximum number of unique testIds retained across fixture match-count\n * and misbehavior counters. When exceeded, the oldest testId\n * (by first-insertion order) is evicted FIFO. Set to 0 (or omit) for\n * unbounded retention. Negative values are rejected at the CLI parse\n * layer; programmatically they are treated as 0 (unbounded) for\n * back-compat. Without a cap this map can grow over time in long-running\n * servers that see many unique testIds.\n */\n fixtureCountsMaxTestIds?: number;\n /**\n * Called with every entry {@link Journal.add} creates, right after it is\n * recorded. The hook exists so an embedder can attribute an entry to the\n * request that produced it WITHOUT touching each of the ~100 `journal.add`\n * call sites spread across the handler modules: the server binds the entry\n * to the in-flight `IncomingMessage` here, and its error arm then amends\n * that exact object rather than guessing from a caller-supplied header.\n *\n * The entry is already recorded when the hook runs, so a hook that throws\n * does not lose the write — but it does propagate out of `add` into the\n * caller's error path, so a hook must not throw.\n */\n onAdd?: (entry: JournalEntry) => void;\n}\n\nexport class Journal implements MisbehaviorCounters {\n private entries: JournalEntry[] = [];\n private readonly matchedFixtures = new WeakMap<JournalEntry, Fixture>();\n /** The fixture a request was served from, where the entry names its caller fixture instead. */\n private readonly servedFixtures = new WeakMap<JournalEntry, Fixture>();\n private readonly fixtureMatchCountsByTestId: Map<string, Map<Fixture, number>> = new Map();\n /** An extended server's view of a fixture to the caller's fixture (configureFixtureViewSource). */\n private fixtureViewSource = (fixture: Fixture): Fixture => fixture;\n private fixtureCountIdentity = (fixture: Fixture): Fixture => fixture;\n /** The key a fixture is counted under: the caller's object behind any view or copy. */\n private readonly fixtureCountKey = (fixture: Fixture): Fixture =>\n this.fixtureCountIdentity(this.fixtureViewSource(fixture));\n private createFixtureCountMap = (): Map<Fixture, number> => new Map();\n private fixtureCountIdentityConfigured = false;\n private fixtureViewSourceConfigured = false;\n private readonly counterTestIds = new Set<string>();\n private readonly misbehaviorCountersByTestId = new Map<\n string,\n Map<string, Map<number, { ordinal: number; firings: number }>>\n >();\n private readonly maxEntries: number;\n private readonly fixtureCountsMaxTestIds: number;\n private readonly onAdd?: (entry: JournalEntry) => void;\n\n constructor(options: JournalOptions = {}) {\n this.onAdd = options.onAdd;\n // Treat 0 or negative as \"unbounded\" to preserve prior behavior when\n // the option is omitted or explicitly disabled.\n const cap = options.maxEntries;\n this.maxEntries = cap !== undefined && cap > 0 ? cap : 0;\n const testIdCap = options.fixtureCountsMaxTestIds;\n this.fixtureCountsMaxTestIds = testIdCap !== undefined && testIdCap > 0 ? testIdCap : 0;\n }\n\n /**\n * The per-test-id cap on fixture match-count maps, for an MCP mount to pass\n * to `McpFakeStore.setMaxTestIds`. Always a non-negative safe integer, `0`\n * meaning unbounded, and never throws, whatever option was passed:\n * omitted, zero, negative or `NaN` is `0`; `Infinity` or a value above\n * `Number.MAX_SAFE_INTEGER` (never reached) is `0`; a fraction of 1 or more\n * is rounded down (`1.5` evicts at 2 ids, so it keeps 1). For these values\n * it is the number of test ids the journal keeps.\n *\n * An option between 0 and 1 is the exception. The journal keeps no test id\n * (each new count map is evicted as soon as it is made, so counts never\n * advance), and a store cap cannot say \"keep none\" because `0` is\n * unbounded. The getter returns `1`, the smallest bounded cap, so a store\n * given it keeps 1 test id where the journal keeps 0.\n */\n get fixtureCountsMaxTestIdsCap(): number {\n const cap = this.fixtureCountsMaxTestIds;\n if (!(cap > 0) || cap > Number.MAX_SAFE_INTEGER) return 0;\n return Math.max(1, Math.floor(cap));\n }\n\n /** Backwards-compatible accessor — returns the default (no testId) count map. */\n get fixtureMatchCounts(): Map<Fixture, number> {\n return this.getFixtureMatchCountsForTest(DEFAULT_TEST_ID);\n }\n\n /** @internal matchedFixture preserves identity when Live supplies safe diagnostics. */\n add(entry: Omit<JournalEntry, \"id\" | \"timestamp\">, matchedFixture?: Fixture): JournalEntry {\n const full: JournalEntry = {\n id: generateId(\"req\"),\n timestamp: Date.now(),\n ...entry,\n body: capBody(entry.body),\n };\n // A request is served from an extended server's view of a fixture, or\n // from LLMock's per-addition copy with misbehavior enabled; the entry\n // names the caller's fixture behind it, as in 1.44.0.\n const fixture = full.response.fixture;\n const source = fixture ? this.fixtureCountKey(fixture) : fixture;\n if (fixture && source !== fixture) {\n full.response = { ...full.response, fixture: source };\n this.servedFixtures.set(full, fixture);\n }\n if (matchedFixture) this.matchedFixtures.set(full, this.fixtureCountKey(matchedFixture));\n this.entries.push(full);\n // FIFO eviction when over capacity. Array.prototype.shift() is O(n)\n // regardless of how many we drop per add; we accept it at small caps\n // (default 1000) because the constant factor is tiny and this runs once\n // per request. For much larger caps, switch to a ring buffer for true\n // O(1) eviction.\n if (this.maxEntries > 0 && this.entries.length > this.maxEntries) {\n this.entries.shift();\n }\n this.onAdd?.(full);\n return full;\n }\n\n /**\n * Return every entry, or with `limit` the most recent `limit` entries.\n * `0` returns `[]` — exactly what `GET /__aimock/journal?limit=0` answers.\n * `slice(-0)` is `slice(0)`, so a `0` used to return the ENTIRE journal; it\n * is special-cased so the library and the HTTP route agree. This is a public\n * library API, so odd limits are tolerated rather than thrown on: a\n * non-finite limit (`NaN`, `Infinity`) means \"no limit\", a fractional limit\n * floors, and any limit at or below `0` asks for nothing.\n */\n getAll(opts?: { limit?: number }): JournalEntry[] {\n if (opts?.limit !== undefined && Number.isFinite(opts.limit)) {\n const limit = Math.floor(opts.limit);\n if (limit <= 0) return [];\n return this.entries.slice(-limit);\n }\n return this.entries.slice();\n }\n\n getLast(): JournalEntry | null {\n return this.entries.length > 0 ? this.entries[this.entries.length - 1] : null;\n }\n\n findByFixture(fixture: Fixture): JournalEntry[] {\n const key = this.fixtureCountKey(fixture);\n return this.entries.filter((e) => (this.matchedFixtures.get(e) ?? e.response.fixture) === key);\n }\n\n /**\n * @internal The fixture `entry`'s request was served from: the extended\n * view or the per-addition copy when the entry names the caller's fixture\n * behind it, otherwise `entry.response.fixture`. The journal JSON shows it.\n */\n servedFixture(entry: JournalEntry): Fixture | null {\n return this.servedFixtures.get(entry) ?? entry.response.fixture;\n }\n\n /**\n * READ-ONLY accessor. Returns the existing count map for `testId`, or an\n * empty transient Map if none exists. Does NOT insert into the cache and\n * does NOT trigger FIFO eviction — callers may read freely without\n * perturbing cache state. For the write path, see\n * `getOrCreateFixtureMatchCountsForTest`.\n */\n getFixtureMatchCountsForTest(testId: string): Map<Fixture, number> {\n return this.fixtureMatchCountsByTestId.get(testId) ?? this.createFixtureCountMap();\n }\n\n /**\n * WRITE path: get the count map for `testId`, inserting a fresh empty Map\n * if missing and running FIFO eviction when the testId cap is exceeded.\n * Only callers that intend to mutate the map (e.g. incrementing a count)\n * should use this.\n */\n private getOrCreateFixtureMatchCountsForTest(testId: string): Map<Fixture, number> {\n let counts = this.fixtureMatchCountsByTestId.get(testId);\n if (!counts) {\n counts = this.createFixtureCountMap();\n this.fixtureMatchCountsByTestId.set(testId, counts);\n this.admitCounterTestId(testId);\n }\n return counts;\n }\n\n /** Match and fault mutations share one admission order and capacity. */\n private admitCounterTestId(testId: string): void {\n this.counterTestIds.add(testId);\n if (\n this.fixtureCountsMaxTestIds > 0 &&\n this.counterTestIds.size > this.fixtureCountsMaxTestIds\n ) {\n const oldest = this.counterTestIds.values().next().value;\n if (oldest !== undefined) {\n this.counterTestIds.delete(oldest);\n this.fixtureMatchCountsByTestId.delete(oldest);\n this.misbehaviorCountersByTestId.delete(oldest);\n }\n }\n }\n\n private getOrCreateMisbehaviorCounter(key: MisbehaviorCounterKey) {\n let sources = this.misbehaviorCountersByTestId.get(key.testId);\n if (!sources) {\n sources = new Map();\n this.misbehaviorCountersByTestId.set(key.testId, sources);\n this.admitCounterTestId(key.testId);\n }\n let entries = sources.get(key.sourceKey);\n if (!entries) {\n entries = new Map();\n sources.set(key.sourceKey, entries);\n }\n let counter = entries.get(key.entryIndex);\n if (!counter) {\n counter = { ordinal: 0, firings: 0 };\n entries.set(key.entryIndex, counter);\n }\n return counter;\n }\n\n getFiringCount(key: MisbehaviorCounterKey): number {\n return (\n this.misbehaviorCountersByTestId.get(key.testId)?.get(key.sourceKey)?.get(key.entryIndex)\n ?.firings ?? 0\n );\n }\n\n nextOrdinal(key: MisbehaviorCounterKey): number {\n return this.getOrCreateMisbehaviorCounter(key).ordinal++;\n }\n\n recordFiring(key: MisbehaviorCounterKey): void {\n this.getOrCreateMisbehaviorCounter(key).firings++;\n }\n\n clearMisbehaviorCounters(testId?: string): void {\n if (testId !== undefined) {\n this.misbehaviorCountersByTestId.delete(testId);\n if (!this.fixtureMatchCountsByTestId.has(testId)) this.counterTestIds.delete(testId);\n } else {\n this.misbehaviorCountersByTestId.clear();\n for (const id of this.counterTestIds) {\n if (!this.fixtureMatchCountsByTestId.has(id)) this.counterTestIds.delete(id);\n }\n }\n }\n\n /** @internal LLMock configures caller count identity before serving requests. */\n configureFixtureCountIdentity(identity: (fixture: Fixture) => Fixture): void {\n if (this.fixtureCountIdentityConfigured || this.fixtureMatchCountsByTestId.size > 0) {\n throw new Error(\"Fixture count identity must be configured once before counting\");\n }\n this.fixtureCountIdentity = identity;\n this.createFixtureCountMap = () => new FixtureCountMap(this.fixtureCountKey);\n this.fixtureCountIdentityConfigured = true;\n }\n\n /**\n * @internal A server with `responsesTools: \"extended\"` matches each fixture\n * through its extended view. `source` maps a view back to the caller's\n * fixture, so counts, `findByFixture` and `JournalEntry.response.fixture`\n * keep the caller's object. Configured once, before counting.\n */\n configureFixtureViewSource(source: (fixture: Fixture) => Fixture): void {\n if (this.fixtureViewSourceConfigured || this.fixtureMatchCountsByTestId.size > 0) {\n throw new Error(\"Fixture view source must be configured once before counting\");\n }\n this.fixtureViewSource = source;\n this.createFixtureCountMap = () => new FixtureCountMap(this.fixtureCountKey);\n this.fixtureViewSourceConfigured = true;\n }\n\n getFixtureMatchCount(fixture: Fixture, testId = DEFAULT_TEST_ID): number {\n return this.getFixtureMatchCountsForTest(testId).get(fixture) ?? 0;\n }\n\n incrementFixtureMatchCount(\n fixture: Fixture,\n allFixtures?: readonly Fixture[],\n testId = DEFAULT_TEST_ID,\n ): void {\n const counts = this.getOrCreateFixtureMatchCountsForTest(testId);\n counts.set(fixture, (counts.get(fixture) ?? 0) + 1);\n // When a sequenced fixture matches, also increment all siblings with matching criteria\n if (fixture.match.sequenceIndex !== undefined && allFixtures) {\n for (const sibling of allFixtures) {\n if (this.fixtureCountKey(sibling) === this.fixtureCountKey(fixture)) continue;\n if (sibling.match.sequenceIndex === undefined) continue;\n if (matchCriteriaEqual(fixture.match, sibling.match)) {\n counts.set(sibling, (counts.get(sibling) ?? 0) + 1);\n }\n }\n }\n }\n\n clearMatchCounts(testId?: string): void {\n if (testId !== undefined) {\n this.fixtureMatchCountsByTestId.delete(testId);\n this.clearMisbehaviorCounters(testId);\n } else {\n this.fixtureMatchCountsByTestId.clear();\n this.clearMisbehaviorCounters();\n }\n }\n\n /**\n * Clear ONLY the request journal entries, preserving fixture match-counts.\n * Match-counts are fixture-matching/sequencing state, not journal data, so\n * clearing the journal must not silently rewind sequenced fixtures. Used by\n * `POST /__aimock/reset/journal`. For a full reset (entries + match-counts),\n * use `clear()` instead.\n */\n clearEntries(): void {\n this.entries = [];\n }\n\n clear(): void {\n this.entries = [];\n this.clearMatchCounts();\n }\n\n get size(): number {\n return this.entries.length;\n }\n}\n"],"mappings":"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"}
@@ -1,4 +1,5 @@
1
1
  const require_live_fixture = require('./live-fixture.cjs');
2
+ const require_record_sanitize = require('./record-sanitize.cjs');
2
3
 
3
4
  //#region src/live-sanitize.ts
4
5
  var LiveUnsafeExportError = class extends Error {
@@ -8,7 +9,6 @@ var LiveUnsafeExportError = class extends Error {
8
9
  this.name = "LiveUnsafeExportError";
9
10
  }
10
11
  };
11
- const credentialKey = /^(?:authorization|proxy-authorization|cookie|set-cookie|x-api-key|api[_-]?key|access[_-]?token|refresh[_-]?token|client[_-]?secret|password|secret|token|sec-websocket-protocol)$/i;
12
12
  const semanticKey = new Set([
13
13
  "instructions",
14
14
  "content",
@@ -37,9 +37,6 @@ const redacted = "[REDACTED]";
37
37
  function fail() {
38
38
  throw new LiveUnsafeExportError();
39
39
  }
40
- function decodeUrlComponent(value) {
41
- return new URLSearchParams(`value=${value.replace(/\+/g, "%2B").replace(/&/g, "%26")}`).get("value");
42
- }
43
40
  function escapePointer(key) {
44
41
  return key.replace(/~/g, "~0").replace(/\//g, "~1");
45
42
  }
@@ -111,32 +108,7 @@ function sanitizeLiveTranscript(transcript, secretValues = [], credentialFields
111
108
  setIdentifier(result.entries[binding.entry].event, binding.pointer, identity.value);
112
109
  binding.name = identity.name;
113
110
  }
114
- function sanitizeUrl(value) {
115
- if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(value)) return value;
116
- let url;
117
- try {
118
- url = new URL(value);
119
- } catch {
120
- return value;
121
- }
122
- if (hasSecret(decodeUrlComponent(url.pathname))) fail();
123
- let changed = false;
124
- if (url.username || url.password) {
125
- url.username = "";
126
- url.password = "";
127
- changed = true;
128
- }
129
- for (const [key, val] of [...url.searchParams]) if (credentialKey.test(key) || hasSecret(key) || hasSecret(val)) {
130
- url.searchParams.delete(key);
131
- changed = true;
132
- }
133
- const decodedHash = decodeUrlComponent(url.hash);
134
- if (url.hash && (hasSecret(decodedHash) || /(?:token|secret|password|api[_-]?key)=/i.test(decodedHash))) {
135
- url.hash = "";
136
- changed = true;
137
- }
138
- return changed ? url.href : value;
139
- }
111
+ const sanitizeUrl = (v) => require_record_sanitize.scrubUrl(v, hasSecret, fail);
140
112
  function providerMetadata(pointer) {
141
113
  const match = /^\/entries\/(\d+)\/event\/event\/(?:response\/(?:user|prompt_cache_key|safety_identifier)|item\/encrypted_content)$/.exec(pointer);
142
114
  if (!match) return false;
@@ -168,13 +140,13 @@ function sanitizeLiveTranscript(transcript, secretValues = [], credentialFields
168
140
  continue;
169
141
  }
170
142
  const protectedBranch = semantic || semanticKey.has(key);
171
- const removable = !protectedBranch && (metadata || credentialKey.test(key));
143
+ const removable = !protectedBranch && (metadata || require_record_sanitize.CREDENTIAL_KEY.test(key));
172
144
  if (descriptors.has(pointer)) {
173
145
  if (!removable) fail();
174
146
  handled.add(pointer);
175
147
  continue;
176
148
  }
177
- if (!semantic && credentialKey.test(key)) continue;
149
+ if (!semantic && require_record_sanitize.CREDENTIAL_KEY.test(key)) continue;
178
150
  if (hasSecret(key)) {
179
151
  if (!metadata || protectedBranch) fail();
180
152
  continue;
@@ -1 +1 @@
1
- {"version":3,"file":"live-sanitize.cjs","names":["validateLiveTranscript"],"sources":["../src/live-sanitize.ts"],"sourcesContent":["import { validateLiveTranscript } from \"./live-fixture.js\";\nimport type { LiveJson, LiveObject, LiveOptions, LiveTranscript } from \"./live-types.js\";\n\n/** Exact transcript-rooted pointer to an observed removable credential metadata field. */\nexport interface LiveCredentialField {\n readonly pointer: string;\n}\n\nexport class LiveUnsafeExportError extends Error {\n readonly category = \"unsafe-export\";\n constructor() {\n // Never reflect untrusted values, keys, pointers, or validation errors.\n super(\"unsafe-export: Live transcript cannot be retained safely\");\n this.name = \"LiveUnsafeExportError\";\n }\n}\n\nconst credentialKey =\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;\n// These branches carry conversation, tool schema, or PCM semantics. A key named\n// \"authorization\" inside tool parameters is a schema property, not a header.\nconst semanticKey = new Set([\n \"instructions\",\n \"content\",\n \"delta\",\n \"audio\",\n \"arguments\",\n \"output\",\n \"input\",\n \"tools\",\n \"parameters\",\n \"client\",\n \"text\",\n \"thinking\",\n]);\nconst metadataKey = new Set([\"metadata\", \"headers\", \"diagnostics\"]);\nconst unsafeKey = new Set([\"__proto__\", \"constructor\", \"prototype\"]);\nconst redacted = \"[REDACTED]\";\nfunction fail(): never {\n throw new LiveUnsafeExportError();\n}\nfunction 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}\nfunction escapePointer(key: string): string {\n return key.replace(/~/g, \"~0\").replace(/\\//g, \"~1\");\n}\nfunction parts(pointer: string): string[] {\n if (!pointer.startsWith(\"/\") || /~(?![01])/u.test(pointer)) fail();\n const keys = pointer\n .slice(1)\n .split(\"/\")\n .map((key) => key.replace(/~1/g, \"/\").replace(/~0/g, \"~\"));\n if (keys.some((key) => unsafeKey.has(key))) fail();\n return keys;\n}\nfunction child(value: LiveJson, key: string): LiveJson {\n if (!value || typeof value !== \"object\" || !Object.hasOwn(value, key)) fail();\n if (Array.isArray(value)) {\n if (!/^(0|[1-9]\\d*)$/.test(key)) fail();\n return value[Number(key)];\n }\n return value[key];\n}\nfunction setIdentifier(event: LiveObject, pointer: string, value: string): void {\n const keys = parts(pointer);\n const last = keys.pop();\n if (last === undefined) fail();\n let parent: LiveJson = event;\n for (const key of keys) parent = child(parent, key);\n if (typeof child(parent, last) !== \"string\" || !parent || typeof parent !== \"object\") fail();\n if (Array.isArray(parent)) parent[Number(last)] = value;\n else parent[last] = value;\n}\n\n/**\n * Sanitize a separately retained successful capture; never mutate the relay.\n * Validation supplies the reviewed identifier descriptors and causal checks.\n * Credential descriptors supplement the built-in policy, not permission to\n * delete arbitrary protocol operands. Ambiguous secret-bearing data fails closed.\n */\nexport function sanitizeLiveTranscript(\n transcript: LiveTranscript,\n secretValues: readonly string[] = [],\n credentialFields: readonly LiveCredentialField[] = [],\n validationOptions: Omit<LiveOptions, \"secretValues\"> = {},\n): LiveTranscript {\n try {\n const result = validateLiveTranscript(transcript, validationOptions);\n if (!Array.isArray(secretValues) || secretValues.some((s) => typeof s !== \"string\")) fail();\n const secrets = [...new Set(secretValues.filter((s) => s.length))].sort(\n (a, b) => b.length - a.length,\n );\n const hasSecret = (value: string) => secrets.some((secret) => value.includes(secret));\n const replaceSecrets = (value: string) => {\n for (const secret of secrets) value = value.split(secret).join(redacted);\n return value;\n };\n const descriptors = new Set(\n credentialFields.map((field) => {\n parts(field.pointer);\n return field.pointer;\n }),\n );\n const handled = new Set<string>();\n // Names themselves may have been derived from provider IDs. Replace both\n // the annotation names and only their explicitly annotated operand values.\n const originalIds = new Set<string>();\n for (const binding of result.bindings) {\n let value: LiveJson = result.entries[binding.entry].event;\n for (const key of parts(binding.pointer)) value = child(value, key);\n if (typeof value !== \"string\") fail();\n originalIds.add(value);\n }\n let nextId = 1;\n const identities = new Map<string, { name: string; value: string }>();\n for (const binding of result.bindings) {\n let identity = identities.get(binding.name);\n if (!identity) {\n const index = identities.size + 1;\n while (originalIds.has(`live-id-${nextId}`)) nextId++;\n identity = { name: `binding-${index}`, value: `live-id-${nextId++}` };\n identities.set(binding.name, identity);\n }\n setIdentifier(result.entries[binding.entry].event, binding.pointer, identity.value);\n binding.name = identity.name;\n }\n function sanitizeUrl(value: string): 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))) fail();\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 (credentialKey.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 function providerMetadata(pointer: string): boolean {\n const match =\n /^\\/entries\\/(\\d+)\\/event\\/event\\/(?:response\\/(?:user|prompt_cache_key|safety_identifier)|item\\/encrypted_content)$/.exec(\n pointer,\n );\n if (!match) return false;\n const event = result.entries[Number(match[1])].event;\n const nested = event.event;\n if (\n event.type !== \"response.event\" ||\n !nested ||\n typeof nested !== \"object\" ||\n Array.isArray(nested)\n )\n return false;\n if (!pointer.endsWith(\"/item/encrypted_content\")) {\n return [\"response.created\", \"response.in_progress\", \"response.completed\"].includes(\n String(nested.type),\n );\n }\n const item = nested.item;\n return (\n [\"response.output_item.added\", \"response.output_item.done\"].includes(String(nested.type)) &&\n !!item &&\n typeof item === \"object\" &&\n !Array.isArray(item) &&\n item.type === \"reasoning\"\n );\n }\n function visit(value: LiveJson, path: string, metadata = false, semantic = false): LiveJson {\n if (typeof value === \"string\") {\n const url = sanitizeUrl(value);\n if ((hasSecret(value) || url !== value) && (!metadata || semantic)) fail();\n return metadata && !semantic ? replaceSecrets(url) : value;\n }\n if (Array.isArray(value))\n return value.map((item, i) => visit(item, `${path}/${i}`, metadata, semantic));\n if (!value || typeof value !== \"object\") return value;\n const output: LiveObject = {};\n for (const [key, item] of Object.entries(value)) {\n const pointer = `${path}/${escapePointer(key)}`;\n // Exact retained metadata sites from the reviewed v3 sanitation\n // evidence. Never infer these by equal values or similar field names.\n if (providerMetadata(pointer) && (typeof item === \"string\" || item === null)) {\n if (descriptors.has(pointer)) handled.add(pointer);\n else output[key] = typeof item === \"string\" ? redacted : item;\n continue;\n }\n const protectedBranch = semantic || semanticKey.has(key);\n const removable = !protectedBranch && (metadata || credentialKey.test(key));\n if (descriptors.has(pointer)) {\n if (!removable) fail();\n handled.add(pointer);\n continue;\n }\n if (!semantic && credentialKey.test(key)) continue;\n if (hasSecret(key)) {\n if (!metadata || protectedBranch) fail();\n continue;\n }\n output[key] = visit(item, pointer, metadata || metadataKey.has(key), protectedBranch);\n }\n return output;\n }\n // Validation clones all bounded JSON, including unknown metadata. Preserve\n // those fields, then validate again to catch any semantic deletion.\n const cleaned = visit(result as LiveTranscript & LiveObject, \"\");\n if (handled.size !== descriptors.size || hasSecret(JSON.stringify(cleaned))) fail();\n return validateLiveTranscript(cleaned, validationOptions);\n } catch {\n fail();\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"live-sanitize.cjs","names":["validateLiveTranscript","scrubUrl","credentialKey"],"sources":["../src/live-sanitize.ts"],"sourcesContent":["import { validateLiveTranscript } from \"./live-fixture.js\";\nimport { CREDENTIAL_KEY as credentialKey, scrubUrl } from \"./record-sanitize.js\";\nimport type { LiveJson, LiveObject, LiveOptions, LiveTranscript } from \"./live-types.js\";\n\n/** Exact transcript-rooted pointer to an observed removable credential metadata field. */\nexport interface LiveCredentialField {\n readonly pointer: string;\n}\n\nexport class LiveUnsafeExportError extends Error {\n readonly category = \"unsafe-export\";\n constructor() {\n // Never reflect untrusted values, keys, pointers, or validation errors.\n super(\"unsafe-export: Live transcript cannot be retained safely\");\n this.name = \"LiveUnsafeExportError\";\n }\n}\n\n// These branches carry conversation, tool schema, or PCM semantics. A key named\n// \"authorization\" inside tool parameters is a schema property, not a header.\nconst semanticKey = new Set([\n \"instructions\",\n \"content\",\n \"delta\",\n \"audio\",\n \"arguments\",\n \"output\",\n \"input\",\n \"tools\",\n \"parameters\",\n \"client\",\n \"text\",\n \"thinking\",\n]);\nconst metadataKey = new Set([\"metadata\", \"headers\", \"diagnostics\"]);\nconst unsafeKey = new Set([\"__proto__\", \"constructor\", \"prototype\"]);\nconst redacted = \"[REDACTED]\";\nfunction fail(): never {\n throw new LiveUnsafeExportError();\n}\nfunction escapePointer(key: string): string {\n return key.replace(/~/g, \"~0\").replace(/\\//g, \"~1\");\n}\nfunction parts(pointer: string): string[] {\n if (!pointer.startsWith(\"/\") || /~(?![01])/u.test(pointer)) fail();\n const keys = pointer\n .slice(1)\n .split(\"/\")\n .map((key) => key.replace(/~1/g, \"/\").replace(/~0/g, \"~\"));\n if (keys.some((key) => unsafeKey.has(key))) fail();\n return keys;\n}\nfunction child(value: LiveJson, key: string): LiveJson {\n if (!value || typeof value !== \"object\" || !Object.hasOwn(value, key)) fail();\n if (Array.isArray(value)) {\n if (!/^(0|[1-9]\\d*)$/.test(key)) fail();\n return value[Number(key)];\n }\n return value[key];\n}\nfunction setIdentifier(event: LiveObject, pointer: string, value: string): void {\n const keys = parts(pointer);\n const last = keys.pop();\n if (last === undefined) fail();\n let parent: LiveJson = event;\n for (const key of keys) parent = child(parent, key);\n if (typeof child(parent, last) !== \"string\" || !parent || typeof parent !== \"object\") fail();\n if (Array.isArray(parent)) parent[Number(last)] = value;\n else parent[last] = value;\n}\n\n/**\n * Sanitize a separately retained successful capture; never mutate the relay.\n * Validation supplies the reviewed identifier descriptors and causal checks.\n * Credential descriptors supplement the built-in policy, not permission to\n * delete arbitrary protocol operands. Ambiguous secret-bearing data fails closed.\n */\nexport function sanitizeLiveTranscript(\n transcript: LiveTranscript,\n secretValues: readonly string[] = [],\n credentialFields: readonly LiveCredentialField[] = [],\n validationOptions: Omit<LiveOptions, \"secretValues\"> = {},\n): LiveTranscript {\n try {\n const result = validateLiveTranscript(transcript, validationOptions);\n if (!Array.isArray(secretValues) || secretValues.some((s) => typeof s !== \"string\")) fail();\n const secrets = [...new Set(secretValues.filter((s) => s.length))].sort(\n (a, b) => b.length - a.length,\n );\n const hasSecret = (value: string) => secrets.some((secret) => value.includes(secret));\n const replaceSecrets = (value: string) => {\n for (const secret of secrets) value = value.split(secret).join(redacted);\n return value;\n };\n const descriptors = new Set(\n credentialFields.map((field) => {\n parts(field.pointer);\n return field.pointer;\n }),\n );\n const handled = new Set<string>();\n // Names themselves may have been derived from provider IDs. Replace both\n // the annotation names and only their explicitly annotated operand values.\n const originalIds = new Set<string>();\n for (const binding of result.bindings) {\n let value: LiveJson = result.entries[binding.entry].event;\n for (const key of parts(binding.pointer)) value = child(value, key);\n if (typeof value !== \"string\") fail();\n originalIds.add(value);\n }\n let nextId = 1;\n const identities = new Map<string, { name: string; value: string }>();\n for (const binding of result.bindings) {\n let identity = identities.get(binding.name);\n if (!identity) {\n const index = identities.size + 1;\n while (originalIds.has(`live-id-${nextId}`)) nextId++;\n identity = { name: `binding-${index}`, value: `live-id-${nextId++}` };\n identities.set(binding.name, identity);\n }\n setIdentifier(result.entries[binding.entry].event, binding.pointer, identity.value);\n binding.name = identity.name;\n }\n const sanitizeUrl = (v: string) => scrubUrl(v, hasSecret, fail);\n function providerMetadata(pointer: string): boolean {\n const match =\n /^\\/entries\\/(\\d+)\\/event\\/event\\/(?:response\\/(?:user|prompt_cache_key|safety_identifier)|item\\/encrypted_content)$/.exec(\n pointer,\n );\n if (!match) return false;\n const event = result.entries[Number(match[1])].event;\n const nested = event.event;\n if (\n event.type !== \"response.event\" ||\n !nested ||\n typeof nested !== \"object\" ||\n Array.isArray(nested)\n )\n return false;\n if (!pointer.endsWith(\"/item/encrypted_content\")) {\n return [\"response.created\", \"response.in_progress\", \"response.completed\"].includes(\n String(nested.type),\n );\n }\n const item = nested.item;\n return (\n [\"response.output_item.added\", \"response.output_item.done\"].includes(String(nested.type)) &&\n !!item &&\n typeof item === \"object\" &&\n !Array.isArray(item) &&\n item.type === \"reasoning\"\n );\n }\n function visit(value: LiveJson, path: string, metadata = false, semantic = false): LiveJson {\n if (typeof value === \"string\") {\n const url = sanitizeUrl(value);\n if ((hasSecret(value) || url !== value) && (!metadata || semantic)) fail();\n return metadata && !semantic ? replaceSecrets(url) : value;\n }\n if (Array.isArray(value))\n return value.map((item, i) => visit(item, `${path}/${i}`, metadata, semantic));\n if (!value || typeof value !== \"object\") return value;\n const output: LiveObject = {};\n for (const [key, item] of Object.entries(value)) {\n const pointer = `${path}/${escapePointer(key)}`;\n // Exact retained metadata sites from the reviewed v3 sanitation\n // evidence. Never infer these by equal values or similar field names.\n if (providerMetadata(pointer) && (typeof item === \"string\" || item === null)) {\n if (descriptors.has(pointer)) handled.add(pointer);\n else output[key] = typeof item === \"string\" ? redacted : item;\n continue;\n }\n const protectedBranch = semantic || semanticKey.has(key);\n const removable = !protectedBranch && (metadata || credentialKey.test(key));\n if (descriptors.has(pointer)) {\n if (!removable) fail();\n handled.add(pointer);\n continue;\n }\n if (!semantic && credentialKey.test(key)) continue;\n if (hasSecret(key)) {\n if (!metadata || protectedBranch) fail();\n continue;\n }\n output[key] = visit(item, pointer, metadata || metadataKey.has(key), protectedBranch);\n }\n return output;\n }\n // Validation clones all bounded JSON, including unknown metadata. Preserve\n // those fields, then validate again to catch any semantic deletion.\n const cleaned = visit(result as LiveTranscript & LiveObject, \"\");\n if (handled.size !== descriptors.size || hasSecret(JSON.stringify(cleaned))) fail();\n return validateLiveTranscript(cleaned, validationOptions);\n } catch {\n fail();\n }\n}\n"],"mappings":"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"}
@@ -1,4 +1,5 @@
1
1
  import { validateLiveTranscript } from "./live-fixture.js";
2
+ import { CREDENTIAL_KEY, scrubUrl } from "./record-sanitize.js";
2
3
 
3
4
  //#region src/live-sanitize.ts
4
5
  var LiveUnsafeExportError = class extends Error {
@@ -8,7 +9,6 @@ var LiveUnsafeExportError = class extends Error {
8
9
  this.name = "LiveUnsafeExportError";
9
10
  }
10
11
  };
11
- const credentialKey = /^(?:authorization|proxy-authorization|cookie|set-cookie|x-api-key|api[_-]?key|access[_-]?token|refresh[_-]?token|client[_-]?secret|password|secret|token|sec-websocket-protocol)$/i;
12
12
  const semanticKey = new Set([
13
13
  "instructions",
14
14
  "content",
@@ -37,9 +37,6 @@ const redacted = "[REDACTED]";
37
37
  function fail() {
38
38
  throw new LiveUnsafeExportError();
39
39
  }
40
- function decodeUrlComponent(value) {
41
- return new URLSearchParams(`value=${value.replace(/\+/g, "%2B").replace(/&/g, "%26")}`).get("value");
42
- }
43
40
  function escapePointer(key) {
44
41
  return key.replace(/~/g, "~0").replace(/\//g, "~1");
45
42
  }
@@ -111,32 +108,7 @@ function sanitizeLiveTranscript(transcript, secretValues = [], credentialFields
111
108
  setIdentifier(result.entries[binding.entry].event, binding.pointer, identity.value);
112
109
  binding.name = identity.name;
113
110
  }
114
- function sanitizeUrl(value) {
115
- if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(value)) return value;
116
- let url;
117
- try {
118
- url = new URL(value);
119
- } catch {
120
- return value;
121
- }
122
- if (hasSecret(decodeUrlComponent(url.pathname))) fail();
123
- let changed = false;
124
- if (url.username || url.password) {
125
- url.username = "";
126
- url.password = "";
127
- changed = true;
128
- }
129
- for (const [key, val] of [...url.searchParams]) if (credentialKey.test(key) || hasSecret(key) || hasSecret(val)) {
130
- url.searchParams.delete(key);
131
- changed = true;
132
- }
133
- const decodedHash = decodeUrlComponent(url.hash);
134
- if (url.hash && (hasSecret(decodedHash) || /(?:token|secret|password|api[_-]?key)=/i.test(decodedHash))) {
135
- url.hash = "";
136
- changed = true;
137
- }
138
- return changed ? url.href : value;
139
- }
111
+ const sanitizeUrl = (v) => scrubUrl(v, hasSecret, fail);
140
112
  function providerMetadata(pointer) {
141
113
  const match = /^\/entries\/(\d+)\/event\/event\/(?:response\/(?:user|prompt_cache_key|safety_identifier)|item\/encrypted_content)$/.exec(pointer);
142
114
  if (!match) return false;
@@ -168,13 +140,13 @@ function sanitizeLiveTranscript(transcript, secretValues = [], credentialFields
168
140
  continue;
169
141
  }
170
142
  const protectedBranch = semantic || semanticKey.has(key);
171
- const removable = !protectedBranch && (metadata || credentialKey.test(key));
143
+ const removable = !protectedBranch && (metadata || CREDENTIAL_KEY.test(key));
172
144
  if (descriptors.has(pointer)) {
173
145
  if (!removable) fail();
174
146
  handled.add(pointer);
175
147
  continue;
176
148
  }
177
- if (!semantic && credentialKey.test(key)) continue;
149
+ if (!semantic && CREDENTIAL_KEY.test(key)) continue;
178
150
  if (hasSecret(key)) {
179
151
  if (!metadata || protectedBranch) fail();
180
152
  continue;
@@ -1 +1 @@
1
- {"version":3,"file":"live-sanitize.js","names":[],"sources":["../src/live-sanitize.ts"],"sourcesContent":["import { validateLiveTranscript } from \"./live-fixture.js\";\nimport type { LiveJson, LiveObject, LiveOptions, LiveTranscript } from \"./live-types.js\";\n\n/** Exact transcript-rooted pointer to an observed removable credential metadata field. */\nexport interface LiveCredentialField {\n readonly pointer: string;\n}\n\nexport class LiveUnsafeExportError extends Error {\n readonly category = \"unsafe-export\";\n constructor() {\n // Never reflect untrusted values, keys, pointers, or validation errors.\n super(\"unsafe-export: Live transcript cannot be retained safely\");\n this.name = \"LiveUnsafeExportError\";\n }\n}\n\nconst credentialKey =\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;\n// These branches carry conversation, tool schema, or PCM semantics. A key named\n// \"authorization\" inside tool parameters is a schema property, not a header.\nconst semanticKey = new Set([\n \"instructions\",\n \"content\",\n \"delta\",\n \"audio\",\n \"arguments\",\n \"output\",\n \"input\",\n \"tools\",\n \"parameters\",\n \"client\",\n \"text\",\n \"thinking\",\n]);\nconst metadataKey = new Set([\"metadata\", \"headers\", \"diagnostics\"]);\nconst unsafeKey = new Set([\"__proto__\", \"constructor\", \"prototype\"]);\nconst redacted = \"[REDACTED]\";\nfunction fail(): never {\n throw new LiveUnsafeExportError();\n}\nfunction 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}\nfunction escapePointer(key: string): string {\n return key.replace(/~/g, \"~0\").replace(/\\//g, \"~1\");\n}\nfunction parts(pointer: string): string[] {\n if (!pointer.startsWith(\"/\") || /~(?![01])/u.test(pointer)) fail();\n const keys = pointer\n .slice(1)\n .split(\"/\")\n .map((key) => key.replace(/~1/g, \"/\").replace(/~0/g, \"~\"));\n if (keys.some((key) => unsafeKey.has(key))) fail();\n return keys;\n}\nfunction child(value: LiveJson, key: string): LiveJson {\n if (!value || typeof value !== \"object\" || !Object.hasOwn(value, key)) fail();\n if (Array.isArray(value)) {\n if (!/^(0|[1-9]\\d*)$/.test(key)) fail();\n return value[Number(key)];\n }\n return value[key];\n}\nfunction setIdentifier(event: LiveObject, pointer: string, value: string): void {\n const keys = parts(pointer);\n const last = keys.pop();\n if (last === undefined) fail();\n let parent: LiveJson = event;\n for (const key of keys) parent = child(parent, key);\n if (typeof child(parent, last) !== \"string\" || !parent || typeof parent !== \"object\") fail();\n if (Array.isArray(parent)) parent[Number(last)] = value;\n else parent[last] = value;\n}\n\n/**\n * Sanitize a separately retained successful capture; never mutate the relay.\n * Validation supplies the reviewed identifier descriptors and causal checks.\n * Credential descriptors supplement the built-in policy, not permission to\n * delete arbitrary protocol operands. Ambiguous secret-bearing data fails closed.\n */\nexport function sanitizeLiveTranscript(\n transcript: LiveTranscript,\n secretValues: readonly string[] = [],\n credentialFields: readonly LiveCredentialField[] = [],\n validationOptions: Omit<LiveOptions, \"secretValues\"> = {},\n): LiveTranscript {\n try {\n const result = validateLiveTranscript(transcript, validationOptions);\n if (!Array.isArray(secretValues) || secretValues.some((s) => typeof s !== \"string\")) fail();\n const secrets = [...new Set(secretValues.filter((s) => s.length))].sort(\n (a, b) => b.length - a.length,\n );\n const hasSecret = (value: string) => secrets.some((secret) => value.includes(secret));\n const replaceSecrets = (value: string) => {\n for (const secret of secrets) value = value.split(secret).join(redacted);\n return value;\n };\n const descriptors = new Set(\n credentialFields.map((field) => {\n parts(field.pointer);\n return field.pointer;\n }),\n );\n const handled = new Set<string>();\n // Names themselves may have been derived from provider IDs. Replace both\n // the annotation names and only their explicitly annotated operand values.\n const originalIds = new Set<string>();\n for (const binding of result.bindings) {\n let value: LiveJson = result.entries[binding.entry].event;\n for (const key of parts(binding.pointer)) value = child(value, key);\n if (typeof value !== \"string\") fail();\n originalIds.add(value);\n }\n let nextId = 1;\n const identities = new Map<string, { name: string; value: string }>();\n for (const binding of result.bindings) {\n let identity = identities.get(binding.name);\n if (!identity) {\n const index = identities.size + 1;\n while (originalIds.has(`live-id-${nextId}`)) nextId++;\n identity = { name: `binding-${index}`, value: `live-id-${nextId++}` };\n identities.set(binding.name, identity);\n }\n setIdentifier(result.entries[binding.entry].event, binding.pointer, identity.value);\n binding.name = identity.name;\n }\n function sanitizeUrl(value: string): 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))) fail();\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 (credentialKey.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 function providerMetadata(pointer: string): boolean {\n const match =\n /^\\/entries\\/(\\d+)\\/event\\/event\\/(?:response\\/(?:user|prompt_cache_key|safety_identifier)|item\\/encrypted_content)$/.exec(\n pointer,\n );\n if (!match) return false;\n const event = result.entries[Number(match[1])].event;\n const nested = event.event;\n if (\n event.type !== \"response.event\" ||\n !nested ||\n typeof nested !== \"object\" ||\n Array.isArray(nested)\n )\n return false;\n if (!pointer.endsWith(\"/item/encrypted_content\")) {\n return [\"response.created\", \"response.in_progress\", \"response.completed\"].includes(\n String(nested.type),\n );\n }\n const item = nested.item;\n return (\n [\"response.output_item.added\", \"response.output_item.done\"].includes(String(nested.type)) &&\n !!item &&\n typeof item === \"object\" &&\n !Array.isArray(item) &&\n item.type === \"reasoning\"\n );\n }\n function visit(value: LiveJson, path: string, metadata = false, semantic = false): LiveJson {\n if (typeof value === \"string\") {\n const url = sanitizeUrl(value);\n if ((hasSecret(value) || url !== value) && (!metadata || semantic)) fail();\n return metadata && !semantic ? replaceSecrets(url) : value;\n }\n if (Array.isArray(value))\n return value.map((item, i) => visit(item, `${path}/${i}`, metadata, semantic));\n if (!value || typeof value !== \"object\") return value;\n const output: LiveObject = {};\n for (const [key, item] of Object.entries(value)) {\n const pointer = `${path}/${escapePointer(key)}`;\n // Exact retained metadata sites from the reviewed v3 sanitation\n // evidence. Never infer these by equal values or similar field names.\n if (providerMetadata(pointer) && (typeof item === \"string\" || item === null)) {\n if (descriptors.has(pointer)) handled.add(pointer);\n else output[key] = typeof item === \"string\" ? redacted : item;\n continue;\n }\n const protectedBranch = semantic || semanticKey.has(key);\n const removable = !protectedBranch && (metadata || credentialKey.test(key));\n if (descriptors.has(pointer)) {\n if (!removable) fail();\n handled.add(pointer);\n continue;\n }\n if (!semantic && credentialKey.test(key)) continue;\n if (hasSecret(key)) {\n if (!metadata || protectedBranch) fail();\n continue;\n }\n output[key] = visit(item, pointer, metadata || metadataKey.has(key), protectedBranch);\n }\n return output;\n }\n // Validation clones all bounded JSON, including unknown metadata. Preserve\n // those fields, then validate again to catch any semantic deletion.\n const cleaned = visit(result as LiveTranscript & LiveObject, \"\");\n if (handled.size !== descriptors.size || hasSecret(JSON.stringify(cleaned))) fail();\n return validateLiveTranscript(cleaned, validationOptions);\n } catch {\n fail();\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"live-sanitize.js","names":["credentialKey"],"sources":["../src/live-sanitize.ts"],"sourcesContent":["import { validateLiveTranscript } from \"./live-fixture.js\";\nimport { CREDENTIAL_KEY as credentialKey, scrubUrl } from \"./record-sanitize.js\";\nimport type { LiveJson, LiveObject, LiveOptions, LiveTranscript } from \"./live-types.js\";\n\n/** Exact transcript-rooted pointer to an observed removable credential metadata field. */\nexport interface LiveCredentialField {\n readonly pointer: string;\n}\n\nexport class LiveUnsafeExportError extends Error {\n readonly category = \"unsafe-export\";\n constructor() {\n // Never reflect untrusted values, keys, pointers, or validation errors.\n super(\"unsafe-export: Live transcript cannot be retained safely\");\n this.name = \"LiveUnsafeExportError\";\n }\n}\n\n// These branches carry conversation, tool schema, or PCM semantics. A key named\n// \"authorization\" inside tool parameters is a schema property, not a header.\nconst semanticKey = new Set([\n \"instructions\",\n \"content\",\n \"delta\",\n \"audio\",\n \"arguments\",\n \"output\",\n \"input\",\n \"tools\",\n \"parameters\",\n \"client\",\n \"text\",\n \"thinking\",\n]);\nconst metadataKey = new Set([\"metadata\", \"headers\", \"diagnostics\"]);\nconst unsafeKey = new Set([\"__proto__\", \"constructor\", \"prototype\"]);\nconst redacted = \"[REDACTED]\";\nfunction fail(): never {\n throw new LiveUnsafeExportError();\n}\nfunction escapePointer(key: string): string {\n return key.replace(/~/g, \"~0\").replace(/\\//g, \"~1\");\n}\nfunction parts(pointer: string): string[] {\n if (!pointer.startsWith(\"/\") || /~(?![01])/u.test(pointer)) fail();\n const keys = pointer\n .slice(1)\n .split(\"/\")\n .map((key) => key.replace(/~1/g, \"/\").replace(/~0/g, \"~\"));\n if (keys.some((key) => unsafeKey.has(key))) fail();\n return keys;\n}\nfunction child(value: LiveJson, key: string): LiveJson {\n if (!value || typeof value !== \"object\" || !Object.hasOwn(value, key)) fail();\n if (Array.isArray(value)) {\n if (!/^(0|[1-9]\\d*)$/.test(key)) fail();\n return value[Number(key)];\n }\n return value[key];\n}\nfunction setIdentifier(event: LiveObject, pointer: string, value: string): void {\n const keys = parts(pointer);\n const last = keys.pop();\n if (last === undefined) fail();\n let parent: LiveJson = event;\n for (const key of keys) parent = child(parent, key);\n if (typeof child(parent, last) !== \"string\" || !parent || typeof parent !== \"object\") fail();\n if (Array.isArray(parent)) parent[Number(last)] = value;\n else parent[last] = value;\n}\n\n/**\n * Sanitize a separately retained successful capture; never mutate the relay.\n * Validation supplies the reviewed identifier descriptors and causal checks.\n * Credential descriptors supplement the built-in policy, not permission to\n * delete arbitrary protocol operands. Ambiguous secret-bearing data fails closed.\n */\nexport function sanitizeLiveTranscript(\n transcript: LiveTranscript,\n secretValues: readonly string[] = [],\n credentialFields: readonly LiveCredentialField[] = [],\n validationOptions: Omit<LiveOptions, \"secretValues\"> = {},\n): LiveTranscript {\n try {\n const result = validateLiveTranscript(transcript, validationOptions);\n if (!Array.isArray(secretValues) || secretValues.some((s) => typeof s !== \"string\")) fail();\n const secrets = [...new Set(secretValues.filter((s) => s.length))].sort(\n (a, b) => b.length - a.length,\n );\n const hasSecret = (value: string) => secrets.some((secret) => value.includes(secret));\n const replaceSecrets = (value: string) => {\n for (const secret of secrets) value = value.split(secret).join(redacted);\n return value;\n };\n const descriptors = new Set(\n credentialFields.map((field) => {\n parts(field.pointer);\n return field.pointer;\n }),\n );\n const handled = new Set<string>();\n // Names themselves may have been derived from provider IDs. Replace both\n // the annotation names and only their explicitly annotated operand values.\n const originalIds = new Set<string>();\n for (const binding of result.bindings) {\n let value: LiveJson = result.entries[binding.entry].event;\n for (const key of parts(binding.pointer)) value = child(value, key);\n if (typeof value !== \"string\") fail();\n originalIds.add(value);\n }\n let nextId = 1;\n const identities = new Map<string, { name: string; value: string }>();\n for (const binding of result.bindings) {\n let identity = identities.get(binding.name);\n if (!identity) {\n const index = identities.size + 1;\n while (originalIds.has(`live-id-${nextId}`)) nextId++;\n identity = { name: `binding-${index}`, value: `live-id-${nextId++}` };\n identities.set(binding.name, identity);\n }\n setIdentifier(result.entries[binding.entry].event, binding.pointer, identity.value);\n binding.name = identity.name;\n }\n const sanitizeUrl = (v: string) => scrubUrl(v, hasSecret, fail);\n function providerMetadata(pointer: string): boolean {\n const match =\n /^\\/entries\\/(\\d+)\\/event\\/event\\/(?:response\\/(?:user|prompt_cache_key|safety_identifier)|item\\/encrypted_content)$/.exec(\n pointer,\n );\n if (!match) return false;\n const event = result.entries[Number(match[1])].event;\n const nested = event.event;\n if (\n event.type !== \"response.event\" ||\n !nested ||\n typeof nested !== \"object\" ||\n Array.isArray(nested)\n )\n return false;\n if (!pointer.endsWith(\"/item/encrypted_content\")) {\n return [\"response.created\", \"response.in_progress\", \"response.completed\"].includes(\n String(nested.type),\n );\n }\n const item = nested.item;\n return (\n [\"response.output_item.added\", \"response.output_item.done\"].includes(String(nested.type)) &&\n !!item &&\n typeof item === \"object\" &&\n !Array.isArray(item) &&\n item.type === \"reasoning\"\n );\n }\n function visit(value: LiveJson, path: string, metadata = false, semantic = false): LiveJson {\n if (typeof value === \"string\") {\n const url = sanitizeUrl(value);\n if ((hasSecret(value) || url !== value) && (!metadata || semantic)) fail();\n return metadata && !semantic ? replaceSecrets(url) : value;\n }\n if (Array.isArray(value))\n return value.map((item, i) => visit(item, `${path}/${i}`, metadata, semantic));\n if (!value || typeof value !== \"object\") return value;\n const output: LiveObject = {};\n for (const [key, item] of Object.entries(value)) {\n const pointer = `${path}/${escapePointer(key)}`;\n // Exact retained metadata sites from the reviewed v3 sanitation\n // evidence. Never infer these by equal values or similar field names.\n if (providerMetadata(pointer) && (typeof item === \"string\" || item === null)) {\n if (descriptors.has(pointer)) handled.add(pointer);\n else output[key] = typeof item === \"string\" ? redacted : item;\n continue;\n }\n const protectedBranch = semantic || semanticKey.has(key);\n const removable = !protectedBranch && (metadata || credentialKey.test(key));\n if (descriptors.has(pointer)) {\n if (!removable) fail();\n handled.add(pointer);\n continue;\n }\n if (!semantic && credentialKey.test(key)) continue;\n if (hasSecret(key)) {\n if (!metadata || protectedBranch) fail();\n continue;\n }\n output[key] = visit(item, pointer, metadata || metadataKey.has(key), protectedBranch);\n }\n return output;\n }\n // Validation clones all bounded JSON, including unknown metadata. Preserve\n // those fields, then validate again to catch any semantic deletion.\n const cleaned = visit(result as LiveTranscript & LiveObject, \"\");\n if (handled.size !== descriptors.size || hasSecret(JSON.stringify(cleaned))) fail();\n return validateLiveTranscript(cleaned, validationOptions);\n } catch {\n fail();\n }\n}\n"],"mappings":";;;;AASA,IAAa,wBAAb,cAA2C,MAAM;CAC/C,AAAS,WAAW;CACpB,cAAc;AAEZ,QAAM,2DAA2D;AACjE,OAAK,OAAO;;;AAMhB,MAAM,cAAc,IAAI,IAAI;CAC1B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD,CAAC;AACF,MAAM,cAAc,IAAI,IAAI;CAAC;CAAY;CAAW;CAAc,CAAC;AACnE,MAAM,YAAY,IAAI,IAAI;CAAC;CAAa;CAAe;CAAY,CAAC;AACpE,MAAM,WAAW;AACjB,SAAS,OAAc;AACrB,OAAM,IAAI,uBAAuB;;AAEnC,SAAS,cAAc,KAAqB;AAC1C,QAAO,IAAI,QAAQ,MAAM,KAAK,CAAC,QAAQ,OAAO,KAAK;;AAErD,SAAS,MAAM,SAA2B;AACxC,KAAI,CAAC,QAAQ,WAAW,IAAI,IAAI,aAAa,KAAK,QAAQ,CAAE,OAAM;CAClE,MAAM,OAAO,QACV,MAAM,EAAE,CACR,MAAM,IAAI,CACV,KAAK,QAAQ,IAAI,QAAQ,OAAO,IAAI,CAAC,QAAQ,OAAO,IAAI,CAAC;AAC5D,KAAI,KAAK,MAAM,QAAQ,UAAU,IAAI,IAAI,CAAC,CAAE,OAAM;AAClD,QAAO;;AAET,SAAS,MAAM,OAAiB,KAAuB;AACrD,KAAI,CAAC,SAAS,OAAO,UAAU,YAAY,CAAC,OAAO,OAAO,OAAO,IAAI,CAAE,OAAM;AAC7E,KAAI,MAAM,QAAQ,MAAM,EAAE;AACxB,MAAI,CAAC,iBAAiB,KAAK,IAAI,CAAE,OAAM;AACvC,SAAO,MAAM,OAAO,IAAI;;AAE1B,QAAO,MAAM;;AAEf,SAAS,cAAc,OAAmB,SAAiB,OAAqB;CAC9E,MAAM,OAAO,MAAM,QAAQ;CAC3B,MAAM,OAAO,KAAK,KAAK;AACvB,KAAI,SAAS,OAAW,OAAM;CAC9B,IAAI,SAAmB;AACvB,MAAK,MAAM,OAAO,KAAM,UAAS,MAAM,QAAQ,IAAI;AACnD,KAAI,OAAO,MAAM,QAAQ,KAAK,KAAK,YAAY,CAAC,UAAU,OAAO,WAAW,SAAU,OAAM;AAC5F,KAAI,MAAM,QAAQ,OAAO,CAAE,QAAO,OAAO,KAAK,IAAI;KAC7C,QAAO,QAAQ;;;;;;;;AAStB,SAAgB,uBACd,YACA,eAAkC,EAAE,EACpC,mBAAmD,EAAE,EACrD,oBAAuD,EAAE,EACzC;AAChB,KAAI;EACF,MAAM,SAAS,uBAAuB,YAAY,kBAAkB;AACpE,MAAI,CAAC,MAAM,QAAQ,aAAa,IAAI,aAAa,MAAM,MAAM,OAAO,MAAM,SAAS,CAAE,OAAM;EAC3F,MAAM,UAAU,CAAC,GAAG,IAAI,IAAI,aAAa,QAAQ,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,MAChE,GAAG,MAAM,EAAE,SAAS,EAAE,OACxB;EACD,MAAM,aAAa,UAAkB,QAAQ,MAAM,WAAW,MAAM,SAAS,OAAO,CAAC;EACrF,MAAM,kBAAkB,UAAkB;AACxC,QAAK,MAAM,UAAU,QAAS,SAAQ,MAAM,MAAM,OAAO,CAAC,KAAK,SAAS;AACxE,UAAO;;EAET,MAAM,cAAc,IAAI,IACtB,iBAAiB,KAAK,UAAU;AAC9B,SAAM,MAAM,QAAQ;AACpB,UAAO,MAAM;IACb,CACH;EACD,MAAM,0BAAU,IAAI,KAAa;EAGjC,MAAM,8BAAc,IAAI,KAAa;AACrC,OAAK,MAAM,WAAW,OAAO,UAAU;GACrC,IAAI,QAAkB,OAAO,QAAQ,QAAQ,OAAO;AACpD,QAAK,MAAM,OAAO,MAAM,QAAQ,QAAQ,CAAE,SAAQ,MAAM,OAAO,IAAI;AACnE,OAAI,OAAO,UAAU,SAAU,OAAM;AACrC,eAAY,IAAI,MAAM;;EAExB,IAAI,SAAS;EACb,MAAM,6BAAa,IAAI,KAA8C;AACrE,OAAK,MAAM,WAAW,OAAO,UAAU;GACrC,IAAI,WAAW,WAAW,IAAI,QAAQ,KAAK;AAC3C,OAAI,CAAC,UAAU;IACb,MAAM,QAAQ,WAAW,OAAO;AAChC,WAAO,YAAY,IAAI,WAAW,SAAS,CAAE;AAC7C,eAAW;KAAE,MAAM,WAAW;KAAS,OAAO,WAAW;KAAY;AACrE,eAAW,IAAI,QAAQ,MAAM,SAAS;;AAExC,iBAAc,OAAO,QAAQ,QAAQ,OAAO,OAAO,QAAQ,SAAS,SAAS,MAAM;AACnF,WAAQ,OAAO,SAAS;;EAE1B,MAAM,eAAe,MAAc,SAAS,GAAG,WAAW,KAAK;EAC/D,SAAS,iBAAiB,SAA0B;GAClD,MAAM,QACJ,sHAAsH,KACpH,QACD;AACH,OAAI,CAAC,MAAO,QAAO;GACnB,MAAM,QAAQ,OAAO,QAAQ,OAAO,MAAM,GAAG,EAAE;GAC/C,MAAM,SAAS,MAAM;AACrB,OACE,MAAM,SAAS,oBACf,CAAC,UACD,OAAO,WAAW,YAClB,MAAM,QAAQ,OAAO,CAErB,QAAO;AACT,OAAI,CAAC,QAAQ,SAAS,0BAA0B,CAC9C,QAAO;IAAC;IAAoB;IAAwB;IAAqB,CAAC,SACxE,OAAO,OAAO,KAAK,CACpB;GAEH,MAAM,OAAO,OAAO;AACpB,UACE,CAAC,8BAA8B,4BAA4B,CAAC,SAAS,OAAO,OAAO,KAAK,CAAC,IACzF,CAAC,CAAC,QACF,OAAO,SAAS,YAChB,CAAC,MAAM,QAAQ,KAAK,IACpB,KAAK,SAAS;;EAGlB,SAAS,MAAM,OAAiB,MAAc,WAAW,OAAO,WAAW,OAAiB;AAC1F,OAAI,OAAO,UAAU,UAAU;IAC7B,MAAM,MAAM,YAAY,MAAM;AAC9B,SAAK,UAAU,MAAM,IAAI,QAAQ,WAAW,CAAC,YAAY,UAAW,OAAM;AAC1E,WAAO,YAAY,CAAC,WAAW,eAAe,IAAI,GAAG;;AAEvD,OAAI,MAAM,QAAQ,MAAM,CACtB,QAAO,MAAM,KAAK,MAAM,MAAM,MAAM,MAAM,GAAG,KAAK,GAAG,KAAK,UAAU,SAAS,CAAC;AAChF,OAAI,CAAC,SAAS,OAAO,UAAU,SAAU,QAAO;GAChD,MAAM,SAAqB,EAAE;AAC7B,QAAK,MAAM,CAAC,KAAK,SAAS,OAAO,QAAQ,MAAM,EAAE;IAC/C,MAAM,UAAU,GAAG,KAAK,GAAG,cAAc,IAAI;AAG7C,QAAI,iBAAiB,QAAQ,KAAK,OAAO,SAAS,YAAY,SAAS,OAAO;AAC5E,SAAI,YAAY,IAAI,QAAQ,CAAE,SAAQ,IAAI,QAAQ;SAC7C,QAAO,OAAO,OAAO,SAAS,WAAW,WAAW;AACzD;;IAEF,MAAM,kBAAkB,YAAY,YAAY,IAAI,IAAI;IACxD,MAAM,YAAY,CAAC,oBAAoB,YAAYA,eAAc,KAAK,IAAI;AAC1E,QAAI,YAAY,IAAI,QAAQ,EAAE;AAC5B,SAAI,CAAC,UAAW,OAAM;AACtB,aAAQ,IAAI,QAAQ;AACpB;;AAEF,QAAI,CAAC,YAAYA,eAAc,KAAK,IAAI,CAAE;AAC1C,QAAI,UAAU,IAAI,EAAE;AAClB,SAAI,CAAC,YAAY,gBAAiB,OAAM;AACxC;;AAEF,WAAO,OAAO,MAAM,MAAM,SAAS,YAAY,YAAY,IAAI,IAAI,EAAE,gBAAgB;;AAEvF,UAAO;;EAIT,MAAM,UAAU,MAAM,QAAuC,GAAG;AAChE,MAAI,QAAQ,SAAS,YAAY,QAAQ,UAAU,KAAK,UAAU,QAAQ,CAAC,CAAE,OAAM;AACnF,SAAO,uBAAuB,SAAS,kBAAkB;SACnD;AACN,QAAM"}