@friggframework/core 2.0.0--canary.710.7cfcf12.0 → 2.0.0--canary.682.451c957.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 (174) hide show
  1. package/CLAUDE.md +3 -41
  2. package/README.md +105 -0
  3. package/application/commands/README.md +11 -17
  4. package/application/commands/entity-commands.js +60 -18
  5. package/application/commands/integration-commands.js +20 -73
  6. package/application/commands/user-commands.js +17 -5
  7. package/application/index.js +6 -2
  8. package/credential/repositories/credential-repository-documentdb.js +34 -0
  9. package/credential/repositories/credential-repository-interface.js +15 -0
  10. package/credential/repositories/credential-repository-mongo.js +23 -0
  11. package/credential/repositories/credential-repository-postgres.js +28 -0
  12. package/credential/use-cases/credential-operations-for-user.js +117 -0
  13. package/credential/use-cases/reauthorize-credential-for-user.js +66 -0
  14. package/database/documentdb-encryption-service.js +33 -2
  15. package/database/documentdb-utils.js +2 -0
  16. package/database/encryption/README.md +3 -3
  17. package/database/encryption/encryption-schema-registry.js +42 -109
  18. package/database/encryption/field-encryption-service.js +16 -13
  19. package/database/organization-where.js +18 -0
  20. package/database/prisma.js +1 -10
  21. package/database/unique-violation.js +25 -0
  22. package/errors/index.js +0 -4
  23. package/generated/prisma-mongodb/edge.js +31 -4
  24. package/generated/prisma-mongodb/index-browser.js +28 -1
  25. package/generated/prisma-mongodb/index.d.ts +3451 -62
  26. package/generated/prisma-mongodb/index.js +31 -4
  27. package/generated/prisma-mongodb/package.json +1 -1
  28. package/generated/prisma-mongodb/schema.prisma +74 -0
  29. package/generated/prisma-mongodb/wasm.js +31 -4
  30. package/generated/prisma-postgresql/edge.js +31 -4
  31. package/generated/prisma-postgresql/index-browser.js +28 -1
  32. package/generated/prisma-postgresql/index.d.ts +3633 -53
  33. package/generated/prisma-postgresql/index.js +31 -4
  34. package/generated/prisma-postgresql/package.json +1 -1
  35. package/generated/prisma-postgresql/schema.prisma +70 -0
  36. package/generated/prisma-postgresql/wasm.js +31 -4
  37. package/handlers/WEBHOOKS.md +0 -29
  38. package/handlers/app-definition-loader.js +33 -8
  39. package/handlers/app-handler-helpers.js +25 -3
  40. package/handlers/backend-utils.js +31 -25
  41. package/handlers/middleware/admin-auth.js +14 -1
  42. package/handlers/routers/auth.js +11 -3
  43. package/handlers/routers/health.js +11 -4
  44. package/handlers/workers/integration-defined-workers.js +5 -2
  45. package/index.js +23 -2
  46. package/infrastructure/scheduler/eventbridge-scheduler-adapter.js +13 -67
  47. package/integrations/index.js +1 -0
  48. package/integrations/integration-base.js +0 -29
  49. package/integrations/integration-router.js +136 -98
  50. package/integrations/repositories/integration-mapping-repository-documentdb.js +0 -50
  51. package/integrations/repositories/integration-mapping-repository-interface.js +0 -15
  52. package/integrations/repositories/integration-mapping-repository-mongo.js +0 -29
  53. package/integrations/repositories/integration-mapping-repository-postgres.js +0 -30
  54. package/integrations/repositories/integration-mapping-repository.js +0 -29
  55. package/integrations/repositories/integration-not-found.js +14 -0
  56. package/integrations/repositories/integration-repository-documentdb.js +35 -35
  57. package/integrations/repositories/integration-repository-interface.js +17 -31
  58. package/integrations/repositories/integration-repository-mongo.js +60 -129
  59. package/integrations/repositories/integration-repository-postgres.js +62 -134
  60. package/integrations/repositories/process-repository-mongo.js +1 -1
  61. package/integrations/tests/doubles/test-integration-repository.js +12 -1
  62. package/integrations/use-cases/create-integration.js +72 -75
  63. package/integrations/use-cases/get-integrations-for-user.js +80 -40
  64. package/integrations/use-cases/index.js +5 -1
  65. package/integrations/use-cases/integration-operations-for-user.js +246 -0
  66. package/integrations/use-cases/resolve-integration-ownership.js +177 -0
  67. package/integrations/utils/map-integration-dto.js +47 -15
  68. package/jest-setup.js +2 -13
  69. package/jest-teardown.js +2 -5
  70. package/jest.config.js +0 -4
  71. package/management-api/create-management-api-router.js +49 -0
  72. package/management-api/error-response.js +116 -0
  73. package/management-api/management-api-config.js +85 -0
  74. package/management-api/meta-router.js +57 -0
  75. package/management-api/openapi/build-openapi.js +144 -0
  76. package/management-api/openapi/components.js +407 -0
  77. package/management-api/openapi/v1-operations.js +83 -0
  78. package/management-api/openapi/v2-operations.js +261 -0
  79. package/management-api/route-registry.js +294 -0
  80. package/management-api/test-support/http-client.js +39 -0
  81. package/management-api/use-cases/get-management-api-meta.js +57 -0
  82. package/management-api/v1-deprecation.js +120 -0
  83. package/management-api/v2/create-v2-router.js +85 -0
  84. package/management-api/v2/dependencies.js +279 -0
  85. package/management-api/v2/dto.js +143 -0
  86. package/management-api/v2/handlers/authorization.js +94 -0
  87. package/management-api/v2/handlers/credentials.js +29 -0
  88. package/management-api/v2/handlers/entities.js +77 -0
  89. package/management-api/v2/handlers/integrations.js +128 -0
  90. package/management-api/v2/handlers/proxy.js +34 -0
  91. package/management-api/v2/request.js +85 -0
  92. package/management-api/v2/test-support/fake-dependencies.js +103 -0
  93. package/modules/domain/authorization-session.js +144 -0
  94. package/modules/entity-scope.js +15 -0
  95. package/modules/entity-type-registry.js +311 -0
  96. package/modules/index.js +14 -0
  97. package/modules/proxy/proxy-errors.js +20 -0
  98. package/modules/proxy/proxy-path.js +179 -0
  99. package/modules/proxy/proxy-policy.js +248 -0
  100. package/modules/repositories/authorization-session-repository-documentdb.js +112 -0
  101. package/modules/repositories/authorization-session-repository-factory.js +31 -0
  102. package/modules/repositories/authorization-session-repository-interface.js +47 -0
  103. package/modules/repositories/authorization-session-repository-prisma.js +87 -0
  104. package/modules/repositories/module-repository-documentdb.js +48 -4
  105. package/modules/repositories/module-repository-interface.js +19 -2
  106. package/modules/repositories/module-repository-mongo.js +65 -73
  107. package/modules/repositories/module-repository-postgres.js +64 -73
  108. package/modules/requester/proxy-request.js +244 -0
  109. package/modules/requester/requester.js +27 -2
  110. package/modules/scope-config.js +155 -0
  111. package/modules/scope-policy.js +80 -0
  112. package/modules/scope-resolution.js +277 -0
  113. package/modules/scope-visibility.js +165 -0
  114. package/modules/tests/doubles/in-memory-authorization-session-repository.js +45 -0
  115. package/modules/tests/doubles/ownership-matching.js +18 -0
  116. package/modules/use-cases/authorize-entity-for-user.js +59 -0
  117. package/modules/use-cases/create-entity-from-credential.js +113 -0
  118. package/modules/use-cases/entity-operations-for-user.js +157 -0
  119. package/modules/use-cases/entity-types.js +139 -0
  120. package/modules/use-cases/execute-entity-proxy-request.js +254 -0
  121. package/modules/use-cases/get-authorization-requirements.js +100 -0
  122. package/modules/use-cases/get-entities-for-user.js +16 -5
  123. package/modules/use-cases/get-entity-module-for-user.js +106 -0
  124. package/modules/use-cases/get-entity-options-by-id.js +16 -8
  125. package/modules/use-cases/get-module.js +27 -11
  126. package/modules/use-cases/multi-step-authorization.js +326 -0
  127. package/modules/use-cases/process-authorization-callback.js +52 -18
  128. package/modules/use-cases/refresh-entity-options.js +16 -8
  129. package/modules/use-cases/test-module-auth.js +16 -8
  130. package/modules/utils/find-module-definition.js +23 -0
  131. package/modules/utils/map-module-dto.js +10 -6
  132. package/modules/utils/public-entity.js +59 -0
  133. package/package.json +6 -7
  134. package/prisma-mongodb/schema.prisma +74 -0
  135. package/prisma-postgresql/migrations/20261003000000_add_authorization_session/migration.sql +28 -0
  136. package/prisma-postgresql/migrations/20261015000000_add_organization_membership/migration.sql +26 -0
  137. package/prisma-postgresql/migrations/20261016000000_add_entity_integration_organization/migration.sql +25 -0
  138. package/prisma-postgresql/schema.prisma +70 -0
  139. package/queues/queuer-util.js +5 -6
  140. package/types/index.d.ts +2 -0
  141. package/types/modules/index.d.ts +155 -0
  142. package/types/user/index.d.ts +117 -0
  143. package/user/adopter-jwt-config.js +177 -0
  144. package/user/adopter-jwt-errors.js +59 -0
  145. package/user/adopter-token-verifier.js +122 -0
  146. package/user/app-user-helpers.js +97 -0
  147. package/user/create-user-authentication.js +64 -0
  148. package/user/membership.js +51 -0
  149. package/user/repositories/organization-membership-record.js +19 -0
  150. package/user/repositories/organization-membership-repository-documentdb.js +106 -0
  151. package/user/repositories/organization-membership-repository-factory.js +42 -0
  152. package/user/repositories/organization-membership-repository-interface.js +57 -0
  153. package/user/repositories/organization-membership-repository-mongo.js +13 -0
  154. package/user/repositories/organization-membership-repository-postgres.js +29 -0
  155. package/user/repositories/organization-membership-repository-prisma.js +81 -0
  156. package/user/repositories/user-repository-documentdb.js +27 -2
  157. package/user/repositories/user-repository-interface.js +18 -0
  158. package/user/repositories/user-repository-mongo.js +24 -0
  159. package/user/repositories/user-repository-postgres.js +18 -0
  160. package/user/tests/doubles/adopter-jwt-fixtures.js +220 -0
  161. package/user/tests/doubles/test-user-repository.js +1 -1
  162. package/user/use-cases/ensure-organization-membership.js +92 -0
  163. package/user/use-cases/find-individual-users-by-organization.js +54 -0
  164. package/user/use-cases/get-user-from-adopter-jwt.js +86 -125
  165. package/user/use-cases/get-user-from-bearer-token.js +2 -7
  166. package/user/use-cases/get-user-from-x-frigg-headers.js +83 -80
  167. package/user/user.js +27 -0
  168. package/database/encryption/ciphertext-format.js +0 -24
  169. package/errors/mapping-already-exists-error.js +0 -14
  170. package/integrations/use-cases/add-entities-to-integration.js +0 -92
  171. package/integrations/use-cases/remove-entities-from-integration.js +0 -70
  172. package/integrations/utils/assert-entity-ids-input.js +0 -42
  173. package/jest.db.config.js +0 -23
  174. package/modules/utils/to-credential-id.js +0 -14
@@ -188,6 +188,29 @@ class CredentialRepositoryMongo extends CredentialRepositoryInterface {
188
188
  };
189
189
  }
190
190
 
191
+ /**
192
+ * All credentials owned by a user, newest first.
193
+ *
194
+ * @param {string} userId - User ID
195
+ * @returns {Promise<Array<Object>>} Credentials with string IDs
196
+ */
197
+ async findCredentialsByUserId(userId) {
198
+ if (!userId) return [];
199
+ const credentials = await this.prisma.credential.findMany({
200
+ where: { userId: String(userId) },
201
+ orderBy: { createdAt: 'desc' },
202
+ });
203
+ return credentials.map((credential) => ({
204
+ ...(credential.data || {}),
205
+ id: credential.id,
206
+ userId: credential.userId,
207
+ externalId: credential.externalId,
208
+ authIsValid: credential.authIsValid,
209
+ createdAt: credential.createdAt,
210
+ updatedAt: credential.updatedAt,
211
+ }));
212
+ }
213
+
191
214
  /**
192
215
  * Update a credential by ID
193
216
  * Replaces: Credential.findByIdAndUpdate(credentialId, { $set: updates })
@@ -205,6 +205,34 @@ class CredentialRepositoryPostgres extends CredentialRepositoryInterface {
205
205
  };
206
206
  }
207
207
 
208
+ /**
209
+ * All credentials owned by a user, newest first.
210
+ *
211
+ * @param {string} userId - User ID (string from application layer)
212
+ * @returns {Promise<Array<Object>>} Credentials with string IDs
213
+ */
214
+ async findCredentialsByUserId(userId) {
215
+ let intUserId;
216
+ try {
217
+ intUserId = this._convertId(userId);
218
+ } catch {
219
+ return [];
220
+ }
221
+ const credentials = await this.prisma.credential.findMany({
222
+ where: { userId: intUserId },
223
+ orderBy: { createdAt: 'desc' },
224
+ });
225
+ return credentials.map((credential) => ({
226
+ ...(credential.data || {}),
227
+ id: credential.id.toString(),
228
+ userId: credential.userId?.toString(),
229
+ externalId: credential.externalId,
230
+ authIsValid: credential.authIsValid,
231
+ createdAt: credential.createdAt,
232
+ updatedAt: credential.updatedAt,
233
+ }));
234
+ }
235
+
208
236
  /**
209
237
  * Update a credential by ID
210
238
  *
@@ -0,0 +1,117 @@
1
+ const Boom = require('@hapi/boom');
2
+ const { userOwns } = require('../../modules/use-cases/get-entity-module-for-user');
3
+
4
+ /**
5
+ * Management API v2 credential operations. A credential has no module name
6
+ * of its own; its `type` and the entities that use it come from the
7
+ * caller's entities. A missing or foreign credential is a 404.
8
+ */
9
+
10
+ const credentialIdOf = (entity) => {
11
+ const ref = entity.credential ?? entity.credentialId;
12
+ if (ref === undefined || ref === null) return null;
13
+ if (typeof ref === 'object') {
14
+ const id = ref.id ?? ref._id;
15
+ return id === undefined || id === null ? null : String(id);
16
+ }
17
+ return String(ref);
18
+ };
19
+
20
+ /** Adds `type` and `entityIds` from the user's entities to each credential. */
21
+ function withEntityUsage(credentials, entities) {
22
+ const usage = new Map();
23
+ for (const entity of entities || []) {
24
+ const credentialId = credentialIdOf(entity);
25
+ if (!credentialId) continue;
26
+ const entry = usage.get(credentialId) || { type: null, entityIds: [] };
27
+ entry.type = entry.type || entity.moduleName || null;
28
+ entry.entityIds.push(String(entity.id));
29
+ usage.set(credentialId, entry);
30
+ }
31
+ return credentials.map((credential) => {
32
+ const entry = usage.get(String(credential.id)) || {
33
+ type: null,
34
+ entityIds: [],
35
+ };
36
+ return { ...credential, type: entry.type, entityIds: entry.entityIds };
37
+ });
38
+ }
39
+
40
+ /** The caller's credentials with their type and entity usage. */
41
+ class ListCredentialsForUser {
42
+ constructor({ credentialRepository, moduleRepository }) {
43
+ this.credentialRepository = credentialRepository;
44
+ this.moduleRepository = moduleRepository;
45
+ }
46
+
47
+ async execute(user) {
48
+ const userId = user.getId();
49
+ const [credentials, entities] = await Promise.all([
50
+ this.credentialRepository.findCredentialsByUserId(userId),
51
+ this.moduleRepository.findEntitiesByUserId(userId),
52
+ ]);
53
+ return withEntityUsage(credentials || [], entities);
54
+ }
55
+ }
56
+
57
+ /** One credential the caller owns, with its type and entity usage. */
58
+ class GetCredentialForUserV2 {
59
+ constructor({ credentialRepository, moduleRepository }) {
60
+ this.credentialRepository = credentialRepository;
61
+ this.moduleRepository = moduleRepository;
62
+ }
63
+
64
+ async execute(credentialId, user) {
65
+ let credential = null;
66
+ try {
67
+ credential = await this.credentialRepository.findCredentialById(
68
+ credentialId
69
+ );
70
+ } catch (error) {
71
+ if (!/Invalid ID|ObjectId|Malformed/i.test(error?.message || '')) {
72
+ throw error;
73
+ }
74
+ }
75
+ if (!credential || !userOwns(user, credential.userId)) {
76
+ throw Boom.notFound(`Credential ${credentialId} not found`, {
77
+ code: 'CREDENTIAL_NOT_FOUND',
78
+ });
79
+ }
80
+ const entities = await this.moduleRepository.findEntitiesByUserId(
81
+ credential.userId
82
+ );
83
+ return withEntityUsage([credential], entities)[0];
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Deletes a credential the caller owns. The entities that used it stay, with
89
+ * their credential unset, so they show as needing re-authorization rather
90
+ * than disappearing from integrations.
91
+ */
92
+ class DeleteCredentialForUser {
93
+ constructor({ credentialRepository, moduleRepository, getCredentialForUser }) {
94
+ this.credentialRepository = credentialRepository;
95
+ this.moduleRepository = moduleRepository;
96
+ this.getCredentialForUser = getCredentialForUser;
97
+ }
98
+
99
+ async execute(credentialId, user) {
100
+ const credential = await this.getCredentialForUser.execute(
101
+ credentialId,
102
+ user
103
+ );
104
+ for (const entityId of credential.entityIds) {
105
+ await this.moduleRepository.unsetCredential(entityId);
106
+ }
107
+ await this.credentialRepository.deleteCredentialById(credential.id);
108
+ return { id: String(credential.id), entityIds: credential.entityIds };
109
+ }
110
+ }
111
+
112
+ module.exports = {
113
+ ListCredentialsForUser,
114
+ GetCredentialForUserV2,
115
+ DeleteCredentialForUser,
116
+ withEntityUsage,
117
+ };
@@ -0,0 +1,66 @@
1
+ const Boom = require('@hapi/boom');
2
+
3
+ /**
4
+ * Re-authorization of a credential the caller owns (Management API v2). It
5
+ * runs the same flow as /api/v2/authorize for the credential's entity type,
6
+ * with the session bound to the credential so it cannot be reused for
7
+ * anything else.
8
+ */
9
+ async function credentialEntityType(getCredentialForUser, credentialId, user) {
10
+ const credential = await getCredentialForUser.execute(credentialId, user);
11
+ if (!credential.type) {
12
+ throw Boom.conflict(
13
+ `Credential ${credentialId} is not used by any entity, so its type is unknown; authorize a new connection instead`,
14
+ { code: 'CREDENTIAL_TYPE_UNKNOWN' }
15
+ );
16
+ }
17
+ return { entityType: credential.type, credentialId: String(credential.id) };
18
+ }
19
+
20
+ /** GET /api/v2/credentials/:credentialId/reauthorize */
21
+ class GetCredentialReauthorization {
22
+ constructor({ getCredentialForUser, getAuthorizationStep }) {
23
+ this.getCredentialForUser = getCredentialForUser;
24
+ this.getAuthorizationStep = getAuthorizationStep;
25
+ }
26
+
27
+ async execute(credentialId, user, { step = 1, sessionId, state } = {}) {
28
+ const binding = await credentialEntityType(
29
+ this.getCredentialForUser,
30
+ credentialId,
31
+ user
32
+ );
33
+ return this.getAuthorizationStep.execute({
34
+ user,
35
+ ...binding,
36
+ step,
37
+ sessionId,
38
+ state,
39
+ });
40
+ }
41
+ }
42
+
43
+ /** POST /api/v2/credentials/:credentialId/reauthorize */
44
+ class ReauthorizeCredentialForUser {
45
+ constructor({ getCredentialForUser, submitAuthorizationStep }) {
46
+ this.getCredentialForUser = getCredentialForUser;
47
+ this.submitAuthorizationStep = submitAuthorizationStep;
48
+ }
49
+
50
+ async execute(credentialId, user, { data, step = 1, sessionId } = {}) {
51
+ const binding = await credentialEntityType(
52
+ this.getCredentialForUser,
53
+ credentialId,
54
+ user
55
+ );
56
+ return this.submitAuthorizationStep.execute({
57
+ user,
58
+ ...binding,
59
+ data,
60
+ step,
61
+ sessionId,
62
+ });
63
+ }
64
+ }
65
+
66
+ module.exports = { GetCredentialReauthorization, ReauthorizeCredentialForUser };
@@ -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
 
@@ -58,6 +58,8 @@ async function insertOne(client, collection, document) {
58
58
 
59
59
  // Provide helpful context for common errors
60
60
  if (error.code === 11000) {
61
+ // `code` lets callers recognise a lost race without parsing text
62
+ // (see database/unique-violation.js).
61
63
  throw Object.assign(
62
64
  new Error(`${errorMsg} - Duplicate key violation`),
63
65
  { code: 11000 }
@@ -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
 
@@ -39,6 +39,12 @@ const CORE_ENCRYPTION_SCHEMA = {
39
39
  Token: {
40
40
  fields: ['token'],
41
41
  },
42
+
43
+ // Multi-step authorization (Management API v2): earlier steps can collect
44
+ // emails, usernames or one-time codes.
45
+ AuthorizationSession: {
46
+ fields: ['stepData'],
47
+ },
42
48
  };
43
49
 
44
50
  let customSchema = {};
@@ -146,16 +152,6 @@ function extractCredentialFieldsFromModules(moduleDefinitions) {
146
152
  }
147
153
 
148
154
  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
155
  const prefixedField = field.startsWith('data.') ? field : `data.${field}`;
160
156
  fields.push(prefixedField);
161
157
  }
@@ -184,46 +180,33 @@ function loadModuleEncryptionSchemas(integrations) {
184
180
  return;
185
181
  }
186
182
 
187
- const { getModulesDefinitionFromIntegrationClasses } = require('../../integrations/utils/map-integration-dto');
183
+ // NOTE: this path does not exist ('../integrations' resolves to
184
+ // database/integrations), so today this function throws MODULE_NOT_FOUND,
185
+ // loadCustomEncryptionSchema swallows it, and module credentialFields are
186
+ // effectively NOT registered. Fixing it changes which fields are encrypted
187
+ // and needs a decrypt that tolerates plaintext, so it ships separately:
188
+ // branch claude/internal-mcp-03b-module-credential-encryption. Left as is
189
+ // here on purpose (ADR-059 must not change what gets encrypted).
190
+ const { getModulesDefinitionFromIntegrationClasses } = require('../integrations/utils/map-integration-dto');
188
191
 
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
- );
196
-
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));
192
+ const moduleDefinitions = getModulesDefinitionFromIntegrationClasses(integrations);
193
+ const credentialFields = extractCredentialFieldsFromModules(moduleDefinitions);
204
194
 
205
195
  if (credentialFields.length === 0) {
206
196
  return;
207
197
  }
208
198
 
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,
199
+ const moduleSchema = {
214
200
  Credential: {
215
- ...customSchema.Credential,
216
- fields: [
217
- ...new Set([...existingCredentialFields, ...credentialFields]),
218
- ],
219
- },
201
+ fields: credentialFields
202
+ }
220
203
  };
221
204
 
222
205
  logger.info(
223
206
  `Registering module-level encryption for ${credentialFields.length} credential fields`
224
207
  );
225
208
 
226
- registerCustomSchema(mergedSchema);
209
+ registerCustomSchema(moduleSchema);
227
210
  }
228
211
 
229
212
  /**
@@ -235,40 +218,7 @@ function loadModuleEncryptionSchemas(integrations) {
235
218
  *
236
219
  * Used by both Prisma (MongoDB/PostgreSQL) and DocumentDB encryption services.
237
220
  */
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
221
  function loadCustomEncryptionSchema() {
271
- let appDefinition;
272
222
  try {
273
223
  // Lazy require to avoid circular dependency issues
274
224
  const path = require('node:path');
@@ -283,54 +233,38 @@ function loadCustomEncryptionSchema() {
283
233
  const backendIndexPath = path.join(backendDir, 'index.js');
284
234
 
285
235
  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
- }
236
+ const appDefinition = backendModule?.Definition;
293
237
 
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.
238
+ if (!appDefinition) {
239
+ return; // No app definition found
240
+ }
300
241
 
301
- // Load app-level custom schema
302
- const customSchema = appDefinition.encryption?.schema;
303
- if (customSchema && Object.keys(customSchema).length > 0) {
304
- try {
242
+ // Load app-level custom schema
243
+ const customSchema = appDefinition.encryption?.schema;
244
+ if (customSchema && Object.keys(customSchema).length > 0) {
305
245
  registerCustomSchema(customSchema);
306
- } catch (error) {
307
- reportInvalidEncryptionConfig('app encryption.schema', error);
308
246
  }
309
- }
310
247
 
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 {
248
+ // Load app-level encryption opt-out — apps can declare fields they
249
+ // don't want encrypted on write (decryption on read still works,
250
+ // so legacy data remains readable).
251
+ const disable = appDefinition.encryption?.disable;
252
+ if (disable && Object.keys(disable).length > 0) {
317
253
  registerEncryptionOptOut(disable);
318
- } catch (error) {
319
- reportInvalidEncryptionConfig('app encryption.disable', error);
320
254
  }
321
- }
322
255
 
323
- // Load module-level encryption schemas from integrations
324
- const integrations = appDefinition.integrations;
325
- if (integrations && Array.isArray(integrations)) {
326
- try {
256
+ // Load module-level encryption schemas from integrations
257
+ const integrations = appDefinition.integrations;
258
+ if (integrations && Array.isArray(integrations)) {
327
259
  loadModuleEncryptionSchemas(integrations);
328
- } catch (error) {
329
- reportInvalidEncryptionConfig(
330
- 'module encryption.credentialFields',
331
- error
332
- );
333
260
  }
261
+ } catch (error) {
262
+ // Silently ignore errors - custom schema is optional
263
+ // This handles cases like:
264
+ // - Backend package.json not found (tests, standalone usage)
265
+ // - No appDefinition defined
266
+ // - No custom encryption schema specified
267
+ logger.debug('Could not load custom encryption schema:', error.message);
334
268
  }
335
269
  }
336
270
 
@@ -465,7 +399,6 @@ function resetCustomSchema() {
465
399
  }
466
400
 
467
401
  module.exports = {
468
- EncryptionSchemaError,
469
402
  CORE_ENCRYPTION_SCHEMA,
470
403
  getEncryptedFields,
471
404
  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) {
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The Prisma `where` fragment for an `organizationId` on MongoDB (ADR-056
3
+ * §3.6), shared by the entity and integration adapters.
4
+ *
5
+ * Prisma on MongoDB: `null` matches only an explicit null and `isSet: false`
6
+ * only a missing field. A row written before the column existed is the common
7
+ * "no organization" case, so `null` here means both.
8
+ */
9
+ function organizationWhere(organizationId) {
10
+ if (organizationId === null) {
11
+ return {
12
+ OR: [{ organizationId: null }, { organizationId: { isSet: false } }],
13
+ };
14
+ }
15
+ return { organizationId };
16
+ }
17
+
18
+ module.exports = { organizationWhere };
@@ -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
@@ -0,0 +1,25 @@
1
+ /** Mongo/DocumentDB duplicate-key error code. */
2
+ const DUPLICATE_KEY_CODE = 11000;
3
+ /** Prisma's unique-constraint error code (PostgreSQL and MongoDB). */
4
+ const PRISMA_UNIQUE_CODE = 'P2002';
5
+
6
+ /**
7
+ * Whether `error` is a unique-constraint violation, however the database
8
+ * layer raised it: a Prisma `P2002`, or a duplicate-key `11000`
9
+ * (`documentdb-utils` `insertOne` sets `code` on the error it throws).
10
+ * Matches on the code only, never on message text.
11
+ *
12
+ * @param {unknown} error
13
+ * @returns {boolean}
14
+ */
15
+ function isUniqueViolation(error) {
16
+ return (
17
+ error?.code === PRISMA_UNIQUE_CODE || error?.code === DUPLICATE_KEY_CODE
18
+ );
19
+ }
20
+
21
+ module.exports = {
22
+ isUniqueViolation,
23
+ DUPLICATE_KEY_CODE,
24
+ PRISMA_UNIQUE_CODE,
25
+ };
package/errors/index.js CHANGED
@@ -6,9 +6,6 @@ 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');
12
9
 
13
10
  module.exports = {
14
11
  BaseError,
@@ -17,5 +14,4 @@ module.exports = {
17
14
  RequiredPropertyError,
18
15
  ParameterTypeError,
19
16
  ClientSafeError,
20
- MappingAlreadyExistsError,
21
17
  };