@friggframework/devtools 2.0.0-next.6 → 2.0.0-next.61

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 (357) hide show
  1. package/frigg-cli/README.md +1289 -0
  2. package/frigg-cli/__tests__/unit/commands/build.test.js +279 -0
  3. package/frigg-cli/__tests__/unit/commands/db-setup.test.js +649 -0
  4. package/frigg-cli/__tests__/unit/commands/deploy.test.js +320 -0
  5. package/frigg-cli/__tests__/unit/commands/doctor.test.js +309 -0
  6. package/frigg-cli/__tests__/unit/commands/install.test.js +400 -0
  7. package/frigg-cli/__tests__/unit/commands/ui.test.js +346 -0
  8. package/frigg-cli/__tests__/unit/dependencies.test.js +74 -0
  9. package/frigg-cli/__tests__/unit/utils/database-validator.test.js +397 -0
  10. package/frigg-cli/__tests__/unit/utils/error-messages.test.js +345 -0
  11. package/frigg-cli/__tests__/unit/version-detection.test.js +171 -0
  12. package/frigg-cli/__tests__/utils/mock-factory.js +270 -0
  13. package/frigg-cli/__tests__/utils/prisma-mock.js +194 -0
  14. package/frigg-cli/__tests__/utils/test-fixtures.js +463 -0
  15. package/frigg-cli/__tests__/utils/test-setup.js +287 -0
  16. package/frigg-cli/build-command/index.js +53 -14
  17. package/frigg-cli/db-setup-command/index.js +246 -0
  18. package/frigg-cli/deploy-command/SPEC-DEPLOY-DRY-RUN.md +981 -0
  19. package/frigg-cli/deploy-command/index.js +295 -17
  20. package/frigg-cli/doctor-command/index.js +335 -0
  21. package/frigg-cli/generate-command/__tests__/generate-command.test.js +301 -0
  22. package/frigg-cli/generate-command/azure-generator.js +43 -0
  23. package/frigg-cli/generate-command/gcp-generator.js +47 -0
  24. package/frigg-cli/generate-command/index.js +332 -0
  25. package/frigg-cli/generate-command/terraform-generator.js +555 -0
  26. package/frigg-cli/generate-iam-command.js +118 -0
  27. package/frigg-cli/index.js +142 -1
  28. package/frigg-cli/index.test.js +1 -4
  29. package/frigg-cli/init-command/backend-first-handler.js +756 -0
  30. package/frigg-cli/init-command/index.js +93 -0
  31. package/frigg-cli/init-command/template-handler.js +143 -0
  32. package/frigg-cli/install-command/index.js +1 -4
  33. package/frigg-cli/jest.config.js +124 -0
  34. package/frigg-cli/package.json +63 -0
  35. package/frigg-cli/repair-command/index.js +564 -0
  36. package/frigg-cli/start-command/index.js +125 -6
  37. package/frigg-cli/start-command/start-command.test.js +297 -0
  38. package/frigg-cli/test/init-command.test.js +180 -0
  39. package/frigg-cli/test/npm-registry.test.js +319 -0
  40. package/frigg-cli/ui-command/index.js +154 -0
  41. package/frigg-cli/utils/app-resolver.js +319 -0
  42. package/frigg-cli/utils/backend-path.js +16 -17
  43. package/frigg-cli/utils/database-validator.js +167 -0
  44. package/frigg-cli/utils/error-messages.js +329 -0
  45. package/frigg-cli/utils/npm-registry.js +167 -0
  46. package/frigg-cli/utils/process-manager.js +199 -0
  47. package/frigg-cli/utils/repo-detection.js +405 -0
  48. package/infrastructure/ARCHITECTURE.md +487 -0
  49. package/infrastructure/CLAUDE.md +481 -0
  50. package/infrastructure/HEALTH.md +468 -0
  51. package/infrastructure/README.md +522 -0
  52. package/infrastructure/__tests__/fixtures/mock-aws-resources.js +391 -0
  53. package/infrastructure/__tests__/helpers/test-utils.js +277 -0
  54. package/infrastructure/__tests__/postgres-config.test.js +914 -0
  55. package/infrastructure/__tests__/template-generation.test.js +687 -0
  56. package/infrastructure/create-frigg-infrastructure.js +129 -20
  57. package/infrastructure/docs/POSTGRES-CONFIGURATION.md +630 -0
  58. package/infrastructure/docs/PRE-DEPLOYMENT-HEALTH-CHECK-SPEC.md +1317 -0
  59. package/infrastructure/docs/WEBSOCKET-CONFIGURATION.md +105 -0
  60. package/infrastructure/docs/deployment-instructions.md +268 -0
  61. package/infrastructure/docs/generate-iam-command.md +278 -0
  62. package/infrastructure/docs/iam-policy-templates.md +193 -0
  63. package/infrastructure/domains/database/aurora-builder.js +809 -0
  64. package/infrastructure/domains/database/aurora-builder.test.js +950 -0
  65. package/infrastructure/domains/database/aurora-discovery.js +87 -0
  66. package/infrastructure/domains/database/aurora-discovery.test.js +188 -0
  67. package/infrastructure/domains/database/aurora-resolver.js +210 -0
  68. package/infrastructure/domains/database/aurora-resolver.test.js +347 -0
  69. package/infrastructure/domains/database/migration-builder.js +701 -0
  70. package/infrastructure/domains/database/migration-builder.test.js +321 -0
  71. package/infrastructure/domains/database/migration-resolver.js +163 -0
  72. package/infrastructure/domains/database/migration-resolver.test.js +337 -0
  73. package/infrastructure/domains/health/application/ports/IPropertyReconciler.js +164 -0
  74. package/infrastructure/domains/health/application/ports/IResourceDetector.js +129 -0
  75. package/infrastructure/domains/health/application/ports/IResourceImporter.js +142 -0
  76. package/infrastructure/domains/health/application/ports/IStackRepository.js +131 -0
  77. package/infrastructure/domains/health/application/ports/index.js +26 -0
  78. package/infrastructure/domains/health/application/use-cases/__tests__/execute-resource-import-use-case.test.js +679 -0
  79. package/infrastructure/domains/health/application/use-cases/__tests__/mismatch-analyzer-method-name.test.js +167 -0
  80. package/infrastructure/domains/health/application/use-cases/__tests__/repair-via-import-use-case.test.js +1130 -0
  81. package/infrastructure/domains/health/application/use-cases/execute-resource-import-use-case.js +221 -0
  82. package/infrastructure/domains/health/application/use-cases/reconcile-properties-use-case.js +152 -0
  83. package/infrastructure/domains/health/application/use-cases/reconcile-properties-use-case.test.js +343 -0
  84. package/infrastructure/domains/health/application/use-cases/repair-via-import-use-case.js +535 -0
  85. package/infrastructure/domains/health/application/use-cases/repair-via-import-use-case.test.js +376 -0
  86. package/infrastructure/domains/health/application/use-cases/run-health-check-use-case.js +213 -0
  87. package/infrastructure/domains/health/application/use-cases/run-health-check-use-case.test.js +441 -0
  88. package/infrastructure/domains/health/docs/ACME-DEV-DRIFT-ANALYSIS.md +267 -0
  89. package/infrastructure/domains/health/docs/BUILD-VS-DEPLOYED-TEMPLATE-ANALYSIS.md +324 -0
  90. package/infrastructure/domains/health/docs/ORPHAN-DETECTION-ANALYSIS.md +386 -0
  91. package/infrastructure/domains/health/docs/SPEC-CLEANUP-COMMAND.md +1419 -0
  92. package/infrastructure/domains/health/docs/TDD-IMPLEMENTATION-SUMMARY.md +391 -0
  93. package/infrastructure/domains/health/docs/TEMPLATE-COMPARISON-IMPLEMENTATION.md +551 -0
  94. package/infrastructure/domains/health/domain/entities/issue.js +299 -0
  95. package/infrastructure/domains/health/domain/entities/issue.test.js +528 -0
  96. package/infrastructure/domains/health/domain/entities/property-mismatch.js +108 -0
  97. package/infrastructure/domains/health/domain/entities/property-mismatch.test.js +275 -0
  98. package/infrastructure/domains/health/domain/entities/resource.js +159 -0
  99. package/infrastructure/domains/health/domain/entities/resource.test.js +432 -0
  100. package/infrastructure/domains/health/domain/entities/stack-health-report.js +306 -0
  101. package/infrastructure/domains/health/domain/entities/stack-health-report.test.js +601 -0
  102. package/infrastructure/domains/health/domain/services/__tests__/health-score-percentage-based.test.js +380 -0
  103. package/infrastructure/domains/health/domain/services/__tests__/import-progress-monitor.test.js +971 -0
  104. package/infrastructure/domains/health/domain/services/__tests__/import-template-generator.test.js +1150 -0
  105. package/infrastructure/domains/health/domain/services/__tests__/logical-id-mapper.test.js +672 -0
  106. package/infrastructure/domains/health/domain/services/__tests__/template-parser.test.js +496 -0
  107. package/infrastructure/domains/health/domain/services/__tests__/update-progress-monitor.test.js +419 -0
  108. package/infrastructure/domains/health/domain/services/health-score-calculator.js +248 -0
  109. package/infrastructure/domains/health/domain/services/health-score-calculator.test.js +504 -0
  110. package/infrastructure/domains/health/domain/services/import-progress-monitor.js +195 -0
  111. package/infrastructure/domains/health/domain/services/import-template-generator.js +435 -0
  112. package/infrastructure/domains/health/domain/services/logical-id-mapper.js +345 -0
  113. package/infrastructure/domains/health/domain/services/mismatch-analyzer.js +234 -0
  114. package/infrastructure/domains/health/domain/services/mismatch-analyzer.test.js +431 -0
  115. package/infrastructure/domains/health/domain/services/property-mutability-config.js +382 -0
  116. package/infrastructure/domains/health/domain/services/template-parser.js +245 -0
  117. package/infrastructure/domains/health/domain/services/update-progress-monitor.js +192 -0
  118. package/infrastructure/domains/health/domain/value-objects/health-score.js +138 -0
  119. package/infrastructure/domains/health/domain/value-objects/health-score.test.js +267 -0
  120. package/infrastructure/domains/health/domain/value-objects/property-mutability.js +161 -0
  121. package/infrastructure/domains/health/domain/value-objects/property-mutability.test.js +198 -0
  122. package/infrastructure/domains/health/domain/value-objects/resource-state.js +167 -0
  123. package/infrastructure/domains/health/domain/value-objects/resource-state.test.js +196 -0
  124. package/infrastructure/domains/health/domain/value-objects/stack-identifier.js +192 -0
  125. package/infrastructure/domains/health/domain/value-objects/stack-identifier.test.js +262 -0
  126. package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-cfn-tagged.test.js +312 -0
  127. package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-multi-stack.test.js +367 -0
  128. package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-relationship-analysis.test.js +432 -0
  129. package/infrastructure/domains/health/infrastructure/adapters/aws-property-reconciler.js +784 -0
  130. package/infrastructure/domains/health/infrastructure/adapters/aws-property-reconciler.test.js +1133 -0
  131. package/infrastructure/domains/health/infrastructure/adapters/aws-resource-detector.js +565 -0
  132. package/infrastructure/domains/health/infrastructure/adapters/aws-resource-detector.test.js +554 -0
  133. package/infrastructure/domains/health/infrastructure/adapters/aws-resource-importer.js +318 -0
  134. package/infrastructure/domains/health/infrastructure/adapters/aws-resource-importer.test.js +398 -0
  135. package/infrastructure/domains/health/infrastructure/adapters/aws-stack-repository.js +777 -0
  136. package/infrastructure/domains/health/infrastructure/adapters/aws-stack-repository.test.js +580 -0
  137. package/infrastructure/domains/integration/integration-builder.js +404 -0
  138. package/infrastructure/domains/integration/integration-builder.test.js +690 -0
  139. package/infrastructure/domains/integration/integration-resolver.js +170 -0
  140. package/infrastructure/domains/integration/integration-resolver.test.js +369 -0
  141. package/infrastructure/domains/integration/websocket-builder.js +69 -0
  142. package/infrastructure/domains/integration/websocket-builder.test.js +195 -0
  143. package/infrastructure/domains/networking/vpc-builder.js +2051 -0
  144. package/infrastructure/domains/networking/vpc-builder.test.js +1960 -0
  145. package/infrastructure/domains/networking/vpc-discovery.js +177 -0
  146. package/infrastructure/domains/networking/vpc-discovery.test.js +350 -0
  147. package/infrastructure/domains/networking/vpc-resolver.js +505 -0
  148. package/infrastructure/domains/networking/vpc-resolver.test.js +801 -0
  149. package/infrastructure/domains/parameters/ssm-builder.js +79 -0
  150. package/infrastructure/domains/parameters/ssm-builder.test.js +189 -0
  151. package/infrastructure/domains/parameters/ssm-discovery.js +84 -0
  152. package/infrastructure/domains/parameters/ssm-discovery.test.js +210 -0
  153. package/infrastructure/domains/security/iam-generator.js +816 -0
  154. package/infrastructure/domains/security/iam-generator.test.js +204 -0
  155. package/infrastructure/domains/security/kms-builder.js +415 -0
  156. package/infrastructure/domains/security/kms-builder.test.js +392 -0
  157. package/infrastructure/domains/security/kms-discovery.js +80 -0
  158. package/infrastructure/domains/security/kms-discovery.test.js +177 -0
  159. package/infrastructure/domains/security/kms-resolver.js +96 -0
  160. package/infrastructure/domains/security/kms-resolver.test.js +216 -0
  161. package/infrastructure/domains/security/templates/frigg-deployment-iam-stack.yaml +401 -0
  162. package/infrastructure/domains/security/templates/iam-policy-basic.json +218 -0
  163. package/infrastructure/domains/security/templates/iam-policy-full.json +288 -0
  164. package/infrastructure/domains/shared/base-builder.js +112 -0
  165. package/infrastructure/domains/shared/base-resolver.js +186 -0
  166. package/infrastructure/domains/shared/base-resolver.test.js +305 -0
  167. package/infrastructure/domains/shared/builder-orchestrator.js +212 -0
  168. package/infrastructure/domains/shared/builder-orchestrator.test.js +213 -0
  169. package/infrastructure/domains/shared/cloudformation-discovery-v2.js +334 -0
  170. package/infrastructure/domains/shared/cloudformation-discovery.js +672 -0
  171. package/infrastructure/domains/shared/cloudformation-discovery.test.js +985 -0
  172. package/infrastructure/domains/shared/environment-builder.js +119 -0
  173. package/infrastructure/domains/shared/environment-builder.test.js +247 -0
  174. package/infrastructure/domains/shared/providers/aws-provider-adapter.js +579 -0
  175. package/infrastructure/domains/shared/providers/aws-provider-adapter.test.js +416 -0
  176. package/infrastructure/domains/shared/providers/azure-provider-adapter.stub.js +93 -0
  177. package/infrastructure/domains/shared/providers/cloud-provider-adapter.js +136 -0
  178. package/infrastructure/domains/shared/providers/gcp-provider-adapter.stub.js +82 -0
  179. package/infrastructure/domains/shared/providers/provider-factory.js +108 -0
  180. package/infrastructure/domains/shared/providers/provider-factory.test.js +170 -0
  181. package/infrastructure/domains/shared/resource-discovery.enhanced.test.js +306 -0
  182. package/infrastructure/domains/shared/resource-discovery.js +233 -0
  183. package/infrastructure/domains/shared/resource-discovery.test.js +588 -0
  184. package/infrastructure/domains/shared/types/app-definition.js +205 -0
  185. package/infrastructure/domains/shared/types/discovery-result.js +106 -0
  186. package/infrastructure/domains/shared/types/discovery-result.test.js +258 -0
  187. package/infrastructure/domains/shared/types/index.js +46 -0
  188. package/infrastructure/domains/shared/types/resource-ownership.js +108 -0
  189. package/infrastructure/domains/shared/types/resource-ownership.test.js +101 -0
  190. package/infrastructure/domains/shared/utilities/base-definition-factory.js +394 -0
  191. package/infrastructure/domains/shared/utilities/base-definition-factory.js.bak +338 -0
  192. package/infrastructure/domains/shared/utilities/base-definition-factory.test.js +291 -0
  193. package/infrastructure/domains/shared/utilities/handler-path-resolver.js +134 -0
  194. package/infrastructure/domains/shared/utilities/handler-path-resolver.test.js +268 -0
  195. package/infrastructure/domains/shared/utilities/prisma-layer-manager.js +159 -0
  196. package/infrastructure/domains/shared/utilities/prisma-layer-manager.test.js +444 -0
  197. package/infrastructure/domains/shared/validation/env-validator.js +78 -0
  198. package/infrastructure/domains/shared/validation/env-validator.test.js +173 -0
  199. package/infrastructure/domains/shared/validation/plugin-validator.js +187 -0
  200. package/infrastructure/domains/shared/validation/plugin-validator.test.js +323 -0
  201. package/infrastructure/esbuild.config.js +53 -0
  202. package/infrastructure/infrastructure-composer.js +117 -0
  203. package/infrastructure/infrastructure-composer.test.js +1895 -0
  204. package/infrastructure/integration.test.js +383 -0
  205. package/infrastructure/scripts/build-prisma-layer.js +701 -0
  206. package/infrastructure/scripts/build-prisma-layer.test.js +170 -0
  207. package/infrastructure/scripts/build-time-discovery.js +238 -0
  208. package/infrastructure/scripts/build-time-discovery.test.js +379 -0
  209. package/infrastructure/scripts/run-discovery.js +110 -0
  210. package/infrastructure/scripts/verify-prisma-layer.js +72 -0
  211. package/layers/prisma/.build-complete +3 -0
  212. package/layers/prisma/nodejs/package.json +8 -0
  213. package/management-ui/.eslintrc.js +22 -0
  214. package/management-ui/README.md +203 -0
  215. package/management-ui/components.json +21 -0
  216. package/management-ui/docs/phase2-integration-guide.md +320 -0
  217. package/management-ui/index.html +13 -0
  218. package/management-ui/package.json +76 -0
  219. package/management-ui/packages/devtools/frigg-cli/ui-command/index.js +302 -0
  220. package/management-ui/postcss.config.js +6 -0
  221. package/management-ui/server/api/backend.js +256 -0
  222. package/management-ui/server/api/cli.js +315 -0
  223. package/management-ui/server/api/codegen.js +663 -0
  224. package/management-ui/server/api/connections.js +857 -0
  225. package/management-ui/server/api/discovery.js +185 -0
  226. package/management-ui/server/api/environment/index.js +1 -0
  227. package/management-ui/server/api/environment/router.js +378 -0
  228. package/management-ui/server/api/environment.js +328 -0
  229. package/management-ui/server/api/integrations.js +876 -0
  230. package/management-ui/server/api/logs.js +248 -0
  231. package/management-ui/server/api/monitoring.js +282 -0
  232. package/management-ui/server/api/open-ide.js +31 -0
  233. package/management-ui/server/api/project.js +1029 -0
  234. package/management-ui/server/api/users/sessions.js +371 -0
  235. package/management-ui/server/api/users/simulation.js +254 -0
  236. package/management-ui/server/api/users.js +362 -0
  237. package/management-ui/server/api-contract.md +275 -0
  238. package/management-ui/server/index.js +873 -0
  239. package/management-ui/server/middleware/errorHandler.js +93 -0
  240. package/management-ui/server/middleware/security.js +32 -0
  241. package/management-ui/server/processManager.js +296 -0
  242. package/management-ui/server/server.js +346 -0
  243. package/management-ui/server/services/aws-monitor.js +413 -0
  244. package/management-ui/server/services/npm-registry.js +347 -0
  245. package/management-ui/server/services/template-engine.js +538 -0
  246. package/management-ui/server/utils/cliIntegration.js +220 -0
  247. package/management-ui/server/utils/environment/auditLogger.js +471 -0
  248. package/management-ui/server/utils/environment/awsParameterStore.js +275 -0
  249. package/management-ui/server/utils/environment/encryption.js +278 -0
  250. package/management-ui/server/utils/environment/envFileManager.js +286 -0
  251. package/management-ui/server/utils/import-commonjs.js +28 -0
  252. package/management-ui/server/utils/response.js +83 -0
  253. package/management-ui/server/websocket/handler.js +325 -0
  254. package/management-ui/src/App.jsx +25 -0
  255. package/management-ui/src/assets/FriggLogo.svg +1 -0
  256. package/management-ui/src/components/AppRouter.jsx +65 -0
  257. package/management-ui/src/components/Button.jsx +70 -0
  258. package/management-ui/src/components/Card.jsx +97 -0
  259. package/management-ui/src/components/EnvironmentCompare.jsx +400 -0
  260. package/management-ui/src/components/EnvironmentEditor.jsx +372 -0
  261. package/management-ui/src/components/EnvironmentImportExport.jsx +469 -0
  262. package/management-ui/src/components/EnvironmentSchema.jsx +491 -0
  263. package/management-ui/src/components/EnvironmentSecurity.jsx +463 -0
  264. package/management-ui/src/components/ErrorBoundary.jsx +73 -0
  265. package/management-ui/src/components/IntegrationCard.jsx +481 -0
  266. package/management-ui/src/components/IntegrationCardEnhanced.jsx +770 -0
  267. package/management-ui/src/components/IntegrationExplorer.jsx +379 -0
  268. package/management-ui/src/components/IntegrationStatus.jsx +336 -0
  269. package/management-ui/src/components/Layout.jsx +716 -0
  270. package/management-ui/src/components/LoadingSpinner.jsx +113 -0
  271. package/management-ui/src/components/RepositoryPicker.jsx +248 -0
  272. package/management-ui/src/components/SessionMonitor.jsx +350 -0
  273. package/management-ui/src/components/StatusBadge.jsx +208 -0
  274. package/management-ui/src/components/UserContextSwitcher.jsx +212 -0
  275. package/management-ui/src/components/UserSimulation.jsx +327 -0
  276. package/management-ui/src/components/Welcome.jsx +434 -0
  277. package/management-ui/src/components/codegen/APIEndpointGenerator.jsx +637 -0
  278. package/management-ui/src/components/codegen/APIModuleSelector.jsx +227 -0
  279. package/management-ui/src/components/codegen/CodeGenerationWizard.jsx +247 -0
  280. package/management-ui/src/components/codegen/CodePreviewEditor.jsx +316 -0
  281. package/management-ui/src/components/codegen/DynamicModuleForm.jsx +271 -0
  282. package/management-ui/src/components/codegen/FormBuilder.jsx +737 -0
  283. package/management-ui/src/components/codegen/IntegrationGenerator.jsx +855 -0
  284. package/management-ui/src/components/codegen/ProjectScaffoldWizard.jsx +797 -0
  285. package/management-ui/src/components/codegen/SchemaBuilder.jsx +303 -0
  286. package/management-ui/src/components/codegen/TemplateSelector.jsx +586 -0
  287. package/management-ui/src/components/codegen/index.js +10 -0
  288. package/management-ui/src/components/connections/ConnectionConfigForm.jsx +362 -0
  289. package/management-ui/src/components/connections/ConnectionHealthMonitor.jsx +182 -0
  290. package/management-ui/src/components/connections/ConnectionTester.jsx +200 -0
  291. package/management-ui/src/components/connections/EntityRelationshipMapper.jsx +292 -0
  292. package/management-ui/src/components/connections/OAuthFlow.jsx +204 -0
  293. package/management-ui/src/components/connections/index.js +5 -0
  294. package/management-ui/src/components/index.js +21 -0
  295. package/management-ui/src/components/monitoring/APIGatewayMetrics.jsx +222 -0
  296. package/management-ui/src/components/monitoring/LambdaMetrics.jsx +169 -0
  297. package/management-ui/src/components/monitoring/MetricsChart.jsx +197 -0
  298. package/management-ui/src/components/monitoring/MonitoringDashboard.jsx +393 -0
  299. package/management-ui/src/components/monitoring/SQSMetrics.jsx +246 -0
  300. package/management-ui/src/components/monitoring/index.js +6 -0
  301. package/management-ui/src/components/monitoring/monitoring.css +218 -0
  302. package/management-ui/src/components/theme-provider.jsx +52 -0
  303. package/management-ui/src/components/theme-toggle.jsx +39 -0
  304. package/management-ui/src/components/ui/badge.tsx +36 -0
  305. package/management-ui/src/components/ui/button.test.jsx +56 -0
  306. package/management-ui/src/components/ui/button.tsx +57 -0
  307. package/management-ui/src/components/ui/card.tsx +76 -0
  308. package/management-ui/src/components/ui/dropdown-menu.tsx +199 -0
  309. package/management-ui/src/components/ui/select.tsx +157 -0
  310. package/management-ui/src/components/ui/skeleton.jsx +15 -0
  311. package/management-ui/src/hooks/useFrigg.jsx +387 -0
  312. package/management-ui/src/hooks/useSocket.jsx +58 -0
  313. package/management-ui/src/index.css +193 -0
  314. package/management-ui/src/lib/utils.ts +6 -0
  315. package/management-ui/src/main.jsx +10 -0
  316. package/management-ui/src/pages/CodeGeneration.jsx +14 -0
  317. package/management-ui/src/pages/Connections.jsx +252 -0
  318. package/management-ui/src/pages/ConnectionsEnhanced.jsx +633 -0
  319. package/management-ui/src/pages/Dashboard.jsx +311 -0
  320. package/management-ui/src/pages/Environment.jsx +314 -0
  321. package/management-ui/src/pages/IntegrationConfigure.jsx +669 -0
  322. package/management-ui/src/pages/IntegrationDiscovery.jsx +567 -0
  323. package/management-ui/src/pages/IntegrationTest.jsx +742 -0
  324. package/management-ui/src/pages/Integrations.jsx +253 -0
  325. package/management-ui/src/pages/Monitoring.jsx +17 -0
  326. package/management-ui/src/pages/Simulation.jsx +155 -0
  327. package/management-ui/src/pages/Users.jsx +492 -0
  328. package/management-ui/src/services/api.js +41 -0
  329. package/management-ui/src/services/apiModuleService.js +193 -0
  330. package/management-ui/src/services/websocket-handlers.js +120 -0
  331. package/management-ui/src/test/api/project.test.js +273 -0
  332. package/management-ui/src/test/components/Welcome.test.jsx +378 -0
  333. package/management-ui/src/test/mocks/server.js +178 -0
  334. package/management-ui/src/test/setup.js +61 -0
  335. package/management-ui/src/test/utils/test-utils.jsx +134 -0
  336. package/management-ui/src/utils/repository.js +98 -0
  337. package/management-ui/src/utils/repository.test.js +118 -0
  338. package/management-ui/src/workflows/phase2-integration-workflows.js +884 -0
  339. package/management-ui/tailwind.config.js +63 -0
  340. package/management-ui/tsconfig.json +37 -0
  341. package/management-ui/tsconfig.node.json +10 -0
  342. package/management-ui/vite.config.js +26 -0
  343. package/management-ui/vitest.config.js +38 -0
  344. package/package.json +35 -14
  345. package/test/index.js +2 -4
  346. package/test/mock-integration.js +4 -14
  347. package/infrastructure/app-handler-helpers.js +0 -57
  348. package/infrastructure/backend-utils.js +0 -87
  349. package/infrastructure/routers/auth.js +0 -26
  350. package/infrastructure/routers/integration-defined-routers.js +0 -42
  351. package/infrastructure/routers/middleware/loadUser.js +0 -15
  352. package/infrastructure/routers/middleware/requireLoggedInUser.js +0 -12
  353. package/infrastructure/routers/user.js +0 -41
  354. package/infrastructure/routers/websocket.js +0 -55
  355. package/infrastructure/serverless-template.js +0 -291
  356. package/infrastructure/workers/integration-defined-workers.js +0 -24
  357. package/test/auther-definition-tester.js +0 -125
@@ -0,0 +1,2051 @@
1
+ /**
2
+ * VPC Infrastructure Builder
3
+ *
4
+ * Domain Layer - Hexagonal Architecture
5
+ *
6
+ * Responsible for building VPC infrastructure including:
7
+ * - VPC creation or discovery
8
+ * - Subnet management (public/private)
9
+ * - Security groups for Lambda functions
10
+ * - NAT Gateways for private subnet internet access
11
+ * - VPC Endpoints (S3, DynamoDB, KMS, Secrets Manager)
12
+ * - Route tables and routing configuration
13
+ * - Self-healing VPC misconfigurations
14
+ *
15
+ * Supports three management modes:
16
+ * 1. create-new: Creates complete VPC infrastructure from scratch
17
+ * 2. use-existing: Uses explicitly provided VPC/subnet IDs
18
+ * 3. discover (default): Discovers and uses existing AWS resources
19
+ */
20
+
21
+ const { InfrastructureBuilder, ValidationResult } = require('../shared/base-builder');
22
+ const VpcResourceResolver = require('./vpc-resolver');
23
+ const { createEmptyDiscoveryResult } = require('../shared/types/discovery-result');
24
+ const { ResourceOwnership } = require('../shared/types/resource-ownership');
25
+
26
+ class VpcBuilder extends InfrastructureBuilder {
27
+ constructor() {
28
+ super();
29
+ this.name = 'VpcBuilder';
30
+ }
31
+
32
+ shouldExecute(appDefinition) {
33
+ // Skip VPC in local mode (when FRIGG_SKIP_AWS_DISCOVERY is set)
34
+ // VPC is an AWS-specific service that should only be created in production
35
+ if (process.env.FRIGG_SKIP_AWS_DISCOVERY === 'true') {
36
+ return false;
37
+ }
38
+
39
+ return appDefinition.vpc?.enable === true;
40
+ }
41
+
42
+ validate(appDefinition) {
43
+ const result = new ValidationResult();
44
+
45
+ if (!appDefinition.vpc) {
46
+ result.addError('VPC configuration is missing');
47
+ return result;
48
+ }
49
+
50
+ const vpc = appDefinition.vpc;
51
+
52
+ // Validate management mode
53
+ const validModes = ['discover', 'create-new', 'use-existing'];
54
+ const management = vpc.management || 'discover';
55
+ if (!validModes.includes(management)) {
56
+ result.addError(`Invalid vpc.management: "${management}". Must be one of: ${validModes.join(', ')}`);
57
+ }
58
+
59
+ // Validate use-existing mode requirements
60
+ if (management === 'use-existing') {
61
+ if (!vpc.vpcId) {
62
+ result.addError('vpc.vpcId is required when management="use-existing"');
63
+ }
64
+ if (!vpc.securityGroupIds || vpc.securityGroupIds.length === 0) {
65
+ result.addWarning('vpc.securityGroupIds not provided - will attempt discovery');
66
+ }
67
+ }
68
+
69
+ // Validate CIDR block format
70
+ if (vpc.cidrBlock) {
71
+ const cidrPattern = /^([0-9]{1,3}\.){3}[0-9]{1,3}\/[0-9]{1,2}$/;
72
+ if (!cidrPattern.test(vpc.cidrBlock)) {
73
+ result.addError(`Invalid CIDR block format: ${vpc.cidrBlock}`);
74
+ }
75
+ }
76
+
77
+ // Validate subnet configuration
78
+ if (vpc.subnets?.management === 'use-existing') {
79
+ if (!vpc.subnets.ids || vpc.subnets.ids.length < 2) {
80
+ result.addError('At least 2 subnet IDs required when subnets.management="use-existing"');
81
+ }
82
+ }
83
+
84
+ return result;
85
+ }
86
+
87
+ /**
88
+ * Warn about ignored options when managementMode='managed'
89
+ */
90
+ warnIgnoredOptions(appDefinition) {
91
+ const ignoredOptions = [];
92
+ if (appDefinition.vpc?.management) ignoredOptions.push('vpc.management');
93
+ if (appDefinition.vpc?.subnets?.management) ignoredOptions.push('vpc.subnets.management');
94
+ if (appDefinition.vpc?.natGateway?.management) ignoredOptions.push('vpc.natGateway.management');
95
+ if (appDefinition.vpc?.shareAcrossStages !== undefined) ignoredOptions.push('vpc.shareAcrossStages');
96
+
97
+ if (ignoredOptions.length > 0) {
98
+ console.log(` ⚠️ managementMode='managed' ignoring: ${ignoredOptions.join(', ')}`);
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Convert flat discovery result to structured discovery result
104
+ * Provides backwards compatibility for tests using old discovery format
105
+ *
106
+ * @param {Object} flatDiscovery - Flat discovery object
107
+ * @param {Object} appDefinition - App definition (used to detect stack-managed resources)
108
+ */
109
+ convertFlatDiscoveryToStructured(flatDiscovery, appDefinition = {}) {
110
+ const discovery = createEmptyDiscoveryResult();
111
+
112
+ if (!flatDiscovery) {
113
+ return discovery;
114
+ }
115
+
116
+ // Special case: managementMode='managed' + vpcIsolation='isolated' with existing resources
117
+ // These resources are from a previous deployment of this stack, so they're stack-managed
118
+ const isManagedIsolated = appDefinition.managementMode === 'managed' &&
119
+ (appDefinition.vpcIsolation === 'isolated' || !appDefinition.vpcIsolation);
120
+ const hasExistingStackResources = isManagedIsolated && flatDiscovery.defaultVpcId &&
121
+ typeof flatDiscovery.defaultVpcId === 'string';
122
+
123
+ // Check if this came from CloudFormation stack
124
+ if (flatDiscovery.fromCloudFormationStack || hasExistingStackResources) {
125
+ discovery.fromCloudFormation = true;
126
+ discovery.stackName = flatDiscovery.stackName || 'assumed-stack';
127
+
128
+ // Add resources to stackManaged array
129
+ let existingLogicalIds = flatDiscovery.existingLogicalIds || [];
130
+
131
+ // If hasExistingStackResources but no existingLogicalIds provided,
132
+ // infer logical IDs from presence of physical IDs
133
+ if (hasExistingStackResources && existingLogicalIds.length === 0) {
134
+ existingLogicalIds = [];
135
+ if (flatDiscovery.defaultVpcId) existingLogicalIds.push('FriggVPC');
136
+ if (flatDiscovery.privateSubnetId1) existingLogicalIds.push('FriggPrivateSubnet1');
137
+ if (flatDiscovery.privateSubnetId2) existingLogicalIds.push('FriggPrivateSubnet2');
138
+ if (flatDiscovery.publicSubnetId1) existingLogicalIds.push('FriggPublicSubnet');
139
+ if (flatDiscovery.publicSubnetId2) existingLogicalIds.push('FriggPublicSubnet2');
140
+ }
141
+
142
+ existingLogicalIds.forEach(logicalId => {
143
+ // Find the resource type and physical ID
144
+ let resourceType = '';
145
+ let physicalId = '';
146
+
147
+ if (logicalId === 'FriggVPC') {
148
+ resourceType = 'AWS::EC2::VPC';
149
+ physicalId = flatDiscovery.defaultVpcId;
150
+ } else if (logicalId === 'FriggLambdaSecurityGroup') {
151
+ resourceType = 'AWS::EC2::SecurityGroup';
152
+ physicalId = flatDiscovery.lambdaSecurityGroupId || flatDiscovery.defaultSecurityGroupId || flatDiscovery.securityGroupId;
153
+ } else if (logicalId === 'FriggPrivateSubnet1') {
154
+ resourceType = 'AWS::EC2::Subnet';
155
+ physicalId = flatDiscovery.privateSubnetId1;
156
+ } else if (logicalId === 'FriggPrivateSubnet2') {
157
+ resourceType = 'AWS::EC2::Subnet';
158
+ physicalId = flatDiscovery.privateSubnetId2;
159
+ } else if (logicalId === 'FriggNATGateway') {
160
+ resourceType = 'AWS::EC2::NatGateway';
161
+ physicalId = flatDiscovery.existingNatGatewayId;
162
+ } else if (logicalId === 'FriggLambdaRouteTable') {
163
+ resourceType = 'AWS::EC2::RouteTable';
164
+ physicalId = flatDiscovery.routeTableId;
165
+ } else if (logicalId === 'FriggS3VPCEndpoint' || logicalId === 'VPCEndpointS3') {
166
+ resourceType = 'AWS::EC2::VPCEndpoint';
167
+ physicalId = flatDiscovery.s3VpcEndpointId;
168
+ } else if (logicalId === 'FriggDynamoDBVPCEndpoint' || logicalId === 'VPCEndpointDynamoDB') {
169
+ resourceType = 'AWS::EC2::VPCEndpoint';
170
+ physicalId = flatDiscovery.dynamodbVpcEndpointId;
171
+ } else if (logicalId === 'FriggKMSVPCEndpoint' || logicalId === 'VPCEndpointKMS') {
172
+ resourceType = 'AWS::EC2::VPCEndpoint';
173
+ physicalId = flatDiscovery.kmsVpcEndpointId;
174
+ } else if (logicalId === 'FriggSecretsManagerVPCEndpoint' || logicalId === 'VPCEndpointSecretsManager') {
175
+ resourceType = 'AWS::EC2::VPCEndpoint';
176
+ physicalId = flatDiscovery.secretsManagerVpcEndpointId;
177
+ } else if (logicalId === 'FriggSQSVPCEndpoint' || logicalId === 'VPCEndpointSQS') {
178
+ resourceType = 'AWS::EC2::VPCEndpoint';
179
+ physicalId = flatDiscovery.sqsVpcEndpointId;
180
+ } else if (logicalId === 'FriggNATRoute' || logicalId === 'FriggPrivateRoute') {
181
+ resourceType = 'AWS::EC2::Route';
182
+ physicalId = flatDiscovery.natRoute;
183
+ }
184
+
185
+ if (physicalId && typeof physicalId === 'string') {
186
+ discovery.stackManaged.push({
187
+ logicalId,
188
+ physicalId,
189
+ resourceType
190
+ });
191
+ }
192
+ });
193
+
194
+ // Also check for external resources extracted via CloudFormation queries
195
+ // (e.g., VPC ID from security group query, subnets from route table associations)
196
+ // These are NOT in the stack but were discovered through stack resources
197
+ this._addExternalResourcesFromCloudFormationQueries(flatDiscovery, discovery, existingLogicalIds);
198
+ } else {
199
+ // Resources discovered from AWS API (not CloudFormation)
200
+ // These go into external array
201
+
202
+ if (flatDiscovery.defaultVpcId && typeof flatDiscovery.defaultVpcId === 'string') {
203
+ discovery.external.push({
204
+ physicalId: flatDiscovery.defaultVpcId,
205
+ resourceType: 'AWS::EC2::VPC',
206
+ source: 'aws-discovery'
207
+ });
208
+ }
209
+
210
+ if (flatDiscovery.defaultSecurityGroupId && typeof flatDiscovery.defaultSecurityGroupId === 'string') {
211
+ discovery.external.push({
212
+ physicalId: flatDiscovery.defaultSecurityGroupId,
213
+ resourceType: 'AWS::EC2::SecurityGroup',
214
+ source: 'aws-discovery'
215
+ });
216
+ }
217
+
218
+ if (flatDiscovery.privateSubnetId1 && typeof flatDiscovery.privateSubnetId1 === 'string') {
219
+ discovery.external.push({
220
+ physicalId: flatDiscovery.privateSubnetId1,
221
+ resourceType: 'AWS::EC2::Subnet',
222
+ source: 'aws-discovery'
223
+ });
224
+ }
225
+
226
+ if (flatDiscovery.privateSubnetId2 && typeof flatDiscovery.privateSubnetId2 === 'string') {
227
+ discovery.external.push({
228
+ physicalId: flatDiscovery.privateSubnetId2,
229
+ resourceType: 'AWS::EC2::Subnet',
230
+ source: 'aws-discovery'
231
+ });
232
+ }
233
+
234
+ // Only add NAT Gateway to external if it's NOT in a private subnet (properly placed)
235
+ // If natGatewayInPrivateSubnet is true, we need a new NAT Gateway
236
+ const natIsProperlyPlaced = flatDiscovery.natGatewayInPrivateSubnet !== true;
237
+
238
+ if (flatDiscovery.natGatewayId && typeof flatDiscovery.natGatewayId === 'string' && natIsProperlyPlaced) {
239
+ discovery.external.push({
240
+ physicalId: flatDiscovery.natGatewayId,
241
+ resourceType: 'AWS::EC2::NatGateway',
242
+ source: 'aws-discovery'
243
+ });
244
+ }
245
+
246
+ if (flatDiscovery.existingNatGatewayId && typeof flatDiscovery.existingNatGatewayId === 'string' && natIsProperlyPlaced) {
247
+ discovery.external.push({
248
+ physicalId: flatDiscovery.existingNatGatewayId,
249
+ resourceType: 'AWS::EC2::NatGateway',
250
+ source: 'aws-discovery'
251
+ });
252
+ }
253
+
254
+ // VPC Endpoints
255
+ if (flatDiscovery.s3VpcEndpointId && typeof flatDiscovery.s3VpcEndpointId === 'string') {
256
+ discovery.external.push({
257
+ physicalId: flatDiscovery.s3VpcEndpointId,
258
+ resourceType: 'AWS::EC2::VPCEndpoint',
259
+ source: 'aws-discovery',
260
+ properties: { ServiceName: 's3' }
261
+ });
262
+ }
263
+
264
+ if (flatDiscovery.dynamodbVpcEndpointId && typeof flatDiscovery.dynamodbVpcEndpointId === 'string') {
265
+ discovery.external.push({
266
+ physicalId: flatDiscovery.dynamodbVpcEndpointId,
267
+ resourceType: 'AWS::EC2::VPCEndpoint',
268
+ source: 'aws-discovery',
269
+ properties: { ServiceName: 'dynamodb' }
270
+ });
271
+ }
272
+
273
+ if (flatDiscovery.kmsVpcEndpointId && typeof flatDiscovery.kmsVpcEndpointId === 'string') {
274
+ discovery.external.push({
275
+ physicalId: flatDiscovery.kmsVpcEndpointId,
276
+ resourceType: 'AWS::EC2::VPCEndpoint',
277
+ source: 'aws-discovery',
278
+ properties: { ServiceName: 'kms' }
279
+ });
280
+ }
281
+
282
+ if (flatDiscovery.secretsManagerVpcEndpointId && typeof flatDiscovery.secretsManagerVpcEndpointId === 'string') {
283
+ discovery.external.push({
284
+ physicalId: flatDiscovery.secretsManagerVpcEndpointId,
285
+ resourceType: 'AWS::EC2::VPCEndpoint',
286
+ source: 'aws-discovery',
287
+ properties: { ServiceName: 'secretsmanager' }
288
+ });
289
+ }
290
+
291
+ if (flatDiscovery.sqsVpcEndpointId && typeof flatDiscovery.sqsVpcEndpointId === 'string') {
292
+ discovery.external.push({
293
+ physicalId: flatDiscovery.sqsVpcEndpointId,
294
+ resourceType: 'AWS::EC2::VPCEndpoint',
295
+ source: 'aws-discovery',
296
+ properties: { ServiceName: 'sqs' }
297
+ });
298
+ }
299
+ }
300
+
301
+ // Add flat discovery properties directly to discovery object for resolver access
302
+ // The resolver checks both discovery.defaultSecurityGroupId and discovery.external array
303
+ discovery.defaultVpcId = flatDiscovery.defaultVpcId;
304
+ discovery.defaultSecurityGroupId = flatDiscovery.defaultSecurityGroupId;
305
+ discovery.privateSubnetId1 = flatDiscovery.privateSubnetId1;
306
+ discovery.privateSubnetId2 = flatDiscovery.privateSubnetId2;
307
+ discovery.natGatewayId = flatDiscovery.natGatewayId;
308
+ discovery.lambdaSecurityGroupId = flatDiscovery.lambdaSecurityGroupId;
309
+
310
+ return discovery;
311
+ }
312
+
313
+ /**
314
+ * Add external resources that were discovered via CloudFormation queries
315
+ * (e.g., VPC ID extracted from security group, subnets from route table associations)
316
+ *
317
+ * @private
318
+ */
319
+ _addExternalResourcesFromCloudFormationQueries(flatDiscovery, discovery, existingLogicalIds) {
320
+ // VPC ID extracted from SG or route table (NOT a stack resource)
321
+ if (flatDiscovery.defaultVpcId &&
322
+ typeof flatDiscovery.defaultVpcId === 'string' &&
323
+ !existingLogicalIds.includes('FriggVPC')) {
324
+ discovery.external.push({
325
+ physicalId: flatDiscovery.defaultVpcId,
326
+ resourceType: 'AWS::EC2::VPC',
327
+ source: 'cloudformation-query'
328
+ });
329
+ }
330
+
331
+ // Subnets extracted from route table associations (NOT stack resources)
332
+ if (flatDiscovery.privateSubnetId1 &&
333
+ typeof flatDiscovery.privateSubnetId1 === 'string' &&
334
+ !existingLogicalIds.includes('FriggPrivateSubnet1')) {
335
+ discovery.external.push({
336
+ physicalId: flatDiscovery.privateSubnetId1,
337
+ resourceType: 'AWS::EC2::Subnet',
338
+ source: 'cloudformation-query'
339
+ });
340
+ }
341
+
342
+ if (flatDiscovery.privateSubnetId2 &&
343
+ typeof flatDiscovery.privateSubnetId2 === 'string' &&
344
+ !existingLogicalIds.includes('FriggPrivateSubnet2')) {
345
+ discovery.external.push({
346
+ physicalId: flatDiscovery.privateSubnetId2,
347
+ resourceType: 'AWS::EC2::Subnet',
348
+ source: 'cloudformation-query'
349
+ });
350
+ }
351
+
352
+ // NAT Gateway extracted from route table routes
353
+ if (flatDiscovery.existingNatGatewayId &&
354
+ typeof flatDiscovery.existingNatGatewayId === 'string' &&
355
+ !existingLogicalIds.includes('FriggNATGateway') &&
356
+ !existingLogicalIds.includes('FriggNatGateway')) {
357
+ discovery.external.push({
358
+ physicalId: flatDiscovery.existingNatGatewayId,
359
+ resourceType: 'AWS::EC2::NatGateway',
360
+ source: 'cloudformation-query'
361
+ });
362
+ }
363
+ }
364
+
365
+ /**
366
+ * Translate legacy configuration (management modes) to new ownership-based configuration
367
+ * Provides backwards compatibility for existing app definitions
368
+ */
369
+ translateLegacyConfig(appDefinition, discoveredResources) {
370
+ // If already using new ownership schema, return as-is
371
+ if (appDefinition.vpc?.ownership) {
372
+ return appDefinition;
373
+ }
374
+
375
+ // Clone to avoid mutating original
376
+ const translated = JSON.parse(JSON.stringify(appDefinition));
377
+
378
+ // Initialize ownership and external sections
379
+ if (!translated.vpc.ownership) {
380
+ translated.vpc.ownership = {};
381
+ }
382
+ if (!translated.vpc.external) {
383
+ translated.vpc.external = {};
384
+ }
385
+ if (!translated.vpc.config) {
386
+ translated.vpc.config = {};
387
+ }
388
+
389
+ // Handle top-level managementMode
390
+ const globalMode = appDefinition.managementMode || 'discover';
391
+ const vpcIsolation = appDefinition.vpcIsolation || 'shared';
392
+
393
+ if (globalMode === 'managed') {
394
+ this.warnIgnoredOptions(appDefinition);
395
+
396
+ if (vpcIsolation === 'isolated') {
397
+ // Check if CloudFormation stack already has resources
398
+ const hasStackVpc = discoveredResources?.defaultVpcId && typeof discoveredResources.defaultVpcId === 'string';
399
+
400
+ if (hasStackVpc) {
401
+ // Stack has VPC - reuse it
402
+ translated.vpc.ownership.vpc = 'auto';
403
+ translated.vpc.ownership.securityGroup = 'auto';
404
+ translated.vpc.ownership.subnets = 'auto';
405
+ translated.vpc.config.selfHeal = true;
406
+ console.log(` managementMode='managed' + vpcIsolation='isolated' → stack has VPC, reusing`);
407
+ } else {
408
+ // No stack VPC - create new
409
+ translated.vpc.ownership.vpc = 'stack';
410
+ translated.vpc.ownership.securityGroup = 'stack';
411
+ translated.vpc.ownership.subnets = 'stack';
412
+ translated.vpc.ownership.natGateway = 'stack';
413
+ translated.vpc.config.natGateway = { enable: true };
414
+ console.log(` managementMode='managed' + vpcIsolation='isolated' → no stack VPC, creating new`);
415
+ }
416
+ } else {
417
+ // Shared VPC
418
+ translated.vpc.ownership.vpc = 'auto';
419
+ translated.vpc.ownership.securityGroup = 'auto';
420
+ translated.vpc.ownership.subnets = 'auto';
421
+ translated.vpc.config.selfHeal = true;
422
+ }
423
+ } else if (globalMode === 'existing') {
424
+ translated.vpc.ownership.vpc = 'external';
425
+ translated.vpc.ownership.securityGroup = 'external';
426
+ translated.vpc.ownership.subnets = 'external';
427
+ }
428
+
429
+ // Handle legacy vpc.management modes
430
+ const vpcManagement = appDefinition.vpc?.management;
431
+ if (vpcManagement === 'create-new') {
432
+ translated.vpc.ownership.vpc = 'stack';
433
+ translated.vpc.ownership.securityGroup = 'stack';
434
+ translated.vpc.ownership.subnets = 'stack';
435
+ } else if (vpcManagement === 'use-existing') {
436
+ translated.vpc.ownership.vpc = 'external';
437
+ translated.vpc.external.vpcId = appDefinition.vpc.vpcId;
438
+
439
+ if (appDefinition.vpc.securityGroupIds) {
440
+ translated.vpc.ownership.securityGroup = 'external';
441
+ translated.vpc.external.securityGroupIds = appDefinition.vpc.securityGroupIds;
442
+ }
443
+
444
+ if (appDefinition.vpc.subnets?.ids) {
445
+ translated.vpc.ownership.subnets = 'external';
446
+ translated.vpc.external.subnetIds = appDefinition.vpc.subnets.ids;
447
+ }
448
+ } else if (vpcManagement === 'discover') {
449
+ // Discover mode - let auto-resolution handle it
450
+ translated.vpc.ownership.vpc = 'auto';
451
+ translated.vpc.ownership.securityGroup = 'auto';
452
+ translated.vpc.ownership.subnets = 'auto';
453
+ }
454
+
455
+ // Handle legacy shareAcrossStages
456
+ if (appDefinition.vpc?.shareAcrossStages !== undefined) {
457
+ if (appDefinition.vpc.shareAcrossStages) {
458
+ // Shared VPC - discover and reuse
459
+ translated.vpc.ownership.vpc = 'auto';
460
+ translated.vpc.ownership.subnets = 'auto';
461
+ } else {
462
+ // Isolated VPC - create stage-specific
463
+ translated.vpc.ownership.vpc = 'stack';
464
+ translated.vpc.ownership.subnets = 'stack';
465
+ translated.vpc.ownership.natGateway = 'stack';
466
+ translated.vpc.config.natGateway = { enable: true };
467
+ }
468
+ }
469
+
470
+ // Handle legacy NAT Gateway management
471
+ if (appDefinition.vpc?.natGateway?.management === 'createAndManage') {
472
+ // Use 'auto' to allow discovering and reusing properly placed external NAT Gateways
473
+ // The resolver will check if there's a good external NAT Gateway and reuse it,
474
+ // or create a new one if needed (or if the existing one is misplaced)
475
+ translated.vpc.ownership.natGateway = 'auto';
476
+ translated.vpc.config.natGateway = { enable: true };
477
+ } else if (appDefinition.vpc?.natGateway?.id) {
478
+ translated.vpc.ownership.natGateway = 'external';
479
+ translated.vpc.external.natGatewayId = appDefinition.vpc.natGateway.id;
480
+ }
481
+
482
+ // Handle legacy subnet management
483
+ if (appDefinition.vpc?.subnets?.management === 'create') {
484
+ translated.vpc.ownership.subnets = 'stack';
485
+ } else if (appDefinition.vpc?.subnets?.management === 'use-existing' && appDefinition.vpc.subnets.ids) {
486
+ translated.vpc.ownership.subnets = 'external';
487
+ translated.vpc.external.subnetIds = appDefinition.vpc.subnets.ids;
488
+ }
489
+
490
+ // Preserve other VPC config
491
+ if (appDefinition.vpc?.cidrBlock) {
492
+ translated.vpc.config.cidrBlock = appDefinition.vpc.cidrBlock;
493
+ }
494
+ if (appDefinition.vpc?.enableVPCEndpoints !== undefined) {
495
+ translated.vpc.config.enableVpcEndpoints = appDefinition.vpc.enableVPCEndpoints;
496
+ }
497
+ if (appDefinition.vpc?.selfHeal !== undefined) {
498
+ translated.vpc.config.selfHeal = appDefinition.vpc.selfHeal;
499
+ }
500
+
501
+ return translated;
502
+ }
503
+
504
+ /**
505
+ * Build complete VPC infrastructure using ownership-based architecture
506
+ */
507
+ async build(appDefinition, discoveredResources) {
508
+ console.log(`\n[${this.name}] Building VPC infrastructure...`);
509
+
510
+ // Backwards compatibility: Translate old schema to new ownership schema
511
+ appDefinition = this.translateLegacyConfig(appDefinition, discoveredResources);
512
+
513
+ // Get structured discovery result (or convert flat discovery to structured)
514
+ // Pass appDefinition to help detect stack-managed resources in managementMode='managed'
515
+ const discovery = discoveredResources._structured || this.convertFlatDiscoveryToStructured(discoveredResources, appDefinition);
516
+
517
+ // Use VpcResourceResolver to make ownership decisions
518
+ const resolver = new VpcResourceResolver();
519
+ const decisions = resolver.resolveAll(appDefinition, discovery);
520
+
521
+ console.log('\n 📋 Resource Ownership Decisions:');
522
+ console.log(` VPC: ${decisions.vpc.ownership} - ${decisions.vpc.reason}`);
523
+ console.log(` Security Group: ${decisions.securityGroup.ownership} - ${decisions.securityGroup.reason}`);
524
+ console.log(` Subnets: ${decisions.subnets.ownership} - ${decisions.subnets.reason}`);
525
+ console.log(` NAT Gateway: ${decisions.natGateway.ownership || 'disabled'} - ${decisions.natGateway.reason}`);
526
+ console.log(` VPC Endpoints:`);
527
+ console.log(` S3: ${decisions.vpcEndpoints.s3.ownership || 'disabled'} - ${decisions.vpcEndpoints.s3.reason}`);
528
+ console.log(` DynamoDB: ${decisions.vpcEndpoints.dynamodb.ownership || 'disabled'} - ${decisions.vpcEndpoints.dynamodb.reason}`);
529
+
530
+ // Initialize result
531
+ const result = {
532
+ resources: {},
533
+ vpcConfig: {
534
+ securityGroupIds: [],
535
+ subnetIds: [],
536
+ },
537
+ iamStatements: [],
538
+ outputs: {},
539
+ environment: {},
540
+ discovery: discoveredResources, // Store for backwards compatibility checks
541
+ };
542
+
543
+ // Add IAM permissions for VPC-enabled Lambda functions
544
+ this.addVpcIamPermissions(result);
545
+
546
+ // Build VPC based on ownership decision
547
+ this.buildVpcFromDecision(decisions.vpc, appDefinition, result);
548
+
549
+ // Build Security Group based on ownership decision
550
+ this.buildSecurityGroupFromDecision(decisions.securityGroup, appDefinition, result);
551
+
552
+ // Build Subnets based on ownership decision
553
+ this.buildSubnetsFromDecision(decisions.subnets, appDefinition, discoveredResources, result);
554
+
555
+ // Build NAT Gateway based on ownership decision
556
+ this.buildNatGatewayFromDecision(decisions.natGateway, appDefinition, discoveredResources, result);
557
+
558
+ // Build VPC Endpoints based on ownership decisions
559
+ this.buildVpcEndpointsFromDecisions(decisions.vpcEndpoints, decisions.securityGroup, appDefinition, discoveredResources, result);
560
+
561
+ // Set VPC_ENABLED environment variable
562
+ result.environment.VPC_ENABLED = 'true';
563
+
564
+ console.log(`\n[${this.name}] ✅ VPC infrastructure built successfully`);
565
+ console.log(` - VPC ID: ${result.vpcId || 'from discovery'}`);
566
+ console.log(` - Subnets: ${result.vpcConfig.subnetIds.length}`);
567
+ console.log(` - Security Groups: ${result.vpcConfig.securityGroupIds.length}`);
568
+
569
+ return result;
570
+ }
571
+
572
+ /**
573
+ * Add IAM permissions for VPC-enabled Lambda functions
574
+ */
575
+ addVpcIamPermissions(result) {
576
+ result.iamStatements.push({
577
+ Effect: 'Allow',
578
+ Action: [
579
+ 'ec2:CreateNetworkInterface',
580
+ 'ec2:DescribeNetworkInterfaces',
581
+ 'ec2:DeleteNetworkInterface',
582
+ 'ec2:AttachNetworkInterface',
583
+ 'ec2:DetachNetworkInterface',
584
+ ],
585
+ Resource: '*',
586
+ });
587
+ }
588
+
589
+ /**
590
+ * Build VPC based on ownership decision
591
+ *
592
+ * For STACK ownership: ALWAYS add definitions to template.
593
+ */
594
+ buildVpcFromDecision(decision, appDefinition, result) {
595
+ if (decision.ownership === ResourceOwnership.STACK) {
596
+ // For STACK ownership: ALWAYS create definitions
597
+ if (decision.physicalId) {
598
+ console.log(` → Adding VPC definition to template (existing: ${decision.physicalId})`);
599
+ } else {
600
+ console.log(' → Adding VPC definition to template (new)');
601
+ }
602
+
603
+ const cidrBlock = appDefinition.vpc?.config?.cidrBlock || appDefinition.vpc?.cidrBlock || '10.0.0.0/16';
604
+
605
+ result.resources.FriggVPC = {
606
+ Type: 'AWS::EC2::VPC',
607
+ Properties: {
608
+ CidrBlock: cidrBlock,
609
+ EnableDnsHostnames: true,
610
+ EnableDnsSupport: true,
611
+ Tags: [
612
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-vpc' },
613
+ { Key: 'ManagedBy', Value: 'Frigg' },
614
+ { Key: 'Service', Value: '${self:service}' },
615
+ { Key: 'Stage', Value: '${self:provider.stage}' },
616
+ ],
617
+ },
618
+ };
619
+
620
+ // Internet Gateway
621
+ result.resources.FriggInternetGateway = {
622
+ Type: 'AWS::EC2::InternetGateway',
623
+ Properties: {
624
+ Tags: [
625
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-igw' },
626
+ { Key: 'ManagedBy', Value: 'Frigg' },
627
+ ],
628
+ },
629
+ };
630
+
631
+ result.resources.FriggVPCGatewayAttachment = {
632
+ Type: 'AWS::EC2::VPCGatewayAttachment',
633
+ Properties: {
634
+ VpcId: { Ref: 'FriggVPC' },
635
+ InternetGatewayId: { Ref: 'FriggInternetGateway' },
636
+ },
637
+ };
638
+
639
+ // Use Ref for stack-managed VPC
640
+ result.vpcId = { Ref: 'FriggVPC' };
641
+ console.log(' ✅ VPC definition added to template');
642
+ } else if (decision.ownership === ResourceOwnership.EXTERNAL) {
643
+ // Use external VPC ID (no definition in template)
644
+ result.vpcId = decision.physicalId;
645
+ console.log(` ✓ Using external VPC: ${decision.physicalId}`);
646
+ }
647
+ }
648
+
649
+ /**
650
+ * Build Security Group based on ownership decision
651
+ */
652
+ buildSecurityGroupFromDecision(decision, appDefinition, result) {
653
+ if (decision.ownership === ResourceOwnership.STACK) {
654
+ // Always create security group resource in template
655
+ console.log(' → Adding Lambda Security Group to template...');
656
+
657
+ result.resources.FriggLambdaSecurityGroup = {
658
+ Type: 'AWS::EC2::SecurityGroup',
659
+ Properties: {
660
+ GroupDescription: 'Security group for Frigg Lambda functions',
661
+ VpcId: result.vpcId,
662
+ SecurityGroupEgress: [
663
+ { IpProtocol: 'tcp', FromPort: 443, ToPort: 443, CidrIp: '0.0.0.0/0', Description: 'HTTPS outbound' },
664
+ { IpProtocol: 'tcp', FromPort: 80, ToPort: 80, CidrIp: '0.0.0.0/0', Description: 'HTTP outbound' },
665
+ { IpProtocol: 'tcp', FromPort: 53, ToPort: 53, CidrIp: '0.0.0.0/0', Description: 'DNS TCP' },
666
+ { IpProtocol: 'udp', FromPort: 53, ToPort: 53, CidrIp: '0.0.0.0/0', Description: 'DNS UDP' },
667
+ { IpProtocol: 'tcp', FromPort: 5432, ToPort: 5432, CidrIp: '0.0.0.0/0', Description: 'PostgreSQL' },
668
+ { IpProtocol: 'tcp', FromPort: 27017, ToPort: 27017, CidrIp: '0.0.0.0/0', Description: 'MongoDB' },
669
+ ],
670
+ Tags: [
671
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-lambda-sg' },
672
+ { Key: 'ManagedBy', Value: 'Frigg' },
673
+ ],
674
+ },
675
+ };
676
+
677
+ // Use CloudFormation Ref since resource is in template
678
+ result.vpcConfig.securityGroupIds = [{ Ref: 'FriggLambdaSecurityGroup' }];
679
+ console.log(' ✅ Security Group added to template');
680
+ } else if (decision.ownership === ResourceOwnership.EXTERNAL) {
681
+ // Use external security group IDs
682
+ const sgIds = Array.isArray(decision.physicalId) ? decision.physicalId : [decision.physicalId];
683
+ result.vpcConfig.securityGroupIds = sgIds;
684
+ console.log(` ✓ Using external security group(s): ${sgIds.join(', ')}`);
685
+ }
686
+ }
687
+
688
+ /**
689
+ * Build Subnets based on ownership decision
690
+ */
691
+ buildSubnetsFromDecision(decision, appDefinition, discoveredResources, result) {
692
+ if (decision.ownership === ResourceOwnership.STACK) {
693
+ // Check if no subnets exist and selfHeal is disabled
694
+ if (!decision.physicalIds || decision.physicalIds.length < 2) {
695
+ const selfHeal = appDefinition.vpc?.config?.selfHeal !== false;
696
+ if (!selfHeal) {
697
+ throw new Error(
698
+ 'No subnets discovered. Enable vpc.selfHeal, set subnets.management to "create", or provide subnet IDs.'
699
+ );
700
+ }
701
+ }
702
+
703
+ // For STACK ownership: ALWAYS add definitions to template
704
+ if (decision.physicalIds && decision.physicalIds.length >= 2) {
705
+ console.log(` → Adding subnet definitions to template (existing: ${decision.physicalIds.join(', ')})`);
706
+ } else {
707
+ console.log(' → Adding subnet definitions to template (new)');
708
+ }
709
+
710
+ this.createSubnetsInTemplate(appDefinition, result, discoveredResources);
711
+
712
+ // Use Refs for stack-managed resources
713
+ result.vpcConfig.subnetIds = [
714
+ { Ref: 'FriggPrivateSubnet1' },
715
+ { Ref: 'FriggPrivateSubnet2' }
716
+ ];
717
+ } else if (decision.ownership === ResourceOwnership.EXTERNAL) {
718
+ // Use external subnet IDs directly (no definitions in template)
719
+ result.vpcConfig.subnetIds = decision.physicalIds;
720
+ console.log(` ✓ Using external subnets: ${decision.physicalIds.join(', ')}`);
721
+ }
722
+ }
723
+
724
+ /**
725
+ * Create subnet resources in CloudFormation template
726
+ */
727
+ createSubnetsInTemplate(appDefinition, result, discoveredResources) {
728
+ // Determine VPC ID for subnets
729
+ const vpcId = result.vpcId;
730
+
731
+ // Generate subnet CIDRs
732
+ const cidrs = this.generateSubnetCidrsForNewVpc(vpcId, discoveredResources);
733
+
734
+ // Private Subnet 1
735
+ result.resources.FriggPrivateSubnet1 = {
736
+ Type: 'AWS::EC2::Subnet',
737
+ DeletionPolicy: 'Retain',
738
+ Properties: {
739
+ VpcId: vpcId,
740
+ CidrBlock: cidrs.private1,
741
+ AvailabilityZone: { 'Fn::Select': [0, { 'Fn::GetAZs': '' }] },
742
+ Tags: [
743
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-private-1' },
744
+ { Key: 'Type', Value: 'Private' },
745
+ { Key: 'ManagedBy', Value: 'Frigg' },
746
+ ],
747
+ },
748
+ };
749
+
750
+ // Private Subnet 2
751
+ result.resources.FriggPrivateSubnet2 = {
752
+ Type: 'AWS::EC2::Subnet',
753
+ DeletionPolicy: 'Retain',
754
+ Properties: {
755
+ VpcId: vpcId,
756
+ CidrBlock: cidrs.private2,
757
+ AvailabilityZone: { 'Fn::Select': [1, { 'Fn::GetAZs': '' }] },
758
+ Tags: [
759
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-private-2' },
760
+ { Key: 'Type', Value: 'Private' },
761
+ { Key: 'ManagedBy', Value: 'Frigg' },
762
+ ],
763
+ },
764
+ };
765
+
766
+ // Public Subnets (for NAT Gateway)
767
+ result.resources.FriggPublicSubnet = {
768
+ Type: 'AWS::EC2::Subnet',
769
+ Properties: {
770
+ VpcId: vpcId,
771
+ CidrBlock: cidrs.public1,
772
+ MapPublicIpOnLaunch: true,
773
+ AvailabilityZone: { 'Fn::Select': [0, { 'Fn::GetAZs': '' }] },
774
+ Tags: [
775
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-public-1' },
776
+ { Key: 'Type', Value: 'Public' },
777
+ { Key: 'ManagedBy', Value: 'Frigg' },
778
+ ],
779
+ },
780
+ };
781
+
782
+ result.resources.FriggPublicSubnet2 = {
783
+ Type: 'AWS::EC2::Subnet',
784
+ Properties: {
785
+ VpcId: vpcId,
786
+ CidrBlock: cidrs.public2,
787
+ MapPublicIpOnLaunch: true,
788
+ AvailabilityZone: { 'Fn::Select': [1, { 'Fn::GetAZs': '' }] },
789
+ Tags: [
790
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-public-2' },
791
+ { Key: 'Type', Value: 'Public' },
792
+ { Key: 'ManagedBy', Value: 'Frigg' },
793
+ ],
794
+ },
795
+ };
796
+
797
+ result.vpcConfig.subnetIds = [
798
+ { Ref: 'FriggPrivateSubnet1' },
799
+ { Ref: 'FriggPrivateSubnet2' },
800
+ ];
801
+
802
+ // Map to discovered resources for other builders
803
+ discoveredResources.privateSubnetId1 = { Ref: 'FriggPrivateSubnet1' };
804
+ discoveredResources.privateSubnetId2 = { Ref: 'FriggPrivateSubnet2' };
805
+ discoveredResources.publicSubnetId1 = { Ref: 'FriggPublicSubnet' };
806
+ discoveredResources.publicSubnetId2 = { Ref: 'FriggPublicSubnet2' };
807
+
808
+ console.log(' ✅ Subnet resources added to template');
809
+ }
810
+
811
+ /**
812
+ * Generate subnet CIDRs for new VPC or existing VPC
813
+ */
814
+ generateSubnetCidrsForNewVpc(vpcId, discoveredResources) {
815
+ // If VPC is a Ref (new VPC), use Fn::Cidr
816
+ if (typeof vpcId === 'object' && vpcId.Ref === 'FriggVPC') {
817
+ return {
818
+ private1: { 'Fn::Select': [0, { 'Fn::Cidr': ['10.0.0.0/16', 4, 8] }] },
819
+ private2: { 'Fn::Select': [1, { 'Fn::Cidr': ['10.0.0.0/16', 4, 8] }] },
820
+ public1: { 'Fn::Select': [2, { 'Fn::Cidr': ['10.0.0.0/16', 4, 8] }] },
821
+ public2: { 'Fn::Select': [3, { 'Fn::Cidr': ['10.0.0.0/16', 4, 8] }] },
822
+ };
823
+ }
824
+
825
+ // For existing VPC, find available CIDRs
826
+ const existingCidrs = new Set();
827
+ if (discoveredResources?.subnets) {
828
+ for (const subnet of discoveredResources.subnets) {
829
+ if (subnet.CidrBlock) {
830
+ existingCidrs.add(subnet.CidrBlock);
831
+ }
832
+ }
833
+ }
834
+
835
+ const findAvailableCidr = (startOctet, endOctet) => {
836
+ for (let octet = startOctet; octet <= endOctet; octet++) {
837
+ const candidate = `172.31.${octet}.0/24`;
838
+ if (!existingCidrs.has(candidate)) {
839
+ existingCidrs.add(candidate);
840
+ return candidate;
841
+ }
842
+ }
843
+ return `172.31.${startOctet}.0/24`;
844
+ };
845
+
846
+ return {
847
+ private1: findAvailableCidr(240, 249),
848
+ private2: findAvailableCidr(240, 249),
849
+ public1: findAvailableCidr(250, 255),
850
+ public2: findAvailableCidr(250, 255),
851
+ };
852
+ }
853
+
854
+ /**
855
+ * Build NAT Gateway based on ownership decision
856
+ */
857
+ buildNatGatewayFromDecision(decision, appDefinition, discoveredResources, result) {
858
+ if (!decision.ownership) {
859
+ console.log(' ⊝ NAT Gateway disabled');
860
+ return;
861
+ }
862
+
863
+ if (decision.ownership === ResourceOwnership.STACK) {
864
+ if (decision.physicalId) {
865
+ // NAT Gateway exists in stack - CloudFormation will handle it
866
+ console.log(` ✓ NAT Gateway in stack: ${decision.physicalId}`);
867
+ // Still need to ensure route tables are set up
868
+ this.createNatGatewayRouting(appDefinition, discoveredResources, result, { Ref: 'FriggNATGateway' });
869
+ } else {
870
+ // Create new NAT Gateway
871
+ console.log(' → Creating NAT Gateway in template...');
872
+ this.createNatGatewayInTemplate(appDefinition, discoveredResources, result);
873
+ }
874
+ } else if (decision.ownership === ResourceOwnership.EXTERNAL) {
875
+ // Use external NAT Gateway
876
+ console.log(` ✓ Using external NAT Gateway: ${decision.physicalId}`);
877
+ result.natGatewayId = decision.physicalId;
878
+ this.createNatGatewayRouting(appDefinition, discoveredResources, result, decision.physicalId);
879
+ }
880
+ }
881
+
882
+ /**
883
+ * Create NAT Gateway resources in CloudFormation template
884
+ */
885
+ createNatGatewayInTemplate(appDefinition, discoveredResources, result) {
886
+ // Elastic IP for NAT Gateway
887
+ result.resources.FriggNATGatewayEIP = {
888
+ Type: 'AWS::EC2::EIP',
889
+ DeletionPolicy: 'Retain',
890
+ UpdateReplacePolicy: 'Retain',
891
+ Properties: {
892
+ Domain: 'vpc',
893
+ Tags: [
894
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-nat-eip' },
895
+ { Key: 'ManagedBy', Value: 'Frigg' },
896
+ ],
897
+ },
898
+ };
899
+
900
+ // NAT Gateway in public subnet
901
+ result.resources.FriggNATGateway = {
902
+ Type: 'AWS::EC2::NatGateway',
903
+ DeletionPolicy: 'Retain',
904
+ UpdateReplacePolicy: 'Retain',
905
+ Properties: {
906
+ AllocationId: { 'Fn::GetAtt': ['FriggNATGatewayEIP', 'AllocationId'] },
907
+ SubnetId: discoveredResources.publicSubnetId1 || { Ref: 'FriggPublicSubnet' },
908
+ Tags: [
909
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-nat' },
910
+ { Key: 'ManagedBy', Value: 'Frigg' },
911
+ ],
912
+ },
913
+ };
914
+
915
+ // Create public routing
916
+ this.createPublicRouting(appDefinition, discoveredResources, result);
917
+
918
+ // Create NAT routing
919
+ this.createNatGatewayRouting(appDefinition, discoveredResources, result, { Ref: 'FriggNATGateway' });
920
+
921
+ console.log(' ✅ NAT Gateway resources added to template');
922
+ }
923
+
924
+ /**
925
+ * Build VPC Endpoints based on ownership decisions
926
+ */
927
+ buildVpcEndpointsFromDecisions(endpointDecisions, securityGroupDecision, appDefinition, discoveredResources, result) {
928
+ const decisions = endpointDecisions; // For backwards compatibility with existing code
929
+ const endpointsToCreate = [];
930
+ const endpointsInStack = [];
931
+ const externalEndpoints = [];
932
+
933
+ // Analyze decisions
934
+ Object.entries(decisions).forEach(([type, decision]) => {
935
+ if (decision.ownership === ResourceOwnership.STACK && !decision.physicalId) {
936
+ endpointsToCreate.push(type);
937
+ } else if (decision.ownership === ResourceOwnership.STACK && decision.physicalId) {
938
+ endpointsInStack.push(type);
939
+ } else if (decision.ownership === ResourceOwnership.EXTERNAL) {
940
+ externalEndpoints.push(type);
941
+ }
942
+ });
943
+
944
+ if (endpointsInStack.length > 0) {
945
+ console.log(` ✓ VPC Endpoints in stack: ${endpointsInStack.join(', ')}`);
946
+ // CRITICAL: Must add stack-managed endpoints back to template or CloudFormation will DELETE them!
947
+ this._addStackManagedEndpointsToTemplate(decisions, securityGroupDecision, discoveredResources, result);
948
+ }
949
+
950
+ if (externalEndpoints.length > 0) {
951
+ console.log(` ✓ External VPC Endpoints: ${externalEndpoints.join(', ')}`);
952
+ }
953
+
954
+ if (endpointsToCreate.length === 0) {
955
+ if (endpointsInStack.length === 0 && externalEndpoints.length === 0) {
956
+ console.log(' ⊝ VPC Endpoints disabled');
957
+ }
958
+ return;
959
+ }
960
+
961
+ console.log(` → Creating VPC Endpoints: ${endpointsToCreate.join(', ')}...`);
962
+
963
+ const vpcId = result.vpcId;
964
+
965
+ // Create route table if needed
966
+ if (!result.resources.FriggLambdaRouteTable) {
967
+ result.resources.FriggLambdaRouteTable = {
968
+ Type: 'AWS::EC2::RouteTable',
969
+ Properties: {
970
+ VpcId: vpcId,
971
+ Tags: [
972
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-lambda-rt' },
973
+ { Key: 'ManagedBy', Value: 'Frigg' },
974
+ ],
975
+ },
976
+ };
977
+ }
978
+
979
+ // Ensure subnet associations
980
+ this.ensureSubnetAssociations(appDefinition, {}, result);
981
+
982
+ // Create endpoints
983
+ if (endpointsToCreate.includes('s3')) {
984
+ result.resources.FriggS3VPCEndpoint = {
985
+ Type: 'AWS::EC2::VPCEndpoint',
986
+ Properties: {
987
+ VpcId: vpcId,
988
+ ServiceName: 'com.amazonaws.${self:provider.region}.s3',
989
+ VpcEndpointType: 'Gateway',
990
+ RouteTableIds: [{ Ref: 'FriggLambdaRouteTable' }],
991
+ },
992
+ };
993
+ }
994
+
995
+ if (endpointsToCreate.includes('dynamodb')) {
996
+ result.resources.FriggDynamoDBVPCEndpoint = {
997
+ Type: 'AWS::EC2::VPCEndpoint',
998
+ Properties: {
999
+ VpcId: vpcId,
1000
+ ServiceName: 'com.amazonaws.${self:provider.region}.dynamodb',
1001
+ VpcEndpointType: 'Gateway',
1002
+ RouteTableIds: [{ Ref: 'FriggLambdaRouteTable' }],
1003
+ },
1004
+ };
1005
+ }
1006
+
1007
+ // Create security group for interface endpoints if needed
1008
+ const needsInterfaceEndpoints = endpointsToCreate.some(type => ['kms', 'secretsManager', 'sqs'].includes(type));
1009
+ if (needsInterfaceEndpoints) {
1010
+ // Determine source security group for ingress rule
1011
+ let sourceSgId;
1012
+ if (securityGroupDecision.ownership === ResourceOwnership.STACK) {
1013
+ sourceSgId = { Ref: 'FriggLambdaSecurityGroup' };
1014
+ } else {
1015
+ // External - use the physical ID
1016
+ sourceSgId = securityGroupDecision.physicalIds[0];
1017
+ }
1018
+
1019
+ result.resources.FriggVPCEndpointSecurityGroup = {
1020
+ Type: 'AWS::EC2::SecurityGroup',
1021
+ Properties: {
1022
+ GroupDescription: 'Security group for VPC Endpoints',
1023
+ VpcId: vpcId,
1024
+ SecurityGroupIngress: [
1025
+ {
1026
+ IpProtocol: 'tcp',
1027
+ FromPort: 443,
1028
+ ToPort: 443,
1029
+ SourceSecurityGroupId: sourceSgId,
1030
+ Description: 'HTTPS from Lambda',
1031
+ },
1032
+ ],
1033
+ Tags: [
1034
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-vpc-endpoint-sg' },
1035
+ { Key: 'ManagedBy', Value: 'Frigg' },
1036
+ ],
1037
+ },
1038
+ };
1039
+ }
1040
+
1041
+ if (endpointsToCreate.includes('kms')) {
1042
+ result.resources.FriggKMSVPCEndpoint = {
1043
+ Type: 'AWS::EC2::VPCEndpoint',
1044
+ Properties: {
1045
+ VpcId: vpcId,
1046
+ ServiceName: 'com.amazonaws.${self:provider.region}.kms',
1047
+ VpcEndpointType: 'Interface',
1048
+ SubnetIds: result.vpcConfig.subnetIds,
1049
+ SecurityGroupIds: [{ Ref: 'FriggVPCEndpointSecurityGroup' }],
1050
+ PrivateDnsEnabled: true,
1051
+ },
1052
+ };
1053
+ }
1054
+
1055
+ if (endpointsToCreate.includes('secretsManager')) {
1056
+ result.resources.FriggSecretsManagerVPCEndpoint = {
1057
+ Type: 'AWS::EC2::VPCEndpoint',
1058
+ Properties: {
1059
+ VpcId: vpcId,
1060
+ ServiceName: 'com.amazonaws.${self:provider.region}.secretsmanager',
1061
+ VpcEndpointType: 'Interface',
1062
+ SubnetIds: result.vpcConfig.subnetIds,
1063
+ SecurityGroupIds: [{ Ref: 'FriggVPCEndpointSecurityGroup' }],
1064
+ PrivateDnsEnabled: true,
1065
+ },
1066
+ };
1067
+ }
1068
+
1069
+ if (endpointsToCreate.includes('sqs')) {
1070
+ result.resources.FriggSQSVPCEndpoint = {
1071
+ Type: 'AWS::EC2::VPCEndpoint',
1072
+ Properties: {
1073
+ VpcId: vpcId,
1074
+ ServiceName: 'com.amazonaws.${self:provider.region}.sqs',
1075
+ VpcEndpointType: 'Interface',
1076
+ SubnetIds: result.vpcConfig.subnetIds,
1077
+ SecurityGroupIds: [{ Ref: 'FriggVPCEndpointSecurityGroup' }],
1078
+ PrivateDnsEnabled: true,
1079
+ },
1080
+ };
1081
+ }
1082
+
1083
+ console.log(` ✅ VPC Endpoint resources added to template`);
1084
+ }
1085
+
1086
+ /**
1087
+ * Perform self-healing checks and fixes
1088
+ */
1089
+ performSelfHealing(discoveredResources, appDefinition) {
1090
+ console.log('🔧 VPC Self-healing mode enabled - checking for misconfigurations...');
1091
+
1092
+ const healingReport = {
1093
+ healed: [],
1094
+ warnings: [],
1095
+ errors: [],
1096
+ };
1097
+
1098
+ // Check for NAT Gateway in private subnet
1099
+ if (discoveredResources.natGatewayInPrivateSubnet) {
1100
+ healingReport.warnings.push(
1101
+ `NAT Gateway ${discoveredResources.natGatewayInPrivateSubnet} is in a private subnet`
1102
+ );
1103
+ healingReport.healed.push(
1104
+ 'Will create new NAT Gateway in public subnet'
1105
+ );
1106
+ discoveredResources.needsNewNatGateway = true;
1107
+ }
1108
+
1109
+ // Check for orphaned Elastic IPs
1110
+ if (discoveredResources.orphanedElasticIps?.length > 0) {
1111
+ healingReport.warnings.push(
1112
+ `Found ${discoveredResources.orphanedElasticIps.length} orphaned Elastic IPs`
1113
+ );
1114
+ }
1115
+
1116
+ // Check for subnet routing issues
1117
+ if (discoveredResources.privateSubnetsWithWrongRoutes?.length > 0) {
1118
+ healingReport.warnings.push(
1119
+ `Found ${discoveredResources.privateSubnetsWithWrongRoutes.length} subnets with wrong routes`
1120
+ );
1121
+ healingReport.healed.push('Will create correct route tables');
1122
+ }
1123
+
1124
+ // Log healing report
1125
+ if (healingReport.healed.length > 0) {
1126
+ console.log(' ✅ Self-healing actions:');
1127
+ healingReport.healed.forEach(action => console.log(` - ${action}`));
1128
+ }
1129
+ if (healingReport.warnings.length > 0) {
1130
+ console.log(' ⚠️ Issues detected:');
1131
+ healingReport.warnings.forEach(warning => console.log(` - ${warning}`));
1132
+ }
1133
+
1134
+ return healingReport;
1135
+ }
1136
+
1137
+ /**
1138
+ * Add stack-managed VPC endpoints back to template
1139
+ *
1140
+ * CRITICAL: CloudFormation will DELETE resources that exist in the previous template
1141
+ * but are missing from the new template. We must re-add discovered stack-managed
1142
+ * endpoints to prevent CloudFormation from deleting them.
1143
+ *
1144
+ * @private
1145
+ */
1146
+ _addStackManagedEndpointsToTemplate(endpointDecisions, securityGroupDecision, discoveredResources, result) {
1147
+ const decisions = endpointDecisions; // For backwards compatibility
1148
+ const vpcId = result.vpcId;
1149
+
1150
+ // Determine logical IDs based on what exists in stack for backwards compatibility
1151
+ // CRITICAL: Frontify production uses OLD naming (VPCEndpointS3, not FriggS3VPCEndpoint)
1152
+ const existingLogicalIds = discoveredResources?.existingLogicalIds || [];
1153
+
1154
+
1155
+ const logicalIdMap = {
1156
+ s3: existingLogicalIds.includes('VPCEndpointS3') ? 'VPCEndpointS3' : 'FriggS3VPCEndpoint',
1157
+ dynamodb: existingLogicalIds.includes('VPCEndpointDynamoDB') ? 'VPCEndpointDynamoDB' : 'FriggDynamoDBVPCEndpoint',
1158
+ kms: existingLogicalIds.includes('VPCEndpointKMS') ? 'VPCEndpointKMS' : 'FriggKMSVPCEndpoint',
1159
+ secretsManager: existingLogicalIds.includes('VPCEndpointSecretsManager') ? 'VPCEndpointSecretsManager' : 'FriggSecretsManagerVPCEndpoint',
1160
+ sqs: existingLogicalIds.includes('VPCEndpointSQS') ? 'VPCEndpointSQS' : 'FriggSQSVPCEndpoint'
1161
+ };
1162
+
1163
+ Object.entries(decisions).forEach(([type, decision]) => {
1164
+ if (decision.ownership === ResourceOwnership.STACK) {
1165
+ const logicalId = logicalIdMap[type];
1166
+
1167
+ // Determine endpoint type and properties based on service
1168
+ if (type === 's3') {
1169
+ result.resources[logicalId] = {
1170
+ Type: 'AWS::EC2::VPCEndpoint',
1171
+ Properties: {
1172
+ VpcId: vpcId,
1173
+ ServiceName: 'com.amazonaws.${self:provider.region}.s3',
1174
+ VpcEndpointType: 'Gateway',
1175
+ RouteTableIds: [{ Ref: 'FriggLambdaRouteTable' }]
1176
+ }
1177
+ };
1178
+ } else if (type === 'dynamodb') {
1179
+ result.resources[logicalId] = {
1180
+ Type: 'AWS::EC2::VPCEndpoint',
1181
+ Properties: {
1182
+ VpcId: vpcId,
1183
+ ServiceName: 'com.amazonaws.${self:provider.region}.dynamodb',
1184
+ VpcEndpointType: 'Gateway',
1185
+ RouteTableIds: [{ Ref: 'FriggLambdaRouteTable' }]
1186
+ }
1187
+ };
1188
+ } else {
1189
+ // Interface endpoints (KMS, Secrets Manager, SQS)
1190
+ const serviceMap = {
1191
+ kms: 'kms',
1192
+ secretsManager: 'secretsmanager',
1193
+ sqs: 'sqs'
1194
+ };
1195
+
1196
+ result.resources[logicalId] = {
1197
+ Type: 'AWS::EC2::VPCEndpoint',
1198
+ Properties: {
1199
+ VpcId: vpcId,
1200
+ ServiceName: `com.amazonaws.\${self:provider.region}.${serviceMap[type]}`,
1201
+ VpcEndpointType: 'Interface',
1202
+ SubnetIds: result.vpcConfig.subnetIds,
1203
+ SecurityGroupIds: [{ Ref: 'FriggVPCEndpointSecurityGroup' }],
1204
+ PrivateDnsEnabled: true
1205
+ }
1206
+ };
1207
+ }
1208
+ }
1209
+ });
1210
+
1211
+ // If any interface endpoints exist, ensure security group is in template
1212
+ const hasInterfaceEndpoints = ['kms', 'secretsManager', 'sqs'].some(
1213
+ type => decisions[type]?.ownership === ResourceOwnership.STACK && decisions[type]?.physicalId
1214
+ );
1215
+
1216
+ if (hasInterfaceEndpoints && !result.resources.FriggVPCEndpointSecurityGroup) {
1217
+ // Determine source security group for ingress rule
1218
+ // If Lambda SG is stack-managed, use CloudFormation Ref
1219
+ // If Lambda SG is external, use the physical ID directly
1220
+ let sourceSgId;
1221
+ if (securityGroupDecision.ownership === ResourceOwnership.STACK) {
1222
+ sourceSgId = { Ref: 'FriggLambdaSecurityGroup' };
1223
+ } else {
1224
+ // External - use the physical ID
1225
+ sourceSgId = securityGroupDecision.physicalIds[0];
1226
+ }
1227
+
1228
+ result.resources.FriggVPCEndpointSecurityGroup = {
1229
+ Type: 'AWS::EC2::SecurityGroup',
1230
+ Properties: {
1231
+ GroupDescription: 'Security group for VPC Endpoints',
1232
+ VpcId: vpcId,
1233
+ SecurityGroupIngress: [
1234
+ {
1235
+ IpProtocol: 'tcp',
1236
+ FromPort: 443,
1237
+ ToPort: 443,
1238
+ SourceSecurityGroupId: sourceSgId
1239
+ }
1240
+ ],
1241
+ Tags: [
1242
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-vpc-endpoint-sg' },
1243
+ { Key: 'ManagedBy', Value: 'Frigg' }
1244
+ ]
1245
+ }
1246
+ };
1247
+ }
1248
+ }
1249
+
1250
+ /**
1251
+ * Build new VPC from scratch
1252
+ */
1253
+ async buildNewVpc(appDefinition, discoveredResources, result) {
1254
+ console.log(' Creating new VPC infrastructure...');
1255
+
1256
+ const cidrBlock = appDefinition.vpc.cidrBlock || '10.0.0.0/16';
1257
+
1258
+ // Main VPC
1259
+ result.resources.FriggVPC = {
1260
+ Type: 'AWS::EC2::VPC',
1261
+ Properties: {
1262
+ CidrBlock: cidrBlock,
1263
+ EnableDnsHostnames: true,
1264
+ EnableDnsSupport: true,
1265
+ Tags: [
1266
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-vpc' },
1267
+ { Key: 'ManagedBy', Value: 'Frigg' },
1268
+ { Key: 'Service', Value: '${self:service}' },
1269
+ { Key: 'Stage', Value: '${self:provider.stage}' },
1270
+ ],
1271
+ },
1272
+ };
1273
+
1274
+ // Internet Gateway
1275
+ result.resources.FriggInternetGateway = {
1276
+ Type: 'AWS::EC2::InternetGateway',
1277
+ Properties: {
1278
+ Tags: [
1279
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-igw' },
1280
+ { Key: 'ManagedBy', Value: 'Frigg' },
1281
+ ],
1282
+ },
1283
+ };
1284
+
1285
+ result.resources.FriggVPCGatewayAttachment = {
1286
+ Type: 'AWS::EC2::VPCGatewayAttachment',
1287
+ Properties: {
1288
+ VpcId: { Ref: 'FriggVPC' },
1289
+ InternetGatewayId: { Ref: 'FriggInternetGateway' },
1290
+ },
1291
+ };
1292
+
1293
+ // Lambda Security Group
1294
+ result.resources.FriggLambdaSecurityGroup = {
1295
+ Type: 'AWS::EC2::SecurityGroup',
1296
+ Properties: {
1297
+ GroupDescription: 'Security group for Frigg Lambda functions',
1298
+ VpcId: { Ref: 'FriggVPC' },
1299
+ SecurityGroupEgress: [
1300
+ { IpProtocol: 'tcp', FromPort: 443, ToPort: 443, CidrIp: '0.0.0.0/0', Description: 'HTTPS outbound' },
1301
+ { IpProtocol: 'tcp', FromPort: 80, ToPort: 80, CidrIp: '0.0.0.0/0', Description: 'HTTP outbound' },
1302
+ { IpProtocol: 'tcp', FromPort: 53, ToPort: 53, CidrIp: '0.0.0.0/0', Description: 'DNS TCP' },
1303
+ { IpProtocol: 'udp', FromPort: 53, ToPort: 53, CidrIp: '0.0.0.0/0', Description: 'DNS UDP' },
1304
+ { IpProtocol: 'tcp', FromPort: 5432, ToPort: 5432, CidrIp: '0.0.0.0/0', Description: 'PostgreSQL' },
1305
+ { IpProtocol: 'tcp', FromPort: 27017, ToPort: 27017, CidrIp: '0.0.0.0/0', Description: 'MongoDB' },
1306
+ ],
1307
+ Tags: [
1308
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-lambda-sg' },
1309
+ { Key: 'ManagedBy', Value: 'Frigg' },
1310
+ ],
1311
+ },
1312
+ };
1313
+
1314
+ result.vpcId = { Ref: 'FriggVPC' };
1315
+ result.vpcConfig.securityGroupIds = [{ Ref: 'FriggLambdaSecurityGroup' }];
1316
+
1317
+ console.log(' ✅ New VPC infrastructure resources created');
1318
+ }
1319
+
1320
+ /**
1321
+ * Use existing VPC (explicitly provided)
1322
+ */
1323
+ async useExistingVpc(appDefinition, discoveredResources, result) {
1324
+ console.log(' Using existing VPC...');
1325
+
1326
+ if (!appDefinition.vpc.vpcId) {
1327
+ throw new Error('vpc.vpcId is required when management="use-existing"');
1328
+ }
1329
+
1330
+ result.vpcId = appDefinition.vpc.vpcId;
1331
+ result.vpcConfig.securityGroupIds = appDefinition.vpc.securityGroupIds ||
1332
+ (discoveredResources.defaultSecurityGroupId ? [discoveredResources.defaultSecurityGroupId] : []);
1333
+
1334
+ console.log(` ✅ Using VPC: ${result.vpcId}`);
1335
+ }
1336
+
1337
+ /**
1338
+ * Discover existing VPC from AWS
1339
+ */
1340
+ async discoverVpc(appDefinition, discoveredResources, result) {
1341
+ console.log(' Discovering existing VPC...');
1342
+
1343
+ if (!discoveredResources.defaultVpcId) {
1344
+ throw new Error(
1345
+ 'VPC discovery failed: No VPC found. Set vpc.management to "create-new" or provide vpc.vpcId with "use-existing".'
1346
+ );
1347
+ }
1348
+
1349
+ result.vpcId = discoveredResources.defaultVpcId;
1350
+
1351
+ // Check if resources came from CloudFormation stack
1352
+ const fromCfStack = discoveredResources.fromCloudFormationStack === true;
1353
+ const existingLogicalIds = discoveredResources.existingLogicalIds || [];
1354
+
1355
+ if (fromCfStack && existingLogicalIds.length > 0) {
1356
+ console.log(` ✓ VPC discovered from CloudFormation stack: ${discoveredResources.stackName}`);
1357
+ console.log(` ✓ Found ${existingLogicalIds.length} existing resources in stack`);
1358
+ console.log(' ℹ Adding resources to template for idempotent deployment');
1359
+ } else {
1360
+ // VPC discovered from AWS API (not from CF stack)
1361
+ console.log(' ℹ VPC discovered from AWS API - will create Lambda security group');
1362
+ }
1363
+
1364
+ // Always create Lambda security group in template for idempotent deployments
1365
+ // CloudFormation will recognize it already exists and won't recreate it
1366
+ result.resources.FriggLambdaSecurityGroup = {
1367
+ Type: 'AWS::EC2::SecurityGroup',
1368
+ Properties: {
1369
+ GroupDescription: 'Security group for Frigg Lambda functions',
1370
+ VpcId: result.vpcId,
1371
+ SecurityGroupEgress: [
1372
+ { IpProtocol: 'tcp', FromPort: 443, ToPort: 443, CidrIp: '0.0.0.0/0', Description: 'HTTPS outbound' },
1373
+ { IpProtocol: 'tcp', FromPort: 80, ToPort: 80, CidrIp: '0.0.0.0/0', Description: 'HTTP outbound' },
1374
+ { IpProtocol: 'tcp', FromPort: 53, ToPort: 53, CidrIp: '0.0.0.0/0', Description: 'DNS TCP' },
1375
+ { IpProtocol: 'udp', FromPort: 53, ToPort: 53, CidrIp: '0.0.0.0/0', Description: 'DNS UDP' },
1376
+ { IpProtocol: 'tcp', FromPort: 5432, ToPort: 5432, CidrIp: '0.0.0.0/0', Description: 'PostgreSQL' },
1377
+ { IpProtocol: 'tcp', FromPort: 27017, ToPort: 27017, CidrIp: '0.0.0.0/0', Description: 'MongoDB' },
1378
+ ],
1379
+ Tags: [
1380
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-lambda-sg' },
1381
+ { Key: 'ManagedBy', Value: 'Frigg' },
1382
+ ],
1383
+ },
1384
+ };
1385
+
1386
+ // Always use Ref since resource is in template
1387
+ result.vpcConfig.securityGroupIds = [{ Ref: 'FriggLambdaSecurityGroup' }];
1388
+
1389
+ console.log(` ✅ Discovered VPC: ${result.vpcId}`);
1390
+ }
1391
+
1392
+ /**
1393
+ * Build subnet infrastructure
1394
+ * @param {Object} vpcManagement - Normalized VPC management mode (passed from build() to ensure consistency)
1395
+ */
1396
+ async buildSubnets(appDefinition, discoveredResources, result, vpcManagement) {
1397
+ // Default subnet management depends on context:
1398
+ // - Stack-managed subnets discovered: discover (reuse existing)
1399
+ // - use-existing mode with subnet IDs provided: use-existing
1400
+ // - create-new mode: create
1401
+ // - discover mode without stack subnets: create (for stage isolation)
1402
+ let defaultSubnetManagement = 'create';
1403
+
1404
+ // Check if stack-managed subnets were discovered from CloudFormation
1405
+ // Only reuse if they're actual subnet IDs (strings), not CloudFormation Refs (objects)
1406
+ const hasStackManagedSubnets =
1407
+ discoveredResources?.privateSubnetId1 &&
1408
+ discoveredResources?.privateSubnetId2 &&
1409
+ typeof discoveredResources.privateSubnetId1 === 'string' &&
1410
+ typeof discoveredResources.privateSubnetId2 === 'string';
1411
+
1412
+ if (hasStackManagedSubnets) {
1413
+ defaultSubnetManagement = 'discover';
1414
+ } else if (vpcManagement === 'use-existing' && appDefinition.vpc.subnets?.ids?.length >= 2) {
1415
+ defaultSubnetManagement = 'use-existing';
1416
+ }
1417
+
1418
+ const subnetManagement = appDefinition.vpc.subnets?.management || defaultSubnetManagement;
1419
+
1420
+ console.log(` Subnet Management Mode: ${subnetManagement} (default: ${defaultSubnetManagement}, explicit: ${appDefinition.vpc.subnets?.management})`);
1421
+
1422
+ switch (subnetManagement) {
1423
+ case 'create':
1424
+ this.createSubnets(appDefinition, discoveredResources, result, vpcManagement);
1425
+ break;
1426
+ case 'use-existing':
1427
+ this.useExistingSubnets(appDefinition, result);
1428
+ break;
1429
+ case 'discover':
1430
+ default:
1431
+ this.discoverSubnets(appDefinition, discoveredResources, result);
1432
+ break;
1433
+ }
1434
+ }
1435
+
1436
+ /**
1437
+ * Create new subnets
1438
+ */
1439
+ createSubnets(appDefinition, discoveredResources, result, vpcManagement) {
1440
+ console.log(' Creating new subnets...');
1441
+
1442
+ const subnetVpcId = vpcManagement === 'create-new' ? { Ref: 'FriggVPC' } : result.vpcId;
1443
+
1444
+ // Generate CIDRs - pass discovered resources to avoid conflicts
1445
+ const cidrs = this.generateSubnetCidrs(vpcManagement, discoveredResources);
1446
+
1447
+ // Private Subnet 1
1448
+ result.resources.FriggPrivateSubnet1 = {
1449
+ Type: 'AWS::EC2::Subnet',
1450
+ DeletionPolicy: 'Retain',
1451
+ Properties: {
1452
+ VpcId: subnetVpcId,
1453
+ CidrBlock: cidrs.private1,
1454
+ AvailabilityZone: { 'Fn::Select': [0, { 'Fn::GetAZs': '' }] },
1455
+ Tags: [
1456
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-private-1' },
1457
+ { Key: 'Type', Value: 'Private' },
1458
+ { Key: 'ManagedBy', Value: 'Frigg' },
1459
+ ],
1460
+ },
1461
+ };
1462
+
1463
+ // Private Subnet 2
1464
+ result.resources.FriggPrivateSubnet2 = {
1465
+ Type: 'AWS::EC2::Subnet',
1466
+ DeletionPolicy: 'Retain',
1467
+ Properties: {
1468
+ VpcId: subnetVpcId,
1469
+ CidrBlock: cidrs.private2,
1470
+ AvailabilityZone: { 'Fn::Select': [1, { 'Fn::GetAZs': '' }] },
1471
+ Tags: [
1472
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-private-2' },
1473
+ { Key: 'Type', Value: 'Private' },
1474
+ { Key: 'ManagedBy', Value: 'Frigg' },
1475
+ ],
1476
+ },
1477
+ };
1478
+
1479
+ // Public Subnets (for NAT Gateway and Aurora if publicly accessible)
1480
+ result.resources.FriggPublicSubnet = {
1481
+ Type: 'AWS::EC2::Subnet',
1482
+ Properties: {
1483
+ VpcId: subnetVpcId,
1484
+ CidrBlock: cidrs.public1,
1485
+ MapPublicIpOnLaunch: true,
1486
+ AvailabilityZone: { 'Fn::Select': [0, { 'Fn::GetAZs': '' }] },
1487
+ Tags: [
1488
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-public-1' },
1489
+ { Key: 'Type', Value: 'Public' },
1490
+ { Key: 'ManagedBy', Value: 'Frigg' },
1491
+ ],
1492
+ },
1493
+ };
1494
+
1495
+ result.resources.FriggPublicSubnet2 = {
1496
+ Type: 'AWS::EC2::Subnet',
1497
+ Properties: {
1498
+ VpcId: subnetVpcId,
1499
+ CidrBlock: cidrs.public2,
1500
+ MapPublicIpOnLaunch: true,
1501
+ AvailabilityZone: { 'Fn::Select': [1, { 'Fn::GetAZs': '' }] },
1502
+ Tags: [
1503
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-public-2' },
1504
+ { Key: 'Type', Value: 'Public' },
1505
+ { Key: 'ManagedBy', Value: 'Frigg' },
1506
+ ],
1507
+ },
1508
+ };
1509
+
1510
+ result.vpcConfig.subnetIds = [
1511
+ { Ref: 'FriggPrivateSubnet1' },
1512
+ { Ref: 'FriggPrivateSubnet2' },
1513
+ ];
1514
+
1515
+ // Map to discovered resources for other builders (Aurora, etc.)
1516
+ discoveredResources.privateSubnetId1 = { Ref: 'FriggPrivateSubnet1' };
1517
+ discoveredResources.privateSubnetId2 = { Ref: 'FriggPrivateSubnet2' };
1518
+ discoveredResources.publicSubnetId1 = { Ref: 'FriggPublicSubnet' };
1519
+ discoveredResources.publicSubnetId2 = { Ref: 'FriggPublicSubnet2' };
1520
+
1521
+ console.log(' ✅ Subnets created');
1522
+ }
1523
+
1524
+ /**
1525
+ * Use existing subnets
1526
+ */
1527
+ useExistingSubnets(appDefinition, result) {
1528
+ console.log(' Using existing subnets...');
1529
+
1530
+ if (!appDefinition.vpc.subnets?.ids || appDefinition.vpc.subnets.ids.length < 2) {
1531
+ throw new Error(
1532
+ 'At least 2 subnet IDs required when subnets.management="use-existing"'
1533
+ );
1534
+ }
1535
+
1536
+ result.vpcConfig.subnetIds = appDefinition.vpc.subnets.ids;
1537
+ console.log(` ✅ Using ${result.vpcConfig.subnetIds.length} existing subnets`);
1538
+ }
1539
+
1540
+ /**
1541
+ * Discover existing subnets from AWS
1542
+ */
1543
+ discoverSubnets(appDefinition, discoveredResources, result) {
1544
+ console.log(' Discovering subnets...');
1545
+
1546
+ // Use explicitly provided subnet IDs first
1547
+ if (appDefinition.vpc.subnets?.ids?.length >= 2) {
1548
+ result.vpcConfig.subnetIds = appDefinition.vpc.subnets.ids;
1549
+ console.log(` ✅ Using ${result.vpcConfig.subnetIds.length} provided subnets`);
1550
+ return;
1551
+ }
1552
+
1553
+ // User explicitly set subnets.management: 'discover', so use discovered subnets
1554
+ // NOTE: This may cause route table conflicts if multiple stages share subnets
1555
+ // Default behavior is now to create stage-specific subnets (subnets.management: 'create')
1556
+ if (discoveredResources.privateSubnetId1 && discoveredResources.privateSubnetId2) {
1557
+ result.vpcConfig.subnetIds = [
1558
+ discoveredResources.privateSubnetId1,
1559
+ discoveredResources.privateSubnetId2,
1560
+ ];
1561
+ console.log(' ✅ Using discovered subnets (backwards compatibility mode)');
1562
+ return;
1563
+ }
1564
+
1565
+ // No subnets found - create if self-heal enabled
1566
+ if (appDefinition.vpc.selfHeal) {
1567
+ console.log(' ⚠️ No subnets found - self-heal will create them');
1568
+ this.createSubnets(appDefinition, discoveredResources, result, 'discover');
1569
+ } else {
1570
+ throw new Error(
1571
+ 'No subnets discovered. Enable vpc.selfHeal, set subnets.management to "create", or provide subnet IDs.'
1572
+ );
1573
+ }
1574
+ }
1575
+
1576
+ /**
1577
+ * Generate subnet CIDR blocks
1578
+ * Finds available CIDRs that don't conflict with existing subnets
1579
+ */
1580
+ generateSubnetCidrs(vpcManagement, discoveredResources) {
1581
+ if (vpcManagement === 'create-new') {
1582
+ // Use CloudFormation Fn::Cidr for dynamic generation
1583
+ return {
1584
+ private1: { 'Fn::Select': [0, { 'Fn::Cidr': ['10.0.0.0/16', 4, 8] }] },
1585
+ private2: { 'Fn::Select': [1, { 'Fn::Cidr': ['10.0.0.0/16', 4, 8] }] },
1586
+ public1: { 'Fn::Select': [2, { 'Fn::Cidr': ['10.0.0.0/16', 4, 8] }] },
1587
+ public2: { 'Fn::Select': [3, { 'Fn::Cidr': ['10.0.0.0/16', 4, 8] }] },
1588
+ };
1589
+ } else {
1590
+ // Find available CIDRs for existing VPC by checking existing subnets
1591
+ const existingCidrs = new Set();
1592
+
1593
+ // Collect all existing subnet CIDRs
1594
+ if (discoveredResources?.subnets) {
1595
+ for (const subnet of discoveredResources.subnets) {
1596
+ if (subnet.CidrBlock) {
1597
+ existingCidrs.add(subnet.CidrBlock);
1598
+ }
1599
+ }
1600
+ }
1601
+
1602
+ console.log(` Found ${existingCidrs.size} existing subnet CIDRs in VPC`);
1603
+
1604
+ // Generate candidates in the default VPC range (172.31.0.0/16)
1605
+ // Private subnets: 240-249, Public subnets: 250-255
1606
+ const findAvailableCidr = (startOctet, endOctet) => {
1607
+ for (let octet = startOctet; octet <= endOctet; octet++) {
1608
+ const candidate = `172.31.${octet}.0/24`;
1609
+ if (!existingCidrs.has(candidate)) {
1610
+ existingCidrs.add(candidate); // Mark as used immediately
1611
+ return candidate;
1612
+ }
1613
+ }
1614
+ // Fallback if range exhausted
1615
+ return `172.31.${startOctet}.0/24`;
1616
+ };
1617
+
1618
+ const privateRange = { start: 240, end: 249 };
1619
+ const publicRange = { start: 250, end: 255 };
1620
+
1621
+ const cidrs = {
1622
+ private1: findAvailableCidr(privateRange.start, privateRange.end),
1623
+ private2: findAvailableCidr(privateRange.start, privateRange.end),
1624
+ public1: findAvailableCidr(publicRange.start, publicRange.end),
1625
+ public2: findAvailableCidr(publicRange.start, publicRange.end),
1626
+ };
1627
+
1628
+ console.log(` Using available CIDRs: ${Object.values(cidrs).join(', ')}`);
1629
+
1630
+ return cidrs;
1631
+ }
1632
+ }
1633
+
1634
+ /**
1635
+ * Build NAT Gateway for private subnet internet access
1636
+ */
1637
+ async buildNatGateway(appDefinition, discoveredResources, result) {
1638
+ const natManagement = appDefinition.vpc.natGateway?.management || 'discover';
1639
+
1640
+ console.log(` NAT Gateway Management: ${natManagement}`);
1641
+
1642
+ // Check if resources came from CloudFormation stack
1643
+ const fromCfStack = discoveredResources.fromCloudFormationStack === true;
1644
+ const existingLogicalIds = discoveredResources.existingLogicalIds || [];
1645
+
1646
+ if (fromCfStack && existingLogicalIds.length > 0) {
1647
+ console.log(' Skipping NAT Gateway - will reuse from CloudFormation stack');
1648
+ return;
1649
+ }
1650
+
1651
+ // Check if we should create NAT Gateway
1652
+ const needsNatGateway = natManagement === 'createAndManage' ||
1653
+ discoveredResources.needsNewNatGateway === true;
1654
+
1655
+ if (!needsNatGateway && natManagement === 'discover') {
1656
+ console.log(' Skipping NAT Gateway (discovery mode)');
1657
+ return;
1658
+ }
1659
+
1660
+ // Check if we should reuse existing
1661
+ if (appDefinition.vpc.natGateway?.id) {
1662
+ console.log(` Using existing NAT Gateway: ${appDefinition.vpc.natGateway.id}`);
1663
+ result.natGatewayId = appDefinition.vpc.natGateway.id;
1664
+ return;
1665
+ }
1666
+
1667
+ if (discoveredResources.existingNatGatewayId && !discoveredResources.natGatewayInPrivateSubnet) {
1668
+ console.log(` Reusing discovered NAT Gateway: ${discoveredResources.existingNatGatewayId}`);
1669
+ result.natGatewayId = discoveredResources.existingNatGatewayId;
1670
+
1671
+ // Still need to create route table and associations for discovered NAT
1672
+ this.createNatGatewayRouting(appDefinition, discoveredResources, result, discoveredResources.existingNatGatewayId);
1673
+ return;
1674
+ }
1675
+
1676
+ // Create new NAT Gateway
1677
+ console.log(' Creating new NAT Gateway...');
1678
+
1679
+ // Elastic IP for NAT Gateway
1680
+ result.resources.FriggNATGatewayEIP = {
1681
+ Type: 'AWS::EC2::EIP',
1682
+ DeletionPolicy: 'Retain',
1683
+ UpdateReplacePolicy: 'Retain',
1684
+ Properties: {
1685
+ Domain: 'vpc',
1686
+ Tags: [
1687
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-nat-eip' },
1688
+ { Key: 'ManagedBy', Value: 'Frigg' },
1689
+ ],
1690
+ },
1691
+ };
1692
+
1693
+ // NAT Gateway in public subnet
1694
+ result.resources.FriggNATGateway = {
1695
+ Type: 'AWS::EC2::NatGateway',
1696
+ DeletionPolicy: 'Retain',
1697
+ UpdateReplacePolicy: 'Retain',
1698
+ Properties: {
1699
+ AllocationId: { 'Fn::GetAtt': ['FriggNATGatewayEIP', 'AllocationId'] },
1700
+ SubnetId: discoveredResources.publicSubnetId1 || { Ref: 'FriggPublicSubnet' },
1701
+ Tags: [
1702
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-nat' },
1703
+ { Key: 'ManagedBy', Value: 'Frigg' },
1704
+ ],
1705
+ },
1706
+ };
1707
+
1708
+ // Create public routing (public subnets → Internet Gateway)
1709
+ this.createPublicRouting(appDefinition, discoveredResources, result);
1710
+
1711
+ // Create routing for the new NAT Gateway (private subnets → NAT → IGW)
1712
+ this.createNatGatewayRouting(appDefinition, discoveredResources, result, { Ref: 'FriggNATGateway' });
1713
+
1714
+ console.log(' ✅ NAT Gateway infrastructure created');
1715
+ }
1716
+
1717
+ /**
1718
+ * Create public route table with Internet Gateway route
1719
+ * Required for NAT Gateway to have internet access
1720
+ */
1721
+ createPublicRouting(appDefinition, discoveredResources, result) {
1722
+ // Public route table with Internet Gateway route
1723
+ result.resources.FriggPublicRouteTable = {
1724
+ Type: 'AWS::EC2::RouteTable',
1725
+ Properties: {
1726
+ VpcId: result.vpcId,
1727
+ Tags: [
1728
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-public-rt' },
1729
+ { Key: 'ManagedBy', Value: 'Frigg' },
1730
+ ],
1731
+ },
1732
+ };
1733
+
1734
+ // Route to Internet Gateway
1735
+ result.resources.FriggPublicRoute = {
1736
+ Type: 'AWS::EC2::Route',
1737
+ DependsOn: 'FriggVPCGatewayAttachment',
1738
+ Properties: {
1739
+ RouteTableId: { Ref: 'FriggPublicRouteTable' },
1740
+ DestinationCidrBlock: '0.0.0.0/0',
1741
+ GatewayId: { Ref: 'FriggInternetGateway' },
1742
+ },
1743
+ };
1744
+
1745
+ // Use discovered public subnets or created ones
1746
+ const publicSubnet1 = discoveredResources.publicSubnetId1 || { Ref: 'FriggPublicSubnet' };
1747
+ const publicSubnet2 = discoveredResources.publicSubnetId2 || { Ref: 'FriggPublicSubnet2' };
1748
+
1749
+ // Associate public subnets with public route table
1750
+ result.resources.FriggPublicSubnet1RouteTableAssociation = {
1751
+ Type: 'AWS::EC2::SubnetRouteTableAssociation',
1752
+ Properties: {
1753
+ SubnetId: publicSubnet1,
1754
+ RouteTableId: { Ref: 'FriggPublicRouteTable' },
1755
+ },
1756
+ };
1757
+
1758
+ result.resources.FriggPublicSubnet2RouteTableAssociation = {
1759
+ Type: 'AWS::EC2::SubnetRouteTableAssociation',
1760
+ Properties: {
1761
+ SubnetId: publicSubnet2,
1762
+ RouteTableId: { Ref: 'FriggPublicRouteTable' },
1763
+ },
1764
+ };
1765
+ }
1766
+
1767
+ /**
1768
+ * Create route table and associations for NAT Gateway
1769
+ * Always adds to template - CloudFormation handles idempotency
1770
+ * Uses existing logical IDs from stack to prevent AlreadyExists errors
1771
+ */
1772
+ createNatGatewayRouting(appDefinition, discoveredResources, result, natGatewayId) {
1773
+ // Note: We always add routing resources to the template.
1774
+ // CloudFormation's idempotency ensures existing resources are updated, not recreated.
1775
+ // Removing resources from the template causes CloudFormation to try CREATE on next deploy → AlreadyExists error
1776
+
1777
+ // Determine which logical ID to use for the NAT route based on what exists in stack
1778
+ // Older stacks use 'FriggNATRoute', newer ones use 'FriggPrivateRoute'
1779
+ // CRITICAL: Must check existingLogicalIds to avoid AlreadyExists errors on logical ID mismatch
1780
+ const existingLogicalIds = discoveredResources?.existingLogicalIds || [];
1781
+
1782
+ const routeLogicalId = existingLogicalIds.includes('FriggNATRoute')
1783
+ ? 'FriggNATRoute' // Use existing logical ID from stack (backwards compatibility)
1784
+ : 'FriggPrivateRoute'; // Default for new stacks
1785
+
1786
+ // Always use new logical IDs to force recreation and fix drift
1787
+ // Old IDs (FriggSubnet1RouteAssociation) may have drifted from CloudFormation state
1788
+ // Using new IDs forces CloudFormation to delete old and create new associations
1789
+ const subnet1AssocLogicalId = 'FriggPrivateSubnet1RouteTableAssociation';
1790
+ const subnet2AssocLogicalId = 'FriggPrivateSubnet2RouteTableAssociation';
1791
+
1792
+ // Private route table with NAT Gateway route
1793
+ if (!result.resources.FriggLambdaRouteTable) {
1794
+ result.resources.FriggLambdaRouteTable = {
1795
+ Type: 'AWS::EC2::RouteTable',
1796
+ Properties: {
1797
+ VpcId: result.vpcId,
1798
+ Tags: [
1799
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-lambda-rt' },
1800
+ { Key: 'ManagedBy', Value: 'Frigg' },
1801
+ ],
1802
+ },
1803
+ };
1804
+ }
1805
+
1806
+ result.resources[routeLogicalId] = {
1807
+ Type: 'AWS::EC2::Route',
1808
+ Properties: {
1809
+ RouteTableId: { Ref: 'FriggLambdaRouteTable' },
1810
+ DestinationCidrBlock: '0.0.0.0/0',
1811
+ NatGatewayId: natGatewayId,
1812
+ },
1813
+ };
1814
+
1815
+ // Associate route table with private subnets
1816
+ // Use discovered subnet IDs or CloudFormation references
1817
+ const subnet1Id = discoveredResources.privateSubnetId1 || { Ref: 'FriggPrivateSubnet1' };
1818
+ const subnet2Id = discoveredResources.privateSubnetId2 || { Ref: 'FriggPrivateSubnet2' };
1819
+
1820
+ result.resources[subnet1AssocLogicalId] = {
1821
+ Type: 'AWS::EC2::SubnetRouteTableAssociation',
1822
+ UpdateReplacePolicy: 'Delete',
1823
+ Properties: {
1824
+ SubnetId: subnet1Id,
1825
+ RouteTableId: { Ref: 'FriggLambdaRouteTable' },
1826
+ },
1827
+ };
1828
+
1829
+ result.resources[subnet2AssocLogicalId] = {
1830
+ Type: 'AWS::EC2::SubnetRouteTableAssociation',
1831
+ UpdateReplacePolicy: 'Delete',
1832
+ Properties: {
1833
+ SubnetId: subnet2Id,
1834
+ RouteTableId: { Ref: 'FriggLambdaRouteTable' },
1835
+ },
1836
+ };
1837
+
1838
+ console.log(' ✅ Route table and subnet associations created');
1839
+ }
1840
+
1841
+ /**
1842
+ * Ensure subnet associations with route table
1843
+ * Called to heal missing associations when route table exists but associations don't
1844
+ */
1845
+ ensureSubnetAssociations(appDefinition, discoveredResources, result) {
1846
+ // Skip if associations already created (by NAT Gateway routing)
1847
+ // Check for both old and new logical ID patterns
1848
+ if (result.resources.FriggPrivateSubnet1RouteTableAssociation ||
1849
+ result.resources.FriggSubnet1RouteAssociation) {
1850
+ return; // Already handled by NAT Gateway routing
1851
+ }
1852
+
1853
+ const routeTableId = discoveredResources.routeTableId || { Ref: 'FriggLambdaRouteTable' };
1854
+ const subnet1Id = discoveredResources.privateSubnetId1 || { Ref: 'FriggPrivateSubnet1' };
1855
+ const subnet2Id = discoveredResources.privateSubnetId2 || { Ref: 'FriggPrivateSubnet2' };
1856
+
1857
+ result.resources.FriggPrivateSubnet1RouteTableAssociation = {
1858
+ Type: 'AWS::EC2::SubnetRouteTableAssociation',
1859
+ UpdateReplacePolicy: 'Delete',
1860
+ Properties: {
1861
+ SubnetId: subnet1Id,
1862
+ RouteTableId: routeTableId,
1863
+ },
1864
+ };
1865
+
1866
+ result.resources.FriggPrivateSubnet2RouteTableAssociation = {
1867
+ Type: 'AWS::EC2::SubnetRouteTableAssociation',
1868
+ UpdateReplacePolicy: 'Delete',
1869
+ Properties: {
1870
+ SubnetId: subnet2Id,
1871
+ RouteTableId: routeTableId,
1872
+ },
1873
+ };
1874
+
1875
+ console.log(' ✓ Ensured subnet associations with route table');
1876
+ }
1877
+
1878
+ /**
1879
+ * Build VPC Endpoints for AWS services
1880
+ */
1881
+ buildVpcEndpoints(appDefinition, discoveredResources, result, existingEndpoints = {}) {
1882
+ // Check if endpoints are from CloudFormation stack (string IDs)
1883
+ // Stack-managed resources should be reused, not recreated
1884
+ const stackManagedEndpoints = {
1885
+ s3: discoveredResources.s3VpcEndpointId && typeof discoveredResources.s3VpcEndpointId === 'string',
1886
+ dynamodb: discoveredResources.dynamodbVpcEndpointId && typeof discoveredResources.dynamodbVpcEndpointId === 'string',
1887
+ kms: discoveredResources.kmsVpcEndpointId && typeof discoveredResources.kmsVpcEndpointId === 'string',
1888
+ secretsManager: discoveredResources.secretsManagerVpcEndpointId && typeof discoveredResources.secretsManagerVpcEndpointId === 'string',
1889
+ sqs: discoveredResources.sqsVpcEndpointId && typeof discoveredResources.sqsVpcEndpointId === 'string',
1890
+ };
1891
+
1892
+ // Build list of what needs creation (not stack-managed, not existing elsewhere)
1893
+ const missing = [];
1894
+ if (!stackManagedEndpoints.s3 && !existingEndpoints.s3) missing.push('S3');
1895
+ if (!stackManagedEndpoints.dynamodb && !existingEndpoints.dynamodb) missing.push('DynamoDB');
1896
+ if (!stackManagedEndpoints.kms && !existingEndpoints.kms && appDefinition.encryption?.fieldLevelEncryptionMethod === 'kms') missing.push('KMS');
1897
+ if (!stackManagedEndpoints.secretsManager && !existingEndpoints.secretsManager) missing.push('Secrets Manager');
1898
+ // SQS endpoint needed for job queues and async processing
1899
+ if (!stackManagedEndpoints.sqs && !existingEndpoints.sqs) missing.push('SQS');
1900
+
1901
+ // Log reused stack-managed endpoints
1902
+ const reused = [];
1903
+ if (stackManagedEndpoints.s3) reused.push('S3');
1904
+ if (stackManagedEndpoints.dynamodb) reused.push('DynamoDB');
1905
+ if (stackManagedEndpoints.kms) reused.push('KMS');
1906
+ if (stackManagedEndpoints.secretsManager) reused.push('Secrets Manager');
1907
+ if (stackManagedEndpoints.sqs) reused.push('SQS');
1908
+
1909
+ if (reused.length > 0) {
1910
+ console.log(` ✓ Reusing stack-managed VPC endpoints: ${reused.join(', ')}`);
1911
+ }
1912
+
1913
+ if (missing.length > 0) {
1914
+ console.log(` Creating missing VPC Endpoints: ${missing.join(', ')}...`);
1915
+ } else if (reused.length === 0) {
1916
+ console.log(' All required VPC Endpoints already exist - skipping creation');
1917
+ return;
1918
+ } else {
1919
+ // All endpoints are stack-managed, no creation needed
1920
+ return;
1921
+ }
1922
+
1923
+ const vpcId = result.vpcId || discoveredResources.defaultVpcId;
1924
+
1925
+ // Create route table for VPC endpoints if it doesn't exist
1926
+ // VPC endpoints (S3, DynamoDB) need to reference a route table
1927
+ if (!result.resources.FriggLambdaRouteTable) {
1928
+ result.resources.FriggLambdaRouteTable = {
1929
+ Type: 'AWS::EC2::RouteTable',
1930
+ Properties: {
1931
+ VpcId: vpcId,
1932
+ Tags: [
1933
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-lambda-rt' },
1934
+ { Key: 'ManagedBy', Value: 'Frigg' },
1935
+ ],
1936
+ },
1937
+ };
1938
+ }
1939
+
1940
+ // Ensure subnet associations exist (healing for VPC endpoints without NAT Gateway)
1941
+ if (result.resources.FriggLambdaRouteTable || discoveredResources.routeTableId) {
1942
+ this.ensureSubnetAssociations(appDefinition, discoveredResources, result);
1943
+ }
1944
+
1945
+ // S3 Gateway Endpoint (only if not stack-managed and missing)
1946
+ if (!stackManagedEndpoints.s3 && !existingEndpoints.s3) {
1947
+ result.resources.FriggS3VPCEndpoint = {
1948
+ Type: 'AWS::EC2::VPCEndpoint',
1949
+ Properties: {
1950
+ VpcId: vpcId,
1951
+ ServiceName: 'com.amazonaws.${self:provider.region}.s3',
1952
+ VpcEndpointType: 'Gateway',
1953
+ RouteTableIds: [{ Ref: 'FriggLambdaRouteTable' }],
1954
+ },
1955
+ };
1956
+ }
1957
+
1958
+ // DynamoDB Gateway Endpoint (only if not stack-managed and missing)
1959
+ if (!stackManagedEndpoints.dynamodb && !existingEndpoints.dynamodb) {
1960
+ result.resources.FriggDynamoDBVPCEndpoint = {
1961
+ Type: 'AWS::EC2::VPCEndpoint',
1962
+ Properties: {
1963
+ VpcId: vpcId,
1964
+ ServiceName: 'com.amazonaws.${self:provider.region}.dynamodb',
1965
+ VpcEndpointType: 'Gateway',
1966
+ RouteTableIds: [{ Ref: 'FriggLambdaRouteTable' }],
1967
+ },
1968
+ };
1969
+ }
1970
+
1971
+ // VPC Endpoint Security Group (only if KMS, Secrets Manager, or SQS are not stack-managed and missing)
1972
+ const needsSecurityGroup =
1973
+ (!stackManagedEndpoints.kms && !existingEndpoints.kms && appDefinition.encryption?.fieldLevelEncryptionMethod === 'kms') ||
1974
+ (!stackManagedEndpoints.secretsManager && !existingEndpoints.secretsManager) ||
1975
+ (!stackManagedEndpoints.sqs && !existingEndpoints.sqs);
1976
+
1977
+ if (needsSecurityGroup) {
1978
+ result.resources.FriggVPCEndpointSecurityGroup = {
1979
+ Type: 'AWS::EC2::SecurityGroup',
1980
+ Properties: {
1981
+ GroupDescription: 'Security group for VPC Endpoints',
1982
+ VpcId: vpcId,
1983
+ SecurityGroupIngress: [
1984
+ {
1985
+ IpProtocol: 'tcp',
1986
+ FromPort: 443,
1987
+ ToPort: 443,
1988
+ SourceSecurityGroupId: { Ref: 'FriggLambdaSecurityGroup' },
1989
+ Description: 'HTTPS from Lambda',
1990
+ },
1991
+ ],
1992
+ Tags: [
1993
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-vpc-endpoint-sg' },
1994
+ { Key: 'ManagedBy', Value: 'Frigg' },
1995
+ ],
1996
+ },
1997
+ };
1998
+ }
1999
+
2000
+ // KMS Interface Endpoint (only if not stack-managed, missing, AND KMS encryption is enabled)
2001
+ if (!stackManagedEndpoints.kms && !existingEndpoints.kms && appDefinition.encryption?.fieldLevelEncryptionMethod === 'kms') {
2002
+ result.resources.FriggKMSVPCEndpoint = {
2003
+ Type: 'AWS::EC2::VPCEndpoint',
2004
+ Properties: {
2005
+ VpcId: vpcId,
2006
+ ServiceName: 'com.amazonaws.${self:provider.region}.kms',
2007
+ VpcEndpointType: 'Interface',
2008
+ SubnetIds: result.vpcConfig.subnetIds,
2009
+ SecurityGroupIds: [{ Ref: 'FriggVPCEndpointSecurityGroup' }],
2010
+ PrivateDnsEnabled: true,
2011
+ },
2012
+ };
2013
+ }
2014
+
2015
+ // Secrets Manager Interface Endpoint (only if not stack-managed and missing)
2016
+ if (!stackManagedEndpoints.secretsManager && !existingEndpoints.secretsManager) {
2017
+ result.resources.FriggSecretsManagerVPCEndpoint = {
2018
+ Type: 'AWS::EC2::VPCEndpoint',
2019
+ Properties: {
2020
+ VpcId: vpcId,
2021
+ ServiceName: 'com.amazonaws.${self:provider.region}.secretsmanager',
2022
+ VpcEndpointType: 'Interface',
2023
+ SubnetIds: result.vpcConfig.subnetIds,
2024
+ SecurityGroupIds: [{ Ref: 'FriggVPCEndpointSecurityGroup' }],
2025
+ PrivateDnsEnabled: true,
2026
+ },
2027
+ };
2028
+ }
2029
+
2030
+ // SQS Interface Endpoint (only if not stack-managed and missing)
2031
+ // Used for job queues and async processing (not just database migrations)
2032
+ if (!stackManagedEndpoints.sqs && !existingEndpoints.sqs) {
2033
+ result.resources.FriggSQSVPCEndpoint = {
2034
+ Type: 'AWS::EC2::VPCEndpoint',
2035
+ Properties: {
2036
+ VpcId: vpcId,
2037
+ ServiceName: 'com.amazonaws.${self:provider.region}.sqs',
2038
+ VpcEndpointType: 'Interface',
2039
+ SubnetIds: result.vpcConfig.subnetIds,
2040
+ SecurityGroupIds: [{ Ref: 'FriggVPCEndpointSecurityGroup' }],
2041
+ PrivateDnsEnabled: true,
2042
+ },
2043
+ };
2044
+ }
2045
+
2046
+ console.log(` ✅ Created ${missing.length} VPC endpoint(s): ${missing.join(', ')}`);
2047
+ }
2048
+ }
2049
+
2050
+ module.exports = { VpcBuilder };
2051
+