@friggframework/core 2.0.0--canary.397.fe6d7a2.0 → 2.0.0--canary.405.45825cd.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 (100) hide show
  1. package/README.md +50 -931
  2. package/core/create-handler.js +0 -1
  3. package/encrypt/encrypt.js +5 -33
  4. package/handlers/app-handler-helpers.js +3 -0
  5. package/handlers/backend-utils.js +44 -42
  6. package/handlers/routers/HEALTHCHECK.md +240 -0
  7. package/handlers/routers/auth.js +14 -3
  8. package/handlers/routers/health.js +451 -0
  9. package/handlers/routers/health.test.js +203 -0
  10. package/handlers/routers/integration-defined-routers.js +5 -8
  11. package/handlers/routers/middleware/loadUser.js +15 -0
  12. package/handlers/routers/middleware/requireLoggedInUser.js +12 -0
  13. package/handlers/routers/user.js +5 -25
  14. package/handlers/workers/integration-defined-workers.js +3 -6
  15. package/index.js +16 -1
  16. package/integrations/create-frigg-backend.js +31 -0
  17. package/integrations/index.js +5 -0
  18. package/integrations/integration-base.js +46 -142
  19. package/integrations/integration-factory.js +251 -0
  20. package/integrations/integration-router.js +181 -303
  21. package/integrations/integration-user.js +144 -0
  22. package/integrations/options.js +1 -1
  23. package/integrations/test/integration-base.test.js +144 -0
  24. package/module-plugin/auther.js +393 -0
  25. package/module-plugin/entity-manager.js +70 -0
  26. package/{modules → module-plugin}/entity.js +0 -1
  27. package/{modules → module-plugin}/index.js +8 -0
  28. package/module-plugin/manager.js +169 -0
  29. package/module-plugin/module-factory.js +61 -0
  30. package/{modules → module-plugin}/test/mock-api/api.js +3 -8
  31. package/{modules → module-plugin}/test/mock-api/definition.js +8 -12
  32. package/package.json +5 -5
  33. package/syncs/sync.js +1 -0
  34. package/types/integrations/index.d.ts +6 -2
  35. package/types/module-plugin/index.d.ts +56 -4
  36. package/types/syncs/index.d.ts +2 -0
  37. package/credential/credential-repository.js +0 -56
  38. package/credential/use-cases/get-credential-for-user.js +0 -21
  39. package/credential/use-cases/update-authentication-status.js +0 -15
  40. package/handlers/app-definition-loader.js +0 -38
  41. package/integrations/integration-repository.js +0 -80
  42. package/integrations/tests/doubles/dummy-integration-class.js +0 -90
  43. package/integrations/tests/doubles/test-integration-repository.js +0 -89
  44. package/integrations/tests/use-cases/create-integration.test.js +0 -124
  45. package/integrations/tests/use-cases/delete-integration-for-user.test.js +0 -143
  46. package/integrations/tests/use-cases/get-integration-for-user.test.js +0 -143
  47. package/integrations/tests/use-cases/get-integration-instance.test.js +0 -169
  48. package/integrations/tests/use-cases/get-integrations-for-user.test.js +0 -169
  49. package/integrations/tests/use-cases/get-possible-integrations.test.js +0 -188
  50. package/integrations/tests/use-cases/update-integration-messages.test.js +0 -142
  51. package/integrations/tests/use-cases/update-integration-status.test.js +0 -103
  52. package/integrations/tests/use-cases/update-integration.test.js +0 -134
  53. package/integrations/use-cases/create-integration.js +0 -71
  54. package/integrations/use-cases/delete-integration-for-user.js +0 -72
  55. package/integrations/use-cases/get-integration-for-user.js +0 -78
  56. package/integrations/use-cases/get-integration-instance-by-definition.js +0 -67
  57. package/integrations/use-cases/get-integration-instance.js +0 -82
  58. package/integrations/use-cases/get-integrations-for-user.js +0 -76
  59. package/integrations/use-cases/get-possible-integrations.js +0 -27
  60. package/integrations/use-cases/index.js +0 -11
  61. package/integrations/use-cases/update-integration-messages.js +0 -31
  62. package/integrations/use-cases/update-integration-status.js +0 -28
  63. package/integrations/use-cases/update-integration.js +0 -91
  64. package/integrations/utils/map-integration-dto.js +0 -36
  65. package/modules/module-factory.js +0 -54
  66. package/modules/module-repository.js +0 -107
  67. package/modules/module.js +0 -218
  68. package/modules/tests/doubles/test-module-factory.js +0 -16
  69. package/modules/tests/doubles/test-module-repository.js +0 -19
  70. package/modules/use-cases/get-entities-for-user.js +0 -32
  71. package/modules/use-cases/get-entity-options-by-id.js +0 -58
  72. package/modules/use-cases/get-entity-options-by-type.js +0 -34
  73. package/modules/use-cases/get-module-instance-from-type.js +0 -31
  74. package/modules/use-cases/get-module.js +0 -56
  75. package/modules/use-cases/process-authorization-callback.js +0 -108
  76. package/modules/use-cases/refresh-entity-options.js +0 -58
  77. package/modules/use-cases/test-module-auth.js +0 -54
  78. package/modules/utils/map-module-dto.js +0 -18
  79. package/user/tests/doubles/test-user-repository.js +0 -72
  80. package/user/tests/use-cases/create-individual-user.test.js +0 -24
  81. package/user/tests/use-cases/create-organization-user.test.js +0 -28
  82. package/user/tests/use-cases/create-token-for-user-id.test.js +0 -19
  83. package/user/tests/use-cases/get-user-from-bearer-token.test.js +0 -64
  84. package/user/tests/use-cases/login-user.test.js +0 -140
  85. package/user/use-cases/create-individual-user.js +0 -61
  86. package/user/use-cases/create-organization-user.js +0 -47
  87. package/user/use-cases/create-token-for-user-id.js +0 -30
  88. package/user/use-cases/get-user-from-bearer-token.js +0 -77
  89. package/user/use-cases/login-user.js +0 -122
  90. package/user/user-repository.js +0 -62
  91. package/user/user.js +0 -77
  92. /package/{modules → module-plugin}/ModuleConstants.js +0 -0
  93. /package/{modules → module-plugin}/credential.js +0 -0
  94. /package/{modules → module-plugin}/requester/api-key.js +0 -0
  95. /package/{modules → module-plugin}/requester/basic.js +0 -0
  96. /package/{modules → module-plugin}/requester/oauth-2.js +0 -0
  97. /package/{modules → module-plugin}/requester/requester.js +0 -0
  98. /package/{modules → module-plugin}/requester/requester.test.js +0 -0
  99. /package/{modules → module-plugin}/test/auther.test.js +0 -0
  100. /package/{modules → module-plugin}/test/mock-api/mocks/hubspot.js +0 -0
@@ -0,0 +1,451 @@
1
+ const { Router } = require('express');
2
+ const mongoose = require('mongoose');
3
+ const https = require('https');
4
+ const http = require('http');
5
+ const { moduleFactory, integrationFactory } = require('./../backend-utils');
6
+ const { createAppHandler } = require('./../app-handler-helpers');
7
+
8
+ const router = Router();
9
+
10
+ const validateApiKey = (req, res, next) => {
11
+ const apiKey = req.headers['x-api-key'];
12
+
13
+ if (req.path === '/health') {
14
+ return next();
15
+ }
16
+
17
+ if (!apiKey || apiKey !== process.env.HEALTH_API_KEY) {
18
+ return res.status(401).json({
19
+ status: 'error',
20
+ message: 'Unauthorized'
21
+ });
22
+ }
23
+
24
+ next();
25
+ };
26
+
27
+ router.use(validateApiKey);
28
+
29
+ const checkExternalAPI = (url, timeout = 5000) => {
30
+ return new Promise((resolve) => {
31
+ const protocol = url.startsWith('https:') ? https : http;
32
+ const startTime = Date.now();
33
+
34
+ try {
35
+ const request = protocol.get(url, { timeout }, (res) => {
36
+ const responseTime = Date.now() - startTime;
37
+ resolve({
38
+ status: 'healthy',
39
+ statusCode: res.statusCode,
40
+ responseTime,
41
+ reachable: res.statusCode < 500
42
+ });
43
+ });
44
+
45
+ request.on('error', (error) => {
46
+ resolve({
47
+ status: 'unhealthy',
48
+ error: error.message,
49
+ responseTime: Date.now() - startTime,
50
+ reachable: false
51
+ });
52
+ });
53
+
54
+ request.on('timeout', () => {
55
+ request.destroy();
56
+ resolve({
57
+ status: 'timeout',
58
+ error: 'Request timeout',
59
+ responseTime: timeout,
60
+ reachable: false
61
+ });
62
+ });
63
+ } catch (error) {
64
+ resolve({
65
+ status: 'error',
66
+ error: error.message,
67
+ responseTime: Date.now() - startTime,
68
+ reachable: false
69
+ });
70
+ }
71
+ });
72
+ };
73
+
74
+ const getDatabaseState = () => {
75
+ const stateMap = {
76
+ 0: 'disconnected',
77
+ 1: 'connected',
78
+ 2: 'connecting',
79
+ 3: 'disconnecting'
80
+ };
81
+ const readyState = mongoose.connection.readyState;
82
+
83
+ return {
84
+ readyState,
85
+ stateName: stateMap[readyState],
86
+ isConnected: readyState === 1
87
+ };
88
+ };
89
+
90
+ const checkDatabaseHealth = async () => {
91
+ const { stateName, isConnected } = getDatabaseState();
92
+ const result = {
93
+ status: isConnected ? 'healthy' : 'unhealthy',
94
+ state: stateName
95
+ };
96
+
97
+ if (isConnected) {
98
+ const pingStart = Date.now();
99
+ await mongoose.connection.db.admin().ping({ maxTimeMS: 2000 });
100
+ result.responseTime = Date.now() - pingStart;
101
+ }
102
+
103
+ return result;
104
+ };
105
+
106
+ const getEncryptionConfiguration = () => {
107
+ const { STAGE, BYPASS_ENCRYPTION_STAGE, KMS_KEY_ARN, AES_KEY_ID } = process.env;
108
+
109
+ const defaultBypassStages = ['dev', 'test', 'local'];
110
+ const useEnv = BYPASS_ENCRYPTION_STAGE !== undefined;
111
+ const bypassStages = useEnv
112
+ ? BYPASS_ENCRYPTION_STAGE.split(',').map((s) => s.trim())
113
+ : defaultBypassStages;
114
+
115
+ const isBypassed = bypassStages.includes(STAGE);
116
+ const hasAES = AES_KEY_ID && AES_KEY_ID.trim() !== '';
117
+ const hasKMS = KMS_KEY_ARN && KMS_KEY_ARN.trim() !== '' && !hasAES;
118
+ const mode = hasAES ? 'aes' : hasKMS ? 'kms' : 'none';
119
+
120
+ return {
121
+ stage: STAGE || null,
122
+ isBypassed,
123
+ hasAES,
124
+ hasKMS,
125
+ mode,
126
+ };
127
+ };
128
+
129
+ const createTestEncryptionModel = () => {
130
+ const { Encrypt } = require('./../../encrypt');
131
+
132
+ const testSchema = new mongoose.Schema({
133
+ testSecret: { type: String, lhEncrypt: true },
134
+ normalField: { type: String },
135
+ nestedSecret: {
136
+ value: { type: String, lhEncrypt: true }
137
+ }
138
+ }, { timestamps: false });
139
+
140
+ testSchema.plugin(Encrypt);
141
+
142
+ return mongoose.models.TestEncryption ||
143
+ mongoose.model('TestEncryption', testSchema);
144
+ };
145
+
146
+ const createTestDocument = async (TestModel) => {
147
+ const testData = {
148
+ testSecret: 'This is a secret value that should be encrypted',
149
+ normalField: 'This is a normal field that should not be encrypted',
150
+ nestedSecret: {
151
+ value: 'This is a nested secret that should be encrypted'
152
+ }
153
+ };
154
+
155
+ const testDoc = new TestModel(testData);
156
+ await testDoc.save();
157
+
158
+ return { testDoc, testData };
159
+ };
160
+
161
+ const verifyDecryption = (retrievedDoc, originalData) => {
162
+ return retrievedDoc &&
163
+ retrievedDoc.testSecret === originalData.testSecret &&
164
+ retrievedDoc.normalField === originalData.normalField &&
165
+ retrievedDoc.nestedSecret?.value === originalData.nestedSecret.value;
166
+ };
167
+
168
+ const verifyEncryptionInDatabase = async (testDoc, originalData, TestModel) => {
169
+ const collectionName = TestModel.collection.name;
170
+ const rawDoc = await mongoose.connection.db
171
+ .collection(collectionName)
172
+ .findOne({ _id: testDoc._id });
173
+
174
+ const secretIsEncrypted = rawDoc &&
175
+ typeof rawDoc.testSecret === 'string' &&
176
+ rawDoc.testSecret.includes(':') &&
177
+ rawDoc.testSecret !== originalData.testSecret;
178
+
179
+ const nestedIsEncrypted = rawDoc?.nestedSecret?.value &&
180
+ typeof rawDoc.nestedSecret.value === 'string' &&
181
+ rawDoc.nestedSecret.value.includes(':') &&
182
+ rawDoc.nestedSecret.value !== originalData.nestedSecret.value;
183
+
184
+ const normalNotEncrypted = rawDoc &&
185
+ rawDoc.normalField === originalData.normalField;
186
+
187
+ return {
188
+ secretIsEncrypted,
189
+ nestedIsEncrypted,
190
+ normalNotEncrypted
191
+ };
192
+ };
193
+
194
+ const evaluateEncryptionTestResults = (decryptionWorks, encryptionResults) => {
195
+ const { secretIsEncrypted, nestedIsEncrypted, normalNotEncrypted } = encryptionResults;
196
+
197
+ if (decryptionWorks && secretIsEncrypted && nestedIsEncrypted && normalNotEncrypted) {
198
+ return {
199
+ status: 'enabled',
200
+ testResult: 'Encryption and decryption verified successfully'
201
+ };
202
+ }
203
+
204
+ if (decryptionWorks && (!secretIsEncrypted || !nestedIsEncrypted)) {
205
+ return {
206
+ status: 'unhealthy',
207
+ testResult: 'Fields are not being encrypted in database'
208
+ };
209
+ }
210
+
211
+ if (decryptionWorks && !normalNotEncrypted) {
212
+ return {
213
+ status: 'unhealthy',
214
+ testResult: 'Normal fields are being incorrectly encrypted'
215
+ };
216
+ }
217
+
218
+ return {
219
+ status: 'unhealthy',
220
+ testResult: 'Decryption failed or data mismatch'
221
+ };
222
+ };
223
+
224
+ const testEncryption = async () => {
225
+ const TestModel = createTestEncryptionModel();
226
+ const { testDoc, testData } = await createTestDocument(TestModel);
227
+
228
+ try {
229
+ const retrievedDoc = await TestModel.findById(testDoc._id);
230
+ const decryptionWorks = verifyDecryption(retrievedDoc, testData);
231
+ const encryptionResults = await verifyEncryptionInDatabase(testDoc, testData, TestModel);
232
+
233
+ const evaluation = evaluateEncryptionTestResults(decryptionWorks, encryptionResults);
234
+
235
+ return {
236
+ ...evaluation,
237
+ encryptionWorks: decryptionWorks
238
+ };
239
+ } finally {
240
+ await TestModel.deleteOne({ _id: testDoc._id });
241
+ }
242
+ };
243
+
244
+ const checkEncryptionHealth = async () => {
245
+ const config = getEncryptionConfiguration();
246
+
247
+ if (config.isBypassed || config.mode === 'none') {
248
+ const testResult = config.isBypassed
249
+ ? 'Encryption bypassed for this stage'
250
+ : 'No encryption keys configured';
251
+
252
+ return {
253
+ status: 'disabled',
254
+ mode: config.mode,
255
+ bypassed: config.isBypassed,
256
+ stage: config.stage,
257
+ testResult,
258
+ encryptionWorks: false,
259
+ debug: {
260
+ hasKMS: config.hasKMS,
261
+ hasAES: config.hasAES
262
+ }
263
+ };
264
+ }
265
+
266
+ try {
267
+ const testResults = await testEncryption();
268
+
269
+ return {
270
+ ...testResults,
271
+ mode: config.mode,
272
+ bypassed: config.isBypassed,
273
+ stage: config.stage,
274
+ debug: {
275
+ hasKMS: config.hasKMS,
276
+ hasAES: config.hasAES
277
+ }
278
+ };
279
+ } catch (error) {
280
+ return {
281
+ status: 'unhealthy',
282
+ mode: config.mode,
283
+ bypassed: config.isBypassed,
284
+ stage: config.stage,
285
+ testResult: `Encryption test failed: ${error.message}`,
286
+ encryptionWorks: false,
287
+ debug: {
288
+ hasKMS: config.hasKMS,
289
+ hasAES: config.hasAES
290
+ }
291
+ };
292
+ }
293
+ };
294
+
295
+ const checkExternalAPIs = async () => {
296
+ const apis = [
297
+ { name: 'github', url: 'https://api.github.com/status' },
298
+ { name: 'npm', url: 'https://registry.npmjs.org' }
299
+ ];
300
+
301
+ const results = await Promise.all(
302
+ apis.map(api =>
303
+ checkExternalAPI(api.url).then(result => ({ name: api.name, ...result }))
304
+ )
305
+ );
306
+
307
+ const apiStatuses = {};
308
+ let allReachable = true;
309
+
310
+ results.forEach(({ name, ...checkResult }) => {
311
+ apiStatuses[name] = checkResult;
312
+ if (!checkResult.reachable) {
313
+ allReachable = false;
314
+ }
315
+ });
316
+
317
+ return { apiStatuses, allReachable };
318
+ };
319
+
320
+ const checkIntegrations = () => {
321
+ const moduleTypes = Array.isArray(moduleFactory.moduleTypes)
322
+ ? moduleFactory.moduleTypes
323
+ : [];
324
+
325
+ const integrationTypes = Array.isArray(integrationFactory.integrationTypes)
326
+ ? integrationFactory.integrationTypes
327
+ : [];
328
+
329
+ return {
330
+ status: 'healthy',
331
+ modules: {
332
+ count: moduleTypes.length,
333
+ available: moduleTypes,
334
+ },
335
+ integrations: {
336
+ count: integrationTypes.length,
337
+ available: integrationTypes,
338
+ },
339
+ };
340
+ };
341
+
342
+ const buildHealthCheckResponse = (startTime) => {
343
+ return {
344
+ service: 'frigg-core-api',
345
+ status: 'healthy',
346
+ timestamp: new Date().toISOString(),
347
+ checks: {},
348
+ calculateResponseTime: () => Date.now() - startTime
349
+ };
350
+ };
351
+
352
+ router.get('/health', async (_req, res) => {
353
+ const status = {
354
+ status: 'ok',
355
+ timestamp: new Date().toISOString(),
356
+ service: 'frigg-core-api'
357
+ };
358
+
359
+ res.status(200).json(status);
360
+ });
361
+
362
+ router.get('/health/detailed', async (_req, res) => {
363
+ const startTime = Date.now();
364
+ const response = buildHealthCheckResponse(startTime);
365
+
366
+ try {
367
+ response.checks.database = await checkDatabaseHealth();
368
+ const dbState = getDatabaseState();
369
+ if (!dbState.isConnected) {
370
+ response.status = 'unhealthy';
371
+ }
372
+ } catch (error) {
373
+ response.checks.database = {
374
+ status: 'unhealthy',
375
+ error: error.message
376
+ };
377
+ response.status = 'unhealthy';
378
+ }
379
+
380
+ try {
381
+ response.checks.encryption = await checkEncryptionHealth();
382
+ if (response.checks.encryption.status === 'unhealthy') {
383
+ response.status = 'unhealthy';
384
+ }
385
+ } catch (error) {
386
+ response.checks.encryption = {
387
+ status: 'unhealthy',
388
+ error: error.message
389
+ };
390
+ response.status = 'unhealthy';
391
+ }
392
+
393
+ const { apiStatuses, allReachable } = await checkExternalAPIs();
394
+ response.checks.externalApis = apiStatuses;
395
+ if (!allReachable) {
396
+ response.status = 'unhealthy';
397
+ }
398
+
399
+ try {
400
+ response.checks.integrations = checkIntegrations();
401
+ } catch (error) {
402
+ response.checks.integrations = {
403
+ status: 'unhealthy',
404
+ error: error.message
405
+ };
406
+ response.status = 'unhealthy';
407
+ }
408
+
409
+ response.responseTime = response.calculateResponseTime();
410
+ delete response.calculateResponseTime;
411
+
412
+ const statusCode = response.status === 'healthy' ? 200 : 503;
413
+ res.status(statusCode).json(response);
414
+ });
415
+
416
+ router.get('/health/live', (_req, res) => {
417
+ res.status(200).json({
418
+ status: 'alive',
419
+ timestamp: new Date().toISOString()
420
+ });
421
+ });
422
+
423
+ router.get('/health/ready', async (_req, res) => {
424
+ const dbState = getDatabaseState();
425
+ const isDbReady = dbState.isConnected;
426
+
427
+ let areModulesReady = false;
428
+ try {
429
+ const moduleTypes = Array.isArray(moduleFactory.moduleTypes)
430
+ ? moduleFactory.moduleTypes
431
+ : [];
432
+ areModulesReady = moduleTypes.length > 0;
433
+ } catch (error) {
434
+ areModulesReady = false;
435
+ }
436
+
437
+ const isReady = isDbReady && areModulesReady;
438
+
439
+ res.status(isReady ? 200 : 503).json({
440
+ ready: isReady,
441
+ timestamp: new Date().toISOString(),
442
+ checks: {
443
+ database: isDbReady,
444
+ modules: areModulesReady
445
+ }
446
+ });
447
+ });
448
+
449
+ const handler = createAppHandler('HTTP Event: Health', router);
450
+
451
+ module.exports = { handler, router };
@@ -0,0 +1,203 @@
1
+ process.env.HEALTH_API_KEY = 'test-api-key';
2
+
3
+ jest.mock('mongoose', () => ({
4
+ set: jest.fn(),
5
+ connection: {
6
+ readyState: 1,
7
+ db: {
8
+ admin: () => ({
9
+ ping: jest.fn().mockResolvedValue(true)
10
+ })
11
+ }
12
+ }
13
+ }));
14
+
15
+ jest.mock('./../backend-utils', () => ({
16
+ moduleFactory: {
17
+ moduleTypes: ['test-module', 'another-module']
18
+ },
19
+ integrationFactory: {
20
+ integrationTypes: ['test-integration', 'another-integration']
21
+ }
22
+ }));
23
+
24
+ jest.mock('./../app-handler-helpers', () => ({
25
+ createAppHandler: jest.fn((name, router) => ({ name, router }))
26
+ }));
27
+
28
+ const { router } = require('./health');
29
+ const mongoose = require('mongoose');
30
+
31
+ const mockRequest = (path, headers = {}) => ({
32
+ path,
33
+ headers
34
+ });
35
+
36
+ const mockResponse = () => {
37
+ const res = {};
38
+ res.status = jest.fn().mockReturnValue(res);
39
+ res.json = jest.fn().mockReturnValue(res);
40
+ return res;
41
+ };
42
+
43
+ describe('Health Check Endpoints', () => {
44
+ beforeEach(() => {
45
+ mongoose.connection.readyState = 1;
46
+ });
47
+
48
+ describe('Middleware - validateApiKey', () => {
49
+ it('should allow access to /health without authentication', async () => {
50
+ expect(true).toBe(true);
51
+ });
52
+ });
53
+
54
+ describe('GET /health', () => {
55
+ it('should return basic health status', async () => {
56
+ const req = mockRequest('/health');
57
+ const res = mockResponse();
58
+
59
+ const routeHandler = router.stack.find(layer =>
60
+ layer.route && layer.route.path === '/health'
61
+ ).route.stack[0].handle;
62
+
63
+ await routeHandler(req, res);
64
+
65
+ expect(res.status).toHaveBeenCalledWith(200);
66
+ expect(res.json).toHaveBeenCalledWith({
67
+ status: 'ok',
68
+ timestamp: expect.any(String),
69
+ service: 'frigg-core-api'
70
+ });
71
+ });
72
+ });
73
+
74
+ describe('GET /health/detailed', () => {
75
+ it('should return detailed health status when healthy', async () => {
76
+ const req = mockRequest('/health/detailed', { 'x-api-key': 'test-api-key' });
77
+ const res = mockResponse();
78
+
79
+ const originalPromiseAll = Promise.all;
80
+ Promise.all = jest.fn().mockResolvedValue([
81
+ { name: 'github', status: 'healthy', reachable: true, statusCode: 200, responseTime: 100 },
82
+ { name: 'npm', status: 'healthy', reachable: true, statusCode: 200, responseTime: 150 }
83
+ ]);
84
+
85
+ const routeHandler = router.stack.find(layer =>
86
+ layer.route && layer.route.path === '/health/detailed'
87
+ ).route.stack[0].handle;
88
+
89
+ await routeHandler(req, res);
90
+
91
+ Promise.all = originalPromiseAll;
92
+
93
+ expect(res.status).toHaveBeenCalledWith(200);
94
+ expect(res.json).toHaveBeenCalledWith(expect.objectContaining({
95
+ status: 'healthy',
96
+ service: 'frigg-core-api',
97
+ timestamp: expect.any(String),
98
+ checks: expect.objectContaining({
99
+ database: expect.objectContaining({
100
+ status: 'healthy',
101
+ state: 'connected'
102
+ }),
103
+ integrations: expect.objectContaining({
104
+ status: 'healthy'
105
+ })
106
+ }),
107
+ responseTime: expect.any(Number)
108
+ }));
109
+
110
+ const response = res.json.mock.calls[0][0];
111
+ expect(response).not.toHaveProperty('version');
112
+ expect(response).not.toHaveProperty('uptime');
113
+ expect(response.checks).not.toHaveProperty('memory');
114
+ expect(response.checks.database).not.toHaveProperty('type');
115
+ });
116
+
117
+ it('should return 503 when database is disconnected', async () => {
118
+ mongoose.connection.readyState = 0;
119
+
120
+ const req = mockRequest('/health/detailed', { 'x-api-key': 'test-api-key' });
121
+ const res = mockResponse();
122
+
123
+ const originalPromiseAll = Promise.all;
124
+ Promise.all = jest.fn().mockResolvedValue([
125
+ { name: 'github', status: 'healthy', reachable: true, statusCode: 200, responseTime: 100 },
126
+ { name: 'npm', status: 'healthy', reachable: true, statusCode: 200, responseTime: 150 }
127
+ ]);
128
+
129
+ const routeHandler = router.stack.find(layer =>
130
+ layer.route && layer.route.path === '/health/detailed'
131
+ ).route.stack[0].handle;
132
+
133
+ await routeHandler(req, res);
134
+
135
+ Promise.all = originalPromiseAll;
136
+
137
+ expect(res.status).toHaveBeenCalledWith(503);
138
+ expect(res.json).toHaveBeenCalledWith(expect.objectContaining({
139
+ status: 'unhealthy'
140
+ }));
141
+ });
142
+ });
143
+
144
+ describe('GET /health/live', () => {
145
+ it('should return alive status', async () => {
146
+ const req = mockRequest('/health/live', { 'x-api-key': 'test-api-key' });
147
+ const res = mockResponse();
148
+
149
+ const routeHandler = router.stack.find(layer =>
150
+ layer.route && layer.route.path === '/health/live'
151
+ ).route.stack[0].handle;
152
+
153
+ routeHandler(req, res);
154
+
155
+ expect(res.status).toHaveBeenCalledWith(200);
156
+ expect(res.json).toHaveBeenCalledWith({
157
+ status: 'alive',
158
+ timestamp: expect.any(String)
159
+ });
160
+ });
161
+ });
162
+
163
+ describe('GET /health/ready', () => {
164
+ it('should return ready when all checks pass', async () => {
165
+ const req = mockRequest('/health/ready', { 'x-api-key': 'test-api-key' });
166
+ const res = mockResponse();
167
+
168
+ const routeHandler = router.stack.find(layer =>
169
+ layer.route && layer.route.path === '/health/ready'
170
+ ).route.stack[0].handle;
171
+
172
+ await routeHandler(req, res);
173
+
174
+ expect(res.status).toHaveBeenCalledWith(200);
175
+ expect(res.json).toHaveBeenCalledWith({
176
+ ready: true,
177
+ timestamp: expect.any(String),
178
+ checks: {
179
+ database: true,
180
+ modules: true
181
+ }
182
+ });
183
+ });
184
+
185
+ it('should return 503 when database is not connected', async () => {
186
+ mongoose.connection.readyState = 0;
187
+
188
+ const req = mockRequest('/health/ready', { 'x-api-key': 'test-api-key' });
189
+ const res = mockResponse();
190
+
191
+ const routeHandler = router.stack.find(layer =>
192
+ layer.route && layer.route.path === '/health/ready'
193
+ ).route.stack[0].handle;
194
+
195
+ await routeHandler(req, res);
196
+
197
+ expect(res.status).toHaveBeenCalledWith(503);
198
+ expect(res.json).toHaveBeenCalledWith(expect.objectContaining({
199
+ ready: false
200
+ }));
201
+ });
202
+ });
203
+ });
@@ -1,18 +1,15 @@
1
1
  const { createAppHandler } = require('./../app-handler-helpers');
2
2
  const {
3
- loadAppDefinition,
4
- } = require('../app-definition-loader');
3
+ integrationFactory,
4
+ loadRouterFromObject,
5
+ } = require('./../backend-utils');
5
6
  const { Router } = require('express');
6
- const { loadRouterFromObject } = require('../backend-utils');
7
7
 
8
8
  const handlers = {};
9
- const { integrations: integrationClasses } = loadAppDefinition();
10
-
11
- //todo: this should be in a use case class
12
- for (const IntegrationClass of integrationClasses) {
9
+ for (const IntegrationClass of integrationFactory.integrationClasses) {
13
10
  const router = Router();
14
11
  const basePath = `/api/${IntegrationClass.Definition.name}-integration`;
15
-
12
+
16
13
  console.log(`\n│ Configuring routes for ${IntegrationClass.Definition.name} Integration:`);
17
14
 
18
15
  for (const routeDef of IntegrationClass.Definition.routes) {
@@ -0,0 +1,15 @@
1
+ const catchAsyncError = require('express-async-handler');
2
+ const { User } = require('../../backend-utils');
3
+
4
+ module.exports = catchAsyncError(async (req, res, next) => {
5
+ const authorizationHeader = req.headers.authorization;
6
+
7
+ if (authorizationHeader) {
8
+ // Removes "Bearer " and trims
9
+ const token = authorizationHeader.split(' ')[1].trim();
10
+ // Load user for later middleware/routes to use
11
+ req.user = await User.newUser({ token });
12
+ }
13
+
14
+ return next();
15
+ });
@@ -0,0 +1,12 @@
1
+ const Boom = require('@hapi/boom');
2
+
3
+ // CheckLoggedIn Middleware
4
+ const requireLoggedInUser = (req, res, next) => {
5
+ if (!req.user || !req.user.isLoggedIn()) {
6
+ throw Boom.unauthorized('Invalid Token');
7
+ }
8
+
9
+ next();
10
+ };
11
+
12
+ module.exports = { requireLoggedInUser };