@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.
Files changed (129) hide show
  1. package/.gitattributes +16 -0
  2. package/.gitkeep +0 -0
  3. package/CHANGELOG.md +38 -0
  4. package/LICENSE +21 -0
  5. package/README.md +454 -0
  6. package/benchmarks/get.bench.ts +26 -0
  7. package/benchmarks/parse.bench.ts +41 -0
  8. package/dist/accessors/abstract-accessor.d.ts +213 -0
  9. package/dist/accessors/abstract-accessor.js +294 -0
  10. package/dist/accessors/formats/any-accessor.d.ts +35 -0
  11. package/dist/accessors/formats/any-accessor.js +44 -0
  12. package/dist/accessors/formats/array-accessor.d.ts +26 -0
  13. package/dist/accessors/formats/array-accessor.js +39 -0
  14. package/dist/accessors/formats/env-accessor.d.ts +27 -0
  15. package/dist/accessors/formats/env-accessor.js +64 -0
  16. package/dist/accessors/formats/ini-accessor.d.ts +41 -0
  17. package/dist/accessors/formats/ini-accessor.js +109 -0
  18. package/dist/accessors/formats/json-accessor.d.ts +26 -0
  19. package/dist/accessors/formats/json-accessor.js +56 -0
  20. package/dist/accessors/formats/ndjson-accessor.d.ts +28 -0
  21. package/dist/accessors/formats/ndjson-accessor.js +71 -0
  22. package/dist/accessors/formats/object-accessor.d.ts +48 -0
  23. package/dist/accessors/formats/object-accessor.js +90 -0
  24. package/dist/accessors/formats/xml-accessor.d.ts +27 -0
  25. package/dist/accessors/formats/xml-accessor.js +52 -0
  26. package/dist/accessors/formats/yaml-accessor.d.ts +29 -0
  27. package/dist/accessors/formats/yaml-accessor.js +46 -0
  28. package/dist/contracts/accessors-interface.d.ts +11 -0
  29. package/dist/contracts/accessors-interface.js +1 -0
  30. package/dist/contracts/factory-accessors-interface.d.ts +16 -0
  31. package/dist/contracts/factory-accessors-interface.js +1 -0
  32. package/dist/contracts/parse-integration-interface.d.ts +31 -0
  33. package/dist/contracts/parse-integration-interface.js +1 -0
  34. package/dist/contracts/path-cache-interface.d.ts +40 -0
  35. package/dist/contracts/path-cache-interface.js +1 -0
  36. package/dist/contracts/readable-accessors-interface.d.ts +79 -0
  37. package/dist/contracts/readable-accessors-interface.js +1 -0
  38. package/dist/contracts/security-guard-interface.d.ts +40 -0
  39. package/dist/contracts/security-guard-interface.js +1 -0
  40. package/dist/contracts/security-parser-interface.d.ts +67 -0
  41. package/dist/contracts/security-parser-interface.js +1 -0
  42. package/dist/contracts/writable-accessors-interface.d.ts +65 -0
  43. package/dist/contracts/writable-accessors-interface.js +1 -0
  44. package/dist/core/dot-notation-parser.d.ts +204 -0
  45. package/dist/core/dot-notation-parser.js +343 -0
  46. package/dist/exceptions/accessor-exception.d.ts +13 -0
  47. package/dist/exceptions/accessor-exception.js +16 -0
  48. package/dist/exceptions/invalid-format-exception.d.ts +14 -0
  49. package/dist/exceptions/invalid-format-exception.js +17 -0
  50. package/dist/exceptions/parser-exception.d.ts +14 -0
  51. package/dist/exceptions/parser-exception.js +17 -0
  52. package/dist/exceptions/path-not-found-exception.d.ts +14 -0
  53. package/dist/exceptions/path-not-found-exception.js +17 -0
  54. package/dist/exceptions/readonly-violation-exception.d.ts +15 -0
  55. package/dist/exceptions/readonly-violation-exception.js +18 -0
  56. package/dist/exceptions/security-exception.d.ts +18 -0
  57. package/dist/exceptions/security-exception.js +21 -0
  58. package/dist/exceptions/unsupported-type-exception.d.ts +14 -0
  59. package/dist/exceptions/unsupported-type-exception.js +17 -0
  60. package/dist/exceptions/yaml-parse-exception.d.ts +17 -0
  61. package/dist/exceptions/yaml-parse-exception.js +20 -0
  62. package/dist/index.d.ts +30 -0
  63. package/dist/index.js +30 -0
  64. package/dist/inline.d.ts +402 -0
  65. package/dist/inline.js +512 -0
  66. package/dist/parser/xml-parser.d.ts +46 -0
  67. package/dist/parser/xml-parser.js +288 -0
  68. package/dist/parser/yaml-parser.d.ts +94 -0
  69. package/dist/parser/yaml-parser.js +286 -0
  70. package/dist/security/forbidden-keys.d.ts +34 -0
  71. package/dist/security/forbidden-keys.js +80 -0
  72. package/dist/security/security-guard.d.ts +94 -0
  73. package/dist/security/security-guard.js +172 -0
  74. package/dist/security/security-parser.d.ts +130 -0
  75. package/dist/security/security-parser.js +192 -0
  76. package/dist/type-format.d.ts +28 -0
  77. package/dist/type-format.js +29 -0
  78. package/eslint.config.js +1 -0
  79. package/package.json +39 -0
  80. package/src/accessors/abstract-accessor.ts +353 -0
  81. package/src/accessors/formats/any-accessor.ts +51 -0
  82. package/src/accessors/formats/array-accessor.ts +45 -0
  83. package/src/accessors/formats/env-accessor.ts +79 -0
  84. package/src/accessors/formats/ini-accessor.ts +124 -0
  85. package/src/accessors/formats/json-accessor.ts +66 -0
  86. package/src/accessors/formats/ndjson-accessor.ts +82 -0
  87. package/src/accessors/formats/object-accessor.ts +100 -0
  88. package/src/accessors/formats/xml-accessor.ts +58 -0
  89. package/src/accessors/formats/yaml-accessor.ts +52 -0
  90. package/src/contracts/accessors-interface.ts +12 -0
  91. package/src/contracts/factory-accessors-interface.ts +16 -0
  92. package/src/contracts/parse-integration-interface.ts +32 -0
  93. package/src/contracts/path-cache-interface.ts +43 -0
  94. package/src/contracts/readable-accessors-interface.ts +88 -0
  95. package/src/contracts/security-guard-interface.ts +43 -0
  96. package/src/contracts/security-parser-interface.ts +74 -0
  97. package/src/contracts/writable-accessors-interface.ts +70 -0
  98. package/src/core/dot-notation-parser.ts +419 -0
  99. package/src/exceptions/accessor-exception.ts +16 -0
  100. package/src/exceptions/invalid-format-exception.ts +18 -0
  101. package/src/exceptions/parser-exception.ts +18 -0
  102. package/src/exceptions/path-not-found-exception.ts +18 -0
  103. package/src/exceptions/readonly-violation-exception.ts +19 -0
  104. package/src/exceptions/security-exception.ts +22 -0
  105. package/src/exceptions/unsupported-type-exception.ts +18 -0
  106. package/src/exceptions/yaml-parse-exception.ts +21 -0
  107. package/src/index.ts +46 -0
  108. package/src/inline.ts +570 -0
  109. package/src/parser/xml-parser.ts +334 -0
  110. package/src/parser/yaml-parser.ts +368 -0
  111. package/src/security/forbidden-keys.ts +81 -0
  112. package/src/security/security-guard.ts +195 -0
  113. package/src/security/security-parser.ts +233 -0
  114. package/src/type-format.ts +28 -0
  115. package/stryker.config.json +24 -0
  116. package/tests/accessors/accessors.test.ts +1017 -0
  117. package/tests/accessors/json-accessor.test.ts +171 -0
  118. package/tests/core/dot-notation-parser.test.ts +587 -0
  119. package/tests/exceptions/parser-exception.test.ts +31 -0
  120. package/tests/inline.test.ts +445 -0
  121. package/tests/mocks/fake-parse-integration.ts +24 -0
  122. package/tests/mocks/fake-path-cache.ts +31 -0
  123. package/tests/parity.test.ts +164 -0
  124. package/tests/parser/xml-parser.test.ts +618 -0
  125. package/tests/parser/yaml-parser.test.ts +463 -0
  126. package/tests/security/security-guard.test.ts +646 -0
  127. package/tests/security/security-parser.test.ts +391 -0
  128. package/tsconfig.json +16 -0
  129. package/vitest.config.ts +19 -0
@@ -0,0 +1,192 @@
1
+ import { SecurityException } from '../exceptions/security-exception.js';
2
+ /**
3
+ * Enforce structural security constraints on parsed data.
4
+ *
5
+ * Validates payload size, maximum key count, recursion depth, and
6
+ * structural depth limits.
7
+ *
8
+ * @example
9
+ * const parser = new SecurityParser({ maxDepth: 10, maxKeys: 100 });
10
+ * parser.assertPayloadSize('{"key":"value"}');
11
+ */
12
+ export class SecurityParser {
13
+ maxDepth;
14
+ maxPayloadBytes;
15
+ maxKeys;
16
+ maxCountRecursiveDepth;
17
+ maxResolveDepth;
18
+ /**
19
+ * Build security options from configuration values.
20
+ *
21
+ * @param options - Configuration overrides.
22
+ * @param options.maxDepth - Maximum allowed structural nesting depth. Default: 512.
23
+ * @param options.maxPayloadBytes - Maximum allowed raw payload size in bytes. Default: 10 MB.
24
+ * @param options.maxKeys - Maximum total number of keys across the entire structure. Default: 10000.
25
+ * This value is also passed to `XmlParser` as the element-count cap for the Node.js manual XML
26
+ * 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.
28
+ * @param options.maxCountRecursiveDepth - Maximum recursion depth when counting keys. Default: 100.
29
+ * @param options.maxResolveDepth - Maximum recursion depth for path resolution. Default: 100.
30
+ */
31
+ constructor(options = {}) {
32
+ this.maxDepth = SecurityParser.clampOption(options.maxDepth, 512);
33
+ this.maxPayloadBytes = SecurityParser.clampOption(options.maxPayloadBytes, 10 * 1024 * 1024);
34
+ this.maxKeys = SecurityParser.clampOption(options.maxKeys, 10_000);
35
+ this.maxCountRecursiveDepth = SecurityParser.clampOption(options.maxCountRecursiveDepth, 100);
36
+ this.maxResolveDepth = SecurityParser.clampOption(options.maxResolveDepth, 100);
37
+ }
38
+ /**
39
+ * Assert that a raw string payload does not exceed the byte limit.
40
+ *
41
+ * @param input - Raw input string to measure.
42
+ * @param maxBytes - Override limit, or undefined to use configured default.
43
+ * @throws {SecurityException} When the payload exceeds the limit.
44
+ *
45
+ * @example
46
+ * parser.assertPayloadSize('small input'); // OK
47
+ */
48
+ assertPayloadSize(input, maxBytes) {
49
+ const limit = maxBytes ?? this.maxPayloadBytes;
50
+ const size = new TextEncoder().encode(input).length;
51
+ if (size > limit) {
52
+ throw new SecurityException(`Payload size ${size} bytes exceeds maximum of ${limit} bytes.`);
53
+ }
54
+ }
55
+ /**
56
+ * Assert that resolve depth does not exceed the configured limit.
57
+ *
58
+ * @param depth - Current depth counter.
59
+ * @throws {SecurityException} When depth exceeds the maximum.
60
+ *
61
+ * @example
62
+ * parser.assertMaxResolveDepth(5); // OK
63
+ */
64
+ assertMaxResolveDepth(depth) {
65
+ if (depth > this.maxResolveDepth) {
66
+ throw new SecurityException(`Deep merge exceeded maximum depth of ${this.maxResolveDepth}`);
67
+ }
68
+ }
69
+ /**
70
+ * Assert that total key count does not exceed the limit.
71
+ *
72
+ * @param data - Data to count keys in.
73
+ * @param maxKeys - Override limit, or undefined to use configured default.
74
+ * @param maxCountDepth - Override recursion depth limit, or undefined for default.
75
+ * @throws {SecurityException} When key count exceeds the limit.
76
+ *
77
+ * @example
78
+ * parser.assertMaxKeys({ a: 1, b: 2 }); // OK
79
+ */
80
+ assertMaxKeys(data, maxKeys, maxCountDepth) {
81
+ const limit = maxKeys ?? this.maxKeys;
82
+ const count = this.countKeys(data, 0, maxCountDepth ?? this.maxCountRecursiveDepth);
83
+ if (count > limit) {
84
+ throw new SecurityException(`Data contains ${count} keys, exceeding maximum of ${limit}.`);
85
+ }
86
+ }
87
+ /**
88
+ * Assert that current recursion depth does not exceed the limit.
89
+ *
90
+ * @param currentDepth - Current depth counter.
91
+ * @param maxDepth - Override limit, or undefined to use configured default.
92
+ * @throws {SecurityException} When the depth exceeds the limit.
93
+ *
94
+ * @example
95
+ * parser.assertMaxDepth(3); // OK
96
+ */
97
+ assertMaxDepth(currentDepth, maxDepth) {
98
+ const limit = maxDepth ?? this.maxDepth;
99
+ if (currentDepth > limit) {
100
+ throw new SecurityException(`Recursion depth ${currentDepth} exceeds maximum of ${limit}.`);
101
+ }
102
+ }
103
+ /**
104
+ * Assert that structural nesting depth does not exceed the policy limit.
105
+ *
106
+ * @param data - Data to measure structural depth of.
107
+ * @param maxDepth - Maximum allowed structural depth.
108
+ * @throws {SecurityException} When structural depth exceeds the limit.
109
+ *
110
+ * @example
111
+ * parser.assertMaxStructuralDepth({ a: { b: 1 } }, 10); // OK
112
+ */
113
+ assertMaxStructuralDepth(data, maxDepth) {
114
+ const depth = this.measureDepth(data, 0, maxDepth + 1);
115
+ if (depth > maxDepth) {
116
+ throw new SecurityException(`Data structural depth ${depth} exceeds policy maximum of ${maxDepth}.`);
117
+ }
118
+ }
119
+ /**
120
+ * Return the configured maximum structural nesting depth.
121
+ *
122
+ * @returns Maximum allowed depth.
123
+ */
124
+ getMaxDepth() {
125
+ return this.maxDepth;
126
+ }
127
+ /**
128
+ * Return the configured maximum path-resolve recursion depth.
129
+ *
130
+ * @returns Maximum allowed resolve depth.
131
+ */
132
+ getMaxResolveDepth() {
133
+ return this.maxResolveDepth;
134
+ }
135
+ /**
136
+ * Return the configured maximum total key count.
137
+ *
138
+ * @returns Maximum allowed key count.
139
+ */
140
+ getMaxKeys() {
141
+ return this.maxKeys;
142
+ }
143
+ /**
144
+ * Recursively count keys in a data structure.
145
+ *
146
+ * @param obj - Data to count keys in.
147
+ * @param depth - Current recursion depth.
148
+ * @param maxDepth - Maximum recursion depth for counting.
149
+ * @returns Total number of keys found.
150
+ */
151
+ countKeys(obj, depth, maxDepth) {
152
+ if (depth > maxDepth) {
153
+ return 0;
154
+ }
155
+ if (typeof obj !== 'object' || obj === null) {
156
+ return 0;
157
+ }
158
+ const entries = Object.entries(obj);
159
+ let count = entries.length;
160
+ for (const [, value] of entries) {
161
+ count += this.countKeys(value, depth + 1, maxDepth);
162
+ }
163
+ return count;
164
+ }
165
+ /**
166
+ * Recursively measure the maximum nesting depth of a data structure.
167
+ *
168
+ * @param value - Data to measure.
169
+ * @param current - Current depth counter.
170
+ * @param maxDepth - Ceiling to stop measuring.
171
+ * @returns Maximum depth found.
172
+ */
173
+ measureDepth(value, current, maxDepth) {
174
+ /* Stryker disable next-line ConditionalExpression,EqualityOperator -- equivalent: >= vs > at ceiling; both correctly stop at maxDepth */
175
+ if (current >= maxDepth || typeof value !== 'object' || value === null) {
176
+ return current;
177
+ }
178
+ let max = current;
179
+ for (const child of Object.values(value)) {
180
+ const d = this.measureDepth(child, current + 1, maxDepth);
181
+ /* Stryker disable next-line ConditionalExpression,EqualityOperator -- equivalent: d > max vs d >= max; assigning d when d===max is a no-op */
182
+ if (d > max) {
183
+ max = d;
184
+ }
185
+ }
186
+ return max;
187
+ }
188
+ static clampOption(value, defaultValue) {
189
+ /* Stryker disable next-line ConditionalExpression -- equivalent: Number.isFinite covers undefined (isFinite(undefined)===false); simplest safe form */
190
+ return Number.isFinite(value) ? value : defaultValue;
191
+ }
192
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Enumeration of supported data format types.
3
+ *
4
+ * Used by {@link Inline.from} to select the appropriate accessor.
5
+ *
6
+ * @example
7
+ * const accessor = Inline.from(TypeFormat.Json, '{"key":"value"}');
8
+ */
9
+ export declare enum TypeFormat {
10
+ /** Plain array or object data. */
11
+ Array = "array",
12
+ /** JavaScript object (class instance or plain). */
13
+ Object = "object",
14
+ /** JSON string. */
15
+ Json = "json",
16
+ /** XML string. */
17
+ Xml = "xml",
18
+ /** YAML string. */
19
+ Yaml = "yaml",
20
+ /** INI configuration string. */
21
+ Ini = "ini",
22
+ /** Dotenv-formatted string. */
23
+ Env = "env",
24
+ /** Newline-delimited JSON string. */
25
+ Ndjson = "ndjson",
26
+ /** Auto-detected format via integration. */
27
+ Any = "any"
28
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Enumeration of supported data format types.
3
+ *
4
+ * Used by {@link Inline.from} to select the appropriate accessor.
5
+ *
6
+ * @example
7
+ * const accessor = Inline.from(TypeFormat.Json, '{"key":"value"}');
8
+ */
9
+ export var TypeFormat;
10
+ (function (TypeFormat) {
11
+ /** Plain array or object data. */
12
+ TypeFormat["Array"] = "array";
13
+ /** JavaScript object (class instance or plain). */
14
+ TypeFormat["Object"] = "object";
15
+ /** JSON string. */
16
+ TypeFormat["Json"] = "json";
17
+ /** XML string. */
18
+ TypeFormat["Xml"] = "xml";
19
+ /** YAML string. */
20
+ TypeFormat["Yaml"] = "yaml";
21
+ /** INI configuration string. */
22
+ TypeFormat["Ini"] = "ini";
23
+ /** Dotenv-formatted string. */
24
+ TypeFormat["Env"] = "env";
25
+ /** Newline-delimited JSON string. */
26
+ TypeFormat["Ndjson"] = "ndjson";
27
+ /** Auto-detected format via integration. */
28
+ TypeFormat["Any"] = "any";
29
+ })(TypeFormat || (TypeFormat = {}));
@@ -0,0 +1 @@
1
+ export { default } from '../../eslint.config.js';
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@safeaccess/inline",
3
+ "version": "0.1.1",
4
+ "private": false,
5
+ "description": "Safe nested data access with dot notation — JavaScript/TypeScript",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "main": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "import": "./dist/index.js",
13
+ "types": "./dist/index.d.ts"
14
+ }
15
+ },
16
+ "scripts": {
17
+ "build": "tsc",
18
+ "test": "vitest run",
19
+ "typecheck": "tsc --noEmit",
20
+ "test:typecheck": "tsc --noEmit",
21
+ "test:parity": "vitest run --reporter=verbose",
22
+ "lint": "npx eslint src/ tests/",
23
+ "bench": "npx vitest bench benchmarks/",
24
+ "test:coverage": "npx vitest run --coverage",
25
+ "test:mutation": "npx stryker run"
26
+ },
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "https://github.com/felipesauer/safeaccess-inline",
30
+ "directory": "packages/js"
31
+ },
32
+ "devDependencies": {
33
+ "@stryker-mutator/core": "^9.6.0",
34
+ "@stryker-mutator/vitest-runner": "^9.6.0",
35
+ "@vitest/coverage-v8": "^4.1.2",
36
+ "typescript": "^6.0.2",
37
+ "vitest": "^4.1.2"
38
+ }
39
+ }
@@ -0,0 +1,353 @@
1
+ import type { AccessorsInterface } from '../contracts/accessors-interface.js';
2
+ import { DotNotationParser } from '../core/dot-notation-parser.js';
3
+ import { PathNotFoundException } from '../exceptions/path-not-found-exception.js';
4
+ import { ReadonlyViolationException } from '../exceptions/readonly-violation-exception.js';
5
+
6
+ /**
7
+ * Base accessor providing read, write, and lifecycle operations.
8
+ *
9
+ * Implements all AccessorsInterface methods with immutable copy
10
+ * semantics for writes, optional readonly enforcement, and strict mode
11
+ * for security validation on data ingestion.
12
+ *
13
+ * Subclasses must implement `parse()` to convert raw input into
14
+ * a normalized plain object.
15
+ */
16
+ interface AccessorState {
17
+ data: Record<string, unknown>;
18
+ isReadonly: boolean;
19
+ isStrict: boolean;
20
+ rawInput: unknown;
21
+ }
22
+
23
+ export abstract class AbstractAccessor implements AccessorsInterface {
24
+ /** @internal Mutable state grouped to allow O(1) shallow clone in mutations. */
25
+ private _state: AccessorState = {
26
+ data: {},
27
+ isReadonly: false,
28
+ isStrict: true,
29
+ rawInput: null,
30
+ };
31
+
32
+ /**
33
+ * @param parser - Dot-notation parser for path operations.
34
+ */
35
+ constructor(protected readonly parser: DotNotationParser) {}
36
+
37
+ /**
38
+ * Convert raw input data into a normalized plain object.
39
+ *
40
+ * @param raw - Raw input in the format expected by the accessor.
41
+ * @returns Parsed data structure.
42
+ * @throws {InvalidFormatException} When the input is malformed.
43
+ */
44
+ protected abstract parse(raw: unknown): Record<string, unknown>;
45
+
46
+ /**
47
+ * Ingest raw data, optionally validating via strict mode.
48
+ *
49
+ * When strict mode is enabled (default), validates payload size for string
50
+ * inputs and runs structural/key-safety validation on parsed data.
51
+ *
52
+ * @param raw - Raw input data.
53
+ * @returns Same instance with data populated.
54
+ * @throws {InvalidFormatException} When the raw data cannot be parsed.
55
+ * @throws {SecurityException} When payload exceeds size limit, data contains forbidden keys, or violates limits.
56
+ */
57
+ protected ingest(raw: unknown): this {
58
+ this._state.rawInput = raw;
59
+ if (this._state.isStrict && typeof raw === 'string') {
60
+ this.parser.assertPayload(raw);
61
+ }
62
+ const parsed = this.parse(raw);
63
+ if (this._state.isStrict) {
64
+ this.parser.validate(parsed);
65
+ }
66
+ this._state.data = parsed;
67
+ return this;
68
+ }
69
+
70
+ /**
71
+ * Hydrate the accessor from raw input data.
72
+ *
73
+ * @param data - Raw input in the format expected by the accessor.
74
+ * @returns Populated accessor instance.
75
+ */
76
+ abstract from(data: unknown): this;
77
+
78
+ /**
79
+ * Retrieve the original raw input data before parsing.
80
+ *
81
+ * @returns Original input passed to `from()`.
82
+ */
83
+ getRaw(): unknown {
84
+ return this._state.rawInput;
85
+ }
86
+
87
+ /**
88
+ * Return a new instance with the given readonly state.
89
+ *
90
+ * @param isReadonly - Whether the new instance should block mutations.
91
+ * @returns New accessor instance with the readonly state applied.
92
+ */
93
+ readonly(isReadonly: boolean = true): this {
94
+ const copy = this.cloneInstance();
95
+ copy._state = { ...copy._state, isReadonly };
96
+ return copy;
97
+ }
98
+
99
+ /**
100
+ * Return a new instance with the given strict mode state.
101
+ *
102
+ * @param strict - Whether to enable strict security validation.
103
+ * @returns New accessor instance with the strict mode applied.
104
+ *
105
+ * @security Passing `false` disables all SecurityGuard and SecurityParser
106
+ * validation (key safety, payload size, depth and key-count limits).
107
+ * Only use with fully trusted, application-controlled input.
108
+ *
109
+ * @example
110
+ * // Trust the input — skip all security checks
111
+ * const accessor = new JsonAccessor(parser).strict(false).from(trustedPayload);
112
+ */
113
+ strict(strict: boolean = true): this {
114
+ const copy = this.cloneInstance();
115
+ copy._state = { ...copy._state, isStrict: strict };
116
+ return copy;
117
+ }
118
+
119
+ /**
120
+ * Retrieve a value at a dot-notation path.
121
+ *
122
+ * @param path - Dot-notation path (e.g. "user.name").
123
+ * @param defaultValue - Fallback when the path does not exist.
124
+ * @returns Resolved value or the default.
125
+ *
126
+ * @example
127
+ * accessor.get('user.name', 'unknown');
128
+ */
129
+ get(path: string, defaultValue: unknown = null): unknown {
130
+ return this.parser.get(this._state.data, path, defaultValue);
131
+ }
132
+
133
+ /**
134
+ * Retrieve a value or throw when the path does not exist.
135
+ *
136
+ * @param path - Dot-notation path.
137
+ * @returns Resolved value.
138
+ * @throws {PathNotFoundException} When the path is missing.
139
+ *
140
+ * @example
141
+ * accessor.getOrFail('user.name'); // throws if not found
142
+ */
143
+ getOrFail(path: string): unknown {
144
+ const sentinel = Object.create(null) as Record<string, never>;
145
+ const result = this.parser.get(this._state.data, path, sentinel);
146
+
147
+ if (result === sentinel) {
148
+ throw new PathNotFoundException(`Path '${path}' not found.`);
149
+ }
150
+
151
+ return result;
152
+ }
153
+
154
+ /**
155
+ * Retrieve a value using pre-parsed key segments.
156
+ *
157
+ * @param segments - Ordered list of keys.
158
+ * @param defaultValue - Fallback when the path does not exist.
159
+ * @returns Resolved value or the default.
160
+ */
161
+ getAt(segments: Array<string | number>, defaultValue: unknown = null): unknown {
162
+ return this.parser.getAt(this._state.data, segments, defaultValue);
163
+ }
164
+
165
+ /**
166
+ * Check whether a dot-notation path exists.
167
+ *
168
+ * @param path - Dot-notation path.
169
+ * @returns True if the path resolves to a value.
170
+ */
171
+ has(path: string): boolean {
172
+ return this.parser.has(this._state.data, path);
173
+ }
174
+
175
+ /**
176
+ * Check whether a path exists using pre-parsed key segments.
177
+ *
178
+ * @param segments - Ordered list of keys.
179
+ * @returns True if the path resolves to a value.
180
+ */
181
+ hasAt(segments: Array<string | number>): boolean {
182
+ return this.parser.hasAt(this._state.data, segments);
183
+ }
184
+
185
+ /**
186
+ * Set a value at a dot-notation path.
187
+ *
188
+ * @param path - Dot-notation path.
189
+ * @param value - Value to assign.
190
+ * @returns New accessor instance with the value set.
191
+ * @throws {ReadonlyViolationException} When the accessor is readonly.
192
+ * @throws {SecurityException} When the path contains forbidden keys.
193
+ */
194
+ set(path: string, value: unknown): this {
195
+ this.assertNotReadOnly();
196
+ return this.mutateTo(this.parser.set(this._state.data, path, value));
197
+ }
198
+
199
+ /**
200
+ * Set a value using pre-parsed key segments.
201
+ *
202
+ * @param segments - Ordered list of keys.
203
+ * @param value - Value to assign.
204
+ * @returns New accessor instance with the value set.
205
+ * @throws {ReadonlyViolationException} When the accessor is readonly.
206
+ * @throws {SecurityException} When segments contain forbidden keys.
207
+ */
208
+ setAt(segments: Array<string | number>, value: unknown): this {
209
+ this.assertNotReadOnly();
210
+ return this.mutateTo(this.parser.setAt(this._state.data, segments, value));
211
+ }
212
+
213
+ /**
214
+ * Remove a value at a dot-notation path.
215
+ *
216
+ * @param path - Dot-notation path to remove.
217
+ * @returns New accessor instance without the specified path.
218
+ * @throws {ReadonlyViolationException} When the accessor is readonly.
219
+ * @throws {SecurityException} When the path contains forbidden keys.
220
+ */
221
+ remove(path: string): this {
222
+ this.assertNotReadOnly();
223
+ return this.mutateTo(this.parser.remove(this._state.data, path));
224
+ }
225
+
226
+ /**
227
+ * Remove a value using pre-parsed key segments.
228
+ *
229
+ * @param segments - Ordered list of keys.
230
+ * @returns New accessor instance without the specified path.
231
+ * @throws {ReadonlyViolationException} When the accessor is readonly.
232
+ * @throws {SecurityException} When segments contain forbidden keys.
233
+ */
234
+ removeAt(segments: Array<string | number>): this {
235
+ this.assertNotReadOnly();
236
+ return this.mutateTo(this.parser.removeAt(this._state.data, segments));
237
+ }
238
+
239
+ /**
240
+ * Retrieve multiple values by their paths with individual defaults.
241
+ *
242
+ * @param paths - Map of path to default value.
243
+ * @returns Map of path to resolved value.
244
+ */
245
+ getMany(paths: Record<string, unknown>): Record<string, unknown> {
246
+ const results: Record<string, unknown> = {};
247
+ for (const [path, defaultValue] of Object.entries(paths)) {
248
+ results[path] = this.get(path, defaultValue);
249
+ }
250
+ return results;
251
+ }
252
+
253
+ /**
254
+ * Return all parsed data as a plain object.
255
+ *
256
+ * @returns Complete internal data.
257
+ */
258
+ all(): Record<string, unknown> {
259
+ return this._state.data;
260
+ }
261
+
262
+ /**
263
+ * Count elements at a path, or the root if undefined.
264
+ *
265
+ * @param path - Dot-notation path, or undefined for root.
266
+ * @returns Number of elements.
267
+ */
268
+ count(path?: string): number {
269
+ const target = path !== undefined ? this.get(path, {}) : this._state.data;
270
+ if (typeof target === 'object' && target !== null) {
271
+ return Object.keys(target).length;
272
+ }
273
+ return 0;
274
+ }
275
+
276
+ /**
277
+ * Retrieve array keys at a path, or root keys if undefined.
278
+ *
279
+ * @param path - Dot-notation path, or undefined for root.
280
+ * @returns List of keys.
281
+ */
282
+ keys(path?: string): string[] {
283
+ const target = path !== undefined ? this.get(path, {}) : this._state.data;
284
+ /* Stryker disable next-line ConditionalExpression -- equivalent: get() always returns an object-type value here; typeof check is a type guard only */
285
+ if (typeof target === 'object' && target !== null) {
286
+ return Object.keys(target);
287
+ }
288
+ return [];
289
+ }
290
+
291
+ /**
292
+ * Deep-merge an object into the value at a dot-notation path.
293
+ *
294
+ * @param path - Dot-notation path to the merge target.
295
+ * @param value - Object to merge into the existing value.
296
+ * @returns New accessor instance with merged data.
297
+ * @throws {ReadonlyViolationException} When the accessor is readonly.
298
+ * @throws {SecurityException} When the path or values contain forbidden keys.
299
+ */
300
+ merge(path: string, value: Record<string, unknown>): this {
301
+ this.assertNotReadOnly();
302
+ return this.mutateTo(this.parser.merge(this._state.data, path, value));
303
+ }
304
+
305
+ /**
306
+ * Deep-merge an object into the root data.
307
+ *
308
+ * @param value - Object to merge into the root.
309
+ * @returns New accessor instance with merged data.
310
+ * @throws {ReadonlyViolationException} When the accessor is readonly.
311
+ * @throws {SecurityException} When values contain forbidden keys.
312
+ */
313
+ mergeAll(value: Record<string, unknown>): this {
314
+ this.assertNotReadOnly();
315
+ return this.mutateTo(this.parser.merge(this._state.data, '', value));
316
+ }
317
+
318
+ /**
319
+ * Assert that the accessor is not in readonly mode.
320
+ *
321
+ * @throws {ReadonlyViolationException} When the accessor is readonly.
322
+ */
323
+ private assertNotReadOnly(): void {
324
+ if (this._state.isReadonly) {
325
+ throw new ReadonlyViolationException();
326
+ }
327
+ }
328
+
329
+ /**
330
+ * Create a shallow clone of this accessor.
331
+ *
332
+ * @returns Cloned accessor instance.
333
+ */
334
+ private cloneInstance(): this {
335
+ const copy = Object.create(Object.getPrototypeOf(this) as object) as this;
336
+ Object.assign(copy, this);
337
+ // Shallow-clone state so mutations on copy don't affect original
338
+ copy._state = { ...this._state };
339
+ return copy;
340
+ }
341
+
342
+ /**
343
+ * Create a clone with new internal data.
344
+ *
345
+ * @param newData - New data for the clone.
346
+ * @returns Cloned accessor with updated data.
347
+ */
348
+ private mutateTo(newData: Record<string, unknown>): this {
349
+ const copy = this.cloneInstance();
350
+ copy._state = { ...copy._state, data: newData };
351
+ return copy;
352
+ }
353
+ }
@@ -0,0 +1,51 @@
1
+ import { AbstractAccessor } from '../abstract-accessor.js';
2
+ import type { ParseIntegrationInterface } from '../../contracts/parse-integration-interface.js';
3
+ import type { DotNotationParser } from '../../core/dot-notation-parser.js';
4
+ import { InvalidFormatException } from '../../exceptions/invalid-format-exception.js';
5
+
6
+ /**
7
+ * Accessor for arbitrary formats via a custom {@link ParseIntegrationInterface}.
8
+ *
9
+ * Delegates format detection and parsing to a user-provided integration.
10
+ * Validates string payloads against security constraints before parsing.
11
+ *
12
+ * @example
13
+ * const integration = new MyCsvIntegration();
14
+ * const accessor = Inline.withParserIntegration(integration).fromAny(csvString);
15
+ * accessor.get('0.name'); // first row, name column
16
+ */
17
+ export class AnyAccessor extends AbstractAccessor {
18
+ private readonly integration: ParseIntegrationInterface;
19
+
20
+ /**
21
+ * @param parser - Dot-notation parser with security configuration.
22
+ * @param integration - Custom format parser for detecting and parsing input.
23
+ */
24
+ constructor(parser: DotNotationParser, integration: ParseIntegrationInterface) {
25
+ super(parser);
26
+ this.integration = integration;
27
+ }
28
+
29
+ /**
30
+ * Hydrate from raw data via the custom integration.
31
+ *
32
+ * @param data - Raw input data in any format supported by the integration.
33
+ * @returns Populated accessor instance.
34
+ * @throws {InvalidFormatException} When the integration rejects the format.
35
+ * @throws {SecurityException} When string input violates payload-size limits.
36
+ */
37
+ from(data: unknown): this {
38
+ if (!this.integration.assertFormat(data)) {
39
+ throw new InvalidFormatException(`AnyAccessor failed, got ${typeof data}`);
40
+ }
41
+
42
+ return this.ingest(data);
43
+ }
44
+
45
+ /**
46
+ * @internal
47
+ */
48
+ protected parse(raw: unknown): Record<string, unknown> {
49
+ return this.integration.parse(raw);
50
+ }
51
+ }