@friggframework/devtools 2.0.0-next.39 → 2.0.0-next.40

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.
@@ -2,28 +2,13 @@ const path = require('path');
2
2
  const fs = require('fs');
3
3
  const { AWSDiscovery } = require('./aws-discovery');
4
4
 
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 (
12
- AppDefinition.vpc?.enable === true ||
13
- AppDefinition.encryption?.fieldLevelEncryptionMethod === 'kms' ||
14
- AppDefinition.ssm?.enable === true
15
- );
16
- };
5
+ const shouldRunDiscovery = (AppDefinition) =>
6
+ AppDefinition.vpc?.enable === true ||
7
+ AppDefinition.encryption?.fieldLevelEncryptionMethod === 'kms' ||
8
+ AppDefinition.ssm?.enable === true;
17
9
 
18
- /**
19
- * Extract environment variables from AppDefinition
20
- * @param {Object} AppDefinition - Application definition
21
- * @returns {Object} Environment variables to set in serverless
22
- */
23
10
  const getAppEnvironmentVars = (AppDefinition) => {
24
11
  const envVars = {};
25
-
26
- // AWS Lambda reserved environment variables that cannot be set (from official AWS docs)
27
12
  const reservedVars = new Set([
28
13
  '_HANDLER',
29
14
  '_X_AMZN_TRACE_ID',
@@ -42,117 +27,80 @@ const getAppEnvironmentVars = (AppDefinition) => {
42
27
  'AWS_SESSION_TOKEN',
43
28
  ]);
44
29
 
45
- if (AppDefinition.environment) {
46
- console.log('📋 Loading environment variables from appDefinition...');
47
- const envKeys = [];
48
- const skippedKeys = [];
30
+ if (!AppDefinition.environment) {
31
+ return envVars;
32
+ }
49
33
 
50
- for (const [key, value] of Object.entries(AppDefinition.environment)) {
51
- if (value === true) {
52
- if (reservedVars.has(key)) {
53
- skippedKeys.push(key);
54
- } else {
55
- envVars[key] = `\${env:${key}, ''}`;
56
- envKeys.push(key);
57
- }
58
- }
59
- }
34
+ console.log('📋 Loading environment variables from appDefinition...');
35
+ const envKeys = [];
36
+ const skippedKeys = [];
60
37
 
61
- if (envKeys.length > 0) {
62
- console.log(
63
- ` Found ${
64
- envKeys.length
65
- } environment variables: ${envKeys.join(', ')}`
66
- );
67
- }
68
- if (skippedKeys.length > 0) {
69
- console.log(
70
- ` ⚠️ Skipped ${
71
- skippedKeys.length
72
- } reserved AWS Lambda variables: ${skippedKeys.join(', ')}`
73
- );
38
+ for (const [key, value] of Object.entries(AppDefinition.environment)) {
39
+ if (value !== true) continue;
40
+ if (reservedVars.has(key)) {
41
+ skippedKeys.push(key);
42
+ continue;
74
43
  }
44
+ envVars[key] = `\${env:${key}, ''}`;
45
+ envKeys.push(key);
46
+ }
47
+
48
+ if (envKeys.length > 0) {
49
+ console.log(` Found ${envKeys.length} environment variables: ${envKeys.join(', ')}`);
50
+ }
51
+ if (skippedKeys.length > 0) {
52
+ console.log(
53
+ ` ⚠️ Skipped ${skippedKeys.length} reserved AWS Lambda variables: ${skippedKeys.join(', ')}`
54
+ );
75
55
  }
76
56
 
77
57
  return envVars;
78
58
  };
79
59
 
80
- /**
81
- * Find the actual path to node_modules directory
82
- * Tries multiple methods to locate node_modules:
83
- * 1. Traversing up from current directory
84
- * 2. Using npm root command
85
- * 3. Looking for package.json and adjacent node_modules
86
- * @returns {string} Path to node_modules directory
87
- */
88
60
  const findNodeModulesPath = () => {
89
61
  try {
90
- // Method 1: Try to find node_modules by traversing up from current directory
91
62
  let currentDir = process.cwd();
92
63
  let nodeModulesPath = null;
93
64
 
94
- // Traverse up to 5 levels to find node_modules
95
65
  for (let i = 0; i < 5; i++) {
96
66
  const potentialPath = path.join(currentDir, 'node_modules');
97
67
  if (fs.existsSync(potentialPath)) {
98
68
  nodeModulesPath = potentialPath;
99
- console.log(
100
- `Found node_modules at: ${nodeModulesPath} (method 1)`
101
- );
69
+ console.log(`Found node_modules at: ${nodeModulesPath} (method 1)`);
102
70
  break;
103
71
  }
104
- // Move up one directory
105
72
  const parentDir = path.dirname(currentDir);
106
- if (parentDir === currentDir) {
107
- // We've reached the root
108
- break;
109
- }
73
+ if (parentDir === currentDir) break;
110
74
  currentDir = parentDir;
111
75
  }
112
76
 
113
- // Method 2: If method 1 fails, try using npm root command
114
77
  if (!nodeModulesPath) {
115
78
  try {
116
- // This requires child_process, so let's require it here
117
79
  const { execSync } = require('node:child_process');
118
- const npmRoot = execSync('npm root', {
119
- encoding: 'utf8',
120
- }).trim();
80
+ const npmRoot = execSync('npm root', { encoding: 'utf8' }).trim();
121
81
  if (fs.existsSync(npmRoot)) {
122
82
  nodeModulesPath = npmRoot;
123
- console.log(
124
- `Found node_modules at: ${nodeModulesPath} (method 2)`
125
- );
83
+ console.log(`Found node_modules at: ${nodeModulesPath} (method 2)`);
126
84
  }
127
85
  } catch (npmError) {
128
86
  console.error('Error executing npm root:', npmError);
129
87
  }
130
88
  }
131
89
 
132
- // Method 3: If all else fails, check for a package.json and assume node_modules is adjacent
133
90
  if (!nodeModulesPath) {
134
91
  currentDir = process.cwd();
135
92
  for (let i = 0; i < 5; i++) {
136
93
  const packageJsonPath = path.join(currentDir, 'package.json');
137
94
  if (fs.existsSync(packageJsonPath)) {
138
- const potentialNodeModules = path.join(
139
- currentDir,
140
- 'node_modules'
141
- );
95
+ const potentialNodeModules = path.join(currentDir, 'node_modules');
142
96
  if (fs.existsSync(potentialNodeModules)) {
143
97
  nodeModulesPath = potentialNodeModules;
144
- console.log(
145
- `Found node_modules at: ${nodeModulesPath} (method 3)`
146
- );
98
+ console.log(`Found node_modules at: ${nodeModulesPath} (method 3)`);
147
99
  break;
148
100
  }
149
101
  }
150
- // Move up one directory
151
102
  const parentDir = path.dirname(currentDir);
152
- if (parentDir === currentDir) {
153
- // We've reached the root
154
- break;
155
- }
103
+ if (parentDir === currentDir) break;
156
104
  currentDir = parentDir;
157
105
  }
158
106
  }
@@ -161,9 +109,7 @@ const findNodeModulesPath = () => {
161
109
  return nodeModulesPath;
162
110
  }
163
111
 
164
- console.warn(
165
- 'Could not find node_modules path, falling back to default'
166
- );
112
+ console.warn('Could not find node_modules path, falling back to default');
167
113
  return path.resolve(process.cwd(), '../node_modules');
168
114
  } catch (error) {
169
115
  console.error('Error finding node_modules path:', error);
@@ -171,14 +117,7 @@ const findNodeModulesPath = () => {
171
117
  }
172
118
  };
173
119
 
174
- /**
175
- * Modify handler paths to point to the correct node_modules location
176
- * Only modifies paths when running in offline mode
177
- * @param {Object} functions - Serverless functions configuration object
178
- * @returns {Object} Modified functions object with updated handler paths
179
- */
180
120
  const modifyHandlerPaths = (functions) => {
181
- // Check if we're running in offline mode
182
121
  const isOffline = process.argv.includes('offline');
183
122
  console.log('isOffline', isOffline);
184
123
 
@@ -194,32 +133,17 @@ const modifyHandlerPaths = (functions) => {
194
133
  console.log('functionName', functionName);
195
134
  const functionDef = modifiedFunctions[functionName];
196
135
  if (functionDef?.handler?.includes('node_modules/')) {
197
- // Replace node_modules/ with the actual path to node_modules/
198
136
  const relativePath = path.relative(process.cwd(), nodeModulesPath);
199
- functionDef.handler = functionDef.handler.replace(
200
- 'node_modules/',
201
- `${relativePath}/`
202
- );
203
- console.log(
204
- `Updated handler for ${functionName}: ${functionDef.handler}`
205
- );
137
+ functionDef.handler = functionDef.handler.replace('node_modules/', `${relativePath}/`);
138
+ console.log(`Updated handler for ${functionName}: ${functionDef.handler}`);
206
139
  }
207
140
  }
208
141
 
209
142
  return modifiedFunctions;
210
143
  };
211
144
 
212
- /**
213
- * Create VPC infrastructure resources for CloudFormation
214
- * Creates VPC, subnets, NAT gateway, route tables, and security groups
215
- * @param {Object} AppDefinition - Application definition object
216
- * @param {Object} AppDefinition.vpc - VPC configuration
217
- * @param {string} [AppDefinition.vpc.cidrBlock='10.0.0.0/16'] - CIDR block for VPC
218
- * @returns {Object} CloudFormation resources for VPC infrastructure
219
- */
220
145
  const createVPCInfrastructure = (AppDefinition) => {
221
146
  const vpcResources = {
222
- // VPC
223
147
  FriggVPC: {
224
148
  Type: 'AWS::EC2::VPC',
225
149
  Properties: {
@@ -227,28 +151,24 @@ const createVPCInfrastructure = (AppDefinition) => {
227
151
  EnableDnsHostnames: true,
228
152
  EnableDnsSupport: true,
229
153
  Tags: [
230
- {
231
- Key: 'Name',
232
- Value: '${self:service}-${self:provider.stage}-vpc',
233
- },
154
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-vpc' },
155
+ { Key: 'ManagedBy', Value: 'Frigg' },
156
+ { Key: 'Service', Value: '${self:service}' },
157
+ { Key: 'Stage', Value: '${self:provider.stage}' },
234
158
  ],
235
159
  },
236
160
  },
237
-
238
- // Internet Gateway
239
161
  FriggInternetGateway: {
240
162
  Type: 'AWS::EC2::InternetGateway',
241
163
  Properties: {
242
164
  Tags: [
243
- {
244
- Key: 'Name',
245
- Value: '${self:service}-${self:provider.stage}-igw',
246
- },
165
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-igw' },
166
+ { Key: 'ManagedBy', Value: 'Frigg' },
167
+ { Key: 'Service', Value: '${self:service}' },
168
+ { Key: 'Stage', Value: '${self:provider.stage}' },
247
169
  ],
248
170
  },
249
171
  },
250
-
251
- // Attach Internet Gateway to VPC
252
172
  FriggVPCGatewayAttachment: {
253
173
  Type: 'AWS::EC2::VPCGatewayAttachment',
254
174
  Properties: {
@@ -256,8 +176,6 @@ const createVPCInfrastructure = (AppDefinition) => {
256
176
  InternetGatewayId: { Ref: 'FriggInternetGateway' },
257
177
  },
258
178
  },
259
-
260
- // Public Subnet for NAT Gateway
261
179
  FriggPublicSubnet: {
262
180
  Type: 'AWS::EC2::Subnet',
263
181
  Properties: {
@@ -266,15 +184,14 @@ const createVPCInfrastructure = (AppDefinition) => {
266
184
  AvailabilityZone: { 'Fn::Select': [0, { 'Fn::GetAZs': '' }] },
267
185
  MapPublicIpOnLaunch: true,
268
186
  Tags: [
269
- {
270
- Key: 'Name',
271
- Value: '${self:service}-${self:provider.stage}-public-subnet',
272
- },
187
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-public-subnet' },
188
+ { Key: 'ManagedBy', Value: 'Frigg' },
189
+ { Key: 'Service', Value: '${self:service}' },
190
+ { Key: 'Stage', Value: '${self:provider.stage}' },
191
+ { Key: 'Type', Value: 'Public' },
273
192
  ],
274
193
  },
275
194
  },
276
-
277
- // Private Subnet 1 for Lambda
278
195
  FriggPrivateSubnet1: {
279
196
  Type: 'AWS::EC2::Subnet',
280
197
  Properties: {
@@ -282,15 +199,14 @@ const createVPCInfrastructure = (AppDefinition) => {
282
199
  CidrBlock: '10.0.2.0/24',
283
200
  AvailabilityZone: { 'Fn::Select': [0, { 'Fn::GetAZs': '' }] },
284
201
  Tags: [
285
- {
286
- Key: 'Name',
287
- Value: '${self:service}-${self:provider.stage}-private-subnet-1',
288
- },
202
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-private-subnet-1' },
203
+ { Key: 'ManagedBy', Value: 'Frigg' },
204
+ { Key: 'Service', Value: '${self:service}' },
205
+ { Key: 'Stage', Value: '${self:provider.stage}' },
206
+ { Key: 'Type', Value: 'Private' },
289
207
  ],
290
208
  },
291
209
  },
292
-
293
- // Private Subnet 2 for Lambda (different AZ for redundancy)
294
210
  FriggPrivateSubnet2: {
295
211
  Type: 'AWS::EC2::Subnet',
296
212
  Properties: {
@@ -298,61 +214,53 @@ const createVPCInfrastructure = (AppDefinition) => {
298
214
  CidrBlock: '10.0.3.0/24',
299
215
  AvailabilityZone: { 'Fn::Select': [1, { 'Fn::GetAZs': '' }] },
300
216
  Tags: [
301
- {
302
- Key: 'Name',
303
- Value: '${self:service}-${self:provider.stage}-private-subnet-2',
304
- },
217
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-private-subnet-2' },
218
+ { Key: 'ManagedBy', Value: 'Frigg' },
219
+ { Key: 'Service', Value: '${self:service}' },
220
+ { Key: 'Stage', Value: '${self:provider.stage}' },
221
+ { Key: 'Type', Value: 'Private' },
305
222
  ],
306
223
  },
307
224
  },
308
-
309
- // Elastic IP for NAT Gateway
310
225
  FriggNATGatewayEIP: {
311
226
  Type: 'AWS::EC2::EIP',
312
227
  Properties: {
313
228
  Domain: 'vpc',
314
229
  Tags: [
315
- {
316
- Key: 'Name',
317
- Value: '${self:service}-${self:provider.stage}-nat-eip',
318
- },
230
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-nat-eip' },
231
+ { Key: 'ManagedBy', Value: 'Frigg' },
232
+ { Key: 'Service', Value: '${self:service}' },
233
+ { Key: 'Stage', Value: '${self:provider.stage}' },
319
234
  ],
320
235
  },
321
236
  DependsOn: 'FriggVPCGatewayAttachment',
322
237
  },
323
-
324
- // NAT Gateway for private subnet internet access
325
238
  FriggNATGateway: {
326
239
  Type: 'AWS::EC2::NatGateway',
327
240
  Properties: {
328
- AllocationId: {
329
- 'Fn::GetAtt': ['FriggNATGatewayEIP', 'AllocationId'],
330
- },
241
+ AllocationId: { 'Fn::GetAtt': ['FriggNATGatewayEIP', 'AllocationId'] },
331
242
  SubnetId: { Ref: 'FriggPublicSubnet' },
332
243
  Tags: [
333
- {
334
- Key: 'Name',
335
- Value: '${self:service}-${self:provider.stage}-nat-gateway',
336
- },
244
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-nat-gateway' },
245
+ { Key: 'ManagedBy', Value: 'Frigg' },
246
+ { Key: 'Service', Value: '${self:service}' },
247
+ { Key: 'Stage', Value: '${self:provider.stage}' },
337
248
  ],
338
249
  },
339
250
  },
340
-
341
- // Public Route Table
342
251
  FriggPublicRouteTable: {
343
252
  Type: 'AWS::EC2::RouteTable',
344
253
  Properties: {
345
254
  VpcId: { Ref: 'FriggVPC' },
346
255
  Tags: [
347
- {
348
- Key: 'Name',
349
- Value: '${self:service}-${self:provider.stage}-public-rt',
350
- },
256
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-public-rt' },
257
+ { Key: 'ManagedBy', Value: 'Frigg' },
258
+ { Key: 'Service', Value: '${self:service}' },
259
+ { Key: 'Stage', Value: '${self:provider.stage}' },
260
+ { Key: 'Type', Value: 'Public' },
351
261
  ],
352
262
  },
353
263
  },
354
-
355
- // Public Route to Internet Gateway
356
264
  FriggPublicRoute: {
357
265
  Type: 'AWS::EC2::Route',
358
266
  Properties: {
@@ -362,8 +270,6 @@ const createVPCInfrastructure = (AppDefinition) => {
362
270
  },
363
271
  DependsOn: 'FriggVPCGatewayAttachment',
364
272
  },
365
-
366
- // Associate Public Subnet with Public Route Table
367
273
  FriggPublicSubnetRouteTableAssociation: {
368
274
  Type: 'AWS::EC2::SubnetRouteTableAssociation',
369
275
  Properties: {
@@ -371,22 +277,19 @@ const createVPCInfrastructure = (AppDefinition) => {
371
277
  RouteTableId: { Ref: 'FriggPublicRouteTable' },
372
278
  },
373
279
  },
374
-
375
- // Private Route Table
376
280
  FriggPrivateRouteTable: {
377
281
  Type: 'AWS::EC2::RouteTable',
378
282
  Properties: {
379
283
  VpcId: { Ref: 'FriggVPC' },
380
284
  Tags: [
381
- {
382
- Key: 'Name',
383
- Value: '${self:service}-${self:provider.stage}-private-rt',
384
- },
285
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-private-rt' },
286
+ { Key: 'ManagedBy', Value: 'Frigg' },
287
+ { Key: 'Service', Value: '${self:service}' },
288
+ { Key: 'Stage', Value: '${self:provider.stage}' },
289
+ { Key: 'Type', Value: 'Private' },
385
290
  ],
386
291
  },
387
292
  },
388
-
389
- // Private Route to NAT Gateway
390
293
  FriggPrivateRoute: {
391
294
  Type: 'AWS::EC2::Route',
392
295
  Properties: {
@@ -395,8 +298,6 @@ const createVPCInfrastructure = (AppDefinition) => {
395
298
  NatGatewayId: { Ref: 'FriggNATGateway' },
396
299
  },
397
300
  },
398
-
399
- // Associate Private Subnet 1 with Private Route Table
400
301
  FriggPrivateSubnet1RouteTableAssociation: {
401
302
  Type: 'AWS::EC2::SubnetRouteTableAssociation',
402
303
  Properties: {
@@ -404,8 +305,6 @@ const createVPCInfrastructure = (AppDefinition) => {
404
305
  RouteTableId: { Ref: 'FriggPrivateRouteTable' },
405
306
  },
406
307
  },
407
-
408
- // Associate Private Subnet 2 with Private Route Table
409
308
  FriggPrivateSubnet2RouteTableAssociation: {
410
309
  Type: 'AWS::EC2::SubnetRouteTableAssociation',
411
310
  Properties: {
@@ -413,63 +312,29 @@ const createVPCInfrastructure = (AppDefinition) => {
413
312
  RouteTableId: { Ref: 'FriggPrivateRouteTable' },
414
313
  },
415
314
  },
416
-
417
- // Security Group for Lambda functions
418
315
  FriggLambdaSecurityGroup: {
419
316
  Type: 'AWS::EC2::SecurityGroup',
420
317
  Properties: {
421
318
  GroupDescription: 'Security group for Frigg Lambda functions',
422
319
  VpcId: { Ref: 'FriggVPC' },
423
320
  SecurityGroupEgress: [
424
- {
425
- IpProtocol: 'tcp',
426
- FromPort: 443,
427
- ToPort: 443,
428
- CidrIp: '0.0.0.0/0',
429
- Description: 'HTTPS outbound',
430
- },
431
- {
432
- IpProtocol: 'tcp',
433
- FromPort: 80,
434
- ToPort: 80,
435
- CidrIp: '0.0.0.0/0',
436
- Description: 'HTTP outbound',
437
- },
438
- {
439
- IpProtocol: 'tcp',
440
- FromPort: 53,
441
- ToPort: 53,
442
- CidrIp: '0.0.0.0/0',
443
- Description: 'DNS TCP',
444
- },
445
- {
446
- IpProtocol: 'udp',
447
- FromPort: 53,
448
- ToPort: 53,
449
- CidrIp: '0.0.0.0/0',
450
- Description: 'DNS UDP',
451
- },
452
- {
453
- IpProtocol: 'tcp',
454
- FromPort: 27017,
455
- ToPort: 27017,
456
- CidrIp: '0.0.0.0/0',
457
- Description: 'MongoDB outbound',
458
- },
321
+ { IpProtocol: 'tcp', FromPort: 443, ToPort: 443, CidrIp: '0.0.0.0/0', Description: 'HTTPS outbound' },
322
+ { IpProtocol: 'tcp', FromPort: 80, ToPort: 80, CidrIp: '0.0.0.0/0', Description: 'HTTP outbound' },
323
+ { IpProtocol: 'tcp', FromPort: 53, ToPort: 53, CidrIp: '0.0.0.0/0', Description: 'DNS TCP' },
324
+ { IpProtocol: 'udp', FromPort: 53, ToPort: 53, CidrIp: '0.0.0.0/0', Description: 'DNS UDP' },
325
+ { IpProtocol: 'tcp', FromPort: 27017, ToPort: 27017, CidrIp: '0.0.0.0/0', Description: 'MongoDB outbound' },
459
326
  ],
460
327
  Tags: [
461
- {
462
- Key: 'Name',
463
- Value: '${self:service}-${self:provider.stage}-lambda-sg',
464
- },
328
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-lambda-sg' },
329
+ { Key: 'ManagedBy', Value: 'Frigg' },
330
+ { Key: 'Service', Value: '${self:service}' },
331
+ { Key: 'Stage', Value: '${self:provider.stage}' },
465
332
  ],
466
333
  },
467
334
  },
468
335
  };
469
336
 
470
- // Add VPC Endpoints for cost optimization
471
337
  if (AppDefinition.vpc.enableVPCEndpoints !== false) {
472
- // S3 Gateway Endpoint (free)
473
338
  vpcResources.FriggS3VPCEndpoint = {
474
339
  Type: 'AWS::EC2::VPCEndpoint',
475
340
  Properties: {
@@ -480,7 +345,6 @@ const createVPCInfrastructure = (AppDefinition) => {
480
345
  },
481
346
  };
482
347
 
483
- // DynamoDB Gateway Endpoint (free)
484
348
  vpcResources.FriggDynamoDBVPCEndpoint = {
485
349
  Type: 'AWS::EC2::VPCEndpoint',
486
350
  Properties: {
@@ -491,7 +355,6 @@ const createVPCInfrastructure = (AppDefinition) => {
491
355
  },
492
356
  };
493
357
 
494
- // KMS Interface Endpoint (paid, but useful if using KMS)
495
358
  if (AppDefinition.encryption?.fieldLevelEncryptionMethod === 'kms') {
496
359
  vpcResources.FriggKMSVPCEndpoint = {
497
360
  Type: 'AWS::EC2::VPCEndpoint',
@@ -499,58 +362,54 @@ const createVPCInfrastructure = (AppDefinition) => {
499
362
  VpcId: { Ref: 'FriggVPC' },
500
363
  ServiceName: 'com.amazonaws.${self:provider.region}.kms',
501
364
  VpcEndpointType: 'Interface',
502
- SubnetIds: [
503
- { Ref: 'FriggPrivateSubnet1' },
504
- { Ref: 'FriggPrivateSubnet2' },
505
- ],
506
- SecurityGroupIds: [
507
- { Ref: 'FriggVPCEndpointSecurityGroup' },
508
- ],
365
+ SubnetIds: [{ Ref: 'FriggPrivateSubnet1' }, { Ref: 'FriggPrivateSubnet2' }],
366
+ SecurityGroupIds: [{ Ref: 'FriggVPCEndpointSecurityGroup' }],
509
367
  PrivateDnsEnabled: true,
510
368
  },
511
369
  };
512
370
  }
513
371
 
514
- // Secrets Manager Interface Endpoint (paid, but useful for secrets)
515
372
  vpcResources.FriggSecretsManagerVPCEndpoint = {
516
373
  Type: 'AWS::EC2::VPCEndpoint',
517
374
  Properties: {
518
375
  VpcId: { Ref: 'FriggVPC' },
519
- ServiceName:
520
- 'com.amazonaws.${self:provider.region}.secretsmanager',
376
+ ServiceName: 'com.amazonaws.${self:provider.region}.secretsmanager',
521
377
  VpcEndpointType: 'Interface',
522
- SubnetIds: [
523
- { Ref: 'FriggPrivateSubnet1' },
524
- { Ref: 'FriggPrivateSubnet2' },
525
- ],
378
+ SubnetIds: [{ Ref: 'FriggPrivateSubnet1' }, { Ref: 'FriggPrivateSubnet2' }],
526
379
  SecurityGroupIds: [{ Ref: 'FriggVPCEndpointSecurityGroup' }],
527
380
  PrivateDnsEnabled: true,
528
381
  },
529
382
  };
530
383
 
531
- // Security Group for VPC Endpoints
532
384
  vpcResources.FriggVPCEndpointSecurityGroup = {
533
385
  Type: 'AWS::EC2::SecurityGroup',
534
386
  Properties: {
535
- GroupDescription: 'Security group for Frigg VPC Endpoints',
387
+ GroupDescription: 'Security group for Frigg VPC Endpoints - allows HTTPS from Lambda functions',
536
388
  VpcId: { Ref: 'FriggVPC' },
537
389
  SecurityGroupIngress: [
538
390
  {
539
391
  IpProtocol: 'tcp',
540
392
  FromPort: 443,
541
393
  ToPort: 443,
542
- SourceSecurityGroupId: {
543
- Ref: 'FriggLambdaSecurityGroup',
544
- },
545
- Description: 'HTTPS from Lambda',
394
+ SourceSecurityGroupId: { Ref: 'FriggLambdaSecurityGroup' },
395
+ Description: 'HTTPS from Lambda security group',
546
396
  },
547
- ],
548
- Tags: [
549
397
  {
550
- Key: 'Name',
551
- Value: '${self:service}-${self:provider.stage}-vpc-endpoint-sg',
398
+ IpProtocol: 'tcp',
399
+ FromPort: 443,
400
+ ToPort: 443,
401
+ CidrIp: AppDefinition.vpc.cidrBlock || '10.0.0.0/16',
402
+ Description: 'HTTPS from VPC CIDR (fallback)',
552
403
  },
553
404
  ],
405
+ Tags: [
406
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-vpc-endpoint-sg' },
407
+ { Key: 'ManagedBy', Value: 'Frigg' },
408
+ { Key: 'Service', Value: '${self:service}' },
409
+ { Key: 'Stage', Value: '${self:provider.stage}' },
410
+ { Key: 'Type', Value: 'VPCEndpoint' },
411
+ { Key: 'Purpose', Value: 'Allow Lambda functions to access VPC endpoints' },
412
+ ],
554
413
  },
555
414
  };
556
415
  }
@@ -558,149 +417,105 @@ const createVPCInfrastructure = (AppDefinition) => {
558
417
  return vpcResources;
559
418
  };
560
419
 
561
- /**
562
- * Compose a complete serverless framework configuration from app definition
563
- * @param {Object} AppDefinition - Application definition object
564
- * @param {string} [AppDefinition.name] - Application name
565
- * @param {string} [AppDefinition.provider='aws'] - Cloud provider
566
- * @param {Array} AppDefinition.integrations - Array of integration definitions
567
- * @param {Object} [AppDefinition.vpc] - VPC configuration
568
- * @param {Object} [AppDefinition.encryption] - KMS encryption configuration
569
- * @param {Object} [AppDefinition.ssm] - SSM parameter store configuration
570
- * @param {Object} [AppDefinition.websockets] - WebSocket configuration
571
- * @param {boolean} [AppDefinition.websockets.enable=false] - Enable WebSocket support for live update streaming
572
- * @returns {Object} Complete serverless framework configuration
573
- */
574
- const composeServerlessDefinition = async (AppDefinition) => {
575
- // Store discovered resources
576
- let discoveredResources = {};
420
+ const gatherDiscoveredResources = async (AppDefinition) => {
421
+ if (!shouldRunDiscovery(AppDefinition)) {
422
+ return {};
423
+ }
577
424
 
578
- // Run AWS discovery if needed
579
- if (shouldRunDiscovery(AppDefinition)) {
580
- console.log(
581
- '🔍 Running AWS resource discovery for serverless template...'
582
- );
583
- try {
584
- const region = process.env.AWS_REGION || 'us-east-1';
585
- const discovery = new AWSDiscovery(region);
586
-
587
- const config = {
588
- vpc: AppDefinition.vpc || {},
589
- encryption: AppDefinition.encryption || {},
590
- ssm: AppDefinition.ssm || {},
591
- };
425
+ console.log('🔍 Running AWS resource discovery for serverless template...');
426
+ try {
427
+ const region = process.env.AWS_REGION || 'us-east-1';
428
+ const discovery = new AWSDiscovery(region);
429
+ const config = {
430
+ vpc: AppDefinition.vpc || {},
431
+ encryption: AppDefinition.encryption || {},
432
+ ssm: AppDefinition.ssm || {},
433
+ };
592
434
 
593
- discoveredResources = await discovery.discoverResources(config);
435
+ const discoveredResources = await discovery.discoverResources(config);
594
436
 
595
- console.log('✅ AWS discovery completed successfully!');
596
- if (discoveredResources.defaultVpcId) {
597
- console.log(` VPC: ${discoveredResources.defaultVpcId}`);
598
- }
599
- if (
600
- discoveredResources.privateSubnetId1 &&
601
- discoveredResources.privateSubnetId2
602
- ) {
603
- console.log(
604
- ` Subnets: ${discoveredResources.privateSubnetId1}, ${discoveredResources.privateSubnetId2}`
605
- );
606
- }
607
- if (discoveredResources.defaultSecurityGroupId) {
608
- console.log(
609
- ` Security Group: ${discoveredResources.defaultSecurityGroupId}`
610
- );
611
- }
612
- if (discoveredResources.defaultKmsKeyId) {
613
- console.log(
614
- ` KMS Key: ${discoveredResources.defaultKmsKeyId}`
615
- );
616
- }
617
- } catch (error) {
618
- console.error('❌ AWS discovery failed:', error.message);
619
- throw new Error(`AWS discovery failed: ${error.message}`);
437
+ console.log('✅ AWS discovery completed successfully!');
438
+ if (discoveredResources.defaultVpcId) {
439
+ console.log(` VPC: ${discoveredResources.defaultVpcId}`);
440
+ }
441
+ if (discoveredResources.privateSubnetId1 && discoveredResources.privateSubnetId2) {
442
+ console.log(
443
+ ` Subnets: ${discoveredResources.privateSubnetId1}, ${discoveredResources.privateSubnetId2}`
444
+ );
620
445
  }
446
+ if (discoveredResources.defaultSecurityGroupId) {
447
+ console.log(` Security Group: ${discoveredResources.defaultSecurityGroupId}`);
448
+ }
449
+ if (discoveredResources.defaultKmsKeyId) {
450
+ console.log(` KMS Key: ${discoveredResources.defaultKmsKeyId}`);
451
+ }
452
+
453
+ return discoveredResources;
454
+ } catch (error) {
455
+ console.error('❌ AWS discovery failed:', error.message);
456
+ throw new Error(`AWS discovery failed: ${error.message}`);
621
457
  }
458
+ };
622
459
 
623
- // Get environment variables from appDefinition
624
- const appEnvironmentVars = getAppEnvironmentVars(AppDefinition);
460
+ const buildEnvironment = (appEnvironmentVars, discoveredResources) => {
461
+ const environment = {
462
+ STAGE: '${opt:stage, "dev"}',
463
+ AWS_NODEJS_CONNECTION_REUSE_ENABLED: 1,
464
+ ...appEnvironmentVars,
465
+ };
466
+
467
+ const discoveryEnvMapping = {
468
+ defaultVpcId: 'AWS_DISCOVERY_VPC_ID',
469
+ defaultSecurityGroupId: 'AWS_DISCOVERY_SECURITY_GROUP_ID',
470
+ privateSubnetId1: 'AWS_DISCOVERY_SUBNET_ID_1',
471
+ privateSubnetId2: 'AWS_DISCOVERY_SUBNET_ID_2',
472
+ publicSubnetId: 'AWS_DISCOVERY_PUBLIC_SUBNET_ID',
473
+ defaultRouteTableId: 'AWS_DISCOVERY_ROUTE_TABLE_ID',
474
+ defaultKmsKeyId: 'AWS_DISCOVERY_KMS_KEY_ID',
475
+ };
476
+
477
+ for (const [key, envKey] of Object.entries(discoveryEnvMapping)) {
478
+ if (discoveredResources[key]) {
479
+ environment[envKey] = discoveredResources[key];
480
+ }
481
+ }
482
+
483
+ return environment;
484
+ };
485
+
486
+ const createBaseDefinition = (AppDefinition, appEnvironmentVars, discoveredResources) => {
487
+ const region = process.env.AWS_REGION || 'us-east-1';
625
488
 
626
- const definition = {
489
+ return {
627
490
  frameworkVersion: '>=3.17.0',
628
491
  service: AppDefinition.name || 'create-frigg-app',
629
492
  package: {
630
493
  individually: true,
631
- exclude: [
632
- '!**/node_modules/aws-sdk/**',
633
- '!**/node_modules/@aws-sdk/**',
634
- '!package.json',
635
- ],
494
+ exclude: ['!**/node_modules/aws-sdk/**', '!**/node_modules/@aws-sdk/**', '!package.json'],
636
495
  },
637
496
  useDotenv: true,
638
497
  provider: {
639
498
  name: AppDefinition.provider || 'aws',
640
499
  runtime: 'nodejs20.x',
641
500
  timeout: 30,
642
- region: process.env.AWS_REGION || 'us-east-1',
501
+ region,
643
502
  stage: '${opt:stage}',
644
- environment: {
645
- STAGE: '${opt:stage, "dev"}',
646
- AWS_NODEJS_CONNECTION_REUSE_ENABLED: 1,
647
- // Add environment variables from appDefinition
648
- ...appEnvironmentVars,
649
- // Add discovered resources to environment if available
650
- ...(discoveredResources.defaultVpcId && {
651
- AWS_DISCOVERY_VPC_ID: discoveredResources.defaultVpcId,
652
- }),
653
- ...(discoveredResources.defaultSecurityGroupId && {
654
- AWS_DISCOVERY_SECURITY_GROUP_ID:
655
- discoveredResources.defaultSecurityGroupId,
656
- }),
657
- ...(discoveredResources.privateSubnetId1 && {
658
- AWS_DISCOVERY_SUBNET_ID_1:
659
- discoveredResources.privateSubnetId1,
660
- }),
661
- ...(discoveredResources.privateSubnetId2 && {
662
- AWS_DISCOVERY_SUBNET_ID_2:
663
- discoveredResources.privateSubnetId2,
664
- }),
665
- ...(discoveredResources.publicSubnetId && {
666
- AWS_DISCOVERY_PUBLIC_SUBNET_ID:
667
- discoveredResources.publicSubnetId,
668
- }),
669
- ...(discoveredResources.defaultRouteTableId && {
670
- AWS_DISCOVERY_ROUTE_TABLE_ID:
671
- discoveredResources.defaultRouteTableId,
672
- }),
673
- ...(discoveredResources.defaultKmsKeyId && {
674
- AWS_DISCOVERY_KMS_KEY_ID:
675
- discoveredResources.defaultKmsKeyId,
676
- }),
677
- },
503
+ environment: buildEnvironment(appEnvironmentVars, discoveredResources),
678
504
  iamRoleStatements: [
679
505
  {
680
506
  Effect: 'Allow',
681
507
  Action: ['sns:Publish'],
682
- Resource: {
683
- Ref: 'InternalErrorBridgeTopic',
684
- },
508
+ Resource: { Ref: 'InternalErrorBridgeTopic' },
685
509
  },
686
510
  {
687
511
  Effect: 'Allow',
688
- Action: [
689
- 'sqs:SendMessage',
690
- 'sqs:SendMessageBatch',
691
- 'sqs:GetQueueUrl',
692
- 'sqs:GetQueueAttributes',
693
- ],
512
+ Action: ['sqs:SendMessage', 'sqs:SendMessageBatch', 'sqs:GetQueueUrl', 'sqs:GetQueueAttributes'],
694
513
  Resource: [
695
- {
696
- 'Fn::GetAtt': ['InternalErrorQueue', 'Arn'],
697
- },
514
+ { 'Fn::GetAtt': ['InternalErrorQueue', 'Arn'] },
698
515
  {
699
516
  'Fn::Join': [
700
517
  ':',
701
- [
702
- 'arn:aws:sqs:${self:provider.region}:*:${self:service}--${self:provider.stage}-*Queue',
703
- ],
518
+ ['arn:aws:sqs:${self:provider.region}:*:${self:service}--${self:provider.stage}-*Queue'],
704
519
  ],
705
520
  },
706
521
  ],
@@ -735,7 +550,7 @@ const composeServerlessDefinition = async (AppDefinition) => {
735
550
  autoCreate: false,
736
551
  apiVersion: '2012-11-05',
737
552
  endpoint: 'http://localhost:4566',
738
- region: process.env.AWS_REGION || 'us-east-1',
553
+ region,
739
554
  accessKeyId: 'root',
740
555
  secretAccessKey: 'root',
741
556
  skipCacheInvalidation: false,
@@ -746,57 +561,22 @@ const composeServerlessDefinition = async (AppDefinition) => {
746
561
  },
747
562
  functions: {
748
563
  auth: {
749
- handler:
750
- 'node_modules/@friggframework/core/handlers/routers/auth.handler',
564
+ handler: 'node_modules/@friggframework/core/handlers/routers/auth.handler',
751
565
  events: [
752
- {
753
- httpApi: {
754
- path: '/api/integrations',
755
- method: 'ANY',
756
- },
757
- },
758
- {
759
- httpApi: {
760
- path: '/api/integrations/{proxy+}',
761
- method: 'ANY',
762
- },
763
- },
764
- {
765
- httpApi: {
766
- path: '/api/authorize',
767
- method: 'ANY',
768
- },
769
- },
566
+ { httpApi: { path: '/api/integrations', method: 'ANY' } },
567
+ { httpApi: { path: '/api/integrations/{proxy+}', method: 'ANY' } },
568
+ { httpApi: { path: '/api/authorize', method: 'ANY' } },
770
569
  ],
771
570
  },
772
571
  user: {
773
- handler:
774
- 'node_modules/@friggframework/core/handlers/routers/user.handler',
775
- events: [
776
- {
777
- httpApi: {
778
- path: '/user/{proxy+}',
779
- method: 'ANY',
780
- },
781
- },
782
- ],
572
+ handler: 'node_modules/@friggframework/core/handlers/routers/user.handler',
573
+ events: [{ httpApi: { path: '/user/{proxy+}', method: 'ANY' } }],
783
574
  },
784
575
  health: {
785
- handler:
786
- 'node_modules/@friggframework/core/handlers/routers/health.handler',
576
+ handler: 'node_modules/@friggframework/core/handlers/routers/health.handler',
787
577
  events: [
788
- {
789
- httpApi: {
790
- path: '/health',
791
- method: 'GET',
792
- },
793
- },
794
- {
795
- httpApi: {
796
- path: '/health/{proxy+}',
797
- method: 'GET',
798
- },
799
- },
578
+ { httpApi: { path: '/health', method: 'GET' } },
579
+ { httpApi: { path: '/health/{proxy+}', method: 'GET' } },
800
580
  ],
801
581
  },
802
582
  },
@@ -805,8 +585,7 @@ const composeServerlessDefinition = async (AppDefinition) => {
805
585
  InternalErrorQueue: {
806
586
  Type: 'AWS::SQS::Queue',
807
587
  Properties: {
808
- QueueName:
809
- '${self:service}-internal-error-queue-${self:provider.stage}',
588
+ QueueName: '${self:service}-internal-error-queue-${self:provider.stage}',
810
589
  MessageRetentionPeriod: 300,
811
590
  },
812
591
  },
@@ -816,9 +595,7 @@ const composeServerlessDefinition = async (AppDefinition) => {
816
595
  Subscription: [
817
596
  {
818
597
  Protocol: 'sqs',
819
- Endpoint: {
820
- 'Fn::GetAtt': ['InternalErrorQueue', 'Arn'],
821
- },
598
+ Endpoint: { 'Fn::GetAtt': ['InternalErrorQueue', 'Arn'] },
822
599
  },
823
600
  ],
824
601
  },
@@ -833,25 +610,11 @@ const composeServerlessDefinition = async (AppDefinition) => {
833
610
  {
834
611
  Sid: 'Allow Dead Letter SNS to publish to SQS',
835
612
  Effect: 'Allow',
836
- Principal: {
837
- Service: 'sns.amazonaws.com',
838
- },
839
- Resource: {
840
- 'Fn::GetAtt': [
841
- 'InternalErrorQueue',
842
- 'Arn',
843
- ],
844
- },
845
- Action: [
846
- 'SQS:SendMessage',
847
- 'SQS:SendMessageBatch',
848
- ],
613
+ Principal: { Service: 'sns.amazonaws.com' },
614
+ Resource: { 'Fn::GetAtt': ['InternalErrorQueue', 'Arn'] },
615
+ Action: ['SQS:SendMessage', 'SQS:SendMessageBatch'],
849
616
  Condition: {
850
- ArnEquals: {
851
- 'aws:SourceArn': {
852
- Ref: 'InternalErrorBridgeTopic',
853
- },
854
- },
617
+ ArnEquals: { 'aws:SourceArn': { Ref: 'InternalErrorBridgeTopic' } },
855
618
  },
856
619
  },
857
620
  ],
@@ -871,469 +634,989 @@ const composeServerlessDefinition = async (AppDefinition) => {
871
634
  Period: 60,
872
635
  AlarmActions: [{ Ref: 'InternalErrorBridgeTopic' }],
873
636
  Dimensions: [
874
- {
875
- Name: 'ApiId',
876
- Value: { Ref: 'HttpApi' },
877
- },
878
- {
879
- Name: 'Stage',
880
- Value: '${self:provider.stage}',
881
- },
637
+ { Name: 'ApiId', Value: { Ref: 'HttpApi' } },
638
+ { Name: 'Stage', Value: '${self:provider.stage}' },
882
639
  ],
883
640
  },
884
641
  },
885
642
  },
886
643
  },
887
644
  };
645
+ };
888
646
 
889
- // KMS Configuration based on App Definition
890
- if (AppDefinition.encryption?.fieldLevelEncryptionMethod === 'kms') {
891
- // Check if a KMS key was discovered
892
- if (discoveredResources.defaultKmsKeyId) {
893
- // Use the existing discovered KMS key
894
- console.log(
895
- `Using existing KMS key: ${discoveredResources.defaultKmsKeyId}`
896
- );
647
+ const applyKmsConfiguration = (definition, AppDefinition, discoveredResources) => {
648
+ if (AppDefinition.encryption?.fieldLevelEncryptionMethod !== 'kms') {
649
+ return;
650
+ }
897
651
 
898
- definition.provider.iamRoleStatements.push({
899
- Effect: 'Allow',
900
- Action: ['kms:GenerateDataKey', 'kms:Decrypt'],
901
- Resource: [discoveredResources.defaultKmsKeyId],
902
- });
652
+ if (discoveredResources.defaultKmsKeyId) {
653
+ console.log(`Using existing KMS key: ${discoveredResources.defaultKmsKeyId}`);
654
+ definition.resources.Resources.FriggKMSKeyAlias = {
655
+ Type: 'AWS::KMS::Alias',
656
+ DeletionPolicy: 'Retain',
657
+ Properties: {
658
+ AliasName: 'alias/${self:service}-${self:provider.stage}-frigg-kms',
659
+ TargetKeyId: discoveredResources.defaultKmsKeyId,
660
+ },
661
+ };
903
662
 
904
- // KMS_KEY_ARN will be set later from custom.kmsGrants for consistency
905
- } else {
906
- // No existing key found - check if we should create one or error
907
- if (AppDefinition.encryption?.createResourceIfNoneFound === true) {
908
- // Create a new KMS key
909
- console.log('No existing KMS key found, creating a new one...');
663
+ definition.provider.iamRoleStatements.push({
664
+ Effect: 'Allow',
665
+ Action: ['kms:GenerateDataKey', 'kms:Decrypt'],
666
+ Resource: [discoveredResources.defaultKmsKeyId],
667
+ });
668
+ } else {
669
+ if (AppDefinition.encryption?.createResourceIfNoneFound !== true) {
670
+ throw new Error(
671
+ 'KMS field-level encryption is enabled but no KMS key was found. ' +
672
+ 'Either provide an existing KMS key or set encryption.createResourceIfNoneFound to true to create a new key.'
673
+ );
674
+ }
910
675
 
911
- definition.resources.Resources.FriggKMSKey = {
912
- Type: 'AWS::KMS::Key',
913
- Properties: {
914
- EnableKeyRotation: true,
915
- Description: 'Frigg KMS key for field-level encryption',
916
- KeyPolicy: {
917
- Version: '2012-10-17',
918
- Statement: [
919
- {
920
- Sid: 'AllowRootAccountAdmin',
921
- Effect: 'Allow',
922
- Principal: {
923
- AWS: {
924
- 'Fn::Sub':
925
- 'arn:aws:iam::${AWS::AccountId}:root',
926
- },
927
- },
928
- Action: 'kms:*',
929
- Resource: '*',
930
- },
931
- {
932
- Sid: 'AllowLambdaService',
933
- Effect: 'Allow',
934
- Principal: {
935
- Service: 'lambda.amazonaws.com',
936
- },
937
- Action: [
938
- 'kms:GenerateDataKey',
939
- 'kms:Decrypt',
940
- 'kms:DescribeKey',
941
- ],
942
- Resource: '*',
943
- Condition: {
944
- StringEquals: {
945
- 'kms:ViaService': `lambda.${
946
- process.env.AWS_REGION ||
947
- 'us-east-1'
948
- }.amazonaws.com`,
949
- },
950
- },
951
- },
952
- ],
953
- },
954
- Tags: [
955
- {
956
- Key: 'Name',
957
- Value: '${self:service}-${self:provider.stage}-frigg-kms-key',
676
+ console.log('No existing KMS key found, creating a new one...');
677
+ definition.resources.Resources.FriggKMSKey = {
678
+ Type: 'AWS::KMS::Key',
679
+ DeletionPolicy: 'Retain',
680
+ UpdateReplacePolicy: 'Retain',
681
+ Properties: {
682
+ EnableKeyRotation: true,
683
+ Description: 'Frigg KMS key for field-level encryption',
684
+ KeyPolicy: {
685
+ Version: '2012-10-17',
686
+ Statement: [
687
+ {
688
+ Sid: 'AllowRootAccountAdmin',
689
+ Effect: 'Allow',
690
+ Principal: {
691
+ AWS: { 'Fn::Sub': 'arn:aws:iam::${AWS::AccountId}:root' },
958
692
  },
959
- {
960
- Key: 'Purpose',
961
- Value: 'Field-level encryption for Frigg application',
693
+ Action: 'kms:*',
694
+ Resource: '*',
695
+ },
696
+ {
697
+ Sid: 'AllowLambdaService',
698
+ Effect: 'Allow',
699
+ Principal: { Service: 'lambda.amazonaws.com' },
700
+ Action: ['kms:GenerateDataKey', 'kms:Decrypt', 'kms:DescribeKey'],
701
+ Resource: '*',
702
+ Condition: {
703
+ StringEquals: {
704
+ 'kms:ViaService': `lambda.${process.env.AWS_REGION || 'us-east-1'}.amazonaws.com`,
705
+ },
962
706
  },
963
- ],
964
- },
965
- };
966
-
967
- definition.provider.iamRoleStatements.push({
968
- Effect: 'Allow',
969
- Action: ['kms:GenerateDataKey', 'kms:Decrypt'],
970
- Resource: [{ 'Fn::GetAtt': ['FriggKMSKey', 'Arn'] }],
971
- });
707
+ },
708
+ ],
709
+ },
710
+ Tags: [
711
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-frigg-kms-key' },
712
+ { Key: 'ManagedBy', Value: 'Frigg' },
713
+ { Key: 'Purpose', Value: 'Field-level encryption for Frigg application' },
714
+ ],
715
+ },
716
+ };
972
717
 
973
- definition.provider.environment.KMS_KEY_ARN = {
974
- 'Fn::GetAtt': ['FriggKMSKey', 'Arn'],
975
- };
718
+ definition.resources.Resources.FriggKMSKeyAlias = {
719
+ Type: 'AWS::KMS::Alias',
720
+ DeletionPolicy: 'Retain',
721
+ Properties: {
722
+ AliasName: 'alias/${self:service}-${self:provider.stage}-frigg-kms',
723
+ TargetKeyId: { 'Fn::GetAtt': ['FriggKMSKey', 'Arn'] },
724
+ },
725
+ };
976
726
 
977
- // Configure KMS grants to reference the created key
978
- definition.custom.kmsGrants = {
979
- kmsKeyId: { 'Fn::GetAtt': ['FriggKMSKey', 'Arn'] }
980
- };
981
- } else {
982
- // No key found and createIfNoneFound is not enabled - error
983
- throw new Error(
984
- 'KMS field-level encryption is enabled but no KMS key was found. ' +
985
- 'Either provide an existing KMS key or set encryption.createResourceIfNoneFound to true to create a new key.'
986
- );
987
- }
988
- }
727
+ definition.provider.iamRoleStatements.push({
728
+ Effect: 'Allow',
729
+ Action: ['kms:GenerateDataKey', 'kms:Decrypt'],
730
+ Resource: [{ 'Fn::GetAtt': ['FriggKMSKey', 'Arn'] }],
731
+ });
989
732
 
990
- definition.plugins.push('serverless-kms-grants');
733
+ definition.provider.environment.KMS_KEY_ARN = { 'Fn::GetAtt': ['FriggKMSKey', 'Arn'] };
734
+ definition.custom.kmsGrants = {
735
+ kmsKeyId: { 'Fn::GetAtt': ['FriggKMSKey', 'Arn'] },
736
+ };
737
+ }
991
738
 
992
- // Configure KMS grants if not already set (when using existing key)
993
- if (!definition.custom.kmsGrants) {
994
- definition.custom.kmsGrants = {
995
- kmsKeyId:
996
- discoveredResources.defaultKmsKeyId ||
997
- '${env:AWS_DISCOVERY_KMS_KEY_ID}',
998
- };
999
- }
739
+ definition.plugins.push('serverless-kms-grants');
740
+ if (!definition.custom.kmsGrants) {
741
+ definition.custom.kmsGrants = {
742
+ kmsKeyId: discoveredResources.defaultKmsKeyId || '${env:AWS_DISCOVERY_KMS_KEY_ID}',
743
+ };
744
+ }
1000
745
 
1001
- // Always set KMS_KEY_ARN from custom.kmsGrants for consistency
1002
- // This translates AWS_DISCOVERY_KMS_KEY_ID to the runtime variable KMS_KEY_ARN
1003
- if (!definition.provider.environment.KMS_KEY_ARN) {
1004
- // Use the discovered value directly when available (from in-process discovery)
1005
- // Otherwise fall back to environment variable (from separate discovery process)
1006
- definition.provider.environment.KMS_KEY_ARN =
1007
- discoveredResources.defaultKmsKeyId ||
1008
- '${env:AWS_DISCOVERY_KMS_KEY_ID}';
1009
- }
746
+ if (!definition.provider.environment.KMS_KEY_ARN) {
747
+ definition.provider.environment.KMS_KEY_ARN =
748
+ discoveredResources.defaultKmsKeyId || '${env:AWS_DISCOVERY_KMS_KEY_ID}';
1010
749
  }
750
+ };
1011
751
 
1012
- // VPC Configuration based on App Definition
1013
- if (AppDefinition.vpc?.enable === true) {
1014
- // Add VPC-related IAM permissions
1015
- definition.provider.iamRoleStatements.push({
1016
- Effect: 'Allow',
1017
- Action: [
1018
- 'ec2:CreateNetworkInterface',
1019
- 'ec2:DescribeNetworkInterfaces',
1020
- 'ec2:DeleteNetworkInterface',
1021
- 'ec2:AttachNetworkInterface',
1022
- 'ec2:DetachNetworkInterface',
1023
- ],
1024
- Resource: '*',
1025
- });
752
+ const healVpcConfiguration = (discoveredResources, AppDefinition) => {
753
+ const healingReport = {
754
+ healed: [],
755
+ warnings: [],
756
+ errors: [],
757
+ recommendations: [],
758
+ criticalActions: [],
759
+ };
1026
760
 
1027
- // Default approach: Use AWS Discovery to find existing VPC resources
1028
- if (AppDefinition.vpc.createNew === true) {
1029
- // Option 1: Create new VPC infrastructure (explicit opt-in)
1030
- const vpcConfig = {};
761
+ if (!AppDefinition.vpc?.selfHeal) {
762
+ return healingReport;
763
+ }
1031
764
 
1032
- if (AppDefinition.vpc.securityGroupIds) {
1033
- // User provided custom security groups
1034
- vpcConfig.securityGroupIds = AppDefinition.vpc.securityGroupIds;
1035
- } else {
1036
- // Use auto-created security group
1037
- vpcConfig.securityGroupIds = [
1038
- { Ref: 'FriggLambdaSecurityGroup' },
1039
- ];
1040
- }
765
+ console.log('🔧 Self-healing mode enabled - checking for VPC misconfigurations...');
1041
766
 
1042
- if (AppDefinition.vpc.subnetIds) {
1043
- // User provided custom subnets
1044
- vpcConfig.subnetIds = AppDefinition.vpc.subnetIds;
1045
- } else {
1046
- // Use auto-created private subnets
1047
- vpcConfig.subnetIds = [
1048
- { Ref: 'FriggPrivateSubnet1' },
1049
- { Ref: 'FriggPrivateSubnet2' },
1050
- ];
1051
- }
767
+ if (discoveredResources.natGatewayInPrivateSubnet) {
768
+ healingReport.warnings.push(
769
+ `NAT Gateway ${discoveredResources.natGatewayInPrivateSubnet} is in a private subnet`
770
+ );
771
+ healingReport.recommendations.push(
772
+ 'NAT Gateway should be recreated in a public subnet for proper internet connectivity'
773
+ );
774
+ discoveredResources.needsNewNatGateway = true;
775
+ healingReport.healed.push('Marked NAT Gateway for recreation in public subnet');
776
+ }
1052
777
 
1053
- // Set VPC config for Lambda functions
1054
- definition.provider.vpc = vpcConfig;
778
+ if (discoveredResources.elasticIpAlreadyAssociated) {
779
+ healingReport.warnings.push(
780
+ `Elastic IP ${discoveredResources.existingElasticIp} is already associated`
781
+ );
1055
782
 
1056
- // Add VPC infrastructure resources to CloudFormation
1057
- const vpcResources = createVPCInfrastructure(AppDefinition);
1058
- Object.assign(definition.resources.Resources, vpcResources);
783
+ if (discoveredResources.existingNatGatewayId) {
784
+ healingReport.healed.push('Will reuse existing NAT Gateway instead of creating a new one');
785
+ discoveredResources.reuseExistingNatGateway = true;
1059
786
  } else {
1060
- // Option 2: Use AWS Discovery (default behavior)
1061
- // VPC configuration using discovered or explicitly provided resources
1062
- const vpcConfig = {
1063
- securityGroupIds:
1064
- AppDefinition.vpc.securityGroupIds ||
1065
- (discoveredResources.defaultSecurityGroupId
1066
- ? [discoveredResources.defaultSecurityGroupId]
1067
- : []),
1068
- subnetIds:
1069
- AppDefinition.vpc.subnetIds ||
1070
- (discoveredResources.privateSubnetId1 &&
1071
- discoveredResources.privateSubnetId2
1072
- ? [
1073
- discoveredResources.privateSubnetId1,
1074
- discoveredResources.privateSubnetId2,
1075
- ]
1076
- : []),
1077
- };
1078
-
1079
- // Set VPC config for Lambda functions only if we have valid subnet IDs
1080
- if (
1081
- vpcConfig.subnetIds.length >= 2 &&
1082
- vpcConfig.securityGroupIds.length > 0
1083
- ) {
1084
- definition.provider.vpc = vpcConfig;
1085
-
1086
- // Check if we have an existing NAT Gateway to use
1087
- if (!discoveredResources.existingNatGatewayId) {
1088
- // No existing NAT Gateway, create new resources
1089
-
1090
- // Only create EIP if we don't have an existing one available
1091
- if (!discoveredResources.existingElasticIpAllocationId) {
1092
- definition.resources.Resources.FriggNATGatewayEIP = {
1093
- Type: 'AWS::EC2::EIP',
1094
- Properties: {
1095
- Domain: 'vpc',
1096
- Tags: [
1097
- {
1098
- Key: 'Name',
1099
- Value: '${self:service}-${self:provider.stage}-nat-eip',
1100
- },
1101
- ],
1102
- },
1103
- };
1104
- }
787
+ healingReport.healed.push('Will allocate a new Elastic IP for NAT Gateway');
788
+ discoveredResources.allocateNewElasticIp = true;
789
+ }
790
+ }
1105
791
 
1106
- definition.resources.Resources.FriggNATGateway = {
1107
- Type: 'AWS::EC2::NatGateway',
792
+ if (
793
+ discoveredResources.privateSubnetsWithWrongRoutes &&
794
+ discoveredResources.privateSubnetsWithWrongRoutes.length > 0
795
+ ) {
796
+ healingReport.warnings.push(
797
+ `Found ${discoveredResources.privateSubnetsWithWrongRoutes.length} subnets that are PUBLIC but will be used for Lambda`
798
+ );
799
+ healingReport.healed.push(
800
+ 'Route tables will be corrected during deployment - converting public subnets to private'
801
+ );
802
+ healingReport.criticalActions.push(
803
+ 'SUBNET ISOLATION: Will create separate route tables to ensure Lambda subnets are private'
804
+ );
805
+ }
806
+
807
+ if (discoveredResources.subnetConversionRequired) {
808
+ healingReport.warnings.push(
809
+ 'Subnet configuration mismatch detected - Lambda functions require private subnets'
810
+ );
811
+ healingReport.healed.push('Will create proper route table configuration for subnet isolation');
812
+ }
813
+
814
+ if (discoveredResources.orphanedElasticIps?.length > 0) {
815
+ healingReport.warnings.push(
816
+ `Found ${discoveredResources.orphanedElasticIps.length} orphaned Elastic IPs`
817
+ );
818
+ healingReport.recommendations.push('Consider releasing orphaned Elastic IPs to avoid charges');
819
+ }
820
+
821
+ if (healingReport.criticalActions.length > 0) {
822
+ console.log('🚨 CRITICAL ACTIONS:');
823
+ healingReport.criticalActions.forEach((action) => console.log(` - ${action}`));
824
+ }
825
+
826
+ if (healingReport.healed.length > 0) {
827
+ console.log('✅ Self-healing actions:');
828
+ healingReport.healed.forEach((action) => console.log(` - ${action}`));
829
+ }
830
+
831
+ if (healingReport.warnings.length > 0) {
832
+ console.log('⚠️ Issues detected:');
833
+ healingReport.warnings.forEach((warning) => console.log(` - ${warning}`));
834
+ }
835
+
836
+ if (healingReport.recommendations.length > 0) {
837
+ console.log('💡 Recommendations:');
838
+ healingReport.recommendations.forEach((rec) => console.log(` - ${rec}`));
839
+ }
840
+
841
+ return healingReport;
842
+ };
843
+
844
+ const configureVpc = (definition, AppDefinition, discoveredResources) => {
845
+ if (AppDefinition.vpc?.enable !== true) {
846
+ return;
847
+ }
848
+
849
+ definition.provider.iamRoleStatements.push({
850
+ Effect: 'Allow',
851
+ Action: [
852
+ 'ec2:CreateNetworkInterface',
853
+ 'ec2:DescribeNetworkInterfaces',
854
+ 'ec2:DeleteNetworkInterface',
855
+ 'ec2:AttachNetworkInterface',
856
+ 'ec2:DetachNetworkInterface',
857
+ ],
858
+ Resource: '*',
859
+ });
860
+
861
+ if (Object.keys(discoveredResources).length > 0) {
862
+ const healingReport = healVpcConfiguration(discoveredResources, AppDefinition);
863
+ if (healingReport.errors.length > 0 && !AppDefinition.vpc?.selfHeal) {
864
+ throw new Error(`VPC configuration errors detected: ${healingReport.errors.join(', ')}`);
865
+ }
866
+ }
867
+
868
+ const vpcManagement = AppDefinition.vpc.management || 'discover';
869
+ let vpcId = null;
870
+ const vpcConfig = {
871
+ securityGroupIds: [],
872
+ subnetIds: [],
873
+ };
874
+
875
+ console.log(`VPC Management Mode: ${vpcManagement}`);
876
+
877
+ if (vpcManagement === 'create-new') {
878
+ const vpcResources = createVPCInfrastructure(AppDefinition);
879
+ Object.assign(definition.resources.Resources, vpcResources);
880
+ vpcId = { Ref: 'FriggVPC' };
881
+ vpcConfig.securityGroupIds = AppDefinition.vpc.securityGroupIds || [{ Ref: 'FriggLambdaSecurityGroup' }];
882
+ } else if (vpcManagement === 'use-existing') {
883
+ if (!AppDefinition.vpc.vpcId) {
884
+ throw new Error('VPC management is set to "use-existing" but no vpcId was provided');
885
+ }
886
+ vpcId = AppDefinition.vpc.vpcId;
887
+ vpcConfig.securityGroupIds =
888
+ AppDefinition.vpc.securityGroupIds ||
889
+ (discoveredResources.defaultSecurityGroupId ? [discoveredResources.defaultSecurityGroupId] : []);
890
+ } else {
891
+ if (!discoveredResources.defaultVpcId) {
892
+ throw new Error(
893
+ 'VPC discovery failed: No VPC found. Either set vpc.management to "create-new" or provide vpc.vpcId with "use-existing".'
894
+ );
895
+ }
896
+ vpcId = discoveredResources.defaultVpcId;
897
+ vpcConfig.securityGroupIds =
898
+ AppDefinition.vpc.securityGroupIds ||
899
+ (discoveredResources.defaultSecurityGroupId ? [discoveredResources.defaultSecurityGroupId] : []);
900
+ }
901
+
902
+ const defaultSubnetManagement = vpcManagement === 'create-new' ? 'create' : 'discover';
903
+ let subnetManagement = AppDefinition.vpc.subnets?.management || defaultSubnetManagement;
904
+ console.log(`Subnet Management Mode: ${subnetManagement}`);
905
+
906
+ const effectiveVpcId = vpcId || discoveredResources.defaultVpcId;
907
+ if (!effectiveVpcId) {
908
+ throw new Error('Cannot manage subnets without a VPC ID');
909
+ }
910
+
911
+ if (subnetManagement === 'create') {
912
+ console.log('Creating new subnets...');
913
+ const subnetVpcId = vpcManagement === 'create-new' ? { Ref: 'FriggVPC' } : effectiveVpcId;
914
+ let subnet1Cidr;
915
+ let subnet2Cidr;
916
+ let publicSubnetCidr;
917
+
918
+ if (vpcManagement === 'create-new') {
919
+ const generatedCidrs = { 'Fn::Cidr': ['10.0.0.0/16', 3, 8] };
920
+ subnet1Cidr = { 'Fn::Select': [0, generatedCidrs] };
921
+ subnet2Cidr = { 'Fn::Select': [1, generatedCidrs] };
922
+ publicSubnetCidr = { 'Fn::Select': [2, generatedCidrs] };
923
+ } else {
924
+ subnet1Cidr = '172.31.240.0/24';
925
+ subnet2Cidr = '172.31.241.0/24';
926
+ publicSubnetCidr = '172.31.250.0/24';
927
+ }
928
+
929
+ definition.resources.Resources.FriggPrivateSubnet1 = {
930
+ Type: 'AWS::EC2::Subnet',
931
+ Properties: {
932
+ VpcId: subnetVpcId,
933
+ CidrBlock: subnet1Cidr,
934
+ AvailabilityZone: { 'Fn::Select': [0, { 'Fn::GetAZs': '' }] },
935
+ Tags: [
936
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-private-1' },
937
+ { Key: 'Type', Value: 'Private' },
938
+ { Key: 'ManagedBy', Value: 'Frigg' },
939
+ ],
940
+ },
941
+ };
942
+
943
+ definition.resources.Resources.FriggPrivateSubnet2 = {
944
+ Type: 'AWS::EC2::Subnet',
945
+ Properties: {
946
+ VpcId: subnetVpcId,
947
+ CidrBlock: subnet2Cidr,
948
+ AvailabilityZone: { 'Fn::Select': [1, { 'Fn::GetAZs': '' }] },
949
+ Tags: [
950
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-private-2' },
951
+ { Key: 'Type', Value: 'Private' },
952
+ { Key: 'ManagedBy', Value: 'Frigg' },
953
+ ],
954
+ },
955
+ };
956
+
957
+ definition.resources.Resources.FriggPublicSubnet = {
958
+ Type: 'AWS::EC2::Subnet',
959
+ Properties: {
960
+ VpcId: subnetVpcId,
961
+ CidrBlock: publicSubnetCidr,
962
+ MapPublicIpOnLaunch: true,
963
+ AvailabilityZone: { 'Fn::Select': [0, { 'Fn::GetAZs': '' }] },
964
+ Tags: [
965
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-public' },
966
+ { Key: 'Type', Value: 'Public' },
967
+ { Key: 'ManagedBy', Value: 'Frigg' },
968
+ ],
969
+ },
970
+ };
971
+
972
+ vpcConfig.subnetIds = [{ Ref: 'FriggPrivateSubnet1' }, { Ref: 'FriggPrivateSubnet2' }];
973
+
974
+ if (!AppDefinition.vpc.natGateway || AppDefinition.vpc.natGateway.management === 'discover') {
975
+ if (vpcManagement === 'create-new' || !discoveredResources.internetGatewayId) {
976
+ if (!definition.resources.Resources.FriggInternetGateway) {
977
+ definition.resources.Resources.FriggInternetGateway = {
978
+ Type: 'AWS::EC2::InternetGateway',
1108
979
  Properties: {
1109
- AllocationId:
1110
- discoveredResources.existingElasticIpAllocationId || {
1111
- 'Fn::GetAtt': [
1112
- 'FriggNATGatewayEIP',
1113
- 'AllocationId',
1114
- ],
1115
- },
1116
- SubnetId:
1117
- discoveredResources.publicSubnetId ||
1118
- discoveredResources.privateSubnetId1, // Use first discovered subnet if no public subnet found
1119
980
  Tags: [
1120
- {
1121
- Key: 'Name',
1122
- Value: '${self:service}-${self:provider.stage}-nat-gateway',
1123
- },
981
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-igw' },
982
+ { Key: 'ManagedBy', Value: 'Frigg' },
1124
983
  ],
1125
984
  },
1126
985
  };
1127
- }
1128
986
 
1129
- // Create route table for Lambda subnets to use NAT Gateway
1130
- definition.resources.Resources.FriggLambdaRouteTable = {
1131
- Type: 'AWS::EC2::RouteTable',
1132
- Properties: {
1133
- VpcId: discoveredResources.defaultVpcId || {
1134
- Ref: 'FriggVPC',
987
+ definition.resources.Resources.FriggIGWAttachment = {
988
+ Type: 'AWS::EC2::VPCGatewayAttachment',
989
+ Properties: {
990
+ VpcId: subnetVpcId,
991
+ InternetGatewayId: { Ref: 'FriggInternetGateway' },
1135
992
  },
1136
- Tags: [
1137
- {
1138
- Key: 'Name',
1139
- Value: '${self:service}-${self:provider.stage}-lambda-rt',
1140
- },
1141
- ],
1142
- },
1143
- };
993
+ };
994
+ }
995
+ }
1144
996
 
1145
- definition.resources.Resources.FriggNATRoute = {
1146
- Type: 'AWS::EC2::Route',
1147
- Properties: {
1148
- RouteTableId: { Ref: 'FriggLambdaRouteTable' },
1149
- DestinationCidrBlock: '0.0.0.0/0',
1150
- NatGatewayId:
1151
- discoveredResources.existingNatGatewayId || {
1152
- Ref: 'FriggNATGateway',
1153
- },
1154
- },
1155
- };
997
+ definition.resources.Resources.FriggPublicRouteTable = {
998
+ Type: 'AWS::EC2::RouteTable',
999
+ Properties: {
1000
+ VpcId: subnetVpcId,
1001
+ Tags: [
1002
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-public-rt' },
1003
+ { Key: 'ManagedBy', Value: 'Frigg' },
1004
+ ],
1005
+ },
1006
+ };
1156
1007
 
1157
- // Associate Lambda subnets with NAT Gateway route table
1158
- definition.resources.Resources.FriggSubnet1RouteAssociation = {
1159
- Type: 'AWS::EC2::SubnetRouteTableAssociation',
1160
- Properties: {
1161
- SubnetId: vpcConfig.subnetIds[0],
1162
- RouteTableId: { Ref: 'FriggLambdaRouteTable' },
1163
- },
1164
- };
1008
+ definition.resources.Resources.FriggPublicRoute = {
1009
+ Type: 'AWS::EC2::Route',
1010
+ DependsOn: vpcManagement === 'create-new' ? 'FriggIGWAttachment' : undefined,
1011
+ Properties: {
1012
+ RouteTableId: { Ref: 'FriggPublicRouteTable' },
1013
+ DestinationCidrBlock: '0.0.0.0/0',
1014
+ GatewayId: discoveredResources.internetGatewayId || { Ref: 'FriggInternetGateway' },
1015
+ },
1016
+ };
1165
1017
 
1166
- definition.resources.Resources.FriggSubnet2RouteAssociation = {
1167
- Type: 'AWS::EC2::SubnetRouteTableAssociation',
1168
- Properties: {
1169
- SubnetId: vpcConfig.subnetIds[1],
1170
- RouteTableId: { Ref: 'FriggLambdaRouteTable' },
1171
- },
1172
- };
1018
+ definition.resources.Resources.FriggPublicSubnetRouteTableAssociation = {
1019
+ Type: 'AWS::EC2::SubnetRouteTableAssociation',
1020
+ Properties: {
1021
+ SubnetId: { Ref: 'FriggPublicSubnet' },
1022
+ RouteTableId: { Ref: 'FriggPublicRouteTable' },
1023
+ },
1024
+ };
1025
+
1026
+ definition.resources.Resources.FriggLambdaRouteTable = {
1027
+ Type: 'AWS::EC2::RouteTable',
1028
+ Properties: {
1029
+ VpcId: subnetVpcId,
1030
+ Tags: [
1031
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-lambda-rt' },
1032
+ { Key: 'ManagedBy', Value: 'Frigg' },
1033
+ ],
1034
+ },
1035
+ };
1036
+
1037
+ definition.resources.Resources.FriggPrivateSubnet1RouteTableAssociation = {
1038
+ Type: 'AWS::EC2::SubnetRouteTableAssociation',
1039
+ Properties: {
1040
+ SubnetId: { Ref: 'FriggPrivateSubnet1' },
1041
+ RouteTableId: { Ref: 'FriggLambdaRouteTable' },
1042
+ },
1043
+ };
1044
+
1045
+ definition.resources.Resources.FriggPrivateSubnet2RouteTableAssociation = {
1046
+ Type: 'AWS::EC2::SubnetRouteTableAssociation',
1047
+ Properties: {
1048
+ SubnetId: { Ref: 'FriggPrivateSubnet2' },
1049
+ RouteTableId: { Ref: 'FriggLambdaRouteTable' },
1050
+ },
1051
+ };
1052
+ }
1053
+ } else if (subnetManagement === 'use-existing') {
1054
+ if (!AppDefinition.vpc.subnets?.ids || AppDefinition.vpc.subnets.ids.length < 2) {
1055
+ throw new Error(
1056
+ 'Subnet management is "use-existing" but less than 2 subnet IDs provided. Provide at least 2 subnet IDs in vpc.subnets.ids.'
1057
+ );
1058
+ }
1059
+ vpcConfig.subnetIds = AppDefinition.vpc.subnets.ids;
1060
+ } else {
1061
+ vpcConfig.subnetIds =
1062
+ AppDefinition.vpc.subnets?.ids?.length > 0
1063
+ ? AppDefinition.vpc.subnets.ids
1064
+ : discoveredResources.privateSubnetId1 && discoveredResources.privateSubnetId2
1065
+ ? [discoveredResources.privateSubnetId1, discoveredResources.privateSubnetId2]
1066
+ : [];
1067
+
1068
+ if (vpcConfig.subnetIds.length < 2) {
1069
+ if (AppDefinition.vpc.selfHeal) {
1070
+ console.log('No subnets found but self-heal enabled - creating minimal subnet setup');
1071
+ subnetManagement = 'create';
1072
+ discoveredResources.createSubnets = true;
1073
+ } else {
1074
+ throw new Error(
1075
+ 'No subnets discovered and subnets.management is "discover". Either enable vpc.selfHeal, set subnets.management to "create", or provide subnet IDs.'
1076
+ );
1077
+ }
1078
+ }
1079
+ }
1080
+
1081
+ if (subnetManagement === 'create' && discoveredResources.createSubnets) {
1082
+ definition.resources.Resources.FriggLambdaRouteTable =
1083
+ definition.resources.Resources.FriggLambdaRouteTable || {
1084
+ Type: 'AWS::EC2::RouteTable',
1085
+ Properties: {
1086
+ VpcId: effectiveVpcId,
1087
+ Tags: [
1088
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-lambda-rt' },
1089
+ { Key: 'ManagedBy', Value: 'Frigg' },
1090
+ { Key: 'Environment', Value: '${self:provider.stage}' },
1091
+ { Key: 'Service', Value: '${self:service}' },
1092
+ ],
1093
+ },
1094
+ };
1095
+ }
1096
+
1097
+ if (
1098
+ vpcConfig.subnetIds.length >= 2 &&
1099
+ vpcConfig.securityGroupIds.length > 0
1100
+ ) {
1101
+ definition.provider.vpc = vpcConfig;
1102
+
1103
+ const natGatewayManagement = AppDefinition.vpc.natGateway?.management || 'discover';
1104
+ let needsNewNatGateway =
1105
+ natGatewayManagement === 'createAndManage' ||
1106
+ discoveredResources.needsNewNatGateway === true;
1107
+
1108
+ console.log('needsNewNatGateway', needsNewNatGateway);
1109
+
1110
+ let reuseExistingNatGateway = false;
1111
+ let useExistingEip = false;
1112
+
1113
+ if (needsNewNatGateway) {
1114
+ console.log('Create mode: Creating dedicated EIP, public subnet, and NAT Gateway...');
1115
+
1116
+ if (
1117
+ discoveredResources.existingNatGatewayId &&
1118
+ discoveredResources.existingElasticIpAllocationId
1119
+ ) {
1120
+ console.log('Found existing Frigg-managed NAT Gateway and EIP');
1121
+ if (!discoveredResources.natGatewayInPrivateSubnet) {
1122
+ console.log('✅ Existing NAT Gateway is in PUBLIC subnet, will reuse it');
1123
+ reuseExistingNatGateway = true;
1124
+ } else {
1125
+ console.log('❌ NAT Gateway is in PRIVATE subnet - MUST create new one in PUBLIC subnet');
1126
+ if (AppDefinition.vpc.selfHeal) {
1127
+ console.log('Self-heal enabled: Creating new NAT Gateway in PUBLIC subnet');
1128
+ reuseExistingNatGateway = false;
1129
+ useExistingEip = false;
1130
+ discoveredResources.needsCleanup = true;
1131
+ } else {
1132
+ throw new Error(
1133
+ 'CRITICAL: NAT Gateway is in PRIVATE subnet (will not work!). Enable vpc.selfHeal to auto-fix or set natGateway.management to "createAndManage".'
1134
+ );
1135
+ }
1136
+ }
1137
+ } else if (
1138
+ discoveredResources.existingElasticIpAllocationId &&
1139
+ !discoveredResources.existingNatGatewayId
1140
+ ) {
1141
+ console.log('Found orphaned EIP, will reuse it for new NAT Gateway in PUBLIC subnet');
1142
+ useExistingEip = true;
1143
+ }
1144
+
1145
+ if (reuseExistingNatGateway) {
1146
+ console.log('Reusing existing NAT Gateway - skipping resource creation');
1147
+ } else {
1148
+ if (!useExistingEip) {
1149
+ definition.resources.Resources.FriggNATGatewayEIP = {
1150
+ Type: 'AWS::EC2::EIP',
1151
+ DeletionPolicy: 'Retain',
1152
+ UpdateReplacePolicy: 'Retain',
1153
+ Properties: {
1154
+ Domain: 'vpc',
1155
+ Tags: [
1156
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-nat-eip' },
1157
+ { Key: 'ManagedBy', Value: 'Frigg' },
1158
+ { Key: 'Service', Value: '${self:service}' },
1159
+ { Key: 'Stage', Value: '${self:provider.stage}' },
1160
+ ],
1161
+ },
1162
+ };
1163
+ }
1164
+
1165
+ if (!discoveredResources.publicSubnetId) {
1166
+ if (discoveredResources.internetGatewayId) {
1167
+ console.log('Reusing existing Internet Gateway for NAT Gateway');
1168
+ } else {
1169
+ definition.resources.Resources.FriggInternetGateway =
1170
+ definition.resources.Resources.FriggInternetGateway || {
1171
+ Type: 'AWS::EC2::InternetGateway',
1172
+ Properties: {
1173
+ Tags: [
1174
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-igw' },
1175
+ { Key: 'ManagedBy', Value: 'Frigg' },
1176
+ ],
1177
+ },
1178
+ };
1179
+
1180
+ definition.resources.Resources.FriggIGWAttachment =
1181
+ definition.resources.Resources.FriggIGWAttachment || {
1182
+ Type: 'AWS::EC2::VPCGatewayAttachment',
1183
+ Properties: {
1184
+ VpcId: discoveredResources.defaultVpcId,
1185
+ InternetGatewayId: { Ref: 'FriggInternetGateway' },
1186
+ },
1187
+ };
1188
+ }
1173
1189
 
1174
- // Add VPC endpoints for AWS service optimization (optional but recommended)
1175
- if (AppDefinition.vpc.enableVPCEndpoints !== false) {
1176
- definition.resources.Resources.VPCEndpointS3 = {
1177
- Type: 'AWS::EC2::VPCEndpoint',
1190
+ definition.resources.Resources.FriggPublicSubnet = {
1191
+ Type: 'AWS::EC2::Subnet',
1178
1192
  Properties: {
1179
1193
  VpcId: discoveredResources.defaultVpcId,
1180
- ServiceName:
1181
- 'com.amazonaws.${self:provider.region}.s3',
1182
- VpcEndpointType: 'Gateway',
1183
- RouteTableIds: [{ Ref: 'FriggLambdaRouteTable' }],
1194
+ CidrBlock:
1195
+ AppDefinition.vpc.natGateway?.publicSubnetCidr || '172.31.250.0/24',
1196
+ AvailabilityZone: { 'Fn::Select': [0, { 'Fn::GetAZs': '' }] },
1197
+ MapPublicIpOnLaunch: true,
1198
+ Tags: [
1199
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-public-subnet' },
1200
+ { Key: 'Type', Value: 'Public' },
1201
+ ],
1184
1202
  },
1185
1203
  };
1186
1204
 
1187
- definition.resources.Resources.VPCEndpointDynamoDB = {
1188
- Type: 'AWS::EC2::VPCEndpoint',
1205
+ definition.resources.Resources.FriggPublicRouteTable = {
1206
+ Type: 'AWS::EC2::RouteTable',
1189
1207
  Properties: {
1190
1208
  VpcId: discoveredResources.defaultVpcId,
1191
- ServiceName:
1192
- 'com.amazonaws.${self:provider.region}.dynamodb',
1193
- VpcEndpointType: 'Gateway',
1194
- RouteTableIds: [{ Ref: 'FriggLambdaRouteTable' }],
1209
+ Tags: [
1210
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-public-rt' },
1211
+ ],
1212
+ },
1213
+ };
1214
+
1215
+ definition.resources.Resources.FriggPublicRoute = {
1216
+ Type: 'AWS::EC2::Route',
1217
+ DependsOn: discoveredResources.internetGatewayId ? [] : 'FriggIGWAttachment',
1218
+ Properties: {
1219
+ RouteTableId: { Ref: 'FriggPublicRouteTable' },
1220
+ DestinationCidrBlock: '0.0.0.0/0',
1221
+ GatewayId: discoveredResources.internetGatewayId || { Ref: 'FriggInternetGateway' },
1222
+ },
1223
+ };
1224
+
1225
+ definition.resources.Resources.FriggPublicSubnetRouteTableAssociation = {
1226
+ Type: 'AWS::EC2::SubnetRouteTableAssociation',
1227
+ Properties: {
1228
+ SubnetId: { Ref: 'FriggPublicSubnet' },
1229
+ RouteTableId: { Ref: 'FriggPublicRouteTable' },
1195
1230
  },
1196
1231
  };
1197
1232
  }
1233
+
1234
+ definition.resources.Resources.FriggNATGateway = {
1235
+ Type: 'AWS::EC2::NatGateway',
1236
+ DeletionPolicy: 'Retain',
1237
+ UpdateReplacePolicy: 'Retain',
1238
+ Properties: {
1239
+ AllocationId: useExistingEip
1240
+ ? discoveredResources.existingElasticIpAllocationId
1241
+ : { 'Fn::GetAtt': ['FriggNATGatewayEIP', 'AllocationId'] },
1242
+ SubnetId:
1243
+ discoveredResources.publicSubnetId || { Ref: 'FriggPublicSubnet' },
1244
+ Tags: [
1245
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-nat-gateway' },
1246
+ { Key: 'ManagedBy', Value: 'Frigg' },
1247
+ { Key: 'Service', Value: '${self:service}' },
1248
+ { Key: 'Stage', Value: '${self:provider.stage}' },
1249
+ ],
1250
+ },
1251
+ };
1252
+ }
1253
+ } else if (
1254
+ natGatewayManagement === 'discover' ||
1255
+ natGatewayManagement === 'useExisting'
1256
+ ) {
1257
+ if (natGatewayManagement === 'useExisting' && AppDefinition.vpc.natGateway?.id) {
1258
+ console.log(`Using explicitly provided NAT Gateway: ${AppDefinition.vpc.natGateway.id}`);
1259
+ discoveredResources.existingNatGatewayId = AppDefinition.vpc.natGateway.id;
1260
+ }
1261
+
1262
+ if (discoveredResources.existingNatGatewayId) {
1263
+ console.log(
1264
+ 'discoveredResources.existingNatGatewayId',
1265
+ discoveredResources.existingNatGatewayId
1266
+ );
1267
+
1268
+ if (discoveredResources.natGatewayInPrivateSubnet) {
1269
+ console.log('❌ CRITICAL: NAT Gateway is in PRIVATE subnet - Internet connectivity will NOT work!');
1270
+
1271
+ if (AppDefinition.vpc.selfHeal === true) {
1272
+ console.log('Self-heal enabled: Will create new NAT Gateway in PUBLIC subnet');
1273
+ needsNewNatGateway = true;
1274
+ discoveredResources.existingNatGatewayId = null;
1275
+ if (!discoveredResources.publicSubnetId) {
1276
+ console.log('No public subnet found - will create one for NAT Gateway');
1277
+ discoveredResources.createPublicSubnet = true;
1278
+ }
1279
+ } else {
1280
+ throw new Error(
1281
+ 'CRITICAL: NAT Gateway is in PRIVATE subnet and will NOT provide internet connectivity! Options: 1) Enable vpc.selfHeal to auto-create proper NAT, 2) Set natGateway.management to "createAndManage", or 3) Manually fix the NAT Gateway placement.'
1282
+ );
1283
+ }
1284
+ } else {
1285
+ console.log(`Using discovered NAT Gateway for routing: ${discoveredResources.existingNatGatewayId}`);
1286
+ }
1287
+ } else if (!needsNewNatGateway && AppDefinition.vpc.natGateway?.id) {
1288
+ console.log(`Using explicitly provided NAT Gateway: ${AppDefinition.vpc.natGateway.id}`);
1289
+ discoveredResources.existingNatGatewayId = AppDefinition.vpc.natGateway.id;
1198
1290
  }
1199
1291
  }
1200
1292
 
1201
- // SSM Parameter Store Configuration based on App Definition
1202
- if (AppDefinition.ssm?.enable === true) {
1203
- // Add AWS Parameters and Secrets Lambda Extension layer
1204
- definition.provider.layers = [
1205
- 'arn:aws:lambda:${self:provider.region}:177933569100:layer:AWS-Parameters-and-Secrets-Lambda-Extension:11',
1206
- ];
1207
-
1208
- // Add SSM IAM permissions
1209
- definition.provider.iamRoleStatements.push({
1210
- Effect: 'Allow',
1211
- Action: [
1212
- 'ssm:GetParameter',
1213
- 'ssm:GetParameters',
1214
- 'ssm:GetParametersByPath',
1215
- ],
1216
- Resource: [
1217
- 'arn:aws:ssm:${self:provider.region}:*:parameter/${self:service}/${self:provider.stage}/*',
1218
- ],
1219
- });
1293
+ definition.resources.Resources.FriggLambdaRouteTable =
1294
+ definition.resources.Resources.FriggLambdaRouteTable || {
1295
+ Type: 'AWS::EC2::RouteTable',
1296
+ Properties: {
1297
+ VpcId: discoveredResources.defaultVpcId || vpcId,
1298
+ Tags: [
1299
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-lambda-rt' },
1300
+ { Key: 'ManagedBy', Value: 'Frigg' },
1301
+ { Key: 'Environment', Value: '${self:provider.stage}' },
1302
+ { Key: 'Service', Value: '${self:service}' },
1303
+ ],
1304
+ },
1305
+ };
1220
1306
 
1221
- // Add environment variable for SSM parameter prefix
1222
- definition.provider.environment.SSM_PARAMETER_PREFIX =
1223
- '/${self:service}/${self:provider.stage}';
1307
+ const routeTableId = { Ref: 'FriggLambdaRouteTable' };
1308
+ let natGatewayIdForRoute;
1309
+
1310
+ if (reuseExistingNatGateway) {
1311
+ natGatewayIdForRoute = discoveredResources.existingNatGatewayId;
1312
+ console.log(`Using discovered NAT Gateway for routing: ${natGatewayIdForRoute}`);
1313
+ } else if (needsNewNatGateway && !reuseExistingNatGateway) {
1314
+ natGatewayIdForRoute = { Ref: 'FriggNATGateway' };
1315
+ console.log('Using newly created NAT Gateway for routing');
1316
+ } else if (discoveredResources.existingNatGatewayId) {
1317
+ natGatewayIdForRoute = discoveredResources.existingNatGatewayId;
1318
+ console.log(`Using discovered NAT Gateway for routing: ${natGatewayIdForRoute}`);
1319
+ } else if (AppDefinition.vpc.natGateway?.id) {
1320
+ natGatewayIdForRoute = AppDefinition.vpc.natGateway.id;
1321
+ console.log(`Using explicitly provided NAT Gateway for routing: ${natGatewayIdForRoute}`);
1322
+ } else if (AppDefinition.vpc.selfHeal === true) {
1323
+ natGatewayIdForRoute = null;
1324
+ console.log('No NAT Gateway available - skipping NAT route creation');
1325
+ } else {
1326
+ throw new Error('No existing NAT Gateway found in discovery mode');
1224
1327
  }
1225
1328
 
1226
- // Add integration-specific functions and resources
1227
- if (
1228
- AppDefinition.integrations &&
1229
- Array.isArray(AppDefinition.integrations)
1230
- ) {
1231
- for (const integration of AppDefinition.integrations) {
1232
- if (
1233
- !integration ||
1234
- !integration.Definition ||
1235
- !integration.Definition.name
1236
- ) {
1237
- throw new Error(
1238
- 'Invalid integration: missing Definition or name'
1239
- );
1329
+ if (natGatewayIdForRoute) {
1330
+ console.log(`Configuring NAT route: 0.0.0.0/0 → ${natGatewayIdForRoute}`);
1331
+ definition.resources.Resources.FriggNATRoute = {
1332
+ Type: 'AWS::EC2::Route',
1333
+ DependsOn: 'FriggLambdaRouteTable',
1334
+ Properties: {
1335
+ RouteTableId: routeTableId,
1336
+ DestinationCidrBlock: '0.0.0.0/0',
1337
+ NatGatewayId: natGatewayIdForRoute,
1338
+ },
1339
+ };
1340
+ } else {
1341
+ console.warn('⚠️ No NAT Gateway configured - Lambda functions will not have internet access');
1342
+ }
1343
+
1344
+ if (typeof vpcConfig.subnetIds[0] === 'string') {
1345
+ definition.resources.Resources.FriggSubnet1RouteAssociation = {
1346
+ Type: 'AWS::EC2::SubnetRouteTableAssociation',
1347
+ Properties: {
1348
+ SubnetId: vpcConfig.subnetIds[0],
1349
+ RouteTableId: routeTableId,
1350
+ },
1351
+ DependsOn: 'FriggLambdaRouteTable',
1352
+ };
1353
+ }
1354
+
1355
+ if (typeof vpcConfig.subnetIds[1] === 'string') {
1356
+ definition.resources.Resources.FriggSubnet2RouteAssociation = {
1357
+ Type: 'AWS::EC2::SubnetRouteTableAssociation',
1358
+ Properties: {
1359
+ SubnetId: vpcConfig.subnetIds[1],
1360
+ RouteTableId: routeTableId,
1361
+ },
1362
+ DependsOn: 'FriggLambdaRouteTable',
1363
+ };
1364
+ }
1365
+
1366
+ if (typeof vpcConfig.subnetIds[0] === 'object' && vpcConfig.subnetIds[0].Ref) {
1367
+ definition.resources.Resources.FriggNewSubnet1RouteAssociation = {
1368
+ Type: 'AWS::EC2::SubnetRouteTableAssociation',
1369
+ Properties: {
1370
+ SubnetId: vpcConfig.subnetIds[0],
1371
+ RouteTableId: routeTableId,
1372
+ },
1373
+ DependsOn: ['FriggLambdaRouteTable', vpcConfig.subnetIds[0].Ref],
1374
+ };
1375
+ }
1376
+
1377
+ if (typeof vpcConfig.subnetIds[1] === 'object' && vpcConfig.subnetIds[1].Ref) {
1378
+ definition.resources.Resources.FriggNewSubnet2RouteAssociation = {
1379
+ Type: 'AWS::EC2::SubnetRouteTableAssociation',
1380
+ Properties: {
1381
+ SubnetId: vpcConfig.subnetIds[1],
1382
+ RouteTableId: routeTableId,
1383
+ },
1384
+ DependsOn: ['FriggLambdaRouteTable', vpcConfig.subnetIds[1].Ref],
1385
+ };
1386
+ }
1387
+
1388
+ if (AppDefinition.vpc.enableVPCEndpoints !== false) {
1389
+ definition.resources.Resources.VPCEndpointS3 = {
1390
+ Type: 'AWS::EC2::VPCEndpoint',
1391
+ Properties: {
1392
+ VpcId: discoveredResources.defaultVpcId,
1393
+ ServiceName: 'com.amazonaws.${self:provider.region}.s3',
1394
+ VpcEndpointType: 'Gateway',
1395
+ RouteTableIds: [routeTableId],
1396
+ },
1397
+ };
1398
+
1399
+ definition.resources.Resources.VPCEndpointDynamoDB = {
1400
+ Type: 'AWS::EC2::VPCEndpoint',
1401
+ Properties: {
1402
+ VpcId: discoveredResources.defaultVpcId,
1403
+ ServiceName: 'com.amazonaws.${self:provider.region}.dynamodb',
1404
+ VpcEndpointType: 'Gateway',
1405
+ RouteTableIds: [routeTableId],
1406
+ },
1407
+ };
1408
+ }
1409
+
1410
+ if (AppDefinition.encryption?.fieldLevelEncryptionMethod === 'kms') {
1411
+ if (!discoveredResources.vpcCidr) {
1412
+ console.warn(
1413
+ '⚠️ Warning: VPC CIDR not discovered. VPC endpoint security group may not work correctly.'
1414
+ );
1415
+ }
1416
+
1417
+ if (!definition.resources.Resources.VPCEndpointSecurityGroup) {
1418
+ const vpcEndpointIngressRules = [];
1419
+
1420
+ if (vpcConfig.securityGroupIds && vpcConfig.securityGroupIds.length > 0) {
1421
+ for (const sg of vpcConfig.securityGroupIds) {
1422
+ if (typeof sg === 'string') {
1423
+ vpcEndpointIngressRules.push({
1424
+ IpProtocol: 'tcp',
1425
+ FromPort: 443,
1426
+ ToPort: 443,
1427
+ SourceSecurityGroupId: sg,
1428
+ Description: 'HTTPS from Lambda security group',
1429
+ });
1430
+ } else if (sg.Ref) {
1431
+ vpcEndpointIngressRules.push({
1432
+ IpProtocol: 'tcp',
1433
+ FromPort: 443,
1434
+ ToPort: 443,
1435
+ SourceSecurityGroupId: { Ref: sg.Ref },
1436
+ Description: 'HTTPS from Lambda security group',
1437
+ });
1438
+ }
1439
+ }
1240
1440
  }
1241
- const integrationName = integration.Definition.name;
1242
1441
 
1243
- // Add function for the integration
1244
- definition.functions[integrationName] = {
1245
- handler: `node_modules/@friggframework/core/handlers/routers/integration-defined-routers.handlers.${integrationName}.handler`,
1246
- events: [
1247
- {
1248
- httpApi: {
1249
- path: `/api/${integrationName}-integration/{proxy+}`,
1250
- method: 'ANY',
1251
- },
1252
- },
1253
- ],
1254
- };
1442
+ if (vpcEndpointIngressRules.length === 0) {
1443
+ if (discoveredResources.vpcCidr) {
1444
+ vpcEndpointIngressRules.push({
1445
+ IpProtocol: 'tcp',
1446
+ FromPort: 443,
1447
+ ToPort: 443,
1448
+ CidrIp: discoveredResources.vpcCidr,
1449
+ Description: 'HTTPS from VPC CIDR (fallback)',
1450
+ });
1451
+ } else {
1452
+ console.warn(
1453
+ '⚠️ WARNING: No Lambda security group or VPC CIDR found. Using default private IP ranges.'
1454
+ );
1455
+ vpcEndpointIngressRules.push({
1456
+ IpProtocol: 'tcp',
1457
+ FromPort: 443,
1458
+ ToPort: 443,
1459
+ CidrIp: '172.31.0.0/16',
1460
+ Description: 'HTTPS from default VPC range',
1461
+ });
1462
+ }
1463
+ }
1255
1464
 
1256
- // Add SQS Queue for the integration
1257
- const queueReference = `${
1258
- integrationName.charAt(0).toUpperCase() +
1259
- integrationName.slice(1)
1260
- }Queue`;
1261
- const queueName = `\${self:service}--\${self:provider.stage}-${queueReference}`;
1262
- definition.resources.Resources[queueReference] = {
1263
- Type: 'AWS::SQS::Queue',
1465
+ definition.resources.Resources.VPCEndpointSecurityGroup = {
1466
+ Type: 'AWS::EC2::SecurityGroup',
1264
1467
  Properties: {
1265
- QueueName: `\${self:custom.${queueReference}}`,
1266
- MessageRetentionPeriod: 60,
1267
- VisibilityTimeout: 1800, // 30 minutes
1268
- RedrivePolicy: {
1269
- maxReceiveCount: 1,
1270
- deadLetterTargetArn: {
1271
- 'Fn::GetAtt': ['InternalErrorQueue', 'Arn'],
1272
- },
1273
- },
1468
+ GroupDescription: 'Security group for VPC endpoints - allows HTTPS from Lambda functions',
1469
+ VpcId: discoveredResources.defaultVpcId,
1470
+ SecurityGroupIngress: vpcEndpointIngressRules,
1471
+ Tags: [
1472
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-vpc-endpoints-sg' },
1473
+ { Key: 'ManagedBy', Value: 'Frigg' },
1474
+ { Key: 'Purpose', Value: 'Allow Lambda functions to access VPC endpoints' },
1475
+ ],
1274
1476
  },
1275
1477
  };
1478
+ }
1276
1479
 
1277
- // Add Queue Worker for the integration
1278
- const queueWorkerName = `${integrationName}QueueWorker`;
1279
- definition.functions[queueWorkerName] = {
1280
- handler: `node_modules/@friggframework/core/handlers/workers/integration-defined-workers.handlers.${integrationName}.queueWorker`,
1281
- reservedConcurrency: 5,
1282
- events: [
1283
- {
1284
- sqs: {
1285
- arn: {
1286
- 'Fn::GetAtt': [queueReference, 'Arn'],
1287
- },
1288
- batchSize: 1,
1289
- },
1290
- },
1291
- ],
1292
- timeout: 600,
1293
- };
1480
+ definition.resources.Resources.VPCEndpointKMS = {
1481
+ Type: 'AWS::EC2::VPCEndpoint',
1482
+ Properties: {
1483
+ VpcId: discoveredResources.defaultVpcId,
1484
+ ServiceName: 'com.amazonaws.${self:provider.region}.kms',
1485
+ VpcEndpointType: 'Interface',
1486
+ SubnetIds: vpcConfig.subnetIds,
1487
+ SecurityGroupIds: [{ Ref: 'VPCEndpointSecurityGroup' }],
1488
+ PrivateDnsEnabled: true,
1489
+ },
1490
+ };
1294
1491
 
1295
- // Add Queue URL for the integration to the ENVironment variables
1296
- definition.provider.environment = {
1297
- ...definition.provider.environment,
1298
- [`${integrationName.toUpperCase()}_QUEUE_URL`]: {
1299
- Ref: queueReference,
1492
+ if (AppDefinition.secretsManager?.enable === true) {
1493
+ definition.resources.Resources.VPCEndpointSecretsManager = {
1494
+ Type: 'AWS::EC2::VPCEndpoint',
1495
+ Properties: {
1496
+ VpcId: discoveredResources.defaultVpcId,
1497
+ ServiceName: 'com.amazonaws.${self:provider.region}.secretsmanager',
1498
+ VpcEndpointType: 'Interface',
1499
+ SubnetIds: vpcConfig.subnetIds,
1500
+ SecurityGroupIds: [{ Ref: 'VPCEndpointSecurityGroup' }],
1501
+ PrivateDnsEnabled: true,
1300
1502
  },
1301
1503
  };
1302
-
1303
- definition.custom[queueReference] = queueName;
1304
1504
  }
1305
1505
  }
1306
1506
  }
1507
+ };
1508
+
1509
+ const configureSsm = (definition, AppDefinition) => {
1510
+ if (AppDefinition.ssm?.enable !== true) {
1511
+ return;
1512
+ }
1513
+
1514
+ definition.provider.layers = [
1515
+ 'arn:aws:lambda:${self:provider.region}:177933569100:layer:AWS-Parameters-and-Secrets-Lambda-Extension:11',
1516
+ ];
1517
+
1518
+ definition.provider.iamRoleStatements.push({
1519
+ Effect: 'Allow',
1520
+ Action: ['ssm:GetParameter', 'ssm:GetParameters', 'ssm:GetParametersByPath'],
1521
+ Resource: ['arn:aws:ssm:${self:provider.region}:*:parameter/${self:service}/${self:provider.stage}/*'],
1522
+ });
1523
+
1524
+ definition.provider.environment.SSM_PARAMETER_PREFIX = '/${self:service}/${self:provider.stage}';
1525
+ };
1526
+
1527
+ const attachIntegrations = (definition, AppDefinition) => {
1528
+ if (!Array.isArray(AppDefinition.integrations) || AppDefinition.integrations.length === 0) {
1529
+ return;
1530
+ }
1307
1531
 
1308
- // Discovery has already run successfully at this point if needed
1309
- // The discoveredResources object contains all the necessary AWS resources
1532
+ console.log(`Processing ${AppDefinition.integrations.length} integrations...`);
1310
1533
 
1311
- // Add websocket function if enabled
1312
- if (AppDefinition.websockets?.enable === true) {
1313
- definition.functions.defaultWebsocket = {
1314
- handler:
1315
- 'node_modules/@friggframework/core/handlers/routers/websocket.handler',
1534
+ for (const integration of AppDefinition.integrations) {
1535
+ if (!integration?.Definition?.name) {
1536
+ throw new Error('Invalid integration: missing Definition or name');
1537
+ }
1538
+
1539
+ const integrationName = integration.Definition.name;
1540
+ const queueReference = `${integrationName.charAt(0).toUpperCase() + integrationName.slice(1)}Queue`;
1541
+ const queueName = `\${self:service}--\${self:provider.stage}-${queueReference}`;
1542
+
1543
+ definition.functions[integrationName] = {
1544
+ handler: `node_modules/@friggframework/core/handlers/routers/integration-defined-routers.handlers.${integrationName}.handler`,
1316
1545
  events: [
1317
1546
  {
1318
- websocket: {
1319
- route: '$connect',
1547
+ httpApi: {
1548
+ path: `/api/${integrationName}-integration/{proxy+}`,
1549
+ method: 'ANY',
1320
1550
  },
1321
1551
  },
1322
- {
1323
- websocket: {
1324
- route: '$default',
1325
- },
1552
+ ],
1553
+ };
1554
+
1555
+ definition.resources.Resources[queueReference] = {
1556
+ Type: 'AWS::SQS::Queue',
1557
+ Properties: {
1558
+ QueueName: `\${self:custom.${queueReference}}`,
1559
+ MessageRetentionPeriod: 60,
1560
+ VisibilityTimeout: 1800,
1561
+ RedrivePolicy: {
1562
+ maxReceiveCount: 1,
1563
+ deadLetterTargetArn: { 'Fn::GetAtt': ['InternalErrorQueue', 'Arn'] },
1326
1564
  },
1565
+ },
1566
+ };
1567
+
1568
+ const queueWorkerName = `${integrationName}QueueWorker`;
1569
+ definition.functions[queueWorkerName] = {
1570
+ handler: `node_modules/@friggframework/core/handlers/workers/integration-defined-workers.handlers.${integrationName}.queueWorker`,
1571
+ reservedConcurrency: 5,
1572
+ events: [
1327
1573
  {
1328
- websocket: {
1329
- route: '$disconnect',
1574
+ sqs: {
1575
+ arn: { 'Fn::GetAtt': [queueReference, 'Arn'] },
1576
+ batchSize: 1,
1330
1577
  },
1331
1578
  },
1332
1579
  ],
1580
+ timeout: 600,
1581
+ };
1582
+
1583
+ definition.provider.environment = {
1584
+ ...definition.provider.environment,
1585
+ [`${integrationName.toUpperCase()}_QUEUE_URL`]: { Ref: queueReference },
1333
1586
  };
1587
+
1588
+ definition.custom[queueReference] = queueName;
1589
+ }
1590
+ };
1591
+
1592
+ const configureWebsockets = (definition, AppDefinition) => {
1593
+ if (AppDefinition.websockets?.enable !== true) {
1594
+ return;
1334
1595
  }
1335
1596
 
1336
- // Modify handler paths to point to the correct node_modules location
1597
+ definition.functions.defaultWebsocket = {
1598
+ handler: 'node_modules/@friggframework/core/handlers/routers/websocket.handler',
1599
+ events: [
1600
+ { websocket: { route: '$connect' } },
1601
+ { websocket: { route: '$default' } },
1602
+ { websocket: { route: '$disconnect' } },
1603
+ ],
1604
+ };
1605
+ };
1606
+
1607
+ const composeServerlessDefinition = async (AppDefinition) => {
1608
+ console.log('composeServerlessDefinition', AppDefinition);
1609
+
1610
+ const discoveredResources = await gatherDiscoveredResources(AppDefinition);
1611
+ const appEnvironmentVars = getAppEnvironmentVars(AppDefinition);
1612
+ const definition = createBaseDefinition(AppDefinition, appEnvironmentVars, discoveredResources);
1613
+
1614
+ applyKmsConfiguration(definition, AppDefinition, discoveredResources);
1615
+ configureVpc(definition, AppDefinition, discoveredResources);
1616
+ configureSsm(definition, AppDefinition);
1617
+ attachIntegrations(definition, AppDefinition);
1618
+ configureWebsockets(definition, AppDefinition);
1619
+
1337
1620
  definition.functions = modifyHandlerPaths(definition.functions);
1338
1621
 
1339
1622
  return definition;