@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.
@@ -0,0 +1,232 @@
1
+ /**
2
+ * @faizahmed/secret-keystore - Object-Based Operations
3
+ *
4
+ * Functions for encrypting/decrypting nested objects using path patterns.
5
+ * Supports ** pattern matching for any-depth selection.
6
+ */
7
+
8
+ const { encryptKMSValue, decryptKMSValue, isAlreadyEncrypted } = require('./kms');
9
+ const { getAllPaths, filterPaths, getByPath, setByPath } = require('./path-matcher');
10
+ const { validateKmsKeyId, buildEncryptOptions, buildDecryptOptions } = require('./options');
11
+ const {
12
+ EncryptionError,
13
+ DecryptionError,
14
+ ENCRYPTION_ERROR_CODES,
15
+ DECRYPTION_ERROR_CODES
16
+ } = require('./errors');
17
+
18
+ // ═══════════════════════════════════════════════════════════════════════════
19
+ // ENCRYPT OBJECT
20
+ // ═══════════════════════════════════════════════════════════════════════════
21
+
22
+ /**
23
+ * Encrypt values at selected paths in a nested object using AWS KMS
24
+ *
25
+ * @param {Object} obj - Source object
26
+ * @param {string} kmsKeyId - KMS key ID (required)
27
+ * @param {Object} [options] - Options
28
+ * @param {Object} [options.aws] - AWS options
29
+ * @param {string[]} [options.paths] - Explicit paths to encrypt
30
+ * @param {string[]} [options.patterns] - Patterns (** only) to match
31
+ * @param {Object} [options.exclude] - Exclusions
32
+ * @param {Object} [options.skip] - Skip options
33
+ * @param {boolean} [options.continueOnError] - Continue on errors
34
+ * @param {Object} [options.output] - Output format options
35
+ * @returns {Promise<Object>} Result with encrypted object
36
+ *
37
+ * @example
38
+ * const result = await encryptKMSObject(config, kmsKeyId, {
39
+ * patterns: ['**.password', '**.secret_key'],
40
+ * exclude: { paths: ['kms.key_id'] }
41
+ * });
42
+ */
43
+ async function encryptKMSObject(obj, kmsKeyId, options = {}) {
44
+ validateKmsKeyId(kmsKeyId);
45
+
46
+ const opts = buildEncryptOptions(options);
47
+ const logger = opts.logger;
48
+
49
+ // Deep clone the object
50
+ const resultObject = structuredClone(obj);
51
+
52
+ // Get all paths and filter based on selection
53
+ const allPaths = getAllPaths(obj);
54
+ const selectedPaths = filterPaths(allPaths, {
55
+ paths: options.paths,
56
+ patterns: options.patterns,
57
+ exclude: options.exclude
58
+ });
59
+
60
+ logger?.debug?.(
61
+ `[encryptKMSObject] Selected ${selectedPaths.length} paths from ${allPaths.length} total`
62
+ );
63
+
64
+ const result = {
65
+ object: resultObject,
66
+ encrypted: [],
67
+ skipped: [],
68
+ failed: []
69
+ };
70
+
71
+ const skipEmpty = opts.skip?.empty !== false;
72
+ const skipAlreadyEncrypted = opts.skip?.alreadyEncrypted !== false;
73
+ const continueOnError = opts.continueOnError === true;
74
+
75
+ for (const path of selectedPaths) {
76
+ const value = getByPath(obj, path);
77
+
78
+ // Only encrypt string values
79
+ if (typeof value !== 'string') {
80
+ logger?.debug?.(`[encryptKMSObject] Skipping non-string at ${path}`);
81
+ result.skipped.push(path);
82
+ continue;
83
+ }
84
+
85
+ // Skip empty values
86
+ if (skipEmpty && (!value || value.trim() === '')) {
87
+ logger?.debug?.(`[encryptKMSObject] Skipping empty value at ${path}`);
88
+ result.skipped.push(path);
89
+ continue;
90
+ }
91
+
92
+ // Skip already encrypted
93
+ if (skipAlreadyEncrypted && isAlreadyEncrypted(value)) {
94
+ logger?.debug?.(`[encryptKMSObject] Skipping already encrypted at ${path}`);
95
+ result.skipped.push(path);
96
+ continue;
97
+ }
98
+
99
+ try {
100
+ const encrypted = await encryptKMSValue(value, kmsKeyId, opts);
101
+ setByPath(resultObject, path, encrypted);
102
+ result.encrypted.push(path);
103
+ logger?.info?.(`[encryptKMSObject] Encrypted: ${path}`);
104
+ } catch (error) {
105
+ if (continueOnError) {
106
+ result.failed.push({ path, error });
107
+ logger?.warn?.(`[encryptKMSObject] Failed to encrypt ${path}: ${error.message}`);
108
+ } else {
109
+ throw new EncryptionError(
110
+ `Failed to encrypt path: ${path}`,
111
+ ENCRYPTION_ERROR_CODES.FAILED,
112
+ path,
113
+ error
114
+ );
115
+ }
116
+ }
117
+ }
118
+
119
+ logger?.info?.(
120
+ `[encryptKMSObject] Complete: ${result.encrypted.length} encrypted, ${result.skipped.length} skipped, ${result.failed.length} failed`
121
+ );
122
+
123
+ return result;
124
+ }
125
+
126
+ // ═══════════════════════════════════════════════════════════════════════════
127
+ // DECRYPT OBJECT
128
+ // ═══════════════════════════════════════════════════════════════════════════
129
+
130
+ /**
131
+ * Decrypt values at selected paths in a nested object using AWS KMS
132
+ *
133
+ * @param {Object} obj - Source object with encrypted values
134
+ * @param {string} kmsKeyId - KMS key ID (required)
135
+ * @param {Object} [options] - Options
136
+ * @param {Object} [options.aws] - AWS options
137
+ * @param {Object} [options.attestation] - Attestation options
138
+ * @param {string[]} [options.paths] - Explicit paths to decrypt
139
+ * @param {string[]} [options.patterns] - Patterns (** only) to match
140
+ * @param {Object} [options.exclude] - Exclusions
141
+ * @param {Object} [options.skip] - Skip options
142
+ * @param {boolean} [options.continueOnError] - Continue on errors
143
+ * @returns {Promise<Object>} Result with decrypted object
144
+ *
145
+ * @example
146
+ * const result = await decryptKMSObject(encryptedConfig, kmsKeyId, {
147
+ * attestation: { enabled: true, document: getAttestationDoc }
148
+ * });
149
+ */
150
+ async function decryptKMSObject(obj, kmsKeyId, options = {}) {
151
+ validateKmsKeyId(kmsKeyId);
152
+
153
+ const opts = buildDecryptOptions(options);
154
+ const logger = opts.logger;
155
+
156
+ // Deep clone the object
157
+ const resultObject = structuredClone(obj);
158
+
159
+ // Get all paths and filter based on selection
160
+ const allPaths = getAllPaths(obj);
161
+ const selectedPaths = filterPaths(allPaths, {
162
+ paths: options.paths,
163
+ patterns: options.patterns,
164
+ exclude: options.exclude
165
+ });
166
+
167
+ logger?.debug?.(
168
+ `[decryptKMSObject] Selected ${selectedPaths.length} paths from ${allPaths.length} total`
169
+ );
170
+
171
+ const result = {
172
+ object: resultObject,
173
+ decrypted: [],
174
+ skipped: [],
175
+ failed: []
176
+ };
177
+
178
+ const skipUnencrypted = opts.skip?.unencrypted !== false;
179
+ const continueOnError = opts.continueOnError === true;
180
+
181
+ for (const path of selectedPaths) {
182
+ const value = getByPath(obj, path);
183
+
184
+ // Only decrypt string values
185
+ if (typeof value !== 'string') {
186
+ logger?.debug?.(`[decryptKMSObject] Skipping non-string at ${path}`);
187
+ result.skipped.push(path);
188
+ continue;
189
+ }
190
+
191
+ // Skip unencrypted values
192
+ if (skipUnencrypted && !isAlreadyEncrypted(value)) {
193
+ logger?.debug?.(`[decryptKMSObject] Skipping unencrypted at ${path}`);
194
+ result.skipped.push(path);
195
+ continue;
196
+ }
197
+
198
+ try {
199
+ const decrypted = await decryptKMSValue(value, kmsKeyId, opts);
200
+ setByPath(resultObject, path, decrypted);
201
+ result.decrypted.push(path);
202
+ logger?.info?.(`[decryptKMSObject] Decrypted: ${path}`);
203
+ } catch (error) {
204
+ if (continueOnError) {
205
+ result.failed.push({ path, error });
206
+ logger?.warn?.(`[decryptKMSObject] Failed to decrypt ${path}: ${error.message}`);
207
+ } else {
208
+ throw new DecryptionError(
209
+ `Failed to decrypt path: ${path}`,
210
+ DECRYPTION_ERROR_CODES.FAILED,
211
+ path,
212
+ error
213
+ );
214
+ }
215
+ }
216
+ }
217
+
218
+ logger?.info?.(
219
+ `[decryptKMSObject] Complete: ${result.decrypted.length} decrypted, ${result.skipped.length} skipped, ${result.failed.length} failed`
220
+ );
221
+
222
+ return result;
223
+ }
224
+
225
+ // ═══════════════════════════════════════════════════════════════════════════
226
+ // EXPORTS
227
+ // ═══════════════════════════════════════════════════════════════════════════
228
+
229
+ module.exports = {
230
+ encryptKMSObject,
231
+ decryptKMSObject
232
+ };