@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.
Files changed (217) hide show
  1. package/CHANGELOG.md +0 -16
  2. package/CLAUDE.md +41 -29
  3. package/README.md +0 -105
  4. package/application/commands/README.md +17 -11
  5. package/application/commands/entity-commands.js +18 -60
  6. package/application/commands/integration-commands.js +73 -20
  7. package/application/commands/scheduler-commands.js +7 -11
  8. package/application/commands/user-commands.js +5 -17
  9. package/application/index.js +2 -6
  10. package/core/Worker.js +7 -31
  11. package/core/create-handler.js +0 -9
  12. package/credential/repositories/credential-repository-documentdb.js +0 -34
  13. package/credential/repositories/credential-repository-interface.js +0 -15
  14. package/credential/repositories/credential-repository-mongo.js +0 -23
  15. package/credential/repositories/credential-repository-postgres.js +0 -28
  16. package/database/config.js +24 -48
  17. package/database/documentdb-encryption-service.js +22 -9
  18. package/database/documentdb-utils.js +0 -2
  19. package/database/encryption/README.md +17 -81
  20. package/database/encryption/documentdb-encryption-service.md +1 -78
  21. package/database/encryption/encryption-schema-registry.js +46 -123
  22. package/database/encryption/field-encryption-service.js +5 -69
  23. package/database/encryption/integration-mapping-encryption.js +1 -1
  24. package/database/prisma.js +38 -15
  25. package/database/use-cases/check-encryption-health-use-case.js +52 -43
  26. package/errors/index.js +4 -0
  27. package/errors/mapping-already-exists-error.js +14 -0
  28. package/generated/prisma-mongodb/edge.js +4 -43
  29. package/generated/prisma-mongodb/index-browser.js +1 -40
  30. package/generated/prisma-mongodb/index.d.ts +111 -4739
  31. package/generated/prisma-mongodb/index.js +4 -43
  32. package/generated/prisma-mongodb/package.json +1 -1
  33. package/generated/prisma-mongodb/schema.prisma +0 -92
  34. package/generated/prisma-mongodb/wasm.js +4 -43
  35. package/generated/prisma-postgresql/edge.js +4 -43
  36. package/generated/prisma-postgresql/index-browser.js +1 -40
  37. package/generated/prisma-postgresql/index.d.ts +33 -4966
  38. package/generated/prisma-postgresql/index.js +4 -43
  39. package/generated/prisma-postgresql/package.json +1 -1
  40. package/generated/prisma-postgresql/schema.prisma +0 -87
  41. package/generated/prisma-postgresql/wasm.js +4 -43
  42. package/handlers/WEBHOOKS.md +29 -0
  43. package/handlers/app-definition-loader.js +19 -121
  44. package/handlers/app-handler-helpers.js +3 -25
  45. package/handlers/backend-utils.js +25 -31
  46. package/handlers/middleware/admin-auth.js +1 -14
  47. package/handlers/routers/auth.js +10 -46
  48. package/handlers/routers/health.js +4 -13
  49. package/handlers/routers/integration-defined-routers.js +92 -144
  50. package/handlers/routers/integration-webhook-routers.js +64 -102
  51. package/handlers/workers/integration-defined-workers.js +2 -5
  52. package/index.js +3 -65
  53. package/infrastructure/scheduler/eventbridge-scheduler-adapter.js +67 -13
  54. package/infrastructure/scheduler/index.js +6 -19
  55. package/infrastructure/scheduler/scheduler-service-factory.js +7 -55
  56. package/integrations/index.js +0 -1
  57. package/integrations/integration-base.js +29 -0
  58. package/integrations/integration-router.js +96 -131
  59. package/integrations/repositories/integration-mapping-repository-documentdb.js +50 -0
  60. package/integrations/repositories/integration-mapping-repository-interface.js +15 -0
  61. package/integrations/repositories/integration-mapping-repository-mongo.js +29 -0
  62. package/integrations/repositories/integration-mapping-repository-postgres.js +30 -0
  63. package/integrations/repositories/integration-mapping-repository.js +29 -0
  64. package/integrations/repositories/integration-repository-documentdb.js +35 -35
  65. package/integrations/repositories/integration-repository-interface.js +31 -17
  66. package/integrations/repositories/integration-repository-mongo.js +129 -60
  67. package/integrations/repositories/integration-repository-postgres.js +134 -62
  68. package/integrations/repositories/process-repository-mongo.js +1 -1
  69. package/integrations/tests/doubles/test-integration-repository.js +1 -12
  70. package/integrations/use-cases/add-entities-to-integration.js +92 -0
  71. package/integrations/use-cases/create-integration.js +75 -72
  72. package/integrations/use-cases/get-integrations-for-user.js +40 -80
  73. package/integrations/use-cases/index.js +1 -7
  74. package/integrations/use-cases/remove-entities-from-integration.js +70 -0
  75. package/integrations/utils/assert-entity-ids-input.js +42 -0
  76. package/integrations/utils/map-integration-dto.js +14 -43
  77. package/jest-setup.js +13 -2
  78. package/jest-teardown.js +5 -2
  79. package/jest.config.js +4 -0
  80. package/jest.db.config.js +23 -0
  81. package/modules/index.js +0 -14
  82. package/modules/module-factory.js +0 -12
  83. package/modules/module.js +2 -53
  84. package/modules/repositories/module-repository-documentdb.js +6 -51
  85. package/modules/repositories/module-repository-interface.js +2 -19
  86. package/modules/repositories/module-repository-mongo.js +74 -67
  87. package/modules/repositories/module-repository-postgres.js +74 -66
  88. package/modules/repositories/module-repository.js +1 -2
  89. package/modules/requester/requester.js +2 -27
  90. package/modules/use-cases/get-entities-for-user.js +13 -26
  91. package/modules/use-cases/get-entity-options-by-id.js +4 -10
  92. package/modules/use-cases/get-entity-options-by-type.js +1 -1
  93. package/modules/use-cases/get-module-instance-from-type.js +1 -1
  94. package/modules/use-cases/get-module.js +4 -10
  95. package/modules/use-cases/process-authorization-callback.js +19 -53
  96. package/modules/use-cases/refresh-entity-options.js +4 -10
  97. package/modules/use-cases/test-module-auth.js +4 -10
  98. package/modules/utils/map-module-dto.js +5 -3
  99. package/modules/utils/to-credential-id.js +14 -0
  100. package/package.json +7 -6
  101. package/prisma-mongodb/schema.prisma +0 -92
  102. package/prisma-postgresql/schema.prisma +0 -87
  103. package/queues/index.js +0 -25
  104. package/queues/queuer-util.js +6 -5
  105. package/types/index.d.ts +0 -2
  106. package/user/repositories/user-repository-documentdb.js +2 -27
  107. package/user/repositories/user-repository-interface.js +0 -18
  108. package/user/repositories/user-repository-mongo.js +0 -24
  109. package/user/repositories/user-repository-postgres.js +0 -18
  110. package/user/tests/doubles/test-user-repository.js +1 -1
  111. package/user/use-cases/authenticate-user.js +1 -7
  112. package/user/use-cases/get-user-from-adopter-jwt.js +124 -95
  113. package/user/use-cases/get-user-from-bearer-token.js +7 -2
  114. package/user/use-cases/get-user-from-x-frigg-headers.js +80 -83
  115. package/user/user.js +1 -64
  116. package/credential/use-cases/credential-operations-for-user.js +0 -202
  117. package/credential/use-cases/reauthorize-credential-for-user.js +0 -73
  118. package/database/encryption/encryption-config.js +0 -219
  119. package/database/encryption/test-support/fake-ciphertext.js +0 -15
  120. package/database/organization-where.js +0 -18
  121. package/database/unique-violation.js +0 -25
  122. package/extensions/bootstrap-runner.js +0 -43
  123. package/extensions/extension-loader.js +0 -187
  124. package/extensions/index.js +0 -41
  125. package/extensions/initialize-app.js +0 -123
  126. package/extensions/route-mounter.js +0 -103
  127. package/extensions/schema-composer.js +0 -188
  128. package/infrastructure/scheduler/netlify-scheduler-adapter.js +0 -220
  129. package/infrastructure/scheduler/scheduled-job-repository.js +0 -118
  130. package/integrations/repositories/integration-not-found.js +0 -14
  131. package/integrations/schedule-config.js +0 -80
  132. package/integrations/use-cases/dispatch-ongoing-syncs.js +0 -100
  133. package/integrations/use-cases/integration-operations-for-user.js +0 -256
  134. package/integrations/use-cases/resolve-integration-ownership.js +0 -177
  135. package/management-api/create-management-api-router.js +0 -49
  136. package/management-api/error-response.js +0 -116
  137. package/management-api/management-api-config.js +0 -151
  138. package/management-api/meta-router.js +0 -76
  139. package/management-api/openapi/build-openapi.js +0 -144
  140. package/management-api/openapi/components.js +0 -408
  141. package/management-api/openapi/v1-operations.js +0 -83
  142. package/management-api/openapi/v2-operations.js +0 -264
  143. package/management-api/route-registry.js +0 -411
  144. package/management-api/test-support/http-client.js +0 -39
  145. package/management-api/use-cases/get-management-api-meta.js +0 -57
  146. package/management-api/v1-deprecation.js +0 -120
  147. package/management-api/v2/create-v2-router.js +0 -85
  148. package/management-api/v2/dependencies.js +0 -281
  149. package/management-api/v2/dto.js +0 -143
  150. package/management-api/v2/handlers/authorization.js +0 -94
  151. package/management-api/v2/handlers/credentials.js +0 -33
  152. package/management-api/v2/handlers/entities.js +0 -77
  153. package/management-api/v2/handlers/integrations.js +0 -128
  154. package/management-api/v2/handlers/proxy.js +0 -34
  155. package/management-api/v2/request.js +0 -85
  156. package/management-api/v2/test-support/fake-dependencies.js +0 -103
  157. package/modules/domain/authorization-session.js +0 -144
  158. package/modules/entity-scope.js +0 -15
  159. package/modules/entity-type-registry.js +0 -311
  160. package/modules/proxy/proxy-errors.js +0 -20
  161. package/modules/proxy/proxy-path.js +0 -179
  162. package/modules/proxy/proxy-policy.js +0 -248
  163. package/modules/repositories/authorization-session-repository-documentdb.js +0 -112
  164. package/modules/repositories/authorization-session-repository-factory.js +0 -31
  165. package/modules/repositories/authorization-session-repository-interface.js +0 -47
  166. package/modules/repositories/authorization-session-repository-prisma.js +0 -87
  167. package/modules/repositories/entity-not-found.js +0 -18
  168. package/modules/requester/proxy-request.js +0 -244
  169. package/modules/scope-config.js +0 -192
  170. package/modules/scope-policy.js +0 -112
  171. package/modules/scope-resolution.js +0 -288
  172. package/modules/scope-visibility.js +0 -247
  173. package/modules/tests/doubles/in-memory-authorization-session-repository.js +0 -45
  174. package/modules/tests/doubles/ownership-matching.js +0 -18
  175. package/modules/use-cases/authorize-entity-for-user.js +0 -61
  176. package/modules/use-cases/create-entity-from-credential.js +0 -113
  177. package/modules/use-cases/entity-operations-for-user.js +0 -162
  178. package/modules/use-cases/entity-types.js +0 -139
  179. package/modules/use-cases/execute-entity-proxy-request.js +0 -286
  180. package/modules/use-cases/get-authorization-requirements.js +0 -100
  181. package/modules/use-cases/get-entity-module-for-user.js +0 -113
  182. package/modules/use-cases/manage-access.js +0 -21
  183. package/modules/use-cases/multi-step-authorization.js +0 -329
  184. package/prisma-postgresql/migrations/20261003000000_add_authorization_session/migration.sql +0 -28
  185. package/prisma-postgresql/migrations/20261015000000_add_organization_membership/migration.sql +0 -26
  186. package/prisma-postgresql/migrations/20261016000000_add_entity_integration_organization/migration.sql +0 -25
  187. package/prisma-postgresql/migrations/20261017000000_add_scheduled_job/migration.sql +0 -28
  188. package/providers/index.js +0 -23
  189. package/providers/resolve-provider.js +0 -93
  190. package/providers/runtime-detection.js +0 -124
  191. package/queues/providers/index.js +0 -11
  192. package/queues/providers/netlify-background-provider.js +0 -187
  193. package/queues/providers/qstash-queue-provider.js +0 -219
  194. package/queues/providers/sqs-queue-provider.js +0 -70
  195. package/queues/queue-client-interface.js +0 -55
  196. package/queues/queue-message-auth.js +0 -193
  197. package/queues/queue-provider-factory.js +0 -101
  198. package/queues/queue-provider.js +0 -65
  199. package/queues/sqs-queue-client.js +0 -81
  200. package/types/modules/index.d.ts +0 -197
  201. package/types/user/index.d.ts +0 -134
  202. package/user/adopter-jwt-config.js +0 -177
  203. package/user/adopter-jwt-errors.js +0 -59
  204. package/user/adopter-token-verifier.js +0 -122
  205. package/user/app-user-helpers.js +0 -97
  206. package/user/create-user-authentication.js +0 -64
  207. package/user/membership.js +0 -51
  208. package/user/repositories/organization-membership-record.js +0 -19
  209. package/user/repositories/organization-membership-repository-documentdb.js +0 -106
  210. package/user/repositories/organization-membership-repository-factory.js +0 -42
  211. package/user/repositories/organization-membership-repository-interface.js +0 -57
  212. package/user/repositories/organization-membership-repository-mongo.js +0 -13
  213. package/user/repositories/organization-membership-repository-postgres.js +0 -29
  214. package/user/repositories/organization-membership-repository-prisma.js +0 -81
  215. package/user/tests/doubles/adopter-jwt-fixtures.js +0 -220
  216. package/user/use-cases/ensure-organization-membership.js +0 -92
  217. 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. **Fails Closed**: Same rule as the Prisma client; a deployed runtime with no key throws, only local runs skip encryption
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, entities) {
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
- // `entities` is the app definition's registry (ADR-059): a module an app
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 moduleDefinitions = getEntityTypeDefinitions(
192
- getEntityTypeRegistry({ integrations, entities })
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 credentialFields = extractCredentialFieldsFromModules(moduleDefinitions);
195
-
196
- // Core fields are encrypted already and a custom schema may not repeat
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) {
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
- const moduleSchema = {
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: { ...customSchema.Credential, fields },
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 ${fields.length} credential fields`
213
+ `Registering module-level encryption for ${credentialFields.length} credential fields`
218
214
  );
219
215
 
220
- registerCustomSchema(moduleSchema);
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
- * Registers encryption schemas declared by app-level extensions
234
- * (`appDefinition.coreExtensions[].encryption`), e.g. the db-credentials
235
- * extension's `OAuthAppCredential.clientSecret`.
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
- if (Object.keys(schema).length > 0) {
257
- logger.info(
258
- `Registering encryption schema from extension "${ext.name}" for models: ${Object.keys(schema).join(', ')}`
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
- function readAppDefinitionForEncryption() {
277
- // Lazy require to avoid circular dependency issues
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
- const backendDir = path.dirname(backendPackagePath);
297
- const backendIndexPath = path.join(backendDir, 'index.js');
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) || appDefinition.entities) {
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 (!this._isEncrypted(value)) {
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, FieldDecryptionError };
254
+ module.exports = { FieldEncryptionService };
@@ -1,4 +1,4 @@
1
- const { getEncryptionConfig } = require('./encryption-config');
1
+ const { getEncryptionConfig } = require('../prisma');
2
2
  const { Cryptor } = require('../../encrypt/Cryptor');
3
3
  const {
4
4
  getFieldsToDecryptOnRead,
@@ -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 candidate of paths) {
74
+ for (const path of paths) {
51
75
  try {
52
- return require(candidate).PrismaClient;
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('Failed to initialize encryption extension:', error);
105
- throw error;
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.error) {
30
- return {
31
- status: 'unhealthy',
32
- ...summary,
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
- ...summary,
42
- testResult: this._disabledReason(config),
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
- return { ...testResults, ...summary };
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
- ...summary,
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 config = resolveEncryptionConfig(process.env);
70
- return { ...config, stage: process.env.STAGE || null };
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
  };