@safeaccess/inline 0.1.2 → 0.1.3

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/CHANGELOG.md CHANGED
@@ -2,12 +2,25 @@
2
2
 
3
3
  All notable changes to the `@safeaccess/inline` JavaScript/TypeScript package are documented in this file.
4
4
 
5
- ## [0.1.2](https://github.com/felipesauer/safeaccess-inline/compare/js-v0.1.1...js-v0.1.2) (2026-04-08)
5
+ ## [0.1.3](https://github.com/felipesauer/safeaccess-inline/compare/js-v0.1.2...js-v0.1.3) (2026-04-09)
6
+
7
+
8
+ ### Bug Fixes
6
9
 
10
+ * **js:** expose readonly extraForbiddenKeys on SecurityGuard for PHP parity ([2b428f6](https://github.com/felipesauer/safeaccess-inline/commit/2b428f6a1fef3607cb968ff18b52d8281158cc92))
11
+ * **php:** correct array<string,mixed> type annotations and NdjsonAccessor integer key coercion ([7849f89](https://github.com/felipesauer/safeaccess-inline/commit/7849f89365bd5970738105ed3be9d2b58a15cd93))
12
+
13
+ ## [0.1.2](https://github.com/felipesauer/safeaccess-inline/compare/js-v0.1.1...js-v0.1.2) (2026-04-08)
7
14
 
8
15
  ### Bug Fixes
9
16
 
10
- * **js:** fix logo image URL in README ([16f4fc5](https://github.com/felipesauer/safeaccess-inline/commit/16f4fc5d69fa7ce86e3017bbbfc9f393925a5c37))
17
+ - **js:** fix logo image URL in README ([16f4fc5](https://github.com/felipesauer/safeaccess-inline/commit/16f4fc5d69fa7ce86e3017bbbfc9f393925a5c37))
18
+
19
+ ### Internal Changes
20
+
21
+ - **js:** expose `readonly extraForbiddenKeys` on `SecurityGuard` for parity with PHP (`public readonly array $extraForbiddenKeys`)
22
+ - **js:** extract `ValidatableParserInterface` from `DotNotationParser` — `AbstractAccessor` now types its parser dependency against this contract instead of the concrete class
23
+ - **js:** `SecurityGuard.sanitize()` handles nested arrays via a dedicated `sanitizeArray()` private method, matching the PHP `sanitizeRecursive` pattern
11
24
 
12
25
  ## [0.1.1](https://github.com/felipesauer/safeaccess-inline/compare/js-v0.1.0...js-v0.1.1) (2026-04-07)
13
26
 
package/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  <p align="center">
2
- <img src="https://github.com/user-attachments/assets/28202f8b-8ef1-4b94-b6d1-ec16f16c9cf9" width="80" alt="safeaccess-inline logo">
2
+ <img src="https://raw.githubusercontent.com/felipesauer/safeaccess-inline/main/.github/assets/logo.svg" width="80" alt="safeaccess-inline logo">
3
3
  </p>
4
4
 
5
5
  <h1 align="center">Safe Access Inline - TypeScript</h1>
@@ -11,7 +11,7 @@
11
11
 
12
12
  ---
13
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.
14
+ Safe nested data access with dot notation for JavaScript and TypeScript. Navigate deeply nested arrays, objects, JSON, YAML, XML, INI, ENV, and NDJSON structures - with built-in security validation, immutable writes, and a fluent builder API.
15
15
 
16
16
  ## Installation
17
17
 
@@ -151,7 +151,7 @@ accessor.get('database.host'); // 'localhost'
151
151
  <summary><strong>ENV (dotenv)</strong></summary>
152
152
 
153
153
  ```typescript
154
- const accessor = Inline.fromEnv('APP_NAME=MyApp\nDB_HOST=localhost');
154
+ const accessor = Inline.fromEnv('APP_NAME=MyApp\nAPP_DEBUG=true\nDB_HOST=localhost');
155
155
  accessor.get('DB_HOST'); // 'localhost'
156
156
  ```
157
157
 
@@ -181,6 +181,20 @@ objAccessor.get('name'); // 'Alice'
181
181
 
182
182
  </details>
183
183
 
184
+ <details>
185
+ <summary><strong>Any (custom format via integration)</strong></summary>
186
+
187
+ ```typescript
188
+ import { Inline } from '@safeaccess/inline';
189
+ import type { ParseIntegrationInterface } from '@safeaccess/inline';
190
+
191
+ // Requires implementing ParseIntegrationInterface
192
+ const accessor = Inline.withParserIntegration(new MyCsvIntegration()).fromAny(csvString);
193
+ accessor.get('0.column_name');
194
+ ```
195
+
196
+ </details>
197
+
184
198
  <details>
185
199
  <summary><strong>Dynamic (by TypeFormat enum)</strong></summary>
186
200
 
@@ -215,10 +229,8 @@ accessor.getMany({
215
229
  accessor.getRaw(); // original JSON string
216
230
 
217
231
  // Write (immutable - every write returns a new instance)
218
- const updated = accessor.set('a.d', 3);
219
- const cleaned = updated.remove('a.c');
220
- const merged = cleaned.merge('a', { e: 4 });
221
- const full = merged.mergeAll({ f: 5 });
232
+ const updated = accessor.set('a.d', 3).remove('a.c').merge('a', { e: 4 }).mergeAll({ f: 5 });
233
+ updated.all(); // { a: { b: 1, d: 3, e: 4 }, f: 5 }
222
234
 
223
235
  // Readonly mode - block all writes
224
236
  const readonly = accessor.readonly();
@@ -245,7 +257,7 @@ const accessor = Inline.withSecurityGuard(new SecurityGuard(512, ['secret']))
245
257
  | ------------------------------------ | ------------------------------------------------ |
246
258
  | `withSecurityGuard(guard)` | Custom forbidden-key rules and depth limits |
247
259
  | `withSecurityParser(parser)` | Custom payload size and structural limits |
248
- | `withPathCache(cache)` | Custom path segment cache for repeated lookups |
260
+ | `withPathCache(cache)` | Path segment cache for repeated lookups |
249
261
  | `withParserIntegration(integration)` | Custom format parser for `fromAny()` |
250
262
  | `withStrictMode(false)` | Disable security validation (trusted input only) |
251
263
 
@@ -391,7 +403,7 @@ const accessor = Inline.withParserIntegration(csvIntegration).fromAny(csvString)
391
403
 
392
404
  ## API Reference
393
405
 
394
- ### Facade: `Inline`
406
+ ### `Inline` Facade
395
407
 
396
408
  #### Static Factory Methods
397
409
 
@@ -442,6 +454,10 @@ const accessor = Inline.withParserIntegration(csvIntegration).fromAny(csvString)
442
454
  | `readonly(flag?)` | Block all writes |
443
455
  | `strict(flag?)` | Toggle security validation |
444
456
 
457
+ #### TypeFormat Enum
458
+
459
+ `Array` · `Object` · `Json` · `Xml` · `Yaml` · `Ini` · `Env` · `Ndjson` · `Any`
460
+
445
461
  ## Exports
446
462
 
447
463
  The package uses **named exports only** (no default exports). All public types are available from the main entry point:
@@ -45,6 +45,8 @@ export declare abstract class AbstractAccessor implements AccessorsInterface {
45
45
  *
46
46
  * @param data - Raw input in the format expected by the accessor.
47
47
  * @returns Populated accessor instance.
48
+ * @throws {InvalidFormatException} When the raw input cannot be parsed.
49
+ * @throws {SecurityException} When payload exceeds size limit, data contains forbidden keys, or violates structural limits.
48
50
  */
49
51
  abstract from(data: unknown): this;
50
52
  /**
package/dist/inline.d.ts CHANGED
@@ -209,6 +209,8 @@ export declare class Inline extends InlineBuilderAccessor {
209
209
  * @param AccessorConstructor - The accessor class to instantiate.
210
210
  * @param data - Raw data to hydrate the accessor with.
211
211
  * @returns Populated accessor instance.
212
+ * @throws {InvalidFormatException} When the data does not match the accessor's expected format.
213
+ * @throws {SecurityException} When security constraints are violated.
212
214
  *
213
215
  * @example
214
216
  * Inline.make(JsonAccessor, '{"key":"value"}').get('key'); // 'value'
@@ -360,6 +362,8 @@ export declare class Inline extends InlineBuilderAccessor {
360
362
  * @param AccessorConstructor - The accessor class to instantiate.
361
363
  * @param data - Raw data to hydrate the accessor with.
362
364
  * @returns Populated accessor instance.
365
+ * @throws {InvalidFormatException} When the data does not match the accessor's expected format.
366
+ * @throws {SecurityException} When security constraints are violated.
363
367
  *
364
368
  * @example
365
369
  * Inline.make(JsonAccessor, '{"key":"value"}').get('key'); // 'value'
package/dist/inline.js CHANGED
@@ -227,6 +227,8 @@ export class Inline extends InlineBuilderAccessor {
227
227
  * @param AccessorConstructor - The accessor class to instantiate.
228
228
  * @param data - Raw data to hydrate the accessor with.
229
229
  * @returns Populated accessor instance.
230
+ * @throws {InvalidFormatException} When the data does not match the accessor's expected format.
231
+ * @throws {SecurityException} When security constraints are violated.
230
232
  *
231
233
  * @example
232
234
  * Inline.make(JsonAccessor, '{"key":"value"}').get('key'); // 'value'
@@ -430,6 +432,8 @@ export class Inline extends InlineBuilderAccessor {
430
432
  * @param AccessorConstructor - The accessor class to instantiate.
431
433
  * @param data - Raw data to hydrate the accessor with.
432
434
  * @returns Populated accessor instance.
435
+ * @throws {InvalidFormatException} When the data does not match the accessor's expected format.
436
+ * @throws {SecurityException} When security constraints are violated.
433
437
  *
434
438
  * @example
435
439
  * Inline.make(JsonAccessor, '{"key":"value"}').get('key'); // 'value'
@@ -24,6 +24,7 @@ import type { SecurityGuardInterface } from '../contracts/security-guard-interfa
24
24
  */
25
25
  export declare class SecurityGuard implements SecurityGuardInterface {
26
26
  readonly maxDepth: number;
27
+ readonly extraForbiddenKeys: ReadonlyArray<string>;
27
28
  private readonly forbiddenKeysMap;
28
29
  /**
29
30
  * Build the guard with default forbidden keys plus any extras.
@@ -25,6 +25,7 @@ import { DEFAULT_FORBIDDEN_KEYS, STREAM_WRAPPER_PREFIXES } from './forbidden-key
25
25
  */
26
26
  export class SecurityGuard {
27
27
  maxDepth;
28
+ extraForbiddenKeys;
28
29
  forbiddenKeysMap;
29
30
  /**
30
31
  * Build the guard with default forbidden keys plus any extras.
@@ -35,6 +36,7 @@ export class SecurityGuard {
35
36
  constructor(maxDepth = 512,
36
37
  /* Stryker disable next-line ArrayDeclaration -- equivalent: default [] produces identical behavior; no extra keys added to Set */ extraForbiddenKeys = []) {
37
38
  this.maxDepth = Number.isFinite(maxDepth) ? maxDepth : 512;
39
+ this.extraForbiddenKeys = [...extraForbiddenKeys];
38
40
  /* Stryker disable next-line ConditionalExpression -- equivalent: if (false) still produces the same forbiddenKeysMap for empty arrays since Set(DEFAULT)=DEFAULT */
39
41
  if (extraForbiddenKeys.length === 0) {
40
42
  this.forbiddenKeysMap = DEFAULT_FORBIDDEN_KEYS;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@safeaccess/inline",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "private": false,
5
5
  "description": "Safe nested data access with dot notation - JavaScript/TypeScript",
6
6
  "license": "MIT",
@@ -75,6 +75,8 @@ export abstract class AbstractAccessor implements AccessorsInterface {
75
75
  *
76
76
  * @param data - Raw input in the format expected by the accessor.
77
77
  * @returns Populated accessor instance.
78
+ * @throws {InvalidFormatException} When the raw input cannot be parsed.
79
+ * @throws {SecurityException} When payload exceeds size limit, data contains forbidden keys, or violates structural limits.
78
80
  */
79
81
  abstract from(data: unknown): this;
80
82
 
package/src/inline.ts CHANGED
@@ -259,6 +259,8 @@ export class Inline extends InlineBuilderAccessor {
259
259
  * @param AccessorConstructor - The accessor class to instantiate.
260
260
  * @param data - Raw data to hydrate the accessor with.
261
261
  * @returns Populated accessor instance.
262
+ * @throws {InvalidFormatException} When the data does not match the accessor's expected format.
263
+ * @throws {SecurityException} When security constraints are violated.
262
264
  *
263
265
  * @example
264
266
  * Inline.make(JsonAccessor, '{"key":"value"}').get('key'); // 'value'
@@ -479,6 +481,8 @@ export class Inline extends InlineBuilderAccessor {
479
481
  * @param AccessorConstructor - The accessor class to instantiate.
480
482
  * @param data - Raw data to hydrate the accessor with.
481
483
  * @returns Populated accessor instance.
484
+ * @throws {InvalidFormatException} When the data does not match the accessor's expected format.
485
+ * @throws {SecurityException} When security constraints are violated.
482
486
  *
483
487
  * @example
484
488
  * Inline.make(JsonAccessor, '{"key":"value"}').get('key'); // 'value'
@@ -28,6 +28,8 @@ import { DEFAULT_FORBIDDEN_KEYS, STREAM_WRAPPER_PREFIXES } from './forbidden-key
28
28
  export class SecurityGuard implements SecurityGuardInterface {
29
29
  readonly maxDepth: number;
30
30
 
31
+ readonly extraForbiddenKeys: ReadonlyArray<string>;
32
+
31
33
  private readonly forbiddenKeysMap: ReadonlySet<string>;
32
34
 
33
35
  /**
@@ -41,6 +43,7 @@ export class SecurityGuard implements SecurityGuardInterface {
41
43
  /* Stryker disable next-line ArrayDeclaration -- equivalent: default [] produces identical behavior; no extra keys added to Set */ extraForbiddenKeys: string[] = [],
42
44
  ) {
43
45
  this.maxDepth = Number.isFinite(maxDepth) ? maxDepth : 512;
46
+ this.extraForbiddenKeys = [...extraForbiddenKeys];
44
47
 
45
48
  /* Stryker disable next-line ConditionalExpression -- equivalent: if (false) still produces the same forbiddenKeysMap for empty arrays since Set(DEFAULT)=DEFAULT */
46
49
  if (extraForbiddenKeys.length === 0) {
@@ -253,6 +253,24 @@ describe(`${XmlParser.name} > linear scanner - nesting counter`, () => {
253
253
  const a = result['a'] as Record<string, unknown>;
254
254
  expect(a['a']).toBe('');
255
255
  });
256
+
257
+ it('increments nestDepth for same-name opening tag with tab after name', () => {
258
+ const result = makeParser().parse('<root><a><a\t>inner</a></a></root>');
259
+ const a = result['a'] as Record<string, unknown>;
260
+ expect(a).toEqual({ a: 'inner' });
261
+ });
262
+
263
+ it('increments nestDepth for same-name opening tag with newline after name', () => {
264
+ const result = makeParser().parse('<root><a><a\n>inner</a></a></root>');
265
+ const a = result['a'] as Record<string, unknown>;
266
+ expect(a).toEqual({ a: 'inner' });
267
+ });
268
+
269
+ it('increments nestDepth for same-name opening tag with carriage return after name', () => {
270
+ const result = makeParser().parse('<root><a><a\r>inner</a></a></root>');
271
+ const a = result['a'] as Record<string, unknown>;
272
+ expect(a).toEqual({ a: 'inner' });
273
+ });
256
274
  });
257
275
 
258
276
  describe(`${XmlParser.name} > linear scanner - self-closing detection`, () => {
@@ -265,6 +283,11 @@ describe(`${XmlParser.name} > linear scanner - self-closing detection`, () => {
265
283
  const result = makeParser().parse('<root><flag enabled="true" /></root>');
266
284
  expect(result['flag']).toBe('');
267
285
  });
286
+
287
+ it('treats <tag / > (space after slash before >) as self-closing', () => {
288
+ const result = makeParser().parse('<root><empty / ></root>');
289
+ expect(result['empty']).toBe('');
290
+ });
268
291
  });
269
292
 
270
293
  describe(`${XmlParser.name} > linear scanner - skip non-element tokens`, () => {
@@ -299,6 +322,30 @@ describe(`${XmlParser.name} > linear scanner - skip non-element tokens`, () => {
299
322
  const result = makeParser().parse('<root><1tag>value</root>');
300
323
  expect(result['#text']).toBe('<1tag>value');
301
324
  });
325
+
326
+ it('does not parse element-like tokens inside XML comments', () => {
327
+ const result = makeParser().parse('<root><!-- <fake>x</fake> --><name>Alice</name></root>');
328
+ expect(result['name']).toBe('Alice');
329
+ expect(Object.keys(result)).toEqual(['name']);
330
+ });
331
+
332
+ it('does not parse element-like tokens inside processing instructions', () => {
333
+ const result = makeParser().parse('<root><?pi <data>x</data> ?><name>Bob</name></root>');
334
+ expect(result['name']).toBe('Bob');
335
+ expect(Object.keys(result)).toEqual(['name']);
336
+ });
337
+
338
+ it('ignores digit-started tag even when it has a matching close tag', () => {
339
+ const result = makeParser().parse('<root><1>v</1><name>w</name></root>');
340
+ expect(result['name']).toBe('w');
341
+ expect(Object.keys(result)).toEqual(['name']);
342
+ });
343
+
344
+ it('does not parse elements embedded inside a stray closing tag prefix', () => {
345
+ const result = makeParser().parse('<root></a<b>text</b><name>v</name></root>');
346
+ expect(result['name']).toBe('v');
347
+ expect(Object.keys(result)).toEqual(['name']);
348
+ });
302
349
  });
303
350
 
304
351
  describe(`${XmlParser.name} > linear scanner - unclosed and malformed tags`, () => {
@@ -291,4 +291,15 @@ describe(`${XmlParser.name} > manual parser edge cases`, () => {
291
291
  const result = makeParser().parse('<root><item>hello <world</item></root>');
292
292
  expect(result['item']).toBe('hello <world');
293
293
  });
294
+
295
+ it('parses elements preceded by plain text in child content', () => {
296
+ const result = makeParser().parse('<root><wrap>prefix<a>v</a></wrap></root>');
297
+ const wrap = result['wrap'] as Record<string, unknown>;
298
+ expect(wrap['a']).toBe('v');
299
+ });
300
+
301
+ it('preserves child object with multiple keys without #text flattening', () => {
302
+ const result = makeParser().parse('<root><w><x>1</x><y>2</y></w></root>');
303
+ expect(result['w']).toEqual({ x: '1', y: '2' });
304
+ });
294
305
  });
@@ -162,4 +162,14 @@ describe(`${SecurityGuard.name} > extraForbiddenKeys`, () => {
162
162
  expect(guard.isForbiddenKey('__proto__')).toBe(true);
163
163
  expect(guard.isForbiddenKey('constructor')).toBe(true);
164
164
  });
165
+
166
+ it('exposes the extra forbidden keys as a readonly array', () => {
167
+ const guard = new SecurityGuard(512, ['custom_a', 'custom_b']);
168
+ expect(guard.extraForbiddenKeys).toEqual(['custom_a', 'custom_b']);
169
+ });
170
+
171
+ it('exposes an empty readonly array when no extra keys are provided', () => {
172
+ const guard = new SecurityGuard();
173
+ expect(guard.extraForbiddenKeys).toEqual([]);
174
+ });
165
175
  });