@friggframework/core 2.0.0--canary.395.0e9373c.0 → 2.0.0--canary.395.2718e67.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.
@@ -0,0 +1,117 @@
1
+ const { mongoose } = require('./mongoose');
2
+
3
+ /**
4
+ * Repository for Health Check database operations.
5
+ * Provides atomic database operations for health testing.
6
+ *
7
+ * Follows DDD/Hexagonal Architecture:
8
+ * - Infrastructure Layer (this repository)
9
+ * - Pure database operations only, no business logic
10
+ * - Used by Application Layer (Use Cases)
11
+ */
12
+ class HealthCheckRepository {
13
+ /**
14
+ * Get database connection state
15
+ * @returns {Object} Object with readyState, stateName, and isConnected
16
+ */
17
+ getDatabaseConnectionState() {
18
+ const stateMap = {
19
+ 0: 'disconnected',
20
+ 1: 'connected',
21
+ 2: 'connecting',
22
+ 3: 'disconnecting',
23
+ };
24
+ const readyState = mongoose.connection.readyState;
25
+
26
+ return {
27
+ readyState,
28
+ stateName: stateMap[readyState],
29
+ isConnected: readyState === 1,
30
+ };
31
+ }
32
+
33
+ /**
34
+ * Ping the database to verify connectivity
35
+ * @param {number} maxTimeMS - Maximum time to wait for ping response
36
+ * @returns {Promise<number>} Response time in milliseconds
37
+ * @throws {Error} If database is not connected or ping fails
38
+ */
39
+ async pingDatabase(maxTimeMS = 2000) {
40
+ const pingStart = Date.now();
41
+ await mongoose.connection.db.admin().ping({ maxTimeMS });
42
+ return Date.now() - pingStart;
43
+ }
44
+
45
+ /**
46
+ * Create a test encryption model
47
+ * @returns {Model} Mongoose model with encryption plugin
48
+ */
49
+ createEncryptionTestModel() {
50
+ const { Encrypt } = require('../encrypt');
51
+
52
+ const testSchema = new mongoose.Schema(
53
+ {
54
+ testSecret: { type: String, lhEncrypt: true },
55
+ normalField: { type: String },
56
+ nestedSecret: {
57
+ value: { type: String, lhEncrypt: true },
58
+ },
59
+ },
60
+ { timestamps: false }
61
+ );
62
+
63
+ testSchema.plugin(Encrypt);
64
+
65
+ return (
66
+ mongoose.models.TestEncryption ||
67
+ mongoose.model('TestEncryption', testSchema)
68
+ );
69
+ }
70
+
71
+ /**
72
+ * Save a test document to the database
73
+ * @param {Model} TestModel - Mongoose model
74
+ * @param {Object} data - Data to save
75
+ * @returns {Promise<Document>} Saved document
76
+ */
77
+ async saveTestDocument(TestModel, data) {
78
+ const testDoc = new TestModel(data);
79
+ await testDoc.save();
80
+ return testDoc;
81
+ }
82
+
83
+ /**
84
+ * Find a test document by ID
85
+ * @param {Model} TestModel - Mongoose model
86
+ * @param {string} id - Document ID
87
+ * @returns {Promise<Document>} Found document
88
+ */
89
+ async findTestDocumentById(TestModel, id) {
90
+ return await TestModel.findById(id);
91
+ }
92
+
93
+ /**
94
+ * Get raw document from collection (bypassing Mongoose)
95
+ * Used for verifying encryption at the database level
96
+ * @param {string} collectionName - Collection name
97
+ * @param {Object} filter - Query filter
98
+ * @returns {Promise<Object>} Raw document from database
99
+ */
100
+ async getRawDocumentFromCollection(collectionName, filter) {
101
+ return await mongoose.connection.db
102
+ .collection(collectionName)
103
+ .findOne(filter);
104
+ }
105
+
106
+ /**
107
+ * Delete a test document
108
+ * @param {Model} TestModel - Mongoose model
109
+ * @param {string} id - Document ID
110
+ * @returns {Promise<Object>} Deletion result
111
+ */
112
+ async deleteTestDocument(TestModel, id) {
113
+ return await TestModel.deleteOne({ _id: id });
114
+ }
115
+ }
116
+
117
+ module.exports = { HealthCheckRepository };
@@ -0,0 +1,105 @@
1
+ const { Token } = require('./models/Token');
2
+
3
+ /**
4
+ * Repository for Token operations.
5
+ * Handles persistence of authentication tokens.
6
+ */
7
+ class TokenRepository {
8
+ /**
9
+ * Create a token with expiration
10
+ * @param {string} userId - The user ID
11
+ * @param {string} rawToken - The raw (unhashed) token string
12
+ * @param {number} minutes - Minutes until expiration
13
+ * @returns {Promise<Object>} The created token record
14
+ */
15
+ async createTokenWithExpire(userId, rawToken, minutes) {
16
+ return await Token.createTokenWithExpire(userId, rawToken, minutes);
17
+ }
18
+
19
+ /**
20
+ * Validate and retrieve token from JSON token object
21
+ * @param {Object} tokenObj - Object with id and token properties
22
+ * @returns {Promise<Object>} The validated token record
23
+ * @throws {Error} If token is invalid, expired, or doesn't exist
24
+ */
25
+ async validateAndGetToken(tokenObj) {
26
+ return await Token.validateAndGetTokenFromJSONToken(tokenObj);
27
+ }
28
+
29
+ /**
30
+ * Find a token by ID
31
+ * @param {string} tokenId - The token ID
32
+ * @returns {Promise<Object|null>} The token record or null
33
+ */
34
+ async findTokenById(tokenId) {
35
+ return await Token.findById(tokenId);
36
+ }
37
+
38
+ /**
39
+ * Find tokens by user ID
40
+ * @param {string} userId - The user ID
41
+ * @returns {Promise<Array>} Array of token records
42
+ */
43
+ async findTokensByUserId(userId) {
44
+ return await Token.find({ user: userId });
45
+ }
46
+
47
+ /**
48
+ * Delete a token by ID
49
+ * @param {string} tokenId - The token ID
50
+ * @returns {Promise<Object>} The deletion result
51
+ */
52
+ async deleteToken(tokenId) {
53
+ return await Token.deleteOne({ _id: tokenId });
54
+ }
55
+
56
+ /**
57
+ * Delete expired tokens
58
+ * @returns {Promise<Object>} The deletion result with count
59
+ */
60
+ async deleteExpiredTokens() {
61
+ return await Token.deleteMany({
62
+ expires: { $lt: new Date() },
63
+ });
64
+ }
65
+
66
+ /**
67
+ * Delete all tokens for a user
68
+ * @param {string} userId - The user ID
69
+ * @returns {Promise<Object>} The deletion result
70
+ */
71
+ async deleteTokensByUserId(userId) {
72
+ return await Token.deleteMany({ user: userId });
73
+ }
74
+
75
+ /**
76
+ * Create JSON token string from token object and raw token
77
+ * @param {Object} token - The token record
78
+ * @param {string} rawToken - The raw token string
79
+ * @returns {string} JSON string with id and token
80
+ */
81
+ createJSONToken(token, rawToken) {
82
+ return Token.createJSONToken(token, rawToken);
83
+ }
84
+
85
+ /**
86
+ * Create base64 encoded buffer token
87
+ * @param {Object} token - The token record
88
+ * @param {string} rawToken - The raw token string
89
+ * @returns {string} Base64 encoded token
90
+ */
91
+ createBase64BufferToken(token, rawToken) {
92
+ return Token.createBase64BufferToken(token, rawToken);
93
+ }
94
+
95
+ /**
96
+ * Parse JSON token from base64 buffer
97
+ * @param {string} buffer - Base64 encoded token string
98
+ * @returns {Object} Parsed token object with id and token
99
+ */
100
+ getJSONTokenFromBase64BufferToken(buffer) {
101
+ return Token.getJSONTokenFromBase64BufferToken(buffer);
102
+ }
103
+ }
104
+
105
+ module.exports = { TokenRepository };
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Use Case for checking database health.
3
+ * Contains business logic for determining database connectivity and health status.
4
+ */
5
+ class CheckDatabaseHealthUseCase {
6
+ /**
7
+ * @param {Object} params
8
+ * @param {import('../health-check-repository').HealthCheckRepository} params.healthCheckRepository
9
+ */
10
+ constructor({ healthCheckRepository }) {
11
+ this.repository = healthCheckRepository;
12
+ }
13
+
14
+ /**
15
+ * Execute database health check
16
+ * @returns {Promise<Object>} Health check result with status, state, and response time
17
+ */
18
+ async execute() {
19
+ const { stateName, isConnected } = this.repository.getDatabaseConnectionState();
20
+
21
+ const result = {
22
+ status: isConnected ? 'healthy' : 'unhealthy',
23
+ state: stateName,
24
+ };
25
+
26
+ if (isConnected) {
27
+ result.responseTime = await this.repository.pingDatabase(2000);
28
+ }
29
+
30
+ return result;
31
+ }
32
+ }
33
+
34
+ module.exports = { CheckDatabaseHealthUseCase };
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Use Case for testing encryption functionality.
3
+ * Contains business logic for verifying that encryption and decryption work correctly.
4
+ *
5
+ * Follows DDD/Hexagonal Architecture:
6
+ * - Application Layer (this use case)
7
+ * - Depends on Infrastructure Layer (HealthCheckRepository)
8
+ */
9
+ class TestEncryptionUseCase {
10
+ /**
11
+ * @param {Object} params
12
+ * @param {import('../health-check-repository').HealthCheckRepository} params.healthCheckRepository
13
+ */
14
+ constructor({ healthCheckRepository }) {
15
+ this.repository = healthCheckRepository;
16
+ }
17
+
18
+ /**
19
+ * Execute encryption test
20
+ * Orchestrates the full encryption test workflow
21
+ * @returns {Promise<Object>} Test results with status and details
22
+ */
23
+ async execute() {
24
+ const TestModel = this.repository.createEncryptionTestModel();
25
+
26
+ const testData = {
27
+ testSecret: 'This is a secret value that should be encrypted',
28
+ normalField: 'This is a normal field that should not be encrypted',
29
+ nestedSecret: {
30
+ value: 'This is a nested secret that should be encrypted',
31
+ },
32
+ };
33
+
34
+ const testDoc = await this._withTimeout(
35
+ this.repository.saveTestDocument(TestModel, testData),
36
+ 5000,
37
+ 'Save operation timed out'
38
+ );
39
+
40
+ try {
41
+ const retrievedDoc = await this._withTimeout(
42
+ this.repository.findTestDocumentById(TestModel, testDoc._id),
43
+ 5000,
44
+ 'Find operation timed out'
45
+ );
46
+
47
+ const decryptionWorks = this._verifyDecryption(
48
+ retrievedDoc,
49
+ testData
50
+ );
51
+
52
+ const rawDoc = await this._withTimeout(
53
+ this.repository.getRawDocumentFromCollection(
54
+ TestModel.collection.name,
55
+ { _id: testDoc._id }
56
+ ),
57
+ 5000,
58
+ 'Database verification timed out'
59
+ );
60
+
61
+ const encryptionResults = this._verifyEncryptionInDatabase(
62
+ rawDoc,
63
+ testData
64
+ );
65
+
66
+ return this._evaluateEncryptionResults(
67
+ decryptionWorks,
68
+ encryptionResults
69
+ );
70
+ } finally {
71
+ await this._withTimeout(
72
+ this.repository.deleteTestDocument(TestModel, testDoc._id),
73
+ 5000,
74
+ 'Delete operation timed out'
75
+ );
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Verify that a document was decrypted correctly
81
+ * @param {Object} retrievedDoc - Document retrieved from database
82
+ * @param {Object} originalData - Original unencrypted data
83
+ * @returns {boolean} True if decryption worked correctly
84
+ * @private
85
+ */
86
+ _verifyDecryption(retrievedDoc, originalData) {
87
+ return (
88
+ retrievedDoc &&
89
+ retrievedDoc.testSecret === originalData.testSecret &&
90
+ retrievedDoc.normalField === originalData.normalField &&
91
+ retrievedDoc.nestedSecret?.value === originalData.nestedSecret.value
92
+ );
93
+ }
94
+
95
+ /**
96
+ * Verify that data was encrypted in the database
97
+ * Business rule: Encrypted fields should contain ':' and differ from original
98
+ * @param {Object} rawDoc - Raw document from database
99
+ * @param {Object} originalData - Original unencrypted data
100
+ * @returns {Object} Encryption verification results
101
+ * @private
102
+ */
103
+ _verifyEncryptionInDatabase(rawDoc, originalData) {
104
+ const secretIsEncrypted =
105
+ rawDoc &&
106
+ typeof rawDoc.testSecret === 'string' &&
107
+ rawDoc.testSecret.includes(':') &&
108
+ rawDoc.testSecret !== originalData.testSecret;
109
+
110
+ const nestedIsEncrypted =
111
+ rawDoc?.nestedSecret?.value &&
112
+ typeof rawDoc.nestedSecret.value === 'string' &&
113
+ rawDoc.nestedSecret.value.includes(':') &&
114
+ rawDoc.nestedSecret.value !== originalData.nestedSecret.value;
115
+
116
+ const normalNotEncrypted =
117
+ rawDoc && rawDoc.normalField === originalData.normalField;
118
+
119
+ return {
120
+ secretIsEncrypted,
121
+ nestedIsEncrypted,
122
+ normalNotEncrypted,
123
+ };
124
+ }
125
+
126
+ /**
127
+ * Evaluate encryption test results
128
+ * Business logic for determining if encryption is healthy
129
+ * @param {boolean} decryptionWorks - Whether decryption succeeded
130
+ * @param {Object} encryptionResults - Encryption verification results
131
+ * @returns {Object} Test status and result message
132
+ * @private
133
+ */
134
+ _evaluateEncryptionResults(decryptionWorks, encryptionResults) {
135
+ const { secretIsEncrypted, nestedIsEncrypted, normalNotEncrypted } =
136
+ encryptionResults;
137
+
138
+ if (
139
+ decryptionWorks &&
140
+ secretIsEncrypted &&
141
+ nestedIsEncrypted &&
142
+ normalNotEncrypted
143
+ ) {
144
+ return {
145
+ status: 'enabled',
146
+ testResult:
147
+ 'Encryption and decryption verified successfully',
148
+ encryptionWorks: true,
149
+ };
150
+ }
151
+
152
+ if (decryptionWorks && (!secretIsEncrypted || !nestedIsEncrypted)) {
153
+ return {
154
+ status: 'unhealthy',
155
+ testResult: 'Fields are not being encrypted in database',
156
+ encryptionWorks: false,
157
+ };
158
+ }
159
+
160
+ if (decryptionWorks && !normalNotEncrypted) {
161
+ return {
162
+ status: 'unhealthy',
163
+ testResult: 'Normal fields are being incorrectly encrypted',
164
+ encryptionWorks: false,
165
+ };
166
+ }
167
+
168
+ return {
169
+ status: 'unhealthy',
170
+ testResult: 'Decryption failed or data mismatch',
171
+ encryptionWorks: false,
172
+ };
173
+ }
174
+
175
+ /**
176
+ * Execute promise with timeout
177
+ * @param {Promise} promise - Promise to execute
178
+ * @param {number} ms - Timeout in milliseconds
179
+ * @param {string} errorMessage - Error message for timeout
180
+ * @returns {Promise} Promise that rejects on timeout
181
+ * @private
182
+ */
183
+ _withTimeout(promise, ms, errorMessage) {
184
+ return Promise.race([
185
+ promise,
186
+ new Promise((_, reject) =>
187
+ setTimeout(() => reject(new Error(errorMessage)), ms)
188
+ ),
189
+ ]);
190
+ }
191
+ }
192
+
193
+ module.exports = { TestEncryptionUseCase };
@@ -0,0 +1,44 @@
1
+ const { WebsocketConnection } = require('./models/WebsocketConnection');
2
+
3
+ /**
4
+ * Repository for WebSocket connection operations.
5
+ * Handles persistence of active WebSocket connections.
6
+ */
7
+ class WebsocketConnectionRepository {
8
+ /**
9
+ * Create a new WebSocket connection record
10
+ * @param {string} connectionId - The WebSocket connection ID
11
+ * @returns {Promise<Object>} The created connection record
12
+ */
13
+ async createConnection(connectionId) {
14
+ return await WebsocketConnection.create({ connectionId });
15
+ }
16
+
17
+ /**
18
+ * Delete a WebSocket connection record
19
+ * @param {string} connectionId - The WebSocket connection ID to delete
20
+ * @returns {Promise<Object>} The deletion result
21
+ */
22
+ async deleteConnection(connectionId) {
23
+ return await WebsocketConnection.deleteOne({ connectionId });
24
+ }
25
+
26
+ /**
27
+ * Get all active WebSocket connections
28
+ * @returns {Promise<Array>} Array of active connection objects with send capability
29
+ */
30
+ async getActiveConnections() {
31
+ return await WebsocketConnection.getActiveConnections();
32
+ }
33
+
34
+ /**
35
+ * Find a connection by ID
36
+ * @param {string} connectionId - The WebSocket connection ID
37
+ * @returns {Promise<Object|null>} The connection record or null
38
+ */
39
+ async findConnection(connectionId) {
40
+ return await WebsocketConnection.findOne({ connectionId });
41
+ }
42
+ }
43
+
44
+ module.exports = { WebsocketConnectionRepository };
@@ -237,4 +237,106 @@ await mongoose.connection.db.admin().ping({ maxTimeMS: 2000 });
237
237
 
238
238
  ## Environment Variables
239
239
 
240
- - `HEALTH_API_KEY`: Required API key for accessing detailed health endpoints
240
+ - `HEALTH_API_KEY`: Required API key for accessing detailed health endpoints
241
+
242
+ ## TODO: DDD/Hexagonal Architecture Refactoring
243
+
244
+ ### Current Architecture Issues
245
+
246
+ The health router (health.js, 677 lines) currently violates DDD/Hexagonal Architecture principles:
247
+
248
+ **✅ What's Good:**
249
+ - Database access properly abstracted through `HealthCheckRepository`
250
+ - `CheckDatabaseHealthUseCase` and `TestEncryptionUseCase` correctly implement use case pattern
251
+ - All tests passing, no breaking changes
252
+
253
+ **❌ Architecture Violations:**
254
+ 1. **Handler contains significant business logic** - Functions like `getEncryptionConfiguration()`, `checkEncryptionHealth()`, `checkKmsDecryptCapability()`, `detectVpcConfiguration()`, `checkExternalAPIs()`, and `checkIntegrations()` contain business logic that should be in use cases
255
+ 2. **Direct infrastructure dependencies** - Handler directly uses `https`, `http`, Node.js `dns`, and factory modules instead of accessing through repositories
256
+ 3. **Mixed concerns** - Single file handles HTTP routing, business logic, infrastructure detection, and response formatting
257
+ 4. **Violates dependency rule** - Handler should only call use cases, never repositories or contain business logic
258
+
259
+ ### Proposed Refactoring Plan
260
+
261
+ #### Priority 1: Extract Core Health Check Use Cases (Immediate)
262
+
263
+ **New Use Cases:**
264
+ 1. `CheckEncryptionHealthUseCase` - Orchestrate encryption testing with configuration checks (from health.js:122-181)
265
+ 2. `CheckKmsConnectivityUseCase` - Test KMS decrypt capability (from health.js:339-490)
266
+ 3. `DetectNetworkConfigurationUseCase` - VPC and network detection (from health.js:244-336)
267
+
268
+ **New Repositories:**
269
+ 1. `EncryptionConfigRepository` - Get encryption mode, bypass rules (from health.js:98-120)
270
+ 2. `KmsRepository` - KMS connectivity testing, decrypt capability checks
271
+ 3. `NetworkRepository` - DNS resolution, VPC detection, TCP connectivity tests
272
+
273
+ #### Priority 2: Extract External Service Checks
274
+
275
+ **New Use Cases:**
276
+ 4. `CheckExternalServicesUseCase` - Check external API availability (from health.js:183-209)
277
+
278
+ **New Repositories:**
279
+ 4. `ExternalServiceRepository` - HTTP-based service health checking with timeout handling
280
+
281
+ #### Priority 3: Extract Integration Checks
282
+
283
+ **New Use Cases:**
284
+ 5. `CheckIntegrationAvailabilityUseCase` - Verify integrations and modules loaded (from health.js:211-231)
285
+
286
+ **Extend Existing:**
287
+ - Add `getAvailableIntegrations()` and `getAvailableModules()` methods to existing `IntegrationRepository`
288
+
289
+ ### Architectural Principles to Follow
290
+
291
+ **The Handler Should Only:**
292
+ - Define routes
293
+ - Call use cases
294
+ - Map use case results to HTTP responses
295
+ - Handle HTTP-specific concerns (status codes, headers)
296
+
297
+ **The Rule:**
298
+ > "Handlers (adapters) should only call use cases, never repositories or business logic directly"
299
+
300
+ **Dependency Direction:**
301
+ ```
302
+ Handler (Adapter Layer)
303
+ ↓ calls
304
+ Use Cases (Application Layer)
305
+ ↓ calls
306
+ Repositories (Infrastructure Layer)
307
+ ↓ calls
308
+ External Systems (Database, APIs, AWS Services)
309
+ ```
310
+
311
+ ### Expected Outcome
312
+
313
+ - Reduce health.js from **677 lines to ~100-150 lines**
314
+ - All business logic moved to use cases
315
+ - All infrastructure access moved to repositories
316
+ - Handler becomes thin HTTP adapter
317
+ - Improved testability (use cases testable without HTTP context)
318
+ - Better reusability (use cases usable in CLI tools, background jobs, etc.)
319
+
320
+ ### Implementation Status
321
+
322
+ - [ ] P1: Extract `CheckEncryptionHealthUseCase`
323
+ - [ ] P1: Create `EncryptionConfigRepository`
324
+ - [ ] P1: Extract `CheckKmsConnectivityUseCase`
325
+ - [ ] P1: Create `KmsRepository`
326
+ - [ ] P1: Extract `DetectNetworkConfigurationUseCase`
327
+ - [ ] P1: Create `NetworkRepository`
328
+ - [ ] P2: Extract `CheckExternalServicesUseCase`
329
+ - [ ] P2: Create `ExternalServiceRepository`
330
+ - [ ] P3: Extract `CheckIntegrationAvailabilityUseCase`
331
+ - [ ] P3: Extend existing `IntegrationRepository`
332
+
333
+ ### Future Considerations (Optional)
334
+
335
+ **Domain Models (Value Objects):**
336
+ - `HealthCheckResult` - Overall health check result with status, checks, timestamp
337
+ - `DatabaseHealth` - Database-specific health information
338
+ - `EncryptionHealth` - Encryption-specific health information
339
+ - `ServiceHealth` - Generic external service health
340
+ - `NetworkConfiguration` - VPC and network detection results
341
+
342
+ These would replace plain objects and provide type safety and business logic encapsulation.