@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/src/options.js ADDED
@@ -0,0 +1,541 @@
1
+ /**
2
+ * @faizahmed/secret-keystore - Options Architecture
3
+ *
4
+ * Layered/namespaced options structure for all library functions.
5
+ * This module provides type definitions, defaults, and validation.
6
+ */
7
+
8
+ const { ValidationError, VALIDATION_ERROR_CODES } = require('./errors');
9
+
10
+ // ═══════════════════════════════════════════════════════════════════════════
11
+ // CONSTANTS
12
+ // ═══════════════════════════════════════════════════════════════════════════
13
+
14
+ /**
15
+ * Reserved keys that should NEVER be encrypted.
16
+ * These are required for the encryption/decryption process itself.
17
+ */
18
+ const RESERVED_KEYS = [
19
+ 'KMS_KEY_ID',
20
+ 'AWS_REGION',
21
+ 'AWS_ACCESS_KEY_ID',
22
+ 'AWS_SECRET_ACCESS_KEY',
23
+ 'AWS_SESSION_TOKEN'
24
+ ];
25
+
26
+ // ═══════════════════════════════════════════════════════════════════════════
27
+ // DEFAULT VALUES
28
+ // ═══════════════════════════════════════════════════════════════════════════
29
+
30
+ const DEFAULT_AWS_OPTIONS = {
31
+ credentials: null, // Uses IAM role by default
32
+ region: null // Uses AWS_REGION env var or SDK default
33
+ };
34
+
35
+ const DEFAULT_ATTESTATION_OPTIONS = {
36
+ enabled: false,
37
+ required: false,
38
+ fallbackToStandard: true,
39
+ // Full attestation mode (default) - library handles key pair generation + CMS unwrap
40
+ endpoint: null, // Attestation endpoint URL (default: localhost:50123)
41
+ timeout: 10000, // Attestation request timeout in ms
42
+ userData: '', // User data to include in attestation document
43
+ // Legacy mode - pre-generated document + private key
44
+ document: null, // Buffer | (() => Buffer) | (() => Promise<Buffer>) | string (base64)
45
+ privateKey: null, // PEM private key for CMS unwrap (required with document in legacy mode)
46
+ // KMS options
47
+ encryptionContext: null // KMS encryption context
48
+ };
49
+
50
+ const DEFAULT_COMMON_OPTIONS = {
51
+ aws: { ...DEFAULT_AWS_OPTIONS },
52
+ attestation: { ...DEFAULT_ATTESTATION_OPTIONS },
53
+ logger: null, // Uses console if not provided
54
+ logLevel: 'info' // 'debug' | 'info' | 'warn' | 'error' | 'silent'
55
+ };
56
+
57
+ const DEFAULT_ENCRYPT_OPTIONS = {
58
+ ...DEFAULT_COMMON_OPTIONS,
59
+ output: {
60
+ format: 'prefixed' // 'base64' | 'buffer' | 'prefixed' (ENC[...])
61
+ },
62
+ skip: {
63
+ empty: true,
64
+ alreadyEncrypted: true
65
+ },
66
+ continueOnError: false
67
+ };
68
+
69
+ const DEFAULT_DECRYPT_OPTIONS = {
70
+ ...DEFAULT_COMMON_OPTIONS,
71
+ input: {
72
+ format: 'auto' // 'auto' | 'base64' | 'buffer' | 'prefixed'
73
+ },
74
+ skip: {
75
+ unencrypted: true
76
+ },
77
+ validation: {
78
+ format: true,
79
+ kmsKeyMatch: true
80
+ },
81
+ continueOnError: false
82
+ };
83
+
84
+ const DEFAULT_PATH_SELECTION_OPTIONS = {
85
+ paths: null, // Explicit dot-notation paths
86
+ patterns: null, // Glob patterns using ** for any-depth matching
87
+ exclude: {
88
+ paths: null,
89
+ patterns: null
90
+ }
91
+ };
92
+
93
+ const DEFAULT_CONTENT_OPTIONS = {
94
+ preserve: {
95
+ comments: true,
96
+ formatting: true,
97
+ anchors: true // YAML only
98
+ }
99
+ };
100
+
101
+ const DEFAULT_KEYSTORE_OPTIONS = {
102
+ ...DEFAULT_DECRYPT_OPTIONS,
103
+ ...DEFAULT_PATH_SELECTION_OPTIONS,
104
+ security: {
105
+ inMemoryEncryption: true,
106
+ secureWipe: true
107
+ },
108
+ access: {
109
+ ttl: null, // Secret expiry (ms), null = never
110
+ autoRefresh: true,
111
+ accessLimit: null, // Max access count per key
112
+ clearOnAccess: false
113
+ },
114
+ validation: {
115
+ noProcessEnvLeak: true,
116
+ throwOnMissingKey: false
117
+ },
118
+ retry: {
119
+ attempts: 3,
120
+ delay: 1000, // ms
121
+ backoff: 'exponential' // 'linear' | 'exponential'
122
+ }
123
+ };
124
+
125
+ // ═══════════════════════════════════════════════════════════════════════════
126
+ // HELPER FUNCTIONS
127
+ // ═══════════════════════════════════════════════════════════════════════════
128
+
129
+ /**
130
+ * Deep merge two objects
131
+ * @param {Object} target - Target object
132
+ * @param {Object} source - Source object
133
+ * @returns {Object}
134
+ */
135
+ function deepMerge(target, source) {
136
+ const result = { ...target };
137
+
138
+ for (const key of Object.keys(source)) {
139
+ if (source[key] === undefined) {
140
+ continue;
141
+ }
142
+
143
+ if (
144
+ source[key] !== null &&
145
+ typeof source[key] === 'object' &&
146
+ !Array.isArray(source[key]) &&
147
+ !Buffer.isBuffer(source[key]) &&
148
+ typeof source[key] !== 'function'
149
+ ) {
150
+ result[key] = deepMerge(target[key] || {}, source[key]);
151
+ } else {
152
+ result[key] = source[key];
153
+ }
154
+ }
155
+
156
+ return result;
157
+ }
158
+
159
+ /**
160
+ * Create a logger with the specified level
161
+ * @param {Object} baseLogger - Base logger (console or custom)
162
+ * @param {string} logLevel - Log level
163
+ * @returns {Object}
164
+ */
165
+ function createLogger(baseLogger, logLevel = 'info') {
166
+ const logger = baseLogger || console;
167
+ const levels = ['debug', 'info', 'warn', 'error', 'silent'];
168
+ const levelIndex = levels.indexOf(logLevel);
169
+
170
+ return {
171
+ debug: (...args) => levelIndex <= 0 && logger.debug?.(...args),
172
+ info: (...args) => levelIndex <= 1 && logger.info?.(...args),
173
+ warn: (...args) => levelIndex <= 2 && logger.warn?.(...args),
174
+ error: (...args) => levelIndex <= 3 && logger.error?.(...args)
175
+ };
176
+ }
177
+
178
+ // ═══════════════════════════════════════════════════════════════════════════
179
+ // VALIDATION FUNCTIONS
180
+ // ═══════════════════════════════════════════════════════════════════════════
181
+
182
+ /**
183
+ * Validate that kmsKeyId is provided
184
+ * @param {string} kmsKeyId - KMS key ID
185
+ * @throws {ValidationError}
186
+ */
187
+ function validateKmsKeyId(kmsKeyId) {
188
+ if (!kmsKeyId || typeof kmsKeyId !== 'string') {
189
+ throw new ValidationError(
190
+ 'kmsKeyId is required and must be a non-empty string',
191
+ VALIDATION_ERROR_CODES.KMS_KEY_REQUIRED,
192
+ 'kmsKeyId'
193
+ );
194
+ }
195
+
196
+ // Basic format validation
197
+ const isArn = kmsKeyId.startsWith('arn:aws:kms:');
198
+ const isAlias = kmsKeyId.startsWith('alias/');
199
+ const isUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(kmsKeyId);
200
+
201
+ if (!isArn && !isAlias && !isUuid) {
202
+ throw new ValidationError(
203
+ 'kmsKeyId must be an ARN (arn:aws:kms:...), alias (alias/...), or UUID',
204
+ VALIDATION_ERROR_CODES.INVALID_VALUE,
205
+ 'kmsKeyId'
206
+ );
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Validate AWS options
212
+ * @param {Object} aws - AWS options
213
+ * @throws {ValidationError}
214
+ */
215
+ function validateAwsOptions(aws) {
216
+ if (!aws) return;
217
+
218
+ if (aws.credentials) {
219
+ const { accessKeyId, secretAccessKey } = aws.credentials;
220
+ if (accessKeyId && !secretAccessKey) {
221
+ throw new ValidationError(
222
+ 'secretAccessKey is required when accessKeyId is provided',
223
+ VALIDATION_ERROR_CODES.REQUIRED_FIELD,
224
+ 'aws.credentials.secretAccessKey'
225
+ );
226
+ }
227
+ if (!accessKeyId && secretAccessKey) {
228
+ throw new ValidationError(
229
+ 'accessKeyId is required when secretAccessKey is provided',
230
+ VALIDATION_ERROR_CODES.REQUIRED_FIELD,
231
+ 'aws.credentials.accessKeyId'
232
+ );
233
+ }
234
+ }
235
+
236
+ if (aws.region && typeof aws.region !== 'string') {
237
+ throw new ValidationError(
238
+ 'aws.region must be a string',
239
+ VALIDATION_ERROR_CODES.INVALID_TYPE,
240
+ 'aws.region'
241
+ );
242
+ }
243
+ }
244
+
245
+ /**
246
+ * Validate attestation document format (legacy mode)
247
+ * @private
248
+ */
249
+ function validateAttestationDocument(attestation) {
250
+ const isBuffer = Buffer.isBuffer(attestation.document);
251
+ const isFunction = typeof attestation.document === 'function';
252
+ const isString = typeof attestation.document === 'string';
253
+
254
+ if (!isBuffer && !isFunction && !isString) {
255
+ throw new ValidationError(
256
+ 'attestation.document must be a Buffer, string (base64), or function',
257
+ VALIDATION_ERROR_CODES.INVALID_TYPE,
258
+ 'attestation.document'
259
+ );
260
+ }
261
+
262
+ // In legacy mode, privateKey is required with document
263
+ if (!attestation.privateKey) {
264
+ throw new ValidationError(
265
+ 'attestation.privateKey is required when attestation.document is provided',
266
+ VALIDATION_ERROR_CODES.REQUIRED_FIELD,
267
+ 'attestation.privateKey'
268
+ );
269
+ }
270
+ }
271
+
272
+ /**
273
+ * Validate private key format
274
+ * @private
275
+ */
276
+ function validatePrivateKeyFormat(privateKey) {
277
+ const isValidPem =
278
+ typeof privateKey === 'string' && privateKey.includes('-----BEGIN PRIVATE KEY-----');
279
+
280
+ if (!isValidPem) {
281
+ throw new ValidationError(
282
+ 'attestation.privateKey must be a PEM-encoded PKCS#8 private key',
283
+ VALIDATION_ERROR_CODES.INVALID_TYPE,
284
+ 'attestation.privateKey'
285
+ );
286
+ }
287
+ }
288
+
289
+ /**
290
+ * Validate attestation options
291
+ * @param {Object} attestation - Attestation options
292
+ * @throws {ValidationError}
293
+ */
294
+ function validateAttestationOptions(attestation) {
295
+ if (!attestation) return;
296
+
297
+ if (attestation.required && !attestation.enabled) {
298
+ throw new ValidationError(
299
+ 'attestation.enabled must be true when attestation.required is true',
300
+ VALIDATION_ERROR_CODES.INVALID_OPTIONS,
301
+ 'attestation'
302
+ );
303
+ }
304
+
305
+ // Validate legacy mode (document + privateKey)
306
+ if (attestation.document) {
307
+ validateAttestationDocument(attestation);
308
+ validatePrivateKeyFormat(attestation.privateKey);
309
+ }
310
+
311
+ // Validate endpoint URL
312
+ if (attestation.endpoint) {
313
+ try {
314
+ new URL(attestation.endpoint);
315
+ } catch {
316
+ throw new ValidationError(
317
+ 'attestation.endpoint must be a valid URL',
318
+ VALIDATION_ERROR_CODES.INVALID_VALUE,
319
+ 'attestation.endpoint'
320
+ );
321
+ }
322
+ }
323
+
324
+ // Validate timeout
325
+ const hasTimeout = attestation.timeout !== undefined && attestation.timeout !== null;
326
+ const isValidTimeout = typeof attestation.timeout === 'number' && attestation.timeout >= 0;
327
+
328
+ if (hasTimeout && !isValidTimeout) {
329
+ throw new ValidationError(
330
+ 'attestation.timeout must be a positive number',
331
+ VALIDATION_ERROR_CODES.INVALID_VALUE,
332
+ 'attestation.timeout'
333
+ );
334
+ }
335
+ }
336
+
337
+ /**
338
+ * Validate path selection options
339
+ * @param {Object} options - Path selection options
340
+ * @throws {ValidationError}
341
+ */
342
+ function validatePathSelectionOptions(options) {
343
+ if (options.paths && !Array.isArray(options.paths)) {
344
+ throw new ValidationError(
345
+ 'paths must be an array of strings',
346
+ VALIDATION_ERROR_CODES.INVALID_TYPE,
347
+ 'paths'
348
+ );
349
+ }
350
+
351
+ if (options.patterns && !Array.isArray(options.patterns)) {
352
+ throw new ValidationError(
353
+ 'patterns must be an array of strings',
354
+ VALIDATION_ERROR_CODES.INVALID_TYPE,
355
+ 'patterns'
356
+ );
357
+ }
358
+
359
+ // Validate patterns only use ** (not single *)
360
+ if (options.patterns) {
361
+ for (const pattern of options.patterns) {
362
+ // Check for single * that's not part of **
363
+ if (/(?<!\*)\*(?!\*)/.test(pattern)) {
364
+ throw new ValidationError(
365
+ `Invalid pattern "${pattern}": only ** (any-depth) is supported, not * (single-level)`,
366
+ VALIDATION_ERROR_CODES.INVALID_VALUE,
367
+ 'patterns'
368
+ );
369
+ }
370
+ }
371
+ }
372
+ }
373
+
374
+ /**
375
+ * Validate common options
376
+ * @param {Object} options - Options to validate
377
+ * @throws {ValidationError}
378
+ */
379
+ function validateCommonOptions(options) {
380
+ if (!options) return;
381
+
382
+ validateAwsOptions(options.aws);
383
+ validateAttestationOptions(options.attestation);
384
+
385
+ if (
386
+ options.logLevel &&
387
+ !['debug', 'info', 'warn', 'error', 'silent'].includes(options.logLevel)
388
+ ) {
389
+ throw new ValidationError(
390
+ 'logLevel must be one of: debug, info, warn, error, silent',
391
+ VALIDATION_ERROR_CODES.INVALID_VALUE,
392
+ 'logLevel'
393
+ );
394
+ }
395
+ }
396
+
397
+ // ═══════════════════════════════════════════════════════════════════════════
398
+ // OPTIONS BUILDERS
399
+ // ═══════════════════════════════════════════════════════════════════════════
400
+
401
+ /**
402
+ * Build common options with defaults
403
+ * @param {Object} options - User provided options
404
+ * @returns {Object}
405
+ */
406
+ function buildCommonOptions(options = {}) {
407
+ const merged = deepMerge(DEFAULT_COMMON_OPTIONS, options);
408
+ merged.logger = createLogger(options.logger, merged.logLevel);
409
+ validateCommonOptions(merged);
410
+ return merged;
411
+ }
412
+
413
+ /**
414
+ * Build encrypt options with defaults
415
+ * @param {Object} options - User provided options
416
+ * @returns {Object}
417
+ */
418
+ function buildEncryptOptions(options = {}) {
419
+ const merged = deepMerge(DEFAULT_ENCRYPT_OPTIONS, options);
420
+ merged.logger = createLogger(options.logger, merged.logLevel);
421
+ validateCommonOptions(merged);
422
+ return merged;
423
+ }
424
+
425
+ /**
426
+ * Build decrypt options with defaults
427
+ * @param {Object} options - User provided options
428
+ * @returns {Object}
429
+ */
430
+ function buildDecryptOptions(options = {}) {
431
+ const merged = deepMerge(DEFAULT_DECRYPT_OPTIONS, options);
432
+ merged.logger = createLogger(options.logger, merged.logLevel);
433
+ validateCommonOptions(merged);
434
+ return merged;
435
+ }
436
+
437
+ /**
438
+ * Build path selection options with defaults
439
+ * @param {Object} options - User provided options
440
+ * @returns {Object}
441
+ */
442
+ function buildPathSelectionOptions(options = {}) {
443
+ const merged = deepMerge(DEFAULT_PATH_SELECTION_OPTIONS, options);
444
+ validatePathSelectionOptions(merged);
445
+ return merged;
446
+ }
447
+
448
+ /**
449
+ * Build content options with defaults
450
+ * @param {Object} options - User provided options
451
+ * @returns {Object}
452
+ */
453
+ function buildContentOptions(options = {}) {
454
+ return deepMerge(DEFAULT_CONTENT_OPTIONS, options);
455
+ }
456
+
457
+ /**
458
+ * Build keystore options with defaults
459
+ * @param {Object} options - User provided options
460
+ * @returns {Object}
461
+ */
462
+ function buildKeystoreOptions(options = {}) {
463
+ const merged = deepMerge(DEFAULT_KEYSTORE_OPTIONS, options);
464
+ merged.logger = createLogger(options.logger, merged.logLevel);
465
+ validateCommonOptions(merged);
466
+ validatePathSelectionOptions(merged);
467
+ return merged;
468
+ }
469
+
470
+ // ═══════════════════════════════════════════════════════════════════════════
471
+ // AWS SDK OPTIONS BUILDER
472
+ // ═══════════════════════════════════════════════════════════════════════════
473
+
474
+ /**
475
+ * Build AWS SDK client options from library options
476
+ * @param {Object} options - Library options (with aws namespace)
477
+ * @returns {Object} AWS SDK client options
478
+ */
479
+ function buildAwsSdkOptions(options = {}) {
480
+ const sdkOptions = {};
481
+ const aws = options.aws || options;
482
+
483
+ // Credentials: Only set if explicitly provided
484
+ if (aws.credentials?.accessKeyId && aws.credentials?.secretAccessKey) {
485
+ sdkOptions.credentials = {
486
+ accessKeyId: aws.credentials.accessKeyId,
487
+ secretAccessKey: aws.credentials.secretAccessKey
488
+ };
489
+ if (aws.credentials.sessionToken) {
490
+ sdkOptions.credentials.sessionToken = aws.credentials.sessionToken;
491
+ }
492
+ }
493
+
494
+ // Region: Use provided > env var > SDK default
495
+ if (aws.region) {
496
+ sdkOptions.region = aws.region;
497
+ } else if (process.env.AWS_REGION) {
498
+ sdkOptions.region = process.env.AWS_REGION;
499
+ }
500
+
501
+ return sdkOptions;
502
+ }
503
+
504
+ // ═══════════════════════════════════════════════════════════════════════════
505
+ // EXPORTS
506
+ // ═══════════════════════════════════════════════════════════════════════════
507
+
508
+ module.exports = {
509
+ // Constants
510
+ RESERVED_KEYS,
511
+
512
+ // Defaults
513
+ DEFAULT_AWS_OPTIONS,
514
+ DEFAULT_ATTESTATION_OPTIONS,
515
+ DEFAULT_COMMON_OPTIONS,
516
+ DEFAULT_ENCRYPT_OPTIONS,
517
+ DEFAULT_DECRYPT_OPTIONS,
518
+ DEFAULT_PATH_SELECTION_OPTIONS,
519
+ DEFAULT_CONTENT_OPTIONS,
520
+ DEFAULT_KEYSTORE_OPTIONS,
521
+
522
+ // Helpers
523
+ deepMerge,
524
+ createLogger,
525
+
526
+ // Validation
527
+ validateKmsKeyId,
528
+ validateAwsOptions,
529
+ validateAttestationOptions,
530
+ validatePathSelectionOptions,
531
+ validateCommonOptions,
532
+
533
+ // Builders
534
+ buildCommonOptions,
535
+ buildEncryptOptions,
536
+ buildDecryptOptions,
537
+ buildPathSelectionOptions,
538
+ buildContentOptions,
539
+ buildKeystoreOptions,
540
+ buildAwsSdkOptions
541
+ };