yamlock 1.0.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -7,6 +7,8 @@
7
7
  [![npm version](https://img.shields.io/npm/v/yamlock)](https://www.npmjs.com/package/yamlock)
8
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
+ [Project website](https://phoenixweiss.github.io/yamlock/)
11
+
10
12
  # yamlock
11
13
 
12
14
  Value-level encryption for YAML and JSON configuration files. The name **yamlock** combines "YAML" and "lock" while also sounding like "warlock", hinting at a little configuration magic.
@@ -40,6 +42,7 @@ yarn add yamlock # project dependency
40
42
  - CLI workflow that processes YAML or JSON files in place.
41
43
  - Safe CLI migration from legacy payloads to authenticated v2 payloads.
42
44
  - Recursively lock/unlock entire objects via `processConfig`.
45
+ - Select repeated fields or subtrees with structural path patterns.
43
46
  - Public API exports that mirror CLI behavior for programmatic use.
44
47
  - Focus on Node.js 22+, ESM modules, and a lightweight dependency set (`js-yaml`).
45
48
 
@@ -57,6 +60,9 @@ yamlock decrypt settings.json --key "super-secret"
57
60
  # Encrypt only selected fields into a new file
58
61
  yamlock encrypt config.json --key "$YAMLOCK_KEY" --paths "db.password,api.token" --output config.secure.json
59
62
 
63
+ # Encrypt repeated tokens and an entire subtree
64
+ yamlock encrypt config.json --key "$YAMLOCK_KEY" --path-patterns 'users[*].token,services.**'
65
+
60
66
  # Inspect CLI metadata
61
67
  yamlock --help
62
68
  yamlock version
@@ -90,6 +96,7 @@ and use `--dry-run` or `--output` when presentation details matter.
90
96
  Options of note:
91
97
  - `--output <file>` writes the result to a separate file instead of overwriting the input.
92
98
  - `--paths <path1,path2>` targets only the specified fields using the [escaped path syntax](#field-path-syntax).
99
+ - `--path-patterns <pattern1,pattern2>` selects structural paths with whole-segment `*`, `[*]`, and `**` wildcards.
93
100
  - `--dry-run` previews an operation without modifying files; encrypt/decrypt print content changes, while migrate prints only counts and target paths.
94
101
  - `migrate` decrypts selected legacy payloads and re-encrypts them as authenticated v2 payloads.
95
102
  - `migrate --allow-mixed` additionally authenticates and preserves selected values that are already v2.
@@ -138,6 +145,33 @@ readable through the default serializer's compatibility path; selecting those
138
145
  keys now requires the canonical escaped spelling. A custom `pathSerializer`
139
146
  keeps its own contract and does not use the default compatibility fallback.
140
147
 
148
+ ### Path patterns
149
+
150
+ Patterns are separate from exact `paths`; using both forms a union. They match
151
+ complete structural leaf paths:
152
+
153
+ - `services.*.token` matches one object-key segment, such as
154
+ `services.api.token`, but not `services[0].token`.
155
+ - `users[*].token` matches array elements such as `users[0].token`.
156
+ - `db.**` matches a leaf at `db` or any descendant below it, including arrays.
157
+
158
+ Wildcards must occupy a complete segment. Partial globs such as `service-*`
159
+ are rejected with `ERR_INVALID_PATH_PATTERNS` before the CLI reads the input
160
+ file. Literal reserved characters use the same escaping as exact paths, and
161
+ `\*` selects a literal asterisk key:
162
+
163
+ ```bash
164
+ yamlock encrypt config.json \
165
+ --key "$YAMLOCK_KEY" \
166
+ --paths 'root.literal' \
167
+ --path-patterns 'services.*.token,users[*].token,labels\,primary'
168
+ ```
169
+
170
+ Patterns only decide which leaves are selected. Encryption, decryption, and
171
+ migration continue to bind every payload to its exact canonical leaf path.
172
+ Node.js callers use `pathPatterns: string[]`; it cannot be combined with a
173
+ custom `pathSerializer`.
174
+
141
175
  ### Node.js API
142
176
 
143
177
  ```js
@@ -147,8 +181,17 @@ const encrypted = encryptValue('swordfish', process.env.YAMLOCK_KEY, 'db.passwor
147
181
  const decrypted = decryptValue(encrypted, process.env.YAMLOCK_KEY, 'db.password');
148
182
 
149
183
  const config = { db: { password: 'swordfish' } };
150
- const locked = processConfig(config, { mode: 'encrypt', key: process.env.YAMLOCK_KEY });
151
- const unlocked = processConfig(locked, { mode: 'decrypt', key: process.env.YAMLOCK_KEY });
184
+ const selectors = { pathPatterns: ['db.**'] };
185
+ const locked = processConfig(config, {
186
+ mode: 'encrypt',
187
+ key: process.env.YAMLOCK_KEY,
188
+ ...selectors
189
+ });
190
+ const unlocked = processConfig(locked, {
191
+ mode: 'decrypt',
192
+ key: process.env.YAMLOCK_KEY,
193
+ ...selectors
194
+ });
152
195
  ```
153
196
 
154
197
  Expected Node.js API failures extend `YamlockError` and expose stable `ERR_*`
@@ -303,10 +346,11 @@ yamlock migrate config.yaml --key "$YAMLOCK_KEY" --paths "db.password,api.token"
303
346
 
304
347
  Migration validates every selected value and builds the complete result before
305
348
  writing. Selected plaintext and non-string values are rejected, so use
306
- `--paths` for partially encrypted configs. Selected v2 values are rejected
307
- unless `--allow-mixed` is set; with that flag they are authenticated and kept
308
- unchanged. In-place writes are atomic, preserve the source file mode, and do
309
- not replace an existing backup. To roll back, verify the backup and then copy
349
+ `--paths` or `--path-patterns` for partially encrypted configs. Selected v2
350
+ values are rejected unless `--allow-mixed` is set; with that flag they are
351
+ authenticated and kept unchanged. In-place writes are atomic, preserve the
352
+ source file mode, and do not replace an existing backup. To roll back, verify
353
+ the backup and then copy
310
354
  `config.yaml.yamlock.bak` over `config.yaml`.
311
355
 
312
356
  Legacy AES-CBC payloads have no authentication, so migration can only validate
@@ -318,7 +362,7 @@ not received a third-party security audit.
318
362
 
319
363
  ## Advanced usage
320
364
 
321
- - **Selective encryption**: combine `--paths` on the CLI or a non-empty `paths: ['db.password']` array in `processConfig` to encrypt only sensitive fields.
365
+ - **Selective encryption**: use exact `--paths`/`paths` selectors, structural `--path-patterns`/`pathPatterns`, or their union to encrypt only sensitive fields.
322
366
  - **Repeated encryption**: valid selected payloads are authenticated and preserved; add `--error-on-encrypted` or `existingPayloadPolicy: 'error'` for strict workflows.
323
367
  - **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
368
  - **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 +385,14 @@ default v2 writer for new data.
341
385
  ## Release information
342
386
 
343
387
  - The badges at the top show the latest npm version and the status of the full CI matrix.
344
- - See [CHANGELOG.md](CHANGELOG.md) for detailed release notes; install a specific tag via `npm install yamlock@<version>`.
388
+ - See [GitHub Releases](https://github.com/phoenixweiss/yamlock/releases) and
389
+ [CHANGELOG.md](CHANGELOG.md) for detailed release notes; install a specific
390
+ version via `npm install yamlock@<version>`.
345
391
  - yamlock versions are bumped with my own release utility,
346
392
  [Bumpster](https://github.com/phoenixweiss/Bumpster). It keeps the tracked
347
393
  `VERSION` file and `package.json` synchronized while publishing the `dev`,
348
- `main`, and `vX.Y.Z` Git refs atomically.
394
+ `main`, and `vX.Y.Z` Git refs atomically. Each stable tag must pass the full
395
+ CI and package preflight before its GitHub Release is published.
349
396
 
350
397
  ### Encrypted value formats
351
398
 
@@ -393,6 +440,9 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for development workflow, available scrip
393
440
 
394
441
  ## Future work
395
442
 
443
+ - Safe subtree and wildcard selectors are specified in the
444
+ [path pattern design proposal](docs/design/path-patterns.md). Existing
445
+ `paths` remain exact; the proposed feature is not implemented yet.
396
446
  - An async encryption API with bounded scrypt concurrency for large configs.
397
447
  - Stricter file-format validation and preservation rules for advanced YAML features.
398
448
 
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(['key', 'output', 'paths', 'dryRun', 'allowMixed', 'noBackup'])],
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 parsePaths(value) {
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 paths = parsePaths(next);
247
- if (paths.length === 0) {
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
- 'ERR_INVALID_OPTION_VALUE',
250
- 'Option --paths requires at least one non-empty field path.'
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
- result.options.paths = paths;
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
- ? structuredCode?.startsWith('ERR_MIGRATION_')
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';
@@ -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, { code });
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 shouldProcess = !normalizedPaths || normalizedPaths.has(currentPath);
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;
@@ -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 selected = !context.paths || context.paths.has(currentPath);
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 --paths to legacy payloads.`
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`, `parentPath`, a custom
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
 
@@ -5,6 +5,9 @@ and serializes that value back to YAML. It does not edit or round-trip the
5
5
  original YAML syntax tree. A write can therefore preserve data while changing
6
6
  or removing presentation details.
7
7
 
8
+ Empty or comment-only files are treated as empty mappings. Multiple YAML
9
+ documents are rejected before processing or writing.
10
+
8
11
  ## What happens during a rewrite
9
12
 
10
13
  | YAML feature | Current behavior |
@@ -39,8 +42,9 @@ unchanged.
39
42
 
40
43
  1. Keep source YAML under version control or make a verified backup.
41
44
  2. Run with `--dry-run` to inspect the complete serialized result.
42
- 3. Use `--paths` to select every resolved path that should be encrypted,
43
- including values originally introduced through aliases or merge keys.
45
+ 3. Use exact `--paths` or structural `--path-patterns` to select every resolved
46
+ path that should be encrypted, including values originally introduced
47
+ through aliases or merge keys.
44
48
  4. Use `--output` when the source document's comments or formatting must remain
45
49
  untouched.
46
50
  5. Do not use unknown application-specific YAML tags in files processed by the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yamlock",
3
- "version": "1.0.0",
3
+ "version": "1.1.1",
4
4
  "author": "PAVEL TKACHEV (phoenixweiss) <mail@phoenixweiss.me>",
5
5
  "description": "Value-level encryption for YAML/JSON configuration files with CLI + Node.js APIs.",
6
6
  "license": "MIT",
@@ -60,11 +60,11 @@
60
60
  "js-yaml": "^4.3.1"
61
61
  },
62
62
  "devDependencies": {
63
- "@eslint/js": "^9.39.5",
63
+ "@eslint/js": "^10.0.1",
64
64
  "@types/node": "^22.0.0",
65
- "eslint": "^9.39.5",
66
- "globals": "^15.0.0",
67
- "rimraf": "^5.0.5",
68
- "typescript": "^5.9.2"
65
+ "eslint": "^10.9.0",
66
+ "globals": "^17.11.0",
67
+ "rimraf": "^6.1.3",
68
+ "typescript": "^7.0.2"
69
69
  }
70
70
  }