@friggframework/core 2.0.0--canary.690.3fe2b27.0 → 2.0.0--canary.705.9cfd819.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/CLAUDE.md +40 -28
- package/README.md +0 -105
- package/application/commands/entity-commands.js +18 -60
- package/application/commands/integration-commands.js +73 -20
- package/application/commands/scheduler-commands.js +7 -11
- package/application/commands/user-commands.js +5 -17
- package/application/index.js +2 -6
- package/core/Worker.js +7 -31
- package/core/create-handler.js +0 -9
- 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/config.js +24 -48
- package/database/documentdb-encryption-service.js +22 -9
- package/database/documentdb-utils.js +0 -2
- package/database/encryption/README.md +14 -78
- package/database/encryption/documentdb-encryption-service.md +1 -78
- package/database/encryption/encryption-schema-registry.js +24 -121
- package/database/encryption/field-encryption-service.js +5 -69
- package/database/encryption/integration-mapping-encryption.js +1 -1
- package/database/prisma.js +38 -15
- package/database/use-cases/check-encryption-health-use-case.js +52 -43
- package/errors/index.js +4 -0
- package/errors/mapping-already-exists-error.js +14 -0
- package/generated/prisma-mongodb/edge.js +4 -43
- package/generated/prisma-mongodb/index-browser.js +1 -40
- package/generated/prisma-mongodb/index.d.ts +111 -4739
- package/generated/prisma-mongodb/index.js +4 -43
- package/generated/prisma-mongodb/package.json +1 -1
- package/generated/prisma-mongodb/schema.prisma +0 -92
- package/generated/prisma-mongodb/wasm.js +4 -43
- package/generated/prisma-postgresql/edge.js +4 -43
- package/generated/prisma-postgresql/index-browser.js +1 -40
- package/generated/prisma-postgresql/index.d.ts +33 -4966
- package/generated/prisma-postgresql/index.js +4 -43
- package/generated/prisma-postgresql/package.json +1 -1
- package/generated/prisma-postgresql/schema.prisma +0 -87
- package/generated/prisma-postgresql/wasm.js +4 -43
- package/handlers/WEBHOOKS.md +29 -0
- package/handlers/app-definition-loader.js +19 -128
- 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 +10 -46
- package/handlers/routers/health.js +4 -13
- package/handlers/routers/integration-defined-routers.js +92 -144
- package/handlers/routers/integration-webhook-routers.js +64 -102
- package/handlers/workers/integration-defined-workers.js +2 -5
- package/index.js +3 -65
- package/infrastructure/scheduler/index.js +6 -19
- package/infrastructure/scheduler/scheduler-service-factory.js +7 -55
- package/integrations/index.js +0 -1
- package/integrations/integration-base.js +29 -0
- package/integrations/integration-router.js +96 -131
- package/integrations/repositories/integration-mapping-repository-documentdb.js +50 -0
- package/integrations/repositories/integration-mapping-repository-interface.js +15 -0
- package/integrations/repositories/integration-mapping-repository-mongo.js +29 -0
- package/integrations/repositories/integration-mapping-repository-postgres.js +30 -0
- package/integrations/repositories/integration-mapping-repository.js +29 -0
- package/integrations/repositories/integration-repository-documentdb.js +35 -35
- package/integrations/repositories/integration-repository-interface.js +31 -17
- package/integrations/repositories/integration-repository-mongo.js +129 -60
- package/integrations/repositories/integration-repository-postgres.js +134 -62
- package/integrations/repositories/process-repository-mongo.js +1 -1
- package/integrations/tests/doubles/test-integration-repository.js +1 -12
- package/integrations/use-cases/add-entities-to-integration.js +92 -0
- package/integrations/use-cases/create-integration.js +75 -72
- package/integrations/use-cases/get-integrations-for-user.js +40 -80
- package/integrations/use-cases/index.js +1 -7
- package/integrations/use-cases/remove-entities-from-integration.js +70 -0
- package/integrations/utils/assert-entity-ids-input.js +42 -0
- package/integrations/utils/map-integration-dto.js +14 -43
- package/modules/index.js +0 -14
- package/modules/module-factory.js +0 -12
- package/modules/module.js +2 -53
- package/modules/repositories/module-repository-documentdb.js +6 -51
- package/modules/repositories/module-repository-interface.js +2 -19
- package/modules/repositories/module-repository-mongo.js +74 -67
- package/modules/repositories/module-repository-postgres.js +74 -66
- package/modules/repositories/module-repository.js +1 -2
- package/modules/requester/requester.js +5 -35
- package/modules/use-cases/get-entities-for-user.js +13 -26
- package/modules/use-cases/get-entity-options-by-id.js +4 -10
- package/modules/use-cases/get-entity-options-by-type.js +1 -1
- package/modules/use-cases/get-module-instance-from-type.js +1 -1
- package/modules/use-cases/get-module.js +4 -10
- package/modules/use-cases/process-authorization-callback.js +19 -53
- package/modules/use-cases/refresh-entity-options.js +4 -10
- package/modules/use-cases/test-module-auth.js +4 -10
- package/modules/utils/map-module-dto.js +5 -3
- package/modules/utils/to-credential-id.js +14 -0
- package/package.json +5 -6
- package/prisma-mongodb/schema.prisma +0 -92
- package/prisma-postgresql/schema.prisma +0 -87
- package/queues/index.js +0 -25
- package/types/index.d.ts +0 -2
- package/user/repositories/user-repository-documentdb.js +2 -27
- package/user/repositories/user-repository-interface.js +0 -18
- package/user/repositories/user-repository-mongo.js +0 -24
- package/user/repositories/user-repository-postgres.js +0 -18
- package/user/tests/doubles/test-user-repository.js +1 -1
- package/user/use-cases/authenticate-user.js +1 -7
- package/user/use-cases/get-user-from-adopter-jwt.js +124 -95
- package/user/use-cases/get-user-from-bearer-token.js +7 -2
- package/user/use-cases/get-user-from-x-frigg-headers.js +80 -83
- package/user/user.js +1 -64
- package/credential/use-cases/credential-operations-for-user.js +0 -202
- package/credential/use-cases/reauthorize-credential-for-user.js +0 -73
- package/database/encryption/encryption-config.js +0 -219
- package/database/encryption/test-support/fake-ciphertext.js +0 -15
- package/database/organization-where.js +0 -18
- package/database/unique-violation.js +0 -25
- package/extensions/bootstrap-runner.js +0 -43
- package/extensions/extension-loader.js +0 -187
- package/extensions/index.js +0 -41
- package/extensions/initialize-app.js +0 -123
- package/extensions/route-mounter.js +0 -103
- package/extensions/schema-composer.js +0 -188
- package/infrastructure/scheduler/netlify-scheduler-adapter.js +0 -216
- package/infrastructure/scheduler/scheduled-job-repository.js +0 -118
- package/integrations/repositories/integration-not-found.js +0 -14
- package/integrations/schedule-config.js +0 -80
- package/integrations/use-cases/dispatch-ongoing-syncs.js +0 -100
- package/integrations/use-cases/integration-operations-for-user.js +0 -256
- package/integrations/use-cases/resolve-integration-ownership.js +0 -177
- 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 -151
- package/management-api/meta-router.js +0 -76
- package/management-api/openapi/build-openapi.js +0 -144
- package/management-api/openapi/components.js +0 -415
- package/management-api/openapi/v1-operations.js +0 -83
- package/management-api/openapi/v2-operations.js +0 -264
- package/management-api/route-registry.js +0 -411
- 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 -281
- package/management-api/v2/dto.js +0 -143
- package/management-api/v2/handlers/authorization.js +0 -94
- package/management-api/v2/handlers/credentials.js +0 -33
- package/management-api/v2/handlers/entities.js +0 -77
- package/management-api/v2/handlers/integrations.js +0 -128
- package/management-api/v2/handlers/proxy.js +0 -34
- package/management-api/v2/request.js +0 -85
- package/management-api/v2/test-support/fake-dependencies.js +0 -103
- package/modules/domain/authorization-session.js +0 -144
- package/modules/entity-scope.js +0 -15
- package/modules/entity-type-registry.js +0 -311
- package/modules/proxy/proxy-bases.js +0 -169
- package/modules/proxy/proxy-errors.js +0 -31
- package/modules/proxy/proxy-path.js +0 -183
- package/modules/proxy/proxy-policy.js +0 -277
- 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/repositories/entity-not-found.js +0 -18
- package/modules/requester/proxy-request.js +0 -245
- package/modules/scope-config.js +0 -192
- package/modules/scope-policy.js +0 -112
- package/modules/scope-resolution.js +0 -288
- package/modules/scope-visibility.js +0 -247
- package/modules/tests/doubles/in-memory-authorization-session-repository.js +0 -45
- package/modules/tests/doubles/ownership-matching.js +0 -18
- package/modules/use-cases/authorize-entity-for-user.js +0 -61
- package/modules/use-cases/create-entity-from-credential.js +0 -113
- package/modules/use-cases/entity-operations-for-user.js +0 -162
- package/modules/use-cases/entity-types.js +0 -139
- package/modules/use-cases/execute-entity-proxy-request.js +0 -331
- package/modules/use-cases/get-authorization-requirements.js +0 -100
- package/modules/use-cases/get-entity-module-for-user.js +0 -113
- package/modules/use-cases/manage-access.js +0 -21
- package/modules/use-cases/multi-step-authorization.js +0 -329
- package/prisma-postgresql/migrations/20261003000000_add_authorization_session/migration.sql +0 -28
- package/prisma-postgresql/migrations/20261015000000_add_organization_membership/migration.sql +0 -26
- package/prisma-postgresql/migrations/20261016000000_add_entity_integration_organization/migration.sql +0 -25
- package/prisma-postgresql/migrations/20261017000000_add_scheduled_job/migration.sql +0 -28
- package/providers/index.js +0 -23
- package/providers/resolve-provider.js +0 -93
- package/providers/runtime-detection.js +0 -124
- package/queues/providers/index.js +0 -11
- package/queues/providers/netlify-background-provider.js +0 -150
- package/queues/providers/qstash-queue-provider.js +0 -219
- package/queues/providers/sqs-queue-provider.js +0 -70
- package/queues/queue-client-interface.js +0 -55
- package/queues/queue-message-auth.js +0 -193
- package/queues/queue-provider-factory.js +0 -101
- package/queues/queue-provider.js +0 -65
- package/queues/sqs-queue-client.js +0 -81
- package/types/modules/index.d.ts +0 -214
- package/types/user/index.d.ts +0 -134
- package/user/adopter-jwt-config.js +0 -177
- package/user/adopter-jwt-errors.js +0 -59
- package/user/adopter-token-verifier.js +0 -122
- package/user/app-user-helpers.js +0 -97
- package/user/create-user-authentication.js +0 -64
- package/user/membership.js +0 -51
- package/user/repositories/organization-membership-record.js +0 -19
- package/user/repositories/organization-membership-repository-documentdb.js +0 -106
- package/user/repositories/organization-membership-repository-factory.js +0 -42
- package/user/repositories/organization-membership-repository-interface.js +0 -57
- package/user/repositories/organization-membership-repository-mongo.js +0 -13
- package/user/repositories/organization-membership-repository-postgres.js +0 -29
- package/user/repositories/organization-membership-repository-prisma.js +0 -81
- package/user/tests/doubles/adopter-jwt-fixtures.js +0 -220
- package/user/use-cases/ensure-organization-membership.js +0 -92
- package/user/use-cases/find-individual-users-by-organization.js +0 -54
|
@@ -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);
|
|
@@ -229,80 +205,22 @@ function loadModuleEncryptionSchemas(integrations, entities) {
|
|
|
229
205
|
*
|
|
230
206
|
* Used by both Prisma (MongoDB/PostgreSQL) and DocumentDB encryption services.
|
|
231
207
|
*/
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
* @param {Array<Object>} extensions - Normalized extensions (extension-loader)
|
|
238
|
-
*/
|
|
239
|
-
function loadExtensionEncryptionSchemas(extensions) {
|
|
240
|
-
if (!extensions || !Array.isArray(extensions) || extensions.length === 0) {
|
|
241
|
-
return;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
for (const ext of extensions) {
|
|
245
|
-
if (!ext.encryption || typeof ext.encryption !== 'object') {
|
|
246
|
-
continue;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
const schema = {};
|
|
250
|
-
for (const [modelName, config] of Object.entries(ext.encryption)) {
|
|
251
|
-
if (config && Array.isArray(config.fields) && config.fields.length > 0) {
|
|
252
|
-
schema[modelName] = { fields: config.fields };
|
|
253
|
-
}
|
|
254
|
-
}
|
|
208
|
+
function loadCustomEncryptionSchema() {
|
|
209
|
+
try {
|
|
210
|
+
// Lazy require to avoid circular dependency issues
|
|
211
|
+
const path = require('node:path');
|
|
212
|
+
const { findNearestBackendPackageJson } = require('../../utils');
|
|
255
213
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
);
|
|
260
|
-
// registerCustomSchema replaces the registered schema, so merge:
|
|
261
|
-
// an extension adds its models to the app's encryption.schema
|
|
262
|
-
// rather than dropping it.
|
|
263
|
-
const merged = { ...customSchema };
|
|
264
|
-
for (const [modelName, config] of Object.entries(schema)) {
|
|
265
|
-
const existing = merged[modelName]?.fields || [];
|
|
266
|
-
merged[modelName] = {
|
|
267
|
-
...merged[modelName],
|
|
268
|
-
fields: [...new Set([...existing, ...config.fields])],
|
|
269
|
-
};
|
|
270
|
-
}
|
|
271
|
-
registerCustomSchema(merged);
|
|
214
|
+
const backendPackagePath = findNearestBackendPackageJson();
|
|
215
|
+
if (!backendPackagePath) {
|
|
216
|
+
return; // No backend found, skip custom schema
|
|
272
217
|
}
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
218
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
const {
|
|
279
|
-
getPresetAppDefinition,
|
|
280
|
-
} = require('../../handlers/app-definition-loader');
|
|
281
|
-
// A definition preset with setAppDefinition() (provider-netlify's
|
|
282
|
-
// preamble) wins: on Netlify process.cwd() cannot find the backend.
|
|
283
|
-
const preset = getPresetAppDefinition();
|
|
284
|
-
if (preset) {
|
|
285
|
-
return preset;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
const path = require('node:path');
|
|
289
|
-
const { findNearestBackendPackageJson } = require('../../utils');
|
|
290
|
-
|
|
291
|
-
const backendPackagePath = findNearestBackendPackageJson();
|
|
292
|
-
if (!backendPackagePath) {
|
|
293
|
-
return null; // No backend found, skip custom schema
|
|
294
|
-
}
|
|
219
|
+
const backendDir = path.dirname(backendPackagePath);
|
|
220
|
+
const backendIndexPath = path.join(backendDir, 'index.js');
|
|
295
221
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
const backendModule = require(backendIndexPath);
|
|
300
|
-
return backendModule?.Definition || null;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
function loadCustomEncryptionSchema() {
|
|
304
|
-
try {
|
|
305
|
-
const appDefinition = readAppDefinitionForEncryption();
|
|
222
|
+
const backendModule = require(backendIndexPath);
|
|
223
|
+
const appDefinition = backendModule?.Definition;
|
|
306
224
|
|
|
307
225
|
if (!appDefinition) {
|
|
308
226
|
return; // No app definition found
|
|
@@ -322,26 +240,12 @@ function loadCustomEncryptionSchema() {
|
|
|
322
240
|
registerEncryptionOptOut(disable);
|
|
323
241
|
}
|
|
324
242
|
|
|
325
|
-
// Load app-level extension encryption schemas. Before the module
|
|
326
|
-
// schemas: loadModuleEncryptionSchemas currently throws (see the
|
|
327
|
-
// NOTE inside it) and the throw would skip anything after it.
|
|
328
|
-
const { loadExtensions } = require('../../extensions/extension-loader');
|
|
329
|
-
const extensions = loadExtensions(appDefinition);
|
|
330
|
-
if (extensions.length > 0) {
|
|
331
|
-
loadExtensionEncryptionSchemas(extensions);
|
|
332
|
-
}
|
|
333
|
-
|
|
334
243
|
// Load module-level encryption schemas from integrations
|
|
335
244
|
const integrations = appDefinition.integrations;
|
|
336
|
-
if (Array.isArray(integrations)
|
|
337
|
-
loadModuleEncryptionSchemas(
|
|
338
|
-
Array.isArray(integrations) ? integrations : [],
|
|
339
|
-
appDefinition.entities
|
|
340
|
-
);
|
|
245
|
+
if (integrations && Array.isArray(integrations)) {
|
|
246
|
+
loadModuleEncryptionSchemas(integrations);
|
|
341
247
|
}
|
|
342
248
|
} catch (error) {
|
|
343
|
-
// A bad entity registry is a configuration error, not "no custom schema".
|
|
344
|
-
if (error instanceof EntityRegistryError) throw error;
|
|
345
249
|
// Silently ignore errors - custom schema is optional
|
|
346
250
|
// This handles cases like:
|
|
347
251
|
// - Backend package.json not found (tests, standalone usage)
|
|
@@ -492,7 +396,6 @@ module.exports = {
|
|
|
492
396
|
registerEncryptionOptOut,
|
|
493
397
|
loadCustomEncryptionSchema,
|
|
494
398
|
loadModuleEncryptionSchemas,
|
|
495
|
-
loadExtensionEncryptionSchemas,
|
|
496
399
|
extractCredentialFieldsFromModules,
|
|
497
400
|
validateCustomSchema,
|
|
498
401
|
validateOptOut,
|
|
@@ -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 };
|
package/database/prisma.js
CHANGED
|
@@ -4,7 +4,6 @@ const {
|
|
|
4
4
|
const { loadCustomEncryptionSchema } = require('./encryption/encryption-schema-registry');
|
|
5
5
|
const { logger } = require('./encryption/logger');
|
|
6
6
|
const { Cryptor } = require('../encrypt/Cryptor');
|
|
7
|
-
const { getEncryptionConfig } = require('./encryption/encryption-config');
|
|
8
7
|
const config = require('./config');
|
|
9
8
|
|
|
10
9
|
/**
|
|
@@ -33,6 +32,33 @@ function ensureMongoDbUrl() {
|
|
|
33
32
|
);
|
|
34
33
|
}
|
|
35
34
|
|
|
35
|
+
function getEncryptionConfig() {
|
|
36
|
+
const STAGE = process.env.STAGE || process.env.NODE_ENV || 'development';
|
|
37
|
+
const shouldBypassEncryption = ['dev', 'test', 'local'].includes(STAGE);
|
|
38
|
+
|
|
39
|
+
if (shouldBypassEncryption) {
|
|
40
|
+
return { enabled: false };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const hasKMS =
|
|
44
|
+
process.env.KMS_KEY_ARN && process.env.KMS_KEY_ARN.trim() !== '';
|
|
45
|
+
const hasAES =
|
|
46
|
+
process.env.AES_KEY_ID && process.env.AES_KEY_ID.trim() !== '';
|
|
47
|
+
|
|
48
|
+
if (!hasKMS && !hasAES) {
|
|
49
|
+
logger.warn(
|
|
50
|
+
'No encryption keys configured (KMS_KEY_ARN or AES_KEY_ID). ' +
|
|
51
|
+
'Field-level encryption disabled. Set STAGE=production and configure keys to enable.'
|
|
52
|
+
);
|
|
53
|
+
return { enabled: false };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
enabled: true,
|
|
58
|
+
method: hasKMS ? 'kms' : 'aes',
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
36
62
|
const prismaClientSingleton = () => {
|
|
37
63
|
let PrismaClient;
|
|
38
64
|
|
|
@@ -41,15 +67,13 @@ const prismaClientSingleton = () => {
|
|
|
41
67
|
const paths = [
|
|
42
68
|
// Lambda layer location (when using Prisma Lambda layer)
|
|
43
69
|
`/opt/nodejs/node_modules/generated/prisma-${dbType}`,
|
|
44
|
-
// __dirname-based absolute path (Netlify functions and bundlers)
|
|
45
|
-
require('node:path').resolve(__dirname, `../generated/prisma-${dbType}`),
|
|
46
70
|
// Local development location (relative to core package)
|
|
47
71
|
`../generated/prisma-${dbType}`,
|
|
48
72
|
];
|
|
49
73
|
|
|
50
|
-
for (const
|
|
74
|
+
for (const path of paths) {
|
|
51
75
|
try {
|
|
52
|
-
return require(
|
|
76
|
+
return require(path).PrismaClient;
|
|
53
77
|
} catch (err) {
|
|
54
78
|
// Continue to next path
|
|
55
79
|
}
|
|
@@ -79,13 +103,9 @@ const prismaClientSingleton = () => {
|
|
|
79
103
|
errorFormat: 'pretty',
|
|
80
104
|
});
|
|
81
105
|
|
|
82
|
-
// Throws when a deployed runtime has no encryption key: the app must not
|
|
83
|
-
// start and write sensitive fields in plaintext.
|
|
84
106
|
const encryptionConfig = getEncryptionConfig();
|
|
85
107
|
|
|
86
108
|
if (encryptionConfig.enabled) {
|
|
87
|
-
// Fail closed: if the extension cannot be installed, do not hand out
|
|
88
|
-
// a client that would write plaintext.
|
|
89
109
|
try {
|
|
90
110
|
// Load custom encryption schema from appDefinition before creating extension
|
|
91
111
|
loadCustomEncryptionSchema();
|
|
@@ -100,14 +120,17 @@ const prismaClientSingleton = () => {
|
|
|
100
120
|
enabled: true,
|
|
101
121
|
})
|
|
102
122
|
);
|
|
123
|
+
|
|
124
|
+
logger.info(
|
|
125
|
+
`Field-level encryption enabled using ${encryptionConfig.method.toUpperCase()}`
|
|
126
|
+
);
|
|
103
127
|
} catch (error) {
|
|
104
|
-
logger.error(
|
|
105
|
-
|
|
128
|
+
logger.error(
|
|
129
|
+
'Failed to initialize encryption extension:',
|
|
130
|
+
error
|
|
131
|
+
);
|
|
132
|
+
logger.warn('Continuing without encryption...');
|
|
106
133
|
}
|
|
107
|
-
|
|
108
|
-
logger.info(
|
|
109
|
-
`Field-level encryption enabled using ${encryptionConfig.method.toUpperCase()}`
|
|
110
|
-
);
|
|
111
134
|
} else {
|
|
112
135
|
logger.info('Field-level encryption disabled');
|
|
113
136
|
}
|
|
@@ -1,12 +1,3 @@
|
|
|
1
|
-
const {
|
|
2
|
-
resolveEncryptionConfig,
|
|
3
|
-
OPT_OUT_VAR,
|
|
4
|
-
} = require('../encryption/encryption-config');
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Reports whether field-level encryption is on and working, using the same
|
|
8
|
-
* rule the Prisma client applies at startup (encryption/encryption-config.js).
|
|
9
|
-
*/
|
|
10
1
|
class CheckEncryptionHealthUseCase {
|
|
11
2
|
constructor({ testEncryptionUseCase }) {
|
|
12
3
|
this.testEncryptionUseCase = testEncryptionUseCase;
|
|
@@ -14,60 +5,78 @@ class CheckEncryptionHealthUseCase {
|
|
|
14
5
|
|
|
15
6
|
async execute() {
|
|
16
7
|
const config = this._getEncryptionConfiguration();
|
|
17
|
-
const summary = {
|
|
18
|
-
mode: config.mode,
|
|
19
|
-
bypassed: config.bypassed,
|
|
20
|
-
optedOut: config.optedOut,
|
|
21
|
-
runtime: config.runtime,
|
|
22
|
-
stage: config.stage,
|
|
23
|
-
debug: {
|
|
24
|
-
hasKMS: config.hasKMS,
|
|
25
|
-
hasAES: config.hasAES,
|
|
26
|
-
},
|
|
27
|
-
};
|
|
28
8
|
|
|
29
|
-
if (config.
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
testResult: config.error,
|
|
34
|
-
encryptionWorks: false,
|
|
35
|
-
};
|
|
36
|
-
}
|
|
9
|
+
if (config.isBypassed || config.mode === 'none') {
|
|
10
|
+
const testResult = config.isBypassed
|
|
11
|
+
? 'Encryption bypassed for this stage'
|
|
12
|
+
: 'No encryption keys configured';
|
|
37
13
|
|
|
38
|
-
if (!config.enabled) {
|
|
39
14
|
return {
|
|
40
15
|
status: 'disabled',
|
|
41
|
-
|
|
42
|
-
|
|
16
|
+
mode: config.mode,
|
|
17
|
+
bypassed: config.isBypassed,
|
|
18
|
+
stage: config.stage,
|
|
19
|
+
testResult,
|
|
43
20
|
encryptionWorks: false,
|
|
21
|
+
debug: {
|
|
22
|
+
hasKMS: config.hasKMS,
|
|
23
|
+
hasAES: config.hasAES,
|
|
24
|
+
},
|
|
44
25
|
};
|
|
45
26
|
}
|
|
46
27
|
|
|
47
28
|
try {
|
|
48
29
|
const testResults = await this.testEncryptionUseCase.execute();
|
|
49
|
-
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
...testResults,
|
|
33
|
+
mode: config.mode,
|
|
34
|
+
bypassed: config.isBypassed,
|
|
35
|
+
stage: config.stage,
|
|
36
|
+
debug: {
|
|
37
|
+
hasKMS: config.hasKMS,
|
|
38
|
+
hasAES: config.hasAES,
|
|
39
|
+
},
|
|
40
|
+
};
|
|
50
41
|
} catch (error) {
|
|
51
42
|
return {
|
|
52
43
|
status: 'unhealthy',
|
|
53
|
-
|
|
44
|
+
mode: config.mode,
|
|
45
|
+
bypassed: config.isBypassed,
|
|
46
|
+
stage: config.stage,
|
|
54
47
|
testResult: `Encryption test failed: ${error.message}`,
|
|
55
48
|
encryptionWorks: false,
|
|
49
|
+
debug: {
|
|
50
|
+
hasKMS: config.hasKMS,
|
|
51
|
+
hasAES: config.hasAES,
|
|
52
|
+
},
|
|
56
53
|
};
|
|
57
54
|
}
|
|
58
55
|
}
|
|
59
56
|
|
|
60
|
-
_disabledReason(config) {
|
|
61
|
-
if (config.bypassed) return 'Encryption bypassed for this stage';
|
|
62
|
-
if (config.optedOut) {
|
|
63
|
-
return `Encryption explicitly disabled (${OPT_OUT_VAR}=true); sensitive fields are stored in plaintext`;
|
|
64
|
-
}
|
|
65
|
-
return 'No encryption keys configured';
|
|
66
|
-
}
|
|
67
|
-
|
|
68
57
|
_getEncryptionConfiguration() {
|
|
69
|
-
const
|
|
70
|
-
|
|
58
|
+
const { STAGE, BYPASS_ENCRYPTION_STAGE, KMS_KEY_ARN, AES_KEY_ID } =
|
|
59
|
+
process.env;
|
|
60
|
+
|
|
61
|
+
const defaultBypassStages = ['dev', 'test', 'local'];
|
|
62
|
+
const useEnv = BYPASS_ENCRYPTION_STAGE !== undefined;
|
|
63
|
+
const bypassStages = useEnv
|
|
64
|
+
? BYPASS_ENCRYPTION_STAGE.split(',').map((s) => s.trim())
|
|
65
|
+
: defaultBypassStages;
|
|
66
|
+
|
|
67
|
+
const isBypassed = bypassStages.includes(STAGE);
|
|
68
|
+
const hasAES = AES_KEY_ID && AES_KEY_ID.trim() !== '';
|
|
69
|
+
const hasKMS = KMS_KEY_ARN && KMS_KEY_ARN.trim() !== '';
|
|
70
|
+
// Prefer KMS over AES when both are configured (KMS is more secure)
|
|
71
|
+
const mode = hasKMS ? 'kms' : hasAES ? 'aes' : 'none';
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
stage: STAGE || null,
|
|
75
|
+
isBypassed,
|
|
76
|
+
hasAES,
|
|
77
|
+
hasKMS,
|
|
78
|
+
mode,
|
|
79
|
+
};
|
|
71
80
|
}
|
|
72
81
|
}
|
|
73
82
|
|
package/errors/index.js
CHANGED
|
@@ -6,6 +6,9 @@ const {
|
|
|
6
6
|
ParameterTypeError,
|
|
7
7
|
} = require('./validation-errors');
|
|
8
8
|
const { ClientSafeError } = require('./client-safe-error');
|
|
9
|
+
const {
|
|
10
|
+
MappingAlreadyExistsError,
|
|
11
|
+
} = require('./mapping-already-exists-error');
|
|
9
12
|
|
|
10
13
|
module.exports = {
|
|
11
14
|
BaseError,
|
|
@@ -14,4 +17,5 @@ module.exports = {
|
|
|
14
17
|
RequiredPropertyError,
|
|
15
18
|
ParameterTypeError,
|
|
16
19
|
ClientSafeError,
|
|
20
|
+
MappingAlreadyExistsError,
|
|
17
21
|
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const { BaseError } = require('./base-error');
|
|
2
|
+
|
|
3
|
+
class MappingAlreadyExistsError extends BaseError {
|
|
4
|
+
constructor(integrationId, sourceId, ...errorOptions) {
|
|
5
|
+
super(
|
|
6
|
+
`Mapping already exists for integration ${integrationId} and sourceId ${sourceId}`,
|
|
7
|
+
...errorOptions
|
|
8
|
+
);
|
|
9
|
+
this.integrationId = integrationId;
|
|
10
|
+
this.sourceId = sourceId;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
module.exports = { MappingAlreadyExistsError };
|