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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (212) hide show
  1. package/infrastructure/ARCHITECTURE.md +487 -0
  2. package/infrastructure/HEALTH.md +468 -0
  3. package/infrastructure/README.md +51 -0
  4. package/infrastructure/__tests__/postgres-config.test.js +914 -0
  5. package/infrastructure/__tests__/template-generation.test.js +687 -0
  6. package/infrastructure/create-frigg-infrastructure.js +1 -1
  7. package/infrastructure/docs/POSTGRES-CONFIGURATION.md +630 -0
  8. package/infrastructure/{DEPLOYMENT-INSTRUCTIONS.md → docs/deployment-instructions.md} +3 -3
  9. package/infrastructure/{IAM-POLICY-TEMPLATES.md → docs/iam-policy-templates.md} +9 -10
  10. package/infrastructure/domains/database/aurora-builder.js +809 -0
  11. package/infrastructure/domains/database/aurora-builder.test.js +950 -0
  12. package/infrastructure/domains/database/aurora-discovery.js +87 -0
  13. package/infrastructure/domains/database/aurora-discovery.test.js +188 -0
  14. package/infrastructure/domains/database/aurora-resolver.js +210 -0
  15. package/infrastructure/domains/database/aurora-resolver.test.js +347 -0
  16. package/infrastructure/domains/database/migration-builder.js +695 -0
  17. package/infrastructure/domains/database/migration-builder.test.js +294 -0
  18. package/infrastructure/domains/database/migration-resolver.js +163 -0
  19. package/infrastructure/domains/database/migration-resolver.test.js +337 -0
  20. package/infrastructure/domains/health/application/ports/IPropertyReconciler.js +164 -0
  21. package/infrastructure/domains/health/application/ports/IResourceDetector.js +129 -0
  22. package/infrastructure/domains/health/application/ports/IResourceImporter.js +142 -0
  23. package/infrastructure/domains/health/application/ports/IStackRepository.js +131 -0
  24. package/infrastructure/domains/health/application/ports/index.js +26 -0
  25. package/infrastructure/domains/health/application/use-cases/__tests__/execute-resource-import-use-case.test.js +679 -0
  26. package/infrastructure/domains/health/application/use-cases/__tests__/mismatch-analyzer-method-name.test.js +167 -0
  27. package/infrastructure/domains/health/application/use-cases/__tests__/repair-via-import-use-case.test.js +1130 -0
  28. package/infrastructure/domains/health/application/use-cases/execute-resource-import-use-case.js +221 -0
  29. package/infrastructure/domains/health/application/use-cases/reconcile-properties-use-case.js +152 -0
  30. package/infrastructure/domains/health/application/use-cases/reconcile-properties-use-case.test.js +343 -0
  31. package/infrastructure/domains/health/application/use-cases/repair-via-import-use-case.js +535 -0
  32. package/infrastructure/domains/health/application/use-cases/repair-via-import-use-case.test.js +376 -0
  33. package/infrastructure/domains/health/application/use-cases/run-health-check-use-case.js +213 -0
  34. package/infrastructure/domains/health/application/use-cases/run-health-check-use-case.test.js +441 -0
  35. package/infrastructure/domains/health/docs/ACME-DEV-DRIFT-ANALYSIS.md +267 -0
  36. package/infrastructure/domains/health/docs/BUILD-VS-DEPLOYED-TEMPLATE-ANALYSIS.md +324 -0
  37. package/infrastructure/domains/health/docs/ORPHAN-DETECTION-ANALYSIS.md +386 -0
  38. package/infrastructure/domains/health/docs/SPEC-CLEANUP-COMMAND.md +1419 -0
  39. package/infrastructure/domains/health/docs/TDD-IMPLEMENTATION-SUMMARY.md +391 -0
  40. package/infrastructure/domains/health/docs/TEMPLATE-COMPARISON-IMPLEMENTATION.md +551 -0
  41. package/infrastructure/domains/health/domain/entities/issue.js +299 -0
  42. package/infrastructure/domains/health/domain/entities/issue.test.js +528 -0
  43. package/infrastructure/domains/health/domain/entities/property-mismatch.js +108 -0
  44. package/infrastructure/domains/health/domain/entities/property-mismatch.test.js +275 -0
  45. package/infrastructure/domains/health/domain/entities/resource.js +159 -0
  46. package/infrastructure/domains/health/domain/entities/resource.test.js +432 -0
  47. package/infrastructure/domains/health/domain/entities/stack-health-report.js +306 -0
  48. package/infrastructure/domains/health/domain/entities/stack-health-report.test.js +601 -0
  49. package/infrastructure/domains/health/domain/services/__tests__/health-score-percentage-based.test.js +380 -0
  50. package/infrastructure/domains/health/domain/services/__tests__/import-progress-monitor.test.js +971 -0
  51. package/infrastructure/domains/health/domain/services/__tests__/import-template-generator.test.js +1150 -0
  52. package/infrastructure/domains/health/domain/services/__tests__/logical-id-mapper.test.js +672 -0
  53. package/infrastructure/domains/health/domain/services/__tests__/template-parser.test.js +496 -0
  54. package/infrastructure/domains/health/domain/services/__tests__/update-progress-monitor.test.js +419 -0
  55. package/infrastructure/domains/health/domain/services/health-score-calculator.js +248 -0
  56. package/infrastructure/domains/health/domain/services/health-score-calculator.test.js +504 -0
  57. package/infrastructure/domains/health/domain/services/import-progress-monitor.js +195 -0
  58. package/infrastructure/domains/health/domain/services/import-template-generator.js +435 -0
  59. package/infrastructure/domains/health/domain/services/logical-id-mapper.js +345 -0
  60. package/infrastructure/domains/health/domain/services/mismatch-analyzer.js +234 -0
  61. package/infrastructure/domains/health/domain/services/mismatch-analyzer.test.js +431 -0
  62. package/infrastructure/domains/health/domain/services/property-mutability-config.js +382 -0
  63. package/infrastructure/domains/health/domain/services/template-parser.js +245 -0
  64. package/infrastructure/domains/health/domain/services/update-progress-monitor.js +192 -0
  65. package/infrastructure/domains/health/domain/value-objects/health-score.js +138 -0
  66. package/infrastructure/domains/health/domain/value-objects/health-score.test.js +267 -0
  67. package/infrastructure/domains/health/domain/value-objects/property-mutability.js +161 -0
  68. package/infrastructure/domains/health/domain/value-objects/property-mutability.test.js +198 -0
  69. package/infrastructure/domains/health/domain/value-objects/resource-state.js +167 -0
  70. package/infrastructure/domains/health/domain/value-objects/resource-state.test.js +196 -0
  71. package/infrastructure/domains/health/domain/value-objects/stack-identifier.js +192 -0
  72. package/infrastructure/domains/health/domain/value-objects/stack-identifier.test.js +262 -0
  73. package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-cfn-tagged.test.js +312 -0
  74. package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-multi-stack.test.js +367 -0
  75. package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-relationship-analysis.test.js +432 -0
  76. package/infrastructure/domains/health/infrastructure/adapters/aws-property-reconciler.js +784 -0
  77. package/infrastructure/domains/health/infrastructure/adapters/aws-property-reconciler.test.js +1133 -0
  78. package/infrastructure/domains/health/infrastructure/adapters/aws-resource-detector.js +565 -0
  79. package/infrastructure/domains/health/infrastructure/adapters/aws-resource-detector.test.js +554 -0
  80. package/infrastructure/domains/health/infrastructure/adapters/aws-resource-importer.js +318 -0
  81. package/infrastructure/domains/health/infrastructure/adapters/aws-resource-importer.test.js +398 -0
  82. package/infrastructure/domains/health/infrastructure/adapters/aws-stack-repository.js +777 -0
  83. package/infrastructure/domains/health/infrastructure/adapters/aws-stack-repository.test.js +580 -0
  84. package/infrastructure/domains/integration/integration-builder.js +397 -0
  85. package/infrastructure/domains/integration/integration-builder.test.js +593 -0
  86. package/infrastructure/domains/integration/integration-resolver.js +170 -0
  87. package/infrastructure/domains/integration/integration-resolver.test.js +369 -0
  88. package/infrastructure/domains/integration/websocket-builder.js +69 -0
  89. package/infrastructure/domains/integration/websocket-builder.test.js +195 -0
  90. package/infrastructure/domains/networking/vpc-builder.js +1829 -0
  91. package/infrastructure/domains/networking/vpc-builder.test.js +1262 -0
  92. package/infrastructure/domains/networking/vpc-discovery.js +177 -0
  93. package/infrastructure/domains/networking/vpc-discovery.test.js +350 -0
  94. package/infrastructure/domains/networking/vpc-resolver.js +324 -0
  95. package/infrastructure/domains/networking/vpc-resolver.test.js +501 -0
  96. package/infrastructure/domains/parameters/ssm-builder.js +79 -0
  97. package/infrastructure/domains/parameters/ssm-builder.test.js +189 -0
  98. package/infrastructure/domains/parameters/ssm-discovery.js +84 -0
  99. package/infrastructure/domains/parameters/ssm-discovery.test.js +210 -0
  100. package/infrastructure/{iam-generator.js → domains/security/iam-generator.js} +2 -2
  101. package/infrastructure/domains/security/kms-builder.js +366 -0
  102. package/infrastructure/domains/security/kms-builder.test.js +374 -0
  103. package/infrastructure/domains/security/kms-discovery.js +80 -0
  104. package/infrastructure/domains/security/kms-discovery.test.js +177 -0
  105. package/infrastructure/domains/security/kms-resolver.js +96 -0
  106. package/infrastructure/domains/security/kms-resolver.test.js +216 -0
  107. package/infrastructure/domains/shared/base-builder.js +112 -0
  108. package/infrastructure/domains/shared/base-resolver.js +186 -0
  109. package/infrastructure/domains/shared/base-resolver.test.js +305 -0
  110. package/infrastructure/domains/shared/builder-orchestrator.js +212 -0
  111. package/infrastructure/domains/shared/builder-orchestrator.test.js +213 -0
  112. package/infrastructure/domains/shared/cloudformation-discovery-v2.js +334 -0
  113. package/infrastructure/domains/shared/cloudformation-discovery.js +375 -0
  114. package/infrastructure/domains/shared/cloudformation-discovery.test.js +590 -0
  115. package/infrastructure/domains/shared/environment-builder.js +119 -0
  116. package/infrastructure/domains/shared/environment-builder.test.js +247 -0
  117. package/infrastructure/domains/shared/providers/aws-provider-adapter.js +544 -0
  118. package/infrastructure/domains/shared/providers/aws-provider-adapter.test.js +377 -0
  119. package/infrastructure/domains/shared/providers/azure-provider-adapter.stub.js +93 -0
  120. package/infrastructure/domains/shared/providers/cloud-provider-adapter.js +136 -0
  121. package/infrastructure/domains/shared/providers/gcp-provider-adapter.stub.js +82 -0
  122. package/infrastructure/domains/shared/providers/provider-factory.js +108 -0
  123. package/infrastructure/domains/shared/providers/provider-factory.test.js +170 -0
  124. package/infrastructure/domains/shared/resource-discovery.js +192 -0
  125. package/infrastructure/domains/shared/resource-discovery.test.js +552 -0
  126. package/infrastructure/domains/shared/types/app-definition.js +205 -0
  127. package/infrastructure/domains/shared/types/discovery-result.js +106 -0
  128. package/infrastructure/domains/shared/types/discovery-result.test.js +258 -0
  129. package/infrastructure/domains/shared/types/index.js +46 -0
  130. package/infrastructure/domains/shared/types/resource-ownership.js +108 -0
  131. package/infrastructure/domains/shared/types/resource-ownership.test.js +101 -0
  132. package/infrastructure/domains/shared/utilities/base-definition-factory.js +380 -0
  133. package/infrastructure/domains/shared/utilities/base-definition-factory.js.bak +338 -0
  134. package/infrastructure/domains/shared/utilities/base-definition-factory.test.js +248 -0
  135. package/infrastructure/domains/shared/utilities/handler-path-resolver.js +134 -0
  136. package/infrastructure/domains/shared/utilities/handler-path-resolver.test.js +268 -0
  137. package/infrastructure/domains/shared/utilities/prisma-layer-manager.js +55 -0
  138. package/infrastructure/domains/shared/utilities/prisma-layer-manager.test.js +138 -0
  139. package/infrastructure/{env-validator.js → domains/shared/validation/env-validator.js} +2 -1
  140. package/infrastructure/domains/shared/validation/env-validator.test.js +173 -0
  141. package/infrastructure/esbuild.config.js +53 -0
  142. package/infrastructure/infrastructure-composer.js +87 -0
  143. package/infrastructure/{serverless-template.test.js → infrastructure-composer.test.js} +115 -24
  144. package/infrastructure/scripts/build-prisma-layer.js +553 -0
  145. package/infrastructure/scripts/build-prisma-layer.test.js +102 -0
  146. package/infrastructure/{build-time-discovery.js → scripts/build-time-discovery.js} +80 -48
  147. package/infrastructure/{build-time-discovery.test.js → scripts/build-time-discovery.test.js} +5 -4
  148. package/layers/prisma/nodejs/package.json +8 -0
  149. package/management-ui/server/utils/cliIntegration.js +1 -1
  150. package/management-ui/server/utils/environment/awsParameterStore.js +29 -18
  151. package/package.json +11 -11
  152. package/frigg-cli/.eslintrc.js +0 -141
  153. package/frigg-cli/__tests__/unit/commands/build.test.js +0 -251
  154. package/frigg-cli/__tests__/unit/commands/db-setup.test.js +0 -548
  155. package/frigg-cli/__tests__/unit/commands/install.test.js +0 -400
  156. package/frigg-cli/__tests__/unit/commands/ui.test.js +0 -346
  157. package/frigg-cli/__tests__/unit/utils/database-validator.test.js +0 -366
  158. package/frigg-cli/__tests__/unit/utils/error-messages.test.js +0 -304
  159. package/frigg-cli/__tests__/unit/utils/prisma-runner.test.js +0 -486
  160. package/frigg-cli/__tests__/utils/mock-factory.js +0 -270
  161. package/frigg-cli/__tests__/utils/prisma-mock.js +0 -194
  162. package/frigg-cli/__tests__/utils/test-fixtures.js +0 -463
  163. package/frigg-cli/__tests__/utils/test-setup.js +0 -287
  164. package/frigg-cli/build-command/index.js +0 -65
  165. package/frigg-cli/db-setup-command/index.js +0 -193
  166. package/frigg-cli/deploy-command/index.js +0 -175
  167. package/frigg-cli/generate-command/__tests__/generate-command.test.js +0 -301
  168. package/frigg-cli/generate-command/azure-generator.js +0 -43
  169. package/frigg-cli/generate-command/gcp-generator.js +0 -47
  170. package/frigg-cli/generate-command/index.js +0 -332
  171. package/frigg-cli/generate-command/terraform-generator.js +0 -555
  172. package/frigg-cli/generate-iam-command.js +0 -118
  173. package/frigg-cli/index.js +0 -75
  174. package/frigg-cli/index.test.js +0 -158
  175. package/frigg-cli/init-command/backend-first-handler.js +0 -756
  176. package/frigg-cli/init-command/index.js +0 -93
  177. package/frigg-cli/init-command/template-handler.js +0 -143
  178. package/frigg-cli/install-command/backend-js.js +0 -33
  179. package/frigg-cli/install-command/commit-changes.js +0 -16
  180. package/frigg-cli/install-command/environment-variables.js +0 -127
  181. package/frigg-cli/install-command/environment-variables.test.js +0 -136
  182. package/frigg-cli/install-command/index.js +0 -54
  183. package/frigg-cli/install-command/install-package.js +0 -13
  184. package/frigg-cli/install-command/integration-file.js +0 -30
  185. package/frigg-cli/install-command/logger.js +0 -12
  186. package/frigg-cli/install-command/template.js +0 -90
  187. package/frigg-cli/install-command/validate-package.js +0 -75
  188. package/frigg-cli/jest.config.js +0 -124
  189. package/frigg-cli/package.json +0 -54
  190. package/frigg-cli/start-command/index.js +0 -149
  191. package/frigg-cli/start-command/start-command.test.js +0 -297
  192. package/frigg-cli/test/init-command.test.js +0 -180
  193. package/frigg-cli/test/npm-registry.test.js +0 -319
  194. package/frigg-cli/ui-command/index.js +0 -154
  195. package/frigg-cli/utils/app-resolver.js +0 -319
  196. package/frigg-cli/utils/backend-path.js +0 -25
  197. package/frigg-cli/utils/database-validator.js +0 -161
  198. package/frigg-cli/utils/error-messages.js +0 -257
  199. package/frigg-cli/utils/npm-registry.js +0 -167
  200. package/frigg-cli/utils/prisma-runner.js +0 -280
  201. package/frigg-cli/utils/process-manager.js +0 -199
  202. package/frigg-cli/utils/repo-detection.js +0 -405
  203. package/infrastructure/aws-discovery.js +0 -1176
  204. package/infrastructure/aws-discovery.test.js +0 -1220
  205. package/infrastructure/serverless-template.js +0 -2094
  206. /package/infrastructure/{WEBSOCKET-CONFIGURATION.md → docs/WEBSOCKET-CONFIGURATION.md} +0 -0
  207. /package/infrastructure/{GENERATE-IAM-DOCS.md → docs/generate-iam-command.md} +0 -0
  208. /package/infrastructure/{iam-generator.test.js → domains/security/iam-generator.test.js} +0 -0
  209. /package/infrastructure/{frigg-deployment-iam-stack.yaml → domains/security/templates/frigg-deployment-iam-stack.yaml} +0 -0
  210. /package/infrastructure/{iam-policy-basic.json → domains/security/templates/iam-policy-basic.json} +0 -0
  211. /package/infrastructure/{iam-policy-full.json → domains/security/templates/iam-policy-full.json} +0 -0
  212. /package/infrastructure/{run-discovery.js → scripts/run-discovery.js} +0 -0
@@ -0,0 +1,501 @@
1
+ const VpcResourceResolver = require('./vpc-resolver');
2
+ const { ResourceOwnership } = require('../shared/types');
3
+
4
+ describe('VpcResourceResolver', () => {
5
+ let resolver;
6
+
7
+ beforeEach(() => {
8
+ resolver = new VpcResourceResolver();
9
+ });
10
+
11
+ describe('resolveVpc', () => {
12
+ it('should resolve to EXTERNAL when user specifies external', () => {
13
+ const appDefinition = {
14
+ vpc: {
15
+ ownership: { vpc: 'external' },
16
+ external: { vpcId: 'vpc-external-123' }
17
+ }
18
+ };
19
+ const discovery = { stackManaged: [], external: [], fromCloudFormation: false };
20
+
21
+ const decision = resolver.resolveVpc(appDefinition, discovery);
22
+
23
+ expect(decision.ownership).toBe(ResourceOwnership.EXTERNAL);
24
+ expect(decision.physicalId).toBe('vpc-external-123');
25
+ expect(decision.reason).toContain('User specified ownership=external');
26
+ });
27
+
28
+ it('should throw when external specified but vpcId missing', () => {
29
+ const appDefinition = {
30
+ vpc: {
31
+ ownership: { vpc: 'external' },
32
+ external: {}
33
+ }
34
+ };
35
+ const discovery = { stackManaged: [], external: [], fromCloudFormation: false };
36
+
37
+ expect(() => resolver.resolveVpc(appDefinition, discovery)).toThrow(
38
+ "ownership='external' for vpcId requires external.vpcId"
39
+ );
40
+ });
41
+
42
+ it('should resolve to STACK when user specifies stack', () => {
43
+ const appDefinition = {
44
+ vpc: {
45
+ ownership: { vpc: 'stack' }
46
+ }
47
+ };
48
+ const discovery = {
49
+ stackManaged: [
50
+ { logicalId: 'FriggVPC', physicalId: 'vpc-stack-123', resourceType: 'AWS::EC2::VPC' }
51
+ ],
52
+ external: [],
53
+ fromCloudFormation: true
54
+ };
55
+
56
+ const decision = resolver.resolveVpc(appDefinition, discovery);
57
+
58
+ expect(decision.ownership).toBe(ResourceOwnership.STACK);
59
+ expect(decision.physicalId).toBe('vpc-stack-123');
60
+ expect(decision.reason).toContain('User specified ownership=stack');
61
+ });
62
+
63
+ it('should auto-resolve to STACK when VPC in stack (CRITICAL)', () => {
64
+ const appDefinition = {
65
+ vpc: { ownership: { vpc: 'auto' } }
66
+ };
67
+ const discovery = {
68
+ stackManaged: [
69
+ { logicalId: 'FriggVPC', physicalId: 'vpc-in-stack', resourceType: 'AWS::EC2::VPC' }
70
+ ],
71
+ external: [],
72
+ fromCloudFormation: true
73
+ };
74
+
75
+ const decision = resolver.resolveVpc(appDefinition, discovery);
76
+
77
+ expect(decision.ownership).toBe(ResourceOwnership.STACK);
78
+ expect(decision.physicalId).toBe('vpc-in-stack');
79
+ expect(decision.reason).toContain('Found in CloudFormation stack');
80
+ });
81
+
82
+ it('should auto-resolve to EXTERNAL when found externally', () => {
83
+ const appDefinition = {
84
+ vpc: { ownership: { vpc: 'auto' } }
85
+ };
86
+ const discovery = {
87
+ stackManaged: [],
88
+ external: [
89
+ { physicalId: 'vpc-external', resourceType: 'AWS::EC2::VPC', source: 'tag-search' }
90
+ ],
91
+ fromCloudFormation: false
92
+ };
93
+
94
+ const decision = resolver.resolveVpc(appDefinition, discovery);
95
+
96
+ expect(decision.ownership).toBe(ResourceOwnership.EXTERNAL);
97
+ expect(decision.physicalId).toBe('vpc-external');
98
+ });
99
+
100
+ it('should auto-resolve to STACK when not found (create new)', () => {
101
+ const appDefinition = {
102
+ vpc: { ownership: { vpc: 'auto' } }
103
+ };
104
+ const discovery = { stackManaged: [], external: [], fromCloudFormation: false };
105
+
106
+ const decision = resolver.resolveVpc(appDefinition, discovery);
107
+
108
+ expect(decision.ownership).toBe(ResourceOwnership.STACK);
109
+ expect(decision.physicalId).toBeUndefined();
110
+ expect(decision.reason).toContain('No existing resource found');
111
+ });
112
+ });
113
+
114
+ describe('resolveSecurityGroup', () => {
115
+ it('should resolve to EXTERNAL with user-provided IDs', () => {
116
+ const appDefinition = {
117
+ vpc: {
118
+ ownership: { securityGroup: 'external' },
119
+ external: { securityGroupIds: ['sg-1', 'sg-2'] }
120
+ }
121
+ };
122
+ const discovery = { stackManaged: [], external: [], fromCloudFormation: false };
123
+
124
+ const decision = resolver.resolveSecurityGroup(appDefinition, discovery);
125
+
126
+ expect(decision.ownership).toBe(ResourceOwnership.EXTERNAL);
127
+ expect(decision.physicalIds).toEqual(['sg-1', 'sg-2']);
128
+ });
129
+
130
+ it('should auto-resolve to STACK when FriggLambdaSecurityGroup in stack', () => {
131
+ const appDefinition = { vpc: { ownership: { securityGroup: 'auto' } } };
132
+ const discovery = {
133
+ stackManaged: [
134
+ { logicalId: 'FriggLambdaSecurityGroup', physicalId: 'sg-069629001ade41c9a', resourceType: 'AWS::EC2::SecurityGroup' }
135
+ ],
136
+ external: [],
137
+ fromCloudFormation: true
138
+ };
139
+
140
+ const decision = resolver.resolveSecurityGroup(appDefinition, discovery);
141
+
142
+ expect(decision.ownership).toBe(ResourceOwnership.STACK);
143
+ expect(decision.physicalId).toBe('sg-069629001ade41c9a');
144
+ expect(decision.reason).toContain('Found FriggLambdaSecurityGroup in CloudFormation stack');
145
+ });
146
+ });
147
+
148
+ describe('resolveSubnets', () => {
149
+ it('should resolve to EXTERNAL with user-provided subnet IDs', () => {
150
+ const appDefinition = {
151
+ vpc: {
152
+ ownership: { subnets: 'external' },
153
+ external: { subnetIds: ['subnet-1', 'subnet-2', 'subnet-3'] }
154
+ }
155
+ };
156
+ const discovery = { stackManaged: [], external: [], fromCloudFormation: false };
157
+
158
+ const decision = resolver.resolveSubnets(appDefinition, discovery);
159
+
160
+ expect(decision.ownership).toBe(ResourceOwnership.EXTERNAL);
161
+ expect(decision.physicalIds).toEqual(['subnet-1', 'subnet-2', 'subnet-3']);
162
+ });
163
+
164
+ it('should resolve to STACK when subnets found in stack', () => {
165
+ const appDefinition = { vpc: { ownership: { subnets: 'auto' } } };
166
+ const discovery = {
167
+ stackManaged: [
168
+ { logicalId: 'FriggPrivateSubnet1', physicalId: 'subnet-a', resourceType: 'AWS::EC2::Subnet' },
169
+ { logicalId: 'FriggPrivateSubnet2', physicalId: 'subnet-b', resourceType: 'AWS::EC2::Subnet' }
170
+ ],
171
+ external: [],
172
+ fromCloudFormation: true
173
+ };
174
+
175
+ const decision = resolver.resolveSubnets(appDefinition, discovery);
176
+
177
+ expect(decision.ownership).toBe(ResourceOwnership.STACK);
178
+ expect(decision.physicalIds).toEqual(['subnet-a', 'subnet-b']);
179
+ expect(decision.metadata.subnet1).toBe('subnet-a');
180
+ expect(decision.metadata.subnet2).toBe('subnet-b');
181
+ });
182
+
183
+ it('should resolve to EXTERNAL when found externally', () => {
184
+ const appDefinition = { vpc: { ownership: { subnets: 'auto' } } };
185
+ const discovery = {
186
+ stackManaged: [],
187
+ external: [
188
+ { physicalId: 'subnet-ext-1', resourceType: 'AWS::EC2::Subnet', source: 'tag-search' },
189
+ { physicalId: 'subnet-ext-2', resourceType: 'AWS::EC2::Subnet', source: 'tag-search' },
190
+ { physicalId: 'subnet-ext-3', resourceType: 'AWS::EC2::Subnet', source: 'tag-search' }
191
+ ],
192
+ fromCloudFormation: false
193
+ };
194
+
195
+ const decision = resolver.resolveSubnets(appDefinition, discovery);
196
+
197
+ expect(decision.ownership).toBe(ResourceOwnership.EXTERNAL);
198
+ expect(decision.physicalIds).toHaveLength(2); // Takes first 2
199
+ expect(decision.physicalIds).toEqual(['subnet-ext-1', 'subnet-ext-2']);
200
+ });
201
+
202
+ it('should resolve to STACK when no subnets found (create new)', () => {
203
+ const appDefinition = { vpc: { ownership: { subnets: 'auto' } } };
204
+ const discovery = { stackManaged: [], external: [], fromCloudFormation: false };
205
+
206
+ const decision = resolver.resolveSubnets(appDefinition, discovery);
207
+
208
+ expect(decision.ownership).toBe(ResourceOwnership.STACK);
209
+ expect(decision.physicalId).toBeNull();
210
+ expect(decision.reason).toContain('No existing subnets found');
211
+ });
212
+ });
213
+
214
+ describe('resolveNatGateway', () => {
215
+ it('should return null decision when NAT disabled', () => {
216
+ const appDefinition = {
217
+ vpc: {
218
+ ownership: { natGateway: 'auto' },
219
+ config: { natGateway: { enable: false } }
220
+ }
221
+ };
222
+ const discovery = { stackManaged: [], external: [], fromCloudFormation: false };
223
+
224
+ const decision = resolver.resolveNatGateway(appDefinition, discovery);
225
+
226
+ expect(decision.ownership).toBeNull();
227
+ expect(decision.reason).toContain('NAT Gateway disabled');
228
+ });
229
+
230
+ it('should resolve to EXTERNAL with user-provided ID', () => {
231
+ const appDefinition = {
232
+ vpc: {
233
+ ownership: { natGateway: 'external' },
234
+ external: { natGatewayId: 'nat-external-123' }
235
+ }
236
+ };
237
+ const discovery = { stackManaged: [], external: [], fromCloudFormation: false };
238
+
239
+ const decision = resolver.resolveNatGateway(appDefinition, discovery);
240
+
241
+ expect(decision.ownership).toBe(ResourceOwnership.EXTERNAL);
242
+ expect(decision.physicalId).toBe('nat-external-123');
243
+ });
244
+
245
+ it('should auto-resolve to STACK when found in stack', () => {
246
+ const appDefinition = { vpc: { ownership: { natGateway: 'auto' } } };
247
+ const discovery = {
248
+ stackManaged: [
249
+ { logicalId: 'FriggNatGateway', physicalId: 'nat-stack-123', resourceType: 'AWS::EC2::NatGateway' }
250
+ ],
251
+ external: [],
252
+ fromCloudFormation: true
253
+ };
254
+
255
+ const decision = resolver.resolveNatGateway(appDefinition, discovery);
256
+
257
+ expect(decision.ownership).toBe(ResourceOwnership.STACK);
258
+ expect(decision.physicalId).toBe('nat-stack-123');
259
+ });
260
+ });
261
+
262
+ describe('resolveVpcEndpoints', () => {
263
+ it('should return null decisions when endpoints disabled', () => {
264
+ const appDefinition = {
265
+ vpc: {
266
+ ownership: { vpcEndpoints: 'auto' },
267
+ config: { enableVpcEndpoints: false }
268
+ }
269
+ };
270
+ const discovery = { stackManaged: [], external: [], fromCloudFormation: false };
271
+
272
+ const decisions = resolver.resolveVpcEndpoints(appDefinition, discovery);
273
+
274
+ expect(decisions.s3.ownership).toBeNull();
275
+ expect(decisions.dynamodb.ownership).toBeNull();
276
+ expect(decisions.kms.ownership).toBeNull();
277
+ expect(decisions.secretsManager.ownership).toBeNull();
278
+ expect(decisions.sqs.ownership).toBeNull();
279
+ });
280
+
281
+ it('should resolve to EXTERNAL with user-provided endpoint IDs', () => {
282
+ const appDefinition = {
283
+ vpc: {
284
+ ownership: { vpcEndpoints: 'external' },
285
+ external: {
286
+ vpcEndpointIds: {
287
+ s3: 'vpce-s3-123',
288
+ dynamodb: 'vpce-ddb-456'
289
+ }
290
+ }
291
+ }
292
+ };
293
+ const discovery = { stackManaged: [], external: [], fromCloudFormation: false };
294
+
295
+ const decisions = resolver.resolveVpcEndpoints(appDefinition, discovery);
296
+
297
+ expect(decisions.s3.ownership).toBe(ResourceOwnership.EXTERNAL);
298
+ expect(decisions.s3.physicalId).toBe('vpce-s3-123');
299
+ expect(decisions.dynamodb.ownership).toBe(ResourceOwnership.EXTERNAL);
300
+ expect(decisions.dynamodb.physicalId).toBe('vpce-ddb-456');
301
+ expect(decisions.kms.ownership).toBeNull(); // Not provided
302
+ });
303
+
304
+ it('should auto-resolve to STACK when endpoints found in stack', () => {
305
+ const appDefinition = { vpc: { ownership: { vpcEndpoints: 'auto' } } };
306
+ const discovery = {
307
+ stackManaged: [
308
+ { logicalId: 'FriggS3VPCEndpoint', physicalId: 'vpce-s3-stack', resourceType: 'AWS::EC2::VPCEndpoint' },
309
+ { logicalId: 'FriggDynamoDBVPCEndpoint', physicalId: 'vpce-ddb-stack', resourceType: 'AWS::EC2::VPCEndpoint' }
310
+ ],
311
+ external: [],
312
+ fromCloudFormation: true
313
+ };
314
+
315
+ const decisions = resolver.resolveVpcEndpoints(appDefinition, discovery);
316
+
317
+ expect(decisions.s3.ownership).toBe(ResourceOwnership.STACK);
318
+ expect(decisions.s3.physicalId).toBe('vpce-s3-stack');
319
+ expect(decisions.dynamodb.ownership).toBe(ResourceOwnership.STACK);
320
+ expect(decisions.dynamodb.physicalId).toBe('vpce-ddb-stack');
321
+ });
322
+
323
+ it('should auto-resolve mixed: some in stack, some new', () => {
324
+ const appDefinition = {
325
+ vpc: { ownership: { vpcEndpoints: 'auto' } },
326
+ encryption: { fieldLevelEncryptionMethod: 'kms' } // Enable KMS endpoint
327
+ };
328
+ const discovery = {
329
+ stackManaged: [
330
+ { logicalId: 'FriggS3VPCEndpoint', physicalId: 'vpce-s3-stack', resourceType: 'AWS::EC2::VPCEndpoint' }
331
+ ],
332
+ external: [],
333
+ fromCloudFormation: true
334
+ };
335
+
336
+ const decisions = resolver.resolveVpcEndpoints(appDefinition, discovery);
337
+
338
+ expect(decisions.s3.ownership).toBe(ResourceOwnership.STACK);
339
+ expect(decisions.s3.physicalId).toBe('vpce-s3-stack');
340
+
341
+ // Others not in stack - should create new
342
+ expect(decisions.dynamodb.ownership).toBe(ResourceOwnership.STACK);
343
+ expect(decisions.dynamodb.physicalId).toBeUndefined();
344
+ expect(decisions.kms.ownership).toBe(ResourceOwnership.STACK);
345
+ expect(decisions.secretsManager.ownership).toBe(ResourceOwnership.STACK);
346
+ expect(decisions.sqs.ownership).toBe(ResourceOwnership.STACK);
347
+ });
348
+ });
349
+
350
+ describe('resolveAll', () => {
351
+ it('should resolve all VPC resources at once', () => {
352
+ const appDefinition = {
353
+ vpc: {
354
+ ownership: {
355
+ vpc: 'auto',
356
+ securityGroup: 'auto',
357
+ subnets: 'auto',
358
+ natGateway: 'auto',
359
+ vpcEndpoints: 'auto'
360
+ }
361
+ }
362
+ };
363
+ const discovery = {
364
+ stackManaged: [
365
+ { logicalId: 'FriggVPC', physicalId: 'vpc-123', resourceType: 'AWS::EC2::VPC' },
366
+ { logicalId: 'FriggLambdaSecurityGroup', physicalId: 'sg-456', resourceType: 'AWS::EC2::SecurityGroup' },
367
+ { logicalId: 'FriggPrivateSubnet1', physicalId: 'subnet-1', resourceType: 'AWS::EC2::Subnet' },
368
+ { logicalId: 'FriggPrivateSubnet2', physicalId: 'subnet-2', resourceType: 'AWS::EC2::Subnet' },
369
+ { logicalId: 'FriggNatGateway', physicalId: 'nat-789', resourceType: 'AWS::EC2::NatGateway' },
370
+ { logicalId: 'FriggS3VPCEndpoint', physicalId: 'vpce-s3', resourceType: 'AWS::EC2::VPCEndpoint' }
371
+ ],
372
+ external: [],
373
+ fromCloudFormation: true
374
+ };
375
+
376
+ const decisions = resolver.resolveAll(appDefinition, discovery);
377
+
378
+ expect(decisions.vpc.ownership).toBe(ResourceOwnership.STACK);
379
+ expect(decisions.securityGroup.ownership).toBe(ResourceOwnership.STACK);
380
+ expect(decisions.subnets.ownership).toBe(ResourceOwnership.STACK);
381
+ expect(decisions.natGateway.ownership).toBe(ResourceOwnership.STACK);
382
+ expect(decisions.vpcEndpoints.s3.ownership).toBe(ResourceOwnership.STACK);
383
+ });
384
+
385
+ it('should handle mixed ownership scenarios', () => {
386
+ const appDefinition = {
387
+ vpc: {
388
+ ownership: {
389
+ vpc: 'external',
390
+ securityGroup: 'stack',
391
+ subnets: 'stack',
392
+ natGateway: 'auto',
393
+ vpcEndpoints: 'auto'
394
+ },
395
+ external: {
396
+ vpcId: 'vpc-shared-production'
397
+ }
398
+ }
399
+ };
400
+ const discovery = {
401
+ stackManaged: [
402
+ { logicalId: 'FriggLambdaSecurityGroup', physicalId: 'sg-stack', resourceType: 'AWS::EC2::SecurityGroup' },
403
+ { logicalId: 'FriggPrivateSubnet1', physicalId: 'subnet-1', resourceType: 'AWS::EC2::Subnet' },
404
+ { logicalId: 'FriggPrivateSubnet2', physicalId: 'subnet-2', resourceType: 'AWS::EC2::Subnet' }
405
+ ],
406
+ external: [],
407
+ fromCloudFormation: true
408
+ };
409
+
410
+ const decisions = resolver.resolveAll(appDefinition, discovery);
411
+
412
+ expect(decisions.vpc.ownership).toBe(ResourceOwnership.EXTERNAL);
413
+ expect(decisions.vpc.physicalId).toBe('vpc-shared-production');
414
+ expect(decisions.securityGroup.ownership).toBe(ResourceOwnership.STACK);
415
+ expect(decisions.subnets.ownership).toBe(ResourceOwnership.STACK);
416
+ expect(decisions.natGateway.ownership).toBe(ResourceOwnership.STACK); // Not found, create new
417
+ });
418
+ });
419
+
420
+ describe('real-world scenarios', () => {
421
+ it('scenario: fresh deploy, no resources exist', () => {
422
+ const appDefinition = {
423
+ vpc: { enable: true, ownership: {} }
424
+ };
425
+ const discovery = { stackManaged: [], external: [], fromCloudFormation: false };
426
+
427
+ const decisions = resolver.resolveAll(appDefinition, discovery);
428
+
429
+ // All should be STACK (create new)
430
+ expect(decisions.vpc.ownership).toBe(ResourceOwnership.STACK);
431
+ expect(decisions.securityGroup.ownership).toBe(ResourceOwnership.STACK);
432
+ expect(decisions.subnets.ownership).toBe(ResourceOwnership.STACK);
433
+ expect(decisions.natGateway.ownership).toBe(ResourceOwnership.STACK);
434
+ });
435
+
436
+ it('scenario: redeploy existing stack (the original bug case)', () => {
437
+ const appDefinition = {
438
+ vpc: { enable: true, ownership: {} }
439
+ };
440
+ const discovery = {
441
+ stackManaged: [
442
+ { logicalId: 'FriggVPC', physicalId: 'vpc-123', resourceType: 'AWS::EC2::VPC' },
443
+ { logicalId: 'FriggLambdaSecurityGroup', physicalId: 'sg-069629001ade41c9a', resourceType: 'AWS::EC2::SecurityGroup' },
444
+ { logicalId: 'FriggPrivateSubnet1', physicalId: 'subnet-1', resourceType: 'AWS::EC2::Subnet' },
445
+ { logicalId: 'FriggPrivateSubnet2', physicalId: 'subnet-2', resourceType: 'AWS::EC2::Subnet' }
446
+ ],
447
+ external: [],
448
+ fromCloudFormation: true,
449
+ stackName: 'create-frigg-app-production'
450
+ };
451
+
452
+ const decisions = resolver.resolveAll(appDefinition, discovery);
453
+
454
+ // CRITICAL: All resources in stack must get STACK ownership
455
+ expect(decisions.vpc.ownership).toBe(ResourceOwnership.STACK);
456
+ expect(decisions.vpc.physicalId).toBe('vpc-123');
457
+
458
+ expect(decisions.securityGroup.ownership).toBe(ResourceOwnership.STACK);
459
+ expect(decisions.securityGroup.physicalId).toBe('sg-069629001ade41c9a');
460
+ expect(decisions.securityGroup.reason).toContain('Found FriggLambdaSecurityGroup in CloudFormation stack');
461
+
462
+ expect(decisions.subnets.ownership).toBe(ResourceOwnership.STACK);
463
+ expect(decisions.subnets.physicalIds).toEqual(['subnet-1', 'subnet-2']);
464
+ });
465
+
466
+ it('scenario: use shared VPC with stack-managed resources', () => {
467
+ const appDefinition = {
468
+ vpc: {
469
+ enable: true,
470
+ ownership: {
471
+ vpc: 'external',
472
+ securityGroup: 'auto',
473
+ subnets: 'auto'
474
+ },
475
+ external: {
476
+ vpcId: 'vpc-shared-across-stages'
477
+ }
478
+ }
479
+ };
480
+ const discovery = {
481
+ stackManaged: [
482
+ { logicalId: 'FriggLambdaSecurityGroup', physicalId: 'sg-stage-specific', resourceType: 'AWS::EC2::SecurityGroup' },
483
+ { logicalId: 'FriggPrivateSubnet1', physicalId: 'subnet-1', resourceType: 'AWS::EC2::Subnet' },
484
+ { logicalId: 'FriggPrivateSubnet2', physicalId: 'subnet-2', resourceType: 'AWS::EC2::Subnet' }
485
+ ],
486
+ external: [],
487
+ fromCloudFormation: true
488
+ };
489
+
490
+ const decisions = resolver.resolveAll(appDefinition, discovery);
491
+
492
+ // VPC is external
493
+ expect(decisions.vpc.ownership).toBe(ResourceOwnership.EXTERNAL);
494
+ expect(decisions.vpc.physicalId).toBe('vpc-shared-across-stages');
495
+
496
+ // But security group and subnets are stack-managed
497
+ expect(decisions.securityGroup.ownership).toBe(ResourceOwnership.STACK);
498
+ expect(decisions.subnets.ownership).toBe(ResourceOwnership.STACK);
499
+ });
500
+ });
501
+ });
@@ -0,0 +1,79 @@
1
+ /**
2
+ * SSM Parameter Store Builder
3
+ *
4
+ * Domain Layer - Hexagonal Architecture
5
+ *
6
+ * Responsible for:
7
+ * - Configuring IAM permissions for SSM Parameter Store access
8
+ * - Setting up SSM parameter references for Lambda functions
9
+ */
10
+
11
+ const { InfrastructureBuilder, ValidationResult } = require('../shared/base-builder');
12
+
13
+ class SsmBuilder extends InfrastructureBuilder {
14
+ constructor() {
15
+ super();
16
+ this.name = 'SsmBuilder';
17
+ }
18
+
19
+ shouldExecute(appDefinition) {
20
+ // Skip SSM in local mode (when FRIGG_SKIP_AWS_DISCOVERY is set)
21
+ // SSM Parameter Store is an AWS-specific service that should only be used in production
22
+ if (process.env.FRIGG_SKIP_AWS_DISCOVERY === 'true') {
23
+ return false;
24
+ }
25
+
26
+ return appDefinition.ssm?.enable === true;
27
+ }
28
+
29
+ validate(appDefinition) {
30
+ const result = new ValidationResult();
31
+
32
+ if (!appDefinition.ssm) {
33
+ result.addError('SSM configuration is missing');
34
+ return result;
35
+ }
36
+
37
+ // Validate parameters if provided
38
+ if (appDefinition.ssm.parameters) {
39
+ if (typeof appDefinition.ssm.parameters !== 'object' || Array.isArray(appDefinition.ssm.parameters)) {
40
+ result.addError('ssm.parameters must be an object (not an array)');
41
+ }
42
+ }
43
+
44
+ return result;
45
+ }
46
+
47
+ /**
48
+ * Build SSM configuration
49
+ */
50
+ async build(appDefinition, discoveredResources) {
51
+ console.log(`\n[${this.name}] Configuring SSM Parameter Store...`);
52
+
53
+ const result = {
54
+ iamStatements: [],
55
+ environment: {},
56
+ };
57
+
58
+ // Add IAM permissions for SSM Parameter Store
59
+ result.iamStatements.push({
60
+ Effect: 'Allow',
61
+ Action: [
62
+ 'ssm:GetParameter',
63
+ 'ssm:GetParameters',
64
+ 'ssm:GetParametersByPath',
65
+ ],
66
+ Resource: {
67
+ 'Fn::Sub': 'arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/*',
68
+ },
69
+ });
70
+
71
+ console.log(' ✅ SSM Parameter Store IAM permissions added');
72
+ console.log(`[${this.name}] ✅ SSM configuration completed`);
73
+
74
+ return result;
75
+ }
76
+ }
77
+
78
+ module.exports = { SsmBuilder };
79
+