@friggframework/devtools 2.0.0--canary.461.ec909cf.0 → 2.0.0--canary.461.7b36f0f.0

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 (70) hide show
  1. package/frigg-cli/__tests__/unit/commands/build.test.js +6 -6
  2. package/frigg-cli/build-command/index.js +1 -1
  3. package/frigg-cli/deploy-command/index.js +6 -6
  4. package/frigg-cli/generate-command/index.js +2 -2
  5. package/frigg-cli/generate-iam-command.js +10 -10
  6. package/frigg-cli/start-command/index.js +1 -1
  7. package/frigg-cli/start-command/start-command.test.js +3 -3
  8. package/frigg-cli/utils/database-validator.js +14 -21
  9. package/infrastructure/REFACTOR.md +532 -0
  10. package/infrastructure/TRANSFORMATION-VISUAL.md +239 -0
  11. package/infrastructure/__tests__/postgres-config.test.js +1 -1
  12. package/infrastructure/create-frigg-infrastructure.js +1 -1
  13. package/infrastructure/{DEPLOYMENT-INSTRUCTIONS.md → docs/deployment-instructions.md} +3 -3
  14. package/infrastructure/{IAM-POLICY-TEMPLATES.md → docs/iam-policy-templates.md} +9 -10
  15. package/infrastructure/domains/database/aurora-discovery.js +81 -0
  16. package/infrastructure/domains/database/aurora-discovery.test.js +188 -0
  17. package/infrastructure/domains/integration/integration-builder.js +178 -0
  18. package/infrastructure/domains/integration/integration-builder.test.js +362 -0
  19. package/infrastructure/domains/integration/websocket-builder.js +69 -0
  20. package/infrastructure/domains/integration/websocket-builder.test.js +195 -0
  21. package/infrastructure/domains/networking/vpc-discovery.test.js +257 -0
  22. package/infrastructure/domains/parameters/ssm-builder.js +79 -0
  23. package/infrastructure/domains/parameters/ssm-builder.test.js +188 -0
  24. package/infrastructure/domains/parameters/ssm-discovery.js +84 -0
  25. package/infrastructure/domains/parameters/ssm-discovery.test.js +210 -0
  26. package/infrastructure/{iam-generator.js → domains/security/iam-generator.js} +2 -2
  27. package/infrastructure/domains/security/kms-builder.js +169 -0
  28. package/infrastructure/domains/security/kms-builder.test.js +354 -0
  29. package/infrastructure/domains/security/kms-discovery.js +80 -0
  30. package/infrastructure/domains/security/kms-discovery.test.js +176 -0
  31. package/infrastructure/domains/shared/base-builder.js +112 -0
  32. package/infrastructure/domains/shared/builder-orchestrator.js +212 -0
  33. package/infrastructure/domains/shared/builder-orchestrator.test.js +213 -0
  34. package/infrastructure/domains/shared/environment-builder.js +118 -0
  35. package/infrastructure/domains/shared/environment-builder.test.js +246 -0
  36. package/infrastructure/domains/shared/providers/aws-provider-adapter.test.js +366 -0
  37. package/infrastructure/domains/shared/providers/azure-provider-adapter.stub.js +93 -0
  38. package/infrastructure/domains/shared/providers/cloud-provider-adapter.js +136 -0
  39. package/infrastructure/domains/shared/providers/gcp-provider-adapter.stub.js +82 -0
  40. package/infrastructure/domains/shared/providers/provider-factory.js +108 -0
  41. package/infrastructure/domains/shared/providers/provider-factory.test.js +170 -0
  42. package/infrastructure/domains/shared/resource-discovery.js +132 -0
  43. package/infrastructure/domains/shared/resource-discovery.test.js +410 -0
  44. package/infrastructure/domains/shared/utilities/base-definition-factory.js.bak +338 -0
  45. package/infrastructure/domains/shared/utilities/base-definition-factory.test.js +248 -0
  46. package/infrastructure/domains/shared/utilities/handler-path-resolver.test.js +259 -0
  47. package/infrastructure/domains/shared/utilities/prisma-layer-manager.js +55 -0
  48. package/infrastructure/domains/shared/utilities/prisma-layer-manager.test.js +134 -0
  49. package/infrastructure/domains/shared/validation/env-validator.test.js +173 -0
  50. package/infrastructure/esbuild.config.js +53 -0
  51. package/infrastructure/infrastructure-composer.js +85 -0
  52. package/infrastructure/scripts/build-prisma-layer.js +60 -47
  53. package/infrastructure/{build-time-discovery.test.js → scripts/build-time-discovery.test.js} +5 -4
  54. package/layers/prisma/nodejs/package.json +8 -0
  55. package/management-ui/server/utils/environment/awsParameterStore.js +29 -18
  56. package/package.json +8 -8
  57. package/infrastructure/aws-discovery.js +0 -1704
  58. package/infrastructure/aws-discovery.test.js +0 -1666
  59. package/infrastructure/serverless-template.js +0 -2804
  60. package/infrastructure/serverless-template.test.js +0 -1897
  61. /package/infrastructure/{POSTGRES-CONFIGURATION.md → docs/POSTGRES-CONFIGURATION.md} +0 -0
  62. /package/infrastructure/{WEBSOCKET-CONFIGURATION.md → docs/WEBSOCKET-CONFIGURATION.md} +0 -0
  63. /package/infrastructure/{GENERATE-IAM-DOCS.md → docs/generate-iam-command.md} +0 -0
  64. /package/infrastructure/{iam-generator.test.js → domains/security/iam-generator.test.js} +0 -0
  65. /package/infrastructure/{frigg-deployment-iam-stack.yaml → domains/security/templates/frigg-deployment-iam-stack.yaml} +0 -0
  66. /package/infrastructure/{iam-policy-basic.json → domains/security/templates/iam-policy-basic.json} +0 -0
  67. /package/infrastructure/{iam-policy-full.json → domains/security/templates/iam-policy-full.json} +0 -0
  68. /package/infrastructure/{env-validator.js → domains/shared/validation/env-validator.js} +0 -0
  69. /package/infrastructure/{build-time-discovery.js → scripts/build-time-discovery.js} +0 -0
  70. /package/infrastructure/{run-discovery.js → scripts/run-discovery.js} +0 -0
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Tests for SSM Builder
3
+ *
4
+ * Tests SSM Parameter Store configuration
5
+ */
6
+
7
+ const { SsmBuilder } = require('./ssm-builder');
8
+ const { ValidationResult } = require('../shared/base-builder');
9
+
10
+ describe('SsmBuilder', () => {
11
+ let ssmBuilder;
12
+
13
+ beforeEach(() => {
14
+ ssmBuilder = new SsmBuilder();
15
+ delete process.env.FRIGG_SKIP_AWS_DISCOVERY;
16
+ });
17
+
18
+ afterEach(() => {
19
+ delete process.env.FRIGG_SKIP_AWS_DISCOVERY;
20
+ });
21
+
22
+ describe('shouldExecute()', () => {
23
+ it('should return true when SSM is enabled', () => {
24
+ const appDefinition = {
25
+ ssm: { enable: true },
26
+ };
27
+
28
+ expect(ssmBuilder.shouldExecute(appDefinition)).toBe(true);
29
+ });
30
+
31
+ it('should return false when SSM is disabled', () => {
32
+ const appDefinition = {
33
+ ssm: { enable: false },
34
+ };
35
+
36
+ expect(ssmBuilder.shouldExecute(appDefinition)).toBe(false);
37
+ });
38
+
39
+ it('should return false when SSM is not defined', () => {
40
+ const appDefinition = {};
41
+
42
+ expect(ssmBuilder.shouldExecute(appDefinition)).toBe(false);
43
+ });
44
+
45
+ it('should return false when FRIGG_SKIP_AWS_DISCOVERY is set (local mode)', () => {
46
+ process.env.FRIGG_SKIP_AWS_DISCOVERY = 'true';
47
+ const appDefinition = {
48
+ ssm: { enable: true },
49
+ };
50
+
51
+ expect(ssmBuilder.shouldExecute(appDefinition)).toBe(false);
52
+ });
53
+ });
54
+
55
+ describe('validate()', () => {
56
+ it('should pass validation for valid SSM config', () => {
57
+ const appDefinition = {
58
+ ssm: {
59
+ enable: true,
60
+ },
61
+ };
62
+
63
+ const result = ssmBuilder.validate(appDefinition);
64
+
65
+ expect(result).toBeInstanceOf(ValidationResult);
66
+ expect(result.valid).toBe(true);
67
+ expect(result.errors).toEqual([]);
68
+ });
69
+
70
+ it('should pass validation with parameters object', () => {
71
+ const appDefinition = {
72
+ ssm: {
73
+ enable: true,
74
+ parameters: {
75
+ DATABASE_URL: '/my-app/database-url',
76
+ API_KEY: '/my-app/api-key',
77
+ },
78
+ },
79
+ };
80
+
81
+ const result = ssmBuilder.validate(appDefinition);
82
+
83
+ expect(result.valid).toBe(true);
84
+ });
85
+
86
+ it('should error if SSM configuration is missing', () => {
87
+ const appDefinition = {};
88
+
89
+ const result = ssmBuilder.validate(appDefinition);
90
+
91
+ expect(result.valid).toBe(false);
92
+ expect(result.errors).toContain('SSM configuration is missing');
93
+ });
94
+
95
+ it('should error if parameters is not an object', () => {
96
+ const appDefinition = {
97
+ ssm: {
98
+ enable: true,
99
+ parameters: 'invalid',
100
+ },
101
+ };
102
+
103
+ const result = ssmBuilder.validate(appDefinition);
104
+
105
+ expect(result.valid).toBe(false);
106
+ expect(result.errors).toContain('ssm.parameters must be an object');
107
+ });
108
+
109
+ it('should error if parameters is an array', () => {
110
+ const appDefinition = {
111
+ ssm: {
112
+ enable: true,
113
+ parameters: ['param1', 'param2'],
114
+ },
115
+ };
116
+
117
+ const result = ssmBuilder.validate(appDefinition);
118
+
119
+ expect(result.valid).toBe(false);
120
+ });
121
+ });
122
+
123
+ describe('build()', () => {
124
+ it('should add IAM permissions for SSM operations', async () => {
125
+ const appDefinition = {
126
+ ssm: {
127
+ enable: true,
128
+ },
129
+ };
130
+
131
+ const result = await ssmBuilder.build(appDefinition, {});
132
+
133
+ expect(result.iamStatements).toHaveLength(1);
134
+ expect(result.iamStatements[0]).toEqual({
135
+ Effect: 'Allow',
136
+ Action: [
137
+ 'ssm:GetParameter',
138
+ 'ssm:GetParameters',
139
+ 'ssm:GetParametersByPath',
140
+ ],
141
+ Resource: {
142
+ 'Fn::Sub': 'arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/*',
143
+ },
144
+ });
145
+ });
146
+
147
+ it('should return environment object even if empty', async () => {
148
+ const appDefinition = {
149
+ ssm: {
150
+ enable: true,
151
+ },
152
+ };
153
+
154
+ const result = await ssmBuilder.build(appDefinition, {});
155
+
156
+ expect(result.environment).toBeDefined();
157
+ expect(typeof result.environment).toBe('object');
158
+ });
159
+
160
+ it('should not depend on discovered resources', async () => {
161
+ const appDefinition = {
162
+ ssm: {
163
+ enable: true,
164
+ },
165
+ };
166
+
167
+ const result1 = await ssmBuilder.build(appDefinition, {});
168
+ const result2 = await ssmBuilder.build(appDefinition, { someResource: 'value' });
169
+
170
+ expect(result1.iamStatements).toEqual(result2.iamStatements);
171
+ });
172
+ });
173
+
174
+ describe('getDependencies()', () => {
175
+ it('should have no dependencies', () => {
176
+ const deps = ssmBuilder.getDependencies();
177
+
178
+ expect(deps).toEqual([]);
179
+ });
180
+ });
181
+
182
+ describe('getName()', () => {
183
+ it('should return SsmBuilder', () => {
184
+ expect(ssmBuilder.getName()).toBe('SsmBuilder');
185
+ });
186
+ });
187
+ });
188
+
@@ -0,0 +1,84 @@
1
+ /**
2
+ * SSM Discovery Service
3
+ *
4
+ * Domain Service - Hexagonal Architecture
5
+ *
6
+ * Discovers SSM Parameter Store and Secrets Manager resources
7
+ * using the cloud provider adapter.
8
+ */
9
+
10
+ class SsmDiscovery {
11
+ /**
12
+ * @param {CloudProviderAdapter} provider - Cloud provider adapter instance
13
+ */
14
+ constructor(provider) {
15
+ this.provider = provider;
16
+ }
17
+
18
+ /**
19
+ * Discover SSM parameters and secrets
20
+ *
21
+ * @param {Object} config - Discovery configuration
22
+ * @param {string} [config.parameterPath] - SSM parameter path prefix
23
+ * @param {string} [config.serviceName] - Service name for filtering
24
+ * @param {string} [config.stage] - Deployment stage
25
+ * @param {boolean} [config.includeSecrets] - Whether to include Secrets Manager
26
+ * @returns {Promise<Object>} Discovered parameter resources
27
+ */
28
+ async discover(config) {
29
+ console.log('🔍 Discovering SSM parameters...');
30
+
31
+ try {
32
+ // Build parameter path if not provided
33
+ if (!config.parameterPath && config.serviceName && config.stage) {
34
+ config.parameterPath = `/${config.serviceName}/${config.stage}`;
35
+ }
36
+
37
+ const rawResources = await this.provider.discoverParameters({
38
+ ...config,
39
+ includeSecrets: config.includeSecrets !== false,
40
+ });
41
+
42
+ const result = {
43
+ parameters: rawResources.parameters || [],
44
+ secrets: rawResources.secrets || [],
45
+ parameterPath: config.parameterPath,
46
+ };
47
+
48
+ // Find database secret if exists
49
+ if (result.secrets.length > 0) {
50
+ const dbSecret = result.secrets.find(
51
+ s => s.Name?.includes('database') || s.Name?.includes('rds')
52
+ );
53
+ if (dbSecret) {
54
+ result.databaseSecretArn = dbSecret.ARN;
55
+ result.databaseSecretName = dbSecret.Name;
56
+ }
57
+ }
58
+
59
+ if (result.parameters.length > 0) {
60
+ console.log(` ✓ Found ${result.parameters.length} SSM parameters`);
61
+ }
62
+ if (result.secrets.length > 0) {
63
+ console.log(` ✓ Found ${result.secrets.length} secrets`);
64
+ }
65
+ if (!result.parameters.length && !result.secrets.length) {
66
+ console.log(' ℹ No parameters or secrets found');
67
+ }
68
+
69
+ return result;
70
+ } catch (error) {
71
+ console.error(' ✗ SSM discovery failed:', error.message);
72
+ return {
73
+ parameters: [],
74
+ secrets: [],
75
+ parameterPath: config.parameterPath,
76
+ };
77
+ }
78
+ }
79
+ }
80
+
81
+ module.exports = {
82
+ SsmDiscovery,
83
+ };
84
+
@@ -0,0 +1,210 @@
1
+ /**
2
+ * Tests for SSM Discovery Service
3
+ *
4
+ * Tests SSM Parameter Store and Secrets Manager discovery with mocked provider
5
+ */
6
+
7
+ const { SsmDiscovery } = require('./ssm-discovery');
8
+
9
+ describe('SsmDiscovery', () => {
10
+ let mockProvider;
11
+ let ssmDiscovery;
12
+
13
+ beforeEach(() => {
14
+ mockProvider = {
15
+ discoverParameters: jest.fn(),
16
+ getName: jest.fn().mockReturnValue('aws'),
17
+ };
18
+ ssmDiscovery = new SsmDiscovery(mockProvider);
19
+ });
20
+
21
+ describe('discover()', () => {
22
+ it('should delegate to provider and transform results', async () => {
23
+ const mockProviderResponse = {
24
+ parameters: [
25
+ {
26
+ Name: '/my-service/prod/API_KEY',
27
+ Value: 'encrypted-value',
28
+ Type: 'SecureString',
29
+ },
30
+ {
31
+ Name: '/my-service/prod/DATABASE_URL',
32
+ Value: 'postgres://...',
33
+ Type: 'SecureString',
34
+ },
35
+ ],
36
+ secrets: [
37
+ {
38
+ Name: 'my-service/database-credentials',
39
+ ARN: 'arn:aws:secretsmanager:us-east-1:123456:secret:my-service/database-credentials',
40
+ },
41
+ ],
42
+ };
43
+
44
+ mockProvider.discoverParameters.mockResolvedValue(mockProviderResponse);
45
+
46
+ const result = await ssmDiscovery.discover({
47
+ serviceName: 'my-service',
48
+ stage: 'prod',
49
+ });
50
+
51
+ expect(mockProvider.discoverParameters).toHaveBeenCalled();
52
+ expect(result.parameters).toHaveLength(2);
53
+ expect(result.secrets).toHaveLength(1);
54
+ expect(result.parameterPath).toBe('/my-service/prod');
55
+ });
56
+
57
+ it('should build parameter path from serviceName and stage', async () => {
58
+ mockProvider.discoverParameters.mockResolvedValue({
59
+ parameters: [],
60
+ secrets: [],
61
+ });
62
+
63
+ await ssmDiscovery.discover({
64
+ serviceName: 'test-app',
65
+ stage: 'dev',
66
+ });
67
+
68
+ expect(mockProvider.discoverParameters).toHaveBeenCalledWith(
69
+ expect.objectContaining({
70
+ parameterPath: '/test-app/dev',
71
+ includeSecrets: true,
72
+ })
73
+ );
74
+ });
75
+
76
+ it('should use provided parameterPath if specified', async () => {
77
+ mockProvider.discoverParameters.mockResolvedValue({
78
+ parameters: [],
79
+ secrets: [],
80
+ });
81
+
82
+ await ssmDiscovery.discover({
83
+ parameterPath: '/custom/path',
84
+ });
85
+
86
+ expect(mockProvider.discoverParameters).toHaveBeenCalledWith(
87
+ expect.objectContaining({
88
+ parameterPath: '/custom/path',
89
+ })
90
+ );
91
+ });
92
+
93
+ it('should handle no parameters found', async () => {
94
+ mockProvider.discoverParameters.mockResolvedValue({
95
+ parameters: [],
96
+ secrets: [],
97
+ });
98
+
99
+ const result = await ssmDiscovery.discover({});
100
+
101
+ expect(result.parameters).toEqual([]);
102
+ expect(result.secrets).toEqual([]);
103
+ });
104
+
105
+ it('should find database secret if exists', async () => {
106
+ mockProvider.discoverParameters.mockResolvedValue({
107
+ parameters: [],
108
+ secrets: [
109
+ {
110
+ Name: 'my-app/config',
111
+ ARN: 'arn:aws:secretsmanager:us-east-1:123456:secret:my-app/config',
112
+ },
113
+ {
114
+ Name: 'my-app/database-secret',
115
+ ARN: 'arn:aws:secretsmanager:us-east-1:123456:secret:my-app/database-secret',
116
+ },
117
+ ],
118
+ });
119
+
120
+ const result = await ssmDiscovery.discover({});
121
+
122
+ expect(result.databaseSecretArn).toBe('arn:aws:secretsmanager:us-east-1:123456:secret:my-app/database-secret');
123
+ expect(result.databaseSecretName).toBe('my-app/database-secret');
124
+ });
125
+
126
+ it('should find RDS secret if exists', async () => {
127
+ mockProvider.discoverParameters.mockResolvedValue({
128
+ parameters: [],
129
+ secrets: [
130
+ {
131
+ Name: 'rds/postgres/credentials',
132
+ ARN: 'arn:aws:secretsmanager:us-east-1:123456:secret:rds/postgres/credentials',
133
+ },
134
+ ],
135
+ });
136
+
137
+ const result = await ssmDiscovery.discover({});
138
+
139
+ expect(result.databaseSecretArn).toBe('arn:aws:secretsmanager:us-east-1:123456:secret:rds/postgres/credentials');
140
+ expect(result.databaseSecretName).toBe('rds/postgres/credentials');
141
+ });
142
+
143
+ it('should handle includeSecrets flag', async () => {
144
+ mockProvider.discoverParameters.mockResolvedValue({
145
+ parameters: [],
146
+ secrets: [],
147
+ });
148
+
149
+ await ssmDiscovery.discover({
150
+ includeSecrets: false,
151
+ });
152
+
153
+ expect(mockProvider.discoverParameters).toHaveBeenCalledWith(
154
+ expect.objectContaining({
155
+ includeSecrets: false,
156
+ })
157
+ );
158
+ });
159
+
160
+ it('should default includeSecrets to true', async () => {
161
+ mockProvider.discoverParameters.mockResolvedValue({
162
+ parameters: [],
163
+ secrets: [],
164
+ });
165
+
166
+ await ssmDiscovery.discover({});
167
+
168
+ expect(mockProvider.discoverParameters).toHaveBeenCalledWith(
169
+ expect.objectContaining({
170
+ includeSecrets: true,
171
+ })
172
+ );
173
+ });
174
+
175
+ it('should handle discovery errors gracefully', async () => {
176
+ mockProvider.discoverParameters.mockRejectedValue(new Error('SSM API Error'));
177
+
178
+ const result = await ssmDiscovery.discover({});
179
+
180
+ expect(result.parameters).toEqual([]);
181
+ expect(result.secrets).toEqual([]);
182
+ });
183
+
184
+ it('should preserve parameterPath in result', async () => {
185
+ mockProvider.discoverParameters.mockResolvedValue({
186
+ parameters: [],
187
+ secrets: [],
188
+ });
189
+
190
+ const result = await ssmDiscovery.discover({
191
+ parameterPath: '/my-service/staging',
192
+ });
193
+
194
+ expect(result.parameterPath).toBe('/my-service/staging');
195
+ });
196
+
197
+ it('should handle null/undefined parameters and secrets', async () => {
198
+ mockProvider.discoverParameters.mockResolvedValue({
199
+ parameters: null,
200
+ secrets: undefined,
201
+ });
202
+
203
+ const result = await ssmDiscovery.discover({});
204
+
205
+ expect(result.parameters).toEqual([]);
206
+ expect(result.secrets).toEqual([]);
207
+ });
208
+ });
209
+ });
210
+
@@ -781,7 +781,7 @@ function getFeatureSummary(appDefinition) {
781
781
  * @returns {Object} Basic IAM policy document
782
782
  */
783
783
  function generateBasicIAMPolicy() {
784
- const basicPolicyPath = path.join(__dirname, 'iam-policy-basic.json');
784
+ const basicPolicyPath = path.join(__dirname, 'templates/iam-policy-basic.json');
785
785
  return require(basicPolicyPath);
786
786
  }
787
787
 
@@ -790,7 +790,7 @@ function generateBasicIAMPolicy() {
790
790
  * @returns {Object} Full IAM policy document
791
791
  */
792
792
  function generateFullIAMPolicy() {
793
- const fullPolicyPath = path.join(__dirname, 'iam-policy-full.json');
793
+ const fullPolicyPath = path.join(__dirname, 'templates/iam-policy-full.json');
794
794
  return require(fullPolicyPath);
795
795
  }
796
796
 
@@ -0,0 +1,169 @@
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 grants via serverless-kms-grants plugin
11
+ */
12
+
13
+ const { InfrastructureBuilder, ValidationResult } = require('../shared/base-builder');
14
+
15
+ class KmsBuilder extends InfrastructureBuilder {
16
+ constructor() {
17
+ super();
18
+ this.name = 'KmsBuilder';
19
+ }
20
+
21
+ shouldExecute(appDefinition) {
22
+ // Skip KMS in local mode (when FRIGG_SKIP_AWS_DISCOVERY is set)
23
+ // KMS is an AWS-specific service that should only be created in production
24
+ if (process.env.FRIGG_SKIP_AWS_DISCOVERY === 'true') {
25
+ return false;
26
+ }
27
+
28
+ return appDefinition.encryption?.fieldLevelEncryptionMethod === 'kms';
29
+ }
30
+
31
+ validate(appDefinition) {
32
+ const result = new ValidationResult();
33
+
34
+ if (!appDefinition.encryption) {
35
+ result.addError('Encryption configuration is missing');
36
+ return result;
37
+ }
38
+
39
+ const encryption = appDefinition.encryption;
40
+
41
+ if (encryption.fieldLevelEncryptionMethod !== 'kms') {
42
+ // Not an error - just not applicable
43
+ return result;
44
+ }
45
+
46
+ // Validate createResourceIfNoneFound is boolean
47
+ if (encryption.createResourceIfNoneFound !== undefined &&
48
+ typeof encryption.createResourceIfNoneFound !== 'boolean') {
49
+ result.addError('encryption.createResourceIfNoneFound must be a boolean');
50
+ }
51
+
52
+ return result;
53
+ }
54
+
55
+ /**
56
+ * Build KMS infrastructure
57
+ */
58
+ async build(appDefinition, discoveredResources) {
59
+ console.log(`\n[${this.name}] Configuring KMS encryption...`);
60
+
61
+ const result = {
62
+ resources: {},
63
+ iamStatements: [],
64
+ environment: {},
65
+ pluginConfig: {},
66
+ plugins: [],
67
+ };
68
+
69
+ // Check if we should create a new KMS key
70
+ if (!discoveredResources.defaultKmsKeyId &&
71
+ appDefinition.encryption.createResourceIfNoneFound === true) {
72
+
73
+ console.log(' Creating new KMS key...');
74
+ result.resources = this.createKmsKey(appDefinition);
75
+ result.environment.KMS_KEY_ARN = { 'Fn::GetAtt': ['FriggKMSKey', 'Arn'] };
76
+ result.pluginConfig.kmsGrants = {
77
+ kmsKeyId: { 'Fn::GetAtt': ['FriggKMSKey', 'Arn'] },
78
+ };
79
+ console.log(' ✅ KMS key resources created');
80
+ } else {
81
+ // Use discovered KMS key
82
+ const kmsKeyId = discoveredResources.defaultKmsKeyId || '${env:AWS_DISCOVERY_KMS_KEY_ID}';
83
+ console.log(` Using ${discoveredResources.defaultKmsKeyId ? 'discovered' : 'environment variable'} KMS key`);
84
+ result.environment.KMS_KEY_ARN = kmsKeyId;
85
+ result.pluginConfig.kmsGrants = { kmsKeyId };
86
+ }
87
+
88
+ // Add IAM permissions
89
+ result.iamStatements.push({
90
+ Effect: 'Allow',
91
+ Action: ['kms:GenerateDataKey', 'kms:Decrypt'],
92
+ Resource: result.environment.KMS_KEY_ARN,
93
+ });
94
+
95
+ // Enable KMS grants plugin
96
+ result.plugins.push('serverless-kms-grants');
97
+
98
+ console.log(`[${this.name}] ✅ KMS configuration completed`);
99
+ return result;
100
+ }
101
+
102
+ /**
103
+ * Create KMS key CloudFormation resources
104
+ */
105
+ createKmsKey(appDefinition) {
106
+ return {
107
+ FriggKMSKey: {
108
+ Type: 'AWS::KMS::Key',
109
+ DeletionPolicy: 'Retain',
110
+ UpdateReplacePolicy: 'Retain',
111
+ Properties: {
112
+ Description: 'Frigg Field-Level Encryption Key for ${self:service}-${self:provider.stage}',
113
+ KeyPolicy: {
114
+ Version: '2012-10-17',
115
+ Id: 'key-policy-1',
116
+ Statement: [
117
+ {
118
+ Sid: 'Enable IAM User Permissions',
119
+ Effect: 'Allow',
120
+ Principal: {
121
+ AWS: {
122
+ 'Fn::Sub': 'arn:aws:iam::${AWS::AccountId}:root',
123
+ },
124
+ },
125
+ Action: 'kms:*',
126
+ Resource: '*',
127
+ },
128
+ {
129
+ Sid: 'Allow Lambda to use the key',
130
+ Effect: 'Allow',
131
+ Principal: {
132
+ Service: 'lambda.amazonaws.com',
133
+ },
134
+ Action: [
135
+ 'kms:Decrypt',
136
+ 'kms:GenerateDataKey',
137
+ 'kms:CreateGrant',
138
+ ],
139
+ Resource: '*',
140
+ Condition: {
141
+ StringEquals: {
142
+ 'kms:ViaService': 'lambda.${self:provider.region}.amazonaws.com',
143
+ },
144
+ },
145
+ },
146
+ ],
147
+ },
148
+ Tags: [
149
+ { Key: 'Name', Value: '${self:service}-${self:provider.stage}-kms' },
150
+ { Key: 'ManagedBy', Value: 'Frigg' },
151
+ { Key: 'Service', Value: '${self:service}' },
152
+ { Key: 'Stage', Value: '${self:provider.stage}' },
153
+ ],
154
+ },
155
+ },
156
+ FriggKMSKeyAlias: {
157
+ Type: 'AWS::KMS::Alias',
158
+ DeletionPolicy: 'Retain',
159
+ Properties: {
160
+ AliasName: 'alias/${self:service}-${self:provider.stage}-frigg-kms',
161
+ TargetKeyId: { 'Fn::GetAtt': ['FriggKMSKey', 'Arn'] },
162
+ },
163
+ },
164
+ };
165
+ }
166
+ }
167
+
168
+ module.exports = { KmsBuilder };
169
+