@friggframework/core 2.0.0--canary.667.a0620c7.0 → 2.0.0--canary.702.d5e6de1.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 (36) hide show
  1. package/CLAUDE.md +1 -41
  2. package/application/commands/README.md +11 -17
  3. package/application/commands/entity-commands.js +60 -18
  4. package/application/commands/integration-commands.js +0 -63
  5. package/database/documentdb-encryption-service.js +33 -2
  6. package/database/encryption/README.md +3 -3
  7. package/database/encryption/encryption-schema-registry.js +29 -109
  8. package/database/encryption/field-encryption-service.js +16 -13
  9. package/database/prisma.js +1 -10
  10. package/handlers/app-definition-loader.js +9 -2
  11. package/integrations/integration-router.js +22 -18
  12. package/integrations/repositories/integration-repository-documentdb.js +0 -32
  13. package/integrations/repositories/integration-repository-interface.js +0 -30
  14. package/integrations/repositories/integration-repository-mongo.js +0 -52
  15. package/integrations/repositories/integration-repository-postgres.js +0 -58
  16. package/integrations/use-cases/create-integration.js +19 -57
  17. package/jest-setup.js +2 -13
  18. package/jest-teardown.js +2 -5
  19. package/jest.config.js +0 -4
  20. package/modules/use-cases/get-entity-options-by-id.js +5 -7
  21. package/modules/use-cases/get-module.js +8 -29
  22. package/modules/use-cases/refresh-entity-options.js +5 -7
  23. package/modules/use-cases/test-module-auth.js +5 -7
  24. package/modules/utils/map-module-dto.js +3 -5
  25. package/package.json +8 -8
  26. package/queues/queuer-util.js +5 -6
  27. package/user/tests/doubles/test-user-repository.js +1 -1
  28. package/user/use-cases/get-user-from-bearer-token.js +2 -7
  29. package/database/encryption/ciphertext-format.js +0 -24
  30. package/integrations/use-cases/add-entities-to-integration.js +0 -92
  31. package/integrations/use-cases/remove-entities-from-integration.js +0 -70
  32. package/integrations/utils/assert-entity-ids-input.js +0 -42
  33. package/jest.db.config.js +0 -23
  34. package/modules/use-cases/create-entity-from-credential.js +0 -105
  35. package/modules/utils/find-module-definition.js +0 -23
  36. package/modules/utils/to-credential-id.js +0 -14
package/CLAUDE.md CHANGED
@@ -206,51 +206,11 @@ packages/core/
206
206
  - `delete-integration-for-user.js` - Remove integration
207
207
  - `get-integration-instance.js` - Load integration with modules
208
208
  - `load-integration-context.js` - Full integration context loading
209
- - `add-entities-to-integration.js` / `remove-entities-from-integration.js` - Attach or detach entities on an existing integration
210
209
 
211
210
  **Repositories**:
212
211
  - `integration-repository-factory.js` - Creates database-specific repositories
213
212
  - `integration-repository-mongo.js` - MongoDB implementation
214
213
  - `integration-repository-postgres.js` - PostgreSQL implementation
215
- - `addEntitiesToIntegration(integrationId, entityIds)` /
216
- `removeEntitiesFromIntegration(integrationId, entityIds)` change the entity
217
- set of an existing integration (all three adapters; `null` for an unknown
218
- integration). Reach them through the `addEntitiesToIntegration` /
219
- `removeEntitiesFromIntegration` commands. The add use case
220
- (`add-entities-to-integration.js`) rejects unknown entities
221
- (`ENTITIES_NOT_FOUND`, 404), entities of another user
222
- (`ENTITY_USER_MISMATCH`, 403) and entities of a module the integration does
223
- not already hold (`ENTITY_MODULE_MISMATCH`, 400). The remove use case
224
- (`remove-entities-from-integration.js`) refuses to leave a module with no
225
- entity (`LAST_ENTITY_OF_MODULE`, 409). Re-adding an attached entity or
226
- removing a detached one changes nothing; a detached entity is not deleted.
227
- `integration-repository-entities-parity.db.test.js` runs every adapter against
228
- real databases when `INTEGRATION_ENTITIES_PARITY_MONGO_URL` /
229
- `INTEGRATION_ENTITIES_PARITY_POSTGRES_URL` are set. The DocumentDB adapter
230
- writes only `Integration.entityIds` (as its `createIntegration` does), not
231
- `Entity.integrationIds`.
232
- **One credential, many accounts**: when one credential covers several
233
- provider accounts, give each account its own entity (externalId = account
234
- id) and attach them all to one integration; webhooks then route with
235
- `findIntegrationByEntityExternalId`. Traps:
236
- (1) `IntegrationBase` keys modules by module name, so `this.<moduleKey>`
237
- holds only one entity's module, and on the queue/webhook path
238
- (`get-integration-instance.js`) `this.entities` holds entity **ids**, not
239
- records. For per-account work (a webhook for one account, teardown of every
240
- account in `onDelete`), load
241
- `this.commands.loadIntegrationContextById(this.id)` and use
242
- `context.modules`: one module per entity, each with its record on
243
- `module.entity` (pick by `module.entity.externalId`).
244
- (2) `findIntegrationByEntityExternalId` counts every entity with that
245
- externalId, attached or not, across users: reuse an existing entity
246
- (`findEntity`) when an account comes back, or delete it when it is revoked,
247
- never create a second one.
248
- (3) `createIntegration` reuses an existing integration of the same user
249
- and type whose entities include all requested ones (an exact match wins),
250
- so a reconnect that sends the original entities finds the integration that
251
- now holds more.
252
- (4) Every module's delegate is the integration: an auth failure on any
253
- account marks the shared credential invalid and the integration ERROR.
254
214
  - `integration-mapping-repository-*.js` - Mapping data persistence.
255
215
  `queryMappings(integrationId, { where, orderBy, skip, take, omit })`
256
216
  returns `{ mappings, total }`: one filtered, ordered page of an
@@ -266,7 +226,7 @@ packages/core/
266
226
  mapping keys to leave out of the rows; never write such rows back. Path
267
227
  segments must match `^[A-Za-z_][A-Za-z0-9_]*$`. The PostgreSQL, MongoDB and
268
228
  DocumentDB adapters give the same pages and totals;
269
- `integration-mapping-repository-query-parity.db.test.js` checks that against
229
+ `integration-mapping-repository-query-parity.test.js` checks that against
270
230
  real databases when `QUERY_MAPPINGS_PARITY_MONGO_URL` /
271
231
  `QUERY_MAPPINGS_PARITY_POSTGRES_URL` are set. Two orderings still differ:
272
232
  strings compare by the database collation on PostgreSQL and by code point
@@ -67,10 +67,9 @@ class MyIntegration extends IntegrationBase {
67
67
  async hydrateFromExternalUser(externalUserId) {
68
68
  // Find integration context by external entity ID
69
69
  const result =
70
- await this.commands.findIntegrationContextByExternalEntityId({
71
- externalId: externalUserId,
72
- type: 'my-integration', // integration config.type to match
73
- });
70
+ await this.commands.findIntegrationContextByExternalEntityId(
71
+ externalUserId
72
+ );
74
73
 
75
74
  if (result.error) {
76
75
  return { error: result.error };
@@ -232,10 +231,9 @@ Manage integration records and load full integration contexts.
232
231
  ```javascript
233
232
  // Find integration context by external entity ID
234
233
  // Returns { context, error } where context includes record + hydrated modules
235
- const result = await commands.findIntegrationContextByExternalEntityId({
236
- externalId: 'external-user-or-workspace-id',
237
- type: 'my-integration', // integration config.type to match
238
- });
234
+ const result = await commands.findIntegrationContextByExternalEntityId(
235
+ 'external-user-or-workspace-id'
236
+ );
239
237
 
240
238
  if (!result.error) {
241
239
  integration.setIntegrationRecord(result.context);
@@ -301,10 +299,7 @@ try {
301
299
  For integration context operations, errors are returned in the result:
302
300
 
303
301
  ```javascript
304
- const result = await commands.findIntegrationContextByExternalEntityId({
305
- externalId: userId,
306
- type: 'my-integration',
307
- });
302
+ const result = await commands.findIntegrationContextByExternalEntityId(userId);
308
303
 
309
304
  if (result.error) {
310
305
  return { error: result.error };
@@ -354,10 +349,9 @@ const result =
354
349
  const { createFriggCommands } = require('@friggframework/core');
355
350
 
356
351
  this.commands = createFriggCommands({ integrationClass: MyIntegration });
357
- const result = await this.commands.findIntegrationContextByExternalEntityId({
358
- externalId: userId,
359
- type: 'my-integration',
360
- });
352
+ const result = await this.commands.findIntegrationContextByExternalEntityId(
353
+ userId
354
+ );
361
355
  ```
362
356
 
363
357
  ## Best Practices
@@ -439,7 +433,7 @@ if (!user) {
439
433
  | | `findEntitiesByIds(ids)` | Find multiple by IDs |
440
434
  | | `updateEntity(id, updates)` | Update entity properties |
441
435
  | | `deleteEntity(id)` | Delete entity |
442
- | **Integration** | `findIntegrationContextByExternalEntityId({ externalId, type })` | Load integration + modules by external ID |
436
+ | **Integration** | `findIntegrationContextByExternalEntityId(externalId)` | Load integration + modules by external ID |
443
437
  | | `loadIntegrationContextById(integrationId)` | Load integration + modules by ID |
444
438
 
445
439
  ## Support
@@ -1,24 +1,12 @@
1
1
  const {
2
2
  createModuleRepository,
3
3
  } = require('../../modules/repositories/module-repository-factory');
4
- const { toCredentialId } = require('../../modules/utils/to-credential-id');
5
4
 
6
5
  const ERROR_CODE_MAP = {
7
6
  ENTITY_NOT_FOUND: 404,
8
7
  INVALID_ENTITY_DATA: 400,
9
8
  };
10
9
 
11
- function toEntityDto(entity) {
12
- return {
13
- id: entity.id,
14
- userId: entity.userId,
15
- externalId: entity.externalId,
16
- name: entity.name,
17
- moduleName: entity.moduleName,
18
- credentialId: toCredentialId(entity.credential),
19
- };
20
- }
21
-
22
10
  function mapErrorToResponse(error) {
23
11
  const status = ERROR_CODE_MAP[error?.code] || 500;
24
12
  return {
@@ -78,7 +66,16 @@ function createEntityCommands() {
78
66
 
79
67
  const entity = await moduleRepo.createEntity(entityData);
80
68
 
81
- return toEntityDto(entity);
69
+ return {
70
+ id: entity.id,
71
+ userId: entity.userId,
72
+ externalId: entity.externalId,
73
+ name: entity.name,
74
+ moduleName: entity.moduleName,
75
+ credentialId: entity.credential?._id
76
+ ? entity.credential._id.toString()
77
+ : entity.credential,
78
+ };
82
79
  } catch (error) {
83
80
  return mapErrorToResponse(error);
84
81
  }
@@ -112,7 +109,16 @@ function createEntityCommands() {
112
109
  return null;
113
110
  }
114
111
 
115
- return toEntityDto(entity);
112
+ return {
113
+ id: entity.id,
114
+ userId: entity.userId,
115
+ externalId: entity.externalId,
116
+ name: entity.name,
117
+ moduleName: entity.moduleName,
118
+ credentialId: entity.credential?._id
119
+ ? entity.credential._id.toString()
120
+ : entity.credential,
121
+ };
116
122
  } catch (error) {
117
123
  return mapErrorToResponse(error);
118
124
  }
@@ -133,7 +139,16 @@ function createEntityCommands() {
133
139
 
134
140
  const entities = await moduleRepo.findEntitiesByUserId(userId);
135
141
 
136
- return entities.map((entity) => toEntityDto(entity));
142
+ return entities.map((entity) => ({
143
+ id: entity.id,
144
+ userId: entity.userId,
145
+ externalId: entity.externalId,
146
+ name: entity.name,
147
+ moduleName: entity.moduleName,
148
+ credentialId: entity.credential?._id
149
+ ? entity.credential._id.toString()
150
+ : entity.credential,
151
+ }));
137
152
  } catch (error) {
138
153
  if (error.code) {
139
154
  return mapErrorToResponse(error);
@@ -165,7 +180,16 @@ function createEntityCommands() {
165
180
  moduleName
166
181
  );
167
182
 
168
- return entities.map((entity) => toEntityDto(entity));
183
+ return entities.map((entity) => ({
184
+ id: entity.id,
185
+ userId: entity.userId,
186
+ externalId: entity.externalId,
187
+ name: entity.name,
188
+ moduleName: entity.moduleName,
189
+ credentialId: entity.credential?._id
190
+ ? entity.credential._id.toString()
191
+ : entity.credential,
192
+ }));
169
193
  } catch (error) {
170
194
  if (error.code) {
171
195
  return mapErrorToResponse(error);
@@ -189,7 +213,16 @@ function createEntityCommands() {
189
213
 
190
214
  const entity = await moduleRepo.findEntityById(entityId);
191
215
 
192
- return toEntityDto(entity);
216
+ return {
217
+ id: entity.id,
218
+ userId: entity.userId,
219
+ externalId: entity.externalId,
220
+ name: entity.name,
221
+ moduleName: entity.moduleName,
222
+ credentialId: entity.credential?._id
223
+ ? entity.credential._id.toString()
224
+ : entity.credential,
225
+ };
193
226
  } catch (error) {
194
227
  return mapErrorToResponse(error);
195
228
  }
@@ -217,7 +250,16 @@ function createEntityCommands() {
217
250
  throw error;
218
251
  }
219
252
 
220
- return toEntityDto(entity);
253
+ return {
254
+ id: entity.id,
255
+ userId: entity.userId,
256
+ externalId: entity.externalId,
257
+ name: entity.name,
258
+ moduleName: entity.moduleName,
259
+ credentialId: entity.credential?._id
260
+ ? entity.credential._id.toString()
261
+ : entity.credential,
262
+ };
221
263
  } catch (error) {
222
264
  return mapErrorToResponse(error);
223
265
  }
@@ -29,12 +29,6 @@ const {
29
29
  const {
30
30
  PatchIntegrationConfig,
31
31
  } = require('../../integrations/use-cases/patch-integration-config');
32
- const {
33
- AddEntitiesToIntegration,
34
- } = require('../../integrations/use-cases/add-entities-to-integration');
35
- const {
36
- RemoveEntitiesFromIntegration,
37
- } = require('../../integrations/use-cases/remove-entities-from-integration');
38
32
  const {
39
33
  getModulesDefinitionFromIntegrationClasses,
40
34
  } = require('../../integrations/utils/map-integration-dto');
@@ -46,11 +40,6 @@ const ERROR_CODE_MAP = {
46
40
  EXTERNAL_ID_REQUIRED: 400,
47
41
  TYPE_REQUIRED: 400,
48
42
  INTEGRATION_RECORD_NOT_FOUND: 404,
49
- INVALID_INTEGRATION_DATA: 400,
50
- ENTITIES_NOT_FOUND: 404,
51
- ENTITY_USER_MISMATCH: 403,
52
- ENTITY_MODULE_MISMATCH: 400,
53
- LAST_ENTITY_OF_MODULE: 409,
54
43
  };
55
44
 
56
45
  function mapErrorToResponse(error) {
@@ -174,17 +163,6 @@ function createIntegrationCommands({ integrationClass } = {}) {
174
163
  integrationRepository,
175
164
  });
176
165
 
177
- const addEntitiesToIntegrationUseCase = new AddEntitiesToIntegration({
178
- integrationRepository,
179
- moduleRepository,
180
- });
181
-
182
- const removeEntitiesFromIntegrationUseCase =
183
- new RemoveEntitiesFromIntegration({
184
- integrationRepository,
185
- moduleRepository,
186
- });
187
-
188
166
  return {
189
167
  findIntegrationById,
190
168
  listIntegrations,
@@ -329,47 +307,6 @@ function createIntegrationCommands({ integrationClass } = {}) {
329
307
  }
330
308
  },
331
309
 
332
- /**
333
- * Attach entities to an existing integration. Every entity must
334
- * exist, belong to the integration's user, and be of a module the
335
- * integration already holds; entities that are already attached are
336
- * left as they are.
337
- * @param {Object} params
338
- * @param {string} params.integrationId - Integration ID
339
- * @param {Array<string>} params.entityIds - Entity IDs to attach
340
- * @returns {Promise<Object>} Updated integration
341
- */
342
- async addEntitiesToIntegration({ integrationId, entityIds }) {
343
- try {
344
- return await addEntitiesToIntegrationUseCase.execute(
345
- integrationId,
346
- entityIds
347
- );
348
- } catch (error) {
349
- return mapErrorToResponse(error);
350
- }
351
- },
352
-
353
- /**
354
- * Detach entities from an integration. Entities that are not
355
- * attached are ignored; detached entities are not deleted. Each
356
- * module must keep at least one entity.
357
- * @param {Object} params
358
- * @param {string} params.integrationId - Integration ID
359
- * @param {Array<string>} params.entityIds - Entity IDs to detach
360
- * @returns {Promise<Object>} Updated integration
361
- */
362
- async removeEntitiesFromIntegration({ integrationId, entityIds }) {
363
- try {
364
- return await removeEntitiesFromIntegrationUseCase.execute(
365
- integrationId,
366
- entityIds
367
- );
368
- } catch (error) {
369
- return mapErrorToResponse(error);
370
- }
371
- },
372
-
373
310
  /**
374
311
  * Delete an integration by ID
375
312
  * @param {string} integrationId - Integration ID to delete
@@ -1,5 +1,4 @@
1
1
  const { Cryptor } = require('../encrypt/Cryptor');
2
- const { isEncryptedEnvelope } = require('./encryption/ciphertext-format');
3
2
  const {
4
3
  getEncryptedFields,
5
4
  getFieldsToEncryptOnWrite,
@@ -296,7 +295,39 @@ class DocumentDBEncryptionService {
296
295
  * _isEncryptedValue({}) // false
297
296
  */
298
297
  _isEncryptedValue(value) {
299
- return isEncryptedEnvelope(value);
298
+ // Must be string
299
+ if (typeof value !== 'string') {
300
+ return false;
301
+ }
302
+
303
+ // Must have exactly 4 colon-separated parts
304
+ const parts = value.split(':');
305
+ if (parts.length !== 4) {
306
+ return false;
307
+ }
308
+
309
+ // Enhanced validation: check for base64 pattern
310
+ // This prevents false positives on URLs, connection strings, etc.
311
+ const base64Pattern = /^[A-Za-z0-9+/=]+$/;
312
+
313
+ // All parts should be base64-encoded
314
+ if (!parts.every(part => base64Pattern.test(part))) {
315
+ return false;
316
+ }
317
+
318
+ // Encrypted values should be sufficiently long to be valid
319
+ // Real encrypted values from Cryptor are always >50 chars due to envelope encryption format:
320
+ // - keyId (base64): ~12 chars minimum
321
+ // - iv (base64): ~24 chars for 16-byte IV
322
+ // - ciphertext (base64): varies, minimum ~16 chars for small values
323
+ // - encryptedKey (base64): ~44 chars for 32-byte data key
324
+ // Total minimum: ~96 chars, so 50 is a safe lower bound
325
+ // This prevents false positives on non-encrypted strings that happen to have 4 colons
326
+ if (value.length < 50) {
327
+ return false;
328
+ }
329
+
330
+ return true;
300
331
  }
301
332
  }
302
333
 
@@ -508,9 +508,9 @@ return entities.map((e) => ({
508
508
 
509
509
  **Verified:**
510
510
 
511
- - ✅ `postgres-relation-decryption.db.test.js` - Proves the bug exists
512
- - ✅ `postgres-decryption-fix-verification.db.test.js` - Verifies separate queries work
513
- - ✅ `mongo-decryption-fix-verification.db.test.js` - Verifies fix for MongoDB
511
+ - ✅ `postgres-relation-decryption.test.js` - Proves the bug exists
512
+ - ✅ `postgres-decryption-fix-verification.test.js` - Verifies separate queries work
513
+ - ✅ `mongo-decryption-fix-verification.test.js` - Verifies fix for MongoDB
514
514
 
515
515
  **Implementation Examples:**
516
516
 
@@ -146,16 +146,6 @@ function extractCredentialFieldsFromModules(moduleDefinitions) {
146
146
  }
147
147
 
148
148
  for (const field of credentialFields) {
149
- // Validate entry by entry: one bad entry must not drop the
150
- // module's (or any other module's) valid fields.
151
- if (typeof field !== 'string' || field.trim() === '') {
152
- logger.warn(
153
- `Ignoring invalid encryption.credentialFields entry ${JSON.stringify(field)} ` +
154
- `in module "${moduleDef.moduleName || moduleDef.name || 'unknown'}" ` +
155
- '(expected a non-empty string)'
156
- );
157
- continue;
158
- }
159
149
  const prefixedField = field.startsWith('data.') ? field : `data.${field}`;
160
150
  fields.push(prefixedField);
161
151
  }
@@ -184,46 +174,26 @@ function loadModuleEncryptionSchemas(integrations) {
184
174
  return;
185
175
  }
186
176
 
187
- const { getModulesDefinitionFromIntegrationClasses } = require('../../integrations/utils/map-integration-dto');
188
-
189
- // Integrations without modules contribute no credential fields.
190
- const integrationsWithModules = integrations.filter(
191
- (integration) => integration?.Definition?.modules
192
- );
193
- const moduleDefinitions = getModulesDefinitionFromIntegrationClasses(
194
- integrationsWithModules
195
- );
177
+ const { getModulesDefinitionFromIntegrationClasses } = require('../integrations/utils/map-integration-dto');
196
178
 
197
- // Fields the core schema already encrypts (e.g. data.api_key) are
198
- // covered; registering them again would fail validation and drop every
199
- // other module field with them.
200
- const coreCredentialFields = CORE_ENCRYPTION_SCHEMA.Credential.fields;
201
- const credentialFields = extractCredentialFieldsFromModules(
202
- moduleDefinitions
203
- ).filter((field) => !coreCredentialFields.includes(field));
179
+ const moduleDefinitions = getModulesDefinitionFromIntegrationClasses(integrations);
180
+ const credentialFields = extractCredentialFieldsFromModules(moduleDefinitions);
204
181
 
205
182
  if (credentialFields.length === 0) {
206
183
  return;
207
184
  }
208
185
 
209
- // Merge with whatever custom schema the app already registered rather
210
- // than replacing it (registerCustomSchema overwrites).
211
- const existingCredentialFields = customSchema.Credential?.fields || [];
212
- const mergedSchema = {
213
- ...customSchema,
186
+ const moduleSchema = {
214
187
  Credential: {
215
- ...customSchema.Credential,
216
- fields: [
217
- ...new Set([...existingCredentialFields, ...credentialFields]),
218
- ],
219
- },
188
+ fields: credentialFields
189
+ }
220
190
  };
221
191
 
222
192
  logger.info(
223
193
  `Registering module-level encryption for ${credentialFields.length} credential fields`
224
194
  );
225
195
 
226
- registerCustomSchema(mergedSchema);
196
+ registerCustomSchema(moduleSchema);
227
197
  }
228
198
 
229
199
  /**
@@ -235,40 +205,7 @@ function loadModuleEncryptionSchemas(integrations) {
235
205
  *
236
206
  * Used by both Prisma (MongoDB/PostgreSQL) and DocumentDB encryption services.
237
207
  */
238
- /**
239
- * Raised when an app's encryption configuration is invalid. In deployed
240
- * stages this fails startup instead of silently writing plaintext.
241
- */
242
- class EncryptionSchemaError extends Error {
243
- constructor(message, { cause } = {}) {
244
- super(message, { cause });
245
- this.name = 'EncryptionSchemaError';
246
- }
247
- }
248
-
249
- /** Stages where field-level encryption is bypassed (see prisma.js). */
250
- function isLocalStage() {
251
- const stage = process.env.STAGE || process.env.NODE_ENV || 'development';
252
- return ['dev', 'test', 'local'].includes(stage.toLowerCase());
253
- }
254
-
255
- /**
256
- * An app definition was found but part of its encryption configuration
257
- * could not be applied. Deployed stages fail closed; local stages warn and
258
- * carry on so development isn't blocked.
259
- */
260
- function reportInvalidEncryptionConfig(what, error) {
261
- const message = `Invalid ${what}: ${error.message}`;
262
- if (isLocalStage()) {
263
- logger.warn(`${message} (ignored in local stage)`);
264
- return;
265
- }
266
- logger.error(message);
267
- throw new EncryptionSchemaError(message, { cause: error });
268
- }
269
-
270
208
  function loadCustomEncryptionSchema() {
271
- let appDefinition;
272
209
  try {
273
210
  // Lazy require to avoid circular dependency issues
274
211
  const path = require('node:path');
@@ -283,54 +220,38 @@ function loadCustomEncryptionSchema() {
283
220
  const backendIndexPath = path.join(backendDir, 'index.js');
284
221
 
285
222
  const backendModule = require(backendIndexPath);
286
- appDefinition = backendModule?.Definition;
287
- } catch (error) {
288
- // No loadable backend (tests, standalone usage): custom schema is
289
- // optional, so this stays at debug level.
290
- logger.debug('Could not load custom encryption schema:', error.message);
291
- return;
292
- }
223
+ const appDefinition = backendModule?.Definition;
293
224
 
294
- if (!appDefinition) {
295
- return; // No app definition found
296
- }
297
-
298
- // From here an app definition exists, so failures are configuration
299
- // errors and must be visible.
225
+ if (!appDefinition) {
226
+ return; // No app definition found
227
+ }
300
228
 
301
- // Load app-level custom schema
302
- const customSchema = appDefinition.encryption?.schema;
303
- if (customSchema && Object.keys(customSchema).length > 0) {
304
- try {
229
+ // Load app-level custom schema
230
+ const customSchema = appDefinition.encryption?.schema;
231
+ if (customSchema && Object.keys(customSchema).length > 0) {
305
232
  registerCustomSchema(customSchema);
306
- } catch (error) {
307
- reportInvalidEncryptionConfig('app encryption.schema', error);
308
233
  }
309
- }
310
234
 
311
- // Load app-level encryption opt-out — apps can declare fields they
312
- // don't want encrypted on write (decryption on read still works,
313
- // so legacy data remains readable).
314
- const disable = appDefinition.encryption?.disable;
315
- if (disable && Object.keys(disable).length > 0) {
316
- try {
235
+ // Load app-level encryption opt-out — apps can declare fields they
236
+ // don't want encrypted on write (decryption on read still works,
237
+ // so legacy data remains readable).
238
+ const disable = appDefinition.encryption?.disable;
239
+ if (disable && Object.keys(disable).length > 0) {
317
240
  registerEncryptionOptOut(disable);
318
- } catch (error) {
319
- reportInvalidEncryptionConfig('app encryption.disable', error);
320
241
  }
321
- }
322
242
 
323
- // Load module-level encryption schemas from integrations
324
- const integrations = appDefinition.integrations;
325
- if (integrations && Array.isArray(integrations)) {
326
- try {
243
+ // Load module-level encryption schemas from integrations
244
+ const integrations = appDefinition.integrations;
245
+ if (integrations && Array.isArray(integrations)) {
327
246
  loadModuleEncryptionSchemas(integrations);
328
- } catch (error) {
329
- reportInvalidEncryptionConfig(
330
- 'module encryption.credentialFields',
331
- error
332
- );
333
247
  }
248
+ } catch (error) {
249
+ // Silently ignore errors - custom schema is optional
250
+ // This handles cases like:
251
+ // - Backend package.json not found (tests, standalone usage)
252
+ // - No appDefinition defined
253
+ // - No custom encryption schema specified
254
+ logger.debug('Could not load custom encryption schema:', error.message);
334
255
  }
335
256
  }
336
257
 
@@ -465,7 +386,6 @@ function resetCustomSchema() {
465
386
  }
466
387
 
467
388
  module.exports = {
468
- EncryptionSchemaError,
469
389
  CORE_ENCRYPTION_SCHEMA,
470
390
  getEncryptedFields,
471
391
  getFieldsToEncryptOnWrite,
@@ -1,5 +1,3 @@
1
- const { isEncryptedEnvelope } = require('./ciphertext-format');
2
-
3
1
  /**
4
2
  * Field Encryption Service
5
3
  *
@@ -149,7 +147,12 @@ class FieldEncryptionService {
149
147
  }
150
148
 
151
149
  _isEncrypted(value) {
152
- return isEncryptedEnvelope(value);
150
+ if (typeof value !== 'string') {
151
+ return false;
152
+ }
153
+
154
+ const parts = value.split(':');
155
+ return parts.length >= 4;
153
156
  }
154
157
 
155
158
  _getNestedValue(obj, path) {
@@ -215,22 +218,22 @@ class FieldEncryptionService {
215
218
  }
216
219
 
217
220
  /**
218
- * Serialize a value for encryption.
219
- * Every value is JSON-encoded, strings included, so the type survives
220
- * the round trip: '123456' is stored as "\"123456\"" and reads back as a
221
- * string, 1234 reads back as a number.
221
+ * Serialize a value for encryption
222
+ * Objects/arrays are JSON stringified, primitives are converted to strings
222
223
  * @private
223
224
  */
224
225
  _serializeForEncryption(value) {
225
- return JSON.stringify(value);
226
+ if (typeof value === 'object' && value !== null) {
227
+ // JSON.stringify for objects and arrays
228
+ return JSON.stringify(value);
229
+ }
230
+ // For primitives (string, number, boolean), convert to string
231
+ return String(value);
226
232
  }
227
233
 
228
234
  /**
229
- * Deserialize a value after decryption.
230
- * Parses the JSON written by _serializeForEncryption. Values encrypted by
231
- * earlier versions stored strings raw: those that aren't valid JSON come
232
- * back unchanged; a raw legacy string that is also valid JSON (e.g.
233
- * '123456') still parses as it always did.
235
+ * Deserialize a value after decryption
236
+ * Attempts to parse as JSON, returns string if parsing fails
234
237
  * @private
235
238
  */
236
239
  _deserializeAfterDecryption(value) {
@@ -1,10 +1,7 @@
1
1
  const {
2
2
  createEncryptionExtension,
3
3
  } = require('./encryption/prisma-encryption-extension');
4
- const {
5
- loadCustomEncryptionSchema,
6
- EncryptionSchemaError,
7
- } = require('./encryption/encryption-schema-registry');
4
+ const { loadCustomEncryptionSchema } = require('./encryption/encryption-schema-registry');
8
5
  const { logger } = require('./encryption/logger');
9
6
  const { Cryptor } = require('../encrypt/Cryptor');
10
7
  const config = require('./config');
@@ -128,12 +125,6 @@ const prismaClientSingleton = () => {
128
125
  `Field-level encryption enabled using ${encryptionConfig.method.toUpperCase()}`
129
126
  );
130
127
  } catch (error) {
131
- // An invalid encryption configuration must fail startup: carrying
132
- // on without the extension would write encrypted fields in
133
- // plaintext.
134
- if (error instanceof EncryptionSchemaError) {
135
- throw error;
136
- }
137
128
  logger.error(
138
129
  'Failed to initialize encryption extension:',
139
130
  error