@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.
Files changed (179) hide show
  1. package/.gitattributes +1 -1
  2. package/CHANGELOG.md +10 -5
  3. package/LICENSE +1 -1
  4. package/README.md +56 -14
  5. package/dist/accessors/abstract-accessor.d.ts +22 -10
  6. package/dist/accessors/abstract-accessor.js +21 -8
  7. package/dist/accessors/abstract-integration-accessor.d.ts +22 -0
  8. package/dist/accessors/abstract-integration-accessor.js +23 -0
  9. package/dist/accessors/formats/any-accessor.d.ts +10 -8
  10. package/dist/accessors/formats/any-accessor.js +9 -8
  11. package/dist/accessors/formats/array-accessor.d.ts +2 -0
  12. package/dist/accessors/formats/array-accessor.js +2 -0
  13. package/dist/accessors/formats/env-accessor.d.ts +2 -0
  14. package/dist/accessors/formats/env-accessor.js +2 -0
  15. package/dist/accessors/formats/ini-accessor.d.ts +2 -0
  16. package/dist/accessors/formats/ini-accessor.js +2 -0
  17. package/dist/accessors/formats/json-accessor.d.ts +2 -0
  18. package/dist/accessors/formats/json-accessor.js +2 -0
  19. package/dist/accessors/formats/ndjson-accessor.d.ts +2 -0
  20. package/dist/accessors/formats/ndjson-accessor.js +2 -0
  21. package/dist/accessors/formats/object-accessor.d.ts +2 -0
  22. package/dist/accessors/formats/object-accessor.js +2 -0
  23. package/dist/accessors/formats/xml-accessor.d.ts +2 -0
  24. package/dist/accessors/formats/xml-accessor.js +2 -0
  25. package/dist/accessors/formats/yaml-accessor.d.ts +3 -1
  26. package/dist/accessors/formats/yaml-accessor.js +4 -2
  27. package/dist/cache/simple-path-cache.d.ts +51 -0
  28. package/dist/cache/simple-path-cache.js +72 -0
  29. package/dist/contracts/accessors-interface.d.ts +2 -0
  30. package/dist/contracts/factory-accessors-interface.d.ts +2 -0
  31. package/dist/contracts/filter-evaluator-interface.d.ts +28 -0
  32. package/dist/contracts/filter-evaluator-interface.js +1 -0
  33. package/dist/contracts/parse-integration-interface.d.ts +2 -0
  34. package/dist/contracts/parser-interface.d.ts +92 -0
  35. package/dist/contracts/parser-interface.js +1 -0
  36. package/dist/contracts/path-cache-interface.d.ts +7 -6
  37. package/dist/contracts/readable-accessors-interface.d.ts +11 -6
  38. package/dist/contracts/security-guard-interface.d.ts +2 -0
  39. package/dist/contracts/security-parser-interface.d.ts +2 -0
  40. package/dist/contracts/validatable-parser-interface.d.ts +59 -0
  41. package/dist/contracts/validatable-parser-interface.js +1 -0
  42. package/dist/contracts/writable-accessors-interface.d.ts +5 -0
  43. package/dist/core/accessor-factory.d.ts +124 -0
  44. package/dist/core/accessor-factory.js +157 -0
  45. package/dist/core/dot-notation-parser.d.ts +34 -5
  46. package/dist/core/dot-notation-parser.js +51 -10
  47. package/dist/core/inline-builder-accessor.d.ts +82 -0
  48. package/dist/core/inline-builder-accessor.js +107 -0
  49. package/dist/exceptions/accessor-exception.d.ts +9 -0
  50. package/dist/exceptions/accessor-exception.js +9 -0
  51. package/dist/exceptions/invalid-format-exception.d.ts +5 -0
  52. package/dist/exceptions/invalid-format-exception.js +5 -0
  53. package/dist/exceptions/parser-exception.d.ts +4 -0
  54. package/dist/exceptions/parser-exception.js +4 -0
  55. package/dist/exceptions/path-not-found-exception.d.ts +4 -0
  56. package/dist/exceptions/path-not-found-exception.js +4 -0
  57. package/dist/exceptions/readonly-violation-exception.d.ts +4 -0
  58. package/dist/exceptions/readonly-violation-exception.js +4 -0
  59. package/dist/exceptions/security-exception.d.ts +6 -0
  60. package/dist/exceptions/security-exception.js +6 -0
  61. package/dist/exceptions/unsupported-type-exception.d.ts +4 -0
  62. package/dist/exceptions/unsupported-type-exception.js +4 -0
  63. package/dist/exceptions/yaml-parse-exception.d.ts +4 -0
  64. package/dist/exceptions/yaml-parse-exception.js +4 -0
  65. package/dist/index.js +2 -1
  66. package/dist/inline.d.ts +22 -56
  67. package/dist/inline.js +39 -111
  68. package/dist/parser/xml-parser.js +23 -10
  69. package/dist/parser/yaml-parser.d.ts +54 -7
  70. package/dist/parser/yaml-parser.js +268 -51
  71. package/dist/path-query/segment-filter-parser.d.ts +142 -0
  72. package/dist/path-query/segment-filter-parser.js +384 -0
  73. package/dist/path-query/segment-parser.d.ts +98 -0
  74. package/dist/path-query/segment-parser.js +283 -0
  75. package/dist/path-query/segment-path-resolver.d.ts +149 -0
  76. package/dist/path-query/segment-path-resolver.js +351 -0
  77. package/dist/path-query/segment-type.d.ts +85 -0
  78. package/dist/path-query/segment-type.js +35 -0
  79. package/dist/security/forbidden-keys.d.ts +2 -2
  80. package/dist/security/forbidden-keys.js +5 -5
  81. package/dist/security/security-guard.d.ts +3 -1
  82. package/dist/security/security-guard.js +5 -2
  83. package/dist/security/security-parser.d.ts +10 -1
  84. package/dist/security/security-parser.js +10 -1
  85. package/dist/type-format.d.ts +2 -0
  86. package/dist/type-format.js +2 -0
  87. package/package.json +11 -3
  88. package/src/accessors/abstract-accessor.ts +23 -19
  89. package/src/accessors/abstract-integration-accessor.ts +27 -0
  90. package/src/accessors/formats/any-accessor.ts +11 -11
  91. package/src/accessors/formats/array-accessor.ts +2 -0
  92. package/src/accessors/formats/env-accessor.ts +2 -0
  93. package/src/accessors/formats/ini-accessor.ts +2 -0
  94. package/src/accessors/formats/json-accessor.ts +2 -0
  95. package/src/accessors/formats/ndjson-accessor.ts +2 -0
  96. package/src/accessors/formats/object-accessor.ts +2 -0
  97. package/src/accessors/formats/xml-accessor.ts +2 -0
  98. package/src/accessors/formats/yaml-accessor.ts +4 -2
  99. package/src/cache/simple-path-cache.ts +77 -0
  100. package/src/contracts/accessors-interface.ts +2 -0
  101. package/src/contracts/factory-accessors-interface.ts +2 -0
  102. package/src/contracts/filter-evaluator-interface.ts +30 -0
  103. package/src/contracts/parse-integration-interface.ts +2 -0
  104. package/src/contracts/parser-interface.ts +114 -0
  105. package/src/contracts/path-cache-interface.ts +8 -6
  106. package/src/contracts/readable-accessors-interface.ts +11 -6
  107. package/src/contracts/security-guard-interface.ts +2 -0
  108. package/src/contracts/security-parser-interface.ts +2 -0
  109. package/src/contracts/validatable-parser-interface.ts +64 -0
  110. package/src/contracts/writable-accessors-interface.ts +5 -0
  111. package/src/core/accessor-factory.ts +173 -0
  112. package/src/core/dot-notation-parser.ts +74 -11
  113. package/src/core/inline-builder-accessor.ts +163 -0
  114. package/src/exceptions/accessor-exception.ts +9 -0
  115. package/src/exceptions/invalid-format-exception.ts +5 -0
  116. package/src/exceptions/parser-exception.ts +4 -0
  117. package/src/exceptions/path-not-found-exception.ts +4 -0
  118. package/src/exceptions/readonly-violation-exception.ts +4 -0
  119. package/src/exceptions/security-exception.ts +6 -0
  120. package/src/exceptions/unsupported-type-exception.ts +4 -0
  121. package/src/exceptions/yaml-parse-exception.ts +4 -0
  122. package/src/index.ts +3 -1
  123. package/src/inline.ts +42 -120
  124. package/src/parser/xml-parser.ts +31 -10
  125. package/src/parser/yaml-parser.ts +310 -45
  126. package/src/path-query/segment-filter-parser.ts +444 -0
  127. package/src/path-query/segment-parser.ts +321 -0
  128. package/src/path-query/segment-path-resolver.ts +521 -0
  129. package/src/path-query/segment-type.ts +82 -0
  130. package/src/security/forbidden-keys.ts +5 -5
  131. package/src/security/security-guard.ts +7 -2
  132. package/src/security/security-parser.ts +18 -3
  133. package/src/type-format.ts +2 -0
  134. package/stryker.config.json +8 -10
  135. package/tests/accessors/abstract-accessor.test.ts +217 -0
  136. package/tests/accessors/abstract-integration-accessor.test.ts +37 -0
  137. package/tests/accessors/formats/any-accessor.test.ts +57 -0
  138. package/tests/accessors/formats/array-accessor.test.ts +42 -0
  139. package/tests/accessors/formats/env-accessor.test.ts +103 -0
  140. package/tests/accessors/formats/ini-accessor.test.ts +186 -0
  141. package/tests/accessors/{json-accessor.test.ts → formats/json-accessor.test.ts} +6 -6
  142. package/tests/accessors/formats/ndjson-accessor.test.ts +49 -0
  143. package/tests/accessors/formats/object-accessor.test.ts +172 -0
  144. package/tests/accessors/formats/xml-accessor.test.ts +162 -0
  145. package/tests/accessors/formats/yaml-accessor.test.ts +36 -0
  146. package/tests/cache/simple-path-cache.test.ts +168 -0
  147. package/tests/core/accessor-factory.test.ts +157 -0
  148. package/tests/core/dot-notation-parser-edge-cases.test.ts +415 -0
  149. package/tests/core/dot-notation-parser.test.ts +0 -288
  150. package/tests/core/inline-builder-accessor.test.ts +114 -0
  151. package/tests/exceptions/accessor-exception.test.ts +28 -0
  152. package/tests/exceptions/invalid-format-exception.test.ts +31 -0
  153. package/tests/exceptions/path-not-found-exception.test.ts +33 -0
  154. package/tests/exceptions/readonly-violation-exception.test.ts +35 -0
  155. package/tests/exceptions/security-exception.test.ts +33 -0
  156. package/tests/exceptions/unsupported-type-exception.test.ts +33 -0
  157. package/tests/exceptions/yaml-parse-exception.test.ts +38 -0
  158. package/tests/mocks/fake-path-cache.ts +4 -3
  159. package/tests/parity-from.test.ts +118 -0
  160. package/tests/parity.test.ts +227 -10
  161. package/tests/parser/xml-parser-mutations.test.ts +579 -0
  162. package/tests/parser/xml-parser-scanner.test.ts +332 -0
  163. package/tests/parser/xml-parser.test.ts +10 -334
  164. package/tests/parser/yaml-parser-mutations.test.ts +750 -0
  165. package/tests/parser/yaml-parser.test.ts +844 -18
  166. package/tests/path-query/segment-filter-parser-mutations.test.ts +735 -0
  167. package/tests/path-query/segment-filter-parser.test.ts +1091 -0
  168. package/tests/path-query/segment-parser-mutations.test.ts +539 -0
  169. package/tests/path-query/segment-parser.test.ts +606 -0
  170. package/tests/path-query/segment-path-resolver-mutations.test.ts +626 -0
  171. package/tests/path-query/segment-path-resolver.test.ts +1009 -0
  172. package/tests/security/security-guard-advanced.test.ts +413 -0
  173. package/tests/security/security-guard-forbidden-keys.test.ts +87 -0
  174. package/tests/security/security-guard.test.ts +3 -484
  175. package/tests/security/security-parser.test.ts +18 -14
  176. package/vitest.config.ts +3 -3
  177. package/benchmarks/get.bench.ts +0 -26
  178. package/benchmarks/parse.bench.ts +0 -41
  179. package/tests/accessors/accessors.test.ts +0 -1017
@@ -0,0 +1,351 @@
1
+ import { SecurityException } from '../exceptions/security-exception.js';
2
+ import { SegmentType } from './segment-type.js';
3
+ /**
4
+ * Resolve typed path segments against nested data structures.
5
+ *
6
+ * Traverses data using segment arrays produced by {@link SegmentParser},
7
+ * dispatching to segment-type-specific handlers for key, wildcard,
8
+ * descent, filter, slice, multi-key/index, and projection operations.
9
+ *
10
+ * @internal
11
+ *
12
+ * @see SegmentParser Produces the segment arrays this resolver consumes.
13
+ * @see SegmentType Enum governing which handler is dispatched.
14
+ * @see FilterEvaluatorInterface Delegate for filter predicate evaluation.
15
+ * @see DotNotationParser Invokes this resolver for path queries.
16
+ */
17
+ export class SegmentPathResolver {
18
+ segmentFilterParser;
19
+ /**
20
+ * Create a resolver with a filter evaluator.
21
+ *
22
+ * @param segmentFilterParser - Delegate for filter evaluation.
23
+ */
24
+ constructor(segmentFilterParser) {
25
+ this.segmentFilterParser = segmentFilterParser;
26
+ }
27
+ /**
28
+ * Resolve a value by walking segments starting at the given index.
29
+ *
30
+ * @param current - Current data node.
31
+ * @param segments - Typed segment array from {@link SegmentParser}.
32
+ * @param index - Current segment index.
33
+ * @param defaultValue - Fallback value when resolution fails.
34
+ * @param maxDepth - Maximum recursion depth.
35
+ * @returns Resolved value or the default.
36
+ *
37
+ * @throws {SecurityException} When recursion depth exceeds the limit.
38
+ */
39
+ resolve(current, segments, index, defaultValue, maxDepth) {
40
+ if (index > maxDepth) {
41
+ throw new SecurityException(`Recursion depth ${index} exceeds maximum of ${maxDepth}.`);
42
+ }
43
+ if (index >= segments.length) {
44
+ return current;
45
+ }
46
+ const segment = segments[index];
47
+ switch (segment.type) {
48
+ case SegmentType.Descent:
49
+ return this.segmentDescent(current, segments, index, defaultValue, maxDepth);
50
+ case SegmentType.DescentMulti:
51
+ return this.segmentDescentMulti(current, segments, index, defaultValue, maxDepth);
52
+ case SegmentType.Wildcard:
53
+ return this.segmentWildcard(current, segments, index, defaultValue, maxDepth);
54
+ case SegmentType.Filter:
55
+ return this.segmentFilter(current, segments, index, defaultValue, maxDepth);
56
+ case SegmentType.MultiKey:
57
+ return this.segmentMultiKey(current, segments, index, defaultValue, maxDepth);
58
+ case SegmentType.MultiIndex:
59
+ return this.segmentMultiIndex(current, segments, index, defaultValue, maxDepth);
60
+ case SegmentType.Slice:
61
+ return this.segmentSlice(current, segments, index, defaultValue, maxDepth);
62
+ case SegmentType.Projection:
63
+ return this.segmentProjection(current, segments, index, defaultValue, maxDepth);
64
+ default:
65
+ return this.segmentAny(current, segments, index, defaultValue, maxDepth);
66
+ }
67
+ }
68
+ /**
69
+ * Resolve a simple key/index segment.
70
+ *
71
+ * @param current - Current data node.
72
+ * @param segments - Typed segment array.
73
+ * @param index - Current segment index.
74
+ * @param defaultValue - Fallback value when resolution fails.
75
+ * @param maxDepth - Maximum recursion depth.
76
+ * @returns Resolved value or the default.
77
+ */
78
+ segmentAny(current, segments, index, defaultValue, maxDepth) {
79
+ const segment = segments[index];
80
+ const keyValue = 'value' in segment ? segment.value : '';
81
+ if (typeof current === 'object' &&
82
+ current !== null &&
83
+ Object.prototype.hasOwnProperty.call(current, keyValue)) {
84
+ return this.resolve(current[keyValue], segments, index + 1, defaultValue, maxDepth);
85
+ }
86
+ return defaultValue;
87
+ }
88
+ /**
89
+ * Resolve a recursive descent segment for a single key.
90
+ *
91
+ * @param current - Current data node.
92
+ * @param segments - Typed segment array.
93
+ * @param index - Current segment index.
94
+ * @param defaultValue - Fallback value when resolution fails.
95
+ * @param maxDepth - Maximum recursion depth.
96
+ * @returns Array of all values matching the descent key.
97
+ */
98
+ segmentDescent(current, segments, index, defaultValue, maxDepth) {
99
+ const results = [];
100
+ const segment = segments[index];
101
+ const descentKey = segment.key;
102
+ this.collectDescent(current, descentKey, segments, index + 1, defaultValue, results, maxDepth);
103
+ return results;
104
+ }
105
+ /**
106
+ * Resolve a recursive descent segment for multiple keys.
107
+ *
108
+ * @param current - Current data node.
109
+ * @param segments - Typed segment array.
110
+ * @param index - Current segment index.
111
+ * @param defaultValue - Fallback value when resolution fails.
112
+ * @param maxDepth - Maximum recursion depth.
113
+ * @returns Array of all values matching any of the descent keys.
114
+ */
115
+ segmentDescentMulti(current, segments, index, defaultValue, maxDepth) {
116
+ const segment = segments[index];
117
+ const descentKeys = segment.keys;
118
+ const results = [];
119
+ for (const dk of descentKeys) {
120
+ this.collectDescent(current, dk, segments, index + 1, defaultValue, results, maxDepth);
121
+ }
122
+ return results.length > 0 ? results : defaultValue;
123
+ }
124
+ /**
125
+ * Resolve a wildcard segment, expanding all children.
126
+ *
127
+ * @param current - Current data node.
128
+ * @param segments - Typed segment array.
129
+ * @param index - Current segment index.
130
+ * @param defaultValue - Fallback value when resolution fails.
131
+ * @param maxDepth - Maximum recursion depth.
132
+ * @returns Array of resolved values for all children.
133
+ */
134
+ segmentWildcard(current, segments, index, defaultValue, maxDepth) {
135
+ if (typeof current !== 'object' || current === null) {
136
+ return defaultValue;
137
+ }
138
+ const items = Array.isArray(current)
139
+ ? current
140
+ : Object.values(current);
141
+ const nextIndex = index + 1;
142
+ return items.map((item) => this.resolve(item, segments, nextIndex, defaultValue, maxDepth));
143
+ }
144
+ /**
145
+ * Resolve a filter segment, applying predicates to array items.
146
+ *
147
+ * @param current - Current data node.
148
+ * @param segments - Typed segment array.
149
+ * @param index - Current segment index.
150
+ * @param defaultValue - Fallback value when resolution fails.
151
+ * @param maxDepth - Maximum recursion depth.
152
+ * @returns Array of items passing the filter predicate.
153
+ */
154
+ segmentFilter(current, segments, index, defaultValue, maxDepth) {
155
+ if (typeof current !== 'object' || current === null) {
156
+ return defaultValue;
157
+ }
158
+ const segment = segments[index];
159
+ const filterExpr = segment.expression;
160
+ const items = Array.isArray(current)
161
+ ? current
162
+ : Object.values(current);
163
+ const filtered = [];
164
+ for (const item of items) {
165
+ if (typeof item === 'object' && item !== null) {
166
+ if (this.segmentFilterParser.evaluate(item, filterExpr)) {
167
+ filtered.push(item);
168
+ }
169
+ }
170
+ }
171
+ const nextIndex = index + 1;
172
+ return filtered.map((item) => this.resolve(item, segments, nextIndex, defaultValue, maxDepth));
173
+ }
174
+ /**
175
+ * Resolve a multi-key segment, selecting values by multiple keys.
176
+ *
177
+ * @param current - Current data node.
178
+ * @param segments - Typed segment array.
179
+ * @param index - Current segment index.
180
+ * @param defaultValue - Fallback value when resolution fails.
181
+ * @param maxDepth - Maximum recursion depth.
182
+ * @returns Array of resolved values for each key.
183
+ */
184
+ segmentMultiKey(current, segments, index, defaultValue, maxDepth) {
185
+ if (typeof current !== 'object' || current === null) {
186
+ return defaultValue;
187
+ }
188
+ const nextIndex = index + 1;
189
+ const segment = segments[index];
190
+ const multiKeys = segment.keys;
191
+ const obj = current;
192
+ return multiKeys.map((k) => {
193
+ const val = Object.prototype.hasOwnProperty.call(obj, k) ? obj[k] : defaultValue;
194
+ return this.resolve(val, segments, nextIndex, defaultValue, maxDepth);
195
+ });
196
+ }
197
+ /**
198
+ * Resolve a multi-index segment, selecting values by multiple indices.
199
+ *
200
+ * @param current - Current data node.
201
+ * @param segments - Typed segment array.
202
+ * @param index - Current segment index.
203
+ * @param defaultValue - Fallback value when resolution fails.
204
+ * @param maxDepth - Maximum recursion depth.
205
+ * @returns Array of resolved values for each index.
206
+ */
207
+ segmentMultiIndex(current, segments, index, defaultValue, maxDepth) {
208
+ if (typeof current !== 'object' || current === null) {
209
+ return defaultValue;
210
+ }
211
+ const nextIndex = index + 1;
212
+ const segment = segments[index];
213
+ const indices = segment.indices;
214
+ const items = Array.isArray(current)
215
+ ? current
216
+ : Object.values(current);
217
+ const len = items.length;
218
+ return indices.map((idx) => {
219
+ const resolved = idx < 0 ? (items[len + idx] ?? null) : (items[idx] ?? null);
220
+ if (resolved === null) {
221
+ return defaultValue;
222
+ }
223
+ return this.resolve(resolved, segments, nextIndex, defaultValue, maxDepth);
224
+ });
225
+ }
226
+ /**
227
+ * Resolve a slice segment on an array (start:end:step).
228
+ *
229
+ * @param current - Current data node.
230
+ * @param segments - Typed segment array.
231
+ * @param index - Current segment index.
232
+ * @param defaultValue - Fallback value when resolution fails.
233
+ * @param maxDepth - Maximum recursion depth.
234
+ * @returns Array of resolved values matching the slice range.
235
+ */
236
+ segmentSlice(current, segments, index, defaultValue, maxDepth) {
237
+ if (typeof current !== 'object' || current === null) {
238
+ return defaultValue;
239
+ }
240
+ const items = Array.isArray(current)
241
+ ? current
242
+ : Object.values(current);
243
+ const len = items.length;
244
+ const segment = segments[index];
245
+ const step = segment.step ?? 1;
246
+ let start = segment.start ?? (step > 0 ? 0 : len - 1);
247
+ let end = segment.end ?? (step > 0 ? len : -len - 1);
248
+ if (start < 0) {
249
+ start = Math.max(len + start, 0);
250
+ }
251
+ if (end < 0) {
252
+ end = len + end;
253
+ }
254
+ if (start >= len) {
255
+ start = len;
256
+ }
257
+ if (end > len) {
258
+ end = len;
259
+ }
260
+ const sliced = [];
261
+ if (step > 0) {
262
+ for (let si = start; si < end; si += step) {
263
+ sliced.push(items[si]);
264
+ }
265
+ }
266
+ else {
267
+ for (let si = start; si > end; si += step) {
268
+ sliced.push(items[si]);
269
+ }
270
+ }
271
+ const nextSliceIndex = index + 1;
272
+ return sliced.map((item) => this.resolve(item, segments, nextSliceIndex, defaultValue, maxDepth));
273
+ }
274
+ /**
275
+ * Resolve a projection segment, selecting specific fields from items.
276
+ *
277
+ * @param current - Current data node.
278
+ * @param segments - Typed segment array.
279
+ * @param index - Current segment index.
280
+ * @param defaultValue - Fallback value when resolution fails.
281
+ * @param maxDepth - Maximum recursion depth.
282
+ * @returns Projected item(s) with only the specified fields.
283
+ */
284
+ segmentProjection(current, segments, index, defaultValue, maxDepth) {
285
+ const segment = segments[index];
286
+ const fields = segment.fields;
287
+ const projectItem = (item) => {
288
+ if (typeof item !== 'object' || item === null) {
289
+ const result = {};
290
+ for (const field of fields) {
291
+ result[field.alias] = null;
292
+ }
293
+ return result;
294
+ }
295
+ const obj = item;
296
+ const result = {};
297
+ for (const field of fields) {
298
+ result[field.alias] = Object.prototype.hasOwnProperty.call(obj, field.source)
299
+ ? obj[field.source]
300
+ : null;
301
+ }
302
+ return result;
303
+ };
304
+ const nextProjectionIndex = index + 1;
305
+ if (Array.isArray(current)) {
306
+ const projected = current.map(projectItem);
307
+ return projected.map((item) => this.resolve(item, segments, nextProjectionIndex, defaultValue, maxDepth));
308
+ }
309
+ if (typeof current === 'object' && current !== null) {
310
+ const result = projectItem(current);
311
+ return this.resolve(result, segments, nextProjectionIndex, defaultValue, maxDepth);
312
+ }
313
+ return defaultValue;
314
+ }
315
+ /**
316
+ * Recursively collect values matching a descent key from nested data.
317
+ *
318
+ * @param current - Current data node.
319
+ * @param key - Key to search for recursively.
320
+ * @param segments - Typed segment array.
321
+ * @param nextIndex - Next segment index after the descent.
322
+ * @param defaultValue - Fallback value.
323
+ * @param results - Collector array (mutated in place).
324
+ * @param maxDepth - Maximum recursion depth.
325
+ */
326
+ collectDescent(current, key, segments, nextIndex, defaultValue, results, maxDepth) {
327
+ if (typeof current !== 'object' || current === null) {
328
+ return;
329
+ }
330
+ const obj = current;
331
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
332
+ if (nextIndex >= segments.length) {
333
+ results.push(obj[key]);
334
+ }
335
+ else {
336
+ const resolved = this.resolve(obj[key], segments, nextIndex, defaultValue, maxDepth);
337
+ if (Array.isArray(resolved)) {
338
+ results.push(...resolved);
339
+ }
340
+ else {
341
+ results.push(resolved);
342
+ }
343
+ }
344
+ }
345
+ for (const child of Object.values(obj)) {
346
+ if (typeof child === 'object' && child !== null) {
347
+ this.collectDescent(child, key, segments, nextIndex, defaultValue, results, maxDepth);
348
+ }
349
+ }
350
+ }
351
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Enumerate all segment types produced by the path parser.
3
+ *
4
+ * Each case represents a distinct addressing mode within a dot-notation
5
+ * path expression. The {@link SegmentPathResolver} dispatches resolution
6
+ * logic based on the segment type.
7
+ *
8
+ * @internal
9
+ *
10
+ * @see SegmentParser Parses path strings into typed segment arrays.
11
+ * @see SegmentPathResolver Resolves data values from typed segments.
12
+ */
13
+ export declare enum SegmentType {
14
+ /** Simple key or index access (e.g. `foo`, `0`). */
15
+ Key = "key",
16
+ /** Numeric index access (e.g. `[0]`). */
17
+ Index = "index",
18
+ /** Wildcard expansion over all children (e.g. `*`, `[*]`). */
19
+ Wildcard = "wildcard",
20
+ /** Recursive descent into a single key (e.g. `..name`). */
21
+ Descent = "descent",
22
+ /** Recursive descent into multiple keys (e.g. `..["a","b"]`). */
23
+ DescentMulti = "descent-multi",
24
+ /** Multi-index selection (e.g. `[0,1,2]`). */
25
+ MultiIndex = "multi-index",
26
+ /** Multi-key selection (e.g. `['a','b']`). */
27
+ MultiKey = "multi-key",
28
+ /** Filter predicate expression (e.g. `[?age>18]`). */
29
+ Filter = "filter",
30
+ /** Array slice notation (e.g. `[0:5]`, `[::2]`). */
31
+ Slice = "slice",
32
+ /** Field projection (e.g. `.{name, age}`). */
33
+ Projection = "projection"
34
+ }
35
+ /** Parsed filter expression structure. */
36
+ export interface FilterExpression {
37
+ readonly conditions: ReadonlyArray<FilterCondition>;
38
+ readonly logicals: ReadonlyArray<string>;
39
+ }
40
+ /** A single parsed filter condition. */
41
+ export interface FilterCondition {
42
+ readonly field: string;
43
+ readonly operator: string;
44
+ readonly value: boolean | null | number | string;
45
+ readonly func?: string;
46
+ readonly funcArgs?: ReadonlyArray<string>;
47
+ }
48
+ /** Projection field mapping. */
49
+ export interface ProjectionField {
50
+ readonly alias: string;
51
+ readonly source: string;
52
+ }
53
+ /** Discriminated union of all possible segment shapes. */
54
+ export type Segment = {
55
+ readonly type: SegmentType.Key;
56
+ readonly value: string;
57
+ } | {
58
+ readonly type: SegmentType.Index;
59
+ readonly value: string;
60
+ } | {
61
+ readonly type: SegmentType.Wildcard;
62
+ } | {
63
+ readonly type: SegmentType.Descent;
64
+ readonly key: string;
65
+ } | {
66
+ readonly type: SegmentType.DescentMulti;
67
+ readonly keys: ReadonlyArray<string>;
68
+ } | {
69
+ readonly type: SegmentType.MultiIndex;
70
+ readonly indices: ReadonlyArray<number>;
71
+ } | {
72
+ readonly type: SegmentType.MultiKey;
73
+ readonly keys: ReadonlyArray<string>;
74
+ } | {
75
+ readonly type: SegmentType.Filter;
76
+ readonly expression: FilterExpression;
77
+ } | {
78
+ readonly type: SegmentType.Slice;
79
+ readonly start: number | null;
80
+ readonly end: number | null;
81
+ readonly step: number | null;
82
+ } | {
83
+ readonly type: SegmentType.Projection;
84
+ readonly fields: ReadonlyArray<ProjectionField>;
85
+ };
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Enumerate all segment types produced by the path parser.
3
+ *
4
+ * Each case represents a distinct addressing mode within a dot-notation
5
+ * path expression. The {@link SegmentPathResolver} dispatches resolution
6
+ * logic based on the segment type.
7
+ *
8
+ * @internal
9
+ *
10
+ * @see SegmentParser Parses path strings into typed segment arrays.
11
+ * @see SegmentPathResolver Resolves data values from typed segments.
12
+ */
13
+ export var SegmentType;
14
+ (function (SegmentType) {
15
+ /** Simple key or index access (e.g. `foo`, `0`). */
16
+ SegmentType["Key"] = "key";
17
+ /** Numeric index access (e.g. `[0]`). */
18
+ SegmentType["Index"] = "index";
19
+ /** Wildcard expansion over all children (e.g. `*`, `[*]`). */
20
+ SegmentType["Wildcard"] = "wildcard";
21
+ /** Recursive descent into a single key (e.g. `..name`). */
22
+ SegmentType["Descent"] = "descent";
23
+ /** Recursive descent into multiple keys (e.g. `..["a","b"]`). */
24
+ SegmentType["DescentMulti"] = "descent-multi";
25
+ /** Multi-index selection (e.g. `[0,1,2]`). */
26
+ SegmentType["MultiIndex"] = "multi-index";
27
+ /** Multi-key selection (e.g. `['a','b']`). */
28
+ SegmentType["MultiKey"] = "multi-key";
29
+ /** Filter predicate expression (e.g. `[?age>18]`). */
30
+ SegmentType["Filter"] = "filter";
31
+ /** Array slice notation (e.g. `[0:5]`, `[::2]`). */
32
+ SegmentType["Slice"] = "slice";
33
+ /** Field projection (e.g. `.{name, age}`). */
34
+ SegmentType["Projection"] = "projection";
35
+ })(SegmentType || (SegmentType = {}));
@@ -5,7 +5,7 @@
5
5
  * the same word prefix (e.g. `node_modules`) are not blocked.
6
6
  *
7
7
  * PHP-specific wrappers (`phar://`, `php://`, `expect://`, `glob://`, `zlib://`,
8
- * `ogg://`, `rar://`, `zip://`, `ssh2.tunnel://`) are intentionally absent they
8
+ * `ogg://`, `rar://`, `zip://`, `ssh2.tunnel://`) are intentionally absent - they
9
9
  * have no meaning in a JavaScript/Node.js runtime.
10
10
  *
11
11
  * `data:` uses a single-colon delimiter (matching the browser RFC 2397 format
@@ -26,7 +26,7 @@ export declare const STREAM_WRAPPER_PREFIXES: readonly string[];
26
26
  * – `hasOwnProperty` shadow (overriding it can bypass guard checks)
27
27
  * – JS-relevant stream wrapper / protocol scheme strings as exact-match defence-in-depth
28
28
  *
29
- * PHP magic methods and PHP superglobals are deliberately absent they are not
29
+ * PHP magic methods and PHP superglobals are deliberately absent - they are not
30
30
  * meaningful in a JavaScript runtime and belong in the PHP package's SecurityGuard only.
31
31
  *
32
32
  * @internal
@@ -5,7 +5,7 @@
5
5
  * the same word prefix (e.g. `node_modules`) are not blocked.
6
6
  *
7
7
  * PHP-specific wrappers (`phar://`, `php://`, `expect://`, `glob://`, `zlib://`,
8
- * `ogg://`, `rar://`, `zip://`, `ssh2.tunnel://`) are intentionally absent they
8
+ * `ogg://`, `rar://`, `zip://`, `ssh2.tunnel://`) are intentionally absent - they
9
9
  * have no meaning in a JavaScript/Node.js runtime.
10
10
  *
11
11
  * `data:` uses a single-colon delimiter (matching the browser RFC 2397 format
@@ -37,7 +37,7 @@ export const STREAM_WRAPPER_PREFIXES = [
37
37
  * – `hasOwnProperty` shadow (overriding it can bypass guard checks)
38
38
  * – JS-relevant stream wrapper / protocol scheme strings as exact-match defence-in-depth
39
39
  *
40
- * PHP magic methods and PHP superglobals are deliberately absent they are not
40
+ * PHP magic methods and PHP superglobals are deliberately absent - they are not
41
41
  * meaningful in a JavaScript runtime and belong in the PHP package's SecurityGuard only.
42
42
  *
43
43
  * @internal
@@ -52,13 +52,13 @@ export const DEFAULT_FORBIDDEN_KEYS = new Set([
52
52
  '__definesetter__',
53
53
  '__lookupgetter__',
54
54
  '__lookupsetter__',
55
- // Object.prototype shadow key overriding it can break hasOwnProperty-based guards
55
+ // Object.prototype shadow key - overriding it can break hasOwnProperty-based guards
56
56
  'hasOwnProperty',
57
- // Node.js module-scope path globals should never appear as data keys to
57
+ // Node.js module-scope path globals - should never appear as data keys to
58
58
  // prevent path-injection risks in code that reads them via dynamic property access
59
59
  '__dirname',
60
60
  '__filename',
61
- // Stream wrapper and protocol exact entries also caught by STREAM_WRAPPER_PREFIXES prefix matching.
61
+ // Stream wrapper and protocol exact entries - also caught by STREAM_WRAPPER_PREFIXES prefix matching.
62
62
  // The Set entries below are intentional defence-in-depth: they allow O(1) exact-key
63
63
  // lookup before the O(n) prefix loop runs.
64
64
  'file://',
@@ -15,6 +15,8 @@ import type { SecurityGuardInterface } from '../contracts/security-guard-interfa
15
15
  * Stream wrapper URIs are matched by prefix so that fully-formed URIs such as
16
16
  * `javascript:alert(1)` are also blocked, not only the bare scheme string.
17
17
  *
18
+ * @api
19
+ *
18
20
  * @example
19
21
  * const guard = new SecurityGuard();
20
22
  * guard.assertSafeKey('name'); // OK
@@ -29,7 +31,7 @@ export declare class SecurityGuard implements SecurityGuardInterface {
29
31
  * @param maxDepth - Maximum recursion depth for recursive key scanning.
30
32
  * @param extraForbiddenKeys - Additional keys to forbid beyond defaults.
31
33
  */
32
- constructor(maxDepth?: number, /* Stryker disable next-line ArrayDeclaration -- equivalent: default [] produces identical behavior; no extra keys added to Set */ extraForbiddenKeys?: string[]);
34
+ constructor(maxDepth?: number, extraForbiddenKeys?: string[]);
33
35
  /**
34
36
  * Check whether a key is in the forbidden list.
35
37
  *
@@ -16,6 +16,8 @@ import { DEFAULT_FORBIDDEN_KEYS, STREAM_WRAPPER_PREFIXES } from './forbidden-key
16
16
  * Stream wrapper URIs are matched by prefix so that fully-formed URIs such as
17
17
  * `javascript:alert(1)` are also blocked, not only the bare scheme string.
18
18
  *
19
+ * @api
20
+ *
19
21
  * @example
20
22
  * const guard = new SecurityGuard();
21
23
  * guard.assertSafeKey('name'); // OK
@@ -30,7 +32,8 @@ export class SecurityGuard {
30
32
  * @param maxDepth - Maximum recursion depth for recursive key scanning.
31
33
  * @param extraForbiddenKeys - Additional keys to forbid beyond defaults.
32
34
  */
33
- constructor(maxDepth = 512, /* Stryker disable next-line ArrayDeclaration -- equivalent: default [] produces identical behavior; no extra keys added to Set */ extraForbiddenKeys = []) {
35
+ constructor(maxDepth = 512,
36
+ /* Stryker disable next-line ArrayDeclaration -- equivalent: default [] produces identical behavior; no extra keys added to Set */ extraForbiddenKeys = []) {
34
37
  this.maxDepth = Number.isFinite(maxDepth) ? maxDepth : 512;
35
38
  /* Stryker disable next-line ConditionalExpression -- equivalent: if (false) still produces the same forbiddenKeysMap for empty arrays since Set(DEFAULT)=DEFAULT */
36
39
  if (extraForbiddenKeys.length === 0) {
@@ -59,7 +62,7 @@ export class SecurityGuard {
59
62
  * guard.isForbiddenKey('name'); // false
60
63
  */
61
64
  isForbiddenKey(key) {
62
- // Normalise __* keys to lowercase catches case variants such as __PROTO__.
65
+ // Normalise __* keys to lowercase - catches case variants such as __PROTO__.
63
66
  const lookupKey = key.startsWith('__') ? key.toLowerCase() : key;
64
67
  if (this.forbiddenKeysMap.has(lookupKey)) {
65
68
  return true;
@@ -5,6 +5,8 @@ import type { SecurityParserInterface } from '../contracts/security-parser-inter
5
5
  * Validates payload size, maximum key count, recursion depth, and
6
6
  * structural depth limits.
7
7
  *
8
+ * @api
9
+ *
8
10
  * @example
9
11
  * const parser = new SecurityParser({ maxDepth: 10, maxKeys: 100 });
10
12
  * parser.assertPayloadSize('{"key":"value"}');
@@ -24,7 +26,7 @@ export declare class SecurityParser implements SecurityParserInterface {
24
26
  * @param options.maxKeys - Maximum total number of keys across the entire structure. Default: 10000.
25
27
  * This value is also passed to `XmlParser` as the element-count cap for the Node.js manual XML
26
28
  * parser path. Setting it below a document's element count will cause `fromXml()` to throw
27
- * `SecurityException`. Non-positive or non-finite values disable that guard prefer the default.
29
+ * `SecurityException`. Non-positive or non-finite values disable that guard - prefer the default.
28
30
  * @param options.maxCountRecursiveDepth - Maximum recursion depth when counting keys. Default: 100.
29
31
  * @param options.maxResolveDepth - Maximum recursion depth for path resolution. Default: 100.
30
32
  */
@@ -126,5 +128,12 @@ export declare class SecurityParser implements SecurityParserInterface {
126
128
  * @returns Maximum depth found.
127
129
  */
128
130
  private measureDepth;
131
+ /**
132
+ * Clamp an optional numeric option to its default when not finite.
133
+ *
134
+ * @param value - User-provided option value.
135
+ * @param defaultValue - Fallback when value is undefined or non-finite.
136
+ * @returns Clamped numeric value.
137
+ */
129
138
  private static clampOption;
130
139
  }
@@ -5,6 +5,8 @@ import { SecurityException } from '../exceptions/security-exception.js';
5
5
  * Validates payload size, maximum key count, recursion depth, and
6
6
  * structural depth limits.
7
7
  *
8
+ * @api
9
+ *
8
10
  * @example
9
11
  * const parser = new SecurityParser({ maxDepth: 10, maxKeys: 100 });
10
12
  * parser.assertPayloadSize('{"key":"value"}');
@@ -24,7 +26,7 @@ export class SecurityParser {
24
26
  * @param options.maxKeys - Maximum total number of keys across the entire structure. Default: 10000.
25
27
  * This value is also passed to `XmlParser` as the element-count cap for the Node.js manual XML
26
28
  * parser path. Setting it below a document's element count will cause `fromXml()` to throw
27
- * `SecurityException`. Non-positive or non-finite values disable that guard prefer the default.
29
+ * `SecurityException`. Non-positive or non-finite values disable that guard - prefer the default.
28
30
  * @param options.maxCountRecursiveDepth - Maximum recursion depth when counting keys. Default: 100.
29
31
  * @param options.maxResolveDepth - Maximum recursion depth for path resolution. Default: 100.
30
32
  */
@@ -185,6 +187,13 @@ export class SecurityParser {
185
187
  }
186
188
  return max;
187
189
  }
190
+ /**
191
+ * Clamp an optional numeric option to its default when not finite.
192
+ *
193
+ * @param value - User-provided option value.
194
+ * @param defaultValue - Fallback when value is undefined or non-finite.
195
+ * @returns Clamped numeric value.
196
+ */
188
197
  static clampOption(value, defaultValue) {
189
198
  /* Stryker disable next-line ConditionalExpression -- equivalent: Number.isFinite covers undefined (isFinite(undefined)===false); simplest safe form */
190
199
  return Number.isFinite(value) ? value : defaultValue;
@@ -3,6 +3,8 @@
3
3
  *
4
4
  * Used by {@link Inline.from} to select the appropriate accessor.
5
5
  *
6
+ * @api
7
+ *
6
8
  * @example
7
9
  * const accessor = Inline.from(TypeFormat.Json, '{"key":"value"}');
8
10
  */
@@ -3,6 +3,8 @@
3
3
  *
4
4
  * Used by {@link Inline.from} to select the appropriate accessor.
5
5
  *
6
+ * @api
7
+ *
6
8
  * @example
7
9
  * const accessor = Inline.from(TypeFormat.Json, '{"key":"value"}');
8
10
  */