yamlock 0.2.1 → 0.2.4

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
@@ -47,10 +47,26 @@ YAMLOCK_KEY="super-secret" yamlock encrypt config.yaml
47
47
 
48
48
  # Decrypt values in place using explicit key/algorithm flags
49
49
  yamlock decrypt settings.json --key "super-secret" --algorithm aes-256-cbc
50
+
51
+ # Encrypt only selected fields into a new file
52
+ yamlock encrypt config.json --key "$YAMLOCK_KEY" --paths "db.password,api.token" --output config.secure.json
53
+
54
+ # Inspect CLI metadata
55
+ yamlock version
56
+ yamlock algorithms
57
+
58
+ # Generate a random key for YAMLOCK_KEY
59
+ yamlock keygen --length 64 --format base64
50
60
  ```
51
61
 
52
62
  The CLI detects YAML (`.yaml`/`.yml`) and JSON extensions automatically and writes the file back in the same format.
53
63
 
64
+ Options of note:
65
+ - `--output <file>` writes the result to a separate file instead of overwriting the input.
66
+ - `--paths <path1,path2>` targets only the specified fields (dot/bracket notation like `db.password` or `users[0].token`).
67
+ - Command `keygen` produces a random key and shows how to store it (shell export or `.env`).
68
+ - Commands `version` and `algorithms` print the installed CLI version and the list of supported ciphers respectively.
69
+
54
70
  ### Node.js API
55
71
 
56
72
  ```js
package/dist/cli/cli.js CHANGED
@@ -2,27 +2,45 @@
2
2
  import { readFileSync, writeFileSync } from 'node:fs';
3
3
  import { resolve, extname } from 'node:path';
4
4
  import { exit } from 'node:process';
5
+ import { createRequire } from 'node:module';
6
+ import { randomBytes } from 'node:crypto';
5
7
 
6
8
  import yaml from 'js-yaml';
7
9
 
8
10
  import { processConfig } from '../utils/config.js';
11
+ import { listSupportedAlgorithms } from '../crypto/utils.js';
9
12
 
10
- const HELP_TEXT = `
13
+ const require = createRequire(import.meta.url);
14
+ const packageJson = require('../../package.json');
15
+
16
+ const BANNER = `
11
17
  ░█░█░█▀█░█▄░▄█░█░░░█▀█░█▀▀░█░█░
12
18
  ░░█░░█▀█░█░▀░█░█░░░█░█░█░░░█▀▄░
13
- ░░▀░░▀░▀░▀░░░▀░▀▀▀░▀▀▀░▀▀▀░▀░▀░
19
+ ░░▀░░▀░▀░▀░░░▀░▀▀▀░▀▀▀░▀▀▀░▀░▀░`;
20
+
21
+ function getHelpText() {
22
+ return `${BANNER}
23
+ Version: ${packageJson.version}
14
24
 
15
25
  Usage:
16
26
  yamlock <command> [options]
17
27
 
18
28
  Commands:
19
- encrypt <file> Encrypt string values in the given YAML/JSON file.
20
- decrypt <file> Decrypt string values in the given YAML/JSON file.
29
+ encrypt <file> Encrypt string values in the given YAML/JSON file.
30
+ decrypt <file> Decrypt string values in the given YAML/JSON file.
31
+ version Print the yamlock CLI version.
32
+ algorithms Print the list of supported cipher algorithms.
33
+ keygen Generate a random YAMLOCK_KEY.
21
34
 
22
35
  Options:
23
36
  -k, --key <value> Encryption key (or use YAMLOCK_KEY env).
24
37
  -a, --algorithm <value> Cipher algorithm (default: aes-256-cbc).
38
+ -o, --output <file> Write the result to a different file (otherwise overwrites the input file).
39
+ -p, --paths <p1,p2> Comma-separated list of field paths to process (dot/bracket notation).
40
+ --length <bytes> (keygen) Number of random bytes to generate (default: 32).
41
+ --format <hex|base64> (keygen) Output format (default: base64).
25
42
  `;
43
+ }
26
44
 
27
45
  function print(message) {
28
46
  console.log(message);
@@ -63,15 +81,35 @@ function writeConfigFile(filePath, format, data) {
63
81
  writeFileSync(filePath, `${serialized}\n`, 'utf8');
64
82
  }
65
83
 
84
+ function parsePaths(value) {
85
+ if (!value) {
86
+ return [];
87
+ }
88
+
89
+ return String(value)
90
+ .split(',')
91
+ .map((segment) => segment.trim())
92
+ .filter((segment) => segment.length > 0);
93
+ }
94
+
66
95
  function parseArgs(argv) {
67
96
  const args = argv.slice(2);
68
97
  const result = {
69
98
  command: args[0],
70
- file: args[1],
99
+ file: undefined,
71
100
  options: {}
72
101
  };
73
102
 
74
- for (let i = 2; i < args.length; i += 1) {
103
+ let index = 1;
104
+ const potentialFile = args[1];
105
+ if (potentialFile && !potentialFile.startsWith('-')) {
106
+ result.file = potentialFile;
107
+ index = 2;
108
+ } else {
109
+ index = 1;
110
+ }
111
+
112
+ for (let i = index; i < args.length; i += 1) {
75
113
  const arg = args[i];
76
114
  const next = args[i + 1];
77
115
 
@@ -81,17 +119,79 @@ function parseArgs(argv) {
81
119
  } else if (arg === '-a' || arg === '--algorithm') {
82
120
  result.options.algorithm = next;
83
121
  i += 1;
122
+ } else if (arg === '-o' || arg === '--output') {
123
+ result.options.output = next;
124
+ i += 1;
125
+ } else if (arg === '-p' || arg === '--paths') {
126
+ result.options.paths = parsePaths(next);
127
+ i += 1;
128
+ } else if (arg === '--length') {
129
+ result.options.length = next;
130
+ i += 1;
131
+ } else if (arg === '--format') {
132
+ result.options.format = next;
133
+ i += 1;
84
134
  }
85
135
  }
86
136
 
87
137
  return result;
88
138
  }
89
139
 
140
+ function generateRandomKey(length, format) {
141
+ const size = Number.isFinite(length) && length > 0 ? Math.floor(length) : 32;
142
+ const buffer = randomBytes(size);
143
+ if (format === 'hex') {
144
+ return buffer.toString('hex');
145
+ }
146
+ return buffer.toString('base64');
147
+ }
148
+
90
149
  export async function runCli(argv = process.argv) {
91
150
  const { command, file, options } = parseArgs(argv);
92
151
 
93
- if (!command || !file) {
94
- print(HELP_TEXT.trim());
152
+ if (!command) {
153
+ print(getHelpText().trim());
154
+ return exit(1);
155
+ }
156
+
157
+ if (command === 'version') {
158
+ print(`yamlock ${packageJson.version}`);
159
+ return exit(0);
160
+ }
161
+
162
+ if (command === 'algorithms') {
163
+ const algorithms = listSupportedAlgorithms();
164
+ print('Supported algorithms:');
165
+ algorithms.forEach((name) => print(`- ${name}`));
166
+ return exit(0);
167
+ }
168
+
169
+ if (command === 'keygen') {
170
+ const desiredLength = options.length ? Number(options.length) : 32;
171
+ const normalizedFormat = (options.format ?? 'base64').toLowerCase();
172
+
173
+ if (!Number.isFinite(desiredLength) || desiredLength <= 0) {
174
+ printError('Key length must be a positive number.');
175
+ return exit(1);
176
+ }
177
+
178
+ if (!['base64', 'hex'].includes(normalizedFormat)) {
179
+ printError('Key format must be either "base64" or "hex".');
180
+ return exit(1);
181
+ }
182
+
183
+ const keyValue = generateRandomKey(desiredLength, normalizedFormat);
184
+ print(`Generated key (${normalizedFormat}, ${Math.floor(desiredLength)} bytes of entropy):`);
185
+ print(keyValue);
186
+ print('\nStore it securely, e.g.');
187
+ print(` export YAMLOCK_KEY="${keyValue}"`);
188
+ print(' # or place in an .env file as YAMLOCK_KEY=your-key');
189
+ return exit(0);
190
+ }
191
+
192
+ if (!file) {
193
+ printError('A file path is required for this command.');
194
+ print(getHelpText().trim());
95
195
  return exit(1);
96
196
  }
97
197
 
@@ -110,23 +210,37 @@ export async function runCli(argv = process.argv) {
110
210
  return exit(1);
111
211
  }
112
212
 
213
+ const outputPath = options.output
214
+ ? resolve(process.cwd(), options.output)
215
+ : absolutePath;
216
+
113
217
  try {
114
218
  if (command === 'encrypt') {
115
- const result = processConfig(config.data, { mode: 'encrypt', key, algorithm: options.algorithm });
116
- writeConfigFile(absolutePath, config.format, result);
117
- print(`Encrypted values in ${file}`);
219
+ const result = processConfig(config.data, {
220
+ mode: 'encrypt',
221
+ key,
222
+ algorithm: options.algorithm,
223
+ paths: options.paths
224
+ });
225
+ writeConfigFile(outputPath, config.format, result);
226
+ print(`Encrypted values in ${outputPath === absolutePath ? file : options.output}`);
118
227
  return exit(0);
119
228
  }
120
229
 
121
230
  if (command === 'decrypt') {
122
- const result = processConfig(config.data, { mode: 'decrypt', key });
123
- writeConfigFile(absolutePath, config.format, result);
124
- print(`Decrypted values in ${file}`);
231
+ const result = processConfig(config.data, {
232
+ mode: 'decrypt',
233
+ key,
234
+ algorithm: options.algorithm,
235
+ paths: options.paths
236
+ });
237
+ writeConfigFile(outputPath, config.format, result);
238
+ print(`Decrypted values in ${outputPath === absolutePath ? file : options.output}`);
125
239
  return exit(0);
126
240
  }
127
241
 
128
242
  printError(`Unknown command: ${command}`);
129
- print(HELP_TEXT.trim());
243
+ print(getHelpText().trim());
130
244
  return exit(1);
131
245
  } catch (error) {
132
246
  printError(`Operation failed: ${error.message}`);
@@ -15,18 +15,33 @@ const MODES = {
15
15
  * @param {string|Buffer} options.key
16
16
  * @param {string|object} [options.algorithm]
17
17
  * @param {object} [options.algorithmOptions]
18
+ * @param {string[]} [options.paths]
18
19
  * @param {Array<string|number>} [options.parentPath]
19
20
  * @returns {Object|Array}
20
21
  */
21
- export function processConfig(node, { mode, key, algorithm, algorithmOptions, parentPath = [] }) {
22
+ export function processConfig(node, options) {
22
23
  if (typeof node !== 'object' || node === null) {
23
24
  throw new Error('processConfig expects a non-null object or array.');
24
25
  }
25
26
 
27
+ const mode = options.mode;
26
28
  if (mode !== MODES.ENCRYPT && mode !== MODES.DECRYPT) {
27
29
  throw new Error(`Unknown processConfig mode: ${mode}`);
28
30
  }
29
31
 
32
+ const normalizedPaths = Array.isArray(options.paths) && options.paths.length > 0
33
+ ? new Set(options.paths.map((path) => String(path).trim()).filter(Boolean))
34
+ : null;
35
+
36
+ return traverseConfig(node, {
37
+ ...options,
38
+ mode,
39
+ parentPath: options.parentPath ?? [],
40
+ normalizedPaths
41
+ });
42
+ }
43
+
44
+ function traverseConfig(node, { mode, key, algorithm, algorithmOptions, parentPath, normalizedPaths }) {
30
45
  const isArrayNode = Array.isArray(node);
31
46
  const result = isArrayNode ? [] : {};
32
47
  const cryptoOptions = algorithmOptions ?? algorithm;
@@ -37,12 +52,13 @@ export function processConfig(node, { mode, key, algorithm, algorithmOptions, pa
37
52
  const currentPath = buildPath(parentPath, segment);
38
53
 
39
54
  if (value !== null && typeof value === 'object') {
40
- result[targetKey] = processConfig(value, {
55
+ result[targetKey] = traverseConfig(value, {
41
56
  mode,
42
57
  key,
43
58
  algorithm: cryptoOptions,
44
59
  algorithmOptions: cryptoOptions,
45
- parentPath: [...parentPath, segment]
60
+ parentPath: [...parentPath, segment],
61
+ normalizedPaths
46
62
  });
47
63
  return;
48
64
  }
@@ -52,6 +68,12 @@ export function processConfig(node, { mode, key, algorithm, algorithmOptions, pa
52
68
  return;
53
69
  }
54
70
 
71
+ const shouldProcess = !normalizedPaths || normalizedPaths.has(currentPath);
72
+ if (!shouldProcess) {
73
+ result[targetKey] = value;
74
+ return;
75
+ }
76
+
55
77
  if (mode === MODES.ENCRYPT) {
56
78
  result[targetKey] = encryptValue(value, key, currentPath, cryptoOptions);
57
79
  } else {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "yamlock",
3
- "version": "0.2.1",
4
- "author": "PAVEL TKACHEV",
3
+ "version": "0.2.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",
7
7
  "type": "module",