@friggframework/devtools 2.0.0--canary.397.5f65dbd.0 → 2.0.0--canary.398.7664c46.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 (36) 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 +10 -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 +659 -48
  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-CbM64Oba.js +1221 -0
  31. package/management-ui/dist/assets/index-CkvseXTC.css +1 -0
  32. package/management-ui/dist/index.html +14 -0
  33. package/package.json +9 -5
  34. package/test/auther-definition-tester.js +125 -0
  35. package/test/index.js +4 -2
  36. 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',
@@ -115,11 +500,19 @@ const composeServerlessDefinition = (AppDefinition) => {
115
500
  name: AppDefinition.provider || 'aws',
116
501
  runtime: 'nodejs20.x',
117
502
  timeout: 30,
118
- region: 'us-east-1',
503
+ region: process.env.AWS_REGION || 'us-east-1',
119
504
  stage: '${opt:stage}',
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
  {
@@ -170,7 +563,7 @@ const composeServerlessDefinition = (AppDefinition) => {
170
563
  autoCreate: false,
171
564
  apiVersion: '2012-11-05',
172
565
  endpoint: 'http://localhost:4566',
173
- region: 'us-east-1',
566
+ region: process.env.AWS_REGION || 'us-east-1',
174
567
  accessKeyId: 'root',
175
568
  secretAccessKey: 'root',
176
569
  skipCacheInvalidation: false,
@@ -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,18 +721,200 @@ 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
- for (const integration of AppDefinition.integrations) {
358
- const integrationName = integration.Definition.name;
909
+ if (AppDefinition.integrations && Array.isArray(AppDefinition.integrations)) {
910
+ for (const integration of AppDefinition.integrations) {
911
+ if (!integration || !integration.Definition || !integration.Definition.name) {
912
+ throw new Error('Invalid integration: missing Definition or name');
913
+ }
914
+ const integrationName = integration.Definition.name;
359
915
 
360
- // Add function for the integration
361
- definition.functions[integrationName] = {
916
+ // Add function for the integration
917
+ definition.functions[integrationName] = {
362
918
  handler: `node_modules/@friggframework/core/handlers/routers/integration-defined-routers.handlers.${integrationName}.handler`,
363
919
  events: [
364
920
  {
@@ -371,11 +927,11 @@ const composeServerlessDefinition = (AppDefinition) => {
371
927
  ],
372
928
  };
373
929
 
374
- // Add SQS Queue for the integration
375
- const queueReference = `${integrationName.charAt(0).toUpperCase() + integrationName.slice(1)
376
- }Queue`;
377
- const queueName = `\${self:service}--\${self:provider.stage}-${queueReference}`;
378
- definition.resources.Resources[queueReference] = {
930
+ // Add SQS Queue for the integration
931
+ const queueReference = `${integrationName.charAt(0).toUpperCase() + integrationName.slice(1)
932
+ }Queue`;
933
+ const queueName = `\${self:service}--\${self:provider.stage}-${queueReference}`;
934
+ definition.resources.Resources[queueReference] = {
379
935
  Type: 'AWS::SQS::Queue',
380
936
  Properties: {
381
937
  QueueName: `\${self:custom.${queueReference}}`,
@@ -390,9 +946,9 @@ const composeServerlessDefinition = (AppDefinition) => {
390
946
  },
391
947
  };
392
948
 
393
- // Add Queue Worker for the integration
394
- const queueWorkerName = `${integrationName}QueueWorker`;
395
- definition.functions[queueWorkerName] = {
949
+ // Add Queue Worker for the integration
950
+ const queueWorkerName = `${integrationName}QueueWorker`;
951
+ definition.functions[queueWorkerName] = {
396
952
  handler: `node_modules/@friggframework/core/handlers/workers/integration-defined-workers.handlers.${integrationName}.queueWorker`,
397
953
  reservedConcurrency: 5,
398
954
  events: [
@@ -408,15 +964,70 @@ const composeServerlessDefinition = (AppDefinition) => {
408
964
  timeout: 600,
409
965
  };
410
966
 
411
- // Add Queue URL for the integration to the ENVironment variables
412
- definition.provider.environment = {
413
- ...definition.provider.environment,
414
- [`${integrationName.toUpperCase()}_QUEUE_URL`]: {
415
- Ref: queueReference,
416
- },
967
+ // Add Queue URL for the integration to the ENVironment variables
968
+ definition.provider.environment = {
969
+ ...definition.provider.environment,
970
+ [`${integrationName.toUpperCase()}_QUEUE_URL`]: {
971
+ Ref: queueReference,
972
+ },
973
+ };
974
+
975
+ definition.custom[queueReference] = queueName;
976
+ }
977
+ }
978
+
979
+ // Discovery has already run successfully at this point if needed
980
+ // The discoveredResources object contains all the necessary AWS resources
981
+
982
+ // Add websocket function if enabled
983
+ if (AppDefinition.websockets?.enable === true) {
984
+ definition.functions.defaultWebsocket = {
985
+ handler: 'node_modules/@friggframework/core/handlers/routers/websocket.handler',
986
+ events: [
987
+ {
988
+ websocket: {
989
+ route: '$connect',
990
+ },
991
+ },
992
+ {
993
+ websocket: {
994
+ route: '$default',
995
+ },
996
+ },
997
+ {
998
+ websocket: {
999
+ route: '$disconnect',
1000
+ },
1001
+ },
1002
+ ],
417
1003
  };
1004
+ }
1005
+
1006
+ // Discovery has already run successfully at this point if needed
1007
+ // The discoveredResources object contains all the necessary AWS resources
418
1008
 
419
- definition.custom[queueReference] = queueName;
1009
+ // Add websocket function if enabled
1010
+ if (AppDefinition.websockets?.enable === true) {
1011
+ definition.functions.defaultWebsocket = {
1012
+ handler: 'node_modules/@friggframework/core/handlers/routers/websocket.handler',
1013
+ events: [
1014
+ {
1015
+ websocket: {
1016
+ route: '$connect',
1017
+ },
1018
+ },
1019
+ {
1020
+ websocket: {
1021
+ route: '$default',
1022
+ },
1023
+ },
1024
+ {
1025
+ websocket: {
1026
+ route: '$disconnect',
1027
+ },
1028
+ },
1029
+ ],
1030
+ };
420
1031
  }
421
1032
 
422
1033
  // Modify handler paths to point to the correct node_modules location