@friggframework/devtools 2.0.0-next.27 → 2.0.0-next.29

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.
Files changed (34) hide show
  1. package/frigg-cli/build-command/index.js +4 -2
  2. package/frigg-cli/deploy-command/index.js +5 -2
  3. package/frigg-cli/generate-iam-command.js +115 -0
  4. package/frigg-cli/index.js +11 -1
  5. package/infrastructure/AWS-DISCOVERY-TROUBLESHOOTING.md +245 -0
  6. package/infrastructure/AWS-IAM-CREDENTIAL-NEEDS.md +596 -0
  7. package/infrastructure/DEPLOYMENT-INSTRUCTIONS.md +268 -0
  8. package/infrastructure/GENERATE-IAM-DOCS.md +253 -0
  9. package/infrastructure/IAM-POLICY-TEMPLATES.md +176 -0
  10. package/infrastructure/README-TESTING.md +332 -0
  11. package/infrastructure/README.md +421 -0
  12. package/infrastructure/WEBSOCKET-CONFIGURATION.md +105 -0
  13. package/infrastructure/__tests__/fixtures/mock-aws-resources.js +391 -0
  14. package/infrastructure/__tests__/helpers/test-utils.js +277 -0
  15. package/infrastructure/aws-discovery.js +568 -0
  16. package/infrastructure/aws-discovery.test.js +373 -0
  17. package/infrastructure/build-time-discovery.js +206 -0
  18. package/infrastructure/build-time-discovery.test.js +375 -0
  19. package/infrastructure/create-frigg-infrastructure.js +2 -2
  20. package/infrastructure/frigg-deployment-iam-stack.yaml +379 -0
  21. package/infrastructure/iam-generator.js +687 -0
  22. package/infrastructure/iam-generator.test.js +169 -0
  23. package/infrastructure/iam-policy-basic.json +212 -0
  24. package/infrastructure/iam-policy-full.json +282 -0
  25. package/infrastructure/integration.test.js +383 -0
  26. package/infrastructure/run-discovery.js +110 -0
  27. package/infrastructure/serverless-template.js +537 -212
  28. package/infrastructure/serverless-template.test.js +541 -0
  29. package/management-ui/dist/assets/FriggLogo-B7Xx8ZW1.svg +1 -0
  30. package/management-ui/dist/assets/index-BA21WgFa.js +1221 -0
  31. package/management-ui/dist/assets/index-CbM64Oba.js +1221 -0
  32. package/management-ui/dist/assets/index-CkvseXTC.css +1 -0
  33. package/management-ui/dist/index.html +14 -0
  34. package/package.json +9 -5
@@ -0,0 +1,568 @@
1
+ let EC2Client, DescribeVpcsCommand, DescribeSubnetsCommand, DescribeSecurityGroupsCommand, DescribeRouteTablesCommand, DescribeNatGatewaysCommand, DescribeAddressesCommand;
2
+ let KMSClient, ListKeysCommand, DescribeKeyCommand;
3
+ let STSClient, GetCallerIdentityCommand;
4
+
5
+ function loadEC2() {
6
+ if (!EC2Client) {
7
+ ({ EC2Client, DescribeVpcsCommand, DescribeSubnetsCommand, DescribeSecurityGroupsCommand, DescribeRouteTablesCommand, DescribeNatGatewaysCommand, DescribeAddressesCommand } = 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.message);
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.message);
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
+ // First, get the subnet details to find its VPC
153
+ const subnetCommand = new DescribeSubnetsCommand({
154
+ SubnetIds: [subnetId]
155
+ });
156
+ const subnetResponse = await this.ec2Client.send(subnetCommand);
157
+
158
+ if (!subnetResponse.Subnets || subnetResponse.Subnets.length === 0) {
159
+ throw new Error(`Subnet ${subnetId} not found`);
160
+ }
161
+
162
+ const subnet = subnetResponse.Subnets[0];
163
+ const vpcId = subnet.VpcId;
164
+
165
+ // Get all route tables for this VPC
166
+ const routeTablesCommand = new DescribeRouteTablesCommand({
167
+ Filters: [
168
+ {
169
+ Name: 'vpc-id',
170
+ Values: [vpcId]
171
+ }
172
+ ]
173
+ });
174
+
175
+ const routeTablesResponse = await this.ec2Client.send(routeTablesCommand);
176
+
177
+ // Find the route table for this subnet
178
+ let routeTable = null;
179
+
180
+ // First check for explicit association
181
+ for (const rt of routeTablesResponse.RouteTables || []) {
182
+ for (const assoc of rt.Associations || []) {
183
+ if (assoc.SubnetId === subnetId) {
184
+ routeTable = rt;
185
+ break;
186
+ }
187
+ }
188
+ if (routeTable) break;
189
+ }
190
+
191
+ // If no explicit association, use the main route table
192
+ if (!routeTable) {
193
+ for (const rt of routeTablesResponse.RouteTables || []) {
194
+ for (const assoc of rt.Associations || []) {
195
+ if (assoc.Main === true) {
196
+ routeTable = rt;
197
+ break;
198
+ }
199
+ }
200
+ if (routeTable) break;
201
+ }
202
+ }
203
+
204
+ if (!routeTable) {
205
+ console.warn(`No route table found for subnet ${subnetId}`);
206
+ return true; // Default to private for safety
207
+ }
208
+
209
+ // Check if route table has a route to an Internet Gateway
210
+ for (const route of routeTable.Routes || []) {
211
+ if (route.GatewayId && route.GatewayId.startsWith('igw-')) {
212
+ return false; // It's a public subnet
213
+ }
214
+ }
215
+
216
+ return true; // No IGW route found, it's private
217
+ } catch (error) {
218
+ console.warn(`Could not determine if subnet ${subnetId} is private:`, error);
219
+ return true; // Default to private for safety
220
+ }
221
+ }
222
+
223
+ /**
224
+ * Find or create a default security group for Lambda functions
225
+ * @param {string} vpcId - The VPC ID to search within
226
+ * @returns {Promise<Object>} Security group object containing GroupId and other properties
227
+ * @throws {Error} If no security group is found for the VPC
228
+ */
229
+ async findDefaultSecurityGroup(vpcId) {
230
+ try {
231
+ // First try to find existing Frigg security group
232
+ const friggSgCommand = new DescribeSecurityGroupsCommand({
233
+ Filters: [
234
+ {
235
+ Name: 'vpc-id',
236
+ Values: [vpcId]
237
+ },
238
+ {
239
+ Name: 'group-name',
240
+ Values: ['frigg-lambda-sg']
241
+ }
242
+ ]
243
+ });
244
+
245
+ const friggResponse = await this.ec2Client.send(friggSgCommand);
246
+ if (friggResponse.SecurityGroups && friggResponse.SecurityGroups.length > 0) {
247
+ return friggResponse.SecurityGroups[0];
248
+ }
249
+
250
+ // Fall back to default security group
251
+ const defaultSgCommand = new DescribeSecurityGroupsCommand({
252
+ Filters: [
253
+ {
254
+ Name: 'vpc-id',
255
+ Values: [vpcId]
256
+ },
257
+ {
258
+ Name: 'group-name',
259
+ Values: ['default']
260
+ }
261
+ ]
262
+ });
263
+
264
+ const defaultResponse = await this.ec2Client.send(defaultSgCommand);
265
+ if (defaultResponse.SecurityGroups && defaultResponse.SecurityGroups.length > 0) {
266
+ return defaultResponse.SecurityGroups[0];
267
+ }
268
+
269
+ throw new Error(`No security group found for VPC ${vpcId}`);
270
+ } catch (error) {
271
+ console.error('Error finding default security group:', error);
272
+ throw error;
273
+ }
274
+ }
275
+
276
+ /**
277
+ * Find public subnets for NAT Gateway placement
278
+ * @param {string} vpcId - The VPC ID to search within
279
+ * @returns {Promise<Object>} First public subnet object for NAT Gateway placement
280
+ * @throws {Error} If no public subnets are found in the VPC
281
+ */
282
+ async findPublicSubnets(vpcId) {
283
+ try {
284
+ const command = new DescribeSubnetsCommand({
285
+ Filters: [
286
+ {
287
+ Name: 'vpc-id',
288
+ Values: [vpcId]
289
+ }
290
+ ]
291
+ });
292
+
293
+ const response = await this.ec2Client.send(command);
294
+
295
+ if (!response.Subnets || response.Subnets.length === 0) {
296
+ throw new Error(`No subnets found in VPC ${vpcId}`);
297
+ }
298
+
299
+ // Find public subnets (have direct route to IGW)
300
+ const publicSubnets = [];
301
+
302
+ for (const subnet of response.Subnets) {
303
+ // Check route tables to determine if subnet is public
304
+ const isPrivate = await this.isSubnetPrivate(subnet.SubnetId);
305
+ if (!isPrivate) {
306
+ publicSubnets.push(subnet);
307
+ }
308
+ }
309
+
310
+ if (publicSubnets.length === 0) {
311
+ throw new Error(`No public subnets found in VPC ${vpcId} for NAT Gateway placement`);
312
+ }
313
+
314
+ // Return first public subnet for NAT Gateway
315
+ return publicSubnets[0];
316
+ } catch (error) {
317
+ console.error('Error finding public subnets:', error);
318
+ throw error;
319
+ }
320
+ }
321
+
322
+ /**
323
+ * Find private route table for VPC endpoints
324
+ * @param {string} vpcId - The VPC ID to search within
325
+ * @returns {Promise<Object>} Route table object containing RouteTableId and other properties
326
+ * @throws {Error} If no route tables are found for the VPC
327
+ */
328
+ async findPrivateRouteTable(vpcId) {
329
+ try {
330
+ const command = new DescribeRouteTablesCommand({
331
+ Filters: [
332
+ {
333
+ Name: 'vpc-id',
334
+ Values: [vpcId]
335
+ }
336
+ ]
337
+ });
338
+
339
+ const response = await this.ec2Client.send(command);
340
+
341
+ if (!response.RouteTables || response.RouteTables.length === 0) {
342
+ throw new Error(`No route tables found for VPC ${vpcId}`);
343
+ }
344
+
345
+ // Find a route table that doesn't have direct IGW route (private)
346
+ for (const routeTable of response.RouteTables) {
347
+ let hasIgwRoute = false;
348
+ for (const route of routeTable.Routes || []) {
349
+ if (route.GatewayId && route.GatewayId.startsWith('igw-')) {
350
+ hasIgwRoute = true;
351
+ break;
352
+ }
353
+ }
354
+ if (!hasIgwRoute) {
355
+ return routeTable;
356
+ }
357
+ }
358
+
359
+ // If no private route table found, return the first one
360
+ return response.RouteTables[0];
361
+ } catch (error) {
362
+ console.error('Error finding private route table:', error);
363
+ throw error;
364
+ }
365
+ }
366
+
367
+ /**
368
+ * Find existing NAT Gateways in the VPC
369
+ * @param {string} vpcId - The VPC ID to search within
370
+ * @returns {Promise<Object|null>} NAT Gateway object or null if none found
371
+ */
372
+ async findExistingNatGateway(vpcId) {
373
+ try {
374
+ const command = new DescribeNatGatewaysCommand({
375
+ Filter: [
376
+ {
377
+ Name: 'vpc-id',
378
+ Values: [vpcId]
379
+ },
380
+ {
381
+ Name: 'state',
382
+ Values: ['available']
383
+ }
384
+ ]
385
+ });
386
+
387
+ const response = await this.ec2Client.send(command);
388
+
389
+ if (response.NatGateways && response.NatGateways.length > 0) {
390
+ // Find a NAT Gateway tagged for Frigg first
391
+ const friggNatGateway = response.NatGateways.find(nat =>
392
+ nat.Tags && nat.Tags.some(tag =>
393
+ tag.Key === 'Name' && tag.Value.includes('frigg')
394
+ )
395
+ );
396
+
397
+ if (friggNatGateway) {
398
+ console.log(`Found existing Frigg NAT Gateway: ${friggNatGateway.NatGatewayId}`);
399
+ return friggNatGateway;
400
+ }
401
+
402
+ // Return first available NAT Gateway if no Frigg-specific one found
403
+ console.log(`Found existing NAT Gateway: ${response.NatGateways[0].NatGatewayId}`);
404
+ return response.NatGateways[0];
405
+ }
406
+
407
+ return null;
408
+ } catch (error) {
409
+ console.warn('Error finding existing NAT Gateway:', error.message);
410
+ return null;
411
+ }
412
+ }
413
+
414
+ /**
415
+ * Find available Elastic IPs
416
+ * @returns {Promise<Object|null>} Available EIP object or null if none found
417
+ */
418
+ async findAvailableElasticIP() {
419
+ try {
420
+ const command = new DescribeAddressesCommand({});
421
+ const response = await this.ec2Client.send(command);
422
+
423
+ if (response.Addresses && response.Addresses.length > 0) {
424
+ // Find an unassociated EIP first
425
+ const availableEIP = response.Addresses.find(eip =>
426
+ !eip.AssociationId && !eip.InstanceId && !eip.NetworkInterfaceId
427
+ );
428
+
429
+ if (availableEIP) {
430
+ console.log(`Found available Elastic IP: ${availableEIP.AllocationId}`);
431
+ return availableEIP;
432
+ }
433
+
434
+ // Check for EIPs tagged for Frigg
435
+ const friggEIP = response.Addresses.find(eip =>
436
+ eip.Tags && eip.Tags.some(tag =>
437
+ tag.Key === 'Name' && tag.Value.includes('frigg')
438
+ )
439
+ );
440
+
441
+ if (friggEIP) {
442
+ console.log(`Found Frigg-tagged Elastic IP: ${friggEIP.AllocationId}`);
443
+ return friggEIP;
444
+ }
445
+ }
446
+
447
+ return null;
448
+ } catch (error) {
449
+ console.warn('Error finding available Elastic IP:', error.message);
450
+ return null;
451
+ }
452
+ }
453
+
454
+ /**
455
+ * Find the default KMS key for the account
456
+ * @returns {Promise<string>} KMS key ARN or wildcard pattern as fallback
457
+ */
458
+ async findDefaultKmsKey() {
459
+ try {
460
+ // First try to find a key with alias/aws/lambda
461
+ const command = new ListKeysCommand({});
462
+ const response = await this.kmsClient.send(command);
463
+
464
+ if (!response.Keys || response.Keys.length === 0) {
465
+ // Return AWS managed key ARN pattern as fallback
466
+ const accountId = await this.getAccountId();
467
+ return `arn:aws:kms:${this.region}:${accountId}:key/*`;
468
+ }
469
+
470
+ // Look for customer managed keys first
471
+ for (const key of response.Keys) {
472
+ try {
473
+ const describeCommand = new DescribeKeyCommand({ KeyId: key.KeyId });
474
+ const keyDetails = await this.kmsClient.send(describeCommand);
475
+
476
+ if (keyDetails.KeyMetadata &&
477
+ keyDetails.KeyMetadata.KeyManager === 'CUSTOMER' &&
478
+ keyDetails.KeyMetadata.KeyState === 'Enabled') {
479
+ return keyDetails.KeyMetadata.Arn;
480
+ }
481
+ } catch (error) {
482
+ // Continue to next key if we can't describe this one
483
+ continue;
484
+ }
485
+ }
486
+
487
+ // Fallback to wildcard pattern for AWS managed keys
488
+ const accountId = await this.getAccountId();
489
+ return `arn:aws:kms:${this.region}:${accountId}:key/*`;
490
+ } catch (error) {
491
+ console.error('Error finding default KMS key:', error);
492
+ // Return wildcard pattern as ultimate fallback
493
+ return '*';
494
+ }
495
+ }
496
+
497
+ /**
498
+ * Discover all AWS resources needed for Frigg deployment
499
+ * @returns {Promise<Object>} Object containing discovered resource IDs:
500
+ * @returns {string} return.defaultVpcId - The default VPC ID
501
+ * @returns {string} return.defaultSecurityGroupId - The default security group ID
502
+ * @returns {string} return.privateSubnetId1 - First private subnet ID
503
+ * @returns {string} return.privateSubnetId2 - Second private subnet ID
504
+ * @returns {string} return.publicSubnetId - Public subnet ID for NAT Gateway
505
+ * @returns {string} return.privateRouteTableId - Private route table ID
506
+ * @returns {string} return.defaultKmsKeyId - Default KMS key ARN
507
+ * @throws {Error} If resource discovery fails
508
+ */
509
+ async discoverResources() {
510
+ try {
511
+ console.log('Discovering AWS resources for Frigg deployment...');
512
+
513
+ const vpc = await this.findDefaultVpc();
514
+ console.log(`Found VPC: ${vpc.VpcId}`);
515
+
516
+ const privateSubnets = await this.findPrivateSubnets(vpc.VpcId);
517
+ console.log(`Found ${privateSubnets.length} private subnets: ${privateSubnets.map(s => s.SubnetId).join(', ')}`);
518
+
519
+ const publicSubnet = await this.findPublicSubnets(vpc.VpcId);
520
+ console.log(`Found public subnet for NAT Gateway: ${publicSubnet.SubnetId}`);
521
+
522
+ const securityGroup = await this.findDefaultSecurityGroup(vpc.VpcId);
523
+ console.log(`Found security group: ${securityGroup.GroupId}`);
524
+
525
+ const routeTable = await this.findPrivateRouteTable(vpc.VpcId);
526
+ console.log(`Found route table: ${routeTable.RouteTableId}`);
527
+
528
+ const kmsKeyArn = await this.findDefaultKmsKey();
529
+ console.log(`Found KMS key: ${kmsKeyArn}`);
530
+
531
+ // Try to find existing NAT Gateway
532
+ const existingNatGateway = await this.findExistingNatGateway(vpc.VpcId);
533
+ let natGatewayId = null;
534
+ let elasticIpAllocationId = null;
535
+
536
+ if (existingNatGateway) {
537
+ natGatewayId = existingNatGateway.NatGatewayId;
538
+ // Get the EIP allocation ID from the NAT Gateway
539
+ if (existingNatGateway.NatGatewayAddresses && existingNatGateway.NatGatewayAddresses.length > 0) {
540
+ elasticIpAllocationId = existingNatGateway.NatGatewayAddresses[0].AllocationId;
541
+ }
542
+ } else {
543
+ // If no NAT Gateway exists, check for available EIP
544
+ const availableEIP = await this.findAvailableElasticIP();
545
+ if (availableEIP) {
546
+ elasticIpAllocationId = availableEIP.AllocationId;
547
+ }
548
+ }
549
+
550
+ return {
551
+ defaultVpcId: vpc.VpcId,
552
+ defaultSecurityGroupId: securityGroup.GroupId,
553
+ privateSubnetId1: privateSubnets[0]?.SubnetId,
554
+ privateSubnetId2: privateSubnets[1]?.SubnetId || privateSubnets[0]?.SubnetId,
555
+ publicSubnetId: publicSubnet.SubnetId,
556
+ privateRouteTableId: routeTable.RouteTableId,
557
+ defaultKmsKeyId: kmsKeyArn,
558
+ existingNatGatewayId: natGatewayId,
559
+ existingElasticIpAllocationId: elasticIpAllocationId
560
+ };
561
+ } catch (error) {
562
+ console.error('Error discovering AWS resources:', error);
563
+ throw error;
564
+ }
565
+ }
566
+ }
567
+
568
+ module.exports = { AWSDiscovery };