@winible/winible-typed 2.118.0 → 2.120.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.
@@ -20,12 +20,19 @@ describe('Lambda Initialization', () => {
20
20
 
21
21
  // Clear environment variables
22
22
  delete process.env.LAMBDA_SECRET_ARN;
23
+ delete process.env.LAMBDA_SECRET_NAME;
24
+ delete process.env.LAMBDA_SECRET;
23
25
  delete process.env.AWS_REGION;
24
26
  delete process.env.TEST_VAR_1;
25
27
  delete process.env.TEST_VAR_2;
26
28
  delete process.env.VAR1;
27
29
  delete process.env.VAR2;
28
30
  delete process.env.VAR3;
31
+ delete process.env.DB_HOST;
32
+ delete process.env.DB_USER;
33
+ delete process.env.DB_PASSWORD;
34
+ delete process.env.REDIS_HOST;
35
+ delete process.env.REDIS_PASSWORD;
29
36
  });
30
37
 
31
38
  afterEach(() => {
@@ -116,8 +123,8 @@ describe('Lambda Initialization', () => {
116
123
  );
117
124
  });
118
125
 
119
- it('should use default secret ARN from environment', async () => {
120
- process.env.LAMBDA_SECRET_ARN = 'env-secret-arn';
126
+ it('should use default secret ARN from LAMBDA_SECRET_NAME environment variable', async () => {
127
+ process.env.LAMBDA_SECRET_NAME = 'env-secret-name';
121
128
  const mockCredentials = { TEST_VAR_1: 'value1' };
122
129
 
123
130
  mockLoadCredentials.mockResolvedValue(mockCredentials);
@@ -127,7 +134,42 @@ describe('Lambda Initialization', () => {
127
134
  });
128
135
 
129
136
  expect(mockLoadCredentials).toHaveBeenCalledWith(
130
- 'env-secret-arn',
137
+ 'env-secret-name',
138
+ 'us-east-1',
139
+ ['TEST_VAR_1']
140
+ );
141
+ });
142
+
143
+ it('should use LAMBDA_SECRET as fallback if LAMBDA_SECRET_NAME is not set', async () => {
144
+ process.env.LAMBDA_SECRET = 'env-secret-fallback';
145
+ const mockCredentials = { TEST_VAR_1: 'value1' };
146
+
147
+ mockLoadCredentials.mockResolvedValue(mockCredentials);
148
+
149
+ await initializeLambda<TestCredentials>({
150
+ requiredEnvVars: ['TEST_VAR_1'],
151
+ });
152
+
153
+ expect(mockLoadCredentials).toHaveBeenCalledWith(
154
+ 'env-secret-fallback',
155
+ 'us-east-1',
156
+ ['TEST_VAR_1']
157
+ );
158
+ });
159
+
160
+ it('should prefer LAMBDA_SECRET_NAME over LAMBDA_SECRET', async () => {
161
+ process.env.LAMBDA_SECRET_NAME = 'preferred-secret';
162
+ process.env.LAMBDA_SECRET = 'fallback-secret';
163
+ const mockCredentials = { TEST_VAR_1: 'value1' };
164
+
165
+ mockLoadCredentials.mockResolvedValue(mockCredentials);
166
+
167
+ await initializeLambda<TestCredentials>({
168
+ requiredEnvVars: ['TEST_VAR_1'],
169
+ });
170
+
171
+ expect(mockLoadCredentials).toHaveBeenCalledWith(
172
+ 'preferred-secret',
131
173
  'us-east-1',
132
174
  ['TEST_VAR_1']
133
175
  );
@@ -302,9 +344,9 @@ describe('Lambda Initialization', () => {
302
344
 
303
345
  describe('integration scenarios', () => {
304
346
  it('should handle a typical Lambda initialization flow', async () => {
305
- // Setup: environment has AWS region and secret ARN
347
+ // Setup: environment has AWS region and secret name
306
348
  process.env.AWS_REGION = 'us-east-1';
307
- process.env.LAMBDA_SECRET_ARN = 'arn:aws:secretsmanager:us-east-1:123456789:secret:my-secret';
349
+ process.env.LAMBDA_SECRET_NAME = 'my-lambda-secret';
308
350
 
309
351
  const mockCredentials = {
310
352
  DB_HOST: 'localhost',
@@ -329,5 +371,233 @@ describe('Lambda Initialization', () => {
329
371
  expect(process.env.DB_PASSWORD).toBe('secret');
330
372
  });
331
373
  });
374
+
375
+ describe('Multi-secret mode', () => {
376
+ interface DatabaseCredentials {
377
+ DB_HOST: string;
378
+ DB_USER: string;
379
+ DB_PASSWORD: string;
380
+ }
381
+
382
+ interface RedisCredentials {
383
+ REDIS_HOST: string;
384
+ REDIS_PASSWORD: string;
385
+ }
386
+
387
+ it('should load credentials from multiple secrets', async () => {
388
+ const dbCredentials = {
389
+ DB_HOST: 'db.example.com',
390
+ DB_USER: 'dbuser',
391
+ DB_PASSWORD: 'dbpass',
392
+ };
393
+
394
+ const redisCredentials = {
395
+ REDIS_HOST: 'redis.example.com',
396
+ REDIS_PASSWORD: 'redispass',
397
+ };
398
+
399
+ mockLoadCredentials
400
+ .mockResolvedValueOnce(dbCredentials)
401
+ .mockResolvedValueOnce(redisCredentials);
402
+
403
+ await initializeLambda({
404
+ secrets: [
405
+ {
406
+ secretArn: 'db-secret-arn',
407
+ requiredEnvVars: ['DB_HOST', 'DB_USER', 'DB_PASSWORD'] as (keyof DatabaseCredentials)[],
408
+ },
409
+ {
410
+ secretArn: 'redis-secret-arn',
411
+ requiredEnvVars: ['REDIS_HOST', 'REDIS_PASSWORD'] as (keyof RedisCredentials)[],
412
+ },
413
+ ],
414
+ });
415
+
416
+ // Verify loadCredentials was called for each secret
417
+ expect(mockLoadCredentials).toHaveBeenCalledTimes(2);
418
+ expect(mockLoadCredentials).toHaveBeenNthCalledWith(
419
+ 1,
420
+ 'db-secret-arn',
421
+ 'us-east-1',
422
+ ['DB_HOST', 'DB_USER', 'DB_PASSWORD']
423
+ );
424
+ expect(mockLoadCredentials).toHaveBeenNthCalledWith(
425
+ 2,
426
+ 'redis-secret-arn',
427
+ 'us-east-1',
428
+ ['REDIS_HOST', 'REDIS_PASSWORD']
429
+ );
430
+
431
+ // Verify all credentials are set in process.env
432
+ expect(process.env.DB_HOST).toBe('db.example.com');
433
+ expect(process.env.DB_USER).toBe('dbuser');
434
+ expect(process.env.DB_PASSWORD).toBe('dbpass');
435
+ expect(process.env.REDIS_HOST).toBe('redis.example.com');
436
+ expect(process.env.REDIS_PASSWORD).toBe('redispass');
437
+
438
+ // Verify console output
439
+ expect(consoleLogSpy).toHaveBeenCalledWith('📦 Loading credentials from 2 secrets');
440
+ expect(consoleLogSpy).toHaveBeenCalledWith('✅ Lambda initialization complete');
441
+ });
442
+
443
+ it('should use custom region for multi-secret mode', async () => {
444
+ const mockCredentials = { DB_HOST: 'localhost' };
445
+
446
+ mockLoadCredentials.mockResolvedValue(mockCredentials);
447
+
448
+ await initializeLambda({
449
+ region: 'eu-west-1',
450
+ secrets: [
451
+ {
452
+ secretArn: 'test-secret',
453
+ requiredEnvVars: ['DB_HOST'],
454
+ },
455
+ ],
456
+ });
457
+
458
+ expect(mockLoadCredentials).toHaveBeenCalledWith(
459
+ 'test-secret',
460
+ 'eu-west-1',
461
+ ['DB_HOST']
462
+ );
463
+ });
464
+
465
+ it('should throw error if one secret fails to load', async () => {
466
+ mockLoadCredentials
467
+ .mockResolvedValueOnce({ DB_HOST: 'localhost' })
468
+ .mockResolvedValueOnce(null as any); // Second secret fails
469
+
470
+ await expect(
471
+ initializeLambda({
472
+ secrets: [
473
+ {
474
+ secretArn: 'db-secret',
475
+ requiredEnvVars: ['DB_HOST'],
476
+ },
477
+ {
478
+ secretArn: 'redis-secret',
479
+ requiredEnvVars: ['REDIS_HOST'],
480
+ },
481
+ ],
482
+ })
483
+ ).rejects.toThrow('Failed to load credentials from secret: redis-secret');
484
+
485
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
486
+ '❌ Lambda initialization failed:',
487
+ expect.any(Error)
488
+ );
489
+ });
490
+
491
+ it('should validate all environment variables across secrets', async () => {
492
+ mockLoadCredentials
493
+ .mockResolvedValueOnce({ DB_HOST: 'localhost' }) // Missing DB_USER
494
+ .mockResolvedValueOnce({ REDIS_HOST: 'localhost' });
495
+
496
+ await expect(
497
+ initializeLambda({
498
+ secrets: [
499
+ {
500
+ secretArn: 'db-secret',
501
+ requiredEnvVars: ['DB_HOST', 'DB_USER'],
502
+ },
503
+ {
504
+ secretArn: 'redis-secret',
505
+ requiredEnvVars: ['REDIS_HOST'],
506
+ },
507
+ ],
508
+ })
509
+ ).rejects.toThrow('Missing required environment variables: DB_USER');
510
+ });
511
+
512
+ it('should handle single secret in multi-secret mode', async () => {
513
+ const mockCredentials = { DB_HOST: 'localhost', DB_USER: 'admin' };
514
+
515
+ mockLoadCredentials.mockResolvedValue(mockCredentials);
516
+
517
+ await initializeLambda({
518
+ secrets: [
519
+ {
520
+ secretArn: 'my-secret',
521
+ requiredEnvVars: ['DB_HOST', 'DB_USER'],
522
+ },
523
+ ],
524
+ });
525
+
526
+ expect(mockLoadCredentials).toHaveBeenCalledTimes(1);
527
+ expect(process.env.DB_HOST).toBe('localhost');
528
+ expect(process.env.DB_USER).toBe('admin');
529
+ expect(consoleLogSpy).toHaveBeenCalledWith('📦 Loading credentials from 1 secrets');
530
+ });
531
+
532
+ it('should handle empty secrets array', async () => {
533
+ await expect(
534
+ initializeLambda({
535
+ secrets: [],
536
+ })
537
+ ).resolves.not.toThrow();
538
+
539
+ expect(mockLoadCredentials).not.toHaveBeenCalled();
540
+ });
541
+
542
+ it('should throw error if secret load fails with exception', async () => {
543
+ mockLoadCredentials.mockRejectedValue(new Error('Network error'));
544
+
545
+ await expect(
546
+ initializeLambda({
547
+ secrets: [
548
+ {
549
+ secretArn: 'my-secret',
550
+ requiredEnvVars: ['DB_HOST'],
551
+ },
552
+ ],
553
+ })
554
+ ).rejects.toThrow('Network error');
555
+ });
556
+
557
+ it('should skip initialization on subsequent calls in multi-secret mode', async () => {
558
+ const mockCredentials = { DB_HOST: 'localhost' };
559
+ mockLoadCredentials.mockResolvedValue(mockCredentials);
560
+
561
+ // First initialization
562
+ await initializeLambda({
563
+ secrets: [
564
+ {
565
+ secretArn: 'my-secret',
566
+ requiredEnvVars: ['DB_HOST'],
567
+ },
568
+ ],
569
+ });
570
+
571
+ // Second initialization
572
+ await initializeLambda({
573
+ secrets: [
574
+ {
575
+ secretArn: 'my-secret',
576
+ requiredEnvVars: ['DB_HOST'],
577
+ },
578
+ ],
579
+ });
580
+
581
+ expect(mockLoadCredentials).toHaveBeenCalledTimes(1);
582
+ expect(consoleLogSpy).toHaveBeenCalledWith('⚡ Lambda already initialized, skipping');
583
+ });
584
+
585
+ it('should handle multi-secret mode with no required vars', async () => {
586
+ const mockCredentials = {};
587
+
588
+ mockLoadCredentials.mockResolvedValue(mockCredentials);
589
+
590
+ await expect(
591
+ initializeLambda({
592
+ secrets: [
593
+ {
594
+ secretArn: 'my-secret',
595
+ requiredEnvVars: [],
596
+ },
597
+ ],
598
+ })
599
+ ).resolves.not.toThrow();
600
+ });
601
+ });
332
602
  });
333
603
  });
@@ -3,13 +3,22 @@ import { loadCredentials } from './secretsManager';
3
3
 
4
4
  let initialized = false;
5
5
 
6
- export interface InitializeLambdaOptions<T extends Record<string, any> = Record<string, any>> {
6
+ export interface SecretConfig<T extends Record<string, any> = Record<string, any>> {
7
7
  /** The ARN or name of the secret in AWS Secrets Manager */
8
+ secretArn: string;
9
+ /** List of required environment variable names to load from this secret */
10
+ requiredEnvVars: (keyof T)[];
11
+ }
12
+
13
+ export interface InitializeLambdaOptions<T extends Record<string, any> = Record<string, any>> {
14
+ /** The ARN or name of the secret in AWS Secrets Manager (single secret mode) */
8
15
  secretArn?: string;
9
16
  /** AWS region for Secrets Manager (defaults to 'us-east-1') */
10
17
  region?: string;
11
- /** List of required environment variable names to validate and load from secrets */
18
+ /** List of required environment variable names to validate and load from secrets (single secret mode) */
12
19
  requiredEnvVars?: (keyof T)[];
20
+ /** Array of secrets to load (multi-secret mode) */
21
+ secrets?: SecretConfig[];
13
22
  }
14
23
 
15
24
  /**
@@ -53,9 +62,10 @@ export function validateEnvConfig(requiredEnvVars: string[]): void {
53
62
  * This should be called once at the start of your Lambda handler
54
63
  *
55
64
  * @param options - Configuration options for initialization
56
- * @param options.secretArn - The ARN or name of the secret (defaults to process.env.LAMBDA_SECRET_ARN)
65
+ * @param options.secretArn - The ARN or name of the secret (defaults to process.env.LAMBDA_SECRET) - single secret mode
57
66
  * @param options.region - AWS region (defaults to process.env.AWS_REGION or 'us-east-1')
58
- * @param options.requiredEnvVars - Array of required environment variable names to validate and load from secrets
67
+ * @param options.requiredEnvVars - Array of required environment variable names to validate and load from secrets - single secret mode
68
+ * @param options.secrets - Array of secrets to load - multi-secret mode (takes precedence over single secret mode)
59
69
  */
60
70
  export async function initializeLambda<T extends Record<string, any>>(
61
71
  options: InitializeLambdaOptions<T> = {}
@@ -69,13 +79,45 @@ export async function initializeLambda<T extends Record<string, any>>(
69
79
 
70
80
  try {
71
81
  const {
72
- secretArn = process.env.LAMBDA_SECRET_NAME || process.env.LAMBDA_SECRET_ARN,
82
+ secretArn = process.env.LAMBDA_SECRET_NAME || process.env.LAMBDA_SECRET,
73
83
  region = process.env.AWS_REGION || 'us-east-1',
74
- requiredEnvVars = []
84
+ requiredEnvVars = [],
85
+ secrets = []
75
86
  } = options;
76
87
 
77
- // Load credentials from Secrets Manager if secretArn is provided
78
- if (secretArn && requiredEnvVars.length > 0) {
88
+ // Multi-secret mode: Load from multiple secrets
89
+ if (secrets.length > 0) {
90
+ console.log(`📦 Loading credentials from ${secrets.length} secrets`);
91
+
92
+ const allRequiredVars: string[] = [];
93
+
94
+ for (const secretConfig of secrets) {
95
+ const credentials = await loadCredentials<T>(
96
+ secretConfig.secretArn,
97
+ region,
98
+ secretConfig.requiredEnvVars
99
+ );
100
+
101
+ if (!credentials) {
102
+ throw new Error(`Failed to load credentials from secret: ${secretConfig.secretArn}`);
103
+ }
104
+
105
+ // Set credentials in process.env
106
+ for (const [key, value] of Object.entries(credentials)) {
107
+ process.env[key] = value;
108
+ }
109
+
110
+ // Track all required vars for validation
111
+ allRequiredVars.push(...secretConfig.requiredEnvVars.map(String));
112
+ }
113
+
114
+ // Validate all required environment variables
115
+ if (allRequiredVars.length > 0) {
116
+ validateEnvConfig(allRequiredVars);
117
+ }
118
+ }
119
+ // Single secret mode: Load from one secret (backward compatibility)
120
+ else if (secretArn && requiredEnvVars.length > 0) {
79
121
  const credentials = await loadCredentials<T>(secretArn, region, requiredEnvVars);
80
122
  if (!credentials) {
81
123
  throw new Error('Failed to load credentials from Secrets Manager');
@@ -0,0 +1,43 @@
1
+ import { Client } from "@opensearch-project/opensearch";
2
+ import logger from "../logger";
3
+
4
+ let openSearchClient: Client | null = null;
5
+
6
+ export async function initializeOpenSearchClient(): Promise<Client> {
7
+ if (openSearchClient) {
8
+ return openSearchClient;
9
+ }
10
+ try {
11
+ openSearchClient = new Client({
12
+ node: process.env.OPEN_SEARCH_DOMAIN!,
13
+ auth: {
14
+ username: process.env.OPEN_SEARCH_USER!,
15
+ password: process.env.OPEN_SEARCH_PASSWORD!,
16
+ },
17
+ ssl: process.env.NODE_ENV === "local" || process.env.OPENSEARCH_SSL === "false"
18
+ ? null
19
+ : {
20
+ rejectUnauthorized: true,
21
+ },
22
+ maxRetries: 1,
23
+ requestTimeout: 5000,
24
+ pingTimeout: 3000,
25
+ sniffOnStart: false,
26
+ });
27
+
28
+ await openSearchClient.ping()
29
+
30
+ logger("OpenSearch client initialized and ping successful", {
31
+ level: "info",
32
+ method: "initializeOpenSearchClient",
33
+ });
34
+ } catch (error) {
35
+ logger("OpenSearch client ping failed", {
36
+ level: "error",
37
+ method: "initializeOpenSearchClient",
38
+ error: error.message,
39
+ });
40
+ }
41
+
42
+ return openSearchClient;
43
+ }