@friggframework/core 2.0.0--canary.680.6bacd26.0 → 2.0.0--canary.663.8de669c.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +0 -11
- package/application/commands/integration-commands.js +8 -18
- package/application/index.js +2 -6
- package/credential/repositories/credential-repository-documentdb.js +0 -34
- package/credential/repositories/credential-repository-interface.js +0 -15
- package/credential/repositories/credential-repository-mongo.js +0 -23
- package/credential/repositories/credential-repository-postgres.js +0 -28
- package/database/encryption/README.md +0 -2
- package/database/encryption/encryption-schema-registry.js +12 -41
- package/database/encryption/field-encryption-service.js +5 -69
- package/generated/prisma-mongodb/edge.js +4 -20
- package/generated/prisma-mongodb/index-browser.js +1 -17
- package/generated/prisma-mongodb/index.d.ts +95 -1543
- package/generated/prisma-mongodb/index.js +4 -20
- package/generated/prisma-mongodb/package.json +1 -1
- package/generated/prisma-mongodb/schema.prisma +0 -27
- package/generated/prisma-mongodb/wasm.js +4 -20
- package/generated/prisma-postgresql/edge.js +4 -20
- package/generated/prisma-postgresql/index-browser.js +1 -17
- package/generated/prisma-postgresql/index.d.ts +84 -1565
- package/generated/prisma-postgresql/index.js +4 -20
- package/generated/prisma-postgresql/package.json +1 -1
- package/generated/prisma-postgresql/schema.prisma +0 -26
- package/generated/prisma-postgresql/wasm.js +4 -20
- package/handlers/app-definition-loader.js +8 -19
- package/handlers/app-handler-helpers.js +3 -25
- package/handlers/backend-utils.js +25 -31
- package/handlers/middleware/admin-auth.js +1 -14
- package/handlers/routers/auth.js +3 -11
- package/handlers/routers/health.js +4 -11
- package/handlers/workers/integration-defined-workers.js +2 -5
- package/index.js +2 -4
- package/integrations/index.js +4 -0
- package/integrations/integration-router.js +23 -16
- package/integrations/utils/map-integration-dto.js +15 -1
- package/modules/index.js +0 -6
- package/modules/requester/requester.js +2 -27
- package/package.json +5 -5
- package/prisma-mongodb/schema.prisma +0 -27
- package/prisma-postgresql/schema.prisma +0 -26
- package/types/index.d.ts +0 -1
- package/credential/use-cases/credential-operations-for-user.js +0 -117
- package/credential/use-cases/reauthorize-credential-for-user.js +0 -66
- package/database/encryption/test-support/fake-ciphertext.js +0 -15
- package/integrations/use-cases/integration-operations-for-user.js +0 -225
- package/management-api/create-management-api-router.js +0 -49
- package/management-api/error-response.js +0 -116
- package/management-api/management-api-config.js +0 -82
- package/management-api/meta-router.js +0 -57
- package/management-api/openapi/build-openapi.js +0 -144
- package/management-api/openapi/components.js +0 -368
- package/management-api/openapi/v1-operations.js +0 -83
- package/management-api/openapi/v2-operations.js +0 -252
- package/management-api/route-registry.js +0 -294
- package/management-api/test-support/http-client.js +0 -39
- package/management-api/use-cases/get-management-api-meta.js +0 -57
- package/management-api/v1-deprecation.js +0 -120
- package/management-api/v2/create-v2-router.js +0 -85
- package/management-api/v2/dependencies.js +0 -256
- package/management-api/v2/dto.js +0 -123
- package/management-api/v2/handlers/authorization.js +0 -93
- package/management-api/v2/handlers/credentials.js +0 -29
- package/management-api/v2/handlers/entities.js +0 -74
- package/management-api/v2/handlers/integrations.js +0 -125
- package/management-api/v2/handlers/proxy.js +0 -34
- package/management-api/v2/request.js +0 -54
- package/management-api/v2/test-support/fake-dependencies.js +0 -103
- package/modules/domain/authorization-session.js +0 -144
- package/modules/entity-type-registry.js +0 -311
- package/modules/proxy/proxy-errors.js +0 -20
- package/modules/proxy/proxy-path.js +0 -179
- package/modules/proxy/proxy-policy.js +0 -248
- package/modules/repositories/authorization-session-repository-documentdb.js +0 -112
- package/modules/repositories/authorization-session-repository-factory.js +0 -31
- package/modules/repositories/authorization-session-repository-interface.js +0 -47
- package/modules/repositories/authorization-session-repository-prisma.js +0 -87
- package/modules/requester/proxy-request.js +0 -244
- package/modules/tests/doubles/in-memory-authorization-session-repository.js +0 -45
- package/modules/use-cases/entity-operations-for-user.js +0 -125
- package/modules/use-cases/entity-types.js +0 -115
- package/modules/use-cases/execute-entity-proxy-request.js +0 -239
- package/modules/use-cases/get-authorization-requirements.js +0 -100
- package/modules/use-cases/get-entity-module-for-user.js +0 -86
- package/modules/use-cases/multi-step-authorization.js +0 -302
- package/prisma-postgresql/migrations/20261003000000_add_authorization_session/migration.sql +0 -28
- package/types/modules/index.d.ts +0 -58
package/CHANGELOG.md
CHANGED
|
@@ -1,14 +1,3 @@
|
|
|
1
|
-
# Unreleased
|
|
2
|
-
|
|
3
|
-
#### Behaviour change: module credential fields are now encrypted
|
|
4
|
-
|
|
5
|
-
- `credentialFields` declared in an API module's `encryption` config (for example `Definition.encryption.credentialFields`) were never registered for encryption: `loadModuleEncryptionSchemas` required a path that does not exist and the error was swallowed. They are now registered, for modules bound by integrations and for modules registered in `appDefinition.entities` (ADR-059).
|
|
6
|
-
- Existing rows are not rewritten. A credential saved before this release stays plaintext until it is next written, and is read as stored.
|
|
7
|
-
- Reads now tolerate plaintext: a value is only treated as ciphertext when it has the exact `keyId:iv:ciphertext:encryptedKey` shape (previously any string with three or more colons, such as `https://user:pw@host:443`), and a value with that shape that fails to decrypt throws `FieldDecryptionError` in a deployed runtime (any stage, AWS Lambda or Netlify) and is returned as stored with a warning on a local run.
|
|
8
|
-
- An app's own `encryption.schema` is no longer replaced when module fields are added to it.
|
|
9
|
-
|
|
10
|
-
---
|
|
11
|
-
|
|
12
1
|
# v1.2.2 (Fri Aug 09 2024)
|
|
13
2
|
|
|
14
3
|
#### 🐛 Bug Fix
|
|
@@ -30,9 +30,8 @@ const {
|
|
|
30
30
|
PatchIntegrationConfig,
|
|
31
31
|
} = require('../../integrations/use-cases/patch-integration-config');
|
|
32
32
|
const {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
} = require('../../modules/entity-type-registry');
|
|
33
|
+
getModulesDefinitionFromIntegrationClasses,
|
|
34
|
+
} = require('../../integrations/utils/map-integration-dto');
|
|
36
35
|
|
|
37
36
|
const ERROR_CODE_MAP = {
|
|
38
37
|
ENTITY_NOT_FOUND: 401,
|
|
@@ -52,16 +51,7 @@ function mapErrorToResponse(error) {
|
|
|
52
51
|
};
|
|
53
52
|
}
|
|
54
53
|
|
|
55
|
-
|
|
56
|
-
* @param {Object} [params]
|
|
57
|
-
* @param {Function} [params.integrationClass]
|
|
58
|
-
* @param {Array<object>} [params.moduleDefinitions] - the app's offered module
|
|
59
|
-
* definitions (`loadAppDefinition().moduleDefinitions`). Integration code
|
|
60
|
-
* calls this from a constructor with no app definition in hand, so when it
|
|
61
|
-
* is omitted the commands hydrate with this integration's own modules only
|
|
62
|
-
* (a registry built from `{ integrations: [integrationClass] }`).
|
|
63
|
-
*/
|
|
64
|
-
function createIntegrationCommands({ integrationClass, moduleDefinitions } = {}) {
|
|
54
|
+
function createIntegrationCommands({ integrationClass } = {}) {
|
|
65
55
|
// Always use Frigg's default repositories and use cases
|
|
66
56
|
const integrationRepository = createIntegrationRepository();
|
|
67
57
|
|
|
@@ -118,13 +108,13 @@ function createIntegrationCommands({ integrationClass, moduleDefinitions } = {})
|
|
|
118
108
|
|
|
119
109
|
const moduleRepository = createModuleRepository();
|
|
120
110
|
|
|
111
|
+
const moduleDefinitions = getModulesDefinitionFromIntegrationClasses([
|
|
112
|
+
integrationClass,
|
|
113
|
+
]);
|
|
114
|
+
|
|
121
115
|
const moduleFactory = new ModuleFactory({
|
|
122
116
|
moduleRepository,
|
|
123
|
-
moduleDefinitions
|
|
124
|
-
moduleDefinitions ??
|
|
125
|
-
getEntityTypeDefinitions(
|
|
126
|
-
getEntityTypeRegistry({ integrations: [integrationClass] })
|
|
127
|
-
),
|
|
117
|
+
moduleDefinitions,
|
|
128
118
|
});
|
|
129
119
|
|
|
130
120
|
const loadIntegrationContextUseCase = new LoadIntegrationContextUseCase({
|
package/application/index.js
CHANGED
|
@@ -17,7 +17,6 @@ const { createUsageCommands } = require('./commands/usage-commands');
|
|
|
17
17
|
*
|
|
18
18
|
* @param {Object} params
|
|
19
19
|
* @param {Object} params.integrationClass - Integration class (required)
|
|
20
|
-
* @param {Array<object>} [params.moduleDefinitions] - the app's offered module definitions; defaults to the integration's own modules
|
|
21
20
|
* @returns {Object} Unified commands object with all CRUD operations
|
|
22
21
|
*
|
|
23
22
|
* @example
|
|
@@ -25,12 +24,9 @@ const { createUsageCommands } = require('./commands/usage-commands');
|
|
|
25
24
|
* const user = await commands.createUser({ username: 'user@example.com' });
|
|
26
25
|
* const credential = await commands.createCredential({ userId: user.id, ... });
|
|
27
26
|
*/
|
|
28
|
-
function createFriggCommands({ integrationClass
|
|
27
|
+
function createFriggCommands({ integrationClass }) {
|
|
29
28
|
// All commands use Frigg's default repositories and use cases
|
|
30
|
-
const integrationCommands = createIntegrationCommands({
|
|
31
|
-
integrationClass,
|
|
32
|
-
moduleDefinitions,
|
|
33
|
-
});
|
|
29
|
+
const integrationCommands = createIntegrationCommands({ integrationClass });
|
|
34
30
|
|
|
35
31
|
const userCommands = createUserCommands();
|
|
36
32
|
|
|
@@ -186,40 +186,6 @@ class CredentialRepositoryDocumentDB extends CredentialRepositoryInterface {
|
|
|
186
186
|
return this._mapCredential(decryptedCredential);
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
-
/**
|
|
190
|
-
* All credentials owned by a user, newest first.
|
|
191
|
-
*
|
|
192
|
-
* @param {string} userId - User ID
|
|
193
|
-
* @returns {Promise<Array<Object>>} Credentials with string IDs
|
|
194
|
-
*/
|
|
195
|
-
async findCredentialsByUserId(userId) {
|
|
196
|
-
const userObjectId = toObjectId(userId);
|
|
197
|
-
if (!userObjectId) return [];
|
|
198
|
-
const docs = await findManyDrained(
|
|
199
|
-
this.prisma,
|
|
200
|
-
'Credential',
|
|
201
|
-
{ userId: userObjectId },
|
|
202
|
-
{ sort: { createdAt: -1 } }
|
|
203
|
-
);
|
|
204
|
-
const credentials = [];
|
|
205
|
-
for (const doc of docs) {
|
|
206
|
-
const decrypted = await this.encryptionService.decryptFields(
|
|
207
|
-
'Credential',
|
|
208
|
-
doc
|
|
209
|
-
);
|
|
210
|
-
credentials.push({
|
|
211
|
-
...this._mapCredential(decrypted),
|
|
212
|
-
id: fromObjectId(doc._id),
|
|
213
|
-
userId: fromObjectId(doc.userId) ?? doc.userId,
|
|
214
|
-
externalId: doc.externalId ?? null,
|
|
215
|
-
authIsValid: doc.authIsValid ?? null,
|
|
216
|
-
createdAt: doc.createdAt ?? null,
|
|
217
|
-
updatedAt: doc.updatedAt ?? null,
|
|
218
|
-
});
|
|
219
|
-
}
|
|
220
|
-
return credentials;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
189
|
async updateCredential(credentialId, updates) {
|
|
224
190
|
const objectId = toObjectId(credentialId);
|
|
225
191
|
if (!objectId) return null;
|
|
@@ -80,21 +80,6 @@ class CredentialRepositoryInterface {
|
|
|
80
80
|
);
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
/**
|
|
84
|
-
* All credentials owned by a user, newest first.
|
|
85
|
-
*
|
|
86
|
-
* @param {string|number} userId - User ID (string from application layer)
|
|
87
|
-
* @returns {Promise<Array<Object>>} Credentials with string IDs, `createdAt`
|
|
88
|
-
* and `updatedAt`, and the (decrypted) data fields spread in, as
|
|
89
|
-
* findCredentialById returns them. Empty when the user has none.
|
|
90
|
-
* @abstract
|
|
91
|
-
*/
|
|
92
|
-
async findCredentialsByUserId(userId) {
|
|
93
|
-
throw new Error(
|
|
94
|
-
'Method findCredentialsByUserId must be implemented by subclass'
|
|
95
|
-
);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
83
|
/**
|
|
99
84
|
* Update a credential by ID
|
|
100
85
|
*
|
|
@@ -188,29 +188,6 @@ class CredentialRepositoryMongo extends CredentialRepositoryInterface {
|
|
|
188
188
|
};
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
-
/**
|
|
192
|
-
* All credentials owned by a user, newest first.
|
|
193
|
-
*
|
|
194
|
-
* @param {string} userId - User ID
|
|
195
|
-
* @returns {Promise<Array<Object>>} Credentials with string IDs
|
|
196
|
-
*/
|
|
197
|
-
async findCredentialsByUserId(userId) {
|
|
198
|
-
if (!userId) return [];
|
|
199
|
-
const credentials = await this.prisma.credential.findMany({
|
|
200
|
-
where: { userId: String(userId) },
|
|
201
|
-
orderBy: { createdAt: 'desc' },
|
|
202
|
-
});
|
|
203
|
-
return credentials.map((credential) => ({
|
|
204
|
-
...(credential.data || {}),
|
|
205
|
-
id: credential.id,
|
|
206
|
-
userId: credential.userId,
|
|
207
|
-
externalId: credential.externalId,
|
|
208
|
-
authIsValid: credential.authIsValid,
|
|
209
|
-
createdAt: credential.createdAt,
|
|
210
|
-
updatedAt: credential.updatedAt,
|
|
211
|
-
}));
|
|
212
|
-
}
|
|
213
|
-
|
|
214
191
|
/**
|
|
215
192
|
* Update a credential by ID
|
|
216
193
|
* Replaces: Credential.findByIdAndUpdate(credentialId, { $set: updates })
|
|
@@ -205,34 +205,6 @@ class CredentialRepositoryPostgres extends CredentialRepositoryInterface {
|
|
|
205
205
|
};
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
-
/**
|
|
209
|
-
* All credentials owned by a user, newest first.
|
|
210
|
-
*
|
|
211
|
-
* @param {string} userId - User ID (string from application layer)
|
|
212
|
-
* @returns {Promise<Array<Object>>} Credentials with string IDs
|
|
213
|
-
*/
|
|
214
|
-
async findCredentialsByUserId(userId) {
|
|
215
|
-
let intUserId;
|
|
216
|
-
try {
|
|
217
|
-
intUserId = this._convertId(userId);
|
|
218
|
-
} catch {
|
|
219
|
-
return [];
|
|
220
|
-
}
|
|
221
|
-
const credentials = await this.prisma.credential.findMany({
|
|
222
|
-
where: { userId: intUserId },
|
|
223
|
-
orderBy: { createdAt: 'desc' },
|
|
224
|
-
});
|
|
225
|
-
return credentials.map((credential) => ({
|
|
226
|
-
...(credential.data || {}),
|
|
227
|
-
id: credential.id.toString(),
|
|
228
|
-
userId: credential.userId?.toString(),
|
|
229
|
-
externalId: credential.externalId,
|
|
230
|
-
authIsValid: credential.authIsValid,
|
|
231
|
-
createdAt: credential.createdAt,
|
|
232
|
-
updatedAt: credential.updatedAt,
|
|
233
|
-
}));
|
|
234
|
-
}
|
|
235
|
-
|
|
236
208
|
/**
|
|
237
209
|
* Update a credential by ID
|
|
238
210
|
*
|
|
@@ -456,8 +456,6 @@ Format: "keyId:encryptedText:encryptedKey"
|
|
|
456
456
|
Example: "base64KeyId:iv:ciphertext:base64EncryptedDataKey"
|
|
457
457
|
```
|
|
458
458
|
|
|
459
|
-
**Recognising ciphertext on read.** A value is decrypted only when it has exactly four colon-separated parts: base64 `keyId`, 32-hex-character `iv`, hex ciphertext, base64 `encryptedKey`. Plaintext that merely contains colons (`https://user:pw@host:443`) is left alone. A value with that shape that still fails to decrypt (a wrong, rotated or missing key) fails the read with `FieldDecryptionError` in a deployed runtime (see [When Encryption Runs](#when-encryption-runs)), so ciphertext never reaches an API module as if it were a token. Locally it is returned as stored and logged as a warning, so one bad field does not fail a whole read. A field that becomes encrypted later (for example a module's `credentialFields`) stays plaintext in existing rows until they are next written.
|
|
460
|
-
|
|
461
459
|
**Why Envelope Encryption?**
|
|
462
460
|
|
|
463
461
|
- Reduces KMS API calls (one DEK per field, cached)
|
|
@@ -10,11 +10,6 @@
|
|
|
10
10
|
|
|
11
11
|
const { logger } = require('./logger');
|
|
12
12
|
const { registerDeniedKeys } = require('../../logs/denied-keys');
|
|
13
|
-
const {
|
|
14
|
-
getEntityTypeRegistry,
|
|
15
|
-
getEntityTypeDefinitions,
|
|
16
|
-
EntityRegistryError,
|
|
17
|
-
} = require('../../modules/entity-type-registry');
|
|
18
13
|
|
|
19
14
|
/**
|
|
20
15
|
* Core encryption schema (immutable - cannot be overridden by custom schemas)
|
|
@@ -44,12 +39,6 @@ const CORE_ENCRYPTION_SCHEMA = {
|
|
|
44
39
|
Token: {
|
|
45
40
|
fields: ['token'],
|
|
46
41
|
},
|
|
47
|
-
|
|
48
|
-
// Multi-step authorization (Management API v2): earlier steps can collect
|
|
49
|
-
// emails, usernames or one-time codes.
|
|
50
|
-
AuthorizationSession: {
|
|
51
|
-
fields: ['stepData'],
|
|
52
|
-
},
|
|
53
42
|
};
|
|
54
43
|
|
|
55
44
|
let customSchema = {};
|
|
@@ -171,9 +160,8 @@ function extractCredentialFieldsFromModules(moduleDefinitions) {
|
|
|
171
160
|
* Each module can declare credentialFields to encrypt in its encryption config.
|
|
172
161
|
*
|
|
173
162
|
* @param {Array} integrations - Array of integration classes with modules
|
|
174
|
-
* @param {object} [entities] - `appDefinition.entities` (ADR-059 registry)
|
|
175
163
|
*/
|
|
176
|
-
function loadModuleEncryptionSchemas(integrations
|
|
164
|
+
function loadModuleEncryptionSchemas(integrations) {
|
|
177
165
|
if (!integrations) {
|
|
178
166
|
throw new Error('integrations parameter is required');
|
|
179
167
|
}
|
|
@@ -182,39 +170,27 @@ function loadModuleEncryptionSchemas(integrations, entities) {
|
|
|
182
170
|
throw new Error('integrations must be an array');
|
|
183
171
|
}
|
|
184
172
|
|
|
185
|
-
|
|
186
|
-
// registers without any integration still carries credentials to encrypt.
|
|
187
|
-
if (integrations.length === 0 && !entities) {
|
|
173
|
+
if (integrations.length === 0) {
|
|
188
174
|
return;
|
|
189
175
|
}
|
|
190
176
|
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
);
|
|
177
|
+
const { getModulesDefinitionFromIntegrationClasses } = require('../integrations/utils/map-integration-dto');
|
|
178
|
+
|
|
179
|
+
const moduleDefinitions = getModulesDefinitionFromIntegrationClasses(integrations);
|
|
194
180
|
const credentialFields = extractCredentialFieldsFromModules(moduleDefinitions);
|
|
195
181
|
|
|
196
|
-
|
|
197
|
-
// them. Everything the app registered itself stays: module fields are added
|
|
198
|
-
// to the custom schema, never substituted for it (other models included).
|
|
199
|
-
const coreFields = new Set(CORE_ENCRYPTION_SCHEMA.Credential?.fields || []);
|
|
200
|
-
const fields = [
|
|
201
|
-
...new Set([
|
|
202
|
-
...(customSchema.Credential?.fields || []),
|
|
203
|
-
...credentialFields.filter((field) => !coreFields.has(field)),
|
|
204
|
-
]),
|
|
205
|
-
];
|
|
206
|
-
|
|
207
|
-
if (fields.length === 0) {
|
|
182
|
+
if (credentialFields.length === 0) {
|
|
208
183
|
return;
|
|
209
184
|
}
|
|
210
185
|
|
|
211
186
|
const moduleSchema = {
|
|
212
|
-
|
|
213
|
-
|
|
187
|
+
Credential: {
|
|
188
|
+
fields: credentialFields
|
|
189
|
+
}
|
|
214
190
|
};
|
|
215
191
|
|
|
216
192
|
logger.info(
|
|
217
|
-
`Registering module-level encryption for ${
|
|
193
|
+
`Registering module-level encryption for ${credentialFields.length} credential fields`
|
|
218
194
|
);
|
|
219
195
|
|
|
220
196
|
registerCustomSchema(moduleSchema);
|
|
@@ -266,15 +242,10 @@ function loadCustomEncryptionSchema() {
|
|
|
266
242
|
|
|
267
243
|
// Load module-level encryption schemas from integrations
|
|
268
244
|
const integrations = appDefinition.integrations;
|
|
269
|
-
if (Array.isArray(integrations)
|
|
270
|
-
loadModuleEncryptionSchemas(
|
|
271
|
-
Array.isArray(integrations) ? integrations : [],
|
|
272
|
-
appDefinition.entities
|
|
273
|
-
);
|
|
245
|
+
if (integrations && Array.isArray(integrations)) {
|
|
246
|
+
loadModuleEncryptionSchemas(integrations);
|
|
274
247
|
}
|
|
275
248
|
} catch (error) {
|
|
276
|
-
// A bad entity registry is a configuration error, not "no custom schema".
|
|
277
|
-
if (error instanceof EntityRegistryError) throw error;
|
|
278
249
|
// Silently ignore errors - custom schema is optional
|
|
279
250
|
// This handles cases like:
|
|
280
251
|
// - Backend package.json not found (tests, standalone usage)
|
|
@@ -4,41 +4,8 @@
|
|
|
4
4
|
* Infrastructure layer service that orchestrates field-level encryption/decryption.
|
|
5
5
|
* Handles nested JSON paths (e.g., 'data.access_token') and bulk operations.
|
|
6
6
|
*/
|
|
7
|
-
const { logger } = require('./logger');
|
|
8
|
-
const { isDeployedRuntime } = require('./encryption-config');
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Thrown when a value that looks like ciphertext cannot be decrypted in a
|
|
12
|
-
* deployed runtime (a wrong, rotated or missing key). Returning the stored
|
|
13
|
-
* ciphertext would hand it to API modules as if it were a token.
|
|
14
|
-
*/
|
|
15
|
-
class FieldDecryptionError extends Error {
|
|
16
|
-
constructor(message, options) {
|
|
17
|
-
super(message, options);
|
|
18
|
-
this.name = 'FieldDecryptionError';
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// Cryptor.encrypt() returns `${keyId}:${iv}:${ciphertext}:${encryptedKey}`:
|
|
23
|
-
// keyId and encryptedKey are base64, iv is 16 bytes of hex, ciphertext is hex.
|
|
24
|
-
// Plaintext with three colons (`https://user:pw@host:443`, `a:b:c:d`) must not
|
|
25
|
-
// be mistaken for it.
|
|
26
|
-
const BASE64_PART = /^[A-Za-z0-9+/]+={0,2}$/;
|
|
27
|
-
const IV_PART = /^[0-9a-f]{32}$/i;
|
|
28
|
-
const CIPHERTEXT_PART = /^(?:[0-9a-f]{2})+$/i;
|
|
29
|
-
|
|
30
7
|
class FieldEncryptionService {
|
|
31
|
-
|
|
32
|
-
* @param {Object} params
|
|
33
|
-
* @param {Object} params.cryptor
|
|
34
|
-
* @param {Object} params.schema
|
|
35
|
-
* @param {() => boolean} [params.failClosedOnDecryptError] - whether a
|
|
36
|
-
* ciphertext-shaped value that fails to decrypt throws. Defaults to
|
|
37
|
-
* "running deployed" (encryption-config's isDeployedRuntime), read on
|
|
38
|
-
* each failure. Locally such a value is returned as stored, with a
|
|
39
|
-
* warning.
|
|
40
|
-
*/
|
|
41
|
-
constructor({ cryptor, schema, failClosedOnDecryptError }) {
|
|
8
|
+
constructor({ cryptor, schema }) {
|
|
42
9
|
if (!cryptor) {
|
|
43
10
|
throw new Error('Cryptor instance required');
|
|
44
11
|
}
|
|
@@ -48,10 +15,6 @@ class FieldEncryptionService {
|
|
|
48
15
|
|
|
49
16
|
this.cryptor = cryptor;
|
|
50
17
|
this.schema = schema;
|
|
51
|
-
this.failClosedOnDecryptError =
|
|
52
|
-
typeof failClosedOnDecryptError === 'function'
|
|
53
|
-
? failClosedOnDecryptError
|
|
54
|
-
: () => isDeployedRuntime();
|
|
55
18
|
}
|
|
56
19
|
|
|
57
20
|
/**
|
|
@@ -134,33 +97,12 @@ class FieldEncryptionService {
|
|
|
134
97
|
const decryptionPromises = fields.map(async (fieldPath) => {
|
|
135
98
|
const value = this._getNestedValue(decrypted, fieldPath);
|
|
136
99
|
|
|
137
|
-
if (
|
|
138
|
-
return null;
|
|
139
|
-
}
|
|
140
|
-
try {
|
|
100
|
+
if (this._isEncrypted(value)) {
|
|
141
101
|
const decryptedValue = await this.cryptor.decrypt(value);
|
|
142
102
|
const deserializedValue = this._deserializeAfterDecryption(decryptedValue);
|
|
143
103
|
return { fieldPath, decryptedValue: deserializedValue };
|
|
144
|
-
} catch (error) {
|
|
145
|
-
// Deployed: a ciphertext-shaped value under a wrong, rotated or
|
|
146
|
-
// missing key fails the read rather than reaching an API
|
|
147
|
-
// module as a token. Legacy plaintext never gets here: it does
|
|
148
|
-
// not look like ciphertext (_isEncrypted).
|
|
149
|
-
if (this.failClosedOnDecryptError()) {
|
|
150
|
-
throw new FieldDecryptionError(
|
|
151
|
-
`Could not decrypt ${modelName}.${fieldPath}`,
|
|
152
|
-
{ cause: error }
|
|
153
|
-
);
|
|
154
|
-
}
|
|
155
|
-
// Local: a value that looks like ciphertext but is not is read
|
|
156
|
-
// as it is stored. One such field must not fail the whole
|
|
157
|
-
// read, findMany included.
|
|
158
|
-
logger.warn(
|
|
159
|
-
`Could not decrypt ${modelName}.${fieldPath}; returning the stored value`,
|
|
160
|
-
{ errorName: error?.name }
|
|
161
|
-
);
|
|
162
|
-
return null;
|
|
163
104
|
}
|
|
105
|
+
return null;
|
|
164
106
|
});
|
|
165
107
|
|
|
166
108
|
const results = await Promise.all(decryptionPromises);
|
|
@@ -210,13 +152,7 @@ class FieldEncryptionService {
|
|
|
210
152
|
}
|
|
211
153
|
|
|
212
154
|
const parts = value.split(':');
|
|
213
|
-
return
|
|
214
|
-
parts.length === 4 &&
|
|
215
|
-
BASE64_PART.test(parts[0]) &&
|
|
216
|
-
IV_PART.test(parts[1]) &&
|
|
217
|
-
CIPHERTEXT_PART.test(parts[2]) &&
|
|
218
|
-
BASE64_PART.test(parts[3])
|
|
219
|
-
);
|
|
155
|
+
return parts.length >= 4;
|
|
220
156
|
}
|
|
221
157
|
|
|
222
158
|
_getNestedValue(obj, path) {
|
|
@@ -315,4 +251,4 @@ class FieldEncryptionService {
|
|
|
315
251
|
}
|
|
316
252
|
}
|
|
317
253
|
|
|
318
|
-
module.exports = { FieldEncryptionService
|
|
254
|
+
module.exports = { FieldEncryptionService };
|