@friggframework/devtools 2.0.0--canary.397.4957a89.0 → 2.0.0--canary.398.bdb6d27.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.
Files changed (31) 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 +594 -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 +174 -0
  10. package/infrastructure/README-TESTING.md +332 -0
  11. package/infrastructure/WEBSOCKET-CONFIGURATION.md +105 -0
  12. package/infrastructure/__tests__/fixtures/mock-aws-resources.js +391 -0
  13. package/infrastructure/__tests__/helpers/test-utils.js +277 -0
  14. package/infrastructure/aws-discovery.js +568 -0
  15. package/infrastructure/aws-discovery.test.js +373 -0
  16. package/infrastructure/build-time-discovery.js +206 -0
  17. package/infrastructure/build-time-discovery.test.js +375 -0
  18. package/infrastructure/create-frigg-infrastructure.js +10 -2
  19. package/infrastructure/frigg-deployment-iam-stack.yaml +377 -0
  20. package/infrastructure/iam-generator.js +696 -0
  21. package/infrastructure/iam-generator.test.js +169 -0
  22. package/infrastructure/iam-policy-basic.json +210 -0
  23. package/infrastructure/iam-policy-full.json +280 -0
  24. package/infrastructure/integration.test.js +383 -0
  25. package/infrastructure/run-discovery.js +110 -0
  26. package/infrastructure/serverless-template.js +606 -27
  27. package/infrastructure/serverless-template.test.js +498 -0
  28. package/package.json +9 -5
  29. package/test/auther-definition-tester.js +125 -0
  30. package/test/index.js +4 -2
  31. package/test/mock-integration.js +14 -4
@@ -1,7 +1,26 @@
1
1
  const path = require('path');
2
2
  const fs = require('fs');
3
+ const { AWSDiscovery } = require('./aws-discovery');
3
4
 
4
- // Function to find the actual path to node_modules
5
+ /**
6
+ * Check if AWS discovery should run based on AppDefinition
7
+ * @param {Object} AppDefinition - Application definition
8
+ * @returns {boolean} True if discovery should run
9
+ */
10
+ const shouldRunDiscovery = (AppDefinition) => {
11
+ return AppDefinition.vpc?.enable === true ||
12
+ AppDefinition.encryption?.useDefaultKMSForFieldLevelEncryption === true ||
13
+ AppDefinition.ssm?.enable === true;
14
+ };
15
+
16
+ /**
17
+ * Find the actual path to node_modules directory
18
+ * Tries multiple methods to locate node_modules:
19
+ * 1. Traversing up from current directory
20
+ * 2. Using npm root command
21
+ * 3. Looking for package.json and adjacent node_modules
22
+ * @returns {string} Path to node_modules directory
23
+ */
5
24
  const findNodeModulesPath = () => {
6
25
  try {
7
26
  // Method 1: Try to find node_modules by traversing up from current directory
@@ -75,7 +94,12 @@ const findNodeModulesPath = () => {
75
94
  }
76
95
  };
77
96
 
78
- // Function to modify handler paths to point to the correct node_modules
97
+ /**
98
+ * Modify handler paths to point to the correct node_modules location
99
+ * Only modifies paths when running in offline mode
100
+ * @param {Object} functions - Serverless functions configuration object
101
+ * @returns {Object} Modified functions object with updated handler paths
102
+ */
79
103
  const modifyHandlerPaths = (functions) => {
80
104
  // Check if we're running in offline mode
81
105
  const isOffline = process.argv.includes('offline');
@@ -94,7 +118,8 @@ const modifyHandlerPaths = (functions) => {
94
118
  const functionDef = modifiedFunctions[functionName];
95
119
  if (functionDef?.handler?.includes('node_modules/')) {
96
120
  // Replace node_modules/ with the actual path to node_modules/
97
- functionDef.handler = functionDef.handler.replace('node_modules/', '../node_modules/');
121
+ const relativePath = path.relative(process.cwd(), nodeModulesPath);
122
+ functionDef.handler = functionDef.handler.replace('node_modules/', `${relativePath}/`);
98
123
  console.log(`Updated handler for ${functionName}: ${functionDef.handler}`);
99
124
  }
100
125
  }
@@ -102,7 +127,367 @@ const modifyHandlerPaths = (functions) => {
102
127
  return modifiedFunctions;
103
128
  };
104
129
 
105
- const composeServerlessDefinition = (AppDefinition) => {
130
+ /**
131
+ * Create VPC infrastructure resources for CloudFormation
132
+ * Creates VPC, subnets, NAT gateway, route tables, and security groups
133
+ * @param {Object} AppDefinition - Application definition object
134
+ * @param {Object} AppDefinition.vpc - VPC configuration
135
+ * @param {string} [AppDefinition.vpc.cidrBlock='10.0.0.0/16'] - CIDR block for VPC
136
+ * @returns {Object} CloudFormation resources for VPC infrastructure
137
+ */
138
+ const createVPCInfrastructure = (AppDefinition) => {
139
+ const vpcResources = {
140
+ // VPC
141
+ FriggVPC: {
142
+ Type: 'AWS::EC2::VPC',
143
+ Properties: {
144
+ CidrBlock: AppDefinition.vpc.cidrBlock || '10.0.0.0/16',
145
+ EnableDnsHostnames: true,
146
+ EnableDnsSupport: true,
147
+ Tags: [
148
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-vpc' }
149
+ ]
150
+ }
151
+ },
152
+
153
+ // Internet Gateway
154
+ FriggInternetGateway: {
155
+ Type: 'AWS::EC2::InternetGateway',
156
+ Properties: {
157
+ Tags: [
158
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-igw' }
159
+ ]
160
+ }
161
+ },
162
+
163
+ // Attach Internet Gateway to VPC
164
+ FriggVPCGatewayAttachment: {
165
+ Type: 'AWS::EC2::VPCGatewayAttachment',
166
+ Properties: {
167
+ VpcId: { Ref: 'FriggVPC' },
168
+ InternetGatewayId: { Ref: 'FriggInternetGateway' }
169
+ }
170
+ },
171
+
172
+ // Public Subnet for NAT Gateway
173
+ FriggPublicSubnet: {
174
+ Type: 'AWS::EC2::Subnet',
175
+ Properties: {
176
+ VpcId: { Ref: 'FriggVPC' },
177
+ CidrBlock: '10.0.1.0/24',
178
+ AvailabilityZone: { 'Fn::Select': [0, { 'Fn::GetAZs': '' }] },
179
+ MapPublicIpOnLaunch: true,
180
+ Tags: [
181
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-public-subnet' }
182
+ ]
183
+ }
184
+ },
185
+
186
+ // Private Subnet 1 for Lambda
187
+ FriggPrivateSubnet1: {
188
+ Type: 'AWS::EC2::Subnet',
189
+ Properties: {
190
+ VpcId: { Ref: 'FriggVPC' },
191
+ CidrBlock: '10.0.2.0/24',
192
+ AvailabilityZone: { 'Fn::Select': [0, { 'Fn::GetAZs': '' }] },
193
+ Tags: [
194
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-private-subnet-1' }
195
+ ]
196
+ }
197
+ },
198
+
199
+ // Private Subnet 2 for Lambda (different AZ for redundancy)
200
+ FriggPrivateSubnet2: {
201
+ Type: 'AWS::EC2::Subnet',
202
+ Properties: {
203
+ VpcId: { Ref: 'FriggVPC' },
204
+ CidrBlock: '10.0.3.0/24',
205
+ AvailabilityZone: { 'Fn::Select': [1, { 'Fn::GetAZs': '' }] },
206
+ Tags: [
207
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-private-subnet-2' }
208
+ ]
209
+ }
210
+ },
211
+
212
+ // Elastic IP for NAT Gateway
213
+ FriggNATGatewayEIP: {
214
+ Type: 'AWS::EC2::EIP',
215
+ Properties: {
216
+ Domain: 'vpc',
217
+ Tags: [
218
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-nat-eip' }
219
+ ]
220
+ },
221
+ DependsOn: 'FriggVPCGatewayAttachment'
222
+ },
223
+
224
+ // NAT Gateway for private subnet internet access
225
+ FriggNATGateway: {
226
+ Type: 'AWS::EC2::NatGateway',
227
+ Properties: {
228
+ AllocationId: { 'Fn::GetAtt': ['FriggNATGatewayEIP', 'AllocationId'] },
229
+ SubnetId: { Ref: 'FriggPublicSubnet' },
230
+ Tags: [
231
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-nat-gateway' }
232
+ ]
233
+ }
234
+ },
235
+
236
+ // Public Route Table
237
+ FriggPublicRouteTable: {
238
+ Type: 'AWS::EC2::RouteTable',
239
+ Properties: {
240
+ VpcId: { Ref: 'FriggVPC' },
241
+ Tags: [
242
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-public-rt' }
243
+ ]
244
+ }
245
+ },
246
+
247
+ // Public Route to Internet Gateway
248
+ FriggPublicRoute: {
249
+ Type: 'AWS::EC2::Route',
250
+ Properties: {
251
+ RouteTableId: { Ref: 'FriggPublicRouteTable' },
252
+ DestinationCidrBlock: '0.0.0.0/0',
253
+ GatewayId: { Ref: 'FriggInternetGateway' }
254
+ },
255
+ DependsOn: 'FriggVPCGatewayAttachment'
256
+ },
257
+
258
+ // Associate Public Subnet with Public Route Table
259
+ FriggPublicSubnetRouteTableAssociation: {
260
+ Type: 'AWS::EC2::SubnetRouteTableAssociation',
261
+ Properties: {
262
+ SubnetId: { Ref: 'FriggPublicSubnet' },
263
+ RouteTableId: { Ref: 'FriggPublicRouteTable' }
264
+ }
265
+ },
266
+
267
+ // Private Route Table
268
+ FriggPrivateRouteTable: {
269
+ Type: 'AWS::EC2::RouteTable',
270
+ Properties: {
271
+ VpcId: { Ref: 'FriggVPC' },
272
+ Tags: [
273
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-private-rt' }
274
+ ]
275
+ }
276
+ },
277
+
278
+ // Private Route to NAT Gateway
279
+ FriggPrivateRoute: {
280
+ Type: 'AWS::EC2::Route',
281
+ Properties: {
282
+ RouteTableId: { Ref: 'FriggPrivateRouteTable' },
283
+ DestinationCidrBlock: '0.0.0.0/0',
284
+ NatGatewayId: { Ref: 'FriggNATGateway' }
285
+ }
286
+ },
287
+
288
+ // Associate Private Subnet 1 with Private Route Table
289
+ FriggPrivateSubnet1RouteTableAssociation: {
290
+ Type: 'AWS::EC2::SubnetRouteTableAssociation',
291
+ Properties: {
292
+ SubnetId: { Ref: 'FriggPrivateSubnet1' },
293
+ RouteTableId: { Ref: 'FriggPrivateRouteTable' }
294
+ }
295
+ },
296
+
297
+ // Associate Private Subnet 2 with Private Route Table
298
+ FriggPrivateSubnet2RouteTableAssociation: {
299
+ Type: 'AWS::EC2::SubnetRouteTableAssociation',
300
+ Properties: {
301
+ SubnetId: { Ref: 'FriggPrivateSubnet2' },
302
+ RouteTableId: { Ref: 'FriggPrivateRouteTable' }
303
+ }
304
+ },
305
+
306
+ // Security Group for Lambda functions
307
+ FriggLambdaSecurityGroup: {
308
+ Type: 'AWS::EC2::SecurityGroup',
309
+ Properties: {
310
+ GroupDescription: 'Security group for Frigg Lambda functions',
311
+ VpcId: { Ref: 'FriggVPC' },
312
+ SecurityGroupEgress: [
313
+ {
314
+ IpProtocol: 'tcp',
315
+ FromPort: 443,
316
+ ToPort: 443,
317
+ CidrIp: '0.0.0.0/0',
318
+ Description: 'HTTPS outbound'
319
+ },
320
+ {
321
+ IpProtocol: 'tcp',
322
+ FromPort: 80,
323
+ ToPort: 80,
324
+ CidrIp: '0.0.0.0/0',
325
+ Description: 'HTTP outbound'
326
+ },
327
+ {
328
+ IpProtocol: 'tcp',
329
+ FromPort: 53,
330
+ ToPort: 53,
331
+ CidrIp: '0.0.0.0/0',
332
+ Description: 'DNS TCP'
333
+ },
334
+ {
335
+ IpProtocol: 'udp',
336
+ FromPort: 53,
337
+ ToPort: 53,
338
+ CidrIp: '0.0.0.0/0',
339
+ Description: 'DNS UDP'
340
+ }
341
+ ],
342
+ Tags: [
343
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-lambda-sg' }
344
+ ]
345
+ }
346
+ }
347
+ };
348
+
349
+ // Add VPC Endpoints for cost optimization
350
+ if (AppDefinition.vpc.enableVPCEndpoints !== false) {
351
+ // S3 Gateway Endpoint (free)
352
+ vpcResources.FriggS3VPCEndpoint = {
353
+ Type: 'AWS::EC2::VPCEndpoint',
354
+ Properties: {
355
+ VpcId: { Ref: 'FriggVPC' },
356
+ ServiceName: 'com.amazonaws.${self:provider.region}.s3',
357
+ VpcEndpointType: 'Gateway',
358
+ RouteTableIds: [
359
+ { Ref: 'FriggPrivateRouteTable' }
360
+ ]
361
+ }
362
+ };
363
+
364
+ // DynamoDB Gateway Endpoint (free)
365
+ vpcResources.FriggDynamoDBVPCEndpoint = {
366
+ Type: 'AWS::EC2::VPCEndpoint',
367
+ Properties: {
368
+ VpcId: { Ref: 'FriggVPC' },
369
+ ServiceName: 'com.amazonaws.${self:provider.region}.dynamodb',
370
+ VpcEndpointType: 'Gateway',
371
+ RouteTableIds: [
372
+ { Ref: 'FriggPrivateRouteTable' }
373
+ ]
374
+ }
375
+ };
376
+
377
+ // KMS Interface Endpoint (paid, but useful if using KMS)
378
+ if (AppDefinition.encryption?.useDefaultKMSForFieldLevelEncryption === true) {
379
+ vpcResources.FriggKMSVPCEndpoint = {
380
+ Type: 'AWS::EC2::VPCEndpoint',
381
+ Properties: {
382
+ VpcId: { Ref: 'FriggVPC' },
383
+ ServiceName: 'com.amazonaws.${self:provider.region}.kms',
384
+ VpcEndpointType: 'Interface',
385
+ SubnetIds: [
386
+ { Ref: 'FriggPrivateSubnet1' },
387
+ { Ref: 'FriggPrivateSubnet2' }
388
+ ],
389
+ SecurityGroupIds: [
390
+ { Ref: 'FriggVPCEndpointSecurityGroup' }
391
+ ],
392
+ PrivateDnsEnabled: true
393
+ }
394
+ };
395
+ }
396
+
397
+ // Secrets Manager Interface Endpoint (paid, but useful for secrets)
398
+ vpcResources.FriggSecretsManagerVPCEndpoint = {
399
+ Type: 'AWS::EC2::VPCEndpoint',
400
+ Properties: {
401
+ VpcId: { Ref: 'FriggVPC' },
402
+ ServiceName: 'com.amazonaws.${self:provider.region}.secretsmanager',
403
+ VpcEndpointType: 'Interface',
404
+ SubnetIds: [
405
+ { Ref: 'FriggPrivateSubnet1' },
406
+ { Ref: 'FriggPrivateSubnet2' }
407
+ ],
408
+ SecurityGroupIds: [
409
+ { Ref: 'FriggVPCEndpointSecurityGroup' }
410
+ ],
411
+ PrivateDnsEnabled: true
412
+ }
413
+ };
414
+
415
+ // Security Group for VPC Endpoints
416
+ vpcResources.FriggVPCEndpointSecurityGroup = {
417
+ Type: 'AWS::EC2::SecurityGroup',
418
+ Properties: {
419
+ GroupDescription: 'Security group for Frigg VPC Endpoints',
420
+ VpcId: { Ref: 'FriggVPC' },
421
+ SecurityGroupIngress: [
422
+ {
423
+ IpProtocol: 'tcp',
424
+ FromPort: 443,
425
+ ToPort: 443,
426
+ SourceSecurityGroupId: { Ref: 'FriggLambdaSecurityGroup' },
427
+ Description: 'HTTPS from Lambda'
428
+ }
429
+ ],
430
+ Tags: [
431
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-vpc-endpoint-sg' }
432
+ ]
433
+ }
434
+ };
435
+ }
436
+
437
+ return vpcResources;
438
+ };
439
+
440
+ /**
441
+ * Compose a complete serverless framework configuration from app definition
442
+ * @param {Object} AppDefinition - Application definition object
443
+ * @param {string} [AppDefinition.name] - Application name
444
+ * @param {string} [AppDefinition.provider='aws'] - Cloud provider
445
+ * @param {Array} AppDefinition.integrations - Array of integration definitions
446
+ * @param {Object} [AppDefinition.vpc] - VPC configuration
447
+ * @param {Object} [AppDefinition.encryption] - KMS encryption configuration
448
+ * @param {Object} [AppDefinition.ssm] - SSM parameter store configuration
449
+ * @param {Object} [AppDefinition.websockets] - WebSocket configuration
450
+ * @param {boolean} [AppDefinition.websockets.enable=false] - Enable WebSocket support for live update streaming
451
+ * @returns {Object} Complete serverless framework configuration
452
+ */
453
+ const composeServerlessDefinition = async (AppDefinition) => {
454
+ // Store discovered resources
455
+ let discoveredResources = {};
456
+
457
+ // Run AWS discovery if needed
458
+ if (shouldRunDiscovery(AppDefinition)) {
459
+ console.log('🔍 Running AWS resource discovery for serverless template...');
460
+ try {
461
+ const region = process.env.AWS_REGION || 'us-east-1';
462
+ const discovery = new AWSDiscovery(region);
463
+
464
+ const config = {
465
+ vpc: AppDefinition.vpc || {},
466
+ encryption: AppDefinition.encryption || {},
467
+ ssm: AppDefinition.ssm || {}
468
+ };
469
+
470
+ discoveredResources = await discovery.discoverResources(config);
471
+
472
+ console.log('✅ AWS discovery completed successfully!');
473
+ if (discoveredResources.defaultVpcId) {
474
+ console.log(` VPC: ${discoveredResources.defaultVpcId}`);
475
+ }
476
+ if (discoveredResources.privateSubnetId1 && discoveredResources.privateSubnetId2) {
477
+ console.log(` Subnets: ${discoveredResources.privateSubnetId1}, ${discoveredResources.privateSubnetId2}`);
478
+ }
479
+ if (discoveredResources.defaultSecurityGroupId) {
480
+ console.log(` Security Group: ${discoveredResources.defaultSecurityGroupId}`);
481
+ }
482
+ if (discoveredResources.defaultKmsKeyId) {
483
+ console.log(` KMS Key: ${discoveredResources.defaultKmsKeyId}`);
484
+ }
485
+ } catch (error) {
486
+ console.error('❌ AWS discovery failed:', error.message);
487
+ throw new Error(`AWS discovery failed: ${error.message}`);
488
+ }
489
+ }
490
+
106
491
  const definition = {
107
492
  frameworkVersion: '>=3.17.0',
108
493
  service: AppDefinition.name || 'create-frigg-app',
@@ -120,6 +505,14 @@ const composeServerlessDefinition = (AppDefinition) => {
120
505
  environment: {
121
506
  STAGE: '${opt:stage}',
122
507
  AWS_NODEJS_CONNECTION_REUSE_ENABLED: 1,
508
+ // Add discovered resources to environment if available
509
+ ...(discoveredResources.defaultVpcId && { AWS_DISCOVERY_VPC_ID: discoveredResources.defaultVpcId }),
510
+ ...(discoveredResources.defaultSecurityGroupId && { AWS_DISCOVERY_SECURITY_GROUP_ID: discoveredResources.defaultSecurityGroupId }),
511
+ ...(discoveredResources.privateSubnetId1 && { AWS_DISCOVERY_SUBNET_ID_1: discoveredResources.privateSubnetId1 }),
512
+ ...(discoveredResources.privateSubnetId2 && { AWS_DISCOVERY_SUBNET_ID_2: discoveredResources.privateSubnetId2 }),
513
+ ...(discoveredResources.publicSubnetId && { AWS_DISCOVERY_PUBLIC_SUBNET_ID: discoveredResources.publicSubnetId }),
514
+ ...(discoveredResources.defaultRouteTableId && { AWS_DISCOVERY_ROUTE_TABLE_ID: discoveredResources.defaultRouteTableId }),
515
+ ...(discoveredResources.defaultKmsKeyId && { AWS_DISCOVERY_KMS_KEY_ID: discoveredResources.defaultKmsKeyId }),
123
516
  },
124
517
  iamRoleStatements: [
125
518
  {
@@ -180,26 +573,6 @@ const composeServerlessDefinition = (AppDefinition) => {
180
573
  },
181
574
  },
182
575
  functions: {
183
- defaultWebsocket: {
184
- handler: 'node_modules/@friggframework/core/handlers/routers/websocket.handler',
185
- events: [
186
- {
187
- websocket: {
188
- route: '$connect',
189
- },
190
- },
191
- {
192
- websocket: {
193
- route: '$default',
194
- },
195
- },
196
- {
197
- websocket: {
198
- route: '$disconnect',
199
- },
200
- },
201
- ],
202
- },
203
576
  auth: {
204
577
  handler: 'node_modules/@friggframework/core/handlers/routers/auth.handler',
205
578
  events: [
@@ -245,7 +618,7 @@ const composeServerlessDefinition = (AppDefinition) => {
245
618
  Type: 'AWS::SQS::Queue',
246
619
  Properties: {
247
620
  QueueName:
248
- 'internal-error-queue-${self:provider.stage}',
621
+ '${self:service}-internal-error-queue-${self:provider.stage}',
249
622
  MessageRetentionPeriod: 300,
250
623
  },
251
624
  },
@@ -329,6 +702,7 @@ const composeServerlessDefinition = (AppDefinition) => {
329
702
  },
330
703
  };
331
704
 
705
+
332
706
  // KMS Configuration based on App Definition
333
707
  if (AppDefinition.encryption?.useDefaultKMSForFieldLevelEncryption === true) {
334
708
  // Add KMS IAM permissions
@@ -347,12 +721,190 @@ const composeServerlessDefinition = (AppDefinition) => {
347
721
  // Add serverless-kms-grants plugin
348
722
  definition.plugins.push('serverless-kms-grants');
349
723
 
350
- // Configure KMS grants with default key
724
+ // Configure KMS grants with discovered default key
351
725
  definition.custom.kmsGrants = {
352
- kmsKeyId: '*'
726
+ kmsKeyId: discoveredResources.defaultKmsKeyId || '${env:AWS_DISCOVERY_KMS_KEY_ID}'
353
727
  };
354
728
  }
355
729
 
730
+ // VPC Configuration based on App Definition
731
+ if (AppDefinition.vpc?.enable === true) {
732
+ // Add VPC-related IAM permissions
733
+ definition.provider.iamRoleStatements.push({
734
+ Effect: 'Allow',
735
+ Action: [
736
+ 'ec2:CreateNetworkInterface',
737
+ 'ec2:DescribeNetworkInterfaces',
738
+ 'ec2:DeleteNetworkInterface',
739
+ 'ec2:AttachNetworkInterface',
740
+ 'ec2:DetachNetworkInterface'
741
+ ],
742
+ Resource: '*'
743
+ });
744
+
745
+ // Default approach: Use AWS Discovery to find existing VPC resources
746
+ if (AppDefinition.vpc.createNew === true) {
747
+ // Option 1: Create new VPC infrastructure (explicit opt-in)
748
+ const vpcConfig = {};
749
+
750
+ if (AppDefinition.vpc.securityGroupIds) {
751
+ // User provided custom security groups
752
+ vpcConfig.securityGroupIds = AppDefinition.vpc.securityGroupIds;
753
+ } else {
754
+ // Use auto-created security group
755
+ vpcConfig.securityGroupIds = [{ Ref: 'FriggLambdaSecurityGroup' }];
756
+ }
757
+
758
+ if (AppDefinition.vpc.subnetIds) {
759
+ // User provided custom subnets
760
+ vpcConfig.subnetIds = AppDefinition.vpc.subnetIds;
761
+ } else {
762
+ // Use auto-created private subnets
763
+ vpcConfig.subnetIds = [
764
+ { Ref: 'FriggPrivateSubnet1' },
765
+ { Ref: 'FriggPrivateSubnet2' }
766
+ ];
767
+ }
768
+
769
+ // Set VPC config for Lambda functions
770
+ definition.provider.vpc = vpcConfig;
771
+
772
+ // Add VPC infrastructure resources to CloudFormation
773
+ const vpcResources = createVPCInfrastructure(AppDefinition);
774
+ Object.assign(definition.resources.Resources, vpcResources);
775
+ } else {
776
+ // Option 2: Use AWS Discovery (default behavior)
777
+ // VPC configuration using discovered or explicitly provided resources
778
+ const vpcConfig = {
779
+ securityGroupIds: AppDefinition.vpc.securityGroupIds ||
780
+ (discoveredResources.defaultSecurityGroupId ? [discoveredResources.defaultSecurityGroupId] : []),
781
+ subnetIds: AppDefinition.vpc.subnetIds ||
782
+ (discoveredResources.privateSubnetId1 && discoveredResources.privateSubnetId2 ?
783
+ [discoveredResources.privateSubnetId1, discoveredResources.privateSubnetId2] :
784
+ [])
785
+ };
786
+
787
+ // Set VPC config for Lambda functions only if we have valid subnet IDs
788
+ if (vpcConfig.subnetIds.length >= 2 && vpcConfig.securityGroupIds.length > 0) {
789
+ definition.provider.vpc = vpcConfig;
790
+
791
+ // Check if we have an existing NAT Gateway to use
792
+ if (!discoveredResources.existingNatGatewayId) {
793
+ // No existing NAT Gateway, create new resources
794
+
795
+ // Only create EIP if we don't have an existing one available
796
+ if (!discoveredResources.existingElasticIpAllocationId) {
797
+ definition.resources.Resources.FriggNATGatewayEIP = {
798
+ Type: 'AWS::EC2::EIP',
799
+ Properties: {
800
+ Domain: 'vpc',
801
+ Tags: [
802
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-nat-eip' }
803
+ ]
804
+ }
805
+ };
806
+ }
807
+
808
+ definition.resources.Resources.FriggNATGateway = {
809
+ Type: 'AWS::EC2::NatGateway',
810
+ Properties: {
811
+ AllocationId: discoveredResources.existingElasticIpAllocationId ||
812
+ { 'Fn::GetAtt': ['FriggNATGatewayEIP', 'AllocationId'] },
813
+ SubnetId: discoveredResources.publicSubnetId || discoveredResources.privateSubnetId1, // Use first discovered subnet if no public subnet found
814
+ Tags: [
815
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-nat-gateway' }
816
+ ]
817
+ }
818
+ };
819
+ }
820
+
821
+ // Create route table for Lambda subnets to use NAT Gateway
822
+ definition.resources.Resources.FriggLambdaRouteTable = {
823
+ Type: 'AWS::EC2::RouteTable',
824
+ Properties: {
825
+ VpcId: discoveredResources.defaultVpcId || { Ref: 'FriggVPC' },
826
+ Tags: [
827
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-lambda-rt' }
828
+ ]
829
+ }
830
+ };
831
+
832
+ definition.resources.Resources.FriggNATRoute = {
833
+ Type: 'AWS::EC2::Route',
834
+ Properties: {
835
+ RouteTableId: { Ref: 'FriggLambdaRouteTable' },
836
+ DestinationCidrBlock: '0.0.0.0/0',
837
+ NatGatewayId: discoveredResources.existingNatGatewayId || { Ref: 'FriggNATGateway' }
838
+ }
839
+ };
840
+
841
+ // Associate Lambda subnets with NAT Gateway route table
842
+ definition.resources.Resources.FriggSubnet1RouteAssociation = {
843
+ Type: 'AWS::EC2::SubnetRouteTableAssociation',
844
+ Properties: {
845
+ SubnetId: vpcConfig.subnetIds[0],
846
+ RouteTableId: { Ref: 'FriggLambdaRouteTable' }
847
+ }
848
+ };
849
+
850
+ definition.resources.Resources.FriggSubnet2RouteAssociation = {
851
+ Type: 'AWS::EC2::SubnetRouteTableAssociation',
852
+ Properties: {
853
+ SubnetId: vpcConfig.subnetIds[1],
854
+ RouteTableId: { Ref: 'FriggLambdaRouteTable' }
855
+ }
856
+ };
857
+
858
+ // Add VPC endpoints for AWS service optimization (optional but recommended)
859
+ if (AppDefinition.vpc.enableVPCEndpoints !== false) {
860
+ definition.resources.Resources.VPCEndpointS3 = {
861
+ Type: 'AWS::EC2::VPCEndpoint',
862
+ Properties: {
863
+ VpcId: discoveredResources.defaultVpcId,
864
+ ServiceName: 'com.amazonaws.${self:provider.region}.s3',
865
+ VpcEndpointType: 'Gateway',
866
+ RouteTableIds: [{ Ref: 'FriggLambdaRouteTable' }]
867
+ }
868
+ };
869
+
870
+ definition.resources.Resources.VPCEndpointDynamoDB = {
871
+ Type: 'AWS::EC2::VPCEndpoint',
872
+ Properties: {
873
+ VpcId: discoveredResources.defaultVpcId,
874
+ ServiceName: 'com.amazonaws.${self:provider.region}.dynamodb',
875
+ VpcEndpointType: 'Gateway',
876
+ RouteTableIds: [{ Ref: 'FriggLambdaRouteTable' }]
877
+ }
878
+ };
879
+ }
880
+ }
881
+ }
882
+ }
883
+
884
+ // SSM Parameter Store Configuration based on App Definition
885
+ if (AppDefinition.ssm?.enable === true) {
886
+ // Add AWS Parameters and Secrets Lambda Extension layer
887
+ definition.provider.layers = [
888
+ 'arn:aws:lambda:${self:provider.region}:177933569100:layer:AWS-Parameters-and-Secrets-Lambda-Extension:11'
889
+ ];
890
+
891
+ // Add SSM IAM permissions
892
+ definition.provider.iamRoleStatements.push({
893
+ Effect: 'Allow',
894
+ Action: [
895
+ 'ssm:GetParameter',
896
+ 'ssm:GetParameters',
897
+ 'ssm:GetParametersByPath'
898
+ ],
899
+ Resource: [
900
+ 'arn:aws:ssm:${self:provider.region}:*:parameter/${self:service}/${self:provider.stage}/*'
901
+ ]
902
+ });
903
+
904
+ // Add environment variable for SSM parameter prefix
905
+ definition.provider.environment.SSM_PARAMETER_PREFIX = '/${self:service}/${self:provider.stage}';
906
+ }
907
+
356
908
  // Add integration-specific functions and resources
357
909
  for (const integration of AppDefinition.integrations) {
358
910
  const integrationName = integration.Definition.name;
@@ -419,6 +971,33 @@ const composeServerlessDefinition = (AppDefinition) => {
419
971
  definition.custom[queueReference] = queueName;
420
972
  }
421
973
 
974
+ // Discovery has already run successfully at this point if needed
975
+ // The discoveredResources object contains all the necessary AWS resources
976
+
977
+ // Add websocket function if enabled
978
+ if (AppDefinition.websockets?.enable === true) {
979
+ definition.functions.defaultWebsocket = {
980
+ handler: 'node_modules/@friggframework/core/handlers/routers/websocket.handler',
981
+ events: [
982
+ {
983
+ websocket: {
984
+ route: '$connect',
985
+ },
986
+ },
987
+ {
988
+ websocket: {
989
+ route: '$default',
990
+ },
991
+ },
992
+ {
993
+ websocket: {
994
+ route: '$disconnect',
995
+ },
996
+ },
997
+ ],
998
+ };
999
+ }
1000
+
422
1001
  // Modify handler paths to point to the correct node_modules location
423
1002
  definition.functions = modifyHandlerPaths(definition.functions);
424
1003