@friggframework/core 2.0.0--canary.499.2ef107f.0 → 2.0.0--canary.499.4c64a49.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.
@@ -21,7 +21,7 @@ class CredentialRepositoryDocumentDB extends CredentialRepositoryInterface {
21
21
  const objectId = toObjectId(id);
22
22
  if (!objectId) return null;
23
23
  const doc = await findOne(this.prisma, 'Credential', { _id: objectId });
24
- return doc ? this._mapCredential(doc) : null;
24
+ return doc ? this._mapCredentialById(doc) : null;
25
25
  }
26
26
 
27
27
  async updateAuthenticationStatus(credentialId, authIsValid) {
@@ -95,8 +95,8 @@ class CredentialRepositoryDocumentDB extends CredentialRepositoryInterface {
95
95
  }
96
96
 
97
97
  const document = {
98
- userId: toObjectId(userId || user) || null,
99
- externalId,
98
+ userId: toObjectId(userId || user || identifiers.user),
99
+ externalId: externalId !== undefined ? externalId : identifiers.externalId,
100
100
  authIsValid: authIsValid ?? null,
101
101
  data: oauthData,
102
102
  createdAt: now,
@@ -183,7 +183,32 @@ class CredentialRepositoryDocumentDB extends CredentialRepositoryInterface {
183
183
  return query;
184
184
  }
185
185
 
186
+ /**
187
+ * Map credential document to application format (without legacy fields)
188
+ * Used by findCredential, upsertCredential, updateCredential
189
+ * Matches MongoDB repository format
190
+ * @private
191
+ */
186
192
  _mapCredential(doc) {
193
+ const data = doc?.data || {};
194
+ const id = fromObjectId(doc?._id);
195
+ const userId = fromObjectId(doc?.userId);
196
+ return {
197
+ id,
198
+ userId,
199
+ externalId: doc?.externalId ?? null,
200
+ authIsValid: doc?.authIsValid ?? null,
201
+ ...data,
202
+ };
203
+ }
204
+
205
+ /**
206
+ * Map credential document with legacy fields for findCredentialById
207
+ * Includes _id and user fields for backward compatibility
208
+ * Matches MongoDB repository format
209
+ * @private
210
+ */
211
+ _mapCredentialById(doc) {
187
212
  const data = doc?.data || {};
188
213
  const id = fromObjectId(doc?._id);
189
214
  const userId = fromObjectId(doc?.userId);
@@ -35,19 +35,16 @@ async function findOne(client, collection, filter = {}, options = {}) {
35
35
  }
36
36
 
37
37
  async function insertOne(client, collection, document) {
38
- const result = await client.$runCommandRaw({
38
+ // Generate ObjectId if not present (MongoDB raw insert doesn't return insertedIds)
39
+ const _id = document._id || new ObjectId();
40
+ const docWithId = { ...document, _id };
41
+
42
+ await client.$runCommandRaw({
39
43
  insert: collection,
40
- documents: [document],
44
+ documents: [docWithId],
41
45
  });
42
- if (Array.isArray(result?.insertedIds)) return result.insertedIds[0];
43
- if (result?.insertedIds && typeof result.insertedIds === 'object') {
44
- const firstKey = Object.keys(result.insertedIds)[0];
45
- if (firstKey !== undefined) {
46
- return result.insertedIds[firstKey];
47
- }
48
- }
49
- if (result?.insertedId) return result.insertedId;
50
- return null;
46
+
47
+ return _id;
51
48
  }
52
49
 
53
50
  async function updateOne(client, collection, filter, update, options = {}) {
@@ -9,11 +9,111 @@ const {
9
9
  deleteOne,
10
10
  } = require('../../database/documentdb-utils');
11
11
  const { ModuleRepositoryInterface } = require('./module-repository-interface');
12
+ const { Cryptor } = require('../../encrypt/Cryptor');
13
+ const { getEncryptedFields } = require('../../database/encryption/encryption-schema-registry');
12
14
 
13
15
  class ModuleRepositoryDocumentDB extends ModuleRepositoryInterface {
14
16
  constructor() {
15
17
  super();
16
18
  this.prisma = prisma;
19
+ this._initializeCryptor();
20
+ }
21
+
22
+ _initializeCryptor() {
23
+ // Match logic from @friggframework/core/database/prisma.js
24
+ const stage = process.env.STAGE || process.env.NODE_ENV || 'development';
25
+ const bypassEncryption = ['dev', 'test', 'local'].includes(stage.toLowerCase());
26
+
27
+ if (bypassEncryption) {
28
+ this.cryptor = null;
29
+ return;
30
+ }
31
+
32
+ // Determine encryption method
33
+ const hasKMS = process.env.KMS_KEY_ARN && process.env.KMS_KEY_ARN.trim() !== '';
34
+ const hasAES = process.env.AES_KEY_ID && process.env.AES_KEY_ID.trim() !== '';
35
+
36
+ if (!hasKMS && !hasAES) {
37
+ console.warn('No encryption keys configured. Encryption disabled.');
38
+ this.cryptor = null;
39
+ return;
40
+ }
41
+
42
+ const shouldUseAws = hasKMS;
43
+ this.cryptor = new Cryptor({ shouldUseAws });
44
+ }
45
+
46
+ _isEncryptedValue(value) {
47
+ // Envelope encryption format: "keyId:encryptedPart1:encryptedPart2:encryptedKey"
48
+ // Must be string with at least 4 colon-separated parts
49
+ if (typeof value !== 'string') {
50
+ return false;
51
+ }
52
+
53
+ const parts = value.split(':');
54
+ return parts.length >= 4;
55
+ }
56
+
57
+ async _decryptField(encryptedValue, context = '') {
58
+ // If encryption is disabled, return as-is
59
+ if (!this.cryptor) {
60
+ return encryptedValue;
61
+ }
62
+
63
+ // If not encrypted format, return as-is
64
+ if (!this._isEncryptedValue(encryptedValue)) {
65
+ return encryptedValue;
66
+ }
67
+
68
+ try {
69
+ // Decrypt using Cryptor
70
+ const decryptedString = await this.cryptor.decrypt(encryptedValue);
71
+
72
+ // Try to parse as JSON (for objects/arrays)
73
+ try {
74
+ return JSON.parse(decryptedString);
75
+ } catch {
76
+ // Not JSON, return as string
77
+ return decryptedString;
78
+ }
79
+ } catch (error) {
80
+ console.error(`Failed to decrypt field${context ? ` (${context})` : ''}:`, error.message);
81
+ // Return null on decryption failure to avoid exposing encrypted data
82
+ return null;
83
+ }
84
+ }
85
+
86
+ async _decryptCredentialData(rawData) {
87
+ if (!rawData || typeof rawData !== 'object') {
88
+ return rawData;
89
+ }
90
+
91
+ // Get encrypted fields from registry
92
+ const encryptedFieldsConfig = getEncryptedFields('Credential');
93
+ if (!encryptedFieldsConfig || !encryptedFieldsConfig.fields) {
94
+ return rawData;
95
+ }
96
+
97
+ const decrypted = {};
98
+
99
+ for (const [key, value] of Object.entries(rawData)) {
100
+ // Check if this field is in the encrypted fields list
101
+ const isEncrypted = encryptedFieldsConfig.fields.some(field => {
102
+ // Support both top-level and nested fields (e.g., 'data.access_token')
103
+ const fieldPath = field.split('.');
104
+ return fieldPath[fieldPath.length - 1] === key;
105
+ });
106
+
107
+ if (isEncrypted) {
108
+ // Decrypt encrypted fields
109
+ decrypted[key] = await this._decryptField(value, `Credential.data.${key}`);
110
+ } else {
111
+ // Pass through non-encrypted fields
112
+ decrypted[key] = value;
113
+ }
114
+ }
115
+
116
+ return decrypted;
17
117
  }
18
118
 
19
119
  async findEntityById(entityId) {
@@ -31,10 +131,13 @@ class ModuleRepositoryDocumentDB extends ModuleRepositoryInterface {
31
131
 
32
132
  async findEntitiesByUserId(userId) {
33
133
  const objectId = toObjectId(userId);
34
- const filter = objectId ? { userId: objectId } : {};
134
+ if (!objectId) {
135
+ throw new Error(`Invalid userId: ${userId}`);
136
+ }
137
+ const filter = { userId: objectId };
35
138
  const docs = await findMany(this.prisma, 'Entity', filter);
36
139
  const credentialMap = await this._fetchCredentialsBulk(docs.map((doc) => doc.credentialId));
37
- return docs.map((doc) => this._mapEntity(doc, credentialMap.get(fromObjectId(doc.credentialId)) || credentialMap.get(doc.credentialId) || null));
140
+ return docs.map((doc) => this._mapEntity(doc, credentialMap.get(fromObjectId(doc.credentialId)) || null));
38
141
  }
39
142
 
40
143
  async findEntitiesByIds(entitiesIds) {
@@ -42,18 +145,21 @@ class ModuleRepositoryDocumentDB extends ModuleRepositoryInterface {
42
145
  if (ids.length === 0) return [];
43
146
  const docs = await findMany(this.prisma, 'Entity', { _id: { $in: ids } });
44
147
  const credentialMap = await this._fetchCredentialsBulk(docs.map((doc) => doc.credentialId));
45
- return docs.map((doc) => this._mapEntity(doc, credentialMap.get(fromObjectId(doc.credentialId)) || credentialMap.get(doc.credentialId) || null));
148
+ return docs.map((doc) => this._mapEntity(doc, credentialMap.get(fromObjectId(doc.credentialId)) || null));
46
149
  }
47
150
 
48
151
  async findEntitiesByUserIdAndModuleName(userId, moduleName) {
49
152
  const objectId = toObjectId(userId);
153
+ if (!objectId) {
154
+ throw new Error(`Invalid userId: ${userId}`);
155
+ }
50
156
  const filter = {
51
- ...(objectId ? { userId: objectId } : {}),
157
+ userId: objectId,
52
158
  moduleName,
53
159
  };
54
160
  const docs = await findMany(this.prisma, 'Entity', filter);
55
161
  const credentialMap = await this._fetchCredentialsBulk(docs.map((doc) => doc.credentialId));
56
- return docs.map((doc) => this._mapEntity(doc, credentialMap.get(fromObjectId(doc.credentialId)) || credentialMap.get(doc.credentialId) || null));
162
+ return docs.map((doc) => this._mapEntity(doc, credentialMap.get(fromObjectId(doc.credentialId)) || null));
57
163
  }
58
164
 
59
165
  async unsetCredential(entityId) {
@@ -66,7 +172,6 @@ class ModuleRepositoryDocumentDB extends ModuleRepositoryInterface {
66
172
  {
67
173
  $set: {
68
174
  credentialId: null,
69
- updatedAt: new Date(),
70
175
  },
71
176
  }
72
177
  );
@@ -82,7 +187,6 @@ class ModuleRepositoryDocumentDB extends ModuleRepositoryInterface {
82
187
  }
83
188
 
84
189
  async createEntity(entityData) {
85
- const now = new Date();
86
190
  const document = {
87
191
  userId: toObjectId(entityData.user || entityData.userId),
88
192
  credentialId: toObjectId(entityData.credential || entityData.credentialId) || null,
@@ -90,10 +194,6 @@ class ModuleRepositoryDocumentDB extends ModuleRepositoryInterface {
90
194
  moduleName: entityData.moduleName ?? null,
91
195
  externalId: entityData.externalId ?? null,
92
196
  accountId: entityData.accountId ?? null,
93
- integrationIds: (entityData.integrationIds || []).map((id) => toObjectId(id)).filter(Boolean),
94
- syncIds: (entityData.syncIds || []).map((id) => toObjectId(id)).filter(Boolean),
95
- createdAt: now,
96
- updatedAt: now,
97
197
  };
98
198
  const insertedId = await insertOne(this.prisma, 'Entity', document);
99
199
  const created = await findOne(this.prisma, 'Entity', { _id: insertedId });
@@ -117,13 +217,6 @@ class ModuleRepositoryDocumentDB extends ModuleRepositoryInterface {
117
217
  if (updates.moduleName !== undefined) updatePayload.moduleName = updates.moduleName;
118
218
  if (updates.externalId !== undefined) updatePayload.externalId = updates.externalId;
119
219
  if (updates.accountId !== undefined) updatePayload.accountId = updates.accountId;
120
- if (updates.integrationIds !== undefined) {
121
- updatePayload.integrationIds = (updates.integrationIds || []).map((id) => toObjectId(id)).filter(Boolean);
122
- }
123
- if (updates.syncIds !== undefined) {
124
- updatePayload.syncIds = (updates.syncIds || []).map((id) => toObjectId(id)).filter(Boolean);
125
- }
126
- updatePayload.updatedAt = new Date();
127
220
  const result = await updateOne(
128
221
  this.prisma,
129
222
  'Entity',
@@ -148,9 +241,40 @@ class ModuleRepositoryDocumentDB extends ModuleRepositoryInterface {
148
241
  async _fetchCredential(credentialId) {
149
242
  const id = fromObjectId(credentialId);
150
243
  if (!id) return null;
151
- return this.prisma.credential.findUnique({
152
- where: { id },
153
- });
244
+
245
+ try {
246
+ // Convert to ObjectId for raw query
247
+ const objectId = toObjectId(id);
248
+ if (!objectId) return null;
249
+
250
+ // Use raw findOne to bypass Prisma encryption extension
251
+ const rawCredential = await findOne(this.prisma, 'Credential', {
252
+ _id: objectId
253
+ });
254
+
255
+ if (!rawCredential) return null;
256
+
257
+ // Manually decrypt data field
258
+ const decryptedData = await this._decryptCredentialData(
259
+ rawCredential.data || {}
260
+ );
261
+
262
+ // Return in same format
263
+ const credential = {
264
+ id: fromObjectId(rawCredential._id),
265
+ userId: fromObjectId(rawCredential.userId),
266
+ externalId: rawCredential.externalId ?? null,
267
+ authIsValid: rawCredential.authIsValid ?? null,
268
+ createdAt: rawCredential.createdAt,
269
+ updatedAt: rawCredential.updatedAt,
270
+ data: decryptedData
271
+ };
272
+
273
+ return this._convertCredentialIds(credential);
274
+ } catch (error) {
275
+ console.error(`Failed to fetch/decrypt credential ${id}:`, error.message);
276
+ return null;
277
+ }
154
278
  }
155
279
 
156
280
  async _fetchCredentialsBulk(credentialIds) {
@@ -158,14 +282,76 @@ class ModuleRepositoryDocumentDB extends ModuleRepositoryInterface {
158
282
  .map((value) => fromObjectId(value))
159
283
  .filter((value) => value !== null && value !== undefined);
160
284
  if (ids.length === 0) return new Map();
161
- const credentials = await this.prisma.credential.findMany({
162
- where: { id: { in: ids } },
163
- });
164
- const map = new Map();
165
- for (const credential of credentials) {
166
- map.set(credential.id, credential);
285
+
286
+ try {
287
+ // Convert string IDs to ObjectIds for bulk query
288
+ const objectIds = ids.map(id => toObjectId(id)).filter(Boolean);
289
+ if (objectIds.length === 0) return new Map();
290
+
291
+ // Use raw findMany to bypass Prisma encryption extension
292
+ const rawCredentials = await findMany(this.prisma, 'Credential', {
293
+ _id: { $in: objectIds }
294
+ });
295
+
296
+ // Decrypt all credentials in parallel
297
+ const decryptionPromises = rawCredentials.map(async (rawCredential) => {
298
+ try {
299
+ // Manually decrypt the data field
300
+ const decryptedData = await this._decryptCredentialData(
301
+ rawCredential.data || {}
302
+ );
303
+
304
+ // Build credential object in same format as Prisma would return
305
+ const credential = {
306
+ id: fromObjectId(rawCredential._id),
307
+ userId: fromObjectId(rawCredential.userId),
308
+ externalId: rawCredential.externalId ?? null,
309
+ authIsValid: rawCredential.authIsValid ?? null,
310
+ createdAt: rawCredential.createdAt,
311
+ updatedAt: rawCredential.updatedAt,
312
+ data: decryptedData
313
+ };
314
+
315
+ return this._convertCredentialIds(credential);
316
+ } catch (error) {
317
+ const credId = fromObjectId(rawCredential._id);
318
+ console.error(`Failed to decrypt credential ${credId}:`, error.message);
319
+ return null;
320
+ }
321
+ });
322
+
323
+ // Wait for all decryptions to complete
324
+ const decryptedCredentials = await Promise.all(decryptionPromises);
325
+
326
+ // Build Map from results, filtering out nulls
327
+ const map = new Map();
328
+ decryptedCredentials.forEach(credential => {
329
+ if (credential) {
330
+ map.set(credential.id, credential);
331
+ }
332
+ });
333
+
334
+ return map;
335
+ } catch (error) {
336
+ console.error('Failed to fetch credentials bulk:', error.message);
337
+ return new Map();
167
338
  }
168
- return map;
339
+ }
340
+
341
+ /**
342
+ * Convert credential object IDs to strings for application layer
343
+ * Ensures consistent credential format across database adapters
344
+ * @private
345
+ * @param {Object|null} credential - Credential object from database
346
+ * @returns {Object|null} Credential with properly formatted IDs
347
+ */
348
+ _convertCredentialIds(credential) {
349
+ if (!credential) return credential;
350
+ return {
351
+ ...credential,
352
+ id: credential.id ? String(credential.id) : null,
353
+ userId: credential.userId ? String(credential.userId) : null,
354
+ };
169
355
  }
170
356
 
171
357
  _buildFilter(filter) {
@@ -183,10 +369,9 @@ class ModuleRepositoryDocumentDB extends ModuleRepositoryInterface {
183
369
  const credObj = toObjectId(filter.credential || filter.credentialId);
184
370
  if (credObj) query.credentialId = credObj;
185
371
  }
186
- if (filter.name !== undefined) query.name = filter.name;
187
- if (filter.moduleName !== undefined) query.moduleName = filter.moduleName;
188
- if (filter.externalId !== undefined) query.externalId = filter.externalId;
189
- if (filter.accountId !== undefined) query.accountId = filter.accountId;
372
+ if (filter.name) query.name = filter.name;
373
+ if (filter.moduleName) query.moduleName = filter.moduleName;
374
+ if (filter.externalId) query.externalId = filter.externalId;
190
375
  return query;
191
376
  }
192
377
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@friggframework/core",
3
3
  "prettier": "@friggframework/prettier-config",
4
- "version": "2.0.0--canary.499.2ef107f.0",
4
+ "version": "2.0.0--canary.499.4c64a49.0",
5
5
  "dependencies": {
6
6
  "@aws-sdk/client-apigatewaymanagementapi": "^3.588.0",
7
7
  "@aws-sdk/client-kms": "^3.588.0",
@@ -38,9 +38,9 @@
38
38
  }
39
39
  },
40
40
  "devDependencies": {
41
- "@friggframework/eslint-config": "2.0.0--canary.499.2ef107f.0",
42
- "@friggframework/prettier-config": "2.0.0--canary.499.2ef107f.0",
43
- "@friggframework/test": "2.0.0--canary.499.2ef107f.0",
41
+ "@friggframework/eslint-config": "2.0.0--canary.499.4c64a49.0",
42
+ "@friggframework/prettier-config": "2.0.0--canary.499.4c64a49.0",
43
+ "@friggframework/test": "2.0.0--canary.499.4c64a49.0",
44
44
  "@prisma/client": "^6.17.0",
45
45
  "@types/lodash": "4.17.15",
46
46
  "@typescript-eslint/eslint-plugin": "^8.0.0",
@@ -80,5 +80,5 @@
80
80
  "publishConfig": {
81
81
  "access": "public"
82
82
  },
83
- "gitHead": "2ef107f34b011aef53330f9c5e4f79316c8fcf3b"
83
+ "gitHead": "4c64a49a369d67e2c2fb831e134db33c6d4cc6ca"
84
84
  }