@friggframework/core 2.0.0-next.41 → 2.0.0-next.43

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. package/CLAUDE.md +693 -0
  2. package/README.md +931 -50
  3. package/application/commands/README.md +421 -0
  4. package/application/commands/credential-commands.js +224 -0
  5. package/application/commands/entity-commands.js +315 -0
  6. package/application/commands/integration-commands.js +160 -0
  7. package/application/commands/integration-commands.test.js +123 -0
  8. package/application/commands/user-commands.js +213 -0
  9. package/application/index.js +69 -0
  10. package/core/CLAUDE.md +690 -0
  11. package/core/create-handler.js +0 -6
  12. package/credential/repositories/credential-repository-factory.js +47 -0
  13. package/credential/repositories/credential-repository-interface.js +98 -0
  14. package/credential/repositories/credential-repository-mongo.js +301 -0
  15. package/credential/repositories/credential-repository-postgres.js +307 -0
  16. package/credential/repositories/credential-repository.js +307 -0
  17. package/credential/use-cases/get-credential-for-user.js +21 -0
  18. package/credential/use-cases/update-authentication-status.js +15 -0
  19. package/database/config.js +117 -0
  20. package/database/encryption/README.md +683 -0
  21. package/database/encryption/encryption-integration.test.js +553 -0
  22. package/database/encryption/encryption-schema-registry.js +141 -0
  23. package/database/encryption/encryption-schema-registry.test.js +392 -0
  24. package/database/encryption/field-encryption-service.js +226 -0
  25. package/database/encryption/field-encryption-service.test.js +525 -0
  26. package/database/encryption/logger.js +79 -0
  27. package/database/encryption/mongo-decryption-fix-verification.test.js +348 -0
  28. package/database/encryption/postgres-decryption-fix-verification.test.js +371 -0
  29. package/database/encryption/postgres-relation-decryption.test.js +245 -0
  30. package/database/encryption/prisma-encryption-extension.js +222 -0
  31. package/database/encryption/prisma-encryption-extension.test.js +439 -0
  32. package/database/index.js +25 -12
  33. package/database/models/readme.md +1 -0
  34. package/database/prisma.js +162 -0
  35. package/database/repositories/health-check-repository-factory.js +38 -0
  36. package/database/repositories/health-check-repository-interface.js +86 -0
  37. package/database/repositories/health-check-repository-mongodb.js +72 -0
  38. package/database/repositories/health-check-repository-postgres.js +75 -0
  39. package/database/repositories/health-check-repository.js +108 -0
  40. package/database/use-cases/check-database-health-use-case.js +34 -0
  41. package/database/use-cases/check-encryption-health-use-case.js +82 -0
  42. package/database/use-cases/test-encryption-use-case.js +252 -0
  43. package/encrypt/Cryptor.js +20 -152
  44. package/encrypt/index.js +1 -2
  45. package/encrypt/test-encrypt.js +0 -2
  46. package/handlers/app-definition-loader.js +38 -0
  47. package/handlers/app-handler-helpers.js +0 -3
  48. package/handlers/auth-flow.integration.test.js +147 -0
  49. package/handlers/backend-utils.js +25 -45
  50. package/handlers/integration-event-dispatcher.js +54 -0
  51. package/handlers/integration-event-dispatcher.test.js +141 -0
  52. package/handlers/routers/HEALTHCHECK.md +103 -1
  53. package/handlers/routers/auth.js +3 -14
  54. package/handlers/routers/health.js +63 -424
  55. package/handlers/routers/health.test.js +7 -0
  56. package/handlers/routers/integration-defined-routers.js +8 -5
  57. package/handlers/routers/user.js +27 -5
  58. package/handlers/routers/websocket.js +5 -3
  59. package/handlers/use-cases/check-external-apis-health-use-case.js +81 -0
  60. package/handlers/use-cases/check-integrations-health-use-case.js +32 -0
  61. package/handlers/workers/integration-defined-workers.js +6 -3
  62. package/index.js +45 -22
  63. package/integrations/index.js +12 -10
  64. package/integrations/integration-base.js +224 -53
  65. package/integrations/integration-router.js +386 -178
  66. package/integrations/options.js +1 -1
  67. package/integrations/repositories/integration-mapping-repository-factory.js +50 -0
  68. package/integrations/repositories/integration-mapping-repository-interface.js +106 -0
  69. package/integrations/repositories/integration-mapping-repository-mongo.js +161 -0
  70. package/integrations/repositories/integration-mapping-repository-postgres.js +227 -0
  71. package/integrations/repositories/integration-mapping-repository.js +156 -0
  72. package/integrations/repositories/integration-repository-factory.js +44 -0
  73. package/integrations/repositories/integration-repository-interface.js +115 -0
  74. package/integrations/repositories/integration-repository-mongo.js +271 -0
  75. package/integrations/repositories/integration-repository-postgres.js +319 -0
  76. package/integrations/tests/doubles/dummy-integration-class.js +90 -0
  77. package/integrations/tests/doubles/test-integration-repository.js +99 -0
  78. package/integrations/tests/use-cases/create-integration.test.js +131 -0
  79. package/integrations/tests/use-cases/delete-integration-for-user.test.js +150 -0
  80. package/integrations/tests/use-cases/find-integration-context-by-external-entity-id.test.js +92 -0
  81. package/integrations/tests/use-cases/get-integration-for-user.test.js +150 -0
  82. package/integrations/tests/use-cases/get-integration-instance.test.js +176 -0
  83. package/integrations/tests/use-cases/get-integrations-for-user.test.js +176 -0
  84. package/integrations/tests/use-cases/get-possible-integrations.test.js +188 -0
  85. package/integrations/tests/use-cases/update-integration-messages.test.js +142 -0
  86. package/integrations/tests/use-cases/update-integration-status.test.js +103 -0
  87. package/integrations/tests/use-cases/update-integration.test.js +141 -0
  88. package/integrations/use-cases/create-integration.js +83 -0
  89. package/integrations/use-cases/delete-integration-for-user.js +73 -0
  90. package/integrations/use-cases/find-integration-context-by-external-entity-id.js +72 -0
  91. package/integrations/use-cases/get-integration-for-user.js +78 -0
  92. package/integrations/use-cases/get-integration-instance-by-definition.js +67 -0
  93. package/integrations/use-cases/get-integration-instance.js +83 -0
  94. package/integrations/use-cases/get-integrations-for-user.js +87 -0
  95. package/integrations/use-cases/get-possible-integrations.js +27 -0
  96. package/integrations/use-cases/index.js +11 -0
  97. package/integrations/use-cases/load-integration-context-full.test.js +329 -0
  98. package/integrations/use-cases/load-integration-context.js +71 -0
  99. package/integrations/use-cases/load-integration-context.test.js +114 -0
  100. package/integrations/use-cases/update-integration-messages.js +44 -0
  101. package/integrations/use-cases/update-integration-status.js +32 -0
  102. package/integrations/use-cases/update-integration.js +93 -0
  103. package/integrations/utils/map-integration-dto.js +36 -0
  104. package/jest-global-setup-noop.js +3 -0
  105. package/jest-global-teardown-noop.js +3 -0
  106. package/{module-plugin → modules}/entity.js +1 -0
  107. package/{module-plugin → modules}/index.js +0 -8
  108. package/modules/module-factory.js +56 -0
  109. package/modules/module-hydration.test.js +205 -0
  110. package/modules/module.js +221 -0
  111. package/modules/repositories/module-repository-factory.js +33 -0
  112. package/modules/repositories/module-repository-interface.js +129 -0
  113. package/modules/repositories/module-repository-mongo.js +386 -0
  114. package/modules/repositories/module-repository-postgres.js +437 -0
  115. package/modules/repositories/module-repository.js +327 -0
  116. package/{module-plugin → modules}/test/mock-api/api.js +8 -3
  117. package/{module-plugin → modules}/test/mock-api/definition.js +12 -8
  118. package/modules/tests/doubles/test-module-factory.js +16 -0
  119. package/modules/tests/doubles/test-module-repository.js +39 -0
  120. package/modules/use-cases/get-entities-for-user.js +32 -0
  121. package/modules/use-cases/get-entity-options-by-id.js +59 -0
  122. package/modules/use-cases/get-entity-options-by-type.js +34 -0
  123. package/modules/use-cases/get-module-instance-from-type.js +31 -0
  124. package/modules/use-cases/get-module.js +56 -0
  125. package/modules/use-cases/process-authorization-callback.js +122 -0
  126. package/modules/use-cases/refresh-entity-options.js +59 -0
  127. package/modules/use-cases/test-module-auth.js +55 -0
  128. package/modules/utils/map-module-dto.js +18 -0
  129. package/package.json +14 -6
  130. package/prisma-mongodb/schema.prisma +318 -0
  131. package/prisma-postgresql/migrations/20250930193005_init/migration.sql +315 -0
  132. package/prisma-postgresql/migrations/20251006135218_init/migration.sql +9 -0
  133. package/prisma-postgresql/migrations/20251010000000_remove_unused_entity_reference_map/migration.sql +3 -0
  134. package/prisma-postgresql/migrations/migration_lock.toml +3 -0
  135. package/prisma-postgresql/schema.prisma +300 -0
  136. package/syncs/manager.js +468 -443
  137. package/syncs/repositories/sync-repository-factory.js +38 -0
  138. package/syncs/repositories/sync-repository-interface.js +109 -0
  139. package/syncs/repositories/sync-repository-mongo.js +239 -0
  140. package/syncs/repositories/sync-repository-postgres.js +319 -0
  141. package/syncs/sync.js +0 -1
  142. package/token/repositories/token-repository-factory.js +33 -0
  143. package/token/repositories/token-repository-interface.js +131 -0
  144. package/token/repositories/token-repository-mongo.js +212 -0
  145. package/token/repositories/token-repository-postgres.js +257 -0
  146. package/token/repositories/token-repository.js +219 -0
  147. package/types/integrations/index.d.ts +2 -6
  148. package/types/module-plugin/index.d.ts +5 -57
  149. package/types/syncs/index.d.ts +0 -2
  150. package/user/repositories/user-repository-factory.js +46 -0
  151. package/user/repositories/user-repository-interface.js +198 -0
  152. package/user/repositories/user-repository-mongo.js +250 -0
  153. package/user/repositories/user-repository-postgres.js +311 -0
  154. package/user/tests/doubles/test-user-repository.js +72 -0
  155. package/user/tests/use-cases/create-individual-user.test.js +24 -0
  156. package/user/tests/use-cases/create-organization-user.test.js +28 -0
  157. package/user/tests/use-cases/create-token-for-user-id.test.js +19 -0
  158. package/user/tests/use-cases/get-user-from-bearer-token.test.js +64 -0
  159. package/user/tests/use-cases/login-user.test.js +140 -0
  160. package/user/use-cases/create-individual-user.js +61 -0
  161. package/user/use-cases/create-organization-user.js +47 -0
  162. package/user/use-cases/create-token-for-user-id.js +30 -0
  163. package/user/use-cases/get-user-from-bearer-token.js +77 -0
  164. package/user/use-cases/login-user.js +122 -0
  165. package/user/user.js +77 -0
  166. package/websocket/repositories/websocket-connection-repository-factory.js +37 -0
  167. package/websocket/repositories/websocket-connection-repository-interface.js +106 -0
  168. package/websocket/repositories/websocket-connection-repository-mongo.js +155 -0
  169. package/websocket/repositories/websocket-connection-repository-postgres.js +195 -0
  170. package/websocket/repositories/websocket-connection-repository.js +160 -0
  171. package/database/models/State.js +0 -9
  172. package/database/models/Token.js +0 -70
  173. package/database/mongo.js +0 -171
  174. package/encrypt/Cryptor.test.js +0 -32
  175. package/encrypt/encrypt.js +0 -104
  176. package/encrypt/encrypt.test.js +0 -1069
  177. package/handlers/routers/middleware/loadUser.js +0 -15
  178. package/handlers/routers/middleware/requireLoggedInUser.js +0 -12
  179. package/integrations/create-frigg-backend.js +0 -31
  180. package/integrations/integration-factory.js +0 -251
  181. package/integrations/integration-mapping.js +0 -43
  182. package/integrations/integration-model.js +0 -46
  183. package/integrations/integration-user.js +0 -144
  184. package/integrations/test/integration-base.test.js +0 -144
  185. package/module-plugin/auther.js +0 -393
  186. package/module-plugin/credential.js +0 -22
  187. package/module-plugin/entity-manager.js +0 -70
  188. package/module-plugin/manager.js +0 -169
  189. package/module-plugin/module-factory.js +0 -61
  190. package/module-plugin/test/auther.test.js +0 -97
  191. /package/{module-plugin → modules}/ModuleConstants.js +0 -0
  192. /package/{module-plugin → modules}/requester/api-key.js +0 -0
  193. /package/{module-plugin → modules}/requester/basic.js +0 -0
  194. /package/{module-plugin → modules}/requester/oauth-2.js +0 -0
  195. /package/{module-plugin → modules}/requester/requester.js +0 -0
  196. /package/{module-plugin → modules}/requester/requester.test.js +0 -0
  197. /package/{module-plugin → modules}/test/mock-api/mocks/hubspot.js +0 -0
@@ -0,0 +1,683 @@
1
+ # Frigg Field-Level Encryption
2
+
3
+ Database-agnostic field-level encryption for Frigg using Prisma Client Extensions and AWS KMS/AES.
4
+
5
+ ## Overview
6
+
7
+ This module provides **transparent field-level encryption** for sensitive data in Frigg integrations. It works identically for MongoDB and PostgreSQL, using Prisma Client Extensions to automatically encrypt data on write and decrypt on read.
8
+
9
+ ### Key Features
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
18
+
19
+ ## Architecture
20
+
21
+ ### Hexagonal Layers
22
+
23
+ ```
24
+ Application Layer (Use Cases)
25
+ ↓ works with plain data
26
+ Infrastructure Layer (Repositories)
27
+ ↓ works with plain data
28
+ Infrastructure Layer (Prisma Extension)
29
+ ↓ transparent encrypt/decrypt
30
+ Infrastructure Layer (Cryptor)
31
+ ↓ calls AWS KMS or crypto library
32
+ External Systems (AWS KMS, Database)
33
+ ```
34
+
35
+ ### Components
36
+
37
+ 1. **encryption-schema-registry.js** - Defines which fields are encrypted
38
+ 2. **field-encryption-service.js** - Orchestrates field-level encryption
39
+ 3. **prisma-encryption-extension.js** - Prisma Client Extension for transparent encryption
40
+ 4. **Cryptor.js** (`../encrypt/`) - Adapter for AWS KMS and AES encryption
41
+
42
+ ## Configuration
43
+
44
+ ### Database Selection
45
+
46
+ Database type is configured in `backend/index.js` app definition:
47
+
48
+ ```javascript
49
+ const appDefinition = {
50
+ database: {
51
+ mongoDB: {
52
+ enable: true, // Use MongoDB
53
+ },
54
+ documentDB: {
55
+ enable: false, // Use DocumentDB (MongoDB-compatible)
56
+ tlsCAFile: './security/global-bundle.pem',
57
+ },
58
+ postgres: {
59
+ enable: false, // Use PostgreSQL
60
+ },
61
+ },
62
+ // ... other config
63
+ };
64
+ ```
65
+
66
+ **Important**: Only enable ONE database at a time. The framework will use the first enabled database in this priority order:
67
+
68
+ 1. PostgreSQL (`postgres.enable = true`)
69
+ 2. MongoDB (`mongoDB.enable = true`)
70
+ 3. DocumentDB (`documentDB.enable = true`)
71
+
72
+ ### Encryption Configuration
73
+
74
+ In `backend/index.js`:
75
+
76
+ ```javascript
77
+ const appDefinition = {
78
+ encryption: {
79
+ fieldLevelEncryptionMethod: 'kms', // or 'aes'
80
+ createResourceIfNoneFound: true, // Auto-create KMS key if missing
81
+ },
82
+ // ... other config
83
+ };
84
+ ```
85
+
86
+ ### Environment Variables
87
+
88
+ #### Production (AWS KMS)
89
+
90
+ ```bash
91
+ # AWS KMS encryption (recommended for production)
92
+ KMS_KEY_ARN=arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012
93
+ STAGE=production
94
+ ```
95
+
96
+ The `KMS_KEY_ARN` is usually auto-discovered by Frigg infrastructure:
97
+
98
+ - Set by AWS discovery: `AWS_DISCOVERY_KMS_KEY_ARN`
99
+ - Copied to `KMS_KEY_ARN` during deployment
100
+
101
+ #### AES Encryption
102
+
103
+ ```bash
104
+ # AES encryption (can be used in any environment including production)
105
+ AES_KEY_ID=local-dev-key
106
+ AES_KEY=your-32-character-secret-key-here
107
+ STAGE=production # or development, staging, etc.
108
+ ```
109
+
110
+ **⚠️ Important**: Encryption is automatically **disabled** when `STAGE` is set to `dev`, `test`, or `local`, regardless of key configuration.
111
+
112
+ ### Bypass Encryption
113
+
114
+ To explicitly disable encryption:
115
+
116
+ ```bash
117
+ # Disable encryption (development only)
118
+ STAGE=development # or dev, test, local
119
+ ```
120
+
121
+ Or simply don't configure any encryption keys. In Production field level encryption **must** be enabled.
122
+
123
+ ## Encrypted Fields
124
+
125
+ Fields are defined in `encryption-schema-registry.js`:
126
+
127
+ ```javascript
128
+ const ENCRYPTION_SCHEMA = {
129
+ Credential: {
130
+ fields: [
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
135
+ ],
136
+ },
137
+ IntegrationMapping: {
138
+ fields: ['mapping'], // Complete mapping object
139
+ },
140
+ User: {
141
+ fields: ['hashword'], // Password hash
142
+ },
143
+ Token: {
144
+ fields: ['token'], // Authentication token
145
+ },
146
+ };
147
+ ```
148
+
149
+ ### Extending Encryption Schema
150
+
151
+ #### Recommended: Custom Schema via appDefinition (Integration Developers)
152
+
153
+ Integration developers can extend encryption without modifying core framework files:
154
+
155
+ **In `backend/index.js`:**
156
+
157
+ ```javascript
158
+ const appDefinition = {
159
+ encryption: {
160
+ fieldLevelEncryptionMethod: 'kms',
161
+ createResourceIfNoneFound: true,
162
+
163
+ // Custom encryption schema
164
+ schema: {
165
+ // Your custom models
166
+ MyCustomModel: {
167
+ fields: ['secretData', 'data.apiKey'],
168
+ },
169
+
170
+ // Extend core models with additional fields
171
+ Credential: {
172
+ fields: ['data.customToken'], // Merged with core fields
173
+ },
174
+ },
175
+ },
176
+ integrations: [MyIntegration],
177
+ // ... rest of config
178
+ };
179
+ ```
180
+
181
+ **Features:**
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
188
+
189
+ **Example with Custom Model:**
190
+
191
+ ```javascript
192
+ // 1. Define custom Prisma model (in your backend prisma schema)
193
+ model AsanaTaskMapping {
194
+ id Int @id @default(autoincrement())
195
+ taskGid String
196
+ webhookToken String // Sensitive!
197
+ customApiSecret String // Sensitive!
198
+ metadata Json
199
+ }
200
+
201
+ // 2. Add to encryption schema in backend/index.js
202
+ const appDefinition = {
203
+ encryption: {
204
+ fieldLevelEncryptionMethod: 'kms',
205
+ schema: {
206
+ AsanaTaskMapping: {
207
+ fields: [
208
+ 'webhookToken',
209
+ 'customApiSecret'
210
+ ]
211
+ }
212
+ }
213
+ }
214
+ };
215
+
216
+ // 3. Use normally in your repositories - encryption is automatic!
217
+ await prisma.asanaTaskMapping.create({
218
+ data: {
219
+ webhookToken: 'secret123', // Auto-encrypted
220
+ customApiSecret: 'api-key' // Auto-encrypted
221
+ }
222
+ });
223
+ ```
224
+
225
+ **Validation:**
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
230
+
231
+ **Debug:**
232
+
233
+ ```bash
234
+ # Enable debug logging to see custom schema loading
235
+ FRIGG_DEBUG=1 npm run frigg:start
236
+ ```
237
+
238
+ #### Advanced: Modifying Core Schema (Framework Developers)
239
+
240
+ Framework developers maintaining core models can modify `encryption-schema-registry.js`:
241
+
242
+ 1. Open `encryption-schema-registry.js`
243
+ 2. Add field to `CORE_ENCRYPTION_SCHEMA`:
244
+
245
+ ```javascript
246
+ const CORE_ENCRYPTION_SCHEMA = {
247
+ Credential: {
248
+ fields: [
249
+ 'data.access_token',
250
+ 'data.refresh_token',
251
+ 'data.new_core_field', // New core field
252
+ ],
253
+ },
254
+ };
255
+ ```
256
+
257
+ 3. Deploy - encryption applied automatically to all integrations
258
+
259
+ **When to use:**
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
264
+
265
+ **When NOT to use:**
266
+
267
+ - Integration-specific sensitive data (use custom schema instead)
268
+ - Temporary/experimental encryption (use custom schema instead)
269
+
270
+ ## How It Works
271
+
272
+ ### Write Operation (Create/Update)
273
+
274
+ ```javascript
275
+ // Application code (use case or repository)
276
+ await prisma.credential.create({
277
+ data: {
278
+ data: { access_token: 'secret123' },
279
+ },
280
+ });
281
+
282
+ // What happens:
283
+ // 1. Prisma extension intercepts query
284
+ // 2. FieldEncryptionService encrypts matching fields
285
+ // 3. Cryptor generates data key via KMS
286
+ // 4. Cryptor encrypts value with data key
287
+ // 5. Database stores: { data: { access_token: 'keyId:iv:cipher:encKey' }}
288
+ // 6. Extension decrypts return value
289
+ // 7. Application receives: { data: { access_token: 'secret123' }}
290
+ ```
291
+
292
+ ### Read Operation (Find)
293
+
294
+ ```javascript
295
+ // Application code
296
+ const credential = await prisma.credential.findUnique({
297
+ where: { id: credentialId },
298
+ });
299
+
300
+ // What happens:
301
+ // 1. Prisma queries database
302
+ // 2. Database returns encrypted data
303
+ // 3. Extension intercepts result
304
+ // 4. FieldEncryptionService decrypts matching fields
305
+ // 5. Cryptor decrypts with KMS
306
+ // 6. Application receives plain data
307
+ ```
308
+
309
+ ### Encryption Format
310
+
311
+ Encrypted values use **envelope encryption**:
312
+
313
+ ```
314
+ Format: "keyId:encryptedText:encryptedKey"
315
+ Example: "base64KeyId:iv:ciphertext:base64EncryptedDataKey"
316
+ ```
317
+
318
+ **Why Envelope Encryption?**
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
324
+
325
+ ### Known Limitations
326
+
327
+ #### Prisma Relations with `include` Bypass Decryption
328
+
329
+ **⚠️ Critical**: When using Prisma's `include` option to fetch related models, the encryption extension **cannot decrypt** nested relation data.
330
+
331
+ **Problem:**
332
+
333
+ ```javascript
334
+ // ❌ WRONG: Credential will NOT be decrypted
335
+ const entity = await prisma.entity.findUnique({
336
+ where: { id: entityId },
337
+ include: { credential: true }, // Nested credential stays encrypted!
338
+ });
339
+
340
+ // entity.credential.data.access_token will be encrypted:
341
+ // "keyId:iv:ciphertext:encKey" instead of plain text
342
+ ```
343
+
344
+ **Root Cause:**
345
+
346
+ The Prisma encryption extension hooks into top-level model queries via `$allModels`. When you use `include`, Prisma internally fetches the nested relation, but the extension only sees the parent model name (`Entity`), not the nested model (`Credential`). Therefore, the `Credential` data bypasses the decryption logic.
347
+
348
+ **Solution:**
349
+
350
+ Always fetch relations with **separate queries**:
351
+
352
+ ```javascript
353
+ // ✅ CORRECT: Fetch entity and credential separately
354
+ const entity = await prisma.entity.findUnique({
355
+ where: { id: entityId },
356
+ });
357
+
358
+ // Separate query ensures decryption
359
+ const credential = await prisma.credential.findUnique({
360
+ where: { id: entity.credentialId },
361
+ });
362
+
363
+ // Combine in application layer
364
+ return {
365
+ ...entity,
366
+ credential, // Now properly decrypted
367
+ };
368
+ ```
369
+
370
+ **Best Practice (Bulk Operations):**
371
+
372
+ For fetching multiple entities with credentials, use bulk fetching to avoid N+1 queries:
373
+
374
+ ```javascript
375
+ // Fetch all entities
376
+ const entities = await prisma.entity.findMany({
377
+ where: { userId },
378
+ });
379
+
380
+ // Bulk fetch credentials (single query)
381
+ const credentialIds = entities.map((e) => e.credentialId).filter(Boolean);
382
+ const credentials = await prisma.credential.findMany({
383
+ where: { id: { in: credentialIds } },
384
+ });
385
+
386
+ // Create lookup map
387
+ const credentialMap = new Map(credentials.map((c) => [c.id, c]));
388
+
389
+ // Combine in application layer
390
+ return entities.map((e) => ({
391
+ ...e,
392
+ credential: credentialMap.get(e.credentialId) || null,
393
+ }));
394
+ ```
395
+
396
+ **Verified:**
397
+
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
401
+
402
+ **Implementation Examples:**
403
+
404
+ See `modules/repositories/module-repository-postgres.js` and `module-repository-mongo.js` for complete implementation examples using `_fetchCredential()` and `_fetchCredentialsBulk()` helper methods.
405
+
406
+ ## Usage Examples
407
+
408
+ ### Repository Code (No Changes Needed!)
409
+
410
+ ```javascript
411
+ // Repositories work with plain data - encryption is transparent
412
+ class CredentialRepository {
413
+ async upsertCredential({ identifiers, details }) {
414
+ // details.data.access_token is plain text here
415
+ const credential = await prisma.credential.upsert({
416
+ where: identifiers,
417
+ create: details,
418
+ update: details,
419
+ });
420
+
421
+ // credential.data.access_token is plain text here (auto-decrypted)
422
+ return credential;
423
+ }
424
+ }
425
+ ```
426
+
427
+ ### Use Case Code (No Changes Needed!)
428
+
429
+ ```javascript
430
+ // Use cases work with plain data - encryption is transparent
431
+ class AuthenticateUserUseCase {
432
+ async execute({ userId, accessToken }) {
433
+ // accessToken is plain text
434
+ await this.credentialRepo.upsertCredential({
435
+ identifiers: { userId },
436
+ details: {
437
+ data: {
438
+ access_token: accessToken, // Plain text
439
+ },
440
+ },
441
+ });
442
+
443
+ // Stored as encrypted, but we work with plain text
444
+ }
445
+ }
446
+ ```
447
+
448
+ ### Testing Encryption
449
+
450
+ Use the health check endpoint to verify encryption:
451
+
452
+ ```bash
453
+ # Check if encryption is working
454
+ curl http://localhost:3000/health/test-encryption
455
+
456
+ # Response when encryption enabled:
457
+ {
458
+ "status": "enabled",
459
+ "testResult": "Encryption and decryption verified successfully",
460
+ "encryptionWorks": true
461
+ }
462
+
463
+ # Response when encryption disabled:
464
+ {
465
+ "status": "disabled",
466
+ "reason": "Encryption bypassed for stage: development"
467
+ }
468
+ ```
469
+
470
+ ## Testing
471
+
472
+ ### Unit Tests
473
+
474
+ ```bash
475
+ # Test encryption schema registry
476
+ npm test -- database/encryption/encryption-schema-registry.test.js
477
+
478
+ # Test field encryption service
479
+ npm test -- database/encryption/field-encryption-service.test.js
480
+
481
+ # Test Prisma extension
482
+ npm test -- database/encryption/prisma-encryption-extension.test.js
483
+
484
+ # Test all encryption
485
+ npm test -- database/encryption/
486
+ ```
487
+
488
+ ### Integration Tests
489
+
490
+ Database type is determined from your app definition in `backend/index.js`:
491
+
492
+ ```javascript
493
+ // backend/index.js
494
+ database: {
495
+ mongoDB: { enable: true }, // For MongoDB tests
496
+ postgres: { enable: false }
497
+ }
498
+ ```
499
+
500
+ ```bash
501
+ # Run encryption tests
502
+ npm test -- database/encryption/
503
+
504
+ # Tests use explicit database type parameter for testing:
505
+ # createHealthCheckRepository('mongodb')
506
+ ```
507
+
508
+ ## Error Handling & Logging
509
+
510
+ ### Error Handling Strategy
511
+
512
+ The encryption system uses **fail-fast error handling**:
513
+
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)
518
+
519
+ **Why fail-fast?**
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)
524
+
525
+ ### Logging Configuration
526
+
527
+ Configure log verbosity with `FRIGG_LOG_LEVEL`:
528
+
529
+ ```bash
530
+ # Production (minimal logging)
531
+ FRIGG_LOG_LEVEL=WARN
532
+
533
+ # Development (detailed logging)
534
+ FRIGG_LOG_LEVEL=DEBUG
535
+
536
+ # Default
537
+ FRIGG_LOG_LEVEL=INFO
538
+ ```
539
+
540
+ **Log Levels:**
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)
546
+
547
+ **Production Safety:**
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
553
+
554
+ ### Performance Optimizations
555
+
556
+ **Parallel field encryption:**
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)
561
+
562
+ **Deep cloning:**
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
567
+
568
+ ## Troubleshooting
569
+
570
+ ### Encryption Not Working
571
+
572
+ **Check environment variables:**
573
+
574
+ ```bash
575
+ echo $STAGE # Should be 'production' (not dev/test/local)
576
+ echo $KMS_KEY_ARN # Should be set (for KMS)
577
+ echo $AES_KEY_ID # Should be set (for AES)
578
+ ```
579
+
580
+ **Check console logs:**
581
+
582
+ ```
583
+ [Frigg] Field-level encryption enabled using KMS
584
+ ```
585
+
586
+ or
587
+
588
+ ```
589
+ [Frigg] Field-level encryption disabled
590
+ ```
591
+
592
+ ### AWS KMS Errors
593
+
594
+ **Error: "User is not authorized to perform: kms:GenerateDataKey"**
595
+
596
+ Solution: Add KMS permissions to Lambda execution role:
597
+
598
+ ```json
599
+ {
600
+ "Effect": "Allow",
601
+ "Action": ["kms:GenerateDataKey", "kms:Decrypt"],
602
+ "Resource": "arn:aws:kms:*:*:key/*"
603
+ }
604
+ ```
605
+
606
+ **Error: "KMS key not found"**
607
+
608
+ Solution: Check `KMS_KEY_ARN` environment variable:
609
+
610
+ ```bash
611
+ aws kms describe-key --key-id $KMS_KEY_ARN
612
+ ```
613
+
614
+ ### Local AES Errors
615
+
616
+ **Error: "No encryption key found with ID"**
617
+
618
+ Solution: Set both `AES_KEY_ID` and `AES_KEY`:
619
+
620
+ ```bash
621
+ export AES_KEY_ID=local-dev-key
622
+ export AES_KEY=$(openssl rand -hex 16) # Generate 32-char key
623
+ ```
624
+
625
+ ### Performance Issues
626
+
627
+ **Symptom: Slow queries with encryption**
628
+
629
+ - Check KMS API throttling (CloudWatch metrics)
630
+ - Consider data key caching (future enhancement)
631
+ - Verify proper field selection (don't encrypt unnecessary fields)
632
+
633
+ ### Data Migration
634
+
635
+ **Migrating from Mongoose encryption:**
636
+
637
+ 1. Export data with old encryption
638
+ 2. Decrypt using old Mongoose plugin
639
+ 3. Re-import with new Prisma encryption
640
+ 4. Verify with `/health/test-encryption`
641
+
642
+ ## Security Best Practices
643
+
644
+ ### DO
645
+
646
+ ✅ Use AWS KMS for production (recommended) or AES encryption (valid alternative)
647
+ ✅ Rotate KMS keys regularly (AWS handles automatically)
648
+ ✅ Restrict KMS key access to Lambda execution role only
649
+ ✅ Use VPC endpoints for KMS (reduce NAT costs)
650
+ ✅ Monitor KMS API usage (CloudWatch)
651
+ ✅ Test encryption with health check endpoint
652
+
653
+ ### DON'T
654
+
655
+ ❌ Store AES keys in code or git (use environment variables)
656
+ ❌ Disable encryption in production
657
+ ❌ Skip encryption for PII data
658
+ ❌ Query on encrypted fields (not supported)
659
+ ❌ Manually decrypt data (use extension)
660
+
661
+ ## Future Enhancements
662
+
663
+ ### Planned
664
+
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)
670
+
671
+ ### Under Consideration
672
+
673
+ - [ ] Multi-region KMS replication
674
+ - [ ] Client-side field level encryption
675
+ - [ ] Encryption at rest + in transit
676
+ - [ ] Compliance reporting (GDPR, HIPAA)
677
+
678
+ ## Related Documentation
679
+
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)