@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,79 @@
1
+ /**
2
+ * SSM Parameter Store Builder
3
+ *
4
+ * Domain Layer - Hexagonal Architecture
5
+ *
6
+ * Responsible for:
7
+ * - Configuring IAM permissions for SSM Parameter Store access
8
+ * - Setting up SSM parameter references for Lambda functions
9
+ */
10
+
11
+ const { InfrastructureBuilder, ValidationResult } = require('../shared/base-builder');
12
+
13
+ class SsmBuilder extends InfrastructureBuilder {
14
+ constructor() {
15
+ super();
16
+ this.name = 'SsmBuilder';
17
+ }
18
+
19
+ shouldExecute(appDefinition) {
20
+ // Skip SSM in local mode (when FRIGG_SKIP_AWS_DISCOVERY is set)
21
+ // SSM Parameter Store is an AWS-specific service that should only be used in production
22
+ if (process.env.FRIGG_SKIP_AWS_DISCOVERY === 'true') {
23
+ return false;
24
+ }
25
+
26
+ return appDefinition.ssm?.enable === true;
27
+ }
28
+
29
+ validate(appDefinition) {
30
+ const result = new ValidationResult();
31
+
32
+ if (!appDefinition.ssm) {
33
+ result.addError('SSM configuration is missing');
34
+ return result;
35
+ }
36
+
37
+ // Validate parameters if provided
38
+ if (appDefinition.ssm.parameters) {
39
+ if (typeof appDefinition.ssm.parameters !== 'object' || Array.isArray(appDefinition.ssm.parameters)) {
40
+ result.addError('ssm.parameters must be an object (not an array)');
41
+ }
42
+ }
43
+
44
+ return result;
45
+ }
46
+
47
+ /**
48
+ * Build SSM configuration
49
+ */
50
+ async build(appDefinition, discoveredResources) {
51
+ console.log(`\n[${this.name}] Configuring SSM Parameter Store...`);
52
+
53
+ const result = {
54
+ iamStatements: [],
55
+ environment: {},
56
+ };
57
+
58
+ // Add IAM permissions for SSM Parameter Store
59
+ result.iamStatements.push({
60
+ Effect: 'Allow',
61
+ Action: [
62
+ 'ssm:GetParameter',
63
+ 'ssm:GetParameters',
64
+ 'ssm:GetParametersByPath',
65
+ ],
66
+ Resource: {
67
+ 'Fn::Sub': 'arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/*',
68
+ },
69
+ });
70
+
71
+ console.log(' ✅ SSM Parameter Store IAM permissions added');
72
+ console.log(`[${this.name}] ✅ SSM configuration completed`);
73
+
74
+ return result;
75
+ }
76
+ }
77
+
78
+ module.exports = { SsmBuilder };
79
+
@@ -0,0 +1,189 @@
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.some(e => e.includes('ssm.parameters must be an object'))).toBe(true);
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
+ expect(result.errors.some(e => e.includes('ssm.parameters must be an object'))).toBe(true);
121
+ });
122
+ });
123
+
124
+ describe('build()', () => {
125
+ it('should add IAM permissions for SSM operations', async () => {
126
+ const appDefinition = {
127
+ ssm: {
128
+ enable: true,
129
+ },
130
+ };
131
+
132
+ const result = await ssmBuilder.build(appDefinition, {});
133
+
134
+ expect(result.iamStatements).toHaveLength(1);
135
+ expect(result.iamStatements[0]).toEqual({
136
+ Effect: 'Allow',
137
+ Action: [
138
+ 'ssm:GetParameter',
139
+ 'ssm:GetParameters',
140
+ 'ssm:GetParametersByPath',
141
+ ],
142
+ Resource: {
143
+ 'Fn::Sub': 'arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/*',
144
+ },
145
+ });
146
+ });
147
+
148
+ it('should return environment object even if empty', async () => {
149
+ const appDefinition = {
150
+ ssm: {
151
+ enable: true,
152
+ },
153
+ };
154
+
155
+ const result = await ssmBuilder.build(appDefinition, {});
156
+
157
+ expect(result.environment).toBeDefined();
158
+ expect(typeof result.environment).toBe('object');
159
+ });
160
+
161
+ it('should not depend on discovered resources', async () => {
162
+ const appDefinition = {
163
+ ssm: {
164
+ enable: true,
165
+ },
166
+ };
167
+
168
+ const result1 = await ssmBuilder.build(appDefinition, {});
169
+ const result2 = await ssmBuilder.build(appDefinition, { someResource: 'value' });
170
+
171
+ expect(result1.iamStatements).toEqual(result2.iamStatements);
172
+ });
173
+ });
174
+
175
+ describe('getDependencies()', () => {
176
+ it('should have no dependencies', () => {
177
+ const deps = ssmBuilder.getDependencies();
178
+
179
+ expect(deps).toEqual([]);
180
+ });
181
+ });
182
+
183
+ describe('getName()', () => {
184
+ it('should return SsmBuilder', () => {
185
+ expect(ssmBuilder.getName()).toBe('SsmBuilder');
186
+ });
187
+ });
188
+ });
189
+
@@ -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
+