@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.
- package/application/commands/credential-commands.js +1 -1
- package/core/create-handler.js +12 -0
- package/credential/repositories/credential-repository-documentdb.js +81 -77
- package/credential/repositories/credential-repository-mongo.js +16 -54
- package/credential/repositories/credential-repository-postgres.js +14 -41
- package/credential/use-cases/get-credential-for-user.js +7 -3
- package/database/encryption/README.md +0 -1
- package/errors/client-safe-error.js +26 -0
- package/errors/fetch-error.js +2 -1
- package/errors/index.js +2 -0
- package/integrations/integration-router.js +6 -6
- package/integrations/repositories/integration-mapping-repository-documentdb.js +178 -33
- package/integrations/repositories/integration-repository-documentdb.js +21 -0
- package/integrations/repositories/process-repository-documentdb.js +143 -41
- package/package.json +5 -5
- package/token/repositories/token-repository-documentdb.js +20 -8
- package/token/repositories/token-repository-mongo.js +10 -3
- package/token/repositories/token-repository-postgres.js +10 -3
- package/user/repositories/user-repository-documentdb.js +177 -37
- package/user/repositories/user-repository-mongo.js +3 -2
- package/user/repositories/user-repository-postgres.js +3 -2
- package/user/use-cases/login-user.js +1 -1
|
@@ -4,14 +4,18 @@ class GetCredentialForUser {
|
|
|
4
4
|
}
|
|
5
5
|
|
|
6
6
|
async execute(credentialId, userId) {
|
|
7
|
-
const credential = await this.credentialRepository.findCredentialById(
|
|
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.
|
|
14
|
-
throw new Error(
|
|
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;
|
|
@@ -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 };
|
package/errors/fetch-error.js
CHANGED
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${
|
|
238
|
-
|
|
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.
|
|
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
|
-
|
|
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(
|
|
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:
|
|
69
|
+
mapping: encryptedUpdate.mapping,
|
|
70
|
+
updatedAt: updateDocument.updatedAt,
|
|
42
71
|
},
|
|
43
72
|
}
|
|
44
73
|
);
|
|
45
|
-
|
|
46
|
-
|
|
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
|
|
50
|
-
integrationId:
|
|
51
|
-
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
|
-
|
|
57
|
-
const
|
|
58
|
-
|
|
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
|
-
|
|
64
|
-
if (integrationObjectId) filter.integrationId = integrationObjectId;
|
|
147
|
+
if (integrationId) filter.integrationId = integrationId;
|
|
65
148
|
const docs = await findMany(this.prisma, 'IntegrationMapping', filter);
|
|
66
|
-
|
|
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(
|
|
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
|
-
|
|
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:
|
|
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', {
|
|
92
|
-
|
|
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
|
-
|
|
110
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|