@friggframework/core 2.0.0--canary.405.b87f8d8.0 → 2.0.0--canary.405.1f6792c.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.
@@ -17,7 +17,6 @@ const findOneEvents = [
17
17
  const shouldBypassEncryption = (STAGE) => {
18
18
  const defaultBypassStages = ['dev', 'test', 'local'];
19
19
  const bypassStageEnv = process.env.BYPASS_ENCRYPTION_STAGE;
20
- // If the env is set to anything or an empty string, use the env. Otherwise, use the default array
21
20
  const useEnv = !String(bypassStageEnv) || !!bypassStageEnv;
22
21
  const bypassStages = useEnv
23
22
  ? bypassStageEnv.split(',').map((stage) => stage.trim())
@@ -25,19 +24,15 @@ const shouldBypassEncryption = (STAGE) => {
25
24
  return bypassStages.includes(STAGE);
26
25
  };
27
26
 
28
- // The Mongoose plug-in function
29
- function Encrypt(schema, options) {
27
+ function Encrypt(schema) {
30
28
  const { STAGE, KMS_KEY_ARN, AES_KEY_ID } = process.env;
31
29
 
32
30
  if (shouldBypassEncryption(STAGE)) {
33
31
  return;
34
32
  }
35
33
 
36
- if (KMS_KEY_ARN && AES_KEY_ID) {
37
- throw new Error(
38
- 'Local and AWS encryption keys are both set in the environment.'
39
- );
40
- }
34
+ const hasAES = AES_KEY_ID && AES_KEY_ID.trim() !== '';
35
+ const hasKMS = KMS_KEY_ARN && KMS_KEY_ARN.trim() !== '' && !hasAES;
41
36
 
42
37
  const fields = Object.values(schema.paths)
43
38
  .map(({ path, options }) => (options.lhEncrypt === true ? path : ''))
@@ -48,25 +43,17 @@ function Encrypt(schema, options) {
48
43
  }
49
44
 
50
45
  const cryptor = new Cryptor({
51
- // Use AWS if the CMK is present
52
- shouldUseAws: !!KMS_KEY_ARN,
53
- // Find all the fields in the schema with lhEncrypt === true
46
+ shouldUseAws: hasKMS,
54
47
  fields: fields,
55
48
  });
56
49
 
57
- // ---------------------------------------------
58
- // ### Encrypt fields before save/update/insert.
59
- // ---------------------------------------------
60
-
61
50
  schema.pre('save', async function encryptionPreSave() {
62
- // `this` will be a doc
63
51
  await cryptor.encryptFieldsInDocuments([this]);
64
52
  });
65
53
 
66
54
  schema.pre(
67
55
  'insertMany',
68
56
  async function encryptionPreInsertMany(_, docs, options) {
69
- // `this` will be the model
70
57
  if (options?.rawResult) {
71
58
  throw new Error(
72
59
  'Raw result not supported for insertMany with Encrypt plugin'
@@ -78,17 +65,14 @@ function Encrypt(schema, options) {
78
65
  );
79
66
 
80
67
  schema.pre(updateOneEvents, async function encryptionPreUpdateOne() {
81
- // `this` will be a query
82
68
  await cryptor.encryptFieldsInQuery(this);
83
69
  });
84
70
 
85
71
  schema.pre('updateMany', async function encryptionPreUpdateMany() {
86
- // `this` will be a query
87
72
  cryptor.expectNotToUpdateManyEncrypted(this.getUpdate());
88
73
  });
89
74
 
90
75
  schema.pre('update', async function encryptionPreUpdate() {
91
- // `this` will be a query
92
76
  const { multiple } = this.getOptions();
93
77
 
94
78
  if (multiple) {
@@ -99,16 +83,11 @@ function Encrypt(schema, options) {
99
83
  await cryptor.encryptFieldsInQuery(this);
100
84
  });
101
85
 
102
- // --------------------------------------------
103
- // ### Decrypt documents after they are loaded.
104
- // --------------------------------------------
105
86
  schema.post('save', async function encryptionPreSave() {
106
- // `this` will be a doc
107
87
  await cryptor.decryptFieldsInDocuments([this]);
108
88
  });
109
89
 
110
90
  schema.post(findOneEvents, async function encryptionPostFindOne(doc) {
111
- // `this` will be a query
112
91
  const { rawResult } = this.getOptions();
113
92
 
114
93
  if (rawResult) {
@@ -119,12 +98,10 @@ function Encrypt(schema, options) {
119
98
  });
120
99
 
121
100
  schema.post('find', async function encryptionPostFind(docs) {
122
- // `this` will be a query
123
101
  await cryptor.decryptFieldsInDocuments(docs);
124
102
  });
125
103
 
126
104
  schema.post('insertMany', async function encryptionPostInsertMany(docs) {
127
- // `this` will be the model
128
105
  await cryptor.decryptFieldsInDocuments(docs);
129
106
  });
130
107
  }
@@ -71,147 +71,354 @@ const checkExternalAPI = (url, timeout = 5000) => {
71
71
  });
72
72
  };
73
73
 
74
- router.get('/health', async (_req, res) => {
75
- const status = {
76
- status: 'ok',
77
- timestamp: new Date().toISOString(),
78
- service: 'frigg-core-api'
74
+ const getDatabaseState = () => {
75
+ const stateMap = {
76
+ 0: 'disconnected',
77
+ 1: 'connected',
78
+ 2: 'connecting',
79
+ 3: 'disconnecting'
79
80
  };
81
+ const readyState = mongoose.connection.readyState;
82
+
83
+ return {
84
+ readyState,
85
+ stateName: stateMap[readyState],
86
+ isConnected: readyState === 1
87
+ };
88
+ };
80
89
 
81
- res.status(200).json(status);
82
- });
90
+ const checkDatabaseHealth = async () => {
91
+ const { stateName, isConnected } = getDatabaseState();
92
+ const result = {
93
+ status: isConnected ? 'healthy' : 'unhealthy',
94
+ state: stateName
95
+ };
83
96
 
84
- router.get('/health/detailed', async (_req, res) => {
85
- const startTime = Date.now();
86
- const checks = {
87
- service: 'frigg-core-api',
88
- status: 'healthy',
89
- timestamp: new Date().toISOString(),
90
- checks: {}
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
+ kmsKeyArn: KMS_KEY_ARN || '(not set)',
127
+ aesKeyId: AES_KEY_ID || '(not set)'
91
128
  };
129
+ };
92
130
 
93
- try {
94
- const dbState = mongoose.connection.readyState;
95
- const dbStateMap = {
96
- 0: 'disconnected',
97
- 1: 'connected',
98
- 2: 'connecting',
99
- 3: 'disconnecting'
100
- };
101
-
102
- checks.checks.database = {
103
- status: dbState === 1 ? 'healthy' : 'unhealthy',
104
- state: dbStateMap[dbState]
105
- };
131
+ const createTestEncryptionModel = () => {
132
+ const { Encrypt } = require('./../../encrypt');
133
+
134
+ const testSchema = new mongoose.Schema({
135
+ testSecret: { type: String, lhEncrypt: true },
136
+ normalField: { type: String },
137
+ nestedSecret: {
138
+ value: { type: String, lhEncrypt: true }
139
+ }
140
+ }, { timestamps: false });
141
+
142
+ testSchema.plugin(Encrypt);
143
+
144
+ return mongoose.models.TestEncryption ||
145
+ mongoose.model('TestEncryption', testSchema);
146
+ };
106
147
 
107
- if (dbState === 1) {
108
- const pingStart = Date.now();
109
- await mongoose.connection.db.admin().ping({ maxTimeMS: 2000 });
110
- checks.checks.database.responseTime = Date.now() - pingStart;
111
- } else {
112
- checks.status = 'unhealthy';
148
+ const createTestDocument = async (TestModel) => {
149
+ const testData = {
150
+ testSecret: 'This is a secret value that should be encrypted',
151
+ normalField: 'This is a normal field that should not be encrypted',
152
+ nestedSecret: {
153
+ value: 'This is a nested secret that should be encrypted'
113
154
  }
114
- } catch (error) {
115
- checks.checks.database = {
155
+ };
156
+
157
+ const testDoc = new TestModel(testData);
158
+ await testDoc.save();
159
+
160
+ return { testDoc, testData };
161
+ };
162
+
163
+ const verifyDecryption = (retrievedDoc, originalData) => {
164
+ return retrievedDoc &&
165
+ retrievedDoc.testSecret === originalData.testSecret &&
166
+ retrievedDoc.normalField === originalData.normalField &&
167
+ retrievedDoc.nestedSecret?.value === originalData.nestedSecret.value;
168
+ };
169
+
170
+ const verifyEncryptionInDatabase = async (testDoc, originalData, TestModel) => {
171
+ const collectionName = TestModel.collection.name;
172
+ const rawDoc = await mongoose.connection.db
173
+ .collection(collectionName)
174
+ .findOne({ _id: testDoc._id });
175
+
176
+ const secretIsEncrypted = rawDoc &&
177
+ typeof rawDoc.testSecret === 'string' &&
178
+ rawDoc.testSecret.includes(':') &&
179
+ rawDoc.testSecret !== originalData.testSecret;
180
+
181
+ const nestedIsEncrypted = rawDoc?.nestedSecret?.value &&
182
+ typeof rawDoc.nestedSecret.value === 'string' &&
183
+ rawDoc.nestedSecret.value.includes(':') &&
184
+ rawDoc.nestedSecret.value !== originalData.nestedSecret.value;
185
+
186
+ const normalNotEncrypted = rawDoc &&
187
+ rawDoc.normalField === originalData.normalField;
188
+
189
+ return {
190
+ secretIsEncrypted,
191
+ nestedIsEncrypted,
192
+ normalNotEncrypted
193
+ };
194
+ };
195
+
196
+ const evaluateEncryptionTestResults = (decryptionWorks, encryptionResults) => {
197
+ const { secretIsEncrypted, nestedIsEncrypted, normalNotEncrypted } = encryptionResults;
198
+
199
+ if (decryptionWorks && secretIsEncrypted && nestedIsEncrypted && normalNotEncrypted) {
200
+ return {
201
+ status: 'enabled',
202
+ testResult: 'Encryption and decryption verified successfully'
203
+ };
204
+ }
205
+
206
+ if (decryptionWorks && (!secretIsEncrypted || !nestedIsEncrypted)) {
207
+ return {
116
208
  status: 'unhealthy',
117
- error: error.message
209
+ testResult: 'Fields are not being encrypted in database'
210
+ };
211
+ }
212
+
213
+ if (decryptionWorks && !normalNotEncrypted) {
214
+ return {
215
+ status: 'unhealthy',
216
+ testResult: 'Normal fields are being incorrectly encrypted'
118
217
  };
119
- checks.status = 'unhealthy';
120
218
  }
219
+
220
+ return {
221
+ status: 'unhealthy',
222
+ testResult: 'Decryption failed or data mismatch'
223
+ };
224
+ };
121
225
 
226
+ const testEncryption = async () => {
227
+ const TestModel = createTestEncryptionModel();
228
+ const { testDoc, testData } = await createTestDocument(TestModel);
229
+
122
230
  try {
123
- const { STAGE, BYPASS_ENCRYPTION_STAGE, KMS_KEY_ARN, AES_KEY_ID } =
124
- process.env;
125
- const defaultBypassStages = ['dev', 'test', 'local'];
126
- const useEnv = BYPASS_ENCRYPTION_STAGE !== undefined;
127
- const bypassStages = useEnv
128
- ? BYPASS_ENCRYPTION_STAGE.split(',').map((s) => s.trim())
129
- : defaultBypassStages;
130
- const bypassed = bypassStages.includes(STAGE);
131
- const mode = KMS_KEY_ARN ? 'kms' : AES_KEY_ID ? 'aes' : 'none';
132
-
133
- let status = 'disabled';
134
- // Having both KMS_KEY_ARN and AES_KEY_ID present is considered unhealthy,
135
- // as only one encryption method should be configured at a time to avoid ambiguity
136
- // and potential security misconfiguration.
137
- if (KMS_KEY_ARN && AES_KEY_ID) {
138
- status = 'unhealthy';
139
- } else if (!bypassed && mode !== 'none') {
140
- status = 'enabled';
141
- } else {
142
- status = 'disabled';
143
- }
231
+ const retrievedDoc = await TestModel.findById(testDoc._id);
232
+ const decryptionWorks = verifyDecryption(retrievedDoc, testData);
233
+ const encryptionResults = await verifyEncryptionInDatabase(testDoc, testData, TestModel);
234
+
235
+ const evaluation = evaluateEncryptionTestResults(decryptionWorks, encryptionResults);
236
+
237
+ return {
238
+ ...evaluation,
239
+ encryptionWorks: decryptionWorks
240
+ };
241
+ } finally {
242
+ await TestModel.deleteOne({ _id: testDoc._id });
243
+ }
244
+ };
144
245
 
145
- checks.checks.encryption = {
146
- status,
147
- mode,
148
- bypassed,
149
- stage: STAGE || null,
246
+ const checkEncryptionHealth = async () => {
247
+ const config = getEncryptionConfiguration();
248
+
249
+ if (config.isBypassed || config.mode === 'none') {
250
+ const testResult = config.isBypassed
251
+ ? 'Encryption bypassed for this stage'
252
+ : 'No encryption keys configured';
253
+
254
+ return {
255
+ status: 'disabled',
256
+ mode: config.mode,
257
+ bypassed: config.isBypassed,
258
+ stage: config.stage,
259
+ testResult,
260
+ encryptionWorks: false,
261
+ debug: {
262
+ KMS_KEY_ARN: config.kmsKeyArn,
263
+ AES_KEY_ID: config.aesKeyId,
264
+ hasKMS: config.hasKMS,
265
+ hasAES: config.hasAES
266
+ }
267
+ };
268
+ }
269
+
270
+ try {
271
+ const testResults = await testEncryption();
272
+
273
+ return {
274
+ ...testResults,
275
+ mode: config.mode,
276
+ bypassed: config.isBypassed,
277
+ stage: config.stage,
278
+ debug: {
279
+ KMS_KEY_ARN: config.kmsKeyArn,
280
+ AES_KEY_ID: config.aesKeyId,
281
+ hasKMS: config.hasKMS,
282
+ hasAES: config.hasAES
283
+ }
150
284
  };
151
- if (status === 'unhealthy') checks.status = 'unhealthy';
152
285
  } catch (error) {
153
- checks.checks.encryption = {
286
+ return {
154
287
  status: 'unhealthy',
155
- error: error.message,
288
+ mode: config.mode,
289
+ bypassed: config.isBypassed,
290
+ stage: config.stage,
291
+ testResult: `Encryption test failed: ${error.message}`,
292
+ encryptionWorks: false,
293
+ debug: {
294
+ KMS_KEY_ARN: config.kmsKeyArn,
295
+ AES_KEY_ID: config.aesKeyId,
296
+ hasKMS: config.hasKMS,
297
+ hasAES: config.hasAES
298
+ }
156
299
  };
157
- checks.status = 'unhealthy';
158
300
  }
301
+ };
159
302
 
160
- const externalAPIs = [
303
+ const checkExternalAPIs = async () => {
304
+ const apis = [
161
305
  { name: 'github', url: 'https://api.github.com/status' },
162
306
  { name: 'npm', url: 'https://registry.npmjs.org' }
163
307
  ];
164
308
 
165
- checks.checks.externalApis = {};
166
-
167
- const apiChecks = await Promise.all(
168
- externalAPIs.map(api =>
309
+ const results = await Promise.all(
310
+ apis.map(api =>
169
311
  checkExternalAPI(api.url).then(result => ({ name: api.name, ...result }))
170
312
  )
171
313
  );
172
314
 
173
- apiChecks.forEach(result => {
174
- const { name, ...checkResult } = result;
175
- checks.checks.externalApis[name] = checkResult;
315
+ const apiStatuses = {};
316
+ let allReachable = true;
317
+
318
+ results.forEach(({ name, ...checkResult }) => {
319
+ apiStatuses[name] = checkResult;
176
320
  if (!checkResult.reachable) {
177
- checks.status = 'unhealthy';
321
+ allReachable = false;
178
322
  }
179
323
  });
324
+
325
+ return { apiStatuses, allReachable };
326
+ };
180
327
 
181
- try {
182
- const moduleTypes = Array.isArray(moduleFactory.moduleTypes)
183
- ? moduleFactory.moduleTypes
184
- : [];
185
- const integrationTypes = Array.isArray(
186
- integrationFactory.integrationTypes
187
- )
188
- ? integrationFactory.integrationTypes
189
- : [];
328
+ const checkIntegrations = () => {
329
+ const moduleTypes = Array.isArray(moduleFactory.moduleTypes)
330
+ ? moduleFactory.moduleTypes
331
+ : [];
332
+
333
+ const integrationTypes = Array.isArray(integrationFactory.integrationTypes)
334
+ ? integrationFactory.integrationTypes
335
+ : [];
336
+
337
+ return {
338
+ status: 'healthy',
339
+ modules: {
340
+ count: moduleTypes.length,
341
+ available: moduleTypes,
342
+ },
343
+ integrations: {
344
+ count: integrationTypes.length,
345
+ available: integrationTypes,
346
+ },
347
+ };
348
+ };
349
+
350
+ const buildHealthCheckResponse = (startTime) => {
351
+ return {
352
+ service: 'frigg-core-api',
353
+ status: 'healthy',
354
+ timestamp: new Date().toISOString(),
355
+ checks: {},
356
+ calculateResponseTime: () => Date.now() - startTime
357
+ };
358
+ };
359
+
360
+ router.get('/health', async (_req, res) => {
361
+ const status = {
362
+ status: 'ok',
363
+ timestamp: new Date().toISOString(),
364
+ service: 'frigg-core-api'
365
+ };
190
366
 
191
- checks.checks.integrations = {
192
- status: 'healthy',
193
- modules: {
194
- count: moduleTypes.length,
195
- available: moduleTypes,
196
- },
197
- integrations: {
198
- count: integrationTypes.length,
199
- available: integrationTypes,
200
- },
367
+ res.status(200).json(status);
368
+ });
369
+
370
+ router.get('/health/detailed', async (_req, res) => {
371
+ const startTime = Date.now();
372
+ const response = buildHealthCheckResponse(startTime);
373
+
374
+ try {
375
+ response.checks.database = await checkDatabaseHealth();
376
+ const dbState = getDatabaseState();
377
+ if (!dbState.isConnected) {
378
+ response.status = 'unhealthy';
379
+ }
380
+ } catch (error) {
381
+ response.checks.database = {
382
+ status: 'unhealthy',
383
+ error: error.message
201
384
  };
385
+ response.status = 'unhealthy';
386
+ }
387
+
388
+ try {
389
+ response.checks.encryption = await checkEncryptionHealth();
390
+ if (response.checks.encryption.status === 'unhealthy') {
391
+ response.status = 'unhealthy';
392
+ }
202
393
  } catch (error) {
203
- checks.checks.integrations = {
394
+ response.checks.encryption = {
204
395
  status: 'unhealthy',
205
396
  error: error.message
206
397
  };
207
- checks.status = 'unhealthy';
398
+ response.status = 'unhealthy';
399
+ }
400
+
401
+ const { apiStatuses, allReachable } = await checkExternalAPIs();
402
+ response.checks.externalApis = apiStatuses;
403
+ if (!allReachable) {
404
+ response.status = 'unhealthy';
208
405
  }
209
406
 
210
- checks.responseTime = Date.now() - startTime;
407
+ try {
408
+ response.checks.integrations = checkIntegrations();
409
+ } catch (error) {
410
+ response.checks.integrations = {
411
+ status: 'unhealthy',
412
+ error: error.message
413
+ };
414
+ response.status = 'unhealthy';
415
+ }
211
416
 
212
- const statusCode = checks.status === 'healthy' ? 200 : 503;
417
+ response.responseTime = response.calculateResponseTime();
418
+ delete response.calculateResponseTime;
213
419
 
214
- res.status(statusCode).json(checks);
420
+ const statusCode = response.status === 'healthy' ? 200 : 503;
421
+ res.status(statusCode).json(response);
215
422
  });
216
423
 
217
424
  router.get('/health/live', (_req, res) => {
@@ -222,28 +429,29 @@ router.get('/health/live', (_req, res) => {
222
429
  });
223
430
 
224
431
  router.get('/health/ready', async (_req, res) => {
225
- const checks = {
226
- ready: true,
227
- timestamp: new Date().toISOString(),
228
- checks: {}
229
- };
230
-
231
- const dbState = mongoose.connection.readyState;
232
- checks.checks.database = dbState === 1;
432
+ const dbState = getDatabaseState();
433
+ const isDbReady = dbState.isConnected;
233
434
 
435
+ let areModulesReady = false;
234
436
  try {
235
437
  const moduleTypes = Array.isArray(moduleFactory.moduleTypes)
236
438
  ? moduleFactory.moduleTypes
237
439
  : [];
238
- checks.checks.modules = moduleTypes.length > 0;
440
+ areModulesReady = moduleTypes.length > 0;
239
441
  } catch (error) {
240
- checks.checks.modules = false;
442
+ areModulesReady = false;
241
443
  }
242
444
 
243
- checks.ready = checks.checks.database && checks.checks.modules;
445
+ const isReady = isDbReady && areModulesReady;
244
446
 
245
- const statusCode = checks.ready ? 200 : 503;
246
- res.status(statusCode).json(checks);
447
+ res.status(isReady ? 200 : 503).json({
448
+ ready: isReady,
449
+ timestamp: new Date().toISOString(),
450
+ checks: {
451
+ database: isDbReady,
452
+ modules: areModulesReady
453
+ }
454
+ });
247
455
  });
248
456
 
249
457
  const handler = createAppHandler('HTTP Event: Health', router);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@friggframework/core",
3
3
  "prettier": "@friggframework/prettier-config",
4
- "version": "2.0.0--canary.405.b87f8d8.0",
4
+ "version": "2.0.0--canary.405.1f6792c.0",
5
5
  "dependencies": {
6
6
  "@hapi/boom": "^10.0.1",
7
7
  "aws-sdk": "^2.1200.0",
@@ -22,9 +22,9 @@
22
22
  "uuid": "^9.0.1"
23
23
  },
24
24
  "devDependencies": {
25
- "@friggframework/eslint-config": "2.0.0--canary.405.b87f8d8.0",
26
- "@friggframework/prettier-config": "2.0.0--canary.405.b87f8d8.0",
27
- "@friggframework/test": "2.0.0--canary.405.b87f8d8.0",
25
+ "@friggframework/eslint-config": "2.0.0--canary.405.1f6792c.0",
26
+ "@friggframework/prettier-config": "2.0.0--canary.405.1f6792c.0",
27
+ "@friggframework/test": "2.0.0--canary.405.1f6792c.0",
28
28
  "@types/lodash": "4.17.15",
29
29
  "@typescript-eslint/eslint-plugin": "^8.0.0",
30
30
  "chai": "^4.3.6",
@@ -53,5 +53,5 @@
53
53
  },
54
54
  "homepage": "https://github.com/friggframework/frigg#readme",
55
55
  "description": "",
56
- "gitHead": "b87f8d874639f6fbb52c8a7efc7841c879a1286f"
56
+ "gitHead": "1f6792ccfcbd08502e14072f07ccbebdf99394cb"
57
57
  }