@friggframework/core 2.0.0-next.83 → 2.0.0-next.84
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.
|
@@ -42,6 +42,17 @@ const CORE_ENCRYPTION_SCHEMA = {
|
|
|
42
42
|
|
|
43
43
|
let customSchema = {};
|
|
44
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Per-model write-side opt-out: fields registered here are NOT encrypted on
|
|
47
|
+
* write, but ARE still decrypted on read so legacy encrypted rows continue to
|
|
48
|
+
* deserialize. Lets apps migrate a model from encrypted to plain JSON without
|
|
49
|
+
* a data migration — touched rows naturally rewrite as plain on the next save,
|
|
50
|
+
* untouched rows stay encrypted-but-readable forever.
|
|
51
|
+
*
|
|
52
|
+
* Shape: `{ ModelName: ['field.path', ...] }`
|
|
53
|
+
*/
|
|
54
|
+
let encryptionOptOut = {};
|
|
55
|
+
|
|
45
56
|
/**
|
|
46
57
|
* Validates a custom encryption schema
|
|
47
58
|
* @returns {{valid: boolean, errors: string[]}}
|
|
@@ -218,6 +229,14 @@ function loadCustomEncryptionSchema() {
|
|
|
218
229
|
registerCustomSchema(customSchema);
|
|
219
230
|
}
|
|
220
231
|
|
|
232
|
+
// Load app-level encryption opt-out — apps can declare fields they
|
|
233
|
+
// don't want encrypted on write (decryption on read still works,
|
|
234
|
+
// so legacy data remains readable).
|
|
235
|
+
const disable = appDefinition.encryption?.disable;
|
|
236
|
+
if (disable && Object.keys(disable).length > 0) {
|
|
237
|
+
registerEncryptionOptOut(disable);
|
|
238
|
+
}
|
|
239
|
+
|
|
221
240
|
// Load module-level encryption schemas from integrations
|
|
222
241
|
const integrations = appDefinition.integrations;
|
|
223
242
|
if (integrations && Array.isArray(integrations)) {
|
|
@@ -240,6 +259,115 @@ function getEncryptedFields(modelName) {
|
|
|
240
259
|
return [...new Set(allFields)];
|
|
241
260
|
}
|
|
242
261
|
|
|
262
|
+
/**
|
|
263
|
+
* Validates an encryption opt-out config.
|
|
264
|
+
*
|
|
265
|
+
* Unlike custom schema validation, opt-out IS allowed to target paths that
|
|
266
|
+
* already live in CORE_ENCRYPTION_SCHEMA — that's the entire point.
|
|
267
|
+
*
|
|
268
|
+
* @param {Object} optOut - Map of `{ ModelName: ['field.path', ...] }`
|
|
269
|
+
* @returns {{valid: boolean, errors: string[]}}
|
|
270
|
+
*/
|
|
271
|
+
function validateOptOut(optOut) {
|
|
272
|
+
const errors = [];
|
|
273
|
+
|
|
274
|
+
if (!optOut || typeof optOut !== 'object') {
|
|
275
|
+
errors.push('Encryption opt-out must be an object');
|
|
276
|
+
return { valid: false, errors };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
for (const [modelName, fields] of Object.entries(optOut)) {
|
|
280
|
+
if (typeof modelName !== 'string' || !modelName) {
|
|
281
|
+
errors.push(`Invalid model name in opt-out: ${modelName}`);
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (!Array.isArray(fields)) {
|
|
286
|
+
errors.push(
|
|
287
|
+
`Model "${modelName}" opt-out must be an array of field paths`
|
|
288
|
+
);
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
for (const fieldPath of fields) {
|
|
293
|
+
if (typeof fieldPath !== 'string' || !fieldPath) {
|
|
294
|
+
errors.push(
|
|
295
|
+
`Model "${modelName}" has invalid opt-out field path: ${fieldPath}`
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return { valid: errors.length === 0, errors };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Registers an encryption opt-out config. Listed fields will be skipped during
|
|
306
|
+
* encryption on write while still being eligible for decryption on read (so
|
|
307
|
+
* legacy encrypted rows still deserialize correctly).
|
|
308
|
+
*
|
|
309
|
+
* Intended call site: `appDefinition.encryption.disable` via
|
|
310
|
+
* `loadCustomEncryptionSchema`.
|
|
311
|
+
*
|
|
312
|
+
* @param {Object} optOut - Map of `{ ModelName: ['field.path', ...] }`
|
|
313
|
+
* @throws {Error} If opt-out validation fails
|
|
314
|
+
*/
|
|
315
|
+
function registerEncryptionOptOut(optOut) {
|
|
316
|
+
if (!optOut || Object.keys(optOut).length === 0) {
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const validation = validateOptOut(optOut);
|
|
321
|
+
if (!validation.valid) {
|
|
322
|
+
throw new Error(
|
|
323
|
+
`Invalid encryption opt-out:\n- ${validation.errors.join('\n- ')}`
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
encryptionOptOut = { ...optOut };
|
|
328
|
+
logger.info(
|
|
329
|
+
`Registered encryption opt-out for models: ${Object.keys(
|
|
330
|
+
encryptionOptOut
|
|
331
|
+
).join(', ')}`
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Returns the field paths that should be encrypted when writing the given
|
|
337
|
+
* model. This is `getEncryptedFields` minus any paths the app has opted out
|
|
338
|
+
* of via `registerEncryptionOptOut`.
|
|
339
|
+
*
|
|
340
|
+
* Use this in the encrypt-on-write path of the FieldEncryptionService.
|
|
341
|
+
*/
|
|
342
|
+
function getFieldsToEncryptOnWrite(modelName) {
|
|
343
|
+
const allFields = getEncryptedFields(modelName);
|
|
344
|
+
const optedOut = new Set(encryptionOptOut[modelName] || []);
|
|
345
|
+
if (optedOut.size === 0) return allFields;
|
|
346
|
+
return allFields.filter((path) => !optedOut.has(path));
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Returns the field paths that should be checked for decryption when reading
|
|
351
|
+
* the given model. Always includes opted-out paths so legacy encrypted rows
|
|
352
|
+
* remain readable after an app opts a field out.
|
|
353
|
+
*
|
|
354
|
+
* `FieldEncryptionService._isEncrypted` already short-circuits for plain JSON
|
|
355
|
+
* values, so listing more fields than necessary here is harmless.
|
|
356
|
+
*
|
|
357
|
+
* Use this in the decrypt-on-read path of the FieldEncryptionService.
|
|
358
|
+
*/
|
|
359
|
+
function getFieldsToDecryptOnRead(modelName) {
|
|
360
|
+
return getEncryptedFields(modelName);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Clears any registered encryption opt-outs. Test-helper; not intended for
|
|
365
|
+
* runtime use.
|
|
366
|
+
*/
|
|
367
|
+
function resetEncryptionOptOut() {
|
|
368
|
+
encryptionOptOut = {};
|
|
369
|
+
}
|
|
370
|
+
|
|
243
371
|
function hasEncryptedFields(modelName) {
|
|
244
372
|
return getEncryptedFields(modelName).length > 0;
|
|
245
373
|
}
|
|
@@ -257,12 +385,17 @@ function resetCustomSchema() {
|
|
|
257
385
|
module.exports = {
|
|
258
386
|
CORE_ENCRYPTION_SCHEMA,
|
|
259
387
|
getEncryptedFields,
|
|
388
|
+
getFieldsToEncryptOnWrite,
|
|
389
|
+
getFieldsToDecryptOnRead,
|
|
260
390
|
hasEncryptedFields,
|
|
261
391
|
getEncryptedModels,
|
|
262
392
|
registerCustomSchema,
|
|
393
|
+
registerEncryptionOptOut,
|
|
263
394
|
loadCustomEncryptionSchema,
|
|
264
395
|
loadModuleEncryptionSchemas,
|
|
265
396
|
extractCredentialFieldsFromModules,
|
|
266
397
|
validateCustomSchema,
|
|
398
|
+
validateOptOut,
|
|
267
399
|
resetCustomSchema,
|
|
400
|
+
resetEncryptionOptOut,
|
|
268
401
|
};
|
|
@@ -17,12 +17,40 @@ class FieldEncryptionService {
|
|
|
17
17
|
this.schema = schema;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Resolve the field paths to encrypt on write. Prefers
|
|
22
|
+
* `schema.getFieldsToEncryptOnWrite` (which respects app opt-outs); falls
|
|
23
|
+
* back to `schema.getEncryptedFields` for backwards compatibility with
|
|
24
|
+
* older schema adapters.
|
|
25
|
+
* @private
|
|
26
|
+
*/
|
|
27
|
+
_getWriteFields(modelName) {
|
|
28
|
+
if (typeof this.schema.getFieldsToEncryptOnWrite === 'function') {
|
|
29
|
+
return this.schema.getFieldsToEncryptOnWrite(modelName);
|
|
30
|
+
}
|
|
31
|
+
return this.schema.getEncryptedFields(modelName);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Resolve the field paths to attempt decryption on read. Prefers
|
|
36
|
+
* `schema.getFieldsToDecryptOnRead` (which IGNORES opt-outs so legacy
|
|
37
|
+
* encrypted rows still deserialize); falls back to
|
|
38
|
+
* `schema.getEncryptedFields` for backwards compatibility.
|
|
39
|
+
* @private
|
|
40
|
+
*/
|
|
41
|
+
_getReadFields(modelName) {
|
|
42
|
+
if (typeof this.schema.getFieldsToDecryptOnRead === 'function') {
|
|
43
|
+
return this.schema.getFieldsToDecryptOnRead(modelName);
|
|
44
|
+
}
|
|
45
|
+
return this.schema.getEncryptedFields(modelName);
|
|
46
|
+
}
|
|
47
|
+
|
|
20
48
|
async encryptFields(modelName, document) {
|
|
21
49
|
if (!document || typeof document !== 'object') {
|
|
22
50
|
return document;
|
|
23
51
|
}
|
|
24
52
|
|
|
25
|
-
const fields = this.
|
|
53
|
+
const fields = this._getWriteFields(modelName);
|
|
26
54
|
if (fields.length === 0) {
|
|
27
55
|
return document;
|
|
28
56
|
}
|
|
@@ -58,7 +86,7 @@ class FieldEncryptionService {
|
|
|
58
86
|
return document;
|
|
59
87
|
}
|
|
60
88
|
|
|
61
|
-
const fields = this.
|
|
89
|
+
const fields = this._getReadFields(modelName);
|
|
62
90
|
if (fields.length === 0) {
|
|
63
91
|
return document;
|
|
64
92
|
}
|
|
@@ -3,7 +3,11 @@
|
|
|
3
3
|
* Intercepts Prisma queries to encrypt on write and decrypt on read.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
const {
|
|
6
|
+
const {
|
|
7
|
+
getEncryptedFields,
|
|
8
|
+
getFieldsToEncryptOnWrite,
|
|
9
|
+
getFieldsToDecryptOnRead,
|
|
10
|
+
} = require('./encryption-schema-registry');
|
|
7
11
|
const { FieldEncryptionService } = require('./field-encryption-service');
|
|
8
12
|
|
|
9
13
|
function createEncryptionExtension({ cryptor, enabled = true }) {
|
|
@@ -19,7 +23,11 @@ function createEncryptionExtension({ cryptor, enabled = true }) {
|
|
|
19
23
|
|
|
20
24
|
const encryptionService = new FieldEncryptionService({
|
|
21
25
|
cryptor,
|
|
22
|
-
schema: {
|
|
26
|
+
schema: {
|
|
27
|
+
getEncryptedFields,
|
|
28
|
+
getFieldsToEncryptOnWrite,
|
|
29
|
+
getFieldsToDecryptOnRead,
|
|
30
|
+
},
|
|
23
31
|
});
|
|
24
32
|
|
|
25
33
|
return {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@friggframework/core",
|
|
3
3
|
"prettier": "@friggframework/prettier-config",
|
|
4
|
-
"version": "2.0.0-next.
|
|
4
|
+
"version": "2.0.0-next.84",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@aws-sdk/client-apigatewaymanagementapi": "^3.588.0",
|
|
7
7
|
"@aws-sdk/client-kms": "^3.588.0",
|
|
@@ -38,9 +38,9 @@
|
|
|
38
38
|
}
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
|
-
"@friggframework/eslint-config": "2.0.0-next.
|
|
42
|
-
"@friggframework/prettier-config": "2.0.0-next.
|
|
43
|
-
"@friggframework/test": "2.0.0-next.
|
|
41
|
+
"@friggframework/eslint-config": "2.0.0-next.84",
|
|
42
|
+
"@friggframework/prettier-config": "2.0.0-next.84",
|
|
43
|
+
"@friggframework/test": "2.0.0-next.84",
|
|
44
44
|
"@prisma/client": "^6.17.0",
|
|
45
45
|
"@types/lodash": "4.17.15",
|
|
46
46
|
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
@@ -80,5 +80,5 @@
|
|
|
80
80
|
"publishConfig": {
|
|
81
81
|
"access": "public"
|
|
82
82
|
},
|
|
83
|
-
"gitHead": "
|
|
83
|
+
"gitHead": "c0b32d4d1f558fc8f737fd2731f0d67e84509ca2"
|
|
84
84
|
}
|