@friggframework/core 2.0.0--canary.686.df38e8e.0 → 2.0.0--canary.710.9045bbc.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 -16
- package/CLAUDE.md +41 -29
- package/README.md +0 -105
- package/application/commands/README.md +17 -11
- 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 +17 -81
- package/database/encryption/documentdb-encryption-service.md +1 -78
- package/database/encryption/encryption-schema-registry.js +46 -123
- 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 -121
- 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/eventbridge-scheduler-adapter.js +67 -13
- 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/jest-setup.js +13 -2
- package/jest-teardown.js +5 -2
- package/jest.config.js +4 -0
- package/jest.db.config.js +23 -0
- 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 +2 -27
- 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 +7 -6
- package/prisma-mongodb/schema.prisma +0 -92
- package/prisma-postgresql/schema.prisma +0 -87
- package/queues/index.js +0 -25
- package/queues/queuer-util.js +6 -5
- 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 -220
- 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 -408
- 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-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/repositories/entity-not-found.js +0 -18
- package/modules/requester/proxy-request.js +0 -244
- 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 -286
- 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 -187
- 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 -197
- 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
|
@@ -225,84 +225,7 @@ Database (ENCRYPTED STORAGE) ✅ SECURE
|
|
|
225
225
|
1. **Consistency**: Same encryption format and Cryptor as Prisma Extension
|
|
226
226
|
2. **Reusability**: Single service used by all DocumentDB repositories
|
|
227
227
|
3. **Schema-Driven**: Uses `encryption-schema-registry.js` (same as Prisma)
|
|
228
|
-
4. **
|
|
229
|
-
5. **Error-Tolerant**: Graceful handling of decryption failures
|
|
230
|
-
6. **Testable**: Can be unit tested independently of repositories
|
|
231
|
-
|
|
232
|
-
---
|
|
233
|
-
|
|
234
|
-
## Technical Specification
|
|
235
|
-
|
|
236
|
-
### Class Design
|
|
237
|
-
|
|
238
|
-
```javascript
|
|
239
|
-
/**
|
|
240
|
-
* Encryption service specifically for DocumentDB repositories
|
|
241
|
-
* that use $runCommandRaw and bypass Prisma Extensions.
|
|
242
|
-
*
|
|
243
|
-
* Provides document-level encryption/decryption,
|
|
244
|
-
* handling nested fields according to the encryption schema registry.
|
|
245
|
-
*/
|
|
246
|
-
class DocumentDBEncryptionService {
|
|
247
|
-
constructor()
|
|
248
|
-
_initializeCryptor()
|
|
249
|
-
async encryptFields(modelName, document)
|
|
250
|
-
async decryptFields(modelName, document)
|
|
251
|
-
async _encryptFieldPath(document, fieldPath, modelName)
|
|
252
|
-
async _decryptFieldPath(document, fieldPath, modelName)
|
|
253
|
-
_isEncryptedValue(value)
|
|
254
|
-
}
|
|
255
|
-
```
|
|
256
|
-
|
|
257
|
-
### Method Specifications
|
|
258
|
-
|
|
259
|
-
#### `constructor()`
|
|
260
|
-
|
|
261
|
-
**Purpose**: Initialize the service and configure Cryptor
|
|
262
|
-
|
|
263
|
-
**Behavior**:
|
|
264
|
-
|
|
265
|
-
- Calls `_initializeCryptor()` immediately
|
|
266
|
-
- Sets up `this.cryptor` and `this.enabled` properties
|
|
267
|
-
|
|
268
|
-
**No parameters**
|
|
269
|
-
|
|
270
|
-
---
|
|
271
|
-
|
|
272
|
-
#### `_initializeCryptor()`
|
|
273
|
-
|
|
274
|
-
**Purpose**: Initialize Cryptor with environment-based configuration
|
|
275
|
-
|
|
276
|
-
**Logic**:
|
|
277
|
-
|
|
278
|
-
```javascript
|
|
279
|
-
1. loadCustomEncryptionSchema()
|
|
280
|
-
2. config = getEncryptionConfig() // encryption/encryption-config.js, shared with the Prisma client
|
|
281
|
-
- throws EncryptionConfigurationError in a deployed runtime with no key
|
|
282
|
-
3. If !config.enabled (local dev/test/local stage, local run without keys,
|
|
283
|
-
or FRIGG_ENCRYPTION_DISABLED=true opt-out):
|
|
284
|
-
- Set this.cryptor = null
|
|
285
|
-
- Set this.enabled = false
|
|
286
|
-
- Return
|
|
287
|
-
4. Create Cryptor({ shouldUseAws: config.method === 'kms' })
|
|
288
|
-
5. Set this.enabled = true
|
|
289
|
-
```
|
|
290
|
-
|
|
291
|
-
**Environment Variables Used** (see "When Encryption Runs" in README.md):
|
|
292
|
-
|
|
293
|
-
- `AWS_LAMBDA_FUNCTION_NAME` / `LAMBDA_TASK_ROOT` / `AWS_EXECUTION_ENV=AWS_Lambda_*`, or a non-`aws` `FRIGG_PROVIDER`, minus `IS_OFFLINE` / `IS_LOCAL` / `NETLIFY_DEV` / `JEST_WORKER_ID`: deployed or local run
|
|
294
|
-
- `STAGE` or `NODE_ENV`: dev/test/local skip encryption on local runs only
|
|
295
|
-
- `KMS_KEY_ARN`: AWS KMS key ARN (enables KMS encryption)
|
|
296
|
-
- `AES_KEY_ID`: AES key identifier (enables AES encryption)
|
|
297
|
-
- `AES_KEY`: AES encryption key (required with AES_KEY_ID in a deployed runtime)
|
|
298
|
-
- `FRIGG_ENCRYPTION_DISABLED`: explicit plaintext opt-out for a deployed stage with no key
|
|
299
|
-
|
|
300
|
-
### Design Principles
|
|
301
|
-
|
|
302
|
-
1. **Consistency**: Same encryption format and Cryptor as Prisma Extension
|
|
303
|
-
2. **Reusability**: Single service used by all DocumentDB repositories
|
|
304
|
-
3. **Schema-Driven**: Uses `encryption-schema-registry.js` (same as Prisma)
|
|
305
|
-
4. **Fails Closed**: Same rule as the Prisma client; a deployed runtime with no key throws, only local runs skip encryption
|
|
228
|
+
4. **Environment-Aware**: Respects STAGE-based bypass (dev/test/local)
|
|
306
229
|
5. **Error-Tolerant**: Graceful handling of decryption failures
|
|
307
230
|
6. **Testable**: Can be unit tested independently of repositories
|
|
308
231
|
|
|
@@ -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,42 +170,50 @@ 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
|
-
|
|
177
|
+
const { getModulesDefinitionFromIntegrationClasses } = require('../../integrations/utils/map-integration-dto');
|
|
178
|
+
|
|
179
|
+
// Integrations without modules contribute no credential fields.
|
|
180
|
+
const integrationsWithModules = integrations.filter(
|
|
181
|
+
(integration) => integration?.Definition?.modules
|
|
193
182
|
);
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
//
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
if (fields.length === 0) {
|
|
183
|
+
const moduleDefinitions = getModulesDefinitionFromIntegrationClasses(
|
|
184
|
+
integrationsWithModules
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
// Fields the core schema already encrypts (e.g. data.api_key) are
|
|
188
|
+
// covered; registering them again would fail validation and drop every
|
|
189
|
+
// other module field with them.
|
|
190
|
+
const coreCredentialFields = CORE_ENCRYPTION_SCHEMA.Credential.fields;
|
|
191
|
+
const credentialFields = extractCredentialFieldsFromModules(
|
|
192
|
+
moduleDefinitions
|
|
193
|
+
).filter((field) => !coreCredentialFields.includes(field));
|
|
194
|
+
|
|
195
|
+
if (credentialFields.length === 0) {
|
|
208
196
|
return;
|
|
209
197
|
}
|
|
210
198
|
|
|
211
|
-
|
|
199
|
+
// Merge with whatever custom schema the app already registered rather
|
|
200
|
+
// than replacing it (registerCustomSchema overwrites).
|
|
201
|
+
const existingCredentialFields = customSchema.Credential?.fields || [];
|
|
202
|
+
const mergedSchema = {
|
|
212
203
|
...customSchema,
|
|
213
|
-
Credential: {
|
|
204
|
+
Credential: {
|
|
205
|
+
...customSchema.Credential,
|
|
206
|
+
fields: [
|
|
207
|
+
...new Set([...existingCredentialFields, ...credentialFields]),
|
|
208
|
+
],
|
|
209
|
+
},
|
|
214
210
|
};
|
|
215
211
|
|
|
216
212
|
logger.info(
|
|
217
|
-
`Registering module-level encryption for ${
|
|
213
|
+
`Registering module-level encryption for ${credentialFields.length} credential fields`
|
|
218
214
|
);
|
|
219
215
|
|
|
220
|
-
registerCustomSchema(
|
|
216
|
+
registerCustomSchema(mergedSchema);
|
|
221
217
|
}
|
|
222
218
|
|
|
223
219
|
/**
|
|
@@ -229,80 +225,22 @@ function loadModuleEncryptionSchemas(integrations, entities) {
|
|
|
229
225
|
*
|
|
230
226
|
* Used by both Prisma (MongoDB/PostgreSQL) and DocumentDB encryption services.
|
|
231
227
|
*/
|
|
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
|
-
}
|
|
228
|
+
function loadCustomEncryptionSchema() {
|
|
229
|
+
try {
|
|
230
|
+
// Lazy require to avoid circular dependency issues
|
|
231
|
+
const path = require('node:path');
|
|
232
|
+
const { findNearestBackendPackageJson } = require('../../utils');
|
|
255
233
|
|
|
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);
|
|
234
|
+
const backendPackagePath = findNearestBackendPackageJson();
|
|
235
|
+
if (!backendPackagePath) {
|
|
236
|
+
return; // No backend found, skip custom schema
|
|
272
237
|
}
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
238
|
|
|
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
|
-
}
|
|
239
|
+
const backendDir = path.dirname(backendPackagePath);
|
|
240
|
+
const backendIndexPath = path.join(backendDir, 'index.js');
|
|
295
241
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
const backendModule = require(backendIndexPath);
|
|
300
|
-
return backendModule?.Definition || null;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
function loadCustomEncryptionSchema() {
|
|
304
|
-
try {
|
|
305
|
-
const appDefinition = readAppDefinitionForEncryption();
|
|
242
|
+
const backendModule = require(backendIndexPath);
|
|
243
|
+
const appDefinition = backendModule?.Definition;
|
|
306
244
|
|
|
307
245
|
if (!appDefinition) {
|
|
308
246
|
return; // No app definition found
|
|
@@ -322,26 +260,12 @@ function loadCustomEncryptionSchema() {
|
|
|
322
260
|
registerEncryptionOptOut(disable);
|
|
323
261
|
}
|
|
324
262
|
|
|
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
263
|
// Load module-level encryption schemas from integrations
|
|
335
264
|
const integrations = appDefinition.integrations;
|
|
336
|
-
if (Array.isArray(integrations)
|
|
337
|
-
loadModuleEncryptionSchemas(
|
|
338
|
-
Array.isArray(integrations) ? integrations : [],
|
|
339
|
-
appDefinition.entities
|
|
340
|
-
);
|
|
265
|
+
if (integrations && Array.isArray(integrations)) {
|
|
266
|
+
loadModuleEncryptionSchemas(integrations);
|
|
341
267
|
}
|
|
342
268
|
} catch (error) {
|
|
343
|
-
// A bad entity registry is a configuration error, not "no custom schema".
|
|
344
|
-
if (error instanceof EntityRegistryError) throw error;
|
|
345
269
|
// Silently ignore errors - custom schema is optional
|
|
346
270
|
// This handles cases like:
|
|
347
271
|
// - Backend package.json not found (tests, standalone usage)
|
|
@@ -492,7 +416,6 @@ module.exports = {
|
|
|
492
416
|
registerEncryptionOptOut,
|
|
493
417
|
loadCustomEncryptionSchema,
|
|
494
418
|
loadModuleEncryptionSchemas,
|
|
495
|
-
loadExtensionEncryptionSchemas,
|
|
496
419
|
extractCredentialFieldsFromModules,
|
|
497
420
|
validateCustomSchema,
|
|
498
421
|
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
|
};
|