@friggframework/core 2.0.0-next.53 → 2.0.0-next.54
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 +2 -1
- package/application/commands/integration-commands.js +1 -1
- package/application/index.js +1 -1
- package/credential/repositories/credential-repository-documentdb.js +300 -0
- package/credential/repositories/credential-repository-factory.js +8 -1
- package/database/config.js +4 -4
- package/database/documentdb-encryption-service.js +330 -0
- package/database/documentdb-utils.js +136 -0
- package/database/encryption/README.md +50 -0
- package/database/encryption/documentdb-encryption-service.md +3270 -0
- package/database/encryption/encryption-schema-registry.js +46 -0
- package/database/prisma.js +7 -47
- package/database/repositories/health-check-repository-documentdb.js +134 -0
- package/database/repositories/health-check-repository-factory.js +6 -1
- package/database/repositories/health-check-repository-interface.js +29 -34
- package/database/repositories/health-check-repository-mongodb.js +1 -3
- package/database/use-cases/check-database-state-use-case.js +3 -3
- package/database/use-cases/run-database-migration-use-case.js +6 -4
- package/database/use-cases/trigger-database-migration-use-case.js +2 -2
- package/database/utils/mongodb-schema-init.js +5 -5
- package/database/utils/prisma-runner.js +15 -9
- package/generated/prisma-mongodb/edge.js +3 -3
- package/generated/prisma-mongodb/index.d.ts +10 -4
- package/generated/prisma-mongodb/index.js +3 -3
- package/generated/prisma-mongodb/package.json +1 -1
- package/generated/prisma-mongodb/schema.prisma +1 -3
- package/generated/prisma-mongodb/wasm.js +2 -2
- package/generated/prisma-postgresql/edge.js +3 -3
- package/generated/prisma-postgresql/index.d.ts +10 -4
- package/generated/prisma-postgresql/index.js +3 -3
- package/generated/prisma-postgresql/package.json +1 -1
- package/generated/prisma-postgresql/schema.prisma +1 -3
- package/generated/prisma-postgresql/wasm.js +2 -2
- package/handlers/routers/db-migration.js +2 -3
- package/handlers/routers/health.js +0 -3
- package/handlers/workers/db-migration.js +8 -8
- package/integrations/repositories/integration-mapping-repository-documentdb.js +135 -0
- package/integrations/repositories/integration-mapping-repository-factory.js +8 -1
- package/integrations/repositories/integration-repository-documentdb.js +189 -0
- package/integrations/repositories/integration-repository-factory.js +8 -1
- package/integrations/repositories/process-repository-documentdb.js +141 -0
- package/integrations/repositories/process-repository-factory.js +8 -1
- package/modules/repositories/module-repository-documentdb.js +307 -0
- package/modules/repositories/module-repository-factory.js +8 -1
- package/package.json +5 -5
- package/prisma-mongodb/schema.prisma +1 -3
- package/prisma-postgresql/migrations/20251112195422_update_user_unique_constraints/migration.sql +69 -0
- package/prisma-postgresql/schema.prisma +1 -3
- package/syncs/repositories/sync-repository-documentdb.js +240 -0
- package/syncs/repositories/sync-repository-factory.js +6 -1
- package/token/repositories/token-repository-documentdb.js +125 -0
- package/token/repositories/token-repository-factory.js +8 -1
- package/user/repositories/user-repository-documentdb.js +292 -0
- package/user/repositories/user-repository-factory.js +6 -1
- package/websocket/repositories/websocket-connection-repository-documentdb.js +119 -0
- package/websocket/repositories/websocket-connection-repository-factory.js +8 -1
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
const { Cryptor } = require('../encrypt/Cryptor');
|
|
2
|
+
const { getEncryptedFields, loadCustomEncryptionSchema } = require('./encryption/encryption-schema-registry');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Encryption service specifically for DocumentDB repositories
|
|
6
|
+
* that use $runCommandRaw and bypass Prisma Client Extensions.
|
|
7
|
+
*
|
|
8
|
+
* Provides document-level encryption/decryption, handling nested fields
|
|
9
|
+
* according to the encryption schema registry.
|
|
10
|
+
*
|
|
11
|
+
* @class DocumentDBEncryptionService
|
|
12
|
+
* @example
|
|
13
|
+
* const service = new DocumentDBEncryptionService();
|
|
14
|
+
*
|
|
15
|
+
* // Encrypt before write
|
|
16
|
+
* const encrypted = await service.encryptFields('Credential', document);
|
|
17
|
+
* await insertOne(prisma, 'Credential', encrypted);
|
|
18
|
+
*
|
|
19
|
+
* // Decrypt after read
|
|
20
|
+
* const doc = await findOne(prisma, 'Credential', filter);
|
|
21
|
+
* const decrypted = await service.decryptFields('Credential', doc);
|
|
22
|
+
*/
|
|
23
|
+
class DocumentDBEncryptionService {
|
|
24
|
+
/**
|
|
25
|
+
* @param {Object} options - Configuration options
|
|
26
|
+
* @param {Cryptor} [options.cryptor] - Optional Cryptor instance for dependency injection (useful for testing)
|
|
27
|
+
*/
|
|
28
|
+
constructor({ cryptor = null } = {}) {
|
|
29
|
+
if (cryptor) {
|
|
30
|
+
// Dependency injection - use provided Cryptor (for testing)
|
|
31
|
+
this.cryptor = cryptor;
|
|
32
|
+
this.enabled = true;
|
|
33
|
+
} else {
|
|
34
|
+
// Default behavior - create Cryptor from environment
|
|
35
|
+
this._initializeCryptor();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Initialize Cryptor with environment-based configuration.
|
|
41
|
+
* Matches the logic from @friggframework/core/database/prisma.js
|
|
42
|
+
*
|
|
43
|
+
* Encryption is bypassed in dev/test/local stages.
|
|
44
|
+
* Production uses AWS KMS (if available) or AES encryption.
|
|
45
|
+
*
|
|
46
|
+
* @private
|
|
47
|
+
*/
|
|
48
|
+
_initializeCryptor() {
|
|
49
|
+
// Load custom encryption schema from app definition BEFORE checking configuration
|
|
50
|
+
// This ensures custom fields (like User.username) are registered before any encryption operations
|
|
51
|
+
loadCustomEncryptionSchema();
|
|
52
|
+
|
|
53
|
+
// Match logic from packages/core/database/prisma.js
|
|
54
|
+
const stage = process.env.STAGE || process.env.NODE_ENV || 'development';
|
|
55
|
+
const bypassEncryption = ['dev', 'test', 'local'].includes(stage.toLowerCase());
|
|
56
|
+
|
|
57
|
+
if (bypassEncryption) {
|
|
58
|
+
this.cryptor = null;
|
|
59
|
+
this.enabled = false;
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Determine encryption method (ensure boolean values)
|
|
64
|
+
const hasKMS = !!(process.env.KMS_KEY_ARN && process.env.KMS_KEY_ARN.trim() !== '');
|
|
65
|
+
const hasAES = !!(process.env.AES_KEY_ID && process.env.AES_KEY_ID.trim() !== '');
|
|
66
|
+
|
|
67
|
+
if (!hasKMS && !hasAES) {
|
|
68
|
+
console.warn('[DocumentDBEncryptionService] No encryption keys configured. Encryption disabled.');
|
|
69
|
+
this.cryptor = null;
|
|
70
|
+
this.enabled = false;
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// KMS takes precedence over AES
|
|
75
|
+
const shouldUseAws = hasKMS;
|
|
76
|
+
this.cryptor = new Cryptor({ shouldUseAws });
|
|
77
|
+
this.enabled = true;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Encrypt sensitive fields in a document before storing to DocumentDB.
|
|
82
|
+
*
|
|
83
|
+
* Reads field paths from encryption-schema-registry.js and encrypts
|
|
84
|
+
* only the fields defined for the given model.
|
|
85
|
+
*
|
|
86
|
+
* @param {string} modelName - Model name from schema registry (e.g., 'User', 'Credential')
|
|
87
|
+
* @param {Object} document - Document to encrypt
|
|
88
|
+
* @returns {Promise<Object>} - New document with encrypted fields (original unchanged)
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* const plainDoc = {
|
|
92
|
+
* userId: '123',
|
|
93
|
+
* data: { access_token: 'plain_secret' }
|
|
94
|
+
* };
|
|
95
|
+
* const encrypted = await service.encryptFields('Credential', plainDoc);
|
|
96
|
+
* // encrypted.data.access_token = "keyId:iv:cipher:encKey"
|
|
97
|
+
*/
|
|
98
|
+
async encryptFields(modelName, document) {
|
|
99
|
+
// Bypass if encryption disabled
|
|
100
|
+
if (!this.enabled || !this.cryptor) {
|
|
101
|
+
return document;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Validate input
|
|
105
|
+
if (!document || typeof document !== 'object') {
|
|
106
|
+
return document;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Get encrypted fields from registry
|
|
110
|
+
const encryptedFieldsConfig = getEncryptedFields(modelName);
|
|
111
|
+
if (!encryptedFieldsConfig || encryptedFieldsConfig.length === 0) {
|
|
112
|
+
return document;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Deep clone to prevent mutation (preserves Date, RegExp, Buffer)
|
|
116
|
+
const result = structuredClone(document);
|
|
117
|
+
|
|
118
|
+
// Encrypt each field path
|
|
119
|
+
for (const fieldPath of encryptedFieldsConfig) {
|
|
120
|
+
await this._encryptFieldPath(result, fieldPath, modelName);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Decrypt sensitive fields in a document after reading from DocumentDB.
|
|
128
|
+
*
|
|
129
|
+
* Reads field paths from encryption-schema-registry.js and decrypts
|
|
130
|
+
* only the fields defined for the given model.
|
|
131
|
+
*
|
|
132
|
+
* @param {string} modelName - Model name from schema registry
|
|
133
|
+
* @param {Object} document - Document to decrypt
|
|
134
|
+
* @returns {Promise<Object>} - New document with decrypted fields (original unchanged)
|
|
135
|
+
*
|
|
136
|
+
* @example
|
|
137
|
+
* const encryptedDoc = {
|
|
138
|
+
* userId: '123',
|
|
139
|
+
* data: { access_token: 'keyId:iv:cipher:encKey' }
|
|
140
|
+
* };
|
|
141
|
+
* const decrypted = await service.decryptFields('Credential', encryptedDoc);
|
|
142
|
+
* // decrypted.data.access_token = "plain_secret"
|
|
143
|
+
*/
|
|
144
|
+
async decryptFields(modelName, document) {
|
|
145
|
+
// Bypass if encryption disabled
|
|
146
|
+
if (!this.enabled || !this.cryptor) {
|
|
147
|
+
return document;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Validate input
|
|
151
|
+
if (!document || typeof document !== 'object') {
|
|
152
|
+
return document;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Get encrypted fields from registry
|
|
156
|
+
const encryptedFieldsConfig = getEncryptedFields(modelName);
|
|
157
|
+
if (!encryptedFieldsConfig || encryptedFieldsConfig.length === 0) {
|
|
158
|
+
return document;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Deep clone to prevent mutation (preserves Date, RegExp, Buffer)
|
|
162
|
+
const result = structuredClone(document);
|
|
163
|
+
|
|
164
|
+
// Decrypt each field path
|
|
165
|
+
for (const fieldPath of encryptedFieldsConfig) {
|
|
166
|
+
await this._decryptFieldPath(result, fieldPath, modelName);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return result;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Encrypt a specific field path in a document (handles nested fields).
|
|
174
|
+
*
|
|
175
|
+
* @private
|
|
176
|
+
* @param {Object} document - Document to modify (mutated in place)
|
|
177
|
+
* @param {string} fieldPath - Field path from schema registry (e.g., 'data.access_token')
|
|
178
|
+
* @param {string} modelName - For error logging context
|
|
179
|
+
*/
|
|
180
|
+
async _encryptFieldPath(document, fieldPath, modelName) {
|
|
181
|
+
// Parse field path
|
|
182
|
+
const parts = fieldPath.split('.');
|
|
183
|
+
|
|
184
|
+
// Navigate to parent object
|
|
185
|
+
let current = document;
|
|
186
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
187
|
+
if (!current[parts[i]]) {
|
|
188
|
+
// Path doesn't exist, nothing to encrypt
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
current = current[parts[i]];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Get field name and value
|
|
195
|
+
const fieldName = parts[parts.length - 1];
|
|
196
|
+
const value = current[fieldName];
|
|
197
|
+
|
|
198
|
+
// Skip if already encrypted or empty
|
|
199
|
+
if (!value || this._isEncryptedValue(value)) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
// Convert to string if needed
|
|
205
|
+
const stringValue = typeof value === 'string'
|
|
206
|
+
? value
|
|
207
|
+
: JSON.stringify(value);
|
|
208
|
+
|
|
209
|
+
// Encrypt using Cryptor
|
|
210
|
+
current[fieldName] = await this.cryptor.encrypt(stringValue);
|
|
211
|
+
} catch (error) {
|
|
212
|
+
console.error(`[DocumentDBEncryptionService] Failed to encrypt ${modelName}.${fieldPath}:`, error.message);
|
|
213
|
+
throw error;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Decrypt a specific field path in a document (handles nested fields).
|
|
219
|
+
*
|
|
220
|
+
* @private
|
|
221
|
+
* @param {Object} document - Document to modify (mutated in place)
|
|
222
|
+
* @param {string} fieldPath - Field path from schema registry
|
|
223
|
+
* @param {string} modelName - For error logging context
|
|
224
|
+
*/
|
|
225
|
+
async _decryptFieldPath(document, fieldPath, modelName) {
|
|
226
|
+
// Parse field path
|
|
227
|
+
const parts = fieldPath.split('.');
|
|
228
|
+
|
|
229
|
+
// Navigate to parent object
|
|
230
|
+
let current = document;
|
|
231
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
232
|
+
if (!current[parts[i]]) {
|
|
233
|
+
// Path doesn't exist, nothing to decrypt
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
current = current[parts[i]];
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Get field name and encrypted value
|
|
240
|
+
const fieldName = parts[parts.length - 1];
|
|
241
|
+
const encryptedValue = current[fieldName];
|
|
242
|
+
|
|
243
|
+
// Skip if not encrypted format
|
|
244
|
+
if (!encryptedValue || !this._isEncryptedValue(encryptedValue)) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
// Decrypt using Cryptor
|
|
250
|
+
const decryptedString = await this.cryptor.decrypt(encryptedValue);
|
|
251
|
+
|
|
252
|
+
// Try to parse as JSON (for objects/arrays)
|
|
253
|
+
try {
|
|
254
|
+
current[fieldName] = JSON.parse(decryptedString);
|
|
255
|
+
} catch {
|
|
256
|
+
// Not JSON, return as string
|
|
257
|
+
current[fieldName] = decryptedString;
|
|
258
|
+
}
|
|
259
|
+
} catch (error) {
|
|
260
|
+
const errorContext = {
|
|
261
|
+
modelName,
|
|
262
|
+
fieldPath,
|
|
263
|
+
encryptedValuePrefix: encryptedValue.substring(0, 20),
|
|
264
|
+
errorMessage: error.message
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
console.error(
|
|
268
|
+
`[DocumentDBEncryptionService] Failed to decrypt ${modelName}.${fieldPath}:`,
|
|
269
|
+
JSON.stringify(errorContext)
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
// Throw error to fail fast - don't silently corrupt data
|
|
273
|
+
throw new Error(`Decryption failed for ${modelName}.${fieldPath}: ${error.message}`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Check if a value is in encrypted format.
|
|
279
|
+
*
|
|
280
|
+
* Encrypted format: "keyId:iv:cipher:encKey" (envelope encryption)
|
|
281
|
+
* All parts are base64-encoded strings.
|
|
282
|
+
*
|
|
283
|
+
* @private
|
|
284
|
+
* @param {any} value - Value to check
|
|
285
|
+
* @returns {boolean} - True if value is encrypted
|
|
286
|
+
*
|
|
287
|
+
* @example
|
|
288
|
+
* _isEncryptedValue("plain_text") // false
|
|
289
|
+
* _isEncryptedValue("YWVzLWtleS0x:TXlJVkhlcmU=:QWN0dWFsQ2lwaGVy:RW5jcnlwdGVk") // true
|
|
290
|
+
* _isEncryptedValue(null) // false
|
|
291
|
+
* _isEncryptedValue({}) // false
|
|
292
|
+
*/
|
|
293
|
+
_isEncryptedValue(value) {
|
|
294
|
+
// Must be string
|
|
295
|
+
if (typeof value !== 'string') {
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Must have exactly 4 colon-separated parts
|
|
300
|
+
const parts = value.split(':');
|
|
301
|
+
if (parts.length !== 4) {
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Enhanced validation: check for base64 pattern
|
|
306
|
+
// This prevents false positives on URLs, connection strings, etc.
|
|
307
|
+
const base64Pattern = /^[A-Za-z0-9+/=]+$/;
|
|
308
|
+
|
|
309
|
+
// All parts should be base64-encoded
|
|
310
|
+
if (!parts.every(part => base64Pattern.test(part))) {
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Encrypted values should be sufficiently long to be valid
|
|
315
|
+
// Real encrypted values from Cryptor are always >50 chars due to envelope encryption format:
|
|
316
|
+
// - keyId (base64): ~12 chars minimum
|
|
317
|
+
// - iv (base64): ~24 chars for 16-byte IV
|
|
318
|
+
// - ciphertext (base64): varies, minimum ~16 chars for small values
|
|
319
|
+
// - encryptedKey (base64): ~44 chars for 32-byte data key
|
|
320
|
+
// Total minimum: ~96 chars, so 50 is a safe lower bound
|
|
321
|
+
// This prevents false positives on non-encrypted strings that happen to have 4 colons
|
|
322
|
+
if (value.length < 50) {
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return true;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
module.exports = { DocumentDBEncryptionService };
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
const { ObjectId } = require('mongodb');
|
|
2
|
+
|
|
3
|
+
function toObjectId(value) {
|
|
4
|
+
if (value === null || value === undefined || value === '') return undefined;
|
|
5
|
+
if (value instanceof ObjectId) return value;
|
|
6
|
+
if (typeof value === 'object' && value.$oid) return new ObjectId(value.$oid);
|
|
7
|
+
if (typeof value === 'string') return ObjectId.isValid(value) ? new ObjectId(value) : undefined;
|
|
8
|
+
return undefined;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function toObjectIdArray(values) {
|
|
12
|
+
if (!Array.isArray(values)) return [];
|
|
13
|
+
return values.map(toObjectId).filter(Boolean);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function fromObjectId(value) {
|
|
17
|
+
if (value instanceof ObjectId) return value.toHexString();
|
|
18
|
+
if (typeof value === 'object' && value !== null && value.$oid) return value.$oid;
|
|
19
|
+
if (typeof value === 'string') return value;
|
|
20
|
+
return value === undefined || value === null ? value : String(value);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function findMany(client, collection, filter = {}, options = {}) {
|
|
24
|
+
const command = { find: collection, filter };
|
|
25
|
+
if (options.projection) command.projection = options.projection;
|
|
26
|
+
if (options.sort) command.sort = options.sort;
|
|
27
|
+
if (options.limit) command.limit = options.limit;
|
|
28
|
+
const result = await client.$runCommandRaw(command);
|
|
29
|
+
return result?.cursor?.firstBatch || [];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function findOne(client, collection, filter = {}, options = {}) {
|
|
33
|
+
const docs = await findMany(client, collection, filter, { ...options, limit: 1 });
|
|
34
|
+
return docs[0] || null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function insertOne(client, collection, document) {
|
|
38
|
+
// Generate ObjectId if not present (MongoDB raw insert doesn't return insertedIds)
|
|
39
|
+
const _id = document._id || new ObjectId();
|
|
40
|
+
const docWithId = { ...document, _id };
|
|
41
|
+
|
|
42
|
+
const result = await client.$runCommandRaw({
|
|
43
|
+
insert: collection,
|
|
44
|
+
documents: [docWithId],
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Validate insert succeeded
|
|
48
|
+
if (result.ok !== 1) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`Insert command failed for collection '${collection}': ${JSON.stringify(result)}`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Check for write errors (duplicate keys, validation errors, etc.)
|
|
55
|
+
if (result.writeErrors && result.writeErrors.length > 0) {
|
|
56
|
+
const error = result.writeErrors[0];
|
|
57
|
+
const errorMsg = `Insert failed in '${collection}': ${error.errmsg} (code: ${error.code})`;
|
|
58
|
+
|
|
59
|
+
// Provide helpful context for common errors
|
|
60
|
+
if (error.code === 11000) {
|
|
61
|
+
throw new Error(`${errorMsg} - Duplicate key violation`);
|
|
62
|
+
}
|
|
63
|
+
throw new Error(errorMsg);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Verify exactly one document was inserted
|
|
67
|
+
if (result.n !== 1) {
|
|
68
|
+
throw new Error(
|
|
69
|
+
`Expected to insert 1 document into '${collection}', but inserted ${result.n}. ` +
|
|
70
|
+
`Result: ${JSON.stringify(result)}`
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return _id;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function updateOne(client, collection, filter, update, options = {}) {
|
|
78
|
+
const updates = [{
|
|
79
|
+
q: filter,
|
|
80
|
+
u: update,
|
|
81
|
+
upsert: Boolean(options.upsert),
|
|
82
|
+
}];
|
|
83
|
+
if (options.arrayFilters) updates[0].arrayFilters = options.arrayFilters;
|
|
84
|
+
const result = await client.$runCommandRaw({
|
|
85
|
+
update: collection,
|
|
86
|
+
updates,
|
|
87
|
+
});
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function deleteOne(client, collection, filter) {
|
|
92
|
+
return client.$runCommandRaw({
|
|
93
|
+
delete: collection,
|
|
94
|
+
deletes: [
|
|
95
|
+
{
|
|
96
|
+
q: filter,
|
|
97
|
+
limit: 1,
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function deleteMany(client, collection, filter) {
|
|
104
|
+
return client.$runCommandRaw({
|
|
105
|
+
delete: collection,
|
|
106
|
+
deletes: [
|
|
107
|
+
{
|
|
108
|
+
q: filter,
|
|
109
|
+
limit: 0,
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function aggregate(client, collection, pipeline) {
|
|
116
|
+
const result = await client.$runCommandRaw({
|
|
117
|
+
aggregate: collection,
|
|
118
|
+
pipeline,
|
|
119
|
+
cursor: {},
|
|
120
|
+
});
|
|
121
|
+
return result?.cursor?.firstBatch || [];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
module.exports = {
|
|
125
|
+
toObjectId,
|
|
126
|
+
toObjectIdArray,
|
|
127
|
+
fromObjectId,
|
|
128
|
+
findMany,
|
|
129
|
+
findOne,
|
|
130
|
+
insertOne,
|
|
131
|
+
updateOne,
|
|
132
|
+
deleteOne,
|
|
133
|
+
deleteMany,
|
|
134
|
+
aggregate,
|
|
135
|
+
};
|
|
136
|
+
|
|
@@ -267,6 +267,14 @@ const CORE_ENCRYPTION_SCHEMA = {
|
|
|
267
267
|
- Integration-specific sensitive data (use custom schema instead)
|
|
268
268
|
- Temporary/experimental encryption (use custom schema instead)
|
|
269
269
|
|
|
270
|
+
#### After Adding Encrypted Fields
|
|
271
|
+
|
|
272
|
+
After adding fields to `encryption-schema-registry.js`:
|
|
273
|
+
|
|
274
|
+
1. **For MongoDB/PostgreSQL**: No code changes needed (automatic via Prisma Extension)
|
|
275
|
+
2. **For DocumentDB**: Encryption is automatic via DocumentDBEncryptionService
|
|
276
|
+
(service reads from same registry)
|
|
277
|
+
|
|
270
278
|
## How It Works
|
|
271
279
|
|
|
272
280
|
### Write Operation (Create/Update)
|
|
@@ -403,6 +411,48 @@ return entities.map((e) => ({
|
|
|
403
411
|
|
|
404
412
|
See `modules/repositories/module-repository-postgres.js` and `module-repository-mongo.js` for complete implementation examples using `_fetchCredential()` and `_fetchCredentialsBulk()` helper methods.
|
|
405
413
|
|
|
414
|
+
## DocumentDB Encryption
|
|
415
|
+
|
|
416
|
+
### Why DocumentDB Needs Manual Encryption
|
|
417
|
+
|
|
418
|
+
DocumentDB repositories use `$runCommandRaw()` for MongoDB protocol compatibility, which bypasses Prisma Client Extensions. This means the automatic encryption extension does not apply.
|
|
419
|
+
|
|
420
|
+
### DocumentDBEncryptionService
|
|
421
|
+
|
|
422
|
+
For DocumentDB repositories, use `DocumentDBEncryptionService` to manually encrypt/decrypt documents before/after database operations.
|
|
423
|
+
|
|
424
|
+
#### Usage Example
|
|
425
|
+
|
|
426
|
+
```javascript
|
|
427
|
+
const { DocumentDBEncryptionService } = require('../documentdb-encryption-service');
|
|
428
|
+
const { insertOne, findOne } = require('../documentdb-utils');
|
|
429
|
+
|
|
430
|
+
class MyRepositoryDocumentDB {
|
|
431
|
+
constructor() {
|
|
432
|
+
this.encryptionService = new DocumentDBEncryptionService();
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
async create(data) {
|
|
436
|
+
// Encrypt before write
|
|
437
|
+
const encrypted = await this.encryptionService.encryptFields('ModelName', data);
|
|
438
|
+
const id = await insertOne(this.prisma, 'CollectionName', encrypted);
|
|
439
|
+
|
|
440
|
+
// Decrypt after read
|
|
441
|
+
const doc = await findOne(this.prisma, 'CollectionName', { _id: id });
|
|
442
|
+
const decrypted = await this.encryptionService.decryptFields('ModelName', doc);
|
|
443
|
+
|
|
444
|
+
return decrypted;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
#### Configuration
|
|
450
|
+
|
|
451
|
+
Uses the same environment variables and Cryptor as the Prisma Extension:
|
|
452
|
+
- `STAGE`: Bypasses encryption for dev/test/local
|
|
453
|
+
- `KMS_KEY_ARN`: AWS KMS encryption (production)
|
|
454
|
+
- `AES_KEY_ID` + `AES_KEY`: AES encryption (fallback)
|
|
455
|
+
|
|
406
456
|
## Usage Examples
|
|
407
457
|
|
|
408
458
|
### Repository Code (No Changes Needed!)
|