@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,366 @@
1
+ /**
2
+ * KMS (Key Management Service) Builder
3
+ *
4
+ * Domain Layer - Hexagonal Architecture
5
+ *
6
+ * Responsible for:
7
+ * - KMS key creation or discovery
8
+ * - KMS key configuration for field-level encryption
9
+ * - IAM permissions for KMS operations
10
+ * - KMS key policy configuration for Lambda execution role
11
+ */
12
+
13
+ const { InfrastructureBuilder, ValidationResult } = require('../shared/base-builder');
14
+ const { KmsResourceResolver } = require('./kms-resolver');
15
+ const { createEmptyDiscoveryResult, ResourceOwnership } = require('../shared/types');
16
+
17
+ class KmsBuilder extends InfrastructureBuilder {
18
+ constructor() {
19
+ super();
20
+ this.name = 'KmsBuilder';
21
+ }
22
+
23
+ shouldExecute(appDefinition) {
24
+ // Skip KMS in local mode (when FRIGG_SKIP_AWS_DISCOVERY is set)
25
+ // KMS is an AWS-specific service that should only be created in production
26
+ if (process.env.FRIGG_SKIP_AWS_DISCOVERY === 'true') {
27
+ return false;
28
+ }
29
+
30
+ return appDefinition.encryption?.fieldLevelEncryptionMethod === 'kms';
31
+ }
32
+
33
+ validate(appDefinition) {
34
+ const result = new ValidationResult();
35
+
36
+ if (!appDefinition.encryption) {
37
+ result.addError('Encryption configuration is missing');
38
+ return result;
39
+ }
40
+
41
+ const encryption = appDefinition.encryption;
42
+
43
+ if (encryption.fieldLevelEncryptionMethod !== 'kms') {
44
+ // Not an error - just not applicable
45
+ return result;
46
+ }
47
+
48
+ // Validate createResourceIfNoneFound is boolean
49
+ if (encryption.createResourceIfNoneFound !== undefined &&
50
+ typeof encryption.createResourceIfNoneFound !== 'boolean') {
51
+ result.addError('encryption.createResourceIfNoneFound must be a boolean');
52
+ }
53
+
54
+ return result;
55
+ }
56
+
57
+ /**
58
+ * Build KMS infrastructure using ownership-based architecture
59
+ */
60
+ async build(appDefinition, discoveredResources) {
61
+ console.log(`\n[${this.name}] Configuring KMS encryption...`);
62
+
63
+ // Backwards compatibility: Translate old schema to new ownership schema
64
+ appDefinition = this.translateLegacyConfig(appDefinition, discoveredResources);
65
+
66
+ const result = {
67
+ resources: {},
68
+ iamStatements: [],
69
+ environment: {},
70
+ pluginConfig: {},
71
+ plugins: [],
72
+ };
73
+
74
+ // Get structured discovery result
75
+ const discovery = discoveredResources._structured || this.convertFlatDiscoveryToStructured(discoveredResources, appDefinition);
76
+
77
+ // Use KmsResourceResolver to make ownership decisions
78
+ const resolver = new KmsResourceResolver();
79
+ const decisions = resolver.resolveAll(appDefinition, discovery);
80
+
81
+ console.log('\n 📋 Resource Ownership Decisions:');
82
+ console.log(` Key: ${decisions.key.ownership} - ${decisions.key.reason}`);
83
+
84
+ // Build resources based on ownership decisions
85
+ await this.buildFromDecisions(decisions, appDefinition, discoveredResources, result);
86
+
87
+ // Add IAM permissions for Lambda role
88
+ result.iamStatements.push({
89
+ Effect: 'Allow',
90
+ Action: ['kms:GenerateDataKey', 'kms:Decrypt', 'kms:Encrypt', 'kms:DescribeKey'],
91
+ Resource: result.environment.KMS_KEY_ARN,
92
+ });
93
+
94
+ console.log(`[${this.name}] ✅ KMS configuration completed`);
95
+ return result;
96
+ }
97
+
98
+ /**
99
+ * Convert flat discovery to structured discovery
100
+ * Provides backwards compatibility for tests
101
+ */
102
+ convertFlatDiscoveryToStructured(flatDiscovery, appDefinition = {}) {
103
+ const discovery = createEmptyDiscoveryResult();
104
+
105
+ if (!flatDiscovery) {
106
+ return discovery;
107
+ }
108
+
109
+ // Check if resources are from CloudFormation stack
110
+ const isManagedIsolated = appDefinition.managementMode === 'managed' &&
111
+ (appDefinition.vpcIsolation === 'isolated' || !appDefinition.vpcIsolation);
112
+ const hasExistingStackResources = isManagedIsolated && flatDiscovery.defaultKmsKeyId &&
113
+ typeof flatDiscovery.defaultKmsKeyId === 'string';
114
+
115
+ if (flatDiscovery.fromCloudFormationStack || hasExistingStackResources) {
116
+ discovery.fromCloudFormation = true;
117
+ discovery.stackName = flatDiscovery.stackName || 'assumed-stack';
118
+
119
+ // Add stack-managed resources
120
+ let existingLogicalIds = flatDiscovery.existingLogicalIds || [];
121
+
122
+ // Infer logical IDs from physical IDs if needed
123
+ if (hasExistingStackResources && existingLogicalIds.length === 0) {
124
+ if (flatDiscovery.defaultKmsKeyId) {
125
+ existingLogicalIds.push('FriggKMSKey');
126
+ existingLogicalIds.push('FriggKMSKeyAlias');
127
+ }
128
+ }
129
+
130
+ existingLogicalIds.forEach(logicalId => {
131
+ let resourceType = '';
132
+ let physicalId = '';
133
+
134
+ if (logicalId === 'FriggKMSKey') {
135
+ resourceType = 'AWS::KMS::Key';
136
+ physicalId = flatDiscovery.defaultKmsKeyId;
137
+ } else if (logicalId === 'FriggKMSKeyAlias') {
138
+ resourceType = 'AWS::KMS::Alias';
139
+ // Extract alias name from KMS key ARN or use default pattern
140
+ const stackName = flatDiscovery.stackName || 'unknown';
141
+ const stage = appDefinition.stage || 'dev';
142
+ physicalId = `alias/${stackName.replace(`-${stage}`, '')}-${stage}-frigg-kms`;
143
+ }
144
+
145
+ if (physicalId && typeof physicalId === 'string') {
146
+ discovery.stackManaged.push({
147
+ logicalId,
148
+ physicalId,
149
+ resourceType
150
+ });
151
+ }
152
+ });
153
+ } else {
154
+ // Resources discovered from AWS API (external)
155
+ if (flatDiscovery.defaultKmsKeyId && typeof flatDiscovery.defaultKmsKeyId === 'string') {
156
+ discovery.external.push({
157
+ physicalId: flatDiscovery.defaultKmsKeyId,
158
+ resourceType: 'AWS::KMS::Key',
159
+ source: 'aws-discovery'
160
+ });
161
+ }
162
+ }
163
+
164
+ return discovery;
165
+ }
166
+
167
+ /**
168
+ * Translate legacy configuration to ownership-based configuration
169
+ * Provides backwards compatibility
170
+ */
171
+ translateLegacyConfig(appDefinition, discoveredResources) {
172
+ // If already using ownership schema, return as-is
173
+ if (appDefinition.encryption?.ownership) {
174
+ return appDefinition;
175
+ }
176
+
177
+ const translated = JSON.parse(JSON.stringify(appDefinition));
178
+
179
+ // Initialize ownership sections
180
+ if (!translated.encryption) translated.encryption = {};
181
+ if (!translated.encryption.ownership) {
182
+ translated.encryption.ownership = {};
183
+ }
184
+
185
+ // Handle top-level managementMode
186
+ const globalMode = appDefinition.managementMode || 'discover';
187
+ const vpcIsolation = appDefinition.vpcIsolation || 'shared';
188
+
189
+ if (globalMode === 'managed') {
190
+ if (appDefinition.encryption?.createResourceIfNoneFound !== undefined) {
191
+ console.log(` ⚠️ managementMode='managed' ignoring: encryption.createResourceIfNoneFound`);
192
+ }
193
+
194
+ if (vpcIsolation === 'isolated') {
195
+ const hasStackKms = discoveredResources?.defaultKmsKeyId &&
196
+ typeof discoveredResources.defaultKmsKeyId === 'string';
197
+
198
+ if (hasStackKms) {
199
+ translated.encryption.ownership.key = 'auto';
200
+ console.log(` managementMode='managed' + vpcIsolation='isolated' → stack has KMS, reusing`);
201
+ } else {
202
+ translated.encryption.ownership.key = 'stack';
203
+ console.log(` managementMode='managed' + vpcIsolation='isolated' → no stack KMS, creating new`);
204
+ }
205
+ } else {
206
+ translated.encryption.ownership.key = 'auto';
207
+ console.log(` managementMode='managed' + vpcIsolation='shared' → discovering KMS`);
208
+ }
209
+ } else {
210
+ // Handle legacy createResourceIfNoneFound
211
+ const createIfNoneFound = appDefinition.encryption?.createResourceIfNoneFound;
212
+ if (createIfNoneFound === true) {
213
+ translated.encryption.ownership.key = 'stack';
214
+ } else if (createIfNoneFound === false || createIfNoneFound === undefined) {
215
+ // When createResourceIfNoneFound is false or not specified:
216
+ // - If KMS found → use it (auto)
217
+ // - If not found → use environment variable (external)
218
+ // We use 'auto' here; the resolver will decide based on discovery
219
+ // But we need special handling in buildFromDecisions for the env var fallback
220
+ translated.encryption.ownership.key = 'auto';
221
+ translated.encryption._useEnvVarFallback = true; // Flag for env var fallback
222
+ }
223
+ }
224
+
225
+ return translated;
226
+ }
227
+
228
+ /**
229
+ * Build all KMS resources based on ownership decisions
230
+ */
231
+ async buildFromDecisions(decisions, appDefinition, discoveredResources, result) {
232
+ // Check for environment variable fallback flag (legacy behavior)
233
+ const useEnvVarFallback = appDefinition.encryption?._useEnvVarFallback;
234
+
235
+ // CRITICAL FIX: Check if KMS key exists OUTSIDE of stack (orphaned resource)
236
+ // If key exists but not in stack, we should use it as EXTERNAL, not try to create it
237
+ const externalKmsKey = discoveredResources?.defaultKmsKeyId ||
238
+ discoveredResources?.kmsKeyArn ||
239
+ discoveredResources?.kmsKeyId;
240
+
241
+ if (decisions.key.ownership === ResourceOwnership.STACK && decisions.key.physicalId) {
242
+ // Key exists in stack - add definitions (CloudFormation idempotency)
243
+ console.log(' → Adding KMS definitions to template (existing in stack)');
244
+ result.resources = this.createKmsKey(appDefinition);
245
+ result.environment.KMS_KEY_ARN = { 'Fn::GetAtt': ['FriggKMSKey', 'Arn'] };
246
+ console.log(' ✅ KMS key resources created');
247
+ } else if (decisions.key.ownership === ResourceOwnership.STACK && !decisions.key.physicalId && externalKmsKey) {
248
+ // ORPHANED KEY FIX: Key exists externally but not in stack
249
+ // Use it as external instead of trying to create (would fail with "already exists")
250
+ console.log(' ⚠️ KMS key exists externally but not in stack - using as external resource');
251
+ console.log(` → Using external KMS key: ${externalKmsKey}`);
252
+
253
+ // Format as ARN if it's just a key ID
254
+ const kmsArn = externalKmsKey.startsWith('arn:')
255
+ ? externalKmsKey
256
+ : `arn:aws:kms:\${self:provider.region}:\${aws:accountId}:key/${externalKmsKey}`;
257
+
258
+ result.environment.KMS_KEY_ARN = kmsArn;
259
+ } else if (decisions.key.ownership === ResourceOwnership.STACK && !decisions.key.physicalId && !useEnvVarFallback) {
260
+ // Create new KMS key (only if not using env var fallback and no external key found)
261
+ console.log(' → Creating new KMS key in stack');
262
+ result.resources = this.createKmsKey(appDefinition);
263
+ result.environment.KMS_KEY_ARN = { 'Fn::GetAtt': ['FriggKMSKey', 'Arn'] };
264
+ console.log(' ✅ KMS key resources created');
265
+ } else if (decisions.key.ownership === ResourceOwnership.STACK && !decisions.key.physicalId && useEnvVarFallback) {
266
+ // Legacy behavior: fallback to environment variable when createResourceIfNoneFound=false/undefined
267
+ const createIfNoneFound = discoveredResources.defaultKmsKeyId ? true : appDefinition.encryption?.createResourceIfNoneFound;
268
+ const formatAsArn = createIfNoneFound === undefined; // Format as ARN when not specified
269
+
270
+ if (formatAsArn) {
271
+ console.log(' → Using environment variable for KMS key (formatted as ARN)');
272
+ result.environment.KMS_KEY_ARN = 'arn:aws:kms:${self:provider.region}:${aws:accountId}:key/${env:AWS_DISCOVERY_KMS_KEY_ID}';
273
+ } else {
274
+ console.log(' → Using environment variable for KMS key');
275
+ result.environment.KMS_KEY_ARN = '${env:AWS_DISCOVERY_KMS_KEY_ID}';
276
+ }
277
+ } else if (decisions.key.ownership === ResourceOwnership.EXTERNAL) {
278
+ // Use discovered KMS key
279
+ const kmsKeyId = decisions.key.physicalId || '${env:AWS_DISCOVERY_KMS_KEY_ID}';
280
+ console.log(` → Using ${decisions.key.physicalId ? 'discovered' : 'environment variable'} KMS key`);
281
+
282
+ // Format as ARN if it's just a key ID (for IAM policies)
283
+ const kmsArn = kmsKeyId.startsWith('arn:')
284
+ ? kmsKeyId
285
+ : `arn:aws:kms:\${self:provider.region}:\${aws:accountId}:key/${kmsKeyId}`;
286
+
287
+ result.environment.KMS_KEY_ARN = kmsArn;
288
+ } else {
289
+ // Fallback
290
+ console.log(' → Using environment variable for KMS key');
291
+ result.environment.KMS_KEY_ARN = 'arn:aws:kms:${self:provider.region}:${aws:accountId}:key/${env:AWS_DISCOVERY_KMS_KEY_ID}';
292
+ }
293
+ }
294
+
295
+ /**
296
+ * Create KMS key CloudFormation resources
297
+ */
298
+ createKmsKey(appDefinition) {
299
+ return {
300
+ FriggKMSKey: {
301
+ Type: 'AWS::KMS::Key',
302
+ DeletionPolicy: 'Retain',
303
+ UpdateReplacePolicy: 'Retain',
304
+ Properties: {
305
+ Description: 'Frigg Field-Level Encryption Key for ${self:service}-${self:provider.stage}',
306
+ EnableKeyRotation: true,
307
+ KeyPolicy: {
308
+ Version: '2012-10-17',
309
+ Id: 'key-policy-1',
310
+ Statement: [
311
+ {
312
+ Sid: 'AllowRootAccountAdmin',
313
+ Effect: 'Allow',
314
+ Principal: {
315
+ AWS: {
316
+ 'Fn::Sub': 'arn:aws:iam::${AWS::AccountId}:root',
317
+ },
318
+ },
319
+ Action: 'kms:*',
320
+ Resource: '*',
321
+ },
322
+ {
323
+ Sid: 'AllowLambdaService',
324
+ Effect: 'Allow',
325
+ Principal: {
326
+ Service: 'lambda.amazonaws.com',
327
+ },
328
+ Action: [
329
+ 'kms:Decrypt',
330
+ 'kms:GenerateDataKey',
331
+ 'kms:CreateGrant',
332
+ ],
333
+ Resource: '*',
334
+ Condition: {
335
+ StringEquals: {
336
+ 'kms:ViaService': 'lambda.${self:provider.region}.amazonaws.com',
337
+ },
338
+ },
339
+ },
340
+ // NOTE: We do NOT add a statement referencing IamRoleLambdaExecution here
341
+ // because it creates a circular dependency (KMS Key → IAM Role → KMS Key).
342
+ // Instead, IAM policies grant the Lambda execution role permissions to use KMS.
343
+ ],
344
+ },
345
+ Tags: [
346
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-kms' },
347
+ { Key: 'ManagedBy', Value: 'Frigg' },
348
+ { Key: 'Service', Value: '${self:service}' },
349
+ { Key: 'Stage', Value: '${self:provider.stage}' },
350
+ ],
351
+ },
352
+ },
353
+ FriggKMSKeyAlias: {
354
+ Type: 'AWS::KMS::Alias',
355
+ DeletionPolicy: 'Retain',
356
+ Properties: {
357
+ AliasName: 'alias/${self:service}-${self:provider.stage}-frigg-kms',
358
+ TargetKeyId: { 'Fn::GetAtt': ['FriggKMSKey', 'Arn'] },
359
+ },
360
+ },
361
+ };
362
+ }
363
+ }
364
+
365
+ module.exports = { KmsBuilder };
366
+