@friggframework/core 2.0.0-next.57 → 2.0.0-next.58

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.
@@ -29,24 +29,24 @@
29
29
 
30
30
  DocumentDB repositories use `$runCommandRaw()` for MongoDB protocol compatibility, which **bypasses Prisma Client Extensions**, including the encryption extension. This results in a **critical security vulnerability** where:
31
31
 
32
- - ✅ **MongoDB/PostgreSQL**: Automatic encryption via Prisma Extension
33
- - ❌ **DocumentDB**: OAuth credentials stored in **plain text**
32
+ - ✅ **MongoDB/PostgreSQL**: Automatic encryption via Prisma Extension
33
+ - ❌ **DocumentDB**: OAuth credentials stored in **plain text**
34
34
 
35
35
  ### The Solution
36
36
 
37
37
  Create `DocumentDBEncryptionService` - a centralized encryption service specifically designed for DocumentDB repositories that:
38
38
 
39
- - Provides document-level encryption/decryption
40
- - Handles nested field paths (e.g., `data.access_token`)
41
- - Uses the same Cryptor and schema registry as Prisma Extension
42
- - Maintains consistency with existing encryption architecture
39
+ - Provides document-level encryption/decryption
40
+ - Handles nested field paths (e.g., `data.access_token`)
41
+ - Uses the same Cryptor and schema registry as Prisma Extension
42
+ - Maintains consistency with existing encryption architecture
43
43
 
44
44
  ### Impact
45
45
 
46
- - **Security**: OAuth credentials encrypted at rest in DocumentDB
47
- - **Architecture**: DRY principle - single source of encryption logic
48
- - **Consistency**: All DocumentDB repos use same encryption pattern
49
- - **Compliance**: Meets production encryption requirements
46
+ - **Security**: OAuth credentials encrypted at rest in DocumentDB
47
+ - **Architecture**: DRY principle - single source of encryption logic
48
+ - **Consistency**: All DocumentDB repos use same encryption pattern
49
+ - **Compliance**: Meets production encryption requirements
50
50
 
51
51
  ---
52
52
 
@@ -69,12 +69,13 @@ Database (encrypted storage)
69
69
  ```
70
70
 
71
71
  **How it works**:
72
+
72
73
  ```javascript
73
74
  // MongoDB Repository - Automatic encryption
74
75
  await prisma.credential.create({
75
76
  data: {
76
- access_token: "plain_secret" // ← Plain text in
77
- }
77
+ access_token: 'plain_secret', // ← Plain text in
78
+ },
78
79
  });
79
80
  // → Prisma Extension intercepts
80
81
  // → FieldEncryptionService.encryptField() called
@@ -100,16 +101,17 @@ Database (PLAIN TEXT STORAGE) ⚠️ SECURITY VULNERABILITY
100
101
  ```
101
102
 
102
103
  **Why it's broken**:
104
+
103
105
  ```javascript
104
106
  // DocumentDB Repository - NO encryption
105
107
  const oauthData = {
106
- access_token: "ya29.actual_google_token", // Plain text!
107
- refresh_token: "1//0secret_refresh_token" // Plain text!
108
+ access_token: 'ya29.actual_google_token', // Plain text!
109
+ refresh_token: '1//0secret_refresh_token', // Plain text!
108
110
  };
109
111
 
110
112
  await prisma.$runCommandRaw({
111
113
  insert: 'Credential',
112
- documents: [{ data: oauthData }]
114
+ documents: [{ data: oauthData }],
113
115
  });
114
116
  // ❌ Prisma Extension NEVER sees this command
115
117
  // ❌ FieldEncryptionService NEVER invoked
@@ -123,18 +125,19 @@ From Prisma documentation:
123
125
  > "$runCommandRaw is a low-level database access method. Prisma Client extensions do not apply to raw database access."
124
126
 
125
127
  **Why DocumentDB needs raw commands**:
126
- - DocumentDB has MongoDB compatibility limitations
127
- - Certain Prisma features don't work (transactions, some aggregations)
128
- - Raw commands provide direct MongoDB protocol access
128
+
129
+ - DocumentDB has MongoDB compatibility limitations
130
+ - Certain Prisma features don't work (transactions, some aggregations)
131
+ - Raw commands provide direct MongoDB protocol access
129
132
 
130
133
  ### Current Repository Status
131
134
 
132
- | Repository | Encryption Status | Security Risk |
133
- |-----------|------------------|---------------|
134
- | **UserRepositoryDocumentDB** | ✅ Has manual encryption for `hashword` | Low - passwords protected |
135
- | **ModuleRepositoryDocumentDB** | ⚠️ Has manual decryption for reads only | Medium - assumes credentials encrypted |
136
- | **CredentialRepositoryDocumentDB** | ❌ **NO encryption on writes, NO decryption on reads** | 🔴 **CRITICAL - OAuth tokens in plain text** |
137
- | **IntegrationRepositoryDocumentDB** | ✅ No encrypted fields, OK | None |
135
+ | Repository | Encryption Status | Security Risk |
136
+ | ----------------------------------- | ------------------------------------------------------ | -------------------------------------------- |
137
+ | **UserRepositoryDocumentDB** | ✅ Has manual encryption for `hashword` | Low - passwords protected |
138
+ | **ModuleRepositoryDocumentDB** | ⚠️ Has manual decryption for reads only | Medium - assumes credentials encrypted |
139
+ | **CredentialRepositoryDocumentDB** | ❌ **NO encryption on writes, NO decryption on reads** | 🔴 **CRITICAL - OAuth tokens in plain text** |
140
+ | **IntegrationRepositoryDocumentDB** | ✅ No encrypted fields, OK | None |
138
141
 
139
142
  ---
140
143
 
@@ -142,18 +145,18 @@ From Prisma documentation:
142
145
 
143
146
  ### Comparison: FieldEncryptionService vs DocumentDBEncryptionService
144
147
 
145
- | Aspect | FieldEncryptionService | DocumentDBEncryptionService |
146
- |--------|------------------------|----------------------------|
147
- | **Purpose** | Encrypt individual fields for Prisma Extension | Encrypt entire documents for raw queries |
148
- | **Invocation** | Automatic (Prisma intercepts queries) | Manual (repository calls explicitly) |
149
- | **Scope** | Single field at a time | Entire document with multiple fields |
150
- | **Nested Fields** | Handled by Prisma Extension traversal | Must manually traverse field paths |
151
- | **Integration** | Via Prisma Client Extension | Direct import in repositories |
152
- | **Query Types** | `create()`, `update()`, `findFirst()`, etc. | `$runCommandRaw()`, via documentdb-utils |
153
- | **Database Support** | MongoDB, PostgreSQL (via Prisma) | DocumentDB (raw MongoDB protocol) |
154
- | **Schema Registry** | Used by Prisma Extension | Directly queries registry |
155
- | **Error Handling** | Prisma transaction rollback | Must handle in repository |
156
- | **Testing** | Integration tests with Prisma | Unit tests + repository tests |
148
+ | Aspect | FieldEncryptionService | DocumentDBEncryptionService |
149
+ | -------------------- | ---------------------------------------------- | ---------------------------------------- |
150
+ | **Purpose** | Encrypt individual fields for Prisma Extension | Encrypt entire documents for raw queries |
151
+ | **Invocation** | Automatic (Prisma intercepts queries) | Manual (repository calls explicitly) |
152
+ | **Scope** | Single field at a time | Entire document with multiple fields |
153
+ | **Nested Fields** | Handled by Prisma Extension traversal | Must manually traverse field paths |
154
+ | **Integration** | Via Prisma Client Extension | Direct import in repositories |
155
+ | **Query Types** | `create()`, `update()`, `findFirst()`, etc. | `$runCommandRaw()`, via documentdb-utils |
156
+ | **Database Support** | MongoDB, PostgreSQL (via Prisma) | DocumentDB (raw MongoDB protocol) |
157
+ | **Schema Registry** | Used by Prisma Extension | Directly queries registry |
158
+ | **Error Handling** | Prisma transaction rollback | Must handle in repository |
159
+ | **Testing** | Integration tests with Prisma | Unit tests + repository tests |
157
160
 
158
161
  ### Proposed Architecture (DocumentDB - FIXED)
159
162
 
@@ -258,8 +261,9 @@ class DocumentDBEncryptionService {
258
261
  **Purpose**: Initialize the service and configure Cryptor
259
262
 
260
263
  **Behavior**:
261
- - Calls `_initializeCryptor()` immediately
262
- - Sets up `this.cryptor` and `this.enabled` properties
264
+
265
+ - Calls `_initializeCryptor()` immediately
266
+ - Sets up `this.cryptor` and `this.enabled` properties
263
267
 
264
268
  **No parameters**
265
269
 
@@ -270,6 +274,7 @@ class DocumentDBEncryptionService {
270
274
  **Purpose**: Initialize Cryptor with environment-based configuration
271
275
 
272
276
  **Logic**:
277
+
273
278
  ```javascript
274
279
  1. Get STAGE from environment (default: 'development')
275
280
  2. If STAGE in ['dev', 'test', 'local']:
@@ -288,10 +293,11 @@ class DocumentDBEncryptionService {
288
293
  ```
289
294
 
290
295
  **Environment Variables Used**:
291
- - `STAGE` or `NODE_ENV`: Determines bypass behavior
292
- - `KMS_KEY_ARN`: AWS KMS key ARN (enables KMS encryption)
293
- - `AES_KEY_ID`: AES key identifier (enables AES encryption)
294
- - `AES_KEY`: AES encryption key (required if AES_KEY_ID present)
296
+
297
+ - `STAGE` or `NODE_ENV`: Determines bypass behavior
298
+ - `KMS_KEY_ARN`: AWS KMS key ARN (enables KMS encryption)
299
+ - `AES_KEY_ID`: AES key identifier (enables AES encryption)
300
+ - `AES_KEY`: AES encryption key (required if AES_KEY_ID present)
295
301
 
296
302
  **Matches**: Logic from `packages/core/database/prisma.js` lines 76-96
297
303
 
@@ -302,12 +308,14 @@ class DocumentDBEncryptionService {
302
308
  **Purpose**: Encrypt fields in a document before storing to DocumentDB
303
309
 
304
310
  **Parameters**:
305
- - `modelName` (string): Model name from schema registry (e.g., 'User', 'Credential')
306
- - `document` (Object): Document to encrypt
311
+
312
+ - `modelName` (string): Model name from schema registry (e.g., 'User', 'Credential')
313
+ - `document` (Object): Document to encrypt
307
314
 
308
315
  **Returns**: `Promise<Object>` - Document with encrypted fields
309
316
 
310
317
  **Algorithm**:
318
+
311
319
  ```javascript
312
320
  1. If !this.enabled or !this.cryptor:
313
321
  - Return document unchanged (bypass)
@@ -324,17 +332,19 @@ class DocumentDBEncryptionService {
324
332
  ```
325
333
 
326
334
  **Error Handling**:
327
- - Invalid inputs: Return unchanged
328
- - Encryption errors: Propagate to caller (repository must handle)
335
+
336
+ - Invalid inputs: Return unchanged
337
+ - Encryption errors: Propagate to caller (repository must handle)
329
338
 
330
339
  **Example**:
340
+
331
341
  ```javascript
332
342
  const plainDoc = {
333
- userId: "123",
343
+ userId: '123',
334
344
  data: {
335
- access_token: "plain_secret",
336
- refresh_token: "plain_refresh"
337
- }
345
+ access_token: 'plain_secret',
346
+ refresh_token: 'plain_refresh',
347
+ },
338
348
  };
339
349
 
340
350
  const encrypted = await service.encryptFields('Credential', plainDoc);
@@ -349,12 +359,14 @@ const encrypted = await service.encryptFields('Credential', plainDoc);
349
359
  **Purpose**: Decrypt fields in a document after reading from DocumentDB
350
360
 
351
361
  **Parameters**:
352
- - `modelName` (string): Model name from schema registry
353
- - `document` (Object): Document to decrypt
362
+
363
+ - `modelName` (string): Model name from schema registry
364
+ - `document` (Object): Document to decrypt
354
365
 
355
366
  **Returns**: `Promise<Object>` - Document with decrypted fields
356
367
 
357
368
  **Algorithm**:
369
+
358
370
  ```javascript
359
371
  1. If !this.enabled or !this.cryptor:
360
372
  - Return document unchanged (bypass)
@@ -371,17 +383,19 @@ const encrypted = await service.encryptFields('Credential', plainDoc);
371
383
  ```
372
384
 
373
385
  **Error Handling**:
374
- - Decryption failures: Set field to null (don't expose encrypted data)
375
- - Log error with context
386
+
387
+ - Decryption failures: Set field to null (don't expose encrypted data)
388
+ - Log error with context
376
389
 
377
390
  **Example**:
391
+
378
392
  ```javascript
379
393
  const encryptedDoc = {
380
- userId: "123",
394
+ userId: '123',
381
395
  data: {
382
- access_token: "aes-key-1:iv:cipher:enckey",
383
- refresh_token: "aes-key-1:iv:cipher:enckey"
384
- }
396
+ access_token: 'aes-key-1:iv:cipher:enckey',
397
+ refresh_token: 'aes-key-1:iv:cipher:enckey',
398
+ },
385
399
  };
386
400
 
387
401
  const decrypted = await service.decryptFields('Credential', encryptedDoc);
@@ -396,11 +410,13 @@ const decrypted = await service.decryptFields('Credential', encryptedDoc);
396
410
  **Purpose**: Encrypt a specific field path in a document (handles nested fields)
397
411
 
398
412
  **Parameters**:
399
- - `document` (Object): Document to modify (mutated in place)
400
- - `fieldPath` (string): Field path from schema registry (e.g., 'data.access_token')
401
- - `modelName` (string): For error logging context
413
+
414
+ - `document` (Object): Document to modify (mutated in place)
415
+ - `fieldPath` (string): Field path from schema registry (e.g., 'data.access_token')
416
+ - `modelName` (string): For error logging context
402
417
 
403
418
  **Algorithm**:
419
+
404
420
  ```javascript
405
421
  1. Split fieldPath by '.': parts = fieldPath.split('.')
406
422
  2. Navigate to parent object:
@@ -422,9 +438,10 @@ const decrypted = await service.decryptFields('Credential', encryptedDoc);
422
438
  ```
423
439
 
424
440
  **Example Field Paths**:
425
- - `hashword` → Encrypts `document.hashword`
426
- - `data.access_token` → Encrypts `document.data.access_token`
427
- - `data.refresh_token` → Encrypts `document.data.refresh_token`
441
+
442
+ - `hashword` → Encrypts `document.hashword`
443
+ - `data.access_token` → Encrypts `document.data.access_token`
444
+ - `data.refresh_token` → Encrypts `document.data.refresh_token`
428
445
 
429
446
  ---
430
447
 
@@ -433,11 +450,13 @@ const decrypted = await service.decryptFields('Credential', encryptedDoc);
433
450
  **Purpose**: Decrypt a specific field path in a document
434
451
 
435
452
  **Parameters**:
436
- - `document` (Object): Document to modify (mutated in place)
437
- - `fieldPath` (string): Field path from schema registry
438
- - `modelName` (string): For error logging context
453
+
454
+ - `document` (Object): Document to modify (mutated in place)
455
+ - `fieldPath` (string): Field path from schema registry
456
+ - `modelName` (string): For error logging context
439
457
 
440
458
  **Algorithm**:
459
+
441
460
  ```javascript
442
461
  1. Split fieldPath by '.': parts = fieldPath.split('.')
443
462
  2. Navigate to parent object:
@@ -460,9 +479,10 @@ const decrypted = await service.decryptFields('Credential', encryptedDoc);
460
479
  ```
461
480
 
462
481
  **Error Tolerance**:
463
- - If decryption fails, set field to `null` instead of throwing
464
- - Prevents exposing encrypted strings to application
465
- - Logs error for debugging
482
+
483
+ - If decryption fails, set field to `null` instead of throwing
484
+ - Prevents exposing encrypted strings to application
485
+ - Logs error for debugging
466
486
 
467
487
  ---
468
488
 
@@ -471,11 +491,13 @@ const decrypted = await service.decryptFields('Credential', encryptedDoc);
471
491
  **Purpose**: Check if a value is in encrypted format
472
492
 
473
493
  **Parameters**:
474
- - `value` (any): Value to check
494
+
495
+ - `value` (any): Value to check
475
496
 
476
497
  **Returns**: `boolean` - True if value is encrypted
477
498
 
478
499
  **Logic**:
500
+
479
501
  ```javascript
480
502
  1. If typeof value !== 'string': return false
481
503
  2. Split by ':': parts = value.split(':')
@@ -485,11 +507,12 @@ const decrypted = await service.decryptFields('Credential', encryptedDoc);
485
507
  **Encrypted Format**: `"keyId:iv:cipher:encKey"` (envelope encryption)
486
508
 
487
509
  **Examples**:
510
+
488
511
  ```javascript
489
- _isEncryptedValue("plain_text") // false
490
- _isEncryptedValue("aes-key-1:iv123:cipher456:enckey789") // true
491
- _isEncryptedValue(null) // false
492
- _isEncryptedValue({}) // false
512
+ _isEncryptedValue('plain_text'); // false
513
+ _isEncryptedValue('aes-key-1:iv123:cipher456:enckey789'); // true
514
+ _isEncryptedValue(null); // false
515
+ _isEncryptedValue({}); // false
493
516
  ```
494
517
 
495
518
  ---
@@ -498,7 +521,9 @@ _isEncryptedValue({}) // false
498
521
 
499
522
  ```javascript
500
523
  const { Cryptor } = require('../encrypt/Cryptor');
501
- const { getEncryptedFields } = require('./encryption/encryption-schema-registry');
524
+ const {
525
+ getEncryptedFields,
526
+ } = require('./encryption/encryption-schema-registry');
502
527
  ```
503
528
 
504
529
  **Cryptor**: Handles actual encryption/decryption (KMS or AES)
@@ -517,10 +542,10 @@ const ENCRYPTED_FIELDS = {
517
542
  'data.access_token',
518
543
  'data.refresh_token',
519
544
  'data.id_token',
520
- 'data.domain'
545
+ 'data.domain',
521
546
  ],
522
547
  IntegrationMapping: ['mapping'],
523
- Token: ['token']
548
+ Token: ['token'],
524
549
  };
525
550
  ```
526
551
 
@@ -533,6 +558,7 @@ const ENCRYPTED_FIELDS = {
533
558
  ### Phase 1: Create DocumentDBEncryptionService (New File)
534
559
 
535
560
  **Files to Create**:
561
+
536
562
  1. `packages/core/database/documentdb-encryption-service.js`
537
563
  2. `packages/core/database/__tests__/documentdb-encryption-service.test.js`
538
564
 
@@ -540,96 +566,96 @@ const ENCRYPTED_FIELDS = {
540
566
 
541
567
  #### 1.1 Service Class (`documentdb-encryption-service.js`)
542
568
 
543
- - [ ] Create file with standard file header comment
544
- - [ ] Import dependencies: `Cryptor`, `getEncryptedFields`
545
- - [ ] Create `DocumentDBEncryptionService` class
546
- - [ ] Implement `constructor()` - calls `_initializeCryptor()`
547
- - [ ] Implement `_initializeCryptor()` - matches `prisma.js` logic
548
- - [ ] Check STAGE environment variable
549
- - [ ] Implement bypass for dev/test/local
550
- - [ ] Check for KMS_KEY_ARN
551
- - [ ] Check for AES_KEY_ID
552
- - [ ] Create Cryptor with shouldUseAws flag
553
- - [ ] Set this.enabled flag
554
- - [ ] Implement `encryptFields(modelName, document)`
555
- - [ ] Early returns for disabled/invalid input
556
- - [ ] Get encrypted fields from registry
557
- - [ ] Loop through field paths
558
- - [ ] Call `_encryptFieldPath()` for each
559
- - [ ] Implement `decryptFields(modelName, document)`
560
- - [ ] Early returns for disabled/invalid input
561
- - [ ] Get encrypted fields from registry
562
- - [ ] Loop through field paths
563
- - [ ] Call `_decryptFieldPath()` for each
564
- - [ ] Implement `_encryptFieldPath(document, fieldPath, modelName)`
565
- - [ ] Parse field path (split by '.')
566
- - [ ] Navigate to parent object
567
- - [ ] Check if already encrypted
568
- - [ ] Convert to string if needed
569
- - [ ] Call `this.cryptor.encrypt()`
570
- - [ ] Error handling with context
571
- - [ ] Implement `_decryptFieldPath(document, fieldPath, modelName)`
572
- - [ ] Parse field path
573
- - [ ] Navigate to parent object
574
- - [ ] Check if encrypted format
575
- - [ ] Call `this.cryptor.decrypt()`
576
- - [ ] Try to parse as JSON
577
- - [ ] Error handling (set to null on failure)
578
- - [ ] Implement `_isEncryptedValue(value)`
579
- - [ ] Type check (must be string)
580
- - [ ] Split by ':'
581
- - [ ] Check for 4+ parts
582
- - [ ] Add JSDoc comments for all public methods
583
- - [ ] Export: `module.exports = { DocumentDBEncryptionService };`
569
+ - [ ] Create file with standard file header comment
570
+ - [ ] Import dependencies: `Cryptor`, `getEncryptedFields`
571
+ - [ ] Create `DocumentDBEncryptionService` class
572
+ - [ ] Implement `constructor()` - calls `_initializeCryptor()`
573
+ - [ ] Implement `_initializeCryptor()` - matches `prisma.js` logic
574
+ - [ ] Check STAGE environment variable
575
+ - [ ] Implement bypass for dev/test/local
576
+ - [ ] Check for KMS_KEY_ARN
577
+ - [ ] Check for AES_KEY_ID
578
+ - [ ] Create Cryptor with shouldUseAws flag
579
+ - [ ] Set this.enabled flag
580
+ - [ ] Implement `encryptFields(modelName, document)`
581
+ - [ ] Early returns for disabled/invalid input
582
+ - [ ] Get encrypted fields from registry
583
+ - [ ] Loop through field paths
584
+ - [ ] Call `_encryptFieldPath()` for each
585
+ - [ ] Implement `decryptFields(modelName, document)`
586
+ - [ ] Early returns for disabled/invalid input
587
+ - [ ] Get encrypted fields from registry
588
+ - [ ] Loop through field paths
589
+ - [ ] Call `_decryptFieldPath()` for each
590
+ - [ ] Implement `_encryptFieldPath(document, fieldPath, modelName)`
591
+ - [ ] Parse field path (split by '.')
592
+ - [ ] Navigate to parent object
593
+ - [ ] Check if already encrypted
594
+ - [ ] Convert to string if needed
595
+ - [ ] Call `this.cryptor.encrypt()`
596
+ - [ ] Error handling with context
597
+ - [ ] Implement `_decryptFieldPath(document, fieldPath, modelName)`
598
+ - [ ] Parse field path
599
+ - [ ] Navigate to parent object
600
+ - [ ] Check if encrypted format
601
+ - [ ] Call `this.cryptor.decrypt()`
602
+ - [ ] Try to parse as JSON
603
+ - [ ] Error handling (set to null on failure)
604
+ - [ ] Implement `_isEncryptedValue(value)`
605
+ - [ ] Type check (must be string)
606
+ - [ ] Split by ':'
607
+ - [ ] Check for 4+ parts
608
+ - [ ] Add JSDoc comments for all public methods
609
+ - [ ] Export: `module.exports = { DocumentDBEncryptionService };`
584
610
 
585
611
  #### 1.2 Service Tests (`__tests__/documentdb-encryption-service.test.js`)
586
612
 
587
- - [ ] Create test file with describe block
588
- - [ ] Mock dependencies: `Cryptor`, `getEncryptedFields`
589
- - [ ] **Test Group: Initialization**
590
- - [ ] Test bypass in dev stage
591
- - [ ] Test bypass in test stage
592
- - [ ] Test bypass in local stage
593
- - [ ] Test enabled with KMS_KEY_ARN in production
594
- - [ ] Test enabled with AES_KEY_ID in production
595
- - [ ] Test disabled with no keys in production
596
- - [ ] Test KMS takes precedence over AES
597
- - [ ] **Test Group: encryptFields()**
598
- - [ ] Test returns unchanged when disabled (dev stage)
599
- - [ ] Test returns unchanged for null document
600
- - [ ] Test returns unchanged for non-object document
601
- - [ ] Test returns unchanged when no encrypted fields in registry
602
- - [ ] Test encrypts User.hashword
603
- - [ ] Test encrypts Credential.data.access_token
604
- - [ ] Test encrypts Credential.data.refresh_token
605
- - [ ] Test encrypts multiple nested fields
606
- - [ ] Test skips already encrypted values
607
- - [ ] Test skips null values
608
- - [ ] Test skips non-existent paths
609
- - [ ] Test encrypts objects (JSON.stringify)
610
- - [ ] Test error handling (propagates error)
611
- - [ ] **Test Group: decryptFields()**
612
- - [ ] Test returns unchanged when disabled
613
- - [ ] Test returns unchanged for null document
614
- - [ ] Test returns unchanged for non-object document
615
- - [ ] Test returns unchanged when no encrypted fields in registry
616
- - [ ] Test decrypts User.hashword
617
- - [ ] Test decrypts Credential.data.access_token
618
- - [ ] Test decrypts multiple nested fields
619
- - [ ] Test skips plain text values
620
- - [ ] Test skips null values
621
- - [ ] Test skips non-existent paths
622
- - [ ] Test parses JSON objects after decryption
623
- - [ ] Test handles non-JSON strings
624
- - [ ] Test error handling (sets field to null)
625
- - [ ] **Test Group: _isEncryptedValue()**
626
- - [ ] Test returns false for plain text
627
- - [ ] Test returns false for null
628
- - [ ] Test returns false for numbers
629
- - [ ] Test returns false for objects
630
- - [ ] Test returns false for short strings (< 4 parts)
631
- - [ ] Test returns true for encrypted format (4+ parts with colons)
632
- - [ ] **Test Coverage Target**: >90% line coverage
613
+ - [ ] Create test file with describe block
614
+ - [ ] Mock dependencies: `Cryptor`, `getEncryptedFields`
615
+ - [ ] **Test Group: Initialization**
616
+ - [ ] Test bypass in dev stage
617
+ - [ ] Test bypass in test stage
618
+ - [ ] Test bypass in local stage
619
+ - [ ] Test enabled with KMS_KEY_ARN in production
620
+ - [ ] Test enabled with AES_KEY_ID in production
621
+ - [ ] Test disabled with no keys in production
622
+ - [ ] Test KMS takes precedence over AES
623
+ - [ ] **Test Group: encryptFields()**
624
+ - [ ] Test returns unchanged when disabled (dev stage)
625
+ - [ ] Test returns unchanged for null document
626
+ - [ ] Test returns unchanged for non-object document
627
+ - [ ] Test returns unchanged when no encrypted fields in registry
628
+ - [ ] Test encrypts User.hashword
629
+ - [ ] Test encrypts Credential.data.access_token
630
+ - [ ] Test encrypts Credential.data.refresh_token
631
+ - [ ] Test encrypts multiple nested fields
632
+ - [ ] Test skips already encrypted values
633
+ - [ ] Test skips null values
634
+ - [ ] Test skips non-existent paths
635
+ - [ ] Test encrypts objects (JSON.stringify)
636
+ - [ ] Test error handling (propagates error)
637
+ - [ ] **Test Group: decryptFields()**
638
+ - [ ] Test returns unchanged when disabled
639
+ - [ ] Test returns unchanged for null document
640
+ - [ ] Test returns unchanged for non-object document
641
+ - [ ] Test returns unchanged when no encrypted fields in registry
642
+ - [ ] Test decrypts User.hashword
643
+ - [ ] Test decrypts Credential.data.access_token
644
+ - [ ] Test decrypts multiple nested fields
645
+ - [ ] Test skips plain text values
646
+ - [ ] Test skips null values
647
+ - [ ] Test skips non-existent paths
648
+ - [ ] Test parses JSON objects after decryption
649
+ - [ ] Test handles non-JSON strings
650
+ - [ ] Test error handling (sets field to null)
651
+ - [ ] **Test Group: \_isEncryptedValue()**
652
+ - [ ] Test returns false for plain text
653
+ - [ ] Test returns false for null
654
+ - [ ] Test returns false for numbers
655
+ - [ ] Test returns false for objects
656
+ - [ ] Test returns false for short strings (< 4 parts)
657
+ - [ ] Test returns true for encrypted format (4+ parts with colons)
658
+ - [ ] **Test Coverage Target**: >90% line coverage
633
659
 
634
660
  **Estimated Time**: 2-3 hours
635
661
 
@@ -648,22 +674,25 @@ const ENCRYPTED_FIELDS = {
648
674
  #### Critical Issue #1: JSON.parse Corrupts Date Objects
649
675
 
650
676
  **Problem**:
677
+
651
678
  ```javascript
652
679
  // Current implementation (lines 101, 147)
653
680
  const result = JSON.parse(JSON.stringify(document));
654
681
  ```
655
682
 
656
683
  **Why it's critical**:
657
- - `JSON.stringify()` converts Date objects to ISO strings
658
- - `JSON.parse()` does NOT convert them back to Date objects
659
- - OAuth tokens often have `expires_at` as Date objects
660
- - This causes **silent data corruption** in production
684
+
685
+ - `JSON.stringify()` converts Date objects to ISO strings
686
+ - `JSON.parse()` does NOT convert them back to Date objects
687
+ - OAuth tokens often have `expires_at` as Date objects
688
+ - This causes **silent data corruption** in production
661
689
 
662
690
  **Example of corruption**:
691
+
663
692
  ```javascript
664
693
  const credential = {
665
694
  data: { access_token: 'secret' },
666
- expires_at: new Date('2025-12-31') // Date object
695
+ expires_at: new Date('2025-12-31'), // Date object
667
696
  };
668
697
 
669
698
  const encrypted = await service.encryptFields('Credential', credential);
@@ -672,26 +701,30 @@ const encrypted = await service.encryptFields('Credential', credential);
672
701
  ```
673
702
 
674
703
  **Fix**:
704
+
675
705
  ```javascript
676
706
  // Use structuredClone (Node.js 17+)
677
707
  const result = structuredClone(document);
678
708
  ```
679
709
 
680
710
  **Benefits of structuredClone**:
681
- - ✅ Preserves Date objects
682
- - ✅ Preserves RegExp objects
683
- - ✅ Preserves Buffer objects
684
- - Handles circular references
685
- - Native Node.js function (no dependencies)
711
+
712
+ - ✅ Preserves Date objects
713
+ - ✅ Preserves RegExp objects
714
+ - Preserves Buffer objects
715
+ - Handles circular references
716
+ - ✅ Native Node.js function (no dependencies)
686
717
 
687
718
  **Files to Update**:
688
- - `documentdb-encryption-service.js` lines 101, 147
719
+
720
+ - `documentdb-encryption-service.js` lines 101, 147
689
721
 
690
722
  **Checklist**:
691
- - [ ] Replace `JSON.parse(JSON.stringify(document))` in `encryptFields()` (line 101)
692
- - [ ] Replace `JSON.parse(JSON.stringify(document))` in `decryptFields()` (line 147)
693
- - [ ] Add test case: `it('preserves Date objects in documents')`
694
- - [ ] Verify Node.js version supports structuredClone (>=17)
723
+
724
+ - [ ] Replace `JSON.parse(JSON.stringify(document))` in `encryptFields()` (line 101)
725
+ - [ ] Replace `JSON.parse(JSON.stringify(document))` in `decryptFields()` (line 147)
726
+ - [ ] Add test case: `it('preserves Date objects in documents')`
727
+ - [ ] Verify Node.js version supports structuredClone (>=17)
695
728
 
696
729
  **Estimated Time**: 5 minutes
697
730
 
@@ -700,6 +733,7 @@ const result = structuredClone(document);
700
733
  #### Critical Issue #2: Decryption Failures Set to Null
701
734
 
702
735
  **Problem**:
736
+
703
737
  ```javascript
704
738
  // Current implementation (_decryptFieldPath, line 258)
705
739
  catch (error) {
@@ -709,12 +743,14 @@ catch (error) {
709
743
  ```
710
744
 
711
745
  **Why it's critical**:
712
- - **Silent credential loss** - Application continues with null tokens
713
- - **Hard to debug** - Error logged but not propagated
714
- - **Security risk** - Could mask key rotation issues or corrupted data
715
- - **Cascade failures** - Null propagates until crash elsewhere
746
+
747
+ - **Silent credential loss** - Application continues with null tokens
748
+ - **Hard to debug** - Error logged but not propagated
749
+ - **Security risk** - Could mask key rotation issues or corrupted data
750
+ - **Cascade failures** - Null propagates until crash elsewhere
716
751
 
717
752
  **Real-world scenario**:
753
+
718
754
  ```javascript
719
755
  // Encrypted credential in database (key rotated or corrupted)
720
756
  const credential = await findCredential(userId);
@@ -726,11 +762,13 @@ const api = new AsanaAPI({ token: credential.access_token });
726
762
  ```
727
763
 
728
764
  **Why this is wrong**:
729
- - Violates fail-fast principle (errors should be discovered immediately)
730
- - Inconsistent with `encryptFields()` which throws errors
731
- - Repository can't distinguish null data from decryption failure
765
+
766
+ - Violates fail-fast principle (errors should be discovered immediately)
767
+ - Inconsistent with `encryptFields()` which throws errors
768
+ - Repository can't distinguish null data from decryption failure
732
769
 
733
770
  **Fix**:
771
+
734
772
  ```javascript
735
773
  // Throw error immediately (fail fast)
736
774
  catch (error) {
@@ -740,15 +778,17 @@ catch (error) {
740
778
  ```
741
779
 
742
780
  **Files to Update**:
743
- - `documentdb-encryption-service.js` line 258
744
- - `documentdb-encryption-service.test.js` update test "sets field to null on decryption error"
781
+
782
+ - `documentdb-encryption-service.js` line 258
783
+ - `documentdb-encryption-service.test.js` update test "sets field to null on decryption error"
745
784
 
746
785
  **Checklist**:
747
- - [ ] Remove `current[fieldName] = null;` from `_decryptFieldPath()` (line 258)
748
- - [ ] Add `throw new Error(...)` with context
749
- - [ ] Update test: change from `expect(result.hashword).toBeNull()` to `expect(...).rejects.toThrow()`
750
- - [ ] Update test name: "throws error on decryption failure" (not "sets field to null")
751
- - [ ] Verify all 56+ tests still pass
786
+
787
+ - [ ] Remove `current[fieldName] = null;` from `_decryptFieldPath()` (line 258)
788
+ - [ ] Add `throw new Error(...)` with context
789
+ - [ ] Update test: change from `expect(result.hashword).toBeNull()` to `expect(...).rejects.toThrow()`
790
+ - [ ] Update test name: "throws error on decryption failure" (not "sets field to null")
791
+ - [ ] Verify all 56+ tests still pass
752
792
 
753
793
  **Estimated Time**: 10 minutes
754
794
 
@@ -757,6 +797,7 @@ catch (error) {
757
797
  #### Critical Issue #3: No Cryptor Dependency Injection
758
798
 
759
799
  **Problem**:
800
+
760
801
  ```javascript
761
802
  // Current implementation (constructor, lines 24-26)
762
803
  constructor() {
@@ -769,12 +810,14 @@ _initializeCryptor() {
769
810
  ```
770
811
 
771
812
  **Why it's critical**:
772
- - **Repository tests break** - Can't mock encryption in Phase 2-4
773
- - **Requires real keys** - Tests need AWS credentials or AES keys
774
- - **Slower tests** - Real encryption is slower than mocks
775
- - **Can't test error scenarios** - Can't simulate Cryptor failures
813
+
814
+ - **Repository tests break** - Can't mock encryption in Phase 2-4
815
+ - **Requires real keys** - Tests need AWS credentials or AES keys
816
+ - **Slower tests** - Real encryption is slower than mocks
817
+ - **Can't test error scenarios** - Can't simulate Cryptor failures
776
818
 
777
819
  **Impact on Phase 2 (UserRepositoryDocumentDB tests)**:
820
+
778
821
  ```javascript
779
822
  describe('UserRepositoryDocumentDB', () => {
780
823
  it('encrypts hashword before saving', async () => {
@@ -782,7 +825,9 @@ describe('UserRepositoryDocumentDB', () => {
782
825
  const service = new DocumentDBEncryptionService();
783
826
  // Tries to create real Cryptor - tests fail without keys
784
827
 
785
- const repo = new UserRepositoryDocumentDB({ encryptionService: service });
828
+ const repo = new UserRepositoryDocumentDB({
829
+ encryptionService: service,
830
+ });
786
831
  await repo.createUser({ hashword: 'password' });
787
832
  // ❌ Real KMS/AES encryption happens in tests
788
833
  });
@@ -790,6 +835,7 @@ describe('UserRepositoryDocumentDB', () => {
790
835
  ```
791
836
 
792
837
  **Fix**:
838
+
793
839
  ```javascript
794
840
  class DocumentDBEncryptionService {
795
841
  constructor({ cryptor = null } = {}) {
@@ -806,11 +852,12 @@ class DocumentDBEncryptionService {
806
852
  ```
807
853
 
808
854
  **Usage**:
855
+
809
856
  ```javascript
810
857
  // In tests (with mock)
811
858
  const mockCryptor = {
812
859
  encrypt: jest.fn().mockResolvedValue('encrypted'),
813
- decrypt: jest.fn().mockResolvedValue('decrypted')
860
+ decrypt: jest.fn().mockResolvedValue('decrypted'),
814
861
  };
815
862
  const service = new DocumentDBEncryptionService({ cryptor: mockCryptor });
816
863
 
@@ -819,16 +866,18 @@ const service = new DocumentDBEncryptionService();
819
866
  ```
820
867
 
821
868
  **Files to Update**:
822
- - `documentdb-encryption-service.js` constructor
823
- - `documentdb-encryption-service.test.js` add dependency injection test
869
+
870
+ - `documentdb-encryption-service.js` constructor
871
+ - `documentdb-encryption-service.test.js` add dependency injection test
824
872
 
825
873
  **Checklist**:
826
- - [ ] Change constructor signature: `constructor({ cryptor = null } = {})`
827
- - [ ] Add conditional logic: if cryptor provided, use it; else call `_initializeCryptor()`
828
- - [ ] Set `this.enabled = true` when cryptor injected
829
- - [ ] Add test: `it('accepts injected Cryptor for testing')`
830
- - [ ] Verify injection test passes
831
- - [ ] Verify all existing tests still pass
874
+
875
+ - [ ] Change constructor signature: `constructor({ cryptor = null } = {})`
876
+ - [ ] Add conditional logic: if cryptor provided, use it; else call `_initializeCryptor()`
877
+ - [ ] Set `this.enabled = true` when cryptor injected
878
+ - [ ] Add test: `it('accepts injected Cryptor for testing')`
879
+ - [ ] Verify injection test passes
880
+ - [ ] Verify all existing tests still pass
832
881
 
833
882
  **Estimated Time**: 15 minutes
834
883
 
@@ -837,37 +886,41 @@ const service = new DocumentDBEncryptionService();
837
886
  #### Phase 1.5 Summary
838
887
 
839
888
  **Total Changes**:
840
- - 3 files modified
841
- - 5 lines of code changed (service implementation)
842
- - 3 new/updated test cases
843
- - 0 breaking changes (backward compatible)
889
+
890
+ - 3 files modified
891
+ - 5 lines of code changed (service implementation)
892
+ - 3 new/updated test cases
893
+ - 0 breaking changes (backward compatible)
844
894
 
845
895
  **Total Time**: ~30 minutes
846
896
 
847
897
  **Success Criteria**:
848
- - ✅ All 56+ tests pass
849
- - Date objects preserved in documents
850
- - Decryption failures throw errors
851
- - Cryptor can be injected for testing
852
- - 100% code coverage maintained
853
- - Code review assessment improves from 6/10 to 8/10
898
+
899
+ - All 56+ tests pass
900
+ - Date objects preserved in documents
901
+ - Decryption failures throw errors
902
+ - Cryptor can be injected for testing
903
+ - 100% code coverage maintained
904
+ - ✅ Code review assessment improves from 6/10 to 8/10
854
905
 
855
906
  **Validation**:
907
+
856
908
  ```javascript
857
909
  // Test 1: Date preservation
858
910
  const doc = { data: { token: 'secret' }, createdAt: new Date() };
859
911
  const encrypted = await service.encryptFields('Model', doc);
860
- expect(encrypted.createdAt).toBeInstanceOf(Date); // ✅ Must pass
912
+ expect(encrypted.createdAt).toBeInstanceOf(Date); // ✅ Must pass
861
913
 
862
914
  // Test 2: Decryption error throws
863
915
  const corrupted = { data: { token: 'corrupted_encrypted_value' } };
864
- await expect(service.decryptFields('Model', corrupted))
865
- .rejects.toThrow('Decryption failed'); // ✅ Must pass
916
+ await expect(service.decryptFields('Model', corrupted)).rejects.toThrow(
917
+ 'Decryption failed'
918
+ ); // ✅ Must pass
866
919
 
867
920
  // Test 3: Dependency injection
868
921
  const mockCryptor = { encrypt: jest.fn(), decrypt: jest.fn() };
869
922
  const service = new DocumentDBEncryptionService({ cryptor: mockCryptor });
870
- expect(service.cryptor).toBe(mockCryptor); // ✅ Must pass
923
+ expect(service.cryptor).toBe(mockCryptor); // ✅ Must pass
871
924
  ```
872
925
 
873
926
  **Next Step**: After Phase 1.5 completion, proceed to Phase 2 (Refactor UserRepositoryDocumentDB)
@@ -880,61 +933,84 @@ expect(service.cryptor).toBe(mockCryptor); // ✅ Must pass
880
933
 
881
934
  **Changes Checklist**:
882
935
 
883
- - [ ] Import DocumentDBEncryptionService at top of file
884
- - [ ] **Remove existing encryption methods** (lines 24-148):
885
- - [ ] Remove `_initializeCryptor()` method
886
- - [ ] Remove `_encryptField()` method
887
- - [ ] Remove `_decryptField()` method
888
- - [ ] Remove `_isEncryptedValue()` method
889
- - [ ] Remove `_encryptHashword()` method
890
- - [ ] Remove `_decryptHashword()` method
891
- - [ ] **Update constructor**:
892
- - [ ] Add: `this.encryptionService = new DocumentDBEncryptionService();`
893
- - [ ] Remove: `this._initializeCryptor();`
894
- - [ ] **Update `createIndividualUser()` method** (around line 183):
895
- - [ ] After building document, before insertOne():
896
- ```javascript
897
- const encryptedDocument = await this.encryptionService.encryptFields('User', document);
898
- const insertedId = await insertOne(this.prisma, 'User', encryptedDocument);
899
- ```
900
- - [ ] After findOne(), before _mapUser():
901
- ```javascript
902
- const decryptedUser = await this.encryptionService.decryptFields('User', created);
903
- return this._mapUser(decryptedUser);
904
- ```
905
- - [ ] **Update `createOrganizationUser()` method**:
906
- - [ ] No changes needed (no encrypted fields for organization users)
907
- - [ ] **Update `findIndividualUserById()` method** (around line 165):
908
- - [ ] After findOne():
909
- ```javascript
910
- const decryptedUser = await this.encryptionService.decryptFields('User', doc);
911
- return this._mapUser(decryptedUser);
912
- ```
913
- - [ ] **Update `findIndividualUserByUsername()` method**:
914
- - [ ] Same pattern: decrypt after findOne()
915
- - [ ] **Update `findIndividualUserByEmail()` method**:
916
- - [ ] Same pattern: decrypt after findOne()
917
- - [ ] **Update `findIndividualUserByAppUserId()` method**:
918
- - [ ] Same pattern: decrypt after findOne()
919
- - [ ] **Update `findUserById()` method**:
920
- - [ ] Same pattern: decrypt after findOne()
921
- - [ ] **Update `updateIndividualUser()` method** (around line 303):
922
- - [ ] After preparing update payload, encrypt before updateOne():
923
- ```javascript
924
- const encryptedPayload = await this.encryptionService.encryptFields('User', payload);
925
- await updateOne(this.prisma, 'User', { _id: objectId, type: 'INDIVIDUAL' },
926
- { $set: encryptedPayload });
927
- ```
928
- - [ ] After findOne(), decrypt before _mapUser():
929
- ```javascript
930
- const decryptedUser = await this.encryptionService.decryptFields('User', updated);
931
- return this._mapUser(decryptedUser);
932
- ```
933
- - [ ] **Update `updateOrganizationUser()` method**:
934
- - [ ] No changes needed (no encrypted fields)
935
- - [ ] Verify no references to old encryption methods remain
936
- - [ ] Run linter to check for issues
937
- - [ ] Test locally
936
+ - [ ] Import DocumentDBEncryptionService at top of file
937
+ - [ ] **Remove existing encryption methods** (lines 24-148):
938
+ - [ ] Remove `_initializeCryptor()` method
939
+ - [ ] Remove `_encryptField()` method
940
+ - [ ] Remove `_decryptField()` method
941
+ - [ ] Remove `_isEncryptedValue()` method
942
+ - [ ] Remove `_encryptHashword()` method
943
+ - [ ] Remove `_decryptHashword()` method
944
+ - [ ] **Update constructor**:
945
+ - [ ] Add: `this.encryptionService = new DocumentDBEncryptionService();`
946
+ - [ ] Remove: `this._initializeCryptor();`
947
+ - [ ] **Update `createIndividualUser()` method** (around line 183):
948
+ - [ ] After building document, before insertOne():
949
+ ```javascript
950
+ const encryptedDocument = await this.encryptionService.encryptFields(
951
+ 'User',
952
+ document
953
+ );
954
+ const insertedId = await insertOne(
955
+ this.prisma,
956
+ 'User',
957
+ encryptedDocument
958
+ );
959
+ ```
960
+ - [ ] After findOne(), before \_mapUser():
961
+ ```javascript
962
+ const decryptedUser = await this.encryptionService.decryptFields(
963
+ 'User',
964
+ created
965
+ );
966
+ return this._mapUser(decryptedUser);
967
+ ```
968
+ - [ ] **Update `createOrganizationUser()` method**:
969
+ - [ ] No changes needed (no encrypted fields for organization users)
970
+ - [ ] **Update `findIndividualUserById()` method** (around line 165):
971
+ - [ ] After findOne():
972
+ ```javascript
973
+ const decryptedUser = await this.encryptionService.decryptFields(
974
+ 'User',
975
+ doc
976
+ );
977
+ return this._mapUser(decryptedUser);
978
+ ```
979
+ - [ ] **Update `findIndividualUserByUsername()` method**:
980
+ - [ ] Same pattern: decrypt after findOne()
981
+ - [ ] **Update `findIndividualUserByEmail()` method**:
982
+ - [ ] Same pattern: decrypt after findOne()
983
+ - [ ] **Update `findIndividualUserByAppUserId()` method**:
984
+ - [ ] Same pattern: decrypt after findOne()
985
+ - [ ] **Update `findIndividualUserById()` method**:
986
+ - [ ] Same pattern: decrypt after findOne()
987
+ - [ ] **Update `updateIndividualUser()` method** (around line 303):
988
+ - [ ] After preparing update payload, encrypt before updateOne():
989
+ ```javascript
990
+ const encryptedPayload = await this.encryptionService.encryptFields(
991
+ 'User',
992
+ payload
993
+ );
994
+ await updateOne(
995
+ this.prisma,
996
+ 'User',
997
+ { _id: objectId, type: 'INDIVIDUAL' },
998
+ { $set: encryptedPayload }
999
+ );
1000
+ ```
1001
+ - [ ] After findOne(), decrypt before \_mapUser():
1002
+ ```javascript
1003
+ const decryptedUser = await this.encryptionService.decryptFields(
1004
+ 'User',
1005
+ updated
1006
+ );
1007
+ return this._mapUser(decryptedUser);
1008
+ ```
1009
+ - [ ] **Update `updateOrganizationUser()` method**:
1010
+ - [ ] No changes needed (no encrypted fields)
1011
+ - [ ] Verify no references to old encryption methods remain
1012
+ - [ ] Run linter to check for issues
1013
+ - [ ] Test locally
938
1014
 
939
1015
  **Estimated Time**: 1 hour
940
1016
 
@@ -946,43 +1022,49 @@ expect(service.cryptor).toBe(mockCryptor); // ✅ Must pass
946
1022
 
947
1023
  **Changes Checklist**:
948
1024
 
949
- - [ ] Import DocumentDBEncryptionService at top of file
950
- - [ ] **Remove existing encryption methods** (lines 22-117):
951
- - [ ] Remove `_initializeCryptor()` method
952
- - [ ] Remove `_encryptField()` method
953
- - [ ] Remove `_decryptField()` method
954
- - [ ] Remove `_isEncryptedValue()` method
955
- - [ ] Remove `_decryptCredentialData()` method
956
- - [ ] **Update constructor**:
957
- - [ ] Add: `this.encryptionService = new DocumentDBEncryptionService();`
958
- - [ ] Remove: `this._initializeCryptor();`
959
- - [ ] **Update `_fetchCredential()` method** (around line 241):
960
- - [ ] After findOne(), before returning:
961
- ```javascript
962
- const decryptedCredential = await this.encryptionService.decryptFields('Credential', rawCredential);
963
- return {
964
- id: fromObjectId(decryptedCredential._id),
965
- userId: fromObjectId(decryptedCredential.userId),
966
- externalId: decryptedCredential.externalId ?? null,
967
- authIsValid: decryptedCredential.authIsValid ?? null,
968
- createdAt: decryptedCredential.createdAt,
969
- updatedAt: decryptedCredential.updatedAt,
970
- data: decryptedCredential.data
971
- };
972
- ```
973
- - [ ] **Update `_fetchCredentialsBulk()` method** (around line 280):
974
- - [ ] Inside the map function for each credential:
975
- ```javascript
976
- const decryptedCredential = await this.encryptionService.decryptFields('Credential', rawCredential);
977
- return this._convertCredentialIds({
978
- id: fromObjectId(decryptedCredential._id),
979
- // ... rest of mapping
980
- data: decryptedCredential.data
981
- });
982
- ```
983
- - [ ] Verify no references to old encryption methods remain
984
- - [ ] Run linter to check for issues
985
- - [ ] Test locally
1025
+ - [ ] Import DocumentDBEncryptionService at top of file
1026
+ - [ ] **Remove existing encryption methods** (lines 22-117):
1027
+ - [ ] Remove `_initializeCryptor()` method
1028
+ - [ ] Remove `_encryptField()` method
1029
+ - [ ] Remove `_decryptField()` method
1030
+ - [ ] Remove `_isEncryptedValue()` method
1031
+ - [ ] Remove `_decryptCredentialData()` method
1032
+ - [ ] **Update constructor**:
1033
+ - [ ] Add: `this.encryptionService = new DocumentDBEncryptionService();`
1034
+ - [ ] Remove: `this._initializeCryptor();`
1035
+ - [ ] **Update `_fetchCredential()` method** (around line 241):
1036
+ - [ ] After findOne(), before returning:
1037
+ ```javascript
1038
+ const decryptedCredential = await this.encryptionService.decryptFields(
1039
+ 'Credential',
1040
+ rawCredential
1041
+ );
1042
+ return {
1043
+ id: fromObjectId(decryptedCredential._id),
1044
+ userId: fromObjectId(decryptedCredential.userId),
1045
+ externalId: decryptedCredential.externalId ?? null,
1046
+ authIsValid: decryptedCredential.authIsValid ?? null,
1047
+ createdAt: decryptedCredential.createdAt,
1048
+ updatedAt: decryptedCredential.updatedAt,
1049
+ data: decryptedCredential.data,
1050
+ };
1051
+ ```
1052
+ - [ ] **Update `_fetchCredentialsBulk()` method** (around line 280):
1053
+ - [ ] Inside the map function for each credential:
1054
+ ```javascript
1055
+ const decryptedCredential = await this.encryptionService.decryptFields(
1056
+ 'Credential',
1057
+ rawCredential
1058
+ );
1059
+ return this._convertCredentialIds({
1060
+ id: fromObjectId(decryptedCredential._id),
1061
+ // ... rest of mapping
1062
+ data: decryptedCredential.data,
1063
+ });
1064
+ ```
1065
+ - [ ] Verify no references to old encryption methods remain
1066
+ - [ ] Run linter to check for issues
1067
+ - [ ] Test locally
986
1068
 
987
1069
  **Note**: ModuleRepository doesn't create/update credentials, only reads them. It relies on CredentialRepository for writes.
988
1070
 
@@ -998,151 +1080,180 @@ expect(service.cryptor).toBe(mockCryptor); // ✅ Must pass
998
1080
 
999
1081
  **Changes Checklist**:
1000
1082
 
1001
- - [ ] Import DocumentDBEncryptionService at top of file
1002
- - [ ] **Update constructor**:
1003
- - [ ] Add: `this.encryptionService = new DocumentDBEncryptionService();`
1004
- - [ ] **Fix `upsertCredential()` method** (around line 50):
1005
- - [ ] **Current problematic code**:
1006
- ```javascript
1007
- const { user, userId, authIsValid, externalId, ...oauthData } = details || {};
1008
- // oauthData contains PLAIN TEXT: access_token, refresh_token, etc.
1083
+ - [ ] Import DocumentDBEncryptionService at top of file
1084
+ - [ ] **Update constructor**:
1085
+ - [ ] Add: `this.encryptionService = new DocumentDBEncryptionService();`
1086
+ - [ ] **Fix `upsertCredential()` method** (around line 50):
1009
1087
 
1010
- const document = {
1011
- data: oauthData // ❌ STORED AS PLAIN TEXT
1012
- };
1013
- await insertOne(this.prisma, 'Credential', document);
1014
- ```
1015
- - [ ] **Replace with ENCRYPTED version**:
1016
- ```javascript
1017
- const { user, userId, authIsValid, externalId, ...oauthData } = details || {};
1018
-
1019
- // Build plain text document
1020
- const plainDocument = {
1021
- userId: toObjectId(userId || user),
1022
- externalId: externalId ?? null,
1023
- authIsValid: authIsValid ?? true,
1024
- data: oauthData, // Still plain text at this point
1025
- createdAt: now,
1026
- updatedAt: now
1027
- };
1088
+ - [ ] **Current problematic code**:
1089
+
1090
+ ```javascript
1091
+ const { user, userId, authIsValid, externalId, ...oauthData } =
1092
+ details || {};
1093
+ // oauthData contains PLAIN TEXT: access_token, refresh_token, etc.
1028
1094
 
1029
- // ENCRYPT before storing
1030
- const encryptedDocument = await this.encryptionService.encryptFields(
1031
- 'Credential',
1032
- plainDocument
1033
- );
1095
+ const document = {
1096
+ data: oauthData, // STORED AS PLAIN TEXT
1097
+ };
1098
+ await insertOne(this.prisma, 'Credential', document);
1099
+ ```
1034
1100
 
1035
- const insertedId = await insertOne(this.prisma, 'Credential', encryptedDocument);
1101
+ - [ ] **Replace with ENCRYPTED version**:
1036
1102
 
1037
- // Read back and decrypt
1038
- const created = await findOne(this.prisma, 'Credential', { _id: insertedId });
1039
- const decryptedCredential = await this.encryptionService.decryptFields(
1040
- 'Credential',
1041
- created
1042
- );
1103
+ ```javascript
1104
+ const { user, userId, authIsValid, externalId, ...oauthData } =
1105
+ details || {};
1043
1106
 
1044
- return this._mapCredential(decryptedCredential);
1045
- ```
1046
- - [ ] **For UPDATE case** (when credential exists):
1047
- ```javascript
1048
- // Merge existing data with new data
1049
- const existingData = existing.data || {};
1050
- const mergedData = { ...existingData, ...oauthData };
1051
-
1052
- // Build update document
1053
- const updateDocument = {
1054
- data: mergedData,
1055
- authIsValid: authIsValid ?? existing.authIsValid,
1056
- updatedAt: now
1057
- };
1107
+ // Build plain text document
1108
+ const plainDocument = {
1109
+ userId: toObjectId(userId || user),
1110
+ externalId: externalId ?? null,
1111
+ authIsValid: authIsValid ?? true,
1112
+ data: oauthData, // Still plain text at this point
1113
+ createdAt: now,
1114
+ updatedAt: now,
1115
+ };
1116
+
1117
+ // ✅ ENCRYPT before storing
1118
+ const encryptedDocument = await this.encryptionService.encryptFields(
1119
+ 'Credential',
1120
+ plainDocument
1121
+ );
1122
+
1123
+ const insertedId = await insertOne(
1124
+ this.prisma,
1125
+ 'Credential',
1126
+ encryptedDocument
1127
+ );
1128
+
1129
+ // Read back and decrypt
1130
+ const created = await findOne(this.prisma, 'Credential', {
1131
+ _id: insertedId,
1132
+ });
1133
+ const decryptedCredential = await this.encryptionService.decryptFields(
1134
+ 'Credential',
1135
+ created
1136
+ );
1137
+
1138
+ return this._mapCredential(decryptedCredential);
1139
+ ```
1140
+
1141
+ - [ ] **For UPDATE case** (when credential exists):
1142
+
1143
+ ```javascript
1144
+ // Merge existing data with new data
1145
+ const existingData = existing.data || {};
1146
+ const mergedData = { ...existingData, ...oauthData };
1147
+
1148
+ // Build update document
1149
+ const updateDocument = {
1150
+ data: mergedData,
1151
+ authIsValid: authIsValid ?? existing.authIsValid,
1152
+ updatedAt: now,
1153
+ };
1154
+
1155
+ // ✅ ENCRYPT before storing
1156
+ const encryptedUpdate = await this.encryptionService.encryptFields(
1157
+ 'Credential',
1158
+ { data: updateDocument.data } // Only encrypt the data field
1159
+ );
1058
1160
 
1059
- // ✅ ENCRYPT before storing
1060
- const encryptedUpdate = await this.encryptionService.encryptFields(
1061
- 'Credential',
1062
- { data: updateDocument.data } // Only encrypt the data field
1063
- );
1064
-
1065
- await updateOne(
1066
- this.prisma,
1067
- 'Credential',
1068
- { _id: existing._id },
1069
- {
1070
- $set: {
1071
- data: encryptedUpdate.data,
1072
- authIsValid: updateDocument.authIsValid,
1073
- updatedAt: updateDocument.updatedAt
1161
+ await updateOne(
1162
+ this.prisma,
1163
+ 'Credential',
1164
+ { _id: existing._id },
1165
+ {
1166
+ $set: {
1167
+ data: encryptedUpdate.data,
1168
+ authIsValid: updateDocument.authIsValid,
1169
+ updatedAt: updateDocument.updatedAt,
1170
+ },
1074
1171
  }
1172
+ );
1173
+
1174
+ // Read back and decrypt
1175
+ const updated = await findOne(this.prisma, 'Credential', {
1176
+ _id: existing._id,
1177
+ });
1178
+ const decryptedCredential = await this.encryptionService.decryptFields(
1179
+ 'Credential',
1180
+ updated
1181
+ );
1182
+
1183
+ return this._mapCredential(decryptedCredential);
1184
+ ```
1185
+
1186
+ - [ ] **Fix `_mapCredential()` method** (around line 192):
1187
+ - [ ] **Current problematic code**:
1188
+ ```javascript
1189
+ _mapCredential(doc) {
1190
+ const data = doc?.data || {};
1191
+ return {
1192
+ id: fromObjectId(doc._id),
1193
+ userId: fromObjectId(doc.userId),
1194
+ externalId: doc.externalId ?? null,
1195
+ authIsValid: doc.authIsValid ?? null,
1196
+ ...data // ❌ Could be encrypted strings
1197
+ };
1075
1198
  }
1076
- );
1199
+ ```
1200
+ - [ ] **Note**: If we decrypt in `upsertCredential()` before calling `_mapCredential()`, this method doesn't need changes. But for safety:
1201
+ ```javascript
1202
+ _mapCredential(doc) {
1203
+ // Assume doc is already decrypted by caller
1204
+ // (upsertCredential, findCredential should decrypt before calling this)
1205
+ const data = doc?.data || {};
1206
+ return {
1207
+ id: fromObjectId(doc._id),
1208
+ userId: fromObjectId(doc.userId),
1209
+ externalId: doc.externalId ?? null,
1210
+ authIsValid: doc.authIsValid ?? null,
1211
+ ...data // Already decrypted
1212
+ };
1213
+ }
1214
+ ```
1215
+ - [ ] **Fix `findCredential()` method** (if exists):
1077
1216
 
1078
- // Read back and decrypt
1079
- const updated = await findOne(this.prisma, 'Credential', { _id: existing._id });
1080
- const decryptedCredential = await this.encryptionService.decryptFields(
1081
- 'Credential',
1082
- updated
1083
- );
1217
+ - [ ] After findOne(), decrypt:
1084
1218
 
1085
- return this._mapCredential(decryptedCredential);
1086
- ```
1087
- - [ ] **Fix `_mapCredential()` method** (around line 192):
1088
- - [ ] **Current problematic code**:
1089
- ```javascript
1090
- _mapCredential(doc) {
1091
- const data = doc?.data || {};
1092
- return {
1093
- id: fromObjectId(doc._id),
1094
- userId: fromObjectId(doc.userId),
1095
- externalId: doc.externalId ?? null,
1096
- authIsValid: doc.authIsValid ?? null,
1097
- ...data // ❌ Could be encrypted strings
1098
- };
1099
- }
1100
- ```
1101
- - [ ] **Note**: If we decrypt in `upsertCredential()` before calling `_mapCredential()`, this method doesn't need changes. But for safety:
1102
- ```javascript
1103
- _mapCredential(doc) {
1104
- // Assume doc is already decrypted by caller
1105
- // (upsertCredential, findCredential should decrypt before calling this)
1106
- const data = doc?.data || {};
1107
- return {
1108
- id: fromObjectId(doc._id),
1109
- userId: fromObjectId(doc.userId),
1110
- externalId: doc.externalId ?? null,
1111
- authIsValid: doc.authIsValid ?? null,
1112
- ...data // Already decrypted
1113
- };
1114
- }
1115
- ```
1116
- - [ ] **Fix `findCredential()` method** (if exists):
1117
- - [ ] After findOne(), decrypt:
1118
- ```javascript
1119
- const doc = await findOne(this.prisma, 'Credential', filter);
1120
- if (!doc) return null;
1219
+ ```javascript
1220
+ const doc = await findOne(this.prisma, 'Credential', filter);
1221
+ if (!doc) return null;
1121
1222
 
1122
- const decryptedDoc = await this.encryptionService.decryptFields('Credential', doc);
1123
- return this._mapCredential(decryptedDoc);
1124
- ```
1125
- - [ ] **Fix `findManyCredentials()` method** (if exists):
1126
- - [ ] After findMany(), decrypt each:
1127
- ```javascript
1128
- const docs = await findMany(this.prisma, 'Credential', filter);
1223
+ const decryptedDoc = await this.encryptionService.decryptFields(
1224
+ 'Credential',
1225
+ doc
1226
+ );
1227
+ return this._mapCredential(decryptedDoc);
1228
+ ```
1129
1229
 
1130
- const decryptedDocs = await Promise.all(
1131
- docs.map(doc => this.encryptionService.decryptFields('Credential', doc))
1132
- );
1230
+ - [ ] **Fix `findManyCredentials()` method** (if exists):
1133
1231
 
1134
- return decryptedDocs.map(doc => this._mapCredential(doc));
1135
- ```
1136
- - [ ] Add JSDoc comments explaining encryption
1137
- - [ ] Verify all credential read/write operations are covered
1138
- - [ ] Run linter
1139
- - [ ] Test locally with real OAuth flow
1232
+ - [ ] After findMany(), decrypt each:
1233
+
1234
+ ```javascript
1235
+ const docs = await findMany(this.prisma, 'Credential', filter);
1236
+
1237
+ const decryptedDocs = await Promise.all(
1238
+ docs.map((doc) =>
1239
+ this.encryptionService.decryptFields('Credential', doc)
1240
+ )
1241
+ );
1242
+
1243
+ return decryptedDocs.map((doc) => this._mapCredential(doc));
1244
+ ```
1245
+
1246
+ - [ ] Add JSDoc comments explaining encryption
1247
+ - [ ] Verify all credential read/write operations are covered
1248
+ - [ ] Run linter
1249
+ - [ ] Test locally with real OAuth flow
1140
1250
 
1141
1251
  **Security Verification**:
1142
- - [ ] Create test credential with `access_token: "test_secret"`
1143
- - [ ] Query database directly (bypass repository)
1144
- - [ ] Verify stored value is encrypted format: `"keyId:iv:cipher:encKey"`
1145
- - [ ] Verify repository returns decrypted value: `"test_secret"`
1252
+
1253
+ - [ ] Create test credential with `access_token: "test_secret"`
1254
+ - [ ] Query database directly (bypass repository)
1255
+ - [ ] Verify stored value is encrypted format: `"keyId:iv:cipher:encKey"`
1256
+ - [ ] Verify repository returns decrypted value: `"test_secret"`
1146
1257
 
1147
1258
  **Estimated Time**: 1.5 hours
1148
1259
 
@@ -1156,33 +1267,33 @@ expect(service.cryptor).toBe(mockCryptor); // ✅ Must pass
1156
1267
 
1157
1268
  **Test Coverage Checklist**:
1158
1269
 
1159
- - [ ] Create test file with describe block
1160
- - [ ] Mock DocumentDBEncryptionService
1161
- - [ ] **Test Group: Encryption on Write**
1162
- - [ ] Test `createIndividualUser()` encrypts hashword before insert
1163
- - [ ] Test `updateIndividualUser()` encrypts hashword before update
1164
- - [ ] Verify encrypted format in database (use direct query)
1165
- - [ ] Verify plain text never stored
1166
- - [ ] **Test Group: Decryption on Read**
1167
- - [ ] Test `findIndividualUserById()` returns decrypted hashword
1168
- - [ ] Test `findIndividualUserByUsername()` returns decrypted hashword
1169
- - [ ] Test `findIndividualUserByEmail()` returns decrypted hashword
1170
- - [ ] Verify application receives plain text
1171
- - [ ] **Test Group: Stage-Based Bypass**
1172
- - [ ] Test encryption bypassed in dev stage
1173
- - [ ] Test encryption bypassed in test stage
1174
- - [ ] Test encryption bypassed in local stage
1175
- - [ ] Test encryption enabled in production stage
1176
- - [ ] **Test Group: Edge Cases**
1177
- - [ ] Test null hashword handling
1178
- - [ ] Test undefined hashword handling
1179
- - [ ] Test empty string hashword
1180
- - [ ] Test already encrypted hashword (idempotent)
1181
- - [ ] **Test Group: Error Handling**
1182
- - [ ] Test encryption service throws error
1183
- - [ ] Test decryption service throws error
1184
- - [ ] Verify error propagation to use case
1185
- - [ ] Run tests: `npm test user-repository-documentdb-encryption.test.js`
1270
+ - [ ] Create test file with describe block
1271
+ - [ ] Mock DocumentDBEncryptionService
1272
+ - [ ] **Test Group: Encryption on Write**
1273
+ - [ ] Test `createIndividualUser()` encrypts hashword before insert
1274
+ - [ ] Test `updateIndividualUser()` encrypts hashword before update
1275
+ - [ ] Verify encrypted format in database (use direct query)
1276
+ - [ ] Verify plain text never stored
1277
+ - [ ] **Test Group: Decryption on Read**
1278
+ - [ ] Test `findIndividualUserById()` returns decrypted hashword
1279
+ - [ ] Test `findIndividualUserByUsername()` returns decrypted hashword
1280
+ - [ ] Test `findIndividualUserByEmail()` returns decrypted hashword
1281
+ - [ ] Verify application receives plain text
1282
+ - [ ] **Test Group: Stage-Based Bypass**
1283
+ - [ ] Test encryption bypassed in dev stage
1284
+ - [ ] Test encryption bypassed in test stage
1285
+ - [ ] Test encryption bypassed in local stage
1286
+ - [ ] Test encryption enabled in production stage
1287
+ - [ ] **Test Group: Edge Cases**
1288
+ - [ ] Test null hashword handling
1289
+ - [ ] Test undefined hashword handling
1290
+ - [ ] Test empty string hashword
1291
+ - [ ] Test already encrypted hashword (idempotent)
1292
+ - [ ] **Test Group: Error Handling**
1293
+ - [ ] Test encryption service throws error
1294
+ - [ ] Test decryption service throws error
1295
+ - [ ] Verify error propagation to use case
1296
+ - [ ] Run tests: `npm test user-repository-documentdb-encryption.test.js`
1186
1297
 
1187
1298
  **Estimated Time**: 1.5 hours
1188
1299
 
@@ -1194,26 +1305,26 @@ expect(service.cryptor).toBe(mockCryptor); // ✅ Must pass
1194
1305
 
1195
1306
  **Test Coverage Checklist**:
1196
1307
 
1197
- - [ ] Create test file with describe block
1198
- - [ ] Mock DocumentDBEncryptionService
1199
- - [ ] Mock credential data in database (pre-encrypted)
1200
- - [ ] **Test Group: Credential Decryption**
1201
- - [ ] Test `_fetchCredential()` decrypts credential data
1202
- - [ ] Test `_fetchCredentialsBulk()` decrypts multiple credentials
1203
- - [ ] Verify nested field decryption (data.access_token)
1204
- - [ ] Verify multiple field decryption (access_token, refresh_token, id_token)
1205
- - [ ] **Test Group: Integration with Entities**
1206
- - [ ] Test `findEntityById()` returns entity with decrypted credential
1207
- - [ ] Test `findEntitiesByUserId()` returns entities with decrypted credentials
1208
- - [ ] Test `findEntitiesByUserIdAndModuleName()` decrypts credentials
1209
- - [ ] **Test Group: Error Handling**
1210
- - [ ] Test corrupted encrypted data (decryption fails)
1211
- - [ ] Test missing credential (null credential)
1212
- - [ ] Verify graceful degradation
1213
- - [ ] **Test Group: Performance**
1214
- - [ ] Test bulk decryption of 10 credentials
1215
- - [ ] Verify parallel decryption (not sequential)
1216
- - [ ] Run tests: `npm test module-repository-documentdb-encryption.test.js`
1308
+ - [ ] Create test file with describe block
1309
+ - [ ] Mock DocumentDBEncryptionService
1310
+ - [ ] Mock credential data in database (pre-encrypted)
1311
+ - [ ] **Test Group: Credential Decryption**
1312
+ - [ ] Test `_fetchCredential()` decrypts credential data
1313
+ - [ ] Test `_fetchCredentialsBulk()` decrypts multiple credentials
1314
+ - [ ] Verify nested field decryption (data.access_token)
1315
+ - [ ] Verify multiple field decryption (access_token, refresh_token, id_token)
1316
+ - [ ] **Test Group: Integration with Entities**
1317
+ - [ ] Test `findEntityById()` returns entity with decrypted credential
1318
+ - [ ] Test `findEntitiesByUserId()` returns entities with decrypted credentials
1319
+ - [ ] Test `findEntitiesByUserIdAndModuleName()` decrypts credentials
1320
+ - [ ] **Test Group: Error Handling**
1321
+ - [ ] Test corrupted encrypted data (decryption fails)
1322
+ - [ ] Test missing credential (null credential)
1323
+ - [ ] Verify graceful degradation
1324
+ - [ ] **Test Group: Performance**
1325
+ - [ ] Test bulk decryption of 10 credentials
1326
+ - [ ] Verify parallel decryption (not sequential)
1327
+ - [ ] Run tests: `npm test module-repository-documentdb-encryption.test.js`
1217
1328
 
1218
1329
  **Estimated Time**: 1.5 hours
1219
1330
 
@@ -1225,69 +1336,77 @@ expect(service.cryptor).toBe(mockCryptor); // ✅ Must pass
1225
1336
 
1226
1337
  **Test Coverage Checklist**:
1227
1338
 
1228
- - [ ] Create test file with describe block
1229
- - [ ] Mock DocumentDBEncryptionService
1230
- - [ ] Setup DocumentDB test database
1231
- - [ ] **Test Group: Encryption on Upsert (INSERT)**
1232
- - [ ] Test encrypts access_token before insert
1233
- - [ ] Test encrypts refresh_token before insert
1234
- - [ ] Test encrypts id_token before insert
1235
- - [ ] Test encrypts domain before insert
1236
- - [ ] **Verify encrypted format in database**:
1237
- ```javascript
1238
- // Direct database query (bypass repository)
1239
- const rawDoc = await prisma.$runCommandRaw({
1240
- find: 'Credential',
1241
- filter: { userId: toObjectId(userId) }
1242
- });
1243
- const storedToken = rawDoc.cursor.firstBatch[0].data.access_token;
1339
+ - [ ] Create test file with describe block
1340
+ - [ ] Mock DocumentDBEncryptionService
1341
+ - [ ] Setup DocumentDB test database
1342
+ - [ ] **Test Group: Encryption on Upsert (INSERT)**
1244
1343
 
1245
- // Must match encrypted format
1246
- expect(storedToken).toMatch(/^[^:]+:[^:]+:[^:]+:[^:]+$/);
1247
- expect(storedToken).not.toBe('plain_secret');
1248
- ```
1249
- - [ ] **Test Group: Encryption on Upsert (UPDATE)**
1250
- - [ ] Test existing credential update encrypts new tokens
1251
- - [ ] Test merges existing encrypted data with new encrypted data
1252
- - [ ] Test updates preserve other credential fields
1253
- - [ ] **Test Group: Decryption on Read**
1254
- - [ ] Test `upsertCredential()` returns decrypted credential
1255
- - [ ] Test `findCredential()` returns decrypted credential (if exists)
1256
- - [ ] Test `_mapCredential()` receives decrypted data
1257
- - [ ] **Verify plain text returned to application**:
1258
- ```javascript
1259
- const credential = await repository.upsertCredential({
1260
- userId, externalId,
1261
- access_token: 'plain_secret',
1262
- refresh_token: 'plain_refresh'
1263
- });
1344
+ - [ ] Test encrypts access_token before insert
1345
+ - [ ] Test encrypts refresh_token before insert
1346
+ - [ ] Test encrypts id_token before insert
1347
+ - [ ] Test encrypts domain before insert
1348
+ - [ ] **Verify encrypted format in database**:
1264
1349
 
1265
- expect(credential.access_token).toBe('plain_secret');
1266
- expect(credential.refresh_token).toBe('plain_refresh');
1267
- ```
1268
- - [ ] **Test Group: Integration Flow**
1269
- - [ ] Test full flow: insert → read → verify
1270
- - [ ] Test full flow: insert → update → read → verify
1271
- - [ ] Test multiple credentials per user
1272
- - [ ] Test credential retrieval by externalId
1273
- - [ ] **Test Group: Security Validation**
1274
- - [ ] Test KMS encryption in production stage
1275
- - [ ] Test AES encryption when KMS unavailable
1276
- - [ ] Test bypass in dev/test/local stages
1277
- - [ ] Test plain text never exposed in logs
1278
- - [ ] **Test Group: Error Handling**
1279
- - [ ] Test encryption service throws error on insert
1280
- - [ ] Test decryption service throws error on read
1281
- - [ ] Test partial credential data (missing fields)
1282
- - [ ] Test null values for optional fields
1283
- - [ ] **Test Group: Edge Cases**
1284
- - [ ] Test empty oauth data
1285
- - [ ] Test very large token values (>1KB)
1286
- - [ ] Test special characters in tokens
1287
- - [ ] Test unicode in tokens
1288
- - [ ] Run tests: `npm test credential-repository-documentdb-encryption.test.js`
1350
+ ```javascript
1351
+ // Direct database query (bypass repository)
1352
+ const rawDoc = await prisma.$runCommandRaw({
1353
+ find: 'Credential',
1354
+ filter: { userId: toObjectId(userId) },
1355
+ });
1356
+ const storedToken = rawDoc.cursor.firstBatch[0].data.access_token;
1357
+
1358
+ // Must match encrypted format
1359
+ expect(storedToken).toMatch(/^[^:]+:[^:]+:[^:]+:[^:]+$/);
1360
+ expect(storedToken).not.toBe('plain_secret');
1361
+ ```
1362
+
1363
+ - [ ] **Test Group: Encryption on Upsert (UPDATE)**
1364
+ - [ ] Test existing credential update encrypts new tokens
1365
+ - [ ] Test merges existing encrypted data with new encrypted data
1366
+ - [ ] Test updates preserve other credential fields
1367
+ - [ ] **Test Group: Decryption on Read**
1368
+
1369
+ - [ ] Test `upsertCredential()` returns decrypted credential
1370
+ - [ ] Test `findCredential()` returns decrypted credential (if exists)
1371
+ - [ ] Test `_mapCredential()` receives decrypted data
1372
+ - [ ] **Verify plain text returned to application**:
1373
+
1374
+ ```javascript
1375
+ const credential = await repository.upsertCredential({
1376
+ userId,
1377
+ externalId,
1378
+ access_token: 'plain_secret',
1379
+ refresh_token: 'plain_refresh',
1380
+ });
1381
+
1382
+ expect(credential.access_token).toBe('plain_secret');
1383
+ expect(credential.refresh_token).toBe('plain_refresh');
1384
+ ```
1385
+
1386
+ - [ ] **Test Group: Integration Flow**
1387
+ - [ ] Test full flow: insert → read → verify
1388
+ - [ ] Test full flow: insert → update → read → verify
1389
+ - [ ] Test multiple credentials per user
1390
+ - [ ] Test credential retrieval by externalId
1391
+ - [ ] **Test Group: Security Validation**
1392
+ - [ ] Test KMS encryption in production stage
1393
+ - [ ] Test AES encryption when KMS unavailable
1394
+ - [ ] Test bypass in dev/test/local stages
1395
+ - [ ] Test plain text never exposed in logs
1396
+ - [ ] **Test Group: Error Handling**
1397
+ - [ ] Test encryption service throws error on insert
1398
+ - [ ] Test decryption service throws error on read
1399
+ - [ ] Test partial credential data (missing fields)
1400
+ - [ ] Test null values for optional fields
1401
+ - [ ] **Test Group: Edge Cases**
1402
+ - [ ] Test empty oauth data
1403
+ - [ ] Test very large token values (>1KB)
1404
+ - [ ] Test special characters in tokens
1405
+ - [ ] Test unicode in tokens
1406
+ - [ ] Run tests: `npm test credential-repository-documentdb-encryption.test.js`
1289
1407
 
1290
1408
  **Security Test Example**:
1409
+
1291
1410
  ```javascript
1292
1411
  describe('Security - Encryption Verification', () => {
1293
1412
  it('stores access_token in encrypted format in database', async () => {
@@ -1299,25 +1418,28 @@ describe('Security - Encryption Verification', () => {
1299
1418
  await credentialRepo.upsertCredential({
1300
1419
  userId: fromObjectId(userId),
1301
1420
  externalId,
1302
- access_token: plainToken
1421
+ access_token: plainToken,
1303
1422
  });
1304
1423
 
1305
1424
  // Query database directly (bypass repository and encryption)
1306
1425
  const rawResult = await prisma.$runCommandRaw({
1307
1426
  find: 'Credential',
1308
- filter: { userId, externalId }
1427
+ filter: { userId, externalId },
1309
1428
  });
1310
1429
 
1311
1430
  const storedCredential = rawResult.cursor.firstBatch[0];
1312
1431
  const storedToken = storedCredential.data.access_token;
1313
1432
 
1314
1433
  // CRITICAL: Verify encrypted format
1315
- expect(storedToken).not.toBe(plainToken); // Must not be plain text
1316
- expect(storedToken).toMatch(/^[^:]+:[^:]+:[^:]+:[^:]+$/); // Must be "keyId:iv:cipher:encKey"
1434
+ expect(storedToken).not.toBe(plainToken); // Must not be plain text
1435
+ expect(storedToken).toMatch(/^[^:]+:[^:]+:[^:]+:[^:]+$/); // Must be "keyId:iv:cipher:encKey"
1317
1436
 
1318
1437
  // Verify repository returns decrypted value
1319
- const retrieved = await credentialRepo.findCredential({ userId, externalId });
1320
- expect(retrieved.access_token).toBe(plainToken); // Must be decrypted
1438
+ const retrieved = await credentialRepo.findCredential({
1439
+ userId,
1440
+ externalId,
1441
+ });
1442
+ expect(retrieved.access_token).toBe(plainToken); // Must be decrypted
1321
1443
  });
1322
1444
  });
1323
1445
  ```
@@ -1329,29 +1451,32 @@ describe('Security - Encryption Verification', () => {
1329
1451
  ### Phase 6: Apply to Both Locations
1330
1452
 
1331
1453
  **Dual Location Rule**: All changes must be applied to BOTH:
1454
+
1332
1455
  1. **Development**: `/Users/danielklotz/projects/lefthook/frontify--frigg/tmp/frigg/packages/core/`
1333
1456
  2. **Runtime**: `/Users/danielklotz/projects/lefthook/frontify--frigg/backend/node_modules/@friggframework/core/`
1334
1457
 
1335
1458
  **Files to Update in Both Locations**:
1336
1459
 
1337
- - [ ] `database/documentdb-encryption-service.js` (NEW)
1338
- - [ ] `database/__tests__/documentdb-encryption-service.test.js` (NEW)
1339
- - [ ] `user/repositories/user-repository-documentdb.js`
1340
- - [ ] `user/repositories/__tests__/user-repository-documentdb-encryption.test.js` (NEW)
1341
- - [ ] `modules/repositories/module-repository-documentdb.js`
1342
- - [ ] `modules/repositories/__tests__/module-repository-documentdb-encryption.test.js` (NEW)
1343
- - [ ] `credential/repositories/credential-repository-documentdb.js`
1344
- - [ ] `credential/repositories/__tests__/credential-repository-documentdb-encryption.test.js` (NEW)
1460
+ - [ ] `database/documentdb-encryption-service.js` (NEW)
1461
+ - [ ] `database/__tests__/documentdb-encryption-service.test.js` (NEW)
1462
+ - [ ] `user/repositories/user-repository-documentdb.js`
1463
+ - [ ] `user/repositories/__tests__/user-repository-documentdb-encryption.test.js` (NEW)
1464
+ - [ ] `modules/repositories/module-repository-documentdb.js`
1465
+ - [ ] `modules/repositories/__tests__/module-repository-documentdb-encryption.test.js` (NEW)
1466
+ - [ ] `credential/repositories/credential-repository-documentdb.js`
1467
+ - [ ] `credential/repositories/__tests__/credential-repository-documentdb-encryption.test.js` (NEW)
1345
1468
 
1346
1469
  **Verification Steps**:
1347
1470
 
1348
1471
  For each file:
1349
- - [ ] Copy from `/tmp/frigg/` to `/backend/node_modules/@friggframework/`
1350
- - [ ] Verify file checksums match
1351
- - [ ] Run `diff` to confirm identical content
1352
- - [ ] Check file permissions
1472
+
1473
+ - [ ] Copy from `/tmp/frigg/` to `/backend/node_modules/@friggframework/`
1474
+ - [ ] Verify file checksums match
1475
+ - [ ] Run `diff` to confirm identical content
1476
+ - [ ] Check file permissions
1353
1477
 
1354
1478
  **Script to Automate** (optional):
1479
+
1355
1480
  ```bash
1356
1481
  #!/bin/bash
1357
1482
  # sync-documentdb-encryption.sh
@@ -1388,45 +1513,55 @@ echo "🎉 All files synced successfully"
1388
1513
 
1389
1514
  **Test Execution Checklist**:
1390
1515
 
1391
- - [ ] **Run DocumentDB encryption service tests**:
1392
- ```bash
1393
- cd /Users/danielklotz/projects/lefthook/frontify--frigg/tmp/frigg
1394
- npm test packages/core/database/__tests__/documentdb-encryption-service.test.js
1395
- ```
1396
- - [ ] Verify all tests pass
1397
- - [ ] Check coverage >90%
1398
-
1399
- - [ ] **Run User repository encryption tests**:
1400
- ```bash
1401
- npm test packages/core/user/repositories/__tests__/user-repository-documentdb-encryption.test.js
1402
- ```
1403
- - [ ] Verify all tests pass
1404
-
1405
- - [ ] **Run Module repository encryption tests**:
1406
- ```bash
1407
- npm test packages/core/modules/repositories/__tests__/module-repository-documentdb-encryption.test.js
1408
- ```
1409
- - [ ] Verify all tests pass
1410
-
1411
- - [ ] **Run Credential repository encryption tests** (CRITICAL):
1412
- ```bash
1413
- npm test packages/core/credential/repositories/__tests__/credential-repository-documentdb-encryption.test.js
1414
- ```
1415
- - [ ] Verify all tests pass
1416
- - [ ] Verify security test passes (encrypted format verification)
1417
-
1418
- - [ ] **Run all repository tests**:
1419
- ```bash
1420
- npm test -- --testPathPattern=documentdb
1421
- ```
1422
- - [ ] Verify no regressions
1423
-
1424
- - [ ] **Run full test suite**:
1425
- ```bash
1426
- npm test
1427
- ```
1428
- - [ ] Verify all tests pass
1429
- - [ ] Check for no unexpected failures
1516
+ - [ ] **Run DocumentDB encryption service tests**:
1517
+
1518
+ ```bash
1519
+ cd /Users/danielklotz/projects/lefthook/frontify--frigg/tmp/frigg
1520
+ npm test packages/core/database/__tests__/documentdb-encryption-service.test.js
1521
+ ```
1522
+
1523
+ - [ ] Verify all tests pass
1524
+ - [ ] Check coverage >90%
1525
+
1526
+ - [ ] **Run User repository encryption tests**:
1527
+
1528
+ ```bash
1529
+ npm test packages/core/user/repositories/__tests__/user-repository-documentdb-encryption.test.js
1530
+ ```
1531
+
1532
+ - [ ] Verify all tests pass
1533
+
1534
+ - [ ] **Run Module repository encryption tests**:
1535
+
1536
+ ```bash
1537
+ npm test packages/core/modules/repositories/__tests__/module-repository-documentdb-encryption.test.js
1538
+ ```
1539
+
1540
+ - [ ] Verify all tests pass
1541
+
1542
+ - [ ] **Run Credential repository encryption tests** (CRITICAL):
1543
+
1544
+ ```bash
1545
+ npm test packages/core/credential/repositories/__tests__/credential-repository-documentdb-encryption.test.js
1546
+ ```
1547
+
1548
+ - [ ] Verify all tests pass
1549
+ - [ ] Verify security test passes (encrypted format verification)
1550
+
1551
+ - [ ] **Run all repository tests**:
1552
+
1553
+ ```bash
1554
+ npm test -- --testPathPattern=documentdb
1555
+ ```
1556
+
1557
+ - [ ] Verify no regressions
1558
+
1559
+ - [ ] **Run full test suite**:
1560
+ ```bash
1561
+ npm test
1562
+ ```
1563
+ - [ ] Verify all tests pass
1564
+ - [ ] Check for no unexpected failures
1430
1565
 
1431
1566
  ---
1432
1567
 
@@ -1434,115 +1569,124 @@ echo "🎉 All files synced successfully"
1434
1569
 
1435
1570
  **Local Environment Setup**:
1436
1571
 
1437
- - [ ] Start MongoDB (DocumentDB simulation):
1438
- ```bash
1439
- cd /Users/danielklotz/projects/lefthook/frontify--frigg/backend
1440
- npm run docker:start
1441
- ```
1442
-
1443
- - [ ] Verify MongoDB is running:
1444
- ```bash
1445
- docker ps | grep mongo
1446
- ```
1447
-
1448
- - [ ] Set environment variables for encryption:
1449
- ```bash
1450
- export STAGE=production
1451
- export AES_KEY_ID=local-test-key
1452
- export AES_KEY=01234567890123456789012345678901 # 32 chars
1453
- ```
1454
-
1455
- - [ ] Start backend:
1456
- ```bash
1457
- cd /Users/danielklotz/projects/lefthook/frontify--frigg/backend
1458
- npm run frigg:start
1459
- ```
1572
+ - [ ] Start MongoDB (DocumentDB simulation):
1573
+
1574
+ ```bash
1575
+ cd /Users/danielklotz/projects/lefthook/frontify--frigg/backend
1576
+ npm run docker:start
1577
+ ```
1578
+
1579
+ - [ ] Verify MongoDB is running:
1580
+
1581
+ ```bash
1582
+ docker ps | grep mongo
1583
+ ```
1584
+
1585
+ - [ ] Set environment variables for encryption:
1586
+
1587
+ ```bash
1588
+ export STAGE=production
1589
+ export AES_KEY_ID=local-test-key
1590
+ export AES_KEY=01234567890123456789012345678901 # 32 chars
1591
+ ```
1592
+
1593
+ - [ ] Start backend:
1594
+ ```bash
1595
+ cd /Users/danielklotz/projects/lefthook/frontify--frigg/backend
1596
+ npm run frigg:start
1597
+ ```
1460
1598
 
1461
1599
  **Manual Test: Credential Creation**
1462
1600
 
1463
- - [ ] Create user and get token:
1464
- ```bash
1465
- curl -X POST http://localhost:3000/user/create \
1466
- -H "Content-Type: application/json" \
1467
- -d '{"username":"test@test.com","password":"test"}' \
1468
- -o /tmp/token.json
1601
+ - [ ] Create user and get token:
1602
+
1603
+ ```bash
1604
+ curl -X POST http://localhost:3000/user/create \
1605
+ -H "Content-Type: application/json" \
1606
+ -d '{"username":"test@test.com","password":"test"}' \
1607
+ -o /tmp/token.json
1469
1608
 
1470
- TOKEN=$(jq -r '.token' /tmp/token.json)
1471
- echo "Token: $TOKEN"
1472
- ```
1609
+ TOKEN=$(jq -r '.token' /tmp/token.json)
1610
+ echo "Token: $TOKEN"
1611
+ ```
1473
1612
 
1474
- - [ ] Create OAuth credential (if endpoint exists, else use Asana OAuth flow):
1475
- ```bash
1476
- # Trigger OAuth flow through application
1477
- # Then verify credential was created encrypted
1478
- ```
1613
+ - [ ] Create OAuth credential (if endpoint exists, else use Asana OAuth flow):
1614
+ ```bash
1615
+ # Trigger OAuth flow through application
1616
+ # Then verify credential was created encrypted
1617
+ ```
1479
1618
 
1480
1619
  **Manual Test: Database Verification**
1481
1620
 
1482
- - [ ] Connect to MongoDB:
1483
- ```bash
1484
- docker exec -it $(docker ps -q -f name=mongo) mongosh
1485
- ```
1621
+ - [ ] Connect to MongoDB:
1622
+
1623
+ ```bash
1624
+ docker exec -it $(docker ps -q -f name=mongo) mongosh
1625
+ ```
1626
+
1627
+ - [ ] Query credential:
1628
+
1629
+ ```javascript
1630
+ use frigg
1631
+ db.Credential.findOne()
1632
+ ```
1486
1633
 
1487
- - [ ] Query credential:
1488
- ```javascript
1489
- use frigg
1490
- db.Credential.findOne()
1491
- ```
1634
+ - [ ] **CRITICAL VERIFICATION**:
1492
1635
 
1493
- - [ ] **CRITICAL VERIFICATION**:
1494
- ```javascript
1495
- // Check data.access_token format
1496
- const cred = db.Credential.findOne({ externalId: "google-user-123" });
1497
- print("access_token:", cred.data.access_token);
1636
+ ```javascript
1637
+ // Check data.access_token format
1638
+ const cred = db.Credential.findOne({ externalId: 'google-user-123' });
1639
+ print('access_token:', cred.data.access_token);
1498
1640
 
1499
- // Expected format: "keyId:iv:cipher:encKey"
1500
- // Example: "aes-key-1:1234567890abcdef:a1b2c3d4e5f6...:9876543210fedcba"
1641
+ // Expected format: "keyId:iv:cipher:encKey"
1642
+ // Example: "aes-key-1:1234567890abcdef:a1b2c3d4e5f6...:9876543210fedcba"
1501
1643
 
1502
- // MUST NOT be plain text like "ya29.a0AfH6SMCX..."
1503
- ```
1644
+ // MUST NOT be plain text like "ya29.a0AfH6SMCX..."
1645
+ ```
1504
1646
 
1505
- - [ ] Verify encrypted format:
1506
- ```javascript
1507
- // Should have 4+ colon-separated parts
1508
- const parts = cred.data.access_token.split(':');
1509
- print("Parts count:", parts.length); // Should be >= 4
1510
- ```
1647
+ - [ ] Verify encrypted format:
1648
+ ```javascript
1649
+ // Should have 4+ colon-separated parts
1650
+ const parts = cred.data.access_token.split(':');
1651
+ print('Parts count:', parts.length); // Should be >= 4
1652
+ ```
1511
1653
 
1512
1654
  **Manual Test: API Usage**
1513
1655
 
1514
- - [ ] Use credential through API:
1515
- ```bash
1516
- # Make API request that uses the credential
1517
- # Example: Fetch Asana user info
1518
- curl -X GET http://localhost:3000/api/asana/me \
1519
- -H "Authorization: Bearer $TOKEN"
1520
- ```
1656
+ - [ ] Use credential through API:
1521
1657
 
1522
- - [ ] Verify API call succeeds (credential was decrypted correctly)
1658
+ ```bash
1659
+ # Make API request that uses the credential
1660
+ # Example: Fetch Asana user info
1661
+ curl -X GET http://localhost:3000/api/asana/me \
1662
+ -H "Authorization: Bearer $TOKEN"
1663
+ ```
1664
+
1665
+ - [ ] Verify API call succeeds (credential was decrypted correctly)
1523
1666
 
1524
1667
  **Manual Test: Stage Bypass**
1525
1668
 
1526
- - [ ] Stop backend
1669
+ - [ ] Stop backend
1527
1670
 
1528
- - [ ] Change to dev stage:
1529
- ```bash
1530
- export STAGE=dev
1531
- unset AES_KEY_ID
1532
- unset AES_KEY
1533
- ```
1671
+ - [ ] Change to dev stage:
1534
1672
 
1535
- - [ ] Start backend
1673
+ ```bash
1674
+ export STAGE=dev
1675
+ unset AES_KEY_ID
1676
+ unset AES_KEY
1677
+ ```
1536
1678
 
1537
- - [ ] Create credential
1679
+ - [ ] Start backend
1538
1680
 
1539
- - [ ] Verify credential stored as plain text (bypass worked):
1540
- ```javascript
1541
- // In mongosh:
1542
- const devCred = db.Credential.findOne({ userId: ObjectId("...") });
1543
- print("access_token:", devCred.data.access_token);
1544
- // Should be plain text (not encrypted) in dev stage
1545
- ```
1681
+ - [ ] Create credential
1682
+
1683
+ - [ ] Verify credential stored as plain text (bypass worked):
1684
+ ```javascript
1685
+ // In mongosh:
1686
+ const devCred = db.Credential.findOne({ userId: ObjectId('...') });
1687
+ print('access_token:', devCred.data.access_token);
1688
+ // Should be plain text (not encrypted) in dev stage
1689
+ ```
1546
1690
 
1547
1691
  ---
1548
1692
 
@@ -1550,33 +1694,34 @@ echo "🎉 All files synced successfully"
1550
1694
 
1551
1695
  **OAuth Flow Testing**:
1552
1696
 
1553
- - [ ] **Asana OAuth Flow**:
1554
- - [ ] Start OAuth flow via Asana integration
1555
- - [ ] Complete OAuth authorization
1556
- - [ ] Verify credential created in database
1557
- - [ ] Check credential is encrypted in database
1558
- - [ ] Verify Asana API calls work (credential decrypted)
1697
+ - [ ] **Asana OAuth Flow**:
1698
+
1699
+ - [ ] Start OAuth flow via Asana integration
1700
+ - [ ] Complete OAuth authorization
1701
+ - [ ] Verify credential created in database
1702
+ - [ ] Check credential is encrypted in database
1703
+ - [ ] Verify Asana API calls work (credential decrypted)
1559
1704
 
1560
- - [ ] **Frontify OAuth Flow**:
1561
- - [ ] Start OAuth flow via Frontify integration
1562
- - [ ] Complete OAuth authorization
1563
- - [ ] Verify credential created in database
1564
- - [ ] Check credential is encrypted in database
1565
- - [ ] Verify Frontify API calls work
1705
+ - [ ] **Frontify OAuth Flow**:
1706
+ - [ ] Start OAuth flow via Frontify integration
1707
+ - [ ] Complete OAuth authorization
1708
+ - [ ] Verify credential created in database
1709
+ - [ ] Check credential is encrypted in database
1710
+ - [ ] Verify Frontify API calls work
1566
1711
 
1567
1712
  **Credential Refresh Testing**:
1568
1713
 
1569
- - [ ] Trigger token refresh (if implemented)
1570
- - [ ] Verify new tokens are encrypted
1571
- - [ ] Verify old tokens are overwritten (not duplicated)
1572
- - [ ] Verify refresh token itself is encrypted
1714
+ - [ ] Trigger token refresh (if implemented)
1715
+ - [ ] Verify new tokens are encrypted
1716
+ - [ ] Verify old tokens are overwritten (not duplicated)
1717
+ - [ ] Verify refresh token itself is encrypted
1573
1718
 
1574
1719
  **Multi-User Testing**:
1575
1720
 
1576
- - [ ] Create credentials for 3 different users
1577
- - [ ] Verify each credential is independently encrypted
1578
- - [ ] Verify users can only access their own credentials
1579
- - [ ] Check for no credential leakage between users
1721
+ - [ ] Create credentials for 3 different users
1722
+ - [ ] Verify each credential is independently encrypted
1723
+ - [ ] Verify users can only access their own credentials
1724
+ - [ ] Check for no credential leakage between users
1580
1725
 
1581
1726
  ---
1582
1727
 
@@ -1584,29 +1729,31 @@ echo "🎉 All files synced successfully"
1584
1729
 
1585
1730
  **Encryption Performance**:
1586
1731
 
1587
- - [ ] Measure encryption time for single credential:
1588
- ```javascript
1589
- const start = Date.now();
1590
- const encrypted = await service.encryptFields('Credential', credential);
1591
- const encryptTime = Date.now() - start;
1592
- console.log(`Encryption time: ${encryptTime}ms`);
1593
- // Should be < 50ms for KMS, < 10ms for AES
1594
- ```
1732
+ - [ ] Measure encryption time for single credential:
1733
+
1734
+ ```javascript
1735
+ const start = Date.now();
1736
+ const encrypted = await service.encryptFields('Credential', credential);
1737
+ const encryptTime = Date.now() - start;
1738
+ console.log(`Encryption time: ${encryptTime}ms`);
1739
+ // Should be < 50ms for KMS, < 10ms for AES
1740
+ ```
1595
1741
 
1596
- - [ ] Measure decryption time for single credential
1742
+ - [ ] Measure decryption time for single credential
1597
1743
 
1598
1744
  **Bulk Operations**:
1599
1745
 
1600
- - [ ] Test bulk credential retrieval (10 credentials):
1601
- ```javascript
1602
- const start = Date.now();
1603
- const entities = await moduleRepo.findEntitiesByUserId(userId);
1604
- const bulkTime = Date.now() - start;
1605
- console.log(`Bulk retrieval time: ${bulkTime}ms`);
1606
- // Should be reasonable (< 500ms for 10 credentials)
1607
- ```
1746
+ - [ ] Test bulk credential retrieval (10 credentials):
1608
1747
 
1609
- - [ ] Verify parallel decryption is used (not sequential)
1748
+ ```javascript
1749
+ const start = Date.now();
1750
+ const entities = await moduleRepo.findEntitiesByUserId(userId);
1751
+ const bulkTime = Date.now() - start;
1752
+ console.log(`Bulk retrieval time: ${bulkTime}ms`);
1753
+ // Should be reasonable (< 500ms for 10 credentials)
1754
+ ```
1755
+
1756
+ - [ ] Verify parallel decryption is used (not sequential)
1610
1757
 
1611
1758
  ---
1612
1759
 
@@ -1614,32 +1761,32 @@ echo "🎉 All files synced successfully"
1614
1761
 
1615
1762
  **Encryption Format Verification**:
1616
1763
 
1617
- - [ ] Create credential with known value
1618
- - [ ] Query database directly
1619
- - [ ] Verify format matches: `keyId:iv:cipher:encKey`
1620
- - [ ] Verify at least 4 colon-separated parts
1621
- - [ ] Verify base64-like characters in each part
1764
+ - [ ] Create credential with known value
1765
+ - [ ] Query database directly
1766
+ - [ ] Verify format matches: `keyId:iv:cipher:encKey`
1767
+ - [ ] Verify at least 4 colon-separated parts
1768
+ - [ ] Verify base64-like characters in each part
1622
1769
 
1623
1770
  **Decryption Verification**:
1624
1771
 
1625
- - [ ] Create credential with known value
1626
- - [ ] Retrieve via repository
1627
- - [ ] Verify decrypted value matches original
1628
- - [ ] Verify no corruption or truncation
1772
+ - [ ] Create credential with known value
1773
+ - [ ] Retrieve via repository
1774
+ - [ ] Verify decrypted value matches original
1775
+ - [ ] Verify no corruption or truncation
1629
1776
 
1630
1777
  **Negative Tests**:
1631
1778
 
1632
- - [ ] Manually corrupt encrypted value in database
1633
- - [ ] Attempt to retrieve credential
1634
- - [ ] Verify graceful handling (field set to null, logged error)
1635
- - [ ] Verify application doesn't crash
1779
+ - [ ] Manually corrupt encrypted value in database
1780
+ - [ ] Attempt to retrieve credential
1781
+ - [ ] Verify graceful handling (field set to null, logged error)
1782
+ - [ ] Verify application doesn't crash
1636
1783
 
1637
1784
  **Key Rotation Simulation** (if time permits):
1638
1785
 
1639
- - [ ] Create credential with key1
1640
- - [ ] Rotate to key2 (change AES_KEY_ID)
1641
- - [ ] Verify old credentials still decrypt (backward compatible)
1642
- - [ ] Verify new credentials use key2
1786
+ - [ ] Create credential with key1
1787
+ - [ ] Rotate to key2 (change AES_KEY_ID)
1788
+ - [ ] Verify old credentials still decrypt (backward compatible)
1789
+ - [ ] Verify new credentials use key2
1643
1790
 
1644
1791
  **Estimated Time**: 1.5 hours
1645
1792
 
@@ -1653,66 +1800,70 @@ echo "🎉 All files synced successfully"
1653
1800
 
1654
1801
  **Sections to Add**:
1655
1802
 
1656
- - [ ] **Add "DocumentDB Encryption" section** (after "How It Works"):
1657
- ```markdown
1658
- ## DocumentDB Encryption
1803
+ - [ ] **Add "DocumentDB Encryption" section** (after "How It Works"):
1804
+
1805
+ ```markdown
1806
+ ## DocumentDB Encryption
1807
+
1808
+ ### Why DocumentDB Needs Manual Encryption
1809
+
1810
+ DocumentDB repositories use `$runCommandRaw()` for MongoDB protocol compatibility,
1811
+ which bypasses Prisma Client Extensions. This means the automatic encryption
1812
+ extension does not apply.
1659
1813
 
1660
- ### Why DocumentDB Needs Manual Encryption
1814
+ ### DocumentDBEncryptionService
1661
1815
 
1662
- DocumentDB repositories use `$runCommandRaw()` for MongoDB protocol compatibility,
1663
- which bypasses Prisma Client Extensions. This means the automatic encryption
1664
- extension does not apply.
1816
+ For DocumentDB repositories, use `DocumentDBEncryptionService` to manually
1817
+ encrypt/decrypt documents before/after database operations.
1665
1818
 
1666
- ### DocumentDBEncryptionService
1819
+ #### Usage Example
1667
1820
 
1668
- For DocumentDB repositories, use `DocumentDBEncryptionService` to manually
1669
- encrypt/decrypt documents before/after database operations.
1821
+ \`\`\`javascript
1822
+ const { DocumentDBEncryptionService } = require('../documentdb-encryption-service');
1823
+ const { insertOne, findOne } = require('../documentdb-utils');
1670
1824
 
1671
- #### Usage Example
1825
+ class MyRepositoryDocumentDB {
1826
+ constructor() {
1827
+ this.encryptionService = new DocumentDBEncryptionService();
1828
+ }
1672
1829
 
1673
- \`\`\`javascript
1674
- const { DocumentDBEncryptionService } = require('../documentdb-encryption-service');
1675
- const { insertOne, findOne } = require('../documentdb-utils');
1830
+ async create(data) {
1831
+ // Encrypt before write
1832
+ const encrypted = await this.encryptionService.encryptFields('ModelName', data);
1833
+ const id = await insertOne(this.prisma, 'CollectionName', encrypted);
1676
1834
 
1677
- class MyRepositoryDocumentDB {
1678
- constructor() {
1679
- this.encryptionService = new DocumentDBEncryptionService();
1680
- }
1835
+ // Decrypt after read
1836
+ const doc = await findOne(this.prisma, 'CollectionName', { _id: id });
1837
+ const decrypted = await this.encryptionService.decryptFields('ModelName', doc);
1681
1838
 
1682
- async create(data) {
1683
- // Encrypt before write
1684
- const encrypted = await this.encryptionService.encryptFields('ModelName', data);
1685
- const id = await insertOne(this.prisma, 'CollectionName', encrypted);
1839
+ return decrypted;
1840
+ }
1686
1841
 
1687
- // Decrypt after read
1688
- const doc = await findOne(this.prisma, 'CollectionName', { _id: id });
1689
- const decrypted = await this.encryptionService.decryptFields('ModelName', doc);
1842
+ }
1843
+ \`\`\`
1690
1844
 
1691
- return decrypted;
1692
- }
1693
- }
1694
- \`\`\`
1845
+ #### Configuration
1695
1846
 
1696
- #### Configuration
1847
+ Uses the same environment variables and Cryptor as the Prisma Extension:
1697
1848
 
1698
- Uses the same environment variables and Cryptor as the Prisma Extension:
1699
- - `STAGE`: Bypasses encryption for dev/test/local
1700
- - `KMS_KEY_ARN`: AWS KMS encryption (production)
1701
- - `AES_KEY_ID` + `AES_KEY`: AES encryption (fallback)
1849
+ - `STAGE`: Bypasses encryption for dev/test/local
1850
+ - `KMS_KEY_ARN`: AWS KMS encryption (production)
1851
+ - `AES_KEY_ID` + `AES_KEY`: AES encryption (fallback)
1702
1852
 
1703
- #### Implementation Details
1853
+ #### Implementation Details
1704
1854
 
1705
- See: [documentdb-encryption-service.md](./documentdb-encryption-service.md)
1706
- ```
1855
+ See: [documentdb-encryption-service.md](./documentdb-encryption-service.md)
1856
+ ```
1707
1857
 
1708
- - [ ] **Update "Adding Encrypted Fields" section**:
1709
- ```markdown
1710
- After adding fields to `encryption-schema-registry.js`:
1858
+ - [ ] **Update "Adding Encrypted Fields" section**:
1711
1859
 
1712
- 1. **For MongoDB/PostgreSQL**: No code changes needed (automatic)
1713
- 2. **For DocumentDB**: Encryption is automatic via DocumentDBEncryptionService
1714
- (service reads from same registry)
1715
- ```
1860
+ ```markdown
1861
+ After adding fields to `encryption-schema-registry.js`:
1862
+
1863
+ 1. **For MongoDB/PostgreSQL**: No code changes needed (automatic)
1864
+ 2. **For DocumentDB**: Encryption is automatic via DocumentDBEncryptionService
1865
+ (service reads from same registry)
1866
+ ```
1716
1867
 
1717
1868
  ---
1718
1869
 
@@ -1720,60 +1871,60 @@ echo "🎉 All files synced successfully"
1720
1871
 
1721
1872
  **UserRepositoryDocumentDB**:
1722
1873
 
1723
- - [ ] Add class-level JSDoc:
1724
- ```javascript
1725
- /**
1726
- * User repository for DocumentDB.
1727
- * Uses DocumentDBEncryptionService for field-level encryption.
1728
- *
1729
- * Encrypted fields: User.hashword
1730
- *
1731
- * @see DocumentDBEncryptionService
1732
- * @see encryption-schema-registry.js
1733
- */
1734
- class UserRepositoryDocumentDB extends UserRepositoryInterface {
1735
- ```
1874
+ - [ ] Add class-level JSDoc:
1875
+ ```javascript
1876
+ /**
1877
+ * User repository for DocumentDB.
1878
+ * Uses DocumentDBEncryptionService for field-level encryption.
1879
+ *
1880
+ * Encrypted fields: User.hashword
1881
+ *
1882
+ * @see DocumentDBEncryptionService
1883
+ * @see encryption-schema-registry.js
1884
+ */
1885
+ class UserRepositoryDocumentDB extends UserRepositoryInterface {
1886
+ ```
1736
1887
 
1737
1888
  **ModuleRepositoryDocumentDB**:
1738
1889
 
1739
- - [ ] Add class-level JSDoc:
1740
- ```javascript
1741
- /**
1742
- * Module/Entity repository for DocumentDB.
1743
- * Uses DocumentDBEncryptionService for credential decryption.
1744
- *
1745
- * Encrypted fields: Credential.data.*
1746
- *
1747
- * Note: This repository only reads credentials. CredentialRepository
1748
- * handles credential creation/updates with encryption.
1749
- *
1750
- * @see DocumentDBEncryptionService
1751
- * @see CredentialRepositoryDocumentDB
1752
- */
1753
- class ModuleRepositoryDocumentDB extends ModuleRepositoryInterface {
1754
- ```
1890
+ - [ ] Add class-level JSDoc:
1891
+ ```javascript
1892
+ /**
1893
+ * Module/Entity repository for DocumentDB.
1894
+ * Uses DocumentDBEncryptionService for credential decryption.
1895
+ *
1896
+ * Encrypted fields: Credential.data.*
1897
+ *
1898
+ * Note: This repository only reads credentials. CredentialRepository
1899
+ * handles credential creation/updates with encryption.
1900
+ *
1901
+ * @see DocumentDBEncryptionService
1902
+ * @see CredentialRepositoryDocumentDB
1903
+ */
1904
+ class ModuleRepositoryDocumentDB extends ModuleRepositoryInterface {
1905
+ ```
1755
1906
 
1756
1907
  **CredentialRepositoryDocumentDB**:
1757
1908
 
1758
- - [ ] Add class-level JSDoc:
1759
- ```javascript
1760
- /**
1761
- * Credential repository for DocumentDB.
1762
- * Uses DocumentDBEncryptionService for field-level encryption.
1763
- *
1764
- * Encrypted fields:
1765
- * - Credential.data.access_token
1766
- * - Credential.data.refresh_token
1767
- * - Credential.data.id_token
1768
- * - Credential.data.domain
1769
- *
1770
- * SECURITY CRITICAL: All OAuth credentials must be encrypted at rest.
1771
- *
1772
- * @see DocumentDBEncryptionService
1773
- * @see encryption-schema-registry.js
1774
- */
1775
- class CredentialRepositoryDocumentDB extends CredentialRepositoryInterface {
1776
- ```
1909
+ - [ ] Add class-level JSDoc:
1910
+ ```javascript
1911
+ /**
1912
+ * Credential repository for DocumentDB.
1913
+ * Uses DocumentDBEncryptionService for field-level encryption.
1914
+ *
1915
+ * Encrypted fields:
1916
+ * - Credential.data.access_token
1917
+ * - Credential.data.refresh_token
1918
+ * - Credential.data.id_token
1919
+ * - Credential.data.domain
1920
+ *
1921
+ * SECURITY CRITICAL: All OAuth credentials must be encrypted at rest.
1922
+ *
1923
+ * @see DocumentDBEncryptionService
1924
+ * @see encryption-schema-registry.js
1925
+ */
1926
+ class CredentialRepositoryDocumentDB extends CredentialRepositoryInterface {
1927
+ ```
1777
1928
 
1778
1929
  **Estimated Time**: 30 minutes
1779
1930
 
@@ -1781,17 +1932,17 @@ echo "🎉 All files synced successfully"
1781
1932
 
1782
1933
  ## Total Implementation Time Estimate
1783
1934
 
1784
- | Phase | Description | Time |
1785
- |-------|-------------|------|
1786
- | Phase 1 | Create DocumentDBEncryptionService + tests | 2-3 hours |
1787
- | Phase 2 | Refactor UserRepositoryDocumentDB | 1 hour |
1788
- | Phase 3 | Refactor ModuleRepositoryDocumentDB | 1 hour |
1789
- | Phase 4 | Fix CredentialRepositoryDocumentDB | 1.5 hours |
1790
- | Phase 5 | Add comprehensive tests (3 repos) | 5 hours |
1791
- | Phase 6 | Apply to both locations | 30 minutes |
1792
- | Phase 7 | Validation and integration testing | 1.5 hours |
1793
- | Phase 8 | Documentation updates | 30 minutes |
1794
- | **Total** | | **~13 hours** |
1935
+ | Phase | Description | Time |
1936
+ | --------- | ------------------------------------------ | ------------- |
1937
+ | Phase 1 | Create DocumentDBEncryptionService + tests | 2-3 hours |
1938
+ | Phase 2 | Refactor UserRepositoryDocumentDB | 1 hour |
1939
+ | Phase 3 | Refactor ModuleRepositoryDocumentDB | 1 hour |
1940
+ | Phase 4 | Fix CredentialRepositoryDocumentDB | 1.5 hours |
1941
+ | Phase 5 | Add comprehensive tests (3 repos) | 5 hours |
1942
+ | Phase 6 | Apply to both locations | 30 minutes |
1943
+ | Phase 7 | Validation and integration testing | 1.5 hours |
1944
+ | Phase 8 | Documentation updates | 30 minutes |
1945
+ | **Total** | | **~13 hours** |
1795
1946
 
1796
1947
  ---
1797
1948
 
@@ -1810,21 +1961,24 @@ class CredentialRepositoryDocumentDB {
1810
1961
 
1811
1962
  async upsertCredential(credentialDetails) {
1812
1963
  const { identifiers, details } = credentialDetails;
1813
- const { user, userId, authIsValid, externalId, ...oauthData } = details || {};
1964
+ const { user, userId, authIsValid, externalId, ...oauthData } =
1965
+ details || {};
1814
1966
 
1815
1967
  // ❌ oauthData contains PLAIN TEXT tokens
1816
1968
  const document = {
1817
1969
  userId: toObjectId(userId || user),
1818
1970
  externalId,
1819
- data: oauthData, // ❌ { access_token: "plain_secret", ... }
1971
+ data: oauthData, // ❌ { access_token: "plain_secret", ... }
1820
1972
  createdAt: new Date(),
1821
- updatedAt: new Date()
1973
+ updatedAt: new Date(),
1822
1974
  };
1823
1975
 
1824
1976
  // ❌ STORED AS PLAIN TEXT
1825
1977
  const insertedId = await insertOne(this.prisma, 'Credential', document);
1826
1978
 
1827
- const created = await findOne(this.prisma, 'Credential', { _id: insertedId });
1979
+ const created = await findOne(this.prisma, 'Credential', {
1980
+ _id: insertedId,
1981
+ });
1828
1982
  // ❌ Returns encrypted string (if previously encrypted) or plain text
1829
1983
  return this._mapCredential(created);
1830
1984
  }
@@ -1834,7 +1988,9 @@ class CredentialRepositoryDocumentDB {
1834
1988
  **AFTER (Secure - Encrypted Storage)**:
1835
1989
 
1836
1990
  ```javascript
1837
- const { DocumentDBEncryptionService } = require('../database/documentdb-encryption-service');
1991
+ const {
1992
+ DocumentDBEncryptionService,
1993
+ } = require('../database/documentdb-encryption-service');
1838
1994
 
1839
1995
  class CredentialRepositoryDocumentDB {
1840
1996
  constructor() {
@@ -1845,15 +2001,16 @@ class CredentialRepositoryDocumentDB {
1845
2001
 
1846
2002
  async upsertCredential(credentialDetails) {
1847
2003
  const { identifiers, details } = credentialDetails;
1848
- const { user, userId, authIsValid, externalId, ...oauthData } = details || {};
2004
+ const { user, userId, authIsValid, externalId, ...oauthData } =
2005
+ details || {};
1849
2006
 
1850
2007
  // Build plain text document
1851
2008
  const plainDocument = {
1852
2009
  userId: toObjectId(userId || user),
1853
2010
  externalId,
1854
- data: oauthData, // Still plain text: { access_token: "plain_secret", ... }
2011
+ data: oauthData, // Still plain text: { access_token: "plain_secret", ... }
1855
2012
  createdAt: new Date(),
1856
- updatedAt: new Date()
2013
+ updatedAt: new Date(),
1857
2014
  };
1858
2015
 
1859
2016
  // ✅ ENCRYPT before storing
@@ -1864,9 +2021,15 @@ class CredentialRepositoryDocumentDB {
1864
2021
  // encryptedDocument.data = { access_token: "keyId:iv:cipher:encKey", ... }
1865
2022
 
1866
2023
  // ✅ STORED AS ENCRYPTED
1867
- const insertedId = await insertOne(this.prisma, 'Credential', encryptedDocument);
2024
+ const insertedId = await insertOne(
2025
+ this.prisma,
2026
+ 'Credential',
2027
+ encryptedDocument
2028
+ );
1868
2029
 
1869
- const created = await findOne(this.prisma, 'Credential', { _id: insertedId });
2030
+ const created = await findOne(this.prisma, 'Credential', {
2031
+ _id: insertedId,
2032
+ });
1870
2033
 
1871
2034
  // ✅ DECRYPT before returning
1872
2035
  const decryptedCredential = await this.encryptionService.decryptFields(
@@ -1892,19 +2055,25 @@ class UserRepositoryDocumentDB {
1892
2055
  const document = {
1893
2056
  type: 'INDIVIDUAL',
1894
2057
  username: params.username,
1895
- hashword: await bcrypt.hash(params.hashword, 10), // Bcrypt hash
1896
- createdAt: new Date()
2058
+ hashword: await bcrypt.hash(params.hashword, 10), // Bcrypt hash
2059
+ createdAt: new Date(),
1897
2060
  };
1898
2061
 
1899
2062
  // Encrypt bcrypt hash before storage
1900
- const encrypted = await this.encryptionService.encryptFields('User', document);
2063
+ const encrypted = await this.encryptionService.encryptFields(
2064
+ 'User',
2065
+ document
2066
+ );
1901
2067
  // encrypted.hashword = "keyId:iv:cipher:encKey"
1902
2068
 
1903
2069
  const id = await insertOne(this.prisma, 'User', encrypted);
1904
2070
  const created = await findOne(this.prisma, 'User', { _id: id });
1905
2071
 
1906
2072
  // Decrypt before returning
1907
- const decrypted = await this.encryptionService.decryptFields('User', created);
2073
+ const decrypted = await this.encryptionService.decryptFields(
2074
+ 'User',
2075
+ created
2076
+ );
1908
2077
  // decrypted.hashword = "$2b$10$..." (bcrypt hash)
1909
2078
 
1910
2079
  return this._mapUser(decrypted);
@@ -1912,23 +2081,26 @@ class UserRepositoryDocumentDB {
1912
2081
  }
1913
2082
  ```
1914
2083
 
1915
- **Pattern 2: Nested Fields Encryption (Credential.data.*)**:
2084
+ **Pattern 2: Nested Fields Encryption (Credential.data.\*)**:
1916
2085
 
1917
2086
  ```javascript
1918
2087
  class CredentialRepositoryDocumentDB {
1919
2088
  async upsertCredential(details) {
1920
2089
  const document = {
1921
2090
  data: {
1922
- access_token: "ya29.actual_token",
1923
- refresh_token: "1//0refresh",
1924
- id_token: "eyJhbGci...",
1925
- expires_at: 1234567890, // Not encrypted (not in registry)
1926
- scope: "openid profile" // Not encrypted
1927
- }
2091
+ access_token: 'ya29.actual_token',
2092
+ refresh_token: '1//0refresh',
2093
+ id_token: 'eyJhbGci...',
2094
+ expires_at: 1234567890, // Not encrypted (not in registry)
2095
+ scope: 'openid profile', // Not encrypted
2096
+ },
1928
2097
  };
1929
2098
 
1930
2099
  // Encrypts only fields defined in encryption-schema-registry.js
1931
- const encrypted = await this.encryptionService.encryptFields('Credential', document);
2100
+ const encrypted = await this.encryptionService.encryptFields(
2101
+ 'Credential',
2102
+ document
2103
+ );
1932
2104
  // encrypted.data = {
1933
2105
  // access_token: "keyId:iv:cipher:encKey", ← ENCRYPTED
1934
2106
  // refresh_token: "keyId:iv:cipher:encKey", ← ENCRYPTED
@@ -1945,11 +2117,13 @@ class CredentialRepositoryDocumentDB {
1945
2117
  ```javascript
1946
2118
  class ModuleRepositoryDocumentDB {
1947
2119
  async _fetchCredentialsBulk(credentialIds) {
1948
- const objectIds = credentialIds.map(id => toObjectId(id)).filter(Boolean);
2120
+ const objectIds = credentialIds
2121
+ .map((id) => toObjectId(id))
2122
+ .filter(Boolean);
1949
2123
 
1950
2124
  // Fetch all credentials (encrypted)
1951
2125
  const rawCredentials = await findMany(this.prisma, 'Credential', {
1952
- _id: { $in: objectIds }
2126
+ _id: { $in: objectIds },
1953
2127
  });
1954
2128
 
1955
2129
  // Decrypt in parallel
@@ -1973,34 +2147,37 @@ class ModuleRepositoryDocumentDB {
1973
2147
  ```javascript
1974
2148
  // 1. User completes OAuth flow, application receives tokens
1975
2149
  const oauthTokens = {
1976
- access_token: "ya29.a0AfH6SMCXyz...",
1977
- refresh_token: "1//0gFz6TRvwUm...",
1978
- id_token: "eyJhbGciOiJSUzI1...",
2150
+ access_token: 'ya29.a0AfH6SMCXyz...',
2151
+ refresh_token: '1//0gFz6TRvwUm...',
2152
+ id_token: 'eyJhbGciOiJSUzI1...',
1979
2153
  expires_in: 3600,
1980
- token_type: "Bearer"
2154
+ token_type: 'Bearer',
1981
2155
  };
1982
2156
 
1983
2157
  // 2. Use case calls repository
1984
2158
  const credential = await credentialRepository.upsertCredential({
1985
- identifiers: { userId: "user123", externalId: "google-user-456" },
1986
- details: oauthTokens
2159
+ identifiers: { userId: 'user123', externalId: 'google-user-456' },
2160
+ details: oauthTokens,
1987
2161
  });
1988
2162
 
1989
2163
  // 3. Inside repository: Build plain document
1990
2164
  const plainDocument = {
1991
- userId: toObjectId("user123"),
1992
- externalId: "google-user-456",
2165
+ userId: toObjectId('user123'),
2166
+ externalId: 'google-user-456',
1993
2167
  data: {
1994
- access_token: "ya29.a0AfH6SMCXyz...",
1995
- refresh_token: "1//0gFz6TRvwUm...",
1996
- id_token: "eyJhbGciOiJSUzI1...",
2168
+ access_token: 'ya29.a0AfH6SMCXyz...',
2169
+ refresh_token: '1//0gFz6TRvwUm...',
2170
+ id_token: 'eyJhbGciOiJSUzI1...',
1997
2171
  expires_in: 3600,
1998
- token_type: "Bearer"
1999
- }
2172
+ token_type: 'Bearer',
2173
+ },
2000
2174
  };
2001
2175
 
2002
2176
  // 4. DocumentDBEncryptionService encrypts sensitive fields
2003
- const encryptedDocument = await this.encryptionService.encryptFields('Credential', plainDocument);
2177
+ const encryptedDocument = await this.encryptionService.encryptFields(
2178
+ 'Credential',
2179
+ plainDocument
2180
+ );
2004
2181
  // Result:
2005
2182
  // {
2006
2183
  // userId: ObjectId("..."),
@@ -2018,11 +2195,16 @@ const encryptedDocument = await this.encryptionService.encryptFields('Credential
2018
2195
  await insertOne(this.prisma, 'Credential', encryptedDocument);
2019
2196
 
2020
2197
  // 6. Read back from DocumentDB
2021
- const rawDocument = await findOne(this.prisma, 'Credential', { userId: objectId });
2198
+ const rawDocument = await findOne(this.prisma, 'Credential', {
2199
+ userId: objectId,
2200
+ });
2022
2201
  // Returns encrypted data as stored
2023
2202
 
2024
2203
  // 7. DocumentDBEncryptionService decrypts sensitive fields
2025
- const decryptedDocument = await this.encryptionService.decryptFields('Credential', rawDocument);
2204
+ const decryptedDocument = await this.encryptionService.decryptFields(
2205
+ 'Credential',
2206
+ rawDocument
2207
+ );
2026
2208
  // Result:
2027
2209
  // {
2028
2210
  // data: {
@@ -2035,11 +2217,11 @@ const decryptedDocument = await this.encryptionService.decryptFields('Credential
2035
2217
  // }
2036
2218
 
2037
2219
  // 8. Use case receives plain text credential
2038
- return credential; // { access_token: "ya29...", refresh_token: "1//0...", ... }
2220
+ return credential; // { access_token: "ya29...", refresh_token: "1//0...", ... }
2039
2221
 
2040
2222
  // 9. Application makes API call
2041
2223
  await fetch('https://www.googleapis.com/oauth2/v1/userinfo', {
2042
- headers: { Authorization: `Bearer ${credential.access_token}` }
2224
+ headers: { Authorization: `Bearer ${credential.access_token}` },
2043
2225
  });
2044
2226
  // ✅ Works! Token is usable
2045
2227
  ```
@@ -2051,9 +2233,10 @@ await fetch('https://www.googleapis.com/oauth2/v1/userinfo', {
2051
2233
  ### Unit Tests: DocumentDBEncryptionService
2052
2234
 
2053
2235
  **Coverage Goals**:
2054
- - 100% line coverage
2055
- - All branches covered
2056
- - All error paths tested
2236
+
2237
+ - 100% line coverage
2238
+ - All branches covered
2239
+ - All error paths tested
2057
2240
 
2058
2241
  **Key Test Cases**:
2059
2242
 
@@ -2069,7 +2252,8 @@ describe('DocumentDBEncryptionService', () => {
2069
2252
 
2070
2253
  it('enables KMS encryption in production with KMS_KEY_ARN', () => {
2071
2254
  process.env.STAGE = 'production';
2072
- process.env.KMS_KEY_ARN = 'arn:aws:kms:us-east-1:123456789012:key/abc123';
2255
+ process.env.KMS_KEY_ARN =
2256
+ 'arn:aws:kms:us-east-1:123456789012:key/abc123';
2073
2257
  const service = new DocumentDBEncryptionService();
2074
2258
  expect(service.enabled).toBe(true);
2075
2259
  expect(service.cryptor.shouldUseAws).toBe(true);
@@ -2089,14 +2273,14 @@ describe('DocumentDBEncryptionService', () => {
2089
2273
  it('encrypts User.hashword', async () => {
2090
2274
  const document = {
2091
2275
  username: 'test@example.com',
2092
- hashword: '$2b$10$plain_bcrypt_hash'
2276
+ hashword: '$2b$10$plain_bcrypt_hash',
2093
2277
  };
2094
2278
 
2095
2279
  const encrypted = await service.encryptFields('User', document);
2096
2280
 
2097
- expect(encrypted.username).toBe('test@example.com'); // Not encrypted
2098
- expect(encrypted.hashword).not.toBe('$2b$10$plain_bcrypt_hash'); // Encrypted
2099
- expect(encrypted.hashword).toMatch(/^[^:]+:[^:]+:[^:]+:[^:]+$/); // Format check
2281
+ expect(encrypted.username).toBe('test@example.com'); // Not encrypted
2282
+ expect(encrypted.hashword).not.toBe('$2b$10$plain_bcrypt_hash'); // Encrypted
2283
+ expect(encrypted.hashword).toMatch(/^[^:]+:[^:]+:[^:]+:[^:]+$/); // Format check
2100
2284
  });
2101
2285
 
2102
2286
  it('encrypts Credential.data.access_token', async () => {
@@ -2104,15 +2288,20 @@ describe('DocumentDBEncryptionService', () => {
2104
2288
  userId: '123',
2105
2289
  data: {
2106
2290
  access_token: 'ya29.token_here',
2107
- scope: 'openid profile' // Not in registry
2108
- }
2291
+ scope: 'openid profile', // Not in registry
2292
+ },
2109
2293
  };
2110
2294
 
2111
- const encrypted = await service.encryptFields('Credential', document);
2295
+ const encrypted = await service.encryptFields(
2296
+ 'Credential',
2297
+ document
2298
+ );
2112
2299
 
2113
2300
  expect(encrypted.data.access_token).not.toBe('ya29.token_here');
2114
- expect(encrypted.data.access_token).toMatch(/^[^:]+:[^:]+:[^:]+:[^:]+$/);
2115
- expect(encrypted.data.scope).toBe('openid profile'); // Not encrypted
2301
+ expect(encrypted.data.access_token).toMatch(
2302
+ /^[^:]+:[^:]+:[^:]+:[^:]+$/
2303
+ );
2304
+ expect(encrypted.data.scope).toBe('openid profile'); // Not encrypted
2116
2305
  });
2117
2306
 
2118
2307
  it('skips already encrypted values', async () => {
@@ -2121,12 +2310,15 @@ describe('DocumentDBEncryptionService', () => {
2121
2310
 
2122
2311
  const result = await service.encryptFields('User', document);
2123
2312
 
2124
- expect(result.hashword).toBe(alreadyEncrypted); // Unchanged
2313
+ expect(result.hashword).toBe(alreadyEncrypted); // Unchanged
2125
2314
  });
2126
2315
 
2127
2316
  it('returns unchanged for unknown model', async () => {
2128
2317
  const document = { field: 'value' };
2129
- const result = await service.encryptFields('UnknownModel', document);
2318
+ const result = await service.encryptFields(
2319
+ 'UnknownModel',
2320
+ document
2321
+ );
2130
2322
  expect(result).toEqual(document);
2131
2323
  });
2132
2324
  });
@@ -2135,7 +2327,7 @@ describe('DocumentDBEncryptionService', () => {
2135
2327
  it('decrypts User.hashword', async () => {
2136
2328
  const encryptedDoc = {
2137
2329
  username: 'test@example.com',
2138
- hashword: 'keyId:iv:cipher:enckey' // Mock encrypted
2330
+ hashword: 'keyId:iv:cipher:enckey', // Mock encrypted
2139
2331
  };
2140
2332
 
2141
2333
  // Mock Cryptor to return known value
@@ -2144,16 +2336,20 @@ describe('DocumentDBEncryptionService', () => {
2144
2336
  const decrypted = await service.decryptFields('User', encryptedDoc);
2145
2337
 
2146
2338
  expect(decrypted.hashword).toBe('$2b$10$plain_bcrypt_hash');
2147
- expect(mockCryptor.decrypt).toHaveBeenCalledWith('keyId:iv:cipher:enckey');
2339
+ expect(mockCryptor.decrypt).toHaveBeenCalledWith(
2340
+ 'keyId:iv:cipher:enckey'
2341
+ );
2148
2342
  });
2149
2343
 
2150
2344
  it('handles decryption failures gracefully', async () => {
2151
2345
  const encryptedDoc = { hashword: 'corrupted:data:here:error' };
2152
- mockCryptor.decrypt.mockRejectedValue(new Error('Decryption failed'));
2346
+ mockCryptor.decrypt.mockRejectedValue(
2347
+ new Error('Decryption failed')
2348
+ );
2153
2349
 
2154
2350
  const result = await service.decryptFields('User', encryptedDoc);
2155
2351
 
2156
- expect(result.hashword).toBeNull(); // Set to null on error
2352
+ expect(result.hashword).toBeNull(); // Set to null on error
2157
2353
  });
2158
2354
 
2159
2355
  it('parses JSON objects after decryption', async () => {
@@ -2161,9 +2357,12 @@ describe('DocumentDBEncryptionService', () => {
2161
2357
  const jsonObject = { nested: 'value', array: [1, 2, 3] };
2162
2358
  mockCryptor.decrypt.mockResolvedValue(JSON.stringify(jsonObject));
2163
2359
 
2164
- const result = await service.decryptFields('CustomModel', encryptedDoc);
2360
+ const result = await service.decryptFields(
2361
+ 'CustomModel',
2362
+ encryptedDoc
2363
+ );
2165
2364
 
2166
- expect(result.data.config).toEqual(jsonObject); // Parsed as object
2365
+ expect(result.data.config).toEqual(jsonObject); // Parsed as object
2167
2366
  });
2168
2367
  });
2169
2368
  });
@@ -2203,29 +2402,29 @@ describe('CredentialRepositoryDocumentDB - Security', () => {
2203
2402
  // Create credential via repository
2204
2403
  await repository.upsertCredential({
2205
2404
  identifiers: { userId: fromObjectId(userId), externalId },
2206
- details: { access_token: plainToken, token_type: 'Bearer' }
2405
+ details: { access_token: plainToken, token_type: 'Bearer' },
2207
2406
  });
2208
2407
 
2209
2408
  // Query database directly (bypass repository)
2210
2409
  const rawResult = await prisma.$runCommandRaw({
2211
2410
  find: 'Credential',
2212
- filter: { userId, externalId }
2411
+ filter: { userId, externalId },
2213
2412
  });
2214
2413
 
2215
2414
  const storedCredential = rawResult.cursor.firstBatch[0];
2216
2415
  const storedToken = storedCredential.data.access_token;
2217
2416
 
2218
2417
  // CRITICAL ASSERTIONS
2219
- expect(storedToken).not.toBe(plainToken); // NOT plain text
2220
- expect(storedToken).toMatch(/^[^:]+:[^:]+:[^:]+:[^:]+$/); // Encrypted format
2221
- expect(storedToken.split(':').length).toBeGreaterThanOrEqual(4); // 4+ parts
2418
+ expect(storedToken).not.toBe(plainToken); // NOT plain text
2419
+ expect(storedToken).toMatch(/^[^:]+:[^:]+:[^:]+:[^:]+$/); // Encrypted format
2420
+ expect(storedToken.split(':').length).toBeGreaterThanOrEqual(4); // 4+ parts
2222
2421
 
2223
2422
  // Verify repository returns decrypted
2224
2423
  const retrieved = await repository.findCredential({
2225
2424
  userId: fromObjectId(userId),
2226
- externalId
2425
+ externalId,
2227
2426
  });
2228
- expect(retrieved.access_token).toBe(plainToken); // Decrypted
2427
+ expect(retrieved.access_token).toBe(plainToken); // Decrypted
2229
2428
  });
2230
2429
 
2231
2430
  it('encrypts refresh_token', async () => {
@@ -2233,13 +2432,16 @@ describe('CredentialRepositoryDocumentDB - Security', () => {
2233
2432
  const plainRefresh = '1//0secret_refresh_token';
2234
2433
 
2235
2434
  await repository.upsertCredential({
2236
- identifiers: { userId: fromObjectId(userId), externalId: 'test-456' },
2237
- details: { refresh_token: plainRefresh }
2435
+ identifiers: {
2436
+ userId: fromObjectId(userId),
2437
+ externalId: 'test-456',
2438
+ },
2439
+ details: { refresh_token: plainRefresh },
2238
2440
  });
2239
2441
 
2240
2442
  const rawResult = await prisma.$runCommandRaw({
2241
2443
  find: 'Credential',
2242
- filter: { userId }
2444
+ filter: { userId },
2243
2445
  });
2244
2446
 
2245
2447
  const stored = rawResult.cursor.firstBatch[0].data.refresh_token;
@@ -2252,13 +2454,16 @@ describe('CredentialRepositoryDocumentDB - Security', () => {
2252
2454
  const plainIdToken = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...';
2253
2455
 
2254
2456
  await repository.upsertCredential({
2255
- identifiers: { userId: fromObjectId(userId), externalId: 'test-789' },
2256
- details: { id_token: plainIdToken }
2457
+ identifiers: {
2458
+ userId: fromObjectId(userId),
2459
+ externalId: 'test-789',
2460
+ },
2461
+ details: { id_token: plainIdToken },
2257
2462
  });
2258
2463
 
2259
2464
  const rawResult = await prisma.$runCommandRaw({
2260
2465
  find: 'Credential',
2261
- filter: { userId }
2466
+ filter: { userId },
2262
2467
  });
2263
2468
 
2264
2469
  const stored = rawResult.cursor.firstBatch[0].data.id_token;
@@ -2270,18 +2475,21 @@ describe('CredentialRepositoryDocumentDB - Security', () => {
2270
2475
  const userId = new ObjectId();
2271
2476
 
2272
2477
  await repository.upsertCredential({
2273
- identifiers: { userId: fromObjectId(userId), externalId: 'test-000' },
2478
+ identifiers: {
2479
+ userId: fromObjectId(userId),
2480
+ externalId: 'test-000',
2481
+ },
2274
2482
  details: {
2275
2483
  access_token: 'token123',
2276
- expires_in: 3600, // Not in encrypted fields registry
2277
- token_type: 'Bearer', // Not in registry
2278
- scope: 'openid profile' // Not in registry
2279
- }
2484
+ expires_in: 3600, // Not in encrypted fields registry
2485
+ token_type: 'Bearer', // Not in registry
2486
+ scope: 'openid profile', // Not in registry
2487
+ },
2280
2488
  });
2281
2489
 
2282
2490
  const rawResult = await prisma.$runCommandRaw({
2283
2491
  find: 'Credential',
2284
- filter: { userId }
2492
+ filter: { userId },
2285
2493
  });
2286
2494
 
2287
2495
  const stored = rawResult.cursor.firstBatch[0].data;
@@ -2302,13 +2510,16 @@ describe('CredentialRepositoryDocumentDB - Security', () => {
2302
2510
  const plainData = {
2303
2511
  access_token: 'test_access_123',
2304
2512
  refresh_token: 'test_refresh_456',
2305
- expires_in: 7200
2513
+ expires_in: 7200,
2306
2514
  };
2307
2515
 
2308
2516
  // Insert
2309
2517
  const created = await repository.upsertCredential({
2310
- identifiers: { userId: fromObjectId(userId), externalId: 'flow-test' },
2311
- details: plainData
2518
+ identifiers: {
2519
+ userId: fromObjectId(userId),
2520
+ externalId: 'flow-test',
2521
+ },
2522
+ details: plainData,
2312
2523
  });
2313
2524
 
2314
2525
  // Verify returned data is plain text
@@ -2318,7 +2529,7 @@ describe('CredentialRepositoryDocumentDB - Security', () => {
2318
2529
  // Read via repository
2319
2530
  const retrieved = await repository.findCredential({
2320
2531
  userId: fromObjectId(userId),
2321
- externalId: 'flow-test'
2532
+ externalId: 'flow-test',
2322
2533
  });
2323
2534
 
2324
2535
  // Verify decrypted correctly
@@ -2328,7 +2539,7 @@ describe('CredentialRepositoryDocumentDB - Security', () => {
2328
2539
  // Verify database has encrypted values
2329
2540
  const rawResult = await prisma.$runCommandRaw({
2330
2541
  find: 'Credential',
2331
- filter: { userId }
2542
+ filter: { userId },
2332
2543
  });
2333
2544
  const stored = rawResult.cursor.firstBatch[0].data;
2334
2545
  expect(stored.access_token).not.toBe('test_access_123');
@@ -2346,17 +2557,20 @@ describe('CredentialRepositoryDocumentDB - Security', () => {
2346
2557
  const plainToken = 'dev_token_plain';
2347
2558
 
2348
2559
  await devRepo.upsertCredential({
2349
- identifiers: { userId: fromObjectId(userId), externalId: 'dev-test' },
2350
- details: { access_token: plainToken }
2560
+ identifiers: {
2561
+ userId: fromObjectId(userId),
2562
+ externalId: 'dev-test',
2563
+ },
2564
+ details: { access_token: plainToken },
2351
2565
  });
2352
2566
 
2353
2567
  // In dev, should be stored as plain text
2354
2568
  const rawResult = await prisma.$runCommandRaw({
2355
2569
  find: 'Credential',
2356
- filter: { userId }
2570
+ filter: { userId },
2357
2571
  });
2358
2572
  const stored = rawResult.cursor.firstBatch[0].data.access_token;
2359
- expect(stored).toBe(plainToken); // Plain text in dev!
2573
+ expect(stored).toBe(plainToken); // Plain text in dev!
2360
2574
 
2361
2575
  // Reset to production
2362
2576
  process.env.STAGE = 'production';
@@ -2484,9 +2698,10 @@ sampleCreds.forEach(function(cred) {
2484
2698
  ```
2485
2699
 
2486
2700
  **Estimate Impact**:
2487
- - Number of affected credentials
2488
- - Number of affected users
2489
- - Third-party services (Asana, Frontify, etc.)
2701
+
2702
+ - Number of affected credentials
2703
+ - Number of affected users
2704
+ - Third-party services (Asana, Frontify, etc.)
2490
2705
 
2491
2706
  ---
2492
2707
 
@@ -2495,22 +2710,24 @@ sampleCreds.forEach(function(cred) {
2495
2710
  **Priority Actions**:
2496
2711
 
2497
2712
  1. **Deploy Fix Immediately**:
2498
- ```bash
2499
- # Deploy encryption fix to stop new plain text storage
2500
- cd backend
2501
- npm install @friggframework/core@latest # With encryption fix
2502
- npm run deploy -- --stage production
2503
- ```
2713
+
2714
+ ```bash
2715
+ # Deploy encryption fix to stop new plain text storage
2716
+ cd backend
2717
+ npm install @friggframework/core@latest # With encryption fix
2718
+ npm run deploy -- --stage production
2719
+ ```
2504
2720
 
2505
2721
  2. **Rotate All Affected Tokens**:
2506
- - Force OAuth re-authentication for all users
2507
- - Revoke old tokens on third-party services
2508
- - Generate new encrypted tokens
2722
+
2723
+ - Force OAuth re-authentication for all users
2724
+ - Revoke old tokens on third-party services
2725
+ - Generate new encrypted tokens
2509
2726
 
2510
2727
  3. **Audit Access**:
2511
- - Review database access logs
2512
- - Identify who had access to plain text credentials
2513
- - Check for unauthorized API usage
2728
+ - Review database access logs
2729
+ - Identify who had access to plain text credentials
2730
+ - Check for unauthorized API usage
2514
2731
 
2515
2732
  ---
2516
2733
 
@@ -2519,9 +2736,18 @@ sampleCreds.forEach(function(cred) {
2519
2736
  **Migration Script** (`migrate-encrypt-credentials.js`):
2520
2737
 
2521
2738
  ```javascript
2522
- const { prisma, connectPrisma, disconnectPrisma } = require('@friggframework/core/database/prisma');
2523
- const { DocumentDBEncryptionService } = require('@friggframework/core/database/documentdb-encryption-service');
2524
- const { toObjectId, fromObjectId } = require('@friggframework/core/database/documentdb-utils');
2739
+ const {
2740
+ prisma,
2741
+ connectPrisma,
2742
+ disconnectPrisma,
2743
+ } = require('@friggframework/core/database/prisma');
2744
+ const {
2745
+ DocumentDBEncryptionService,
2746
+ } = require('@friggframework/core/database/documentdb-encryption-service');
2747
+ const {
2748
+ toObjectId,
2749
+ fromObjectId,
2750
+ } = require('@friggframework/core/database/documentdb-utils');
2525
2751
 
2526
2752
  /**
2527
2753
  * Migrate plain text credentials to encrypted format.
@@ -2540,14 +2766,16 @@ async function migrateCredentials() {
2540
2766
  const encryptionService = new DocumentDBEncryptionService();
2541
2767
 
2542
2768
  if (!encryptionService.enabled) {
2543
- console.error('❌ Encryption not enabled! Check environment variables.');
2769
+ console.error(
2770
+ '❌ Encryption not enabled! Check environment variables.'
2771
+ );
2544
2772
  process.exit(1);
2545
2773
  }
2546
2774
 
2547
2775
  // Fetch all credentials
2548
2776
  const result = await prisma.$runCommandRaw({
2549
2777
  find: 'Credential',
2550
- filter: {}
2778
+ filter: {},
2551
2779
  });
2552
2780
 
2553
2781
  const credentials = result.cursor.firstBatch;
@@ -2564,7 +2792,9 @@ async function migrateCredentials() {
2564
2792
  // Check if already encrypted
2565
2793
  const token = cred.data?.access_token;
2566
2794
  if (!token) {
2567
- console.log(`⏭️ Skipping credential ${credId} (no access_token)`);
2795
+ console.log(
2796
+ `⏭️ Skipping credential ${credId} (no access_token)`
2797
+ );
2568
2798
  continue;
2569
2799
  }
2570
2800
 
@@ -2577,29 +2807,36 @@ async function migrateCredentials() {
2577
2807
 
2578
2808
  // Encrypt credential data
2579
2809
  console.log(`🔐 Encrypting credential ${credId}...`);
2580
- const encryptedData = await encryptionService.encryptFields('Credential', {
2581
- data: cred.data
2582
- });
2810
+ const encryptedData = await encryptionService.encryptFields(
2811
+ 'Credential',
2812
+ {
2813
+ data: cred.data,
2814
+ }
2815
+ );
2583
2816
 
2584
2817
  // Update database
2585
2818
  await prisma.$runCommandRaw({
2586
2819
  update: 'Credential',
2587
- updates: [{
2588
- q: { _id: cred._id },
2589
- u: {
2590
- $set: {
2591
- data: encryptedData.data,
2592
- updatedAt: new Date()
2593
- }
2594
- }
2595
- }]
2820
+ updates: [
2821
+ {
2822
+ q: { _id: cred._id },
2823
+ u: {
2824
+ $set: {
2825
+ data: encryptedData.data,
2826
+ updatedAt: new Date(),
2827
+ },
2828
+ },
2829
+ },
2830
+ ],
2596
2831
  });
2597
2832
 
2598
2833
  console.log(`✅ Encrypted credential ${credId}`);
2599
2834
  encryptedCount++;
2600
-
2601
2835
  } catch (error) {
2602
- console.error(`❌ Failed to encrypt credential ${credId}:`, error.message);
2836
+ console.error(
2837
+ `❌ Failed to encrypt credential ${credId}:`,
2838
+ error.message
2839
+ );
2603
2840
  errorCount++;
2604
2841
  }
2605
2842
  }
@@ -2615,7 +2852,7 @@ async function migrateCredentials() {
2615
2852
  }
2616
2853
 
2617
2854
  // Run migration
2618
- migrateCredentials().catch(error => {
2855
+ migrateCredentials().catch((error) => {
2619
2856
  console.error('💥 Migration failed:', error);
2620
2857
  process.exit(1);
2621
2858
  });
@@ -2642,7 +2879,11 @@ node verify-encryption.js # See verification script below
2642
2879
  **Verification Script** (`verify-encryption.js`):
2643
2880
 
2644
2881
  ```javascript
2645
- const { prisma, connectPrisma, disconnectPrisma } = require('@friggframework/core/database/prisma');
2882
+ const {
2883
+ prisma,
2884
+ connectPrisma,
2885
+ disconnectPrisma,
2886
+ } = require('@friggframework/core/database/prisma');
2646
2887
 
2647
2888
  async function verifyEncryption() {
2648
2889
  console.log('🔍 Verifying credential encryption...');
@@ -2651,7 +2892,7 @@ async function verifyEncryption() {
2651
2892
 
2652
2893
  const result = await prisma.$runCommandRaw({
2653
2894
  find: 'Credential',
2654
- filter: {}
2895
+ filter: {},
2655
2896
  });
2656
2897
 
2657
2898
  const credentials = result.cursor.firstBatch;
@@ -2678,14 +2919,16 @@ async function verifyEncryption() {
2678
2919
  console.log(` Plain text: ${failCount}`);
2679
2920
 
2680
2921
  if (failCount > 0) {
2681
- console.error('\n❌ Verification failed! Plain text credentials still exist.');
2922
+ console.error(
2923
+ '\n❌ Verification failed! Plain text credentials still exist.'
2924
+ );
2682
2925
  process.exit(1);
2683
2926
  } else {
2684
2927
  console.log('\n✅ Verification passed! All credentials encrypted.');
2685
2928
  }
2686
2929
  }
2687
2930
 
2688
- verifyEncryption().catch(error => {
2931
+ verifyEncryption().catch((error) => {
2689
2932
  console.error('💥 Verification failed:', error);
2690
2933
  process.exit(1);
2691
2934
  });
@@ -2696,20 +2939,22 @@ verifyEncryption().catch(error => {
2696
2939
  ### Step 5: Post-Migration Cleanup
2697
2940
 
2698
2941
  1. **Delete Migration Scripts**:
2699
- ```bash
2700
- rm migrate-encrypt-credentials.js
2701
- rm verify-encryption.js
2702
- ```
2942
+
2943
+ ```bash
2944
+ rm migrate-encrypt-credentials.js
2945
+ rm verify-encryption.js
2946
+ ```
2703
2947
 
2704
2948
  2. **Update Documentation**:
2705
- - Document the incident
2706
- - Document lessons learned
2707
- - Update security procedures
2949
+
2950
+ - Document the incident
2951
+ - Document lessons learned
2952
+ - Update security procedures
2708
2953
 
2709
2954
  3. **Monitor**:
2710
- - Set up alerts for plain text detection
2711
- - Monitor API error rates (in case decryption fails)
2712
- - Watch for OAuth re-authentication requests
2955
+ - Set up alerts for plain text detection
2956
+ - Monitor API error rates (in case decryption fails)
2957
+ - Watch for OAuth re-authentication requests
2713
2958
 
2714
2959
  ---
2715
2960
 
@@ -2720,18 +2965,20 @@ verifyEncryption().catch(error => {
2720
2965
  1. **Stop the migration script**
2721
2966
 
2722
2967
  2. **Restore from backup**:
2723
- ```bash
2724
- # Restore MongoDB backup from before migration
2725
- mongorestore --uri="mongodb://..." --archive=backup-before-migration.archive
2726
- ```
2968
+
2969
+ ```bash
2970
+ # Restore MongoDB backup from before migration
2971
+ mongorestore --uri="mongodb://..." --archive=backup-before-migration.archive
2972
+ ```
2727
2973
 
2728
2974
  3. **Revert code deployment**:
2729
- ```bash
2730
- # Rollback to previous version
2731
- cd backend
2732
- npm install @friggframework/core@<previous-version>
2733
- npm run deploy -- --stage production
2734
- ```
2975
+
2976
+ ```bash
2977
+ # Rollback to previous version
2978
+ cd backend
2979
+ npm install @friggframework/core@<previous-version>
2980
+ npm run deploy -- --stage production
2981
+ ```
2735
2982
 
2736
2983
  4. **Investigate and fix issues**
2737
2984
 
@@ -2744,28 +2991,31 @@ verifyEncryption().catch(error => {
2744
2991
  For large deployments:
2745
2992
 
2746
2993
  1. **Phase 1: Deploy encryption fix** (don't migrate yet)
2747
- - New credentials will be encrypted
2748
- - Old credentials remain as-is
2749
- - Application handles both encrypted and plain text
2994
+
2995
+ - New credentials will be encrypted
2996
+ - Old credentials remain as-is
2997
+ - Application handles both encrypted and plain text
2750
2998
 
2751
2999
  2. **Phase 2: Migrate in batches**
2752
- ```javascript
2753
- // Migrate 100 credentials at a time
2754
- const batchSize = 100;
2755
- for (let skip = 0; skip < totalCredentials; skip += batchSize) {
2756
- await migrateBatch(skip, batchSize);
2757
- await sleep(1000); // 1 second between batches
2758
- }
2759
- ```
3000
+
3001
+ ```javascript
3002
+ // Migrate 100 credentials at a time
3003
+ const batchSize = 100;
3004
+ for (let skip = 0; skip < totalCredentials; skip += batchSize) {
3005
+ await migrateBatch(skip, batchSize);
3006
+ await sleep(1000); // 1 second between batches
3007
+ }
3008
+ ```
2760
3009
 
2761
3010
  3. **Phase 3: Verify**
2762
- - Check random samples
2763
- - Monitor error rates
2764
- - Verify API calls still work
3011
+
3012
+ - Check random samples
3013
+ - Monitor error rates
3014
+ - Verify API calls still work
2765
3015
 
2766
3016
  4. **Phase 4: Complete**
2767
- - Remove backward compatibility code
2768
- - Update monitoring alerts
3017
+ - Remove backward compatibility code
3018
+ - Update monitoring alerts
2769
3019
 
2770
3020
  ---
2771
3021
 
@@ -2774,17 +3024,20 @@ For large deployments:
2774
3024
  ### Encryption Format
2775
3025
 
2776
3026
  **Envelope Encryption Pattern**:
3027
+
2777
3028
  ```
2778
3029
  keyId:iv:cipher:encKey
2779
3030
  ```
2780
3031
 
2781
3032
  **Components**:
2782
- - `keyId`: Identifier for the encryption key (e.g., "aes-key-1", KMS key ID)
2783
- - `iv`: Initialization vector (base64-encoded)
2784
- - `cipher`: Encrypted data (base64-encoded)
2785
- - `encKey`: Encrypted data encryption key (base64-encoded)
3033
+
3034
+ - `keyId`: Identifier for the encryption key (e.g., "aes-key-1", KMS key ID)
3035
+ - `iv`: Initialization vector (base64-encoded)
3036
+ - `cipher`: Encrypted data (base64-encoded)
3037
+ - `encKey`: Encrypted data encryption key (base64-encoded)
2786
3038
 
2787
3039
  **Example**:
3040
+
2788
3041
  ```
2789
3042
  aes-key-1:MTIzNDU2Nzg5MGFiY2RlZg==:ZW5jcnlwdGVkX2RhdGFfaGVyZQ==:ZGVrX2VuY3J5cHRlZA==
2790
3043
  ```
@@ -2794,6 +3047,7 @@ aes-key-1:MTIzNDU2Nzg5MGFiY2RlZg==:ZW5jcnlwdGVkX2RhdGFfaGVyZQ==:ZGVrX2VuY3J5cHRl
2794
3047
  ### Key Management
2795
3048
 
2796
3049
  **Production (KMS - Recommended)**:
3050
+
2797
3051
  ```bash
2798
3052
  # AWS KMS key is auto-discovered by Frigg infrastructure
2799
3053
  # Or set explicitly:
@@ -2804,13 +3058,15 @@ export STAGE=production
2804
3058
  ```
2805
3059
 
2806
3060
  **Benefits**:
2807
- - ✅ AWS-managed key rotation
2808
- - Audit trail via CloudTrail
2809
- - Fine-grained IAM permissions
2810
- - Hardware security module (HSM) backed
2811
- - Compliance-ready (HIPAA, PCI-DSS, etc.)
3061
+
3062
+ - AWS-managed key rotation
3063
+ - Audit trail via CloudTrail
3064
+ - Fine-grained IAM permissions
3065
+ - Hardware security module (HSM) backed
3066
+ - ✅ Compliance-ready (HIPAA, PCI-DSS, etc.)
2812
3067
 
2813
3068
  **Alternative (AES - Any Environment)**:
3069
+
2814
3070
  ```bash
2815
3071
  # Generate a 32-character key
2816
3072
  export AES_KEY_ID=my-app-key-v1
@@ -2821,14 +3077,16 @@ export STAGE=production
2821
3077
  ```
2822
3078
 
2823
3079
  **Benefits**:
2824
- - ✅ Works in any environment (no AWS required)
2825
- - Faster than KMS (no network calls)
2826
- - No AWS costs
3080
+
3081
+ - Works in any environment (no AWS required)
3082
+ - Faster than KMS (no network calls)
3083
+ - ✅ No AWS costs
2827
3084
 
2828
3085
  **Drawbacks**:
2829
- - ⚠️ Must securely manage key yourself
2830
- - ⚠️ No automatic key rotation
2831
- - ⚠️ Key stored in environment/config
3086
+
3087
+ - ⚠️ Must securely manage key yourself
3088
+ - ⚠️ No automatic key rotation
3089
+ - ⚠️ Key stored in environment/config
2832
3090
 
2833
3091
  ---
2834
3092
 
@@ -2837,18 +3095,21 @@ export STAGE=production
2837
3095
  **Purpose**: Skip encryption in local development for easier debugging
2838
3096
 
2839
3097
  **Bypassed Stages**:
2840
- - `dev`
2841
- - `test`
2842
- - `local`
3098
+
3099
+ - `dev`
3100
+ - `test`
3101
+ - `local`
2843
3102
 
2844
3103
  **Production Stages** (encryption enabled):
2845
- - `production`
2846
- - `prod`
2847
- - `staging`
2848
- - `stage`
2849
- - Any other value
3104
+
3105
+ - `production`
3106
+ - `prod`
3107
+ - `staging`
3108
+ - `stage`
3109
+ - Any other value
2850
3110
 
2851
3111
  **Configuration**:
3112
+
2852
3113
  ```bash
2853
3114
  # Bypass encryption (dev)
2854
3115
  export STAGE=dev
@@ -2871,6 +3132,7 @@ export KMS_KEY_ARN=...
2871
3132
  **Location**: `packages/core/database/encryption/encryption-schema-registry.js`
2872
3133
 
2873
3134
  **Current Encrypted Fields**:
3135
+
2874
3136
  ```javascript
2875
3137
  const ENCRYPTED_FIELDS = {
2876
3138
  User: ['hashword'],
@@ -2878,10 +3140,10 @@ const ENCRYPTED_FIELDS = {
2878
3140
  'data.access_token',
2879
3141
  'data.refresh_token',
2880
3142
  'data.id_token',
2881
- 'data.domain'
3143
+ 'data.domain',
2882
3144
  ],
2883
3145
  IntegrationMapping: ['mapping'],
2884
- Token: ['token']
3146
+ Token: ['token'],
2885
3147
  };
2886
3148
  ```
2887
3149
 
@@ -2889,42 +3151,47 @@ const ENCRYPTED_FIELDS = {
2889
3151
 
2890
3152
  1. Open `encryption-schema-registry.js`
2891
3153
  2. Add field path to appropriate model:
2892
- ```javascript
2893
- Credential: [
2894
- 'data.access_token',
2895
- 'data.refresh_token',
2896
- 'data.id_token',
2897
- 'data.domain',
2898
- 'data.client_secret' // ← NEW
2899
- ]
2900
- ```
3154
+ ```javascript
3155
+ Credential: [
3156
+ 'data.access_token',
3157
+ 'data.refresh_token',
3158
+ 'data.id_token',
3159
+ 'data.domain',
3160
+ 'data.client_secret', // ← NEW
3161
+ ];
3162
+ ```
2901
3163
  3. Deploy - encryption applied automatically (no code changes needed)
2902
3164
 
2903
3165
  **Field Path Examples**:
2904
- - Top-level: `hashword` → encrypts `document.hashword`
2905
- - Nested: `data.access_token` → encrypts `document.data.access_token`
2906
- - Deep nesting supported: `config.secrets.apiKey`
3166
+
3167
+ - Top-level: `hashword` → encrypts `document.hashword`
3168
+ - Nested: `data.access_token` encrypts `document.data.access_token`
3169
+ - Deep nesting supported: `config.secrets.apiKey`
2907
3170
 
2908
3171
  ---
2909
3172
 
2910
3173
  ### Compliance & Best Practices
2911
3174
 
2912
3175
  **GDPR Compliance**:
2913
- - ✅ Data encrypted at rest
2914
- - Encryption keys managed securely
2915
- - User data can be deleted (right to erasure)
3176
+
3177
+ - Data encrypted at rest
3178
+ - Encryption keys managed securely
3179
+ - ✅ User data can be deleted (right to erasure)
2916
3180
 
2917
3181
  **PCI-DSS Compliance** (if storing payment data):
2918
- - ✅ Encryption of cardholder data
2919
- - Key management procedures
2920
- - Audit logging (via CloudTrail with KMS)
3182
+
3183
+ - Encryption of cardholder data
3184
+ - Key management procedures
3185
+ - ✅ Audit logging (via CloudTrail with KMS)
2921
3186
 
2922
3187
  **HIPAA Compliance** (if storing health data):
2923
- - ✅ Encryption at rest (required)
2924
- - Access controls (AWS KMS IAM)
2925
- - Audit trail (CloudTrail)
3188
+
3189
+ - Encryption at rest (required)
3190
+ - Access controls (AWS KMS IAM)
3191
+ - ✅ Audit trail (CloudTrail)
2926
3192
 
2927
3193
  **Best Practices**:
3194
+
2928
3195
  1. **Use KMS in production** - Better security, compliance, key rotation
2929
3196
  2. **Rotate keys periodically** - Even with KMS, review and rotate annually
2930
3197
  3. **Monitor decryption failures** - Alert on >1% failure rate
@@ -2938,16 +3205,16 @@ const ENCRYPTED_FIELDS = {
2938
3205
 
2939
3206
  Before going to production:
2940
3207
 
2941
- - [ ] Verify `STAGE=production` in environment
2942
- - [ ] Verify encryption keys configured (`KMS_KEY_ARN` or `AES_KEY_ID`)
2943
- - [ ] Run security tests (verify encrypted format in database)
2944
- - [ ] Test credential creation and retrieval end-to-end
2945
- - [ ] Verify OAuth flows work (tokens decrypted correctly)
2946
- - [ ] Check logs for decryption errors
2947
- - [ ] Review IAM permissions (if using KMS)
2948
- - [ ] Test key rotation procedure (if using KMS)
2949
- - [ ] Document encryption architecture for auditors
2950
- - [ ] Set up monitoring alerts (decryption failures, plain text detection)
3208
+ - [ ] Verify `STAGE=production` in environment
3209
+ - [ ] Verify encryption keys configured (`KMS_KEY_ARN` or `AES_KEY_ID`)
3210
+ - [ ] Run security tests (verify encrypted format in database)
3211
+ - [ ] Test credential creation and retrieval end-to-end
3212
+ - [ ] Verify OAuth flows work (tokens decrypted correctly)
3213
+ - [ ] Check logs for decryption errors
3214
+ - [ ] Review IAM permissions (if using KMS)
3215
+ - [ ] Test key rotation procedure (if using KMS)
3216
+ - [ ] Document encryption architecture for auditors
3217
+ - [ ] Set up monitoring alerts (decryption failures, plain text detection)
2951
3218
 
2952
3219
  ---
2953
3220
 
@@ -2958,44 +3225,51 @@ Before going to production:
2958
3225
  When creating a new DocumentDB repository that handles encrypted data:
2959
3226
 
2960
3227
  1. **Import DocumentDBEncryptionService**:
2961
- ```javascript
2962
- const { DocumentDBEncryptionService } = require('../database/documentdb-encryption-service');
2963
- ```
3228
+
3229
+ ```javascript
3230
+ const {
3231
+ DocumentDBEncryptionService,
3232
+ } = require('../database/documentdb-encryption-service');
3233
+ ```
2964
3234
 
2965
3235
  2. **Initialize in constructor**:
2966
- ```javascript
2967
- constructor() {
2968
- this.prisma = prisma;
2969
- this.encryptionService = new DocumentDBEncryptionService();
2970
- }
2971
- ```
3236
+
3237
+ ```javascript
3238
+ constructor() {
3239
+ this.prisma = prisma;
3240
+ this.encryptionService = new DocumentDBEncryptionService();
3241
+ }
3242
+ ```
2972
3243
 
2973
3244
  3. **Encrypt before writes**:
2974
- ```javascript
2975
- async create(data) {
2976
- const encrypted = await this.encryptionService.encryptFields('ModelName', data);
2977
- const id = await insertOne(this.prisma, 'CollectionName', encrypted);
2978
- // ...
2979
- }
2980
- ```
3245
+
3246
+ ```javascript
3247
+ async create(data) {
3248
+ const encrypted = await this.encryptionService.encryptFields('ModelName', data);
3249
+ const id = await insertOne(this.prisma, 'CollectionName', encrypted);
3250
+ // ...
3251
+ }
3252
+ ```
2981
3253
 
2982
3254
  4. **Decrypt after reads**:
2983
- ```javascript
2984
- async findById(id) {
2985
- const doc = await findOne(this.prisma, 'CollectionName', { _id: toObjectId(id) });
2986
- const decrypted = await this.encryptionService.decryptFields('ModelName', doc);
2987
- return this._mapModel(decrypted);
2988
- }
2989
- ```
3255
+
3256
+ ```javascript
3257
+ async findById(id) {
3258
+ const doc = await findOne(this.prisma, 'CollectionName', { _id: toObjectId(id) });
3259
+ const decrypted = await this.encryptionService.decryptFields('ModelName', doc);
3260
+ return this._mapModel(decrypted);
3261
+ }
3262
+ ```
2990
3263
 
2991
3264
  5. **Add encrypted fields to registry** (if new model):
2992
- ```javascript
2993
- // packages/core/database/encryption/encryption-schema-registry.js
2994
- const ENCRYPTED_FIELDS = {
2995
- // ... existing models
2996
- NewModel: ['sensitiveField1', 'nested.field2']
2997
- };
2998
- ```
3265
+
3266
+ ```javascript
3267
+ // packages/core/database/encryption/encryption-schema-registry.js
3268
+ const ENCRYPTED_FIELDS = {
3269
+ // ... existing models
3270
+ NewModel: ['sensitiveField1', 'nested.field2'],
3271
+ };
3272
+ ```
2999
3273
 
3000
3274
  6. **Add tests** (see Phase 5 for test patterns)
3001
3275
 
@@ -3006,88 +3280,98 @@ When creating a new DocumentDB repository that handles encrypted data:
3006
3280
  To encrypt a new field in an existing model:
3007
3281
 
3008
3282
  1. **Update encryption-schema-registry.js**:
3009
- ```javascript
3010
- const ENCRYPTED_FIELDS = {
3011
- Credential: [
3012
- 'data.access_token',
3013
- 'data.refresh_token',
3014
- 'data.id_token',
3015
- 'data.domain',
3016
- 'data.client_secret' // ← NEW FIELD
3017
- ]
3018
- };
3019
- ```
3283
+
3284
+ ```javascript
3285
+ const ENCRYPTED_FIELDS = {
3286
+ Credential: [
3287
+ 'data.access_token',
3288
+ 'data.refresh_token',
3289
+ 'data.id_token',
3290
+ 'data.domain',
3291
+ 'data.client_secret', // ← NEW FIELD
3292
+ ],
3293
+ };
3294
+ ```
3020
3295
 
3021
3296
  2. **No code changes needed** - DocumentDBEncryptionService reads from registry
3022
3297
 
3023
3298
  3. **Deploy** - new field will be encrypted automatically
3024
3299
 
3025
3300
  4. **Migrate existing data** (if field already has plain text values):
3026
- ```javascript
3027
- // Run migration script to encrypt existing plain text values
3028
- // Similar to credential migration script
3029
- ```
3301
+ ```javascript
3302
+ // Run migration script to encrypt existing plain text values
3303
+ // Similar to credential migration script
3304
+ ```
3030
3305
 
3031
3306
  ---
3032
3307
 
3033
3308
  ### Known Limitations
3034
3309
 
3035
3310
  1. **Performance**: Encryption/decryption adds latency
3036
- - KMS: ~50ms per field (network call to AWS)
3037
- - AES: ~5-10ms per field (local crypto)
3038
- - **Mitigation**: Use bulk operations, consider caching decrypted values
3311
+
3312
+ - KMS: ~50ms per field (network call to AWS)
3313
+ - AES: ~5-10ms per field (local crypto)
3314
+ - **Mitigation**: Use bulk operations, consider caching decrypted values
3039
3315
 
3040
3316
  2. **DocumentDB-specific**: Only needed for DocumentDB
3041
- - MongoDB/PostgreSQL use automatic Prisma Extension
3042
- - Duplicate logic unavoidable (Prisma raw queries bypass extensions)
3317
+
3318
+ - MongoDB/PostgreSQL use automatic Prisma Extension
3319
+ - Duplicate logic unavoidable (Prisma raw queries bypass extensions)
3043
3320
 
3044
3321
  3. **Manual encryption required**: Developers must remember to call service
3045
- - **Mitigation**: Code reviews, tests, linting rules
3322
+
3323
+ - **Mitigation**: Code reviews, tests, linting rules
3046
3324
 
3047
3325
  4. **No transactional encryption**: Encryption happens outside transactions
3048
- - **Risk**: If encryption fails mid-operation, could leave inconsistent state
3049
- - **Mitigation**: Encrypt before transaction starts, handle errors
3326
+
3327
+ - **Risk**: If encryption fails mid-operation, could leave inconsistent state
3328
+ - **Mitigation**: Encrypt before transaction starts, handle errors
3050
3329
 
3051
3330
  5. **Field-level only**: Doesn't encrypt entire documents or collections
3052
- - **Alternative**: Use database-level encryption (AWS DocumentDB encryption at rest)
3331
+ - **Alternative**: Use database-level encryption (AWS DocumentDB encryption at rest)
3053
3332
 
3054
3333
  ---
3055
3334
 
3056
3335
  ### Future Improvements
3057
3336
 
3058
3337
  1. **Automatic Repository Decorator**:
3059
- ```javascript
3060
- // Potential future API
3061
- @encryptDocumentDB(['User', 'Credential'])
3062
- class MyRepositoryDocumentDB {
3063
- // Encryption applied automatically by decorator
3064
- }
3065
- ```
3338
+
3339
+ ```javascript
3340
+ // Potential future API
3341
+ @encryptDocumentDB(['User', 'Credential'])
3342
+ class MyRepositoryDocumentDB {
3343
+ // Encryption applied automatically by decorator
3344
+ }
3345
+ ```
3066
3346
 
3067
3347
  2. **Encryption Caching**:
3068
- - Cache decrypted values for frequently accessed credentials
3069
- - Invalidate cache on credential update
3070
- - Reduce KMS API calls
3348
+
3349
+ - Cache decrypted values for frequently accessed credentials
3350
+ - Invalidate cache on credential update
3351
+ - Reduce KMS API calls
3071
3352
 
3072
3353
  3. **Field Compression**:
3073
- - Compress large fields before encryption
3074
- - Reduce storage and transfer costs
3075
- - Especially useful for `IntegrationMapping.mapping`
3354
+
3355
+ - Compress large fields before encryption
3356
+ - Reduce storage and transfer costs
3357
+ - Especially useful for `IntegrationMapping.mapping`
3076
3358
 
3077
3359
  4. **Key Versioning**:
3078
- - Support multiple active keys
3079
- - Gradual key rotation without migration
3080
- - Store key version with encrypted data
3360
+
3361
+ - Support multiple active keys
3362
+ - Gradual key rotation without migration
3363
+ - Store key version with encrypted data
3081
3364
 
3082
3365
  5. **Encryption Metrics**:
3083
- - Track encryption/decryption performance
3084
- - Monitor failure rates
3085
- - Alert on anomalies
3366
+
3367
+ - Track encryption/decryption performance
3368
+ - Monitor failure rates
3369
+ - Alert on anomalies
3086
3370
 
3087
3371
  6. **Integration with Prisma Extension**:
3088
- - Potential future Prisma feature: Extension support for raw queries
3089
- - Would eliminate need for DocumentDBEncryptionService
3090
- - Track: https://github.com/prisma/prisma/issues/...
3372
+ - Potential future Prisma feature: Extension support for raw queries
3373
+ - Would eliminate need for DocumentDBEncryptionService
3374
+ - Track: https://github.com/prisma/prisma/issues/...
3091
3375
 
3092
3376
  ---
3093
3377
 
@@ -3096,39 +3380,43 @@ To encrypt a new field in an existing model:
3096
3380
  **Recommended Metrics**:
3097
3381
 
3098
3382
  1. **Encryption Failures**:
3099
- ```javascript
3100
- // Log when encryption fails
3101
- console.error('Encryption failed', { modelName, fieldPath, error });
3102
- // Alert if >1% of operations fail
3103
- ```
3383
+
3384
+ ```javascript
3385
+ // Log when encryption fails
3386
+ console.error('Encryption failed', { modelName, fieldPath, error });
3387
+ // Alert if >1% of operations fail
3388
+ ```
3104
3389
 
3105
3390
  2. **Decryption Failures**:
3106
- ```javascript
3107
- // Log when decryption fails
3108
- console.error('Decryption failed', { modelName, fieldPath, error });
3109
- // Alert immediately (could indicate data corruption)
3110
- ```
3391
+
3392
+ ```javascript
3393
+ // Log when decryption fails
3394
+ console.error('Decryption failed', { modelName, fieldPath, error });
3395
+ // Alert immediately (could indicate data corruption)
3396
+ ```
3111
3397
 
3112
3398
  3. **Plain Text Detection**:
3113
- ```javascript
3114
- // Periodic scan of database
3115
- // Alert if any plain text credentials found
3116
- ```
3399
+
3400
+ ```javascript
3401
+ // Periodic scan of database
3402
+ // Alert if any plain text credentials found
3403
+ ```
3117
3404
 
3118
3405
  4. **Performance Metrics**:
3119
- ```javascript
3120
- // Track encryption/decryption time
3121
- const start = Date.now();
3122
- await service.encryptFields(...);
3123
- const duration = Date.now() - start;
3124
- metrics.histogram('encryption_duration_ms', duration);
3125
- ```
3406
+ ```javascript
3407
+ // Track encryption/decryption time
3408
+ const start = Date.now();
3409
+ await service.encryptFields(...);
3410
+ const duration = Date.now() - start;
3411
+ metrics.histogram('encryption_duration_ms', duration);
3412
+ ```
3126
3413
 
3127
3414
  **CloudWatch Dashboards** (for AWS deployments):
3128
- - Encryption operation count
3129
- - Average encryption duration
3130
- - Decryption failure rate
3131
- - KMS API call count (if using KMS)
3415
+
3416
+ - Encryption operation count
3417
+ - Average encryption duration
3418
+ - Decryption failure rate
3419
+ - KMS API call count (if using KMS)
3132
3420
 
3133
3421
  ---
3134
3422
 
@@ -3137,31 +3425,36 @@ To encrypt a new field in an existing model:
3137
3425
  **Common Issues**:
3138
3426
 
3139
3427
  1. **"No encryption keys configured"**
3140
- - **Cause**: Missing `KMS_KEY_ARN` or `AES_KEY_ID` in production
3141
- - **Fix**: Set environment variables, restart application
3428
+
3429
+ - **Cause**: Missing `KMS_KEY_ARN` or `AES_KEY_ID` in production
3430
+ - **Fix**: Set environment variables, restart application
3142
3431
 
3143
3432
  2. **"Decryption failed"**
3144
- - **Cause**: Wrong key, corrupted data, or key rotation
3145
- - **Fix**: Check key configuration, verify data integrity, check key version
3433
+
3434
+ - **Cause**: Wrong key, corrupted data, or key rotation
3435
+ - **Fix**: Check key configuration, verify data integrity, check key version
3146
3436
 
3147
3437
  3. **"Cannot read property 'access_token' of undefined"**
3148
- - **Cause**: Credential data is null or decryption returned null
3149
- - **Fix**: Check if credential exists, verify encryption didn't fail on write
3438
+
3439
+ - **Cause**: Credential data is null or decryption returned null
3440
+ - **Fix**: Check if credential exists, verify encryption didn't fail on write
3150
3441
 
3151
3442
  4. **"Encryption too slow"**
3152
- - **Cause**: Using KMS with high latency
3153
- - **Fix**: Switch to AES for non-production, optimize KMS calls (batching)
3443
+
3444
+ - **Cause**: Using KMS with high latency
3445
+ - **Fix**: Switch to AES for non-production, optimize KMS calls (batching)
3154
3446
 
3155
3447
  5. **"Credentials not encrypted after deployment"**
3156
- - **Cause**: `STAGE=dev` in production, or missing encryption keys
3157
- - **Fix**: Set `STAGE=production`, configure keys, redeploy
3448
+ - **Cause**: `STAGE=dev` in production, or missing encryption keys
3449
+ - **Fix**: Set `STAGE=production`, configure keys, redeploy
3158
3450
 
3159
3451
  **Getting Help**:
3160
- - Check logs for error details
3161
- - Review encryption-schema-registry.js configuration
3162
- - Verify environment variables
3163
- - Run health check: `curl http://localhost:3000/health/detailed`
3164
- - Check encryption status in health response
3452
+
3453
+ - Check logs for error details
3454
+ - Review encryption-schema-registry.js configuration
3455
+ - Verify environment variables
3456
+ - Run health check: `curl http://localhost:3000/health/detailed`
3457
+ - Check encryption status in health response
3165
3458
 
3166
3459
  ---
3167
3460
 
@@ -3170,59 +3463,69 @@ To encrypt a new field in an existing model:
3170
3463
  ### Related Files
3171
3464
 
3172
3465
  **Core Encryption**:
3173
- - `packages/core/database/encryption/README.md` - Main encryption documentation
3174
- - `packages/core/database/encryption/encryption-schema-registry.js` - Encrypted fields definition
3175
- - `packages/core/database/encryption/field-encryption-service.js` - Field-level encryption (Prisma Extension)
3176
- - `packages/core/database/encryption/prisma-encryption-extension.js` - Prisma Client Extension
3177
- - `packages/core/encrypt/Cryptor.js` - Encryption adapter (KMS/AES)
3466
+
3467
+ - `packages/core/database/encryption/README.md` - Main encryption documentation
3468
+ - `packages/core/database/encryption/encryption-schema-registry.js` - Encrypted fields definition
3469
+ - `packages/core/database/encryption/field-encryption-service.js` - Field-level encryption (Prisma Extension)
3470
+ - `packages/core/database/encryption/prisma-encryption-extension.js` - Prisma Client Extension
3471
+ - `packages/core/encrypt/Cryptor.js` - Encryption adapter (KMS/AES)
3178
3472
 
3179
3473
  **DocumentDB**:
3180
- - `packages/core/database/documentdb-utils.js` - Raw query utilities
3181
- - `packages/core/database/prisma.js` - Prisma client initialization
3474
+
3475
+ - `packages/core/database/documentdb-utils.js` - Raw query utilities
3476
+ - `packages/core/database/prisma.js` - Prisma client initialization
3182
3477
 
3183
3478
  **Repositories**:
3184
- - `packages/core/user/repositories/user-repository-documentdb.js` - User repository
3185
- - `packages/core/modules/repositories/module-repository-documentdb.js` - Module/Entity repository
3186
- - `packages/core/credential/repositories/credential-repository-documentdb.js` - Credential repository
3187
- - `packages/core/integrations/repositories/integration-repository-documentdb.js` - Integration repository
3479
+
3480
+ - `packages/core/user/repositories/user-repository-documentdb.js` - User repository
3481
+ - `packages/core/modules/repositories/module-repository-documentdb.js` - Module/Entity repository
3482
+ - `packages/core/credential/repositories/credential-repository-documentdb.js` - Credential repository
3483
+ - `packages/core/integrations/repositories/integration-repository-documentdb.js` - Integration repository
3188
3484
 
3189
3485
  **Tests**:
3190
- - `packages/core/database/encryption/*.test.js` - Encryption unit tests
3191
- - `packages/core/**/repositories/__tests__/*.test.js` - Repository tests
3486
+
3487
+ - `packages/core/database/encryption/*.test.js` - Encryption unit tests
3488
+ - `packages/core/**/repositories/__tests__/*.test.js` - Repository tests
3192
3489
 
3193
3490
  ---
3194
3491
 
3195
3492
  ### External Documentation
3196
3493
 
3197
3494
  **Prisma**:
3198
- - [Prisma Client Extensions](https://www.prisma.io/docs/concepts/components/prisma-client/client-extensions)
3199
- - [Raw Database Access](https://www.prisma.io/docs/concepts/components/prisma-client/raw-database-access)
3200
- - [MongoDB Support](https://www.prisma.io/docs/concepts/database-connectors/mongodb)
3495
+
3496
+ - [Prisma Client Extensions](https://www.prisma.io/docs/concepts/components/prisma-client/client-extensions)
3497
+ - [Raw Database Access](https://www.prisma.io/docs/concepts/components/prisma-client/raw-database-access)
3498
+ - [MongoDB Support](https://www.prisma.io/docs/concepts/database-connectors/mongodb)
3201
3499
 
3202
3500
  **AWS DocumentDB**:
3203
- - [AWS DocumentDB Documentation](https://docs.aws.amazon.com/documentdb/)
3204
- - [MongoDB Compatibility](https://docs.aws.amazon.com/documentdb/latest/developerguide/functional-differences.html)
3501
+
3502
+ - [AWS DocumentDB Documentation](https://docs.aws.amazon.com/documentdb/)
3503
+ - [MongoDB Compatibility](https://docs.aws.amazon.com/documentdb/latest/developerguide/functional-differences.html)
3205
3504
 
3206
3505
  **AWS KMS**:
3207
- - [AWS KMS Developer Guide](https://docs.aws.amazon.com/kms/latest/developerguide/)
3208
- - [Envelope Encryption](https://docs.aws.amazon.com/kms/latest/developerguide/concepts.html#enveloping)
3506
+
3507
+ - [AWS KMS Developer Guide](https://docs.aws.amazon.com/kms/latest/developerguide/)
3508
+ - [Envelope Encryption](https://docs.aws.amazon.com/kms/latest/developerguide/concepts.html#enveloping)
3209
3509
 
3210
3510
  **Encryption Best Practices**:
3211
- - [OWASP Cryptographic Storage Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cryptographic_Storage_Cheat_Sheet.html)
3212
- - [NIST Encryption Standards](https://csrc.nist.gov/projects/cryptographic-standards-and-guidelines)
3511
+
3512
+ - [OWASP Cryptographic Storage Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cryptographic_Storage_Cheat_Sheet.html)
3513
+ - [NIST Encryption Standards](https://csrc.nist.gov/projects/cryptographic-standards-and-guidelines)
3213
3514
 
3214
3515
  ---
3215
3516
 
3216
3517
  ### Frigg Framework
3217
3518
 
3218
3519
  **Core Documentation**:
3219
- - [Frigg Framework Docs](https://docs.friggframework.org)
3220
- - [GitHub Repository](https://github.com/friggframework/frigg)
3221
- - [Community Slack](https://friggframework.org/#contact)
3520
+
3521
+ - [Frigg Framework Docs](https://docs.friggframework.org)
3522
+ - [GitHub Repository](https://github.com/friggframework/frigg)
3523
+ - [Community Slack](https://friggframework.org/#contact)
3222
3524
 
3223
3525
  **Related Issues**:
3224
- - GitHub Issue: DocumentDB encryption support [#TBD]
3225
- - GitHub PR: Implement DocumentDBEncryptionService [#TBD]
3526
+
3527
+ - GitHub Issue: DocumentDB encryption support [#TBD]
3528
+ - GitHub PR: Implement DocumentDBEncryptionService [#TBD]
3226
3529
 
3227
3530
  ---
3228
3531
 
@@ -3231,29 +3534,31 @@ To encrypt a new field in an existing model:
3231
3534
  ### Glossary
3232
3535
 
3233
3536
  **Terms**:
3234
- - **DocumentDB**: AWS DocumentDB, a MongoDB-compatible database service
3235
- - **Prisma Extension**: Prisma feature that intercepts and modifies queries
3236
- - **Raw Query**: Low-level database command that bypasses Prisma ORM
3237
- - **Envelope Encryption**: Encryption pattern using data keys encrypted by master keys
3238
- - **KMS**: AWS Key Management Service
3239
- - **AES**: Advanced Encryption Standard (symmetric encryption)
3240
- - **Field-Level Encryption**: Encrypting individual fields within documents
3537
+
3538
+ - **DocumentDB**: AWS DocumentDB, a MongoDB-compatible database service
3539
+ - **Prisma Extension**: Prisma feature that intercepts and modifies queries
3540
+ - **Raw Query**: Low-level database command that bypasses Prisma ORM
3541
+ - **Envelope Encryption**: Encryption pattern using data keys encrypted by master keys
3542
+ - **KMS**: AWS Key Management Service
3543
+ - **AES**: Advanced Encryption Standard (symmetric encryption)
3544
+ - **Field-Level Encryption**: Encrypting individual fields within documents
3241
3545
 
3242
3546
  **Acronyms**:
3243
- - **DRY**: Don't Repeat Yourself
3244
- - **IAM**: Identity and Access Management
3245
- - **HSM**: Hardware Security Module
3246
- - **GDPR**: General Data Protection Regulation
3247
- - **PCI-DSS**: Payment Card Industry Data Security Standard
3248
- - **HIPAA**: Health Insurance Portability and Accountability Act
3547
+
3548
+ - **DRY**: Don't Repeat Yourself
3549
+ - **IAM**: Identity and Access Management
3550
+ - **HSM**: Hardware Security Module
3551
+ - **GDPR**: General Data Protection Regulation
3552
+ - **PCI-DSS**: Payment Card Industry Data Security Standard
3553
+ - **HIPAA**: Health Insurance Portability and Accountability Act
3249
3554
 
3250
3555
  ---
3251
3556
 
3252
3557
  ### Changelog
3253
3558
 
3254
- | Version | Date | Author | Changes |
3255
- |---------|------|--------|---------|
3256
- | 1.0 | 2025-01-13 | System | Initial documentation |
3559
+ | Version | Date | Author | Changes |
3560
+ | ------- | ---------- | ------ | --------------------- |
3561
+ | 1.0 | 2025-01-13 | System | Initial documentation |
3257
3562
 
3258
3563
  ---
3259
3564