yamlock 0.3.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 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
@@ -5,7 +5,7 @@
5
5
  ```
6
6
 
7
7
  [![npm version](https://img.shields.io/npm/v/yamlock)](https://www.npmjs.com/package/yamlock)
8
- [![Tests](https://img.shields.io/badge/tests-node--test-green)](https://github.com/phoenixweiss/yamlock/actions)
8
+ [![CI](https://github.com/phoenixweiss/yamlock/actions/workflows/ci.yml/badge.svg)](https://github.com/phoenixweiss/yamlock/actions/workflows/ci.yml)
9
9
 
10
10
  # yamlock
11
11
 
@@ -13,8 +13,10 @@ Value-level encryption for YAML and JSON configuration files. The name **yamlock
13
13
 
14
14
  ## Requirements
15
15
 
16
- - Node.js 22.x via `asdf`
17
- - Yarn Classic (1.x)
16
+ - Node.js 22 or newer
17
+
18
+ The package works with npm or another Node.js package manager. This repository
19
+ uses Yarn Classic 1.22.22 for development and release checks.
18
20
 
19
21
  ## Installation
20
22
 
@@ -34,9 +36,11 @@ yarn add yamlock # project dependency
34
36
 
35
37
  ## Features
36
38
 
37
- - Encrypt/decrypt individual configuration values with deterministic field-path salts.
39
+ - Encrypt/decrypt individual values with authenticated field-path metadata.
38
40
  - CLI workflow that processes YAML or JSON files in place.
41
+ - Safe CLI migration from legacy payloads to authenticated v2 payloads.
39
42
  - Recursively lock/unlock entire objects via `processConfig`.
43
+ - Select repeated fields or subtrees with structural path patterns.
40
44
  - Public API exports that mirror CLI behavior for programmatic use.
41
45
  - Focus on Node.js 22+, ESM modules, and a lightweight dependency set (`js-yaml`).
42
46
 
@@ -48,13 +52,17 @@ yarn add yamlock # project dependency
48
52
  # Encrypt values in a YAML file
49
53
  YAMLOCK_KEY="super-secret" yamlock encrypt config.yaml
50
54
 
51
- # Decrypt values in place using explicit key/algorithm flags
52
- yamlock decrypt settings.json --key "super-secret" --algorithm aes-256-cbc
55
+ # Decrypt values in place; the payload determines its format and algorithm
56
+ yamlock decrypt settings.json --key "super-secret"
53
57
 
54
58
  # Encrypt only selected fields into a new file
55
59
  yamlock encrypt config.json --key "$YAMLOCK_KEY" --paths "db.password,api.token" --output config.secure.json
56
60
 
61
+ # Encrypt repeated tokens and an entire subtree
62
+ yamlock encrypt config.json --key "$YAMLOCK_KEY" --path-patterns 'users[*].token,services.**'
63
+
57
64
  # Inspect CLI metadata
65
+ yamlock --help
58
66
  yamlock version
59
67
  yamlock algorithms
60
68
 
@@ -63,39 +71,170 @@ yamlock keygen --length 64 --format base64
63
71
 
64
72
  # Preview changes without touching files
65
73
  yamlock encrypt config.yml -o config.enc.yml -p db.password -k "my-secret-key" -d
74
+
75
+ # Preview a legacy-to-v2 migration without printing config contents
76
+ yamlock migrate config.yml -k "$YAMLOCK_KEY" -p db.password -d
77
+
78
+ # Explicitly write legacy v1 for a temporary compatibility requirement
79
+ yamlock encrypt config.yml -k "$YAMLOCK_KEY" --legacy --algorithm aes-256-cbc
66
80
  ```
67
81
 
68
- The CLI detects YAML (`.yaml`/`.yml`) and JSON extensions automatically and writes the file back in the same format.
82
+ The CLI detects YAML (`.yaml`/`.yml`) and JSON extensions automatically and
83
+ writes the file back in the same format. Normal `encrypt` and `decrypt` writes
84
+ use an fsynced temporary file plus an atomic rename. In-place writes preserve
85
+ the source mode; a new `--output` inherits the source mode, while an existing
86
+ regular output preserves its own mode. Mutating operations reject symbolic-link
87
+ inputs and outputs rather than following or replacing their targets.
88
+
89
+ YAML writes preserve parsed values, not the original syntax tree. Comments,
90
+ anchor/alias syntax, merge keys, explicit tags, quoting, and formatting may be
91
+ removed or normalized. Review [the YAML rewrite contract](docs/yaml-behavior.md)
92
+ and use `--dry-run` or `--output` when presentation details matter.
69
93
 
70
94
  Options of note:
71
95
  - `--output <file>` writes the result to a separate file instead of overwriting the input.
72
- - `--paths <path1,path2>` targets only the specified fields (dot/bracket notation like `db.password` or `users[0].token`).
73
- - `--dry-run` shows the would-be changes without modifying files (prints original vs new content).
74
- - Command `keygen` produces a random key and shows how to store it (shell export or `.env`).
75
- - Command `algorithms` prints two lists: tested presets (covered by yamlock) and additional ciphers available from the runtime.
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.
98
+ - `--dry-run` previews an operation without modifying files; encrypt/decrypt print content changes, while migrate prints only counts and target paths.
99
+ - `migrate` decrypts selected legacy payloads and re-encrypts them as authenticated v2 payloads.
100
+ - `migrate --allow-mixed` additionally authenticates and preserves selected values that are already v2.
101
+ - In-place migration creates `<file>.yamlock.bak` by default; `--no-backup` disables it explicitly.
102
+ - `encrypt --legacy` writes the legacy v1 format; `--algorithm` is accepted for encryption only together with `--legacy`.
103
+ - `encrypt --error-on-encrypted` fails when a selected value is already encrypted instead of preserving it.
104
+ - `encrypt --force-encrypt` explicitly treats selected `yl|...` strings as plaintext and adds another encryption layer.
105
+ - Command `keygen` produces a random key of 1–4096 whole bytes and shows how to store it (shell export or `.env`).
106
+ - Command `algorithms` prints the fixed v2 profile, tested legacy presets, and additional legacy ciphers available from the runtime.
76
107
  - Command `version` prints the installed CLI version.
77
108
 
109
+ The CLI rejects unknown or duplicate options, missing option values, extra
110
+ positional arguments, and options that do not apply to the selected command.
111
+ Argument errors use structured `[yamlock:ERR_*]` codes and are reported before
112
+ the CLI reads or modifies a configuration file.
113
+
114
+ ### Field path syntax
115
+
116
+ Default field paths use dots for object nesting and brackets for array indexes:
117
+ `db.password` and `users[0].token`. Inside an object key, backslashes, dots,
118
+ brackets, and commas are escaped with a backslash. For example,
119
+ `db\.primary.token` selects `token` below the literal key `db.primary`, while
120
+ `db.primary.token` selects three nested object keys. Quote escaped CLI paths so
121
+ the shell passes each backslash unchanged:
122
+
123
+ ```bash
124
+ yamlock encrypt config.json --key "$YAMLOCK_KEY" --paths 'db\.primary.token,labels\,primary'
125
+ ```
126
+
127
+ Node.js callers can build the same canonical strings from unambiguous segments:
128
+
129
+ ```js
130
+ import { processConfig, serializePath } from 'yamlock';
131
+
132
+ const selectedPath = serializePath(['db.primary', 'token']);
133
+ const encrypted = processConfig(config, {
134
+ mode: 'encrypt',
135
+ key: process.env.YAMLOCK_KEY,
136
+ paths: [selectedPath]
137
+ });
138
+ ```
139
+
140
+ New payloads bind authentication to the escaped canonical path. Payloads written
141
+ by older yamlock versions for keys containing reserved characters remain
142
+ readable through the default serializer's compatibility path; selecting those
143
+ keys now requires the canonical escaped spelling. A custom `pathSerializer`
144
+ keeps its own contract and does not use the default compatibility fallback.
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
+
78
173
  ### Node.js API
79
174
 
80
175
  ```js
81
- import { encryptValue, decryptValue, processConfig } from 'yamlock';
176
+ import { encryptValue, decryptValue, processConfig, serializePath } from 'yamlock';
82
177
 
83
178
  const encrypted = encryptValue('swordfish', process.env.YAMLOCK_KEY, 'db.password');
84
179
  const decrypted = decryptValue(encrypted, process.env.YAMLOCK_KEY, 'db.password');
85
180
 
86
181
  const config = { db: { password: 'swordfish' } };
87
- const locked = processConfig(config, { mode: 'encrypt', key: process.env.YAMLOCK_KEY });
88
- const unlocked = processConfig(locked, { mode: 'decrypt', key: process.env.YAMLOCK_KEY });
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
+ });
193
+ ```
194
+
195
+ Expected Node.js API failures extend `YamlockError` and expose stable `ERR_*`
196
+ codes. Specialized classes distinguish validation, payload, authentication,
197
+ legacy decryption, and config-processing failures; `YAMLOCK_ERROR_CODES` avoids
198
+ repeating code strings. See the [Node.js error contract](docs/errors.md).
199
+
200
+ The package includes TypeScript declarations. The supported exports, option
201
+ types, and `1.x` compatibility guarantees are listed in the
202
+ [public Node.js API contract](docs/api.md).
203
+
204
+ Repeated encryption is safe by default. `processConfig` authenticates selected
205
+ existing payloads with the supplied key and field path, preserves them
206
+ unchanged, and encrypts only selected plaintext values. Use
207
+ `existingPayloadPolicy: 'error'` when existing encrypted values should fail the
208
+ operation:
209
+
210
+ ```js
211
+ processConfig(config, {
212
+ mode: 'encrypt',
213
+ key: process.env.YAMLOCK_KEY,
214
+ existingPayloadPolicy: 'error'
215
+ });
89
216
  ```
90
217
 
218
+ Malformed payloads, incorrect keys, and incorrect field paths are never silently
219
+ skipped. Re-running `yamlock encrypt` on a fully encrypted input does not rewrite
220
+ the source file. Use `yamlock migrate` rather than `encrypt` to convert preserved
221
+ legacy values to v2.
222
+
223
+ If plaintext intentionally begins with `yl|`, use
224
+ `existingPayloadPolicy: 'encrypt'` or CLI `--force-encrypt`. This also permits
225
+ deliberate nested encryption, so it should not be enabled in routine workflows;
226
+ each added layer requires a matching decrypt operation.
227
+
91
228
  See `examples/basic.js` for a runnable end-to-end script (`node examples/basic.js`).
92
229
 
93
- ### Algorithm customization
230
+ ### Legacy algorithm customization
94
231
 
95
- Each function accepts either a cipher name or an options object:
232
+ V2 deliberately has no free-form cipher settings. For temporary legacy
233
+ compatibility, select format version 1 and provide the old cipher options:
96
234
 
97
235
  ```js
98
236
  const encrypted = encryptValue('swordfish', KEY, 'db.password', {
237
+ formatVersion: 1,
99
238
  algorithm: 'chacha20-poly1305',
100
239
  ivLength: 12 // override the IV size used during encryption
101
240
  });
@@ -110,6 +249,7 @@ const processed = processConfig(
110
249
  {
111
250
  mode: 'encrypt',
112
251
  key: KEY,
252
+ formatVersion: 1,
113
253
  algorithm: { algorithm: 'aes-192-cbc', ivLength: 24 }
114
254
  }
115
255
  );
@@ -118,6 +258,7 @@ const processed = processConfig(
118
258
  const restored = processConfig(processed, {
119
259
  mode: 'decrypt',
120
260
  key: KEY,
261
+ formatVersion: 1,
121
262
  algorithm: { algorithm: 'aes-192-cbc', ivLength: 24 }
122
263
  });
123
264
 
@@ -126,7 +267,7 @@ const mixedConfig = { db: { password: 'secret', retries: 3 } };
126
267
  const lockedMixed = processConfig(mixedConfig, {
127
268
  mode: 'encrypt',
128
269
  key: KEY,
129
- nonStringPolicy: 'stringify', // stringifies numbers/objects before encrypting
270
+ nonStringPolicy: 'stringify', // stringifies finite numbers, booleans, and null
130
271
  pathSerializer: (segments) => segments.join('/') // custom path naming (db/password instead of dot notation)
131
272
  });
132
273
 
@@ -146,49 +287,135 @@ const lockedUsers = processConfig(
146
287
  );
147
288
  ```
148
289
 
290
+ `processConfig` recursively traverses arrays and plain objects. With the default
291
+ `nonStringPolicy: 'ignore'`, selected non-string and opaque values such as
292
+ `Date`, `undefined`, or `BigInt` are preserved unchanged. Policy `'error'`
293
+ rejects a selected non-string value. Policy `'stringify'` accepts only finite
294
+ numbers, booleans, and `null`; values that native JSON conversion could omit or
295
+ silently change are rejected instead of being encrypted with lost type
296
+ information. Decrypted stringified primitives remain strings.
297
+
298
+ Circular arrays/objects are rejected. A custom `pathSerializer` must return a
299
+ non-empty, unique string for every leaf and the same serializer must be used for
300
+ encryption and decryption. Policies apply only to values selected by `paths`;
301
+ unselected leaf values are preserved.
302
+
303
+ Direct `processConfig` calls preserve empty containers, sparse-array length and
304
+ holes, null-prototype objects, and own keys such as `__proto__`. JSON and YAML
305
+ files retain the values their parsers can represent, but those formats do not
306
+ encode JavaScript sparse-array holes as a distinct portable value.
307
+
308
+ ### Authenticated payload v2 (default)
309
+
310
+ `encryptValue`, `processConfig`, and `yamlock encrypt` write authenticated v2
311
+ payloads by default. `decryptValue` and `processConfig` automatically read both
312
+ v1 and v2.
313
+
314
+ ```js
315
+ const encrypted = encryptValue('swordfish', KEY, 'db.password');
316
+
317
+ const locked = processConfig(
318
+ { db: { password: 'swordfish' } },
319
+ { mode: 'encrypt', key: KEY }
320
+ );
321
+
322
+ // No format or algorithm option is required when decrypting.
323
+ const original = decryptValue(encrypted, KEY, 'db.password');
324
+ const unlocked = processConfig(locked, { mode: 'decrypt', key: KEY });
325
+ ```
326
+
327
+ V2 uses a fixed AES-256-GCM profile, a random 12-byte nonce, a 16-byte
328
+ authentication tag, and scrypt with a separate random KDF salt. The field path
329
+ and security-critical metadata are authenticated. Free-form cipher and size
330
+ overrides are intentionally unavailable for v2.
331
+
332
+ Existing legacy files can be migrated safely with the CLI:
333
+
334
+ ```bash
335
+ # Preview counts and target paths; config contents are not printed.
336
+ yamlock migrate config.yaml --key "$YAMLOCK_KEY" --paths "db.password,api.token" --dry-run
337
+
338
+ # Migrate in place and create config.yaml.yamlock.bak.
339
+ yamlock migrate config.yaml --key "$YAMLOCK_KEY" --paths "db.password,api.token"
340
+
341
+ # Preserve the source and write a new file. Existing outputs are never replaced.
342
+ yamlock migrate config.yaml --key "$YAMLOCK_KEY" --paths "db.password,api.token" --output config.v2.yaml
343
+ ```
344
+
345
+ Migration validates every selected value and builds the complete result before
346
+ writing. Selected plaintext and non-string values are rejected, so use
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
352
+ `config.yaml.yamlock.bak` over `config.yaml`.
353
+
354
+ Legacy AES-CBC payloads have no authentication, so migration can only validate
355
+ their structure, field path, and successful decryption. Backups use the source
356
+ file permissions and match `*.yamlock.bak` in the repository `.gitignore`.
357
+ See [the payload v2 design](docs/design/payload-v2.md) for the format, threat
358
+ model, limits, and staged migration plan. This design and implementation have
359
+ not received a third-party security audit.
360
+
149
361
  ## Advanced usage
150
362
 
151
- - **Selective encryption**: combine `--paths` on the CLI or `paths: []` in `processConfig` to encrypt only sensitive sections of a config file.
152
- - **Non-string handling**: use `nonStringPolicy: 'ignore' | 'stringify' | 'error'` to control how numbers/objects are treated, and `pathSerializer` to change how traversal paths are represented (e.g., `db/password` instead of dot notation).
363
+ - **Selective encryption**: use exact `--paths`/`paths` selectors, structural `--path-patterns`/`pathPatterns`, or their union to encrypt only sensitive fields.
364
+ - **Repeated encryption**: valid selected payloads are authenticated and preserved; add `--error-on-encrypted` or `existingPayloadPolicy: 'error'` for strict workflows.
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).
153
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.
154
367
  - **Key rotation**: follow [examples/docs/key-rotation.md](examples/docs/key-rotation.md) for a step-by-step process, including scripting tips for large repos.
155
368
 
156
369
  ### Supported algorithms
157
370
 
158
- | Algorithm | Type | Notes |
159
- |-----------|------|-------|
160
- | `aes-128-cbc` | Block cipher (CBC) | 128-bit keys, 16-byte IV. Works well for backward-compatibility scenarios. |
161
- | `aes-192-cbc` | Block cipher (CBC) | 192-bit keys, 16-byte IV. Slightly stronger than AES-128 with the same IV requirements. |
162
- | `aes-256-cbc` (default) | Block cipher (CBC) | 256-bit keys, 16-byte IV. Balanced combination of strength and compatibility. |
163
- | `chacha20-poly1305` | AEAD stream cipher | 256-bit keys, 12-byte nonce, 16-byte auth tag. Provides built-in integrity/authentication. |
371
+ | Algorithm | Format | Notes |
372
+ |-----------|--------|-------|
373
+ | `aes-256-gcm` | v2 default | Fixed authenticated profile with scrypt, a 12-byte nonce, and a 16-byte tag. |
374
+ | `aes-128-cbc` | legacy v1 | Compatibility only; ciphertext and metadata are not authenticated. |
375
+ | `aes-192-cbc` | legacy v1 | Compatibility only; ciphertext and metadata are not authenticated. |
376
+ | `aes-256-cbc` | legacy v1 | Compatibility default when `--legacy` is used without `--algorithm`. |
377
+ | `chacha20-poly1305` | legacy v1 | Authenticates ciphertext, but not all serialized metadata protected by v2. |
164
378
 
165
- You can also pass any algorithm supported by the current Node.js runtime (`crypto.getCiphers()`), along with custom `keyLength`, `ivLength`, or `authTagLength` overrides. Only the algorithms above are actively tested; additional presets may be added or revised in future releases.
379
+ Additional algorithms exposed by `crypto.getCiphers()` are available only in
380
+ explicit legacy mode and are not part of the supported v2 profile. Prefer the
381
+ default v2 writer for new data.
166
382
 
167
383
  ## Release information
168
384
 
169
- - The badges at the top show the latest npm version and the status of the Node test suite.
170
- - See [CHANGELOG.md](CHANGELOG.md) for detailed release notes; install a specific tag via `npm install yamlock@<version>`.
385
+ - The badges at the top show the latest npm version and the status of the full CI matrix.
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>`.
389
+ - yamlock versions are bumped with my own release utility,
390
+ [Bumpster](https://github.com/phoenixweiss/Bumpster). It keeps the tracked
391
+ `VERSION` file and `package.json` synchronized while publishing the `dev`,
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.
394
+
395
+ ### Encrypted value formats
396
+
397
+ New values use the authenticated v2 envelope:
171
398
 
172
- ### Encrypted value format
399
+ ```txt
400
+ yl|2|aes-256-gcm|scrypt|32768|8|1|<kdf_salt>|<nonce>|<path>|<ciphertext>|<tag>
401
+ ```
173
402
 
174
- Every locked string follows the format:
403
+ Legacy values remain readable and can still be written explicitly:
175
404
 
176
405
  ```txt
177
406
  yl|<algorithm>|<salt_base64>|<iv_base64>|<data_base64>
178
407
  ```
179
408
 
180
- Where:
181
- - `yl` - format marker prefix
182
- - `<algorithm>` - algorithm name (e.g., aes-256-cbc)
183
- - `<salt_base64>` - Base64-encoded field path
184
- - `<iv_base64>` - Base64-encoded initialization vector
185
- - `<data_base64>` - Base64-encoded encrypted data
409
+ The legacy name `<salt_base64>` is historical: that segment is only the
410
+ Base64-encoded field path and is not a random salt or KDF input.
186
411
 
187
- The salt is derived from the full field path. Moving or renaming the field invalidates the salt, preventing accidental decryption in the wrong location.
412
+ See [the payload v2 design](docs/design/payload-v2.md) for the canonical field
413
+ definitions, limits, compatibility rules, and legacy security limitations.
188
414
 
189
415
  ### Key rotation
190
416
 
191
- See [docs/key-rotation.md](docs/key-rotation.md) for a step-by-step guide to rotating `YAMLOCK_KEY` without losing data.
417
+ See [the key rotation guide](examples/docs/key-rotation.md) for a step-by-step
418
+ workflow that re-encrypts values with v2 and a new `YAMLOCK_KEY`.
192
419
 
193
420
  ## Inspiration and motivation
194
421
 
@@ -211,9 +438,15 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for development workflow, available scrip
211
438
 
212
439
  ## Future work
213
440
 
214
- - Additional cipher presets and stronger default algorithms.
215
- - More CLI/API examples for rotating keys, selective field targeting, and CI automation.
216
- - Configurable behavior for non-string values (skip vs. coerce) and stricter file format validation.
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.
444
+ - An async encryption API with bounded scrypt concurrency for large configs.
445
+ - Stricter file-format validation and preservation rules for advanced YAML features.
446
+
447
+ ## Author
448
+
449
+ Created and maintained by [Pavel Tkachev (@phoenixweiss)](https://github.com/phoenixweiss).
217
450
 
218
451
  ## License
219
452
 
package/bin/yamlock CHANGED
@@ -1,6 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  async function loadCliEntry() {
4
+ if (process.env.YAMLOCK_TEST_SOURCE === '1') {
5
+ const sourceModule = await import('../src/cli/cli.js');
6
+ if (sourceModule.runCli) {
7
+ return sourceModule.runCli;
8
+ }
9
+ throw new Error('Source CLI entry does not export runCli.');
10
+ }
11
+
4
12
  try {
5
13
  const distModule = await import('../dist/cli/cli.js');
6
14
  if (distModule.runCli) {