@@ -253,6 +253,125 @@ describe("scrubSecretShapedValue key handling", () => {
253253 } ) ;
254254} ) ;
255255
256+ describe ( "bare-KEY names in JSON paths" , ( ) => {
257+ test ( "redacts bare-KEY names in JSON objects" , ( ) => {
258+ const out = scrubSecretShapedValue ( {
259+ OPENAI_KEY : "sk-live-abc123" ,
260+ nested : { MY_API_KEY : "stripe-live-xyz" } ,
261+ } ) ;
262+ expect ( out ) . toEqual ( {
263+ OPENAI_KEY : CREDENTIAL_REDACTION ,
264+ nested : { MY_API_KEY : CREDENTIAL_REDACTION } ,
265+ } ) ;
266+ } ) ;
267+
268+ test ( "redacts bare-KEY names in JSON text" , ( ) => {
269+ const pretty = scrubSecretShapedContent ( '{"OPENAI_KEY": "sk-live-abc123"}' ) ;
270+ expect ( pretty ) . toBe ( `{"OPENAI_KEY": "${ CREDENTIAL_REDACTION } "}` ) ;
271+ const compact = scrubSecretShapedContent ( '{"OPENAI_KEY":"sk-live-abc123"}' ) ;
272+ expect ( compact ) . toBe ( `{"OPENAI_KEY":"${ CREDENTIAL_REDACTION } "}` ) ;
273+ expect ( compact ) . not . toContain ( "sk-live-abc123" ) ;
274+ } ) ;
275+
276+ test ( "leaves MONKEY-style words and ordinary key fields alone" , ( ) => {
277+ const value = scrubSecretShapedValue ( {
278+ MONKEY : "banana" ,
279+ monkey : "banana" ,
280+ key : "ctrl+s" ,
281+ hotkey : "ctrl+s" ,
282+ } ) ;
283+ expect ( value ) . toEqual ( {
284+ MONKEY : "banana" ,
285+ monkey : "banana" ,
286+ key : "ctrl+s" ,
287+ hotkey : "ctrl+s" ,
288+ } ) ;
289+ const text = '{"MONKEY": "banana", "key": "ctrl+s"}' ;
290+ expect ( scrubSecretShapedContent ( text ) ) . toBe ( text ) ;
291+ } ) ;
292+ } ) ;
293+
294+ describe ( "query secrets keep characters after quotes" , ( ) => {
295+ test ( "does not leave a tail after a quote in password, api_key, or token" , ( ) => {
296+ for ( const name of [ "password" , "api_key" , "token" ] as const ) {
297+ for ( const quote of [ "'" , '"' ] as const ) {
298+ const text = `GET https://provider.invalid/v1?${ name } =p${ quote } LEAKTAIL&format=json` ;
299+ const out = scrubSecretShapedContent ( text ) ;
300+ expect ( out ) . toBe (
301+ `GET https://provider.invalid/v1?${ name } =${ CREDENTIAL_REDACTION } &format=json` ,
302+ ) ;
303+ expect ( out ) . not . toContain ( "LEAKTAIL" ) ;
304+ }
305+ }
306+ } ) ;
307+ } ) ;
308+
309+ describe ( "storage key names stay readable" , ( ) => {
310+ test ( "keeps partition, sort, primary, cache, and prompt_cache keys" , ( ) => {
311+ const value = {
312+ partition_key : "tenant" ,
313+ sort_key : 2 ,
314+ primary_key : [ "id" ] ,
315+ cache_key : [ 1 , 2 ] ,
316+ prompt_cache_key : 9 ,
317+ OPENAI_KEY : "sk-live-abc123" ,
318+ api_key : [ "still-secret" ] ,
319+ } ;
320+ expect ( scrubSecretShapedValue ( value ) ) . toEqual ( {
321+ partition_key : "tenant" ,
322+ sort_key : 2 ,
323+ primary_key : [ "id" ] ,
324+ cache_key : [ 1 , 2 ] ,
325+ prompt_cache_key : 9 ,
326+ OPENAI_KEY : CREDENTIAL_REDACTION ,
327+ api_key : CREDENTIAL_REDACTION ,
328+ } ) ;
329+
330+ const text =
331+ '{"partition_key":"tenant","sort_key":2,"primary_key":["id"],"cache_key":[1],"prompt_cache_key":"sess","OPENAI_KEY":"sk-live-abc123","api_key":"still-secret"}' ;
332+ const scrubbed = scrubSecretShapedContent ( text ) ;
333+ expect ( scrubbed ) . toContain ( '"partition_key":"tenant"' ) ;
334+ expect ( scrubbed ) . toContain ( '"sort_key":2' ) ;
335+ expect ( scrubbed ) . toContain ( '"primary_key":["id"]' ) ;
336+ expect ( scrubbed ) . toContain ( '"cache_key":[1]' ) ;
337+ expect ( scrubbed ) . toContain ( '"prompt_cache_key":"sess"' ) ;
338+ expect ( scrubbed ) . not . toContain ( "sk-live-abc123" ) ;
339+ expect ( scrubbed ) . not . toContain ( "still-secret" ) ;
340+
341+ const query =
342+ "GET https://provider.invalid/v1?sort_key=created_at&partition_key=tenant&format=json" ;
343+ expect ( scrubSecretShapedContent ( query ) ) . toBe ( query ) ;
344+ } ) ;
345+ } ) ;
346+
347+ describe ( "bare-KEY names in query parameters" , ( ) => {
348+ test ( "redacts ?OPENAI_KEY= among benign params, adjacent params survive" , ( ) => {
349+ const text =
350+ "GET https://provider.invalid/v1?model=test&OPENAI_KEY=live-value&format=json" ;
351+ const out = scrubSecretShapedContent ( text ) ;
352+ expect ( out ) . toBe (
353+ `GET https://provider.invalid/v1?model=test&OPENAI_KEY=${ CREDENTIAL_REDACTION } &format=json` ,
354+ ) ;
355+ expect ( out ) . not . toContain ( "live-value" ) ;
356+ } ) ;
357+
358+ test ( "redacts bare-KEY query params case-insensitively with quote bounds" , ( ) => {
359+ const out = scrubSecretShapedContent (
360+ '{"url": "https://provider.invalid/v1?openai_key=live-value&model=test"}' ,
361+ ) ;
362+ expect ( out ) . toBe (
363+ `{"url": "https://provider.invalid/v1?openai_key=${ CREDENTIAL_REDACTION } &model=test"}` ,
364+ ) ;
365+ expect ( out ) . not . toContain ( "live-value" ) ;
366+ } ) ;
367+
368+ test ( "leaves MONKEY-style and ordinary query params alone" , ( ) => {
369+ const text =
370+ "GET https://provider.invalid/v1?MONKEY=banana&model=test&format=json" ;
371+ expect ( scrubSecretShapedContent ( text ) ) . toBe ( text ) ;
372+ } ) ;
373+ } ) ;
374+
256375describe ( "toolResultSecretScrubPlugin" , ( ) => {
257376 const next =
258377 ( content : ToolResult [ "content" ] , isError = false ) =>
0 commit comments