@friggframework/devtools 2.0.0-next.62 → 2.0.0-next.63

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 (165) hide show
  1. package/infrastructure/ARCHITECTURE.md +487 -0
  2. package/infrastructure/CLAUDE.md +481 -0
  3. package/infrastructure/HEALTH.md +468 -0
  4. package/infrastructure/README.md +522 -0
  5. package/infrastructure/__tests__/fixtures/mock-aws-resources.js +391 -0
  6. package/infrastructure/__tests__/helpers/test-utils.js +277 -0
  7. package/infrastructure/__tests__/postgres-config.test.js +914 -0
  8. package/infrastructure/__tests__/template-generation.test.js +687 -0
  9. package/infrastructure/create-frigg-infrastructure.js +147 -0
  10. package/infrastructure/docs/POSTGRES-CONFIGURATION.md +630 -0
  11. package/infrastructure/docs/PRE-DEPLOYMENT-HEALTH-CHECK-SPEC.md +1317 -0
  12. package/infrastructure/docs/WEBSOCKET-CONFIGURATION.md +105 -0
  13. package/infrastructure/docs/deployment-instructions.md +268 -0
  14. package/infrastructure/docs/generate-iam-command.md +278 -0
  15. package/infrastructure/docs/iam-policy-templates.md +193 -0
  16. package/infrastructure/domains/database/aurora-builder.js +809 -0
  17. package/infrastructure/domains/database/aurora-builder.test.js +950 -0
  18. package/infrastructure/domains/database/aurora-discovery.js +87 -0
  19. package/infrastructure/domains/database/aurora-discovery.test.js +188 -0
  20. package/infrastructure/domains/database/aurora-resolver.js +210 -0
  21. package/infrastructure/domains/database/aurora-resolver.test.js +347 -0
  22. package/infrastructure/domains/database/migration-builder.js +701 -0
  23. package/infrastructure/domains/database/migration-builder.test.js +321 -0
  24. package/infrastructure/domains/database/migration-resolver.js +163 -0
  25. package/infrastructure/domains/database/migration-resolver.test.js +337 -0
  26. package/infrastructure/domains/health/application/ports/IPropertyReconciler.js +164 -0
  27. package/infrastructure/domains/health/application/ports/IResourceDetector.js +129 -0
  28. package/infrastructure/domains/health/application/ports/IResourceImporter.js +142 -0
  29. package/infrastructure/domains/health/application/ports/IStackRepository.js +131 -0
  30. package/infrastructure/domains/health/application/ports/index.js +26 -0
  31. package/infrastructure/domains/health/application/use-cases/__tests__/execute-resource-import-use-case.test.js +679 -0
  32. package/infrastructure/domains/health/application/use-cases/__tests__/mismatch-analyzer-method-name.test.js +167 -0
  33. package/infrastructure/domains/health/application/use-cases/__tests__/repair-via-import-use-case.test.js +1130 -0
  34. package/infrastructure/domains/health/application/use-cases/execute-resource-import-use-case.js +221 -0
  35. package/infrastructure/domains/health/application/use-cases/reconcile-properties-use-case.js +152 -0
  36. package/infrastructure/domains/health/application/use-cases/reconcile-properties-use-case.test.js +343 -0
  37. package/infrastructure/domains/health/application/use-cases/repair-via-import-use-case.js +535 -0
  38. package/infrastructure/domains/health/application/use-cases/repair-via-import-use-case.test.js +376 -0
  39. package/infrastructure/domains/health/application/use-cases/run-health-check-use-case.js +213 -0
  40. package/infrastructure/domains/health/application/use-cases/run-health-check-use-case.test.js +441 -0
  41. package/infrastructure/domains/health/docs/ACME-DEV-DRIFT-ANALYSIS.md +267 -0
  42. package/infrastructure/domains/health/docs/BUILD-VS-DEPLOYED-TEMPLATE-ANALYSIS.md +324 -0
  43. package/infrastructure/domains/health/docs/ORPHAN-DETECTION-ANALYSIS.md +386 -0
  44. package/infrastructure/domains/health/docs/SPEC-CLEANUP-COMMAND.md +1419 -0
  45. package/infrastructure/domains/health/docs/TDD-IMPLEMENTATION-SUMMARY.md +391 -0
  46. package/infrastructure/domains/health/docs/TEMPLATE-COMPARISON-IMPLEMENTATION.md +551 -0
  47. package/infrastructure/domains/health/domain/entities/issue.js +299 -0
  48. package/infrastructure/domains/health/domain/entities/issue.test.js +528 -0
  49. package/infrastructure/domains/health/domain/entities/property-mismatch.js +108 -0
  50. package/infrastructure/domains/health/domain/entities/property-mismatch.test.js +275 -0
  51. package/infrastructure/domains/health/domain/entities/resource.js +159 -0
  52. package/infrastructure/domains/health/domain/entities/resource.test.js +432 -0
  53. package/infrastructure/domains/health/domain/entities/stack-health-report.js +306 -0
  54. package/infrastructure/domains/health/domain/entities/stack-health-report.test.js +601 -0
  55. package/infrastructure/domains/health/domain/services/__tests__/health-score-percentage-based.test.js +380 -0
  56. package/infrastructure/domains/health/domain/services/__tests__/import-progress-monitor.test.js +971 -0
  57. package/infrastructure/domains/health/domain/services/__tests__/import-template-generator.test.js +1150 -0
  58. package/infrastructure/domains/health/domain/services/__tests__/logical-id-mapper.test.js +672 -0
  59. package/infrastructure/domains/health/domain/services/__tests__/template-parser.test.js +496 -0
  60. package/infrastructure/domains/health/domain/services/__tests__/update-progress-monitor.test.js +419 -0
  61. package/infrastructure/domains/health/domain/services/health-score-calculator.js +248 -0
  62. package/infrastructure/domains/health/domain/services/health-score-calculator.test.js +504 -0
  63. package/infrastructure/domains/health/domain/services/import-progress-monitor.js +195 -0
  64. package/infrastructure/domains/health/domain/services/import-template-generator.js +435 -0
  65. package/infrastructure/domains/health/domain/services/logical-id-mapper.js +345 -0
  66. package/infrastructure/domains/health/domain/services/mismatch-analyzer.js +234 -0
  67. package/infrastructure/domains/health/domain/services/mismatch-analyzer.test.js +431 -0
  68. package/infrastructure/domains/health/domain/services/property-mutability-config.js +382 -0
  69. package/infrastructure/domains/health/domain/services/template-parser.js +245 -0
  70. package/infrastructure/domains/health/domain/services/update-progress-monitor.js +192 -0
  71. package/infrastructure/domains/health/domain/value-objects/health-score.js +138 -0
  72. package/infrastructure/domains/health/domain/value-objects/health-score.test.js +267 -0
  73. package/infrastructure/domains/health/domain/value-objects/property-mutability.js +161 -0
  74. package/infrastructure/domains/health/domain/value-objects/property-mutability.test.js +198 -0
  75. package/infrastructure/domains/health/domain/value-objects/resource-state.js +167 -0
  76. package/infrastructure/domains/health/domain/value-objects/resource-state.test.js +196 -0
  77. package/infrastructure/domains/health/domain/value-objects/stack-identifier.js +192 -0
  78. package/infrastructure/domains/health/domain/value-objects/stack-identifier.test.js +262 -0
  79. package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-cfn-tagged.test.js +312 -0
  80. package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-multi-stack.test.js +367 -0
  81. package/infrastructure/domains/health/infrastructure/adapters/__tests__/orphan-detection-relationship-analysis.test.js +432 -0
  82. package/infrastructure/domains/health/infrastructure/adapters/aws-property-reconciler.js +784 -0
  83. package/infrastructure/domains/health/infrastructure/adapters/aws-property-reconciler.test.js +1133 -0
  84. package/infrastructure/domains/health/infrastructure/adapters/aws-resource-detector.js +565 -0
  85. package/infrastructure/domains/health/infrastructure/adapters/aws-resource-detector.test.js +554 -0
  86. package/infrastructure/domains/health/infrastructure/adapters/aws-resource-importer.js +318 -0
  87. package/infrastructure/domains/health/infrastructure/adapters/aws-resource-importer.test.js +398 -0
  88. package/infrastructure/domains/health/infrastructure/adapters/aws-stack-repository.js +777 -0
  89. package/infrastructure/domains/health/infrastructure/adapters/aws-stack-repository.test.js +580 -0
  90. package/infrastructure/domains/integration/integration-builder.js +404 -0
  91. package/infrastructure/domains/integration/integration-builder.test.js +690 -0
  92. package/infrastructure/domains/integration/integration-resolver.js +170 -0
  93. package/infrastructure/domains/integration/integration-resolver.test.js +369 -0
  94. package/infrastructure/domains/integration/websocket-builder.js +69 -0
  95. package/infrastructure/domains/integration/websocket-builder.test.js +195 -0
  96. package/infrastructure/domains/networking/vpc-builder.js +2051 -0
  97. package/infrastructure/domains/networking/vpc-builder.test.js +1960 -0
  98. package/infrastructure/domains/networking/vpc-discovery.js +177 -0
  99. package/infrastructure/domains/networking/vpc-discovery.test.js +350 -0
  100. package/infrastructure/domains/networking/vpc-resolver.js +505 -0
  101. package/infrastructure/domains/networking/vpc-resolver.test.js +801 -0
  102. package/infrastructure/domains/parameters/ssm-builder.js +79 -0
  103. package/infrastructure/domains/parameters/ssm-builder.test.js +189 -0
  104. package/infrastructure/domains/parameters/ssm-discovery.js +84 -0
  105. package/infrastructure/domains/parameters/ssm-discovery.test.js +210 -0
  106. package/infrastructure/domains/security/iam-generator.js +816 -0
  107. package/infrastructure/domains/security/iam-generator.test.js +204 -0
  108. package/infrastructure/domains/security/kms-builder.js +415 -0
  109. package/infrastructure/domains/security/kms-builder.test.js +392 -0
  110. package/infrastructure/domains/security/kms-discovery.js +80 -0
  111. package/infrastructure/domains/security/kms-discovery.test.js +177 -0
  112. package/infrastructure/domains/security/kms-resolver.js +96 -0
  113. package/infrastructure/domains/security/kms-resolver.test.js +216 -0
  114. package/infrastructure/domains/security/templates/frigg-deployment-iam-stack.yaml +401 -0
  115. package/infrastructure/domains/security/templates/iam-policy-basic.json +218 -0
  116. package/infrastructure/domains/security/templates/iam-policy-full.json +288 -0
  117. package/infrastructure/domains/shared/base-builder.js +112 -0
  118. package/infrastructure/domains/shared/base-resolver.js +186 -0
  119. package/infrastructure/domains/shared/base-resolver.test.js +305 -0
  120. package/infrastructure/domains/shared/builder-orchestrator.js +212 -0
  121. package/infrastructure/domains/shared/builder-orchestrator.test.js +213 -0
  122. package/infrastructure/domains/shared/cloudformation-discovery-v2.js +334 -0
  123. package/infrastructure/domains/shared/cloudformation-discovery.js +672 -0
  124. package/infrastructure/domains/shared/cloudformation-discovery.test.js +985 -0
  125. package/infrastructure/domains/shared/environment-builder.js +119 -0
  126. package/infrastructure/domains/shared/environment-builder.test.js +247 -0
  127. package/infrastructure/domains/shared/providers/aws-provider-adapter.js +579 -0
  128. package/infrastructure/domains/shared/providers/aws-provider-adapter.test.js +416 -0
  129. package/infrastructure/domains/shared/providers/azure-provider-adapter.stub.js +93 -0
  130. package/infrastructure/domains/shared/providers/cloud-provider-adapter.js +136 -0
  131. package/infrastructure/domains/shared/providers/gcp-provider-adapter.stub.js +82 -0
  132. package/infrastructure/domains/shared/providers/provider-factory.js +108 -0
  133. package/infrastructure/domains/shared/providers/provider-factory.test.js +170 -0
  134. package/infrastructure/domains/shared/resource-discovery.enhanced.test.js +306 -0
  135. package/infrastructure/domains/shared/resource-discovery.js +233 -0
  136. package/infrastructure/domains/shared/resource-discovery.test.js +588 -0
  137. package/infrastructure/domains/shared/types/app-definition.js +205 -0
  138. package/infrastructure/domains/shared/types/discovery-result.js +106 -0
  139. package/infrastructure/domains/shared/types/discovery-result.test.js +258 -0
  140. package/infrastructure/domains/shared/types/index.js +46 -0
  141. package/infrastructure/domains/shared/types/resource-ownership.js +108 -0
  142. package/infrastructure/domains/shared/types/resource-ownership.test.js +101 -0
  143. package/infrastructure/domains/shared/utilities/base-definition-factory.js +408 -0
  144. package/infrastructure/domains/shared/utilities/base-definition-factory.js.bak +338 -0
  145. package/infrastructure/domains/shared/utilities/base-definition-factory.test.js +291 -0
  146. package/infrastructure/domains/shared/utilities/handler-path-resolver.js +134 -0
  147. package/infrastructure/domains/shared/utilities/handler-path-resolver.test.js +268 -0
  148. package/infrastructure/domains/shared/utilities/prisma-layer-manager.js +159 -0
  149. package/infrastructure/domains/shared/utilities/prisma-layer-manager.test.js +444 -0
  150. package/infrastructure/domains/shared/validation/env-validator.js +78 -0
  151. package/infrastructure/domains/shared/validation/env-validator.test.js +173 -0
  152. package/infrastructure/domains/shared/validation/plugin-validator.js +187 -0
  153. package/infrastructure/domains/shared/validation/plugin-validator.test.js +323 -0
  154. package/infrastructure/esbuild.config.js +53 -0
  155. package/infrastructure/index.js +4 -0
  156. package/infrastructure/infrastructure-composer.js +117 -0
  157. package/infrastructure/infrastructure-composer.test.js +1895 -0
  158. package/infrastructure/integration.test.js +383 -0
  159. package/infrastructure/scripts/build-prisma-layer.js +701 -0
  160. package/infrastructure/scripts/build-prisma-layer.test.js +170 -0
  161. package/infrastructure/scripts/build-time-discovery.js +238 -0
  162. package/infrastructure/scripts/build-time-discovery.test.js +379 -0
  163. package/infrastructure/scripts/run-discovery.js +110 -0
  164. package/infrastructure/scripts/verify-prisma-layer.js +72 -0
  165. package/package.json +8 -7
@@ -0,0 +1,204 @@
1
+ const { generateIAMCloudFormation, getFeatureSummary } = require('./iam-generator');
2
+
3
+ describe('IAM Generator', () => {
4
+ describe('getFeatureSummary', () => {
5
+ it('should detect all features when enabled', () => {
6
+ const appDefinition = {
7
+ name: 'test-app',
8
+ integrations: ['Integration1', 'Integration2'],
9
+ vpc: { enable: true },
10
+ encryption: { fieldLevelEncryptionMethod: 'kms' },
11
+ ssm: { enable: true },
12
+ websockets: { enable: true }
13
+ };
14
+
15
+ const summary = getFeatureSummary(appDefinition);
16
+
17
+ expect(summary.appName).toBe('test-app');
18
+ expect(summary.integrationCount).toBe(2);
19
+ expect(summary.features.core).toBe(true);
20
+ expect(summary.features.vpc).toBe(true);
21
+ expect(summary.features.kms).toBe(true);
22
+ expect(summary.features.ssm).toBe(true);
23
+ expect(summary.features.websockets).toBe(true);
24
+ });
25
+
26
+ it('should detect minimal features when disabled', () => {
27
+ const appDefinition = {
28
+ integrations: []
29
+ };
30
+
31
+ const summary = getFeatureSummary(appDefinition);
32
+
33
+ expect(summary.appName).toBe('Unnamed Frigg App');
34
+ expect(summary.integrationCount).toBe(0);
35
+ expect(summary.features.core).toBe(true);
36
+ expect(summary.features.vpc).toBe(false);
37
+ expect(summary.features.kms).toBe(false);
38
+ expect(summary.features.ssm).toBe(false);
39
+ expect(summary.features.websockets).toBe(false);
40
+ });
41
+ });
42
+
43
+ describe('generateIAMCloudFormation', () => {
44
+ it('should generate valid CloudFormation YAML', () => {
45
+ const appDefinition = {
46
+ name: 'test-app',
47
+ integrations: [],
48
+ vpc: { enable: false },
49
+ encryption: { fieldLevelEncryptionMethod: 'aes' },
50
+ ssm: { enable: false },
51
+ websockets: { enable: false }
52
+ };
53
+
54
+ const summary = getFeatureSummary(appDefinition);
55
+ const yaml = generateIAMCloudFormation({
56
+ appName: summary.appName,
57
+ features: summary.features
58
+ });
59
+
60
+ expect(yaml).toContain('AWSTemplateFormatVersion');
61
+ expect(yaml).toContain('FriggDeploymentUser');
62
+ expect(yaml).toContain('FriggCoreDeploymentPolicy');
63
+ expect(yaml).toContain('FriggDiscoveryPolicy');
64
+ });
65
+
66
+ it('should include VPC policy when VPC is enabled', () => {
67
+ const appDefinition = {
68
+ name: 'test-app',
69
+ integrations: [],
70
+ vpc: { enable: true }
71
+ };
72
+
73
+ const summary = getFeatureSummary(appDefinition);
74
+ const yaml = generateIAMCloudFormation({
75
+ appName: summary.appName,
76
+ features: summary.features
77
+ });
78
+
79
+ expect(yaml).toContain('FriggVPCPolicy');
80
+ expect(yaml).toContain('CreateVPCPermissions');
81
+ expect(yaml).toContain('EnableVPCSupport');
82
+ expect(yaml).toContain('ec2:ReplaceRoute');
83
+ });
84
+
85
+ it('should include KMS policy when encryption is enabled', () => {
86
+ const appDefinition = {
87
+ name: 'test-app',
88
+ integrations: [],
89
+ encryption: { fieldLevelEncryptionMethod: 'kms' }
90
+ };
91
+
92
+ const summary = getFeatureSummary(appDefinition);
93
+ const yaml = generateIAMCloudFormation({
94
+ appName: summary.appName,
95
+ features: summary.features
96
+ });
97
+
98
+ expect(yaml).toContain('FriggKMSPolicy');
99
+ expect(yaml).toContain('CreateKMSPermissions');
100
+ expect(yaml).toContain('EnableKMSSupport');
101
+ expect(yaml).toContain('FriggKMSKeyAlias');
102
+ expect(yaml).toContain('kms:CreateAlias');
103
+ });
104
+
105
+ it('should include SSM policy when SSM is enabled', () => {
106
+ const appDefinition = {
107
+ name: 'test-app',
108
+ integrations: [],
109
+ ssm: { enable: true }
110
+ };
111
+
112
+ const summary = getFeatureSummary(appDefinition);
113
+ const yaml = generateIAMCloudFormation({
114
+ appName: summary.appName,
115
+ features: summary.features
116
+ });
117
+
118
+ expect(yaml).toContain('FriggSSMPolicy');
119
+ expect(yaml).toContain('CreateSSMPermissions');
120
+ expect(yaml).toContain('EnableSSMSupport');
121
+ });
122
+
123
+ it('should set correct default parameter values based on features', () => {
124
+ const appDefinition = {
125
+ name: 'test-app',
126
+ integrations: [],
127
+ vpc: { enable: true },
128
+ encryption: { fieldLevelEncryptionMethod: 'aes' },
129
+ ssm: { enable: true }
130
+ };
131
+
132
+ const summary = getFeatureSummary(appDefinition);
133
+ const yaml = generateIAMCloudFormation({
134
+ appName: summary.appName,
135
+ features: summary.features
136
+ });
137
+
138
+ // Check parameter defaults match the enabled features
139
+ expect(yaml).toContain("Default: 'true'"); // VPC enabled
140
+ expect(yaml).toContain("Default: 'false'"); // KMS disabled
141
+ expect(yaml).toContain('alias/frigg-deployment');
142
+ });
143
+
144
+ it('should include all core permissions', () => {
145
+ const appDefinition = {
146
+ name: 'test-app',
147
+ integrations: []
148
+ };
149
+
150
+ const summary = getFeatureSummary(appDefinition);
151
+ const yaml = generateIAMCloudFormation({
152
+ appName: summary.appName,
153
+ features: summary.features
154
+ });
155
+
156
+ // Check for core permissions
157
+ expect(yaml).toContain('cloudformation:CreateStack');
158
+ expect(yaml).toContain('cloudformation:ListStackResources');
159
+ expect(yaml).toContain('lambda:CreateFunction');
160
+ expect(yaml).toContain('iam:CreateRole');
161
+ expect(yaml).toContain('s3:CreateBucket');
162
+ expect(yaml).toContain('sqs:CreateQueue');
163
+ expect(yaml).toContain('sns:CreateTopic');
164
+ expect(yaml).toContain('logs:CreateLogGroup');
165
+ expect(yaml).toContain('apigateway:POST');
166
+ expect(yaml).toContain('lambda:ListVersionsByFunction');
167
+ expect(yaml).toContain('iam:ListPolicyVersions');
168
+ });
169
+
170
+ it('should include internal-error-queue pattern in SQS resources', () => {
171
+ const appDefinition = {
172
+ name: 'test-app',
173
+ integrations: []
174
+ };
175
+
176
+ const summary = getFeatureSummary(appDefinition);
177
+ const yaml = generateIAMCloudFormation({
178
+ appName: summary.appName,
179
+ features: summary.features
180
+ });
181
+
182
+ expect(yaml).toContain('internal-error-queue-*');
183
+ });
184
+
185
+ it('should generate outputs section', () => {
186
+ const appDefinition = {
187
+ name: 'test-app',
188
+ integrations: []
189
+ };
190
+
191
+ const summary = getFeatureSummary(appDefinition);
192
+ const yaml = generateIAMCloudFormation({
193
+ appName: summary.appName,
194
+ features: summary.features
195
+ });
196
+
197
+ expect(yaml).toContain('Outputs:');
198
+ expect(yaml).toContain('DeploymentUserArn:');
199
+ expect(yaml).toContain('AccessKeyId:');
200
+ expect(yaml).toContain('SecretAccessKeyCommand:');
201
+ expect(yaml).toContain('CredentialsSecretArn:');
202
+ });
203
+ });
204
+ });
@@ -0,0 +1,415 @@
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
+ // Check if external key exists (for accurate logging)
82
+ const externalKmsKey = discoveredResources?.defaultKmsKeyId ||
83
+ discoveredResources?.kmsKeyArn ||
84
+ discoveredResources?.kmsKeyId;
85
+ const willUseExternal = decisions.key.ownership === ResourceOwnership.STACK &&
86
+ !decisions.key.physicalId &&
87
+ externalKmsKey;
88
+
89
+ console.log('\n 📋 Resource Ownership Decisions:');
90
+ if (willUseExternal) {
91
+ console.log(` Key: external - Found external KMS key (not in stack)`);
92
+ } else {
93
+ console.log(` Key: ${decisions.key.ownership} - ${decisions.key.reason}`);
94
+ }
95
+
96
+ // Build resources based on ownership decisions
97
+ await this.buildFromDecisions(decisions, appDefinition, discoveredResources, result);
98
+
99
+ // Add IAM permissions for Lambda role
100
+ result.iamStatements.push({
101
+ Effect: 'Allow',
102
+ Action: ['kms:GenerateDataKey', 'kms:Decrypt', 'kms:Encrypt', 'kms:DescribeKey'],
103
+ Resource: result.environment.KMS_KEY_ARN,
104
+ });
105
+
106
+ console.log(`[${this.name}] ✅ KMS configuration completed`);
107
+ return result;
108
+ }
109
+
110
+ /**
111
+ * Convert flat discovery to structured discovery
112
+ * Provides backwards compatibility for tests
113
+ */
114
+ convertFlatDiscoveryToStructured(flatDiscovery, appDefinition = {}) {
115
+ const discovery = createEmptyDiscoveryResult();
116
+
117
+ if (!flatDiscovery) {
118
+ return discovery;
119
+ }
120
+
121
+ // Check if resources are from CloudFormation stack
122
+ const isManagedIsolated = appDefinition.managementMode === 'managed' &&
123
+ (appDefinition.vpcIsolation === 'isolated' || !appDefinition.vpcIsolation);
124
+ const hasExistingStackResources = isManagedIsolated && flatDiscovery.defaultKmsKeyId &&
125
+ typeof flatDiscovery.defaultKmsKeyId === 'string';
126
+
127
+ if (flatDiscovery.fromCloudFormationStack || hasExistingStackResources) {
128
+ discovery.fromCloudFormation = true;
129
+ discovery.stackName = flatDiscovery.stackName || 'assumed-stack';
130
+
131
+ // Add stack-managed resources
132
+ let existingLogicalIds = flatDiscovery.existingLogicalIds || [];
133
+
134
+ // Infer logical IDs from physical IDs if needed
135
+ if (hasExistingStackResources && existingLogicalIds.length === 0) {
136
+ if (flatDiscovery.defaultKmsKeyId) {
137
+ existingLogicalIds.push('FriggKMSKey');
138
+ existingLogicalIds.push('FriggKMSKeyAlias');
139
+ }
140
+ }
141
+
142
+ existingLogicalIds.forEach(logicalId => {
143
+ let resourceType = '';
144
+ let physicalId = '';
145
+
146
+ if (logicalId === 'FriggKMSKey') {
147
+ resourceType = 'AWS::KMS::Key';
148
+ physicalId = flatDiscovery.defaultKmsKeyId;
149
+ } else if (logicalId === 'FriggKMSKeyAlias') {
150
+ resourceType = 'AWS::KMS::Alias';
151
+ // Extract alias name from KMS key ARN or use default pattern
152
+ const stackName = flatDiscovery.stackName || 'unknown';
153
+ const stage = appDefinition.stage || 'dev';
154
+ physicalId = `alias/${stackName.replace(`-${stage}`, '')}-${stage}-frigg-kms`;
155
+ }
156
+
157
+ if (physicalId && typeof physicalId === 'string') {
158
+ discovery.stackManaged.push({
159
+ logicalId,
160
+ physicalId,
161
+ resourceType
162
+ });
163
+ }
164
+ });
165
+ } else {
166
+ // Resources discovered from AWS API (external)
167
+ if (flatDiscovery.defaultKmsKeyId && typeof flatDiscovery.defaultKmsKeyId === 'string') {
168
+ discovery.external.push({
169
+ physicalId: flatDiscovery.defaultKmsKeyId,
170
+ resourceType: 'AWS::KMS::Key',
171
+ source: 'aws-discovery'
172
+ });
173
+ }
174
+ }
175
+
176
+ return discovery;
177
+ }
178
+
179
+ /**
180
+ * Translate legacy configuration to ownership-based configuration
181
+ * Provides backwards compatibility
182
+ */
183
+ translateLegacyConfig(appDefinition, discoveredResources) {
184
+ // If already using ownership schema, return as-is
185
+ if (appDefinition.encryption?.ownership) {
186
+ return appDefinition;
187
+ }
188
+
189
+ const translated = JSON.parse(JSON.stringify(appDefinition));
190
+
191
+ // Initialize ownership sections
192
+ if (!translated.encryption) translated.encryption = {};
193
+ if (!translated.encryption.ownership) {
194
+ translated.encryption.ownership = {};
195
+ }
196
+
197
+ // Handle top-level managementMode
198
+ const globalMode = appDefinition.managementMode || 'discover';
199
+ const vpcIsolation = appDefinition.vpcIsolation || 'shared';
200
+
201
+ if (globalMode === 'managed') {
202
+ if (appDefinition.encryption?.createResourceIfNoneFound !== undefined) {
203
+ console.log(` ⚠️ managementMode='managed' ignoring: encryption.createResourceIfNoneFound`);
204
+ }
205
+
206
+ if (vpcIsolation === 'isolated') {
207
+ const hasStackKms = discoveredResources?.defaultKmsKeyId &&
208
+ typeof discoveredResources.defaultKmsKeyId === 'string';
209
+
210
+ if (hasStackKms) {
211
+ translated.encryption.ownership.key = 'auto';
212
+ console.log(` managementMode='managed' + vpcIsolation='isolated' → stack has KMS, reusing`);
213
+ } else {
214
+ translated.encryption.ownership.key = 'stack';
215
+ console.log(` managementMode='managed' + vpcIsolation='isolated' → no stack KMS, creating new`);
216
+ }
217
+ } else {
218
+ translated.encryption.ownership.key = 'auto';
219
+ console.log(` managementMode='managed' + vpcIsolation='shared' → discovering KMS`);
220
+ }
221
+ } else {
222
+ // Handle legacy createResourceIfNoneFound
223
+ const createIfNoneFound = appDefinition.encryption?.createResourceIfNoneFound;
224
+ if (createIfNoneFound === true) {
225
+ translated.encryption.ownership.key = 'stack';
226
+ } else if (createIfNoneFound === false || createIfNoneFound === undefined) {
227
+ // When createResourceIfNoneFound is false or not specified:
228
+ // - If KMS found → use it (auto)
229
+ // - If not found → use environment variable (external)
230
+ // We use 'auto' here; the resolver will decide based on discovery
231
+ // But we need special handling in buildFromDecisions for the env var fallback
232
+ translated.encryption.ownership.key = 'auto';
233
+ translated.encryption._useEnvVarFallback = true; // Flag for env var fallback
234
+ }
235
+ }
236
+
237
+ return translated;
238
+ }
239
+
240
+ /**
241
+ * Build all KMS resources based on ownership decisions
242
+ */
243
+ async buildFromDecisions(decisions, appDefinition, discoveredResources, result) {
244
+ // Check for environment variable fallback flag (legacy behavior)
245
+ const useEnvVarFallback = appDefinition.encryption?._useEnvVarFallback;
246
+
247
+ // CRITICAL FIX: Check if KMS key exists OUTSIDE of stack (orphaned resource)
248
+ // If key exists but not in stack, we should use it as EXTERNAL, not try to create it
249
+ const externalKmsKey = discoveredResources?.defaultKmsKeyId ||
250
+ discoveredResources?.kmsKeyArn ||
251
+ discoveredResources?.kmsKeyId;
252
+
253
+ if (decisions.key.ownership === ResourceOwnership.STACK && decisions.key.physicalId) {
254
+ // Key exists in stack - add definitions (CloudFormation idempotency)
255
+ console.log(' → Adding KMS definitions to template (existing in stack)');
256
+
257
+ // CRITICAL: Check if alias exists in stack before trying to create it
258
+ // Matches old serverless-template.js behavior: only create alias if it doesn't exist
259
+ const aliasExistsInStack = discoveredResources?.existingLogicalIds?.includes('FriggKMSKeyAlias');
260
+ if (!aliasExistsInStack) {
261
+ if (appDefinition.encryption?.kmsKeyAlias !== true) {
262
+ // Alias doesn't exist in stack - skip creation unless explicitly enabled
263
+ // This avoids kms:CreateAlias permission errors
264
+ console.log(' ℹ KMS alias not in stack - skipping creation (set kmsKeyAlias: true to force)');
265
+ appDefinition.encryption = appDefinition.encryption || {};
266
+ appDefinition.encryption.kmsKeyAlias = false;
267
+ } else {
268
+ console.log(' → Will create KMS alias (kmsKeyAlias: true explicitly set)');
269
+ }
270
+ } else {
271
+ console.log(' ✓ KMS alias found in stack - will keep in template');
272
+ }
273
+
274
+ result.resources = this.createKmsKey(appDefinition);
275
+ result.environment.KMS_KEY_ARN = { 'Fn::GetAtt': ['FriggKMSKey', 'Arn'] };
276
+ console.log(' ✅ KMS key resources created');
277
+ } else if (decisions.key.ownership === ResourceOwnership.STACK && !decisions.key.physicalId && externalKmsKey) {
278
+ // ORPHANED KEY FIX: Key exists externally but not in stack
279
+ // Use it as external instead of trying to create (would fail with "already exists")
280
+ console.log(` → Using external KMS key: ${externalKmsKey}`);
281
+
282
+ // Format as ARN if it's just a key ID
283
+ const kmsArn = externalKmsKey.startsWith('arn:')
284
+ ? externalKmsKey
285
+ : `arn:aws:kms:\${self:provider.region}:\${aws:accountId}:key/${externalKmsKey}`;
286
+
287
+ result.environment.KMS_KEY_ARN = kmsArn;
288
+ } else if (decisions.key.ownership === ResourceOwnership.STACK && !decisions.key.physicalId && !useEnvVarFallback) {
289
+ // Create new KMS key (only if not using env var fallback and no external key found)
290
+ console.log(' → Creating new KMS key in stack');
291
+
292
+ // CRITICAL: Don't create alias by default to avoid kms:CreateAlias permission errors
293
+ // Matches old serverless-template.js behavior: only create alias if explicitly requested
294
+ if (appDefinition.encryption?.kmsKeyAlias !== true) {
295
+ console.log(' ℹ Skipping KMS alias creation by default (set kmsKeyAlias: true to enable)');
296
+ appDefinition.encryption = appDefinition.encryption || {};
297
+ appDefinition.encryption.kmsKeyAlias = false;
298
+ } else {
299
+ console.log(' → Will create KMS alias (kmsKeyAlias: true explicitly set)');
300
+ }
301
+
302
+ result.resources = this.createKmsKey(appDefinition);
303
+ result.environment.KMS_KEY_ARN = { 'Fn::GetAtt': ['FriggKMSKey', 'Arn'] };
304
+ console.log(' ✅ KMS key resources created');
305
+ } else if (decisions.key.ownership === ResourceOwnership.STACK && !decisions.key.physicalId && useEnvVarFallback) {
306
+ // Legacy behavior: fallback to environment variable when createResourceIfNoneFound=false/undefined
307
+ const createIfNoneFound = discoveredResources.defaultKmsKeyId ? true : appDefinition.encryption?.createResourceIfNoneFound;
308
+ const formatAsArn = createIfNoneFound === undefined; // Format as ARN when not specified
309
+
310
+ if (formatAsArn) {
311
+ console.log(' → Using environment variable for KMS key (formatted as ARN)');
312
+ result.environment.KMS_KEY_ARN = 'arn:aws:kms:${self:provider.region}:${aws:accountId}:key/${env:AWS_DISCOVERY_KMS_KEY_ID}';
313
+ } else {
314
+ console.log(' → Using environment variable for KMS key');
315
+ result.environment.KMS_KEY_ARN = '${env:AWS_DISCOVERY_KMS_KEY_ID}';
316
+ }
317
+ } else if (decisions.key.ownership === ResourceOwnership.EXTERNAL) {
318
+ // Use discovered KMS key
319
+ const kmsKeyId = decisions.key.physicalId || '${env:AWS_DISCOVERY_KMS_KEY_ID}';
320
+ console.log(` → Using ${decisions.key.physicalId ? 'discovered' : 'environment variable'} KMS key`);
321
+
322
+ // Format as ARN if it's just a key ID (for IAM policies)
323
+ const kmsArn = kmsKeyId.startsWith('arn:')
324
+ ? kmsKeyId
325
+ : `arn:aws:kms:\${self:provider.region}:\${aws:accountId}:key/${kmsKeyId}`;
326
+
327
+ result.environment.KMS_KEY_ARN = kmsArn;
328
+ } else {
329
+ // Fallback
330
+ console.log(' → Using environment variable for KMS key');
331
+ result.environment.KMS_KEY_ARN = 'arn:aws:kms:${self:provider.region}:${aws:accountId}:key/${env:AWS_DISCOVERY_KMS_KEY_ID}';
332
+ }
333
+ }
334
+
335
+ /**
336
+ * Create KMS key CloudFormation resources
337
+ */
338
+ createKmsKey(appDefinition) {
339
+ const resources = {
340
+ FriggKMSKey: {
341
+ Type: 'AWS::KMS::Key',
342
+ DeletionPolicy: 'Retain',
343
+ UpdateReplacePolicy: 'Retain',
344
+ Properties: {
345
+ Description: 'Frigg Field-Level Encryption Key for ${self:service}-${self:provider.stage}',
346
+ EnableKeyRotation: true,
347
+ KeyPolicy: {
348
+ Version: '2012-10-17',
349
+ Id: 'key-policy-1',
350
+ Statement: [
351
+ {
352
+ Sid: 'AllowRootAccountAdmin',
353
+ Effect: 'Allow',
354
+ Principal: {
355
+ AWS: {
356
+ 'Fn::Sub': 'arn:aws:iam::${AWS::AccountId}:root',
357
+ },
358
+ },
359
+ Action: 'kms:*',
360
+ Resource: '*',
361
+ },
362
+ {
363
+ Sid: 'AllowLambdaService',
364
+ Effect: 'Allow',
365
+ Principal: {
366
+ Service: 'lambda.amazonaws.com',
367
+ },
368
+ Action: [
369
+ 'kms:Decrypt',
370
+ 'kms:GenerateDataKey',
371
+ 'kms:CreateGrant',
372
+ ],
373
+ Resource: '*',
374
+ Condition: {
375
+ StringEquals: {
376
+ 'kms:ViaService': 'lambda.${self:provider.region}.amazonaws.com',
377
+ },
378
+ },
379
+ },
380
+ // NOTE: We do NOT add a statement referencing IamRoleLambdaExecution here
381
+ // because it creates a circular dependency (KMS Key → IAM Role → KMS Key).
382
+ // Instead, IAM policies grant the Lambda execution role permissions to use KMS.
383
+ ],
384
+ },
385
+ Tags: [
386
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-kms' },
387
+ { Key: 'ManagedBy', Value: 'Frigg' },
388
+ { Key: 'Service', Value: '${self:service}' },
389
+ { Key: 'Stage', Value: '${self:provider.stage}' },
390
+ ],
391
+ },
392
+ },
393
+ };
394
+
395
+ // Only create alias if explicitly enabled (default: true for backwards compatibility)
396
+ const createAlias = appDefinition.encryption?.kmsKeyAlias !== false;
397
+ if (createAlias) {
398
+ resources.FriggKMSKeyAlias = {
399
+ Type: 'AWS::KMS::Alias',
400
+ DeletionPolicy: 'Retain',
401
+ Properties: {
402
+ AliasName: 'alias/${self:service}-${self:provider.stage}-frigg-kms',
403
+ TargetKeyId: { 'Fn::GetAtt': ['FriggKMSKey', 'Arn'] },
404
+ },
405
+ };
406
+ } else {
407
+ console.log(' ℹ Skipping KMS key alias creation (kmsKeyAlias: false)');
408
+ }
409
+
410
+ return resources;
411
+ }
412
+ }
413
+
414
+ module.exports = { KmsBuilder };
415
+