@heroku/js-blanket 0.0.0 → 1.0.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 (82) hide show
  1. package/README.md +4 -1
  2. package/dist/cjs/.tsbuildinfo +1 -0
  3. package/dist/cjs/adapters/logging/generic.js +23 -0
  4. package/dist/cjs/adapters/logging/generic.js.map +1 -0
  5. package/dist/cjs/adapters/logging/generic.test.js +432 -0
  6. package/dist/cjs/adapters/logging/generic.test.js.map +1 -0
  7. package/dist/cjs/core/patterns.js +17 -0
  8. package/dist/cjs/core/patterns.js.map +1 -0
  9. package/dist/cjs/core/presets.js +116 -0
  10. package/dist/cjs/core/presets.js.map +1 -0
  11. package/dist/cjs/core/scrubber.js +260 -0
  12. package/dist/cjs/core/scrubber.js.map +1 -0
  13. package/dist/cjs/core/scrubber.test.js +392 -0
  14. package/dist/cjs/core/scrubber.test.js.map +1 -0
  15. package/dist/cjs/core/types.js +3 -0
  16. package/dist/cjs/core/types.js.map +1 -0
  17. package/dist/cjs/core/types.test.js +326 -0
  18. package/dist/cjs/core/types.test.js.map +1 -0
  19. package/dist/cjs/index.js +16 -0
  20. package/dist/cjs/index.js.map +1 -0
  21. package/dist/cjs/index.test.js +31 -0
  22. package/dist/cjs/index.test.js.map +1 -0
  23. package/dist/cjs/package.json +1 -0
  24. package/dist/esm/.tsbuildinfo +1 -0
  25. package/{src/adapters/logging/generic.ts → dist/esm/adapters/logging/generic.d.ts} +1 -4
  26. package/dist/esm/adapters/logging/generic.js +20 -0
  27. package/dist/esm/adapters/logging/generic.js.map +1 -0
  28. package/dist/esm/adapters/logging/generic.test.d.ts +7 -0
  29. package/dist/esm/adapters/logging/generic.test.js +430 -0
  30. package/dist/esm/adapters/logging/generic.test.js.map +1 -0
  31. package/dist/esm/core/patterns.d.ts +4 -0
  32. package/dist/esm/core/patterns.js +14 -0
  33. package/dist/esm/core/patterns.js.map +1 -0
  34. package/dist/esm/core/presets.d.ts +64 -0
  35. package/{src/core/presets.ts → dist/esm/core/presets.js} +46 -55
  36. package/dist/esm/core/presets.js.map +1 -0
  37. package/dist/esm/core/scrubber.d.ts +131 -0
  38. package/dist/esm/core/scrubber.js +256 -0
  39. package/dist/esm/core/scrubber.js.map +1 -0
  40. package/dist/esm/core/scrubber.test.d.ts +1 -0
  41. package/dist/esm/core/scrubber.test.js +390 -0
  42. package/dist/esm/core/scrubber.test.js.map +1 -0
  43. package/dist/esm/core/types.d.ts +169 -0
  44. package/dist/esm/core/types.js +2 -0
  45. package/dist/esm/core/types.js.map +1 -0
  46. package/dist/esm/core/types.test.d.ts +9 -0
  47. package/dist/esm/core/types.test.js +324 -0
  48. package/dist/esm/core/types.test.js.map +1 -0
  49. package/{src/index.ts → dist/esm/index.d.ts} +0 -3
  50. package/dist/esm/index.js +7 -0
  51. package/dist/esm/index.js.map +1 -0
  52. package/dist/esm/index.test.d.ts +1 -0
  53. package/dist/esm/index.test.js +29 -0
  54. package/dist/esm/index.test.js.map +1 -0
  55. package/package.json +45 -47
  56. package/.c8rc.json +0 -11
  57. package/.editorconfig +0 -11
  58. package/.github/PULL_REQUEST_TEMPLATE.md +0 -41
  59. package/.github/copilot-instructions.md +0 -117
  60. package/.github/workflows/ci.yml +0 -25
  61. package/.husky/pre-commit +0 -1
  62. package/.lintstagedrc.json +0 -4
  63. package/.tool-versions +0 -1
  64. package/CODEOWNERS +0 -8
  65. package/CODE_OF_CONDUCT.md +0 -111
  66. package/CONTRIBUTING.md +0 -123
  67. package/SECURITY.md +0 -8
  68. package/docs/examples/logging-integration.md +0 -736
  69. package/eslint.config.mjs +0 -108
  70. package/prettier.config.mjs +0 -10
  71. package/scripts/test-setup.mjs +0 -24
  72. package/src/adapters/logging/generic.test.ts +0 -531
  73. package/src/core/patterns.ts +0 -22
  74. package/src/core/scrubber.test.ts +0 -465
  75. package/src/core/scrubber.ts +0 -284
  76. package/src/core/types.test.ts +0 -516
  77. package/src/core/types.ts +0 -176
  78. package/src/index.test.ts +0 -41
  79. package/tsconfig.cjs.json +0 -12
  80. package/tsconfig.esm.json +0 -12
  81. package/tsconfig.json +0 -32
  82. package/tsconfig.test.json +0 -9
@@ -1,465 +0,0 @@
1
- import { describe, it } from 'mocha';
2
- import { expect } from 'chai';
3
- import { Scrubber } from './scrubber.js';
4
-
5
- describe('Scrubber', () => {
6
- describe('Field-based scrubbing', () => {
7
- it('scrubs sensitive fields at any depth', () => {
8
- const scrubber = new Scrubber({
9
- fields: ['access_token'],
10
- });
11
-
12
- const input = {
13
- user: {
14
- profile: {
15
- settings: {
16
- auth: { access_token: 'secret123' },
17
- },
18
- },
19
- },
20
- };
21
-
22
- const { data } = scrubber.scrub(input);
23
- expect(data.user.profile.settings.auth.access_token).to.equal(
24
- '[SCRUBBED]'
25
- );
26
- });
27
-
28
- it('handles case-insensitive field matching', () => {
29
- const scrubber = new Scrubber({
30
- fields: ['password'],
31
- });
32
-
33
- const input = {
34
- Password: 'secret1',
35
- PASSWORD: 'secret2',
36
- password: 'secret3',
37
- };
38
-
39
- const { data } = scrubber.scrub(input);
40
- expect(data.Password).to.equal('[SCRUBBED]');
41
- expect(data.PASSWORD).to.equal('[SCRUBBED]');
42
- expect(data.password).to.equal('[SCRUBBED]');
43
- });
44
-
45
- it('supports regex field patterns', () => {
46
- const scrubber = new Scrubber({
47
- fields: [/api[-_]?key/i], // Matches api_key, api-key, apikey (case insensitive)
48
- });
49
-
50
- const input = {
51
- user_api_key: 'secret',
52
- API_KEY_V2: 'secret',
53
- myApiKeyHere: 'secret',
54
- };
55
-
56
- const { data } = scrubber.scrub(input);
57
- expect(data.user_api_key).to.equal('[SCRUBBED]');
58
- expect(data.API_KEY_V2).to.equal('[SCRUBBED]');
59
- expect(data.myApiKeyHere).to.equal('[SCRUBBED]');
60
- });
61
- });
62
-
63
- describe('Path-based scrubbing', () => {
64
- it('scrubs specific paths only', () => {
65
- const scrubber = new Scrubber({
66
- paths: ['user.profile.email'],
67
- });
68
-
69
- const input = {
70
- user: {
71
- profile: { email: 'bob@example.com', name: 'Bob' },
72
- settings: { email: 'notifications@example.com' },
73
- },
74
- };
75
-
76
- const { data } = scrubber.scrub(input);
77
- expect(data.user.profile.email).to.equal('[SCRUBBED]');
78
- expect(data.user.settings.email).to.equal('notifications@example.com');
79
- });
80
-
81
- it('scrubs array items by index', () => {
82
- const scrubber = new Scrubber({
83
- paths: ['users[0].password'],
84
- });
85
-
86
- const input = {
87
- users: [
88
- { name: 'bob', password: 'secret1' },
89
- { name: 'alice', password: 'secret2' },
90
- ],
91
- };
92
-
93
- const { data } = scrubber.scrub(input);
94
- expect(data.users?.[0]?.password).to.equal('[SCRUBBED]');
95
- expect(data.users?.[1]?.password).to.equal('secret2');
96
- });
97
- });
98
-
99
- describe('Pattern-based scrubbing', () => {
100
- it('scrubs SSN patterns in strings', () => {
101
- const scrubber = new Scrubber({
102
- patterns: [/\b\d{3}-\d{2}-\d{4}\b/g],
103
- });
104
-
105
- const input = { message: 'User SSN is 123-45-6789' };
106
- const { data } = scrubber.scrub(input);
107
- expect(data.message).to.contain('[SCRUBBED]');
108
- expect(data.message).not.to.contain('123-45-6789');
109
- });
110
-
111
- it('scrubs email patterns in strings', () => {
112
- const scrubber = new Scrubber({
113
- patterns: [/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g],
114
- });
115
-
116
- const input = { log: 'Auth failed for user@example.com' };
117
- const { data } = scrubber.scrub(input);
118
- expect(data.log).to.contain('[SCRUBBED]');
119
- expect(data.log).not.to.contain('user@example.com');
120
- });
121
-
122
- it('scrubs multiple patterns in same string', () => {
123
- const scrubber = new Scrubber({
124
- patterns: [
125
- /\b\d{3}-\d{2}-\d{4}\b/g, // SSN
126
- /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g, // Email
127
- ],
128
- });
129
-
130
- const input = {
131
- log: 'User bob@example.com has SSN 123-45-6789',
132
- };
133
-
134
- const { data } = scrubber.scrub(input);
135
- expect(data.log).not.to.contain('bob@example.com');
136
- expect(data.log).not.to.contain('123-45-6789');
137
- });
138
- });
139
-
140
- describe('Array handling', () => {
141
- it('scrubs fields across all array items', () => {
142
- const scrubber = new Scrubber({
143
- fields: ['password'],
144
- });
145
-
146
- const users = [
147
- { name: 'bob', password: 'secret' },
148
- { name: 'alice', password: 'hidden' },
149
- ];
150
-
151
- const { data } = scrubber.scrub(users);
152
- expect(data[0]?.password).to.equal('[SCRUBBED]');
153
- expect(data[1]?.password).to.equal('[SCRUBBED]');
154
- expect(data[0]?.name).to.equal('bob');
155
- expect(data[1]?.name).to.equal('alice');
156
- });
157
-
158
- it('handles nested arrays', () => {
159
- const scrubber = new Scrubber({
160
- fields: ['api_key'],
161
- });
162
-
163
- const input = {
164
- teams: [
165
- {
166
- members: [
167
- { name: 'bob', api_key: 'secret1' },
168
- { name: 'alice', api_key: 'secret2' },
169
- ],
170
- },
171
- ],
172
- };
173
-
174
- const { data } = scrubber.scrub(input);
175
- expect(data.teams?.[0]?.members?.[0]?.api_key).to.equal('[SCRUBBED]');
176
- expect(data.teams?.[0]?.members?.[1]?.api_key).to.equal('[SCRUBBED]');
177
- });
178
- });
179
-
180
- describe('Circular reference handling', () => {
181
- it('handles circular references without crashing', () => {
182
- const scrubber = new Scrubber({ fields: [] });
183
- const input: any = { name: 'test' };
184
- input.self = input;
185
-
186
- const { data } = scrubber.scrub(input);
187
- expect(data.self).to.equal('[Circular Reference]');
188
- });
189
-
190
- it('scrubs fields before detecting circular references', () => {
191
- const scrubber = new Scrubber({ fields: ['password'] });
192
- const input: any = { name: 'test', password: 'secret' };
193
- input.self = input;
194
-
195
- const { data } = scrubber.scrub(input);
196
- expect(data.password).to.equal('[SCRUBBED]');
197
- expect(data.self).to.equal('[Circular Reference]');
198
- });
199
-
200
- it('handles nested circular references', () => {
201
- const scrubber = new Scrubber({ fields: [] });
202
- const input: any = { name: 'test', nested: { level: 1 } };
203
- input.self = input;
204
- input.nested.parent = input;
205
-
206
- const { data } = scrubber.scrub(input);
207
- expect(data.self).to.equal('[Circular Reference]');
208
- expect(data.nested.parent).to.equal('[Circular Reference]');
209
- });
210
- });
211
-
212
- describe('Combined modes', () => {
213
- it('applies field + path + pattern scrubbing together', () => {
214
- const scrubber = new Scrubber({
215
- fields: ['api_key'],
216
- paths: ['user.email'],
217
- patterns: [/\b\d{3}-\d{2}-\d{4}\b/g],
218
- });
219
-
220
- const input = {
221
- user: {
222
- email: 'bob@example.com', // Path-based
223
- api_key: 'secret-key-123', // Field-based
224
- },
225
- log: 'SSN: 123-45-6789', // Pattern-based
226
- nested: {
227
- service: {
228
- api_key: 'another-secret', // Field-based (any depth)
229
- },
230
- },
231
- };
232
-
233
- const { data, scrubbedPaths } = scrubber.scrub(input);
234
- expect(data.user?.email).to.equal('[SCRUBBED]');
235
- expect(data.user?.api_key).to.equal('[SCRUBBED]');
236
- expect(data.log).not.to.contain('123-45-6789');
237
- expect(data.nested?.service?.api_key).to.equal('[SCRUBBED]');
238
- expect(scrubbedPaths.length).to.be.greaterThan(0);
239
- });
240
- });
241
-
242
- describe('Scrub result metadata', () => {
243
- it('tracks scrubbed paths', () => {
244
- const scrubber = new Scrubber({
245
- fields: ['password'],
246
- paths: ['user.email'],
247
- });
248
-
249
- const input = {
250
- user: { email: 'test@example.com', password: 'secret' },
251
- };
252
-
253
- const result = scrubber.scrub(input);
254
- expect(result.scrubbed).to.be.true;
255
- expect(result.scrubbedPaths).to.include.members([
256
- 'user.email',
257
- 'user.password',
258
- ]);
259
- });
260
-
261
- it('reports scrubbed=false when nothing was scrubbed', () => {
262
- const scrubber = new Scrubber({
263
- fields: ['password'],
264
- });
265
-
266
- const input = { name: 'Bob', age: 30 };
267
- const result = scrubber.scrub(input);
268
- expect(result.scrubbed).to.be.false;
269
- expect(result.scrubbedPaths).to.have.length(0);
270
- });
271
- });
272
-
273
- describe('Immutability', () => {
274
- it('does not mutate original object', () => {
275
- const scrubber = new Scrubber({
276
- fields: ['password'],
277
- });
278
-
279
- const input = { user: { password: 'secret', name: 'Bob' } };
280
- const original = JSON.stringify(input);
281
-
282
- scrubber.scrub(input);
283
-
284
- expect(JSON.stringify(input)).to.equal(original);
285
- expect(input.user.password).to.equal('secret');
286
- });
287
- });
288
-
289
- describe('Custom replacement text', () => {
290
- it('uses custom replacement string', () => {
291
- const scrubber = new Scrubber({
292
- fields: ['password'],
293
- replacement: '***REDACTED***',
294
- });
295
-
296
- const input = { password: 'secret' };
297
- const { data } = scrubber.scrub(input);
298
- expect(data.password).to.equal('***REDACTED***');
299
- });
300
- });
301
-
302
- describe('Edge cases', () => {
303
- it('handles null values', () => {
304
- const scrubber = new Scrubber({ fields: ['password'] });
305
- const input = { user: null };
306
- const { data } = scrubber.scrub(input);
307
- expect(data.user).to.be.null;
308
- });
309
-
310
- it('handles undefined values', () => {
311
- const scrubber = new Scrubber({ fields: ['password'] });
312
- const input = { user: undefined };
313
- const { data } = scrubber.scrub(input);
314
- expect(data.user).to.be.undefined;
315
- });
316
-
317
- it('handles empty objects', () => {
318
- const scrubber = new Scrubber({ fields: ['password'] });
319
- const input = {};
320
- const { data } = scrubber.scrub(input);
321
- expect(data).to.deep.equal({});
322
- });
323
-
324
- it('handles empty arrays', () => {
325
- const scrubber = new Scrubber({ fields: ['password'] });
326
- const input: any[] = [];
327
- const { data } = scrubber.scrub(input);
328
- expect(data).to.deep.equal([]);
329
- });
330
-
331
- it('handles primitive values', () => {
332
- const scrubber = new Scrubber({ fields: ['password'] });
333
- expect(scrubber.scrub('test').data).to.equal('test');
334
- expect(scrubber.scrub(123).data).to.equal(123);
335
- expect(scrubber.scrub(true).data).to.equal(true);
336
- });
337
-
338
- it('scrubs entire array element by index path', () => {
339
- // Tests lines 76-78: scrubbing entire array element, not just a field
340
- const scrubber = new Scrubber({
341
- paths: ['users[0]', 'items[1]'], // Scrub specific array elements by full path
342
- });
343
-
344
- const input = {
345
- users: [
346
- { name: 'bob', email: 'bob@example.com' }, // Should be scrubbed entirely
347
- { name: 'alice', email: 'alice@example.com' }, // Not scrubbed
348
- ],
349
- items: [
350
- { id: 1, value: 'keep' }, // Not scrubbed
351
- { id: 2, value: 'scrub' }, // Should be scrubbed entirely
352
- ],
353
- };
354
-
355
- const { data } = scrubber.scrub(input);
356
- expect(data.users?.[0]).to.equal('[SCRUBBED]');
357
- expect(data.users?.[1]?.name).to.equal('alice'); // Not scrubbed
358
- expect(data.items?.[0]?.value).to.equal('keep'); // Not scrubbed
359
- expect(data.items?.[1]).to.equal('[SCRUBBED]'); // Entire element scrubbed
360
- });
361
-
362
- it('scrubs array index across all arrays', () => {
363
- // Tests that index-only paths (e.g., '0') scrub that index in ALL arrays
364
- const scrubber = new Scrubber({
365
- paths: ['1'], // Scrub index 1 of ANY array
366
- });
367
-
368
- const input = {
369
- users: [
370
- { name: 'bob' }, // Index 0 - not scrubbed
371
- { name: 'alice' }, // Index 1 - scrubbed
372
- { name: 'charlie' }, // Index 2 - not scrubbed
373
- ],
374
- teams: [
375
- { id: 'team-a' }, // Index 0 - not scrubbed
376
- { id: 'team-b' }, // Index 1 - scrubbed
377
- ],
378
- };
379
-
380
- const { data } = scrubber.scrub(input);
381
- expect(data.users?.[0]?.name).to.equal('bob');
382
- expect(data.users?.[1]).to.equal('[SCRUBBED]'); // Scrubbed by index
383
- expect(data.users?.[2]?.name).to.equal('charlie');
384
- expect(data.teams?.[0]?.id).to.equal('team-a');
385
- expect(data.teams?.[1]).to.equal('[SCRUBBED]'); // Scrubbed by index
386
- });
387
-
388
- it('handles deeply nested objects (10+ levels)', () => {
389
- // Validates discovery doc requirement: "Scrubs nested objects 10+ levels deep"
390
- const scrubber = new Scrubber({
391
- fields: ['secret'],
392
- });
393
-
394
- // Build a 15-level deep object
395
- const input: any = { level: 1 };
396
- let current = input;
397
- for (let i = 2; i <= 15; i++) {
398
- current.nested = { level: i };
399
- current = current.nested;
400
- }
401
- // Add secret at the deepest level
402
- current.secret = 'deep-secret';
403
- current.public = 'visible';
404
-
405
- const { data } = scrubber.scrub(input);
406
-
407
- // Navigate to the deepest level
408
- let deepest: any = data;
409
- for (let i = 1; i < 15; i++) {
410
- expect(deepest.level).to.equal(i);
411
- deepest = deepest.nested;
412
- }
413
-
414
- // Verify scrubbing worked at 15 levels deep
415
- expect(deepest.level).to.equal(15);
416
- expect(deepest.secret).to.equal('[SCRUBBED]');
417
- expect(deepest.public).to.equal('visible');
418
- });
419
-
420
- it('handles circular references in deep clone fallback (arrays)', () => {
421
- // Tests lines 166-172: circular reference deep clone for arrays
422
- const scrubber = new Scrubber({
423
- fields: ['password'],
424
- });
425
-
426
- // Create an object with circular references that will trigger the fallback clone
427
- const parent: any = { name: 'parent', password: 'secret' };
428
- const child1: any = { name: 'child1', items: [] };
429
- const child2 = { name: 'child2', password: 'hidden' };
430
-
431
- // Create circular reference in an array
432
- child1.items = [child2, parent]; // Array contains parent
433
- parent.children = [child1]; // Parent contains array with circular ref
434
-
435
- const { data } = scrubber.scrub(parent);
436
-
437
- // Verify scrubbing happened
438
- expect(data.password).to.equal('[SCRUBBED]');
439
- expect(data.children?.[0]?.name).to.equal('child1');
440
-
441
- // Verify circular reference was handled in the array
442
- expect(data.children?.[0]?.items?.[1]).to.equal('[Circular Reference]');
443
- });
444
-
445
- it('handles arrays with circular self-reference', () => {
446
- // Additional test for circular array deep cloning
447
- const scrubber = new Scrubber({
448
- fields: ['token'],
449
- });
450
-
451
- const obj: any = {
452
- token: 'secret-token',
453
- list: [{ name: 'item1' }],
454
- };
455
- // Array references the parent object
456
- obj.list.push(obj);
457
-
458
- const { data } = scrubber.scrub(obj);
459
-
460
- expect(data.token).to.equal('[SCRUBBED]');
461
- expect(data.list?.[0]?.name).to.equal('item1');
462
- expect(data.list?.[1]).to.equal('[Circular Reference]');
463
- });
464
- });
465
- });