@friggframework/core 2.0.0-next.41 → 2.0.0-next.42
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.
- package/CLAUDE.md +693 -0
- package/README.md +931 -50
- package/application/commands/README.md +421 -0
- package/application/commands/credential-commands.js +224 -0
- package/application/commands/entity-commands.js +315 -0
- package/application/commands/integration-commands.js +160 -0
- package/application/commands/integration-commands.test.js +123 -0
- package/application/commands/user-commands.js +213 -0
- package/application/index.js +69 -0
- package/core/CLAUDE.md +690 -0
- package/core/create-handler.js +0 -6
- package/credential/repositories/credential-repository-factory.js +47 -0
- package/credential/repositories/credential-repository-interface.js +98 -0
- package/credential/repositories/credential-repository-mongo.js +301 -0
- package/credential/repositories/credential-repository-postgres.js +307 -0
- package/credential/repositories/credential-repository.js +307 -0
- package/credential/use-cases/get-credential-for-user.js +21 -0
- package/credential/use-cases/update-authentication-status.js +15 -0
- package/database/config.js +117 -0
- package/database/encryption/README.md +683 -0
- package/database/encryption/encryption-integration.test.js +553 -0
- package/database/encryption/encryption-schema-registry.js +141 -0
- package/database/encryption/encryption-schema-registry.test.js +392 -0
- package/database/encryption/field-encryption-service.js +226 -0
- package/database/encryption/field-encryption-service.test.js +525 -0
- package/database/encryption/logger.js +79 -0
- package/database/encryption/mongo-decryption-fix-verification.test.js +348 -0
- package/database/encryption/postgres-decryption-fix-verification.test.js +371 -0
- package/database/encryption/postgres-relation-decryption.test.js +245 -0
- package/database/encryption/prisma-encryption-extension.js +222 -0
- package/database/encryption/prisma-encryption-extension.test.js +439 -0
- package/database/index.js +25 -12
- package/database/models/readme.md +1 -0
- package/database/prisma.js +162 -0
- package/database/repositories/health-check-repository-factory.js +38 -0
- package/database/repositories/health-check-repository-interface.js +86 -0
- package/database/repositories/health-check-repository-mongodb.js +72 -0
- package/database/repositories/health-check-repository-postgres.js +75 -0
- package/database/repositories/health-check-repository.js +108 -0
- package/database/use-cases/check-database-health-use-case.js +34 -0
- package/database/use-cases/check-encryption-health-use-case.js +82 -0
- package/database/use-cases/test-encryption-use-case.js +252 -0
- package/encrypt/Cryptor.js +20 -152
- package/encrypt/index.js +1 -2
- package/encrypt/test-encrypt.js +0 -2
- package/handlers/app-definition-loader.js +38 -0
- package/handlers/app-handler-helpers.js +0 -3
- package/handlers/auth-flow.integration.test.js +147 -0
- package/handlers/backend-utils.js +25 -45
- package/handlers/integration-event-dispatcher.js +54 -0
- package/handlers/integration-event-dispatcher.test.js +141 -0
- package/handlers/routers/HEALTHCHECK.md +103 -1
- package/handlers/routers/auth.js +3 -14
- package/handlers/routers/health.js +63 -424
- package/handlers/routers/health.test.js +7 -0
- package/handlers/routers/integration-defined-routers.js +8 -5
- package/handlers/routers/user.js +25 -5
- package/handlers/routers/websocket.js +5 -3
- package/handlers/use-cases/check-external-apis-health-use-case.js +81 -0
- package/handlers/use-cases/check-integrations-health-use-case.js +32 -0
- package/handlers/workers/integration-defined-workers.js +6 -3
- package/index.js +45 -22
- package/integrations/index.js +12 -10
- package/integrations/integration-base.js +224 -53
- package/integrations/integration-router.js +386 -178
- package/integrations/options.js +1 -1
- package/integrations/repositories/integration-mapping-repository-factory.js +50 -0
- package/integrations/repositories/integration-mapping-repository-interface.js +106 -0
- package/integrations/repositories/integration-mapping-repository-mongo.js +161 -0
- package/integrations/repositories/integration-mapping-repository-postgres.js +227 -0
- package/integrations/repositories/integration-mapping-repository.js +156 -0
- package/integrations/repositories/integration-repository-factory.js +44 -0
- package/integrations/repositories/integration-repository-interface.js +115 -0
- package/integrations/repositories/integration-repository-mongo.js +271 -0
- package/integrations/repositories/integration-repository-postgres.js +319 -0
- package/integrations/tests/doubles/dummy-integration-class.js +90 -0
- package/integrations/tests/doubles/test-integration-repository.js +99 -0
- package/integrations/tests/use-cases/create-integration.test.js +131 -0
- package/integrations/tests/use-cases/delete-integration-for-user.test.js +150 -0
- package/integrations/tests/use-cases/find-integration-context-by-external-entity-id.test.js +92 -0
- package/integrations/tests/use-cases/get-integration-for-user.test.js +150 -0
- package/integrations/tests/use-cases/get-integration-instance.test.js +176 -0
- package/integrations/tests/use-cases/get-integrations-for-user.test.js +176 -0
- package/integrations/tests/use-cases/get-possible-integrations.test.js +188 -0
- package/integrations/tests/use-cases/update-integration-messages.test.js +142 -0
- package/integrations/tests/use-cases/update-integration-status.test.js +103 -0
- package/integrations/tests/use-cases/update-integration.test.js +141 -0
- package/integrations/use-cases/create-integration.js +83 -0
- package/integrations/use-cases/delete-integration-for-user.js +73 -0
- package/integrations/use-cases/find-integration-context-by-external-entity-id.js +72 -0
- package/integrations/use-cases/get-integration-for-user.js +78 -0
- package/integrations/use-cases/get-integration-instance-by-definition.js +67 -0
- package/integrations/use-cases/get-integration-instance.js +83 -0
- package/integrations/use-cases/get-integrations-for-user.js +87 -0
- package/integrations/use-cases/get-possible-integrations.js +27 -0
- package/integrations/use-cases/index.js +11 -0
- package/integrations/use-cases/load-integration-context-full.test.js +329 -0
- package/integrations/use-cases/load-integration-context.js +71 -0
- package/integrations/use-cases/load-integration-context.test.js +114 -0
- package/integrations/use-cases/update-integration-messages.js +44 -0
- package/integrations/use-cases/update-integration-status.js +32 -0
- package/integrations/use-cases/update-integration.js +93 -0
- package/integrations/utils/map-integration-dto.js +36 -0
- package/jest-global-setup-noop.js +3 -0
- package/jest-global-teardown-noop.js +3 -0
- package/{module-plugin → modules}/entity.js +1 -0
- package/{module-plugin → modules}/index.js +0 -8
- package/modules/module-factory.js +56 -0
- package/modules/module-hydration.test.js +205 -0
- package/modules/module.js +221 -0
- package/modules/repositories/module-repository-factory.js +33 -0
- package/modules/repositories/module-repository-interface.js +129 -0
- package/modules/repositories/module-repository-mongo.js +386 -0
- package/modules/repositories/module-repository-postgres.js +437 -0
- package/modules/repositories/module-repository.js +327 -0
- package/{module-plugin → modules}/test/mock-api/api.js +8 -3
- package/{module-plugin → modules}/test/mock-api/definition.js +12 -8
- package/modules/tests/doubles/test-module-factory.js +16 -0
- package/modules/tests/doubles/test-module-repository.js +39 -0
- package/modules/use-cases/get-entities-for-user.js +32 -0
- package/modules/use-cases/get-entity-options-by-id.js +59 -0
- package/modules/use-cases/get-entity-options-by-type.js +34 -0
- package/modules/use-cases/get-module-instance-from-type.js +31 -0
- package/modules/use-cases/get-module.js +56 -0
- package/modules/use-cases/process-authorization-callback.js +121 -0
- package/modules/use-cases/refresh-entity-options.js +59 -0
- package/modules/use-cases/test-module-auth.js +55 -0
- package/modules/utils/map-module-dto.js +18 -0
- package/package.json +14 -6
- package/prisma-mongodb/schema.prisma +321 -0
- package/prisma-postgresql/migrations/20250930193005_init/migration.sql +315 -0
- package/prisma-postgresql/migrations/20251006135218_init/migration.sql +9 -0
- package/prisma-postgresql/migrations/migration_lock.toml +3 -0
- package/prisma-postgresql/schema.prisma +303 -0
- package/syncs/manager.js +468 -443
- package/syncs/repositories/sync-repository-factory.js +38 -0
- package/syncs/repositories/sync-repository-interface.js +109 -0
- package/syncs/repositories/sync-repository-mongo.js +239 -0
- package/syncs/repositories/sync-repository-postgres.js +319 -0
- package/syncs/sync.js +0 -1
- package/token/repositories/token-repository-factory.js +33 -0
- package/token/repositories/token-repository-interface.js +131 -0
- package/token/repositories/token-repository-mongo.js +212 -0
- package/token/repositories/token-repository-postgres.js +257 -0
- package/token/repositories/token-repository.js +219 -0
- package/types/integrations/index.d.ts +2 -6
- package/types/module-plugin/index.d.ts +5 -57
- package/types/syncs/index.d.ts +0 -2
- package/user/repositories/user-repository-factory.js +46 -0
- package/user/repositories/user-repository-interface.js +198 -0
- package/user/repositories/user-repository-mongo.js +250 -0
- package/user/repositories/user-repository-postgres.js +311 -0
- package/user/tests/doubles/test-user-repository.js +72 -0
- package/user/tests/use-cases/create-individual-user.test.js +24 -0
- package/user/tests/use-cases/create-organization-user.test.js +28 -0
- package/user/tests/use-cases/create-token-for-user-id.test.js +19 -0
- package/user/tests/use-cases/get-user-from-bearer-token.test.js +64 -0
- package/user/tests/use-cases/login-user.test.js +140 -0
- package/user/use-cases/create-individual-user.js +61 -0
- package/user/use-cases/create-organization-user.js +47 -0
- package/user/use-cases/create-token-for-user-id.js +30 -0
- package/user/use-cases/get-user-from-bearer-token.js +77 -0
- package/user/use-cases/login-user.js +122 -0
- package/user/user.js +77 -0
- package/websocket/repositories/websocket-connection-repository-factory.js +37 -0
- package/websocket/repositories/websocket-connection-repository-interface.js +106 -0
- package/websocket/repositories/websocket-connection-repository-mongo.js +155 -0
- package/websocket/repositories/websocket-connection-repository-postgres.js +195 -0
- package/websocket/repositories/websocket-connection-repository.js +160 -0
- package/database/models/State.js +0 -9
- package/database/models/Token.js +0 -70
- package/database/mongo.js +0 -171
- package/encrypt/Cryptor.test.js +0 -32
- package/encrypt/encrypt.js +0 -104
- package/encrypt/encrypt.test.js +0 -1069
- package/handlers/routers/middleware/loadUser.js +0 -15
- package/handlers/routers/middleware/requireLoggedInUser.js +0 -12
- package/integrations/create-frigg-backend.js +0 -31
- package/integrations/integration-factory.js +0 -251
- package/integrations/integration-mapping.js +0 -43
- package/integrations/integration-model.js +0 -46
- package/integrations/integration-user.js +0 -144
- package/integrations/test/integration-base.test.js +0 -144
- package/module-plugin/auther.js +0 -393
- package/module-plugin/credential.js +0 -22
- package/module-plugin/entity-manager.js +0 -70
- package/module-plugin/manager.js +0 -169
- package/module-plugin/module-factory.js +0 -61
- package/module-plugin/test/auther.test.js +0 -97
- /package/{module-plugin → modules}/ModuleConstants.js +0 -0
- /package/{module-plugin → modules}/requester/api-key.js +0 -0
- /package/{module-plugin → modules}/requester/basic.js +0 -0
- /package/{module-plugin → modules}/requester/oauth-2.js +0 -0
- /package/{module-plugin → modules}/requester/requester.js +0 -0
- /package/{module-plugin → modules}/requester/requester.test.js +0 -0
- /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)
|