@friggframework/devtools 2.0.0-next.4 → 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.
Files changed (197) hide show
  1. package/frigg-cli/.eslintrc.js +141 -0
  2. package/frigg-cli/__tests__/jest.config.js +102 -0
  3. package/frigg-cli/__tests__/unit/commands/build.test.js +483 -0
  4. package/frigg-cli/__tests__/unit/commands/install.test.js +418 -0
  5. package/frigg-cli/__tests__/unit/commands/ui.test.js +592 -0
  6. package/frigg-cli/__tests__/utils/command-tester.js +170 -0
  7. package/frigg-cli/__tests__/utils/mock-factory.js +270 -0
  8. package/frigg-cli/__tests__/utils/test-fixtures.js +463 -0
  9. package/frigg-cli/__tests__/utils/test-setup.js +286 -0
  10. package/frigg-cli/build-command/index.js +54 -0
  11. package/frigg-cli/deploy-command/index.js +175 -0
  12. package/frigg-cli/generate-command/__tests__/generate-command.test.js +312 -0
  13. package/frigg-cli/generate-command/azure-generator.js +43 -0
  14. package/frigg-cli/generate-command/gcp-generator.js +47 -0
  15. package/frigg-cli/generate-command/index.js +332 -0
  16. package/frigg-cli/generate-command/terraform-generator.js +555 -0
  17. package/frigg-cli/generate-iam-command.js +115 -0
  18. package/frigg-cli/index.js +47 -1
  19. package/frigg-cli/index.test.js +1 -4
  20. package/frigg-cli/init-command/backend-first-handler.js +756 -0
  21. package/frigg-cli/init-command/index.js +93 -0
  22. package/frigg-cli/init-command/template-handler.js +143 -0
  23. package/frigg-cli/install-command/index.js +1 -4
  24. package/frigg-cli/package.json +51 -0
  25. package/frigg-cli/start-command/index.js +24 -4
  26. package/frigg-cli/test/init-command.test.js +180 -0
  27. package/frigg-cli/test/npm-registry.test.js +319 -0
  28. package/frigg-cli/ui-command/index.js +154 -0
  29. package/frigg-cli/utils/app-resolver.js +319 -0
  30. package/frigg-cli/utils/backend-path.js +16 -17
  31. package/frigg-cli/utils/npm-registry.js +167 -0
  32. package/frigg-cli/utils/process-manager.js +199 -0
  33. package/frigg-cli/utils/repo-detection.js +405 -0
  34. package/infrastructure/DEPLOYMENT-INSTRUCTIONS.md +268 -0
  35. package/infrastructure/GENERATE-IAM-DOCS.md +278 -0
  36. package/infrastructure/IAM-POLICY-TEMPLATES.md +176 -0
  37. package/infrastructure/README.md +443 -0
  38. package/infrastructure/WEBSOCKET-CONFIGURATION.md +105 -0
  39. package/infrastructure/__tests__/fixtures/mock-aws-resources.js +391 -0
  40. package/infrastructure/__tests__/helpers/test-utils.js +277 -0
  41. package/infrastructure/aws-discovery.js +1176 -0
  42. package/infrastructure/aws-discovery.test.js +1220 -0
  43. package/infrastructure/build-time-discovery.js +206 -0
  44. package/infrastructure/build-time-discovery.test.js +378 -0
  45. package/infrastructure/create-frigg-infrastructure.js +3 -5
  46. package/infrastructure/env-validator.js +77 -0
  47. package/infrastructure/frigg-deployment-iam-stack.yaml +401 -0
  48. package/infrastructure/iam-generator.js +836 -0
  49. package/infrastructure/iam-generator.test.js +172 -0
  50. package/infrastructure/iam-policy-basic.json +218 -0
  51. package/infrastructure/iam-policy-full.json +288 -0
  52. package/infrastructure/integration.test.js +383 -0
  53. package/infrastructure/run-discovery.js +110 -0
  54. package/infrastructure/serverless-template.js +1472 -138
  55. package/infrastructure/serverless-template.test.js +1759 -0
  56. package/management-ui/.eslintrc.js +22 -0
  57. package/management-ui/README.md +203 -0
  58. package/management-ui/components.json +21 -0
  59. package/management-ui/docs/phase2-integration-guide.md +320 -0
  60. package/management-ui/index.html +13 -0
  61. package/management-ui/package-lock.json +16517 -0
  62. package/management-ui/package.json +76 -0
  63. package/management-ui/packages/devtools/frigg-cli/ui-command/index.js +302 -0
  64. package/management-ui/postcss.config.js +6 -0
  65. package/management-ui/server/api/backend.js +256 -0
  66. package/management-ui/server/api/cli.js +315 -0
  67. package/management-ui/server/api/codegen.js +663 -0
  68. package/management-ui/server/api/connections.js +857 -0
  69. package/management-ui/server/api/discovery.js +185 -0
  70. package/management-ui/server/api/environment/index.js +1 -0
  71. package/management-ui/server/api/environment/router.js +378 -0
  72. package/management-ui/server/api/environment.js +328 -0
  73. package/management-ui/server/api/integrations.js +876 -0
  74. package/management-ui/server/api/logs.js +248 -0
  75. package/management-ui/server/api/monitoring.js +282 -0
  76. package/management-ui/server/api/open-ide.js +31 -0
  77. package/management-ui/server/api/project.js +1029 -0
  78. package/management-ui/server/api/users/sessions.js +371 -0
  79. package/management-ui/server/api/users/simulation.js +254 -0
  80. package/management-ui/server/api/users.js +362 -0
  81. package/management-ui/server/api-contract.md +275 -0
  82. package/management-ui/server/index.js +873 -0
  83. package/management-ui/server/middleware/errorHandler.js +93 -0
  84. package/management-ui/server/middleware/security.js +32 -0
  85. package/management-ui/server/processManager.js +296 -0
  86. package/management-ui/server/server.js +346 -0
  87. package/management-ui/server/services/aws-monitor.js +413 -0
  88. package/management-ui/server/services/npm-registry.js +347 -0
  89. package/management-ui/server/services/template-engine.js +538 -0
  90. package/management-ui/server/utils/cliIntegration.js +220 -0
  91. package/management-ui/server/utils/environment/auditLogger.js +471 -0
  92. package/management-ui/server/utils/environment/awsParameterStore.js +264 -0
  93. package/management-ui/server/utils/environment/encryption.js +278 -0
  94. package/management-ui/server/utils/environment/envFileManager.js +286 -0
  95. package/management-ui/server/utils/import-commonjs.js +28 -0
  96. package/management-ui/server/utils/response.js +83 -0
  97. package/management-ui/server/websocket/handler.js +325 -0
  98. package/management-ui/src/App.jsx +109 -0
  99. package/management-ui/src/assets/FriggLogo.svg +1 -0
  100. package/management-ui/src/components/AppRouter.jsx +65 -0
  101. package/management-ui/src/components/Button.jsx +70 -0
  102. package/management-ui/src/components/Card.jsx +97 -0
  103. package/management-ui/src/components/EnvironmentCompare.jsx +400 -0
  104. package/management-ui/src/components/EnvironmentEditor.jsx +372 -0
  105. package/management-ui/src/components/EnvironmentImportExport.jsx +469 -0
  106. package/management-ui/src/components/EnvironmentSchema.jsx +491 -0
  107. package/management-ui/src/components/EnvironmentSecurity.jsx +463 -0
  108. package/management-ui/src/components/ErrorBoundary.jsx +73 -0
  109. package/management-ui/src/components/IntegrationCard.jsx +481 -0
  110. package/management-ui/src/components/IntegrationCardEnhanced.jsx +770 -0
  111. package/management-ui/src/components/IntegrationExplorer.jsx +379 -0
  112. package/management-ui/src/components/IntegrationStatus.jsx +336 -0
  113. package/management-ui/src/components/Layout.jsx +716 -0
  114. package/management-ui/src/components/LoadingSpinner.jsx +113 -0
  115. package/management-ui/src/components/RepositoryPicker.jsx +248 -0
  116. package/management-ui/src/components/SessionMonitor.jsx +350 -0
  117. package/management-ui/src/components/StatusBadge.jsx +208 -0
  118. package/management-ui/src/components/UserContextSwitcher.jsx +212 -0
  119. package/management-ui/src/components/UserSimulation.jsx +327 -0
  120. package/management-ui/src/components/Welcome.jsx +434 -0
  121. package/management-ui/src/components/codegen/APIEndpointGenerator.jsx +637 -0
  122. package/management-ui/src/components/codegen/APIModuleSelector.jsx +227 -0
  123. package/management-ui/src/components/codegen/CodeGenerationWizard.jsx +247 -0
  124. package/management-ui/src/components/codegen/CodePreviewEditor.jsx +316 -0
  125. package/management-ui/src/components/codegen/DynamicModuleForm.jsx +271 -0
  126. package/management-ui/src/components/codegen/FormBuilder.jsx +737 -0
  127. package/management-ui/src/components/codegen/IntegrationGenerator.jsx +855 -0
  128. package/management-ui/src/components/codegen/ProjectScaffoldWizard.jsx +797 -0
  129. package/management-ui/src/components/codegen/SchemaBuilder.jsx +303 -0
  130. package/management-ui/src/components/codegen/TemplateSelector.jsx +586 -0
  131. package/management-ui/src/components/codegen/index.js +10 -0
  132. package/management-ui/src/components/connections/ConnectionConfigForm.jsx +362 -0
  133. package/management-ui/src/components/connections/ConnectionHealthMonitor.jsx +182 -0
  134. package/management-ui/src/components/connections/ConnectionTester.jsx +200 -0
  135. package/management-ui/src/components/connections/EntityRelationshipMapper.jsx +292 -0
  136. package/management-ui/src/components/connections/OAuthFlow.jsx +204 -0
  137. package/management-ui/src/components/connections/index.js +5 -0
  138. package/management-ui/src/components/index.js +21 -0
  139. package/management-ui/src/components/monitoring/APIGatewayMetrics.jsx +222 -0
  140. package/management-ui/src/components/monitoring/LambdaMetrics.jsx +169 -0
  141. package/management-ui/src/components/monitoring/MetricsChart.jsx +197 -0
  142. package/management-ui/src/components/monitoring/MonitoringDashboard.jsx +393 -0
  143. package/management-ui/src/components/monitoring/SQSMetrics.jsx +246 -0
  144. package/management-ui/src/components/monitoring/index.js +6 -0
  145. package/management-ui/src/components/monitoring/monitoring.css +218 -0
  146. package/management-ui/src/components/theme-provider.jsx +52 -0
  147. package/management-ui/src/components/theme-toggle.jsx +39 -0
  148. package/management-ui/src/components/ui/badge.tsx +36 -0
  149. package/management-ui/src/components/ui/button.test.jsx +56 -0
  150. package/management-ui/src/components/ui/button.tsx +57 -0
  151. package/management-ui/src/components/ui/card.tsx +76 -0
  152. package/management-ui/src/components/ui/dropdown-menu.tsx +199 -0
  153. package/management-ui/src/components/ui/select.tsx +157 -0
  154. package/management-ui/src/components/ui/skeleton.jsx +15 -0
  155. package/management-ui/src/hooks/useFrigg.jsx +601 -0
  156. package/management-ui/src/hooks/useSocket.jsx +58 -0
  157. package/management-ui/src/index.css +193 -0
  158. package/management-ui/src/lib/utils.ts +6 -0
  159. package/management-ui/src/main.jsx +10 -0
  160. package/management-ui/src/pages/CodeGeneration.jsx +14 -0
  161. package/management-ui/src/pages/Connections.jsx +252 -0
  162. package/management-ui/src/pages/ConnectionsEnhanced.jsx +633 -0
  163. package/management-ui/src/pages/Dashboard.jsx +311 -0
  164. package/management-ui/src/pages/Environment.jsx +314 -0
  165. package/management-ui/src/pages/IntegrationConfigure.jsx +669 -0
  166. package/management-ui/src/pages/IntegrationDiscovery.jsx +567 -0
  167. package/management-ui/src/pages/IntegrationTest.jsx +742 -0
  168. package/management-ui/src/pages/Integrations.jsx +253 -0
  169. package/management-ui/src/pages/Monitoring.jsx +17 -0
  170. package/management-ui/src/pages/Simulation.jsx +155 -0
  171. package/management-ui/src/pages/Users.jsx +492 -0
  172. package/management-ui/src/services/api.js +41 -0
  173. package/management-ui/src/services/apiModuleService.js +193 -0
  174. package/management-ui/src/services/websocket-handlers.js +120 -0
  175. package/management-ui/src/test/api/project.test.js +273 -0
  176. package/management-ui/src/test/components/Welcome.test.jsx +378 -0
  177. package/management-ui/src/test/mocks/server.js +178 -0
  178. package/management-ui/src/test/setup.js +61 -0
  179. package/management-ui/src/test/utils/test-utils.jsx +134 -0
  180. package/management-ui/src/utils/repository.js +98 -0
  181. package/management-ui/src/utils/repository.test.js +118 -0
  182. package/management-ui/src/workflows/phase2-integration-workflows.js +884 -0
  183. package/management-ui/tailwind.config.js +63 -0
  184. package/management-ui/tsconfig.json +37 -0
  185. package/management-ui/tsconfig.node.json +10 -0
  186. package/management-ui/vite.config.js +26 -0
  187. package/management-ui/vitest.config.js +38 -0
  188. package/package.json +20 -9
  189. package/infrastructure/app-handler-helpers.js +0 -57
  190. package/infrastructure/backend-utils.js +0 -90
  191. package/infrastructure/routers/auth.js +0 -26
  192. package/infrastructure/routers/integration-defined-routers.js +0 -37
  193. package/infrastructure/routers/middleware/loadUser.js +0 -15
  194. package/infrastructure/routers/middleware/requireLoggedInUser.js +0 -12
  195. package/infrastructure/routers/user.js +0 -41
  196. package/infrastructure/routers/websocket.js +0 -55
  197. package/infrastructure/workers/integration-defined-workers.js +0 -24
@@ -1,35 +1,540 @@
1
1
  const path = require('path');
2
2
  const fs = require('fs');
3
+ const { AWSDiscovery } = require('./aws-discovery');
3
4
 
4
- const composeServerlessDefinition = (AppDefinition, IntegrationFactory) => {
5
- const definition = {
5
+ const shouldRunDiscovery = (AppDefinition) =>
6
+ AppDefinition.vpc?.enable === true ||
7
+ AppDefinition.encryption?.fieldLevelEncryptionMethod === 'kms' ||
8
+ AppDefinition.ssm?.enable === true;
9
+
10
+ const getAppEnvironmentVars = (AppDefinition) => {
11
+ const envVars = {};
12
+ const reservedVars = new Set([
13
+ '_HANDLER',
14
+ '_X_AMZN_TRACE_ID',
15
+ 'AWS_DEFAULT_REGION',
16
+ 'AWS_EXECUTION_ENV',
17
+ 'AWS_REGION',
18
+ 'AWS_LAMBDA_FUNCTION_NAME',
19
+ 'AWS_LAMBDA_FUNCTION_MEMORY_SIZE',
20
+ 'AWS_LAMBDA_FUNCTION_VERSION',
21
+ 'AWS_LAMBDA_INITIALIZATION_TYPE',
22
+ 'AWS_LAMBDA_LOG_GROUP_NAME',
23
+ 'AWS_LAMBDA_LOG_STREAM_NAME',
24
+ 'AWS_ACCESS_KEY',
25
+ 'AWS_ACCESS_KEY_ID',
26
+ 'AWS_SECRET_ACCESS_KEY',
27
+ 'AWS_SESSION_TOKEN',
28
+ ]);
29
+
30
+ if (!AppDefinition.environment) {
31
+ return envVars;
32
+ }
33
+
34
+ console.log('📋 Loading environment variables from appDefinition...');
35
+ const envKeys = [];
36
+ const skippedKeys = [];
37
+
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;
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
+ );
55
+ }
56
+
57
+ return envVars;
58
+ };
59
+
60
+ const findNodeModulesPath = () => {
61
+ try {
62
+ let currentDir = process.cwd();
63
+ let nodeModulesPath = null;
64
+
65
+ for (let i = 0; i < 5; i++) {
66
+ const potentialPath = path.join(currentDir, 'node_modules');
67
+ if (fs.existsSync(potentialPath)) {
68
+ nodeModulesPath = potentialPath;
69
+ console.log(`Found node_modules at: ${nodeModulesPath} (method 1)`);
70
+ break;
71
+ }
72
+ const parentDir = path.dirname(currentDir);
73
+ if (parentDir === currentDir) break;
74
+ currentDir = parentDir;
75
+ }
76
+
77
+ if (!nodeModulesPath) {
78
+ try {
79
+ const { execSync } = require('node:child_process');
80
+ const npmRoot = execSync('npm root', { encoding: 'utf8' }).trim();
81
+ if (fs.existsSync(npmRoot)) {
82
+ nodeModulesPath = npmRoot;
83
+ console.log(`Found node_modules at: ${nodeModulesPath} (method 2)`);
84
+ }
85
+ } catch (npmError) {
86
+ console.error('Error executing npm root:', npmError);
87
+ }
88
+ }
89
+
90
+ if (!nodeModulesPath) {
91
+ currentDir = process.cwd();
92
+ for (let i = 0; i < 5; i++) {
93
+ const packageJsonPath = path.join(currentDir, 'package.json');
94
+ if (fs.existsSync(packageJsonPath)) {
95
+ const potentialNodeModules = path.join(currentDir, 'node_modules');
96
+ if (fs.existsSync(potentialNodeModules)) {
97
+ nodeModulesPath = potentialNodeModules;
98
+ console.log(`Found node_modules at: ${nodeModulesPath} (method 3)`);
99
+ break;
100
+ }
101
+ }
102
+ const parentDir = path.dirname(currentDir);
103
+ if (parentDir === currentDir) break;
104
+ currentDir = parentDir;
105
+ }
106
+ }
107
+
108
+ if (nodeModulesPath) {
109
+ return nodeModulesPath;
110
+ }
111
+
112
+ console.warn('Could not find node_modules path, falling back to default');
113
+ return path.resolve(process.cwd(), '../node_modules');
114
+ } catch (error) {
115
+ console.error('Error finding node_modules path:', error);
116
+ return path.resolve(process.cwd(), '../node_modules');
117
+ }
118
+ };
119
+
120
+ const modifyHandlerPaths = (functions) => {
121
+ const isOffline = process.argv.includes('offline');
122
+ console.log('isOffline', isOffline);
123
+
124
+ if (!isOffline) {
125
+ console.log('Not in offline mode, skipping handler path modification');
126
+ return functions;
127
+ }
128
+
129
+ const nodeModulesPath = findNodeModulesPath();
130
+ const modifiedFunctions = { ...functions };
131
+
132
+ for (const functionName of Object.keys(modifiedFunctions)) {
133
+ console.log('functionName', functionName);
134
+ const functionDef = modifiedFunctions[functionName];
135
+ if (functionDef?.handler?.includes('node_modules/')) {
136
+ const relativePath = path.relative(process.cwd(), nodeModulesPath);
137
+ functionDef.handler = functionDef.handler.replace('node_modules/', `${relativePath}/`);
138
+ console.log(`Updated handler for ${functionName}: ${functionDef.handler}`);
139
+ }
140
+ }
141
+
142
+ return modifiedFunctions;
143
+ };
144
+
145
+ const createVPCInfrastructure = (AppDefinition) => {
146
+ const vpcResources = {
147
+ FriggVPC: {
148
+ Type: 'AWS::EC2::VPC',
149
+ Properties: {
150
+ CidrBlock: AppDefinition.vpc.cidrBlock || '10.0.0.0/16',
151
+ EnableDnsHostnames: true,
152
+ EnableDnsSupport: true,
153
+ Tags: [
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}' },
158
+ ],
159
+ },
160
+ },
161
+ FriggInternetGateway: {
162
+ Type: 'AWS::EC2::InternetGateway',
163
+ Properties: {
164
+ Tags: [
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}' },
169
+ ],
170
+ },
171
+ },
172
+ FriggVPCGatewayAttachment: {
173
+ Type: 'AWS::EC2::VPCGatewayAttachment',
174
+ Properties: {
175
+ VpcId: { Ref: 'FriggVPC' },
176
+ InternetGatewayId: { Ref: 'FriggInternetGateway' },
177
+ },
178
+ },
179
+ FriggPublicSubnet: {
180
+ Type: 'AWS::EC2::Subnet',
181
+ Properties: {
182
+ VpcId: { Ref: 'FriggVPC' },
183
+ CidrBlock: '10.0.1.0/24',
184
+ AvailabilityZone: { 'Fn::Select': [0, { 'Fn::GetAZs': '' }] },
185
+ MapPublicIpOnLaunch: true,
186
+ Tags: [
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' },
192
+ ],
193
+ },
194
+ },
195
+ FriggPrivateSubnet1: {
196
+ Type: 'AWS::EC2::Subnet',
197
+ Properties: {
198
+ VpcId: { Ref: 'FriggVPC' },
199
+ CidrBlock: '10.0.2.0/24',
200
+ AvailabilityZone: { 'Fn::Select': [0, { 'Fn::GetAZs': '' }] },
201
+ Tags: [
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' },
207
+ ],
208
+ },
209
+ },
210
+ FriggPrivateSubnet2: {
211
+ Type: 'AWS::EC2::Subnet',
212
+ Properties: {
213
+ VpcId: { Ref: 'FriggVPC' },
214
+ CidrBlock: '10.0.3.0/24',
215
+ AvailabilityZone: { 'Fn::Select': [1, { 'Fn::GetAZs': '' }] },
216
+ Tags: [
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' },
222
+ ],
223
+ },
224
+ },
225
+ FriggNATGatewayEIP: {
226
+ Type: 'AWS::EC2::EIP',
227
+ Properties: {
228
+ Domain: 'vpc',
229
+ Tags: [
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}' },
234
+ ],
235
+ },
236
+ DependsOn: 'FriggVPCGatewayAttachment',
237
+ },
238
+ FriggNATGateway: {
239
+ Type: 'AWS::EC2::NatGateway',
240
+ Properties: {
241
+ AllocationId: { 'Fn::GetAtt': ['FriggNATGatewayEIP', 'AllocationId'] },
242
+ SubnetId: { Ref: 'FriggPublicSubnet' },
243
+ Tags: [
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}' },
248
+ ],
249
+ },
250
+ },
251
+ FriggPublicRouteTable: {
252
+ Type: 'AWS::EC2::RouteTable',
253
+ Properties: {
254
+ VpcId: { Ref: 'FriggVPC' },
255
+ Tags: [
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' },
261
+ ],
262
+ },
263
+ },
264
+ FriggPublicRoute: {
265
+ Type: 'AWS::EC2::Route',
266
+ Properties: {
267
+ RouteTableId: { Ref: 'FriggPublicRouteTable' },
268
+ DestinationCidrBlock: '0.0.0.0/0',
269
+ GatewayId: { Ref: 'FriggInternetGateway' },
270
+ },
271
+ DependsOn: 'FriggVPCGatewayAttachment',
272
+ },
273
+ FriggPublicSubnetRouteTableAssociation: {
274
+ Type: 'AWS::EC2::SubnetRouteTableAssociation',
275
+ Properties: {
276
+ SubnetId: { Ref: 'FriggPublicSubnet' },
277
+ RouteTableId: { Ref: 'FriggPublicRouteTable' },
278
+ },
279
+ },
280
+ FriggPrivateRouteTable: {
281
+ Type: 'AWS::EC2::RouteTable',
282
+ Properties: {
283
+ VpcId: { Ref: 'FriggVPC' },
284
+ Tags: [
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' },
290
+ ],
291
+ },
292
+ },
293
+ FriggPrivateRoute: {
294
+ Type: 'AWS::EC2::Route',
295
+ Properties: {
296
+ RouteTableId: { Ref: 'FriggPrivateRouteTable' },
297
+ DestinationCidrBlock: '0.0.0.0/0',
298
+ NatGatewayId: { Ref: 'FriggNATGateway' },
299
+ },
300
+ },
301
+ FriggPrivateSubnet1RouteTableAssociation: {
302
+ Type: 'AWS::EC2::SubnetRouteTableAssociation',
303
+ Properties: {
304
+ SubnetId: { Ref: 'FriggPrivateSubnet1' },
305
+ RouteTableId: { Ref: 'FriggPrivateRouteTable' },
306
+ },
307
+ },
308
+ FriggPrivateSubnet2RouteTableAssociation: {
309
+ Type: 'AWS::EC2::SubnetRouteTableAssociation',
310
+ Properties: {
311
+ SubnetId: { Ref: 'FriggPrivateSubnet2' },
312
+ RouteTableId: { Ref: 'FriggPrivateRouteTable' },
313
+ },
314
+ },
315
+ FriggLambdaSecurityGroup: {
316
+ Type: 'AWS::EC2::SecurityGroup',
317
+ Properties: {
318
+ GroupDescription: 'Security group for Frigg Lambda functions',
319
+ VpcId: { Ref: 'FriggVPC' },
320
+ SecurityGroupEgress: [
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' },
326
+ ],
327
+ Tags: [
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}' },
332
+ ],
333
+ },
334
+ },
335
+ };
336
+
337
+ if (AppDefinition.vpc.enableVPCEndpoints !== false) {
338
+ vpcResources.FriggS3VPCEndpoint = {
339
+ Type: 'AWS::EC2::VPCEndpoint',
340
+ Properties: {
341
+ VpcId: { Ref: 'FriggVPC' },
342
+ ServiceName: 'com.amazonaws.${self:provider.region}.s3',
343
+ VpcEndpointType: 'Gateway',
344
+ RouteTableIds: [{ Ref: 'FriggPrivateRouteTable' }],
345
+ },
346
+ };
347
+
348
+ vpcResources.FriggDynamoDBVPCEndpoint = {
349
+ Type: 'AWS::EC2::VPCEndpoint',
350
+ Properties: {
351
+ VpcId: { Ref: 'FriggVPC' },
352
+ ServiceName: 'com.amazonaws.${self:provider.region}.dynamodb',
353
+ VpcEndpointType: 'Gateway',
354
+ RouteTableIds: [{ Ref: 'FriggPrivateRouteTable' }],
355
+ },
356
+ };
357
+
358
+ if (AppDefinition.encryption?.fieldLevelEncryptionMethod === 'kms') {
359
+ vpcResources.FriggKMSVPCEndpoint = {
360
+ Type: 'AWS::EC2::VPCEndpoint',
361
+ Properties: {
362
+ VpcId: { Ref: 'FriggVPC' },
363
+ ServiceName: 'com.amazonaws.${self:provider.region}.kms',
364
+ VpcEndpointType: 'Interface',
365
+ SubnetIds: [{ Ref: 'FriggPrivateSubnet1' }, { Ref: 'FriggPrivateSubnet2' }],
366
+ SecurityGroupIds: [{ Ref: 'FriggVPCEndpointSecurityGroup' }],
367
+ PrivateDnsEnabled: true,
368
+ },
369
+ };
370
+ }
371
+
372
+ vpcResources.FriggSecretsManagerVPCEndpoint = {
373
+ Type: 'AWS::EC2::VPCEndpoint',
374
+ Properties: {
375
+ VpcId: { Ref: 'FriggVPC' },
376
+ ServiceName: 'com.amazonaws.${self:provider.region}.secretsmanager',
377
+ VpcEndpointType: 'Interface',
378
+ SubnetIds: [{ Ref: 'FriggPrivateSubnet1' }, { Ref: 'FriggPrivateSubnet2' }],
379
+ SecurityGroupIds: [{ Ref: 'FriggVPCEndpointSecurityGroup' }],
380
+ PrivateDnsEnabled: true,
381
+ },
382
+ };
383
+
384
+ vpcResources.FriggVPCEndpointSecurityGroup = {
385
+ Type: 'AWS::EC2::SecurityGroup',
386
+ Properties: {
387
+ GroupDescription: 'Security group for Frigg VPC Endpoints - allows HTTPS from Lambda functions',
388
+ VpcId: { Ref: 'FriggVPC' },
389
+ SecurityGroupIngress: [
390
+ {
391
+ IpProtocol: 'tcp',
392
+ FromPort: 443,
393
+ ToPort: 443,
394
+ SourceSecurityGroupId: { Ref: 'FriggLambdaSecurityGroup' },
395
+ Description: 'HTTPS from Lambda security group',
396
+ },
397
+ {
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)',
403
+ },
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
+ ],
413
+ },
414
+ };
415
+ }
416
+
417
+ return vpcResources;
418
+ };
419
+
420
+ const gatherDiscoveredResources = async (AppDefinition) => {
421
+ if (!shouldRunDiscovery(AppDefinition)) {
422
+ return {};
423
+ }
424
+
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
+ };
434
+
435
+ const discoveredResources = await discovery.discoverResources(config);
436
+
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
+ );
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}`);
457
+ }
458
+ };
459
+
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';
488
+
489
+ return {
6
490
  frameworkVersion: '>=3.17.0',
7
491
  service: AppDefinition.name || 'create-frigg-app',
8
492
  package: {
9
493
  individually: true,
494
+ exclude: ['!**/node_modules/aws-sdk/**', '!**/node_modules/@aws-sdk/**', '!package.json'],
10
495
  },
11
496
  useDotenv: true,
12
497
  provider: {
13
498
  name: AppDefinition.provider || 'aws',
14
499
  runtime: 'nodejs20.x',
15
500
  timeout: 30,
16
- region: 'us-east-1',
501
+ region,
17
502
  stage: '${opt:stage}',
18
- environment: {
19
- STAGE: '${opt:stage}',
20
- AWS_NODEJS_CONNECTION_REUSE_ENABLED: 1,
21
- },
503
+ environment: buildEnvironment(appEnvironmentVars, discoveredResources),
22
504
  iamRoleStatements: [
23
505
  {
24
506
  Effect: 'Allow',
25
507
  Action: ['sns:Publish'],
26
- Resource: {
27
- Ref: 'InternalErrorBridgeTopic',
28
- },
508
+ Resource: { Ref: 'InternalErrorBridgeTopic' },
509
+ },
510
+ {
511
+ Effect: 'Allow',
512
+ Action: ['sqs:SendMessage', 'sqs:SendMessageBatch', 'sqs:GetQueueUrl', 'sqs:GetQueueAttributes'],
513
+ Resource: [
514
+ { 'Fn::GetAtt': ['InternalErrorQueue', 'Arn'] },
515
+ {
516
+ 'Fn::Join': [
517
+ ':',
518
+ ['arn:aws:sqs:${self:provider.region}:*:${self:service}--${self:provider.stage}-*Queue'],
519
+ ],
520
+ },
521
+ ],
29
522
  },
30
523
  ],
524
+ httpApi: {
525
+ payload: '2.0',
526
+ cors: {
527
+ allowedOrigins: ['*'],
528
+ allowedHeaders: ['*'],
529
+ allowedMethods: ['*'],
530
+ allowCredentials: false,
531
+ },
532
+ name: '${opt:stage, "dev"}-${self:service}',
533
+ disableDefaultEndpoint: false,
534
+ },
31
535
  },
32
536
  plugins: [
537
+ 'serverless-jetpack',
33
538
  'serverless-dotenv-plugin',
34
539
  'serverless-offline-sqs',
35
540
  'serverless-offline',
@@ -45,80 +550,33 @@ const composeServerlessDefinition = (AppDefinition, IntegrationFactory) => {
45
550
  autoCreate: false,
46
551
  apiVersion: '2012-11-05',
47
552
  endpoint: 'http://localhost:4566',
48
- region: 'us-east-1',
553
+ region,
49
554
  accessKeyId: 'root',
50
555
  secretAccessKey: 'root',
51
556
  skipCacheInvalidation: false,
52
557
  },
53
- webpack: {
54
- webpackConfig: 'webpack.config.js',
55
- includeModules: {
56
- forceExclude: ['aws-sdk'],
57
- },
58
- packager: 'npm',
59
- excludeFiles: ['src/**/*.test.js', 'test/'],
558
+ jetpack: {
559
+ base: '..',
60
560
  },
61
561
  },
62
562
  functions: {
63
- defaultWebsocket: {
64
- handler:
65
- '/../node_modules/@friggframework/devtools/infrastructure/routers/websocket.handler',
66
- events: [
67
- {
68
- websocket: {
69
- route: '$connect',
70
- },
71
- },
72
- {
73
- websocket: {
74
- route: '$default',
75
- },
76
- },
77
- {
78
- websocket: {
79
- route: '$disconnect',
80
- },
81
- },
82
- ],
83
- },
84
563
  auth: {
85
- handler:
86
- '/../node_modules/@friggframework/devtools/infrastructure/routers/auth.handler',
564
+ handler: 'node_modules/@friggframework/core/handlers/routers/auth.handler',
87
565
  events: [
88
- {
89
- http: {
90
- path: '/api/integrations',
91
- method: 'ANY',
92
- cors: true,
93
- },
94
- },
95
- {
96
- http: {
97
- path: '/api/integrations/{proxy+}',
98
- method: 'ANY',
99
- cors: true,
100
- },
101
- },
102
- {
103
- http: {
104
- path: '/api/authorize',
105
- method: 'ANY',
106
- cors: true,
107
- },
108
- },
566
+ { httpApi: { path: '/api/integrations', method: 'ANY' } },
567
+ { httpApi: { path: '/api/integrations/{proxy+}', method: 'ANY' } },
568
+ { httpApi: { path: '/api/authorize', method: 'ANY' } },
109
569
  ],
110
570
  },
111
571
  user: {
112
- handler:
113
- '/../node_modules/@friggframework/devtools/infrastructure/routers/user.handler',
572
+ handler: 'node_modules/@friggframework/core/handlers/routers/user.handler',
573
+ events: [{ httpApi: { path: '/user/{proxy+}', method: 'ANY' } }],
574
+ },
575
+ health: {
576
+ handler: 'node_modules/@friggframework/core/handlers/routers/health.handler',
114
577
  events: [
115
- {
116
- http: {
117
- path: '/user/{proxy+}',
118
- method: 'ANY',
119
- cors: true,
120
- },
121
- },
578
+ { httpApi: { path: '/health', method: 'GET' } },
579
+ { httpApi: { path: '/health/{proxy+}', method: 'GET' } },
122
580
  ],
123
581
  },
124
582
  },
@@ -127,8 +585,7 @@ const composeServerlessDefinition = (AppDefinition, IntegrationFactory) => {
127
585
  InternalErrorQueue: {
128
586
  Type: 'AWS::SQS::Queue',
129
587
  Properties: {
130
- QueueName:
131
- 'internal-error-queue-${self:provider.stage}',
588
+ QueueName: '${self:service}-internal-error-queue-${self:provider.stage}',
132
589
  MessageRetentionPeriod: 300,
133
590
  },
134
591
  },
@@ -138,9 +595,7 @@ const composeServerlessDefinition = (AppDefinition, IntegrationFactory) => {
138
595
  Subscription: [
139
596
  {
140
597
  Protocol: 'sqs',
141
- Endpoint: {
142
- 'Fn::GetAtt': ['InternalErrorQueue', 'Arn'],
143
- },
598
+ Endpoint: { 'Fn::GetAtt': ['InternalErrorQueue', 'Arn'] },
144
599
  },
145
600
  ],
146
601
  },
@@ -155,25 +610,11 @@ const composeServerlessDefinition = (AppDefinition, IntegrationFactory) => {
155
610
  {
156
611
  Sid: 'Allow Dead Letter SNS to publish to SQS',
157
612
  Effect: 'Allow',
158
- Principal: {
159
- Service: 'sns.amazonaws.com',
160
- },
161
- Resource: {
162
- 'Fn::GetAtt': [
163
- 'InternalErrorQueue',
164
- 'Arn',
165
- ],
166
- },
167
- Action: [
168
- 'SQS:SendMessage',
169
- 'SQS:SendMessageBatch',
170
- ],
613
+ Principal: { Service: 'sns.amazonaws.com' },
614
+ Resource: { 'Fn::GetAtt': ['InternalErrorQueue', 'Arn'] },
615
+ Action: ['SQS:SendMessage', 'SQS:SendMessageBatch'],
171
616
  Condition: {
172
- ArnEquals: {
173
- 'aws:SourceArn': {
174
- Ref: 'InternalErrorBridgeTopic',
175
- },
176
- },
617
+ ArnEquals: { 'aws:SourceArn': { Ref: 'InternalErrorBridgeTopic' } },
177
618
  },
178
619
  },
179
620
  ],
@@ -193,80 +634,945 @@ const composeServerlessDefinition = (AppDefinition, IntegrationFactory) => {
193
634
  Period: 60,
194
635
  AlarmActions: [{ Ref: 'InternalErrorBridgeTopic' }],
195
636
  Dimensions: [
196
- {
197
- Name: 'ApiName',
198
- Value: {
199
- 'Fn::Join': [
200
- '-',
201
- [
202
- '${self:provider.stage}',
203
- '${self:service}',
204
- ],
205
- ],
206
- },
207
- },
637
+ { Name: 'ApiId', Value: { Ref: 'HttpApi' } },
638
+ { Name: 'Stage', Value: '${self:provider.stage}' },
208
639
  ],
209
640
  },
210
641
  },
211
642
  },
212
643
  },
213
644
  };
645
+ };
646
+
647
+ const applyKmsConfiguration = (definition, AppDefinition, discoveredResources) => {
648
+ if (AppDefinition.encryption?.fieldLevelEncryptionMethod !== 'kms') {
649
+ return;
650
+ }
651
+
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
+ };
662
+
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
+ }
675
+
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' },
692
+ },
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
+ },
706
+ },
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
+ };
717
+
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
+ };
726
+
727
+ definition.provider.iamRoleStatements.push({
728
+ Effect: 'Allow',
729
+ Action: ['kms:GenerateDataKey', 'kms:Decrypt'],
730
+ Resource: [{ 'Fn::GetAtt': ['FriggKMSKey', 'Arn'] }],
731
+ });
732
+
733
+ definition.provider.environment.KMS_KEY_ARN = { 'Fn::GetAtt': ['FriggKMSKey', 'Arn'] };
734
+ definition.custom.kmsGrants = {
735
+ kmsKeyId: { 'Fn::GetAtt': ['FriggKMSKey', 'Arn'] },
736
+ };
737
+ }
738
+
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
+ }
745
+
746
+ if (!definition.provider.environment.KMS_KEY_ARN) {
747
+ definition.provider.environment.KMS_KEY_ARN =
748
+ discoveredResources.defaultKmsKeyId || '${env:AWS_DISCOVERY_KMS_KEY_ID}';
749
+ }
750
+ };
751
+
752
+ const healVpcConfiguration = (discoveredResources, AppDefinition) => {
753
+ const healingReport = {
754
+ healed: [],
755
+ warnings: [],
756
+ errors: [],
757
+ recommendations: [],
758
+ criticalActions: [],
759
+ };
760
+
761
+ if (!AppDefinition.vpc?.selfHeal) {
762
+ return healingReport;
763
+ }
764
+
765
+ console.log('🔧 Self-healing mode enabled - checking for VPC misconfigurations...');
766
+
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
+ }
777
+
778
+ if (discoveredResources.elasticIpAlreadyAssociated) {
779
+ healingReport.warnings.push(
780
+ `Elastic IP ${discoveredResources.existingElasticIp} is already associated`
781
+ );
782
+
783
+ if (discoveredResources.existingNatGatewayId) {
784
+ healingReport.healed.push('Will reuse existing NAT Gateway instead of creating a new one');
785
+ discoveredResources.reuseExistingNatGateway = true;
786
+ } else {
787
+ healingReport.healed.push('Will allocate a new Elastic IP for NAT Gateway');
788
+ discoveredResources.allocateNewElasticIp = true;
789
+ }
790
+ }
791
+
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',
979
+ Properties: {
980
+ Tags: [
981
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-igw' },
982
+ { Key: 'ManagedBy', Value: 'Frigg' },
983
+ ],
984
+ },
985
+ };
986
+
987
+ definition.resources.Resources.FriggIGWAttachment = {
988
+ Type: 'AWS::EC2::VPCGatewayAttachment',
989
+ Properties: {
990
+ VpcId: subnetVpcId,
991
+ InternetGatewayId: { Ref: 'FriggInternetGateway' },
992
+ },
993
+ };
994
+ }
995
+ }
996
+
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
+ };
1007
+
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
+ };
1017
+
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
+ }
1189
+
1190
+ definition.resources.Resources.FriggPublicSubnet = {
1191
+ Type: 'AWS::EC2::Subnet',
1192
+ Properties: {
1193
+ VpcId: discoveredResources.defaultVpcId,
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
+ ],
1202
+ },
1203
+ };
1204
+
1205
+ definition.resources.Resources.FriggPublicRouteTable = {
1206
+ Type: 'AWS::EC2::RouteTable',
1207
+ Properties: {
1208
+ VpcId: discoveredResources.defaultVpcId,
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' },
1230
+ },
1231
+ };
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;
1290
+ }
1291
+ }
1292
+
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
+ };
1306
+
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');
1327
+ }
1328
+
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
+ }
1440
+ }
1441
+
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
+ }
1464
+
1465
+ definition.resources.Resources.VPCEndpointSecurityGroup = {
1466
+ Type: 'AWS::EC2::SecurityGroup',
1467
+ Properties: {
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
+ ],
1476
+ },
1477
+ };
1478
+ }
1479
+
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
+ };
1491
+
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,
1502
+ },
1503
+ };
1504
+ }
1505
+ }
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
+ }
1531
+
1532
+ console.log(`Processing ${AppDefinition.integrations.length} integrations...`);
1533
+
1534
+ for (const integration of AppDefinition.integrations) {
1535
+ if (!integration?.Definition?.name) {
1536
+ throw new Error('Invalid integration: missing Definition or name');
1537
+ }
214
1538
 
215
- // Add integration-specific functions and resources
216
- AppDefinition.integrations.forEach((integration) => {
217
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}`;
218
1542
 
219
- // Add function for the integration
220
1543
  definition.functions[integrationName] = {
221
- handler: `/../node_modules/@friggframework/devtools/infrastructure/routers/integration-defined-routers.handlers.${integrationName}.handler`,
222
- // events: integration.Definition.routes.map((route) => ({
223
- // http: {
224
- // path: `/api/${integrationName}-integration${route.path}`,
225
- // method: route.method || 'ANY',
226
- // cors: true,
227
- // },
228
- // })),
1544
+ handler: `node_modules/@friggframework/core/handlers/routers/integration-defined-routers.handlers.${integrationName}.handler`,
229
1545
  events: [
230
1546
  {
231
- http: {
232
- path: `/api/${integrationName}-integration/{proxy*}`,
1547
+ httpApi: {
1548
+ path: `/api/${integrationName}-integration/{proxy+}`,
233
1549
  method: 'ANY',
234
- cors: true,
235
1550
  },
236
1551
  },
237
1552
  ],
238
1553
  };
239
1554
 
240
- // Add SQS Queue for the integration
241
- const queueReference = `${
242
- integrationName.charAt(0).toUpperCase() + integrationName.slice(1)
243
- }Queue`;
244
- const queueName = `\${self:service}--\${self:provider.stage}-${queueReference}`;
245
1555
  definition.resources.Resources[queueReference] = {
246
1556
  Type: 'AWS::SQS::Queue',
247
1557
  Properties: {
248
1558
  QueueName: `\${self:custom.${queueReference}}`,
249
1559
  MessageRetentionPeriod: 60,
1560
+ VisibilityTimeout: 1800,
250
1561
  RedrivePolicy: {
251
1562
  maxReceiveCount: 1,
252
- deadLetterTargetArn: {
253
- 'Fn::GetAtt': ['InternalErrorQueue', 'Arn'],
254
- },
1563
+ deadLetterTargetArn: { 'Fn::GetAtt': ['InternalErrorQueue', 'Arn'] },
255
1564
  },
256
1565
  },
257
1566
  };
258
1567
 
259
- // Add Queue Worker for the integration
260
1568
  const queueWorkerName = `${integrationName}QueueWorker`;
261
1569
  definition.functions[queueWorkerName] = {
262
- handler: `/../node_modules/@friggframework/devtools/infrastructure/workers/integration-defined-workers.handlers.${integrationName}.queueWorker`,
1570
+ handler: `node_modules/@friggframework/core/handlers/workers/integration-defined-workers.handlers.${integrationName}.queueWorker`,
263
1571
  reservedConcurrency: 5,
264
1572
  events: [
265
1573
  {
266
1574
  sqs: {
267
- arn: {
268
- 'Fn::GetAtt': [queueReference, 'Arn'],
269
- },
1575
+ arn: { 'Fn::GetAtt': [queueReference, 'Arn'] },
270
1576
  batchSize: 1,
271
1577
  },
272
1578
  },
@@ -274,16 +1580,44 @@ const composeServerlessDefinition = (AppDefinition, IntegrationFactory) => {
274
1580
  timeout: 600,
275
1581
  };
276
1582
 
277
- // Add Queue URL for the integration to the ENVironment variables
278
1583
  definition.provider.environment = {
279
1584
  ...definition.provider.environment,
280
- [integrationName.toUpperCase() + '_QUEUE_URL']: {
281
- Ref: queueReference,
282
- },
1585
+ [`${integrationName.toUpperCase()}_QUEUE_URL`]: { Ref: queueReference },
283
1586
  };
284
1587
 
285
1588
  definition.custom[queueReference] = queueName;
286
- });
1589
+ }
1590
+ };
1591
+
1592
+ const configureWebsockets = (definition, AppDefinition) => {
1593
+ if (AppDefinition.websockets?.enable !== true) {
1594
+ return;
1595
+ }
1596
+
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
+
1620
+ definition.functions = modifyHandlerPaths(definition.functions);
287
1621
 
288
1622
  return definition;
289
1623
  };