@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
package/.gitattributes
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Distribution archive ignore — files excluded from npm/archive downloads
|
|
2
|
+
# Note: npm uses the `files` field in package.json for publish filtering.
|
|
3
|
+
# These export-ignore rules apply to git archive and the split repo.
|
|
4
|
+
|
|
5
|
+
/tests/ export-ignore
|
|
6
|
+
/benchmarks/ export-ignore
|
|
7
|
+
/vitest.config.ts export-ignore
|
|
8
|
+
/eslint.config.js export-ignore
|
|
9
|
+
/tsconfig.json export-ignore
|
|
10
|
+
/tsup.config.ts export-ignore
|
|
11
|
+
/stryker.config.json export-ignore
|
|
12
|
+
/.prettierrc export-ignore
|
|
13
|
+
/.gitattributes export-ignore
|
|
14
|
+
/.gitignore export-ignore
|
|
15
|
+
/coverage/ export-ignore
|
|
16
|
+
/reports/ export-ignore
|
package/.gitkeep
ADDED
|
File without changes
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to the `@safeaccess/inline` JavaScript/TypeScript package are documented in this file.
|
|
4
|
+
|
|
5
|
+
## [0.1.1](https://github.com/felipesauer/safeaccess-inline/compare/js-v0.1.0...js-v0.1.1) (2026-04-07)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **js:** bootstrap release tracking for rebranded package ([5fc07d7](https://github.com/felipesauer/safeaccess-inline/commit/5fc07d7126870d72145bbfc80609370c9d1509c7))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* **js:** add repository field for npm provenance validation ([b34cdef](https://github.com/felipesauer/safeaccess-inline/commit/b34cdeff01e7e7566921f04b11f33fbd391aa8d2))
|
|
16
|
+
|
|
17
|
+
## 0.1.0 (2026-04-07)
|
|
18
|
+
|
|
19
|
+
### Bug Fixes
|
|
20
|
+
|
|
21
|
+
- **ci:** achieve 100% branch coverage on Vitest 4.x and fix docs-ci workflow ([#14](https://github.com/felipesauer/safeaccess-inline/issues/14)) ([11daf5a](https://github.com/felipesauer/safeaccess-inline/commit/11daf5aaa1ff1b901c8297921533485f1584a330))
|
|
22
|
+
|
|
23
|
+
## [0.1.0] — 2026-04-06
|
|
24
|
+
|
|
25
|
+
### Features
|
|
26
|
+
|
|
27
|
+
- Initial release.
|
|
28
|
+
- `Inline` class: static and instance factory methods `fromArray`, `fromObject`, `fromJson`, `fromXml`, `fromYaml`, `fromIni`, `fromEnv`, `fromNdjson`, `fromAny`, `from`, `make`.
|
|
29
|
+
- Builder pattern: `withSecurityGuard`, `withSecurityParser`, `withPathCache`, `withParserIntegration`, `withStrictMode`.
|
|
30
|
+
- Dot-notation read API: `get`, `getOrFail`, `getAt`, `has`, `hasAt`, `getMany`, `all`, `count`, `keys`, `getRaw`.
|
|
31
|
+
- Dot-notation write API: `set`, `setAt`, `remove`, `removeAt`, `merge`, `mergeAll`; honours `readonly()` mode.
|
|
32
|
+
- `TypeFormat` enum with 9 cases: `Array`, `Object`, `Json`, `Xml`, `Yaml`, `Ini`, `Env`, `Ndjson`, `Any`.
|
|
33
|
+
- `SecurityGuard` with configurable depth limit, forbidden-key list (magic methods, prototype-pollution, Node.js-specific vectors), and `sanitize()` helper. All limits are `readonly`.
|
|
34
|
+
- `SecurityParser` with configurable payload-size, key-count, structural-depth, and resolve-depth limits. All limits are `readonly`.
|
|
35
|
+
- Custom-parser extension point via `ParseIntegrationInterface`.
|
|
36
|
+
- Path-result caching via `PathCacheInterface`.
|
|
37
|
+
- 8 typed exception classes extending `AccessorException`: `InvalidFormatException`, `ParserException`, `PathNotFoundException`, `ReadonlyViolationException`, `SecurityException`, `UnsupportedTypeException`, `YamlParseException`.
|
|
38
|
+
- Strict TypeScript types throughout; no `any`; full ESM output.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Felipe Sauer
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="../../public/logo.svg" width="80" alt="safeaccess-inline logo">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h1 align="center">Safe Access Inline — TypeScript</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<a href="https://www.npmjs.com/package/@safeaccess/inline"><img src="https://img.shields.io/npm/v/@safeaccess/inline?label=npm" alt="npm"></a>
|
|
9
|
+
<a href="../../LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License: MIT"></a>
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
Safe nested data access with dot notation for JavaScript and TypeScript. Navigate deeply nested objects, JSON, YAML, XML, INI, ENV, and NDJSON structures — with built-in security validation, immutable writes, and a fluent builder API.
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install @safeaccess/inline
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
**Requirements:** Node.js 22+
|
|
23
|
+
|
|
24
|
+
## Quick Start
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
import { Inline } from '@safeaccess/inline';
|
|
28
|
+
|
|
29
|
+
const accessor = Inline.fromJson('{"user": {"name": "Alice", "age": 30}}');
|
|
30
|
+
|
|
31
|
+
accessor.get('user.name'); // 'Alice'
|
|
32
|
+
accessor.get('user.email', 'N/A'); // 'N/A' (default when missing)
|
|
33
|
+
accessor.has('user.age'); // true
|
|
34
|
+
accessor.getOrFail('user.name'); // 'Alice' (throws if missing)
|
|
35
|
+
|
|
36
|
+
// Immutable writes — original is never modified
|
|
37
|
+
const updated = accessor.set('user.email', 'alice@example.com');
|
|
38
|
+
updated.get('user.email'); // 'alice@example.com'
|
|
39
|
+
accessor.has('user.email'); // false (original unchanged)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Dot Notation Syntax
|
|
43
|
+
|
|
44
|
+
The TypeScript package supports dot-separated key access:
|
|
45
|
+
|
|
46
|
+
| Syntax | Example | Description |
|
|
47
|
+
| ----------- | -------------- | ------------------------- |
|
|
48
|
+
| `key.key` | `user.name` | Nested key access |
|
|
49
|
+
| `key.0.key` | `users.0.name` | Numeric key (array index) |
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
const data = Inline.fromJson('{"users": [{"name": "Alice"}, {"name": "Bob"}]}');
|
|
53
|
+
data.get('users.0.name'); // 'Alice'
|
|
54
|
+
data.get('users.1.name'); // 'Bob'
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
> **Note:** Advanced PathQuery features (wildcards, filters, slices, recursive descent, projections) are available in the PHP package only. See the [PHP README](../php/README.md#advanced-pathquery) for details.
|
|
58
|
+
|
|
59
|
+
## Supported Formats
|
|
60
|
+
|
|
61
|
+
Each format has a dedicated accessor with automatic parsing and security validation.
|
|
62
|
+
|
|
63
|
+
<details>
|
|
64
|
+
<summary><strong>JSON</strong></summary>
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
const accessor = Inline.fromJson('{"users": [{"name": "Alice"}, {"name": "Bob"}]}');
|
|
68
|
+
accessor.get('users.0.name'); // 'Alice'
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
</details>
|
|
72
|
+
|
|
73
|
+
<details>
|
|
74
|
+
<summary><strong>YAML</strong></summary>
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
const yaml = `database:
|
|
78
|
+
host: localhost
|
|
79
|
+
port: 5432
|
|
80
|
+
credentials:
|
|
81
|
+
user: admin`;
|
|
82
|
+
|
|
83
|
+
const accessor = Inline.fromYaml(yaml);
|
|
84
|
+
accessor.get('database.credentials.user'); // 'admin'
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
</details>
|
|
88
|
+
|
|
89
|
+
<details>
|
|
90
|
+
<summary><strong>XML</strong></summary>
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
const accessor = Inline.fromXml('<config><database><host>localhost</host></database></config>');
|
|
94
|
+
accessor.get('database.host'); // 'localhost'
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
</details>
|
|
98
|
+
|
|
99
|
+
<details>
|
|
100
|
+
<summary><strong>INI</strong></summary>
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
const accessor = Inline.fromIni('[database]\nhost=localhost\nport=5432');
|
|
104
|
+
accessor.get('database.host'); // 'localhost'
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
</details>
|
|
108
|
+
|
|
109
|
+
<details>
|
|
110
|
+
<summary><strong>ENV (dotenv)</strong></summary>
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
const accessor = Inline.fromEnv('APP_NAME=MyApp\nDB_HOST=localhost');
|
|
114
|
+
accessor.get('DB_HOST'); // 'localhost'
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
</details>
|
|
118
|
+
|
|
119
|
+
<details>
|
|
120
|
+
<summary><strong>NDJSON</strong></summary>
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
const accessor = Inline.fromNdjson('{"id":1,"name":"Alice"}\n{"id":2,"name":"Bob"}');
|
|
124
|
+
accessor.get('0.name'); // 'Alice'
|
|
125
|
+
accessor.get('1.name'); // 'Bob'
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
</details>
|
|
129
|
+
|
|
130
|
+
<details>
|
|
131
|
+
<summary><strong>Array / Object</strong></summary>
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
const accessor = Inline.fromArray({ users: [{ name: 'Alice' }, { name: 'Bob' }] });
|
|
135
|
+
accessor.get('users.0.name'); // 'Alice'
|
|
136
|
+
|
|
137
|
+
const objAccessor = Inline.fromObject({ name: 'Alice', age: 30 });
|
|
138
|
+
objAccessor.get('name'); // 'Alice'
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
</details>
|
|
142
|
+
|
|
143
|
+
<details>
|
|
144
|
+
<summary><strong>Dynamic (by TypeFormat enum)</strong></summary>
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
import { Inline, TypeFormat } from '@safeaccess/inline';
|
|
148
|
+
|
|
149
|
+
const accessor = Inline.from(TypeFormat.Json, '{"key": "value"}');
|
|
150
|
+
accessor.get('key'); // 'value'
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
</details>
|
|
154
|
+
|
|
155
|
+
## Reading & Writing
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
const accessor = Inline.fromJson('{"a": {"b": 1, "c": 2}}');
|
|
159
|
+
|
|
160
|
+
// Read
|
|
161
|
+
accessor.get('a.b'); // 1
|
|
162
|
+
accessor.get('a.missing', 'default'); // 'default'
|
|
163
|
+
accessor.getOrFail('a.b'); // 1 (throws PathNotFoundException if missing)
|
|
164
|
+
accessor.has('a.b'); // true
|
|
165
|
+
accessor.all(); // { a: { b: 1, c: 2 } }
|
|
166
|
+
accessor.count(); // 1 (root keys)
|
|
167
|
+
accessor.count('a'); // 2 (keys under 'a')
|
|
168
|
+
accessor.keys(); // ['a']
|
|
169
|
+
accessor.keys('a'); // ['b', 'c']
|
|
170
|
+
accessor.getMany({
|
|
171
|
+
'a.b': null,
|
|
172
|
+
'a.x': 'fallback',
|
|
173
|
+
}); // { 'a.b': 1, 'a.x': 'fallback' }
|
|
174
|
+
accessor.getRaw(); // original JSON string
|
|
175
|
+
|
|
176
|
+
// Write (immutable — every write returns a new instance)
|
|
177
|
+
const updated = accessor.set('a.d', 3);
|
|
178
|
+
const cleaned = updated.remove('a.c');
|
|
179
|
+
const merged = cleaned.merge('a', { e: 4 });
|
|
180
|
+
const full = merged.mergeAll({ f: 5 });
|
|
181
|
+
|
|
182
|
+
// Readonly mode — block all writes
|
|
183
|
+
const readonly = accessor.readonly();
|
|
184
|
+
readonly.get('a.b'); // 1 (reads work)
|
|
185
|
+
readonly.set('a.b', 99); // throws ReadonlyViolationException
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Configure
|
|
189
|
+
|
|
190
|
+
### Builder Pattern
|
|
191
|
+
|
|
192
|
+
```typescript
|
|
193
|
+
import { Inline, SecurityGuard, SecurityParser } from '@safeaccess/inline';
|
|
194
|
+
|
|
195
|
+
const accessor = Inline.withSecurityGuard(new SecurityGuard(512, ['secret']))
|
|
196
|
+
.withSecurityParser(new SecurityParser({ maxDepth: 5 }))
|
|
197
|
+
.withStrictMode(true)
|
|
198
|
+
.fromJson(untrustedInput);
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Builder Methods
|
|
202
|
+
|
|
203
|
+
| Method | Description |
|
|
204
|
+
| ------------------------------------ | ------------------------------------------------ |
|
|
205
|
+
| `withSecurityGuard(guard)` | Custom forbidden-key rules and depth limits |
|
|
206
|
+
| `withSecurityParser(parser)` | Custom payload size and structural limits |
|
|
207
|
+
| `withPathCache(cache)` | Custom path segment cache for repeated lookups |
|
|
208
|
+
| `withParserIntegration(integration)` | Custom format parser for `fromAny()` |
|
|
209
|
+
| `withStrictMode(false)` | Disable security validation (trusted input only) |
|
|
210
|
+
|
|
211
|
+
## Security
|
|
212
|
+
|
|
213
|
+
All public entry points validate input **by default**. Every key passes through `SecurityGuard` and `SecurityParser`.
|
|
214
|
+
|
|
215
|
+
### Forbidden Keys
|
|
216
|
+
|
|
217
|
+
| Category | Examples | Reason |
|
|
218
|
+
| ----------------------------- | ------------------------------------------------------------------------------------------------------------ | --------------------------------------------------- |
|
|
219
|
+
| Prototype pollution | `__proto__`, `constructor`, `prototype` | Prevents prototype pollution attacks |
|
|
220
|
+
| Legacy prototype manipulation | `__defineGetter__`, `__defineSetter__`, `__lookupGetter__`, `__lookupSetter__` | Prevents legacy prototype tampering |
|
|
221
|
+
| Property shadow | `hasOwnProperty` | Overriding it can bypass guard checks |
|
|
222
|
+
| Node.js globals | `__dirname`, `__filename` | Prevents path-injection via dynamic property access |
|
|
223
|
+
| Protocol / stream URIs | `javascript:`, `blob:`, `ws://`, `wss://`, `node:`, `file://`, `http://`, `https://`, `ftp://`, `data:`, ... | Prevents URI injection and XSS vectors |
|
|
224
|
+
|
|
225
|
+
Add custom forbidden keys:
|
|
226
|
+
|
|
227
|
+
```typescript
|
|
228
|
+
const guard = new SecurityGuard(512, ['secret', 'internal_token']);
|
|
229
|
+
const accessor = Inline.withSecurityGuard(guard).fromJson(data);
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### Structural Limits
|
|
233
|
+
|
|
234
|
+
| Limit | Default | Description |
|
|
235
|
+
| ------------------------ | ------- | ------------------------------------- |
|
|
236
|
+
| `maxPayloadBytes` | 10 MB | Maximum raw string input size |
|
|
237
|
+
| `maxKeys` | 10,000 | Maximum total key count |
|
|
238
|
+
| `maxDepth` | 512 | Maximum structural nesting depth |
|
|
239
|
+
| `maxResolveDepth` | 100 | Maximum recursion for path resolution |
|
|
240
|
+
| `maxCountRecursiveDepth` | 100 | Maximum recursion when counting keys |
|
|
241
|
+
|
|
242
|
+
### Format-Specific Protections
|
|
243
|
+
|
|
244
|
+
| Format | Protection |
|
|
245
|
+
| ------ | ------------------------------------------------ |
|
|
246
|
+
| XML | Rejects `<!DOCTYPE` — prevents XXE attacks |
|
|
247
|
+
| YAML | Blocks unsafe tags, anchors, aliases, merge keys |
|
|
248
|
+
| All | Forbidden key validation on every parsed key |
|
|
249
|
+
|
|
250
|
+
> Disable for trusted input: `Inline.withStrictMode(false).fromJson(trustedInput)`
|
|
251
|
+
|
|
252
|
+
For vulnerability reports, see [SECURITY.md](../../SECURITY.md).
|
|
253
|
+
|
|
254
|
+
## Error Handling
|
|
255
|
+
|
|
256
|
+
All exceptions extend `AccessorException`:
|
|
257
|
+
|
|
258
|
+
```typescript
|
|
259
|
+
import {
|
|
260
|
+
Inline,
|
|
261
|
+
AccessorException,
|
|
262
|
+
InvalidFormatException,
|
|
263
|
+
SecurityException,
|
|
264
|
+
PathNotFoundException,
|
|
265
|
+
ReadonlyViolationException,
|
|
266
|
+
} from '@safeaccess/inline';
|
|
267
|
+
|
|
268
|
+
try {
|
|
269
|
+
const accessor = Inline.fromJson(untrustedInput);
|
|
270
|
+
const value = accessor.getOrFail('config.key');
|
|
271
|
+
} catch (e) {
|
|
272
|
+
if (e instanceof InvalidFormatException) {
|
|
273
|
+
// Malformed JSON, XML, INI, or NDJSON
|
|
274
|
+
}
|
|
275
|
+
if (e instanceof SecurityException) {
|
|
276
|
+
// Forbidden key, payload too large, depth/key-count exceeded
|
|
277
|
+
}
|
|
278
|
+
if (e instanceof PathNotFoundException) {
|
|
279
|
+
// Path does not exist
|
|
280
|
+
}
|
|
281
|
+
if (e instanceof ReadonlyViolationException) {
|
|
282
|
+
// Write on readonly accessor
|
|
283
|
+
}
|
|
284
|
+
if (e instanceof AccessorException) {
|
|
285
|
+
// Catch-all for any library error
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### Exception Hierarchy
|
|
291
|
+
|
|
292
|
+
| Exception | Extends | When |
|
|
293
|
+
| ---------------------------- | ------------------------ | ----------------------------------------- |
|
|
294
|
+
| `AccessorException` | `Error` | Root — catch-all |
|
|
295
|
+
| `SecurityException` | `AccessorException` | Forbidden key, payload, structural limits |
|
|
296
|
+
| `InvalidFormatException` | `AccessorException` | Malformed JSON, XML, INI, NDJSON |
|
|
297
|
+
| `YamlParseException` | `InvalidFormatException` | Unsafe or malformed YAML |
|
|
298
|
+
| `PathNotFoundException` | `AccessorException` | `getOrFail()` on missing path |
|
|
299
|
+
| `ReadonlyViolationException` | `AccessorException` | Write on readonly accessor |
|
|
300
|
+
| `UnsupportedTypeException` | `AccessorException` | Unknown accessor class in `make()` |
|
|
301
|
+
| `ParserException` | `AccessorException` | Internal parser errors |
|
|
302
|
+
|
|
303
|
+
## Advanced Usage
|
|
304
|
+
|
|
305
|
+
### Strict Mode
|
|
306
|
+
|
|
307
|
+
```typescript
|
|
308
|
+
// Disable all security validation for trusted input
|
|
309
|
+
const accessor = Inline.withStrictMode(false).fromJson(trustedPayload);
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
> **Warning:** Disabling strict mode skips **all** validation. Only use with application-controlled input.
|
|
313
|
+
|
|
314
|
+
### Path Cache
|
|
315
|
+
|
|
316
|
+
```typescript
|
|
317
|
+
// Implement PathCacheInterface for repeated lookups
|
|
318
|
+
const cache: PathCacheInterface = {
|
|
319
|
+
get: (path) => cacheMap.get(path) ?? null,
|
|
320
|
+
set: (path, segments) => {
|
|
321
|
+
cacheMap.set(path, segments);
|
|
322
|
+
},
|
|
323
|
+
has: (path) => cacheMap.has(path),
|
|
324
|
+
clear: () => {
|
|
325
|
+
cacheMap.clear();
|
|
326
|
+
return cache;
|
|
327
|
+
},
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
const accessor = Inline.withPathCache(cache).fromJson(data);
|
|
331
|
+
accessor.get('deeply.nested.path'); // parses path
|
|
332
|
+
accessor.get('deeply.nested.path'); // cache hit
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
### Custom Format Integration
|
|
336
|
+
|
|
337
|
+
```typescript
|
|
338
|
+
// Implement ParseIntegrationInterface for custom formats
|
|
339
|
+
const csvIntegration: ParseIntegrationInterface = {
|
|
340
|
+
assertFormat: (raw: unknown) => typeof raw === 'string' && raw.includes(','),
|
|
341
|
+
parse: (raw: unknown) => {
|
|
342
|
+
// Parse CSV to object
|
|
343
|
+
return parsed;
|
|
344
|
+
},
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
const accessor = Inline.withParserIntegration(csvIntegration).fromAny(csvString);
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
## API Reference
|
|
351
|
+
|
|
352
|
+
### Facade: `Inline`
|
|
353
|
+
|
|
354
|
+
#### Static Factory Methods
|
|
355
|
+
|
|
356
|
+
| Method | Input | Returns |
|
|
357
|
+
| ----------------------------- | ---------------------------------------- | -------------------- |
|
|
358
|
+
| `fromArray(data)` | `Record<string, unknown>` or `unknown[]` | `ArrayAccessor` |
|
|
359
|
+
| `fromObject(data)` | `object` | `ObjectAccessor` |
|
|
360
|
+
| `fromJson(data)` | JSON `string` | `JsonAccessor` |
|
|
361
|
+
| `fromXml(data)` | XML `string` | `XmlAccessor` |
|
|
362
|
+
| `fromYaml(data)` | YAML `string` | `YamlAccessor` |
|
|
363
|
+
| `fromIni(data)` | INI `string` | `IniAccessor` |
|
|
364
|
+
| `fromEnv(data)` | dotenv `string` | `EnvAccessor` |
|
|
365
|
+
| `fromNdjson(data)` | NDJSON `string` | `NdjsonAccessor` |
|
|
366
|
+
| `fromAny(data, integration?)` | `unknown` | `AnyAccessor` |
|
|
367
|
+
| `from(typeFormat, data)` | `TypeFormat` enum | `AccessorsInterface` |
|
|
368
|
+
| `make(AccessorClass, data)` | Accessor constructor | `AbstractAccessor` |
|
|
369
|
+
|
|
370
|
+
#### Accessor Read Methods
|
|
371
|
+
|
|
372
|
+
| Method | Returns |
|
|
373
|
+
| --------------------------- | --------------------------------------- |
|
|
374
|
+
| `get(path, default?)` | Value at path, or default |
|
|
375
|
+
| `getOrFail(path)` | Value or throws `PathNotFoundException` |
|
|
376
|
+
| `getAt(segments, default?)` | Value at key segments |
|
|
377
|
+
| `has(path)` | `boolean` |
|
|
378
|
+
| `hasAt(segments)` | `boolean` |
|
|
379
|
+
| `getMany(paths)` | `Record<string, unknown>` |
|
|
380
|
+
| `all()` | `Record<string, unknown>` |
|
|
381
|
+
| `count(path?)` | `number` |
|
|
382
|
+
| `keys(path?)` | `string[]` |
|
|
383
|
+
| `getRaw()` | `unknown` |
|
|
384
|
+
|
|
385
|
+
#### Accessor Write Methods (immutable)
|
|
386
|
+
|
|
387
|
+
| Method | Description |
|
|
388
|
+
| ------------------------ | ---------------------- |
|
|
389
|
+
| `set(path, value)` | Set at path |
|
|
390
|
+
| `setAt(segments, value)` | Set at key segments |
|
|
391
|
+
| `remove(path)` | Remove at path |
|
|
392
|
+
| `removeAt(segments)` | Remove at key segments |
|
|
393
|
+
| `merge(path, value)` | Deep-merge at path |
|
|
394
|
+
| `mergeAll(value)` | Deep-merge at root |
|
|
395
|
+
|
|
396
|
+
#### Modifier Methods
|
|
397
|
+
|
|
398
|
+
| Method | Description |
|
|
399
|
+
| ----------------- | -------------------------- |
|
|
400
|
+
| `readonly(flag?)` | Block all writes |
|
|
401
|
+
| `strict(flag?)` | Toggle security validation |
|
|
402
|
+
|
|
403
|
+
## Exports
|
|
404
|
+
|
|
405
|
+
The package uses **named exports only** (no default exports). All public types are available from the main entry point:
|
|
406
|
+
|
|
407
|
+
```typescript
|
|
408
|
+
import {
|
|
409
|
+
Inline,
|
|
410
|
+
TypeFormat,
|
|
411
|
+
SecurityGuard,
|
|
412
|
+
SecurityParser,
|
|
413
|
+
// Accessors
|
|
414
|
+
AbstractAccessor,
|
|
415
|
+
ArrayAccessor,
|
|
416
|
+
ObjectAccessor,
|
|
417
|
+
JsonAccessor,
|
|
418
|
+
XmlAccessor,
|
|
419
|
+
YamlAccessor,
|
|
420
|
+
IniAccessor,
|
|
421
|
+
EnvAccessor,
|
|
422
|
+
NdjsonAccessor,
|
|
423
|
+
AnyAccessor,
|
|
424
|
+
// Exceptions
|
|
425
|
+
AccessorException,
|
|
426
|
+
SecurityException,
|
|
427
|
+
InvalidFormatException,
|
|
428
|
+
YamlParseException,
|
|
429
|
+
ParserException,
|
|
430
|
+
PathNotFoundException,
|
|
431
|
+
ReadonlyViolationException,
|
|
432
|
+
UnsupportedTypeException,
|
|
433
|
+
} from '@safeaccess/inline';
|
|
434
|
+
|
|
435
|
+
// Contracts (type-only imports)
|
|
436
|
+
import type {
|
|
437
|
+
AccessorsInterface,
|
|
438
|
+
ReadableAccessorsInterface,
|
|
439
|
+
WritableAccessorsInterface,
|
|
440
|
+
FactoryAccessorsInterface,
|
|
441
|
+
SecurityGuardInterface,
|
|
442
|
+
SecurityParserInterface,
|
|
443
|
+
PathCacheInterface,
|
|
444
|
+
ParseIntegrationInterface,
|
|
445
|
+
} from '@safeaccess/inline';
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
## Contributing
|
|
449
|
+
|
|
450
|
+
See [CONTRIBUTING.md](../../CONTRIBUTING.md) for development setup, commit conventions, and pull request guidelines.
|
|
451
|
+
|
|
452
|
+
## License
|
|
453
|
+
|
|
454
|
+
[MIT](../../LICENSE) © Felipe Sauer
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { bench, describe } from 'vitest';
|
|
2
|
+
import { Inline } from '../src/inline.js';
|
|
3
|
+
|
|
4
|
+
const accessor = Inline.fromArray({
|
|
5
|
+
user: { profile: { name: 'Alice', age: 30 } },
|
|
6
|
+
config: { debug: false, version: '1.0.0' },
|
|
7
|
+
items: [1, 2, 3, 4, 5],
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
describe('ArrayAccessor.get', () => {
|
|
11
|
+
bench('shallow key', () => {
|
|
12
|
+
accessor.get('config.debug');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
bench('deep key (3 levels)', () => {
|
|
16
|
+
accessor.get('user.profile.name');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
bench('missing key with default', () => {
|
|
20
|
+
accessor.get('user.profile.missing', null);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
bench('repeated path (cache)', () => {
|
|
24
|
+
accessor.get('user.profile.name');
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { bench, describe } from 'vitest';
|
|
2
|
+
import { Inline } from '../src/inline.js';
|
|
3
|
+
|
|
4
|
+
const arrayPayload = {
|
|
5
|
+
user: { profile: { name: 'Alice', age: 30 } },
|
|
6
|
+
config: { debug: false, version: '1.0.0' },
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const jsonPayload = JSON.stringify(arrayPayload);
|
|
10
|
+
|
|
11
|
+
const yamlPayload = `user:
|
|
12
|
+
profile:
|
|
13
|
+
name: Alice
|
|
14
|
+
age: 30
|
|
15
|
+
config:
|
|
16
|
+
debug: false
|
|
17
|
+
version: '1.0.0'
|
|
18
|
+
`;
|
|
19
|
+
|
|
20
|
+
const iniPayload = `[config]
|
|
21
|
+
debug=false
|
|
22
|
+
version=1.0.0
|
|
23
|
+
`;
|
|
24
|
+
|
|
25
|
+
describe('Inline.from* (parse)', () => {
|
|
26
|
+
bench('fromArray', () => {
|
|
27
|
+
Inline.fromArray(arrayPayload);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
bench('fromJson', () => {
|
|
31
|
+
Inline.fromJson(jsonPayload);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
bench('fromYaml', () => {
|
|
35
|
+
Inline.fromYaml(yamlPayload);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
bench('fromIni', () => {
|
|
39
|
+
Inline.fromIni(iniPayload);
|
|
40
|
+
});
|
|
41
|
+
});
|