@friggframework/core 2.0.0-next.53 → 2.0.0-next.54

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 (56) hide show
  1. package/CLAUDE.md +2 -1
  2. package/application/commands/integration-commands.js +1 -1
  3. package/application/index.js +1 -1
  4. package/credential/repositories/credential-repository-documentdb.js +300 -0
  5. package/credential/repositories/credential-repository-factory.js +8 -1
  6. package/database/config.js +4 -4
  7. package/database/documentdb-encryption-service.js +330 -0
  8. package/database/documentdb-utils.js +136 -0
  9. package/database/encryption/README.md +50 -0
  10. package/database/encryption/documentdb-encryption-service.md +3270 -0
  11. package/database/encryption/encryption-schema-registry.js +46 -0
  12. package/database/prisma.js +7 -47
  13. package/database/repositories/health-check-repository-documentdb.js +134 -0
  14. package/database/repositories/health-check-repository-factory.js +6 -1
  15. package/database/repositories/health-check-repository-interface.js +29 -34
  16. package/database/repositories/health-check-repository-mongodb.js +1 -3
  17. package/database/use-cases/check-database-state-use-case.js +3 -3
  18. package/database/use-cases/run-database-migration-use-case.js +6 -4
  19. package/database/use-cases/trigger-database-migration-use-case.js +2 -2
  20. package/database/utils/mongodb-schema-init.js +5 -5
  21. package/database/utils/prisma-runner.js +15 -9
  22. package/generated/prisma-mongodb/edge.js +3 -3
  23. package/generated/prisma-mongodb/index.d.ts +10 -4
  24. package/generated/prisma-mongodb/index.js +3 -3
  25. package/generated/prisma-mongodb/package.json +1 -1
  26. package/generated/prisma-mongodb/schema.prisma +1 -3
  27. package/generated/prisma-mongodb/wasm.js +2 -2
  28. package/generated/prisma-postgresql/edge.js +3 -3
  29. package/generated/prisma-postgresql/index.d.ts +10 -4
  30. package/generated/prisma-postgresql/index.js +3 -3
  31. package/generated/prisma-postgresql/package.json +1 -1
  32. package/generated/prisma-postgresql/schema.prisma +1 -3
  33. package/generated/prisma-postgresql/wasm.js +2 -2
  34. package/handlers/routers/db-migration.js +2 -3
  35. package/handlers/routers/health.js +0 -3
  36. package/handlers/workers/db-migration.js +8 -8
  37. package/integrations/repositories/integration-mapping-repository-documentdb.js +135 -0
  38. package/integrations/repositories/integration-mapping-repository-factory.js +8 -1
  39. package/integrations/repositories/integration-repository-documentdb.js +189 -0
  40. package/integrations/repositories/integration-repository-factory.js +8 -1
  41. package/integrations/repositories/process-repository-documentdb.js +141 -0
  42. package/integrations/repositories/process-repository-factory.js +8 -1
  43. package/modules/repositories/module-repository-documentdb.js +307 -0
  44. package/modules/repositories/module-repository-factory.js +8 -1
  45. package/package.json +5 -5
  46. package/prisma-mongodb/schema.prisma +1 -3
  47. package/prisma-postgresql/migrations/20251112195422_update_user_unique_constraints/migration.sql +69 -0
  48. package/prisma-postgresql/schema.prisma +1 -3
  49. package/syncs/repositories/sync-repository-documentdb.js +240 -0
  50. package/syncs/repositories/sync-repository-factory.js +6 -1
  51. package/token/repositories/token-repository-documentdb.js +125 -0
  52. package/token/repositories/token-repository-factory.js +8 -1
  53. package/user/repositories/user-repository-documentdb.js +292 -0
  54. package/user/repositories/user-repository-factory.js +6 -1
  55. package/websocket/repositories/websocket-connection-repository-documentdb.js +119 -0
  56. package/websocket/repositories/websocket-connection-repository-factory.js +8 -1
package/CLAUDE.md CHANGED
@@ -27,7 +27,7 @@ This file provides guidance to Claude Code when working with the Frigg Framework
27
27
  `@friggframework/core` is the foundational package of the Frigg Framework, providing:
28
28
 
29
29
  - **IntegrationBase**: Base class all integrations extend
30
- - **Database Layer**: Multi-database support (MongoDB, PostgreSQL) with Prisma ORM
30
+ - **Database Layer**: Multi-database support (MongoDB, DocumentDB, PostgreSQL) with Prisma ORM
31
31
  - **Encryption**: Transparent field-level encryption with AWS KMS or AES
32
32
  - **User Management**: Individual and organizational user support
33
33
  - **Module System**: API module loading and credential management
@@ -256,6 +256,7 @@ class MyIntegration extends IntegrationBase {
256
256
  - `health-check-repository.js` - Database health monitoring
257
257
  - `token-repository.js` - Authentication tokens
258
258
  - `websocket-connection-repository.js` - WebSocket connections
259
+ - DocumentDB-enabled adapters mirror the MongoDB APIs but execute raw commands (`$runCommandRaw`, `$aggregateRaw`) for compatibility; encrypted models (e.g., credentials) still delegate reads to Prisma so the encryption extension can decrypt secrets transparently.
259
260
 
260
261
  **Use Cases**:
261
262
  - `check-database-health-use-case.js` - Database health checks
@@ -38,7 +38,7 @@ function mapErrorToResponse(error) {
38
38
  };
39
39
  }
40
40
 
41
- function createIntegrationCommands({ integrationClass } = {}) {
41
+ function createIntegrationCommands({ integrationClass }) {
42
42
  if (!integrationClass) {
43
43
  throw new Error('integrationClass is required');
44
44
  }
@@ -23,7 +23,7 @@ const {
23
23
  * const user = await commands.createUser({ username: 'user@example.com' });
24
24
  * const credential = await commands.createCredential({ userId: user.id, ... });
25
25
  */
26
- function createFriggCommands({ integrationClass } = {}) {
26
+ function createFriggCommands({ integrationClass }) {
27
27
  // All commands use Frigg's default repositories and use cases
28
28
  const integrationCommands = createIntegrationCommands({ integrationClass });
29
29
 
@@ -0,0 +1,300 @@
1
+ const { prisma } = require('../../database/prisma');
2
+ const {
3
+ toObjectId,
4
+ fromObjectId,
5
+ findOne,
6
+ insertOne,
7
+ updateOne,
8
+ deleteOne,
9
+ } = require('../../database/documentdb-utils');
10
+ const {
11
+ CredentialRepositoryInterface,
12
+ } = require('./credential-repository-interface');
13
+ const { DocumentDBEncryptionService } = require('../../database/documentdb-encryption-service');
14
+
15
+ /**
16
+ * Credential repository for DocumentDB.
17
+ * Uses DocumentDBEncryptionService for field-level encryption.
18
+ *
19
+ * Encrypted fields:
20
+ * - Credential.data.access_token
21
+ * - Credential.data.refresh_token
22
+ * - Credential.data.id_token
23
+ * - Credential.data.domain
24
+ *
25
+ * SECURITY CRITICAL: All OAuth credentials must be encrypted at rest.
26
+ *
27
+ * @see DocumentDBEncryptionService
28
+ * @see encryption-schema-registry.js
29
+ */
30
+ class CredentialRepositoryDocumentDB extends CredentialRepositoryInterface {
31
+ constructor() {
32
+ super();
33
+ this.prisma = prisma;
34
+ this.encryptionService = new DocumentDBEncryptionService();
35
+ }
36
+
37
+ async findCredentialById(id) {
38
+ const objectId = toObjectId(id);
39
+ if (!objectId) return null;
40
+ const doc = await findOne(this.prisma, 'Credential', { _id: objectId });
41
+ if (!doc) return null;
42
+
43
+ // Decrypt sensitive fields using service
44
+ const decryptedCredential = await this.encryptionService.decryptFields('Credential', doc);
45
+ return this._mapCredentialById(decryptedCredential);
46
+ }
47
+
48
+ async updateAuthenticationStatus(credentialId, authIsValid) {
49
+ const objectId = toObjectId(credentialId);
50
+ if (!objectId) return { acknowledged: false, modifiedCount: 0 };
51
+ const result = await updateOne(
52
+ this.prisma,
53
+ 'Credential',
54
+ { _id: objectId },
55
+ {
56
+ $set: { authIsValid, updatedAt: new Date() },
57
+ }
58
+ );
59
+ const modified = result?.nModified ?? result?.n ?? 0;
60
+ return { acknowledged: true, modifiedCount: modified };
61
+ }
62
+
63
+ async deleteCredentialById(credentialId) {
64
+ const objectId = toObjectId(credentialId);
65
+ if (!objectId) return { acknowledged: true, deletedCount: 0 };
66
+ const result = await deleteOne(this.prisma, 'Credential', { _id: objectId });
67
+ const deleted = result?.n ?? 0;
68
+ return { acknowledged: true, deletedCount: deleted };
69
+ }
70
+
71
+ async upsertCredential(credentialDetails) {
72
+ const { identifiers, details } = credentialDetails;
73
+ if (!identifiers) throw new Error('identifiers required to upsert credential');
74
+ if (!identifiers.user && !identifiers.userId) {
75
+ throw new Error('user or userId required in identifiers');
76
+ }
77
+ if (!identifiers.externalId) {
78
+ throw new Error(
79
+ 'externalId required in identifiers to prevent credential collision. When multiple credentials exist for the same user, both userId and externalId are needed to uniquely identify which credential to update.'
80
+ );
81
+ }
82
+
83
+ const filter = this._buildIdentifierFilter(identifiers);
84
+ const existing = await findOne(this.prisma, 'Credential', filter);
85
+
86
+ const {
87
+ user,
88
+ userId,
89
+ authIsValid,
90
+ externalId,
91
+ ...oauthData
92
+ } = details || {};
93
+
94
+ const now = new Date();
95
+
96
+ if (existing) {
97
+ // Decrypt existing credential data first
98
+ const decryptedExisting = await this.encryptionService.decryptFields('Credential', existing);
99
+ const mergedData = { ...(decryptedExisting.data || {}), ...oauthData };
100
+
101
+ // Build update document
102
+ const updateDocument = {
103
+ userId: toObjectId(userId || user) || existing.userId || null,
104
+ externalId: externalId !== undefined ? externalId : existing.externalId,
105
+ authIsValid: authIsValid !== undefined ? authIsValid : existing.authIsValid,
106
+ data: mergedData,
107
+ updatedAt: now,
108
+ };
109
+
110
+ // Encrypt before storing
111
+ const encryptedUpdate = await this.encryptionService.encryptFields(
112
+ 'Credential',
113
+ { data: updateDocument.data }
114
+ );
115
+
116
+ await updateOne(
117
+ this.prisma,
118
+ 'Credential',
119
+ { _id: existing._id },
120
+ {
121
+ $set: {
122
+ userId: updateDocument.userId,
123
+ externalId: updateDocument.externalId,
124
+ authIsValid: updateDocument.authIsValid,
125
+ data: encryptedUpdate.data,
126
+ updatedAt: updateDocument.updatedAt,
127
+ },
128
+ }
129
+ );
130
+
131
+ // Read back and decrypt
132
+ const updated = await findOne(this.prisma, 'Credential', { _id: existing._id });
133
+ const decryptedCredential = await this.encryptionService.decryptFields('Credential', updated);
134
+ return this._mapCredential(decryptedCredential);
135
+ }
136
+
137
+ // Build plain text document
138
+ const plainDocument = {
139
+ userId: toObjectId(userId || user || identifiers.user),
140
+ externalId: externalId !== undefined ? externalId : identifiers.externalId,
141
+ authIsValid: authIsValid ?? null,
142
+ data: oauthData,
143
+ createdAt: now,
144
+ updatedAt: now,
145
+ };
146
+
147
+ // Encrypt before storing
148
+ const encryptedDocument = await this.encryptionService.encryptFields(
149
+ 'Credential',
150
+ plainDocument
151
+ );
152
+
153
+ const insertedId = await insertOne(this.prisma, 'Credential', encryptedDocument);
154
+
155
+ // Read back and decrypt
156
+ const created = await findOne(this.prisma, 'Credential', { _id: insertedId });
157
+ const decryptedCredential = await this.encryptionService.decryptFields('Credential', created);
158
+ return this._mapCredential(decryptedCredential);
159
+ }
160
+
161
+ async findCredential(filter) {
162
+ const query = this._buildFilter(filter);
163
+ const credential = await findOne(this.prisma, 'Credential', query);
164
+ if (!credential) return null;
165
+
166
+ // Decrypt sensitive fields using service
167
+ const decryptedCredential = await this.encryptionService.decryptFields('Credential', credential);
168
+ return this._mapCredential(decryptedCredential);
169
+ }
170
+
171
+ async updateCredential(credentialId, updates) {
172
+ const objectId = toObjectId(credentialId);
173
+ if (!objectId) return null;
174
+ const existing = await findOne(this.prisma, 'Credential', { _id: objectId });
175
+ if (!existing) return null;
176
+
177
+ const {
178
+ user,
179
+ userId,
180
+ authIsValid,
181
+ externalId,
182
+ ...oauthData
183
+ } = updates || {};
184
+
185
+ // Decrypt existing credential data first
186
+ const decryptedExisting = await this.encryptionService.decryptFields('Credential', existing);
187
+ const mergedData = { ...(decryptedExisting.data || {}), ...oauthData };
188
+
189
+ // Build update document
190
+ const updateDocument = {
191
+ userId: toObjectId(userId || user) || existing.userId || null,
192
+ externalId: externalId !== undefined ? externalId : existing.externalId,
193
+ authIsValid: authIsValid !== undefined ? authIsValid : existing.authIsValid,
194
+ data: mergedData,
195
+ updatedAt: new Date(),
196
+ };
197
+
198
+ // Encrypt before storing
199
+ const encryptedUpdate = await this.encryptionService.encryptFields(
200
+ 'Credential',
201
+ { data: updateDocument.data }
202
+ );
203
+
204
+ await updateOne(
205
+ this.prisma,
206
+ 'Credential',
207
+ { _id: objectId },
208
+ {
209
+ $set: {
210
+ userId: updateDocument.userId,
211
+ externalId: updateDocument.externalId,
212
+ authIsValid: updateDocument.authIsValid,
213
+ data: encryptedUpdate.data,
214
+ updatedAt: updateDocument.updatedAt,
215
+ },
216
+ }
217
+ );
218
+
219
+ // Read back and decrypt
220
+ const updated = await findOne(this.prisma, 'Credential', { _id: objectId });
221
+ const decryptedCredential = await this.encryptionService.decryptFields('Credential', updated);
222
+ return this._mapCredential(decryptedCredential);
223
+ }
224
+
225
+ _buildIdentifierFilter(identifiers) {
226
+ const filter = {};
227
+ if (identifiers._id || identifiers.id) {
228
+ const idObj = toObjectId(identifiers._id || identifiers.id);
229
+ if (idObj) filter._id = idObj;
230
+ }
231
+ if (identifiers.user || identifiers.userId) {
232
+ const userObj = toObjectId(identifiers.user || identifiers.userId);
233
+ if (userObj) filter.userId = userObj;
234
+ }
235
+ if (identifiers.externalId !== undefined) {
236
+ filter.externalId = identifiers.externalId;
237
+ }
238
+ return filter;
239
+ }
240
+
241
+ _buildFilter(filter) {
242
+ const query = {};
243
+ if (!filter) return query;
244
+ if (filter.credentialId || filter.id) {
245
+ const idObj = toObjectId(filter.credentialId || filter.id);
246
+ if (idObj) query._id = idObj;
247
+ }
248
+ if (filter.user || filter.userId) {
249
+ const userObj = toObjectId(filter.user || filter.userId);
250
+ if (userObj) query.userId = userObj;
251
+ }
252
+ if (filter.externalId !== undefined) {
253
+ query.externalId = filter.externalId;
254
+ }
255
+ return query;
256
+ }
257
+
258
+ /**
259
+ * Map credential document to application format (without legacy fields)
260
+ * Used by findCredential, upsertCredential, updateCredential
261
+ * Matches MongoDB repository format
262
+ * @private
263
+ */
264
+ _mapCredential(doc) {
265
+ const data = doc?.data || {};
266
+ const id = fromObjectId(doc?._id);
267
+ const userId = fromObjectId(doc?.userId);
268
+ return {
269
+ id,
270
+ userId,
271
+ externalId: doc?.externalId ?? null,
272
+ authIsValid: doc?.authIsValid ?? null,
273
+ ...data,
274
+ };
275
+ }
276
+
277
+ /**
278
+ * Map credential document with legacy fields for findCredentialById
279
+ * Includes _id and user fields for backward compatibility
280
+ * Matches MongoDB repository format
281
+ * @private
282
+ */
283
+ _mapCredentialById(doc) {
284
+ const data = doc?.data || {};
285
+ const id = fromObjectId(doc?._id);
286
+ const userId = fromObjectId(doc?.userId);
287
+ return {
288
+ _id: id,
289
+ id,
290
+ user: userId,
291
+ userId,
292
+ externalId: doc?.externalId ?? null,
293
+ authIsValid: doc?.authIsValid ?? null,
294
+ ...data,
295
+ };
296
+ }
297
+ }
298
+
299
+ module.exports = { CredentialRepositoryDocumentDB };
300
+
@@ -2,6 +2,9 @@ const { CredentialRepositoryMongo } = require('./credential-repository-mongo');
2
2
  const {
3
3
  CredentialRepositoryPostgres,
4
4
  } = require('./credential-repository-postgres');
5
+ const {
6
+ CredentialRepositoryDocumentDB,
7
+ } = require('./credential-repository-documentdb');
5
8
  const config = require('../../database/config');
6
9
 
7
10
  /**
@@ -32,9 +35,12 @@ function createCredentialRepository() {
32
35
  case 'postgresql':
33
36
  return new CredentialRepositoryPostgres();
34
37
 
38
+ case 'documentdb':
39
+ return new CredentialRepositoryDocumentDB();
40
+
35
41
  default:
36
42
  throw new Error(
37
- `Unsupported database type: ${dbType}. Supported values: 'mongodb', 'postgresql'`
43
+ `Unsupported database type: ${dbType}. Supported values: 'mongodb', 'documentdb', 'postgresql'`
38
44
  );
39
45
  }
40
46
  }
@@ -44,4 +50,5 @@ module.exports = {
44
50
  // Export adapters for direct testing
45
51
  CredentialRepositoryMongo,
46
52
  CredentialRepositoryPostgres,
53
+ CredentialRepositoryDocumentDB,
47
54
  };
@@ -10,7 +10,7 @@
10
10
  * 1. DB_TYPE environment variable (set for migration handlers)
11
11
  * 2. App definition (backend/index.js Definition.database configuration)
12
12
  *
13
- * @returns {'mongodb'|'postgresql'} Database type
13
+ * @returns {'mongodb'|'postgresql'|'documentdb'} Database type
14
14
  * @throws {Error} If database type cannot be determined or app definition missing
15
15
  */
16
16
  function getDatabaseType() {
@@ -93,7 +93,7 @@ function getDatabaseType() {
93
93
  return 'mongodb';
94
94
  }
95
95
  if (database.documentDB?.enable === true) {
96
- return 'mongodb'; // DocumentDB is MongoDB-compatible
96
+ return 'documentdb';
97
97
  }
98
98
 
99
99
  throw new Error(
@@ -114,7 +114,7 @@ function getDatabaseType() {
114
114
 
115
115
  /**
116
116
  * Cached database type (lazy evaluation)
117
- * @type {'mongodb'|'postgresql'|null}
117
+ * @type {'mongodb'|'postgresql'|'documentdb'|null}
118
118
  */
119
119
  let cachedDbType = null;
120
120
 
@@ -140,7 +140,7 @@ module.exports = {
140
140
  /**
141
141
  * Lazy-evaluated database type determined from app definition
142
142
  * Only evaluates when accessed, preventing module load failures in test environments
143
- * @type {'mongodb'|'postgresql'}
143
+ * @type {'mongodb'|'postgresql'|'documentdb'}
144
144
  */
145
145
  Object.defineProperty(module.exports, 'DB_TYPE', {
146
146
  get() {