yamlock 0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 PAVEL TKACHEV
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,115 @@
1
+ ```ascii
2
+ ░█░█░█▀█░█▄░▄█░█░░░█▀█░█▀▀░█░█░
3
+ ░░█░░█▀█░█░▀░█░█░░░█░█░█░░░█▀▄░
4
+ ░░▀░░▀░▀░▀░░░▀░▀▀▀░▀▀▀░▀▀▀░▀░▀░
5
+ ```
6
+
7
+ # yamlock
8
+
9
+ 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.
10
+
11
+ ## Requirements
12
+
13
+ - Node.js 22.x via `asdf`
14
+ - Yarn Classic (1.x)
15
+
16
+ ## Current status
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`.
24
+
25
+ ## Working locally
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.
30
+
31
+ ## Available scripts
32
+
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.
37
+
38
+ ## Usage
39
+
40
+ ### CLI
41
+
42
+ ```bash
43
+ # Encrypt values in a YAML file
44
+ YAMLOCK_KEY="super-secret" yamlock encrypt config.yaml
45
+
46
+ # Decrypt values in place using explicit key/algorithm flags
47
+ yamlock decrypt settings.json --key "super-secret" --algorithm aes-256-cbc
48
+ ```
49
+
50
+ The CLI detects YAML (`.yaml`/`.yml`) and JSON extensions automatically and writes the file back in the same format.
51
+
52
+ ### Node.js API
53
+
54
+ ```js
55
+ import { encryptValue, decryptValue, processConfig } from 'yamlock';
56
+
57
+ const encrypted = encryptValue('swordfish', process.env.YAMLOCK_KEY, 'db.password');
58
+ const decrypted = decryptValue(encrypted, process.env.YAMLOCK_KEY, 'db.password');
59
+
60
+ const config = { db: { password: 'swordfish' } };
61
+ const locked = processConfig(config, { mode: 'encrypt', key: process.env.YAMLOCK_KEY });
62
+ const unlocked = processConfig(locked, { mode: 'decrypt', key: process.env.YAMLOCK_KEY });
63
+ ```
64
+
65
+ See `examples/basic.js` for a runnable end-to-end script (`node examples/basic.js`).
66
+
67
+ ### Encrypted value format
68
+
69
+ Every locked string follows the format:
70
+
71
+ ```txt
72
+ yl|<algorithm>|<salt_base64>|<iv_base64>|<data_base64>
73
+ ```
74
+
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.
76
+
77
+ ### Key rotation
78
+
79
+ See [docs/key-rotation.md](docs/key-rotation.md) for a step-by-step guide to rotating `YAMLOCK_KEY` without losing data.
80
+
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
+ ## Inspiration and motivation
95
+
96
+ 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:
97
+
98
+ - [autoapply/yaml-crypt](https://github.com/autoapply/yaml-crypt)
99
+ - [huwtl/secure_yaml](https://github.com/huwtl/secure_yaml)
100
+ - [bitnami-labs/sealed-secrets](https://github.com/bitnami-labs/sealed-secrets)
101
+ - [getsops/sops](https://github.com/getsops/sops)
102
+
103
+ Each of those projects solves secure config storage differently, yet none fit my exact needs. **yamlock** is the bicycle I am building for my own projects to add an extra layer of encryption for sensitive YAML/JSON values while keeping the workflow lightweight.
104
+
105
+ ## Contributing
106
+
107
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for development workflow, coding standards, and release instructions.
108
+
109
+ ## Exit codes
110
+
111
+ `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
+
113
+ ## License
114
+
115
+ MIT © PAVEL TKACHEV
package/bin/yamlock ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+
3
+ import('../dist/cli/cli.js').catch((error) => {
4
+ console.error('[yamlock] CLI failed to start:', error);
5
+ process.exit(1);
6
+ });
File without changes
@@ -0,0 +1,139 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync, writeFileSync } from 'node:fs';
3
+ import { resolve, extname } from 'node:path';
4
+ import { exit } from 'node:process';
5
+
6
+ import yaml from 'js-yaml';
7
+
8
+ import { processConfig } from '../utils/config.js';
9
+
10
+ const HELP_TEXT = `
11
+ ░█░█░█▀█░█▄░▄█░█░░░█▀█░█▀▀░█░█░
12
+ ░░█░░█▀█░█░▀░█░█░░░█░█░█░░░█▀▄░
13
+ ░░▀░░▀░▀░▀░░░▀░▀▀▀░▀▀▀░▀▀▀░▀░▀░
14
+
15
+ Usage:
16
+ yamlock <command> [options]
17
+
18
+ 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.
21
+
22
+ Options:
23
+ -k, --key <value> Encryption key (or use YAMLOCK_KEY env).
24
+ -a, --algorithm <value> Cipher algorithm (default: aes-256-cbc).
25
+ `;
26
+
27
+ function print(message) {
28
+ console.log(message);
29
+ }
30
+
31
+ function printError(message) {
32
+ console.error(message);
33
+ }
34
+
35
+ function detectFormat(filePath) {
36
+ const extension = extname(filePath).toLowerCase();
37
+ if (extension === '.yaml' || extension === '.yml') {
38
+ return 'yaml';
39
+ }
40
+
41
+ return 'json';
42
+ }
43
+
44
+ function readConfigFile(filePath) {
45
+ const content = readFileSync(filePath, 'utf8');
46
+ const format = detectFormat(filePath);
47
+
48
+ if (format === 'yaml') {
49
+ return { format, data: yaml.load(content) ?? {} };
50
+ }
51
+
52
+ return { format, data: JSON.parse(content) };
53
+ }
54
+
55
+ function writeConfigFile(filePath, format, data) {
56
+ if (format === 'yaml') {
57
+ const serialized = yaml.dump(data, { lineWidth: 120 });
58
+ writeFileSync(filePath, serialized, 'utf8');
59
+ return;
60
+ }
61
+
62
+ const serialized = JSON.stringify(data, null, 2);
63
+ writeFileSync(filePath, `${serialized}\n`, 'utf8');
64
+ }
65
+
66
+ function parseArgs(argv) {
67
+ const args = argv.slice(2);
68
+ const result = {
69
+ command: args[0],
70
+ file: args[1],
71
+ options: {}
72
+ };
73
+
74
+ for (let i = 2; i < args.length; i += 1) {
75
+ const arg = args[i];
76
+ const next = args[i + 1];
77
+
78
+ if (arg === '-k' || arg === '--key') {
79
+ result.options.key = next;
80
+ i += 1;
81
+ } else if (arg === '-a' || arg === '--algorithm') {
82
+ result.options.algorithm = next;
83
+ i += 1;
84
+ }
85
+ }
86
+
87
+ return result;
88
+ }
89
+
90
+ async function main() {
91
+ const { command, file, options } = parseArgs(process.argv);
92
+
93
+ if (!command || !file) {
94
+ print(HELP_TEXT.trim());
95
+ return exit(1);
96
+ }
97
+
98
+ const key = options.key ?? process.env.YAMLOCK_KEY;
99
+ if (!key) {
100
+ printError('Encryption key is required via --key or YAMLOCK_KEY.');
101
+ return exit(1);
102
+ }
103
+
104
+ const absolutePath = resolve(process.cwd(), file);
105
+ let config;
106
+ try {
107
+ config = readConfigFile(absolutePath);
108
+ } catch (error) {
109
+ printError(`Failed to read config file: ${error.message}`);
110
+ return exit(1);
111
+ }
112
+
113
+ try {
114
+ 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}`);
118
+ return exit(0);
119
+ }
120
+
121
+ if (command === 'decrypt') {
122
+ const result = processConfig(config.data, { mode: 'decrypt', key });
123
+ writeConfigFile(absolutePath, config.format, result);
124
+ print(`Decrypted values in ${file}`);
125
+ return exit(0);
126
+ }
127
+
128
+ printError(`Unknown command: ${command}`);
129
+ print(HELP_TEXT.trim());
130
+ return exit(1);
131
+ } catch (error) {
132
+ printError(`Operation failed: ${error.message}`);
133
+ return exit(1);
134
+ }
135
+ }
136
+
137
+ if (import.meta.url === `file://${process.argv[1]}`) {
138
+ main();
139
+ }
File without changes
@@ -0,0 +1,40 @@
1
+ import { createDecipheriv } from 'node:crypto';
2
+
3
+ import {
4
+ decodeFieldPathSalt,
5
+ deriveKey,
6
+ ensureAlgorithm,
7
+ isYamlockPayload,
8
+ parsePayload
9
+ } from './utils.js';
10
+
11
+ /**
12
+ * Decrypts a yamlock payload for the provided field path.
13
+ * @param {string} encryptedValue
14
+ * @param {string|Buffer} key
15
+ * @param {string} fieldPath
16
+ * @returns {string}
17
+ */
18
+ export function decryptValue(encryptedValue, key, fieldPath) {
19
+ if (!isYamlockPayload(encryptedValue)) {
20
+ throw new Error('decryptValue expects a yamlock-formatted payload.');
21
+ }
22
+
23
+ const payload = parsePayload(encryptedValue);
24
+ const normalizedAlgorithm = ensureAlgorithm(payload.algorithm);
25
+ const saltFieldPath = decodeFieldPathSalt(payload.salt);
26
+
27
+ if (!fieldPath) {
28
+ throw new Error('Field path is required to decrypt a value.');
29
+ }
30
+
31
+ if (saltFieldPath !== fieldPath) {
32
+ throw new Error('Field path does not match the encrypted payload.');
33
+ }
34
+
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()]);
38
+
39
+ return decrypted.toString('utf8');
40
+ }
@@ -0,0 +1,40 @@
1
+ import { createCipheriv } from 'node:crypto';
2
+
3
+ import {
4
+ deriveKey,
5
+ encodeFieldPathSalt,
6
+ ensureAlgorithm,
7
+ formatPayload,
8
+ generateIv
9
+ } from './utils.js';
10
+
11
+ const DEFAULT_ALGORITHM = 'aes-256-cbc';
12
+
13
+ /**
14
+ * Encrypts a string value for a specific configuration field path.
15
+ * @param {string} value
16
+ * @param {string|Buffer} key
17
+ * @param {string} fieldPath
18
+ * @param {string} [algorithm=DEFAULT_ALGORITHM]
19
+ * @returns {string}
20
+ */
21
+ export function encryptValue(value, key, fieldPath, algorithm = DEFAULT_ALGORITHM) {
22
+ if (typeof value !== 'string') {
23
+ throw new Error('encryptValue expects the value to be a string.');
24
+ }
25
+
26
+ const normalizedAlgorithm = ensureAlgorithm(algorithm);
27
+ const derivedKey = deriveKey(key, normalizedAlgorithm);
28
+ const iv = generateIv(normalizedAlgorithm);
29
+ const salt = encodeFieldPathSalt(fieldPath);
30
+
31
+ const cipher = createCipheriv(normalizedAlgorithm, derivedKey, iv);
32
+ const encrypted = Buffer.concat([cipher.update(value, 'utf8'), cipher.final()]);
33
+
34
+ return formatPayload({
35
+ algorithm: normalizedAlgorithm,
36
+ salt,
37
+ iv,
38
+ data: encrypted
39
+ });
40
+ }
@@ -0,0 +1,172 @@
1
+ import { createHash, getCipherInfo, getCiphers, randomBytes } from 'node:crypto';
2
+
3
+ export const YAMLOCK_PREFIX = 'yl';
4
+ export const YAMLOCK_DELIMITER = '|';
5
+
6
+ /**
7
+ * Returns the sorted list of cipher algorithms supported by the current runtime.
8
+ * @returns {string[]}
9
+ */
10
+ export function listSupportedAlgorithms() {
11
+ return [...new Set(getCiphers())].sort();
12
+ }
13
+
14
+ /**
15
+ * Ensures the provided algorithm is available in the current runtime.
16
+ * @param {string} algorithm
17
+ * @returns {string}
18
+ */
19
+ export function ensureAlgorithm(algorithm) {
20
+ if (!algorithm) {
21
+ throw new Error('Encryption algorithm is required.');
22
+ }
23
+
24
+ if (!listSupportedAlgorithms().includes(algorithm)) {
25
+ throw new Error(`Unsupported algorithm: ${algorithm}`);
26
+ }
27
+
28
+ return algorithm;
29
+ }
30
+
31
+ /**
32
+ * Derives a key buffer with the exact size required by the cipher.
33
+ * @param {string|Buffer} secret
34
+ * @param {string} algorithm
35
+ * @returns {Buffer}
36
+ */
37
+ export function deriveKey(secret, algorithm) {
38
+ const normalizedAlgorithm = ensureAlgorithm(algorithm);
39
+ if (secret === undefined || secret === null || secret === '') {
40
+ throw new Error('Encryption key is required to derive a cipher key.');
41
+ }
42
+
43
+ const baseBuffer = Buffer.isBuffer(secret)
44
+ ? secret
45
+ : Buffer.from(String(secret), 'utf8');
46
+
47
+ const keyLength = getCipherInfo(normalizedAlgorithm)?.keyLength ?? 32;
48
+ if (baseBuffer.length === keyLength) {
49
+ return baseBuffer;
50
+ }
51
+
52
+ const hash = createHash('sha512').update(baseBuffer).digest();
53
+ if (hash.length >= keyLength) {
54
+ return hash.subarray(0, keyLength);
55
+ }
56
+
57
+ const result = Buffer.allocUnsafe(keyLength);
58
+ let offset = 0;
59
+ let material = hash;
60
+ while (offset < keyLength) {
61
+ const chunk = material.subarray(0, Math.min(material.length, keyLength - offset));
62
+ chunk.copy(result, offset);
63
+ offset += chunk.length;
64
+ material = createHash('sha512').update(material).digest();
65
+ }
66
+
67
+ return result;
68
+ }
69
+
70
+ /**
71
+ * Generates an IV buffer for the given algorithm (defaults to 16 bytes).
72
+ * @param {string} algorithm
73
+ * @returns {Buffer}
74
+ */
75
+ export function generateIv(algorithm) {
76
+ const normalizedAlgorithm = ensureAlgorithm(algorithm);
77
+ const ivLength = getCipherInfo(normalizedAlgorithm)?.ivLength ?? 16;
78
+ if (ivLength === 0) {
79
+ return Buffer.alloc(0);
80
+ }
81
+
82
+ return randomBytes(ivLength);
83
+ }
84
+
85
+ /**
86
+ * Creates a deterministic salt (Base64 field path) used when encrypting.
87
+ * @param {string} fieldPath
88
+ * @returns {string}
89
+ */
90
+ export function encodeFieldPathSalt(fieldPath) {
91
+ if (!fieldPath) {
92
+ throw new Error('Field path is required to create a salt.');
93
+ }
94
+
95
+ return Buffer.from(String(fieldPath), 'utf8').toString('base64');
96
+ }
97
+
98
+ /**
99
+ * Decodes a Base64 salt string back to the original field path.
100
+ * @param {string} salt
101
+ * @returns {string}
102
+ */
103
+ export function decodeFieldPathSalt(salt) {
104
+ if (!salt) {
105
+ throw new Error('Salt value is required.');
106
+ }
107
+
108
+ return Buffer.from(String(salt), 'base64').toString('utf8');
109
+ }
110
+
111
+ /**
112
+ * Formats the encrypted payload into the canonical yamlock string.
113
+ * @param {Object} payload
114
+ * @param {string} payload.algorithm
115
+ * @param {string} payload.salt
116
+ * @param {Buffer} payload.iv
117
+ * @param {Buffer} payload.data
118
+ * @returns {string}
119
+ */
120
+ export function formatPayload({ algorithm, salt, iv, data }) {
121
+ if (!algorithm || !salt || !iv || !data) {
122
+ throw new Error('Algorithm, salt, IV, and data are required to format payload.');
123
+ }
124
+
125
+ return [
126
+ YAMLOCK_PREFIX,
127
+ algorithm,
128
+ salt,
129
+ iv.toString('base64'),
130
+ data.toString('base64')
131
+ ].join(YAMLOCK_DELIMITER);
132
+ }
133
+
134
+ /**
135
+ * Checks whether a string looks like a yamlock payload.
136
+ * @param {unknown} candidate
137
+ * @returns {boolean}
138
+ */
139
+ export function isYamlockPayload(candidate) {
140
+ return (
141
+ typeof candidate === 'string' &&
142
+ candidate.startsWith(`${YAMLOCK_PREFIX}${YAMLOCK_DELIMITER}`)
143
+ );
144
+ }
145
+
146
+ /**
147
+ * Parses a yamlock payload and returns its structured pieces.
148
+ * @param {string} value
149
+ * @returns {{ algorithm: string, salt: string, iv: Buffer, data: Buffer }}
150
+ */
151
+ export function parsePayload(value) {
152
+ if (!isYamlockPayload(value)) {
153
+ throw new Error('Value is not a yamlock payload.');
154
+ }
155
+
156
+ const parts = value.split(YAMLOCK_DELIMITER);
157
+ if (parts.length !== 5) {
158
+ throw new Error('Malformed yamlock payload.');
159
+ }
160
+
161
+ const [, algorithm, salt, ivBase64, dataBase64] = parts;
162
+ if (!algorithm || !salt || !ivBase64 || !dataBase64) {
163
+ throw new Error('Malformed yamlock payload segments.');
164
+ }
165
+
166
+ return {
167
+ algorithm,
168
+ salt,
169
+ iv: Buffer.from(ivBase64, 'base64'),
170
+ data: Buffer.from(dataBase64, 'base64')
171
+ };
172
+ }
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { encryptValue } from './crypto/encrypt.js';
2
+ export { decryptValue } from './crypto/decrypt.js';
3
+ export { processConfig } from './utils/config.js';
4
+ export { listSupportedAlgorithms as getSupportedAlgorithms } from './crypto/utils.js';
File without changes
@@ -0,0 +1,60 @@
1
+ import { encryptValue } from '../crypto/encrypt.js';
2
+ import { decryptValue } from '../crypto/decrypt.js';
3
+ import { buildPath } from './path.js';
4
+
5
+ const MODES = {
6
+ ENCRYPT: 'encrypt',
7
+ DECRYPT: 'decrypt'
8
+ };
9
+
10
+ /**
11
+ * Recursively processes a config object or array, encrypting/decrypting string values.
12
+ * @param {Object|Array} node
13
+ * @param {Object} options
14
+ * @param {'encrypt'|'decrypt'} options.mode
15
+ * @param {string|Buffer} options.key
16
+ * @param {string} [options.algorithm]
17
+ * @param {Array<string|number>} [options.parentPath]
18
+ * @returns {Object|Array}
19
+ */
20
+ export function processConfig(node, { mode, key, algorithm, parentPath = [] }) {
21
+ if (typeof node !== 'object' || node === null) {
22
+ throw new Error('processConfig expects a non-null object or array.');
23
+ }
24
+
25
+ if (mode !== MODES.ENCRYPT && mode !== MODES.DECRYPT) {
26
+ throw new Error(`Unknown processConfig mode: ${mode}`);
27
+ }
28
+
29
+ const isArrayNode = Array.isArray(node);
30
+ const result = isArrayNode ? [] : {};
31
+
32
+ Object.entries(node).forEach(([rawKey, value]) => {
33
+ const segment = isArrayNode ? Number(rawKey) : rawKey;
34
+ const targetKey = isArrayNode ? segment : rawKey;
35
+ const currentPath = buildPath(parentPath, segment);
36
+
37
+ if (value !== null && typeof value === 'object') {
38
+ result[targetKey] = processConfig(value, {
39
+ mode,
40
+ key,
41
+ algorithm,
42
+ parentPath: [...parentPath, segment]
43
+ });
44
+ return;
45
+ }
46
+
47
+ if (typeof value !== 'string') {
48
+ result[targetKey] = value;
49
+ return;
50
+ }
51
+
52
+ if (mode === MODES.ENCRYPT) {
53
+ result[targetKey] = encryptValue(value, key, currentPath, algorithm);
54
+ } else {
55
+ result[targetKey] = decryptValue(value, key, currentPath);
56
+ }
57
+ });
58
+
59
+ return result;
60
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Builds a dot/bracket path string that uniquely identifies a value
3
+ * inside a nested object/array structure.
4
+ * Example: ["db", "users", 0, "password"] => "db.users[0].password"
5
+ *
6
+ * @param {Array<string|number>} segments
7
+ * @returns {string}
8
+ */
9
+ export function serializePath(segments) {
10
+ if (!Array.isArray(segments) || segments.length === 0) {
11
+ throw new Error('serializePath requires a non-empty segments array.');
12
+ }
13
+
14
+ return segments
15
+ .map((segment, index) => {
16
+ if (typeof segment === 'number') {
17
+ return `[${segment}]`;
18
+ }
19
+
20
+ if (typeof segment === 'string' && segment.length > 0) {
21
+ return index === 0 ? segment : `.${segment}`;
22
+ }
23
+
24
+ throw new Error('Path segments must be non-empty strings or numbers.');
25
+ })
26
+ .join('');
27
+ }
28
+
29
+ /**
30
+ * Returns the full path string for a given traversal context.
31
+ * @param {Array<string|number>} parentSegments
32
+ * @param {string|number} currentSegment
33
+ * @returns {string}
34
+ */
35
+ export function buildPath(parentSegments, currentSegment) {
36
+ const segments = [...(parentSegments ?? [])];
37
+ segments.push(currentSegment);
38
+ return serializePath(segments);
39
+ }
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "yamlock",
3
+ "version": "0.1.1",
4
+ "description": "Value-level encryption for YAML/JSON configuration files with CLI + Node.js APIs.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "dist/index.js",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js"
11
+ },
12
+ "./package.json": "./package.json"
13
+ },
14
+ "bin": {
15
+ "yamlock": "bin/yamlock"
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "bin",
20
+ "README.md",
21
+ "LICENSE"
22
+ ],
23
+ "keywords": [
24
+ "yaml",
25
+ "cli",
26
+ "config",
27
+ "json",
28
+ "encryption",
29
+ "security"
30
+ ],
31
+ "engines": {
32
+ "node": ">=22.0.0"
33
+ },
34
+ "scripts": {
35
+ "build": "rimraf dist && cp -R src dist",
36
+ "lint": "eslint .",
37
+ "prepare": "yarn run build",
38
+ "test": "node --test"
39
+ },
40
+ "repository": {
41
+ "type": "git",
42
+ "url": "git+https://github.com/phoenixweiss/yamlock.git"
43
+ },
44
+ "bugs": {
45
+ "url": "https://github.com/phoenixweiss/yamlock/issues"
46
+ },
47
+ "homepage": "https://github.com/phoenixweiss/yamlock#readme",
48
+ "dependencies": {
49
+ "js-yaml": "^4.1.0"
50
+ },
51
+ "devDependencies": {
52
+ "@eslint/js": "^9.0.0",
53
+ "eslint": "^9.0.0",
54
+ "globals": "^15.0.0",
55
+ "rimraf": "^5.0.5"
56
+ }
57
+ }