@friggframework/devtools 2.0.0--canary.397.155fecd.0 → 2.0.0--canary.398.c9e5d61.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,416 @@
1
+ let EC2Client, DescribeVpcsCommand, DescribeSubnetsCommand, DescribeSecurityGroupsCommand, DescribeRouteTablesCommand;
2
+ let KMSClient, ListKeysCommand, DescribeKeyCommand;
3
+ let STSClient, GetCallerIdentityCommand;
4
+
5
+ function loadEC2() {
6
+ if (!EC2Client) {
7
+ ({ EC2Client, DescribeVpcsCommand, DescribeSubnetsCommand, DescribeSecurityGroupsCommand, DescribeRouteTablesCommand } = require('@aws-sdk/client-ec2'));
8
+ }
9
+ }
10
+
11
+ function loadKMS() {
12
+ if (!KMSClient) {
13
+ ({ KMSClient, ListKeysCommand, DescribeKeyCommand } = require('@aws-sdk/client-kms'));
14
+ }
15
+ }
16
+
17
+ function loadSTS() {
18
+ if (!STSClient) {
19
+ ({ STSClient, GetCallerIdentityCommand } = require('@aws-sdk/client-sts'));
20
+ }
21
+ }
22
+
23
+ /**
24
+ * AWS Resource Discovery utilities for Frigg applications
25
+ * These functions use AWS credentials to discover default resources during build time
26
+ */
27
+ class AWSDiscovery {
28
+ /**
29
+ * Creates an instance of AWSDiscovery
30
+ * @param {string} [region='us-east-1'] - AWS region to use for discovery
31
+ */
32
+ constructor(region = 'us-east-1') {
33
+ this.region = region;
34
+ loadEC2();
35
+ loadKMS();
36
+ loadSTS();
37
+ this.ec2Client = new EC2Client({ region });
38
+ this.kmsClient = new KMSClient({ region });
39
+ this.stsClient = new STSClient({ region });
40
+ }
41
+
42
+ /**
43
+ * Get AWS account ID
44
+ * @returns {Promise<string>} The AWS account ID
45
+ * @throws {Error} If unable to retrieve account ID
46
+ */
47
+ async getAccountId() {
48
+ try {
49
+ const command = new GetCallerIdentityCommand({});
50
+ const response = await this.stsClient.send(command);
51
+ return response.Account;
52
+ } catch (error) {
53
+ console.error('Error getting AWS account ID:', error);
54
+ throw error;
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Find the default VPC for the account
60
+ * @returns {Promise<Object>} VPC object containing VpcId and other properties
61
+ * @throws {Error} If no VPC is found in the account
62
+ */
63
+ async findDefaultVpc() {
64
+ try {
65
+ const command = new DescribeVpcsCommand({
66
+ Filters: [
67
+ {
68
+ Name: 'is-default',
69
+ Values: ['true']
70
+ }
71
+ ]
72
+ });
73
+
74
+ const response = await this.ec2Client.send(command);
75
+
76
+ if (response.Vpcs && response.Vpcs.length > 0) {
77
+ return response.Vpcs[0];
78
+ }
79
+
80
+ // If no default VPC, get the first available VPC
81
+ const allVpcsCommand = new DescribeVpcsCommand({});
82
+ const allVpcsResponse = await this.ec2Client.send(allVpcsCommand);
83
+
84
+ if (allVpcsResponse.Vpcs && allVpcsResponse.Vpcs.length > 0) {
85
+ console.log('No default VPC found, using first available VPC');
86
+ return allVpcsResponse.Vpcs[0];
87
+ }
88
+
89
+ throw new Error('No VPC found in the account');
90
+ } catch (error) {
91
+ console.error('Error finding default VPC:', error);
92
+ throw error;
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Find private subnets for the given VPC
98
+ * @param {string} vpcId - The VPC ID to search within
99
+ * @returns {Promise<Array>} Array of subnet objects (at least 2 for high availability)
100
+ * @throws {Error} If no subnets are found in the VPC
101
+ */
102
+ async findPrivateSubnets(vpcId) {
103
+ try {
104
+ const command = new DescribeSubnetsCommand({
105
+ Filters: [
106
+ {
107
+ Name: 'vpc-id',
108
+ Values: [vpcId]
109
+ }
110
+ ]
111
+ });
112
+
113
+ const response = await this.ec2Client.send(command);
114
+
115
+ if (!response.Subnets || response.Subnets.length === 0) {
116
+ throw new Error(`No subnets found in VPC ${vpcId}`);
117
+ }
118
+
119
+ // Prefer private subnets (no direct route to IGW)
120
+ const privateSubnets = [];
121
+ const publicSubnets = [];
122
+
123
+ for (const subnet of response.Subnets) {
124
+ // Check route tables to determine if subnet is private
125
+ const isPrivate = await this.isSubnetPrivate(subnet.SubnetId);
126
+ if (isPrivate) {
127
+ privateSubnets.push(subnet);
128
+ } else {
129
+ publicSubnets.push(subnet);
130
+ }
131
+ }
132
+
133
+ // Return at least 2 subnets for high availability
134
+ const selectedSubnets = privateSubnets.length >= 2 ?
135
+ privateSubnets.slice(0, 2) :
136
+ response.Subnets.slice(0, 2);
137
+
138
+ return selectedSubnets;
139
+ } catch (error) {
140
+ console.error('Error finding private subnets:', error);
141
+ throw error;
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Check if a subnet is private (no direct route to Internet Gateway)
147
+ * @param {string} subnetId - The subnet ID to check
148
+ * @returns {Promise<boolean>} True if subnet is private, false if public
149
+ */
150
+ async isSubnetPrivate(subnetId) {
151
+ try {
152
+ const command = new DescribeRouteTablesCommand({
153
+ Filters: [
154
+ {
155
+ Name: 'association.subnet-id',
156
+ Values: [subnetId]
157
+ }
158
+ ]
159
+ });
160
+
161
+ const response = await this.ec2Client.send(command);
162
+
163
+ for (const routeTable of response.RouteTables || []) {
164
+ for (const route of routeTable.Routes || []) {
165
+ // If there's a route to an Internet Gateway, it's a public subnet
166
+ if (route.GatewayId && route.GatewayId.startsWith('igw-')) {
167
+ return false;
168
+ }
169
+ }
170
+ }
171
+
172
+ return true; // No IGW route found, assume private
173
+ } catch (error) {
174
+ console.warn(`Could not determine if subnet ${subnetId} is private:`, error);
175
+ return true; // Default to private for safety
176
+ }
177
+ }
178
+
179
+ /**
180
+ * Find or create a default security group for Lambda functions
181
+ * @param {string} vpcId - The VPC ID to search within
182
+ * @returns {Promise<Object>} Security group object containing GroupId and other properties
183
+ * @throws {Error} If no security group is found for the VPC
184
+ */
185
+ async findDefaultSecurityGroup(vpcId) {
186
+ try {
187
+ // First try to find existing Frigg security group
188
+ const friggSgCommand = new DescribeSecurityGroupsCommand({
189
+ Filters: [
190
+ {
191
+ Name: 'vpc-id',
192
+ Values: [vpcId]
193
+ },
194
+ {
195
+ Name: 'group-name',
196
+ Values: ['frigg-lambda-sg']
197
+ }
198
+ ]
199
+ });
200
+
201
+ const friggResponse = await this.ec2Client.send(friggSgCommand);
202
+ if (friggResponse.SecurityGroups && friggResponse.SecurityGroups.length > 0) {
203
+ return friggResponse.SecurityGroups[0];
204
+ }
205
+
206
+ // Fall back to default security group
207
+ const defaultSgCommand = new DescribeSecurityGroupsCommand({
208
+ Filters: [
209
+ {
210
+ Name: 'vpc-id',
211
+ Values: [vpcId]
212
+ },
213
+ {
214
+ Name: 'group-name',
215
+ Values: ['default']
216
+ }
217
+ ]
218
+ });
219
+
220
+ const defaultResponse = await this.ec2Client.send(defaultSgCommand);
221
+ if (defaultResponse.SecurityGroups && defaultResponse.SecurityGroups.length > 0) {
222
+ return defaultResponse.SecurityGroups[0];
223
+ }
224
+
225
+ throw new Error(`No security group found for VPC ${vpcId}`);
226
+ } catch (error) {
227
+ console.error('Error finding default security group:', error);
228
+ throw error;
229
+ }
230
+ }
231
+
232
+ /**
233
+ * Find public subnets for NAT Gateway placement
234
+ * @param {string} vpcId - The VPC ID to search within
235
+ * @returns {Promise<Object>} First public subnet object for NAT Gateway placement
236
+ * @throws {Error} If no public subnets are found in the VPC
237
+ */
238
+ async findPublicSubnets(vpcId) {
239
+ try {
240
+ const command = new DescribeSubnetsCommand({
241
+ Filters: [
242
+ {
243
+ Name: 'vpc-id',
244
+ Values: [vpcId]
245
+ }
246
+ ]
247
+ });
248
+
249
+ const response = await this.ec2Client.send(command);
250
+
251
+ if (!response.Subnets || response.Subnets.length === 0) {
252
+ throw new Error(`No subnets found in VPC ${vpcId}`);
253
+ }
254
+
255
+ // Find public subnets (have direct route to IGW)
256
+ const publicSubnets = [];
257
+
258
+ for (const subnet of response.Subnets) {
259
+ // Check route tables to determine if subnet is public
260
+ const isPrivate = await this.isSubnetPrivate(subnet.SubnetId);
261
+ if (!isPrivate) {
262
+ publicSubnets.push(subnet);
263
+ }
264
+ }
265
+
266
+ if (publicSubnets.length === 0) {
267
+ throw new Error(`No public subnets found in VPC ${vpcId} for NAT Gateway placement`);
268
+ }
269
+
270
+ // Return first public subnet for NAT Gateway
271
+ return publicSubnets[0];
272
+ } catch (error) {
273
+ console.error('Error finding public subnets:', error);
274
+ throw error;
275
+ }
276
+ }
277
+
278
+ /**
279
+ * Find private route table for VPC endpoints
280
+ * @param {string} vpcId - The VPC ID to search within
281
+ * @returns {Promise<Object>} Route table object containing RouteTableId and other properties
282
+ * @throws {Error} If no route tables are found for the VPC
283
+ */
284
+ async findPrivateRouteTable(vpcId) {
285
+ try {
286
+ const command = new DescribeRouteTablesCommand({
287
+ Filters: [
288
+ {
289
+ Name: 'vpc-id',
290
+ Values: [vpcId]
291
+ }
292
+ ]
293
+ });
294
+
295
+ const response = await this.ec2Client.send(command);
296
+
297
+ if (!response.RouteTables || response.RouteTables.length === 0) {
298
+ throw new Error(`No route tables found for VPC ${vpcId}`);
299
+ }
300
+
301
+ // Find a route table that doesn't have direct IGW route (private)
302
+ for (const routeTable of response.RouteTables) {
303
+ let hasIgwRoute = false;
304
+ for (const route of routeTable.Routes || []) {
305
+ if (route.GatewayId && route.GatewayId.startsWith('igw-')) {
306
+ hasIgwRoute = true;
307
+ break;
308
+ }
309
+ }
310
+ if (!hasIgwRoute) {
311
+ return routeTable;
312
+ }
313
+ }
314
+
315
+ // If no private route table found, return the first one
316
+ return response.RouteTables[0];
317
+ } catch (error) {
318
+ console.error('Error finding private route table:', error);
319
+ throw error;
320
+ }
321
+ }
322
+
323
+ /**
324
+ * Find the default KMS key for the account
325
+ * @returns {Promise<string>} KMS key ARN or wildcard pattern as fallback
326
+ */
327
+ async findDefaultKmsKey() {
328
+ try {
329
+ // First try to find a key with alias/aws/lambda
330
+ const command = new ListKeysCommand({});
331
+ const response = await this.kmsClient.send(command);
332
+
333
+ if (!response.Keys || response.Keys.length === 0) {
334
+ // Return AWS managed key ARN pattern as fallback
335
+ const accountId = await this.getAccountId();
336
+ return `arn:aws:kms:${this.region}:${accountId}:key/*`;
337
+ }
338
+
339
+ // Look for customer managed keys first
340
+ for (const key of response.Keys) {
341
+ try {
342
+ const describeCommand = new DescribeKeyCommand({ KeyId: key.KeyId });
343
+ const keyDetails = await this.kmsClient.send(describeCommand);
344
+
345
+ if (keyDetails.KeyMetadata &&
346
+ keyDetails.KeyMetadata.KeyManager === 'CUSTOMER' &&
347
+ keyDetails.KeyMetadata.KeyState === 'Enabled') {
348
+ return keyDetails.KeyMetadata.Arn;
349
+ }
350
+ } catch (error) {
351
+ // Continue to next key if we can't describe this one
352
+ continue;
353
+ }
354
+ }
355
+
356
+ // Fallback to wildcard pattern for AWS managed keys
357
+ const accountId = await this.getAccountId();
358
+ return `arn:aws:kms:${this.region}:${accountId}:key/*`;
359
+ } catch (error) {
360
+ console.error('Error finding default KMS key:', error);
361
+ // Return wildcard pattern as ultimate fallback
362
+ return '*';
363
+ }
364
+ }
365
+
366
+ /**
367
+ * Discover all AWS resources needed for Frigg deployment
368
+ * @returns {Promise<Object>} Object containing discovered resource IDs:
369
+ * @returns {string} return.defaultVpcId - The default VPC ID
370
+ * @returns {string} return.defaultSecurityGroupId - The default security group ID
371
+ * @returns {string} return.privateSubnetId1 - First private subnet ID
372
+ * @returns {string} return.privateSubnetId2 - Second private subnet ID
373
+ * @returns {string} return.publicSubnetId - Public subnet ID for NAT Gateway
374
+ * @returns {string} return.privateRouteTableId - Private route table ID
375
+ * @returns {string} return.defaultKmsKeyId - Default KMS key ARN
376
+ * @throws {Error} If resource discovery fails
377
+ */
378
+ async discoverResources() {
379
+ try {
380
+ console.log('Discovering AWS resources for Frigg deployment...');
381
+
382
+ const vpc = await this.findDefaultVpc();
383
+ console.log(`Found VPC: ${vpc.VpcId}`);
384
+
385
+ const privateSubnets = await this.findPrivateSubnets(vpc.VpcId);
386
+ console.log(`Found ${privateSubnets.length} private subnets: ${privateSubnets.map(s => s.SubnetId).join(', ')}`);
387
+
388
+ const publicSubnet = await this.findPublicSubnets(vpc.VpcId);
389
+ console.log(`Found public subnet for NAT Gateway: ${publicSubnet.SubnetId}`);
390
+
391
+ const securityGroup = await this.findDefaultSecurityGroup(vpc.VpcId);
392
+ console.log(`Found security group: ${securityGroup.GroupId}`);
393
+
394
+ const routeTable = await this.findPrivateRouteTable(vpc.VpcId);
395
+ console.log(`Found route table: ${routeTable.RouteTableId}`);
396
+
397
+ const kmsKeyArn = await this.findDefaultKmsKey();
398
+ console.log(`Found KMS key: ${kmsKeyArn}`);
399
+
400
+ return {
401
+ defaultVpcId: vpc.VpcId,
402
+ defaultSecurityGroupId: securityGroup.GroupId,
403
+ privateSubnetId1: privateSubnets[0]?.SubnetId,
404
+ privateSubnetId2: privateSubnets[1]?.SubnetId || privateSubnets[0]?.SubnetId,
405
+ publicSubnetId: publicSubnet.SubnetId,
406
+ privateRouteTableId: routeTable.RouteTableId,
407
+ defaultKmsKeyId: kmsKeyArn
408
+ };
409
+ } catch (error) {
410
+ console.error('Error discovering AWS resources:', error);
411
+ throw error;
412
+ }
413
+ }
414
+ }
415
+
416
+ module.exports = { AWSDiscovery };