@friggframework/devtools 2.0.0-next.45 → 2.0.0-next.46

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 (212) hide show
  1. package/infrastructure/ARCHITECTURE.md +487 -0
  2. package/infrastructure/HEALTH.md +468 -0
  3. package/infrastructure/README.md +51 -0
  4. package/infrastructure/__tests__/postgres-config.test.js +914 -0
  5. package/infrastructure/__tests__/template-generation.test.js +687 -0
  6. package/infrastructure/create-frigg-infrastructure.js +1 -1
  7. package/infrastructure/docs/POSTGRES-CONFIGURATION.md +630 -0
  8. package/infrastructure/{DEPLOYMENT-INSTRUCTIONS.md → docs/deployment-instructions.md} +3 -3
  9. package/infrastructure/{IAM-POLICY-TEMPLATES.md → docs/iam-policy-templates.md} +9 -10
  10. package/infrastructure/domains/database/aurora-builder.js +809 -0
  11. package/infrastructure/domains/database/aurora-builder.test.js +950 -0
  12. package/infrastructure/domains/database/aurora-discovery.js +87 -0
  13. package/infrastructure/domains/database/aurora-discovery.test.js +188 -0
  14. package/infrastructure/domains/database/aurora-resolver.js +210 -0
  15. package/infrastructure/domains/database/aurora-resolver.test.js +347 -0
  16. package/infrastructure/domains/database/migration-builder.js +633 -0
  17. package/infrastructure/domains/database/migration-builder.test.js +294 -0
  18. package/infrastructure/domains/database/migration-resolver.js +163 -0
  19. package/infrastructure/domains/database/migration-resolver.test.js +337 -0
  20. package/infrastructure/domains/health/application/ports/IPropertyReconciler.js +164 -0
  21. package/infrastructure/domains/health/application/ports/IResourceDetector.js +129 -0
  22. package/infrastructure/domains/health/application/ports/IResourceImporter.js +142 -0
  23. package/infrastructure/domains/health/application/ports/IStackRepository.js +131 -0
  24. package/infrastructure/domains/health/application/ports/index.js +26 -0
  25. package/infrastructure/domains/health/application/use-cases/__tests__/execute-resource-import-use-case.test.js +679 -0
  26. package/infrastructure/domains/health/application/use-cases/__tests__/mismatch-analyzer-method-name.test.js +167 -0
  27. package/infrastructure/domains/health/application/use-cases/__tests__/repair-via-import-use-case.test.js +1130 -0
  28. package/infrastructure/domains/health/application/use-cases/execute-resource-import-use-case.js +221 -0
  29. package/infrastructure/domains/health/application/use-cases/reconcile-properties-use-case.js +152 -0
  30. package/infrastructure/domains/health/application/use-cases/reconcile-properties-use-case.test.js +343 -0
  31. package/infrastructure/domains/health/application/use-cases/repair-via-import-use-case.js +535 -0
  32. package/infrastructure/domains/health/application/use-cases/repair-via-import-use-case.test.js +376 -0
  33. package/infrastructure/domains/health/application/use-cases/run-health-check-use-case.js +213 -0
  34. package/infrastructure/domains/health/application/use-cases/run-health-check-use-case.test.js +441 -0
  35. package/infrastructure/domains/health/docs/ACME-DEV-DRIFT-ANALYSIS.md +267 -0
  36. package/infrastructure/domains/health/docs/BUILD-VS-DEPLOYED-TEMPLATE-ANALYSIS.md +324 -0
  37. package/infrastructure/domains/health/docs/ORPHAN-DETECTION-ANALYSIS.md +386 -0
  38. package/infrastructure/domains/health/docs/SPEC-CLEANUP-COMMAND.md +1419 -0
  39. package/infrastructure/domains/health/docs/TDD-IMPLEMENTATION-SUMMARY.md +391 -0
  40. package/infrastructure/domains/health/docs/TEMPLATE-COMPARISON-IMPLEMENTATION.md +551 -0
  41. package/infrastructure/domains/health/domain/entities/issue.js +299 -0
  42. package/infrastructure/domains/health/domain/entities/issue.test.js +528 -0
  43. package/infrastructure/domains/health/domain/entities/property-mismatch.js +108 -0
  44. package/infrastructure/domains/health/domain/entities/property-mismatch.test.js +275 -0
  45. package/infrastructure/domains/health/domain/entities/resource.js +159 -0
  46. package/infrastructure/domains/health/domain/entities/resource.test.js +432 -0
  47. package/infrastructure/domains/health/domain/entities/stack-health-report.js +306 -0
  48. package/infrastructure/domains/health/domain/entities/stack-health-report.test.js +601 -0
  49. package/infrastructure/domains/health/domain/services/__tests__/health-score-percentage-based.test.js +380 -0
  50. package/infrastructure/domains/health/domain/services/__tests__/import-progress-monitor.test.js +971 -0
  51. package/infrastructure/domains/health/domain/services/__tests__/import-template-generator.test.js +1150 -0
  52. package/infrastructure/domains/health/domain/services/__tests__/logical-id-mapper.test.js +672 -0
  53. package/infrastructure/domains/health/domain/services/__tests__/template-parser.test.js +496 -0
  54. package/infrastructure/domains/health/domain/services/__tests__/update-progress-monitor.test.js +419 -0
  55. package/infrastructure/domains/health/domain/services/health-score-calculator.js +248 -0
  56. package/infrastructure/domains/health/domain/services/health-score-calculator.test.js +504 -0
  57. package/infrastructure/domains/health/domain/services/import-progress-monitor.js +195 -0
  58. package/infrastructure/domains/health/domain/services/import-template-generator.js +435 -0
  59. package/infrastructure/domains/health/domain/services/logical-id-mapper.js +345 -0
  60. package/infrastructure/domains/health/domain/services/mismatch-analyzer.js +234 -0
  61. package/infrastructure/domains/health/domain/services/mismatch-analyzer.test.js +431 -0
  62. package/infrastructure/domains/health/domain/services/property-mutability-config.js +382 -0
  63. package/infrastructure/domains/health/domain/services/template-parser.js +245 -0
  64. package/infrastructure/domains/health/domain/services/update-progress-monitor.js +192 -0
  65. package/infrastructure/domains/health/domain/value-objects/health-score.js +138 -0
  66. package/infrastructure/domains/health/domain/value-objects/health-score.test.js +267 -0
  67. package/infrastructure/domains/health/domain/value-objects/property-mutability.js +161 -0
  68. package/infrastructure/domains/health/domain/value-objects/property-mutability.test.js +198 -0
  69. package/infrastructure/domains/health/domain/value-objects/resource-state.js +167 -0
  70. package/infrastructure/domains/health/domain/value-objects/resource-state.test.js +196 -0
  71. package/infrastructure/domains/health/domain/value-objects/stack-identifier.js +192 -0
  72. package/infrastructure/domains/health/domain/value-objects/stack-identifier.test.js +262 -0
  73. package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-cfn-tagged.test.js +312 -0
  74. package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-multi-stack.test.js +367 -0
  75. package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-relationship-analysis.test.js +432 -0
  76. package/infrastructure/domains/health/infrastructure/adapters/aws-property-reconciler.js +784 -0
  77. package/infrastructure/domains/health/infrastructure/adapters/aws-property-reconciler.test.js +1133 -0
  78. package/infrastructure/domains/health/infrastructure/adapters/aws-resource-detector.js +565 -0
  79. package/infrastructure/domains/health/infrastructure/adapters/aws-resource-detector.test.js +554 -0
  80. package/infrastructure/domains/health/infrastructure/adapters/aws-resource-importer.js +318 -0
  81. package/infrastructure/domains/health/infrastructure/adapters/aws-resource-importer.test.js +398 -0
  82. package/infrastructure/domains/health/infrastructure/adapters/aws-stack-repository.js +777 -0
  83. package/infrastructure/domains/health/infrastructure/adapters/aws-stack-repository.test.js +580 -0
  84. package/infrastructure/domains/integration/integration-builder.js +397 -0
  85. package/infrastructure/domains/integration/integration-builder.test.js +593 -0
  86. package/infrastructure/domains/integration/integration-resolver.js +170 -0
  87. package/infrastructure/domains/integration/integration-resolver.test.js +369 -0
  88. package/infrastructure/domains/integration/websocket-builder.js +69 -0
  89. package/infrastructure/domains/integration/websocket-builder.test.js +195 -0
  90. package/infrastructure/domains/networking/vpc-builder.js +1829 -0
  91. package/infrastructure/domains/networking/vpc-builder.test.js +1262 -0
  92. package/infrastructure/domains/networking/vpc-discovery.js +177 -0
  93. package/infrastructure/domains/networking/vpc-discovery.test.js +350 -0
  94. package/infrastructure/domains/networking/vpc-resolver.js +324 -0
  95. package/infrastructure/domains/networking/vpc-resolver.test.js +501 -0
  96. package/infrastructure/domains/parameters/ssm-builder.js +79 -0
  97. package/infrastructure/domains/parameters/ssm-builder.test.js +189 -0
  98. package/infrastructure/domains/parameters/ssm-discovery.js +84 -0
  99. package/infrastructure/domains/parameters/ssm-discovery.test.js +210 -0
  100. package/infrastructure/{iam-generator.js → domains/security/iam-generator.js} +2 -2
  101. package/infrastructure/domains/security/kms-builder.js +366 -0
  102. package/infrastructure/domains/security/kms-builder.test.js +374 -0
  103. package/infrastructure/domains/security/kms-discovery.js +80 -0
  104. package/infrastructure/domains/security/kms-discovery.test.js +177 -0
  105. package/infrastructure/domains/security/kms-resolver.js +96 -0
  106. package/infrastructure/domains/security/kms-resolver.test.js +216 -0
  107. package/infrastructure/domains/shared/base-builder.js +112 -0
  108. package/infrastructure/domains/shared/base-resolver.js +186 -0
  109. package/infrastructure/domains/shared/base-resolver.test.js +305 -0
  110. package/infrastructure/domains/shared/builder-orchestrator.js +212 -0
  111. package/infrastructure/domains/shared/builder-orchestrator.test.js +213 -0
  112. package/infrastructure/domains/shared/cloudformation-discovery-v2.js +334 -0
  113. package/infrastructure/domains/shared/cloudformation-discovery.js +375 -0
  114. package/infrastructure/domains/shared/cloudformation-discovery.test.js +590 -0
  115. package/infrastructure/domains/shared/environment-builder.js +119 -0
  116. package/infrastructure/domains/shared/environment-builder.test.js +247 -0
  117. package/infrastructure/domains/shared/providers/aws-provider-adapter.js +544 -0
  118. package/infrastructure/domains/shared/providers/aws-provider-adapter.test.js +377 -0
  119. package/infrastructure/domains/shared/providers/azure-provider-adapter.stub.js +93 -0
  120. package/infrastructure/domains/shared/providers/cloud-provider-adapter.js +136 -0
  121. package/infrastructure/domains/shared/providers/gcp-provider-adapter.stub.js +82 -0
  122. package/infrastructure/domains/shared/providers/provider-factory.js +108 -0
  123. package/infrastructure/domains/shared/providers/provider-factory.test.js +170 -0
  124. package/infrastructure/domains/shared/resource-discovery.js +192 -0
  125. package/infrastructure/domains/shared/resource-discovery.test.js +552 -0
  126. package/infrastructure/domains/shared/types/app-definition.js +205 -0
  127. package/infrastructure/domains/shared/types/discovery-result.js +106 -0
  128. package/infrastructure/domains/shared/types/discovery-result.test.js +258 -0
  129. package/infrastructure/domains/shared/types/index.js +46 -0
  130. package/infrastructure/domains/shared/types/resource-ownership.js +108 -0
  131. package/infrastructure/domains/shared/types/resource-ownership.test.js +101 -0
  132. package/infrastructure/domains/shared/utilities/base-definition-factory.js +380 -0
  133. package/infrastructure/domains/shared/utilities/base-definition-factory.js.bak +338 -0
  134. package/infrastructure/domains/shared/utilities/base-definition-factory.test.js +248 -0
  135. package/infrastructure/domains/shared/utilities/handler-path-resolver.js +134 -0
  136. package/infrastructure/domains/shared/utilities/handler-path-resolver.test.js +268 -0
  137. package/infrastructure/domains/shared/utilities/prisma-layer-manager.js +55 -0
  138. package/infrastructure/domains/shared/utilities/prisma-layer-manager.test.js +138 -0
  139. package/infrastructure/{env-validator.js → domains/shared/validation/env-validator.js} +2 -1
  140. package/infrastructure/domains/shared/validation/env-validator.test.js +173 -0
  141. package/infrastructure/esbuild.config.js +53 -0
  142. package/infrastructure/infrastructure-composer.js +87 -0
  143. package/infrastructure/{serverless-template.test.js → infrastructure-composer.test.js} +115 -24
  144. package/infrastructure/scripts/build-prisma-layer.js +553 -0
  145. package/infrastructure/scripts/build-prisma-layer.test.js +102 -0
  146. package/infrastructure/{build-time-discovery.js → scripts/build-time-discovery.js} +80 -48
  147. package/infrastructure/{build-time-discovery.test.js → scripts/build-time-discovery.test.js} +5 -4
  148. package/layers/prisma/nodejs/package.json +8 -0
  149. package/management-ui/server/utils/cliIntegration.js +1 -1
  150. package/management-ui/server/utils/environment/awsParameterStore.js +29 -18
  151. package/package.json +11 -11
  152. package/frigg-cli/.eslintrc.js +0 -141
  153. package/frigg-cli/__tests__/unit/commands/build.test.js +0 -251
  154. package/frigg-cli/__tests__/unit/commands/db-setup.test.js +0 -548
  155. package/frigg-cli/__tests__/unit/commands/install.test.js +0 -400
  156. package/frigg-cli/__tests__/unit/commands/ui.test.js +0 -346
  157. package/frigg-cli/__tests__/unit/utils/database-validator.test.js +0 -366
  158. package/frigg-cli/__tests__/unit/utils/error-messages.test.js +0 -304
  159. package/frigg-cli/__tests__/unit/utils/prisma-runner.test.js +0 -486
  160. package/frigg-cli/__tests__/utils/mock-factory.js +0 -270
  161. package/frigg-cli/__tests__/utils/prisma-mock.js +0 -194
  162. package/frigg-cli/__tests__/utils/test-fixtures.js +0 -463
  163. package/frigg-cli/__tests__/utils/test-setup.js +0 -287
  164. package/frigg-cli/build-command/index.js +0 -65
  165. package/frigg-cli/db-setup-command/index.js +0 -193
  166. package/frigg-cli/deploy-command/index.js +0 -175
  167. package/frigg-cli/generate-command/__tests__/generate-command.test.js +0 -301
  168. package/frigg-cli/generate-command/azure-generator.js +0 -43
  169. package/frigg-cli/generate-command/gcp-generator.js +0 -47
  170. package/frigg-cli/generate-command/index.js +0 -332
  171. package/frigg-cli/generate-command/terraform-generator.js +0 -555
  172. package/frigg-cli/generate-iam-command.js +0 -118
  173. package/frigg-cli/index.js +0 -75
  174. package/frigg-cli/index.test.js +0 -158
  175. package/frigg-cli/init-command/backend-first-handler.js +0 -756
  176. package/frigg-cli/init-command/index.js +0 -93
  177. package/frigg-cli/init-command/template-handler.js +0 -143
  178. package/frigg-cli/install-command/backend-js.js +0 -33
  179. package/frigg-cli/install-command/commit-changes.js +0 -16
  180. package/frigg-cli/install-command/environment-variables.js +0 -127
  181. package/frigg-cli/install-command/environment-variables.test.js +0 -136
  182. package/frigg-cli/install-command/index.js +0 -54
  183. package/frigg-cli/install-command/install-package.js +0 -13
  184. package/frigg-cli/install-command/integration-file.js +0 -30
  185. package/frigg-cli/install-command/logger.js +0 -12
  186. package/frigg-cli/install-command/template.js +0 -90
  187. package/frigg-cli/install-command/validate-package.js +0 -75
  188. package/frigg-cli/jest.config.js +0 -124
  189. package/frigg-cli/package.json +0 -54
  190. package/frigg-cli/start-command/index.js +0 -149
  191. package/frigg-cli/start-command/start-command.test.js +0 -297
  192. package/frigg-cli/test/init-command.test.js +0 -180
  193. package/frigg-cli/test/npm-registry.test.js +0 -319
  194. package/frigg-cli/ui-command/index.js +0 -154
  195. package/frigg-cli/utils/app-resolver.js +0 -319
  196. package/frigg-cli/utils/backend-path.js +0 -25
  197. package/frigg-cli/utils/database-validator.js +0 -161
  198. package/frigg-cli/utils/error-messages.js +0 -257
  199. package/frigg-cli/utils/npm-registry.js +0 -167
  200. package/frigg-cli/utils/prisma-runner.js +0 -280
  201. package/frigg-cli/utils/process-manager.js +0 -199
  202. package/frigg-cli/utils/repo-detection.js +0 -405
  203. package/infrastructure/aws-discovery.js +0 -1176
  204. package/infrastructure/aws-discovery.test.js +0 -1220
  205. package/infrastructure/serverless-template.js +0 -2094
  206. /package/infrastructure/{WEBSOCKET-CONFIGURATION.md → docs/WEBSOCKET-CONFIGURATION.md} +0 -0
  207. /package/infrastructure/{GENERATE-IAM-DOCS.md → docs/generate-iam-command.md} +0 -0
  208. /package/infrastructure/{iam-generator.test.js → domains/security/iam-generator.test.js} +0 -0
  209. /package/infrastructure/{frigg-deployment-iam-stack.yaml → domains/security/templates/frigg-deployment-iam-stack.yaml} +0 -0
  210. /package/infrastructure/{iam-policy-basic.json → domains/security/templates/iam-policy-basic.json} +0 -0
  211. /package/infrastructure/{iam-policy-full.json → domains/security/templates/iam-policy-full.json} +0 -0
  212. /package/infrastructure/{run-discovery.js → scripts/run-discovery.js} +0 -0
@@ -1,2094 +0,0 @@
1
- const path = require('path');
2
- const fs = require('fs');
3
- const { AWSDiscovery } = require('./aws-discovery');
4
-
5
- const shouldRunDiscovery = (AppDefinition) => {
6
- console.log(
7
- '⚙️ Checking FRIGG_SKIP_AWS_DISCOVERY:',
8
- process.env.FRIGG_SKIP_AWS_DISCOVERY
9
- );
10
- if (process.env.FRIGG_SKIP_AWS_DISCOVERY === 'true') {
11
- console.log(
12
- '⚙️ Skipping AWS discovery because FRIGG_SKIP_AWS_DISCOVERY is set.'
13
- );
14
- return false;
15
- }
16
-
17
- return (
18
- AppDefinition.vpc?.enable === true ||
19
- AppDefinition.encryption?.fieldLevelEncryptionMethod === 'kms' ||
20
- AppDefinition.ssm?.enable === true
21
- );
22
- };
23
-
24
- const getAppEnvironmentVars = (AppDefinition) => {
25
- const envVars = {};
26
- const reservedVars = new Set([
27
- '_HANDLER',
28
- '_X_AMZN_TRACE_ID',
29
- 'AWS_DEFAULT_REGION',
30
- 'AWS_EXECUTION_ENV',
31
- 'AWS_REGION',
32
- 'AWS_LAMBDA_FUNCTION_NAME',
33
- 'AWS_LAMBDA_FUNCTION_MEMORY_SIZE',
34
- 'AWS_LAMBDA_FUNCTION_VERSION',
35
- 'AWS_LAMBDA_INITIALIZATION_TYPE',
36
- 'AWS_LAMBDA_LOG_GROUP_NAME',
37
- 'AWS_LAMBDA_LOG_STREAM_NAME',
38
- 'AWS_ACCESS_KEY',
39
- 'AWS_ACCESS_KEY_ID',
40
- 'AWS_SECRET_ACCESS_KEY',
41
- 'AWS_SESSION_TOKEN',
42
- ]);
43
-
44
- if (!AppDefinition.environment) {
45
- return envVars;
46
- }
47
-
48
- console.log('📋 Loading environment variables from appDefinition...');
49
- const envKeys = [];
50
- const skippedKeys = [];
51
-
52
- for (const [key, value] of Object.entries(AppDefinition.environment)) {
53
- if (value !== true) continue;
54
- if (reservedVars.has(key)) {
55
- skippedKeys.push(key);
56
- continue;
57
- }
58
- envVars[key] = `\${env:${key}, ''}`;
59
- envKeys.push(key);
60
- }
61
-
62
- if (envKeys.length > 0) {
63
- console.log(
64
- ` Found ${envKeys.length} environment variables: ${envKeys.join(
65
- ', '
66
- )}`
67
- );
68
- }
69
- if (skippedKeys.length > 0) {
70
- console.log(
71
- ` ⚠️ Skipped ${skippedKeys.length
72
- } reserved AWS Lambda variables: ${skippedKeys.join(', ')}`
73
- );
74
- }
75
-
76
- return envVars;
77
- };
78
-
79
- const findNodeModulesPath = () => {
80
- try {
81
- let currentDir = process.cwd();
82
- let nodeModulesPath = null;
83
-
84
- for (let i = 0; i < 5; i++) {
85
- const potentialPath = path.join(currentDir, 'node_modules');
86
- if (fs.existsSync(potentialPath)) {
87
- nodeModulesPath = potentialPath;
88
- console.log(
89
- `Found node_modules at: ${nodeModulesPath} (method 1)`
90
- );
91
- break;
92
- }
93
- const parentDir = path.dirname(currentDir);
94
- if (parentDir === currentDir) break;
95
- currentDir = parentDir;
96
- }
97
-
98
- if (!nodeModulesPath) {
99
- try {
100
- const { execSync } = require('node:child_process');
101
- const npmRoot = execSync('npm root', {
102
- encoding: 'utf8',
103
- }).trim();
104
- if (fs.existsSync(npmRoot)) {
105
- nodeModulesPath = npmRoot;
106
- console.log(
107
- `Found node_modules at: ${nodeModulesPath} (method 2)`
108
- );
109
- }
110
- } catch (npmError) {
111
- console.error('Error executing npm root:', npmError);
112
- }
113
- }
114
-
115
- if (!nodeModulesPath) {
116
- currentDir = process.cwd();
117
- for (let i = 0; i < 5; i++) {
118
- const packageJsonPath = path.join(currentDir, 'package.json');
119
- if (fs.existsSync(packageJsonPath)) {
120
- const potentialNodeModules = path.join(
121
- currentDir,
122
- 'node_modules'
123
- );
124
- if (fs.existsSync(potentialNodeModules)) {
125
- nodeModulesPath = potentialNodeModules;
126
- console.log(
127
- `Found node_modules at: ${nodeModulesPath} (method 3)`
128
- );
129
- break;
130
- }
131
- }
132
- const parentDir = path.dirname(currentDir);
133
- if (parentDir === currentDir) break;
134
- currentDir = parentDir;
135
- }
136
- }
137
-
138
- if (nodeModulesPath) {
139
- return nodeModulesPath;
140
- }
141
-
142
- console.warn(
143
- 'Could not find node_modules path, falling back to default'
144
- );
145
- return path.resolve(process.cwd(), '../node_modules');
146
- } catch (error) {
147
- console.error('Error finding node_modules path:', error);
148
- return path.resolve(process.cwd(), '../node_modules');
149
- }
150
- };
151
-
152
- const modifyHandlerPaths = (functions) => {
153
- const isOffline = process.argv.includes('offline');
154
- console.log('isOffline', isOffline);
155
-
156
- if (!isOffline) {
157
- console.log('Not in offline mode, skipping handler path modification');
158
- return functions;
159
- }
160
-
161
- const nodeModulesPath = findNodeModulesPath();
162
- const modifiedFunctions = { ...functions };
163
-
164
- for (const functionName of Object.keys(modifiedFunctions)) {
165
- console.log('functionName', functionName);
166
- const functionDef = modifiedFunctions[functionName];
167
- if (functionDef?.handler?.includes('node_modules/')) {
168
- const relativePath = path.relative(process.cwd(), nodeModulesPath);
169
- functionDef.handler = functionDef.handler.replace(
170
- 'node_modules/',
171
- `${relativePath}/`
172
- );
173
- console.log(
174
- `Updated handler for ${functionName}: ${functionDef.handler}`
175
- );
176
- }
177
- }
178
-
179
- return modifiedFunctions;
180
- };
181
-
182
- const createVPCInfrastructure = (AppDefinition) => {
183
- const vpcResources = {
184
- FriggVPC: {
185
- Type: 'AWS::EC2::VPC',
186
- Properties: {
187
- CidrBlock: AppDefinition.vpc.cidrBlock || '10.0.0.0/16',
188
- EnableDnsHostnames: true,
189
- EnableDnsSupport: true,
190
- Tags: [
191
- {
192
- Key: 'Name',
193
- Value: '${self:service}-${self:provider.stage}-vpc',
194
- },
195
- { Key: 'ManagedBy', Value: 'Frigg' },
196
- { Key: 'Service', Value: '${self:service}' },
197
- { Key: 'Stage', Value: '${self:provider.stage}' },
198
- ],
199
- },
200
- },
201
- FriggInternetGateway: {
202
- Type: 'AWS::EC2::InternetGateway',
203
- Properties: {
204
- Tags: [
205
- {
206
- Key: 'Name',
207
- Value: '${self:service}-${self:provider.stage}-igw',
208
- },
209
- { Key: 'ManagedBy', Value: 'Frigg' },
210
- { Key: 'Service', Value: '${self:service}' },
211
- { Key: 'Stage', Value: '${self:provider.stage}' },
212
- ],
213
- },
214
- },
215
- FriggVPCGatewayAttachment: {
216
- Type: 'AWS::EC2::VPCGatewayAttachment',
217
- Properties: {
218
- VpcId: { Ref: 'FriggVPC' },
219
- InternetGatewayId: { Ref: 'FriggInternetGateway' },
220
- },
221
- },
222
- FriggPublicSubnet: {
223
- Type: 'AWS::EC2::Subnet',
224
- Properties: {
225
- VpcId: { Ref: 'FriggVPC' },
226
- CidrBlock: '10.0.1.0/24',
227
- AvailabilityZone: { 'Fn::Select': [0, { 'Fn::GetAZs': '' }] },
228
- MapPublicIpOnLaunch: true,
229
- Tags: [
230
- {
231
- Key: 'Name',
232
- Value: '${self:service}-${self:provider.stage}-public-subnet',
233
- },
234
- { Key: 'ManagedBy', Value: 'Frigg' },
235
- { Key: 'Service', Value: '${self:service}' },
236
- { Key: 'Stage', Value: '${self:provider.stage}' },
237
- { Key: 'Type', Value: 'Public' },
238
- ],
239
- },
240
- },
241
- FriggPrivateSubnet1: {
242
- Type: 'AWS::EC2::Subnet',
243
- Properties: {
244
- VpcId: { Ref: 'FriggVPC' },
245
- CidrBlock: '10.0.2.0/24',
246
- AvailabilityZone: { 'Fn::Select': [0, { 'Fn::GetAZs': '' }] },
247
- Tags: [
248
- {
249
- Key: 'Name',
250
- Value: '${self:service}-${self:provider.stage}-private-subnet-1',
251
- },
252
- { Key: 'ManagedBy', Value: 'Frigg' },
253
- { Key: 'Service', Value: '${self:service}' },
254
- { Key: 'Stage', Value: '${self:provider.stage}' },
255
- { Key: 'Type', Value: 'Private' },
256
- ],
257
- },
258
- },
259
- FriggPrivateSubnet2: {
260
- Type: 'AWS::EC2::Subnet',
261
- Properties: {
262
- VpcId: { Ref: 'FriggVPC' },
263
- CidrBlock: '10.0.3.0/24',
264
- AvailabilityZone: { 'Fn::Select': [1, { 'Fn::GetAZs': '' }] },
265
- Tags: [
266
- {
267
- Key: 'Name',
268
- Value: '${self:service}-${self:provider.stage}-private-subnet-2',
269
- },
270
- { Key: 'ManagedBy', Value: 'Frigg' },
271
- { Key: 'Service', Value: '${self:service}' },
272
- { Key: 'Stage', Value: '${self:provider.stage}' },
273
- { Key: 'Type', Value: 'Private' },
274
- ],
275
- },
276
- },
277
- FriggNATGatewayEIP: {
278
- Type: 'AWS::EC2::EIP',
279
- Properties: {
280
- Domain: 'vpc',
281
- Tags: [
282
- {
283
- Key: 'Name',
284
- Value: '${self:service}-${self:provider.stage}-nat-eip',
285
- },
286
- { Key: 'ManagedBy', Value: 'Frigg' },
287
- { Key: 'Service', Value: '${self:service}' },
288
- { Key: 'Stage', Value: '${self:provider.stage}' },
289
- ],
290
- },
291
- DependsOn: 'FriggVPCGatewayAttachment',
292
- },
293
- FriggNATGateway: {
294
- Type: 'AWS::EC2::NatGateway',
295
- Properties: {
296
- AllocationId: {
297
- 'Fn::GetAtt': ['FriggNATGatewayEIP', 'AllocationId'],
298
- },
299
- SubnetId: { Ref: 'FriggPublicSubnet' },
300
- Tags: [
301
- {
302
- Key: 'Name',
303
- Value: '${self:service}-${self:provider.stage}-nat-gateway',
304
- },
305
- { Key: 'ManagedBy', Value: 'Frigg' },
306
- { Key: 'Service', Value: '${self:service}' },
307
- { Key: 'Stage', Value: '${self:provider.stage}' },
308
- ],
309
- },
310
- },
311
- FriggPublicRouteTable: {
312
- Type: 'AWS::EC2::RouteTable',
313
- Properties: {
314
- VpcId: { Ref: 'FriggVPC' },
315
- Tags: [
316
- {
317
- Key: 'Name',
318
- Value: '${self:service}-${self:provider.stage}-public-rt',
319
- },
320
- { Key: 'ManagedBy', Value: 'Frigg' },
321
- { Key: 'Service', Value: '${self:service}' },
322
- { Key: 'Stage', Value: '${self:provider.stage}' },
323
- { Key: 'Type', Value: 'Public' },
324
- ],
325
- },
326
- },
327
- FriggPublicRoute: {
328
- Type: 'AWS::EC2::Route',
329
- Properties: {
330
- RouteTableId: { Ref: 'FriggPublicRouteTable' },
331
- DestinationCidrBlock: '0.0.0.0/0',
332
- GatewayId: { Ref: 'FriggInternetGateway' },
333
- },
334
- DependsOn: 'FriggVPCGatewayAttachment',
335
- },
336
- FriggPublicSubnetRouteTableAssociation: {
337
- Type: 'AWS::EC2::SubnetRouteTableAssociation',
338
- Properties: {
339
- SubnetId: { Ref: 'FriggPublicSubnet' },
340
- RouteTableId: { Ref: 'FriggPublicRouteTable' },
341
- },
342
- },
343
- FriggPrivateRouteTable: {
344
- Type: 'AWS::EC2::RouteTable',
345
- Properties: {
346
- VpcId: { Ref: 'FriggVPC' },
347
- Tags: [
348
- {
349
- Key: 'Name',
350
- Value: '${self:service}-${self:provider.stage}-private-rt',
351
- },
352
- { Key: 'ManagedBy', Value: 'Frigg' },
353
- { Key: 'Service', Value: '${self:service}' },
354
- { Key: 'Stage', Value: '${self:provider.stage}' },
355
- { Key: 'Type', Value: 'Private' },
356
- ],
357
- },
358
- },
359
- FriggPrivateRoute: {
360
- Type: 'AWS::EC2::Route',
361
- Properties: {
362
- RouteTableId: { Ref: 'FriggPrivateRouteTable' },
363
- DestinationCidrBlock: '0.0.0.0/0',
364
- NatGatewayId: { Ref: 'FriggNATGateway' },
365
- },
366
- },
367
- FriggPrivateSubnet1RouteTableAssociation: {
368
- Type: 'AWS::EC2::SubnetRouteTableAssociation',
369
- Properties: {
370
- SubnetId: { Ref: 'FriggPrivateSubnet1' },
371
- RouteTableId: { Ref: 'FriggPrivateRouteTable' },
372
- },
373
- },
374
- FriggPrivateSubnet2RouteTableAssociation: {
375
- Type: 'AWS::EC2::SubnetRouteTableAssociation',
376
- Properties: {
377
- SubnetId: { Ref: 'FriggPrivateSubnet2' },
378
- RouteTableId: { Ref: 'FriggPrivateRouteTable' },
379
- },
380
- },
381
- FriggLambdaSecurityGroup: {
382
- Type: 'AWS::EC2::SecurityGroup',
383
- Properties: {
384
- GroupDescription: 'Security group for Frigg Lambda functions',
385
- VpcId: { Ref: 'FriggVPC' },
386
- SecurityGroupEgress: [
387
- {
388
- IpProtocol: 'tcp',
389
- FromPort: 443,
390
- ToPort: 443,
391
- CidrIp: '0.0.0.0/0',
392
- Description: 'HTTPS outbound',
393
- },
394
- {
395
- IpProtocol: 'tcp',
396
- FromPort: 80,
397
- ToPort: 80,
398
- CidrIp: '0.0.0.0/0',
399
- Description: 'HTTP outbound',
400
- },
401
- {
402
- IpProtocol: 'tcp',
403
- FromPort: 53,
404
- ToPort: 53,
405
- CidrIp: '0.0.0.0/0',
406
- Description: 'DNS TCP',
407
- },
408
- {
409
- IpProtocol: 'udp',
410
- FromPort: 53,
411
- ToPort: 53,
412
- CidrIp: '0.0.0.0/0',
413
- Description: 'DNS UDP',
414
- },
415
- {
416
- IpProtocol: 'tcp',
417
- FromPort: 27017,
418
- ToPort: 27017,
419
- CidrIp: '0.0.0.0/0',
420
- Description: 'MongoDB outbound',
421
- },
422
- ],
423
- Tags: [
424
- {
425
- Key: 'Name',
426
- Value: '${self:service}-${self:provider.stage}-lambda-sg',
427
- },
428
- { Key: 'ManagedBy', Value: 'Frigg' },
429
- { Key: 'Service', Value: '${self:service}' },
430
- { Key: 'Stage', Value: '${self:provider.stage}' },
431
- ],
432
- },
433
- },
434
- };
435
-
436
- if (AppDefinition.vpc.enableVPCEndpoints !== false) {
437
- vpcResources.FriggS3VPCEndpoint = {
438
- Type: 'AWS::EC2::VPCEndpoint',
439
- Properties: {
440
- VpcId: { Ref: 'FriggVPC' },
441
- ServiceName: 'com.amazonaws.${self:provider.region}.s3',
442
- VpcEndpointType: 'Gateway',
443
- RouteTableIds: [{ Ref: 'FriggPrivateRouteTable' }],
444
- },
445
- };
446
-
447
- vpcResources.FriggDynamoDBVPCEndpoint = {
448
- Type: 'AWS::EC2::VPCEndpoint',
449
- Properties: {
450
- VpcId: { Ref: 'FriggVPC' },
451
- ServiceName: 'com.amazonaws.${self:provider.region}.dynamodb',
452
- VpcEndpointType: 'Gateway',
453
- RouteTableIds: [{ Ref: 'FriggPrivateRouteTable' }],
454
- },
455
- };
456
-
457
- if (AppDefinition.encryption?.fieldLevelEncryptionMethod === 'kms') {
458
- vpcResources.FriggKMSVPCEndpoint = {
459
- Type: 'AWS::EC2::VPCEndpoint',
460
- Properties: {
461
- VpcId: { Ref: 'FriggVPC' },
462
- ServiceName: 'com.amazonaws.${self:provider.region}.kms',
463
- VpcEndpointType: 'Interface',
464
- SubnetIds: [
465
- { Ref: 'FriggPrivateSubnet1' },
466
- { Ref: 'FriggPrivateSubnet2' },
467
- ],
468
- SecurityGroupIds: [
469
- { Ref: 'FriggVPCEndpointSecurityGroup' },
470
- ],
471
- PrivateDnsEnabled: true,
472
- },
473
- };
474
- }
475
-
476
- vpcResources.FriggSecretsManagerVPCEndpoint = {
477
- Type: 'AWS::EC2::VPCEndpoint',
478
- Properties: {
479
- VpcId: { Ref: 'FriggVPC' },
480
- ServiceName:
481
- 'com.amazonaws.${self:provider.region}.secretsmanager',
482
- VpcEndpointType: 'Interface',
483
- SubnetIds: [
484
- { Ref: 'FriggPrivateSubnet1' },
485
- { Ref: 'FriggPrivateSubnet2' },
486
- ],
487
- SecurityGroupIds: [{ Ref: 'FriggVPCEndpointSecurityGroup' }],
488
- PrivateDnsEnabled: true,
489
- },
490
- };
491
-
492
- vpcResources.FriggVPCEndpointSecurityGroup = {
493
- Type: 'AWS::EC2::SecurityGroup',
494
- Properties: {
495
- GroupDescription:
496
- 'Security group for Frigg VPC Endpoints - allows HTTPS from Lambda functions',
497
- VpcId: { Ref: 'FriggVPC' },
498
- SecurityGroupIngress: [
499
- {
500
- IpProtocol: 'tcp',
501
- FromPort: 443,
502
- ToPort: 443,
503
- SourceSecurityGroupId: {
504
- Ref: 'FriggLambdaSecurityGroup',
505
- },
506
- Description: 'HTTPS from Lambda security group',
507
- },
508
- {
509
- IpProtocol: 'tcp',
510
- FromPort: 443,
511
- ToPort: 443,
512
- CidrIp: AppDefinition.vpc.cidrBlock || '10.0.0.0/16',
513
- Description: 'HTTPS from VPC CIDR (fallback)',
514
- },
515
- ],
516
- Tags: [
517
- {
518
- Key: 'Name',
519
- Value: '${self:service}-${self:provider.stage}-vpc-endpoint-sg',
520
- },
521
- { Key: 'ManagedBy', Value: 'Frigg' },
522
- { Key: 'Service', Value: '${self:service}' },
523
- { Key: 'Stage', Value: '${self:provider.stage}' },
524
- { Key: 'Type', Value: 'VPCEndpoint' },
525
- {
526
- Key: 'Purpose',
527
- Value: 'Allow Lambda functions to access VPC endpoints',
528
- },
529
- ],
530
- },
531
- };
532
- }
533
-
534
- return vpcResources;
535
- };
536
-
537
- const gatherDiscoveredResources = async (AppDefinition) => {
538
- if (!shouldRunDiscovery(AppDefinition)) {
539
- return {};
540
- }
541
-
542
- console.log('🔍 Running AWS resource discovery for serverless template...');
543
- try {
544
- const region = process.env.AWS_REGION || 'us-east-1';
545
- const discovery = new AWSDiscovery(region);
546
- const config = {
547
- vpc: AppDefinition.vpc || {},
548
- encryption: AppDefinition.encryption || {},
549
- ssm: AppDefinition.ssm || {},
550
- };
551
-
552
- const discoveredResources = await discovery.discoverResources(config);
553
-
554
- console.log('✅ AWS discovery completed successfully!');
555
- if (discoveredResources.defaultVpcId) {
556
- console.log(` VPC: ${discoveredResources.defaultVpcId}`);
557
- }
558
- if (
559
- discoveredResources.privateSubnetId1 &&
560
- discoveredResources.privateSubnetId2
561
- ) {
562
- console.log(
563
- ` Subnets: ${discoveredResources.privateSubnetId1}, ${discoveredResources.privateSubnetId2}`
564
- );
565
- }
566
- if (discoveredResources.defaultSecurityGroupId) {
567
- console.log(
568
- ` Security Group: ${discoveredResources.defaultSecurityGroupId}`
569
- );
570
- }
571
- if (discoveredResources.defaultKmsKeyId) {
572
- console.log(` KMS Key: ${discoveredResources.defaultKmsKeyId}`);
573
- }
574
-
575
- return discoveredResources;
576
- } catch (error) {
577
- console.error('❌ AWS discovery failed:', error.message);
578
- throw new Error(`AWS discovery failed: ${error.message}`);
579
- }
580
- };
581
-
582
- const buildEnvironment = (appEnvironmentVars, discoveredResources) => {
583
- const environment = {
584
- STAGE: '${opt:stage, "dev"}',
585
- AWS_NODEJS_CONNECTION_REUSE_ENABLED: 1,
586
- ...appEnvironmentVars,
587
- };
588
-
589
- const discoveryEnvMapping = {
590
- defaultVpcId: 'AWS_DISCOVERY_VPC_ID',
591
- defaultSecurityGroupId: 'AWS_DISCOVERY_SECURITY_GROUP_ID',
592
- privateSubnetId1: 'AWS_DISCOVERY_SUBNET_ID_1',
593
- privateSubnetId2: 'AWS_DISCOVERY_SUBNET_ID_2',
594
- publicSubnetId: 'AWS_DISCOVERY_PUBLIC_SUBNET_ID',
595
- defaultRouteTableId: 'AWS_DISCOVERY_ROUTE_TABLE_ID',
596
- defaultKmsKeyId: 'AWS_DISCOVERY_KMS_KEY_ID',
597
- };
598
-
599
- for (const [key, envKey] of Object.entries(discoveryEnvMapping)) {
600
- if (discoveredResources[key]) {
601
- environment[envKey] = discoveredResources[key];
602
- }
603
- }
604
-
605
- return environment;
606
- };
607
-
608
- const createBaseDefinition = (
609
- AppDefinition,
610
- appEnvironmentVars,
611
- discoveredResources
612
- ) => {
613
- const region = process.env.AWS_REGION || 'us-east-1';
614
-
615
- return {
616
- frameworkVersion: '>=3.17.0',
617
- service: AppDefinition.name || 'create-frigg-app',
618
- package: {
619
- individually: true,
620
- exclude: [
621
- '!**/node_modules/aws-sdk/**',
622
- '!**/node_modules/@aws-sdk/**',
623
- '!package.json',
624
- ],
625
- },
626
- useDotenv: true,
627
- provider: {
628
- name: AppDefinition.provider || 'aws',
629
- runtime: 'nodejs20.x',
630
- timeout: 30,
631
- region,
632
- stage: '${opt:stage}',
633
- environment: buildEnvironment(
634
- appEnvironmentVars,
635
- discoveredResources
636
- ),
637
- iamRoleStatements: [
638
- {
639
- Effect: 'Allow',
640
- Action: ['sns:Publish'],
641
- Resource: { Ref: 'InternalErrorBridgeTopic' },
642
- },
643
- {
644
- Effect: 'Allow',
645
- Action: [
646
- 'sqs:SendMessage',
647
- 'sqs:SendMessageBatch',
648
- 'sqs:GetQueueUrl',
649
- 'sqs:GetQueueAttributes',
650
- ],
651
- Resource: [
652
- { 'Fn::GetAtt': ['InternalErrorQueue', 'Arn'] },
653
- {
654
- 'Fn::Join': [
655
- ':',
656
- [
657
- 'arn:aws:sqs:${self:provider.region}:*:${self:service}--${self:provider.stage}-*Queue',
658
- ],
659
- ],
660
- },
661
- ],
662
- },
663
- ],
664
- httpApi: {
665
- payload: '2.0',
666
- cors: {
667
- allowedOrigins: ['*'],
668
- allowedHeaders: ['*'],
669
- allowedMethods: ['*'],
670
- allowCredentials: false,
671
- },
672
- name: '${opt:stage, "dev"}-${self:service}',
673
- disableDefaultEndpoint: false,
674
- },
675
- },
676
- plugins: [
677
- // Temporarily disabled Jetpack - it ignores package.patterns in dependency mode
678
- // 'serverless-jetpack',
679
- 'serverless-dotenv-plugin',
680
- 'serverless-offline-sqs',
681
- 'serverless-offline',
682
- '@friggframework/serverless-plugin',
683
- ],
684
- custom: {
685
- 'serverless-offline': {
686
- httpPort: 3001,
687
- lambdaPort: 4001,
688
- websocketPort: 3002,
689
- },
690
- 'serverless-offline-sqs': {
691
- autoCreate: false,
692
- apiVersion: '2012-11-05',
693
- endpoint: 'http://localhost:4566',
694
- region,
695
- accessKeyId: 'root',
696
- secretAccessKey: 'root',
697
- skipCacheInvalidation: false,
698
- },
699
- // Jetpack config removed - testing with standard Serverless packaging
700
- // jetpack: {
701
- // base: '..',
702
- // },
703
- },
704
- functions: {
705
- auth: {
706
- handler:
707
- 'node_modules/@friggframework/core/handlers/routers/auth.handler',
708
- events: [
709
- { httpApi: { path: '/api/integrations', method: 'ANY' } },
710
- {
711
- httpApi: {
712
- path: '/api/integrations/{proxy+}',
713
- method: 'ANY',
714
- },
715
- },
716
- { httpApi: { path: '/api/authorize', method: 'ANY' } },
717
- ],
718
- },
719
- user: {
720
- handler:
721
- 'node_modules/@friggframework/core/handlers/routers/user.handler',
722
- events: [
723
- { httpApi: { path: '/user/{proxy+}', method: 'ANY' } },
724
- ],
725
- },
726
- health: {
727
- handler:
728
- 'node_modules/@friggframework/core/handlers/routers/health.handler',
729
- events: [
730
- { httpApi: { path: '/health', method: 'GET' } },
731
- { httpApi: { path: '/health/{proxy+}', method: 'GET' } },
732
- ],
733
- },
734
- },
735
- resources: {
736
- Resources: {
737
- InternalErrorQueue: {
738
- Type: 'AWS::SQS::Queue',
739
- Properties: {
740
- QueueName:
741
- '${self:service}-internal-error-queue-${self:provider.stage}',
742
- MessageRetentionPeriod: 300,
743
- },
744
- },
745
- InternalErrorBridgeTopic: {
746
- Type: 'AWS::SNS::Topic',
747
- Properties: {
748
- Subscription: [
749
- {
750
- Protocol: 'sqs',
751
- Endpoint: {
752
- 'Fn::GetAtt': ['InternalErrorQueue', 'Arn'],
753
- },
754
- },
755
- ],
756
- },
757
- },
758
- InternalErrorBridgePolicy: {
759
- Type: 'AWS::SQS::QueuePolicy',
760
- Properties: {
761
- Queues: [{ Ref: 'InternalErrorQueue' }],
762
- PolicyDocument: {
763
- Version: '2012-10-17',
764
- Statement: [
765
- {
766
- Sid: 'Allow Dead Letter SNS to publish to SQS',
767
- Effect: 'Allow',
768
- Principal: { Service: 'sns.amazonaws.com' },
769
- Resource: {
770
- 'Fn::GetAtt': [
771
- 'InternalErrorQueue',
772
- 'Arn',
773
- ],
774
- },
775
- Action: [
776
- 'SQS:SendMessage',
777
- 'SQS:SendMessageBatch',
778
- ],
779
- Condition: {
780
- ArnEquals: {
781
- 'aws:SourceArn': {
782
- Ref: 'InternalErrorBridgeTopic',
783
- },
784
- },
785
- },
786
- },
787
- ],
788
- },
789
- },
790
- },
791
- ApiGatewayAlarm5xx: {
792
- Type: 'AWS::CloudWatch::Alarm',
793
- Properties: {
794
- AlarmDescription: 'API Gateway 5xx Errors',
795
- Namespace: 'AWS/ApiGateway',
796
- MetricName: '5XXError',
797
- Statistic: 'Sum',
798
- Threshold: 0,
799
- ComparisonOperator: 'GreaterThanThreshold',
800
- EvaluationPeriods: 1,
801
- Period: 60,
802
- AlarmActions: [{ Ref: 'InternalErrorBridgeTopic' }],
803
- Dimensions: [
804
- { Name: 'ApiId', Value: { Ref: 'HttpApi' } },
805
- { Name: 'Stage', Value: '${self:provider.stage}' },
806
- ],
807
- },
808
- },
809
- },
810
- },
811
- };
812
- };
813
-
814
- const applyKmsConfiguration = (
815
- definition,
816
- AppDefinition,
817
- discoveredResources
818
- ) => {
819
- if (AppDefinition.encryption?.fieldLevelEncryptionMethod !== 'kms') {
820
- return;
821
- }
822
-
823
- // Skip KMS configuration for local development when AWS discovery is disabled
824
- if (process.env.FRIGG_SKIP_AWS_DISCOVERY === 'true') {
825
- console.log(
826
- '⚙️ Skipping KMS configuration for local development (FRIGG_SKIP_AWS_DISCOVERY is set)'
827
- );
828
- return;
829
- }
830
-
831
- if (discoveredResources.defaultKmsKeyId) {
832
- console.log(
833
- `Using existing KMS key: ${discoveredResources.defaultKmsKeyId}`
834
- );
835
- definition.resources.Resources.FriggKMSKeyAlias = {
836
- Type: 'AWS::KMS::Alias',
837
- DeletionPolicy: 'Retain',
838
- Properties: {
839
- AliasName:
840
- 'alias/${self:service}-${self:provider.stage}-frigg-kms',
841
- TargetKeyId: discoveredResources.defaultKmsKeyId,
842
- },
843
- };
844
-
845
- definition.provider.iamRoleStatements.push({
846
- Effect: 'Allow',
847
- Action: ['kms:GenerateDataKey', 'kms:Decrypt'],
848
- Resource: [discoveredResources.defaultKmsKeyId],
849
- });
850
- } else {
851
- if (AppDefinition.encryption?.createResourceIfNoneFound !== true) {
852
- throw new Error(
853
- 'KMS field-level encryption is enabled but no KMS key was found. ' +
854
- 'Either provide an existing KMS key or set encryption.createResourceIfNoneFound to true to create a new key.'
855
- );
856
- }
857
-
858
- console.log('No existing KMS key found, creating a new one...');
859
- definition.resources.Resources.FriggKMSKey = {
860
- Type: 'AWS::KMS::Key',
861
- DeletionPolicy: 'Retain',
862
- UpdateReplacePolicy: 'Retain',
863
- Properties: {
864
- EnableKeyRotation: true,
865
- Description: 'Frigg KMS key for field-level encryption',
866
- KeyPolicy: {
867
- Version: '2012-10-17',
868
- Statement: [
869
- {
870
- Sid: 'AllowRootAccountAdmin',
871
- Effect: 'Allow',
872
- Principal: {
873
- AWS: {
874
- 'Fn::Sub':
875
- 'arn:aws:iam::${AWS::AccountId}:root',
876
- },
877
- },
878
- Action: 'kms:*',
879
- Resource: '*',
880
- },
881
- {
882
- Sid: 'AllowLambdaService',
883
- Effect: 'Allow',
884
- Principal: { Service: 'lambda.amazonaws.com' },
885
- Action: [
886
- 'kms:GenerateDataKey',
887
- 'kms:Decrypt',
888
- 'kms:DescribeKey',
889
- ],
890
- Resource: '*',
891
- Condition: {
892
- StringEquals: {
893
- 'kms:ViaService': `lambda.${process.env.AWS_REGION || 'us-east-1'
894
- }.amazonaws.com`,
895
- },
896
- },
897
- },
898
- ],
899
- },
900
- Tags: [
901
- {
902
- Key: 'Name',
903
- Value: '${self:service}-${self:provider.stage}-frigg-kms-key',
904
- },
905
- { Key: 'ManagedBy', Value: 'Frigg' },
906
- {
907
- Key: 'Purpose',
908
- Value: 'Field-level encryption for Frigg application',
909
- },
910
- ],
911
- },
912
- };
913
-
914
- definition.resources.Resources.FriggKMSKeyAlias = {
915
- Type: 'AWS::KMS::Alias',
916
- DeletionPolicy: 'Retain',
917
- Properties: {
918
- AliasName:
919
- 'alias/${self:service}-${self:provider.stage}-frigg-kms',
920
- TargetKeyId: { 'Fn::GetAtt': ['FriggKMSKey', 'Arn'] },
921
- },
922
- };
923
-
924
- definition.provider.iamRoleStatements.push({
925
- Effect: 'Allow',
926
- Action: ['kms:GenerateDataKey', 'kms:Decrypt'],
927
- Resource: [{ 'Fn::GetAtt': ['FriggKMSKey', 'Arn'] }],
928
- });
929
-
930
- definition.provider.environment.KMS_KEY_ARN = {
931
- 'Fn::GetAtt': ['FriggKMSKey', 'Arn'],
932
- };
933
- definition.custom.kmsGrants = {
934
- kmsKeyId: { 'Fn::GetAtt': ['FriggKMSKey', 'Arn'] },
935
- };
936
- }
937
-
938
- definition.plugins.push('serverless-kms-grants');
939
- if (!definition.custom.kmsGrants) {
940
- definition.custom.kmsGrants = {
941
- kmsKeyId:
942
- discoveredResources.defaultKmsKeyId ||
943
- '${env:AWS_DISCOVERY_KMS_KEY_ID}',
944
- };
945
- }
946
-
947
- if (!definition.provider.environment.KMS_KEY_ARN) {
948
- definition.provider.environment.KMS_KEY_ARN =
949
- discoveredResources.defaultKmsKeyId ||
950
- '${env:AWS_DISCOVERY_KMS_KEY_ID}';
951
- }
952
- };
953
-
954
- const healVpcConfiguration = (discoveredResources, AppDefinition) => {
955
- const healingReport = {
956
- healed: [],
957
- warnings: [],
958
- errors: [],
959
- recommendations: [],
960
- criticalActions: [],
961
- };
962
-
963
- if (!AppDefinition.vpc?.selfHeal) {
964
- return healingReport;
965
- }
966
-
967
- console.log(
968
- '🔧 Self-healing mode enabled - checking for VPC misconfigurations...'
969
- );
970
-
971
- if (discoveredResources.natGatewayInPrivateSubnet) {
972
- healingReport.warnings.push(
973
- `NAT Gateway ${discoveredResources.natGatewayInPrivateSubnet} is in a private subnet`
974
- );
975
- healingReport.recommendations.push(
976
- 'NAT Gateway should be recreated in a public subnet for proper internet connectivity'
977
- );
978
- discoveredResources.needsNewNatGateway = true;
979
- healingReport.healed.push(
980
- 'Marked NAT Gateway for recreation in public subnet'
981
- );
982
- }
983
-
984
- if (discoveredResources.elasticIpAlreadyAssociated) {
985
- healingReport.warnings.push(
986
- `Elastic IP ${discoveredResources.existingElasticIp} is already associated`
987
- );
988
-
989
- if (discoveredResources.existingNatGatewayId) {
990
- healingReport.healed.push(
991
- 'Will reuse existing NAT Gateway instead of creating a new one'
992
- );
993
- discoveredResources.reuseExistingNatGateway = true;
994
- } else {
995
- healingReport.healed.push(
996
- 'Will allocate a new Elastic IP for NAT Gateway'
997
- );
998
- discoveredResources.allocateNewElasticIp = true;
999
- }
1000
- }
1001
-
1002
- if (
1003
- discoveredResources.privateSubnetsWithWrongRoutes &&
1004
- discoveredResources.privateSubnetsWithWrongRoutes.length > 0
1005
- ) {
1006
- healingReport.warnings.push(
1007
- `Found ${discoveredResources.privateSubnetsWithWrongRoutes.length} subnets that are PUBLIC but will be used for Lambda`
1008
- );
1009
- healingReport.healed.push(
1010
- 'Route tables will be corrected during deployment - converting public subnets to private'
1011
- );
1012
- healingReport.criticalActions.push(
1013
- 'SUBNET ISOLATION: Will create separate route tables to ensure Lambda subnets are private'
1014
- );
1015
- }
1016
-
1017
- if (discoveredResources.subnetConversionRequired) {
1018
- healingReport.warnings.push(
1019
- 'Subnet configuration mismatch detected - Lambda functions require private subnets'
1020
- );
1021
- healingReport.healed.push(
1022
- 'Will create proper route table configuration for subnet isolation'
1023
- );
1024
- }
1025
-
1026
- if (discoveredResources.orphanedElasticIps?.length > 0) {
1027
- healingReport.warnings.push(
1028
- `Found ${discoveredResources.orphanedElasticIps.length} orphaned Elastic IPs`
1029
- );
1030
- healingReport.recommendations.push(
1031
- 'Consider releasing orphaned Elastic IPs to avoid charges'
1032
- );
1033
- }
1034
-
1035
- if (healingReport.criticalActions.length > 0) {
1036
- console.log('🚨 CRITICAL ACTIONS:');
1037
- healingReport.criticalActions.forEach((action) =>
1038
- console.log(` - ${action}`)
1039
- );
1040
- }
1041
-
1042
- if (healingReport.healed.length > 0) {
1043
- console.log('✅ Self-healing actions:');
1044
- healingReport.healed.forEach((action) => console.log(` - ${action}`));
1045
- }
1046
-
1047
- if (healingReport.warnings.length > 0) {
1048
- console.log('⚠️ Issues detected:');
1049
- healingReport.warnings.forEach((warning) =>
1050
- console.log(` - ${warning}`)
1051
- );
1052
- }
1053
-
1054
- if (healingReport.recommendations.length > 0) {
1055
- console.log('💡 Recommendations:');
1056
- healingReport.recommendations.forEach((rec) =>
1057
- console.log(` - ${rec}`)
1058
- );
1059
- }
1060
-
1061
- return healingReport;
1062
- };
1063
-
1064
- const configureVpc = (definition, AppDefinition, discoveredResources) => {
1065
- if (AppDefinition.vpc?.enable !== true) {
1066
- return;
1067
- }
1068
-
1069
- // Skip VPC configuration for local development when AWS discovery is disabled
1070
- if (process.env.FRIGG_SKIP_AWS_DISCOVERY === 'true') {
1071
- console.log(
1072
- '⚙️ Skipping VPC configuration for local development (FRIGG_SKIP_AWS_DISCOVERY is set)'
1073
- );
1074
- return;
1075
- }
1076
-
1077
- definition.provider.iamRoleStatements.push({
1078
- Effect: 'Allow',
1079
- Action: [
1080
- 'ec2:CreateNetworkInterface',
1081
- 'ec2:DescribeNetworkInterfaces',
1082
- 'ec2:DeleteNetworkInterface',
1083
- 'ec2:AttachNetworkInterface',
1084
- 'ec2:DetachNetworkInterface',
1085
- ],
1086
- Resource: '*',
1087
- });
1088
-
1089
- if (Object.keys(discoveredResources).length > 0) {
1090
- const healingReport = healVpcConfiguration(
1091
- discoveredResources,
1092
- AppDefinition
1093
- );
1094
- if (healingReport.errors.length > 0 && !AppDefinition.vpc?.selfHeal) {
1095
- throw new Error(
1096
- `VPC configuration errors detected: ${healingReport.errors.join(
1097
- ', '
1098
- )}`
1099
- );
1100
- }
1101
- }
1102
-
1103
- const vpcManagement = AppDefinition.vpc.management || 'discover';
1104
- let vpcId = null;
1105
- const vpcConfig = {
1106
- securityGroupIds: [],
1107
- subnetIds: [],
1108
- };
1109
-
1110
- console.log(`VPC Management Mode: ${vpcManagement}`);
1111
-
1112
- if (vpcManagement === 'create-new') {
1113
- const vpcResources = createVPCInfrastructure(AppDefinition);
1114
- Object.assign(definition.resources.Resources, vpcResources);
1115
- vpcId = { Ref: 'FriggVPC' };
1116
- vpcConfig.securityGroupIds = AppDefinition.vpc.securityGroupIds || [
1117
- { Ref: 'FriggLambdaSecurityGroup' },
1118
- ];
1119
- } else if (vpcManagement === 'use-existing') {
1120
- if (!AppDefinition.vpc.vpcId) {
1121
- throw new Error(
1122
- 'VPC management is set to "use-existing" but no vpcId was provided'
1123
- );
1124
- }
1125
- vpcId = AppDefinition.vpc.vpcId;
1126
- vpcConfig.securityGroupIds =
1127
- AppDefinition.vpc.securityGroupIds ||
1128
- (discoveredResources.defaultSecurityGroupId
1129
- ? [discoveredResources.defaultSecurityGroupId]
1130
- : []);
1131
- } else {
1132
- if (!discoveredResources.defaultVpcId) {
1133
- throw new Error(
1134
- 'VPC discovery failed: No VPC found. Either set vpc.management to "create-new" or provide vpc.vpcId with "use-existing".'
1135
- );
1136
- }
1137
- vpcId = discoveredResources.defaultVpcId;
1138
- vpcConfig.securityGroupIds =
1139
- AppDefinition.vpc.securityGroupIds ||
1140
- (discoveredResources.defaultSecurityGroupId
1141
- ? [discoveredResources.defaultSecurityGroupId]
1142
- : []);
1143
- }
1144
-
1145
- const defaultSubnetManagement =
1146
- vpcManagement === 'create-new' ? 'create' : 'discover';
1147
- let subnetManagement =
1148
- AppDefinition.vpc.subnets?.management || defaultSubnetManagement;
1149
- console.log(`Subnet Management Mode: ${subnetManagement}`);
1150
-
1151
- const effectiveVpcId = vpcId || discoveredResources.defaultVpcId;
1152
- if (!effectiveVpcId) {
1153
- throw new Error('Cannot manage subnets without a VPC ID');
1154
- }
1155
-
1156
- if (subnetManagement === 'create') {
1157
- console.log('Creating new subnets...');
1158
- const subnetVpcId =
1159
- vpcManagement === 'create-new'
1160
- ? { Ref: 'FriggVPC' }
1161
- : effectiveVpcId;
1162
- let subnet1Cidr;
1163
- let subnet2Cidr;
1164
- let publicSubnetCidr;
1165
-
1166
- if (vpcManagement === 'create-new') {
1167
- const generatedCidrs = { 'Fn::Cidr': ['10.0.0.0/16', 3, 8] };
1168
- subnet1Cidr = { 'Fn::Select': [0, generatedCidrs] };
1169
- subnet2Cidr = { 'Fn::Select': [1, generatedCidrs] };
1170
- publicSubnetCidr = { 'Fn::Select': [2, generatedCidrs] };
1171
- } else {
1172
- subnet1Cidr = '172.31.240.0/24';
1173
- subnet2Cidr = '172.31.241.0/24';
1174
- publicSubnetCidr = '172.31.250.0/24';
1175
- }
1176
-
1177
- definition.resources.Resources.FriggPrivateSubnet1 = {
1178
- Type: 'AWS::EC2::Subnet',
1179
- Properties: {
1180
- VpcId: subnetVpcId,
1181
- CidrBlock: subnet1Cidr,
1182
- AvailabilityZone: { 'Fn::Select': [0, { 'Fn::GetAZs': '' }] },
1183
- Tags: [
1184
- {
1185
- Key: 'Name',
1186
- Value: '${self:service}-${self:provider.stage}-private-1',
1187
- },
1188
- { Key: 'Type', Value: 'Private' },
1189
- { Key: 'ManagedBy', Value: 'Frigg' },
1190
- ],
1191
- },
1192
- };
1193
-
1194
- definition.resources.Resources.FriggPrivateSubnet2 = {
1195
- Type: 'AWS::EC2::Subnet',
1196
- Properties: {
1197
- VpcId: subnetVpcId,
1198
- CidrBlock: subnet2Cidr,
1199
- AvailabilityZone: { 'Fn::Select': [1, { 'Fn::GetAZs': '' }] },
1200
- Tags: [
1201
- {
1202
- Key: 'Name',
1203
- Value: '${self:service}-${self:provider.stage}-private-2',
1204
- },
1205
- { Key: 'Type', Value: 'Private' },
1206
- { Key: 'ManagedBy', Value: 'Frigg' },
1207
- ],
1208
- },
1209
- };
1210
-
1211
- definition.resources.Resources.FriggPublicSubnet = {
1212
- Type: 'AWS::EC2::Subnet',
1213
- Properties: {
1214
- VpcId: subnetVpcId,
1215
- CidrBlock: publicSubnetCidr,
1216
- MapPublicIpOnLaunch: true,
1217
- AvailabilityZone: { 'Fn::Select': [0, { 'Fn::GetAZs': '' }] },
1218
- Tags: [
1219
- {
1220
- Key: 'Name',
1221
- Value: '${self:service}-${self:provider.stage}-public',
1222
- },
1223
- { Key: 'Type', Value: 'Public' },
1224
- { Key: 'ManagedBy', Value: 'Frigg' },
1225
- ],
1226
- },
1227
- };
1228
-
1229
- vpcConfig.subnetIds = [
1230
- { Ref: 'FriggPrivateSubnet1' },
1231
- { Ref: 'FriggPrivateSubnet2' },
1232
- ];
1233
-
1234
- if (
1235
- !AppDefinition.vpc.natGateway ||
1236
- AppDefinition.vpc.natGateway.management === 'discover'
1237
- ) {
1238
- if (
1239
- vpcManagement === 'create-new' ||
1240
- !discoveredResources.internetGatewayId
1241
- ) {
1242
- if (!definition.resources.Resources.FriggInternetGateway) {
1243
- definition.resources.Resources.FriggInternetGateway = {
1244
- Type: 'AWS::EC2::InternetGateway',
1245
- Properties: {
1246
- Tags: [
1247
- {
1248
- Key: 'Name',
1249
- Value: '${self:service}-${self:provider.stage}-igw',
1250
- },
1251
- { Key: 'ManagedBy', Value: 'Frigg' },
1252
- ],
1253
- },
1254
- };
1255
-
1256
- definition.resources.Resources.FriggIGWAttachment = {
1257
- Type: 'AWS::EC2::VPCGatewayAttachment',
1258
- Properties: {
1259
- VpcId: subnetVpcId,
1260
- InternetGatewayId: { Ref: 'FriggInternetGateway' },
1261
- },
1262
- };
1263
- }
1264
- }
1265
-
1266
- definition.resources.Resources.FriggPublicRouteTable = {
1267
- Type: 'AWS::EC2::RouteTable',
1268
- Properties: {
1269
- VpcId: subnetVpcId,
1270
- Tags: [
1271
- {
1272
- Key: 'Name',
1273
- Value: '${self:service}-${self:provider.stage}-public-rt',
1274
- },
1275
- { Key: 'ManagedBy', Value: 'Frigg' },
1276
- ],
1277
- },
1278
- };
1279
-
1280
- definition.resources.Resources.FriggPublicRoute = {
1281
- Type: 'AWS::EC2::Route',
1282
- DependsOn:
1283
- vpcManagement === 'create-new'
1284
- ? 'FriggIGWAttachment'
1285
- : undefined,
1286
- Properties: {
1287
- RouteTableId: { Ref: 'FriggPublicRouteTable' },
1288
- DestinationCidrBlock: '0.0.0.0/0',
1289
- GatewayId: discoveredResources.internetGatewayId || {
1290
- Ref: 'FriggInternetGateway',
1291
- },
1292
- },
1293
- };
1294
-
1295
- definition.resources.Resources.FriggPublicSubnetRouteTableAssociation =
1296
- {
1297
- Type: 'AWS::EC2::SubnetRouteTableAssociation',
1298
- Properties: {
1299
- SubnetId: { Ref: 'FriggPublicSubnet' },
1300
- RouteTableId: { Ref: 'FriggPublicRouteTable' },
1301
- },
1302
- };
1303
-
1304
- definition.resources.Resources.FriggLambdaRouteTable = {
1305
- Type: 'AWS::EC2::RouteTable',
1306
- Properties: {
1307
- VpcId: subnetVpcId,
1308
- Tags: [
1309
- {
1310
- Key: 'Name',
1311
- Value: '${self:service}-${self:provider.stage}-lambda-rt',
1312
- },
1313
- { Key: 'ManagedBy', Value: 'Frigg' },
1314
- ],
1315
- },
1316
- };
1317
-
1318
- definition.resources.Resources.FriggPrivateSubnet1RouteTableAssociation =
1319
- {
1320
- Type: 'AWS::EC2::SubnetRouteTableAssociation',
1321
- Properties: {
1322
- SubnetId: { Ref: 'FriggPrivateSubnet1' },
1323
- RouteTableId: { Ref: 'FriggLambdaRouteTable' },
1324
- },
1325
- };
1326
-
1327
- definition.resources.Resources.FriggPrivateSubnet2RouteTableAssociation =
1328
- {
1329
- Type: 'AWS::EC2::SubnetRouteTableAssociation',
1330
- Properties: {
1331
- SubnetId: { Ref: 'FriggPrivateSubnet2' },
1332
- RouteTableId: { Ref: 'FriggLambdaRouteTable' },
1333
- },
1334
- };
1335
- }
1336
- } else if (subnetManagement === 'use-existing') {
1337
- if (
1338
- !AppDefinition.vpc.subnets?.ids ||
1339
- AppDefinition.vpc.subnets.ids.length < 2
1340
- ) {
1341
- throw new Error(
1342
- 'Subnet management is "use-existing" but less than 2 subnet IDs provided. Provide at least 2 subnet IDs in vpc.subnets.ids.'
1343
- );
1344
- }
1345
- vpcConfig.subnetIds = AppDefinition.vpc.subnets.ids;
1346
- } else {
1347
- vpcConfig.subnetIds =
1348
- AppDefinition.vpc.subnets?.ids?.length > 0
1349
- ? AppDefinition.vpc.subnets.ids
1350
- : discoveredResources.privateSubnetId1 &&
1351
- discoveredResources.privateSubnetId2
1352
- ? [
1353
- discoveredResources.privateSubnetId1,
1354
- discoveredResources.privateSubnetId2,
1355
- ]
1356
- : [];
1357
-
1358
- if (vpcConfig.subnetIds.length < 2) {
1359
- if (AppDefinition.vpc.selfHeal) {
1360
- console.log(
1361
- 'No subnets found but self-heal enabled - creating minimal subnet setup'
1362
- );
1363
- subnetManagement = 'create';
1364
- discoveredResources.createSubnets = true;
1365
- } else {
1366
- throw new Error(
1367
- 'No subnets discovered and subnets.management is "discover". Either enable vpc.selfHeal, set subnets.management to "create", or provide subnet IDs.'
1368
- );
1369
- }
1370
- }
1371
- }
1372
-
1373
- if (subnetManagement === 'create' && discoveredResources.createSubnets) {
1374
- definition.resources.Resources.FriggLambdaRouteTable = definition
1375
- .resources.Resources.FriggLambdaRouteTable || {
1376
- Type: 'AWS::EC2::RouteTable',
1377
- Properties: {
1378
- VpcId: effectiveVpcId,
1379
- Tags: [
1380
- {
1381
- Key: 'Name',
1382
- Value: '${self:service}-${self:provider.stage}-lambda-rt',
1383
- },
1384
- { Key: 'ManagedBy', Value: 'Frigg' },
1385
- { Key: 'Environment', Value: '${self:provider.stage}' },
1386
- { Key: 'Service', Value: '${self:service}' },
1387
- ],
1388
- },
1389
- };
1390
- }
1391
-
1392
- if (
1393
- vpcConfig.subnetIds.length >= 2 &&
1394
- vpcConfig.securityGroupIds.length > 0
1395
- ) {
1396
- definition.provider.vpc = vpcConfig;
1397
-
1398
- const natGatewayManagement =
1399
- AppDefinition.vpc.natGateway?.management || 'discover';
1400
- let needsNewNatGateway =
1401
- natGatewayManagement === 'createAndManage' ||
1402
- discoveredResources.needsNewNatGateway === true;
1403
-
1404
- console.log('needsNewNatGateway', needsNewNatGateway);
1405
-
1406
- let reuseExistingNatGateway = false;
1407
- let useExistingEip = false;
1408
-
1409
- if (needsNewNatGateway) {
1410
- console.log(
1411
- 'Create mode: Creating dedicated EIP, public subnet, and NAT Gateway...'
1412
- );
1413
-
1414
- if (
1415
- discoveredResources.existingNatGatewayId &&
1416
- discoveredResources.existingElasticIpAllocationId
1417
- ) {
1418
- console.log('Found existing Frigg-managed NAT Gateway and EIP');
1419
- if (!discoveredResources.natGatewayInPrivateSubnet) {
1420
- console.log(
1421
- '✅ Existing NAT Gateway is in PUBLIC subnet, will reuse it'
1422
- );
1423
- reuseExistingNatGateway = true;
1424
- } else {
1425
- console.log(
1426
- '❌ NAT Gateway is in PRIVATE subnet - MUST create new one in PUBLIC subnet'
1427
- );
1428
- if (AppDefinition.vpc.selfHeal) {
1429
- console.log(
1430
- 'Self-heal enabled: Creating new NAT Gateway in PUBLIC subnet'
1431
- );
1432
- reuseExistingNatGateway = false;
1433
- useExistingEip = false;
1434
- discoveredResources.needsCleanup = true;
1435
- } else {
1436
- throw new Error(
1437
- 'CRITICAL: NAT Gateway is in PRIVATE subnet (will not work!). Enable vpc.selfHeal to auto-fix or set natGateway.management to "createAndManage".'
1438
- );
1439
- }
1440
- }
1441
- } else if (
1442
- discoveredResources.existingElasticIpAllocationId &&
1443
- !discoveredResources.existingNatGatewayId
1444
- ) {
1445
- console.log(
1446
- 'Found orphaned EIP, will reuse it for new NAT Gateway in PUBLIC subnet'
1447
- );
1448
- useExistingEip = true;
1449
- }
1450
-
1451
- if (reuseExistingNatGateway) {
1452
- console.log(
1453
- 'Reusing existing NAT Gateway - skipping resource creation'
1454
- );
1455
- } else {
1456
- if (!useExistingEip) {
1457
- definition.resources.Resources.FriggNATGatewayEIP = {
1458
- Type: 'AWS::EC2::EIP',
1459
- DeletionPolicy: 'Retain',
1460
- UpdateReplacePolicy: 'Retain',
1461
- Properties: {
1462
- Domain: 'vpc',
1463
- Tags: [
1464
- {
1465
- Key: 'Name',
1466
- Value: '${self:service}-${self:provider.stage}-nat-eip',
1467
- },
1468
- { Key: 'ManagedBy', Value: 'Frigg' },
1469
- { Key: 'Service', Value: '${self:service}' },
1470
- {
1471
- Key: 'Stage',
1472
- Value: '${self:provider.stage}',
1473
- },
1474
- ],
1475
- },
1476
- };
1477
- }
1478
-
1479
- if (!discoveredResources.publicSubnetId) {
1480
- if (discoveredResources.internetGatewayId) {
1481
- console.log(
1482
- 'Reusing existing Internet Gateway for NAT Gateway'
1483
- );
1484
- } else {
1485
- definition.resources.Resources.FriggInternetGateway =
1486
- definition.resources.Resources
1487
- .FriggInternetGateway || {
1488
- Type: 'AWS::EC2::InternetGateway',
1489
- Properties: {
1490
- Tags: [
1491
- {
1492
- Key: 'Name',
1493
- Value: '${self:service}-${self:provider.stage}-igw',
1494
- },
1495
- { Key: 'ManagedBy', Value: 'Frigg' },
1496
- ],
1497
- },
1498
- };
1499
-
1500
- definition.resources.Resources.FriggIGWAttachment =
1501
- definition.resources.Resources
1502
- .FriggIGWAttachment || {
1503
- Type: 'AWS::EC2::VPCGatewayAttachment',
1504
- Properties: {
1505
- VpcId: discoveredResources.defaultVpcId,
1506
- InternetGatewayId: {
1507
- Ref: 'FriggInternetGateway',
1508
- },
1509
- },
1510
- };
1511
- }
1512
-
1513
- definition.resources.Resources.FriggPublicSubnet = {
1514
- Type: 'AWS::EC2::Subnet',
1515
- Properties: {
1516
- VpcId: discoveredResources.defaultVpcId,
1517
- CidrBlock:
1518
- AppDefinition.vpc.natGateway
1519
- ?.publicSubnetCidr || '172.31.250.0/24',
1520
- AvailabilityZone: {
1521
- 'Fn::Select': [0, { 'Fn::GetAZs': '' }],
1522
- },
1523
- MapPublicIpOnLaunch: true,
1524
- Tags: [
1525
- {
1526
- Key: 'Name',
1527
- Value: '${self:service}-${self:provider.stage}-public-subnet',
1528
- },
1529
- { Key: 'Type', Value: 'Public' },
1530
- ],
1531
- },
1532
- };
1533
-
1534
- definition.resources.Resources.FriggPublicRouteTable = {
1535
- Type: 'AWS::EC2::RouteTable',
1536
- Properties: {
1537
- VpcId: discoveredResources.defaultVpcId,
1538
- Tags: [
1539
- {
1540
- Key: 'Name',
1541
- Value: '${self:service}-${self:provider.stage}-public-rt',
1542
- },
1543
- ],
1544
- },
1545
- };
1546
-
1547
- definition.resources.Resources.FriggPublicRoute = {
1548
- Type: 'AWS::EC2::Route',
1549
- DependsOn: discoveredResources.internetGatewayId
1550
- ? []
1551
- : 'FriggIGWAttachment',
1552
- Properties: {
1553
- RouteTableId: { Ref: 'FriggPublicRouteTable' },
1554
- DestinationCidrBlock: '0.0.0.0/0',
1555
- GatewayId:
1556
- discoveredResources.internetGatewayId || {
1557
- Ref: 'FriggInternetGateway',
1558
- },
1559
- },
1560
- };
1561
-
1562
- definition.resources.Resources.FriggPublicSubnetRouteTableAssociation =
1563
- {
1564
- Type: 'AWS::EC2::SubnetRouteTableAssociation',
1565
- Properties: {
1566
- SubnetId: { Ref: 'FriggPublicSubnet' },
1567
- RouteTableId: { Ref: 'FriggPublicRouteTable' },
1568
- },
1569
- };
1570
- }
1571
-
1572
- definition.resources.Resources.FriggNATGateway = {
1573
- Type: 'AWS::EC2::NatGateway',
1574
- DeletionPolicy: 'Retain',
1575
- UpdateReplacePolicy: 'Retain',
1576
- Properties: {
1577
- AllocationId: useExistingEip
1578
- ? discoveredResources.existingElasticIpAllocationId
1579
- : {
1580
- 'Fn::GetAtt': [
1581
- 'FriggNATGatewayEIP',
1582
- 'AllocationId',
1583
- ],
1584
- },
1585
- SubnetId: discoveredResources.publicSubnetId || {
1586
- Ref: 'FriggPublicSubnet',
1587
- },
1588
- Tags: [
1589
- {
1590
- Key: 'Name',
1591
- Value: '${self:service}-${self:provider.stage}-nat-gateway',
1592
- },
1593
- { Key: 'ManagedBy', Value: 'Frigg' },
1594
- { Key: 'Service', Value: '${self:service}' },
1595
- { Key: 'Stage', Value: '${self:provider.stage}' },
1596
- ],
1597
- },
1598
- };
1599
- }
1600
- } else if (
1601
- natGatewayManagement === 'discover' ||
1602
- natGatewayManagement === 'useExisting'
1603
- ) {
1604
- if (
1605
- natGatewayManagement === 'useExisting' &&
1606
- AppDefinition.vpc.natGateway?.id
1607
- ) {
1608
- console.log(
1609
- `Using explicitly provided NAT Gateway: ${AppDefinition.vpc.natGateway.id}`
1610
- );
1611
- discoveredResources.existingNatGatewayId =
1612
- AppDefinition.vpc.natGateway.id;
1613
- }
1614
-
1615
- if (discoveredResources.existingNatGatewayId) {
1616
- console.log(
1617
- 'discoveredResources.existingNatGatewayId',
1618
- discoveredResources.existingNatGatewayId
1619
- );
1620
-
1621
- if (discoveredResources.natGatewayInPrivateSubnet) {
1622
- console.log(
1623
- '❌ CRITICAL: NAT Gateway is in PRIVATE subnet - Internet connectivity will NOT work!'
1624
- );
1625
-
1626
- if (AppDefinition.vpc.selfHeal === true) {
1627
- console.log(
1628
- 'Self-heal enabled: Will create new NAT Gateway in PUBLIC subnet'
1629
- );
1630
- needsNewNatGateway = true;
1631
- discoveredResources.existingNatGatewayId = null;
1632
- if (!discoveredResources.publicSubnetId) {
1633
- console.log(
1634
- 'No public subnet found - will create one for NAT Gateway'
1635
- );
1636
- discoveredResources.createPublicSubnet = true;
1637
- }
1638
- } else {
1639
- throw new Error(
1640
- '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.'
1641
- );
1642
- }
1643
- } else {
1644
- console.log(
1645
- `Using discovered NAT Gateway for routing: ${discoveredResources.existingNatGatewayId}`
1646
- );
1647
- }
1648
- } else if (
1649
- !needsNewNatGateway &&
1650
- AppDefinition.vpc.natGateway?.id
1651
- ) {
1652
- console.log(
1653
- `Using explicitly provided NAT Gateway: ${AppDefinition.vpc.natGateway.id}`
1654
- );
1655
- discoveredResources.existingNatGatewayId =
1656
- AppDefinition.vpc.natGateway.id;
1657
- }
1658
- }
1659
-
1660
- definition.resources.Resources.FriggLambdaRouteTable = definition
1661
- .resources.Resources.FriggLambdaRouteTable || {
1662
- Type: 'AWS::EC2::RouteTable',
1663
- Properties: {
1664
- VpcId: discoveredResources.defaultVpcId || vpcId,
1665
- Tags: [
1666
- {
1667
- Key: 'Name',
1668
- Value: '${self:service}-${self:provider.stage}-lambda-rt',
1669
- },
1670
- { Key: 'ManagedBy', Value: 'Frigg' },
1671
- { Key: 'Environment', Value: '${self:provider.stage}' },
1672
- { Key: 'Service', Value: '${self:service}' },
1673
- ],
1674
- },
1675
- };
1676
-
1677
- const routeTableId = { Ref: 'FriggLambdaRouteTable' };
1678
- let natGatewayIdForRoute;
1679
-
1680
- if (reuseExistingNatGateway) {
1681
- natGatewayIdForRoute = discoveredResources.existingNatGatewayId;
1682
- console.log(
1683
- `Using discovered NAT Gateway for routing: ${natGatewayIdForRoute}`
1684
- );
1685
- } else if (needsNewNatGateway && !reuseExistingNatGateway) {
1686
- natGatewayIdForRoute = { Ref: 'FriggNATGateway' };
1687
- console.log('Using newly created NAT Gateway for routing');
1688
- } else if (discoveredResources.existingNatGatewayId) {
1689
- natGatewayIdForRoute = discoveredResources.existingNatGatewayId;
1690
- console.log(
1691
- `Using discovered NAT Gateway for routing: ${natGatewayIdForRoute}`
1692
- );
1693
- } else if (AppDefinition.vpc.natGateway?.id) {
1694
- natGatewayIdForRoute = AppDefinition.vpc.natGateway.id;
1695
- console.log(
1696
- `Using explicitly provided NAT Gateway for routing: ${natGatewayIdForRoute}`
1697
- );
1698
- } else if (AppDefinition.vpc.selfHeal === true) {
1699
- natGatewayIdForRoute = null;
1700
- console.log(
1701
- 'No NAT Gateway available - skipping NAT route creation'
1702
- );
1703
- } else {
1704
- throw new Error('No existing NAT Gateway found in discovery mode');
1705
- }
1706
-
1707
- if (natGatewayIdForRoute) {
1708
- console.log(
1709
- `Configuring NAT route: 0.0.0.0/0 → ${natGatewayIdForRoute}`
1710
- );
1711
- definition.resources.Resources.FriggNATRoute = {
1712
- Type: 'AWS::EC2::Route',
1713
- DependsOn: 'FriggLambdaRouteTable',
1714
- Properties: {
1715
- RouteTableId: routeTableId,
1716
- DestinationCidrBlock: '0.0.0.0/0',
1717
- NatGatewayId: natGatewayIdForRoute,
1718
- },
1719
- };
1720
- } else {
1721
- console.warn(
1722
- '⚠️ No NAT Gateway configured - Lambda functions will not have internet access'
1723
- );
1724
- }
1725
-
1726
- if (typeof vpcConfig.subnetIds[0] === 'string') {
1727
- definition.resources.Resources.FriggSubnet1RouteAssociation = {
1728
- Type: 'AWS::EC2::SubnetRouteTableAssociation',
1729
- Properties: {
1730
- SubnetId: vpcConfig.subnetIds[0],
1731
- RouteTableId: routeTableId,
1732
- },
1733
- DependsOn: 'FriggLambdaRouteTable',
1734
- };
1735
- }
1736
-
1737
- if (typeof vpcConfig.subnetIds[1] === 'string') {
1738
- definition.resources.Resources.FriggSubnet2RouteAssociation = {
1739
- Type: 'AWS::EC2::SubnetRouteTableAssociation',
1740
- Properties: {
1741
- SubnetId: vpcConfig.subnetIds[1],
1742
- RouteTableId: routeTableId,
1743
- },
1744
- DependsOn: 'FriggLambdaRouteTable',
1745
- };
1746
- }
1747
-
1748
- if (
1749
- typeof vpcConfig.subnetIds[0] === 'object' &&
1750
- vpcConfig.subnetIds[0].Ref
1751
- ) {
1752
- definition.resources.Resources.FriggNewSubnet1RouteAssociation = {
1753
- Type: 'AWS::EC2::SubnetRouteTableAssociation',
1754
- Properties: {
1755
- SubnetId: vpcConfig.subnetIds[0],
1756
- RouteTableId: routeTableId,
1757
- },
1758
- DependsOn: [
1759
- 'FriggLambdaRouteTable',
1760
- vpcConfig.subnetIds[0].Ref,
1761
- ],
1762
- };
1763
- }
1764
-
1765
- if (
1766
- typeof vpcConfig.subnetIds[1] === 'object' &&
1767
- vpcConfig.subnetIds[1].Ref
1768
- ) {
1769
- definition.resources.Resources.FriggNewSubnet2RouteAssociation = {
1770
- Type: 'AWS::EC2::SubnetRouteTableAssociation',
1771
- Properties: {
1772
- SubnetId: vpcConfig.subnetIds[1],
1773
- RouteTableId: routeTableId,
1774
- },
1775
- DependsOn: [
1776
- 'FriggLambdaRouteTable',
1777
- vpcConfig.subnetIds[1].Ref,
1778
- ],
1779
- };
1780
- }
1781
-
1782
- if (AppDefinition.vpc.enableVPCEndpoints !== false) {
1783
- definition.resources.Resources.VPCEndpointS3 = {
1784
- Type: 'AWS::EC2::VPCEndpoint',
1785
- Properties: {
1786
- VpcId: discoveredResources.defaultVpcId,
1787
- ServiceName: 'com.amazonaws.${self:provider.region}.s3',
1788
- VpcEndpointType: 'Gateway',
1789
- RouteTableIds: [routeTableId],
1790
- },
1791
- };
1792
-
1793
- definition.resources.Resources.VPCEndpointDynamoDB = {
1794
- Type: 'AWS::EC2::VPCEndpoint',
1795
- Properties: {
1796
- VpcId: discoveredResources.defaultVpcId,
1797
- ServiceName:
1798
- 'com.amazonaws.${self:provider.region}.dynamodb',
1799
- VpcEndpointType: 'Gateway',
1800
- RouteTableIds: [routeTableId],
1801
- },
1802
- };
1803
- }
1804
-
1805
- if (AppDefinition.encryption?.fieldLevelEncryptionMethod === 'kms') {
1806
- if (!discoveredResources.vpcCidr) {
1807
- console.warn(
1808
- '⚠️ Warning: VPC CIDR not discovered. VPC endpoint security group may not work correctly.'
1809
- );
1810
- }
1811
-
1812
- if (!definition.resources.Resources.VPCEndpointSecurityGroup) {
1813
- const vpcEndpointIngressRules = [];
1814
-
1815
- if (
1816
- vpcConfig.securityGroupIds &&
1817
- vpcConfig.securityGroupIds.length > 0
1818
- ) {
1819
- for (const sg of vpcConfig.securityGroupIds) {
1820
- if (typeof sg === 'string') {
1821
- vpcEndpointIngressRules.push({
1822
- IpProtocol: 'tcp',
1823
- FromPort: 443,
1824
- ToPort: 443,
1825
- SourceSecurityGroupId: sg,
1826
- Description: 'HTTPS from Lambda security group',
1827
- });
1828
- } else if (sg.Ref) {
1829
- vpcEndpointIngressRules.push({
1830
- IpProtocol: 'tcp',
1831
- FromPort: 443,
1832
- ToPort: 443,
1833
- SourceSecurityGroupId: { Ref: sg.Ref },
1834
- Description: 'HTTPS from Lambda security group',
1835
- });
1836
- }
1837
- }
1838
- }
1839
-
1840
- if (vpcEndpointIngressRules.length === 0) {
1841
- if (discoveredResources.vpcCidr) {
1842
- vpcEndpointIngressRules.push({
1843
- IpProtocol: 'tcp',
1844
- FromPort: 443,
1845
- ToPort: 443,
1846
- CidrIp: discoveredResources.vpcCidr,
1847
- Description: 'HTTPS from VPC CIDR (fallback)',
1848
- });
1849
- } else {
1850
- console.warn(
1851
- '⚠️ WARNING: No Lambda security group or VPC CIDR found. Using default private IP ranges.'
1852
- );
1853
- vpcEndpointIngressRules.push({
1854
- IpProtocol: 'tcp',
1855
- FromPort: 443,
1856
- ToPort: 443,
1857
- CidrIp: '172.31.0.0/16',
1858
- Description: 'HTTPS from default VPC range',
1859
- });
1860
- }
1861
- }
1862
-
1863
- definition.resources.Resources.VPCEndpointSecurityGroup = {
1864
- Type: 'AWS::EC2::SecurityGroup',
1865
- Properties: {
1866
- GroupDescription:
1867
- 'Security group for VPC endpoints - allows HTTPS from Lambda functions',
1868
- VpcId: discoveredResources.defaultVpcId,
1869
- SecurityGroupIngress: vpcEndpointIngressRules,
1870
- Tags: [
1871
- {
1872
- Key: 'Name',
1873
- Value: '${self:service}-${self:provider.stage}-vpc-endpoints-sg',
1874
- },
1875
- { Key: 'ManagedBy', Value: 'Frigg' },
1876
- {
1877
- Key: 'Purpose',
1878
- Value: 'Allow Lambda functions to access VPC endpoints',
1879
- },
1880
- ],
1881
- },
1882
- };
1883
- }
1884
-
1885
- definition.resources.Resources.VPCEndpointKMS = {
1886
- Type: 'AWS::EC2::VPCEndpoint',
1887
- Properties: {
1888
- VpcId: discoveredResources.defaultVpcId,
1889
- ServiceName: 'com.amazonaws.${self:provider.region}.kms',
1890
- VpcEndpointType: 'Interface',
1891
- SubnetIds: vpcConfig.subnetIds,
1892
- SecurityGroupIds: [{ Ref: 'VPCEndpointSecurityGroup' }],
1893
- PrivateDnsEnabled: true,
1894
- },
1895
- };
1896
-
1897
- if (AppDefinition.secretsManager?.enable === true) {
1898
- definition.resources.Resources.VPCEndpointSecretsManager = {
1899
- Type: 'AWS::EC2::VPCEndpoint',
1900
- Properties: {
1901
- VpcId: discoveredResources.defaultVpcId,
1902
- ServiceName:
1903
- 'com.amazonaws.${self:provider.region}.secretsmanager',
1904
- VpcEndpointType: 'Interface',
1905
- SubnetIds: vpcConfig.subnetIds,
1906
- SecurityGroupIds: [{ Ref: 'VPCEndpointSecurityGroup' }],
1907
- PrivateDnsEnabled: true,
1908
- },
1909
- };
1910
- }
1911
- }
1912
- }
1913
- };
1914
-
1915
- const configureSsm = (definition, AppDefinition) => {
1916
- if (AppDefinition.ssm?.enable !== true) {
1917
- return;
1918
- }
1919
-
1920
- definition.provider.layers = [
1921
- 'arn:aws:lambda:${self:provider.region}:177933569100:layer:AWS-Parameters-and-Secrets-Lambda-Extension:11',
1922
- ];
1923
-
1924
- definition.provider.iamRoleStatements.push({
1925
- Effect: 'Allow',
1926
- Action: [
1927
- 'ssm:GetParameter',
1928
- 'ssm:GetParameters',
1929
- 'ssm:GetParametersByPath',
1930
- ],
1931
- Resource: [
1932
- 'arn:aws:ssm:${self:provider.region}:*:parameter/${self:service}/${self:provider.stage}/*',
1933
- ],
1934
- });
1935
-
1936
- definition.provider.environment.SSM_PARAMETER_PREFIX =
1937
- '/${self:service}/${self:provider.stage}';
1938
- };
1939
-
1940
- const attachIntegrations = (definition, AppDefinition) => {
1941
- if (
1942
- !Array.isArray(AppDefinition.integrations) ||
1943
- AppDefinition.integrations.length === 0
1944
- ) {
1945
- return;
1946
- }
1947
-
1948
- console.log(
1949
- `Processing ${AppDefinition.integrations.length} integrations...`
1950
- );
1951
-
1952
- for (const integration of AppDefinition.integrations) {
1953
- if (!integration?.Definition?.name) {
1954
- throw new Error('Invalid integration: missing Definition or name');
1955
- }
1956
-
1957
- const integrationName = integration.Definition.name;
1958
- const queueReference = `${integrationName.charAt(0).toUpperCase() + integrationName.slice(1)
1959
- }Queue`;
1960
- const queueName = `\${self:service}--\${self:provider.stage}-${queueReference}`;
1961
-
1962
- definition.functions[integrationName] = {
1963
- handler: `node_modules/@friggframework/core/handlers/routers/integration-defined-routers.handlers.${integrationName}.handler`,
1964
- events: [
1965
- {
1966
- httpApi: {
1967
- path: `/api/${integrationName}-integration/{proxy+}`,
1968
- method: 'ANY',
1969
- },
1970
- },
1971
- ],
1972
- };
1973
-
1974
- definition.resources.Resources[queueReference] = {
1975
- Type: 'AWS::SQS::Queue',
1976
- Properties: {
1977
- QueueName: `\${self:custom.${queueReference}}`,
1978
- MessageRetentionPeriod: 60,
1979
- VisibilityTimeout: 1800,
1980
- RedrivePolicy: {
1981
- maxReceiveCount: 1,
1982
- deadLetterTargetArn: {
1983
- 'Fn::GetAtt': ['InternalErrorQueue', 'Arn'],
1984
- },
1985
- },
1986
- },
1987
- };
1988
-
1989
- const queueWorkerName = `${integrationName}QueueWorker`;
1990
- definition.functions[queueWorkerName] = {
1991
- handler: `node_modules/@friggframework/core/handlers/workers/integration-defined-workers.handlers.${integrationName}.queueWorker`,
1992
- reservedConcurrency: 5,
1993
- events: [
1994
- {
1995
- sqs: {
1996
- arn: { 'Fn::GetAtt': [queueReference, 'Arn'] },
1997
- batchSize: 1,
1998
- },
1999
- },
2000
- ],
2001
- timeout: 600,
2002
- };
2003
-
2004
- definition.provider.environment = {
2005
- ...definition.provider.environment,
2006
- [`${integrationName.toUpperCase()}_QUEUE_URL`]: {
2007
- Ref: queueReference,
2008
- },
2009
- };
2010
-
2011
- definition.custom[queueReference] = queueName;
2012
-
2013
- // Add webhook handler if enabled
2014
- const webhookConfig = integration.Definition.webhooks;
2015
- if (webhookConfig && (webhookConfig === true || webhookConfig.enabled === true)) {
2016
- const webhookFunctionName = `${integrationName}Webhook`;
2017
-
2018
- definition.functions[webhookFunctionName] = {
2019
- handler: `node_modules/@friggframework/core/handlers/routers/integration-webhook-routers.handlers.${integrationName}Webhook.handler`,
2020
- events: [
2021
- {
2022
- httpApi: {
2023
- path: `/api/${integrationName}-integration/webhooks`,
2024
- method: 'POST',
2025
- },
2026
- },
2027
- {
2028
- httpApi: {
2029
- path: `/api/${integrationName}-integration/webhooks/{integrationId}`,
2030
- method: 'POST',
2031
- },
2032
- },
2033
- ],
2034
- };
2035
- }
2036
- }
2037
- };
2038
-
2039
- const configureWebsockets = (definition, AppDefinition) => {
2040
- if (AppDefinition.websockets?.enable !== true) {
2041
- return;
2042
- }
2043
-
2044
- definition.functions.defaultWebsocket = {
2045
- handler:
2046
- 'node_modules/@friggframework/core/handlers/routers/websocket.handler',
2047
- events: [
2048
- { websocket: { route: '$connect' } },
2049
- { websocket: { route: '$default' } },
2050
- { websocket: { route: '$disconnect' } },
2051
- ],
2052
- };
2053
- };
2054
-
2055
- const composeServerlessDefinition = async (AppDefinition) => {
2056
- console.log('composeServerlessDefinition', AppDefinition);
2057
-
2058
- const discoveredResources = await gatherDiscoveredResources(AppDefinition);
2059
- const appEnvironmentVars = getAppEnvironmentVars(AppDefinition);
2060
- const definition = createBaseDefinition(
2061
- AppDefinition,
2062
- appEnvironmentVars,
2063
- discoveredResources
2064
- );
2065
-
2066
- // Check if we're in local build mode (AWS discovery was skipped)
2067
- const isLocalBuild = !shouldRunDiscovery(AppDefinition);
2068
-
2069
- if (isLocalBuild) {
2070
- console.log(
2071
- '🏠 Local build mode detected - skipping AWS-dependent configurations'
2072
- );
2073
- }
2074
-
2075
- // Apply configurations (skip AWS-dependent ones in local build mode)
2076
- if (!isLocalBuild) {
2077
- applyKmsConfiguration(definition, AppDefinition, discoveredResources);
2078
- configureVpc(definition, AppDefinition, discoveredResources);
2079
- configureSsm(definition, AppDefinition);
2080
- } else {
2081
- console.log(
2082
- ' ⏭️ Skipping: KMS, VPC, PostgreSQL, SSM configurations'
2083
- );
2084
- }
2085
-
2086
- attachIntegrations(definition, AppDefinition);
2087
- configureWebsockets(definition, AppDefinition);
2088
-
2089
- definition.functions = modifyHandlerPaths(definition.functions);
2090
-
2091
- return definition;
2092
- };
2093
-
2094
- module.exports = { composeServerlessDefinition };