@friggframework/devtools 2.0.0-next.4 → 2.0.0-next.41

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