@friggframework/devtools 2.0.0--canary.397.155fecd.0 → 2.0.0--canary.398.e2147f7.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,277 @@
1
+ /**
2
+ * Test utilities for VPC/KMS/SSM testing
3
+ */
4
+
5
+ const { mockEnvironmentVariables, mockFallbackEnvironmentVariables } = require('../fixtures/mock-aws-resources');
6
+
7
+ /**
8
+ * Set up environment variables for testing
9
+ * @param {Object} envVars - Environment variables to set
10
+ */
11
+ function setTestEnvironmentVariables(envVars = mockEnvironmentVariables) {
12
+ Object.keys(envVars).forEach(key => {
13
+ process.env[key] = envVars[key];
14
+ });
15
+ }
16
+
17
+ /**
18
+ * Clean up environment variables after testing
19
+ * @param {Object} envVars - Environment variables to clean up
20
+ */
21
+ function cleanupTestEnvironmentVariables(envVars = mockEnvironmentVariables) {
22
+ Object.keys(envVars).forEach(key => {
23
+ delete process.env[key];
24
+ });
25
+ }
26
+
27
+ /**
28
+ * Set up fallback environment variables for error testing
29
+ */
30
+ function setFallbackEnvironmentVariables() {
31
+ setTestEnvironmentVariables(mockFallbackEnvironmentVariables);
32
+ }
33
+
34
+ /**
35
+ * Create a mock AWS SDK client send function
36
+ * @param {Array} responses - Array of responses to return in order
37
+ * @returns {Function} Mock send function
38
+ */
39
+ function createMockSendFunction(responses) {
40
+ let callCount = 0;
41
+ return jest.fn().mockImplementation(() => {
42
+ const response = responses[callCount] || responses[responses.length - 1];
43
+ callCount++;
44
+ return Promise.resolve(response);
45
+ });
46
+ }
47
+
48
+ /**
49
+ * Create a mock serverless object for plugin testing
50
+ * @param {Object} serviceConfig - Serverless service configuration
51
+ * @param {Array} commands - Commands in processedInput
52
+ * @returns {Object} Mock serverless object
53
+ */
54
+ function createMockServerless(serviceConfig = {}, commands = []) {
55
+ return {
56
+ cli: {
57
+ log: jest.fn()
58
+ },
59
+ service: {
60
+ provider: {
61
+ name: 'aws',
62
+ region: 'us-east-1',
63
+ ...serviceConfig.provider
64
+ },
65
+ plugins: serviceConfig.plugins || [],
66
+ custom: serviceConfig.custom || {},
67
+ functions: serviceConfig.functions || {},
68
+ ...serviceConfig
69
+ },
70
+ processedInput: {
71
+ commands: commands
72
+ },
73
+ getProvider: jest.fn(() => ({})),
74
+ extendConfiguration: jest.fn()
75
+ };
76
+ }
77
+
78
+ /**
79
+ * Verify that environment variables are set correctly
80
+ * @param {Object} expectedVars - Expected environment variables
81
+ */
82
+ function verifyEnvironmentVariables(expectedVars) {
83
+ Object.keys(expectedVars).forEach(key => {
84
+ expect(process.env[key]).toBe(expectedVars[key]);
85
+ });
86
+ }
87
+
88
+ /**
89
+ * Create a mock integration definition
90
+ * @param {string} name - Integration name
91
+ * @returns {Object} Mock integration
92
+ */
93
+ function createMockIntegration(name = 'testIntegration') {
94
+ return {
95
+ Definition: {
96
+ name: name
97
+ }
98
+ };
99
+ }
100
+
101
+ /**
102
+ * Create a mock app definition with specified features
103
+ * @param {Object} features - Features to enable (vpc, kms, ssm)
104
+ * @param {Array} integrations - Integration definitions
105
+ * @returns {Object} Mock app definition
106
+ */
107
+ function createMockAppDefinition(features = {}, integrations = []) {
108
+ const appDefinition = {
109
+ name: 'test-app',
110
+ integrations: integrations
111
+ };
112
+
113
+ if (features.vpc) {
114
+ appDefinition.vpc = { enable: true };
115
+ }
116
+
117
+ if (features.kms) {
118
+ appDefinition.encryption = { useDefaultKMSForFieldLevelEncryption: true };
119
+ }
120
+
121
+ if (features.ssm) {
122
+ appDefinition.ssm = { enable: true };
123
+ }
124
+
125
+ return appDefinition;
126
+ }
127
+
128
+ /**
129
+ * Verify serverless configuration contains expected VPC settings
130
+ * @param {Object} config - Serverless configuration
131
+ */
132
+ function verifyVpcConfiguration(config) {
133
+ expect(config.provider.vpc).toBe('${self:custom.vpc.${self:provider.stage}}');
134
+ expect(config.custom.vpc).toEqual({
135
+ '${self:provider.stage}': {
136
+ securityGroupIds: ['${env:AWS_DISCOVERY_SECURITY_GROUP_ID}'],
137
+ subnetIds: [
138
+ '${env:AWS_DISCOVERY_SUBNET_ID_1}',
139
+ '${env:AWS_DISCOVERY_SUBNET_ID_2}'
140
+ ]
141
+ }
142
+ });
143
+ expect(config.resources.Resources.VPCEndpointS3).toBeDefined();
144
+ }
145
+
146
+ /**
147
+ * Verify serverless configuration contains expected KMS settings
148
+ * @param {Object} config - Serverless configuration
149
+ */
150
+ function verifyKmsConfiguration(config) {
151
+ expect(config.plugins).toContain('serverless-kms-grants');
152
+ expect(config.provider.environment.KMS_KEY_ARN).toBe('${self:custom.kmsGrants.kmsKeyId}');
153
+ expect(config.custom.kmsGrants).toEqual({
154
+ kmsKeyId: '${env:AWS_DISCOVERY_KMS_KEY_ID}'
155
+ });
156
+
157
+ // Verify KMS IAM permissions
158
+ const kmsPermission = config.provider.iamRoleStatements.find(
159
+ statement => statement.Action.includes('kms:GenerateDataKey')
160
+ );
161
+ expect(kmsPermission).toBeDefined();
162
+ }
163
+
164
+ /**
165
+ * Verify serverless configuration contains expected SSM settings
166
+ * @param {Object} config - Serverless configuration
167
+ */
168
+ function verifySsmConfiguration(config) {
169
+ expect(config.provider.layers).toEqual([
170
+ 'arn:aws:lambda:${self:provider.region}:177933569100:layer:AWS-Parameters-and-Secrets-Lambda-Extension:11'
171
+ ]);
172
+ expect(config.provider.environment.SSM_PARAMETER_PREFIX).toBe('/${self:service}/${self:provider.stage}');
173
+
174
+ // Verify SSM IAM permissions
175
+ const ssmPermission = config.provider.iamRoleStatements.find(
176
+ statement => statement.Action.includes('ssm:GetParameter')
177
+ );
178
+ expect(ssmPermission).toBeDefined();
179
+ }
180
+
181
+ /**
182
+ * Verify integration-specific resources are created
183
+ * @param {Object} config - Serverless configuration
184
+ * @param {string} integrationName - Name of the integration
185
+ */
186
+ function verifyIntegrationConfiguration(config, integrationName) {
187
+ const capitalizedName = integrationName.charAt(0).toUpperCase() + integrationName.slice(1);
188
+
189
+ // Verify integration function
190
+ expect(config.functions[integrationName]).toBeDefined();
191
+
192
+ // Verify queue worker function
193
+ expect(config.functions[`${integrationName}QueueWorker`]).toBeDefined();
194
+
195
+ // Verify SQS queue resource
196
+ expect(config.resources.Resources[`${capitalizedName}Queue`]).toBeDefined();
197
+
198
+ // Verify environment variable
199
+ expect(config.provider.environment[`${integrationName.toUpperCase()}_QUEUE_URL`]).toBeDefined();
200
+ }
201
+
202
+ /**
203
+ * Wait for async operations to complete
204
+ * @param {number} ms - Milliseconds to wait
205
+ */
206
+ function wait(ms = 0) {
207
+ return new Promise(resolve => setTimeout(resolve, ms));
208
+ }
209
+
210
+ /**
211
+ * Capture console output for testing
212
+ */
213
+ function captureConsoleOutput() {
214
+ const originalLog = console.log;
215
+ const originalError = console.error;
216
+ const originalWarn = console.warn;
217
+
218
+ const logs = [];
219
+ const errors = [];
220
+ const warnings = [];
221
+
222
+ console.log = (...args) => {
223
+ logs.push(args.join(' '));
224
+ };
225
+
226
+ console.error = (...args) => {
227
+ errors.push(args.join(' '));
228
+ };
229
+
230
+ console.warn = (...args) => {
231
+ warnings.push(args.join(' '));
232
+ };
233
+
234
+ return {
235
+ logs,
236
+ errors,
237
+ warnings,
238
+ restore: () => {
239
+ console.log = originalLog;
240
+ console.error = originalError;
241
+ console.warn = originalWarn;
242
+ }
243
+ };
244
+ }
245
+
246
+ /**
247
+ * Mock process.argv for testing
248
+ * @param {Array} argv - Arguments to set
249
+ */
250
+ function mockProcessArgv(argv = ['node', 'test']) {
251
+ const originalArgv = process.argv;
252
+ jest.spyOn(process, 'argv', 'get').mockReturnValue(argv);
253
+
254
+ return {
255
+ restore: () => {
256
+ process.argv = originalArgv;
257
+ }
258
+ };
259
+ }
260
+
261
+ module.exports = {
262
+ setTestEnvironmentVariables,
263
+ cleanupTestEnvironmentVariables,
264
+ setFallbackEnvironmentVariables,
265
+ createMockSendFunction,
266
+ createMockServerless,
267
+ verifyEnvironmentVariables,
268
+ createMockIntegration,
269
+ createMockAppDefinition,
270
+ verifyVpcConfiguration,
271
+ verifyKmsConfiguration,
272
+ verifySsmConfiguration,
273
+ verifyIntegrationConfiguration,
274
+ wait,
275
+ captureConsoleOutput,
276
+ mockProcessArgv
277
+ };
@@ -0,0 +1,395 @@
1
+ const { EC2Client, DescribeVpcsCommand, DescribeSubnetsCommand, DescribeSecurityGroupsCommand, DescribeRouteTablesCommand } = require('@aws-sdk/client-ec2');
2
+ const { KMSClient, ListKeysCommand, DescribeKeyCommand } = require('@aws-sdk/client-kms');
3
+ const { STSClient, GetCallerIdentityCommand } = require('@aws-sdk/client-sts');
4
+
5
+ /**
6
+ * AWS Resource Discovery utilities for Frigg applications
7
+ * These functions use AWS credentials to discover default resources during build time
8
+ */
9
+ class AWSDiscovery {
10
+ /**
11
+ * Creates an instance of AWSDiscovery
12
+ * @param {string} [region='us-east-1'] - AWS region to use for discovery
13
+ */
14
+ constructor(region = 'us-east-1') {
15
+ this.region = region;
16
+ this.ec2Client = new EC2Client({ region });
17
+ this.kmsClient = new KMSClient({ region });
18
+ this.stsClient = new STSClient({ region });
19
+ }
20
+
21
+ /**
22
+ * Get AWS account ID
23
+ * @returns {Promise<string>} The AWS account ID
24
+ * @throws {Error} If unable to retrieve account ID
25
+ */
26
+ async getAccountId() {
27
+ try {
28
+ const command = new GetCallerIdentityCommand({});
29
+ const response = await this.stsClient.send(command);
30
+ return response.Account;
31
+ } catch (error) {
32
+ console.error('Error getting AWS account ID:', error);
33
+ throw error;
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Find the default VPC for the account
39
+ * @returns {Promise<Object>} VPC object containing VpcId and other properties
40
+ * @throws {Error} If no VPC is found in the account
41
+ */
42
+ async findDefaultVpc() {
43
+ try {
44
+ const command = new DescribeVpcsCommand({
45
+ Filters: [
46
+ {
47
+ Name: 'is-default',
48
+ Values: ['true']
49
+ }
50
+ ]
51
+ });
52
+
53
+ const response = await this.ec2Client.send(command);
54
+
55
+ if (response.Vpcs && response.Vpcs.length > 0) {
56
+ return response.Vpcs[0];
57
+ }
58
+
59
+ // If no default VPC, get the first available VPC
60
+ const allVpcsCommand = new DescribeVpcsCommand({});
61
+ const allVpcsResponse = await this.ec2Client.send(allVpcsCommand);
62
+
63
+ if (allVpcsResponse.Vpcs && allVpcsResponse.Vpcs.length > 0) {
64
+ console.log('No default VPC found, using first available VPC');
65
+ return allVpcsResponse.Vpcs[0];
66
+ }
67
+
68
+ throw new Error('No VPC found in the account');
69
+ } catch (error) {
70
+ console.error('Error finding default VPC:', error);
71
+ throw error;
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Find private subnets for the given VPC
77
+ * @param {string} vpcId - The VPC ID to search within
78
+ * @returns {Promise<Array>} Array of subnet objects (at least 2 for high availability)
79
+ * @throws {Error} If no subnets are found in the VPC
80
+ */
81
+ async findPrivateSubnets(vpcId) {
82
+ try {
83
+ const command = new DescribeSubnetsCommand({
84
+ Filters: [
85
+ {
86
+ Name: 'vpc-id',
87
+ Values: [vpcId]
88
+ }
89
+ ]
90
+ });
91
+
92
+ const response = await this.ec2Client.send(command);
93
+
94
+ if (!response.Subnets || response.Subnets.length === 0) {
95
+ throw new Error(`No subnets found in VPC ${vpcId}`);
96
+ }
97
+
98
+ // Prefer private subnets (no direct route to IGW)
99
+ const privateSubnets = [];
100
+ const publicSubnets = [];
101
+
102
+ for (const subnet of response.Subnets) {
103
+ // Check route tables to determine if subnet is private
104
+ const isPrivate = await this.isSubnetPrivate(subnet.SubnetId);
105
+ if (isPrivate) {
106
+ privateSubnets.push(subnet);
107
+ } else {
108
+ publicSubnets.push(subnet);
109
+ }
110
+ }
111
+
112
+ // Return at least 2 subnets for high availability
113
+ const selectedSubnets = privateSubnets.length >= 2 ?
114
+ privateSubnets.slice(0, 2) :
115
+ response.Subnets.slice(0, 2);
116
+
117
+ return selectedSubnets;
118
+ } catch (error) {
119
+ console.error('Error finding private subnets:', error);
120
+ throw error;
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Check if a subnet is private (no direct route to Internet Gateway)
126
+ * @param {string} subnetId - The subnet ID to check
127
+ * @returns {Promise<boolean>} True if subnet is private, false if public
128
+ */
129
+ async isSubnetPrivate(subnetId) {
130
+ try {
131
+ const command = new DescribeRouteTablesCommand({
132
+ Filters: [
133
+ {
134
+ Name: 'association.subnet-id',
135
+ Values: [subnetId]
136
+ }
137
+ ]
138
+ });
139
+
140
+ const response = await this.ec2Client.send(command);
141
+
142
+ for (const routeTable of response.RouteTables || []) {
143
+ for (const route of routeTable.Routes || []) {
144
+ // If there's a route to an Internet Gateway, it's a public subnet
145
+ if (route.GatewayId && route.GatewayId.startsWith('igw-')) {
146
+ return false;
147
+ }
148
+ }
149
+ }
150
+
151
+ return true; // No IGW route found, assume private
152
+ } catch (error) {
153
+ console.warn(`Could not determine if subnet ${subnetId} is private:`, error);
154
+ return true; // Default to private for safety
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Find or create a default security group for Lambda functions
160
+ * @param {string} vpcId - The VPC ID to search within
161
+ * @returns {Promise<Object>} Security group object containing GroupId and other properties
162
+ * @throws {Error} If no security group is found for the VPC
163
+ */
164
+ async findDefaultSecurityGroup(vpcId) {
165
+ try {
166
+ // First try to find existing Frigg security group
167
+ const friggSgCommand = new DescribeSecurityGroupsCommand({
168
+ Filters: [
169
+ {
170
+ Name: 'vpc-id',
171
+ Values: [vpcId]
172
+ },
173
+ {
174
+ Name: 'group-name',
175
+ Values: ['frigg-lambda-sg']
176
+ }
177
+ ]
178
+ });
179
+
180
+ const friggResponse = await this.ec2Client.send(friggSgCommand);
181
+ if (friggResponse.SecurityGroups && friggResponse.SecurityGroups.length > 0) {
182
+ return friggResponse.SecurityGroups[0];
183
+ }
184
+
185
+ // Fall back to default security group
186
+ const defaultSgCommand = new DescribeSecurityGroupsCommand({
187
+ Filters: [
188
+ {
189
+ Name: 'vpc-id',
190
+ Values: [vpcId]
191
+ },
192
+ {
193
+ Name: 'group-name',
194
+ Values: ['default']
195
+ }
196
+ ]
197
+ });
198
+
199
+ const defaultResponse = await this.ec2Client.send(defaultSgCommand);
200
+ if (defaultResponse.SecurityGroups && defaultResponse.SecurityGroups.length > 0) {
201
+ return defaultResponse.SecurityGroups[0];
202
+ }
203
+
204
+ throw new Error(`No security group found for VPC ${vpcId}`);
205
+ } catch (error) {
206
+ console.error('Error finding default security group:', error);
207
+ throw error;
208
+ }
209
+ }
210
+
211
+ /**
212
+ * Find public subnets for NAT Gateway placement
213
+ * @param {string} vpcId - The VPC ID to search within
214
+ * @returns {Promise<Object>} First public subnet object for NAT Gateway placement
215
+ * @throws {Error} If no public subnets are found in the VPC
216
+ */
217
+ async findPublicSubnets(vpcId) {
218
+ try {
219
+ const command = new DescribeSubnetsCommand({
220
+ Filters: [
221
+ {
222
+ Name: 'vpc-id',
223
+ Values: [vpcId]
224
+ }
225
+ ]
226
+ });
227
+
228
+ const response = await this.ec2Client.send(command);
229
+
230
+ if (!response.Subnets || response.Subnets.length === 0) {
231
+ throw new Error(`No subnets found in VPC ${vpcId}`);
232
+ }
233
+
234
+ // Find public subnets (have direct route to IGW)
235
+ const publicSubnets = [];
236
+
237
+ for (const subnet of response.Subnets) {
238
+ // Check route tables to determine if subnet is public
239
+ const isPrivate = await this.isSubnetPrivate(subnet.SubnetId);
240
+ if (!isPrivate) {
241
+ publicSubnets.push(subnet);
242
+ }
243
+ }
244
+
245
+ if (publicSubnets.length === 0) {
246
+ throw new Error(`No public subnets found in VPC ${vpcId} for NAT Gateway placement`);
247
+ }
248
+
249
+ // Return first public subnet for NAT Gateway
250
+ return publicSubnets[0];
251
+ } catch (error) {
252
+ console.error('Error finding public subnets:', error);
253
+ throw error;
254
+ }
255
+ }
256
+
257
+ /**
258
+ * Find private route table for VPC endpoints
259
+ * @param {string} vpcId - The VPC ID to search within
260
+ * @returns {Promise<Object>} Route table object containing RouteTableId and other properties
261
+ * @throws {Error} If no route tables are found for the VPC
262
+ */
263
+ async findPrivateRouteTable(vpcId) {
264
+ try {
265
+ const command = new DescribeRouteTablesCommand({
266
+ Filters: [
267
+ {
268
+ Name: 'vpc-id',
269
+ Values: [vpcId]
270
+ }
271
+ ]
272
+ });
273
+
274
+ const response = await this.ec2Client.send(command);
275
+
276
+ if (!response.RouteTables || response.RouteTables.length === 0) {
277
+ throw new Error(`No route tables found for VPC ${vpcId}`);
278
+ }
279
+
280
+ // Find a route table that doesn't have direct IGW route (private)
281
+ for (const routeTable of response.RouteTables) {
282
+ let hasIgwRoute = false;
283
+ for (const route of routeTable.Routes || []) {
284
+ if (route.GatewayId && route.GatewayId.startsWith('igw-')) {
285
+ hasIgwRoute = true;
286
+ break;
287
+ }
288
+ }
289
+ if (!hasIgwRoute) {
290
+ return routeTable;
291
+ }
292
+ }
293
+
294
+ // If no private route table found, return the first one
295
+ return response.RouteTables[0];
296
+ } catch (error) {
297
+ console.error('Error finding private route table:', error);
298
+ throw error;
299
+ }
300
+ }
301
+
302
+ /**
303
+ * Find the default KMS key for the account
304
+ * @returns {Promise<string>} KMS key ARN or wildcard pattern as fallback
305
+ */
306
+ async findDefaultKmsKey() {
307
+ try {
308
+ // First try to find a key with alias/aws/lambda
309
+ const command = new ListKeysCommand({});
310
+ const response = await this.kmsClient.send(command);
311
+
312
+ if (!response.Keys || response.Keys.length === 0) {
313
+ // Return AWS managed key ARN pattern as fallback
314
+ const accountId = await this.getAccountId();
315
+ return `arn:aws:kms:${this.region}:${accountId}:key/*`;
316
+ }
317
+
318
+ // Look for customer managed keys first
319
+ for (const key of response.Keys) {
320
+ try {
321
+ const describeCommand = new DescribeKeyCommand({ KeyId: key.KeyId });
322
+ const keyDetails = await this.kmsClient.send(describeCommand);
323
+
324
+ if (keyDetails.KeyMetadata &&
325
+ keyDetails.KeyMetadata.KeyManager === 'CUSTOMER' &&
326
+ keyDetails.KeyMetadata.KeyState === 'Enabled') {
327
+ return keyDetails.KeyMetadata.Arn;
328
+ }
329
+ } catch (error) {
330
+ // Continue to next key if we can't describe this one
331
+ continue;
332
+ }
333
+ }
334
+
335
+ // Fallback to wildcard pattern for AWS managed keys
336
+ const accountId = await this.getAccountId();
337
+ return `arn:aws:kms:${this.region}:${accountId}:key/*`;
338
+ } catch (error) {
339
+ console.error('Error finding default KMS key:', error);
340
+ // Return wildcard pattern as ultimate fallback
341
+ return '*';
342
+ }
343
+ }
344
+
345
+ /**
346
+ * Discover all AWS resources needed for Frigg deployment
347
+ * @returns {Promise<Object>} Object containing discovered resource IDs:
348
+ * @returns {string} return.defaultVpcId - The default VPC ID
349
+ * @returns {string} return.defaultSecurityGroupId - The default security group ID
350
+ * @returns {string} return.privateSubnetId1 - First private subnet ID
351
+ * @returns {string} return.privateSubnetId2 - Second private subnet ID
352
+ * @returns {string} return.publicSubnetId - Public subnet ID for NAT Gateway
353
+ * @returns {string} return.privateRouteTableId - Private route table ID
354
+ * @returns {string} return.defaultKmsKeyId - Default KMS key ARN
355
+ * @throws {Error} If resource discovery fails
356
+ */
357
+ async discoverResources() {
358
+ try {
359
+ console.log('Discovering AWS resources for Frigg deployment...');
360
+
361
+ const vpc = await this.findDefaultVpc();
362
+ console.log(`Found VPC: ${vpc.VpcId}`);
363
+
364
+ const privateSubnets = await this.findPrivateSubnets(vpc.VpcId);
365
+ console.log(`Found ${privateSubnets.length} private subnets: ${privateSubnets.map(s => s.SubnetId).join(', ')}`);
366
+
367
+ const publicSubnet = await this.findPublicSubnets(vpc.VpcId);
368
+ console.log(`Found public subnet for NAT Gateway: ${publicSubnet.SubnetId}`);
369
+
370
+ const securityGroup = await this.findDefaultSecurityGroup(vpc.VpcId);
371
+ console.log(`Found security group: ${securityGroup.GroupId}`);
372
+
373
+ const routeTable = await this.findPrivateRouteTable(vpc.VpcId);
374
+ console.log(`Found route table: ${routeTable.RouteTableId}`);
375
+
376
+ const kmsKeyArn = await this.findDefaultKmsKey();
377
+ console.log(`Found KMS key: ${kmsKeyArn}`);
378
+
379
+ return {
380
+ defaultVpcId: vpc.VpcId,
381
+ defaultSecurityGroupId: securityGroup.GroupId,
382
+ privateSubnetId1: privateSubnets[0]?.SubnetId,
383
+ privateSubnetId2: privateSubnets[1]?.SubnetId || privateSubnets[0]?.SubnetId,
384
+ publicSubnetId: publicSubnet.SubnetId,
385
+ privateRouteTableId: routeTable.RouteTableId,
386
+ defaultKmsKeyId: kmsKeyArn
387
+ };
388
+ } catch (error) {
389
+ console.error('Error discovering AWS resources:', error);
390
+ throw error;
391
+ }
392
+ }
393
+ }
394
+
395
+ module.exports = { AWSDiscovery };