@librechat/data-schemas 0.0.14 → 0.0.16

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 (51) hide show
  1. package/README.md +318 -0
  2. package/dist/index.cjs +2668 -1041
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.es.js +2650 -1018
  5. package/dist/index.es.js.map +1 -1
  6. package/dist/types/common/enum.d.ts +13 -0
  7. package/dist/types/common/index.d.ts +1 -0
  8. package/dist/types/index.d.ts +2 -0
  9. package/dist/types/methods/accessRole.d.ts +42 -0
  10. package/dist/types/methods/accessRole.spec.d.ts +1 -0
  11. package/dist/types/methods/aclEntry.d.ts +52 -0
  12. package/dist/types/methods/aclEntry.spec.d.ts +1 -0
  13. package/dist/types/methods/agentCategory.d.ts +48 -0
  14. package/dist/types/methods/group.d.ts +40 -0
  15. package/dist/types/methods/group.spec.d.ts +1 -0
  16. package/dist/types/methods/index.d.ts +116 -6
  17. package/dist/types/methods/pluginAuth.d.ts +1 -1
  18. package/dist/types/methods/user.d.ts +9 -1
  19. package/dist/types/methods/userGroup.d.ts +73 -0
  20. package/dist/types/methods/userGroup.methods.spec.d.ts +1 -0
  21. package/dist/types/methods/userGroup.roles.spec.d.ts +1 -0
  22. package/dist/types/methods/userGroup.spec.d.ts +1 -0
  23. package/dist/types/models/accessRole.d.ts +30 -0
  24. package/dist/types/models/aclEntry.d.ts +30 -0
  25. package/dist/types/models/agentCategory.d.ts +30 -0
  26. package/dist/types/models/group.d.ts +30 -0
  27. package/dist/types/models/index.d.ts +4 -0
  28. package/dist/types/schema/accessRole.d.ts +39 -0
  29. package/dist/types/schema/aclEntry.d.ts +39 -0
  30. package/dist/types/schema/agentCategory.d.ts +39 -0
  31. package/dist/types/schema/defaults.d.ts +4 -0
  32. package/dist/types/schema/group.d.ts +37 -0
  33. package/dist/types/schema/index.d.ts +2 -0
  34. package/dist/types/schema/preset.d.ts +1 -0
  35. package/dist/types/schema/prompt.d.ts +2 -9
  36. package/dist/types/schema/promptGroup.d.ts +4 -18
  37. package/dist/types/types/accessRole.d.ts +40 -0
  38. package/dist/types/types/aclEntry.d.ts +52 -0
  39. package/dist/types/types/agent.d.ts +8 -0
  40. package/dist/types/types/agentCategory.d.ts +41 -0
  41. package/dist/types/types/convo.d.ts +1 -0
  42. package/dist/types/types/group.d.ts +45 -0
  43. package/dist/types/types/index.d.ts +5 -0
  44. package/dist/types/types/pluginAuth.d.ts +1 -0
  45. package/dist/types/types/prompts.d.ts +50 -0
  46. package/dist/types/types/user.d.ts +2 -0
  47. package/dist/types/utils/index.d.ts +1 -0
  48. package/dist/types/utils/object-traverse.d.ts +23 -0
  49. package/dist/types/utils/transactions.d.ts +14 -0
  50. package/package.json +1 -3
  51. package/dist/types/utils/traverse-wrapper.d.ts +0 -9
package/dist/index.es.js CHANGED
@@ -1,16 +1,30 @@
1
+ import { PermissionBits, FileSources, Constants, PermissionTypes, Permissions, SystemRoles, ResourceType, PrincipalType, PrincipalModel, roleDefaults, AccessRoleIds } from 'librechat-data-provider';
1
2
  import jwt from 'jsonwebtoken';
2
3
  import { webcrypto } from 'node:crypto';
3
- import mongoose, { Schema } from 'mongoose';
4
- import { FileSources, Constants, PermissionTypes, Permissions, SystemRoles, roleDefaults } from 'librechat-data-provider';
5
- import _ from 'lodash';
6
- import { MeiliSearch } from 'meilisearch';
4
+ import mongoose, { Schema, Types } from 'mongoose';
7
5
  import winston from 'winston';
8
6
  import 'winston-daily-rotate-file';
9
- import path from 'path';
10
7
  import { klona } from 'klona';
11
- import traverseLib from 'traverse';
8
+ import path from 'path';
9
+ import _ from 'lodash';
10
+ import { MeiliSearch } from 'meilisearch';
12
11
  import { nanoid } from 'nanoid';
13
12
 
13
+ /**
14
+ * Common role combinations
15
+ */
16
+ var RoleBits;
17
+ (function (RoleBits) {
18
+ /** 0001 = 1 */
19
+ RoleBits[RoleBits["VIEWER"] = PermissionBits.VIEW] = "VIEWER";
20
+ /** 0011 = 3 */
21
+ RoleBits[RoleBits["EDITOR"] = PermissionBits.VIEW | PermissionBits.EDIT] = "EDITOR";
22
+ /** 0111 = 7 */
23
+ RoleBits[RoleBits["MANAGER"] = PermissionBits.VIEW | PermissionBits.EDIT | PermissionBits.DELETE] = "MANAGER";
24
+ /** 1111 = 15 */
25
+ RoleBits[RoleBits["OWNER"] = PermissionBits.VIEW | PermissionBits.EDIT | PermissionBits.DELETE | PermissionBits.SHARE] = "OWNER";
26
+ })(RoleBits || (RoleBits = {}));
27
+
14
28
  async function signPayload({ payload, secret, expirationTime, }) {
15
29
  return jwt.sign(payload, secret, { expiresIn: expirationTime });
16
30
  }
@@ -154,9 +168,59 @@ const agentSchema = new Schema({
154
168
  type: [Schema.Types.Mixed],
155
169
  default: [],
156
170
  },
171
+ category: {
172
+ type: String,
173
+ trim: true,
174
+ index: true,
175
+ default: 'general',
176
+ },
177
+ support_contact: {
178
+ type: Schema.Types.Mixed,
179
+ default: undefined,
180
+ },
181
+ is_promoted: {
182
+ type: Boolean,
183
+ default: false,
184
+ index: true,
185
+ },
186
+ }, {
187
+ timestamps: true,
188
+ });
189
+ agentSchema.index({ updatedAt: -1, _id: 1 });
190
+
191
+ const agentCategorySchema = new Schema({
192
+ value: {
193
+ type: String,
194
+ required: true,
195
+ unique: true,
196
+ trim: true,
197
+ lowercase: true,
198
+ index: true,
199
+ },
200
+ label: {
201
+ type: String,
202
+ required: true,
203
+ trim: true,
204
+ },
205
+ description: {
206
+ type: String,
207
+ trim: true,
208
+ default: '',
209
+ },
210
+ order: {
211
+ type: Number,
212
+ default: 0,
213
+ index: true,
214
+ },
215
+ isActive: {
216
+ type: Boolean,
217
+ default: true,
218
+ index: true,
219
+ },
157
220
  }, {
158
221
  timestamps: true,
159
222
  });
223
+ agentCategorySchema.index({ isActive: 1, order: 1 });
160
224
 
161
225
  const assistantSchema = new Schema({
162
226
  user: {
@@ -442,6 +506,10 @@ const conversationPreset = {
442
506
  reasoning_summary: {
443
507
  type: String,
444
508
  },
509
+ /** Verbosity control */
510
+ verbosity: {
511
+ type: String,
512
+ },
445
513
  };
446
514
 
447
515
  const convoSchema = new Schema({
@@ -899,69 +967,56 @@ promptGroupSchema.index({ createdAt: 1, updatedAt: 1 });
899
967
  */
900
968
  const rolePermissionsSchema = new Schema({
901
969
  [PermissionTypes.BOOKMARKS]: {
902
- [Permissions.USE]: { type: Boolean, default: true },
970
+ [Permissions.USE]: { type: Boolean },
903
971
  },
904
972
  [PermissionTypes.PROMPTS]: {
905
- [Permissions.SHARED_GLOBAL]: { type: Boolean, default: false },
906
- [Permissions.USE]: { type: Boolean, default: true },
907
- [Permissions.CREATE]: { type: Boolean, default: true },
973
+ [Permissions.SHARED_GLOBAL]: { type: Boolean },
974
+ [Permissions.USE]: { type: Boolean },
975
+ [Permissions.CREATE]: { type: Boolean },
908
976
  },
909
977
  [PermissionTypes.MEMORIES]: {
910
- [Permissions.USE]: { type: Boolean, default: true },
911
- [Permissions.CREATE]: { type: Boolean, default: true },
912
- [Permissions.UPDATE]: { type: Boolean, default: true },
913
- [Permissions.READ]: { type: Boolean, default: true },
914
- [Permissions.OPT_OUT]: { type: Boolean, default: true },
978
+ [Permissions.USE]: { type: Boolean },
979
+ [Permissions.CREATE]: { type: Boolean },
980
+ [Permissions.UPDATE]: { type: Boolean },
981
+ [Permissions.READ]: { type: Boolean },
982
+ [Permissions.OPT_OUT]: { type: Boolean },
915
983
  },
916
984
  [PermissionTypes.AGENTS]: {
917
- [Permissions.SHARED_GLOBAL]: { type: Boolean, default: false },
918
- [Permissions.USE]: { type: Boolean, default: true },
919
- [Permissions.CREATE]: { type: Boolean, default: true },
985
+ [Permissions.SHARED_GLOBAL]: { type: Boolean },
986
+ [Permissions.USE]: { type: Boolean },
987
+ [Permissions.CREATE]: { type: Boolean },
920
988
  },
921
989
  [PermissionTypes.MULTI_CONVO]: {
922
- [Permissions.USE]: { type: Boolean, default: true },
990
+ [Permissions.USE]: { type: Boolean },
923
991
  },
924
992
  [PermissionTypes.TEMPORARY_CHAT]: {
925
- [Permissions.USE]: { type: Boolean, default: true },
993
+ [Permissions.USE]: { type: Boolean },
926
994
  },
927
995
  [PermissionTypes.RUN_CODE]: {
928
- [Permissions.USE]: { type: Boolean, default: true },
996
+ [Permissions.USE]: { type: Boolean },
929
997
  },
930
998
  [PermissionTypes.WEB_SEARCH]: {
931
- [Permissions.USE]: { type: Boolean, default: true },
999
+ [Permissions.USE]: { type: Boolean },
1000
+ },
1001
+ [PermissionTypes.PEOPLE_PICKER]: {
1002
+ [Permissions.VIEW_USERS]: { type: Boolean },
1003
+ [Permissions.VIEW_GROUPS]: { type: Boolean },
1004
+ [Permissions.VIEW_ROLES]: { type: Boolean },
1005
+ },
1006
+ [PermissionTypes.MARKETPLACE]: {
1007
+ [Permissions.USE]: { type: Boolean },
932
1008
  },
933
1009
  [PermissionTypes.FILE_SEARCH]: {
934
- [Permissions.USE]: { type: Boolean, default: true },
1010
+ [Permissions.USE]: { type: Boolean },
1011
+ },
1012
+ [PermissionTypes.FILE_CITATIONS]: {
1013
+ [Permissions.USE]: { type: Boolean },
935
1014
  },
936
1015
  }, { _id: false });
937
1016
  const roleSchema = new Schema({
938
1017
  name: { type: String, required: true, unique: true, index: true },
939
1018
  permissions: {
940
1019
  type: rolePermissionsSchema,
941
- default: () => ({
942
- [PermissionTypes.BOOKMARKS]: { [Permissions.USE]: true },
943
- [PermissionTypes.PROMPTS]: {
944
- [Permissions.SHARED_GLOBAL]: false,
945
- [Permissions.USE]: true,
946
- [Permissions.CREATE]: true,
947
- },
948
- [PermissionTypes.MEMORIES]: {
949
- [Permissions.USE]: true,
950
- [Permissions.CREATE]: true,
951
- [Permissions.UPDATE]: true,
952
- [Permissions.READ]: true,
953
- },
954
- [PermissionTypes.AGENTS]: {
955
- [Permissions.SHARED_GLOBAL]: false,
956
- [Permissions.USE]: true,
957
- [Permissions.CREATE]: true,
958
- },
959
- [PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: true },
960
- [PermissionTypes.TEMPORARY_CHAT]: { [Permissions.USE]: true },
961
- [PermissionTypes.RUN_CODE]: { [Permissions.USE]: true },
962
- [PermissionTypes.WEB_SEARCH]: { [Permissions.USE]: true },
963
- [PermissionTypes.FILE_SEARCH]: { [Permissions.USE]: true },
964
- }),
965
1020
  },
966
1021
  });
967
1022
 
@@ -1151,6 +1206,7 @@ const userSchema = new Schema({
1151
1206
  trim: true,
1152
1207
  minlength: 8,
1153
1208
  maxlength: 128,
1209
+ select: false,
1154
1210
  },
1155
1211
  avatar: {
1156
1212
  type: String,
@@ -1214,6 +1270,7 @@ const userSchema = new Schema({
1214
1270
  },
1215
1271
  totpSecret: {
1216
1272
  type: String,
1273
+ select: false,
1217
1274
  },
1218
1275
  backupCodes: {
1219
1276
  type: [BackupCodeSchema],
@@ -1238,6 +1295,11 @@ const userSchema = new Schema({
1238
1295
  },
1239
1296
  default: {},
1240
1297
  },
1298
+ /** Field for external source identification (for consistency with TPrincipal schema) */
1299
+ idOnTheSource: {
1300
+ type: String,
1301
+ sparse: true,
1302
+ },
1241
1303
  }, { timestamps: true });
1242
1304
 
1243
1305
  const MemoryEntrySchema = new Schema({
@@ -1269,96 +1331,478 @@ const MemoryEntrySchema = new Schema({
1269
1331
  },
1270
1332
  });
1271
1333
 
1334
+ const groupSchema = new Schema({
1335
+ name: {
1336
+ type: String,
1337
+ required: true,
1338
+ index: true,
1339
+ },
1340
+ description: {
1341
+ type: String,
1342
+ required: false,
1343
+ },
1344
+ email: {
1345
+ type: String,
1346
+ required: false,
1347
+ index: true,
1348
+ },
1349
+ avatar: {
1350
+ type: String,
1351
+ required: false,
1352
+ },
1353
+ memberIds: [
1354
+ {
1355
+ type: String,
1356
+ },
1357
+ ],
1358
+ source: {
1359
+ type: String,
1360
+ enum: ['local', 'entra'],
1361
+ default: 'local',
1362
+ },
1363
+ /** External ID (e.g., Entra ID) */
1364
+ idOnTheSource: {
1365
+ type: String,
1366
+ sparse: true,
1367
+ index: true,
1368
+ required: function () {
1369
+ return this.source !== 'local';
1370
+ },
1371
+ },
1372
+ }, { timestamps: true });
1373
+ groupSchema.index({ idOnTheSource: 1, source: 1 }, {
1374
+ unique: true,
1375
+ partialFilterExpression: { idOnTheSource: { $exists: true } },
1376
+ });
1377
+ groupSchema.index({ memberIds: 1 });
1378
+
1272
1379
  /**
1273
- * Creates or returns the User model using the provided mongoose instance and schema
1380
+ * ESM-native object traversal utility
1381
+ * Simplified implementation focused on the forEach use case
1274
1382
  */
1275
- function createUserModel(mongoose) {
1276
- return mongoose.models.User || mongoose.model('User', userSchema);
1383
+ function isObject(value) {
1384
+ if (value === null || typeof value !== 'object') {
1385
+ return false;
1386
+ }
1387
+ // Treat these built-in types as leaf nodes, not objects to traverse
1388
+ if (value instanceof Date)
1389
+ return false;
1390
+ if (value instanceof RegExp)
1391
+ return false;
1392
+ if (value instanceof Error)
1393
+ return false;
1394
+ if (value instanceof URL)
1395
+ return false;
1396
+ // Check for Buffer (Node.js)
1397
+ if (typeof Buffer !== 'undefined' && Buffer.isBuffer(value))
1398
+ return false;
1399
+ // Check for TypedArrays and ArrayBuffer
1400
+ if (ArrayBuffer.isView(value))
1401
+ return false;
1402
+ if (value instanceof ArrayBuffer)
1403
+ return false;
1404
+ if (value instanceof SharedArrayBuffer)
1405
+ return false;
1406
+ // Check for other built-in types that shouldn't be traversed
1407
+ if (value instanceof Promise)
1408
+ return false;
1409
+ if (value instanceof WeakMap)
1410
+ return false;
1411
+ if (value instanceof WeakSet)
1412
+ return false;
1413
+ if (value instanceof Map)
1414
+ return false;
1415
+ if (value instanceof Set)
1416
+ return false;
1417
+ // Check if it's a primitive wrapper object
1418
+ const stringTag = Object.prototype.toString.call(value);
1419
+ if (stringTag === '[object Boolean]' ||
1420
+ stringTag === '[object Number]' ||
1421
+ stringTag === '[object String]') {
1422
+ return false;
1423
+ }
1424
+ return true;
1425
+ }
1426
+ // Helper to safely set a property on an object or array
1427
+ function setProperty(obj, key, value) {
1428
+ if (Array.isArray(obj) && typeof key === 'number') {
1429
+ obj[key] = value;
1430
+ }
1431
+ else if (!Array.isArray(obj) && typeof key === 'string') {
1432
+ obj[key] = value;
1433
+ }
1434
+ else if (!Array.isArray(obj) && typeof key === 'number') {
1435
+ // Handle numeric keys on objects
1436
+ obj[key] = value;
1437
+ }
1438
+ }
1439
+ // Helper to safely delete a property from an object
1440
+ function deleteProperty(obj, key) {
1441
+ if (Array.isArray(obj) && typeof key === 'number') {
1442
+ // For arrays, we should use splice, but this is handled in remove()
1443
+ // This function is only called for non-array deletion
1444
+ return;
1445
+ }
1446
+ if (!Array.isArray(obj)) {
1447
+ delete obj[key];
1448
+ }
1449
+ }
1450
+ function forEach(obj, callback) {
1451
+ const visited = new WeakSet();
1452
+ function walk(node, path = [], parent) {
1453
+ // Check for circular references
1454
+ let circular = null;
1455
+ if (isObject(node)) {
1456
+ if (visited.has(node)) {
1457
+ // Find the circular reference in the parent chain
1458
+ let p = parent;
1459
+ while (p) {
1460
+ if (p.node === node) {
1461
+ circular = p;
1462
+ break;
1463
+ }
1464
+ p = p.parent;
1465
+ }
1466
+ return; // Skip circular references
1467
+ }
1468
+ visited.add(node);
1469
+ }
1470
+ const key = path.length > 0 ? path[path.length - 1] : undefined;
1471
+ const isRoot = path.length === 0;
1472
+ const level = path.length;
1473
+ // Determine if this is a leaf node
1474
+ const isLeaf = !isObject(node) ||
1475
+ (Array.isArray(node) && node.length === 0) ||
1476
+ Object.keys(node).length === 0;
1477
+ // Create context
1478
+ const context = {
1479
+ node,
1480
+ path: [...path],
1481
+ parent,
1482
+ key,
1483
+ isLeaf,
1484
+ notLeaf: !isLeaf,
1485
+ isRoot,
1486
+ notRoot: !isRoot,
1487
+ level,
1488
+ circular,
1489
+ update(value) {
1490
+ if (!isRoot && parent && key !== undefined && isObject(parent.node)) {
1491
+ setProperty(parent.node, key, value);
1492
+ }
1493
+ this.node = value;
1494
+ },
1495
+ remove() {
1496
+ if (!isRoot && parent && key !== undefined && isObject(parent.node)) {
1497
+ if (Array.isArray(parent.node) && typeof key === 'number') {
1498
+ parent.node.splice(key, 1);
1499
+ }
1500
+ else {
1501
+ deleteProperty(parent.node, key);
1502
+ }
1503
+ }
1504
+ },
1505
+ };
1506
+ // Call the callback with the context
1507
+ callback.call(context, node);
1508
+ // Traverse children if not circular and is an object
1509
+ if (!circular && isObject(node) && !isLeaf) {
1510
+ if (Array.isArray(node)) {
1511
+ for (let i = 0; i < node.length; i++) {
1512
+ walk(node[i], [...path, i], context);
1513
+ }
1514
+ }
1515
+ else {
1516
+ for (const [childKey, childValue] of Object.entries(node)) {
1517
+ walk(childValue, [...path, childKey], context);
1518
+ }
1519
+ }
1520
+ }
1521
+ }
1522
+ walk(obj);
1523
+ }
1524
+ // Main traverse function that returns an object with forEach method
1525
+ function traverse(obj) {
1526
+ return {
1527
+ forEach(callback) {
1528
+ forEach(obj, callback);
1529
+ },
1530
+ };
1277
1531
  }
1278
1532
 
1533
+ const SPLAT_SYMBOL = Symbol.for('splat');
1534
+ const MESSAGE_SYMBOL = Symbol.for('message');
1535
+ const CONSOLE_JSON_STRING_LENGTH = parseInt(process.env.CONSOLE_JSON_STRING_LENGTH || '', 10) || 255;
1536
+ const sensitiveKeys = [
1537
+ /^(sk-)[^\s]+/, // OpenAI API key pattern
1538
+ /(Bearer )[^\s]+/, // Header: Bearer token pattern
1539
+ /(api-key:? )[^\s]+/, // Header: API key pattern
1540
+ /(key=)[^\s]+/, // URL query param: sensitive key pattern (Google)
1541
+ ];
1279
1542
  /**
1280
- * Creates or returns the Token model using the provided mongoose instance and schema
1543
+ * Determines if a given value string is sensitive and returns matching regex patterns.
1544
+ *
1545
+ * @param valueStr - The value string to check.
1546
+ * @returns An array of regex patterns that match the value string.
1281
1547
  */
1282
- function createTokenModel(mongoose) {
1283
- return mongoose.models.Token || mongoose.model('Token', tokenSchema);
1548
+ function getMatchingSensitivePatterns(valueStr) {
1549
+ if (valueStr) {
1550
+ // Filter and return all regex patterns that match the value string
1551
+ return sensitiveKeys.filter((regex) => regex.test(valueStr));
1552
+ }
1553
+ return [];
1284
1554
  }
1285
-
1286
1555
  /**
1287
- * Creates or returns the Session model using the provided mongoose instance and schema
1556
+ * Redacts sensitive information from a console message and trims it to a specified length if provided.
1557
+ * @param str - The console message to be redacted.
1558
+ * @param trimLength - The optional length at which to trim the redacted message.
1559
+ * @returns The redacted and optionally trimmed console message.
1288
1560
  */
1289
- function createSessionModel(mongoose) {
1290
- return mongoose.models.Session || mongoose.model('Session', sessionSchema);
1561
+ function redactMessage(str, trimLength) {
1562
+ if (!str) {
1563
+ return '';
1564
+ }
1565
+ const patterns = getMatchingSensitivePatterns(str);
1566
+ patterns.forEach((pattern) => {
1567
+ str = str.replace(pattern, '$1[REDACTED]');
1568
+ });
1569
+ return str;
1291
1570
  }
1292
-
1293
1571
  /**
1294
- * Creates or returns the Balance model using the provided mongoose instance and schema
1572
+ * Redacts sensitive information from log messages if the log level is 'error'.
1573
+ * Note: Intentionally mutates the object.
1574
+ * @param info - The log information object.
1575
+ * @returns The modified log information object.
1295
1576
  */
1296
- function createBalanceModel(mongoose) {
1297
- return mongoose.models.Balance || mongoose.model('Balance', balanceSchema);
1298
- }
1299
-
1577
+ const redactFormat = winston.format((info) => {
1578
+ if (info.level === 'error') {
1579
+ // Type guard to ensure message is a string
1580
+ if (typeof info.message === 'string') {
1581
+ info.message = redactMessage(info.message);
1582
+ }
1583
+ // Handle MESSAGE_SYMBOL with type safety
1584
+ const symbolValue = info[MESSAGE_SYMBOL];
1585
+ if (typeof symbolValue === 'string') {
1586
+ info[MESSAGE_SYMBOL] = redactMessage(symbolValue);
1587
+ }
1588
+ }
1589
+ return info;
1590
+ });
1300
1591
  /**
1301
- * Determine the log directory in a cross-compatible way.
1302
- * Priority:
1303
- * 1. LIBRECHAT_LOG_DIR environment variable
1304
- * 2. If running within LibreChat monorepo (when cwd ends with /api), use api/logs
1305
- * 3. If api/logs exists relative to cwd, use that (for running from project root)
1306
- * 4. Otherwise, use logs directory relative to process.cwd()
1592
+ * Truncates long strings, especially base64 image data, within log messages.
1307
1593
  *
1308
- * This avoids using __dirname which is not available in ESM modules
1594
+ * @param value - The value to be inspected and potentially truncated.
1595
+ * @param length - The length at which to truncate the value. Default: 100.
1596
+ * @returns The truncated or original value.
1309
1597
  */
1310
- const getLogDirectory = () => {
1311
- if (process.env.LIBRECHAT_LOG_DIR) {
1312
- return process.env.LIBRECHAT_LOG_DIR;
1598
+ const truncateLongStrings = (value, length = 100) => {
1599
+ if (typeof value === 'string') {
1600
+ return value.length > length ? value.substring(0, length) + '... [truncated]' : value;
1313
1601
  }
1314
- const cwd = process.cwd();
1315
- // Check if we're running from within the api directory
1316
- if (cwd.endsWith('/api') || cwd.endsWith('\\api')) {
1317
- return path.join(cwd, 'logs');
1602
+ return value;
1603
+ };
1604
+ /**
1605
+ * An array mapping function that truncates long strings (objects converted to JSON strings).
1606
+ * @param item - The item to be condensed.
1607
+ * @returns The condensed item.
1608
+ */
1609
+ const condenseArray = (item) => {
1610
+ if (typeof item === 'string') {
1611
+ return truncateLongStrings(JSON.stringify(item));
1318
1612
  }
1319
- // Check if api/logs exists relative to current directory (running from project root)
1320
- // We'll just use the path and let the file system create it if needed
1321
- const apiLogsPath = path.join(cwd, 'api', 'logs');
1322
- // For LibreChat project structure, use api/logs
1323
- // For external consumers, they should set LIBRECHAT_LOG_DIR
1324
- if (cwd.includes('LibreChat')) {
1325
- return apiLogsPath;
1613
+ else if (typeof item === 'object') {
1614
+ return truncateLongStrings(JSON.stringify(item));
1326
1615
  }
1327
- // Default to logs directory relative to current working directory
1328
- return path.join(cwd, 'logs');
1329
- };
1330
-
1331
- const logDir$1 = getLogDirectory();
1332
- const { NODE_ENV: NODE_ENV$1, DEBUG_LOGGING: DEBUG_LOGGING$1 = 'false' } = process.env;
1333
- const useDebugLogging$1 = (typeof DEBUG_LOGGING$1 === 'string' && DEBUG_LOGGING$1.toLowerCase() === 'true') ||
1334
- DEBUG_LOGGING$1 === 'true';
1335
- const levels$1 = {
1336
- error: 0,
1337
- warn: 1,
1338
- info: 2,
1339
- http: 3,
1340
- verbose: 4,
1341
- debug: 5,
1342
- activity: 6,
1343
- silly: 7,
1616
+ return item;
1344
1617
  };
1345
- winston.addColors({
1346
- info: 'green',
1347
- warn: 'italic yellow',
1348
- error: 'red',
1618
+ /**
1619
+ * Formats log messages for debugging purposes.
1620
+ * - Truncates long strings within log messages.
1621
+ * - Condenses arrays by truncating long strings and objects as strings within array items.
1622
+ * - Redacts sensitive information from log messages if the log level is 'error'.
1623
+ * - Converts log information object to a formatted string.
1624
+ *
1625
+ * @param options - The options for formatting log messages.
1626
+ * @returns The formatted log message.
1627
+ */
1628
+ const debugTraverse = winston.format.printf(({ level, message, timestamp, ...metadata }) => {
1629
+ if (!message) {
1630
+ return `${timestamp} ${level}`;
1631
+ }
1632
+ // Type-safe version of the CJS logic: !message?.trim || typeof message !== 'string'
1633
+ if (typeof message !== 'string' || !message.trim) {
1634
+ return `${timestamp} ${level}: ${JSON.stringify(message)}`;
1635
+ }
1636
+ const msgParts = [
1637
+ `${timestamp} ${level}: ${truncateLongStrings(message.trim(), 150)}`,
1638
+ ];
1639
+ try {
1640
+ if (level !== 'debug') {
1641
+ return msgParts[0];
1642
+ }
1643
+ if (!metadata) {
1644
+ return msgParts[0];
1645
+ }
1646
+ // Type-safe access to SPLAT_SYMBOL using bracket notation
1647
+ const metadataRecord = metadata;
1648
+ const splatArray = metadataRecord[SPLAT_SYMBOL];
1649
+ const debugValue = Array.isArray(splatArray) ? splatArray[0] : undefined;
1650
+ if (!debugValue) {
1651
+ return msgParts[0];
1652
+ }
1653
+ if (debugValue && Array.isArray(debugValue)) {
1654
+ msgParts.push(`\n${JSON.stringify(debugValue.map(condenseArray))}`);
1655
+ return msgParts.join('');
1656
+ }
1657
+ if (typeof debugValue !== 'object') {
1658
+ msgParts.push(` ${debugValue}`);
1659
+ return msgParts.join('');
1660
+ }
1661
+ msgParts.push('\n{');
1662
+ const copy = klona(metadata);
1663
+ try {
1664
+ const traversal = traverse(copy);
1665
+ traversal.forEach(function (value) {
1666
+ var _a;
1667
+ if (typeof (this === null || this === void 0 ? void 0 : this.key) === 'symbol') {
1668
+ return;
1669
+ }
1670
+ let _parentKey = '';
1671
+ const parent = this.parent;
1672
+ if (typeof (parent === null || parent === void 0 ? void 0 : parent.key) !== 'symbol' && (parent === null || parent === void 0 ? void 0 : parent.key) !== undefined) {
1673
+ _parentKey = String(parent.key);
1674
+ }
1675
+ const parentKey = `${parent && parent.notRoot ? _parentKey + '.' : ''}`;
1676
+ const tabs = `${parent && parent.notRoot ? ' ' : ' '}`;
1677
+ const currentKey = (_a = this === null || this === void 0 ? void 0 : this.key) !== null && _a !== void 0 ? _a : 'unknown';
1678
+ if (this.isLeaf && typeof value === 'string') {
1679
+ const truncatedText = truncateLongStrings(value);
1680
+ msgParts.push(`\n${tabs}${parentKey}${currentKey}: ${JSON.stringify(truncatedText)},`);
1681
+ }
1682
+ else if (this.notLeaf && Array.isArray(value) && value.length > 0) {
1683
+ const currentMessage = `\n${tabs}// ${value.length} ${String(currentKey).replace(/s$/, '')}(s)`;
1684
+ this.update(currentMessage);
1685
+ msgParts.push(currentMessage);
1686
+ const stringifiedArray = value.map(condenseArray);
1687
+ msgParts.push(`\n${tabs}${parentKey}${currentKey}: [${stringifiedArray}],`);
1688
+ }
1689
+ else if (this.isLeaf && typeof value === 'function') {
1690
+ msgParts.push(`\n${tabs}${parentKey}${currentKey}: function,`);
1691
+ }
1692
+ else if (this.isLeaf) {
1693
+ msgParts.push(`\n${tabs}${parentKey}${currentKey}: ${value},`);
1694
+ }
1695
+ });
1696
+ }
1697
+ catch (e) {
1698
+ const errorMessage = e instanceof Error ? e.message : 'Unknown error';
1699
+ msgParts.push(`\n[LOGGER TRAVERSAL ERROR] ${errorMessage}`);
1700
+ }
1701
+ msgParts.push('\n}');
1702
+ return msgParts.join('');
1703
+ }
1704
+ catch (e) {
1705
+ const errorMessage = e instanceof Error ? e.message : 'Unknown error';
1706
+ msgParts.push(`\n[LOGGER PARSING ERROR] ${errorMessage}`);
1707
+ return msgParts.join('');
1708
+ }
1709
+ });
1710
+ /**
1711
+ * Truncates long string values in JSON log objects.
1712
+ * Prevents outputting extremely long values (e.g., base64, blobs).
1713
+ */
1714
+ const jsonTruncateFormat = winston.format((info) => {
1715
+ const truncateLongStrings = (str, maxLength) => str.length > maxLength ? str.substring(0, maxLength) + '...' : str;
1716
+ const seen = new WeakSet();
1717
+ const truncateObject = (obj) => {
1718
+ if (typeof obj !== 'object' || obj === null) {
1719
+ return obj;
1720
+ }
1721
+ // Handle circular references - now with proper object type
1722
+ if (seen.has(obj)) {
1723
+ return '[Circular]';
1724
+ }
1725
+ seen.add(obj);
1726
+ if (Array.isArray(obj)) {
1727
+ return obj.map((item) => truncateObject(item));
1728
+ }
1729
+ // We know this is an object at this point
1730
+ const objectRecord = obj;
1731
+ const newObj = {};
1732
+ Object.entries(objectRecord).forEach(([key, value]) => {
1733
+ if (typeof value === 'string') {
1734
+ newObj[key] = truncateLongStrings(value, CONSOLE_JSON_STRING_LENGTH);
1735
+ }
1736
+ else {
1737
+ newObj[key] = truncateObject(value);
1738
+ }
1739
+ });
1740
+ return newObj;
1741
+ };
1742
+ return truncateObject(info);
1743
+ });
1744
+
1745
+ /**
1746
+ * Determine the log directory in a cross-compatible way.
1747
+ * Priority:
1748
+ * 1. LIBRECHAT_LOG_DIR environment variable
1749
+ * 2. If running within LibreChat monorepo (when cwd ends with /api), use api/logs
1750
+ * 3. If api/logs exists relative to cwd, use that (for running from project root)
1751
+ * 4. Otherwise, use logs directory relative to process.cwd()
1752
+ *
1753
+ * This avoids using __dirname which is not available in ESM modules
1754
+ */
1755
+ const getLogDirectory = () => {
1756
+ if (process.env.LIBRECHAT_LOG_DIR) {
1757
+ return process.env.LIBRECHAT_LOG_DIR;
1758
+ }
1759
+ const cwd = process.cwd();
1760
+ // Check if we're running from within the api directory
1761
+ if (cwd.endsWith('/api') || cwd.endsWith('\\api')) {
1762
+ return path.join(cwd, 'logs');
1763
+ }
1764
+ // Check if api/logs exists relative to current directory (running from project root)
1765
+ // We'll just use the path and let the file system create it if needed
1766
+ const apiLogsPath = path.join(cwd, 'api', 'logs');
1767
+ // For LibreChat project structure, use api/logs
1768
+ // For external consumers, they should set LIBRECHAT_LOG_DIR
1769
+ if (cwd.includes('LibreChat')) {
1770
+ return apiLogsPath;
1771
+ }
1772
+ // Default to logs directory relative to current working directory
1773
+ return path.join(cwd, 'logs');
1774
+ };
1775
+
1776
+ const logDir$1 = getLogDirectory();
1777
+ const { NODE_ENV: NODE_ENV$1, DEBUG_LOGGING: DEBUG_LOGGING$1, CONSOLE_JSON, DEBUG_CONSOLE } = process.env;
1778
+ const useConsoleJson = typeof CONSOLE_JSON === 'string' && CONSOLE_JSON.toLowerCase() === 'true';
1779
+ const useDebugConsole = typeof DEBUG_CONSOLE === 'string' && DEBUG_CONSOLE.toLowerCase() === 'true';
1780
+ const useDebugLogging$1 = typeof DEBUG_LOGGING$1 === 'string' && DEBUG_LOGGING$1.toLowerCase() === 'true';
1781
+ const levels$1 = {
1782
+ error: 0,
1783
+ warn: 1,
1784
+ info: 2,
1785
+ http: 3,
1786
+ verbose: 4,
1787
+ debug: 5,
1788
+ activity: 6,
1789
+ silly: 7,
1790
+ };
1791
+ winston.addColors({
1792
+ info: 'green',
1793
+ warn: 'italic yellow',
1794
+ error: 'red',
1349
1795
  debug: 'blue',
1350
1796
  });
1351
1797
  const level$1 = () => {
1352
1798
  const env = NODE_ENV$1 || 'development';
1353
- const isDevelopment = env === 'development';
1354
- return isDevelopment ? 'debug' : 'warn';
1799
+ return env === 'development' ? 'debug' : 'warn';
1355
1800
  };
1356
- const fileFormat$1 = winston.format.combine(winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), winston.format.errors({ stack: true }), winston.format.splat());
1357
- const logLevel = useDebugLogging$1 ? 'debug' : 'error';
1801
+ const fileFormat$1 = winston.format.combine(redactFormat(), winston.format.timestamp({ format: () => new Date().toISOString() }), winston.format.errors({ stack: true }), winston.format.splat());
1358
1802
  const transports$1 = [
1359
1803
  new winston.transports.DailyRotateFile({
1360
- level: logLevel,
1361
- filename: `${logDir$1}/meiliSync-%DATE%.log`,
1804
+ level: 'error',
1805
+ filename: `${logDir$1}/error-%DATE%.log`,
1362
1806
  datePattern: 'YYYY-MM-DD',
1363
1807
  zippedArchive: true,
1364
1808
  maxSize: '20m',
@@ -1366,17 +1810,174 @@ const transports$1 = [
1366
1810
  format: fileFormat$1,
1367
1811
  }),
1368
1812
  ];
1369
- const consoleFormat$1 = winston.format.combine(winston.format.colorize({ all: true }), winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), winston.format.printf((info) => `${info.timestamp} ${info.level}: ${info.message}`));
1370
- transports$1.push(new winston.transports.Console({
1371
- level: 'info',
1372
- format: consoleFormat$1,
1813
+ if (useDebugLogging$1) {
1814
+ transports$1.push(new winston.transports.DailyRotateFile({
1815
+ level: 'debug',
1816
+ filename: `${logDir$1}/debug-%DATE%.log`,
1817
+ datePattern: 'YYYY-MM-DD',
1818
+ zippedArchive: true,
1819
+ maxSize: '20m',
1820
+ maxFiles: '14d',
1821
+ format: winston.format.combine(fileFormat$1, debugTraverse),
1822
+ }));
1823
+ }
1824
+ const consoleFormat$1 = winston.format.combine(redactFormat(), winston.format.colorize({ all: true }), winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), winston.format.printf((info) => {
1825
+ const message = `${info.timestamp} ${info.level}: ${info.message}`;
1826
+ return info.level.includes('error') ? redactMessage(message) : message;
1373
1827
  }));
1828
+ let consoleLogLevel = 'info';
1829
+ if (useDebugConsole) {
1830
+ consoleLogLevel = 'debug';
1831
+ }
1832
+ // Add console transport
1833
+ if (useDebugConsole) {
1834
+ transports$1.push(new winston.transports.Console({
1835
+ level: consoleLogLevel,
1836
+ format: useConsoleJson
1837
+ ? winston.format.combine(fileFormat$1, jsonTruncateFormat(), winston.format.json())
1838
+ : winston.format.combine(fileFormat$1, debugTraverse),
1839
+ }));
1840
+ }
1841
+ else if (useConsoleJson) {
1842
+ transports$1.push(new winston.transports.Console({
1843
+ level: consoleLogLevel,
1844
+ format: winston.format.combine(fileFormat$1, jsonTruncateFormat(), winston.format.json()),
1845
+ }));
1846
+ }
1847
+ else {
1848
+ transports$1.push(new winston.transports.Console({
1849
+ level: consoleLogLevel,
1850
+ format: consoleFormat$1,
1851
+ }));
1852
+ }
1853
+ // Create logger
1374
1854
  const logger$1 = winston.createLogger({
1375
1855
  level: level$1(),
1376
1856
  levels: levels$1,
1377
1857
  transports: transports$1,
1378
1858
  });
1379
1859
 
1860
+ /**
1861
+ * Checks if the connected MongoDB deployment supports transactions
1862
+ * This requires a MongoDB replica set configuration
1863
+ *
1864
+ * @returns True if transactions are supported, false otherwise
1865
+ */
1866
+ const supportsTransactions = async (mongoose) => {
1867
+ var _a;
1868
+ try {
1869
+ const session = await mongoose.startSession();
1870
+ try {
1871
+ session.startTransaction();
1872
+ await ((_a = mongoose.connection.db) === null || _a === void 0 ? void 0 : _a.collection('__transaction_test__').findOne({}, { session }));
1873
+ await session.abortTransaction();
1874
+ logger$1.debug('MongoDB transactions are supported');
1875
+ return true;
1876
+ }
1877
+ catch (transactionError) {
1878
+ logger$1.debug('MongoDB transactions not supported (transaction error):', (transactionError === null || transactionError === void 0 ? void 0 : transactionError.message) || 'Unknown error');
1879
+ return false;
1880
+ }
1881
+ finally {
1882
+ await session.endSession();
1883
+ }
1884
+ }
1885
+ catch (error) {
1886
+ logger$1.debug('MongoDB transactions not supported (session error):', (error === null || error === void 0 ? void 0 : error.message) || 'Unknown error');
1887
+ return false;
1888
+ }
1889
+ };
1890
+ /**
1891
+ * Gets whether the current MongoDB deployment supports transactions
1892
+ * Caches the result for performance
1893
+ *
1894
+ * @returns True if transactions are supported, false otherwise
1895
+ */
1896
+ const getTransactionSupport = async (mongoose, transactionSupportCache) => {
1897
+ let transactionsSupported = false;
1898
+ if (transactionSupportCache === null) {
1899
+ transactionsSupported = await supportsTransactions(mongoose);
1900
+ }
1901
+ return transactionsSupported;
1902
+ };
1903
+
1904
+ /**
1905
+ * Creates or returns the User model using the provided mongoose instance and schema
1906
+ */
1907
+ function createUserModel(mongoose) {
1908
+ return mongoose.models.User || mongoose.model('User', userSchema);
1909
+ }
1910
+
1911
+ /**
1912
+ * Creates or returns the Token model using the provided mongoose instance and schema
1913
+ */
1914
+ function createTokenModel(mongoose) {
1915
+ return mongoose.models.Token || mongoose.model('Token', tokenSchema);
1916
+ }
1917
+
1918
+ /**
1919
+ * Creates or returns the Session model using the provided mongoose instance and schema
1920
+ */
1921
+ function createSessionModel(mongoose) {
1922
+ return mongoose.models.Session || mongoose.model('Session', sessionSchema);
1923
+ }
1924
+
1925
+ /**
1926
+ * Creates or returns the Balance model using the provided mongoose instance and schema
1927
+ */
1928
+ function createBalanceModel(mongoose) {
1929
+ return mongoose.models.Balance || mongoose.model('Balance', balanceSchema);
1930
+ }
1931
+
1932
+ const logDir = getLogDirectory();
1933
+ const { NODE_ENV, DEBUG_LOGGING = 'false' } = process.env;
1934
+ const useDebugLogging = (typeof DEBUG_LOGGING === 'string' && DEBUG_LOGGING.toLowerCase() === 'true') ||
1935
+ DEBUG_LOGGING === 'true';
1936
+ const levels = {
1937
+ error: 0,
1938
+ warn: 1,
1939
+ info: 2,
1940
+ http: 3,
1941
+ verbose: 4,
1942
+ debug: 5,
1943
+ activity: 6,
1944
+ silly: 7,
1945
+ };
1946
+ winston.addColors({
1947
+ info: 'green',
1948
+ warn: 'italic yellow',
1949
+ error: 'red',
1950
+ debug: 'blue',
1951
+ });
1952
+ const level = () => {
1953
+ const env = NODE_ENV || 'development';
1954
+ const isDevelopment = env === 'development';
1955
+ return isDevelopment ? 'debug' : 'warn';
1956
+ };
1957
+ const fileFormat = winston.format.combine(winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), winston.format.errors({ stack: true }), winston.format.splat());
1958
+ const logLevel = useDebugLogging ? 'debug' : 'error';
1959
+ const transports = [
1960
+ new winston.transports.DailyRotateFile({
1961
+ level: logLevel,
1962
+ filename: `${logDir}/meiliSync-%DATE%.log`,
1963
+ datePattern: 'YYYY-MM-DD',
1964
+ zippedArchive: true,
1965
+ maxSize: '20m',
1966
+ maxFiles: '14d',
1967
+ format: fileFormat,
1968
+ }),
1969
+ ];
1970
+ const consoleFormat = winston.format.combine(winston.format.colorize({ all: true }), winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), winston.format.printf((info) => `${info.timestamp} ${info.level}: ${info.message}`));
1971
+ transports.push(new winston.transports.Console({
1972
+ level: 'info',
1973
+ format: consoleFormat,
1974
+ }));
1975
+ const logger = winston.createLogger({
1976
+ level: level(),
1977
+ levels,
1978
+ transports,
1979
+ });
1980
+
1380
1981
  // Environment flags
1381
1982
  /**
1382
1983
  * Flag to indicate if search is enabled based on environment variables.
@@ -1472,7 +2073,7 @@ const createMeiliMongooseModel = ({ index, attributesToIndex, syncOptions, }) =>
1472
2073
  try {
1473
2074
  const startTime = Date.now();
1474
2075
  const { batchSize, delayMs } = syncConfig;
1475
- logger$1.info(`[syncWithMeili] Starting sync for ${primaryKey === 'messageId' ? 'messages' : 'conversations'} with batch size ${batchSize}`);
2076
+ logger.info(`[syncWithMeili] Starting sync for ${primaryKey === 'messageId' ? 'messages' : 'conversations'} with batch size ${batchSize}`);
1476
2077
  // Build query with resume capability
1477
2078
  const query = {};
1478
2079
  if (options === null || options === void 0 ? void 0 : options.resumeFromId) {
@@ -1514,7 +2115,7 @@ const createMeiliMongooseModel = ({ index, attributesToIndex, syncOptions, }) =>
1514
2115
  updateOps = [];
1515
2116
  // Log progress
1516
2117
  const progress = Math.round((processedCount / totalCount) * 100);
1517
- logger$1.info(`[syncWithMeili] Progress: ${progress}% (${processedCount}/${totalCount})`);
2118
+ logger.info(`[syncWithMeili] Progress: ${progress}% (${processedCount}/${totalCount})`);
1518
2119
  // Add delay to prevent overwhelming resources
1519
2120
  if (delayMs > 0) {
1520
2121
  await new Promise((resolve) => setTimeout(resolve, delayMs));
@@ -1526,10 +2127,10 @@ const createMeiliMongooseModel = ({ index, attributesToIndex, syncOptions, }) =>
1526
2127
  await this.processSyncBatch(index, documentBatch, updateOps);
1527
2128
  }
1528
2129
  const duration = Date.now() - startTime;
1529
- logger$1.info(`[syncWithMeili] Completed sync for ${primaryKey === 'messageId' ? 'messages' : 'conversations'} in ${duration}ms`);
2130
+ logger.info(`[syncWithMeili] Completed sync for ${primaryKey === 'messageId' ? 'messages' : 'conversations'} in ${duration}ms`);
1530
2131
  }
1531
2132
  catch (error) {
1532
- logger$1.error('[syncWithMeili] Error during sync:', error);
2133
+ logger.error('[syncWithMeili] Error during sync:', error);
1533
2134
  throw error;
1534
2135
  }
1535
2136
  }
@@ -1549,7 +2150,7 @@ const createMeiliMongooseModel = ({ index, attributesToIndex, syncOptions, }) =>
1549
2150
  }
1550
2151
  }
1551
2152
  catch (error) {
1552
- logger$1.error('[processSyncBatch] Error processing batch:', error);
2153
+ logger.error('[processSyncBatch] Error processing batch:', error);
1553
2154
  // Don't throw - allow sync to continue with other documents
1554
2155
  }
1555
2156
  }
@@ -1576,7 +2177,7 @@ const createMeiliMongooseModel = ({ index, attributesToIndex, syncOptions, }) =>
1576
2177
  const toDelete = meiliIds.filter((id) => !existingIds.has(id));
1577
2178
  if (toDelete.length > 0) {
1578
2179
  await Promise.all(toDelete.map((id) => index.deleteDocument(id)));
1579
- logger$1.debug(`[cleanupMeiliIndex] Deleted ${toDelete.length} orphaned documents`);
2180
+ logger.debug(`[cleanupMeiliIndex] Deleted ${toDelete.length} orphaned documents`);
1580
2181
  }
1581
2182
  offset += batchSize;
1582
2183
  // Add delay between batches
@@ -1586,7 +2187,7 @@ const createMeiliMongooseModel = ({ index, attributesToIndex, syncOptions, }) =>
1586
2187
  }
1587
2188
  }
1588
2189
  catch (error) {
1589
- logger$1.error('[cleanupMeiliIndex] Error during cleanup:', error);
2190
+ logger.error('[cleanupMeiliIndex] Error during cleanup:', error);
1590
2191
  }
1591
2192
  }
1592
2193
  /**
@@ -1656,7 +2257,7 @@ const createMeiliMongooseModel = ({ index, attributesToIndex, syncOptions, }) =>
1656
2257
  catch (error) {
1657
2258
  retryCount++;
1658
2259
  if (retryCount >= maxRetries) {
1659
- logger$1.error('[addObjectToMeili] Error adding document to Meili after retries:', error);
2260
+ logger.error('[addObjectToMeili] Error adding document to Meili after retries:', error);
1660
2261
  return next();
1661
2262
  }
1662
2263
  // Exponential backoff
@@ -1667,7 +2268,7 @@ const createMeiliMongooseModel = ({ index, attributesToIndex, syncOptions, }) =>
1667
2268
  await this.collection.updateMany({ _id: this._id }, { $set: { _meiliIndex: true } });
1668
2269
  }
1669
2270
  catch (error) {
1670
- logger$1.error('[addObjectToMeili] Error updating _meiliIndex field:', error);
2271
+ logger.error('[addObjectToMeili] Error updating _meiliIndex field:', error);
1671
2272
  return next();
1672
2273
  }
1673
2274
  next();
@@ -1682,7 +2283,7 @@ const createMeiliMongooseModel = ({ index, attributesToIndex, syncOptions, }) =>
1682
2283
  next();
1683
2284
  }
1684
2285
  catch (error) {
1685
- logger$1.error('[updateObjectToMeili] Error updating document in Meili:', error);
2286
+ logger.error('[updateObjectToMeili] Error updating document in Meili:', error);
1686
2287
  return next();
1687
2288
  }
1688
2289
  }
@@ -1697,7 +2298,7 @@ const createMeiliMongooseModel = ({ index, attributesToIndex, syncOptions, }) =>
1697
2298
  next();
1698
2299
  }
1699
2300
  catch (error) {
1700
- logger$1.error('[deleteObjectFromMeili] Error deleting document from Meili:', error);
2301
+ logger.error('[deleteObjectFromMeili] Error deleting document from Meili:', error);
1701
2302
  return next();
1702
2303
  }
1703
2304
  }
@@ -1789,23 +2390,23 @@ function mongoMeili(schema, options) {
1789
2390
  (async () => {
1790
2391
  try {
1791
2392
  await index.getRawInfo();
1792
- logger$1.debug(`[mongoMeili] Index ${indexName} already exists`);
2393
+ logger.debug(`[mongoMeili] Index ${indexName} already exists`);
1793
2394
  }
1794
2395
  catch (error) {
1795
2396
  const errorCode = error === null || error === void 0 ? void 0 : error.code;
1796
2397
  if (errorCode === 'index_not_found') {
1797
2398
  try {
1798
- logger$1.info(`[mongoMeili] Creating new index: ${indexName}`);
2399
+ logger.info(`[mongoMeili] Creating new index: ${indexName}`);
1799
2400
  await client.createIndex(indexName, { primaryKey });
1800
- logger$1.info(`[mongoMeili] Successfully created index: ${indexName}`);
2401
+ logger.info(`[mongoMeili] Successfully created index: ${indexName}`);
1801
2402
  }
1802
2403
  catch (createError) {
1803
2404
  // Index might have been created by another instance
1804
- logger$1.debug(`[mongoMeili] Index ${indexName} may already exist:`, createError);
2405
+ logger.debug(`[mongoMeili] Index ${indexName} may already exist:`, createError);
1805
2406
  }
1806
2407
  }
1807
2408
  else {
1808
- logger$1.error(`[mongoMeili] Error checking index ${indexName}:`, error);
2409
+ logger.error(`[mongoMeili] Error checking index ${indexName}:`, error);
1809
2410
  }
1810
2411
  }
1811
2412
  })();
@@ -1868,7 +2469,7 @@ function mongoMeili(schema, options) {
1868
2469
  }
1869
2470
  catch (error) {
1870
2471
  if (meiliEnabled) {
1871
- logger$1.error('[MeiliMongooseModel.deleteMany] There was an issue deleting conversation indexes upon deletion. Next startup may trigger syncing.', error);
2472
+ logger.error('[MeiliMongooseModel.deleteMany] There was an issue deleting conversation indexes upon deletion. Next startup may trigger syncing.', error);
1872
2473
  }
1873
2474
  return next();
1874
2475
  }
@@ -1888,7 +2489,7 @@ function mongoMeili(schema, options) {
1888
2489
  meiliDoc = await client.index('convos').getDocument(doc.conversationId);
1889
2490
  }
1890
2491
  catch (error) {
1891
- logger$1.debug('[MeiliMongooseModel.findOneAndUpdate] Convo not found in MeiliSearch and will index ' +
2492
+ logger.debug('[MeiliMongooseModel.findOneAndUpdate] Convo not found in MeiliSearch and will index ' +
1892
2493
  doc.conversationId, error);
1893
2494
  }
1894
2495
  }
@@ -1939,6 +2540,13 @@ function createAgentModel(mongoose) {
1939
2540
  return mongoose.models.Agent || mongoose.model('Agent', agentSchema);
1940
2541
  }
1941
2542
 
2543
+ /**
2544
+ * Creates or returns the AgentCategory model using the provided mongoose instance and schema
2545
+ */
2546
+ function createAgentCategoryModel(mongoose) {
2547
+ return mongoose.models.AgentCategory || mongoose.model('AgentCategory', agentCategorySchema);
2548
+ }
2549
+
1942
2550
  /**
1943
2551
  * Creates or returns the Role model using the provided mongoose instance and schema
1944
2552
  */
@@ -2050,6 +2658,108 @@ function createMemoryModel(mongoose) {
2050
2658
  return mongoose.models.MemoryEntry || mongoose.model('MemoryEntry', MemoryEntrySchema);
2051
2659
  }
2052
2660
 
2661
+ const accessRoleSchema = new Schema({
2662
+ accessRoleId: {
2663
+ type: String,
2664
+ required: true,
2665
+ index: true,
2666
+ unique: true,
2667
+ },
2668
+ name: {
2669
+ type: String,
2670
+ required: true,
2671
+ },
2672
+ description: String,
2673
+ resourceType: {
2674
+ type: String,
2675
+ enum: ['agent', 'project', 'file', 'promptGroup'],
2676
+ required: true,
2677
+ default: 'agent',
2678
+ },
2679
+ permBits: {
2680
+ type: Number,
2681
+ required: true,
2682
+ },
2683
+ }, { timestamps: true });
2684
+
2685
+ /**
2686
+ * Creates or returns the AccessRole model using the provided mongoose instance and schema
2687
+ */
2688
+ function createAccessRoleModel(mongoose) {
2689
+ return (mongoose.models.AccessRole || mongoose.model('AccessRole', accessRoleSchema));
2690
+ }
2691
+
2692
+ const aclEntrySchema = new Schema({
2693
+ principalType: {
2694
+ type: String,
2695
+ enum: Object.values(PrincipalType),
2696
+ required: true,
2697
+ },
2698
+ principalId: {
2699
+ type: Schema.Types.Mixed, // Can be ObjectId for users/groups or String for roles
2700
+ refPath: 'principalModel',
2701
+ required: function () {
2702
+ return this.principalType !== PrincipalType.PUBLIC;
2703
+ },
2704
+ index: true,
2705
+ },
2706
+ principalModel: {
2707
+ type: String,
2708
+ enum: Object.values(PrincipalModel),
2709
+ required: function () {
2710
+ return this.principalType !== PrincipalType.PUBLIC;
2711
+ },
2712
+ },
2713
+ resourceType: {
2714
+ type: String,
2715
+ enum: Object.values(ResourceType),
2716
+ required: true,
2717
+ },
2718
+ resourceId: {
2719
+ type: Schema.Types.ObjectId,
2720
+ required: true,
2721
+ index: true,
2722
+ },
2723
+ permBits: {
2724
+ type: Number,
2725
+ default: 1,
2726
+ },
2727
+ roleId: {
2728
+ type: Schema.Types.ObjectId,
2729
+ ref: 'AccessRole',
2730
+ },
2731
+ inheritedFrom: {
2732
+ type: Schema.Types.ObjectId,
2733
+ sparse: true,
2734
+ index: true,
2735
+ },
2736
+ grantedBy: {
2737
+ type: Schema.Types.ObjectId,
2738
+ ref: 'User',
2739
+ },
2740
+ grantedAt: {
2741
+ type: Date,
2742
+ default: Date.now,
2743
+ },
2744
+ }, { timestamps: true });
2745
+ aclEntrySchema.index({ principalId: 1, principalType: 1, resourceType: 1, resourceId: 1 });
2746
+ aclEntrySchema.index({ resourceId: 1, principalType: 1, principalId: 1 });
2747
+ aclEntrySchema.index({ principalId: 1, permBits: 1, resourceType: 1 });
2748
+
2749
+ /**
2750
+ * Creates or returns the AclEntry model using the provided mongoose instance and schema
2751
+ */
2752
+ function createAclEntryModel(mongoose) {
2753
+ return mongoose.models.AclEntry || mongoose.model('AclEntry', aclEntrySchema);
2754
+ }
2755
+
2756
+ /**
2757
+ * Creates or returns the Group model using the provided mongoose instance and schema
2758
+ */
2759
+ function createGroupModel(mongoose) {
2760
+ return mongoose.models.Group || mongoose.model('Group', groupSchema);
2761
+ }
2762
+
2053
2763
  /**
2054
2764
  * Creates all database models for all collections
2055
2765
  */
@@ -2062,6 +2772,7 @@ function createModels(mongoose) {
2062
2772
  Conversation: createConversationModel(mongoose),
2063
2773
  Message: createMessageModel(mongoose),
2064
2774
  Agent: createAgentModel(mongoose),
2775
+ AgentCategory: createAgentCategoryModel(mongoose),
2065
2776
  Role: createRoleModel(mongoose),
2066
2777
  Action: createActionModel(mongoose),
2067
2778
  Assistant: createAssistantModel(mongoose),
@@ -2078,935 +2789,1951 @@ function createModels(mongoose) {
2078
2789
  SharedLink: createSharedLinkModel(mongoose),
2079
2790
  ToolCall: createToolCallModel(mongoose),
2080
2791
  MemoryEntry: createMemoryModel(mongoose),
2792
+ AccessRole: createAccessRoleModel(mongoose),
2793
+ AclEntry: createAclEntryModel(mongoose),
2794
+ Group: createGroupModel(mongoose),
2081
2795
  };
2082
2796
  }
2083
2797
 
2084
- /** Factory function that takes mongoose instance and returns the methods */
2085
- function createUserMethods(mongoose) {
2086
- /**
2087
- * Search for a single user based on partial data and return matching user document as plain object.
2088
- */
2089
- async function findUser(searchCriteria, fieldsToSelect) {
2090
- const User = mongoose.models.User;
2091
- const query = User.findOne(searchCriteria);
2092
- if (fieldsToSelect) {
2093
- query.select(fieldsToSelect);
2094
- }
2095
- return (await query.lean());
2096
- }
2097
- /**
2098
- * Count the number of user documents in the collection based on the provided filter.
2099
- */
2100
- async function countUsers(filter = {}) {
2101
- const User = mongoose.models.User;
2102
- return await User.countDocuments(filter);
2798
+ var _a;
2799
+ class SessionError extends Error {
2800
+ constructor(message, code = 'SESSION_ERROR') {
2801
+ super(message);
2802
+ this.name = 'SessionError';
2803
+ this.code = code;
2103
2804
  }
2805
+ }
2806
+ const { REFRESH_TOKEN_EXPIRY } = (_a = process.env) !== null && _a !== void 0 ? _a : {};
2807
+ const expires = REFRESH_TOKEN_EXPIRY ? eval(REFRESH_TOKEN_EXPIRY) : 1000 * 60 * 60 * 24 * 7; // 7 days default
2808
+ // Factory function that takes mongoose instance and returns the methods
2809
+ function createSessionMethods(mongoose) {
2104
2810
  /**
2105
- * Creates a new user, optionally with a TTL of 1 week.
2811
+ * Creates a new session for a user
2106
2812
  */
2107
- async function createUser(data, balanceConfig, disableTTL = true, returnUser = false) {
2108
- const User = mongoose.models.User;
2109
- const Balance = mongoose.models.Balance;
2110
- const userData = {
2111
- ...data,
2112
- expiresAt: disableTTL ? undefined : new Date(Date.now() + 604800 * 1000), // 1 week in milliseconds
2113
- };
2114
- if (disableTTL) {
2115
- delete userData.expiresAt;
2813
+ async function createSession(userId, options = {}) {
2814
+ if (!userId) {
2815
+ throw new SessionError('User ID is required', 'INVALID_USER_ID');
2116
2816
  }
2117
- const user = await User.create(userData);
2118
- // If balance is enabled, create or update a balance record for the user
2119
- if ((balanceConfig === null || balanceConfig === void 0 ? void 0 : balanceConfig.enabled) && (balanceConfig === null || balanceConfig === void 0 ? void 0 : balanceConfig.startBalance)) {
2120
- const update = {
2121
- $inc: { tokenCredits: balanceConfig.startBalance },
2122
- };
2123
- if (balanceConfig.autoRefillEnabled &&
2124
- balanceConfig.refillIntervalValue != null &&
2125
- balanceConfig.refillIntervalUnit != null &&
2126
- balanceConfig.refillAmount != null) {
2127
- update.$set = {
2128
- autoRefillEnabled: true,
2129
- refillIntervalValue: balanceConfig.refillIntervalValue,
2130
- refillIntervalUnit: balanceConfig.refillIntervalUnit,
2131
- refillAmount: balanceConfig.refillAmount,
2132
- };
2133
- }
2134
- await Balance.findOneAndUpdate({ user: user._id }, update, {
2135
- upsert: true,
2136
- new: true,
2137
- }).lean();
2817
+ try {
2818
+ const Session = mongoose.models.Session;
2819
+ const currentSession = new Session({
2820
+ user: userId,
2821
+ expiration: options.expiration || new Date(Date.now() + expires),
2822
+ });
2823
+ const refreshToken = await generateRefreshToken(currentSession);
2824
+ return { session: currentSession, refreshToken };
2138
2825
  }
2139
- if (returnUser) {
2140
- return user.toObject();
2826
+ catch (error) {
2827
+ logger$1.error('[createSession] Error creating session:', error);
2828
+ throw new SessionError('Failed to create session', 'CREATE_SESSION_FAILED');
2141
2829
  }
2142
- return user._id;
2143
2830
  }
2144
2831
  /**
2145
- * Update a user with new data without overwriting existing properties.
2832
+ * Finds a session by various parameters
2146
2833
  */
2147
- async function updateUser(userId, updateData) {
2148
- const User = mongoose.models.User;
2149
- const updateOperation = {
2150
- $set: updateData,
2151
- $unset: { expiresAt: '' }, // Remove the expiresAt field to prevent TTL
2152
- };
2153
- return (await User.findByIdAndUpdate(userId, updateOperation, {
2154
- new: true,
2155
- runValidators: true,
2156
- }).lean());
2834
+ async function findSession(params, options = { lean: true }) {
2835
+ try {
2836
+ const Session = mongoose.models.Session;
2837
+ const query = {};
2838
+ if (!params.refreshToken && !params.userId && !params.sessionId) {
2839
+ throw new SessionError('At least one search parameter is required', 'INVALID_SEARCH_PARAMS');
2840
+ }
2841
+ if (params.refreshToken) {
2842
+ const tokenHash = await hashToken(params.refreshToken);
2843
+ query.refreshTokenHash = tokenHash;
2844
+ }
2845
+ if (params.userId) {
2846
+ query.user = params.userId;
2847
+ }
2848
+ if (params.sessionId) {
2849
+ const sessionId = typeof params.sessionId === 'object' &&
2850
+ params.sessionId !== null &&
2851
+ 'sessionId' in params.sessionId
2852
+ ? params.sessionId.sessionId
2853
+ : params.sessionId;
2854
+ if (!mongoose.Types.ObjectId.isValid(sessionId)) {
2855
+ throw new SessionError('Invalid session ID format', 'INVALID_SESSION_ID');
2856
+ }
2857
+ query._id = sessionId;
2858
+ }
2859
+ // Add expiration check to only return valid sessions
2860
+ query.expiration = { $gt: new Date() };
2861
+ const sessionQuery = Session.findOne(query);
2862
+ if (options.lean) {
2863
+ return (await sessionQuery.lean());
2864
+ }
2865
+ return await sessionQuery.exec();
2866
+ }
2867
+ catch (error) {
2868
+ logger$1.error('[findSession] Error finding session:', error);
2869
+ throw new SessionError('Failed to find session', 'FIND_SESSION_FAILED');
2870
+ }
2157
2871
  }
2158
2872
  /**
2159
- * Retrieve a user by ID and convert the found user document to a plain object.
2873
+ * Updates session expiration
2160
2874
  */
2161
- async function getUserById(userId, fieldsToSelect) {
2162
- const User = mongoose.models.User;
2163
- const query = User.findById(userId);
2164
- if (fieldsToSelect) {
2165
- query.select(fieldsToSelect);
2875
+ async function updateExpiration(session, newExpiration) {
2876
+ try {
2877
+ const Session = mongoose.models.Session;
2878
+ const sessionDoc = typeof session === 'string' ? await Session.findById(session) : session;
2879
+ if (!sessionDoc) {
2880
+ throw new SessionError('Session not found', 'SESSION_NOT_FOUND');
2881
+ }
2882
+ sessionDoc.expiration = newExpiration || new Date(Date.now() + expires);
2883
+ return await sessionDoc.save();
2884
+ }
2885
+ catch (error) {
2886
+ logger$1.error('[updateExpiration] Error updating session:', error);
2887
+ throw new SessionError('Failed to update session expiration', 'UPDATE_EXPIRATION_FAILED');
2166
2888
  }
2167
- return (await query.lean());
2168
2889
  }
2169
2890
  /**
2170
- * Delete a user by their unique ID.
2891
+ * Deletes a session by refresh token or session ID
2171
2892
  */
2172
- async function deleteUserById(userId) {
2893
+ async function deleteSession(params) {
2173
2894
  try {
2174
- const User = mongoose.models.User;
2175
- const result = await User.deleteOne({ _id: userId });
2895
+ const Session = mongoose.models.Session;
2896
+ if (!params.refreshToken && !params.sessionId) {
2897
+ throw new SessionError('Either refreshToken or sessionId is required', 'INVALID_DELETE_PARAMS');
2898
+ }
2899
+ const query = {};
2900
+ if (params.refreshToken) {
2901
+ query.refreshTokenHash = await hashToken(params.refreshToken);
2902
+ }
2903
+ if (params.sessionId) {
2904
+ query._id = params.sessionId;
2905
+ }
2906
+ const result = await Session.deleteOne(query);
2176
2907
  if (result.deletedCount === 0) {
2177
- return { deletedCount: 0, message: 'No user found with that ID.' };
2908
+ logger$1.warn('[deleteSession] No session found to delete');
2178
2909
  }
2179
- return { deletedCount: result.deletedCount, message: 'User was deleted successfully.' };
2910
+ return result;
2180
2911
  }
2181
2912
  catch (error) {
2182
- const errorMessage = error instanceof Error ? error.message : 'Unknown error';
2183
- throw new Error('Error deleting user: ' + errorMessage);
2913
+ logger$1.error('[deleteSession] Error deleting session:', error);
2914
+ throw new SessionError('Failed to delete session', 'DELETE_SESSION_FAILED');
2184
2915
  }
2185
2916
  }
2186
2917
  /**
2187
- * Generates a JWT token for a given user.
2918
+ * Deletes all sessions for a user
2188
2919
  */
2189
- async function generateToken(user) {
2190
- if (!user) {
2191
- throw new Error('No user provided');
2192
- }
2193
- let expires = 1000 * 60 * 15;
2194
- if (process.env.SESSION_EXPIRY !== undefined && process.env.SESSION_EXPIRY !== '') {
2195
- try {
2196
- const evaluated = eval(process.env.SESSION_EXPIRY);
2197
- if (evaluated) {
2198
- expires = evaluated;
2199
- }
2920
+ async function deleteAllUserSessions(userId, options = {}) {
2921
+ try {
2922
+ const Session = mongoose.models.Session;
2923
+ if (!userId) {
2924
+ throw new SessionError('User ID is required', 'INVALID_USER_ID');
2200
2925
  }
2201
- catch (error) {
2202
- console.warn('Invalid SESSION_EXPIRY expression, using default:', error);
2926
+ const userIdString = typeof userId === 'object' && userId !== null ? userId.userId : userId;
2927
+ if (!mongoose.Types.ObjectId.isValid(userIdString)) {
2928
+ throw new SessionError('Invalid user ID format', 'INVALID_USER_ID_FORMAT');
2203
2929
  }
2204
- }
2205
- return await signPayload({
2930
+ const query = { user: userIdString };
2931
+ if (options.excludeCurrentSession && options.currentSessionId) {
2932
+ query._id = { $ne: options.currentSessionId };
2933
+ }
2934
+ const result = await Session.deleteMany(query);
2935
+ if (result.deletedCount && result.deletedCount > 0) {
2936
+ logger$1.debug(`[deleteAllUserSessions] Deleted ${result.deletedCount} sessions for user ${userIdString}.`);
2937
+ }
2938
+ return result;
2939
+ }
2940
+ catch (error) {
2941
+ logger$1.error('[deleteAllUserSessions] Error deleting user sessions:', error);
2942
+ throw new SessionError('Failed to delete user sessions', 'DELETE_ALL_SESSIONS_FAILED');
2943
+ }
2944
+ }
2945
+ /**
2946
+ * Generates a refresh token for a session
2947
+ */
2948
+ async function generateRefreshToken(session) {
2949
+ if (!session || !session.user) {
2950
+ throw new SessionError('Invalid session object', 'INVALID_SESSION');
2951
+ }
2952
+ try {
2953
+ const expiresIn = session.expiration ? session.expiration.getTime() : Date.now() + expires;
2954
+ if (!session.expiration) {
2955
+ session.expiration = new Date(expiresIn);
2956
+ }
2957
+ const refreshToken = await signPayload({
2958
+ payload: {
2959
+ id: session.user,
2960
+ sessionId: session._id,
2961
+ },
2962
+ secret: process.env.JWT_REFRESH_SECRET,
2963
+ expirationTime: Math.floor((expiresIn - Date.now()) / 1000),
2964
+ });
2965
+ session.refreshTokenHash = await hashToken(refreshToken);
2966
+ await session.save();
2967
+ return refreshToken;
2968
+ }
2969
+ catch (error) {
2970
+ logger$1.error('[generateRefreshToken] Error generating refresh token:', error);
2971
+ throw new SessionError('Failed to generate refresh token', 'GENERATE_TOKEN_FAILED');
2972
+ }
2973
+ }
2974
+ /**
2975
+ * Counts active sessions for a user
2976
+ */
2977
+ async function countActiveSessions(userId) {
2978
+ try {
2979
+ const Session = mongoose.models.Session;
2980
+ if (!userId) {
2981
+ throw new SessionError('User ID is required', 'INVALID_USER_ID');
2982
+ }
2983
+ return await Session.countDocuments({
2984
+ user: userId,
2985
+ expiration: { $gt: new Date() },
2986
+ });
2987
+ }
2988
+ catch (error) {
2989
+ logger$1.error('[countActiveSessions] Error counting active sessions:', error);
2990
+ throw new SessionError('Failed to count active sessions', 'COUNT_SESSIONS_FAILED');
2991
+ }
2992
+ }
2993
+ return {
2994
+ findSession,
2995
+ SessionError,
2996
+ deleteSession,
2997
+ createSession,
2998
+ updateExpiration,
2999
+ countActiveSessions,
3000
+ generateRefreshToken,
3001
+ deleteAllUserSessions,
3002
+ };
3003
+ }
3004
+
3005
+ // Factory function that takes mongoose instance and returns the methods
3006
+ function createTokenMethods(mongoose) {
3007
+ /**
3008
+ * Creates a new Token instance.
3009
+ */
3010
+ async function createToken(tokenData) {
3011
+ try {
3012
+ const Token = mongoose.models.Token;
3013
+ const currentTime = new Date();
3014
+ const expiresAt = new Date(currentTime.getTime() + tokenData.expiresIn * 1000);
3015
+ const newTokenData = {
3016
+ ...tokenData,
3017
+ createdAt: currentTime,
3018
+ expiresAt,
3019
+ };
3020
+ return await Token.create(newTokenData);
3021
+ }
3022
+ catch (error) {
3023
+ logger$1.debug('An error occurred while creating token:', error);
3024
+ throw error;
3025
+ }
3026
+ }
3027
+ /**
3028
+ * Updates a Token document that matches the provided query.
3029
+ */
3030
+ async function updateToken(query, updateData) {
3031
+ try {
3032
+ const Token = mongoose.models.Token;
3033
+ return await Token.findOneAndUpdate(query, updateData, { new: true });
3034
+ }
3035
+ catch (error) {
3036
+ logger$1.debug('An error occurred while updating token:', error);
3037
+ throw error;
3038
+ }
3039
+ }
3040
+ /**
3041
+ * Deletes all Token documents that match the provided token, user ID, or email.
3042
+ */
3043
+ async function deleteTokens(query) {
3044
+ try {
3045
+ const Token = mongoose.models.Token;
3046
+ return await Token.deleteMany({
3047
+ $or: [
3048
+ { userId: query.userId },
3049
+ { token: query.token },
3050
+ { email: query.email },
3051
+ { identifier: query.identifier },
3052
+ ],
3053
+ });
3054
+ }
3055
+ catch (error) {
3056
+ logger$1.debug('An error occurred while deleting tokens:', error);
3057
+ throw error;
3058
+ }
3059
+ }
3060
+ /**
3061
+ * Finds a Token document that matches the provided query.
3062
+ */
3063
+ async function findToken(query) {
3064
+ try {
3065
+ const Token = mongoose.models.Token;
3066
+ const conditions = [];
3067
+ if (query.userId) {
3068
+ conditions.push({ userId: query.userId });
3069
+ }
3070
+ if (query.token) {
3071
+ conditions.push({ token: query.token });
3072
+ }
3073
+ if (query.email) {
3074
+ conditions.push({ email: query.email });
3075
+ }
3076
+ if (query.identifier) {
3077
+ conditions.push({ identifier: query.identifier });
3078
+ }
3079
+ const token = await Token.findOne({
3080
+ $and: conditions,
3081
+ }).lean();
3082
+ return token;
3083
+ }
3084
+ catch (error) {
3085
+ logger$1.debug('An error occurred while finding token:', error);
3086
+ throw error;
3087
+ }
3088
+ }
3089
+ // Return all methods
3090
+ return {
3091
+ findToken,
3092
+ createToken,
3093
+ updateToken,
3094
+ deleteTokens,
3095
+ };
3096
+ }
3097
+
3098
+ // Factory function that takes mongoose instance and returns the methods
3099
+ function createRoleMethods(mongoose) {
3100
+ /**
3101
+ * Initialize default roles in the system.
3102
+ * Creates the default roles (ADMIN, USER) if they don't exist in the database.
3103
+ * Updates existing roles with new permission types if they're missing.
3104
+ */
3105
+ async function initializeRoles() {
3106
+ var _a, _b;
3107
+ const Role = mongoose.models.Role;
3108
+ for (const roleName of [SystemRoles.ADMIN, SystemRoles.USER]) {
3109
+ let role = await Role.findOne({ name: roleName });
3110
+ const defaultPerms = roleDefaults[roleName].permissions;
3111
+ if (!role) {
3112
+ role = new Role(roleDefaults[roleName]);
3113
+ }
3114
+ else {
3115
+ const permissions = (_b = (_a = role.toObject()) === null || _a === void 0 ? void 0 : _a.permissions) !== null && _b !== void 0 ? _b : {};
3116
+ role.permissions = role.permissions || {};
3117
+ for (const permType of Object.keys(defaultPerms)) {
3118
+ if (permissions[permType] == null || Object.keys(permissions[permType]).length === 0) {
3119
+ role.permissions[permType] = defaultPerms[permType];
3120
+ }
3121
+ }
3122
+ }
3123
+ await role.save();
3124
+ }
3125
+ }
3126
+ /**
3127
+ * List all roles in the system (for testing purposes)
3128
+ * Returns an array of all roles with their names and permissions
3129
+ */
3130
+ async function listRoles() {
3131
+ const Role = mongoose.models.Role;
3132
+ return await Role.find({}).select('name permissions').lean();
3133
+ }
3134
+ // Return all methods you want to expose
3135
+ return {
3136
+ listRoles,
3137
+ initializeRoles,
3138
+ };
3139
+ }
3140
+
3141
+ /** Factory function that takes mongoose instance and returns the methods */
3142
+ function createUserMethods(mongoose) {
3143
+ /**
3144
+ * Search for a single user based on partial data and return matching user document as plain object.
3145
+ */
3146
+ async function findUser(searchCriteria, fieldsToSelect) {
3147
+ const User = mongoose.models.User;
3148
+ const query = User.findOne(searchCriteria);
3149
+ if (fieldsToSelect) {
3150
+ query.select(fieldsToSelect);
3151
+ }
3152
+ return (await query.lean());
3153
+ }
3154
+ /**
3155
+ * Count the number of user documents in the collection based on the provided filter.
3156
+ */
3157
+ async function countUsers(filter = {}) {
3158
+ const User = mongoose.models.User;
3159
+ return await User.countDocuments(filter);
3160
+ }
3161
+ /**
3162
+ * Creates a new user, optionally with a TTL of 1 week.
3163
+ */
3164
+ async function createUser(data, balanceConfig, disableTTL = true, returnUser = false) {
3165
+ const User = mongoose.models.User;
3166
+ const Balance = mongoose.models.Balance;
3167
+ const userData = {
3168
+ ...data,
3169
+ expiresAt: disableTTL ? undefined : new Date(Date.now() + 604800 * 1000), // 1 week in milliseconds
3170
+ };
3171
+ if (disableTTL) {
3172
+ delete userData.expiresAt;
3173
+ }
3174
+ const user = await User.create(userData);
3175
+ // If balance is enabled, create or update a balance record for the user
3176
+ if ((balanceConfig === null || balanceConfig === void 0 ? void 0 : balanceConfig.enabled) && (balanceConfig === null || balanceConfig === void 0 ? void 0 : balanceConfig.startBalance)) {
3177
+ const update = {
3178
+ $inc: { tokenCredits: balanceConfig.startBalance },
3179
+ };
3180
+ if (balanceConfig.autoRefillEnabled &&
3181
+ balanceConfig.refillIntervalValue != null &&
3182
+ balanceConfig.refillIntervalUnit != null &&
3183
+ balanceConfig.refillAmount != null) {
3184
+ update.$set = {
3185
+ autoRefillEnabled: true,
3186
+ refillIntervalValue: balanceConfig.refillIntervalValue,
3187
+ refillIntervalUnit: balanceConfig.refillIntervalUnit,
3188
+ refillAmount: balanceConfig.refillAmount,
3189
+ };
3190
+ }
3191
+ await Balance.findOneAndUpdate({ user: user._id }, update, {
3192
+ upsert: true,
3193
+ new: true,
3194
+ }).lean();
3195
+ }
3196
+ if (returnUser) {
3197
+ return user.toObject();
3198
+ }
3199
+ return user._id;
3200
+ }
3201
+ /**
3202
+ * Update a user with new data without overwriting existing properties.
3203
+ */
3204
+ async function updateUser(userId, updateData) {
3205
+ const User = mongoose.models.User;
3206
+ const updateOperation = {
3207
+ $set: updateData,
3208
+ $unset: { expiresAt: '' }, // Remove the expiresAt field to prevent TTL
3209
+ };
3210
+ return (await User.findByIdAndUpdate(userId, updateOperation, {
3211
+ new: true,
3212
+ runValidators: true,
3213
+ }).lean());
3214
+ }
3215
+ /**
3216
+ * Retrieve a user by ID and convert the found user document to a plain object.
3217
+ */
3218
+ async function getUserById(userId, fieldsToSelect) {
3219
+ const User = mongoose.models.User;
3220
+ const query = User.findById(userId);
3221
+ if (fieldsToSelect) {
3222
+ query.select(fieldsToSelect);
3223
+ }
3224
+ return (await query.lean());
3225
+ }
3226
+ /**
3227
+ * Delete a user by their unique ID.
3228
+ */
3229
+ async function deleteUserById(userId) {
3230
+ try {
3231
+ const User = mongoose.models.User;
3232
+ const result = await User.deleteOne({ _id: userId });
3233
+ if (result.deletedCount === 0) {
3234
+ return { deletedCount: 0, message: 'No user found with that ID.' };
3235
+ }
3236
+ return { deletedCount: result.deletedCount, message: 'User was deleted successfully.' };
3237
+ }
3238
+ catch (error) {
3239
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
3240
+ throw new Error('Error deleting user: ' + errorMessage);
3241
+ }
3242
+ }
3243
+ /**
3244
+ * Generates a JWT token for a given user.
3245
+ */
3246
+ async function generateToken(user) {
3247
+ if (!user) {
3248
+ throw new Error('No user provided');
3249
+ }
3250
+ let expires = 1000 * 60 * 15;
3251
+ if (process.env.SESSION_EXPIRY !== undefined && process.env.SESSION_EXPIRY !== '') {
3252
+ try {
3253
+ const evaluated = eval(process.env.SESSION_EXPIRY);
3254
+ if (evaluated) {
3255
+ expires = evaluated;
3256
+ }
3257
+ }
3258
+ catch (error) {
3259
+ console.warn('Invalid SESSION_EXPIRY expression, using default:', error);
3260
+ }
3261
+ }
3262
+ return await signPayload({
2206
3263
  payload: {
2207
3264
  id: user._id,
2208
3265
  username: user.username,
2209
3266
  provider: user.provider,
2210
3267
  email: user.email,
2211
3268
  },
2212
- secret: process.env.JWT_SECRET,
2213
- expirationTime: expires / 1000,
2214
- });
3269
+ secret: process.env.JWT_SECRET,
3270
+ expirationTime: expires / 1000,
3271
+ });
3272
+ }
3273
+ /**
3274
+ * Update a user's personalization memories setting.
3275
+ * Handles the edge case where the personalization object doesn't exist.
3276
+ */
3277
+ async function toggleUserMemories(userId, memoriesEnabled) {
3278
+ const User = mongoose.models.User;
3279
+ // First, ensure the personalization object exists
3280
+ const user = await User.findById(userId);
3281
+ if (!user) {
3282
+ return null;
3283
+ }
3284
+ // Use $set to update the nested field, which will create the personalization object if it doesn't exist
3285
+ const updateOperation = {
3286
+ $set: {
3287
+ 'personalization.memories': memoriesEnabled,
3288
+ },
3289
+ };
3290
+ return (await User.findByIdAndUpdate(userId, updateOperation, {
3291
+ new: true,
3292
+ runValidators: true,
3293
+ }).lean());
3294
+ }
3295
+ /**
3296
+ * Search for users by pattern matching on name, email, or username (case-insensitive)
3297
+ * @param searchPattern - The pattern to search for
3298
+ * @param limit - Maximum number of results to return
3299
+ * @param fieldsToSelect - The fields to include or exclude in the returned documents
3300
+ * @returns Array of matching user documents
3301
+ */
3302
+ const searchUsers = async function ({ searchPattern, limit = 20, fieldsToSelect = null, }) {
3303
+ if (!searchPattern || searchPattern.trim().length === 0) {
3304
+ return [];
3305
+ }
3306
+ const regex = new RegExp(searchPattern.trim(), 'i');
3307
+ const User = mongoose.models.User;
3308
+ const query = User.find({
3309
+ $or: [{ email: regex }, { name: regex }, { username: regex }],
3310
+ }).limit(limit * 2); // Get more results to allow for relevance sorting
3311
+ if (fieldsToSelect) {
3312
+ query.select(fieldsToSelect);
3313
+ }
3314
+ const users = await query.lean();
3315
+ // Score results by relevance
3316
+ const exactRegex = new RegExp(`^${searchPattern.trim()}$`, 'i');
3317
+ const startsWithPattern = searchPattern.trim().toLowerCase();
3318
+ const scoredUsers = users.map((user) => {
3319
+ const searchableFields = [user.name, user.email, user.username].filter(Boolean);
3320
+ let maxScore = 0;
3321
+ for (const field of searchableFields) {
3322
+ const fieldLower = field.toLowerCase();
3323
+ let score = 0;
3324
+ // Exact match gets highest score
3325
+ if (exactRegex.test(field)) {
3326
+ score = 100;
3327
+ }
3328
+ // Starts with query gets high score
3329
+ else if (fieldLower.startsWith(startsWithPattern)) {
3330
+ score = 80;
3331
+ }
3332
+ // Contains query gets medium score
3333
+ else if (fieldLower.includes(startsWithPattern)) {
3334
+ score = 50;
3335
+ }
3336
+ // Default score for regex match
3337
+ else {
3338
+ score = 10;
3339
+ }
3340
+ maxScore = Math.max(maxScore, score);
3341
+ }
3342
+ return { ...user, _searchScore: maxScore };
3343
+ });
3344
+ /** Top results sorted by relevance */
3345
+ return scoredUsers
3346
+ .sort((a, b) => b._searchScore - a._searchScore)
3347
+ .slice(0, limit)
3348
+ .map((user) => {
3349
+ // Remove the search score from final results
3350
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
3351
+ const { _searchScore, ...userWithoutScore } = user;
3352
+ return userWithoutScore;
3353
+ });
3354
+ };
3355
+ return {
3356
+ findUser,
3357
+ countUsers,
3358
+ createUser,
3359
+ updateUser,
3360
+ searchUsers,
3361
+ getUserById,
3362
+ generateToken,
3363
+ deleteUserById,
3364
+ toggleUserMemories,
3365
+ };
3366
+ }
3367
+
3368
+ /**
3369
+ * Formats a date in YYYY-MM-DD format
3370
+ */
3371
+ const formatDate = (date) => {
3372
+ return date.toISOString().split('T')[0];
3373
+ };
3374
+ // Factory function that takes mongoose instance and returns the methods
3375
+ function createMemoryMethods(mongoose) {
3376
+ /**
3377
+ * Creates a new memory entry for a user
3378
+ * Throws an error if a memory with the same key already exists
3379
+ */
3380
+ async function createMemory({ userId, key, value, tokenCount = 0, }) {
3381
+ try {
3382
+ if ((key === null || key === void 0 ? void 0 : key.toLowerCase()) === 'nothing') {
3383
+ return { ok: false };
3384
+ }
3385
+ const MemoryEntry = mongoose.models.MemoryEntry;
3386
+ const existingMemory = await MemoryEntry.findOne({ userId, key });
3387
+ if (existingMemory) {
3388
+ throw new Error('Memory with this key already exists');
3389
+ }
3390
+ await MemoryEntry.create({
3391
+ userId,
3392
+ key,
3393
+ value,
3394
+ tokenCount,
3395
+ updated_at: new Date(),
3396
+ });
3397
+ return { ok: true };
3398
+ }
3399
+ catch (error) {
3400
+ throw new Error(`Failed to create memory: ${error instanceof Error ? error.message : 'Unknown error'}`);
3401
+ }
3402
+ }
3403
+ /**
3404
+ * Sets or updates a memory entry for a user
3405
+ */
3406
+ async function setMemory({ userId, key, value, tokenCount = 0, }) {
3407
+ try {
3408
+ if ((key === null || key === void 0 ? void 0 : key.toLowerCase()) === 'nothing') {
3409
+ return { ok: false };
3410
+ }
3411
+ const MemoryEntry = mongoose.models.MemoryEntry;
3412
+ await MemoryEntry.findOneAndUpdate({ userId, key }, {
3413
+ value,
3414
+ tokenCount,
3415
+ updated_at: new Date(),
3416
+ }, {
3417
+ upsert: true,
3418
+ new: true,
3419
+ });
3420
+ return { ok: true };
3421
+ }
3422
+ catch (error) {
3423
+ throw new Error(`Failed to set memory: ${error instanceof Error ? error.message : 'Unknown error'}`);
3424
+ }
3425
+ }
3426
+ /**
3427
+ * Deletes a specific memory entry for a user
3428
+ */
3429
+ async function deleteMemory({ userId, key }) {
3430
+ try {
3431
+ const MemoryEntry = mongoose.models.MemoryEntry;
3432
+ const result = await MemoryEntry.findOneAndDelete({ userId, key });
3433
+ return { ok: !!result };
3434
+ }
3435
+ catch (error) {
3436
+ throw new Error(`Failed to delete memory: ${error instanceof Error ? error.message : 'Unknown error'}`);
3437
+ }
3438
+ }
3439
+ /**
3440
+ * Gets all memory entries for a user
3441
+ */
3442
+ async function getAllUserMemories(userId) {
3443
+ try {
3444
+ const MemoryEntry = mongoose.models.MemoryEntry;
3445
+ return (await MemoryEntry.find({ userId }).lean());
3446
+ }
3447
+ catch (error) {
3448
+ throw new Error(`Failed to get all memories: ${error instanceof Error ? error.message : 'Unknown error'}`);
3449
+ }
3450
+ }
3451
+ /**
3452
+ * Gets and formats all memories for a user in two different formats
3453
+ */
3454
+ async function getFormattedMemories({ userId, }) {
3455
+ try {
3456
+ const memories = await getAllUserMemories(userId);
3457
+ if (!memories || memories.length === 0) {
3458
+ return { withKeys: '', withoutKeys: '', totalTokens: 0 };
3459
+ }
3460
+ const sortedMemories = memories.sort((a, b) => new Date(a.updated_at).getTime() - new Date(b.updated_at).getTime());
3461
+ const totalTokens = sortedMemories.reduce((sum, memory) => {
3462
+ return sum + (memory.tokenCount || 0);
3463
+ }, 0);
3464
+ const withKeys = sortedMemories
3465
+ .map((memory, index) => {
3466
+ const date = formatDate(new Date(memory.updated_at));
3467
+ const tokenInfo = memory.tokenCount ? ` [${memory.tokenCount} tokens]` : '';
3468
+ return `${index + 1}. [${date}]. ["key": "${memory.key}"]${tokenInfo}. ["value": "${memory.value}"]`;
3469
+ })
3470
+ .join('\n\n');
3471
+ const withoutKeys = sortedMemories
3472
+ .map((memory, index) => {
3473
+ const date = formatDate(new Date(memory.updated_at));
3474
+ return `${index + 1}. [${date}]. ${memory.value}`;
3475
+ })
3476
+ .join('\n\n');
3477
+ return { withKeys, withoutKeys, totalTokens };
3478
+ }
3479
+ catch (error) {
3480
+ logger$1.error('Failed to get formatted memories:', error);
3481
+ return { withKeys: '', withoutKeys: '', totalTokens: 0 };
3482
+ }
3483
+ }
3484
+ return {
3485
+ setMemory,
3486
+ createMemory,
3487
+ deleteMemory,
3488
+ getAllUserMemories,
3489
+ getFormattedMemories,
3490
+ };
3491
+ }
3492
+
3493
+ function createAgentCategoryMethods(mongoose) {
3494
+ /**
3495
+ * Get all active categories sorted by order
3496
+ * @returns Array of active categories
3497
+ */
3498
+ async function getActiveCategories() {
3499
+ const AgentCategory = mongoose.models.AgentCategory;
3500
+ return await AgentCategory.find({ isActive: true }).sort({ order: 1, label: 1 }).lean();
3501
+ }
3502
+ /**
3503
+ * Get categories with agent counts
3504
+ * @returns Categories with agent counts
3505
+ */
3506
+ async function getCategoriesWithCounts() {
3507
+ const Agent = mongoose.models.Agent;
3508
+ const categoryCounts = await Agent.aggregate([
3509
+ { $match: { category: { $exists: true, $ne: null } } },
3510
+ { $group: { _id: '$category', count: { $sum: 1 } } },
3511
+ ]);
3512
+ const countMap = new Map(categoryCounts.map((c) => [c._id, c.count]));
3513
+ const categories = await getActiveCategories();
3514
+ return categories.map((category) => ({
3515
+ ...category,
3516
+ agentCount: countMap.get(category.value) || 0,
3517
+ }));
3518
+ }
3519
+ /**
3520
+ * Get valid category values for Agent model validation
3521
+ * @returns Array of valid category values
3522
+ */
3523
+ async function getValidCategoryValues() {
3524
+ const AgentCategory = mongoose.models.AgentCategory;
3525
+ return await AgentCategory.find({ isActive: true }).distinct('value').lean();
3526
+ }
3527
+ /**
3528
+ * Seed initial categories from existing constants
3529
+ * @param categories - Array of category data to seed
3530
+ * @returns Bulk write result
3531
+ */
3532
+ async function seedCategories(categories) {
3533
+ const AgentCategory = mongoose.models.AgentCategory;
3534
+ const operations = categories.map((category, index) => ({
3535
+ updateOne: {
3536
+ filter: { value: category.value },
3537
+ update: {
3538
+ $setOnInsert: {
3539
+ value: category.value,
3540
+ label: category.label || category.value,
3541
+ description: category.description || '',
3542
+ order: category.order || index,
3543
+ isActive: true,
3544
+ },
3545
+ },
3546
+ upsert: true,
3547
+ },
3548
+ }));
3549
+ return await AgentCategory.bulkWrite(operations);
2215
3550
  }
2216
3551
  /**
2217
- * Update a user's personalization memories setting.
2218
- * Handles the edge case where the personalization object doesn't exist.
3552
+ * Find a category by value
3553
+ * @param value - The category value to search for
3554
+ * @returns The category document or null
2219
3555
  */
2220
- async function toggleUserMemories(userId, memoriesEnabled) {
2221
- const User = mongoose.models.User;
2222
- // First, ensure the personalization object exists
2223
- const user = await User.findById(userId);
2224
- if (!user) {
2225
- return null;
3556
+ async function findCategoryByValue(value) {
3557
+ const AgentCategory = mongoose.models.AgentCategory;
3558
+ return await AgentCategory.findOne({ value }).lean();
3559
+ }
3560
+ /**
3561
+ * Create a new category
3562
+ * @param categoryData - The category data to create
3563
+ * @returns The created category
3564
+ */
3565
+ async function createCategory(categoryData) {
3566
+ const AgentCategory = mongoose.models.AgentCategory;
3567
+ const category = await AgentCategory.create(categoryData);
3568
+ return category.toObject();
3569
+ }
3570
+ /**
3571
+ * Update a category by value
3572
+ * @param value - The category value to update
3573
+ * @param updateData - The data to update
3574
+ * @returns The updated category or null
3575
+ */
3576
+ async function updateCategory(value, updateData) {
3577
+ const AgentCategory = mongoose.models.AgentCategory;
3578
+ return await AgentCategory.findOneAndUpdate({ value }, { $set: updateData }, { new: true, runValidators: true }).lean();
3579
+ }
3580
+ /**
3581
+ * Delete a category by value
3582
+ * @param value - The category value to delete
3583
+ * @returns Whether the deletion was successful
3584
+ */
3585
+ async function deleteCategory(value) {
3586
+ const AgentCategory = mongoose.models.AgentCategory;
3587
+ const result = await AgentCategory.deleteOne({ value });
3588
+ return result.deletedCount > 0;
3589
+ }
3590
+ /**
3591
+ * Find a category by ID
3592
+ * @param id - The category ID to search for
3593
+ * @returns The category document or null
3594
+ */
3595
+ async function findCategoryById(id) {
3596
+ const AgentCategory = mongoose.models.AgentCategory;
3597
+ return await AgentCategory.findById(id).lean();
3598
+ }
3599
+ /**
3600
+ * Get all categories (active and inactive)
3601
+ * @returns Array of all categories
3602
+ */
3603
+ async function getAllCategories() {
3604
+ const AgentCategory = mongoose.models.AgentCategory;
3605
+ return await AgentCategory.find({}).sort({ order: 1, label: 1 }).lean();
3606
+ }
3607
+ /**
3608
+ * Ensure default categories exist, seed them if none are present
3609
+ * @returns Promise<boolean> - true if categories were seeded, false if they already existed
3610
+ */
3611
+ async function ensureDefaultCategories() {
3612
+ const existingCategories = await getAllCategories();
3613
+ if (existingCategories.length > 0) {
3614
+ return false; // Categories already exist
2226
3615
  }
2227
- // Use $set to update the nested field, which will create the personalization object if it doesn't exist
2228
- const updateOperation = {
2229
- $set: {
2230
- 'personalization.memories': memoriesEnabled,
3616
+ const defaultCategories = [
3617
+ {
3618
+ value: 'general',
3619
+ label: 'General',
3620
+ description: 'General purpose agents for common tasks and inquiries',
3621
+ order: 0,
2231
3622
  },
2232
- };
2233
- return (await User.findByIdAndUpdate(userId, updateOperation, {
2234
- new: true,
2235
- runValidators: true,
2236
- }).lean());
3623
+ {
3624
+ value: 'hr',
3625
+ label: 'Human Resources',
3626
+ description: 'Agents specialized in HR processes, policies, and employee support',
3627
+ order: 1,
3628
+ },
3629
+ {
3630
+ value: 'rd',
3631
+ label: 'Research & Development',
3632
+ description: 'Agents focused on R&D processes, innovation, and technical research',
3633
+ order: 2,
3634
+ },
3635
+ {
3636
+ value: 'finance',
3637
+ label: 'Finance',
3638
+ description: 'Agents specialized in financial analysis, budgeting, and accounting',
3639
+ order: 3,
3640
+ },
3641
+ {
3642
+ value: 'it',
3643
+ label: 'IT',
3644
+ description: 'Agents for IT support, technical troubleshooting, and system administration',
3645
+ order: 4,
3646
+ },
3647
+ {
3648
+ value: 'sales',
3649
+ label: 'Sales',
3650
+ description: 'Agents focused on sales processes, customer relations.',
3651
+ order: 5,
3652
+ },
3653
+ {
3654
+ value: 'aftersales',
3655
+ label: 'After Sales',
3656
+ description: 'Agents specialized in post-sale support, maintenance, and customer service',
3657
+ order: 6,
3658
+ },
3659
+ ];
3660
+ await seedCategories(defaultCategories);
3661
+ return true; // Categories were seeded
2237
3662
  }
2238
- // Return all methods
2239
3663
  return {
2240
- findUser,
2241
- countUsers,
2242
- createUser,
2243
- updateUser,
2244
- getUserById,
2245
- deleteUserById,
2246
- generateToken,
2247
- toggleUserMemories,
3664
+ getActiveCategories,
3665
+ getCategoriesWithCounts,
3666
+ getValidCategoryValues,
3667
+ seedCategories,
3668
+ findCategoryByValue,
3669
+ createCategory,
3670
+ updateCategory,
3671
+ deleteCategory,
3672
+ findCategoryById,
3673
+ getAllCategories,
3674
+ ensureDefaultCategories,
2248
3675
  };
2249
3676
  }
2250
3677
 
2251
- /**
2252
- * Wrapper for the traverse module to handle CommonJS imports in ESM context
2253
- */
2254
- // @ts-ignore - traverse doesn't have proper ESM exports
2255
- // Handle both default export and named exports
2256
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
2257
- const traverse = (traverseLib.default || traverseLib);
2258
-
2259
- const SPLAT_SYMBOL = Symbol.for('splat');
2260
- const MESSAGE_SYMBOL = Symbol.for('message');
2261
- const CONSOLE_JSON_STRING_LENGTH = parseInt(process.env.CONSOLE_JSON_STRING_LENGTH || '', 10) || 255;
2262
- const sensitiveKeys = [
2263
- /^(sk-)[^\s]+/, // OpenAI API key pattern
2264
- /(Bearer )[^\s]+/, // Header: Bearer token pattern
2265
- /(api-key:? )[^\s]+/, // Header: API key pattern
2266
- /(key=)[^\s]+/, // URL query param: sensitive key pattern (Google)
2267
- ];
2268
- /**
2269
- * Determines if a given value string is sensitive and returns matching regex patterns.
2270
- *
2271
- * @param valueStr - The value string to check.
2272
- * @returns An array of regex patterns that match the value string.
2273
- */
2274
- function getMatchingSensitivePatterns(valueStr) {
2275
- if (valueStr) {
2276
- // Filter and return all regex patterns that match the value string
2277
- return sensitiveKeys.filter((regex) => regex.test(valueStr));
3678
+ // Factory function that takes mongoose instance and returns the methods
3679
+ function createPluginAuthMethods(mongoose) {
3680
+ /**
3681
+ * Finds a single plugin auth entry by userId and authField (and optionally pluginKey)
3682
+ */
3683
+ async function findOnePluginAuth({ userId, authField, pluginKey, }) {
3684
+ try {
3685
+ const PluginAuth = mongoose.models.PluginAuth;
3686
+ return await PluginAuth.findOne({
3687
+ userId,
3688
+ authField,
3689
+ ...(pluginKey && { pluginKey }),
3690
+ }).lean();
3691
+ }
3692
+ catch (error) {
3693
+ throw new Error(`Failed to find plugin auth: ${error instanceof Error ? error.message : 'Unknown error'}`);
3694
+ }
2278
3695
  }
2279
- return [];
3696
+ /**
3697
+ * Finds multiple plugin auth entries by userId and pluginKeys
3698
+ */
3699
+ async function findPluginAuthsByKeys({ userId, pluginKeys, }) {
3700
+ try {
3701
+ if (!pluginKeys || pluginKeys.length === 0) {
3702
+ return [];
3703
+ }
3704
+ const PluginAuth = mongoose.models.PluginAuth;
3705
+ return await PluginAuth.find({
3706
+ userId,
3707
+ pluginKey: { $in: pluginKeys },
3708
+ }).lean();
3709
+ }
3710
+ catch (error) {
3711
+ throw new Error(`Failed to find plugin auths: ${error instanceof Error ? error.message : 'Unknown error'}`);
3712
+ }
3713
+ }
3714
+ /**
3715
+ * Updates or creates a plugin auth entry
3716
+ */
3717
+ async function updatePluginAuth({ userId, authField, pluginKey, value, }) {
3718
+ try {
3719
+ const PluginAuth = mongoose.models.PluginAuth;
3720
+ const existingAuth = await PluginAuth.findOne({ userId, pluginKey, authField }).lean();
3721
+ if (existingAuth) {
3722
+ return await PluginAuth.findOneAndUpdate({ userId, pluginKey, authField }, { $set: { value } }, { new: true, upsert: true }).lean();
3723
+ }
3724
+ else {
3725
+ const newPluginAuth = await new PluginAuth({
3726
+ userId,
3727
+ authField,
3728
+ value,
3729
+ pluginKey,
3730
+ });
3731
+ await newPluginAuth.save();
3732
+ return newPluginAuth.toObject();
3733
+ }
3734
+ }
3735
+ catch (error) {
3736
+ throw new Error(`Failed to update plugin auth: ${error instanceof Error ? error.message : 'Unknown error'}`);
3737
+ }
3738
+ }
3739
+ /**
3740
+ * Deletes plugin auth entries based on provided parameters
3741
+ */
3742
+ async function deletePluginAuth({ userId, authField, pluginKey, all = false, }) {
3743
+ try {
3744
+ const PluginAuth = mongoose.models.PluginAuth;
3745
+ if (all) {
3746
+ const filter = { userId };
3747
+ if (pluginKey) {
3748
+ filter.pluginKey = pluginKey;
3749
+ }
3750
+ return await PluginAuth.deleteMany(filter);
3751
+ }
3752
+ if (!authField) {
3753
+ throw new Error('authField is required when all is false');
3754
+ }
3755
+ return await PluginAuth.deleteOne({ userId, authField });
3756
+ }
3757
+ catch (error) {
3758
+ throw new Error(`Failed to delete plugin auth: ${error instanceof Error ? error.message : 'Unknown error'}`);
3759
+ }
3760
+ }
3761
+ /**
3762
+ * Deletes all plugin auth entries for a user
3763
+ */
3764
+ async function deleteAllUserPluginAuths(userId) {
3765
+ try {
3766
+ const PluginAuth = mongoose.models.PluginAuth;
3767
+ return await PluginAuth.deleteMany({ userId });
3768
+ }
3769
+ catch (error) {
3770
+ throw new Error(`Failed to delete all user plugin auths: ${error instanceof Error ? error.message : 'Unknown error'}`);
3771
+ }
3772
+ }
3773
+ return {
3774
+ findOnePluginAuth,
3775
+ findPluginAuthsByKeys,
3776
+ updatePluginAuth,
3777
+ deletePluginAuth,
3778
+ deleteAllUserPluginAuths,
3779
+ };
2280
3780
  }
2281
- /**
2282
- * Redacts sensitive information from a console message and trims it to a specified length if provided.
2283
- * @param str - The console message to be redacted.
2284
- * @param trimLength - The optional length at which to trim the redacted message.
2285
- * @returns The redacted and optionally trimmed console message.
2286
- */
2287
- function redactMessage(str, trimLength) {
2288
- if (!str) {
2289
- return '';
3781
+
3782
+ function createAccessRoleMethods(mongoose) {
3783
+ /**
3784
+ * Find an access role by its ID
3785
+ * @param roleId - The role ID
3786
+ * @returns The role document or null if not found
3787
+ */
3788
+ async function findRoleById(roleId) {
3789
+ const AccessRole = mongoose.models.AccessRole;
3790
+ return await AccessRole.findById(roleId).lean();
3791
+ }
3792
+ /**
3793
+ * Find an access role by its unique identifier
3794
+ * @param accessRoleId - The unique identifier (e.g., "agent_viewer")
3795
+ * @returns The role document or null if not found
3796
+ */
3797
+ async function findRoleByIdentifier(accessRoleId) {
3798
+ const AccessRole = mongoose.models.AccessRole;
3799
+ return await AccessRole.findOne({ accessRoleId }).lean();
3800
+ }
3801
+ /**
3802
+ * Find all access roles for a specific resource type
3803
+ * @param resourceType - The type of resource ('agent', 'project', 'file')
3804
+ * @returns Array of role documents
3805
+ */
3806
+ async function findRolesByResourceType(resourceType) {
3807
+ const AccessRole = mongoose.models.AccessRole;
3808
+ return await AccessRole.find({ resourceType }).lean();
3809
+ }
3810
+ /**
3811
+ * Find an access role by resource type and permission bits
3812
+ * @param resourceType - The type of resource
3813
+ * @param permBits - The permission bits (use PermissionBits or RoleBits enum)
3814
+ * @returns The role document or null if not found
3815
+ */
3816
+ async function findRoleByPermissions(resourceType, permBits) {
3817
+ const AccessRole = mongoose.models.AccessRole;
3818
+ return await AccessRole.findOne({ resourceType, permBits }).lean();
3819
+ }
3820
+ /**
3821
+ * Create a new access role
3822
+ * @param roleData - Role data (accessRoleId, name, description, resourceType, permBits)
3823
+ * @returns The created role document
3824
+ */
3825
+ async function createRole(roleData) {
3826
+ const AccessRole = mongoose.models.AccessRole;
3827
+ return await AccessRole.create(roleData);
2290
3828
  }
2291
- const patterns = getMatchingSensitivePatterns(str);
2292
- patterns.forEach((pattern) => {
2293
- str = str.replace(pattern, '$1[REDACTED]');
2294
- });
2295
- return str;
2296
- }
2297
- /**
2298
- * Redacts sensitive information from log messages if the log level is 'error'.
2299
- * Note: Intentionally mutates the object.
2300
- * @param info - The log information object.
2301
- * @returns The modified log information object.
2302
- */
2303
- const redactFormat = winston.format((info) => {
2304
- if (info.level === 'error') {
2305
- // Type guard to ensure message is a string
2306
- if (typeof info.message === 'string') {
2307
- info.message = redactMessage(info.message);
2308
- }
2309
- // Handle MESSAGE_SYMBOL with type safety
2310
- const symbolValue = info[MESSAGE_SYMBOL];
2311
- if (typeof symbolValue === 'string') {
2312
- info[MESSAGE_SYMBOL] = redactMessage(symbolValue);
2313
- }
3829
+ /**
3830
+ * Update an existing access role
3831
+ * @param accessRoleId - The unique identifier of the role to update
3832
+ * @param updateData - Data to update
3833
+ * @returns The updated role document or null if not found
3834
+ */
3835
+ async function updateRole(accessRoleId, updateData) {
3836
+ const AccessRole = mongoose.models.AccessRole;
3837
+ return await AccessRole.findOneAndUpdate({ accessRoleId }, { $set: updateData }, { new: true }).lean();
2314
3838
  }
2315
- return info;
2316
- });
2317
- /**
2318
- * Truncates long strings, especially base64 image data, within log messages.
2319
- *
2320
- * @param value - The value to be inspected and potentially truncated.
2321
- * @param length - The length at which to truncate the value. Default: 100.
2322
- * @returns The truncated or original value.
2323
- */
2324
- const truncateLongStrings = (value, length = 100) => {
2325
- if (typeof value === 'string') {
2326
- return value.length > length ? value.substring(0, length) + '... [truncated]' : value;
3839
+ /**
3840
+ * Delete an access role
3841
+ * @param accessRoleId - The unique identifier of the role to delete
3842
+ * @returns The result of the delete operation
3843
+ */
3844
+ async function deleteRole(accessRoleId) {
3845
+ const AccessRole = mongoose.models.AccessRole;
3846
+ return await AccessRole.deleteOne({ accessRoleId });
2327
3847
  }
2328
- return value;
2329
- };
2330
- /**
2331
- * An array mapping function that truncates long strings (objects converted to JSON strings).
2332
- * @param item - The item to be condensed.
2333
- * @returns The condensed item.
2334
- */
2335
- const condenseArray = (item) => {
2336
- if (typeof item === 'string') {
2337
- return truncateLongStrings(JSON.stringify(item));
3848
+ /**
3849
+ * Get all predefined roles
3850
+ * @returns Array of all role documents
3851
+ */
3852
+ async function getAllRoles() {
3853
+ const AccessRole = mongoose.models.AccessRole;
3854
+ return await AccessRole.find().lean();
2338
3855
  }
2339
- else if (typeof item === 'object') {
2340
- return truncateLongStrings(JSON.stringify(item));
3856
+ /**
3857
+ * Seed default roles if they don't exist
3858
+ * @returns Object containing created roles
3859
+ */
3860
+ async function seedDefaultRoles() {
3861
+ const AccessRole = mongoose.models.AccessRole;
3862
+ const defaultRoles = [
3863
+ {
3864
+ accessRoleId: AccessRoleIds.AGENT_VIEWER,
3865
+ name: 'com_ui_role_viewer',
3866
+ description: 'com_ui_role_viewer_desc',
3867
+ resourceType: ResourceType.AGENT,
3868
+ permBits: RoleBits.VIEWER,
3869
+ },
3870
+ {
3871
+ accessRoleId: AccessRoleIds.AGENT_EDITOR,
3872
+ name: 'com_ui_role_editor',
3873
+ description: 'com_ui_role_editor_desc',
3874
+ resourceType: ResourceType.AGENT,
3875
+ permBits: RoleBits.EDITOR,
3876
+ },
3877
+ {
3878
+ accessRoleId: AccessRoleIds.AGENT_OWNER,
3879
+ name: 'com_ui_role_owner',
3880
+ description: 'com_ui_role_owner_desc',
3881
+ resourceType: ResourceType.AGENT,
3882
+ permBits: RoleBits.OWNER,
3883
+ },
3884
+ {
3885
+ accessRoleId: AccessRoleIds.PROMPTGROUP_VIEWER,
3886
+ name: 'com_ui_role_viewer',
3887
+ description: 'com_ui_role_viewer_desc',
3888
+ resourceType: ResourceType.PROMPTGROUP,
3889
+ permBits: RoleBits.VIEWER,
3890
+ },
3891
+ {
3892
+ accessRoleId: AccessRoleIds.PROMPTGROUP_EDITOR,
3893
+ name: 'com_ui_role_editor',
3894
+ description: 'com_ui_role_editor_desc',
3895
+ resourceType: ResourceType.PROMPTGROUP,
3896
+ permBits: RoleBits.EDITOR,
3897
+ },
3898
+ {
3899
+ accessRoleId: AccessRoleIds.PROMPTGROUP_OWNER,
3900
+ name: 'com_ui_role_owner',
3901
+ description: 'com_ui_role_owner_desc',
3902
+ resourceType: ResourceType.PROMPTGROUP,
3903
+ permBits: RoleBits.OWNER,
3904
+ },
3905
+ ];
3906
+ const result = {};
3907
+ for (const role of defaultRoles) {
3908
+ const upsertedRole = await AccessRole.findOneAndUpdate({ accessRoleId: role.accessRoleId }, { $setOnInsert: role }, { upsert: true, new: true }).lean();
3909
+ result[role.accessRoleId] = upsertedRole;
3910
+ }
3911
+ return result;
2341
3912
  }
2342
- return item;
2343
- };
2344
- /**
2345
- * Formats log messages for debugging purposes.
2346
- * - Truncates long strings within log messages.
2347
- * - Condenses arrays by truncating long strings and objects as strings within array items.
2348
- * - Redacts sensitive information from log messages if the log level is 'error'.
2349
- * - Converts log information object to a formatted string.
2350
- *
2351
- * @param options - The options for formatting log messages.
2352
- * @returns The formatted log message.
2353
- */
2354
- const debugTraverse = winston.format.printf(({ level, message, timestamp, ...metadata }) => {
2355
- if (!message) {
2356
- return `${timestamp} ${level}`;
3913
+ /**
3914
+ * Helper to get the appropriate role for a set of permissions
3915
+ * @param resourceType - The type of resource
3916
+ * @param permBits - The permission bits
3917
+ * @returns The matching role or null if none found
3918
+ */
3919
+ async function getRoleForPermissions(resourceType, permBits) {
3920
+ const AccessRole = mongoose.models.AccessRole;
3921
+ const exactMatch = await AccessRole.findOne({ resourceType, permBits }).lean();
3922
+ if (exactMatch) {
3923
+ return exactMatch;
3924
+ }
3925
+ /** If no exact match, the closest role without exceeding permissions */
3926
+ const roles = await AccessRole.find({ resourceType }).sort({ permBits: -1 }).lean();
3927
+ return roles.find((role) => (role.permBits & permBits) === role.permBits) || null;
2357
3928
  }
2358
- // Type-safe version of the CJS logic: !message?.trim || typeof message !== 'string'
2359
- if (typeof message !== 'string' || !message.trim) {
2360
- return `${timestamp} ${level}: ${JSON.stringify(message)}`;
3929
+ return {
3930
+ createRole,
3931
+ updateRole,
3932
+ deleteRole,
3933
+ getAllRoles,
3934
+ findRoleById,
3935
+ seedDefaultRoles,
3936
+ findRoleByIdentifier,
3937
+ getRoleForPermissions,
3938
+ findRoleByPermissions,
3939
+ findRolesByResourceType,
3940
+ };
3941
+ }
3942
+
3943
+ function createUserGroupMethods(mongoose) {
3944
+ /**
3945
+ * Find a group by its ID
3946
+ * @param groupId - The group ID
3947
+ * @param projection - Optional projection of fields to return
3948
+ * @param session - Optional MongoDB session for transactions
3949
+ * @returns The group document or null if not found
3950
+ */
3951
+ async function findGroupById(groupId, projection = {}, session) {
3952
+ const Group = mongoose.models.Group;
3953
+ const query = Group.findOne({ _id: groupId }, projection);
3954
+ if (session) {
3955
+ query.session(session);
3956
+ }
3957
+ return await query.lean();
2361
3958
  }
2362
- let msg = `${timestamp} ${level}: ${truncateLongStrings(message.trim(), 150)}`;
2363
- try {
2364
- if (level !== 'debug') {
2365
- return msg;
3959
+ /**
3960
+ * Find a group by its external ID (e.g., Entra ID)
3961
+ * @param idOnTheSource - The external ID
3962
+ * @param source - The source ('entra' or 'local')
3963
+ * @param projection - Optional projection of fields to return
3964
+ * @param session - Optional MongoDB session for transactions
3965
+ * @returns The group document or null if not found
3966
+ */
3967
+ async function findGroupByExternalId(idOnTheSource, source = 'entra', projection = {}, session) {
3968
+ const Group = mongoose.models.Group;
3969
+ const query = Group.findOne({ idOnTheSource, source }, projection);
3970
+ if (session) {
3971
+ query.session(session);
2366
3972
  }
2367
- if (!metadata) {
2368
- return msg;
3973
+ return await query.lean();
3974
+ }
3975
+ /**
3976
+ * Find groups by name pattern (case-insensitive partial match)
3977
+ * @param namePattern - The name pattern to search for
3978
+ * @param source - Optional source filter ('entra', 'local', or null for all)
3979
+ * @param limit - Maximum number of results to return
3980
+ * @param session - Optional MongoDB session for transactions
3981
+ * @returns Array of matching groups
3982
+ */
3983
+ async function findGroupsByNamePattern(namePattern, source = null, limit = 20, session) {
3984
+ const Group = mongoose.models.Group;
3985
+ const regex = new RegExp(namePattern, 'i');
3986
+ const query = {
3987
+ $or: [{ name: regex }, { email: regex }, { description: regex }],
3988
+ };
3989
+ if (source) {
3990
+ query.source = source;
2369
3991
  }
2370
- // Type-safe access to SPLAT_SYMBOL using bracket notation
2371
- const metadataRecord = metadata;
2372
- const splatArray = metadataRecord[SPLAT_SYMBOL];
2373
- const debugValue = Array.isArray(splatArray) ? splatArray[0] : undefined;
2374
- if (!debugValue) {
2375
- return msg;
3992
+ const dbQuery = Group.find(query).limit(limit);
3993
+ if (session) {
3994
+ dbQuery.session(session);
2376
3995
  }
2377
- if (debugValue && Array.isArray(debugValue)) {
2378
- msg += `\n${JSON.stringify(debugValue.map(condenseArray))}`;
2379
- return msg;
3996
+ return await dbQuery.lean();
3997
+ }
3998
+ /**
3999
+ * Find all groups a user is a member of by their ID or idOnTheSource
4000
+ * @param userId - The user ID
4001
+ * @param session - Optional MongoDB session for transactions
4002
+ * @returns Array of groups the user is a member of
4003
+ */
4004
+ async function findGroupsByMemberId(userId, session) {
4005
+ const User = mongoose.models.User;
4006
+ const Group = mongoose.models.Group;
4007
+ const userQuery = User.findById(userId, 'idOnTheSource');
4008
+ if (session) {
4009
+ userQuery.session(session);
2380
4010
  }
2381
- if (typeof debugValue !== 'object') {
2382
- return (msg += ` ${debugValue}`);
4011
+ const user = (await userQuery.lean());
4012
+ if (!user) {
4013
+ return [];
2383
4014
  }
2384
- msg += '\n{';
2385
- const copy = klona(metadata);
2386
- traverse(copy).forEach(function (value) {
2387
- var _a;
2388
- if (typeof (this === null || this === void 0 ? void 0 : this.key) === 'symbol') {
2389
- return;
2390
- }
2391
- let _parentKey = '';
2392
- const parent = this.parent;
2393
- if (typeof (parent === null || parent === void 0 ? void 0 : parent.key) !== 'symbol' && (parent === null || parent === void 0 ? void 0 : parent.key)) {
2394
- _parentKey = parent.key;
2395
- }
2396
- const parentKey = `${parent && parent.notRoot ? _parentKey + '.' : ''}`;
2397
- const tabs = `${parent && parent.notRoot ? ' ' : ' '}`;
2398
- const currentKey = (_a = this === null || this === void 0 ? void 0 : this.key) !== null && _a !== void 0 ? _a : 'unknown';
2399
- if (this.isLeaf && typeof value === 'string') {
2400
- const truncatedText = truncateLongStrings(value);
2401
- msg += `\n${tabs}${parentKey}${currentKey}: ${JSON.stringify(truncatedText)},`;
2402
- }
2403
- else if (this.notLeaf && Array.isArray(value) && value.length > 0) {
2404
- const currentMessage = `\n${tabs}// ${value.length} ${currentKey.replace(/s$/, '')}(s)`;
2405
- this.update(currentMessage, true);
2406
- msg += currentMessage;
2407
- const stringifiedArray = value.map(condenseArray);
2408
- msg += `\n${tabs}${parentKey}${currentKey}: [${stringifiedArray}],`;
2409
- }
2410
- else if (this.isLeaf && typeof value === 'function') {
2411
- msg += `\n${tabs}${parentKey}${currentKey}: function,`;
2412
- }
2413
- else if (this.isLeaf) {
2414
- msg += `\n${tabs}${parentKey}${currentKey}: ${value},`;
2415
- }
2416
- });
2417
- msg += '\n}';
2418
- return msg;
4015
+ const userIdOnTheSource = user.idOnTheSource || userId.toString();
4016
+ const query = Group.find({ memberIds: userIdOnTheSource });
4017
+ if (session) {
4018
+ query.session(session);
4019
+ }
4020
+ return await query.lean();
2419
4021
  }
2420
- catch (e) {
2421
- const errorMessage = e instanceof Error ? e.message : 'Unknown error';
2422
- return (msg += `\n[LOGGER PARSING ERROR] ${errorMessage}`);
4022
+ /**
4023
+ * Create a new group
4024
+ * @param groupData - Group data including name, source, and optional idOnTheSource
4025
+ * @param session - Optional MongoDB session for transactions
4026
+ * @returns The created group
4027
+ */
4028
+ async function createGroup(groupData, session) {
4029
+ const Group = mongoose.models.Group;
4030
+ const options = session ? { session } : {};
4031
+ return await Group.create([groupData], options).then((groups) => groups[0]);
2423
4032
  }
2424
- });
2425
- /**
2426
- * Truncates long string values in JSON log objects.
2427
- * Prevents outputting extremely long values (e.g., base64, blobs).
2428
- */
2429
- const jsonTruncateFormat = winston.format((info) => {
2430
- const truncateLongStrings = (str, maxLength) => str.length > maxLength ? str.substring(0, maxLength) + '...' : str;
2431
- const seen = new WeakSet();
2432
- const truncateObject = (obj) => {
2433
- if (typeof obj !== 'object' || obj === null) {
2434
- return obj;
2435
- }
2436
- // Handle circular references - now with proper object type
2437
- if (seen.has(obj)) {
2438
- return '[Circular]';
4033
+ /**
4034
+ * Update or create a group by external ID
4035
+ * @param idOnTheSource - The external ID
4036
+ * @param source - The source ('entra' or 'local')
4037
+ * @param updateData - Data to update or set if creating
4038
+ * @param session - Optional MongoDB session for transactions
4039
+ * @returns The updated or created group
4040
+ */
4041
+ async function upsertGroupByExternalId(idOnTheSource, source, updateData, session) {
4042
+ const Group = mongoose.models.Group;
4043
+ const options = {
4044
+ new: true,
4045
+ upsert: true,
4046
+ ...(session ? { session } : {}),
4047
+ };
4048
+ return await Group.findOneAndUpdate({ idOnTheSource, source }, { $set: updateData }, options);
4049
+ }
4050
+ /**
4051
+ * Add a user to a group
4052
+ * Only updates Group.memberIds (one-way relationship)
4053
+ * Note: memberIds stores idOnTheSource values, not ObjectIds
4054
+ *
4055
+ * @param userId - The user ID
4056
+ * @param groupId - The group ID to add
4057
+ * @param session - Optional MongoDB session for transactions
4058
+ * @returns The user and updated group documents
4059
+ */
4060
+ async function addUserToGroup(userId, groupId, session) {
4061
+ const User = mongoose.models.User;
4062
+ const Group = mongoose.models.Group;
4063
+ const options = { new: true, ...(session ? { session } : {}) };
4064
+ const user = (await User.findById(userId, 'idOnTheSource', options).lean());
4065
+ if (!user) {
4066
+ throw new Error(`User not found: ${userId}`);
2439
4067
  }
2440
- seen.add(obj);
2441
- if (Array.isArray(obj)) {
2442
- return obj.map((item) => truncateObject(item));
4068
+ const userIdOnTheSource = user.idOnTheSource || userId.toString();
4069
+ const updatedGroup = await Group.findByIdAndUpdate(groupId, { $addToSet: { memberIds: userIdOnTheSource } }, options).lean();
4070
+ return { user: user, group: updatedGroup };
4071
+ }
4072
+ /**
4073
+ * Remove a user from a group
4074
+ * Only updates Group.memberIds (one-way relationship)
4075
+ * Note: memberIds stores idOnTheSource values, not ObjectIds
4076
+ *
4077
+ * @param userId - The user ID
4078
+ * @param groupId - The group ID to remove
4079
+ * @param session - Optional MongoDB session for transactions
4080
+ * @returns The user and updated group documents
4081
+ */
4082
+ async function removeUserFromGroup(userId, groupId, session) {
4083
+ const User = mongoose.models.User;
4084
+ const Group = mongoose.models.Group;
4085
+ const options = { new: true, ...(session ? { session } : {}) };
4086
+ const user = (await User.findById(userId, 'idOnTheSource', options).lean());
4087
+ if (!user) {
4088
+ throw new Error(`User not found: ${userId}`);
2443
4089
  }
2444
- // We know this is an object at this point
2445
- const objectRecord = obj;
2446
- const newObj = {};
2447
- Object.entries(objectRecord).forEach(([key, value]) => {
2448
- if (typeof value === 'string') {
2449
- newObj[key] = truncateLongStrings(value, CONSOLE_JSON_STRING_LENGTH);
2450
- }
2451
- else {
2452
- newObj[key] = truncateObject(value);
2453
- }
2454
- });
2455
- return newObj;
2456
- };
2457
- return truncateObject(info);
2458
- });
2459
-
2460
- const logDir = getLogDirectory();
2461
- const { NODE_ENV, DEBUG_LOGGING, CONSOLE_JSON, DEBUG_CONSOLE } = process.env;
2462
- const useConsoleJson = typeof CONSOLE_JSON === 'string' && CONSOLE_JSON.toLowerCase() === 'true';
2463
- const useDebugConsole = typeof DEBUG_CONSOLE === 'string' && DEBUG_CONSOLE.toLowerCase() === 'true';
2464
- const useDebugLogging = typeof DEBUG_LOGGING === 'string' && DEBUG_LOGGING.toLowerCase() === 'true';
2465
- const levels = {
2466
- error: 0,
2467
- warn: 1,
2468
- info: 2,
2469
- http: 3,
2470
- verbose: 4,
2471
- debug: 5,
2472
- activity: 6,
2473
- silly: 7,
2474
- };
2475
- winston.addColors({
2476
- info: 'green',
2477
- warn: 'italic yellow',
2478
- error: 'red',
2479
- debug: 'blue',
2480
- });
2481
- const level = () => {
2482
- const env = NODE_ENV || 'development';
2483
- return env === 'development' ? 'debug' : 'warn';
2484
- };
2485
- const fileFormat = winston.format.combine(redactFormat(), winston.format.timestamp({ format: () => new Date().toISOString() }), winston.format.errors({ stack: true }), winston.format.splat());
2486
- const transports = [
2487
- new winston.transports.DailyRotateFile({
2488
- level: 'error',
2489
- filename: `${logDir}/error-%DATE%.log`,
2490
- datePattern: 'YYYY-MM-DD',
2491
- zippedArchive: true,
2492
- maxSize: '20m',
2493
- maxFiles: '14d',
2494
- format: fileFormat,
2495
- }),
2496
- ];
2497
- if (useDebugLogging) {
2498
- transports.push(new winston.transports.DailyRotateFile({
2499
- level: 'debug',
2500
- filename: `${logDir}/debug-%DATE%.log`,
2501
- datePattern: 'YYYY-MM-DD',
2502
- zippedArchive: true,
2503
- maxSize: '20m',
2504
- maxFiles: '14d',
2505
- format: winston.format.combine(fileFormat, debugTraverse),
2506
- }));
2507
- }
2508
- const consoleFormat = winston.format.combine(redactFormat(), winston.format.colorize({ all: true }), winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), winston.format.printf((info) => {
2509
- const message = `${info.timestamp} ${info.level}: ${info.message}`;
2510
- return info.level.includes('error') ? redactMessage(message) : message;
2511
- }));
2512
- let consoleLogLevel = 'info';
2513
- if (useDebugConsole) {
2514
- consoleLogLevel = 'debug';
2515
- }
2516
- // Add console transport
2517
- if (useDebugConsole) {
2518
- transports.push(new winston.transports.Console({
2519
- level: consoleLogLevel,
2520
- format: useConsoleJson
2521
- ? winston.format.combine(fileFormat, jsonTruncateFormat(), winston.format.json())
2522
- : winston.format.combine(fileFormat, debugTraverse),
2523
- }));
2524
- }
2525
- else if (useConsoleJson) {
2526
- transports.push(new winston.transports.Console({
2527
- level: consoleLogLevel,
2528
- format: winston.format.combine(fileFormat, jsonTruncateFormat(), winston.format.json()),
2529
- }));
2530
- }
2531
- else {
2532
- transports.push(new winston.transports.Console({
2533
- level: consoleLogLevel,
2534
- format: consoleFormat,
2535
- }));
2536
- }
2537
- // Create logger
2538
- const logger = winston.createLogger({
2539
- level: level(),
2540
- levels,
2541
- transports,
2542
- });
2543
-
2544
- var _a;
2545
- class SessionError extends Error {
2546
- constructor(message, code = 'SESSION_ERROR') {
2547
- super(message);
2548
- this.name = 'SessionError';
2549
- this.code = code;
4090
+ const userIdOnTheSource = user.idOnTheSource || userId.toString();
4091
+ const updatedGroup = await Group.findByIdAndUpdate(groupId, { $pull: { memberIds: userIdOnTheSource } }, options).lean();
4092
+ return { user: user, group: updatedGroup };
4093
+ }
4094
+ /**
4095
+ * Get all groups a user is a member of
4096
+ * @param userId - The user ID
4097
+ * @param session - Optional MongoDB session for transactions
4098
+ * @returns Array of group documents
4099
+ */
4100
+ async function getUserGroups(userId, session) {
4101
+ return await findGroupsByMemberId(userId, session);
2550
4102
  }
2551
- }
2552
- const { REFRESH_TOKEN_EXPIRY } = (_a = process.env) !== null && _a !== void 0 ? _a : {};
2553
- const expires = REFRESH_TOKEN_EXPIRY ? eval(REFRESH_TOKEN_EXPIRY) : 1000 * 60 * 60 * 24 * 7; // 7 days default
2554
- // Factory function that takes mongoose instance and returns the methods
2555
- function createSessionMethods(mongoose) {
2556
4103
  /**
2557
- * Creates a new session for a user
4104
+ * Get a list of all principal identifiers for a user (user ID + group IDs + public)
4105
+ * For use in permission checks
4106
+ * @param params - Parameters object
4107
+ * @param params.userId - The user ID
4108
+ * @param params.role - Optional user role (if not provided, will query from DB)
4109
+ * @param session - Optional MongoDB session for transactions
4110
+ * @returns Array of principal objects with type and id
2558
4111
  */
2559
- async function createSession(userId, options = {}) {
2560
- if (!userId) {
2561
- throw new SessionError('User ID is required', 'INVALID_USER_ID');
4112
+ async function getUserPrincipals(params, session) {
4113
+ const { userId, role } = params;
4114
+ /** `userId` must be an `ObjectId` for USER principal since ACL entries store `ObjectId`s */
4115
+ const userObjectId = typeof userId === 'string' ? new Types.ObjectId(userId) : userId;
4116
+ const principals = [
4117
+ { principalType: PrincipalType.USER, principalId: userObjectId },
4118
+ ];
4119
+ // If role is not provided, query user to get it
4120
+ let userRole = role;
4121
+ if (userRole === undefined) {
4122
+ const User = mongoose.models.User;
4123
+ const query = User.findById(userId).select('role');
4124
+ if (session) {
4125
+ query.session(session);
4126
+ }
4127
+ const user = await query.lean();
4128
+ userRole = user === null || user === void 0 ? void 0 : user.role;
2562
4129
  }
2563
- try {
2564
- const Session = mongoose.models.Session;
2565
- const currentSession = new Session({
2566
- user: userId,
2567
- expiration: options.expiration || new Date(Date.now() + expires),
2568
- });
2569
- const refreshToken = await generateRefreshToken(currentSession);
2570
- return { session: currentSession, refreshToken };
4130
+ // Add role as a principal if user has one
4131
+ if (userRole && userRole.trim()) {
4132
+ principals.push({ principalType: PrincipalType.ROLE, principalId: userRole });
2571
4133
  }
2572
- catch (error) {
2573
- logger.error('[createSession] Error creating session:', error);
2574
- throw new SessionError('Failed to create session', 'CREATE_SESSION_FAILED');
4134
+ const userGroups = await getUserGroups(userId, session);
4135
+ if (userGroups && userGroups.length > 0) {
4136
+ userGroups.forEach((group) => {
4137
+ principals.push({ principalType: PrincipalType.GROUP, principalId: group._id });
4138
+ });
2575
4139
  }
4140
+ principals.push({ principalType: PrincipalType.PUBLIC });
4141
+ return principals;
2576
4142
  }
2577
4143
  /**
2578
- * Finds a session by various parameters
4144
+ * Sync a user's Entra ID group memberships
4145
+ * @param userId - The user ID
4146
+ * @param entraGroups - Array of Entra groups with id and name
4147
+ * @param session - Optional MongoDB session for transactions
4148
+ * @returns The updated user with new group memberships
2579
4149
  */
2580
- async function findSession(params, options = { lean: true }) {
2581
- try {
2582
- const Session = mongoose.models.Session;
2583
- const query = {};
2584
- if (!params.refreshToken && !params.userId && !params.sessionId) {
2585
- throw new SessionError('At least one search parameter is required', 'INVALID_SEARCH_PARAMS');
2586
- }
2587
- if (params.refreshToken) {
2588
- const tokenHash = await hashToken(params.refreshToken);
2589
- query.refreshTokenHash = tokenHash;
2590
- }
2591
- if (params.userId) {
2592
- query.user = params.userId;
4150
+ async function syncUserEntraGroups(userId, entraGroups, session) {
4151
+ var _a;
4152
+ const User = mongoose.models.User;
4153
+ const Group = mongoose.models.Group;
4154
+ const query = User.findById(userId, { idOnTheSource: 1 });
4155
+ if (session) {
4156
+ query.session(session);
4157
+ }
4158
+ const user = (await query.lean());
4159
+ if (!user) {
4160
+ throw new Error(`User not found: ${userId}`);
4161
+ }
4162
+ /** Get user's idOnTheSource for storing in group.memberIds */
4163
+ const userIdOnTheSource = user.idOnTheSource || userId.toString();
4164
+ const entraIdMap = new Map();
4165
+ const addedGroups = [];
4166
+ const removedGroups = [];
4167
+ for (const entraGroup of entraGroups) {
4168
+ entraIdMap.set(entraGroup.id, true);
4169
+ let group = await findGroupByExternalId(entraGroup.id, 'entra', {}, session);
4170
+ if (!group) {
4171
+ group = await createGroup({
4172
+ name: entraGroup.name,
4173
+ description: entraGroup.description,
4174
+ email: entraGroup.email,
4175
+ idOnTheSource: entraGroup.id,
4176
+ source: 'entra',
4177
+ memberIds: [userIdOnTheSource],
4178
+ }, session);
4179
+ addedGroups.push(group);
2593
4180
  }
2594
- if (params.sessionId) {
2595
- const sessionId = typeof params.sessionId === 'object' &&
2596
- params.sessionId !== null &&
2597
- 'sessionId' in params.sessionId
2598
- ? params.sessionId.sessionId
2599
- : params.sessionId;
2600
- if (!mongoose.Types.ObjectId.isValid(sessionId)) {
2601
- throw new SessionError('Invalid session ID format', 'INVALID_SESSION_ID');
4181
+ else if (!((_a = group.memberIds) === null || _a === void 0 ? void 0 : _a.includes(userIdOnTheSource))) {
4182
+ const { group: updatedGroup } = await addUserToGroup(userId, group._id, session);
4183
+ if (updatedGroup) {
4184
+ addedGroups.push(updatedGroup);
2602
4185
  }
2603
- query._id = sessionId;
2604
- }
2605
- // Add expiration check to only return valid sessions
2606
- query.expiration = { $gt: new Date() };
2607
- const sessionQuery = Session.findOne(query);
2608
- if (options.lean) {
2609
- return (await sessionQuery.lean());
2610
4186
  }
2611
- return await sessionQuery.exec();
2612
4187
  }
2613
- catch (error) {
2614
- logger.error('[findSession] Error finding session:', error);
2615
- throw new SessionError('Failed to find session', 'FIND_SESSION_FAILED');
4188
+ const groupsQuery = Group.find({ source: 'entra', memberIds: userIdOnTheSource }, { _id: 1, idOnTheSource: 1 });
4189
+ if (session) {
4190
+ groupsQuery.session(session);
2616
4191
  }
2617
- }
2618
- /**
2619
- * Updates session expiration
2620
- */
2621
- async function updateExpiration(session, newExpiration) {
2622
- try {
2623
- const Session = mongoose.models.Session;
2624
- const sessionDoc = typeof session === 'string' ? await Session.findById(session) : session;
2625
- if (!sessionDoc) {
2626
- throw new SessionError('Session not found', 'SESSION_NOT_FOUND');
4192
+ const existingGroups = (await groupsQuery.lean());
4193
+ for (const group of existingGroups) {
4194
+ if (group.idOnTheSource && !entraIdMap.has(group.idOnTheSource)) {
4195
+ const { group: removedGroup } = await removeUserFromGroup(userId, group._id, session);
4196
+ if (removedGroup) {
4197
+ removedGroups.push(removedGroup);
4198
+ }
2627
4199
  }
2628
- sessionDoc.expiration = newExpiration || new Date(Date.now() + expires);
2629
- return await sessionDoc.save();
2630
4200
  }
2631
- catch (error) {
2632
- logger.error('[updateExpiration] Error updating session:', error);
2633
- throw new SessionError('Failed to update session expiration', 'UPDATE_EXPIRATION_FAILED');
4201
+ const userQuery = User.findById(userId);
4202
+ if (session) {
4203
+ userQuery.session(session);
4204
+ }
4205
+ const updatedUser = await userQuery.lean();
4206
+ if (!updatedUser) {
4207
+ throw new Error(`User not found after update: ${userId}`);
2634
4208
  }
4209
+ return {
4210
+ user: updatedUser,
4211
+ addedGroups,
4212
+ removedGroups,
4213
+ };
2635
4214
  }
2636
4215
  /**
2637
- * Deletes a session by refresh token or session ID
4216
+ * Calculate relevance score for a search result
4217
+ * @param item - The search result item
4218
+ * @param searchPattern - The search pattern
4219
+ * @returns Relevance score (0-100)
2638
4220
  */
2639
- async function deleteSession(params) {
2640
- try {
2641
- const Session = mongoose.models.Session;
2642
- if (!params.refreshToken && !params.sessionId) {
2643
- throw new SessionError('Either refreshToken or sessionId is required', 'INVALID_DELETE_PARAMS');
4221
+ function calculateRelevanceScore(item, searchPattern) {
4222
+ const exactRegex = new RegExp(`^${searchPattern}$`, 'i');
4223
+ const startsWithPattern = searchPattern.toLowerCase();
4224
+ /** Get searchable text based on type */
4225
+ const searchableFields = item.type === PrincipalType.USER
4226
+ ? [item.name, item.email, item.username].filter(Boolean)
4227
+ : [item.name, item.email, item.description].filter(Boolean);
4228
+ let maxScore = 0;
4229
+ for (const field of searchableFields) {
4230
+ if (!field)
4231
+ continue;
4232
+ const fieldLower = field.toLowerCase();
4233
+ let score = 0;
4234
+ /** Exact match gets highest score */
4235
+ if (exactRegex.test(field)) {
4236
+ score = 100;
2644
4237
  }
2645
- const query = {};
2646
- if (params.refreshToken) {
2647
- query.refreshTokenHash = await hashToken(params.refreshToken);
4238
+ else if (fieldLower.startsWith(startsWithPattern)) {
4239
+ /** Starts with query gets high score */
4240
+ score = 80;
2648
4241
  }
2649
- if (params.sessionId) {
2650
- query._id = params.sessionId;
4242
+ else if (fieldLower.includes(startsWithPattern)) {
4243
+ /** Contains query gets medium score */
4244
+ score = 50;
2651
4245
  }
2652
- const result = await Session.deleteOne(query);
2653
- if (result.deletedCount === 0) {
2654
- logger.warn('[deleteSession] No session found to delete');
4246
+ else {
4247
+ /** Default score for regex match */
4248
+ score = 10;
2655
4249
  }
2656
- return result;
2657
- }
2658
- catch (error) {
2659
- logger.error('[deleteSession] Error deleting session:', error);
2660
- throw new SessionError('Failed to delete session', 'DELETE_SESSION_FAILED');
4250
+ maxScore = Math.max(maxScore, score);
2661
4251
  }
4252
+ return maxScore;
2662
4253
  }
2663
4254
  /**
2664
- * Deletes all sessions for a user
4255
+ * Sort principals by relevance score and type priority
4256
+ * @param results - Array of results with _searchScore property
4257
+ * @returns Sorted array
2665
4258
  */
2666
- async function deleteAllUserSessions(userId, options = {}) {
2667
- try {
2668
- const Session = mongoose.models.Session;
2669
- if (!userId) {
2670
- throw new SessionError('User ID is required', 'INVALID_USER_ID');
4259
+ function sortPrincipalsByRelevance(results) {
4260
+ return results.sort((a, b) => {
4261
+ if (b._searchScore !== a._searchScore) {
4262
+ return (b._searchScore || 0) - (a._searchScore || 0);
2671
4263
  }
2672
- const userIdString = typeof userId === 'object' && userId !== null ? userId.userId : userId;
2673
- if (!mongoose.Types.ObjectId.isValid(userIdString)) {
2674
- throw new SessionError('Invalid user ID format', 'INVALID_USER_ID_FORMAT');
2675
- }
2676
- const query = { user: userIdString };
2677
- if (options.excludeCurrentSession && options.currentSessionId) {
2678
- query._id = { $ne: options.currentSessionId };
2679
- }
2680
- const result = await Session.deleteMany(query);
2681
- if (result.deletedCount && result.deletedCount > 0) {
2682
- logger.debug(`[deleteAllUserSessions] Deleted ${result.deletedCount} sessions for user ${userIdString}.`);
4264
+ if (a.type !== b.type) {
4265
+ return a.type === PrincipalType.USER ? -1 : 1;
2683
4266
  }
2684
- return result;
2685
- }
2686
- catch (error) {
2687
- logger.error('[deleteAllUserSessions] Error deleting user sessions:', error);
2688
- throw new SessionError('Failed to delete user sessions', 'DELETE_ALL_SESSIONS_FAILED');
2689
- }
4267
+ const aName = a.name || a.email || '';
4268
+ const bName = b.name || b.email || '';
4269
+ return aName.localeCompare(bName);
4270
+ });
2690
4271
  }
2691
4272
  /**
2692
- * Generates a refresh token for a session
4273
+ * Transform user object to TPrincipalSearchResult format
4274
+ * @param user - User object from database
4275
+ * @returns Transformed user result
2693
4276
  */
2694
- async function generateRefreshToken(session) {
2695
- if (!session || !session.user) {
2696
- throw new SessionError('Invalid session object', 'INVALID_SESSION');
4277
+ function transformUserToTPrincipalSearchResult(user) {
4278
+ return {
4279
+ id: user.id,
4280
+ type: PrincipalType.USER,
4281
+ name: user.name || user.email,
4282
+ email: user.email,
4283
+ username: user.username,
4284
+ avatar: user.avatar,
4285
+ provider: user.provider,
4286
+ source: 'local',
4287
+ idOnTheSource: user.idOnTheSource || user.id,
4288
+ };
4289
+ }
4290
+ /**
4291
+ * Transform group object to TPrincipalSearchResult format
4292
+ * @param group - Group object from database
4293
+ * @returns Transformed group result
4294
+ */
4295
+ function transformGroupToTPrincipalSearchResult(group) {
4296
+ var _a, _b;
4297
+ return {
4298
+ id: (_a = group._id) === null || _a === void 0 ? void 0 : _a.toString(),
4299
+ type: PrincipalType.GROUP,
4300
+ name: group.name,
4301
+ email: group.email,
4302
+ avatar: group.avatar,
4303
+ description: group.description,
4304
+ source: group.source || 'local',
4305
+ memberCount: group.memberIds ? group.memberIds.length : 0,
4306
+ idOnTheSource: group.idOnTheSource || ((_b = group._id) === null || _b === void 0 ? void 0 : _b.toString()),
4307
+ };
4308
+ }
4309
+ /**
4310
+ * Search for principals (users and groups) by pattern matching on name/email
4311
+ * Returns combined results in TPrincipalSearchResult format without sorting
4312
+ * @param searchPattern - The pattern to search for
4313
+ * @param limitPerType - Maximum number of results to return
4314
+ * @param typeFilter - Optional array of types to filter by, or null for all types
4315
+ * @param session - Optional MongoDB session for transactions
4316
+ * @returns Array of principals in TPrincipalSearchResult format
4317
+ */
4318
+ async function searchPrincipals(searchPattern, limitPerType = 10, typeFilter = null, session) {
4319
+ if (!searchPattern || searchPattern.trim().length === 0) {
4320
+ return [];
2697
4321
  }
2698
- try {
2699
- const expiresIn = session.expiration ? session.expiration.getTime() : Date.now() + expires;
2700
- if (!session.expiration) {
2701
- session.expiration = new Date(expiresIn);
4322
+ const trimmedPattern = searchPattern.trim();
4323
+ const promises = [];
4324
+ if (!typeFilter || typeFilter.includes(PrincipalType.USER)) {
4325
+ /** Note: searchUsers is imported from ~/models and needs to be passed in or implemented */
4326
+ const userFields = 'name email username avatar provider idOnTheSource';
4327
+ /** For now, we'll use a direct query instead of searchUsers */
4328
+ const User = mongoose.models.User;
4329
+ const regex = new RegExp(trimmedPattern, 'i');
4330
+ const userQuery = User.find({
4331
+ $or: [{ name: regex }, { email: regex }, { username: regex }],
4332
+ })
4333
+ .select(userFields)
4334
+ .limit(limitPerType);
4335
+ if (session) {
4336
+ userQuery.session(session);
2702
4337
  }
2703
- const refreshToken = await signPayload({
2704
- payload: {
2705
- id: session.user,
2706
- sessionId: session._id,
2707
- },
2708
- secret: process.env.JWT_REFRESH_SECRET,
2709
- expirationTime: Math.floor((expiresIn - Date.now()) / 1000),
2710
- });
2711
- session.refreshTokenHash = await hashToken(refreshToken);
2712
- await session.save();
2713
- return refreshToken;
4338
+ promises.push(userQuery.lean().then((users) => users.map((user) => {
4339
+ var _a;
4340
+ const userWithId = user;
4341
+ return transformUserToTPrincipalSearchResult({
4342
+ id: ((_a = userWithId._id) === null || _a === void 0 ? void 0 : _a.toString()) || '',
4343
+ name: userWithId.name,
4344
+ email: userWithId.email,
4345
+ username: userWithId.username,
4346
+ avatar: userWithId.avatar,
4347
+ provider: userWithId.provider,
4348
+ });
4349
+ })));
2714
4350
  }
2715
- catch (error) {
2716
- logger.error('[generateRefreshToken] Error generating refresh token:', error);
2717
- throw new SessionError('Failed to generate refresh token', 'GENERATE_TOKEN_FAILED');
4351
+ else {
4352
+ promises.push(Promise.resolve([]));
2718
4353
  }
2719
- }
2720
- /**
2721
- * Counts active sessions for a user
2722
- */
2723
- async function countActiveSessions(userId) {
2724
- try {
2725
- const Session = mongoose.models.Session;
2726
- if (!userId) {
2727
- throw new SessionError('User ID is required', 'INVALID_USER_ID');
4354
+ if (!typeFilter || typeFilter.includes(PrincipalType.GROUP)) {
4355
+ promises.push(findGroupsByNamePattern(trimmedPattern, null, limitPerType, session).then((groups) => groups.map(transformGroupToTPrincipalSearchResult)));
4356
+ }
4357
+ else {
4358
+ promises.push(Promise.resolve([]));
4359
+ }
4360
+ if (!typeFilter || typeFilter.includes(PrincipalType.ROLE)) {
4361
+ const Role = mongoose.models.Role;
4362
+ if (Role) {
4363
+ const regex = new RegExp(trimmedPattern, 'i');
4364
+ const roleQuery = Role.find({ name: regex }).select('name').limit(limitPerType);
4365
+ if (session) {
4366
+ roleQuery.session(session);
4367
+ }
4368
+ promises.push(roleQuery.lean().then((roles) => roles.map((role) => ({
4369
+ /** Role name as ID */
4370
+ id: role.name,
4371
+ type: PrincipalType.ROLE,
4372
+ name: role.name,
4373
+ source: 'local',
4374
+ idOnTheSource: role.name,
4375
+ }))));
2728
4376
  }
2729
- return await Session.countDocuments({
2730
- user: userId,
2731
- expiration: { $gt: new Date() },
2732
- });
2733
4377
  }
2734
- catch (error) {
2735
- logger.error('[countActiveSessions] Error counting active sessions:', error);
2736
- throw new SessionError('Failed to count active sessions', 'COUNT_SESSIONS_FAILED');
4378
+ else {
4379
+ promises.push(Promise.resolve([]));
2737
4380
  }
4381
+ const results = await Promise.all(promises);
4382
+ const combined = results.flat();
4383
+ return combined;
2738
4384
  }
2739
4385
  return {
2740
- findSession,
2741
- SessionError,
2742
- deleteSession,
2743
- createSession,
2744
- updateExpiration,
2745
- countActiveSessions,
2746
- generateRefreshToken,
2747
- deleteAllUserSessions,
4386
+ findGroupById,
4387
+ findGroupByExternalId,
4388
+ findGroupsByNamePattern,
4389
+ findGroupsByMemberId,
4390
+ createGroup,
4391
+ upsertGroupByExternalId,
4392
+ addUserToGroup,
4393
+ removeUserFromGroup,
4394
+ getUserGroups,
4395
+ getUserPrincipals,
4396
+ syncUserEntraGroups,
4397
+ searchPrincipals,
4398
+ calculateRelevanceScore,
4399
+ sortPrincipalsByRelevance,
2748
4400
  };
2749
4401
  }
2750
4402
 
2751
- // Factory function that takes mongoose instance and returns the methods
2752
- function createTokenMethods(mongoose) {
4403
+ function createAclEntryMethods(mongoose) {
2753
4404
  /**
2754
- * Creates a new Token instance.
4405
+ * Find ACL entries for a specific principal (user or group)
4406
+ * @param principalType - The type of principal ('user', 'group')
4407
+ * @param principalId - The ID of the principal
4408
+ * @param resourceType - Optional filter by resource type
4409
+ * @returns Array of ACL entries
2755
4410
  */
2756
- async function createToken(tokenData) {
2757
- try {
2758
- const Token = mongoose.models.Token;
2759
- const currentTime = new Date();
2760
- const expiresAt = new Date(currentTime.getTime() + tokenData.expiresIn * 1000);
2761
- const newTokenData = {
2762
- ...tokenData,
2763
- createdAt: currentTime,
2764
- expiresAt,
2765
- };
2766
- return await Token.create(newTokenData);
2767
- }
2768
- catch (error) {
2769
- logger.debug('An error occurred while creating token:', error);
2770
- throw error;
4411
+ async function findEntriesByPrincipal(principalType, principalId, resourceType) {
4412
+ const AclEntry = mongoose.models.AclEntry;
4413
+ const query = { principalType, principalId };
4414
+ if (resourceType) {
4415
+ query.resourceType = resourceType;
2771
4416
  }
4417
+ return await AclEntry.find(query).lean();
2772
4418
  }
2773
4419
  /**
2774
- * Updates a Token document that matches the provided query.
4420
+ * Find ACL entries for a specific resource
4421
+ * @param resourceType - The type of resource ('agent', 'project', 'file')
4422
+ * @param resourceId - The ID of the resource
4423
+ * @returns Array of ACL entries
2775
4424
  */
2776
- async function updateToken(query, updateData) {
2777
- try {
2778
- const Token = mongoose.models.Token;
2779
- return await Token.findOneAndUpdate(query, updateData, { new: true });
2780
- }
2781
- catch (error) {
2782
- logger.debug('An error occurred while updating token:', error);
2783
- throw error;
2784
- }
4425
+ async function findEntriesByResource(resourceType, resourceId) {
4426
+ const AclEntry = mongoose.models.AclEntry;
4427
+ return await AclEntry.find({ resourceType, resourceId }).lean();
2785
4428
  }
2786
4429
  /**
2787
- * Deletes all Token documents that match the provided token, user ID, or email.
4430
+ * Find all ACL entries for a set of principals (including public)
4431
+ * @param principalsList - List of principals, each containing { principalType, principalId }
4432
+ * @param resourceType - The type of resource
4433
+ * @param resourceId - The ID of the resource
4434
+ * @returns Array of matching ACL entries
2788
4435
  */
2789
- async function deleteTokens(query) {
2790
- try {
2791
- const Token = mongoose.models.Token;
2792
- return await Token.deleteMany({
2793
- $or: [
2794
- { userId: query.userId },
2795
- { token: query.token },
2796
- { email: query.email },
2797
- { identifier: query.identifier },
2798
- ],
2799
- });
2800
- }
2801
- catch (error) {
2802
- logger.debug('An error occurred while deleting tokens:', error);
2803
- throw error;
4436
+ async function findEntriesByPrincipalsAndResource(principalsList, resourceType, resourceId) {
4437
+ const AclEntry = mongoose.models.AclEntry;
4438
+ const principalsQuery = principalsList.map((p) => ({
4439
+ principalType: p.principalType,
4440
+ ...(p.principalType !== PrincipalType.PUBLIC && { principalId: p.principalId }),
4441
+ }));
4442
+ return await AclEntry.find({
4443
+ $or: principalsQuery,
4444
+ resourceType,
4445
+ resourceId,
4446
+ }).lean();
4447
+ }
4448
+ /**
4449
+ * Check if a set of principals has a specific permission on a resource
4450
+ * @param principalsList - List of principals, each containing { principalType, principalId }
4451
+ * @param resourceType - The type of resource
4452
+ * @param resourceId - The ID of the resource
4453
+ * @param permissionBit - The permission bit to check (use PermissionBits enum)
4454
+ * @returns Whether any of the principals has the permission
4455
+ */
4456
+ async function hasPermission(principalsList, resourceType, resourceId, permissionBit) {
4457
+ const AclEntry = mongoose.models.AclEntry;
4458
+ const principalsQuery = principalsList.map((p) => ({
4459
+ principalType: p.principalType,
4460
+ ...(p.principalType !== PrincipalType.PUBLIC && { principalId: p.principalId }),
4461
+ }));
4462
+ const entry = await AclEntry.findOne({
4463
+ $or: principalsQuery,
4464
+ resourceType,
4465
+ resourceId,
4466
+ permBits: { $bitsAllSet: permissionBit },
4467
+ }).lean();
4468
+ return !!entry;
4469
+ }
4470
+ /**
4471
+ * Get the combined effective permissions for a set of principals on a resource
4472
+ * @param principalsList - List of principals, each containing { principalType, principalId }
4473
+ * @param resourceType - The type of resource
4474
+ * @param resourceId - The ID of the resource
4475
+ * @returns {Promise<number>} Effective permission bitmask
4476
+ */
4477
+ async function getEffectivePermissions(principalsList, resourceType, resourceId) {
4478
+ const aclEntries = await findEntriesByPrincipalsAndResource(principalsList, resourceType, resourceId);
4479
+ let effectiveBits = 0;
4480
+ for (const entry of aclEntries) {
4481
+ effectiveBits |= entry.permBits;
2804
4482
  }
4483
+ return effectiveBits;
2805
4484
  }
2806
4485
  /**
2807
- * Finds a Token document that matches the provided query.
4486
+ * Grant permission to a principal for a resource
4487
+ * @param principalType - The type of principal ('user', 'group', 'public')
4488
+ * @param principalId - The ID of the principal (null for 'public')
4489
+ * @param resourceType - The type of resource
4490
+ * @param resourceId - The ID of the resource
4491
+ * @param permBits - The permission bits to grant
4492
+ * @param grantedBy - The ID of the user granting the permission
4493
+ * @param session - Optional MongoDB session for transactions
4494
+ * @param roleId - Optional role ID to associate with this permission
4495
+ * @returns The created or updated ACL entry
2808
4496
  */
2809
- async function findToken(query) {
2810
- try {
2811
- const Token = mongoose.models.Token;
2812
- const conditions = [];
2813
- if (query.userId) {
2814
- conditions.push({ userId: query.userId });
2815
- }
2816
- if (query.token) {
2817
- conditions.push({ token: query.token });
4497
+ async function grantPermission(principalType, principalId, resourceType, resourceId, permBits, grantedBy, session, roleId) {
4498
+ const AclEntry = mongoose.models.AclEntry;
4499
+ const query = {
4500
+ principalType,
4501
+ resourceType,
4502
+ resourceId,
4503
+ };
4504
+ if (principalType !== PrincipalType.PUBLIC) {
4505
+ query.principalId =
4506
+ typeof principalId === 'string' && principalType !== PrincipalType.ROLE
4507
+ ? new Types.ObjectId(principalId)
4508
+ : principalId;
4509
+ if (principalType === PrincipalType.USER) {
4510
+ query.principalModel = PrincipalModel.USER;
2818
4511
  }
2819
- if (query.email) {
2820
- conditions.push({ email: query.email });
4512
+ else if (principalType === PrincipalType.GROUP) {
4513
+ query.principalModel = PrincipalModel.GROUP;
2821
4514
  }
2822
- if (query.identifier) {
2823
- conditions.push({ identifier: query.identifier });
4515
+ else if (principalType === PrincipalType.ROLE) {
4516
+ query.principalModel = PrincipalModel.ROLE;
2824
4517
  }
2825
- const token = await Token.findOne({
2826
- $and: conditions,
2827
- }).lean();
2828
- return token;
2829
4518
  }
2830
- catch (error) {
2831
- logger.debug('An error occurred while finding token:', error);
2832
- throw error;
4519
+ const update = {
4520
+ $set: {
4521
+ permBits,
4522
+ grantedBy,
4523
+ grantedAt: new Date(),
4524
+ ...(roleId && { roleId }),
4525
+ },
4526
+ };
4527
+ const options = {
4528
+ upsert: true,
4529
+ new: true,
4530
+ ...(session ? { session } : {}),
4531
+ };
4532
+ return await AclEntry.findOneAndUpdate(query, update, options);
4533
+ }
4534
+ /**
4535
+ * Revoke permissions from a principal for a resource
4536
+ * @param principalType - The type of principal ('user', 'group', 'public')
4537
+ * @param principalId - The ID of the principal (null for 'public')
4538
+ * @param resourceType - The type of resource
4539
+ * @param resourceId - The ID of the resource
4540
+ * @param session - Optional MongoDB session for transactions
4541
+ * @returns The result of the delete operation
4542
+ */
4543
+ async function revokePermission(principalType, principalId, resourceType, resourceId, session) {
4544
+ const AclEntry = mongoose.models.AclEntry;
4545
+ const query = {
4546
+ principalType,
4547
+ resourceType,
4548
+ resourceId,
4549
+ };
4550
+ if (principalType !== PrincipalType.PUBLIC) {
4551
+ query.principalId =
4552
+ typeof principalId === 'string' && principalType !== PrincipalType.ROLE
4553
+ ? new Types.ObjectId(principalId)
4554
+ : principalId;
4555
+ }
4556
+ const options = session ? { session } : {};
4557
+ return await AclEntry.deleteOne(query, options);
4558
+ }
4559
+ /**
4560
+ * Modify existing permission bits for a principal on a resource
4561
+ * @param principalType - The type of principal ('user', 'group', 'public')
4562
+ * @param principalId - The ID of the principal (null for 'public')
4563
+ * @param resourceType - The type of resource
4564
+ * @param resourceId - The ID of the resource
4565
+ * @param addBits - Permission bits to add
4566
+ * @param removeBits - Permission bits to remove
4567
+ * @param session - Optional MongoDB session for transactions
4568
+ * @returns The updated ACL entry
4569
+ */
4570
+ async function modifyPermissionBits(principalType, principalId, resourceType, resourceId, addBits, removeBits, session) {
4571
+ const AclEntry = mongoose.models.AclEntry;
4572
+ const query = {
4573
+ principalType,
4574
+ resourceType,
4575
+ resourceId,
4576
+ };
4577
+ if (principalType !== PrincipalType.PUBLIC) {
4578
+ query.principalId =
4579
+ typeof principalId === 'string' && principalType !== PrincipalType.ROLE
4580
+ ? new Types.ObjectId(principalId)
4581
+ : principalId;
4582
+ }
4583
+ const update = {};
4584
+ if (addBits) {
4585
+ update.$bit = { permBits: { or: addBits } };
2833
4586
  }
4587
+ if (removeBits) {
4588
+ if (!update.$bit)
4589
+ update.$bit = {};
4590
+ const bitUpdate = update.$bit;
4591
+ bitUpdate.permBits = { ...bitUpdate.permBits, and: ~removeBits };
4592
+ }
4593
+ const options = {
4594
+ new: true,
4595
+ ...(session ? { session } : {}),
4596
+ };
4597
+ return await AclEntry.findOneAndUpdate(query, update, options);
4598
+ }
4599
+ /**
4600
+ * Find all resources of a specific type that a set of principals has access to
4601
+ * @param principalsList - List of principals, each containing { principalType, principalId }
4602
+ * @param resourceType - The type of resource
4603
+ * @param requiredPermBit - Required permission bit (use PermissionBits enum)
4604
+ * @returns Array of resource IDs
4605
+ */
4606
+ async function findAccessibleResources(principalsList, resourceType, requiredPermBit) {
4607
+ const AclEntry = mongoose.models.AclEntry;
4608
+ const principalsQuery = principalsList.map((p) => ({
4609
+ principalType: p.principalType,
4610
+ ...(p.principalType !== PrincipalType.PUBLIC && { principalId: p.principalId }),
4611
+ }));
4612
+ const entries = await AclEntry.find({
4613
+ $or: principalsQuery,
4614
+ resourceType,
4615
+ permBits: { $bitsAllSet: requiredPermBit },
4616
+ }).distinct('resourceId');
4617
+ return entries;
2834
4618
  }
2835
- // Return all methods
2836
4619
  return {
2837
- findToken,
2838
- createToken,
2839
- updateToken,
2840
- deleteTokens,
4620
+ findEntriesByPrincipal,
4621
+ findEntriesByResource,
4622
+ findEntriesByPrincipalsAndResource,
4623
+ hasPermission,
4624
+ getEffectivePermissions,
4625
+ grantPermission,
4626
+ revokePermission,
4627
+ modifyPermissionBits,
4628
+ findAccessibleResources,
2841
4629
  };
2842
4630
  }
2843
4631
 
2844
- // Factory function that takes mongoose instance and returns the methods
2845
- function createRoleMethods(mongoose) {
4632
+ function createGroupMethods(mongoose) {
2846
4633
  /**
2847
- * Initialize default roles in the system.
2848
- * Creates the default roles (ADMIN, USER) if they don't exist in the database.
2849
- * Updates existing roles with new permission types if they're missing.
4634
+ * Find a group by its ID
4635
+ * @param groupId - The group ID
4636
+ * @returns The group document or null if not found
2850
4637
  */
2851
- async function initializeRoles() {
2852
- const Role = mongoose.models.Role;
2853
- for (const roleName of [SystemRoles.ADMIN, SystemRoles.USER]) {
2854
- let role = await Role.findOne({ name: roleName });
2855
- const defaultPerms = roleDefaults[roleName].permissions;
2856
- if (!role) {
2857
- // Create new role if it doesn't exist.
2858
- role = new Role(roleDefaults[roleName]);
2859
- }
2860
- else {
2861
- // Ensure role.permissions is defined.
2862
- role.permissions = role.permissions || {};
2863
- // For each permission type in defaults, add it if missing.
2864
- for (const permType of Object.keys(defaultPerms)) {
2865
- if (role.permissions[permType] == null) {
2866
- role.permissions[permType] = defaultPerms[permType];
2867
- }
2868
- }
2869
- }
2870
- await role.save();
2871
- }
4638
+ async function findGroupById(groupId) {
4639
+ const Group = mongoose.models.Group;
4640
+ return await Group.findById(groupId).lean();
2872
4641
  }
2873
4642
  /**
2874
- * List all roles in the system (for testing purposes)
2875
- * Returns an array of all roles with their names and permissions
4643
+ * Create a new group
4644
+ * @param groupData - Group data including name, source, and optional fields
4645
+ * @returns The created group
2876
4646
  */
2877
- async function listRoles() {
2878
- const Role = mongoose.models.Role;
2879
- return await Role.find({}).select('name permissions').lean();
4647
+ async function createGroup(groupData) {
4648
+ const Group = mongoose.models.Group;
4649
+ return await Group.create(groupData);
2880
4650
  }
2881
- // Return all methods you want to expose
2882
- return {
2883
- listRoles,
2884
- initializeRoles,
2885
- };
2886
- }
2887
-
2888
- /**
2889
- * Formats a date in YYYY-MM-DD format
2890
- */
2891
- const formatDate = (date) => {
2892
- return date.toISOString().split('T')[0];
2893
- };
2894
- // Factory function that takes mongoose instance and returns the methods
2895
- function createMemoryMethods(mongoose) {
2896
4651
  /**
2897
- * Creates a new memory entry for a user
2898
- * Throws an error if a memory with the same key already exists
4652
+ * Update an existing group
4653
+ * @param groupId - The ID of the group to update
4654
+ * @param updateData - Data to update
4655
+ * @returns The updated group document or null if not found
2899
4656
  */
2900
- async function createMemory({ userId, key, value, tokenCount = 0, }) {
2901
- try {
2902
- if ((key === null || key === void 0 ? void 0 : key.toLowerCase()) === 'nothing') {
2903
- return { ok: false };
2904
- }
2905
- const MemoryEntry = mongoose.models.MemoryEntry;
2906
- const existingMemory = await MemoryEntry.findOne({ userId, key });
2907
- if (existingMemory) {
2908
- throw new Error('Memory with this key already exists');
2909
- }
2910
- await MemoryEntry.create({
2911
- userId,
2912
- key,
2913
- value,
2914
- tokenCount,
2915
- updated_at: new Date(),
2916
- });
2917
- return { ok: true };
2918
- }
2919
- catch (error) {
2920
- throw new Error(`Failed to create memory: ${error instanceof Error ? error.message : 'Unknown error'}`);
2921
- }
4657
+ async function updateGroup(groupId, updateData) {
4658
+ const Group = mongoose.models.Group;
4659
+ return await Group.findByIdAndUpdate(groupId, { $set: updateData }, { new: true }).lean();
2922
4660
  }
2923
4661
  /**
2924
- * Sets or updates a memory entry for a user
4662
+ * Delete a group
4663
+ * @param groupId - The ID of the group to delete
4664
+ * @returns The result of the delete operation
2925
4665
  */
2926
- async function setMemory({ userId, key, value, tokenCount = 0, }) {
2927
- try {
2928
- if ((key === null || key === void 0 ? void 0 : key.toLowerCase()) === 'nothing') {
2929
- return { ok: false };
2930
- }
2931
- const MemoryEntry = mongoose.models.MemoryEntry;
2932
- await MemoryEntry.findOneAndUpdate({ userId, key }, {
2933
- value,
2934
- tokenCount,
2935
- updated_at: new Date(),
2936
- }, {
2937
- upsert: true,
2938
- new: true,
2939
- });
2940
- return { ok: true };
2941
- }
2942
- catch (error) {
2943
- throw new Error(`Failed to set memory: ${error instanceof Error ? error.message : 'Unknown error'}`);
2944
- }
4666
+ async function deleteGroup(groupId) {
4667
+ const Group = mongoose.models.Group;
4668
+ return await Group.deleteOne({ _id: groupId });
2945
4669
  }
2946
4670
  /**
2947
- * Deletes a specific memory entry for a user
4671
+ * Find all groups
4672
+ * @returns Array of all group documents
2948
4673
  */
2949
- async function deleteMemory({ userId, key }) {
2950
- try {
2951
- const MemoryEntry = mongoose.models.MemoryEntry;
2952
- const result = await MemoryEntry.findOneAndDelete({ userId, key });
2953
- return { ok: !!result };
2954
- }
2955
- catch (error) {
2956
- throw new Error(`Failed to delete memory: ${error instanceof Error ? error.message : 'Unknown error'}`);
2957
- }
4674
+ async function getAllGroups() {
4675
+ const Group = mongoose.models.Group;
4676
+ return await Group.find().lean();
2958
4677
  }
2959
4678
  /**
2960
- * Gets all memory entries for a user
4679
+ * Find groups by source
4680
+ * @param source - The source ('local' or 'entra')
4681
+ * @returns Array of group documents
2961
4682
  */
2962
- async function getAllUserMemories(userId) {
2963
- try {
2964
- const MemoryEntry = mongoose.models.MemoryEntry;
2965
- return (await MemoryEntry.find({ userId }).lean());
2966
- }
2967
- catch (error) {
2968
- throw new Error(`Failed to get all memories: ${error instanceof Error ? error.message : 'Unknown error'}`);
2969
- }
4683
+ async function findGroupsBySource(source) {
4684
+ const Group = mongoose.models.Group;
4685
+ return await Group.find({ source }).lean();
2970
4686
  }
2971
4687
  /**
2972
- * Gets and formats all memories for a user in two different formats
4688
+ * Find a group by its external ID
4689
+ * @param idOnTheSource - The external ID
4690
+ * @param source - The source ('entra' or 'local')
4691
+ * @returns The group document or null if not found
2973
4692
  */
2974
- async function getFormattedMemories({ userId, }) {
2975
- try {
2976
- const memories = await getAllUserMemories(userId);
2977
- if (!memories || memories.length === 0) {
2978
- return { withKeys: '', withoutKeys: '', totalTokens: 0 };
2979
- }
2980
- const sortedMemories = memories.sort((a, b) => new Date(a.updated_at).getTime() - new Date(b.updated_at).getTime());
2981
- const totalTokens = sortedMemories.reduce((sum, memory) => {
2982
- return sum + (memory.tokenCount || 0);
2983
- }, 0);
2984
- const withKeys = sortedMemories
2985
- .map((memory, index) => {
2986
- const date = formatDate(new Date(memory.updated_at));
2987
- const tokenInfo = memory.tokenCount ? ` [${memory.tokenCount} tokens]` : '';
2988
- return `${index + 1}. [${date}]. ["key": "${memory.key}"]${tokenInfo}. ["value": "${memory.value}"]`;
2989
- })
2990
- .join('\n\n');
2991
- const withoutKeys = sortedMemories
2992
- .map((memory, index) => {
2993
- const date = formatDate(new Date(memory.updated_at));
2994
- return `${index + 1}. [${date}]. ${memory.value}`;
2995
- })
2996
- .join('\n\n');
2997
- return { withKeys, withoutKeys, totalTokens };
2998
- }
2999
- catch (error) {
3000
- logger.error('Failed to get formatted memories:', error);
3001
- return { withKeys: '', withoutKeys: '', totalTokens: 0 };
3002
- }
4693
+ async function findGroupByExternalId(idOnTheSource, source = 'entra') {
4694
+ const Group = mongoose.models.Group;
4695
+ return await Group.findOne({ idOnTheSource, source }).lean();
4696
+ }
4697
+ /**
4698
+ * Add a member to a group
4699
+ * @param groupId - The group ID
4700
+ * @param memberId - The member ID to add (idOnTheSource value)
4701
+ * @returns The updated group or null if not found
4702
+ */
4703
+ async function addMemberToGroup(groupId, memberId) {
4704
+ const Group = mongoose.models.Group;
4705
+ return await Group.findByIdAndUpdate(groupId, { $addToSet: { memberIds: memberId } }, { new: true }).lean();
4706
+ }
4707
+ /**
4708
+ * Remove a member from a group
4709
+ * @param groupId - The group ID
4710
+ * @param memberId - The member ID to remove (idOnTheSource value)
4711
+ * @returns The updated group or null if not found
4712
+ */
4713
+ async function removeMemberFromGroup(groupId, memberId) {
4714
+ const Group = mongoose.models.Group;
4715
+ return await Group.findByIdAndUpdate(groupId, { $pull: { memberIds: memberId } }, { new: true }).lean();
4716
+ }
4717
+ /**
4718
+ * Find all groups that contain a specific member
4719
+ * @param memberId - The member ID (idOnTheSource value)
4720
+ * @returns Array of groups containing the member
4721
+ */
4722
+ async function findGroupsByMemberId(memberId) {
4723
+ const Group = mongoose.models.Group;
4724
+ return await Group.find({ memberIds: memberId }).lean();
3003
4725
  }
3004
4726
  return {
3005
- setMemory,
3006
- createMemory,
3007
- deleteMemory,
3008
- getAllUserMemories,
3009
- getFormattedMemories,
4727
+ createGroup,
4728
+ updateGroup,
4729
+ deleteGroup,
4730
+ getAllGroups,
4731
+ findGroupById,
4732
+ addMemberToGroup,
4733
+ findGroupsBySource,
4734
+ removeMemberFromGroup,
4735
+ findGroupsByMemberId,
4736
+ findGroupByExternalId,
3010
4737
  };
3011
4738
  }
3012
4739
 
@@ -3099,7 +4826,7 @@ function createShareMethods(mongoose) {
3099
4826
  return result;
3100
4827
  }
3101
4828
  catch (error) {
3102
- logger.error('[getSharedMessages] Error getting share link', {
4829
+ logger$1.error('[getSharedMessages] Error getting share link', {
3103
4830
  error: error instanceof Error ? error.message : 'Unknown error',
3104
4831
  shareId,
3105
4832
  });
@@ -3137,7 +4864,7 @@ function createShareMethods(mongoose) {
3137
4864
  query['conversationId'] = { $in: conversationIds };
3138
4865
  }
3139
4866
  catch (searchError) {
3140
- logger.error('[getSharedLinks] Meilisearch error', {
4867
+ logger$1.error('[getSharedLinks] Meilisearch error', {
3141
4868
  error: searchError instanceof Error ? searchError.message : 'Unknown error',
3142
4869
  user,
3143
4870
  });
@@ -3173,7 +4900,7 @@ function createShareMethods(mongoose) {
3173
4900
  };
3174
4901
  }
3175
4902
  catch (error) {
3176
- logger.error('[getSharedLinks] Error getting shares', {
4903
+ logger$1.error('[getSharedLinks] Error getting shares', {
3177
4904
  error: error instanceof Error ? error.message : 'Unknown error',
3178
4905
  user,
3179
4906
  });
@@ -3193,7 +4920,7 @@ function createShareMethods(mongoose) {
3193
4920
  };
3194
4921
  }
3195
4922
  catch (error) {
3196
- logger.error('[deleteAllSharedLinks] Error deleting shared links', {
4923
+ logger$1.error('[deleteAllSharedLinks] Error deleting shared links', {
3197
4924
  error: error instanceof Error ? error.message : 'Unknown error',
3198
4925
  user,
3199
4926
  });
@@ -3218,7 +4945,7 @@ function createShareMethods(mongoose) {
3218
4945
  Message.find({ conversationId, user }).sort({ createdAt: 1 }).lean(),
3219
4946
  ]);
3220
4947
  if (existingShare && existingShare.isPublic) {
3221
- logger.error('[createSharedLink] Share already exists', {
4948
+ logger$1.error('[createSharedLink] Share already exists', {
3222
4949
  user,
3223
4950
  conversationId,
3224
4951
  });
@@ -3251,7 +4978,7 @@ function createShareMethods(mongoose) {
3251
4978
  if (error instanceof ShareServiceError) {
3252
4979
  throw error;
3253
4980
  }
3254
- logger.error('[createSharedLink] Error creating shared link', {
4981
+ logger$1.error('[createSharedLink] Error creating shared link', {
3255
4982
  error: error instanceof Error ? error.message : 'Unknown error',
3256
4983
  user,
3257
4984
  conversationId,
@@ -3277,7 +5004,7 @@ function createShareMethods(mongoose) {
3277
5004
  return { shareId: share.shareId || null, success: true };
3278
5005
  }
3279
5006
  catch (error) {
3280
- logger.error('[getSharedLink] Error getting shared link', {
5007
+ logger$1.error('[getSharedLink] Error getting shared link', {
3281
5008
  error: error instanceof Error ? error.message : 'Unknown error',
3282
5009
  user,
3283
5010
  conversationId,
@@ -3322,7 +5049,7 @@ function createShareMethods(mongoose) {
3322
5049
  return { shareId: newShareId, conversationId: updatedShare.conversationId };
3323
5050
  }
3324
5051
  catch (error) {
3325
- logger.error('[updateSharedLink] Error updating shared link', {
5052
+ logger$1.error('[updateSharedLink] Error updating shared link', {
3326
5053
  error: error instanceof Error ? error.message : 'Unknown error',
3327
5054
  user,
3328
5055
  shareId,
@@ -3350,7 +5077,7 @@ function createShareMethods(mongoose) {
3350
5077
  };
3351
5078
  }
3352
5079
  catch (error) {
3353
- logger.error('[deleteSharedLink] Error deleting shared link', {
5080
+ logger$1.error('[deleteSharedLink] Error deleting shared link', {
3354
5081
  error: error instanceof Error ? error.message : 'Unknown error',
3355
5082
  user,
3356
5083
  shareId,
@@ -3370,106 +5097,6 @@ function createShareMethods(mongoose) {
3370
5097
  };
3371
5098
  }
3372
5099
 
3373
- // Factory function that takes mongoose instance and returns the methods
3374
- function createPluginAuthMethods(mongoose) {
3375
- /**
3376
- * Finds a single plugin auth entry by userId and authField
3377
- */
3378
- async function findOnePluginAuth({ userId, authField, }) {
3379
- try {
3380
- const PluginAuth = mongoose.models.PluginAuth;
3381
- return await PluginAuth.findOne({ userId, authField }).lean();
3382
- }
3383
- catch (error) {
3384
- throw new Error(`Failed to find plugin auth: ${error instanceof Error ? error.message : 'Unknown error'}`);
3385
- }
3386
- }
3387
- /**
3388
- * Finds multiple plugin auth entries by userId and pluginKeys
3389
- */
3390
- async function findPluginAuthsByKeys({ userId, pluginKeys, }) {
3391
- try {
3392
- if (!pluginKeys || pluginKeys.length === 0) {
3393
- return [];
3394
- }
3395
- const PluginAuth = mongoose.models.PluginAuth;
3396
- return await PluginAuth.find({
3397
- userId,
3398
- pluginKey: { $in: pluginKeys },
3399
- }).lean();
3400
- }
3401
- catch (error) {
3402
- throw new Error(`Failed to find plugin auths: ${error instanceof Error ? error.message : 'Unknown error'}`);
3403
- }
3404
- }
3405
- /**
3406
- * Updates or creates a plugin auth entry
3407
- */
3408
- async function updatePluginAuth({ userId, authField, pluginKey, value, }) {
3409
- try {
3410
- const PluginAuth = mongoose.models.PluginAuth;
3411
- const existingAuth = await PluginAuth.findOne({ userId, pluginKey, authField }).lean();
3412
- if (existingAuth) {
3413
- return await PluginAuth.findOneAndUpdate({ userId, pluginKey, authField }, { $set: { value } }, { new: true, upsert: true }).lean();
3414
- }
3415
- else {
3416
- const newPluginAuth = await new PluginAuth({
3417
- userId,
3418
- authField,
3419
- value,
3420
- pluginKey,
3421
- });
3422
- await newPluginAuth.save();
3423
- return newPluginAuth.toObject();
3424
- }
3425
- }
3426
- catch (error) {
3427
- throw new Error(`Failed to update plugin auth: ${error instanceof Error ? error.message : 'Unknown error'}`);
3428
- }
3429
- }
3430
- /**
3431
- * Deletes plugin auth entries based on provided parameters
3432
- */
3433
- async function deletePluginAuth({ userId, authField, pluginKey, all = false, }) {
3434
- try {
3435
- const PluginAuth = mongoose.models.PluginAuth;
3436
- if (all) {
3437
- const filter = { userId };
3438
- if (pluginKey) {
3439
- filter.pluginKey = pluginKey;
3440
- }
3441
- return await PluginAuth.deleteMany(filter);
3442
- }
3443
- if (!authField) {
3444
- throw new Error('authField is required when all is false');
3445
- }
3446
- return await PluginAuth.deleteOne({ userId, authField });
3447
- }
3448
- catch (error) {
3449
- throw new Error(`Failed to delete plugin auth: ${error instanceof Error ? error.message : 'Unknown error'}`);
3450
- }
3451
- }
3452
- /**
3453
- * Deletes all plugin auth entries for a user
3454
- */
3455
- async function deleteAllUserPluginAuths(userId) {
3456
- try {
3457
- const PluginAuth = mongoose.models.PluginAuth;
3458
- return await PluginAuth.deleteMany({ userId });
3459
- }
3460
- catch (error) {
3461
- throw new Error(`Failed to delete all user plugin auths: ${error instanceof Error ? error.message : 'Unknown error'}`);
3462
- }
3463
- }
3464
- return {
3465
- findOnePluginAuth,
3466
- findPluginAuthsByKeys,
3467
- updatePluginAuth,
3468
- deletePluginAuth,
3469
- deleteAllUserPluginAuths,
3470
- };
3471
- }
3472
-
3473
5100
  /**
3474
5101
  * Creates all database methods for all collections
3475
5102
  */
@@ -3480,10 +5107,15 @@ function createMethods(mongoose) {
3480
5107
  ...createTokenMethods(mongoose),
3481
5108
  ...createRoleMethods(mongoose),
3482
5109
  ...createMemoryMethods(mongoose),
5110
+ ...createAgentCategoryMethods(mongoose),
5111
+ ...createAccessRoleMethods(mongoose),
5112
+ ...createUserGroupMethods(mongoose),
5113
+ ...createAclEntryMethods(mongoose),
5114
+ ...createGroupMethods(mongoose),
3483
5115
  ...createShareMethods(mongoose),
3484
5116
  ...createPluginAuthMethods(mongoose),
3485
5117
  };
3486
5118
  }
3487
5119
 
3488
- export { Action as actionSchema, agentSchema, assistantSchema, balanceSchema, bannerSchema, categoriesSchema, conversationTag as conversationTagSchema, convoSchema, createMethods, createModels, file as fileSchema, hashToken, keySchema, logger, logger$1 as meiliLogger, MemoryEntrySchema as memorySchema, messageSchema, pluginAuthSchema, presetSchema, projectSchema, promptGroupSchema, promptSchema, roleSchema, sessionSchema, shareSchema, signPayload, tokenSchema, toolCallSchema, transactionSchema, userSchema };
5120
+ export { RoleBits, Action as actionSchema, agentCategorySchema, agentSchema, assistantSchema, balanceSchema, bannerSchema, categoriesSchema, conversationTag as conversationTagSchema, convoSchema, createMethods, createModels, file as fileSchema, getTransactionSupport, groupSchema, hashToken, keySchema, logger$1 as logger, logger as meiliLogger, MemoryEntrySchema as memorySchema, messageSchema, pluginAuthSchema, presetSchema, projectSchema, promptGroupSchema, promptSchema, roleSchema, sessionSchema, shareSchema, signPayload, supportsTransactions, tokenSchema, toolCallSchema, transactionSchema, userSchema };
3489
5121
  //# sourceMappingURL=index.es.js.map