@safeaccess/inline 0.1.1 → 0.1.2
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.
- package/.gitattributes +1 -1
- package/CHANGELOG.md +10 -5
- package/LICENSE +1 -1
- package/README.md +56 -14
- package/dist/accessors/abstract-accessor.d.ts +22 -10
- package/dist/accessors/abstract-accessor.js +21 -8
- package/dist/accessors/abstract-integration-accessor.d.ts +22 -0
- package/dist/accessors/abstract-integration-accessor.js +23 -0
- package/dist/accessors/formats/any-accessor.d.ts +10 -8
- package/dist/accessors/formats/any-accessor.js +9 -8
- package/dist/accessors/formats/array-accessor.d.ts +2 -0
- package/dist/accessors/formats/array-accessor.js +2 -0
- package/dist/accessors/formats/env-accessor.d.ts +2 -0
- package/dist/accessors/formats/env-accessor.js +2 -0
- package/dist/accessors/formats/ini-accessor.d.ts +2 -0
- package/dist/accessors/formats/ini-accessor.js +2 -0
- package/dist/accessors/formats/json-accessor.d.ts +2 -0
- package/dist/accessors/formats/json-accessor.js +2 -0
- package/dist/accessors/formats/ndjson-accessor.d.ts +2 -0
- package/dist/accessors/formats/ndjson-accessor.js +2 -0
- package/dist/accessors/formats/object-accessor.d.ts +2 -0
- package/dist/accessors/formats/object-accessor.js +2 -0
- package/dist/accessors/formats/xml-accessor.d.ts +2 -0
- package/dist/accessors/formats/xml-accessor.js +2 -0
- package/dist/accessors/formats/yaml-accessor.d.ts +3 -1
- package/dist/accessors/formats/yaml-accessor.js +4 -2
- package/dist/cache/simple-path-cache.d.ts +51 -0
- package/dist/cache/simple-path-cache.js +72 -0
- package/dist/contracts/accessors-interface.d.ts +2 -0
- package/dist/contracts/factory-accessors-interface.d.ts +2 -0
- package/dist/contracts/filter-evaluator-interface.d.ts +28 -0
- package/dist/contracts/filter-evaluator-interface.js +1 -0
- package/dist/contracts/parse-integration-interface.d.ts +2 -0
- package/dist/contracts/parser-interface.d.ts +92 -0
- package/dist/contracts/parser-interface.js +1 -0
- package/dist/contracts/path-cache-interface.d.ts +7 -6
- package/dist/contracts/readable-accessors-interface.d.ts +11 -6
- package/dist/contracts/security-guard-interface.d.ts +2 -0
- package/dist/contracts/security-parser-interface.d.ts +2 -0
- package/dist/contracts/validatable-parser-interface.d.ts +59 -0
- package/dist/contracts/validatable-parser-interface.js +1 -0
- package/dist/contracts/writable-accessors-interface.d.ts +5 -0
- package/dist/core/accessor-factory.d.ts +124 -0
- package/dist/core/accessor-factory.js +157 -0
- package/dist/core/dot-notation-parser.d.ts +34 -5
- package/dist/core/dot-notation-parser.js +51 -10
- package/dist/core/inline-builder-accessor.d.ts +82 -0
- package/dist/core/inline-builder-accessor.js +107 -0
- package/dist/exceptions/accessor-exception.d.ts +9 -0
- package/dist/exceptions/accessor-exception.js +9 -0
- package/dist/exceptions/invalid-format-exception.d.ts +5 -0
- package/dist/exceptions/invalid-format-exception.js +5 -0
- package/dist/exceptions/parser-exception.d.ts +4 -0
- package/dist/exceptions/parser-exception.js +4 -0
- package/dist/exceptions/path-not-found-exception.d.ts +4 -0
- package/dist/exceptions/path-not-found-exception.js +4 -0
- package/dist/exceptions/readonly-violation-exception.d.ts +4 -0
- package/dist/exceptions/readonly-violation-exception.js +4 -0
- package/dist/exceptions/security-exception.d.ts +6 -0
- package/dist/exceptions/security-exception.js +6 -0
- package/dist/exceptions/unsupported-type-exception.d.ts +4 -0
- package/dist/exceptions/unsupported-type-exception.js +4 -0
- package/dist/exceptions/yaml-parse-exception.d.ts +4 -0
- package/dist/exceptions/yaml-parse-exception.js +4 -0
- package/dist/index.js +2 -1
- package/dist/inline.d.ts +22 -56
- package/dist/inline.js +39 -111
- package/dist/parser/xml-parser.js +23 -10
- package/dist/parser/yaml-parser.d.ts +54 -7
- package/dist/parser/yaml-parser.js +268 -51
- package/dist/path-query/segment-filter-parser.d.ts +142 -0
- package/dist/path-query/segment-filter-parser.js +384 -0
- package/dist/path-query/segment-parser.d.ts +98 -0
- package/dist/path-query/segment-parser.js +283 -0
- package/dist/path-query/segment-path-resolver.d.ts +149 -0
- package/dist/path-query/segment-path-resolver.js +351 -0
- package/dist/path-query/segment-type.d.ts +85 -0
- package/dist/path-query/segment-type.js +35 -0
- package/dist/security/forbidden-keys.d.ts +2 -2
- package/dist/security/forbidden-keys.js +5 -5
- package/dist/security/security-guard.d.ts +3 -1
- package/dist/security/security-guard.js +5 -2
- package/dist/security/security-parser.d.ts +10 -1
- package/dist/security/security-parser.js +10 -1
- package/dist/type-format.d.ts +2 -0
- package/dist/type-format.js +2 -0
- package/package.json +11 -3
- package/src/accessors/abstract-accessor.ts +23 -19
- package/src/accessors/abstract-integration-accessor.ts +27 -0
- package/src/accessors/formats/any-accessor.ts +11 -11
- package/src/accessors/formats/array-accessor.ts +2 -0
- package/src/accessors/formats/env-accessor.ts +2 -0
- package/src/accessors/formats/ini-accessor.ts +2 -0
- package/src/accessors/formats/json-accessor.ts +2 -0
- package/src/accessors/formats/ndjson-accessor.ts +2 -0
- package/src/accessors/formats/object-accessor.ts +2 -0
- package/src/accessors/formats/xml-accessor.ts +2 -0
- package/src/accessors/formats/yaml-accessor.ts +4 -2
- package/src/cache/simple-path-cache.ts +77 -0
- package/src/contracts/accessors-interface.ts +2 -0
- package/src/contracts/factory-accessors-interface.ts +2 -0
- package/src/contracts/filter-evaluator-interface.ts +30 -0
- package/src/contracts/parse-integration-interface.ts +2 -0
- package/src/contracts/parser-interface.ts +114 -0
- package/src/contracts/path-cache-interface.ts +8 -6
- package/src/contracts/readable-accessors-interface.ts +11 -6
- package/src/contracts/security-guard-interface.ts +2 -0
- package/src/contracts/security-parser-interface.ts +2 -0
- package/src/contracts/validatable-parser-interface.ts +64 -0
- package/src/contracts/writable-accessors-interface.ts +5 -0
- package/src/core/accessor-factory.ts +173 -0
- package/src/core/dot-notation-parser.ts +74 -11
- package/src/core/inline-builder-accessor.ts +163 -0
- package/src/exceptions/accessor-exception.ts +9 -0
- package/src/exceptions/invalid-format-exception.ts +5 -0
- package/src/exceptions/parser-exception.ts +4 -0
- package/src/exceptions/path-not-found-exception.ts +4 -0
- package/src/exceptions/readonly-violation-exception.ts +4 -0
- package/src/exceptions/security-exception.ts +6 -0
- package/src/exceptions/unsupported-type-exception.ts +4 -0
- package/src/exceptions/yaml-parse-exception.ts +4 -0
- package/src/index.ts +3 -1
- package/src/inline.ts +42 -120
- package/src/parser/xml-parser.ts +31 -10
- package/src/parser/yaml-parser.ts +310 -45
- package/src/path-query/segment-filter-parser.ts +444 -0
- package/src/path-query/segment-parser.ts +321 -0
- package/src/path-query/segment-path-resolver.ts +521 -0
- package/src/path-query/segment-type.ts +82 -0
- package/src/security/forbidden-keys.ts +5 -5
- package/src/security/security-guard.ts +7 -2
- package/src/security/security-parser.ts +18 -3
- package/src/type-format.ts +2 -0
- package/stryker.config.json +8 -10
- package/tests/accessors/abstract-accessor.test.ts +217 -0
- package/tests/accessors/abstract-integration-accessor.test.ts +37 -0
- package/tests/accessors/formats/any-accessor.test.ts +57 -0
- package/tests/accessors/formats/array-accessor.test.ts +42 -0
- package/tests/accessors/formats/env-accessor.test.ts +103 -0
- package/tests/accessors/formats/ini-accessor.test.ts +186 -0
- package/tests/accessors/{json-accessor.test.ts → formats/json-accessor.test.ts} +6 -6
- package/tests/accessors/formats/ndjson-accessor.test.ts +49 -0
- package/tests/accessors/formats/object-accessor.test.ts +172 -0
- package/tests/accessors/formats/xml-accessor.test.ts +162 -0
- package/tests/accessors/formats/yaml-accessor.test.ts +36 -0
- package/tests/cache/simple-path-cache.test.ts +168 -0
- package/tests/core/accessor-factory.test.ts +157 -0
- package/tests/core/dot-notation-parser-edge-cases.test.ts +415 -0
- package/tests/core/dot-notation-parser.test.ts +0 -288
- package/tests/core/inline-builder-accessor.test.ts +114 -0
- package/tests/exceptions/accessor-exception.test.ts +28 -0
- package/tests/exceptions/invalid-format-exception.test.ts +31 -0
- package/tests/exceptions/path-not-found-exception.test.ts +33 -0
- package/tests/exceptions/readonly-violation-exception.test.ts +35 -0
- package/tests/exceptions/security-exception.test.ts +33 -0
- package/tests/exceptions/unsupported-type-exception.test.ts +33 -0
- package/tests/exceptions/yaml-parse-exception.test.ts +38 -0
- package/tests/mocks/fake-path-cache.ts +4 -3
- package/tests/parity-from.test.ts +118 -0
- package/tests/parity.test.ts +227 -10
- package/tests/parser/xml-parser-mutations.test.ts +579 -0
- package/tests/parser/xml-parser-scanner.test.ts +332 -0
- package/tests/parser/xml-parser.test.ts +10 -334
- package/tests/parser/yaml-parser-mutations.test.ts +750 -0
- package/tests/parser/yaml-parser.test.ts +844 -18
- package/tests/path-query/segment-filter-parser-mutations.test.ts +735 -0
- package/tests/path-query/segment-filter-parser.test.ts +1091 -0
- package/tests/path-query/segment-parser-mutations.test.ts +539 -0
- package/tests/path-query/segment-parser.test.ts +606 -0
- package/tests/path-query/segment-path-resolver-mutations.test.ts +626 -0
- package/tests/path-query/segment-path-resolver.test.ts +1009 -0
- package/tests/security/security-guard-advanced.test.ts +413 -0
- package/tests/security/security-guard-forbidden-keys.test.ts +87 -0
- package/tests/security/security-guard.test.ts +3 -484
- package/tests/security/security-parser.test.ts +18 -14
- package/vitest.config.ts +3 -3
- package/benchmarks/get.bench.ts +0 -26
- package/benchmarks/parse.bench.ts +0 -41
- package/tests/accessors/accessors.test.ts +0 -1017
|
@@ -2,95 +2,6 @@ import { describe, expect, it } from 'vitest';
|
|
|
2
2
|
import { SecurityGuard } from '../../src/security/security-guard.js';
|
|
3
3
|
import { SecurityException } from '../../src/exceptions/security-exception.js';
|
|
4
4
|
|
|
5
|
-
// Every key in DEFAULT_FORBIDDEN_KEYS must block individually.
|
|
6
|
-
// This prevents Stryker StringLiteral mutants (each key mutated to "") from surviving.
|
|
7
|
-
describe(`${SecurityGuard.name} > all individual forbidden keys`, () => {
|
|
8
|
-
const prototypePollutiondVectors = [
|
|
9
|
-
'__proto__',
|
|
10
|
-
'constructor',
|
|
11
|
-
'prototype',
|
|
12
|
-
];
|
|
13
|
-
const jsLegacyPrototype = [
|
|
14
|
-
'__definegetter__',
|
|
15
|
-
'__definesetter__',
|
|
16
|
-
'__lookupgetter__',
|
|
17
|
-
'__lookupsetter__',
|
|
18
|
-
];
|
|
19
|
-
const objectShadowKeys = ['hasOwnProperty'];
|
|
20
|
-
const nodejsPathGlobals = ['__dirname', '__filename'];
|
|
21
|
-
const exactStreamSchemes = [
|
|
22
|
-
'file://',
|
|
23
|
-
'http://',
|
|
24
|
-
'https://',
|
|
25
|
-
'ftp://',
|
|
26
|
-
'data:',
|
|
27
|
-
'data://',
|
|
28
|
-
'javascript:',
|
|
29
|
-
'blob:',
|
|
30
|
-
'ws://',
|
|
31
|
-
'wss://',
|
|
32
|
-
'node:',
|
|
33
|
-
];
|
|
34
|
-
|
|
35
|
-
const all = [
|
|
36
|
-
...prototypePollutiondVectors,
|
|
37
|
-
...jsLegacyPrototype,
|
|
38
|
-
...objectShadowKeys,
|
|
39
|
-
...nodejsPathGlobals,
|
|
40
|
-
...exactStreamSchemes,
|
|
41
|
-
];
|
|
42
|
-
|
|
43
|
-
for (const key of all) {
|
|
44
|
-
it(`blocks "${key}" as forbidden`, () => {
|
|
45
|
-
const guard = new SecurityGuard();
|
|
46
|
-
expect(guard.isForbiddenKey(key)).toBe(true);
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// Stream-wrapper prefix matching (full URIs, not just bare schemes already in the set above)
|
|
51
|
-
const streamWrapperUris = [
|
|
52
|
-
'http://evil.com/payload',
|
|
53
|
-
'https://attacker.com/exploit',
|
|
54
|
-
'ftp://server/file.txt',
|
|
55
|
-
'file:///etc/passwd',
|
|
56
|
-
'data:',
|
|
57
|
-
'data://text/plain;base64,aGVsbG8=',
|
|
58
|
-
'data:text/html,<script>alert(1)</script>',
|
|
59
|
-
'javascript:alert(1)',
|
|
60
|
-
'blob:https://example.com/file',
|
|
61
|
-
'ws://attacker.com/socket',
|
|
62
|
-
'wss://attacker.com/socket',
|
|
63
|
-
'node:child_process',
|
|
64
|
-
];
|
|
65
|
-
|
|
66
|
-
for (const uri of streamWrapperUris) {
|
|
67
|
-
it(`blocks stream wrapper URI "${uri}" as forbidden`, () => {
|
|
68
|
-
const guard = new SecurityGuard();
|
|
69
|
-
expect(guard.isForbiddenKey(uri)).toBe(true);
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
describe(`${SecurityGuard.name} > sanitize with null check`, () => {
|
|
75
|
-
it('preserves non-null primitive values in sanitize', () => {
|
|
76
|
-
const guard = new SecurityGuard();
|
|
77
|
-
const result = guard.sanitize({ name: 'Alice', count: 0, flag: false, empty: '' });
|
|
78
|
-
expect(result).toEqual({ name: 'Alice', count: 0, flag: false, empty: '' });
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it('preserves null values in sanitize (only removes forbidden keys)', () => {
|
|
82
|
-
const guard = new SecurityGuard();
|
|
83
|
-
const result = guard.sanitize({ key: null });
|
|
84
|
-
expect(result).toEqual({ key: null });
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it('error message on sanitize depth contains the depth limit', () => {
|
|
88
|
-
const guard = new SecurityGuard(1);
|
|
89
|
-
const deep = { a: { b: { c: 'deep' } } };
|
|
90
|
-
expect(() => guard.sanitize(deep)).toThrow(/Recursion depth \d+ exceeds maximum/);
|
|
91
|
-
});
|
|
92
|
-
});
|
|
93
|
-
|
|
94
5
|
describe(SecurityGuard.name, () => {
|
|
95
6
|
it('allows a safe key', () => {
|
|
96
7
|
const guard = new SecurityGuard();
|
|
@@ -163,7 +74,9 @@ describe(`${SecurityGuard.name} > assertSafeKey`, () => {
|
|
|
163
74
|
|
|
164
75
|
it('error message contains the forbidden key name', () => {
|
|
165
76
|
const guard = new SecurityGuard();
|
|
166
|
-
expect(() => guard.assertSafeKey('__proto__')).toThrow(
|
|
77
|
+
expect(() => guard.assertSafeKey('__proto__')).toThrow(
|
|
78
|
+
"Forbidden key '__proto__' detected.",
|
|
79
|
+
);
|
|
167
80
|
});
|
|
168
81
|
});
|
|
169
82
|
|
|
@@ -250,397 +163,3 @@ describe(`${SecurityGuard.name} > extraForbiddenKeys`, () => {
|
|
|
250
163
|
expect(guard.isForbiddenKey('constructor')).toBe(true);
|
|
251
164
|
});
|
|
252
165
|
});
|
|
253
|
-
|
|
254
|
-
describe(`${SecurityGuard.name} > assertSafeKeys with non-object input`, () => {
|
|
255
|
-
// Kills line 212 `typeof data !== 'object' || data === null` early return:
|
|
256
|
-
// passing a number (non-object) should return early without throwing.
|
|
257
|
-
it('returns early for number input without throwing', () => {
|
|
258
|
-
const guard = new SecurityGuard();
|
|
259
|
-
expect(() => guard.assertSafeKeys(42 as unknown as Record<string, unknown>)).not.toThrow();
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
it('returns early for boolean input without throwing', () => {
|
|
263
|
-
const guard = new SecurityGuard();
|
|
264
|
-
expect(() => guard.assertSafeKeys(true as unknown as Record<string, unknown>)).not.toThrow();
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
it('returns early for string input without throwing', () => {
|
|
268
|
-
const guard = new SecurityGuard();
|
|
269
|
-
expect(() => guard.assertSafeKeys('hello' as unknown as Record<string, unknown>)).not.toThrow();
|
|
270
|
-
});
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
describe(`${SecurityGuard.name} > sanitize array passthrough`, () => {
|
|
274
|
-
// Array values must be preserved safely (primitives kept, objects inside sanitized).
|
|
275
|
-
it('preserves primitive array values in sanitize', () => {
|
|
276
|
-
const guard = new SecurityGuard();
|
|
277
|
-
const result = guard.sanitize({ items: ['a', 'b', 'c'] });
|
|
278
|
-
expect(result).toEqual({ items: ['a', 'b', 'c'] });
|
|
279
|
-
expect(Array.isArray(result['items'])).toBe(true);
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
it('preserves nested primitive arrays in sanitize', () => {
|
|
283
|
-
const guard = new SecurityGuard();
|
|
284
|
-
const result = guard.sanitize({ matrix: [[1, 2], [3, 4]] });
|
|
285
|
-
expect(result).toEqual({ matrix: [[1, 2], [3, 4]] });
|
|
286
|
-
});
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
describe(`${SecurityGuard.name} > depth boundary (assertSafeKeys)`, () => {
|
|
290
|
-
it('does not throw at exactly maxDepth nesting level', () => {
|
|
291
|
-
// maxDepth=1: root is depth 0; value of root key is depth 1; that value is a number → returns early
|
|
292
|
-
const guard = new SecurityGuard(1);
|
|
293
|
-
// depth 0 at root, depth 1 at 'a' value → value is object, depth 2 > 1 → throws
|
|
294
|
-
// So with maxDepth=1, we need depth=1 to not throw → use flat object { a: 1 }
|
|
295
|
-
expect(() => guard.assertSafeKeys({ a: 1 })).not.toThrow();
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
it('throws at exactly maxDepth+1 nesting level', () => {
|
|
299
|
-
const guard = new SecurityGuard(0);
|
|
300
|
-
// root is depth 0; 0 > 0 is false → iterates; 'a' value is object at depth 1; 1 > 0 → throws
|
|
301
|
-
expect(() => guard.assertSafeKeys({ a: { b: 1 } })).toThrow(SecurityException);
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
it('does not throw when data has exactly maxDepth nested objects', () => {
|
|
305
|
-
const guard = new SecurityGuard(2);
|
|
306
|
-
// depth 0: root; depth 1: 'a' value; depth 2: 'b' value is a number → returns early
|
|
307
|
-
expect(() => guard.assertSafeKeys({ a: { b: { c: 42 } } })).not.toThrow();
|
|
308
|
-
});
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
describe(`${SecurityGuard.name} > constructor — NaN/Infinity clamping (SEC-020)`, () => {
|
|
312
|
-
it('clamps NaN maxDepth to default 512 so depth guard still fires', () => {
|
|
313
|
-
const guard = new SecurityGuard(NaN);
|
|
314
|
-
expect(guard.maxDepth).toBe(512);
|
|
315
|
-
});
|
|
316
|
-
|
|
317
|
-
it('clamps Infinity maxDepth to default 512', () => {
|
|
318
|
-
const guard = new SecurityGuard(Infinity);
|
|
319
|
-
expect(guard.maxDepth).toBe(512);
|
|
320
|
-
});
|
|
321
|
-
|
|
322
|
-
it('assertSafeKeys still enforces depth after NaN maxDepth is clamped to 512', () => {
|
|
323
|
-
const guard = new SecurityGuard(NaN);
|
|
324
|
-
// With 512 levels clamped, a 1-level object is safe
|
|
325
|
-
expect(() => guard.assertSafeKeys({ safe: 1 })).not.toThrow();
|
|
326
|
-
});
|
|
327
|
-
});
|
|
328
|
-
|
|
329
|
-
describe(`${SecurityGuard.name} > depth boundary (sanitize)`, () => {
|
|
330
|
-
it('does not throw sanitize at exactly maxDepth nesting level', () => {
|
|
331
|
-
const guard = new SecurityGuard(1);
|
|
332
|
-
// depth 0: root; sanitizing {a: 1} → 'a' value is number → recurse depth 1; number exits early
|
|
333
|
-
expect(() => guard.sanitize({ a: 1 })).not.toThrow();
|
|
334
|
-
});
|
|
335
|
-
|
|
336
|
-
it('throws sanitize at exactly maxDepth+1 nesting level', () => {
|
|
337
|
-
const guard = new SecurityGuard(0);
|
|
338
|
-
expect(() => guard.sanitize({ a: { b: 1 } })).toThrow(SecurityException);
|
|
339
|
-
});
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
describe(`${SecurityGuard.name} > extraForbiddenKeys constructor default`, () => {
|
|
343
|
-
it('default empty array for extraForbiddenKeys produces only defaults', () => {
|
|
344
|
-
// ArrayDeclaration survivor: default [] must not add any extra keys
|
|
345
|
-
const guardDefault = new SecurityGuard(512);
|
|
346
|
-
const guardExplicitEmpty = new SecurityGuard(512, []);
|
|
347
|
-
expect(guardDefault.isForbiddenKey('__proto__')).toBe(true);
|
|
348
|
-
expect(guardExplicitEmpty.isForbiddenKey('__proto__')).toBe(true);
|
|
349
|
-
// A custom key not in defaults must NOT be blocked by either
|
|
350
|
-
expect(guardDefault.isForbiddenKey('my_safe_key')).toBe(false);
|
|
351
|
-
expect(guardExplicitEmpty.isForbiddenKey('my_safe_key')).toBe(false);
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
// Kills ArrayDeclaration survivor: default extraForbiddenKeys=[] must not add foreign keys
|
|
355
|
-
it('default guard does not block an arbitrary non-default key', () => {
|
|
356
|
-
const guard = new SecurityGuard();
|
|
357
|
-
// The Stryker ArrayDeclaration mutant replaces [] with ["Stryker was here"]
|
|
358
|
-
// this key would be blocked with mutant but not with default
|
|
359
|
-
expect(guard.isForbiddenKey('regular_user_key')).toBe(false);
|
|
360
|
-
expect(guard.isForbiddenKey('data')).toBe(false);
|
|
361
|
-
expect(guard.isForbiddenKey('config')).toBe(false);
|
|
362
|
-
});
|
|
363
|
-
|
|
364
|
-
// Kills ConditionalExpression survivor at line 120: if (extraForbiddenKeys.length === 0) → if (false)
|
|
365
|
-
// When [] is passed, must use DEFAULT_FORBIDDEN_KEYS directly (not build a new Set)
|
|
366
|
-
it('uses DEFAULT_FORBIDDEN_KEYS set directly when empty array passed (not combined)', () => {
|
|
367
|
-
const guard = new SecurityGuard(512, []);
|
|
368
|
-
// Default forbidden keys must still work
|
|
369
|
-
expect(guard.isForbiddenKey('__proto__')).toBe(true);
|
|
370
|
-
// A key outside defaults must not be blocked
|
|
371
|
-
expect(guard.isForbiddenKey('normalfield')).toBe(false);
|
|
372
|
-
});
|
|
373
|
-
});
|
|
374
|
-
|
|
375
|
-
describe(`${SecurityGuard.name} > sanitize recursion into arrays`, () => {
|
|
376
|
-
it('strips forbidden keys from objects nested inside an array', () => {
|
|
377
|
-
const guard = new SecurityGuard();
|
|
378
|
-
const data = { users: [{ name: 'Alice', __proto__: 'bad' }] };
|
|
379
|
-
const result = guard.sanitize(data);
|
|
380
|
-
expect(result).toEqual({ users: [{ name: 'Alice' }] });
|
|
381
|
-
});
|
|
382
|
-
|
|
383
|
-
it('strips forbidden keys from deeply nested arrays of objects', () => {
|
|
384
|
-
const guard = new SecurityGuard();
|
|
385
|
-
const data = { matrix: [[{ constructor: 'bad', ok: 1 }]] };
|
|
386
|
-
const result = guard.sanitize(data);
|
|
387
|
-
expect(result).toEqual({ matrix: [[{ ok: 1 }]] });
|
|
388
|
-
});
|
|
389
|
-
|
|
390
|
-
it('preserves safe objects inside arrays', () => {
|
|
391
|
-
const guard = new SecurityGuard();
|
|
392
|
-
const data = { items: [{ name: 'Alice' }, { name: 'Bob' }] };
|
|
393
|
-
const result = guard.sanitize(data);
|
|
394
|
-
expect(result).toEqual({ items: [{ name: 'Alice' }, { name: 'Bob' }] });
|
|
395
|
-
});
|
|
396
|
-
|
|
397
|
-
it('preserves primitive values inside arrays', () => {
|
|
398
|
-
const guard = new SecurityGuard();
|
|
399
|
-
const data = { tags: ['a', 'b', 'c'] };
|
|
400
|
-
const result = guard.sanitize(data);
|
|
401
|
-
expect(result).toEqual({ tags: ['a', 'b', 'c'] });
|
|
402
|
-
});
|
|
403
|
-
|
|
404
|
-
it('strips stream wrapper keys from objects inside arrays', () => {
|
|
405
|
-
const guard = new SecurityGuard();
|
|
406
|
-
const data = { rows: [{ 'javascript:alert(1)': 'bad', value: 1 }] };
|
|
407
|
-
const result = guard.sanitize(data);
|
|
408
|
-
expect(result).toEqual({ rows: [{ value: 1 }] });
|
|
409
|
-
});
|
|
410
|
-
|
|
411
|
-
it('handles mixed arrays with objects and primitives', () => {
|
|
412
|
-
const guard = new SecurityGuard();
|
|
413
|
-
const data = { list: ['safe', { __proto__: 'bad', ok: true }, 42] };
|
|
414
|
-
const result = guard.sanitize(data);
|
|
415
|
-
expect(result).toEqual({ list: ['safe', { ok: true }, 42] });
|
|
416
|
-
});
|
|
417
|
-
|
|
418
|
-
it('throws SecurityException when array nesting exceeds maxDepth', () => {
|
|
419
|
-
const guard = new SecurityGuard(1);
|
|
420
|
-
const data = { a: [{ b: [{ c: 'deep' }] }] };
|
|
421
|
-
expect(() => guard.sanitize(data)).toThrow(SecurityException);
|
|
422
|
-
});
|
|
423
|
-
});
|
|
424
|
-
|
|
425
|
-
describe(`${SecurityGuard.name} > prototype pollution keys`, () => {
|
|
426
|
-
it('returns true for __proto__ as forbidden', () => {
|
|
427
|
-
const guard = new SecurityGuard();
|
|
428
|
-
expect(guard.isForbiddenKey('__proto__')).toBe(true);
|
|
429
|
-
});
|
|
430
|
-
|
|
431
|
-
it('returns true for constructor as forbidden', () => {
|
|
432
|
-
const guard = new SecurityGuard();
|
|
433
|
-
expect(guard.isForbiddenKey('constructor')).toBe(true);
|
|
434
|
-
});
|
|
435
|
-
|
|
436
|
-
it('returns true for prototype as forbidden', () => {
|
|
437
|
-
const guard = new SecurityGuard();
|
|
438
|
-
expect(guard.isForbiddenKey('prototype')).toBe(true);
|
|
439
|
-
});
|
|
440
|
-
|
|
441
|
-
it('assertSafeKey throws SecurityException for __proto__', () => {
|
|
442
|
-
const guard = new SecurityGuard();
|
|
443
|
-
expect(() => guard.assertSafeKey('__proto__')).toThrow(SecurityException);
|
|
444
|
-
});
|
|
445
|
-
|
|
446
|
-
it('assertSafeKey throws SecurityException for constructor', () => {
|
|
447
|
-
const guard = new SecurityGuard();
|
|
448
|
-
expect(() => guard.assertSafeKey('constructor')).toThrow(SecurityException);
|
|
449
|
-
});
|
|
450
|
-
|
|
451
|
-
it('assertSafeKeys throws for nested __proto__ key', () => {
|
|
452
|
-
const guard = new SecurityGuard();
|
|
453
|
-
// Use JSON.parse to create an own property named __proto__ (object literals set the prototype instead)
|
|
454
|
-
const data = JSON.parse('{"safe": {"__proto__": {"isAdmin": true}}}') as Record<string, unknown>;
|
|
455
|
-
expect(() => guard.assertSafeKeys(data)).toThrow(
|
|
456
|
-
SecurityException,
|
|
457
|
-
);
|
|
458
|
-
});
|
|
459
|
-
|
|
460
|
-
it('sanitize removes __proto__ key', () => {
|
|
461
|
-
const guard = new SecurityGuard();
|
|
462
|
-
// Use JSON.parse to create an own property named __proto__
|
|
463
|
-
const data = JSON.parse('{"__proto__": {"isAdmin": true}, "name": "Alice"}') as Record<string, unknown>;
|
|
464
|
-
const result = guard.sanitize(data);
|
|
465
|
-
expect(result).toEqual({ name: 'Alice' });
|
|
466
|
-
});
|
|
467
|
-
|
|
468
|
-
it('sanitize removes constructor key from nested objects', () => {
|
|
469
|
-
const guard = new SecurityGuard();
|
|
470
|
-
const result = guard.sanitize({ user: { constructor: 'bad', name: 'Alice' } });
|
|
471
|
-
expect(result).toEqual({ user: { name: 'Alice' } });
|
|
472
|
-
});
|
|
473
|
-
|
|
474
|
-
it('returns true for __PROTO__ (uppercase, case-insensitive magic match)', () => {
|
|
475
|
-
const guard = new SecurityGuard();
|
|
476
|
-
expect(guard.isForbiddenKey('__PROTO__')).toBe(true);
|
|
477
|
-
});
|
|
478
|
-
});
|
|
479
|
-
|
|
480
|
-
describe(`${SecurityGuard.name} > case-insensitive stream wrapper prefix`, () => {
|
|
481
|
-
it('returns true for uppercase JAVASCRIPT: protocol URI', () => {
|
|
482
|
-
const guard = new SecurityGuard();
|
|
483
|
-
expect(guard.isForbiddenKey('JAVASCRIPT:alert(1)')).toBe(true);
|
|
484
|
-
});
|
|
485
|
-
|
|
486
|
-
it('returns true for mixed-case Javascript: protocol URI', () => {
|
|
487
|
-
const guard = new SecurityGuard();
|
|
488
|
-
expect(guard.isForbiddenKey('Javascript:void(0)')).toBe(true);
|
|
489
|
-
});
|
|
490
|
-
|
|
491
|
-
it('returns true for uppercase HTTP:// stream wrapper URI', () => {
|
|
492
|
-
const guard = new SecurityGuard();
|
|
493
|
-
expect(guard.isForbiddenKey('HTTP://evil.com/data')).toBe(true);
|
|
494
|
-
});
|
|
495
|
-
|
|
496
|
-
it('returns true for uppercase FILE:// stream wrapper URI', () => {
|
|
497
|
-
const guard = new SecurityGuard();
|
|
498
|
-
expect(guard.isForbiddenKey('FILE:///etc/passwd')).toBe(true);
|
|
499
|
-
});
|
|
500
|
-
|
|
501
|
-
it('assertSafeKey throws SecurityException for HTTP://host', () => {
|
|
502
|
-
const guard = new SecurityGuard();
|
|
503
|
-
expect(() => guard.assertSafeKey('HTTP://attacker.com/payload')).toThrow(
|
|
504
|
-
SecurityException,
|
|
505
|
-
);
|
|
506
|
-
});
|
|
507
|
-
|
|
508
|
-
it('returns false for a word starting with node but without scheme', () => {
|
|
509
|
-
const guard = new SecurityGuard();
|
|
510
|
-
expect(guard.isForbiddenKey('node_modules')).toBe(false);
|
|
511
|
-
});
|
|
512
|
-
|
|
513
|
-
it('returns true for DATA:// uppercase stream wrapper', () => {
|
|
514
|
-
const guard = new SecurityGuard();
|
|
515
|
-
expect(guard.isForbiddenKey('DATA://text/plain;base64,abc')).toBe(true);
|
|
516
|
-
});
|
|
517
|
-
});
|
|
518
|
-
|
|
519
|
-
describe(`${SecurityGuard.name} > sanitizeArray depth check (QUAL-009)`, () => {
|
|
520
|
-
it('throws SecurityException when array-only nesting exceeds maxDepth', () => {
|
|
521
|
-
const guard = new SecurityGuard(2);
|
|
522
|
-
const data = { a: [[[['deep']]]] };
|
|
523
|
-
expect(() => guard.sanitize(data)).toThrow(SecurityException);
|
|
524
|
-
});
|
|
525
|
-
|
|
526
|
-
it('sanitizeArray depth error message contains depth and limit', () => {
|
|
527
|
-
const guard = new SecurityGuard(2);
|
|
528
|
-
const data = { a: [[[['deep']]]] };
|
|
529
|
-
expect(() => guard.sanitize(data)).toThrow(/Recursion depth \d+ exceeds maximum/);
|
|
530
|
-
});
|
|
531
|
-
});
|
|
532
|
-
|
|
533
|
-
describe(`${SecurityGuard.name} > sanitizeArray null handling (QUAL-011)`, () => {
|
|
534
|
-
it('preserves null elements inside arrays during sanitize', () => {
|
|
535
|
-
const guard = new SecurityGuard();
|
|
536
|
-
const result = guard.sanitize({ items: [null, { name: 'ok' }, null] });
|
|
537
|
-
expect(result).toEqual({ items: [null, { name: 'ok' }, null] });
|
|
538
|
-
});
|
|
539
|
-
});
|
|
540
|
-
|
|
541
|
-
describe(`${SecurityGuard.name} > assertSafeKeys depth message (QUAL-012)`, () => {
|
|
542
|
-
it('assertSafeKeys error message contains depth and limit', () => {
|
|
543
|
-
const guard = new SecurityGuard(1);
|
|
544
|
-
const deep = { a: { b: { c: 'too deep' } } };
|
|
545
|
-
expect(() => guard.assertSafeKeys(deep)).toThrow(/Recursion depth \d+ exceeds maximum/);
|
|
546
|
-
});
|
|
547
|
-
});
|
|
548
|
-
|
|
549
|
-
describe(`${SecurityGuard.name} > isForbiddenKey startsWith normalization (QUAL-013)`, () => {
|
|
550
|
-
it('normalizes case for keys starting with __ but not ending with __', () => {
|
|
551
|
-
const guard = new SecurityGuard();
|
|
552
|
-
// __DIRNAME starts with __ but does NOT end with __ → only startsWith normalises it
|
|
553
|
-
expect(guard.isForbiddenKey('__DIRNAME')).toBe(true);
|
|
554
|
-
expect(guard.isForbiddenKey('__FILENAME')).toBe(true);
|
|
555
|
-
});
|
|
556
|
-
});
|
|
557
|
-
|
|
558
|
-
describe(`${SecurityGuard.name} > data: URI scheme blocking (SEC-014)`, () => {
|
|
559
|
-
it('T1: blocks browser data: URI carrying executable HTML payload', () => {
|
|
560
|
-
const guard = new SecurityGuard();
|
|
561
|
-
expect(guard.isForbiddenKey('data:text/html,<script>alert(1)</script>')).toBe(true);
|
|
562
|
-
});
|
|
563
|
-
|
|
564
|
-
it('T2: allows a key that starts with "data" but has no scheme delimiter', () => {
|
|
565
|
-
const guard = new SecurityGuard();
|
|
566
|
-
expect(guard.isForbiddenKey('data')).toBe(false);
|
|
567
|
-
expect(guard.isForbiddenKey('database')).toBe(false);
|
|
568
|
-
});
|
|
569
|
-
|
|
570
|
-
it('T3: blocks bare data: scheme exactly', () => {
|
|
571
|
-
const guard = new SecurityGuard();
|
|
572
|
-
expect(guard.isForbiddenKey('data:')).toBe(true);
|
|
573
|
-
});
|
|
574
|
-
|
|
575
|
-
it('T4: blocks data: URI with uppercase letters (case-insensitive prefix match)', () => {
|
|
576
|
-
const guard = new SecurityGuard();
|
|
577
|
-
expect(guard.isForbiddenKey('DATA:text/html,evil')).toBe(true);
|
|
578
|
-
expect(guard.isForbiddenKey('Data:image/svg+xml,<svg/>')).toBe(true);
|
|
579
|
-
});
|
|
580
|
-
|
|
581
|
-
it('T5: blocks data: key nested inside an object via assertSafeKeys', () => {
|
|
582
|
-
const guard = new SecurityGuard();
|
|
583
|
-
const nested = { outer: { 'data:text/html,evil': 'payload' } };
|
|
584
|
-
expect(() => guard.assertSafeKeys(nested)).toThrow(SecurityException);
|
|
585
|
-
});
|
|
586
|
-
|
|
587
|
-
it('T6: SecurityException message identifies the forbidden key', () => {
|
|
588
|
-
const guard = new SecurityGuard();
|
|
589
|
-
expect(() => guard.assertSafeKey('data:text/html,xss')).toThrow(
|
|
590
|
-
"Forbidden key 'data:text/html,xss' detected.",
|
|
591
|
-
);
|
|
592
|
-
});
|
|
593
|
-
|
|
594
|
-
it('T7: data:// (PHP-style) is still blocked alongside new data: browser URI rule', () => {
|
|
595
|
-
const guard = new SecurityGuard();
|
|
596
|
-
expect(guard.isForbiddenKey('data://')).toBe(true);
|
|
597
|
-
expect(guard.isForbiddenKey('data://text/plain;base64,aGVsbG8=')).toBe(true);
|
|
598
|
-
});
|
|
599
|
-
});
|
|
600
|
-
|
|
601
|
-
describe(`${SecurityGuard.name} > SEC-013 de-coupled keys (PHP-specific keys not blocked in JS)`, () => {
|
|
602
|
-
it('does not block PHP magic method __construct', () => {
|
|
603
|
-
const guard = new SecurityGuard();
|
|
604
|
-
expect(guard.isForbiddenKey('__construct')).toBe(false);
|
|
605
|
-
});
|
|
606
|
-
|
|
607
|
-
it('does not block PHP magic method __destruct', () => {
|
|
608
|
-
const guard = new SecurityGuard();
|
|
609
|
-
expect(guard.isForbiddenKey('__destruct')).toBe(false);
|
|
610
|
-
});
|
|
611
|
-
|
|
612
|
-
it('does not block PHP magic method __callStatic', () => {
|
|
613
|
-
const guard = new SecurityGuard();
|
|
614
|
-
expect(guard.isForbiddenKey('__callStatic')).toBe(false);
|
|
615
|
-
});
|
|
616
|
-
|
|
617
|
-
it('does not block PHP superglobal _GET', () => {
|
|
618
|
-
const guard = new SecurityGuard();
|
|
619
|
-
expect(guard.isForbiddenKey('_GET')).toBe(false);
|
|
620
|
-
});
|
|
621
|
-
|
|
622
|
-
it('does not block PHP superglobal _POST', () => {
|
|
623
|
-
const guard = new SecurityGuard();
|
|
624
|
-
expect(guard.isForbiddenKey('_POST')).toBe(false);
|
|
625
|
-
});
|
|
626
|
-
|
|
627
|
-
it('does not block PHP superglobal GLOBALS', () => {
|
|
628
|
-
const guard = new SecurityGuard();
|
|
629
|
-
expect(guard.isForbiddenKey('GLOBALS')).toBe(false);
|
|
630
|
-
});
|
|
631
|
-
|
|
632
|
-
it('does not block PHP-only stream wrapper phar://', () => {
|
|
633
|
-
const guard = new SecurityGuard();
|
|
634
|
-
expect(guard.isForbiddenKey('phar://')).toBe(false);
|
|
635
|
-
});
|
|
636
|
-
|
|
637
|
-
it('does not block PHP-only stream wrapper php://', () => {
|
|
638
|
-
const guard = new SecurityGuard();
|
|
639
|
-
expect(guard.isForbiddenKey('php://')).toBe(false);
|
|
640
|
-
});
|
|
641
|
-
|
|
642
|
-
it('does not block PHP-only stream wrapper zlib://', () => {
|
|
643
|
-
const guard = new SecurityGuard();
|
|
644
|
-
expect(guard.isForbiddenKey('zlib://')).toBe(false);
|
|
645
|
-
});
|
|
646
|
-
});
|
|
@@ -32,13 +32,13 @@ describe(SecurityParser.name, () => {
|
|
|
32
32
|
expect(parser.maxDepth).toBe(512);
|
|
33
33
|
});
|
|
34
34
|
|
|
35
|
-
it('uses 0 when option is explicitly 0 (nullish coalescing
|
|
35
|
+
it('uses 0 when option is explicitly 0 (nullish coalescing - not falsy)', () => {
|
|
36
36
|
const parser = new SecurityParser({ maxDepth: 0 });
|
|
37
37
|
expect(parser.maxDepth).toBe(0);
|
|
38
38
|
});
|
|
39
39
|
});
|
|
40
40
|
|
|
41
|
-
describe(`${SecurityParser.name} > constructor
|
|
41
|
+
describe(`${SecurityParser.name} > constructor - NaN/Infinity clamping (SEC-020)`, () => {
|
|
42
42
|
it('clamps NaN maxDepth to default 512', () => {
|
|
43
43
|
const parser = new SecurityParser({ maxDepth: NaN });
|
|
44
44
|
expect(parser.maxDepth).toBe(512);
|
|
@@ -251,10 +251,10 @@ describe(`${SecurityParser.name} > assertMaxKeys`, () => {
|
|
|
251
251
|
expect(() => parser.assertMaxKeys({ a: 1, b: 'hello', c: true })).not.toThrow();
|
|
252
252
|
});
|
|
253
253
|
|
|
254
|
-
// Kills line 164 `depth > maxDepth`
|
|
254
|
+
// Kills line 164 `depth > maxDepth` - distinguishes `>` from `>=`:
|
|
255
255
|
// with maxCountRecursiveDepth=1, depth=1 should still recurse (1 > 1 is false),
|
|
256
256
|
// so the child object's key gets counted.
|
|
257
|
-
it('counts keys at exactly maxCountRecursiveDepth level (boundary
|
|
257
|
+
it('counts keys at exactly maxCountRecursiveDepth level (boundary - > not >=)', () => {
|
|
258
258
|
// maxCountRecursiveDepth=1: at depth 0, recurse into 'a' (depth becomes 1).
|
|
259
259
|
// At depth 1 (= maxDepth), guard is 1 > 1 = false → still counts keys.
|
|
260
260
|
// At depth 2 (> maxDepth), guard is 2 > 1 = true → stops.
|
|
@@ -355,7 +355,7 @@ describe(`${SecurityParser.name} > assertMaxStructuralDepth`, () => {
|
|
|
355
355
|
expect(() => parser.assertMaxStructuralDepth({ a: { b: 1 } }, 2)).not.toThrow();
|
|
356
356
|
});
|
|
357
357
|
|
|
358
|
-
// Kills line 183 `current >= maxDepth` in measureDepth
|
|
358
|
+
// Kills line 183 `current >= maxDepth` in measureDepth - distinguishes `>=` from `>`:
|
|
359
359
|
// measureDepth is called with maxDepth = policy_limit + 1 (early termination ceiling).
|
|
360
360
|
// At current == maxDepth-1 (still below ceiling), we must still recurse measuring children.
|
|
361
361
|
it('measures depth correctly at ceiling - 1 (>= vs > boundary)', () => {
|
|
@@ -363,29 +363,33 @@ describe(`${SecurityParser.name} > assertMaxStructuralDepth`, () => {
|
|
|
363
363
|
// Policy maxDepth=3. measureDepth passes maxDepth+1=4 as ceiling.
|
|
364
364
|
// { a: { b: { c: { d: 1 } } } } is depth 4.
|
|
365
365
|
// If `>=` mutated to `>`, current=4 would NOT trigger early return at ceiling 4 → wrong depth.
|
|
366
|
-
expect(() => parser.assertMaxStructuralDepth({ a: { b: { c: { d: 1 } } } }, 3)).toThrow(
|
|
366
|
+
expect(() => parser.assertMaxStructuralDepth({ a: { b: { c: { d: 1 } } } }, 3)).toThrow(
|
|
367
|
+
SecurityException,
|
|
368
|
+
);
|
|
367
369
|
});
|
|
368
370
|
|
|
369
|
-
// Kills line 191 `d > max` in measureDepth
|
|
370
|
-
// sibling branches: one shallower, one equal depth
|
|
371
|
+
// Kills line 191 `d > max` in measureDepth - distinguishes `>` from `>=`:
|
|
372
|
+
// sibling branches: one shallower, one equal depth - max must NOT be updated on equal (no regression).
|
|
371
373
|
it('does not update max when sibling branch depth equals current max (> not >=)', () => {
|
|
372
374
|
const parser = new SecurityParser();
|
|
373
|
-
// { a: 1, b: 1 }
|
|
375
|
+
// { a: 1, b: 1 } - both branches have same depth (1). Result should be 1, not 2.
|
|
374
376
|
expect(() => parser.assertMaxStructuralDepth({ a: 1, b: 1 }, 1)).not.toThrow();
|
|
375
377
|
});
|
|
376
378
|
|
|
377
|
-
// Deep tree
|
|
379
|
+
// Deep tree - ensures the deepest branch wins
|
|
378
380
|
it('picks the deepest branch in an asymmetric tree', () => {
|
|
379
381
|
const parser = new SecurityParser();
|
|
380
|
-
// { a: 1, b: { c: { d: 1 } } }
|
|
382
|
+
// { a: 1, b: { c: { d: 1 } } } - branch b is deepest (depth 3).
|
|
381
383
|
// maxDepth=2 → depth 3 > 2 → throws.
|
|
382
|
-
expect(() => parser.assertMaxStructuralDepth({ a: 1, b: { c: { d: 1 } } }, 2)).toThrow(
|
|
384
|
+
expect(() => parser.assertMaxStructuralDepth({ a: 1, b: { c: { d: 1 } } }, 2)).toThrow(
|
|
385
|
+
SecurityException,
|
|
386
|
+
);
|
|
383
387
|
});
|
|
384
388
|
|
|
385
|
-
// Shallow sibling
|
|
389
|
+
// Shallow sibling - the max is NOT increased by a shallower second branch
|
|
386
390
|
it('does not throw when the deepest branch exactly meets maxDepth', () => {
|
|
387
391
|
const parser = new SecurityParser();
|
|
388
|
-
// { a: 1, b: { c: 1 } }
|
|
392
|
+
// { a: 1, b: { c: 1 } } - deepest is b.c at depth 2. maxDepth=2 → OK.
|
|
389
393
|
expect(() => parser.assertMaxStructuralDepth({ a: 1, b: { c: 1 } }, 2)).not.toThrow();
|
|
390
394
|
});
|
|
391
395
|
});
|
package/vitest.config.ts
CHANGED
|
@@ -3,11 +3,11 @@ import { defineConfig } from 'vitest/config';
|
|
|
3
3
|
export default defineConfig({
|
|
4
4
|
test: {
|
|
5
5
|
globals: false,
|
|
6
|
+
exclude: ['node_modules/**', '.stryker-tmp/**'],
|
|
6
7
|
coverage: {
|
|
8
|
+
reporter: ['text', 'json', 'clover'],
|
|
7
9
|
include: ['src/**/*.ts'],
|
|
8
|
-
exclude: [
|
|
9
|
-
'src/index.ts',
|
|
10
|
-
],
|
|
10
|
+
exclude: ['src/index.ts', 'src/contracts/**'],
|
|
11
11
|
thresholds: {
|
|
12
12
|
lines: 100,
|
|
13
13
|
branches: 100,
|
package/benchmarks/get.bench.ts
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { bench, describe } from 'vitest';
|
|
2
|
-
import { Inline } from '../src/inline.js';
|
|
3
|
-
|
|
4
|
-
const accessor = Inline.fromArray({
|
|
5
|
-
user: { profile: { name: 'Alice', age: 30 } },
|
|
6
|
-
config: { debug: false, version: '1.0.0' },
|
|
7
|
-
items: [1, 2, 3, 4, 5],
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
describe('ArrayAccessor.get', () => {
|
|
11
|
-
bench('shallow key', () => {
|
|
12
|
-
accessor.get('config.debug');
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
bench('deep key (3 levels)', () => {
|
|
16
|
-
accessor.get('user.profile.name');
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
bench('missing key with default', () => {
|
|
20
|
-
accessor.get('user.profile.missing', null);
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
bench('repeated path (cache)', () => {
|
|
24
|
-
accessor.get('user.profile.name');
|
|
25
|
-
});
|
|
26
|
-
});
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import { bench, describe } from 'vitest';
|
|
2
|
-
import { Inline } from '../src/inline.js';
|
|
3
|
-
|
|
4
|
-
const arrayPayload = {
|
|
5
|
-
user: { profile: { name: 'Alice', age: 30 } },
|
|
6
|
-
config: { debug: false, version: '1.0.0' },
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
const jsonPayload = JSON.stringify(arrayPayload);
|
|
10
|
-
|
|
11
|
-
const yamlPayload = `user:
|
|
12
|
-
profile:
|
|
13
|
-
name: Alice
|
|
14
|
-
age: 30
|
|
15
|
-
config:
|
|
16
|
-
debug: false
|
|
17
|
-
version: '1.0.0'
|
|
18
|
-
`;
|
|
19
|
-
|
|
20
|
-
const iniPayload = `[config]
|
|
21
|
-
debug=false
|
|
22
|
-
version=1.0.0
|
|
23
|
-
`;
|
|
24
|
-
|
|
25
|
-
describe('Inline.from* (parse)', () => {
|
|
26
|
-
bench('fromArray', () => {
|
|
27
|
-
Inline.fromArray(arrayPayload);
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
bench('fromJson', () => {
|
|
31
|
-
Inline.fromJson(jsonPayload);
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
bench('fromYaml', () => {
|
|
35
|
-
Inline.fromYaml(yamlPayload);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
bench('fromIni', () => {
|
|
39
|
-
Inline.fromIni(iniPayload);
|
|
40
|
-
});
|
|
41
|
-
});
|