yamlock 0.1.1 → 0.2.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/README.md CHANGED
@@ -13,27 +13,29 @@ Value-level encryption for YAML and JSON configuration files. The name **yamlock
13
13
  - Node.js 22.x via `asdf`
14
14
  - Yarn Classic (1.x)
15
15
 
16
- ## Current status
16
+ ## Installation
17
17
 
18
- - Package metadata, linting config, and MIT license are in place.
19
- - Core crypto helpers (`encryptValue`, `decryptValue`, and supporting utils) work with per-field salts and have unit tests.
20
- - `processConfig` can walk nested objects/arrays and apply encryption/decryption to every string value.
21
- - Public API exports (`encryptValue`, `decryptValue`, `processConfig`, `getSupportedAlgorithms`) are wired and verified by tests.
22
- - Directory structure for source, CLI, tests, and examples exists.
23
- - CLI binary can encrypt/decrypt YAML and JSON files by calling `processConfig`.
18
+ ### npm
24
19
 
25
- ## Working locally
20
+ ```bash
21
+ npm install -g yamlock # CLI usage
22
+ npm install yamlock # project dependency
23
+ ```
24
+
25
+ ### Yarn Classic
26
26
 
27
- 1. Install the toolchain: `asdf install nodejs 22` and `yarn set version classic` if needed.
28
- 2. Install dependencies with `yarn install`.
29
- 3. Use the scripts below during development.
27
+ ```bash
28
+ yarn global add yamlock # CLI usage
29
+ yarn add yamlock # project dependency
30
+ ```
30
31
 
31
- ## Available scripts
32
+ ## Features
32
33
 
33
- - `yarn build` copies the current `src` tree into `dist` (temporary until a real build pipeline appears).
34
- - `yarn prepare` invokes `yarn build` automatically when installing from git.
35
- - `yarn test` runs Node built-in test runner.
36
- - `yarn lint` executes ESLint with the provided config.
34
+ - Encrypt/decrypt individual configuration values with deterministic field-path salts.
35
+ - CLI workflow that processes YAML or JSON files in place.
36
+ - Recursively lock/unlock entire objects via `processConfig`.
37
+ - Public API exports that mirror CLI behavior for programmatic use.
38
+ - Focus on Node.js 22+, ESM modules, and a lightweight dependency set (`js-yaml`).
37
39
 
38
40
  ## Usage
39
41
 
@@ -64,6 +66,49 @@ const unlocked = processConfig(locked, { mode: 'decrypt', key: process.env.YAMLO
64
66
 
65
67
  See `examples/basic.js` for a runnable end-to-end script (`node examples/basic.js`).
66
68
 
69
+ ### Algorithm customization
70
+
71
+ Each function accepts either a cipher name or an options object:
72
+
73
+ ```js
74
+ const encrypted = encryptValue('swordfish', KEY, 'db.password', {
75
+ algorithm: 'chacha20-poly1305',
76
+ ivLength: 12 // override the IV size used during encryption
77
+ });
78
+
79
+ // When decrypting, the algorithm is inferred from the payload,
80
+ // but you can still override key/IV sizes if the cipher requires it.
81
+ const decrypted = decryptValue(encrypted, KEY, 'db.password', /* optional overrides */);
82
+
83
+ // processConfig propagates the same options through every nested field.
84
+ const processed = processConfig(
85
+ { db: { password: 'swordfish' }, api: { token: 'secret' } },
86
+ {
87
+ mode: 'encrypt',
88
+ key: KEY,
89
+ algorithm: { algorithm: 'aes-192-cbc', ivLength: 24 }
90
+ }
91
+ );
92
+
93
+ // Later you can decrypt with the same options:
94
+ const restored = processConfig(processed, {
95
+ mode: 'decrypt',
96
+ key: KEY,
97
+ algorithm: { algorithm: 'aes-192-cbc', ivLength: 24 }
98
+ });
99
+ ```
100
+
101
+ ### Supported algorithms
102
+
103
+ | Algorithm | Type | Notes |
104
+ |-----------|------|-------|
105
+ | `aes-128-cbc` | Block cipher (CBC) | 128-bit keys, 16-byte IV. Works well for backward-compatibility scenarios. |
106
+ | `aes-192-cbc` | Block cipher (CBC) | 192-bit keys, 16-byte IV. Slightly stronger than AES-128 with the same IV requirements. |
107
+ | `aes-256-cbc` (default) | Block cipher (CBC) | 256-bit keys, 16-byte IV. Balanced combination of strength and compatibility. |
108
+ | `chacha20-poly1305` | AEAD stream cipher | 256-bit keys, 12-byte nonce, 16-byte auth tag. Provides built-in integrity/authentication. |
109
+
110
+ 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.
111
+
67
112
  ### Encrypted value format
68
113
 
69
114
  Every locked string follows the format:
@@ -72,25 +117,19 @@ Every locked string follows the format:
72
117
  yl|<algorithm>|<salt_base64>|<iv_base64>|<data_base64>
73
118
  ```
74
119
 
75
- where the salt is derived from the full field path. Moving or renaming the field invalidates the salt, preventing accidental decryption in the wrong location.
120
+ Where:
121
+ - `yl` - format marker prefix
122
+ - `<algorithm>` - algorithm name (e.g., aes-256-cbc)
123
+ - `<salt_base64>` - Base64-encoded field path
124
+ - `<iv_base64>` - Base64-encoded initialization vector
125
+ - `<data_base64>` - Base64-encoded encrypted data
126
+
127
+ The salt is derived from the full field path. Moving or renaming the field invalidates the salt, preventing accidental decryption in the wrong location.
76
128
 
77
129
  ### Key rotation
78
130
 
79
131
  See [docs/key-rotation.md](docs/key-rotation.md) for a step-by-step guide to rotating `YAMLOCK_KEY` without losing data.
80
132
 
81
- ## Project structure
82
-
83
- ```txt
84
- yamlock/
85
- ├── src/ # Source files (API, CLI, utilities)
86
- ├── dist/ # Build output created by `yarn build`
87
- ├── bin/ # CLI entry point (loads dist/cli/cli.js)
88
- ├── test/ # Unit and integration suites
89
- ├── examples/ # Usage demos (TBD)
90
- ├── CHANGELOG.md # Step-by-step release history
91
- └── README.md / LICENSE
92
- ```
93
-
94
133
  ## Inspiration and motivation
95
134
 
96
135
  I have worked with Ruby on Rails apps for more than ten years and appreciated how its secret management evolved between 4.2 and 6.x. That flow influenced **yamlock**, but I also explored modern tools such as:
@@ -104,12 +143,18 @@ Each of those projects solves secure config storage differently, yet none fit my
104
143
 
105
144
  ## Contributing
106
145
 
107
- See [CONTRIBUTING.md](CONTRIBUTING.md) for development workflow, coding standards, and release instructions.
146
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for development workflow, available scripts, and release instructions.
108
147
 
109
148
  ## Exit codes
110
149
 
111
150
  `yamlock` returns `0` when encryption/decryption completes successfully and `1` on validation or runtime errors (missing keys, malformed payloads, failed file reads). Use these exit codes to gate CI jobs or deployment steps.
112
151
 
152
+ ## Future work
153
+
154
+ - Additional cipher presets and stronger default algorithms.
155
+ - More CLI/API examples for rotating keys, selective field targeting, and CI automation.
156
+ - Configurable behavior for non-string values (skip vs. coerce) and stricter file format validation.
157
+
113
158
  ## License
114
159
 
115
160
  MIT © PAVEL TKACHEV
package/bin/yamlock CHANGED
@@ -1,6 +1,34 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import('../dist/cli/cli.js').catch((error) => {
4
- console.error('[yamlock] CLI failed to start:', error);
5
- process.exit(1);
6
- });
3
+ async function loadCliEntry() {
4
+ try {
5
+ const distModule = await import('../dist/cli/cli.js');
6
+ if (distModule.runCli) {
7
+ return distModule.runCli;
8
+ }
9
+ } catch (error) {
10
+ if (!(error.code === 'ERR_MODULE_NOT_FOUND' || (error.message && error.message.includes('dist/cli/cli.js')))) {
11
+ throw error;
12
+ }
13
+ }
14
+
15
+ try {
16
+ const srcModule = await import('../src/cli/cli.js');
17
+ if (srcModule.runCli) {
18
+ return srcModule.runCli;
19
+ }
20
+ } catch (error) {
21
+ if (!(error.code === 'ERR_MODULE_NOT_FOUND')) {
22
+ throw error;
23
+ }
24
+ }
25
+
26
+ throw new Error('CLI entry not found.');
27
+ }
28
+
29
+ loadCliEntry()
30
+ .then((runCli) => runCli(process.argv))
31
+ .catch((error) => {
32
+ console.error('[yamlock] CLI failed to start:', error);
33
+ process.exit(1);
34
+ });
package/dist/cli/cli.js CHANGED
@@ -87,8 +87,8 @@ function parseArgs(argv) {
87
87
  return result;
88
88
  }
89
89
 
90
- async function main() {
91
- const { command, file, options } = parseArgs(process.argv);
90
+ export async function runCli(argv = process.argv) {
91
+ const { command, file, options } = parseArgs(argv);
92
92
 
93
93
  if (!command || !file) {
94
94
  print(HELP_TEXT.trim());
@@ -135,5 +135,5 @@ async function main() {
135
135
  }
136
136
 
137
137
  if (import.meta.url === `file://${process.argv[1]}`) {
138
- main();
138
+ runCli();
139
139
  }
@@ -3,25 +3,37 @@ import { createDecipheriv } from 'node:crypto';
3
3
  import {
4
4
  decodeFieldPathSalt,
5
5
  deriveKey,
6
- ensureAlgorithm,
7
6
  isYamlockPayload,
8
- parsePayload
7
+ parsePayload,
8
+ resolveAlgorithmOptions
9
9
  } from './utils.js';
10
10
 
11
+ function resolveDecryptOptions(payloadAlgorithm, overrides) {
12
+ if (typeof overrides === 'string' || overrides === undefined) {
13
+ return resolveAlgorithmOptions({ algorithm: payloadAlgorithm });
14
+ }
15
+
16
+ return resolveAlgorithmOptions({
17
+ ...overrides,
18
+ algorithm: payloadAlgorithm
19
+ });
20
+ }
21
+
11
22
  /**
12
23
  * Decrypts a yamlock payload for the provided field path.
13
24
  * @param {string} encryptedValue
14
25
  * @param {string|Buffer} key
15
26
  * @param {string} fieldPath
27
+ * @param {string|object} [algorithmOptions]
16
28
  * @returns {string}
17
29
  */
18
- export function decryptValue(encryptedValue, key, fieldPath) {
30
+ export function decryptValue(encryptedValue, key, fieldPath, algorithmOptions) {
19
31
  if (!isYamlockPayload(encryptedValue)) {
20
32
  throw new Error('decryptValue expects a yamlock-formatted payload.');
21
33
  }
22
34
 
23
35
  const payload = parsePayload(encryptedValue);
24
- const normalizedAlgorithm = ensureAlgorithm(payload.algorithm);
36
+ const resolvedOptions = resolveDecryptOptions(payload.algorithm, algorithmOptions);
25
37
  const saltFieldPath = decodeFieldPathSalt(payload.salt);
26
38
 
27
39
  if (!fieldPath) {
@@ -32,9 +44,27 @@ export function decryptValue(encryptedValue, key, fieldPath) {
32
44
  throw new Error('Field path does not match the encrypted payload.');
33
45
  }
34
46
 
35
- const derivedKey = deriveKey(key, normalizedAlgorithm);
36
- const decipher = createDecipheriv(normalizedAlgorithm, derivedKey, payload.iv);
37
- const decrypted = Buffer.concat([decipher.update(payload.data), decipher.final()]);
47
+ const derivedKey = deriveKey(key, resolvedOptions);
48
+ let ciphertext = payload.data;
49
+ let authTag;
50
+ if (resolvedOptions.authTagLength) {
51
+ if (ciphertext.length < resolvedOptions.authTagLength) {
52
+ throw new Error('Encrypted payload is missing an authentication tag.');
53
+ }
54
+ authTag = ciphertext.subarray(ciphertext.length - resolvedOptions.authTagLength);
55
+ ciphertext = ciphertext.subarray(0, ciphertext.length - resolvedOptions.authTagLength);
56
+ }
57
+
58
+ const decipherOptions = resolvedOptions.authTagLength ? { authTagLength: resolvedOptions.authTagLength } : undefined;
59
+ const decipher = createDecipheriv(resolvedOptions.algorithm, derivedKey, payload.iv, decipherOptions);
60
+ if (authTag) {
61
+ if (typeof decipher.setAuthTag !== 'function') {
62
+ throw new Error(`Algorithm ${resolvedOptions.algorithm} requires auth tags but setAuthTag is unavailable.`);
63
+ }
64
+ decipher.setAuthTag(authTag);
65
+ }
66
+
67
+ const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
38
68
 
39
69
  return decrypted.toString('utf8');
40
70
  }
@@ -1,38 +1,52 @@
1
1
  import { createCipheriv } from 'node:crypto';
2
2
 
3
3
  import {
4
+ DEFAULT_ALGORITHM,
4
5
  deriveKey,
5
6
  encodeFieldPathSalt,
6
- ensureAlgorithm,
7
7
  formatPayload,
8
- generateIv
8
+ generateIv,
9
+ resolveAlgorithmOptions
9
10
  } from './utils.js';
10
11
 
11
- const DEFAULT_ALGORITHM = 'aes-256-cbc';
12
+ function resolveOptions(input) {
13
+ if (typeof input === 'string' || input === undefined) {
14
+ return resolveAlgorithmOptions({ algorithm: input ?? DEFAULT_ALGORITHM });
15
+ }
16
+ return resolveAlgorithmOptions(input);
17
+ }
12
18
 
13
19
  /**
14
20
  * Encrypts a string value for a specific configuration field path.
15
21
  * @param {string} value
16
22
  * @param {string|Buffer} key
17
23
  * @param {string} fieldPath
18
- * @param {string} [algorithm=DEFAULT_ALGORITHM]
24
+ * @param {string|object} [algorithmOptions=DEFAULT_ALGORITHM]
19
25
  * @returns {string}
20
26
  */
21
- export function encryptValue(value, key, fieldPath, algorithm = DEFAULT_ALGORITHM) {
27
+ export function encryptValue(value, key, fieldPath, algorithmOptions = DEFAULT_ALGORITHM) {
22
28
  if (typeof value !== 'string') {
23
29
  throw new Error('encryptValue expects the value to be a string.');
24
30
  }
25
31
 
26
- const normalizedAlgorithm = ensureAlgorithm(algorithm);
27
- const derivedKey = deriveKey(key, normalizedAlgorithm);
28
- const iv = generateIv(normalizedAlgorithm);
32
+ const resolvedOptions = resolveOptions(algorithmOptions);
33
+ const derivedKey = deriveKey(key, resolvedOptions);
34
+ const iv = generateIv(resolvedOptions);
29
35
  const salt = encodeFieldPathSalt(fieldPath);
30
36
 
31
- const cipher = createCipheriv(normalizedAlgorithm, derivedKey, iv);
32
- const encrypted = Buffer.concat([cipher.update(value, 'utf8'), cipher.final()]);
37
+ const cipherOptions = resolvedOptions.authTagLength ? { authTagLength: resolvedOptions.authTagLength } : undefined;
38
+ const cipher = createCipheriv(resolvedOptions.algorithm, derivedKey, iv, cipherOptions);
39
+ let encrypted = Buffer.concat([cipher.update(value, 'utf8'), cipher.final()]);
40
+ if (resolvedOptions.authTagLength) {
41
+ if (typeof cipher.getAuthTag !== 'function') {
42
+ throw new Error(`Algorithm ${resolvedOptions.algorithm} requires auth tags but getAuthTag is unavailable.`);
43
+ }
44
+ const authTag = cipher.getAuthTag();
45
+ encrypted = Buffer.concat([encrypted, authTag]);
46
+ }
33
47
 
34
48
  return formatPayload({
35
- algorithm: normalizedAlgorithm,
49
+ algorithm: resolvedOptions.algorithm,
36
50
  salt,
37
51
  iv,
38
52
  data: encrypted
@@ -2,6 +2,13 @@ import { createHash, getCipherInfo, getCiphers, randomBytes } from 'node:crypto'
2
2
 
3
3
  export const YAMLOCK_PREFIX = 'yl';
4
4
  export const YAMLOCK_DELIMITER = '|';
5
+ export const DEFAULT_ALGORITHM = 'aes-256-cbc';
6
+ export const ALGORITHM_PRESETS = {
7
+ 'aes-256-cbc': { keyLength: 32, ivLength: 16, authTagLength: 0 },
8
+ 'aes-192-cbc': { keyLength: 24, ivLength: 16, authTagLength: 0 },
9
+ 'aes-128-cbc': { keyLength: 16, ivLength: 16, authTagLength: 0 },
10
+ 'chacha20-poly1305': { keyLength: 32, ivLength: 12, authTagLength: 16 }
11
+ };
5
12
 
6
13
  /**
7
14
  * Returns the sorted list of cipher algorithms supported by the current runtime.
@@ -28,14 +35,46 @@ export function ensureAlgorithm(algorithm) {
28
35
  return algorithm;
29
36
  }
30
37
 
38
+ function normalizeAlgorithmInput(input) {
39
+ if (!input || typeof input === 'string') {
40
+ return { algorithm: input };
41
+ }
42
+ return input;
43
+ }
44
+
45
+ /**
46
+ * Resolves algorithm settings with optional key/IV overrides.
47
+ * @param {object|string} [input]
48
+ * @param {string} [input.algorithm]
49
+ * @param {number} [input.keyLength]
50
+ * @param {number} [input.ivLength]
51
+ * @returns {{ algorithm: string, keyLength: number, ivLength: number, authTagLength: number }}
52
+ */
53
+ export function resolveAlgorithmOptions(input) {
54
+ const normalized = normalizeAlgorithmInput(input);
55
+ const algorithmName = ensureAlgorithm(normalized.algorithm ?? DEFAULT_ALGORITHM);
56
+ const preset = ALGORITHM_PRESETS[algorithmName] ?? {};
57
+ const cipherInfo = getCipherInfo(algorithmName) ?? {};
58
+
59
+ const keyLength = normalized.keyLength ?? preset.keyLength ?? cipherInfo.keyLength ?? 32;
60
+ const ivLength = normalized.ivLength ?? preset.ivLength ?? cipherInfo.ivLength ?? 16;
61
+ const authTagLength = normalized.authTagLength ?? preset.authTagLength ?? 0;
62
+
63
+ return {
64
+ algorithm: algorithmName,
65
+ keyLength,
66
+ ivLength,
67
+ authTagLength
68
+ };
69
+ }
70
+
31
71
  /**
32
72
  * Derives a key buffer with the exact size required by the cipher.
33
73
  * @param {string|Buffer} secret
34
- * @param {string} algorithm
74
+ * @param {{ algorithm: string, keyLength: number }} options
35
75
  * @returns {Buffer}
36
76
  */
37
- export function deriveKey(secret, algorithm) {
38
- const normalizedAlgorithm = ensureAlgorithm(algorithm);
77
+ export function deriveKey(secret, { algorithm, keyLength }) {
39
78
  if (secret === undefined || secret === null || secret === '') {
40
79
  throw new Error('Encryption key is required to derive a cipher key.');
41
80
  }
@@ -44,21 +83,22 @@ export function deriveKey(secret, algorithm) {
44
83
  ? secret
45
84
  : Buffer.from(String(secret), 'utf8');
46
85
 
47
- const keyLength = getCipherInfo(normalizedAlgorithm)?.keyLength ?? 32;
48
- if (baseBuffer.length === keyLength) {
86
+ const normalizedAlgorithm = ensureAlgorithm(algorithm);
87
+ const requiredLength = keyLength ?? getCipherInfo(normalizedAlgorithm)?.keyLength ?? 32;
88
+ if (baseBuffer.length === requiredLength) {
49
89
  return baseBuffer;
50
90
  }
51
91
 
52
92
  const hash = createHash('sha512').update(baseBuffer).digest();
53
- if (hash.length >= keyLength) {
54
- return hash.subarray(0, keyLength);
93
+ if (hash.length >= requiredLength) {
94
+ return hash.subarray(0, requiredLength);
55
95
  }
56
96
 
57
- const result = Buffer.allocUnsafe(keyLength);
97
+ const result = Buffer.allocUnsafe(requiredLength);
58
98
  let offset = 0;
59
99
  let material = hash;
60
- while (offset < keyLength) {
61
- const chunk = material.subarray(0, Math.min(material.length, keyLength - offset));
100
+ while (offset < requiredLength) {
101
+ const chunk = material.subarray(0, Math.min(material.length, requiredLength - offset));
62
102
  chunk.copy(result, offset);
63
103
  offset += chunk.length;
64
104
  material = createHash('sha512').update(material).digest();
@@ -68,18 +108,18 @@ export function deriveKey(secret, algorithm) {
68
108
  }
69
109
 
70
110
  /**
71
- * Generates an IV buffer for the given algorithm (defaults to 16 bytes).
72
- * @param {string} algorithm
111
+ * Generates an IV buffer for the given algorithm configuration.
112
+ * @param {{ algorithm: string, ivLength: number }} options
73
113
  * @returns {Buffer}
74
114
  */
75
- export function generateIv(algorithm) {
115
+ export function generateIv({ algorithm, ivLength }) {
76
116
  const normalizedAlgorithm = ensureAlgorithm(algorithm);
77
- const ivLength = getCipherInfo(normalizedAlgorithm)?.ivLength ?? 16;
78
- if (ivLength === 0) {
117
+ const length = ivLength ?? getCipherInfo(normalizedAlgorithm)?.ivLength ?? 16;
118
+ if (length === 0) {
79
119
  return Buffer.alloc(0);
80
120
  }
81
121
 
82
- return randomBytes(ivLength);
122
+ return randomBytes(length);
83
123
  }
84
124
 
85
125
  /**
@@ -13,11 +13,12 @@ const MODES = {
13
13
  * @param {Object} options
14
14
  * @param {'encrypt'|'decrypt'} options.mode
15
15
  * @param {string|Buffer} options.key
16
- * @param {string} [options.algorithm]
16
+ * @param {string|object} [options.algorithm]
17
+ * @param {object} [options.algorithmOptions]
17
18
  * @param {Array<string|number>} [options.parentPath]
18
19
  * @returns {Object|Array}
19
20
  */
20
- export function processConfig(node, { mode, key, algorithm, parentPath = [] }) {
21
+ export function processConfig(node, { mode, key, algorithm, algorithmOptions, parentPath = [] }) {
21
22
  if (typeof node !== 'object' || node === null) {
22
23
  throw new Error('processConfig expects a non-null object or array.');
23
24
  }
@@ -28,6 +29,7 @@ export function processConfig(node, { mode, key, algorithm, parentPath = [] }) {
28
29
 
29
30
  const isArrayNode = Array.isArray(node);
30
31
  const result = isArrayNode ? [] : {};
32
+ const cryptoOptions = algorithmOptions ?? algorithm;
31
33
 
32
34
  Object.entries(node).forEach(([rawKey, value]) => {
33
35
  const segment = isArrayNode ? Number(rawKey) : rawKey;
@@ -38,7 +40,8 @@ export function processConfig(node, { mode, key, algorithm, parentPath = [] }) {
38
40
  result[targetKey] = processConfig(value, {
39
41
  mode,
40
42
  key,
41
- algorithm,
43
+ algorithm: cryptoOptions,
44
+ algorithmOptions: cryptoOptions,
42
45
  parentPath: [...parentPath, segment]
43
46
  });
44
47
  return;
@@ -50,9 +53,9 @@ export function processConfig(node, { mode, key, algorithm, parentPath = [] }) {
50
53
  }
51
54
 
52
55
  if (mode === MODES.ENCRYPT) {
53
- result[targetKey] = encryptValue(value, key, currentPath, algorithm);
56
+ result[targetKey] = encryptValue(value, key, currentPath, cryptoOptions);
54
57
  } else {
55
- result[targetKey] = decryptValue(value, key, currentPath);
58
+ result[targetKey] = decryptValue(value, key, currentPath, cryptoOptions);
56
59
  }
57
60
  });
58
61
 
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "yamlock",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
+ "author": "PAVEL TKACHEV",
4
5
  "description": "Value-level encryption for YAML/JSON configuration files with CLI + Node.js APIs.",
5
6
  "license": "MIT",
6
7
  "type": "module",