yamlock 1.0.0 → 1.1.0
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 +205 -0
- package/README.md +57 -9
- package/dist/cli/cli.js +42 -15
- package/dist/errors.js +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/utils/config.js +41 -4
- package/dist/utils/migrate.js +8 -2
- package/dist/utils/path-pattern.js +260 -0
- package/docs/api.md +9 -2
- package/docs/design/path-patterns.md +213 -0
- package/docs/errors.md +2 -1
- package/docs/yaml-behavior.md +3 -2
- package/package.json +1 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file. Release versions are finalized in dedicated commits and use matching numeric Git tags.
|
|
4
|
+
|
|
5
|
+
## [Unreleased]
|
|
6
|
+
|
|
7
|
+
## [1.1.0] - 2026-08-14
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- Tag-gated GitHub Release automation that reuses the full CI matrix, validates the tag against `main`, derives notes from the matching changelog section, verifies a draft, and publishes the release as latest.
|
|
11
|
+
- Structural `pathPatterns` / `--path-patterns` selectors for `processConfig` and CLI encrypt/decrypt/migrate workflows, with whole-segment `*`, `[*]`, and `**` matching, exact-path authentication, and fail-closed validation without reinterpreting existing `paths`.
|
|
12
|
+
|
|
13
|
+
## [1.0.0] - 2026-08-11
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
- Authenticated payload v2 for the Node.js API, using AES-256-GCM, scrypt, strict parsing, and authenticated field-path metadata.
|
|
17
|
+
- Frozen legacy payload fixtures and v2 regression tests for deterministic vectors, tampering, malformed input, mixed-format configs, and resource limits.
|
|
18
|
+
- Payload v2 design documentation covering the threat model, serialized format, compatibility, and staged migration.
|
|
19
|
+
- Safe `yamlock migrate` workflow with dry-run summaries, selective paths, mixed-format validation, explicit backups, separate outputs, and atomic permission-preserving writes.
|
|
20
|
+
- Explicit CLI legacy-write compatibility through `yamlock encrypt --legacy`; custom encryption algorithms require this mode.
|
|
21
|
+
- Strict repeated-encryption checks through CLI `--error-on-encrypted` and Node.js `existingPayloadPolicy: 'error'`.
|
|
22
|
+
- Explicit `--force-encrypt` / `existingPayloadPolicy: 'encrypt'` handling for intentional `yl|...` plaintext or nested encryption.
|
|
23
|
+
- YAML rewrite documentation and regression coverage for comments, formatting, anchors, aliases, merge keys, and explicit/custom tags.
|
|
24
|
+
- Public `serializePath(segments)` helper for constructing canonical field paths without string ambiguity.
|
|
25
|
+
- Edge-case regression coverage for empty containers, sparse arrays, special and Unicode keys, empty strings, and large values.
|
|
26
|
+
- Automated npm tarball smoke coverage for package contents, public API imports, and the installed distribution-first CLI.
|
|
27
|
+
- Stable public Node.js error classes and `ERR_*` codes for validation, payload, authentication, legacy decryption, and config-processing failures.
|
|
28
|
+
- Bundled TypeScript declarations and a documented `1.x` stability contract for the public Node.js API.
|
|
29
|
+
- Built-in coverage thresholds plus automated Markdown-link and release-metadata checks.
|
|
30
|
+
- Packaged runnable API and CI/CD examples with regression coverage against the installed distribution.
|
|
31
|
+
- Conventional `help`, `-h`, and `--help` CLI entry points with installed-package smoke coverage.
|
|
32
|
+
- Tracked Bumpster configuration, synchronized `VERSION` metadata, and a pre-bump release gate.
|
|
33
|
+
|
|
34
|
+
### Changed
|
|
35
|
+
- Updated `js-yaml` to 4.3.1 to address merge-key, alias, and ordered-map denial-of-service advisories; refreshed ESLint 9 tooling within the existing major version.
|
|
36
|
+
- Package builds now exclude ignored local dotfiles, and tarball smoke checks recursively for local workflow artifacts.
|
|
37
|
+
- CI now covers supported Node.js releases on Linux and macOS, uses current Node 24-based GitHub Actions, and performs a tag-gated package preflight.
|
|
38
|
+
- Development and dependency updates now target `dev`; Bumpster releases atomically synchronize `dev`, `main`, and `vX.Y.Z` tags.
|
|
39
|
+
- `encryptValue`, `processConfig`, and `yamlock encrypt` now write authenticated v2 payloads by default.
|
|
40
|
+
- `decryptValue` and `processConfig` continue to auto-detect and read both legacy and v2 payloads; API callers can request legacy writing explicitly with `formatVersion: 1` or legacy algorithm options.
|
|
41
|
+
- Repeated `processConfig`/CLI encryption now authenticates and preserves existing payloads instead of adding another encryption layer; fully encrypted in-place inputs are not rewritten.
|
|
42
|
+
- CLI argument parsing now rejects unknown or duplicate options, missing values, extra positional arguments, and options that do not belong to the selected command before reading input files; `keygen --length` accepts only integers from 1 to 4096 bytes.
|
|
43
|
+
- CLI integration tests now force an explicit source mode through the real `bin/yamlock` launcher, preventing stale local `dist` output from masking source changes while preserving distribution-first behavior for normal runs.
|
|
44
|
+
- `processConfig` now validates non-string policies, path options, serializer output, collisions, and circular structures; opaque values are preserved by `ignore`, while `stringify` fails closed for values that cannot be converted without type loss.
|
|
45
|
+
- Selective processing applies non-string policies only to selected leaves, and YAML timestamp values are no longer collapsed into empty objects during CLI traversal.
|
|
46
|
+
- Normal CLI encrypt/decrypt writes now use the shared atomic temporary-file writer, verify that the source did not change after reading, preserve source or existing-output modes, reject symbolic-link paths, and clean up temporary files after failures.
|
|
47
|
+
- CLI help now warns that YAML presentation details are normalized during writes and points users toward `--dry-run` or `--output` workflows.
|
|
48
|
+
- YAML and JSON parse failures now report sanitized diagnostics without echoing source lines that may contain secrets.
|
|
49
|
+
- Default field paths now escape reserved characters in object keys, CLI path lists understand escaped commas, and existing payloads with legacy ambiguous paths remain readable.
|
|
50
|
+
- Config processing and migration now preserve sparse-array length and holes; `processConfig` also retains null prototypes and own keys such as `__proto__` without prototype assignment.
|
|
51
|
+
|
|
52
|
+
## [0.3.0] - 2025-12-01
|
|
53
|
+
### Added
|
|
54
|
+
- `processConfig` non-string policies (`ignore`, `stringify`, `error`) and optional `pathSerializer` hook.
|
|
55
|
+
- Unit tests demonstrating the new policies.
|
|
56
|
+
|
|
57
|
+
### Changed
|
|
58
|
+
- README highlights non-string handling in the advanced usage section.
|
|
59
|
+
|
|
60
|
+
## [0.2.10] - 2025-12-01
|
|
61
|
+
### Added
|
|
62
|
+
- README example demonstrating the `--dry-run` workflow (with sample diff output).
|
|
63
|
+
|
|
64
|
+
### Changed
|
|
65
|
+
- Package version bumped to keep documentation in sync.
|
|
66
|
+
|
|
67
|
+
## [0.2.9] - 2025-12-01
|
|
68
|
+
### Added
|
|
69
|
+
- CLI `--dry-run` flag prints the diff without touching files.
|
|
70
|
+
- Structured error codes (e.g., `[yamlock:ERR_MISSING_KEY]`) for machine-readable failure handling.
|
|
71
|
+
- Integration tests covering dry-run and the new error format.
|
|
72
|
+
|
|
73
|
+
### Changed
|
|
74
|
+
- README documents the `--dry-run` flag and behavior.
|
|
75
|
+
|
|
76
|
+
## [0.2.8] - 2025-12-01
|
|
77
|
+
### Added
|
|
78
|
+
- Advanced usage docs: selective path tips, CI/CD workflow example (`examples/docs/ci-cd.md`), and an expanded key-rotation guide with scripting advice.
|
|
79
|
+
- README now links to the new docs and highlights selective encryption in a dedicated section.
|
|
80
|
+
|
|
81
|
+
### Changed
|
|
82
|
+
- Package version bumped to keep documentation in sync.
|
|
83
|
+
|
|
84
|
+
## [0.2.7] - 2025-12-01
|
|
85
|
+
### Added
|
|
86
|
+
- README badges for npm version and Node test status, plus a release information section summarizing install options.
|
|
87
|
+
|
|
88
|
+
### Changed
|
|
89
|
+
- Package version bumped to keep documentation in sync.
|
|
90
|
+
|
|
91
|
+
## [0.2.6] - 2025-11-28
|
|
92
|
+
### Added
|
|
93
|
+
- Unit tests covering `processConfig` path filters and CLI integration tests for custom algorithms/decrypt paths.
|
|
94
|
+
|
|
95
|
+
### Changed
|
|
96
|
+
- README/CLI behavior unchanged.
|
|
97
|
+
|
|
98
|
+
## [0.2.5] - 2025-11-28
|
|
99
|
+
### Added
|
|
100
|
+
- `yamlock algorithms` now distinguishes between tested presets and other OpenSSL-provided ciphers.
|
|
101
|
+
- Integration tests updated to confirm the new output format.
|
|
102
|
+
|
|
103
|
+
### Changed
|
|
104
|
+
- README notes that the algorithms command shows both tested and additional cipher lists.
|
|
105
|
+
|
|
106
|
+
## [0.2.4] - 2025-11-28
|
|
107
|
+
### Added
|
|
108
|
+
- `yamlock keygen` command generates random keys with configurable length and format, including guidance on storing them.
|
|
109
|
+
- Integration tests covering key generation.
|
|
110
|
+
|
|
111
|
+
### Changed
|
|
112
|
+
- README documents the key generator usage.
|
|
113
|
+
|
|
114
|
+
## [0.2.3] - 2025-11-27
|
|
115
|
+
### Added
|
|
116
|
+
- `yamlock version` command prints the installed CLI version.
|
|
117
|
+
- `yamlock algorithms` lists all ciphers available in the current runtime.
|
|
118
|
+
- Integration tests cover the new commands.
|
|
119
|
+
|
|
120
|
+
### Changed
|
|
121
|
+
- CLI help and README highlight the new commands.
|
|
122
|
+
- Moved shared fixtures under `fixtures/` so the test runner skips them as standalone suites.
|
|
123
|
+
|
|
124
|
+
## [0.2.2] - 2025-11-27
|
|
125
|
+
### Added
|
|
126
|
+
- CLI support for `--paths` (partial encryption/decryption) and `--output` (separate destination files).
|
|
127
|
+
- `processConfig` now accepts targeted path lists so API/CLI share the same behavior.
|
|
128
|
+
|
|
129
|
+
### Changed
|
|
130
|
+
- README documents the new CLI options with examples.
|
|
131
|
+
|
|
132
|
+
## [0.2.1] - 2025-11-26
|
|
133
|
+
### Changed
|
|
134
|
+
- Unified unit tests around shared fixtures/helpers to remove duplication and ensure every supported algorithm is exercised consistently.
|
|
135
|
+
|
|
136
|
+
## [0.2.0] - 2025-11-26
|
|
137
|
+
### Added
|
|
138
|
+
- Algorithm presets with override support (e.g., `chacha20-poly1305` with auth tags, configurable key/IV lengths).
|
|
139
|
+
- API support for passing algorithm options through `encryptValue`, `decryptValue`, and `processConfig`.
|
|
140
|
+
- Additional unit coverage for presets and algorithm-aware config processing.
|
|
141
|
+
|
|
142
|
+
## [0.1.2] - 2025-11-26
|
|
143
|
+
### Fixed
|
|
144
|
+
- CLI binary now falls back to the source entry during development, ensuring `yamlock` works when installed globally or run via `node bin/yamlock`.
|
|
145
|
+
- Integration tests call the published bin, catching future regressions.
|
|
146
|
+
|
|
147
|
+
## [0.1.1] - 2025-11-26
|
|
148
|
+
### Added
|
|
149
|
+
- Key rotation guide under `docs/key-rotation.md` and README link.
|
|
150
|
+
- Documented CLI exit codes and reference to the example script.
|
|
151
|
+
|
|
152
|
+
## [0.1.0] - 2025-11-26
|
|
153
|
+
### Added
|
|
154
|
+
- Example script under `examples/basic.js` demonstrating encrypt/decrypt flows.
|
|
155
|
+
- GitHub Actions CI workflow to lint, test, and build on pushes/PRs.
|
|
156
|
+
- README now links to the example script.
|
|
157
|
+
|
|
158
|
+
## [0.0.10] - 2025-11-26
|
|
159
|
+
### Added
|
|
160
|
+
- Expanded README with usage examples, encrypted format description, and contributing link.
|
|
161
|
+
- CONTRIBUTING guide plus GitHub issue and pull request templates.
|
|
162
|
+
|
|
163
|
+
## [0.0.9] - 2025-11-26
|
|
164
|
+
### Added
|
|
165
|
+
- CLI now auto-detects YAML vs JSON, loads via js-yaml, and writes back preserving the original format.
|
|
166
|
+
- Integration tests covering YAML encrypt/decrypt flows.
|
|
167
|
+
|
|
168
|
+
## [0.0.8] - 2025-11-26
|
|
169
|
+
### Added
|
|
170
|
+
- CLI skeleton capable of encrypting/decrypting JSON files with `--key` and `--algorithm` options.
|
|
171
|
+
- Integration tests covering CLI encrypt/decrypt flows and key validation.
|
|
172
|
+
|
|
173
|
+
## [0.0.7] - 2025-11-26
|
|
174
|
+
### Added
|
|
175
|
+
- Public API exports for `encryptValue`, `decryptValue`, `processConfig`, and `getSupportedAlgorithms`.
|
|
176
|
+
- Unit tests ensuring the API surface is available and round-trips data correctly.
|
|
177
|
+
|
|
178
|
+
## [0.0.6] - 2025-11-25
|
|
179
|
+
### Added
|
|
180
|
+
- `processConfig` helper to walk objects/arrays and encrypt or decrypt every string value based on field paths.
|
|
181
|
+
- Unit tests covering nested traversal, arrays, round-trips, and validation errors.
|
|
182
|
+
|
|
183
|
+
## [0.0.5] - 2025-11-25
|
|
184
|
+
### Added
|
|
185
|
+
- Field path utilities that serialize nested object/array locations (`src/utils/path.js`).
|
|
186
|
+
- Unit tests covering dot/bracket formatting and invalid input handling.
|
|
187
|
+
|
|
188
|
+
## [0.0.4] - 2025-11-25
|
|
189
|
+
### Added
|
|
190
|
+
- `decryptValue` support with field-path validation and yamlock payload parsing.
|
|
191
|
+
- Unit tests for decrypt flow, mismatch handling, and invalid payload detection.
|
|
192
|
+
|
|
193
|
+
## [0.0.3] - 2025-11-24
|
|
194
|
+
### Added
|
|
195
|
+
- `encryptValue` function that encrypts string fields with per-path salts and random IVs.
|
|
196
|
+
- Unit tests covering payload structure, IV randomness, and validation checks.
|
|
197
|
+
|
|
198
|
+
## [0.0.2] - 2025-11-24
|
|
199
|
+
### Added
|
|
200
|
+
- Crypto utility helpers for key derivation, IV generation, salt encoding, and payload formatting/parsing.
|
|
201
|
+
- Unit tests covering the utility layer and yamlock payload parsing.
|
|
202
|
+
|
|
203
|
+
## [0.0.1] - 2025-11-24
|
|
204
|
+
### Added
|
|
205
|
+
- Initial project scaffold: package metadata, README baseline, ESLint config, CLI stub, and directory structure.
|
package/README.md
CHANGED
|
@@ -40,6 +40,7 @@ yarn add yamlock # project dependency
|
|
|
40
40
|
- CLI workflow that processes YAML or JSON files in place.
|
|
41
41
|
- Safe CLI migration from legacy payloads to authenticated v2 payloads.
|
|
42
42
|
- Recursively lock/unlock entire objects via `processConfig`.
|
|
43
|
+
- Select repeated fields or subtrees with structural path patterns.
|
|
43
44
|
- Public API exports that mirror CLI behavior for programmatic use.
|
|
44
45
|
- Focus on Node.js 22+, ESM modules, and a lightweight dependency set (`js-yaml`).
|
|
45
46
|
|
|
@@ -57,6 +58,9 @@ yamlock decrypt settings.json --key "super-secret"
|
|
|
57
58
|
# Encrypt only selected fields into a new file
|
|
58
59
|
yamlock encrypt config.json --key "$YAMLOCK_KEY" --paths "db.password,api.token" --output config.secure.json
|
|
59
60
|
|
|
61
|
+
# Encrypt repeated tokens and an entire subtree
|
|
62
|
+
yamlock encrypt config.json --key "$YAMLOCK_KEY" --path-patterns 'users[*].token,services.**'
|
|
63
|
+
|
|
60
64
|
# Inspect CLI metadata
|
|
61
65
|
yamlock --help
|
|
62
66
|
yamlock version
|
|
@@ -90,6 +94,7 @@ and use `--dry-run` or `--output` when presentation details matter.
|
|
|
90
94
|
Options of note:
|
|
91
95
|
- `--output <file>` writes the result to a separate file instead of overwriting the input.
|
|
92
96
|
- `--paths <path1,path2>` targets only the specified fields using the [escaped path syntax](#field-path-syntax).
|
|
97
|
+
- `--path-patterns <pattern1,pattern2>` selects structural paths with whole-segment `*`, `[*]`, and `**` wildcards.
|
|
93
98
|
- `--dry-run` previews an operation without modifying files; encrypt/decrypt print content changes, while migrate prints only counts and target paths.
|
|
94
99
|
- `migrate` decrypts selected legacy payloads and re-encrypts them as authenticated v2 payloads.
|
|
95
100
|
- `migrate --allow-mixed` additionally authenticates and preserves selected values that are already v2.
|
|
@@ -138,6 +143,33 @@ readable through the default serializer's compatibility path; selecting those
|
|
|
138
143
|
keys now requires the canonical escaped spelling. A custom `pathSerializer`
|
|
139
144
|
keeps its own contract and does not use the default compatibility fallback.
|
|
140
145
|
|
|
146
|
+
### Path patterns
|
|
147
|
+
|
|
148
|
+
Patterns are separate from exact `paths`; using both forms a union. They match
|
|
149
|
+
complete structural leaf paths:
|
|
150
|
+
|
|
151
|
+
- `services.*.token` matches one object-key segment, such as
|
|
152
|
+
`services.api.token`, but not `services[0].token`.
|
|
153
|
+
- `users[*].token` matches array elements such as `users[0].token`.
|
|
154
|
+
- `db.**` matches a leaf at `db` or any descendant below it, including arrays.
|
|
155
|
+
|
|
156
|
+
Wildcards must occupy a complete segment. Partial globs such as `service-*`
|
|
157
|
+
are rejected with `ERR_INVALID_PATH_PATTERNS` before the CLI reads the input
|
|
158
|
+
file. Literal reserved characters use the same escaping as exact paths, and
|
|
159
|
+
`\*` selects a literal asterisk key:
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
yamlock encrypt config.json \
|
|
163
|
+
--key "$YAMLOCK_KEY" \
|
|
164
|
+
--paths 'root.literal' \
|
|
165
|
+
--path-patterns 'services.*.token,users[*].token,labels\,primary'
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Patterns only decide which leaves are selected. Encryption, decryption, and
|
|
169
|
+
migration continue to bind every payload to its exact canonical leaf path.
|
|
170
|
+
Node.js callers use `pathPatterns: string[]`; it cannot be combined with a
|
|
171
|
+
custom `pathSerializer`.
|
|
172
|
+
|
|
141
173
|
### Node.js API
|
|
142
174
|
|
|
143
175
|
```js
|
|
@@ -147,8 +179,17 @@ const encrypted = encryptValue('swordfish', process.env.YAMLOCK_KEY, 'db.passwor
|
|
|
147
179
|
const decrypted = decryptValue(encrypted, process.env.YAMLOCK_KEY, 'db.password');
|
|
148
180
|
|
|
149
181
|
const config = { db: { password: 'swordfish' } };
|
|
150
|
-
const
|
|
151
|
-
const
|
|
182
|
+
const selectors = { pathPatterns: ['db.**'] };
|
|
183
|
+
const locked = processConfig(config, {
|
|
184
|
+
mode: 'encrypt',
|
|
185
|
+
key: process.env.YAMLOCK_KEY,
|
|
186
|
+
...selectors
|
|
187
|
+
});
|
|
188
|
+
const unlocked = processConfig(locked, {
|
|
189
|
+
mode: 'decrypt',
|
|
190
|
+
key: process.env.YAMLOCK_KEY,
|
|
191
|
+
...selectors
|
|
192
|
+
});
|
|
152
193
|
```
|
|
153
194
|
|
|
154
195
|
Expected Node.js API failures extend `YamlockError` and expose stable `ERR_*`
|
|
@@ -303,10 +344,11 @@ yamlock migrate config.yaml --key "$YAMLOCK_KEY" --paths "db.password,api.token"
|
|
|
303
344
|
|
|
304
345
|
Migration validates every selected value and builds the complete result before
|
|
305
346
|
writing. Selected plaintext and non-string values are rejected, so use
|
|
306
|
-
`--paths` for partially encrypted configs. Selected v2
|
|
307
|
-
unless `--allow-mixed` is set; with that flag they are
|
|
308
|
-
unchanged. In-place writes are atomic, preserve the
|
|
309
|
-
not replace an existing backup. To roll back, verify
|
|
347
|
+
`--paths` or `--path-patterns` for partially encrypted configs. Selected v2
|
|
348
|
+
values are rejected unless `--allow-mixed` is set; with that flag they are
|
|
349
|
+
authenticated and kept unchanged. In-place writes are atomic, preserve the
|
|
350
|
+
source file mode, and do not replace an existing backup. To roll back, verify
|
|
351
|
+
the backup and then copy
|
|
310
352
|
`config.yaml.yamlock.bak` over `config.yaml`.
|
|
311
353
|
|
|
312
354
|
Legacy AES-CBC payloads have no authentication, so migration can only validate
|
|
@@ -318,7 +360,7 @@ not received a third-party security audit.
|
|
|
318
360
|
|
|
319
361
|
## Advanced usage
|
|
320
362
|
|
|
321
|
-
- **Selective encryption**:
|
|
363
|
+
- **Selective encryption**: use exact `--paths`/`paths` selectors, structural `--path-patterns`/`pathPatterns`, or their union to encrypt only sensitive fields.
|
|
322
364
|
- **Repeated encryption**: valid selected payloads are authenticated and preserved; add `--error-on-encrypted` or `existingPayloadPolicy: 'error'` for strict workflows.
|
|
323
365
|
- **Non-string handling**: use `nonStringPolicy: 'ignore' | 'stringify' | 'error'` to preserve opaque leaves, stringify finite JSON primitives, or reject selected non-string values; use `pathSerializer` to change path representation (e.g., `db/password` instead of dot notation).
|
|
324
366
|
- **CI/CD flows**: see [examples/docs/ci-cd.md](examples/docs/ci-cd.md) for a GitHub Actions job that decrypts configs for builds and re-encrypts them before publishing artifacts.
|
|
@@ -341,11 +383,14 @@ default v2 writer for new data.
|
|
|
341
383
|
## Release information
|
|
342
384
|
|
|
343
385
|
- The badges at the top show the latest npm version and the status of the full CI matrix.
|
|
344
|
-
- See [
|
|
386
|
+
- See [GitHub Releases](https://github.com/phoenixweiss/yamlock/releases) and
|
|
387
|
+
[CHANGELOG.md](CHANGELOG.md) for detailed release notes; install a specific
|
|
388
|
+
version via `npm install yamlock@<version>`.
|
|
345
389
|
- yamlock versions are bumped with my own release utility,
|
|
346
390
|
[Bumpster](https://github.com/phoenixweiss/Bumpster). It keeps the tracked
|
|
347
391
|
`VERSION` file and `package.json` synchronized while publishing the `dev`,
|
|
348
|
-
`main`, and `vX.Y.Z` Git refs atomically.
|
|
392
|
+
`main`, and `vX.Y.Z` Git refs atomically. Each stable tag must pass the full
|
|
393
|
+
CI and package preflight before its GitHub Release is published.
|
|
349
394
|
|
|
350
395
|
### Encrypted value formats
|
|
351
396
|
|
|
@@ -393,6 +438,9 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for development workflow, available scrip
|
|
|
393
438
|
|
|
394
439
|
## Future work
|
|
395
440
|
|
|
441
|
+
- Safe subtree and wildcard selectors are specified in the
|
|
442
|
+
[path pattern design proposal](docs/design/path-patterns.md). Existing
|
|
443
|
+
`paths` remain exact; the proposed feature is not implemented yet.
|
|
396
444
|
- An async encryption API with bounded scrypt concurrency for large configs.
|
|
397
445
|
- Stricter file-format validation and preservation rules for advanced YAML features.
|
|
398
446
|
|
package/dist/cli/cli.js
CHANGED
|
@@ -14,6 +14,7 @@ import yaml from 'js-yaml';
|
|
|
14
14
|
import { processConfig } from '../utils/config.js';
|
|
15
15
|
import { writeFileAtomically } from '../utils/file.js';
|
|
16
16
|
import { migrateConfig } from '../utils/migrate.js';
|
|
17
|
+
import { compilePathPatterns } from '../utils/path-pattern.js';
|
|
17
18
|
import { listSupportedAlgorithms, TESTED_ALGORITHMS } from '../crypto/utils.js';
|
|
18
19
|
|
|
19
20
|
const require = createRequire(import.meta.url);
|
|
@@ -32,6 +33,7 @@ const OPTION_SPECS = [
|
|
|
32
33
|
{ key: 'algorithm', names: ['-a', '--algorithm'], takesValue: true },
|
|
33
34
|
{ key: 'output', names: ['-o', '--output'], takesValue: true },
|
|
34
35
|
{ key: 'paths', names: ['-p', '--paths'], takesValue: true },
|
|
36
|
+
{ key: 'pathPatterns', names: ['--path-patterns'], takesValue: true },
|
|
35
37
|
{ key: 'dryRun', names: ['-d', '--dry-run'], takesValue: false },
|
|
36
38
|
{ key: 'allowMixed', names: ['--allow-mixed'], takesValue: false },
|
|
37
39
|
{ key: 'noBackup', names: ['--no-backup'], takesValue: false },
|
|
@@ -53,13 +55,22 @@ const COMMAND_OPTIONS = new Map([
|
|
|
53
55
|
'algorithm',
|
|
54
56
|
'output',
|
|
55
57
|
'paths',
|
|
58
|
+
'pathPatterns',
|
|
56
59
|
'dryRun',
|
|
57
60
|
'legacy',
|
|
58
61
|
'errorOnEncrypted',
|
|
59
62
|
'forceEncrypt'
|
|
60
63
|
])],
|
|
61
|
-
['decrypt', new Set(['key', 'output', 'paths', 'dryRun'])],
|
|
62
|
-
['migrate', new Set([
|
|
64
|
+
['decrypt', new Set(['key', 'output', 'paths', 'pathPatterns', 'dryRun'])],
|
|
65
|
+
['migrate', new Set([
|
|
66
|
+
'key',
|
|
67
|
+
'output',
|
|
68
|
+
'paths',
|
|
69
|
+
'pathPatterns',
|
|
70
|
+
'dryRun',
|
|
71
|
+
'allowMixed',
|
|
72
|
+
'noBackup'
|
|
73
|
+
])],
|
|
63
74
|
['keygen', new Set(['length', 'format'])],
|
|
64
75
|
['help', new Set()],
|
|
65
76
|
['version', new Set()],
|
|
@@ -87,6 +98,7 @@ Options:
|
|
|
87
98
|
-a, --algorithm <value> Legacy cipher algorithm (encrypt --legacy only).
|
|
88
99
|
-o, --output <file> Write the result to a different file (otherwise overwrites the input file).
|
|
89
100
|
-p, --paths <p1,p2> Comma-separated escaped field paths to process (dot/bracket notation).
|
|
101
|
+
--path-patterns <p1,p2> Structural selectors using *, [*], and ** whole-segment wildcards.
|
|
90
102
|
-d, --dry-run Preview the operation without modifying files.
|
|
91
103
|
--allow-mixed (migrate) Authenticate and preserve selected v2 values.
|
|
92
104
|
--no-backup (migrate) Replace the input without creating <file>.yamlock.bak.
|
|
@@ -104,6 +116,7 @@ YAML rewrite note:
|
|
|
104
116
|
Path syntax:
|
|
105
117
|
Object-key backslashes, dots, brackets, and commas must be backslash-escaped.
|
|
106
118
|
Example: db\\.primary.token selects { "db.primary": { "token": ... } }.
|
|
119
|
+
Patterns are separate selectors; e.g. services.*.token or users[*].token.
|
|
107
120
|
`;
|
|
108
121
|
}
|
|
109
122
|
|
|
@@ -164,7 +177,7 @@ function serializeConfig(format, data) {
|
|
|
164
177
|
return `${JSON.stringify(data, null, 2)}\n`;
|
|
165
178
|
}
|
|
166
179
|
|
|
167
|
-
function
|
|
180
|
+
function splitPathList(value) {
|
|
168
181
|
if (!value) {
|
|
169
182
|
return [];
|
|
170
183
|
}
|
|
@@ -189,9 +202,7 @@ function parsePaths(value) {
|
|
|
189
202
|
}
|
|
190
203
|
paths.push(current);
|
|
191
204
|
|
|
192
|
-
return paths
|
|
193
|
-
.map((segment) => segment.trim())
|
|
194
|
-
.filter((segment) => segment.length > 0);
|
|
205
|
+
return paths.map((segment) => segment.trim());
|
|
195
206
|
}
|
|
196
207
|
|
|
197
208
|
function parseArgs(argv) {
|
|
@@ -242,15 +253,26 @@ function parseArgs(argv) {
|
|
|
242
253
|
);
|
|
243
254
|
}
|
|
244
255
|
|
|
245
|
-
if (spec.key === 'paths') {
|
|
246
|
-
const
|
|
247
|
-
|
|
256
|
+
if (spec.key === 'paths' || spec.key === 'pathPatterns') {
|
|
257
|
+
const splitSelectors = splitPathList(next);
|
|
258
|
+
const selectors = spec.key === 'pathPatterns'
|
|
259
|
+
? splitSelectors
|
|
260
|
+
: splitSelectors.filter((selector) => selector.length > 0);
|
|
261
|
+
if (
|
|
262
|
+
selectors.length === 0 ||
|
|
263
|
+
(spec.key === 'pathPatterns' && selectors.some((selector) => selector.length === 0))
|
|
264
|
+
) {
|
|
248
265
|
throw cliError(
|
|
249
|
-
'
|
|
250
|
-
|
|
266
|
+
spec.key === 'pathPatterns'
|
|
267
|
+
? 'ERR_INVALID_PATH_PATTERNS'
|
|
268
|
+
: 'ERR_INVALID_OPTION_VALUE',
|
|
269
|
+
`Option ${OPTION_LABELS.get(spec.key)} requires at least one non-empty selector.`
|
|
251
270
|
);
|
|
252
271
|
}
|
|
253
|
-
|
|
272
|
+
if (spec.key === 'pathPatterns') {
|
|
273
|
+
compilePathPatterns(selectors);
|
|
274
|
+
}
|
|
275
|
+
result.options[spec.key] = selectors;
|
|
254
276
|
} else {
|
|
255
277
|
result.options[spec.key] = next;
|
|
256
278
|
}
|
|
@@ -435,6 +457,7 @@ function handleMigration({ file, absolutePath, outputPath, config, key, options
|
|
|
435
457
|
const result = migrateConfig(config.data, {
|
|
436
458
|
key,
|
|
437
459
|
paths: options.paths,
|
|
460
|
+
pathPatterns: options.pathPatterns,
|
|
438
461
|
allowMixed: options.allowMixed
|
|
439
462
|
});
|
|
440
463
|
const serialized = serializeConfig(config.format, result.data);
|
|
@@ -629,7 +652,8 @@ export async function runCli(argv = process.argv) {
|
|
|
629
652
|
: options.errorOnEncrypted
|
|
630
653
|
? 'error'
|
|
631
654
|
: 'preserve',
|
|
632
|
-
paths: options.paths
|
|
655
|
+
paths: options.paths,
|
|
656
|
+
pathPatterns: options.pathPatterns
|
|
633
657
|
});
|
|
634
658
|
if (outputPath === absolutePath && isDeepStrictEqual(result, config.data)) {
|
|
635
659
|
print('No plaintext values required encryption. No files were modified.');
|
|
@@ -651,7 +675,8 @@ export async function runCli(argv = process.argv) {
|
|
|
651
675
|
const result = processConfig(config.data, {
|
|
652
676
|
mode: 'decrypt',
|
|
653
677
|
key,
|
|
654
|
-
paths: options.paths
|
|
678
|
+
paths: options.paths,
|
|
679
|
+
pathPatterns: options.pathPatterns
|
|
655
680
|
});
|
|
656
681
|
handleWrite({
|
|
657
682
|
dryRun: options.dryRun,
|
|
@@ -671,8 +696,10 @@ export async function runCli(argv = process.argv) {
|
|
|
671
696
|
const structuredCode = typeof error.code === 'string' && error.code.startsWith('ERR_')
|
|
672
697
|
? error.code
|
|
673
698
|
: null;
|
|
699
|
+
const isMigrationCode = structuredCode === 'ERR_INVALID_PATH_PATTERNS' ||
|
|
700
|
+
structuredCode?.startsWith('ERR_MIGRATION_');
|
|
674
701
|
const code = command === 'migrate'
|
|
675
|
-
?
|
|
702
|
+
? isMigrationCode
|
|
676
703
|
? structuredCode
|
|
677
704
|
: 'ERR_MIGRATION_FAILED'
|
|
678
705
|
: structuredCode ?? 'ERR_PROCESS_FAILED';
|
package/dist/errors.js
CHANGED
|
@@ -15,6 +15,7 @@ export const YAMLOCK_ERROR_CODES = Object.freeze({
|
|
|
15
15
|
INVALID_NON_STRING_POLICY: 'ERR_INVALID_NON_STRING_POLICY',
|
|
16
16
|
INVALID_OPTIONS: 'ERR_INVALID_OPTIONS',
|
|
17
17
|
INVALID_PATH_SEGMENTS: 'ERR_INVALID_PATH_SEGMENTS',
|
|
18
|
+
INVALID_PATH_PATTERNS: 'ERR_INVALID_PATH_PATTERNS',
|
|
18
19
|
INVALID_PATH_SERIALIZER: 'ERR_INVALID_PATH_SERIALIZER',
|
|
19
20
|
INVALID_PATHS: 'ERR_INVALID_PATHS',
|
|
20
21
|
INVALID_PAYLOAD: 'ERR_INVALID_PAYLOAD',
|
package/dist/index.d.ts
CHANGED
|
@@ -25,6 +25,7 @@ export interface ProcessConfigCommonOptions {
|
|
|
25
25
|
nonStringPolicy?: YamlockNonStringPolicy;
|
|
26
26
|
pathSerializer?: (segments: YamlockPathSegment[]) => string;
|
|
27
27
|
paths?: string[];
|
|
28
|
+
pathPatterns?: string[];
|
|
28
29
|
parentPath?: YamlockPathSegment[];
|
|
29
30
|
}
|
|
30
31
|
|
|
@@ -57,6 +58,7 @@ export const YAMLOCK_ERROR_CODES: Readonly<{
|
|
|
57
58
|
INVALID_NON_STRING_POLICY: 'ERR_INVALID_NON_STRING_POLICY';
|
|
58
59
|
INVALID_OPTIONS: 'ERR_INVALID_OPTIONS';
|
|
59
60
|
INVALID_PATH_SEGMENTS: 'ERR_INVALID_PATH_SEGMENTS';
|
|
61
|
+
INVALID_PATH_PATTERNS: 'ERR_INVALID_PATH_PATTERNS';
|
|
60
62
|
INVALID_PATH_SERIALIZER: 'ERR_INVALID_PATH_SERIALIZER';
|
|
61
63
|
INVALID_PATHS: 'ERR_INVALID_PATHS';
|
|
62
64
|
INVALID_PAYLOAD: 'ERR_INVALID_PAYLOAD';
|
package/dist/utils/config.js
CHANGED
|
@@ -4,6 +4,11 @@ import { detectPayloadVersion } from '../crypto/payload-v2.js';
|
|
|
4
4
|
import { isYamlockPayload } from '../crypto/utils.js';
|
|
5
5
|
import { YamlockConfigError } from '../errors.js';
|
|
6
6
|
import { serializeLegacyPath, serializePath } from './path.js';
|
|
7
|
+
import {
|
|
8
|
+
compilePathPatterns,
|
|
9
|
+
matchesAnyPathPattern,
|
|
10
|
+
PathPatternSyntaxError
|
|
11
|
+
} from './path-pattern.js';
|
|
7
12
|
|
|
8
13
|
const MODES = {
|
|
9
14
|
ENCRYPT: 'encrypt',
|
|
@@ -13,8 +18,11 @@ const MODES = {
|
|
|
13
18
|
const NON_STRING_POLICIES = new Set(['ignore', 'stringify', 'error']);
|
|
14
19
|
const EXISTING_PAYLOAD_POLICIES = new Set(['preserve', 'error', 'encrypt']);
|
|
15
20
|
|
|
16
|
-
function createConfigError(code, message) {
|
|
17
|
-
return new YamlockConfigError(message, {
|
|
21
|
+
function createConfigError(code, message, cause) {
|
|
22
|
+
return new YamlockConfigError(message, {
|
|
23
|
+
code,
|
|
24
|
+
...(cause === undefined ? {} : { cause })
|
|
25
|
+
});
|
|
18
26
|
}
|
|
19
27
|
|
|
20
28
|
function isConfigContainer(value) {
|
|
@@ -83,6 +91,22 @@ function normalizePaths(paths) {
|
|
|
83
91
|
return new Set(normalized);
|
|
84
92
|
}
|
|
85
93
|
|
|
94
|
+
function normalizePathPatterns(pathPatterns) {
|
|
95
|
+
try {
|
|
96
|
+
return compilePathPatterns(pathPatterns);
|
|
97
|
+
} catch (error) {
|
|
98
|
+
if (!(error instanceof PathPatternSyntaxError)) {
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
throw createConfigError(
|
|
103
|
+
'ERR_INVALID_PATH_PATTERNS',
|
|
104
|
+
error.message,
|
|
105
|
+
error
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
86
110
|
function resolveCurrentPaths(segments, pathSerializer) {
|
|
87
111
|
let currentPath;
|
|
88
112
|
try {
|
|
@@ -150,6 +174,7 @@ function stringifyConfigLeaf(value, currentPath) {
|
|
|
150
174
|
* @param {"preserve"|"error"|"encrypt"} [options.existingPayloadPolicy]
|
|
151
175
|
* @param {(segments: Array<string|number>) => string} [options.pathSerializer]
|
|
152
176
|
* @param {string[]} [options.paths]
|
|
177
|
+
* @param {string[]} [options.pathPatterns]
|
|
153
178
|
* @param {Array<string|number>} [options.parentPath]
|
|
154
179
|
* @returns {Object|Array}
|
|
155
180
|
*/
|
|
@@ -195,6 +220,13 @@ export function processConfig(node, options) {
|
|
|
195
220
|
const parentPath = options.parentPath ?? [];
|
|
196
221
|
validatePathSegments(parentPath, 'parentPath');
|
|
197
222
|
const normalizedPaths = normalizePaths(options.paths);
|
|
223
|
+
const compiledPathPatterns = normalizePathPatterns(options.pathPatterns);
|
|
224
|
+
if (compiledPathPatterns.length > 0 && options.pathSerializer !== undefined) {
|
|
225
|
+
throw createConfigError(
|
|
226
|
+
'ERR_INVALID_PATH_PATTERNS',
|
|
227
|
+
'pathPatterns cannot be combined with pathSerializer.'
|
|
228
|
+
);
|
|
229
|
+
}
|
|
198
230
|
const existingPayloadPolicy = options.existingPayloadPolicy ?? 'preserve';
|
|
199
231
|
if (!EXISTING_PAYLOAD_POLICIES.has(existingPayloadPolicy)) {
|
|
200
232
|
throw createConfigError(
|
|
@@ -208,6 +240,7 @@ export function processConfig(node, options) {
|
|
|
208
240
|
mode,
|
|
209
241
|
parentPath,
|
|
210
242
|
normalizedPaths,
|
|
243
|
+
compiledPathPatterns,
|
|
211
244
|
nonStringPolicy,
|
|
212
245
|
existingPayloadPolicy,
|
|
213
246
|
pathSerializer: options.pathSerializer,
|
|
@@ -216,7 +249,7 @@ export function processConfig(node, options) {
|
|
|
216
249
|
});
|
|
217
250
|
}
|
|
218
251
|
|
|
219
|
-
function traverseConfig(node, { mode, key, algorithm, algorithmOptions, formatVersion, parentPath, normalizedPaths, nonStringPolicy, existingPayloadPolicy, pathSerializer, ancestors, seenPaths }) {
|
|
252
|
+
function traverseConfig(node, { mode, key, algorithm, algorithmOptions, formatVersion, parentPath, normalizedPaths, compiledPathPatterns, nonStringPolicy, existingPayloadPolicy, pathSerializer, ancestors, seenPaths }) {
|
|
220
253
|
const isArrayNode = Array.isArray(node);
|
|
221
254
|
const result = createResultContainer(node);
|
|
222
255
|
const selectedCryptoOptions = algorithmOptions ?? algorithm;
|
|
@@ -249,6 +282,7 @@ function traverseConfig(node, { mode, key, algorithm, algorithmOptions, formatVe
|
|
|
249
282
|
algorithmOptions: cryptoOptions,
|
|
250
283
|
parentPath: pathSegments,
|
|
251
284
|
normalizedPaths,
|
|
285
|
+
compiledPathPatterns,
|
|
252
286
|
nonStringPolicy,
|
|
253
287
|
existingPayloadPolicy,
|
|
254
288
|
pathSerializer,
|
|
@@ -266,7 +300,10 @@ function traverseConfig(node, { mode, key, algorithm, algorithmOptions, formatVe
|
|
|
266
300
|
}
|
|
267
301
|
seenPaths.add(currentPath);
|
|
268
302
|
|
|
269
|
-
const
|
|
303
|
+
const hasSelectors = normalizedPaths !== null || compiledPathPatterns.length > 0;
|
|
304
|
+
const shouldProcess = !hasSelectors ||
|
|
305
|
+
normalizedPaths?.has(currentPath) ||
|
|
306
|
+
matchesAnyPathPattern(compiledPathPatterns, pathSegments);
|
|
270
307
|
if (!shouldProcess) {
|
|
271
308
|
setResultValue(result, targetKey, originalValue);
|
|
272
309
|
continue;
|
package/dist/utils/migrate.js
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
} from '../crypto/payload-v2.js';
|
|
7
7
|
import { isYamlockPayload } from '../crypto/utils.js';
|
|
8
8
|
import { serializeLegacyPath, serializePath } from './path.js';
|
|
9
|
+
import { compilePathPatterns, matchesAnyPathPattern } from './path-pattern.js';
|
|
9
10
|
|
|
10
11
|
function createMigrationError(code, message) {
|
|
11
12
|
const error = new Error(message);
|
|
@@ -59,7 +60,10 @@ function traverse(node, context) {
|
|
|
59
60
|
return;
|
|
60
61
|
}
|
|
61
62
|
|
|
62
|
-
const
|
|
63
|
+
const hasSelectors = context.paths !== null || context.pathPatterns.length > 0;
|
|
64
|
+
const selected = !hasSelectors ||
|
|
65
|
+
context.paths?.has(currentPath) ||
|
|
66
|
+
matchesAnyPathPattern(context.pathPatterns, pathSegments);
|
|
63
67
|
if (!selected) {
|
|
64
68
|
setResultValue(result, targetKey, value);
|
|
65
69
|
return;
|
|
@@ -77,7 +81,7 @@ function traverse(node, context) {
|
|
|
77
81
|
if (!isYamlockPayload(value)) {
|
|
78
82
|
throw createMigrationError(
|
|
79
83
|
'ERR_MIGRATION_PLAINTEXT',
|
|
80
|
-
`Selected value at ${currentPath} is not encrypted; narrow
|
|
84
|
+
`Selected value at ${currentPath} is not encrypted; narrow the path selectors to legacy payloads.`
|
|
81
85
|
);
|
|
82
86
|
}
|
|
83
87
|
|
|
@@ -124,6 +128,7 @@ function traverse(node, context) {
|
|
|
124
128
|
* @param {Object} options
|
|
125
129
|
* @param {string|Buffer} options.key
|
|
126
130
|
* @param {string[]} [options.paths]
|
|
131
|
+
* @param {string[]} [options.pathPatterns]
|
|
127
132
|
* @param {boolean} [options.allowMixed=false]
|
|
128
133
|
* @returns {{ data: Object|Array, changed: boolean, stats: { selected: number, migrated: number, preservedV2: number } }}
|
|
129
134
|
*/
|
|
@@ -150,6 +155,7 @@ export function migrateConfig(node, options) {
|
|
|
150
155
|
const data = traverse(node, {
|
|
151
156
|
key: options.key,
|
|
152
157
|
paths: normalizePaths(options.paths),
|
|
158
|
+
pathPatterns: compilePathPatterns(options.pathPatterns),
|
|
153
159
|
allowMixed: options.allowMixed === true,
|
|
154
160
|
parentPath: [],
|
|
155
161
|
stats
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { YAMLOCK_ERROR_CODES } from '../errors.js';
|
|
2
|
+
|
|
3
|
+
const COMPILED_PATTERN = Symbol('yamlock.compiledPathPattern');
|
|
4
|
+
const ALLOWED_ESCAPES = new Set(['\\', '.', '[', ']', ',', '*']);
|
|
5
|
+
const EMPTY_COMPILED_PATTERNS = Object.freeze([]);
|
|
6
|
+
|
|
7
|
+
const TOKEN_TYPES = Object.freeze({
|
|
8
|
+
ARRAY_INDEX: 'array-index',
|
|
9
|
+
ARRAY_WILDCARD: 'array-wildcard',
|
|
10
|
+
GLOBSTAR: 'globstar',
|
|
11
|
+
OBJECT_KEY: 'object-key',
|
|
12
|
+
OBJECT_WILDCARD: 'object-wildcard'
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export class PathPatternSyntaxError extends Error {
|
|
16
|
+
constructor(message, offset = 0) {
|
|
17
|
+
super(`Invalid path pattern at offset ${offset}: ${message}`);
|
|
18
|
+
this.name = 'PathPatternSyntaxError';
|
|
19
|
+
this.code = YAMLOCK_ERROR_CODES.INVALID_PATH_PATTERNS;
|
|
20
|
+
this.offset = offset;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function syntaxError(message, offset) {
|
|
25
|
+
throw new PathPatternSyntaxError(message, offset);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function freezeToken(type, value) {
|
|
29
|
+
return Object.freeze(value === undefined ? { type } : { type, value });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function parseObjectToken(source, start) {
|
|
33
|
+
let index = start;
|
|
34
|
+
let value = '';
|
|
35
|
+
let hasUnescapedWildcard = false;
|
|
36
|
+
|
|
37
|
+
while (index < source.length) {
|
|
38
|
+
const character = source[index];
|
|
39
|
+
if (character === '.' || character === '[') {
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (character === '\\') {
|
|
44
|
+
const escaped = source[index + 1];
|
|
45
|
+
if (escaped === undefined) {
|
|
46
|
+
syntaxError('A trailing backslash is not allowed.', index);
|
|
47
|
+
}
|
|
48
|
+
if (!ALLOWED_ESCAPES.has(escaped)) {
|
|
49
|
+
syntaxError(`Unsupported escape \\${escaped}.`, index);
|
|
50
|
+
}
|
|
51
|
+
value += escaped;
|
|
52
|
+
index += 2;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (character === ']') {
|
|
57
|
+
syntaxError('Unexpected closing bracket.', index);
|
|
58
|
+
}
|
|
59
|
+
if (character === ',') {
|
|
60
|
+
syntaxError('Literal commas must be escaped.', index);
|
|
61
|
+
}
|
|
62
|
+
if (character === '*') {
|
|
63
|
+
hasUnescapedWildcard = true;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
value += character;
|
|
67
|
+
index += 1;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (index === start) {
|
|
71
|
+
syntaxError('Object path segments must not be empty.', start);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const rawSegment = source.slice(start, index);
|
|
75
|
+
if (hasUnescapedWildcard) {
|
|
76
|
+
if (rawSegment === '*') {
|
|
77
|
+
return {
|
|
78
|
+
nextIndex: index,
|
|
79
|
+
token: freezeToken(TOKEN_TYPES.OBJECT_WILDCARD)
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
if (rawSegment === '**') {
|
|
83
|
+
return {
|
|
84
|
+
nextIndex: index,
|
|
85
|
+
token: freezeToken(TOKEN_TYPES.GLOBSTAR)
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
syntaxError('Wildcards must occupy a complete object segment.', start);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
nextIndex: index,
|
|
93
|
+
token: freezeToken(TOKEN_TYPES.OBJECT_KEY, value)
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function parseArrayToken(source, start) {
|
|
98
|
+
const closingBracket = source.indexOf(']', start + 1);
|
|
99
|
+
if (closingBracket === -1) {
|
|
100
|
+
syntaxError('Array segments require a closing bracket.', start);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const content = source.slice(start + 1, closingBracket);
|
|
104
|
+
if (content === '*') {
|
|
105
|
+
return {
|
|
106
|
+
nextIndex: closingBracket + 1,
|
|
107
|
+
token: freezeToken(TOKEN_TYPES.ARRAY_WILDCARD)
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!/^(?:0|[1-9]\d*)$/u.test(content)) {
|
|
112
|
+
syntaxError('Array indexes must be canonical non-negative integers or *.', start);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const value = Number(content);
|
|
116
|
+
if (!Number.isSafeInteger(value)) {
|
|
117
|
+
syntaxError('Array indexes must be safe integers.', start);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
nextIndex: closingBracket + 1,
|
|
122
|
+
token: freezeToken(TOKEN_TYPES.ARRAY_INDEX, value)
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function compileNormalizedPattern(source) {
|
|
127
|
+
const tokens = [];
|
|
128
|
+
let index = 0;
|
|
129
|
+
|
|
130
|
+
while (index < source.length) {
|
|
131
|
+
const parsed = source[index] === '['
|
|
132
|
+
? parseArrayToken(source, index)
|
|
133
|
+
: parseObjectToken(source, index);
|
|
134
|
+
tokens.push(parsed.token);
|
|
135
|
+
index = parsed.nextIndex;
|
|
136
|
+
|
|
137
|
+
if (index === source.length) {
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (source[index] === '[') {
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (source[index] !== '.') {
|
|
146
|
+
syntaxError(`Unexpected character ${source[index]}.`, index);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
index += 1;
|
|
150
|
+
if (index === source.length) {
|
|
151
|
+
syntaxError('A pattern must not end with a dot.', index - 1);
|
|
152
|
+
}
|
|
153
|
+
if (source[index] === '[') {
|
|
154
|
+
syntaxError('Array segments must not follow a dot.', index);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const compiled = {
|
|
159
|
+
source,
|
|
160
|
+
tokens: Object.freeze(tokens)
|
|
161
|
+
};
|
|
162
|
+
Object.defineProperty(compiled, COMPILED_PATTERN, { value: true });
|
|
163
|
+
return Object.freeze(compiled);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function compilePathPattern(pattern) {
|
|
167
|
+
if (typeof pattern !== 'string') {
|
|
168
|
+
syntaxError('A pattern must be a string.', 0);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const source = pattern.trim();
|
|
172
|
+
if (source.length === 0) {
|
|
173
|
+
syntaxError('A pattern must not be empty.', 0);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return compileNormalizedPattern(source);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function compilePathPatterns(patterns) {
|
|
180
|
+
if (patterns === undefined || (Array.isArray(patterns) && patterns.length === 0)) {
|
|
181
|
+
return EMPTY_COMPILED_PATTERNS;
|
|
182
|
+
}
|
|
183
|
+
if (!Array.isArray(patterns)) {
|
|
184
|
+
syntaxError('pathPatterns must be an array of non-empty strings.', 0);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const compiledBySource = new Map();
|
|
188
|
+
for (const pattern of patterns) {
|
|
189
|
+
const compiled = compilePathPattern(pattern);
|
|
190
|
+
compiledBySource.set(compiled.source, compiled);
|
|
191
|
+
}
|
|
192
|
+
return Object.freeze([...compiledBySource.values()]);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function validatePathSegments(segments) {
|
|
196
|
+
if (!Array.isArray(segments)) {
|
|
197
|
+
throw new TypeError('Path segments must be an array.');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const invalid = segments.some((segment) => (
|
|
201
|
+
(typeof segment !== 'string' || segment.length === 0) &&
|
|
202
|
+
(!Number.isInteger(segment) || segment < 0)
|
|
203
|
+
));
|
|
204
|
+
if (invalid) {
|
|
205
|
+
throw new TypeError('Path segments must be non-empty strings or non-negative integers.');
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function tokenMatchesSegment(token, segment) {
|
|
210
|
+
switch (token.type) {
|
|
211
|
+
case TOKEN_TYPES.OBJECT_KEY:
|
|
212
|
+
return typeof segment === 'string' && segment === token.value;
|
|
213
|
+
case TOKEN_TYPES.ARRAY_INDEX:
|
|
214
|
+
return typeof segment === 'number' && segment === token.value;
|
|
215
|
+
case TOKEN_TYPES.OBJECT_WILDCARD:
|
|
216
|
+
return typeof segment === 'string';
|
|
217
|
+
case TOKEN_TYPES.ARRAY_WILDCARD:
|
|
218
|
+
return typeof segment === 'number';
|
|
219
|
+
default:
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function matchesPathPattern(compiledPattern, segments) {
|
|
225
|
+
if (!compiledPattern?.[COMPILED_PATTERN]) {
|
|
226
|
+
throw new TypeError('matchesPathPattern requires a compiled path pattern.');
|
|
227
|
+
}
|
|
228
|
+
validatePathSegments(segments);
|
|
229
|
+
|
|
230
|
+
let previous = new Array(segments.length + 1).fill(false);
|
|
231
|
+
previous[0] = true;
|
|
232
|
+
|
|
233
|
+
for (const token of compiledPattern.tokens) {
|
|
234
|
+
const current = new Array(segments.length + 1).fill(false);
|
|
235
|
+
if (token.type === TOKEN_TYPES.GLOBSTAR) {
|
|
236
|
+
current[0] = previous[0];
|
|
237
|
+
for (let index = 1; index <= segments.length; index += 1) {
|
|
238
|
+
current[index] = previous[index] || current[index - 1];
|
|
239
|
+
}
|
|
240
|
+
} else {
|
|
241
|
+
for (let index = 1; index <= segments.length; index += 1) {
|
|
242
|
+
current[index] = previous[index - 1] && tokenMatchesSegment(
|
|
243
|
+
token,
|
|
244
|
+
segments[index - 1]
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
previous = current;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return previous[segments.length];
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function matchesAnyPathPattern(compiledPatterns, segments) {
|
|
255
|
+
if (!Array.isArray(compiledPatterns)) {
|
|
256
|
+
throw new TypeError('matchesAnyPathPattern requires an array of compiled patterns.');
|
|
257
|
+
}
|
|
258
|
+
validatePathSegments(segments);
|
|
259
|
+
return compiledPatterns.some((pattern) => matchesPathPattern(pattern, segments));
|
|
260
|
+
}
|
package/docs/api.md
CHANGED
|
@@ -45,12 +45,19 @@ options object with `algorithm`, `keyLength`, `ivLength`, and `authTagLength`.
|
|
|
45
45
|
The algorithm stored in a payload is authoritative during decryption; sizing
|
|
46
46
|
overrides exist only for low-level legacy compatibility.
|
|
47
47
|
|
|
48
|
-
`processConfig` additionally accepts exact `paths`, `
|
|
49
|
-
`pathSerializer`, `nonStringPolicy`, and encrypt-only
|
|
48
|
+
`processConfig` additionally accepts exact `paths`, structural `pathPatterns`,
|
|
49
|
+
`parentPath`, a custom `pathSerializer`, `nonStringPolicy`, and encrypt-only
|
|
50
50
|
`existingPayloadPolicy`. It returns a new config container and does not mutate
|
|
51
51
|
the input. With `nonStringPolicy: 'stringify'`, selected finite JSON primitives
|
|
52
52
|
may become strings, so the TypeScript return type is intentionally widened.
|
|
53
53
|
|
|
54
|
+
`paths` remains exact throughout the `1.x` line. `pathPatterns` is a separate
|
|
55
|
+
selector list with whole-segment `*`, `[*]`, and `**` wildcards. Exact paths
|
|
56
|
+
and patterns form a union, while payload authentication always uses the exact
|
|
57
|
+
leaf path rather than pattern text. `pathPatterns` cannot be combined with a
|
|
58
|
+
custom `pathSerializer`. See the [path pattern design](design/path-patterns.md)
|
|
59
|
+
for the grammar and compatibility rules.
|
|
60
|
+
|
|
54
61
|
See the [Node.js error contract](errors.md) and the
|
|
55
62
|
[payload v2 design](design/payload-v2.md) for the security and serialization
|
|
56
63
|
details.
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# Path pattern design
|
|
2
|
+
|
|
3
|
+
Status: implemented on `dev` for a future `1.x` minor release; release and
|
|
4
|
+
hosted-CI verification remain pending.
|
|
5
|
+
|
|
6
|
+
## Goals
|
|
7
|
+
|
|
8
|
+
- Select a config subtree without listing every leaf.
|
|
9
|
+
- Select repeated object fields and array elements with structural wildcards.
|
|
10
|
+
- Keep the existing exact `paths` contract fully backward compatible.
|
|
11
|
+
- Keep selector syntax separate from the exact field path authenticated by a
|
|
12
|
+
payload.
|
|
13
|
+
- Share the same selection behavior across `processConfig` and the CLI
|
|
14
|
+
`encrypt`, `decrypt`, and `migrate` commands.
|
|
15
|
+
- Reject malformed or ambiguous patterns before reading or modifying a config
|
|
16
|
+
file.
|
|
17
|
+
|
|
18
|
+
## Non-goals
|
|
19
|
+
|
|
20
|
+
- Changing `serializePath`, payload metadata, key derivation, or authenticated
|
|
21
|
+
field paths.
|
|
22
|
+
- Treating existing `paths` strings as globs.
|
|
23
|
+
- Regular expressions, partial-segment wildcards, character classes, braces,
|
|
24
|
+
negation, or exclusion rules.
|
|
25
|
+
- Selecting containers as values. yamlock continues to process leaves only.
|
|
26
|
+
- Pattern matching against a custom `pathSerializer` in the first release.
|
|
27
|
+
|
|
28
|
+
## Compatibility decision
|
|
29
|
+
|
|
30
|
+
The Node.js API adds a separate `pathPatterns?: string[]` option. The CLI adds
|
|
31
|
+
`--path-patterns <pattern1,pattern2>`. Existing `paths` and `--paths`
|
|
32
|
+
remain exact selectors, including strings containing literal `*` characters.
|
|
33
|
+
|
|
34
|
+
Exact selectors and patterns form a union: a leaf is selected when either its
|
|
35
|
+
exact serialized path is present in `paths` or its structural segments match a
|
|
36
|
+
pattern. When both selector arrays are omitted or empty, all leaves remain
|
|
37
|
+
selected as they are today. Duplicate and overlapping selectors process a leaf
|
|
38
|
+
only once.
|
|
39
|
+
|
|
40
|
+
Patterns operate on the original string/array path segments. They never become
|
|
41
|
+
the field path passed to `encryptValue` or `decryptValue`; encryption and
|
|
42
|
+
authentication continue to use the exact canonical path, or the exact output
|
|
43
|
+
of `pathSerializer` where supported. This prevents a broad selector such as
|
|
44
|
+
`db.**` from weakening field-path binding.
|
|
45
|
+
|
|
46
|
+
`pathPatterns` and `pathSerializer` are mutually exclusive. Exact
|
|
47
|
+
`paths` remain available with a custom serializer. Failing closed avoids an
|
|
48
|
+
ambiguous API where pattern syntax appears to use a serializer but actually
|
|
49
|
+
matches a different structural representation.
|
|
50
|
+
|
|
51
|
+
## Pattern grammar
|
|
52
|
+
|
|
53
|
+
Patterns use the existing dot/bracket structure plus three whole-segment
|
|
54
|
+
wildcards:
|
|
55
|
+
|
|
56
|
+
| Syntax | Meaning |
|
|
57
|
+
| --- | --- |
|
|
58
|
+
| `name` | One exact object-key segment |
|
|
59
|
+
| `[0]` | One exact array-index segment |
|
|
60
|
+
| `*` | Any one object-key segment |
|
|
61
|
+
| `[*]` | Any one array-index segment |
|
|
62
|
+
| `**` | Zero or more object-key or array-index segments |
|
|
63
|
+
|
|
64
|
+
Examples:
|
|
65
|
+
|
|
66
|
+
| Pattern | Matches | Does not match |
|
|
67
|
+
| --- | --- | --- |
|
|
68
|
+
| `db.**` | `db.password`, `db.replica.password`, a leaf at `db` | `database.password` |
|
|
69
|
+
| `services.*.token` | `services.api.token` | `services[0].token` |
|
|
70
|
+
| `users[*].token` | `users[0].token`, `users[12].token` | `users.admin.token` |
|
|
71
|
+
| `**.token` | `token`, `api.token`, `users[0].token` | `token.value` |
|
|
72
|
+
| `matrix[*][*].secret` | `matrix[0][1].secret` | `matrix.primary.secret` |
|
|
73
|
+
|
|
74
|
+
`*` and `**` are special only when they occupy a complete object segment.
|
|
75
|
+
`[*]` is special only as a complete array segment. Partial globs such as
|
|
76
|
+
`service-*.token` are invalid rather than being interpreted differently by
|
|
77
|
+
different callers.
|
|
78
|
+
|
|
79
|
+
## Escaping
|
|
80
|
+
|
|
81
|
+
Pattern literals retain the canonical escaping rules for `\\`, `.`, `[`, `]`,
|
|
82
|
+
and `,`. In addition, `\*` represents a literal asterisk inside an object key.
|
|
83
|
+
Examples:
|
|
84
|
+
|
|
85
|
+
| Pattern | Selected structural path |
|
|
86
|
+
| --- | --- |
|
|
87
|
+
| `a\.b.**` | descendants of the literal root key `a.b` |
|
|
88
|
+
| `labels\,primary` | the literal root key `labels,primary` |
|
|
89
|
+
| `\*` | the literal root key `*` |
|
|
90
|
+
| `features.\*\.enabled` | the literal key `*.enabled` below `features` |
|
|
91
|
+
| `items\[\*\]` | the literal object key `items[*]` |
|
|
92
|
+
|
|
93
|
+
A trailing backslash, an unsupported escape, an empty segment, malformed
|
|
94
|
+
brackets, a negative/non-integer array index, or a wildcard embedded in a
|
|
95
|
+
literal segment is invalid. CLI comma splitting continues to preserve escaped
|
|
96
|
+
commas before the shared pattern parser validates each item.
|
|
97
|
+
|
|
98
|
+
This escaping affects selectors only. It does not add `*` to the reserved
|
|
99
|
+
characters used by `serializePath`, so existing payload paths and exact
|
|
100
|
+
selectors do not change.
|
|
101
|
+
|
|
102
|
+
## Matching semantics
|
|
103
|
+
|
|
104
|
+
- Patterns match complete leaf paths, not string prefixes.
|
|
105
|
+
- `**` may consume zero, one, or many structural segments, including array
|
|
106
|
+
indexes.
|
|
107
|
+
- `db` selects a leaf exactly at `db`; it does not select descendants. Use
|
|
108
|
+
`db.**` for the subtree.
|
|
109
|
+
- `*` never matches an array index, and `[*]` never matches an object key.
|
|
110
|
+
- A pattern can contain more than one `**`; matching must remain deterministic.
|
|
111
|
+
- Empty containers and sparse-array holes contain no leaves and therefore do
|
|
112
|
+
not produce matches.
|
|
113
|
+
- `parentPath` participates in the full structural path before matching.
|
|
114
|
+
- Pattern order does not affect the result.
|
|
115
|
+
|
|
116
|
+
Patterns must be parsed into structural tokens once before traversal. Matching
|
|
117
|
+
must use a bounded dynamic-programming or equivalent token algorithm rather
|
|
118
|
+
than converting user input into a backtracking regular expression. A malformed
|
|
119
|
+
pattern must fail before traversal, crypto work, or file access.
|
|
120
|
+
|
|
121
|
+
## API and CLI
|
|
122
|
+
|
|
123
|
+
Node.js API:
|
|
124
|
+
|
|
125
|
+
```js
|
|
126
|
+
processConfig(config, {
|
|
127
|
+
mode: 'encrypt',
|
|
128
|
+
key,
|
|
129
|
+
paths: ['root.literal'],
|
|
130
|
+
pathPatterns: ['db.**', 'users[*].token']
|
|
131
|
+
});
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
CLI:
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
yamlock encrypt config.yaml \
|
|
138
|
+
--key "$YAMLOCK_KEY" \
|
|
139
|
+
--paths 'root.literal' \
|
|
140
|
+
--path-patterns 'db.**,users[*].token'
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
The CLI option applies to `encrypt`, `decrypt`, and `migrate`. It requires at
|
|
144
|
+
least one non-empty pattern. Invalid syntax uses the structured
|
|
145
|
+
`[yamlock:ERR_INVALID_PATH_PATTERNS]` error and exit code `1` before the input
|
|
146
|
+
file is read. The Node.js API adds the same stable error code through
|
|
147
|
+
`YamlockConfigError`.
|
|
148
|
+
|
|
149
|
+
## Interaction with existing behavior
|
|
150
|
+
|
|
151
|
+
### Encryption
|
|
152
|
+
|
|
153
|
+
Each matched plaintext leaf is encrypted with its exact field path. Existing
|
|
154
|
+
payload handling still follows `existingPayloadPolicy`; overlapping patterns do
|
|
155
|
+
not create nested encryption layers.
|
|
156
|
+
|
|
157
|
+
### Decryption
|
|
158
|
+
|
|
159
|
+
Every matched leaf must satisfy the same payload, key, and exact field-path
|
|
160
|
+
checks as an exact selection. A broad pattern does not silently skip plaintext
|
|
161
|
+
or malformed values.
|
|
162
|
+
|
|
163
|
+
### Migration
|
|
164
|
+
|
|
165
|
+
Every matched leaf follows the existing fail-closed migration rules. Plaintext
|
|
166
|
+
fails, selected v2 payloads require `--allow-mixed`, and legacy payloads migrate
|
|
167
|
+
to v2 only after the complete selection validates. Pattern overlap does not
|
|
168
|
+
inflate migration statistics.
|
|
169
|
+
|
|
170
|
+
### Non-string values
|
|
171
|
+
|
|
172
|
+
Pattern selection occurs before `nonStringPolicy`, matching the current exact
|
|
173
|
+
selector behavior. Selected values are ignored, stringified, or rejected by
|
|
174
|
+
the configured policy; unselected values remain unchanged.
|
|
175
|
+
|
|
176
|
+
### YAML
|
|
177
|
+
|
|
178
|
+
Patterns operate on the resolved object/array structure returned by `js-yaml`.
|
|
179
|
+
Anchors, aliases, and merge keys therefore follow their independent resolved
|
|
180
|
+
paths, consistent with the existing YAML rewrite contract.
|
|
181
|
+
|
|
182
|
+
## Required tests
|
|
183
|
+
|
|
184
|
+
- Existing exact `paths` tests remain unchanged, including literal `*` keys.
|
|
185
|
+
- Exact object keys and array indexes stay distinct.
|
|
186
|
+
- `*`, `[*]`, and `**` cover zero/one/many segment matches and mixed nesting.
|
|
187
|
+
- Escaped dots, brackets, commas, backslashes, and literal asterisks match only
|
|
188
|
+
their intended object keys.
|
|
189
|
+
- Malformed patterns fail before traversal and before CLI file reads.
|
|
190
|
+
- `parentPath` is included; `pathSerializer` plus patterns fails explicitly.
|
|
191
|
+
- Exact selectors and patterns form a union; duplicates and overlap process a
|
|
192
|
+
leaf once.
|
|
193
|
+
- Sparse arrays, empty containers, Unicode keys, null-prototype objects, and
|
|
194
|
+
own `__proto__` keys retain their current behavior.
|
|
195
|
+
- Encrypt/decrypt round trips authenticate exact leaf paths rather than pattern
|
|
196
|
+
text.
|
|
197
|
+
- Wrong keys, wrong paths, malformed payloads, plaintext decrypt selections,
|
|
198
|
+
and unsafe migration selections still fail closed.
|
|
199
|
+
- CLI integration tests cover JSON and YAML, escaped comma splitting, dry-run,
|
|
200
|
+
separate output, migration, and no-write failure cases.
|
|
201
|
+
- TypeScript declarations, public error codes, documentation, package smoke,
|
|
202
|
+
and the installed CLI expose the same contract.
|
|
203
|
+
|
|
204
|
+
## Delivery sequence
|
|
205
|
+
|
|
206
|
+
1. Implement and unit-test the pattern tokenizer/compiler and matcher without
|
|
207
|
+
connecting it to config traversal.
|
|
208
|
+
2. Add `pathPatterns` to `processConfig`, types, and stable errors while keeping
|
|
209
|
+
exact `paths` regression tests frozen.
|
|
210
|
+
3. Add shared CLI validation plus `--path-patterns` for encrypt/decrypt.
|
|
211
|
+
4. Add migration support and integration tests for atomic/no-write failures.
|
|
212
|
+
5. Update public documentation and package smoke, then release the completed
|
|
213
|
+
additive feature in a minor version after full CI.
|
package/docs/errors.md
CHANGED
|
@@ -53,6 +53,7 @@ repeat string literals.
|
|
|
53
53
|
| `ERR_INVALID_FIELD_PATH` | The caller supplied an invalid field path. |
|
|
54
54
|
| `ERR_INVALID_OPTIONS` | Crypto options have an invalid shape or unsupported override. |
|
|
55
55
|
| `ERR_INVALID_MODE` | `processConfig` received an unknown mode. |
|
|
56
|
+
| `ERR_INVALID_PATH_PATTERNS` | A path pattern list or pattern syntax is invalid. |
|
|
56
57
|
| `ERR_UNSUPPORTED_ALGORITHM` | The requested writer algorithm is unavailable or unsupported. |
|
|
57
58
|
| `ERR_UNSUPPORTED_PAYLOAD_VERSION` | The payload or requested writer version is unsupported. |
|
|
58
59
|
| `ERR_INVALID_PAYLOAD` | The payload is missing or malformed. |
|
|
@@ -63,7 +64,7 @@ repeat string literals.
|
|
|
63
64
|
| `ERR_DECRYPTION_FAILED` | A legacy payload could not be decrypted. |
|
|
64
65
|
|
|
65
66
|
`processConfig` also preserves its specific codes for invalid roots/options,
|
|
66
|
-
policies, path serializers, path lists, circular input, path collisions,
|
|
67
|
+
policies, path serializers, exact path lists, path patterns, circular input, path collisions,
|
|
67
68
|
non-string values, unsupported values, and already encrypted values. These are
|
|
68
69
|
available through `YAMLOCK_ERROR_CODES` and use `YamlockConfigError`.
|
|
69
70
|
|
package/docs/yaml-behavior.md
CHANGED
|
@@ -39,8 +39,9 @@ unchanged.
|
|
|
39
39
|
|
|
40
40
|
1. Keep source YAML under version control or make a verified backup.
|
|
41
41
|
2. Run with `--dry-run` to inspect the complete serialized result.
|
|
42
|
-
3. Use `--paths` to select every resolved
|
|
43
|
-
|
|
42
|
+
3. Use exact `--paths` or structural `--path-patterns` to select every resolved
|
|
43
|
+
path that should be encrypted, including values originally introduced
|
|
44
|
+
through aliases or merge keys.
|
|
44
45
|
4. Use `--output` when the source document's comments or formatting must remain
|
|
45
46
|
untouched.
|
|
46
47
|
5. Do not use unknown application-specific YAML tags in files processed by the
|
package/package.json
CHANGED