yamlock 0.2.5 → 0.2.9

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
@@ -4,6 +4,9 @@
4
4
  ░░▀░░▀░▀░▀░░░▀░▀▀▀░▀▀▀░▀▀▀░▀░▀░
5
5
  ```
6
6
 
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)
9
+
7
10
  # yamlock
8
11
 
9
12
  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.
@@ -64,6 +67,7 @@ The CLI detects YAML (`.yaml`/`.yml`) and JSON extensions automatically and writ
64
67
  Options of note:
65
68
  - `--output <file>` writes the result to a separate file instead of overwriting the input.
66
69
  - `--paths <path1,path2>` targets only the specified fields (dot/bracket notation like `db.password` or `users[0].token`).
70
+ - `--dry-run` shows the would-be changes without modifying files (prints original vs new content).
67
71
  - Command `keygen` produces a random key and shows how to store it (shell export or `.env`).
68
72
  - Command `algorithms` prints two lists: tested presets (covered by yamlock) and additional ciphers available from the runtime.
69
73
  - Command `version` prints the installed CLI version.
@@ -115,6 +119,12 @@ const restored = processConfig(processed, {
115
119
  });
116
120
  ```
117
121
 
122
+ ## Advanced usage
123
+
124
+ - **Selective encryption**: combine `--paths` on the CLI or `paths: []` in `processConfig` to encrypt only sensitive sections of a config file.
125
+ - **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.
126
+ - **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.
127
+
118
128
  ### Supported algorithms
119
129
 
120
130
  | Algorithm | Type | Notes |
@@ -126,6 +136,11 @@ const restored = processConfig(processed, {
126
136
 
127
137
  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.
128
138
 
139
+ ## Release information
140
+
141
+ - The badges at the top show the latest npm version and the status of the Node test suite.
142
+ - See [CHANGELOG.md](CHANGELOG.md) for detailed release notes; install a specific tag via `npm install yamlock@<version>`.
143
+
129
144
  ### Encrypted value format
130
145
 
131
146
  Every locked string follows the format:
package/dist/cli/cli.js CHANGED
@@ -37,6 +37,7 @@ Options:
37
37
  -a, --algorithm <value> Cipher algorithm (default: aes-256-cbc).
38
38
  -o, --output <file> Write the result to a different file (otherwise overwrites the input file).
39
39
  -p, --paths <p1,p2> Comma-separated list of field paths to process (dot/bracket notation).
40
+ -d, --dry-run Show the diff without modifying files.
40
41
  --length <bytes> (keygen) Number of random bytes to generate (default: 32).
41
42
  --format <hex|base64> (keygen) Output format (default: base64).
42
43
  `;
@@ -64,21 +65,22 @@ function readConfigFile(filePath) {
64
65
  const format = detectFormat(filePath);
65
66
 
66
67
  if (format === 'yaml') {
67
- return { format, data: yaml.load(content) ?? {} };
68
+ return { format, data: yaml.load(content) ?? {}, raw: content };
68
69
  }
69
70
 
70
- return { format, data: JSON.parse(content) };
71
+ return { format, data: JSON.parse(content), raw: content };
71
72
  }
72
73
 
73
- function writeConfigFile(filePath, format, data) {
74
+ function serializeConfig(format, data) {
74
75
  if (format === 'yaml') {
75
- const serialized = yaml.dump(data, { lineWidth: 120 });
76
- writeFileSync(filePath, serialized, 'utf8');
77
- return;
76
+ return yaml.dump(data, { lineWidth: 120 });
78
77
  }
79
78
 
80
- const serialized = JSON.stringify(data, null, 2);
81
- writeFileSync(filePath, `${serialized}\n`, 'utf8');
79
+ return `${JSON.stringify(data, null, 2)}\n`;
80
+ }
81
+
82
+ function writeConfigFile(filePath, serialized) {
83
+ writeFileSync(filePath, serialized, 'utf8');
82
84
  }
83
85
 
84
86
  function parsePaths(value) {
@@ -97,7 +99,7 @@ function parseArgs(argv) {
97
99
  const result = {
98
100
  command: args[0],
99
101
  file: undefined,
100
- options: {}
102
+ options: { dryRun: false }
101
103
  };
102
104
 
103
105
  let index = 1;
@@ -131,9 +133,15 @@ function parseArgs(argv) {
131
133
  } else if (arg === '--format') {
132
134
  result.options.format = next;
133
135
  i += 1;
136
+ } else if (arg === '-d' || arg === '--dry-run') {
137
+ result.options.dryRun = true;
134
138
  }
135
139
  }
136
140
 
141
+ if (result.options.dryRun && result.command && !result.file && !['version', 'algorithms', 'keygen'].includes(result.command)) {
142
+ // dry-run without file is invalid, but we'll let later validation handle file requirement
143
+ }
144
+
137
145
  return result;
138
146
  }
139
147
 
@@ -146,6 +154,30 @@ function generateRandomKey(length, format) {
146
154
  return buffer.toString('base64');
147
155
  }
148
156
 
157
+ function fail(code, message) {
158
+ printError(`[yamlock:${code}] ${message}`);
159
+ return exit(1);
160
+ }
161
+
162
+ function handleWrite({ dryRun, file, outputPath, format, originalRaw, data, operation }) {
163
+ const serialized = serializeConfig(format, data);
164
+ if (dryRun) {
165
+ print(`DRY-RUN (${operation}) ${file}`);
166
+ print('--- original');
167
+ print((originalRaw ?? '').trimEnd());
168
+ print('+++ result');
169
+ print(serialized.trimEnd());
170
+ if (outputPath !== file) {
171
+ print(`(would write to ${outputPath})`);
172
+ }
173
+ print('No files were modified.');
174
+ return;
175
+ }
176
+
177
+ writeConfigFile(outputPath, serialized);
178
+ print(`${operation === 'encrypt' ? 'Encrypted' : 'Decrypted'} values in ${outputPath}`);
179
+ }
180
+
149
181
  export async function runCli(argv = process.argv) {
150
182
  const { command, file, options } = parseArgs(argv);
151
183
 
@@ -181,13 +213,11 @@ export async function runCli(argv = process.argv) {
181
213
  const normalizedFormat = (options.format ?? 'base64').toLowerCase();
182
214
 
183
215
  if (!Number.isFinite(desiredLength) || desiredLength <= 0) {
184
- printError('Key length must be a positive number.');
185
- return exit(1);
216
+ return fail('ERR_INVALID_LENGTH', 'Key length must be a positive number.');
186
217
  }
187
218
 
188
219
  if (!['base64', 'hex'].includes(normalizedFormat)) {
189
- printError('Key format must be either "base64" or "hex".');
190
- return exit(1);
220
+ return fail('ERR_INVALID_FORMAT', 'Key format must be either "base64" or "hex".');
191
221
  }
192
222
 
193
223
  const keyValue = generateRandomKey(desiredLength, normalizedFormat);
@@ -200,15 +230,13 @@ export async function runCli(argv = process.argv) {
200
230
  }
201
231
 
202
232
  if (!file) {
203
- printError('A file path is required for this command.');
204
233
  print(getHelpText().trim());
205
- return exit(1);
234
+ return fail('ERR_FILE_REQUIRED', 'A file path is required for this command.');
206
235
  }
207
236
 
208
237
  const key = options.key ?? process.env.YAMLOCK_KEY;
209
238
  if (!key) {
210
- printError('Encryption key is required via --key or YAMLOCK_KEY.');
211
- return exit(1);
239
+ return fail('ERR_MISSING_KEY', 'Encryption key is required via --key or YAMLOCK_KEY.');
212
240
  }
213
241
 
214
242
  const absolutePath = resolve(process.cwd(), file);
@@ -216,8 +244,7 @@ export async function runCli(argv = process.argv) {
216
244
  try {
217
245
  config = readConfigFile(absolutePath);
218
246
  } catch (error) {
219
- printError(`Failed to read config file: ${error.message}`);
220
- return exit(1);
247
+ return fail('ERR_READ_FAILED', `Failed to read config file: ${error.message}`);
221
248
  }
222
249
 
223
250
  const outputPath = options.output
@@ -232,8 +259,15 @@ export async function runCli(argv = process.argv) {
232
259
  algorithm: options.algorithm,
233
260
  paths: options.paths
234
261
  });
235
- writeConfigFile(outputPath, config.format, result);
236
- print(`Encrypted values in ${outputPath === absolutePath ? file : options.output}`);
262
+ handleWrite({
263
+ dryRun: options.dryRun,
264
+ file,
265
+ outputPath,
266
+ format: config.format,
267
+ originalRaw: config.raw,
268
+ data: result,
269
+ operation: 'encrypt'
270
+ });
237
271
  return exit(0);
238
272
  }
239
273
 
@@ -244,17 +278,22 @@ export async function runCli(argv = process.argv) {
244
278
  algorithm: options.algorithm,
245
279
  paths: options.paths
246
280
  });
247
- writeConfigFile(outputPath, config.format, result);
248
- print(`Decrypted values in ${outputPath === absolutePath ? file : options.output}`);
281
+ handleWrite({
282
+ dryRun: options.dryRun,
283
+ file,
284
+ outputPath,
285
+ format: config.format,
286
+ originalRaw: config.raw,
287
+ data: result,
288
+ operation: 'decrypt'
289
+ });
249
290
  return exit(0);
250
291
  }
251
292
 
252
- printError(`Unknown command: ${command}`);
253
293
  print(getHelpText().trim());
254
- return exit(1);
294
+ return fail('ERR_UNKNOWN_COMMAND', `Unknown command: ${command}`);
255
295
  } catch (error) {
256
- printError(`Operation failed: ${error.message}`);
257
- return exit(1);
296
+ return fail('ERR_PROCESS_FAILED', `Operation failed: ${error.message}`);
258
297
  }
259
298
  }
260
299
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yamlock",
3
- "version": "0.2.5",
3
+ "version": "0.2.9",
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",
package/dist/cli/.gitkeep DELETED
File without changes
File without changes
File without changes