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

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.
@@ -4,14 +4,18 @@ class GetCredentialForUser {
4
4
  }
5
5
 
6
6
  async execute(credentialId, userId) {
7
- const credential = await this.credentialRepository.findCredentialById(credentialId);
7
+ const credential = await this.credentialRepository.findCredentialById(
8
+ credentialId
9
+ );
8
10
 
9
11
  if (!credential) {
10
12
  throw new Error(`Credential with id ${credentialId} not found`);
11
13
  }
12
14
 
13
- if (credential.user.toString() !== userId.toString()) {
14
- throw new Error(`Credential ${credentialId} does not belong to user ${userId}`);
15
+ if (credential.userId.toString() !== userId.toString()) {
16
+ throw new Error(
17
+ `Credential ${credentialId} does not belong to user ${userId}`
18
+ );
15
19
  }
16
20
 
17
21
  return credential;
@@ -130,7 +130,6 @@ const ENCRYPTION_SCHEMA = {
130
130
  fields: [
131
131
  'data.access_token', // OAuth access token
132
132
  'data.refresh_token', // OAuth refresh token
133
- 'data.domain', // Service domain
134
133
  'data.id_token', // OpenID Connect ID token
135
134
  ],
136
135
  },
@@ -0,0 +1,26 @@
1
+ const { BaseError } = require('./base-error');
2
+
3
+ /**
4
+ * ClientSafeError - An error that is safe to expose to end users
5
+ *
6
+ * Use this error class when the error message does not contain sensitive
7
+ * implementation details and can be safely shown to users.
8
+ *
9
+ * Examples:
10
+ * - "Invalid Token: Token is expired"
11
+ * - "User not found"
12
+ * - "Invalid credentials"
13
+ *
14
+ * @param {string} message - The user-safe error message
15
+ * @param {number} statusCode - HTTP status code (default: 400)
16
+ * @param {object} options - Additional error options (cause, etc.)
17
+ */
18
+ class ClientSafeError extends BaseError {
19
+ constructor(message, statusCode = 400, options) {
20
+ super(message, options);
21
+ this.statusCode = statusCode;
22
+ this.isClientSafe = true;
23
+ }
24
+ }
25
+
26
+ module.exports = { ClientSafeError };
@@ -61,8 +61,9 @@ class FetchError extends BaseError {
61
61
  ];
62
62
 
63
63
  super(messageParts.filter(Boolean).join('\n'));
64
-
64
+
65
65
  this.response = response;
66
+ this.statusCode = response?.status;
66
67
  }
67
68
 
68
69
  static async create(options = {}) {
package/errors/index.js CHANGED
@@ -5,6 +5,7 @@ const {
5
5
  RequiredPropertyError,
6
6
  ParameterTypeError,
7
7
  } = require('./validation-errors');
8
+ const { ClientSafeError } = require('./client-safe-error');
8
9
 
9
10
  module.exports = {
10
11
  BaseError,
@@ -12,4 +13,5 @@ module.exports = {
12
13
  HaltError,
13
14
  RequiredPropertyError,
14
15
  ParameterTypeError,
16
+ ClientSafeError,
15
17
  };
@@ -65,9 +65,7 @@ const {
65
65
  const {
66
66
  AuthenticateWithSharedSecret,
67
67
  } = require('../user/use-cases/authenticate-with-shared-secret');
68
- const {
69
- AuthenticateUser,
70
- } = require('../user/use-cases/authenticate-user');
68
+ const { AuthenticateUser } = require('../user/use-cases/authenticate-user');
71
69
  const {
72
70
  ProcessAuthorizationCallback,
73
71
  } = require('../modules/use-cases/process-authorization-callback');
@@ -234,8 +232,10 @@ function checkRequiredParams(params, requiredKeys) {
234
232
 
235
233
  if (missingKeys.length > 0) {
236
234
  throw Boom.badRequest(
237
- `Missing Parameter${missingKeys.length === 1 ? '' : 's'
238
- }: ${missingKeys.join(', ')} ${missingKeys.length === 1 ? 'is' : 'are'
235
+ `Missing Parameter${
236
+ missingKeys.length === 1 ? '' : 's'
237
+ }: ${missingKeys.join(', ')} ${
238
+ missingKeys.length === 1 ? 'is' : 'are'
239
239
  } required.`
240
240
  );
241
241
  }
@@ -584,7 +584,7 @@ function setEntityRoutes(router, authenticateUser, useCases) {
584
584
  req.params.credentialId,
585
585
  userId
586
586
  );
587
- if (credential.user._id.toString() !== userId) {
587
+ if (credential.userId.toString() !== userId) {
588
588
  throw Boom.forbidden('Credential does not belong to user');
589
589
  }
590
590
 
@@ -12,74 +12,167 @@ const {
12
12
  const {
13
13
  IntegrationMappingRepositoryInterface,
14
14
  } = require('./integration-mapping-repository-interface');
15
-
15
+ const {
16
+ DocumentDBEncryptionService,
17
+ } = require('../../database/documentdb-encryption-service');
16
18
  class IntegrationMappingRepositoryDocumentDB extends IntegrationMappingRepositoryInterface {
17
19
  constructor() {
18
20
  super();
19
21
  this.prisma = prisma;
22
+ this.encryptionService = new DocumentDBEncryptionService();
20
23
  }
21
24
 
22
25
  async findMappingBy(integrationId, sourceId) {
23
26
  const filter = this._compositeFilter(integrationId, sourceId);
24
27
  const doc = await findOne(this.prisma, 'IntegrationMapping', filter);
25
- return doc ? this._mapMapping(doc) : null;
28
+ if (!doc) return null;
29
+
30
+ const decryptedMapping = await this.encryptionService.decryptFields(
31
+ 'IntegrationMapping',
32
+ doc
33
+ );
34
+ return this._mapMapping(decryptedMapping);
26
35
  }
27
36
 
28
37
  async upsertMapping(integrationId, sourceId, mapping) {
29
38
  const filter = this._compositeFilter(integrationId, sourceId);
30
- const existing = await findOne(this.prisma, 'IntegrationMapping', filter);
39
+ const existing = await findOne(
40
+ this.prisma,
41
+ 'IntegrationMapping',
42
+ filter
43
+ );
31
44
  const now = new Date();
32
45
 
33
46
  if (existing) {
47
+ const decryptedExisting =
48
+ await this.encryptionService.decryptFields(
49
+ 'IntegrationMapping',
50
+ existing
51
+ );
52
+
53
+ const updateDocument = {
54
+ mapping,
55
+ updatedAt: now,
56
+ };
57
+
58
+ const encryptedUpdate = await this.encryptionService.encryptFields(
59
+ 'IntegrationMapping',
60
+ { mapping: updateDocument.mapping }
61
+ );
62
+
34
63
  await updateOne(
35
64
  this.prisma,
36
65
  'IntegrationMapping',
37
66
  { _id: existing._id },
38
67
  {
39
68
  $set: {
40
- mapping,
41
- updatedAt: now,
69
+ mapping: encryptedUpdate.mapping,
70
+ updatedAt: updateDocument.updatedAt,
42
71
  },
43
72
  }
44
73
  );
45
- const updated = await findOne(this.prisma, 'IntegrationMapping', { _id: existing._id });
46
- return this._mapMapping(updated);
74
+
75
+ const updated = await findOne(this.prisma, 'IntegrationMapping', {
76
+ _id: existing._id,
77
+ });
78
+ if (!updated) {
79
+ console.error(
80
+ '[IntegrationMappingRepositoryDocumentDB] Mapping not found after update',
81
+ {
82
+ mappingId: fromObjectId(existing._id),
83
+ integrationId,
84
+ sourceId,
85
+ }
86
+ );
87
+ throw new Error(
88
+ 'Failed to update mapping: Document not found after update. ' +
89
+ 'This indicates a database consistency issue.'
90
+ );
91
+ }
92
+ const decryptedMapping = await this.encryptionService.decryptFields(
93
+ 'IntegrationMapping',
94
+ updated
95
+ );
96
+ return this._mapMapping(decryptedMapping);
47
97
  }
48
98
 
49
- const document = {
50
- integrationId: toObjectId(integrationId),
51
- sourceId: sourceId === null || sourceId === undefined ? null : String(sourceId),
99
+ const plainDocument = {
100
+ integrationId: integrationId,
101
+ sourceId:
102
+ sourceId === null || sourceId === undefined
103
+ ? null
104
+ : String(sourceId),
52
105
  mapping,
53
106
  createdAt: now,
54
107
  updatedAt: now,
55
108
  };
56
- const insertedId = await insertOne(this.prisma, 'IntegrationMapping', document);
57
- const created = await findOne(this.prisma, 'IntegrationMapping', { _id: insertedId });
58
- return this._mapMapping(created);
109
+
110
+ const encryptedDocument = await this.encryptionService.encryptFields(
111
+ 'IntegrationMapping',
112
+ plainDocument
113
+ );
114
+
115
+ const insertedId = await insertOne(
116
+ this.prisma,
117
+ 'IntegrationMapping',
118
+ encryptedDocument
119
+ );
120
+
121
+ const created = await findOne(this.prisma, 'IntegrationMapping', {
122
+ _id: insertedId,
123
+ });
124
+ if (!created) {
125
+ console.error(
126
+ '[IntegrationMappingRepositoryDocumentDB] Mapping not found after insert',
127
+ {
128
+ insertedId: fromObjectId(insertedId),
129
+ integrationId,
130
+ sourceId,
131
+ }
132
+ );
133
+ throw new Error(
134
+ 'Failed to create mapping: Document not found after insert. ' +
135
+ 'This indicates a database consistency issue.'
136
+ );
137
+ }
138
+ const decryptedMapping = await this.encryptionService.decryptFields(
139
+ 'IntegrationMapping',
140
+ created
141
+ );
142
+ return this._mapMapping(decryptedMapping);
59
143
  }
60
144
 
61
145
  async findMappingsByIntegration(integrationId) {
62
146
  const filter = {};
63
- const integrationObjectId = toObjectId(integrationId);
64
- if (integrationObjectId) filter.integrationId = integrationObjectId;
147
+ if (integrationId) filter.integrationId = integrationId;
65
148
  const docs = await findMany(this.prisma, 'IntegrationMapping', filter);
66
- return docs.map((doc) => this._mapMapping(doc));
149
+
150
+ const decryptedDocs = await Promise.all(
151
+ docs.map((doc) =>
152
+ this.encryptionService.decryptFields('IntegrationMapping', doc)
153
+ )
154
+ );
155
+
156
+ return decryptedDocs.map((doc) => this._mapMapping(doc));
67
157
  }
68
158
 
69
159
  async deleteMapping(integrationId, sourceId) {
70
160
  const filter = this._compositeFilter(integrationId, sourceId);
71
- const result = await deleteOne(this.prisma, 'IntegrationMapping', filter);
161
+ const result = await deleteOne(
162
+ this.prisma,
163
+ 'IntegrationMapping',
164
+ filter
165
+ );
72
166
  const deleted = result?.n ?? 0;
73
167
  return { acknowledged: true, deletedCount: deleted };
74
168
  }
75
169
 
76
170
  async deleteMappingsByIntegration(integrationId) {
77
- const integrationObjectId = toObjectId(integrationId);
78
- if (!integrationObjectId) {
171
+ if (!integrationId) {
79
172
  return { acknowledged: true, deletedCount: 0 };
80
173
  }
81
174
  const result = await deleteMany(this.prisma, 'IntegrationMapping', {
82
- integrationId: integrationObjectId,
175
+ integrationId: integrationId,
83
176
  });
84
177
  const deleted = result?.n ?? 0;
85
178
  return { acknowledged: true, deletedCount: deleted };
@@ -88,32 +181,84 @@ class IntegrationMappingRepositoryDocumentDB extends IntegrationMappingRepositor
88
181
  async findMappingById(id) {
89
182
  const objectId = toObjectId(id);
90
183
  if (!objectId) return null;
91
- const doc = await findOne(this.prisma, 'IntegrationMapping', { _id: objectId });
92
- return doc ? this._mapMapping(doc) : null;
184
+ const doc = await findOne(this.prisma, 'IntegrationMapping', {
185
+ _id: objectId,
186
+ });
187
+ if (!doc) return null;
188
+
189
+ const decryptedMapping = await this.encryptionService.decryptFields(
190
+ 'IntegrationMapping',
191
+ doc
192
+ );
193
+ return this._mapMapping(decryptedMapping);
93
194
  }
94
195
 
95
196
  async updateMapping(id, updates) {
96
197
  const objectId = toObjectId(id);
97
198
  if (!objectId) return null;
199
+
200
+ const existing = await findOne(this.prisma, 'IntegrationMapping', {
201
+ _id: objectId,
202
+ });
203
+ if (!existing) return null;
204
+
205
+ const decryptedExisting = await this.encryptionService.decryptFields(
206
+ 'IntegrationMapping',
207
+ existing
208
+ );
209
+
210
+ const mergedMapping =
211
+ updates.mapping !== undefined
212
+ ? updates.mapping
213
+ : decryptedExisting.mapping;
214
+
215
+ const updateDocument = {
216
+ ...updates,
217
+ updatedAt: new Date(),
218
+ };
219
+
220
+ if (mergedMapping !== undefined) {
221
+ const encryptedUpdate = await this.encryptionService.encryptFields(
222
+ 'IntegrationMapping',
223
+ { mapping: mergedMapping }
224
+ );
225
+ updateDocument.mapping = encryptedUpdate.mapping;
226
+ }
227
+
98
228
  await updateOne(
99
229
  this.prisma,
100
230
  'IntegrationMapping',
101
231
  { _id: objectId },
102
232
  {
103
- $set: {
104
- ...updates,
105
- updatedAt: new Date(),
106
- },
233
+ $set: updateDocument,
107
234
  }
108
235
  );
109
- const updated = await findOne(this.prisma, 'IntegrationMapping', { _id: objectId });
110
- return updated ? this._mapMapping(updated) : null;
236
+
237
+ const updated = await findOne(this.prisma, 'IntegrationMapping', {
238
+ _id: objectId,
239
+ });
240
+ if (!updated) {
241
+ console.error(
242
+ '[IntegrationMappingRepositoryDocumentDB] Mapping not found after update',
243
+ {
244
+ mappingId: fromObjectId(objectId),
245
+ }
246
+ );
247
+ throw new Error(
248
+ 'Failed to update mapping: Document not found after update. ' +
249
+ 'This indicates a database consistency issue.'
250
+ );
251
+ }
252
+ const decryptedMapping = await this.encryptionService.decryptFields(
253
+ 'IntegrationMapping',
254
+ updated
255
+ );
256
+ return this._mapMapping(decryptedMapping);
111
257
  }
112
258
 
113
259
  _compositeFilter(integrationId, sourceId) {
114
260
  const filter = {};
115
- const integrationObjectId = toObjectId(integrationId);
116
- if (integrationObjectId) filter.integrationId = integrationObjectId;
261
+ if (integrationId) filter.integrationId = integrationId;
117
262
  if (sourceId !== undefined) {
118
263
  filter.sourceId = sourceId === null ? null : String(sourceId);
119
264
  }
@@ -123,13 +268,13 @@ class IntegrationMappingRepositoryDocumentDB extends IntegrationMappingRepositor
123
268
  _mapMapping(doc) {
124
269
  return {
125
270
  id: fromObjectId(doc?._id),
126
- integrationId: fromObjectId(doc?.integrationId),
271
+ integrationId: doc?.integrationId ?? null,
127
272
  sourceId: doc?.sourceId ?? null,
128
273
  mapping: doc?.mapping ?? null,
274
+ createdAt: doc?.createdAt,
275
+ updatedAt: doc?.updatedAt,
129
276
  };
130
277
  }
131
278
  }
132
279
 
133
280
  module.exports = { IntegrationMappingRepositoryDocumentDB };
134
-
135
-
@@ -127,6 +127,17 @@ class IntegrationRepositoryDocumentDB extends IntegrationRepositoryInterface {
127
127
  };
128
128
  const insertedId = await insertOne(this.prisma, 'Integration', document);
129
129
  const created = await findOne(this.prisma, 'Integration', { _id: insertedId });
130
+ if (!created) {
131
+ console.error('[IntegrationRepositoryDocumentDB] Integration not found after insert', {
132
+ insertedId: fromObjectId(insertedId),
133
+ userId,
134
+ config,
135
+ });
136
+ throw new Error(
137
+ 'Failed to create integration: Document not found after insert. ' +
138
+ 'This indicates a database consistency issue.'
139
+ );
140
+ }
130
141
  return this._mapIntegration(created);
131
142
  }
132
143
 
@@ -157,6 +168,16 @@ class IntegrationRepositoryDocumentDB extends IntegrationRepositoryInterface {
157
168
  }
158
169
  );
159
170
  const updated = await findOne(this.prisma, 'Integration', { _id: objectId });
171
+ if (!updated) {
172
+ console.error('[IntegrationRepositoryDocumentDB] Integration not found after update', {
173
+ integrationId: fromObjectId(objectId),
174
+ config,
175
+ });
176
+ throw new Error(
177
+ 'Failed to update integration: Document not found after update. ' +
178
+ 'This indicates a database consistency issue.'
179
+ );
180
+ }
160
181
  return this._mapIntegration(updated);
161
182
  }
162
183