@safeaccess/inline 0.1.1
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 +16 -0
- package/.gitkeep +0 -0
- package/CHANGELOG.md +38 -0
- package/LICENSE +21 -0
- package/README.md +454 -0
- package/benchmarks/get.bench.ts +26 -0
- package/benchmarks/parse.bench.ts +41 -0
- package/dist/accessors/abstract-accessor.d.ts +213 -0
- package/dist/accessors/abstract-accessor.js +294 -0
- package/dist/accessors/formats/any-accessor.d.ts +35 -0
- package/dist/accessors/formats/any-accessor.js +44 -0
- package/dist/accessors/formats/array-accessor.d.ts +26 -0
- package/dist/accessors/formats/array-accessor.js +39 -0
- package/dist/accessors/formats/env-accessor.d.ts +27 -0
- package/dist/accessors/formats/env-accessor.js +64 -0
- package/dist/accessors/formats/ini-accessor.d.ts +41 -0
- package/dist/accessors/formats/ini-accessor.js +109 -0
- package/dist/accessors/formats/json-accessor.d.ts +26 -0
- package/dist/accessors/formats/json-accessor.js +56 -0
- package/dist/accessors/formats/ndjson-accessor.d.ts +28 -0
- package/dist/accessors/formats/ndjson-accessor.js +71 -0
- package/dist/accessors/formats/object-accessor.d.ts +48 -0
- package/dist/accessors/formats/object-accessor.js +90 -0
- package/dist/accessors/formats/xml-accessor.d.ts +27 -0
- package/dist/accessors/formats/xml-accessor.js +52 -0
- package/dist/accessors/formats/yaml-accessor.d.ts +29 -0
- package/dist/accessors/formats/yaml-accessor.js +46 -0
- package/dist/contracts/accessors-interface.d.ts +11 -0
- package/dist/contracts/accessors-interface.js +1 -0
- package/dist/contracts/factory-accessors-interface.d.ts +16 -0
- package/dist/contracts/factory-accessors-interface.js +1 -0
- package/dist/contracts/parse-integration-interface.d.ts +31 -0
- package/dist/contracts/parse-integration-interface.js +1 -0
- package/dist/contracts/path-cache-interface.d.ts +40 -0
- package/dist/contracts/path-cache-interface.js +1 -0
- package/dist/contracts/readable-accessors-interface.d.ts +79 -0
- package/dist/contracts/readable-accessors-interface.js +1 -0
- package/dist/contracts/security-guard-interface.d.ts +40 -0
- package/dist/contracts/security-guard-interface.js +1 -0
- package/dist/contracts/security-parser-interface.d.ts +67 -0
- package/dist/contracts/security-parser-interface.js +1 -0
- package/dist/contracts/writable-accessors-interface.d.ts +65 -0
- package/dist/contracts/writable-accessors-interface.js +1 -0
- package/dist/core/dot-notation-parser.d.ts +204 -0
- package/dist/core/dot-notation-parser.js +343 -0
- package/dist/exceptions/accessor-exception.d.ts +13 -0
- package/dist/exceptions/accessor-exception.js +16 -0
- package/dist/exceptions/invalid-format-exception.d.ts +14 -0
- package/dist/exceptions/invalid-format-exception.js +17 -0
- package/dist/exceptions/parser-exception.d.ts +14 -0
- package/dist/exceptions/parser-exception.js +17 -0
- package/dist/exceptions/path-not-found-exception.d.ts +14 -0
- package/dist/exceptions/path-not-found-exception.js +17 -0
- package/dist/exceptions/readonly-violation-exception.d.ts +15 -0
- package/dist/exceptions/readonly-violation-exception.js +18 -0
- package/dist/exceptions/security-exception.d.ts +18 -0
- package/dist/exceptions/security-exception.js +21 -0
- package/dist/exceptions/unsupported-type-exception.d.ts +14 -0
- package/dist/exceptions/unsupported-type-exception.js +17 -0
- package/dist/exceptions/yaml-parse-exception.d.ts +17 -0
- package/dist/exceptions/yaml-parse-exception.js +20 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.js +30 -0
- package/dist/inline.d.ts +402 -0
- package/dist/inline.js +512 -0
- package/dist/parser/xml-parser.d.ts +46 -0
- package/dist/parser/xml-parser.js +288 -0
- package/dist/parser/yaml-parser.d.ts +94 -0
- package/dist/parser/yaml-parser.js +286 -0
- package/dist/security/forbidden-keys.d.ts +34 -0
- package/dist/security/forbidden-keys.js +80 -0
- package/dist/security/security-guard.d.ts +94 -0
- package/dist/security/security-guard.js +172 -0
- package/dist/security/security-parser.d.ts +130 -0
- package/dist/security/security-parser.js +192 -0
- package/dist/type-format.d.ts +28 -0
- package/dist/type-format.js +29 -0
- package/eslint.config.js +1 -0
- package/package.json +39 -0
- package/src/accessors/abstract-accessor.ts +353 -0
- package/src/accessors/formats/any-accessor.ts +51 -0
- package/src/accessors/formats/array-accessor.ts +45 -0
- package/src/accessors/formats/env-accessor.ts +79 -0
- package/src/accessors/formats/ini-accessor.ts +124 -0
- package/src/accessors/formats/json-accessor.ts +66 -0
- package/src/accessors/formats/ndjson-accessor.ts +82 -0
- package/src/accessors/formats/object-accessor.ts +100 -0
- package/src/accessors/formats/xml-accessor.ts +58 -0
- package/src/accessors/formats/yaml-accessor.ts +52 -0
- package/src/contracts/accessors-interface.ts +12 -0
- package/src/contracts/factory-accessors-interface.ts +16 -0
- package/src/contracts/parse-integration-interface.ts +32 -0
- package/src/contracts/path-cache-interface.ts +43 -0
- package/src/contracts/readable-accessors-interface.ts +88 -0
- package/src/contracts/security-guard-interface.ts +43 -0
- package/src/contracts/security-parser-interface.ts +74 -0
- package/src/contracts/writable-accessors-interface.ts +70 -0
- package/src/core/dot-notation-parser.ts +419 -0
- package/src/exceptions/accessor-exception.ts +16 -0
- package/src/exceptions/invalid-format-exception.ts +18 -0
- package/src/exceptions/parser-exception.ts +18 -0
- package/src/exceptions/path-not-found-exception.ts +18 -0
- package/src/exceptions/readonly-violation-exception.ts +19 -0
- package/src/exceptions/security-exception.ts +22 -0
- package/src/exceptions/unsupported-type-exception.ts +18 -0
- package/src/exceptions/yaml-parse-exception.ts +21 -0
- package/src/index.ts +46 -0
- package/src/inline.ts +570 -0
- package/src/parser/xml-parser.ts +334 -0
- package/src/parser/yaml-parser.ts +368 -0
- package/src/security/forbidden-keys.ts +81 -0
- package/src/security/security-guard.ts +195 -0
- package/src/security/security-parser.ts +233 -0
- package/src/type-format.ts +28 -0
- package/stryker.config.json +24 -0
- package/tests/accessors/accessors.test.ts +1017 -0
- package/tests/accessors/json-accessor.test.ts +171 -0
- package/tests/core/dot-notation-parser.test.ts +587 -0
- package/tests/exceptions/parser-exception.test.ts +31 -0
- package/tests/inline.test.ts +445 -0
- package/tests/mocks/fake-parse-integration.ts +24 -0
- package/tests/mocks/fake-path-cache.ts +31 -0
- package/tests/parity.test.ts +164 -0
- package/tests/parser/xml-parser.test.ts +618 -0
- package/tests/parser/yaml-parser.test.ts +463 -0
- package/tests/security/security-guard.test.ts +646 -0
- package/tests/security/security-parser.test.ts +391 -0
- package/tsconfig.json +16 -0
- package/vitest.config.ts +19 -0
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { SecurityParser } from '../../src/security/security-parser.js';
|
|
3
|
+
import { SecurityException } from '../../src/exceptions/security-exception.js';
|
|
4
|
+
|
|
5
|
+
describe(SecurityParser.name, () => {
|
|
6
|
+
it('creates instance with default values', () => {
|
|
7
|
+
const parser = new SecurityParser();
|
|
8
|
+
expect(parser.maxDepth).toBe(512);
|
|
9
|
+
expect(parser.maxPayloadBytes).toBe(10 * 1024 * 1024);
|
|
10
|
+
expect(parser.maxKeys).toBe(10_000);
|
|
11
|
+
expect(parser.maxCountRecursiveDepth).toBe(100);
|
|
12
|
+
expect(parser.maxResolveDepth).toBe(100);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('accepts custom options', () => {
|
|
16
|
+
const parser = new SecurityParser({
|
|
17
|
+
maxDepth: 5,
|
|
18
|
+
maxPayloadBytes: 100,
|
|
19
|
+
maxKeys: 10,
|
|
20
|
+
maxCountRecursiveDepth: 3,
|
|
21
|
+
maxResolveDepth: 4,
|
|
22
|
+
});
|
|
23
|
+
expect(parser.maxDepth).toBe(5);
|
|
24
|
+
expect(parser.maxPayloadBytes).toBe(100);
|
|
25
|
+
expect(parser.maxKeys).toBe(10);
|
|
26
|
+
expect(parser.maxCountRecursiveDepth).toBe(3);
|
|
27
|
+
expect(parser.maxResolveDepth).toBe(4);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('uses default when option is undefined (nullish coalescing)', () => {
|
|
31
|
+
const parser = new SecurityParser({ maxDepth: undefined });
|
|
32
|
+
expect(parser.maxDepth).toBe(512);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('uses 0 when option is explicitly 0 (nullish coalescing — not falsy)', () => {
|
|
36
|
+
const parser = new SecurityParser({ maxDepth: 0 });
|
|
37
|
+
expect(parser.maxDepth).toBe(0);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe(`${SecurityParser.name} > constructor — NaN/Infinity clamping (SEC-020)`, () => {
|
|
42
|
+
it('clamps NaN maxDepth to default 512', () => {
|
|
43
|
+
const parser = new SecurityParser({ maxDepth: NaN });
|
|
44
|
+
expect(parser.maxDepth).toBe(512);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('clamps Infinity maxDepth to default 512', () => {
|
|
48
|
+
const parser = new SecurityParser({ maxDepth: Infinity });
|
|
49
|
+
expect(parser.maxDepth).toBe(512);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('clamps NaN maxPayloadBytes to default', () => {
|
|
53
|
+
const parser = new SecurityParser({ maxPayloadBytes: NaN });
|
|
54
|
+
expect(parser.maxPayloadBytes).toBe(10 * 1024 * 1024);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('clamps NaN maxKeys to default 10 000', () => {
|
|
58
|
+
const parser = new SecurityParser({ maxKeys: NaN });
|
|
59
|
+
expect(parser.maxKeys).toBe(10_000);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('clamps Infinity maxKeys to default 10 000', () => {
|
|
63
|
+
const parser = new SecurityParser({ maxKeys: Infinity });
|
|
64
|
+
expect(parser.maxKeys).toBe(10_000);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('clamps NaN maxCountRecursiveDepth to default 100', () => {
|
|
68
|
+
const parser = new SecurityParser({ maxCountRecursiveDepth: NaN });
|
|
69
|
+
expect(parser.maxCountRecursiveDepth).toBe(100);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('clamps NaN maxResolveDepth to default 100', () => {
|
|
73
|
+
const parser = new SecurityParser({ maxResolveDepth: NaN });
|
|
74
|
+
expect(parser.maxResolveDepth).toBe(100);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('assertPayloadSize still fires when maxPayloadBytes was clamped from NaN', () => {
|
|
78
|
+
const parser = new SecurityParser({ maxPayloadBytes: NaN });
|
|
79
|
+
const large = 'x'.repeat(10 * 1024 * 1024 + 1);
|
|
80
|
+
expect(() => parser.assertPayloadSize(large)).toThrow(SecurityException);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('assertMaxKeys still fires when maxKeys was clamped from NaN', () => {
|
|
84
|
+
const parser = new SecurityParser({ maxKeys: NaN });
|
|
85
|
+
const data: Record<string, unknown> = {};
|
|
86
|
+
for (let i = 0; i < 10_001; i++) {
|
|
87
|
+
data[`k${i}`] = i;
|
|
88
|
+
}
|
|
89
|
+
expect(() => parser.assertMaxKeys(data)).toThrow(SecurityException);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe(`${SecurityParser.name} > getMaxDepth`, () => {
|
|
94
|
+
it('returns the configured max depth', () => {
|
|
95
|
+
const parser = new SecurityParser({ maxDepth: 42 });
|
|
96
|
+
expect(parser.getMaxDepth()).toBe(42);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('returns the default max depth when not overridden', () => {
|
|
100
|
+
const parser = new SecurityParser();
|
|
101
|
+
expect(parser.getMaxDepth()).toBe(512);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe(`${SecurityParser.name} > getMaxResolveDepth`, () => {
|
|
106
|
+
it('returns the configured max resolve depth', () => {
|
|
107
|
+
const parser = new SecurityParser({ maxResolveDepth: 7 });
|
|
108
|
+
expect(parser.getMaxResolveDepth()).toBe(7);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('returns the default max resolve depth when not overridden', () => {
|
|
112
|
+
const parser = new SecurityParser();
|
|
113
|
+
expect(parser.getMaxResolveDepth()).toBe(100);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe(`${SecurityParser.name} > getMaxKeys`, () => {
|
|
118
|
+
it('returns the configured max key count', () => {
|
|
119
|
+
const parser = new SecurityParser({ maxKeys: 500 });
|
|
120
|
+
expect(parser.getMaxKeys()).toBe(500);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('returns the default max key count when not overridden', () => {
|
|
124
|
+
const parser = new SecurityParser();
|
|
125
|
+
expect(parser.getMaxKeys()).toBe(10_000);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe(`${SecurityParser.name} > assertPayloadSize`, () => {
|
|
130
|
+
it('does not throw for a small payload', () => {
|
|
131
|
+
const parser = new SecurityParser({ maxPayloadBytes: 100 });
|
|
132
|
+
expect(() => parser.assertPayloadSize('small')).not.toThrow();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('throws SecurityException when payload exceeds the limit', () => {
|
|
136
|
+
const parser = new SecurityParser({ maxPayloadBytes: 5 });
|
|
137
|
+
expect(() => parser.assertPayloadSize('123456')).toThrow(SecurityException);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('throws SecurityException at exactly limit + 1 byte', () => {
|
|
141
|
+
const parser = new SecurityParser({ maxPayloadBytes: 4 });
|
|
142
|
+
expect(() => parser.assertPayloadSize('12345')).toThrow(SecurityException);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('does not throw at exactly the limit', () => {
|
|
146
|
+
const parser = new SecurityParser({ maxPayloadBytes: 5 });
|
|
147
|
+
expect(() => parser.assertPayloadSize('12345')).not.toThrow();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('uses the override maxBytes when provided', () => {
|
|
151
|
+
const parser = new SecurityParser({ maxPayloadBytes: 1000 });
|
|
152
|
+
expect(() => parser.assertPayloadSize('123456', 3)).toThrow(SecurityException);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('uses the default limit when maxBytes override is not provided', () => {
|
|
156
|
+
const parser = new SecurityParser({ maxPayloadBytes: 3 });
|
|
157
|
+
expect(() => parser.assertPayloadSize('1234')).toThrow(SecurityException);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('error message contains byte size and limit', () => {
|
|
161
|
+
const parser = new SecurityParser({ maxPayloadBytes: 3 });
|
|
162
|
+
expect(() => parser.assertPayloadSize('1234')).toThrow(
|
|
163
|
+
/Payload size \d+ bytes exceeds maximum of 3 bytes/,
|
|
164
|
+
);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
describe(`${SecurityParser.name} > assertMaxResolveDepth`, () => {
|
|
169
|
+
it('does not throw for depth below the limit', () => {
|
|
170
|
+
const parser = new SecurityParser({ maxResolveDepth: 10 });
|
|
171
|
+
expect(() => parser.assertMaxResolveDepth(5)).not.toThrow();
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('does not throw for depth equal to the limit', () => {
|
|
175
|
+
const parser = new SecurityParser({ maxResolveDepth: 10 });
|
|
176
|
+
expect(() => parser.assertMaxResolveDepth(10)).not.toThrow();
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('throws SecurityException for depth above the limit', () => {
|
|
180
|
+
const parser = new SecurityParser({ maxResolveDepth: 10 });
|
|
181
|
+
expect(() => parser.assertMaxResolveDepth(11)).toThrow(SecurityException);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('throws SecurityException at depth = limit + 1', () => {
|
|
185
|
+
const parser = new SecurityParser({ maxResolveDepth: 3 });
|
|
186
|
+
expect(() => parser.assertMaxResolveDepth(4)).toThrow(SecurityException);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('error message contains the max resolve depth', () => {
|
|
190
|
+
const parser = new SecurityParser({ maxResolveDepth: 3 });
|
|
191
|
+
expect(() => parser.assertMaxResolveDepth(4)).toThrow(
|
|
192
|
+
'Deep merge exceeded maximum depth of 3',
|
|
193
|
+
);
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
describe(`${SecurityParser.name} > assertMaxKeys`, () => {
|
|
198
|
+
it('does not throw for data with few keys', () => {
|
|
199
|
+
const parser = new SecurityParser({ maxKeys: 10 });
|
|
200
|
+
expect(() => parser.assertMaxKeys({ a: 1, b: 2 })).not.toThrow();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('throws SecurityException when total key count exceeds limit', () => {
|
|
204
|
+
const parser = new SecurityParser({ maxKeys: 2 });
|
|
205
|
+
expect(() => parser.assertMaxKeys({ a: 1, b: 2, c: 3 })).toThrow(SecurityException);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('counts nested keys recursively', () => {
|
|
209
|
+
const parser = new SecurityParser({ maxKeys: 3 });
|
|
210
|
+
expect(() => parser.assertMaxKeys({ a: { b: { c: 1 } } })).not.toThrow();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('throws when nested key count exceeds limit', () => {
|
|
214
|
+
const parser = new SecurityParser({ maxKeys: 2 });
|
|
215
|
+
expect(() => parser.assertMaxKeys({ a: { b: { c: 1 } } })).toThrow(SecurityException);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('uses the override maxKeys when provided', () => {
|
|
219
|
+
const parser = new SecurityParser({ maxKeys: 100 });
|
|
220
|
+
expect(() => parser.assertMaxKeys({ a: 1, b: 2 }, 1)).toThrow(SecurityException);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('error message contains count and limit', () => {
|
|
224
|
+
const parser = new SecurityParser({ maxKeys: 2 });
|
|
225
|
+
expect(() => parser.assertMaxKeys({ a: 1, b: 2, c: 3 })).toThrow(
|
|
226
|
+
/Data contains \d+ keys, exceeding maximum of 2/,
|
|
227
|
+
);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('does not count past maxCountDepth', () => {
|
|
231
|
+
// With very low recursion depth, deeply nested keys don't get counted
|
|
232
|
+
const parser = new SecurityParser({ maxKeys: 1, maxCountRecursiveDepth: 0 });
|
|
233
|
+
// Root has 1 key 'a'. With depth 0, we don't recurse into value.
|
|
234
|
+
// count = 1 (not > 1), should pass.
|
|
235
|
+
expect(() => parser.assertMaxKeys({ a: { b: 1 } })).not.toThrow();
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('counts correctly at the boundary', () => {
|
|
239
|
+
const parser = new SecurityParser({ maxKeys: 3 });
|
|
240
|
+
expect(() => parser.assertMaxKeys({ a: 1, b: 2, c: 3 })).not.toThrow();
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('does not throw for empty object', () => {
|
|
244
|
+
const parser = new SecurityParser({ maxKeys: 0 });
|
|
245
|
+
expect(() => parser.assertMaxKeys({})).not.toThrow();
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('does not count non-object leaf values', () => {
|
|
249
|
+
const parser = new SecurityParser({ maxKeys: 3 });
|
|
250
|
+
// a, b, c are 3 keys; b's value is a string (not counted recursively)
|
|
251
|
+
expect(() => parser.assertMaxKeys({ a: 1, b: 'hello', c: true })).not.toThrow();
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// Kills line 164 `depth > maxDepth` — distinguishes `>` from `>=`:
|
|
255
|
+
// with maxCountRecursiveDepth=1, depth=1 should still recurse (1 > 1 is false),
|
|
256
|
+
// so the child object's key gets counted.
|
|
257
|
+
it('counts keys at exactly maxCountRecursiveDepth level (boundary — > not >=)', () => {
|
|
258
|
+
// maxCountRecursiveDepth=1: at depth 0, recurse into 'a' (depth becomes 1).
|
|
259
|
+
// At depth 1 (= maxDepth), guard is 1 > 1 = false → still counts keys.
|
|
260
|
+
// At depth 2 (> maxDepth), guard is 2 > 1 = true → stops.
|
|
261
|
+
// Data { a: { b: 1 } } → total keys: 2 (a + b)
|
|
262
|
+
const parser = new SecurityParser({ maxKeys: 1, maxCountRecursiveDepth: 1 });
|
|
263
|
+
expect(() => parser.assertMaxKeys({ a: { b: 1 } })).toThrow(SecurityException);
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
describe(`${SecurityParser.name} > assertMaxDepth`, () => {
|
|
268
|
+
it('does not throw for depth below the limit', () => {
|
|
269
|
+
const parser = new SecurityParser({ maxDepth: 10 });
|
|
270
|
+
expect(() => parser.assertMaxDepth(5)).not.toThrow();
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('does not throw for depth equal to the limit', () => {
|
|
274
|
+
const parser = new SecurityParser({ maxDepth: 10 });
|
|
275
|
+
expect(() => parser.assertMaxDepth(10)).not.toThrow();
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('throws SecurityException for depth above the limit', () => {
|
|
279
|
+
const parser = new SecurityParser({ maxDepth: 10 });
|
|
280
|
+
expect(() => parser.assertMaxDepth(11)).toThrow(SecurityException);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('uses the override maxDepth when provided', () => {
|
|
284
|
+
const parser = new SecurityParser({ maxDepth: 100 });
|
|
285
|
+
expect(() => parser.assertMaxDepth(5, 3)).toThrow(SecurityException);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it('uses the default maxDepth when override is not provided', () => {
|
|
289
|
+
const parser = new SecurityParser({ maxDepth: 3 });
|
|
290
|
+
expect(() => parser.assertMaxDepth(4)).toThrow(SecurityException);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('error message contains depth and limit', () => {
|
|
294
|
+
const parser = new SecurityParser({ maxDepth: 3 });
|
|
295
|
+
expect(() => parser.assertMaxDepth(4)).toThrow('Recursion depth 4 exceeds maximum of 3.');
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it('uses 0 as limit override correctly', () => {
|
|
299
|
+
const parser = new SecurityParser({ maxDepth: 100 });
|
|
300
|
+
expect(() => parser.assertMaxDepth(1, 0)).toThrow(SecurityException);
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
describe(`${SecurityParser.name} > assertMaxStructuralDepth`, () => {
|
|
305
|
+
it('does not throw for flat data', () => {
|
|
306
|
+
const parser = new SecurityParser();
|
|
307
|
+
expect(() => parser.assertMaxStructuralDepth({ a: 1, b: 2 }, 5)).not.toThrow();
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it('does not throw for 2-level nesting within limit', () => {
|
|
311
|
+
const parser = new SecurityParser();
|
|
312
|
+
expect(() => parser.assertMaxStructuralDepth({ a: { b: 1 } }, 5)).not.toThrow();
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it('throws SecurityException for data that exceeds maxDepth', () => {
|
|
316
|
+
const parser = new SecurityParser();
|
|
317
|
+
expect(() => parser.assertMaxStructuralDepth({ a: { b: { c: { d: 1 } } } }, 2)).toThrow(
|
|
318
|
+
SecurityException,
|
|
319
|
+
);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it('throws SecurityException at exactly maxDepth + 1 levels', () => {
|
|
323
|
+
// data is 1 level deep; maxDepth = 0 → depth 1 > 0 → throws
|
|
324
|
+
const parser = new SecurityParser();
|
|
325
|
+
expect(() => parser.assertMaxStructuralDepth({ a: 1 }, 0)).toThrow(SecurityException);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it('does not throw for empty object at any limit', () => {
|
|
329
|
+
const parser = new SecurityParser();
|
|
330
|
+
expect(() => parser.assertMaxStructuralDepth({}, 0)).not.toThrow();
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it('does not throw for null-valued keys', () => {
|
|
334
|
+
const parser = new SecurityParser();
|
|
335
|
+
expect(() => parser.assertMaxStructuralDepth({ a: null }, 1)).not.toThrow();
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it('error message contains depth and policy maximum', () => {
|
|
339
|
+
const parser = new SecurityParser();
|
|
340
|
+
expect(() => parser.assertMaxStructuralDepth({ a: { b: { c: 1 } } }, 1)).toThrow(
|
|
341
|
+
/Data structural depth \d+ exceeds policy maximum of 1/,
|
|
342
|
+
);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it('correctly measures depth for multi-branch trees', () => {
|
|
346
|
+
const parser = new SecurityParser();
|
|
347
|
+
const data = { a: { x: 1, y: 2 }, b: { z: { w: 3 } } };
|
|
348
|
+
// b.z.w is depth 3; maxDepth=2 → throws
|
|
349
|
+
expect(() => parser.assertMaxStructuralDepth(data, 2)).toThrow(SecurityException);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it('does not throw at exactly maxDepth levels', () => {
|
|
353
|
+
const parser = new SecurityParser();
|
|
354
|
+
// { a: { b: 1 } } is 2 levels deep; maxDepth=2 → OK
|
|
355
|
+
expect(() => parser.assertMaxStructuralDepth({ a: { b: 1 } }, 2)).not.toThrow();
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
// Kills line 183 `current >= maxDepth` in measureDepth — distinguishes `>=` from `>`:
|
|
359
|
+
// measureDepth is called with maxDepth = policy_limit + 1 (early termination ceiling).
|
|
360
|
+
// At current == maxDepth-1 (still below ceiling), we must still recurse measuring children.
|
|
361
|
+
it('measures depth correctly at ceiling - 1 (>= vs > boundary)', () => {
|
|
362
|
+
const parser = new SecurityParser();
|
|
363
|
+
// Policy maxDepth=3. measureDepth passes maxDepth+1=4 as ceiling.
|
|
364
|
+
// { a: { b: { c: { d: 1 } } } } is depth 4.
|
|
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(SecurityException);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
// Kills line 191 `d > max` in measureDepth — distinguishes `>` from `>=`:
|
|
370
|
+
// sibling branches: one shallower, one equal depth — max must NOT be updated on equal (no regression).
|
|
371
|
+
it('does not update max when sibling branch depth equals current max (> not >=)', () => {
|
|
372
|
+
const parser = new SecurityParser();
|
|
373
|
+
// { a: 1, b: 1 } — both branches have same depth (1). Result should be 1, not 2.
|
|
374
|
+
expect(() => parser.assertMaxStructuralDepth({ a: 1, b: 1 }, 1)).not.toThrow();
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
// Deep tree — ensures the deepest branch wins
|
|
378
|
+
it('picks the deepest branch in an asymmetric tree', () => {
|
|
379
|
+
const parser = new SecurityParser();
|
|
380
|
+
// { a: 1, b: { c: { d: 1 } } } — branch b is deepest (depth 3).
|
|
381
|
+
// maxDepth=2 → depth 3 > 2 → throws.
|
|
382
|
+
expect(() => parser.assertMaxStructuralDepth({ a: 1, b: { c: { d: 1 } } }, 2)).toThrow(SecurityException);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
// Shallow sibling — the max is NOT increased by a shallower second branch
|
|
386
|
+
it('does not throw when the deepest branch exactly meets maxDepth', () => {
|
|
387
|
+
const parser = new SecurityParser();
|
|
388
|
+
// { a: 1, b: { c: 1 } } — deepest is b.c at depth 2. maxDepth=2 → OK.
|
|
389
|
+
expect(() => parser.assertMaxStructuralDepth({ a: 1, b: { c: 1 } }, 2)).not.toThrow();
|
|
390
|
+
});
|
|
391
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"declaration": true,
|
|
8
|
+
"outDir": "./dist",
|
|
9
|
+
"rootDir": "./src",
|
|
10
|
+
"noImplicitAny": true,
|
|
11
|
+
"noImplicitReturns": true,
|
|
12
|
+
"exactOptionalPropertyTypes": true
|
|
13
|
+
},
|
|
14
|
+
"include": ["src/**/*"],
|
|
15
|
+
"exclude": ["node_modules", "dist", "tests", "vitest.config.ts"]
|
|
16
|
+
}
|
package/vitest.config.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { defineConfig } from 'vitest/config';
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
test: {
|
|
5
|
+
globals: false,
|
|
6
|
+
coverage: {
|
|
7
|
+
include: ['src/**/*.ts'],
|
|
8
|
+
exclude: [
|
|
9
|
+
'src/index.ts',
|
|
10
|
+
],
|
|
11
|
+
thresholds: {
|
|
12
|
+
lines: 100,
|
|
13
|
+
branches: 100,
|
|
14
|
+
functions: 100,
|
|
15
|
+
statements: 100,
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
});
|