restless-sdk 0.1.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.
@@ -0,0 +1,775 @@
1
+ {
2
+ "$generated": "GENERATED FILE - do not edit by hand. Inputs live in spec/cases.ts; run `npm run spec:vectors` to regenerate.",
3
+ "$contract": "spec/CONTRACT.md",
4
+ "specVersion": "1.0.0",
5
+ "cases": [
6
+ {
7
+ "id": "redactValue/long",
8
+ "requirement": "REDACT-001",
9
+ "op": "redactValue",
10
+ "input": {
11
+ "value": "sk_1234567890abcdef"
12
+ },
13
+ "expected": "<REDACTED:19:cdef>"
14
+ },
15
+ {
16
+ "id": "redactValue/boundary-7",
17
+ "requirement": "REDACT-001",
18
+ "op": "redactValue",
19
+ "input": {
20
+ "value": "1234567"
21
+ },
22
+ "expected": "<REDACTED:7>",
23
+ "note": "One below the tail threshold: no tail."
24
+ },
25
+ {
26
+ "id": "redactValue/boundary-8",
27
+ "requirement": "REDACT-001",
28
+ "op": "redactValue",
29
+ "input": {
30
+ "value": "12345678"
31
+ },
32
+ "expected": "<REDACTED:8:5678>",
33
+ "note": "Exactly at the threshold: tail appears."
34
+ },
35
+ {
36
+ "id": "redactValue/empty",
37
+ "requirement": "REDACT-001",
38
+ "op": "redactValue",
39
+ "input": {
40
+ "value": ""
41
+ },
42
+ "expected": "<REDACTED:0>"
43
+ },
44
+ {
45
+ "id": "redactValue/astral",
46
+ "requirement": "REDACT-002",
47
+ "op": "redactValue",
48
+ "input": {
49
+ "value": "πŸ™‚πŸ™‚πŸ™‚πŸ™‚πŸ™‚πŸ™‚πŸ™‚πŸ™‚"
50
+ },
51
+ "expected": "<REDACTED:8:πŸ™‚πŸ™‚πŸ™‚πŸ™‚>",
52
+ "note": "8 code points, 16 UTF-16 code units, 32 UTF-8 bytes. The sentinel must say 8. Catches the most common porting mistake in this file."
53
+ },
54
+ {
55
+ "id": "redactValue/mixed-width",
56
+ "requirement": "REDACT-002",
57
+ "op": "redactValue",
58
+ "input": {
59
+ "value": "abc-ζ—₯本θͺž-πŸš€-xyz"
60
+ },
61
+ "expected": "<REDACTED:13:-xyz>"
62
+ },
63
+ {
64
+ "id": "redactValue/astral-tail-split",
65
+ "requirement": "REDACT-002",
66
+ "op": "redactValue",
67
+ "input": {
68
+ "value": "0123456789πŸš€"
69
+ },
70
+ "expected": "<REDACTED:11:789πŸš€>",
71
+ "note": "A UTF-16 slice(-4) ends mid-surrogate here and produces an ill-formed tail."
72
+ },
73
+ {
74
+ "id": "redactHeaders/defaults",
75
+ "requirement": "REDACT-011",
76
+ "op": "redactHeaders",
77
+ "input": {
78
+ "headers": {
79
+ "authorization": "Bearer abcdef0123456789",
80
+ "cookie": "sid=xyz9999",
81
+ "content-type": "application/json",
82
+ "host": "example.com"
83
+ }
84
+ },
85
+ "expected": {
86
+ "authorization": "Bearer <REDACTED:16:6789>",
87
+ "cookie": "<REDACTED:11:9999>",
88
+ "content-type": "application/json",
89
+ "host": "example.com"
90
+ }
91
+ },
92
+ {
93
+ "id": "redactHeaders/case-insensitive",
94
+ "requirement": "REDACT-010",
95
+ "op": "redactHeaders",
96
+ "input": {
97
+ "headers": {
98
+ "Authorization": "Bearer xxxxxxxxxxxx",
99
+ "X-API-KEY": "sk_abcdef0123"
100
+ }
101
+ },
102
+ "expected": {
103
+ "Authorization": "Bearer <REDACTED:12:xxxx>",
104
+ "X-API-KEY": "<REDACTED:13:0123>"
105
+ }
106
+ },
107
+ {
108
+ "id": "redactHeaders/scheme-preserved-bearer",
109
+ "requirement": "REDACT-016",
110
+ "op": "redactHeaders",
111
+ "input": {
112
+ "headers": {
113
+ "authorization": "Bearer abcdef0123456789"
114
+ }
115
+ },
116
+ "expected": {
117
+ "authorization": "Bearer <REDACTED:16:6789>"
118
+ }
119
+ },
120
+ {
121
+ "id": "redactHeaders/scheme-preserved-basic",
122
+ "requirement": "REDACT-016",
123
+ "op": "redactHeaders",
124
+ "input": {
125
+ "headers": {
126
+ "authorization": "Basic dXNlcjpwYXNzd29yZA=="
127
+ }
128
+ },
129
+ "expected": {
130
+ "authorization": "Basic <REDACTED:20:ZA==>"
131
+ }
132
+ },
133
+ {
134
+ "id": "redactHeaders/scheme-multi-space",
135
+ "requirement": "REDACT-016",
136
+ "op": "redactHeaders",
137
+ "input": {
138
+ "headers": {
139
+ "authorization": "Bearer abcdef0123456789"
140
+ }
141
+ },
142
+ "expected": {
143
+ "authorization": "Bearer <REDACTED:16:6789>"
144
+ },
145
+ "note": "Split on the first whitespace RUN; the run itself is preserved verbatim."
146
+ },
147
+ {
148
+ "id": "redactHeaders/scheme-with-cr-in-credential",
149
+ "requirement": "REDACT-016",
150
+ "op": "redactHeaders",
151
+ "input": {
152
+ "headers": {
153
+ "authorization": "Bearer abc\rdef0123456789"
154
+ }
155
+ },
156
+ "expected": {
157
+ "authorization": "Bearer <REDACTED:17:6789>"
158
+ },
159
+ "note": "Found by differential fuzz. The old /^(\\S+)(\\s+)(\\S.*)$/ did not match here because JS '.' excludes CR, so Node redacted the whole value while Python (re.DOTALL) preserved the scheme. The explicit scan makes both preserve it."
160
+ },
161
+ {
162
+ "id": "redactHeaders/scheme-with-astral-credential",
163
+ "requirement": "REDACT-016",
164
+ "op": "redactHeaders",
165
+ "input": {
166
+ "headers": {
167
+ "authorization": "Bearer abcdef012345πŸš€"
168
+ }
169
+ },
170
+ "expected": {
171
+ "authorization": "Bearer <REDACTED:13:345πŸš€>"
172
+ },
173
+ "note": "Scheme split walks code points, so the sentinel length counts the rocket once."
174
+ },
175
+ {
176
+ "id": "redactHeaders/leading-whitespace",
177
+ "requirement": "REDACT-016",
178
+ "op": "redactHeaders",
179
+ "input": {
180
+ "headers": {
181
+ "authorization": " Bearer abcdef0123"
182
+ }
183
+ },
184
+ "expected": {
185
+ "authorization": "<REDACTED:20:0123>"
186
+ },
187
+ "note": "Value starts with whitespace: there is no scheme to preserve, so redact whole."
188
+ },
189
+ {
190
+ "id": "redactHeaders/scheme-only-trailing-space",
191
+ "requirement": "REDACT-016",
192
+ "op": "redactHeaders",
193
+ "input": {
194
+ "headers": {
195
+ "authorization": "Bearer "
196
+ }
197
+ },
198
+ "expected": {
199
+ "authorization": "<REDACTED:9:r >"
200
+ },
201
+ "note": "Whitespace but no credential after it: redact whole."
202
+ },
203
+ {
204
+ "id": "redactHeaders/no-scheme",
205
+ "requirement": "REDACT-017",
206
+ "op": "redactHeaders",
207
+ "input": {
208
+ "headers": {
209
+ "authorization": "eyJhbGciOiJIUzI1NiJ9.payload.sig"
210
+ }
211
+ },
212
+ "expected": {
213
+ "authorization": "<REDACTED:32:.sig>"
214
+ },
215
+ "note": "A bare JWT has no space, so the whole value is the secret."
216
+ },
217
+ {
218
+ "id": "redactHeaders/proxy-authorization",
219
+ "requirement": "REDACT-016",
220
+ "op": "redactHeaders",
221
+ "input": {
222
+ "headers": {
223
+ "proxy-authorization": "Digest abcdef0123456789"
224
+ }
225
+ },
226
+ "expected": {
227
+ "proxy-authorization": "Digest <REDACTED:16:6789>"
228
+ }
229
+ },
230
+ {
231
+ "id": "redactHeaders/x-api-key-whole",
232
+ "requirement": "REDACT-018",
233
+ "op": "redactHeaders",
234
+ "input": {
235
+ "headers": {
236
+ "x-api-key": "Bearer looks-like-a-scheme-but-is-not"
237
+ }
238
+ },
239
+ "expected": {
240
+ "x-api-key": "<REDACTED:37:-not>"
241
+ },
242
+ "note": "Only authorization / proxy-authorization get scheme treatment. Everything else is redacted whole even if it looks schemed."
243
+ },
244
+ {
245
+ "id": "redactHeaders/set-cookie",
246
+ "requirement": "REDACT-011",
247
+ "op": "redactHeaders",
248
+ "input": {
249
+ "headers": {
250
+ "set-cookie": "sid=abcdef; HttpOnly"
251
+ }
252
+ },
253
+ "expected": {
254
+ "set-cookie": "<REDACTED:20:Only>"
255
+ }
256
+ },
257
+ {
258
+ "id": "redactHeaders/unicode-fold-kelvin",
259
+ "requirement": "REDACT-010",
260
+ "op": "redactHeaders",
261
+ "input": {
262
+ "headers": {
263
+ "x-api-β„ͺey": "supersecretvalue"
264
+ }
265
+ },
266
+ "expected": {
267
+ "x-api-β„ͺey": "<REDACTED:16:alue>"
268
+ },
269
+ "note": "SECURITY. U+212A KELVIN SIGN full-lowercases to 'k', so this IS x-api-key and MUST be redacted. An ASCII-only fold leaves it unmatched and ships the secret. Every port initially got this backwards and leaked the value, which is why this case exists."
270
+ },
271
+ {
272
+ "id": "redactHeaders/unicode-fold-longs",
273
+ "requirement": "REDACT-010",
274
+ "op": "redactHeaders",
275
+ "input": {
276
+ "headers": {
277
+ "ΕΏet-cookie": "sid=abcdefgh"
278
+ }
279
+ },
280
+ "expected": {
281
+ "ΕΏet-cookie": "sid=abcdefgh"
282
+ },
283
+ "note": "U+017F LATIN SMALL LETTER LONG S is already lowercase, so full lowercasing does NOT turn it into 's'. This one must NOT be redacted - the rule is fold, not fuzzy-match."
284
+ },
285
+ {
286
+ "id": "redactHeaders/extra-denylist",
287
+ "requirement": "REDACT-015",
288
+ "op": "redactHeaders",
289
+ "input": {
290
+ "headers": {
291
+ "x-custom-secret": "verylongsecretvalue"
292
+ },
293
+ "extra": [
294
+ "x-custom-secret"
295
+ ]
296
+ },
297
+ "expected": {
298
+ "x-custom-secret": "<REDACTED:19:alue>"
299
+ }
300
+ },
301
+ {
302
+ "id": "redactHeaders/extra-normalized",
303
+ "requirement": "REDACT-010",
304
+ "op": "redactHeaders",
305
+ "input": {
306
+ "headers": {
307
+ "X_Custom_Secret": "verylongsecretvalue"
308
+ },
309
+ "extra": [
310
+ "x-custom-secret"
311
+ ]
312
+ },
313
+ "expected": {
314
+ "X_Custom_Secret": "<REDACTED:19:alue>"
315
+ },
316
+ "note": "Extension entries normalize the same way defaults do."
317
+ },
318
+ {
319
+ "id": "redactHeaders/defaults-not-removable",
320
+ "requirement": "REDACT-014",
321
+ "op": "redactHeaders",
322
+ "input": {
323
+ "headers": {
324
+ "authorization": "Bearer abcdef0123456789"
325
+ },
326
+ "extra": [
327
+ "unrelated"
328
+ ]
329
+ },
330
+ "expected": {
331
+ "authorization": "Bearer <REDACTED:16:6789>"
332
+ }
333
+ },
334
+ {
335
+ "id": "redactUrl/basic",
336
+ "requirement": "REDACT-025",
337
+ "op": "redactUrl",
338
+ "input": {
339
+ "url": "https://api.example/v1/foo?bar=ok&api_key=sk_abcdef123&token=tk_9876543210"
340
+ },
341
+ "expected": "https://api.example/v1/foo?bar=ok&api_key=%3CREDACTED%3A12%3Af123%3E&token=%3CREDACTED%3A13%3A3210%3E"
342
+ },
343
+ {
344
+ "id": "redactUrl/clean",
345
+ "requirement": "REDACT-025",
346
+ "op": "redactUrl",
347
+ "input": {
348
+ "url": "https://api.example/v1/foo?bar=ok"
349
+ },
350
+ "expected": "https://api.example/v1/foo?bar=ok"
351
+ },
352
+ {
353
+ "id": "redactUrl/not-a-url",
354
+ "requirement": "REDACT-026",
355
+ "op": "redactUrl",
356
+ "input": {
357
+ "url": "not a url"
358
+ },
359
+ "expected": "not a url"
360
+ },
361
+ {
362
+ "id": "redactUrl/no-normalization",
363
+ "requirement": "REDACT-025",
364
+ "op": "redactUrl",
365
+ "input": {
366
+ "url": "https://api.example:443/v1/foo?a=1&password=hunter2hunter2"
367
+ },
368
+ "expected": "https://api.example:443/v1/foo?a=1&password=%3CREDACTED%3A14%3Ater2%3E",
369
+ "note": "The default port SURVIVES. Nothing outside the matched values may change, so there is no URL round trip to normalize it away - which is also the only reason a Python or Go port can match us without reimplementing WHATWG."
370
+ },
371
+ {
372
+ "id": "redactUrl/plus-in-query",
373
+ "requirement": "REDACT-029",
374
+ "op": "redactUrl",
375
+ "input": {
376
+ "url": "https://api.example/s?q=a+b&secret=abcdefghij"
377
+ },
378
+ "expected": "https://api.example/s?q=a+b&secret=%3CREDACTED%3A10%3Aghij%3E",
379
+ "note": "'+' decodes to a space when reading a value, and untouched params keep their bytes exactly."
380
+ },
381
+ {
382
+ "id": "redactUrl/repeated-param",
383
+ "requirement": "REDACT-027",
384
+ "op": "redactUrl",
385
+ "input": {
386
+ "url": "https://api.example/s?token=aaaaaaaaaa&token=bbbbbbbbbb"
387
+ },
388
+ "expected": "https://api.example/s?token=%3CREDACTED%3A10%3Aaaaa%3E&token=%3CREDACTED%3A10%3Abbbb%3E",
389
+ "note": "Both params survive and are redacted independently. The old URL round trip collapsed them into one, silently dropping the second value, and re-redacted the first sentinel to <REDACTED:18:aaa>>."
390
+ },
391
+ {
392
+ "id": "redactUrl/empty-value",
393
+ "requirement": "REDACT-025",
394
+ "op": "redactUrl",
395
+ "input": {
396
+ "url": "https://api.example/s?token="
397
+ },
398
+ "expected": "https://api.example/s?token=%3CREDACTED%3A0%3E"
399
+ },
400
+ {
401
+ "id": "redactUrl/valueless-param",
402
+ "requirement": "REDACT-025",
403
+ "op": "redactUrl",
404
+ "input": {
405
+ "url": "https://api.example/s?token&a=1"
406
+ },
407
+ "expected": "https://api.example/s?token&a=1",
408
+ "note": "A pair with no '=' is not a key/value pair and is left alone."
409
+ },
410
+ {
411
+ "id": "redactUrl/fragment-preserved",
412
+ "requirement": "REDACT-025",
413
+ "op": "redactUrl",
414
+ "input": {
415
+ "url": "https://api.example/s?token=abcdefghij#section-2"
416
+ },
417
+ "expected": "https://api.example/s?token=%3CREDACTED%3A10%3Aghij%3E#section-2"
418
+ },
419
+ {
420
+ "id": "redactUrl/encoded-key",
421
+ "requirement": "REDACT-029",
422
+ "op": "redactUrl",
423
+ "input": {
424
+ "url": "https://api.example/s?api%5Fkey=abcdefghij"
425
+ },
426
+ "expected": "https://api.example/s?api%5Fkey=%3CREDACTED%3A10%3Aghij%3E",
427
+ "note": "The key is percent-decoded before the denylist test, so an encoded underscore still matches."
428
+ },
429
+ {
430
+ "id": "redactUrl/encoded-value",
431
+ "requirement": "REDACT-029",
432
+ "op": "redactUrl",
433
+ "input": {
434
+ "url": "https://api.example/s?token=a%20b%2Fc%C3%A9"
435
+ },
436
+ "expected": "https://api.example/s?token=%3CREDACTED%3A6%3E",
437
+ "note": "The sentinel length is of the DECODED value (6 code points), not the encoded text."
438
+ },
439
+ {
440
+ "id": "redactUrl/sentinel-encoding",
441
+ "requirement": "REDACT-028",
442
+ "op": "redactUrl",
443
+ "input": {
444
+ "url": "https://api.example/s?token=abcdef~-._x"
445
+ },
446
+ "expected": "https://api.example/s?token=%3CREDACTED%3A11%3A-._x%3E",
447
+ "note": "Pins the encode set: unreserved chars in the tail stay literal, everything else is uppercase percent-hex."
448
+ },
449
+ {
450
+ "id": "redactUrl/astral-value",
451
+ "requirement": "REDACT-029",
452
+ "op": "redactUrl",
453
+ "input": {
454
+ "url": "https://api.example/s?password=πŸš€+L"
455
+ },
456
+ "expected": "https://api.example/s?password=%3CREDACTED%3A3%3E",
457
+ "note": "Found by differential fuzz. Node's percentDecode indexed UTF-16 code units, so it split the rocket's surrogate pair and turned one emoji into two U+FFFD - reporting length 5 where Python reported 4. Decoding walks code points now."
458
+ },
459
+ {
460
+ "id": "redactUrl/invalid-percent-escape",
461
+ "requirement": "PRIM-006",
462
+ "op": "redactUrl",
463
+ "input": {
464
+ "url": "https://api.example/s?token=FUY$%D k0"
465
+ },
466
+ "expected": "https://api.example/s?token=%3CREDACTED%3A9%3AD%E2%80%89k0%3E",
467
+ "note": "Found by differential fuzz. '%D' followed by U+2009 is not two hex digits, so the '%' is literal. Python's int(s,16) strips Unicode whitespace and decoded it as a byte; the digits must be validated explicitly."
468
+ },
469
+ {
470
+ "id": "redactUrl/no-query",
471
+ "requirement": "REDACT-025",
472
+ "op": "redactUrl",
473
+ "input": {
474
+ "url": "https://api.example/v1/foo"
475
+ },
476
+ "expected": "https://api.example/v1/foo"
477
+ },
478
+ {
479
+ "id": "redactBody/basic",
480
+ "requirement": "REDACT-022",
481
+ "op": "redactBody",
482
+ "input": {
483
+ "body": "{\"username\":\"jane\",\"password\":\"hunter2hunter2\",\"cc_number\":\"4111111111111111\"}",
484
+ "contentType": "application/json"
485
+ },
486
+ "expected": "{\"username\":\"jane\",\"password\":\"<REDACTED:14:ter2>\",\"cc_number\":\"<REDACTED:16:1111>\"}",
487
+ "compare": "json"
488
+ },
489
+ {
490
+ "id": "redactBody/name-normalization",
491
+ "requirement": "REDACT-010",
492
+ "op": "redactBody",
493
+ "input": {
494
+ "body": "{\"apiKey\":\"abcdefghijk\",\"api_key\":\"abcdefghijk\",\"API-Key\":\"abcdefghijk\",\"APIKEY\":\"abcdefghijk\"}",
495
+ "contentType": "application/json"
496
+ },
497
+ "expected": "{\"apiKey\":\"<REDACTED:11:hijk>\",\"api_key\":\"<REDACTED:11:hijk>\",\"API-Key\":\"<REDACTED:11:hijk>\",\"APIKEY\":\"<REDACTED:11:hijk>\"}",
498
+ "compare": "json"
499
+ },
500
+ {
501
+ "id": "redactBody/nested-and-arrays",
502
+ "requirement": "REDACT-022",
503
+ "op": "redactBody",
504
+ "input": {
505
+ "body": "{\"user\":{\"name\":\"jane\",\"password\":\"hunter2hunter2\"},\"auths\":[{\"token\":\"tk_abcdef123\"}]}",
506
+ "contentType": "application/json"
507
+ },
508
+ "expected": "{\"user\":{\"name\":\"jane\",\"password\":\"<REDACTED:14:ter2>\"},\"auths\":[{\"token\":\"<REDACTED:12:f123>\"}]}",
509
+ "compare": "json"
510
+ },
511
+ {
512
+ "id": "redactBody/non-string-secret",
513
+ "requirement": "REDACT-004",
514
+ "op": "redactBody",
515
+ "input": {
516
+ "body": "{\"password\":12345,\"token\":null,\"secret\":{\"a\":1}}",
517
+ "contentType": "application/json"
518
+ },
519
+ "expected": "{\"password\":\"<REDACTED>\",\"token\":null,\"secret\":\"<REDACTED>\"}",
520
+ "compare": "json"
521
+ },
522
+ {
523
+ "id": "redactBody/passthrough-no-secrets",
524
+ "requirement": "REDACT-020",
525
+ "op": "redactBody",
526
+ "input": {
527
+ "body": "{ \"a\" : 1.0, \"b\" : [ 1, 2 ] }",
528
+ "contentType": "application/json"
529
+ },
530
+ "expected": "{ \"a\" : 1.0, \"b\" : [ 1, 2 ] }",
531
+ "note": "No denylisted key anywhere, so the ORIGINAL string comes back byte for byte: whitespace intact, 1.0 still 1.0. Compared exactly, not JSON-normalized."
532
+ },
533
+ {
534
+ "id": "redactBody/passthrough-preserves-int64",
535
+ "requirement": "REDACT-020",
536
+ "op": "redactBody",
537
+ "input": {
538
+ "body": "{\"id\":9007199254740993,\"big\":123456789012345678901234567890}",
539
+ "contentType": "application/json"
540
+ },
541
+ "expected": "{\"id\":9007199254740993,\"big\":123456789012345678901234567890}",
542
+ "note": "The bug this rule fixes. A parse/serialize round trip in JS turns 9007199254740993 into ...992. Passthrough keeps it exact."
543
+ },
544
+ {
545
+ "id": "redactBody/passthrough-preserves-key-order",
546
+ "requirement": "REDACT-020",
547
+ "op": "redactBody",
548
+ "input": {
549
+ "body": "{\"z\":1,\"a\":2,\"m\":3}",
550
+ "contentType": "application/json"
551
+ },
552
+ "expected": "{\"z\":1,\"a\":2,\"m\":3}",
553
+ "note": "Also removes the Go encoding/json key-sorting divergence for every body with nothing to redact."
554
+ },
555
+ {
556
+ "id": "redactBody/reserialize-when-secret-present",
557
+ "requirement": "REDACT-022",
558
+ "op": "redactBody",
559
+ "input": {
560
+ "body": "{ \"id\" : 42 , \"password\" : \"hunter2hunter2\" }",
561
+ "contentType": "application/json"
562
+ },
563
+ "expected": "{\"id\":42,\"password\":\"<REDACTED:14:ter2>\"}",
564
+ "compare": "json",
565
+ "note": "Once something must be rewritten, re-serialization is unavoidable: compact separators, key order preserved."
566
+ },
567
+ {
568
+ "id": "redactBody/unicode-not-escaped",
569
+ "requirement": "PRIM-032",
570
+ "op": "redactBody",
571
+ "input": {
572
+ "body": "{\"name\":\"cafΓ© ζ—₯本\",\"token\":\"abcdefghij\"}",
573
+ "contentType": "application/json"
574
+ },
575
+ "expected": "{\"name\":\"cafΓ© ζ—₯本\",\"token\":\"<REDACTED:10:ghij>\"}",
576
+ "compare": "json"
577
+ },
578
+ {
579
+ "id": "redactBody/non-json-content-type",
580
+ "requirement": "REDACT-023",
581
+ "op": "redactBody",
582
+ "input": {
583
+ "body": "{\"password\":\"hunter2hunter2\"}",
584
+ "contentType": "text/plain"
585
+ },
586
+ "expected": "{\"password\":\"hunter2hunter2\"}",
587
+ "note": "Content type gates redaction entirely. A JSON-shaped body under text/plain is untouched."
588
+ },
589
+ {
590
+ "id": "redactBody/content-type-with-charset",
591
+ "requirement": "REDACT-023",
592
+ "op": "redactBody",
593
+ "input": {
594
+ "body": "{\"password\":\"hunter2hunter2\"}",
595
+ "contentType": "application/json; charset=utf-8"
596
+ },
597
+ "expected": "{\"password\":\"<REDACTED:14:ter2>\"}",
598
+ "compare": "json"
599
+ },
600
+ {
601
+ "id": "redactBody/unparseable",
602
+ "requirement": "REDACT-024",
603
+ "op": "redactBody",
604
+ "input": {
605
+ "body": "{not json",
606
+ "contentType": "application/json"
607
+ },
608
+ "expected": "{not json"
609
+ },
610
+ {
611
+ "id": "redactBody/empty",
612
+ "requirement": "REDACT-024",
613
+ "op": "redactBody",
614
+ "input": {
615
+ "body": "",
616
+ "contentType": "application/json"
617
+ },
618
+ "expected": ""
619
+ },
620
+ {
621
+ "id": "redactBody/scalar-root",
622
+ "requirement": "REDACT-020",
623
+ "op": "redactBody",
624
+ "input": {
625
+ "body": "\"just a string\"",
626
+ "contentType": "application/json"
627
+ },
628
+ "expected": "\"just a string\""
629
+ },
630
+ {
631
+ "id": "redactBody/array-root-with-secret",
632
+ "requirement": "REDACT-021",
633
+ "op": "redactBody",
634
+ "input": {
635
+ "body": "[{\"token\":\"abcdefghij\"},{\"ok\":1}]",
636
+ "contentType": "application/json"
637
+ },
638
+ "expected": "[{\"token\":\"<REDACTED:10:ghij>\"},{\"ok\":1}]",
639
+ "compare": "json",
640
+ "note": "The denied-key walk descends through arrays at the root."
641
+ },
642
+ {
643
+ "id": "redactBody/deeply-nested-secret",
644
+ "requirement": "REDACT-021",
645
+ "op": "redactBody",
646
+ "input": {
647
+ "body": "{\"a\":{\"b\":{\"c\":{\"d\":{\"password\":\"hunter2hunter2\"}}}}}",
648
+ "contentType": "application/json"
649
+ },
650
+ "expected": "{\"a\":{\"b\":{\"c\":{\"d\":{\"password\":\"<REDACTED:14:ter2>\"}}}}}",
651
+ "compare": "json"
652
+ },
653
+ {
654
+ "id": "redactBody/unicode-fold-kelvin",
655
+ "requirement": "REDACT-010",
656
+ "op": "redactBody",
657
+ "input": {
658
+ "body": "{\"toβ„ͺen\":\"supersecretvalue\"}",
659
+ "contentType": "application/json"
660
+ },
661
+ "expected": "{\"toβ„ͺen\":\"<REDACTED:16:alue>\"}",
662
+ "compare": "json",
663
+ "note": "SECURITY. The same KELVIN SIGN bypass in a body key. The reference redacts it; every port that implemented the withdrawn ASCII fold leaked the value."
664
+ },
665
+ {
666
+ "id": "redactBody/lone-surrogate",
667
+ "requirement": "PRIM-034",
668
+ "op": "redactBody",
669
+ "input": {
670
+ "body": "{\"a\":\"\ud83d\",\"token\":\"abcdefghij\"}",
671
+ "contentType": "application/json"
672
+ },
673
+ "expected": "{\"a\":\"\\ud83d\",\"token\":\"<REDACTED:10:ghij>\"}",
674
+ "note": "Found by differential fuzz. A lone surrogate has no UTF-8 encoding, so it must be re-emitted as a \\uXXXX escape. Python's ensure_ascii=False wrote the raw character and produced a string that could not be encoded at all."
675
+ },
676
+ {
677
+ "id": "redactBody/extra-denylist",
678
+ "requirement": "REDACT-015",
679
+ "op": "redactBody",
680
+ "input": {
681
+ "body": "{\"mySecretField\":\"verylongsecret123\"}",
682
+ "contentType": "application/json",
683
+ "extra": [
684
+ "mySecretField"
685
+ ]
686
+ },
687
+ "expected": "{\"mySecretField\":\"<REDACTED:17:t123>\"}",
688
+ "compare": "json"
689
+ },
690
+ {
691
+ "id": "truncateBody/under-limit",
692
+ "requirement": "REDACT-030",
693
+ "op": "truncateBody",
694
+ "input": {
695
+ "body": "short",
696
+ "maxBytes": 100
697
+ },
698
+ "expected": "short"
699
+ },
700
+ {
701
+ "id": "truncateBody/exactly-at-limit",
702
+ "requirement": "REDACT-030",
703
+ "op": "truncateBody",
704
+ "input": {
705
+ "body": "0123456789",
706
+ "maxBytes": 10
707
+ },
708
+ "expected": "0123456789",
709
+ "note": "<= the limit passes through untouched."
710
+ },
711
+ {
712
+ "id": "truncateBody/one-over",
713
+ "requirement": "REDACT-030",
714
+ "op": "truncateBody",
715
+ "input": {
716
+ "body": "0123456789A",
717
+ "maxBytes": 10
718
+ },
719
+ "expected": "0123456789\n[...TRUNCATED: original 11 bytes]"
720
+ },
721
+ {
722
+ "id": "truncateBody/multibyte-boundary",
723
+ "requirement": "REDACT-031",
724
+ "op": "truncateBody",
725
+ "input": {
726
+ "body": "aΓ©ζ—₯πŸš€aΓ©ζ—₯πŸš€aΓ©ζ—₯πŸš€",
727
+ "maxBytes": 12
728
+ },
729
+ "expected": "aΓ©ζ—₯πŸš€a\n[...TRUNCATED: original 30 bytes]",
730
+ "note": "The cut lands mid-character. Backing off must yield a well-formed prefix, never a broken sequence or a lone surrogate."
731
+ },
732
+ {
733
+ "id": "truncateBody/cut-inside-emoji",
734
+ "requirement": "REDACT-031",
735
+ "op": "truncateBody",
736
+ "input": {
737
+ "body": "πŸš€πŸš€πŸš€πŸš€",
738
+ "maxBytes": 6
739
+ },
740
+ "expected": "πŸš€\n[...TRUNCATED: original 16 bytes]",
741
+ "note": "6 bytes is one and a half rockets. The kept prefix must be one rocket."
742
+ },
743
+ {
744
+ "id": "truncateBody/original-length-is-bytes",
745
+ "requirement": "REDACT-032",
746
+ "op": "truncateBody",
747
+ "input": {
748
+ "body": "ζ—₯本θͺžζ—₯本θͺžζ—₯本θͺžζ—₯本θͺž",
749
+ "maxBytes": 9
750
+ },
751
+ "expected": "ζ—₯本θͺž\n[...TRUNCATED: original 36 bytes]",
752
+ "note": "The reported original length is UTF-8 bytes (36), not characters (12)."
753
+ },
754
+ {
755
+ "id": "truncateBody/empty",
756
+ "requirement": "REDACT-030",
757
+ "op": "truncateBody",
758
+ "input": {
759
+ "body": "",
760
+ "maxBytes": 10
761
+ },
762
+ "expected": ""
763
+ },
764
+ {
765
+ "id": "truncateBody/null",
766
+ "requirement": "REDACT-030",
767
+ "op": "truncateBody",
768
+ "input": {
769
+ "body": null,
770
+ "maxBytes": 10
771
+ },
772
+ "expected": null
773
+ }
774
+ ]
775
+ }