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

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.
@@ -10,6 +10,7 @@ const {
10
10
  deleteMany,
11
11
  } = require('../../database/documentdb-utils');
12
12
  const { TokenRepositoryInterface } = require('./token-repository-interface');
13
+ const { ClientSafeError } = require('../../errors');
13
14
 
14
15
  const BCRYPT_ROUNDS = 10;
15
16
 
@@ -30,25 +31,36 @@ class TokenRepositoryDocumentDB extends TokenRepositoryInterface {
30
31
  created: now,
31
32
  };
32
33
  const insertedId = await insertOne(this.prisma, 'Token', document);
33
- const created = await findOne(this.prisma, 'Token', { _id: insertedId });
34
+ const created = await findOne(this.prisma, 'Token', {
35
+ _id: insertedId,
36
+ });
34
37
  return this._mapToken(created);
35
38
  }
36
39
 
37
40
  async validateAndGetToken(tokenObj) {
38
41
  const objectId = toObjectId(tokenObj.id);
39
42
  if (!objectId) {
40
- throw new Error('Invalid Token: Token does not exist');
43
+ throw new ClientSafeError(
44
+ 'Invalid Token: Token does not exist',
45
+ 401
46
+ );
41
47
  }
42
48
  const record = await findOne(this.prisma, 'Token', { _id: objectId });
43
49
  if (!record) {
44
- throw new Error('Invalid Token: Token does not exist');
50
+ throw new ClientSafeError(
51
+ 'Invalid Token: Token does not exist',
52
+ 401
53
+ );
45
54
  }
46
55
  const isValid = await bcrypt.compare(tokenObj.token, record.token);
47
56
  if (!isValid) {
48
- throw new Error('Invalid Token: Token does not match');
57
+ throw new ClientSafeError(
58
+ 'Invalid Token: Token does not match',
59
+ 401
60
+ );
49
61
  }
50
62
  if (record.expires && new Date(record.expires) < new Date()) {
51
- throw new Error('Invalid Token: Token is expired');
63
+ throw new ClientSafeError('Invalid Token: Token is expired', 401);
52
64
  }
53
65
  return this._mapToken(record);
54
66
  }
@@ -86,7 +98,9 @@ class TokenRepositoryDocumentDB extends TokenRepositoryInterface {
86
98
  async deleteTokensByUserId(userId) {
87
99
  const objectId = toObjectId(userId);
88
100
  if (!objectId) return { acknowledged: true, deletedCount: 0 };
89
- const result = await deleteMany(this.prisma, 'Token', { userId: objectId });
101
+ const result = await deleteMany(this.prisma, 'Token', {
102
+ userId: objectId,
103
+ });
90
104
  const deleted = result?.n ?? 0;
91
105
  return { acknowledged: true, deletedCount: deleted };
92
106
  }
@@ -121,5 +135,3 @@ class TokenRepositoryDocumentDB extends TokenRepositoryInterface {
121
135
  }
122
136
 
123
137
  module.exports = { TokenRepositoryDocumentDB };
124
-
125
-
@@ -1,6 +1,7 @@
1
1
  const { prisma } = require('../../database/prisma');
2
2
  const bcrypt = require('bcryptjs');
3
3
  const { TokenRepositoryInterface } = require('./token-repository-interface');
4
+ const { ClientSafeError } = require('../../errors');
4
5
 
5
6
  const BCRYPT_ROUNDS = 10;
6
7
 
@@ -58,7 +59,10 @@ class TokenRepositoryMongo extends TokenRepositoryInterface {
58
59
  });
59
60
 
60
61
  if (!sessionToken) {
61
- throw new Error('Invalid Token: Token does not exist');
62
+ throw new ClientSafeError(
63
+ 'Invalid Token: Token does not exist',
64
+ 401
65
+ );
62
66
  }
63
67
 
64
68
  // Verify token hash matches
@@ -67,7 +71,10 @@ class TokenRepositoryMongo extends TokenRepositoryInterface {
67
71
  sessionToken.token
68
72
  );
69
73
  if (!isValid) {
70
- throw new Error('Invalid Token: Token does not match');
74
+ throw new ClientSafeError(
75
+ 'Invalid Token: Token does not match',
76
+ 401
77
+ );
71
78
  }
72
79
 
73
80
  // Check if token is expired
@@ -75,7 +82,7 @@ class TokenRepositoryMongo extends TokenRepositoryInterface {
75
82
  sessionToken.expires &&
76
83
  new Date(sessionToken.expires) < new Date()
77
84
  ) {
78
- throw new Error('Invalid Token: Token is expired');
85
+ throw new ClientSafeError('Invalid Token: Token is expired', 401);
79
86
  }
80
87
 
81
88
  return sessionToken;
@@ -1,6 +1,7 @@
1
1
  const { prisma } = require('../../database/prisma');
2
2
  const bcrypt = require('bcryptjs');
3
3
  const { TokenRepositoryInterface } = require('./token-repository-interface');
4
+ const { ClientSafeError } = require('../../errors');
4
5
 
5
6
  const BCRYPT_ROUNDS = 10;
6
7
 
@@ -92,7 +93,10 @@ class TokenRepositoryPostgres extends TokenRepositoryInterface {
92
93
  });
93
94
 
94
95
  if (!sessionToken) {
95
- throw new Error('Invalid Token: Token does not exist');
96
+ throw new ClientSafeError(
97
+ 'Invalid Token: Token does not exist',
98
+ 401
99
+ );
96
100
  }
97
101
 
98
102
  // Verify token hash matches
@@ -101,7 +105,10 @@ class TokenRepositoryPostgres extends TokenRepositoryInterface {
101
105
  sessionToken.token
102
106
  );
103
107
  if (!isValid) {
104
- throw new Error('Invalid Token: Token does not match');
108
+ throw new ClientSafeError(
109
+ 'Invalid Token: Token does not match',
110
+ 401
111
+ );
105
112
  }
106
113
 
107
114
  // Check if token is expired
@@ -109,7 +116,7 @@ class TokenRepositoryPostgres extends TokenRepositoryInterface {
109
116
  sessionToken.expires &&
110
117
  new Date(sessionToken.expires) < new Date()
111
118
  ) {
112
- throw new Error('Invalid Token: Token is expired');
119
+ throw new ClientSafeError('Invalid Token: Token is expired', 401);
113
120
  }
114
121
 
115
122
  return this._convertTokenIds(sessionToken);
@@ -8,9 +8,14 @@ const {
8
8
  updateOne,
9
9
  deleteOne,
10
10
  } = require('../../database/documentdb-utils');
11
- const { createTokenRepository } = require('../../token/repositories/token-repository-factory');
11
+ const {
12
+ createTokenRepository,
13
+ } = require('../../token/repositories/token-repository-factory');
12
14
  const { UserRepositoryInterface } = require('./user-repository-interface');
13
- const { DocumentDBEncryptionService } = require('../../database/documentdb-encryption-service');
15
+ const { ClientSafeError } = require('../../errors');
16
+ const {
17
+ DocumentDBEncryptionService,
18
+ } = require('../../database/documentdb-encryption-service');
14
19
 
15
20
  /**
16
21
  * User repository for DocumentDB.
@@ -30,8 +35,11 @@ class UserRepositoryDocumentDB extends UserRepositoryInterface {
30
35
  }
31
36
 
32
37
  async getSessionToken(token) {
33
- const jsonToken = this.tokenRepository.getJSONTokenFromBase64BufferToken(token);
34
- const sessionToken = await this.tokenRepository.validateAndGetToken(jsonToken);
38
+ const jsonToken =
39
+ this.tokenRepository.getJSONTokenFromBase64BufferToken(token);
40
+ const sessionToken = await this.tokenRepository.validateAndGetToken(
41
+ jsonToken
42
+ );
35
43
  return sessionToken;
36
44
  }
37
45
 
@@ -40,7 +48,10 @@ class UserRepositoryDocumentDB extends UserRepositoryInterface {
40
48
  _id: toObjectId(userId),
41
49
  type: 'ORGANIZATION',
42
50
  });
43
- const decrypted = await this.encryptionService.decryptFields('User', doc);
51
+ const decrypted = await this.encryptionService.decryptFields(
52
+ 'User',
53
+ doc
54
+ );
44
55
  return this._mapUser(decrypted);
45
56
  }
46
57
 
@@ -49,7 +60,10 @@ class UserRepositoryDocumentDB extends UserRepositoryInterface {
49
60
  _id: toObjectId(userId),
50
61
  type: 'INDIVIDUAL',
51
62
  });
52
- const decrypted = await this.encryptionService.decryptFields('User', doc);
63
+ const decrypted = await this.encryptionService.decryptFields(
64
+ 'User',
65
+ doc
66
+ );
53
67
  return this._mapUser(decrypted);
54
68
  }
55
69
 
@@ -59,7 +73,10 @@ class UserRepositoryDocumentDB extends UserRepositoryInterface {
59
73
  rawToken,
60
74
  minutes
61
75
  );
62
- return this.tokenRepository.createBase64BufferToken(createdToken, rawToken);
76
+ return this.tokenRepository.createBase64BufferToken(
77
+ createdToken,
78
+ rawToken
79
+ );
63
80
  }
64
81
 
65
82
  async createIndividualUser(params) {
@@ -84,7 +101,7 @@ class UserRepositoryDocumentDB extends UserRepositoryInterface {
84
101
  params.hashword !== ''
85
102
  ) {
86
103
  if (typeof params.hashword !== 'string') {
87
- throw new Error('Password must be a string');
104
+ throw new ClientSafeError('Password must be a string', 400);
88
105
  }
89
106
 
90
107
  if (params.hashword.startsWith('$2')) {
@@ -98,28 +115,41 @@ class UserRepositoryDocumentDB extends UserRepositoryInterface {
98
115
  }
99
116
 
100
117
  // Encrypt sensitive fields before insert
101
- const encryptedDocument = await this.encryptionService.encryptFields('User', document);
102
- const insertedId = await insertOne(this.prisma, 'User', encryptedDocument);
118
+ const encryptedDocument = await this.encryptionService.encryptFields(
119
+ 'User',
120
+ document
121
+ );
122
+ const insertedId = await insertOne(
123
+ this.prisma,
124
+ 'User',
125
+ encryptedDocument
126
+ );
103
127
  const created = await findOne(this.prisma, 'User', { _id: insertedId });
104
128
 
105
129
  // Defensive check: verify document was found after insert
106
130
  if (!created) {
107
- console.error('[UserRepositoryDocumentDB] User not found after insert', {
108
- insertedId: fromObjectId(insertedId),
109
- params: {
110
- username: params.username,
111
- appUserId: params.appUserId,
112
- email: params.email
131
+ console.error(
132
+ '[UserRepositoryDocumentDB] User not found after insert',
133
+ {
134
+ insertedId: fromObjectId(insertedId),
135
+ params: {
136
+ username: params.username,
137
+ appUserId: params.appUserId,
138
+ email: params.email,
139
+ },
113
140
  }
114
- });
141
+ );
115
142
  throw new Error(
116
143
  'Failed to create individual user: Document not found after insert. ' +
117
- 'This indicates a database consistency issue.'
144
+ 'This indicates a database consistency issue.'
118
145
  );
119
146
  }
120
147
 
121
148
  // Decrypt sensitive fields after read
122
- const decrypted = await this.encryptionService.decryptFields('User', created);
149
+ const decrypted = await this.encryptionService.decryptFields(
150
+ 'User',
151
+ created
152
+ );
123
153
 
124
154
  return this._mapUser(decrypted);
125
155
  }
@@ -134,9 +164,42 @@ class UserRepositoryDocumentDB extends UserRepositoryInterface {
134
164
  updatedAt: now,
135
165
  };
136
166
 
137
- const insertedId = await insertOne(this.prisma, 'User', document);
167
+ // Encrypt sensitive fields before insert (consistency with individual user)
168
+ const encryptedDocument = await this.encryptionService.encryptFields(
169
+ 'User',
170
+ document
171
+ );
172
+ const insertedId = await insertOne(
173
+ this.prisma,
174
+ 'User',
175
+ encryptedDocument
176
+ );
138
177
  const created = await findOne(this.prisma, 'User', { _id: insertedId });
139
- return this._mapUser(created);
178
+
179
+ // Defensive check: verify document was found after insert
180
+ if (!created) {
181
+ console.error(
182
+ '[UserRepositoryDocumentDB] Organization user not found after insert',
183
+ {
184
+ insertedId: fromObjectId(insertedId),
185
+ params: {
186
+ appOrgId: params.appOrgId,
187
+ name: params.name,
188
+ },
189
+ }
190
+ );
191
+ throw new Error(
192
+ 'Failed to create organization user: Document not found after insert. ' +
193
+ 'This indicates a database consistency issue.'
194
+ );
195
+ }
196
+
197
+ // Decrypt sensitive fields after read
198
+ const decrypted = await this.encryptionService.decryptFields(
199
+ 'User',
200
+ created
201
+ );
202
+ return this._mapUser(decrypted);
140
203
  }
141
204
 
142
205
  async findIndividualUserByUsername(username) {
@@ -144,7 +207,10 @@ class UserRepositoryDocumentDB extends UserRepositoryInterface {
144
207
  type: 'INDIVIDUAL',
145
208
  username,
146
209
  });
147
- const decrypted = await this.encryptionService.decryptFields('User', doc);
210
+ const decrypted = await this.encryptionService.decryptFields(
211
+ 'User',
212
+ doc
213
+ );
148
214
  return this._mapUser(decrypted);
149
215
  }
150
216
 
@@ -153,7 +219,10 @@ class UserRepositoryDocumentDB extends UserRepositoryInterface {
153
219
  type: 'INDIVIDUAL',
154
220
  appUserId,
155
221
  });
156
- const decrypted = await this.encryptionService.decryptFields('User', doc);
222
+ const decrypted = await this.encryptionService.decryptFields(
223
+ 'User',
224
+ doc
225
+ );
157
226
  return this._mapUser(decrypted);
158
227
  }
159
228
 
@@ -162,13 +231,21 @@ class UserRepositoryDocumentDB extends UserRepositoryInterface {
162
231
  type: 'ORGANIZATION',
163
232
  appOrgId,
164
233
  });
165
- const decrypted = await this.encryptionService.decryptFields('User', doc);
234
+ const decrypted = await this.encryptionService.decryptFields(
235
+ 'User',
236
+ doc
237
+ );
166
238
  return this._mapUser(decrypted);
167
239
  }
168
240
 
169
241
  async findUserById(userId) {
170
- const doc = await findOne(this.prisma, 'User', { _id: toObjectId(userId) });
171
- const decrypted = await this.encryptionService.decryptFields('User', doc);
242
+ const doc = await findOne(this.prisma, 'User', {
243
+ _id: toObjectId(userId),
244
+ });
245
+ const decrypted = await this.encryptionService.decryptFields(
246
+ 'User',
247
+ doc
248
+ );
172
249
  return this._mapUser(decrypted);
173
250
  }
174
251
 
@@ -177,7 +254,10 @@ class UserRepositoryDocumentDB extends UserRepositoryInterface {
177
254
  type: 'INDIVIDUAL',
178
255
  email,
179
256
  });
180
- const decrypted = await this.encryptionService.decryptFields('User', doc);
257
+ const decrypted = await this.encryptionService.decryptFields(
258
+ 'User',
259
+ doc
260
+ );
181
261
  return this._mapUser(decrypted);
182
262
  }
183
263
 
@@ -189,7 +269,10 @@ class UserRepositoryDocumentDB extends UserRepositoryInterface {
189
269
  payload.updatedAt = new Date();
190
270
 
191
271
  // Encrypt sensitive fields before update
192
- const encryptedPayload = await this.encryptionService.encryptFields('User', payload);
272
+ const encryptedPayload = await this.encryptionService.encryptFields(
273
+ 'User',
274
+ payload
275
+ );
193
276
 
194
277
  await updateOne(
195
278
  this.prisma,
@@ -199,7 +282,26 @@ class UserRepositoryDocumentDB extends UserRepositoryInterface {
199
282
  );
200
283
 
201
284
  const updated = await findOne(this.prisma, 'User', { _id: objectId });
202
- const decrypted = await this.encryptionService.decryptFields('User', updated);
285
+
286
+ // Defensive check: verify document was found after update
287
+ if (!updated) {
288
+ console.error(
289
+ '[UserRepositoryDocumentDB] Individual user not found after update',
290
+ {
291
+ userId: fromObjectId(objectId),
292
+ updates,
293
+ }
294
+ );
295
+ throw new Error(
296
+ 'Failed to update individual user: Document not found after update. ' +
297
+ 'This indicates a database consistency issue.'
298
+ );
299
+ }
300
+
301
+ const decrypted = await this.encryptionService.decryptFields(
302
+ 'User',
303
+ updated
304
+ );
203
305
  return this._mapUser(decrypted);
204
306
  }
205
307
 
@@ -209,15 +311,38 @@ class UserRepositoryDocumentDB extends UserRepositoryInterface {
209
311
 
210
312
  const payload = { ...updates, updatedAt: new Date() };
211
313
 
314
+ const encryptedPayload = await this.encryptionService.encryptFields(
315
+ 'User',
316
+ payload
317
+ );
318
+
212
319
  await updateOne(
213
320
  this.prisma,
214
321
  'User',
215
322
  { _id: objectId, type: 'ORGANIZATION' },
216
- { $set: payload }
323
+ { $set: encryptedPayload }
217
324
  );
218
325
 
219
326
  const updated = await findOne(this.prisma, 'User', { _id: objectId });
220
- const decrypted = await this.encryptionService.decryptFields('User', updated);
327
+
328
+ if (!updated) {
329
+ console.error(
330
+ '[UserRepositoryDocumentDB] Organization user not found after update',
331
+ {
332
+ userId: fromObjectId(objectId),
333
+ updates,
334
+ }
335
+ );
336
+ throw new Error(
337
+ 'Failed to update organization user: Document not found after update. ' +
338
+ 'This indicates a database consistency issue.'
339
+ );
340
+ }
341
+
342
+ const decrypted = await this.encryptionService.decryptFields(
343
+ 'User',
344
+ updated
345
+ );
221
346
  return this._mapUser(decrypted);
222
347
  }
223
348
 
@@ -232,7 +357,9 @@ class UserRepositoryDocumentDB extends UserRepositoryInterface {
232
357
 
233
358
  _mapUser(doc) {
234
359
  if (!doc) {
235
- console.warn('[UserRepositoryDocumentDB] _mapUser received null/undefined document');
360
+ console.warn(
361
+ '[UserRepositoryDocumentDB] _mapUser received null/undefined document'
362
+ );
236
363
  return null;
237
364
  }
238
365
 
@@ -244,11 +371,13 @@ class UserRepositoryDocumentDB extends UserRepositoryInterface {
244
371
  username: doc?.username ?? null,
245
372
  hashword: doc?.hashword ?? null,
246
373
  appUserId: doc?.appUserId ?? null,
247
- organizationId: doc?.organizationId ? fromObjectId(doc.organizationId) : null,
374
+ organizationId: doc?.organizationId
375
+ ? fromObjectId(doc.organizationId)
376
+ : null,
248
377
  appOrgId: doc?.appOrgId ?? null,
249
378
  name: doc?.name ?? null,
250
- createdAt: doc?.createdAt ? new Date(doc.createdAt) : undefined,
251
- updatedAt: doc?.updatedAt ? new Date(doc.updatedAt) : undefined,
379
+ createdAt: this._parseDate(doc?.createdAt),
380
+ updatedAt: this._parseDate(doc?.updatedAt),
252
381
  };
253
382
  }
254
383
 
@@ -261,7 +390,7 @@ class UserRepositoryDocumentDB extends UserRepositoryInterface {
261
390
  payload.hashword !== ''
262
391
  ) {
263
392
  if (typeof payload.hashword !== 'string') {
264
- throw new Error('Password must be a string');
393
+ throw new ClientSafeError('Password must be a string', 400);
265
394
  }
266
395
 
267
396
  if (payload.hashword.startsWith('$2')) {
@@ -286,7 +415,18 @@ class UserRepositoryDocumentDB extends UserRepositoryInterface {
286
415
 
287
416
  return payload;
288
417
  }
418
+
419
+ /**
420
+ * Parse date value safely, returning undefined for invalid dates
421
+ * @private
422
+ * @param {*} value - Date value from database
423
+ * @returns {Date|undefined} Valid Date object or undefined
424
+ */
425
+ _parseDate(value) {
426
+ if (!value) return undefined;
427
+ const date = new Date(value);
428
+ return isNaN(date.getTime()) ? undefined : date;
429
+ }
289
430
  }
290
431
 
291
432
  module.exports = { UserRepositoryDocumentDB };
292
-
@@ -4,6 +4,7 @@ const {
4
4
  createTokenRepository,
5
5
  } = require('../../token/repositories/token-repository-factory');
6
6
  const { UserRepositoryInterface } = require('./user-repository-interface');
7
+ const { ClientSafeError } = require('../../errors');
7
8
 
8
9
  /**
9
10
  * MongoDB User Repository Adapter
@@ -113,7 +114,7 @@ class UserRepositoryMongo extends UserRepositoryInterface {
113
114
  params.hashword !== ''
114
115
  ) {
115
116
  if (typeof params.hashword !== 'string') {
116
- throw new Error('Password must be a string');
117
+ throw new ClientSafeError('Password must be a string', 400);
117
118
  }
118
119
 
119
120
  // Prevent double-hashing: bcrypt hashes start with $2a$ or $2b$
@@ -235,7 +236,7 @@ class UserRepositoryMongo extends UserRepositoryInterface {
235
236
  data.hashword !== ''
236
237
  ) {
237
238
  if (typeof data.hashword !== 'string') {
238
- throw new Error('Password must be a string');
239
+ throw new ClientSafeError('Password must be a string', 400);
239
240
  }
240
241
 
241
242
  // Prevent double-hashing: bcrypt hashes start with $2a$ or $2b$
@@ -4,6 +4,7 @@ const {
4
4
  createTokenRepository,
5
5
  } = require('../../token/repositories/token-repository-factory');
6
6
  const { UserRepositoryInterface } = require('./user-repository-interface');
7
+ const { ClientSafeError } = require('../../errors');
7
8
 
8
9
  /**
9
10
  * PostgreSQL User Repository Adapter
@@ -150,7 +151,7 @@ class UserRepositoryPostgres extends UserRepositoryInterface {
150
151
  params.hashword !== ''
151
152
  ) {
152
153
  if (typeof params.hashword !== 'string') {
153
- throw new Error('Password must be a string');
154
+ throw new ClientSafeError('Password must be a string', 400);
154
155
  }
155
156
 
156
157
  // Prevent double-hashing: bcrypt hashes start with $2a$ or $2b$
@@ -290,7 +291,7 @@ class UserRepositoryPostgres extends UserRepositoryInterface {
290
291
  data.hashword !== ''
291
292
  ) {
292
293
  if (typeof data.hashword !== 'string') {
293
- throw new Error('Password must be a string');
294
+ throw new ClientSafeError('Password must be a string', 400);
294
295
  }
295
296
 
296
297
  // Prevent double-hashing: bcrypt hashes start with $2a$ or $2b$
@@ -115,7 +115,7 @@ class LoginUser {
115
115
  return organizationUser;
116
116
  }
117
117
 
118
- return null;
118
+ throw new Error('User configuration must require either individualUserRequired or organizationUserRequired');
119
119
  }
120
120
  }
121
121