@faizahmed/secret-keystore 1.1.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/LICENSE +21 -0
- package/README.md +1203 -0
- package/SECURITY.md +505 -0
- package/bin/cli.js +969 -0
- package/package.json +77 -0
- package/src/attestation/attestation-client.js +146 -0
- package/src/attestation/attestation-manager.js +339 -0
- package/src/attestation/cms-unwrap.js +166 -0
- package/src/attestation/index.js +66 -0
- package/src/attestation/key-pair.js +129 -0
- package/src/config.js +130 -0
- package/src/content-operations.js +494 -0
- package/src/errors.js +372 -0
- package/src/index.d.ts +641 -0
- package/src/index.js +438 -0
- package/src/keystore.js +678 -0
- package/src/kms.js +858 -0
- package/src/object-operations.js +232 -0
- package/src/options.js +541 -0
- package/src/path-matcher.js +319 -0
- package/src/rotate.js +92 -0
- package/src/yaml-utils.js +265 -0
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @faizahmed/secret-keystore - Content-Based Operations
|
|
3
|
+
*
|
|
4
|
+
* Functions for encrypting/decrypting content strings (ENV, JSON, YAML).
|
|
5
|
+
* Preserves comments and formatting during transformation.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { encryptKMSValue, decryptKMSValue, isAlreadyEncrypted } = require('./kms');
|
|
9
|
+
const { encryptKMSObject, decryptKMSObject } = require('./object-operations');
|
|
10
|
+
const { validateKmsKeyId, buildEncryptOptions, buildDecryptOptions } = require('./options');
|
|
11
|
+
const { ContentError, CONTENT_ERROR_CODES } = require('./errors');
|
|
12
|
+
|
|
13
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
14
|
+
// ENV CONTENT OPERATIONS
|
|
15
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Extract value and inline comment from a quoted string
|
|
19
|
+
* @param {string} valueWithComment - The value portion after =
|
|
20
|
+
* @param {string} quoteChar - The quote character (" or ')
|
|
21
|
+
* @returns {{value: string, inlineComment: string|null}}
|
|
22
|
+
*/
|
|
23
|
+
function parseQuotedValue(valueWithComment, quoteChar) {
|
|
24
|
+
const closeQuote = valueWithComment.indexOf(quoteChar, 1);
|
|
25
|
+
if (closeQuote === -1) {
|
|
26
|
+
return { value: valueWithComment.slice(1), inlineComment: null };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const value = valueWithComment.slice(1, closeQuote);
|
|
30
|
+
const afterQuote = valueWithComment.slice(closeQuote + 1).trim();
|
|
31
|
+
const inlineComment = afterQuote.startsWith('#') ? afterQuote : null;
|
|
32
|
+
|
|
33
|
+
return { value, inlineComment };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Extract value and inline comment from an unquoted string
|
|
38
|
+
* @param {string} valueWithComment - The value portion after =
|
|
39
|
+
* @returns {{value: string, inlineComment: string|null}}
|
|
40
|
+
*/
|
|
41
|
+
function parseUnquotedValue(valueWithComment) {
|
|
42
|
+
const hashIndex = valueWithComment.indexOf('#');
|
|
43
|
+
if (hashIndex === -1) {
|
|
44
|
+
return { value: valueWithComment.trim(), inlineComment: null };
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
value: valueWithComment.slice(0, hashIndex).trim(),
|
|
48
|
+
inlineComment: valueWithComment.slice(hashIndex)
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Parse .env content into structured entries
|
|
54
|
+
* @param {string} content - Raw .env content
|
|
55
|
+
* @returns {Array<Object>} Parsed entries
|
|
56
|
+
*/
|
|
57
|
+
function parseEnvContent(content) {
|
|
58
|
+
const lines = content.split('\n');
|
|
59
|
+
const parsed = [];
|
|
60
|
+
|
|
61
|
+
for (const line of lines) {
|
|
62
|
+
const trimmed = line.trim();
|
|
63
|
+
|
|
64
|
+
if (!trimmed) {
|
|
65
|
+
parsed.push({ type: 'empty', raw: line });
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (trimmed.startsWith('#')) {
|
|
70
|
+
parsed.push({ type: 'comment', raw: line });
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const match = trimmed.match(/^([^=]+)=(.*)$/);
|
|
75
|
+
if (!match) {
|
|
76
|
+
parsed.push({ type: 'other', raw: line });
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const key = match[1].trim();
|
|
81
|
+
const valueWithComment = match[2];
|
|
82
|
+
|
|
83
|
+
let result;
|
|
84
|
+
if (valueWithComment.startsWith('"')) {
|
|
85
|
+
result = parseQuotedValue(valueWithComment, '"');
|
|
86
|
+
} else if (valueWithComment.startsWith("'")) {
|
|
87
|
+
result = parseQuotedValue(valueWithComment, "'");
|
|
88
|
+
} else {
|
|
89
|
+
result = parseUnquotedValue(valueWithComment);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
parsed.push({
|
|
93
|
+
type: 'keyvalue',
|
|
94
|
+
key,
|
|
95
|
+
value: result.value,
|
|
96
|
+
inlineComment: result.inlineComment,
|
|
97
|
+
raw: line
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return parsed;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Reconstruct .env content from parsed entries
|
|
106
|
+
* @param {Array<Object>} parsed - Parsed entries
|
|
107
|
+
* @returns {string} Reconstructed content
|
|
108
|
+
*/
|
|
109
|
+
function reconstructEnvContent(parsed) {
|
|
110
|
+
return parsed
|
|
111
|
+
.map(entry => {
|
|
112
|
+
if (entry.type === 'keyvalue') {
|
|
113
|
+
const needsQuotes =
|
|
114
|
+
entry.value.includes(' ') ||
|
|
115
|
+
entry.value.includes('#') ||
|
|
116
|
+
entry.value.includes('=') ||
|
|
117
|
+
entry.value.includes('\n');
|
|
118
|
+
|
|
119
|
+
let line = needsQuotes
|
|
120
|
+
? `${entry.key}="${entry.value}"`
|
|
121
|
+
: `${entry.key}=${entry.value}`;
|
|
122
|
+
|
|
123
|
+
if (entry.inlineComment) {
|
|
124
|
+
line += ` ${entry.inlineComment}`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return line;
|
|
128
|
+
}
|
|
129
|
+
return entry.raw;
|
|
130
|
+
})
|
|
131
|
+
.join('\n');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Encrypt .env content string using AWS KMS
|
|
136
|
+
*
|
|
137
|
+
* @param {string} content - Raw .env content
|
|
138
|
+
* @param {string} kmsKeyId - KMS key ID (required)
|
|
139
|
+
* @param {Object} [options] - Options
|
|
140
|
+
* @param {string[]} [options.paths] - Keys to encrypt (encrypt all if not provided)
|
|
141
|
+
* @param {Object} [options.exclude] - Keys to exclude
|
|
142
|
+
* @param {Object} [options.preserve] - Preservation options
|
|
143
|
+
* @returns {Promise<Object>} Result with encrypted content
|
|
144
|
+
*/
|
|
145
|
+
async function encryptKMSEnvContent(content, kmsKeyId, options = {}) {
|
|
146
|
+
validateKmsKeyId(kmsKeyId);
|
|
147
|
+
|
|
148
|
+
const opts = buildEncryptOptions(options);
|
|
149
|
+
const logger = opts.logger;
|
|
150
|
+
|
|
151
|
+
if (!content || typeof content !== 'string') {
|
|
152
|
+
throw new ContentError(
|
|
153
|
+
'Content must be a non-empty string',
|
|
154
|
+
CONTENT_ERROR_CODES.EMPTY_CONTENT,
|
|
155
|
+
'env'
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const parsed = parseEnvContent(content);
|
|
160
|
+
const keyValueEntries = parsed.filter(e => e.type === 'keyvalue');
|
|
161
|
+
|
|
162
|
+
// Determine which keys to encrypt
|
|
163
|
+
let keysToEncrypt;
|
|
164
|
+
if (options.paths && options.paths.length > 0) {
|
|
165
|
+
keysToEncrypt = options.paths;
|
|
166
|
+
} else {
|
|
167
|
+
keysToEncrypt = keyValueEntries.map(e => e.key);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Apply exclusions
|
|
171
|
+
if (options.exclude?.paths) {
|
|
172
|
+
keysToEncrypt = keysToEncrypt.filter(k => !options.exclude.paths.includes(k));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const result = {
|
|
176
|
+
content: '',
|
|
177
|
+
encrypted: [],
|
|
178
|
+
skipped: [],
|
|
179
|
+
failed: []
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const skipEmpty = opts.skip?.empty !== false;
|
|
183
|
+
const skipAlreadyEncrypted = opts.skip?.alreadyEncrypted !== false;
|
|
184
|
+
const continueOnError = opts.continueOnError === true;
|
|
185
|
+
|
|
186
|
+
for (const entry of keyValueEntries) {
|
|
187
|
+
if (!keysToEncrypt.includes(entry.key)) {
|
|
188
|
+
result.skipped.push(entry.key);
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Skip empty
|
|
193
|
+
if (skipEmpty && (!entry.value || entry.value.trim() === '')) {
|
|
194
|
+
result.skipped.push(entry.key);
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Skip already encrypted
|
|
199
|
+
if (skipAlreadyEncrypted && isAlreadyEncrypted(entry.value)) {
|
|
200
|
+
result.skipped.push(entry.key);
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
entry.value = await encryptKMSValue(entry.value, kmsKeyId, opts);
|
|
206
|
+
result.encrypted.push(entry.key);
|
|
207
|
+
logger?.info?.(`[encryptKMSEnvContent] Encrypted: ${entry.key}`);
|
|
208
|
+
} catch (error) {
|
|
209
|
+
if (continueOnError) {
|
|
210
|
+
result.failed.push({ key: entry.key, error });
|
|
211
|
+
logger?.warn?.(`[encryptKMSEnvContent] Failed: ${entry.key}`);
|
|
212
|
+
} else {
|
|
213
|
+
throw error;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
result.content = reconstructEnvContent(parsed);
|
|
219
|
+
return result;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Decrypt .env content string using AWS KMS
|
|
224
|
+
*
|
|
225
|
+
* @param {string} content - Encrypted .env content
|
|
226
|
+
* @param {string} kmsKeyId - KMS key ID (required)
|
|
227
|
+
* @param {Object} [options] - Options
|
|
228
|
+
* @param {string[]} [options.paths] - Keys to decrypt (decrypt all if not provided)
|
|
229
|
+
* @param {Object} [options.attestation] - Attestation options
|
|
230
|
+
* @returns {Promise<Object>} Result with decrypted content
|
|
231
|
+
*/
|
|
232
|
+
async function decryptKMSEnvContent(content, kmsKeyId, options = {}) {
|
|
233
|
+
validateKmsKeyId(kmsKeyId);
|
|
234
|
+
|
|
235
|
+
const opts = buildDecryptOptions(options);
|
|
236
|
+
const logger = opts.logger;
|
|
237
|
+
|
|
238
|
+
if (!content || typeof content !== 'string') {
|
|
239
|
+
throw new ContentError(
|
|
240
|
+
'Content must be a non-empty string',
|
|
241
|
+
CONTENT_ERROR_CODES.EMPTY_CONTENT,
|
|
242
|
+
'env'
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const parsed = parseEnvContent(content);
|
|
247
|
+
const keyValueEntries = parsed.filter(e => e.type === 'keyvalue');
|
|
248
|
+
|
|
249
|
+
// Determine which keys to decrypt
|
|
250
|
+
let keysToDecrypt;
|
|
251
|
+
if (options.paths && options.paths.length > 0) {
|
|
252
|
+
keysToDecrypt = options.paths;
|
|
253
|
+
} else {
|
|
254
|
+
keysToDecrypt = keyValueEntries.map(e => e.key);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Apply exclusions
|
|
258
|
+
if (options.exclude?.paths) {
|
|
259
|
+
keysToDecrypt = keysToDecrypt.filter(k => !options.exclude.paths.includes(k));
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const result = {
|
|
263
|
+
content: '',
|
|
264
|
+
decrypted: [],
|
|
265
|
+
skipped: [],
|
|
266
|
+
failed: []
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
const skipUnencrypted = opts.skip?.unencrypted !== false;
|
|
270
|
+
const continueOnError = opts.continueOnError === true;
|
|
271
|
+
|
|
272
|
+
for (const entry of keyValueEntries) {
|
|
273
|
+
if (!keysToDecrypt.includes(entry.key)) {
|
|
274
|
+
result.skipped.push(entry.key);
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Skip unencrypted
|
|
279
|
+
if (skipUnencrypted && !isAlreadyEncrypted(entry.value)) {
|
|
280
|
+
result.skipped.push(entry.key);
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
try {
|
|
285
|
+
entry.value = await decryptKMSValue(entry.value, kmsKeyId, opts);
|
|
286
|
+
result.decrypted.push(entry.key);
|
|
287
|
+
logger?.info?.(`[decryptKMSEnvContent] Decrypted: ${entry.key}`);
|
|
288
|
+
} catch (error) {
|
|
289
|
+
if (continueOnError) {
|
|
290
|
+
result.failed.push({ key: entry.key, error });
|
|
291
|
+
logger?.warn?.(`[decryptKMSEnvContent] Failed: ${entry.key}`);
|
|
292
|
+
} else {
|
|
293
|
+
throw error;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
result.content = reconstructEnvContent(parsed);
|
|
299
|
+
return result;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
303
|
+
// JSON CONTENT OPERATIONS
|
|
304
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Encrypt JSON content string using AWS KMS
|
|
308
|
+
*
|
|
309
|
+
* @param {string} content - JSON content string
|
|
310
|
+
* @param {string} kmsKeyId - KMS key ID (required)
|
|
311
|
+
* @param {Object} [options] - Options (same as encryptKMSObject)
|
|
312
|
+
* @returns {Promise<Object>} Result with encrypted JSON content
|
|
313
|
+
*/
|
|
314
|
+
async function encryptKMSJsonContent(content, kmsKeyId, options = {}) {
|
|
315
|
+
validateKmsKeyId(kmsKeyId);
|
|
316
|
+
|
|
317
|
+
if (!content || typeof content !== 'string') {
|
|
318
|
+
throw new ContentError(
|
|
319
|
+
'Content must be a non-empty string',
|
|
320
|
+
CONTENT_ERROR_CODES.EMPTY_CONTENT,
|
|
321
|
+
'json'
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
let obj;
|
|
326
|
+
try {
|
|
327
|
+
obj = JSON.parse(content);
|
|
328
|
+
} catch (error) {
|
|
329
|
+
throw new ContentError(
|
|
330
|
+
`Failed to parse JSON content: ${error.message}`,
|
|
331
|
+
CONTENT_ERROR_CODES.PARSE_FAILED,
|
|
332
|
+
'json',
|
|
333
|
+
error
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const objectResult = await encryptKMSObject(obj, kmsKeyId, options);
|
|
338
|
+
|
|
339
|
+
return {
|
|
340
|
+
content: JSON.stringify(objectResult.object, null, 2),
|
|
341
|
+
encrypted: objectResult.encrypted,
|
|
342
|
+
skipped: objectResult.skipped,
|
|
343
|
+
failed: objectResult.failed
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Decrypt JSON content string using AWS KMS
|
|
349
|
+
*
|
|
350
|
+
* @param {string} content - Encrypted JSON content string
|
|
351
|
+
* @param {string} kmsKeyId - KMS key ID (required)
|
|
352
|
+
* @param {Object} [options] - Options (same as decryptKMSObject)
|
|
353
|
+
* @returns {Promise<Object>} Result with decrypted JSON content
|
|
354
|
+
*/
|
|
355
|
+
async function decryptKMSJsonContent(content, kmsKeyId, options = {}) {
|
|
356
|
+
validateKmsKeyId(kmsKeyId);
|
|
357
|
+
|
|
358
|
+
if (!content || typeof content !== 'string') {
|
|
359
|
+
throw new ContentError(
|
|
360
|
+
'Content must be a non-empty string',
|
|
361
|
+
CONTENT_ERROR_CODES.EMPTY_CONTENT,
|
|
362
|
+
'json'
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
let obj;
|
|
367
|
+
try {
|
|
368
|
+
obj = JSON.parse(content);
|
|
369
|
+
} catch (error) {
|
|
370
|
+
throw new ContentError(
|
|
371
|
+
`Failed to parse JSON content: ${error.message}`,
|
|
372
|
+
CONTENT_ERROR_CODES.PARSE_FAILED,
|
|
373
|
+
'json',
|
|
374
|
+
error
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const objectResult = await decryptKMSObject(obj, kmsKeyId, options);
|
|
379
|
+
|
|
380
|
+
return {
|
|
381
|
+
content: JSON.stringify(objectResult.object, null, 2),
|
|
382
|
+
decrypted: objectResult.decrypted,
|
|
383
|
+
skipped: objectResult.skipped,
|
|
384
|
+
failed: objectResult.failed
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
389
|
+
// YAML CONTENT OPERATIONS
|
|
390
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
391
|
+
|
|
392
|
+
const { parseYaml, serializeYaml, isJsYamlAvailable } = require('./yaml-utils');
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Encrypt YAML content string using AWS KMS
|
|
396
|
+
*
|
|
397
|
+
* Uses js-yaml if installed, otherwise falls back to simple parser for basic YAML.
|
|
398
|
+
* Complex YAML features (anchors, multi-line strings) require js-yaml.
|
|
399
|
+
*
|
|
400
|
+
* @param {string} content - YAML content string
|
|
401
|
+
* @param {string} kmsKeyId - KMS key ID (required)
|
|
402
|
+
* @param {Object} [options] - Options (same as encryptKMSObject)
|
|
403
|
+
* @returns {Promise<Object>} Result with encrypted YAML content
|
|
404
|
+
*/
|
|
405
|
+
async function encryptKMSYamlContent(content, kmsKeyId, options = {}) {
|
|
406
|
+
validateKmsKeyId(kmsKeyId);
|
|
407
|
+
|
|
408
|
+
if (!content || typeof content !== 'string') {
|
|
409
|
+
throw new ContentError(
|
|
410
|
+
'Content must be a non-empty string',
|
|
411
|
+
CONTENT_ERROR_CODES.EMPTY_CONTENT,
|
|
412
|
+
'yaml'
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Log warning if js-yaml not available
|
|
417
|
+
const opts = buildEncryptOptions(options);
|
|
418
|
+
if (!isJsYamlAvailable()) {
|
|
419
|
+
opts.logger?.warn?.(
|
|
420
|
+
'[encryptKMSYamlContent] js-yaml not installed. Using simple parser (limited features).'
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const obj = parseYaml(content);
|
|
425
|
+
const objectResult = await encryptKMSObject(obj, kmsKeyId, options);
|
|
426
|
+
|
|
427
|
+
return {
|
|
428
|
+
content: serializeYaml(objectResult.object),
|
|
429
|
+
encrypted: objectResult.encrypted,
|
|
430
|
+
skipped: objectResult.skipped,
|
|
431
|
+
failed: objectResult.failed
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Decrypt YAML content string using AWS KMS
|
|
437
|
+
*
|
|
438
|
+
* Uses js-yaml if installed, otherwise falls back to simple parser for basic YAML.
|
|
439
|
+
* Complex YAML features (anchors, multi-line strings) require js-yaml.
|
|
440
|
+
*
|
|
441
|
+
* @param {string} content - Encrypted YAML content string
|
|
442
|
+
* @param {string} kmsKeyId - KMS key ID (required)
|
|
443
|
+
* @param {Object} [options] - Options (same as decryptKMSObject)
|
|
444
|
+
* @returns {Promise<Object>} Result with decrypted YAML content
|
|
445
|
+
*/
|
|
446
|
+
async function decryptKMSYamlContent(content, kmsKeyId, options = {}) {
|
|
447
|
+
validateKmsKeyId(kmsKeyId);
|
|
448
|
+
|
|
449
|
+
if (!content || typeof content !== 'string') {
|
|
450
|
+
throw new ContentError(
|
|
451
|
+
'Content must be a non-empty string',
|
|
452
|
+
CONTENT_ERROR_CODES.EMPTY_CONTENT,
|
|
453
|
+
'yaml'
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Log warning if js-yaml not available
|
|
458
|
+
const opts = buildDecryptOptions(options);
|
|
459
|
+
if (!isJsYamlAvailable()) {
|
|
460
|
+
opts.logger?.warn?.(
|
|
461
|
+
'[decryptKMSYamlContent] js-yaml not installed. Using simple parser (limited features).'
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const obj = parseYaml(content);
|
|
466
|
+
const objectResult = await decryptKMSObject(obj, kmsKeyId, options);
|
|
467
|
+
|
|
468
|
+
return {
|
|
469
|
+
content: serializeYaml(objectResult.object),
|
|
470
|
+
decrypted: objectResult.decrypted,
|
|
471
|
+
skipped: objectResult.skipped,
|
|
472
|
+
failed: objectResult.failed
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
477
|
+
// EXPORTS
|
|
478
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
479
|
+
|
|
480
|
+
module.exports = {
|
|
481
|
+
// ENV operations
|
|
482
|
+
encryptKMSEnvContent,
|
|
483
|
+
decryptKMSEnvContent,
|
|
484
|
+
parseEnvContent,
|
|
485
|
+
reconstructEnvContent,
|
|
486
|
+
|
|
487
|
+
// JSON operations
|
|
488
|
+
encryptKMSJsonContent,
|
|
489
|
+
decryptKMSJsonContent,
|
|
490
|
+
|
|
491
|
+
// YAML operations
|
|
492
|
+
encryptKMSYamlContent,
|
|
493
|
+
decryptKMSYamlContent
|
|
494
|
+
};
|