@friggframework/core 2.0.0--canary.395.ada1d9d.0 → 2.0.0--canary.395.4af60d3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,13 +8,13 @@ This module provides **transparent field-level encryption** for sensitive data i
8
8
 
9
9
  ### Key Features
10
10
 
11
- - ✅ **Database-agnostic**: Works with MongoDB, PostgreSQL, and future databases
12
- - ✅ **Transparent**: Repositories and use cases work with plain data
13
- - ✅ **Hexagonal architecture**: Clean separation of concerns
14
- - ✅ **AWS KMS support**: Enterprise-grade encryption with AWS Key Management Service
15
- - ✅ **Local AES fallback**: Development mode using local encryption keys
16
- - ✅ **Environment-based**: Automatic bypass in dev/test/local environments
17
- - ✅ **Envelope encryption**: Secure key management pattern
11
+ - ✅ **Database-agnostic**: Works with MongoDB, PostgreSQL, and future databases
12
+ - ✅ **Transparent**: Repositories and use cases work with plain data
13
+ - ✅ **Hexagonal architecture**: Clean separation of concerns
14
+ - ✅ **AWS KMS support**: Enterprise-grade encryption with AWS Key Management Service
15
+ - ✅ **Local AES fallback**: Development mode using local encryption keys
16
+ - ✅ **Environment-based**: Automatic bypass in dev/test/local environments
17
+ - ✅ **Envelope encryption**: Secure key management pattern
18
18
 
19
19
  ## Architecture
20
20
 
@@ -49,14 +49,14 @@ Database type is configured in `backend/index.js` app definition:
49
49
  const appDefinition = {
50
50
  database: {
51
51
  mongoDB: {
52
- enable: true, // Use MongoDB
52
+ enable: true, // Use MongoDB
53
53
  },
54
54
  documentDB: {
55
- enable: false, // Use DocumentDB (MongoDB-compatible)
55
+ enable: false, // Use DocumentDB (MongoDB-compatible)
56
56
  tlsCAFile: './security/global-bundle.pem',
57
57
  },
58
58
  postgres: {
59
- enable: false, // Use PostgreSQL
59
+ enable: false, // Use PostgreSQL
60
60
  },
61
61
  },
62
62
  // ... other config
@@ -64,6 +64,7 @@ const appDefinition = {
64
64
  ```
65
65
 
66
66
  **Important**: Only enable ONE database at a time. The framework will use the first enabled database in this priority order:
67
+
67
68
  1. PostgreSQL (`postgres.enable = true`)
68
69
  2. MongoDB (`mongoDB.enable = true`)
69
70
  3. DocumentDB (`documentDB.enable = true`)
@@ -75,8 +76,8 @@ In `backend/index.js`:
75
76
  ```javascript
76
77
  const appDefinition = {
77
78
  encryption: {
78
- fieldLevelEncryptionMethod: 'kms', // or 'aes'
79
- createResourceIfNoneFound: true, // Auto-create KMS key if missing
79
+ fieldLevelEncryptionMethod: 'kms', // or 'aes'
80
+ createResourceIfNoneFound: true, // Auto-create KMS key if missing
80
81
  },
81
82
  // ... other config
82
83
  };
@@ -93,16 +94,17 @@ STAGE=production
93
94
  ```
94
95
 
95
96
  The `KMS_KEY_ARN` is usually auto-discovered by Frigg infrastructure:
96
- - Set by AWS discovery: `AWS_DISCOVERY_KMS_KEY_ARN`
97
- - Copied to `KMS_KEY_ARN` during deployment
98
97
 
99
- #### Development (Local AES)
98
+ - Set by AWS discovery: `AWS_DISCOVERY_KMS_KEY_ARN`
99
+ - Copied to `KMS_KEY_ARN` during deployment
100
+
101
+ #### AES Encryption
100
102
 
101
103
  ```bash
102
- # Local AES encryption (development/testing only)
104
+ # AES encryption (can be used in any environment including production)
103
105
  AES_KEY_ID=local-dev-key
104
106
  AES_KEY=your-32-character-secret-key-here
105
- STAGE=development # or dev, test, local
107
+ STAGE=production # or development, staging, etc.
106
108
  ```
107
109
 
108
110
  **⚠️ Important**: Encryption is automatically **disabled** when `STAGE` is set to `dev`, `test`, or `local`, regardless of key configuration.
@@ -116,7 +118,7 @@ To explicitly disable encryption:
116
118
  STAGE=development # or dev, test, local
117
119
  ```
118
120
 
119
- Or simply don't configure any encryption keys.
121
+ Or simply don't configure any encryption keys. In Production field level encryption **must** be enabled.
120
122
 
121
123
  ## Encrypted Fields
122
124
 
@@ -126,20 +128,20 @@ Fields are defined in `encryption-schema-registry.js`:
126
128
  const ENCRYPTION_SCHEMA = {
127
129
  Credential: {
128
130
  fields: [
129
- 'data.access_token', // OAuth access token
130
- 'data.refresh_token', // OAuth refresh token
131
- 'data.domain', // Service domain
132
- 'data.id_token', // OpenID Connect ID token
131
+ 'data.access_token', // OAuth access token
132
+ 'data.refresh_token', // OAuth refresh token
133
+ 'data.domain', // Service domain
134
+ 'data.id_token', // OpenID Connect ID token
133
135
  ],
134
136
  },
135
137
  IntegrationMapping: {
136
- fields: ['mapping'], // Complete mapping object
138
+ fields: ['mapping'], // Complete mapping object
137
139
  },
138
140
  User: {
139
- fields: ['hashword'], // Password hash
141
+ fields: ['hashword'], // Password hash
140
142
  },
141
143
  Token: {
142
- fields: ['token'], // Authentication token
144
+ fields: ['token'], // Authentication token
143
145
  },
144
146
  };
145
147
  ```
@@ -162,14 +164,14 @@ const appDefinition = {
162
164
  schema: {
163
165
  // Your custom models
164
166
  MyCustomModel: {
165
- fields: ['secretData', 'data.apiKey']
167
+ fields: ['secretData', 'data.apiKey'],
166
168
  },
167
169
 
168
170
  // Extend core models with additional fields
169
171
  Credential: {
170
- fields: ['data.customToken'] // Merged with core fields
171
- }
172
- }
172
+ fields: ['data.customToken'], // Merged with core fields
173
+ },
174
+ },
173
175
  },
174
176
  integrations: [MyIntegration],
175
177
  // ... rest of config
@@ -177,11 +179,12 @@ const appDefinition = {
177
179
  ```
178
180
 
179
181
  **Features:**
180
- - ✅ No framework file modifications needed
181
- - ✅ Encryption for custom Prisma models
182
- - ✅ Extends core models with additional fields
183
- - ✅ Automatic validation on startup
184
- - ✅ Protects against overriding core encrypted fields
182
+
183
+ - ✅ No framework file modifications needed
184
+ - ✅ Encryption for custom Prisma models
185
+ - ✅ Extends core models with additional fields
186
+ - ✅ Automatic validation on startup
187
+ - ✅ Protects against overriding core encrypted fields
185
188
 
186
189
  **Example with Custom Model:**
187
190
 
@@ -220,11 +223,13 @@ await prisma.asanaTaskMapping.create({
220
223
  ```
221
224
 
222
225
  **Validation:**
223
- - Invalid field paths → Error on startup with clear message
224
- - Attempting to override core fields → Error on startup
225
- - Empty/null schema → Silently ignored
226
+
227
+ - Invalid field paths → Error on startup with clear message
228
+ - Attempting to override core fields → Error on startup
229
+ - Empty/null schema → Silently ignored
226
230
 
227
231
  **Debug:**
232
+
228
233
  ```bash
229
234
  # Enable debug logging to see custom schema loading
230
235
  FRIGG_DEBUG=1 npm run frigg:start
@@ -243,7 +248,7 @@ const CORE_ENCRYPTION_SCHEMA = {
243
248
  fields: [
244
249
  'data.access_token',
245
250
  'data.refresh_token',
246
- 'data.new_core_field', // New core field
251
+ 'data.new_core_field', // New core field
247
252
  ],
248
253
  },
249
254
  };
@@ -252,13 +257,15 @@ const CORE_ENCRYPTION_SCHEMA = {
252
257
  3. Deploy - encryption applied automatically to all integrations
253
258
 
254
259
  **When to use:**
255
- - Adding encryption for new framework-level sensitive fields
256
- - Adding new core models (User, Token, etc.)
257
- - Security baseline changes affecting all integrations
260
+
261
+ - Adding encryption for new framework-level sensitive fields
262
+ - Adding new core models (User, Token, etc.)
263
+ - Security baseline changes affecting all integrations
258
264
 
259
265
  **When NOT to use:**
260
- - Integration-specific sensitive data (use custom schema instead)
261
- - Temporary/experimental encryption (use custom schema instead)
266
+
267
+ - Integration-specific sensitive data (use custom schema instead)
268
+ - Temporary/experimental encryption (use custom schema instead)
262
269
 
263
270
  ## How It Works
264
271
 
@@ -268,8 +275,8 @@ const CORE_ENCRYPTION_SCHEMA = {
268
275
  // Application code (use case or repository)
269
276
  await prisma.credential.create({
270
277
  data: {
271
- data: { access_token: 'secret123' }
272
- }
278
+ data: { access_token: 'secret123' },
279
+ },
273
280
  });
274
281
 
275
282
  // What happens:
@@ -287,7 +294,7 @@ await prisma.credential.create({
287
294
  ```javascript
288
295
  // Application code
289
296
  const credential = await prisma.credential.findUnique({
290
- where: { id: credentialId }
297
+ where: { id: credentialId },
291
298
  });
292
299
 
293
300
  // What happens:
@@ -309,10 +316,11 @@ Example: "base64KeyId:iv:ciphertext:base64EncryptedDataKey"
309
316
  ```
310
317
 
311
318
  **Why Envelope Encryption?**
312
- - Reduces KMS API calls (one DEK per field, cached)
313
- - Master key never leaves KMS
314
- - Enables key rotation without re-encrypting all data
315
- - Better performance at scale
319
+
320
+ - Reduces KMS API calls (one DEK per field, cached)
321
+ - Master key never leaves KMS
322
+ - Enables key rotation without re-encrypting all data
323
+ - Better performance at scale
316
324
 
317
325
  ### Known Limitations
318
326
 
@@ -326,7 +334,7 @@ Example: "base64KeyId:iv:ciphertext:base64EncryptedDataKey"
326
334
  // ❌ WRONG: Credential will NOT be decrypted
327
335
  const entity = await prisma.entity.findUnique({
328
336
  where: { id: entityId },
329
- include: { credential: true } // Nested credential stays encrypted!
337
+ include: { credential: true }, // Nested credential stays encrypted!
330
338
  });
331
339
 
332
340
  // entity.credential.data.access_token will be encrypted:
@@ -344,18 +352,18 @@ Always fetch relations with **separate queries**:
344
352
  ```javascript
345
353
  // ✅ CORRECT: Fetch entity and credential separately
346
354
  const entity = await prisma.entity.findUnique({
347
- where: { id: entityId }
355
+ where: { id: entityId },
348
356
  });
349
357
 
350
358
  // Separate query ensures decryption
351
359
  const credential = await prisma.credential.findUnique({
352
- where: { id: entity.credentialId }
360
+ where: { id: entity.credentialId },
353
361
  });
354
362
 
355
363
  // Combine in application layer
356
364
  return {
357
365
  ...entity,
358
- credential // Now properly decrypted
366
+ credential, // Now properly decrypted
359
367
  };
360
368
  ```
361
369
 
@@ -366,32 +374,30 @@ For fetching multiple entities with credentials, use bulk fetching to avoid N+1
366
374
  ```javascript
367
375
  // Fetch all entities
368
376
  const entities = await prisma.entity.findMany({
369
- where: { userId }
377
+ where: { userId },
370
378
  });
371
379
 
372
380
  // Bulk fetch credentials (single query)
373
- const credentialIds = entities.map(e => e.credentialId).filter(Boolean);
381
+ const credentialIds = entities.map((e) => e.credentialId).filter(Boolean);
374
382
  const credentials = await prisma.credential.findMany({
375
- where: { id: { in: credentialIds } }
383
+ where: { id: { in: credentialIds } },
376
384
  });
377
385
 
378
386
  // Create lookup map
379
- const credentialMap = new Map(
380
- credentials.map(c => [c.id, c])
381
- );
387
+ const credentialMap = new Map(credentials.map((c) => [c.id, c]));
382
388
 
383
389
  // Combine in application layer
384
- return entities.map(e => ({
390
+ return entities.map((e) => ({
385
391
  ...e,
386
- credential: credentialMap.get(e.credentialId) || null
392
+ credential: credentialMap.get(e.credentialId) || null,
387
393
  }));
388
394
  ```
389
395
 
390
396
  **Verified:**
391
397
 
392
- - ✅ `postgres-relation-decryption.test.js` - Proves the bug exists
393
- - ✅ `postgres-decryption-fix-verification.test.js` - Verifies separate queries work
394
- - ✅ `mongo-decryption-fix-verification.test.js` - Verifies fix for MongoDB
398
+ - ✅ `postgres-relation-decryption.test.js` - Proves the bug exists
399
+ - ✅ `postgres-decryption-fix-verification.test.js` - Verifies separate queries work
400
+ - ✅ `mongo-decryption-fix-verification.test.js` - Verifies fix for MongoDB
395
401
 
396
402
  **Implementation Examples:**
397
403
 
@@ -429,9 +435,9 @@ class AuthenticateUserUseCase {
429
435
  identifiers: { userId },
430
436
  details: {
431
437
  data: {
432
- access_token: accessToken // Plain text
433
- }
434
- }
438
+ access_token: accessToken, // Plain text
439
+ },
440
+ },
435
441
  });
436
442
 
437
443
  // Stored as encrypted, but we work with plain text
@@ -505,15 +511,16 @@ npm test -- database/encryption/
505
511
 
506
512
  The encryption system uses **fail-fast error handling**:
507
513
 
508
- - **Encryption failures**: Throw errors immediately (don't save corrupted/unencrypted sensitive data)
509
- - **Decryption failures**: Throw errors immediately (prevents exposing invalid data)
510
- - **Configuration errors**: Warn and disable encryption (graceful degradation for development)
511
- - **Validation errors**: Throw errors on startup (catch issues before production)
514
+ - **Encryption failures**: Throw errors immediately (don't save corrupted/unencrypted sensitive data)
515
+ - **Decryption failures**: Throw errors immediately (prevents exposing invalid data)
516
+ - **Configuration errors**: Warn and disable encryption (graceful degradation for development)
517
+ - **Validation errors**: Throw errors on startup (catch issues before production)
512
518
 
513
519
  **Why fail-fast?**
514
- - Security-critical operations must not silently fail
515
- - Better to expose issues during development than risk data breaches
516
- - Prevents inconsistent database state (partially encrypted data)
520
+
521
+ - Security-critical operations must not silently fail
522
+ - Better to expose issues during development than risk data breaches
523
+ - Prevents inconsistent database state (partially encrypted data)
517
524
 
518
525
  ### Logging Configuration
519
526
 
@@ -531,34 +538,39 @@ FRIGG_LOG_LEVEL=INFO
531
538
  ```
532
539
 
533
540
  **Log Levels:**
534
- - `DEBUG`: Detailed encryption operations (includes schema loading, key checks)
535
- - `INFO`: High-level status (encryption enabled/disabled, custom schema registration)
536
- - `WARN`: Configuration issues (missing keys, bypassed encryption)
537
- - `ERROR`: Operation failures (encryption/decryption errors)
541
+
542
+ - `DEBUG`: Detailed encryption operations (includes schema loading, key checks)
543
+ - `INFO`: High-level status (encryption enabled/disabled, custom schema registration)
544
+ - `WARN`: Configuration issues (missing keys, bypassed encryption)
545
+ - `ERROR`: Operation failures (encryption/decryption errors)
538
546
 
539
547
  **Production Safety:**
540
- - Sensitive data automatically sanitized in logs
541
- - Long base64 strings truncated (prevents key leakage)
542
- - Stack traces omitted in production (`STAGE=production`)
543
- - Key IDs never logged
548
+
549
+ - Sensitive data automatically sanitized in logs
550
+ - Long base64 strings truncated (prevents key leakage)
551
+ - Stack traces omitted in production (`STAGE=production`)
552
+ - Key IDs never logged
544
553
 
545
554
  ### Performance Optimizations
546
555
 
547
556
  **Parallel field encryption:**
548
- - Multiple fields encrypted concurrently using `Promise.all()`
549
- - Significantly faster for models with many encrypted fields
550
- - Example: 3 fields encrypted in ~30ms vs ~90ms (3x speedup)
557
+
558
+ - Multiple fields encrypted concurrently using `Promise.all()`
559
+ - Significantly faster for models with many encrypted fields
560
+ - Example: 3 fields encrypted in ~30ms vs ~90ms (3x speedup)
551
561
 
552
562
  **Deep cloning:**
553
- - Uses native `structuredClone()` on Node.js 17+ (2-5x faster)
554
- - Falls back to custom implementation for compatibility
555
- - No external dependencies required
563
+
564
+ - Uses native `structuredClone()` on Node.js 17+ (2-5x faster)
565
+ - Falls back to custom implementation for compatibility
566
+ - No external dependencies required
556
567
 
557
568
  ## Troubleshooting
558
569
 
559
570
  ### Encryption Not Working
560
571
 
561
572
  **Check environment variables:**
573
+
562
574
  ```bash
563
575
  echo $STAGE # Should be 'production' (not dev/test/local)
564
576
  echo $KMS_KEY_ARN # Should be set (for KMS)
@@ -566,6 +578,7 @@ echo $AES_KEY_ID # Should be set (for AES)
566
578
  ```
567
579
 
568
580
  **Check console logs:**
581
+
569
582
  ```
570
583
  [Frigg] Field-level encryption enabled using KMS
571
584
  ```
@@ -581,13 +594,11 @@ or
581
594
  **Error: "User is not authorized to perform: kms:GenerateDataKey"**
582
595
 
583
596
  Solution: Add KMS permissions to Lambda execution role:
597
+
584
598
  ```json
585
599
  {
586
600
  "Effect": "Allow",
587
- "Action": [
588
- "kms:GenerateDataKey",
589
- "kms:Decrypt"
590
- ],
601
+ "Action": ["kms:GenerateDataKey", "kms:Decrypt"],
591
602
  "Resource": "arn:aws:kms:*:*:key/*"
592
603
  }
593
604
  ```
@@ -595,6 +606,7 @@ Solution: Add KMS permissions to Lambda execution role:
595
606
  **Error: "KMS key not found"**
596
607
 
597
608
  Solution: Check `KMS_KEY_ARN` environment variable:
609
+
598
610
  ```bash
599
611
  aws kms describe-key --key-id $KMS_KEY_ARN
600
612
  ```
@@ -604,6 +616,7 @@ aws kms describe-key --key-id $KMS_KEY_ARN
604
616
  **Error: "No encryption key found with ID"**
605
617
 
606
618
  Solution: Set both `AES_KEY_ID` and `AES_KEY`:
619
+
607
620
  ```bash
608
621
  export AES_KEY_ID=local-dev-key
609
622
  export AES_KEY=$(openssl rand -hex 16) # Generate 32-char key
@@ -613,9 +626,9 @@ export AES_KEY=$(openssl rand -hex 16) # Generate 32-char key
613
626
 
614
627
  **Symptom: Slow queries with encryption**
615
628
 
616
- - Check KMS API throttling (CloudWatch metrics)
617
- - Consider data key caching (future enhancement)
618
- - Verify proper field selection (don't encrypt unnecessary fields)
629
+ - Check KMS API throttling (CloudWatch metrics)
630
+ - Consider data key caching (future enhancement)
631
+ - Verify proper field selection (don't encrypt unnecessary fields)
619
632
 
620
633
  ### Data Migration
621
634
 
@@ -630,7 +643,7 @@ export AES_KEY=$(openssl rand -hex 16) # Generate 32-char key
630
643
 
631
644
  ### DO
632
645
 
633
- ✅ Use AWS KMS in production
646
+ ✅ Use AWS KMS for production (recommended) or AES encryption (valid alternative)
634
647
  ✅ Rotate KMS keys regularly (AWS handles automatically)
635
648
  ✅ Restrict KMS key access to Lambda execution role only
636
649
  ✅ Use VPC endpoints for KMS (reduce NAT costs)
@@ -639,7 +652,6 @@ export AES_KEY=$(openssl rand -hex 16) # Generate 32-char key
639
652
 
640
653
  ### DON'T
641
654
 
642
- ❌ Use AES keys in production (development only)
643
655
  ❌ Store AES keys in code or git (use environment variables)
644
656
  ❌ Disable encryption in production
645
657
  ❌ Skip encryption for PII data
@@ -650,22 +662,22 @@ export AES_KEY=$(openssl rand -hex 16) # Generate 32-char key
650
662
 
651
663
  ### Planned
652
664
 
653
- - [ ] Data key caching (reduce KMS API calls)
654
- - [ ] Key rotation automation
655
- - [ ] Encryption metrics (CloudWatch)
656
- - [ ] Field-level audit logging
657
- - [ ] Support for queryable encryption (MongoDB CSFLE)
665
+ - [ ] Data key caching (reduce KMS API calls)
666
+ - [ ] Key rotation automation
667
+ - [ ] Encryption metrics (CloudWatch)
668
+ - [ ] Field-level audit logging
669
+ - [ ] Support for queryable encryption (MongoDB CSFLE)
658
670
 
659
671
  ### Under Consideration
660
672
 
661
- - [ ] Multi-region KMS replication
662
- - [ ] Client-side field level encryption
663
- - [ ] Encryption at rest + in transit
664
- - [ ] Compliance reporting (GDPR, HIPAA)
673
+ - [ ] Multi-region KMS replication
674
+ - [ ] Client-side field level encryption
675
+ - [ ] Encryption at rest + in transit
676
+ - [ ] Compliance reporting (GDPR, HIPAA)
665
677
 
666
678
  ## Related Documentation
667
679
 
668
- - [Prisma Client Extensions](https://www.prisma.io/docs/orm/prisma-client/client-extensions)
669
- - [AWS KMS Envelope Encryption](https://docs.aws.amazon.com/kms/latest/developerguide/concepts.html#enveloping)
670
- - [Frigg Infrastructure](../../../devtools/infrastructure/CLAUDE.md)
671
- - [Hexagonal Architecture](../../CLAUDE.md#dddhexagonal-architecture-patterns)
680
+ - [Prisma Client Extensions](https://www.prisma.io/docs/orm/prisma-client/client-extensions)
681
+ - [AWS KMS Envelope Encryption](https://docs.aws.amazon.com/kms/latest/developerguide/concepts.html#enveloping)
682
+ - [Frigg Infrastructure](../../../devtools/infrastructure/CLAUDE.md)
683
+ - [Hexagonal Architecture](../../CLAUDE.md#dddhexagonal-architecture-patterns)
@@ -34,7 +34,8 @@ class FieldEncryptionService {
34
34
  const value = this._getNestedValue(encrypted, fieldPath);
35
35
 
36
36
  if (this._shouldEncrypt(value)) {
37
- const encryptedValue = await this.cryptor.encrypt(String(value));
37
+ const serializedValue = this._serializeForEncryption(value);
38
+ const encryptedValue = await this.cryptor.encrypt(serializedValue);
38
39
  return { fieldPath, encryptedValue };
39
40
  }
40
41
  return null;
@@ -70,7 +71,8 @@ class FieldEncryptionService {
70
71
 
71
72
  if (this._isEncrypted(value)) {
72
73
  const decryptedValue = await this.cryptor.decrypt(value);
73
- return { fieldPath, decryptedValue };
74
+ const deserializedValue = this._deserializeAfterDecryption(decryptedValue);
75
+ return { fieldPath, decryptedValue: deserializedValue };
74
76
  }
75
77
  return null;
76
78
  });
@@ -186,6 +188,39 @@ class FieldEncryptionService {
186
188
 
187
189
  return cloned;
188
190
  }
191
+
192
+ /**
193
+ * Serialize a value for encryption
194
+ * Objects/arrays are JSON stringified, primitives are converted to strings
195
+ * @private
196
+ */
197
+ _serializeForEncryption(value) {
198
+ if (typeof value === 'object' && value !== null) {
199
+ // JSON.stringify for objects and arrays
200
+ return JSON.stringify(value);
201
+ }
202
+ // For primitives (string, number, boolean), convert to string
203
+ return String(value);
204
+ }
205
+
206
+ /**
207
+ * Deserialize a value after decryption
208
+ * Attempts to parse as JSON, returns string if parsing fails
209
+ * @private
210
+ */
211
+ _deserializeAfterDecryption(value) {
212
+ if (typeof value !== 'string') {
213
+ return value;
214
+ }
215
+
216
+ // Try to parse as JSON
217
+ try {
218
+ return JSON.parse(value);
219
+ } catch {
220
+ // Not valid JSON, return as-is (likely was a plain string field)
221
+ return value;
222
+ }
223
+ }
189
224
  }
190
225
 
191
226
  module.exports = { FieldEncryptionService };
@@ -16,8 +16,25 @@ describe('FieldEncryptionService', () => {
16
16
  decrypt: jest
17
17
  .fn()
18
18
  .mockImplementation((value) => {
19
- const parts = value.split(':');
20
- return parts[1]; // Return original value
19
+ // Handle multiple encrypted formats
20
+ // Format 1: "encrypted:ORIGINAL:keydata:enckey"
21
+ // Format 2: "keyId:ORIGINAL:iv:enckey"
22
+
23
+ // Try format 1 (from our new tests)
24
+ const prefix1 = 'encrypted:';
25
+ const suffix1 = ':keydata:enckey';
26
+ if (value.startsWith(prefix1) && value.endsWith(suffix1)) {
27
+ return value.slice(prefix1.length, -suffix1.length);
28
+ }
29
+
30
+ // Try format 2 (from existing tests)
31
+ const prefix2 = 'keyId:';
32
+ const suffix2 = ':iv:enckey';
33
+ if (value.startsWith(prefix2) && value.endsWith(suffix2)) {
34
+ return value.slice(prefix2.length, -suffix2.length);
35
+ }
36
+
37
+ return value; // Fallback for non-standard format
21
38
  }),
22
39
  };
23
40
 
@@ -27,6 +44,7 @@ describe('FieldEncryptionService', () => {
27
44
  const schemas = {
28
45
  Credential: ['data.access_token', 'data.refresh_token'],
29
46
  User: ['hashword'],
47
+ IntegrationMapping: ['mapping'],
30
48
  EmptyModel: [],
31
49
  };
32
50
  return schemas[modelName] || [];
@@ -183,6 +201,48 @@ describe('FieldEncryptionService', () => {
183
201
  expect(document).toEqual(original);
184
202
  });
185
203
 
204
+ it('should properly encrypt object/JSON values (IntegrationMapping.mapping)', async () => {
205
+ // This test demonstrates the bug: objects are converted to "[object Object]"
206
+ // Expected behavior: object should be JSON.stringify'd before encryption
207
+ const mappingObject = {
208
+ action: 'upload',
209
+ formData: {
210
+ container: 'project_123',
211
+ folderId: '456',
212
+ attachments: ['att-1', 'att-2'],
213
+ },
214
+ taskId: 'task-789',
215
+ status: 'pending',
216
+ };
217
+
218
+ const document = {
219
+ id: 1,
220
+ integrationId: 1,
221
+ sourceId: 'task-789',
222
+ mapping: mappingObject,
223
+ };
224
+
225
+ const encrypted = await service.encryptFields('IntegrationMapping', document);
226
+
227
+ // The cryptor should receive JSON string, not "[object Object]"
228
+ expect(mockCryptor.encrypt).toHaveBeenCalledWith(
229
+ JSON.stringify(mappingObject)
230
+ );
231
+
232
+ // The encrypted value should be the JSON string encrypted
233
+ expect(encrypted.mapping).toBe(
234
+ `encrypted:${JSON.stringify(mappingObject)}:keydata:enckey`
235
+ );
236
+
237
+ // Now decrypt and verify object is restored
238
+ const decrypted = await service.decryptFields('IntegrationMapping', encrypted);
239
+
240
+ // After decryption, the object should be fully restored
241
+ expect(decrypted.mapping).toEqual(mappingObject);
242
+ expect(decrypted.mapping.action).toBe('upload');
243
+ expect(decrypted.mapping.formData.attachments).toEqual(['att-1', 'att-2']);
244
+ });
245
+
186
246
  it('should throw on encryption errors', async () => {
187
247
  mockCryptor.encrypt.mockRejectedValueOnce(
188
248
  new Error('Encryption failed')
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@friggframework/core",
3
3
  "prettier": "@friggframework/prettier-config",
4
- "version": "2.0.0--canary.395.ada1d9d.0",
4
+ "version": "2.0.0--canary.395.4af60d3.0",
5
5
  "dependencies": {
6
6
  "@hapi/boom": "^10.0.1",
7
7
  "@prisma/client": "^6.16.3",
@@ -23,9 +23,9 @@
23
23
  "uuid": "^9.0.1"
24
24
  },
25
25
  "devDependencies": {
26
- "@friggframework/eslint-config": "2.0.0--canary.395.ada1d9d.0",
27
- "@friggframework/prettier-config": "2.0.0--canary.395.ada1d9d.0",
28
- "@friggframework/test": "2.0.0--canary.395.ada1d9d.0",
26
+ "@friggframework/eslint-config": "2.0.0--canary.395.4af60d3.0",
27
+ "@friggframework/prettier-config": "2.0.0--canary.395.4af60d3.0",
28
+ "@friggframework/test": "2.0.0--canary.395.4af60d3.0",
29
29
  "@types/lodash": "4.17.15",
30
30
  "@typescript-eslint/eslint-plugin": "^8.0.0",
31
31
  "chai": "^4.3.6",
@@ -64,5 +64,5 @@
64
64
  "publishConfig": {
65
65
  "access": "public"
66
66
  },
67
- "gitHead": "ada1d9dff6aa44abc366cf9b1c5a2b76b1bb9d6c"
67
+ "gitHead": "4af60d3d54fa6e29cb5e1693beff3d987a206c7c"
68
68
  }