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