@librechat/data-schemas 0.0.31 → 0.0.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -5,8 +5,9 @@ var winston = require('winston');
5
5
  require('winston-daily-rotate-file');
6
6
  var klona = require('klona');
7
7
  var path = require('path');
8
+ require('dotenv/config');
8
9
  var jwt = require('jsonwebtoken');
9
- var node_crypto = require('node:crypto');
10
+ var crypto = require('node:crypto');
10
11
  var mongoose = require('mongoose');
11
12
  var _ = require('lodash');
12
13
  var meilisearch = require('meilisearch');
@@ -856,6 +857,153 @@ function azureConfigSetup(config) {
856
857
  };
857
858
  }
858
859
 
860
+ /**
861
+ * Default Vertex AI models available through Google Cloud
862
+ * These are the standard Anthropic model names as served by Vertex AI
863
+ */
864
+ const defaultVertexModels = [
865
+ 'claude-sonnet-4-20250514',
866
+ 'claude-3-7-sonnet-20250219',
867
+ 'claude-3-5-sonnet-v2@20241022',
868
+ 'claude-3-5-sonnet@20240620',
869
+ 'claude-3-5-haiku@20241022',
870
+ 'claude-3-opus@20240229',
871
+ 'claude-3-haiku@20240307',
872
+ ];
873
+ /**
874
+ * Processes models configuration and creates deployment name mapping
875
+ * Similar to Azure's model mapping logic
876
+ * @param models - The models configuration (can be array or object)
877
+ * @param defaultDeploymentName - Optional default deployment name
878
+ * @returns Object containing modelNames array and modelDeploymentMap
879
+ */
880
+ function processVertexModels(models, defaultDeploymentName) {
881
+ const modelNames = [];
882
+ const modelDeploymentMap = {};
883
+ if (!models) {
884
+ // No models specified, use defaults
885
+ for (const model of defaultVertexModels) {
886
+ modelNames.push(model);
887
+ modelDeploymentMap[model] = model; // Default: model name = deployment name
888
+ }
889
+ return { modelNames, modelDeploymentMap };
890
+ }
891
+ if (Array.isArray(models)) {
892
+ // Legacy format: simple array of model names
893
+ for (const modelName of models) {
894
+ modelNames.push(modelName);
895
+ // If a default deployment name is provided, use it for all models
896
+ // Otherwise, model name is the deployment name
897
+ modelDeploymentMap[modelName] = defaultDeploymentName || modelName;
898
+ }
899
+ }
900
+ else {
901
+ // New format: object with model names as keys and config as values
902
+ for (const [modelName, modelConfig] of Object.entries(models)) {
903
+ modelNames.push(modelName);
904
+ if (typeof modelConfig === 'boolean') {
905
+ // Model is set to true/false - use default deployment name or model name
906
+ modelDeploymentMap[modelName] = defaultDeploymentName || modelName;
907
+ }
908
+ else if (modelConfig === null || modelConfig === void 0 ? void 0 : modelConfig.deploymentName) {
909
+ // Model has its own deployment name specified
910
+ modelDeploymentMap[modelName] = modelConfig.deploymentName;
911
+ }
912
+ else {
913
+ // Model is an object but no deployment name - use default or model name
914
+ modelDeploymentMap[modelName] = defaultDeploymentName || modelName;
915
+ }
916
+ }
917
+ }
918
+ return { modelNames, modelDeploymentMap };
919
+ }
920
+ /**
921
+ * Validates and processes Vertex AI configuration
922
+ * @param vertexConfig - The Vertex AI configuration object
923
+ * @returns Validated configuration with errors if any
924
+ */
925
+ function validateVertexConfig(vertexConfig) {
926
+ if (!vertexConfig) {
927
+ return null;
928
+ }
929
+ const errors = [];
930
+ // Extract and validate environment variables
931
+ // projectId is optional - will be auto-detected from service key if not provided
932
+ const projectId = vertexConfig.projectId ? librechatDataProvider.extractEnvVariable(vertexConfig.projectId) : undefined;
933
+ const region = librechatDataProvider.extractEnvVariable(vertexConfig.region || 'us-east5');
934
+ const serviceKeyFile = vertexConfig.serviceKeyFile
935
+ ? librechatDataProvider.extractEnvVariable(vertexConfig.serviceKeyFile)
936
+ : undefined;
937
+ const defaultDeploymentName = vertexConfig.deploymentName
938
+ ? librechatDataProvider.extractEnvVariable(vertexConfig.deploymentName)
939
+ : undefined;
940
+ // Check for unresolved environment variables
941
+ if (projectId && librechatDataProvider.envVarRegex.test(projectId)) {
942
+ errors.push(`Vertex AI projectId environment variable "${vertexConfig.projectId}" was not found.`);
943
+ }
944
+ if (librechatDataProvider.envVarRegex.test(region)) {
945
+ errors.push(`Vertex AI region environment variable "${vertexConfig.region}" was not found.`);
946
+ }
947
+ if (serviceKeyFile && librechatDataProvider.envVarRegex.test(serviceKeyFile)) {
948
+ errors.push(`Vertex AI serviceKeyFile environment variable "${vertexConfig.serviceKeyFile}" was not found.`);
949
+ }
950
+ if (defaultDeploymentName && librechatDataProvider.envVarRegex.test(defaultDeploymentName)) {
951
+ errors.push(`Vertex AI deploymentName environment variable "${vertexConfig.deploymentName}" was not found.`);
952
+ }
953
+ // Process models and create deployment mapping
954
+ const { modelNames, modelDeploymentMap } = processVertexModels(vertexConfig.models, defaultDeploymentName);
955
+ // Note: projectId is optional - if not provided, it will be auto-detected from the service key file
956
+ const isValid = errors.length === 0;
957
+ return {
958
+ enabled: vertexConfig.enabled !== false,
959
+ projectId,
960
+ region,
961
+ serviceKeyFile,
962
+ deploymentName: defaultDeploymentName,
963
+ models: vertexConfig.models,
964
+ modelNames,
965
+ modelDeploymentMap,
966
+ isValid,
967
+ errors,
968
+ };
969
+ }
970
+ /**
971
+ * Sets up the Vertex AI configuration from the config (`librechat.yaml`) file.
972
+ * Similar to azureConfigSetup, this processes and validates the Vertex AI configuration.
973
+ * @param config - The loaded custom configuration.
974
+ * @returns The validated Vertex AI configuration or null if not configured.
975
+ */
976
+ function vertexConfigSetup(config) {
977
+ var _a, _b;
978
+ const anthropicConfig = (_a = config.endpoints) === null || _a === void 0 ? void 0 : _a[librechatDataProvider.EModelEndpoint.anthropic];
979
+ if (!(anthropicConfig === null || anthropicConfig === void 0 ? void 0 : anthropicConfig.vertex)) {
980
+ return null;
981
+ }
982
+ const vertexConfig = anthropicConfig.vertex;
983
+ // Skip if explicitly disabled (enabled: false)
984
+ // When vertex config exists, it's enabled by default unless explicitly set to false
985
+ if (vertexConfig.enabled === false) {
986
+ return null;
987
+ }
988
+ const validatedConfig = validateVertexConfig(vertexConfig);
989
+ if (!validatedConfig) {
990
+ return null;
991
+ }
992
+ if (!validatedConfig.isValid) {
993
+ const errorString = validatedConfig.errors.join('\n');
994
+ const errorMessage = 'Invalid Vertex AI configuration:\n' + errorString;
995
+ logger$1.error(errorMessage);
996
+ throw new Error(errorMessage);
997
+ }
998
+ logger$1.info('Vertex AI configuration loaded successfully', {
999
+ projectId: validatedConfig.projectId,
1000
+ region: validatedConfig.region,
1001
+ modelCount: ((_b = validatedConfig.modelNames) === null || _b === void 0 ? void 0 : _b.length) || 0,
1002
+ models: validatedConfig.modelNames,
1003
+ });
1004
+ return validatedConfig;
1005
+ }
1006
+
859
1007
  /**
860
1008
  * Loads custom config endpoints
861
1009
  * @param [config]
@@ -878,12 +1026,24 @@ const loadEndpoints = (config, agentsDefaults) => {
878
1026
  loadedEndpoints[librechatDataProvider.EModelEndpoint.assistants] = assistantsConfigSetup(config, librechatDataProvider.EModelEndpoint.assistants, loadedEndpoints[librechatDataProvider.EModelEndpoint.assistants]);
879
1027
  }
880
1028
  loadedEndpoints[librechatDataProvider.EModelEndpoint.agents] = agentsConfigSetup(config, agentsDefaults);
1029
+ // Handle Anthropic endpoint with Vertex AI configuration
1030
+ if (endpoints === null || endpoints === void 0 ? void 0 : endpoints[librechatDataProvider.EModelEndpoint.anthropic]) {
1031
+ const anthropicConfig = endpoints[librechatDataProvider.EModelEndpoint.anthropic];
1032
+ const vertexConfig = vertexConfigSetup(config);
1033
+ loadedEndpoints[librechatDataProvider.EModelEndpoint.anthropic] = {
1034
+ ...anthropicConfig,
1035
+ // If Vertex AI is enabled, use the visible model names from vertex config
1036
+ // Otherwise, use the models array from anthropic config
1037
+ ...((vertexConfig === null || vertexConfig === void 0 ? void 0 : vertexConfig.modelNames) && { models: vertexConfig.modelNames }),
1038
+ // Attach validated Vertex AI config if present
1039
+ ...(vertexConfig && { vertexConfig }),
1040
+ };
1041
+ }
881
1042
  const endpointKeys = [
882
1043
  librechatDataProvider.EModelEndpoint.openAI,
883
1044
  librechatDataProvider.EModelEndpoint.google,
884
1045
  librechatDataProvider.EModelEndpoint.custom,
885
1046
  librechatDataProvider.EModelEndpoint.bedrock,
886
- librechatDataProvider.EModelEndpoint.anthropic,
887
1047
  ];
888
1048
  endpointKeys.forEach((key) => {
889
1049
  const currentKey = key;
@@ -938,7 +1098,9 @@ const AppService = async (params) => {
938
1098
  const imageOutputType = (_e = config === null || config === void 0 ? void 0 : config.imageOutputType) !== null && _e !== void 0 ? _e : configDefaults.imageOutputType;
939
1099
  process.env.CDN_PROVIDER = fileStrategy;
940
1100
  const availableTools = systemTools;
941
- const mcpConfig = config.mcpServers || null;
1101
+ const mcpServersConfig = config.mcpServers || null;
1102
+ const mcpSettings = config.mcpSettings || null;
1103
+ const actions = config.actions;
942
1104
  const registration = (_f = config.registration) !== null && _f !== void 0 ? _f : configDefaults.registration;
943
1105
  const interfaceConfig = await loadDefaultInterface({ config, configDefaults });
944
1106
  const turnstileConfig = loadTurnstileConfig(config, configDefaults);
@@ -950,8 +1112,10 @@ const AppService = async (params) => {
950
1112
  memory,
951
1113
  speech,
952
1114
  balance,
1115
+ actions,
953
1116
  transactions,
954
- mcpConfig,
1117
+ mcpConfig: mcpServersConfig,
1118
+ mcpSettings,
955
1119
  webSearch,
956
1120
  fileStrategy,
957
1121
  registration,
@@ -999,14 +1163,144 @@ exports.RoleBits = void 0;
999
1163
  RoleBits[RoleBits["OWNER"] = librechatDataProvider.PermissionBits.VIEW | librechatDataProvider.PermissionBits.EDIT | librechatDataProvider.PermissionBits.DELETE | librechatDataProvider.PermissionBits.SHARE] = "OWNER";
1000
1164
  })(exports.RoleBits || (exports.RoleBits = {}));
1001
1165
 
1166
+ var _a, _b;
1167
+ const { webcrypto } = crypto;
1168
+ /** Use hex decoding for both key and IV for legacy methods */
1169
+ const key = Buffer.from((_a = process.env.CREDS_KEY) !== null && _a !== void 0 ? _a : '', 'hex');
1170
+ const iv = Buffer.from((_b = process.env.CREDS_IV) !== null && _b !== void 0 ? _b : '', 'hex');
1171
+ const algorithm = 'AES-CBC';
1002
1172
  async function signPayload({ payload, secret, expirationTime, }) {
1003
1173
  return jwt.sign(payload, secret, { expiresIn: expirationTime });
1004
1174
  }
1005
1175
  async function hashToken(str) {
1006
1176
  const data = new TextEncoder().encode(str);
1007
- const hashBuffer = await node_crypto.webcrypto.subtle.digest('SHA-256', data);
1177
+ const hashBuffer = await webcrypto.subtle.digest('SHA-256', data);
1008
1178
  return Buffer.from(hashBuffer).toString('hex');
1009
1179
  }
1180
+ /** --- Legacy v1/v2 Setup: AES-CBC with fixed key and IV --- */
1181
+ /**
1182
+ * Encrypts a value using AES-CBC
1183
+ * @param value - The plaintext to encrypt
1184
+ * @returns The encrypted string in hex format
1185
+ */
1186
+ async function encrypt(value) {
1187
+ const cryptoKey = await webcrypto.subtle.importKey('raw', key, { name: algorithm }, false, [
1188
+ 'encrypt',
1189
+ ]);
1190
+ const encoder = new TextEncoder();
1191
+ const data = encoder.encode(value);
1192
+ const encryptedBuffer = await webcrypto.subtle.encrypt({ name: algorithm, iv: iv }, cryptoKey, data);
1193
+ return Buffer.from(encryptedBuffer).toString('hex');
1194
+ }
1195
+ /**
1196
+ * Decrypts an encrypted value using AES-CBC
1197
+ * @param encryptedValue - The encrypted string in hex format
1198
+ * @returns The decrypted plaintext
1199
+ */
1200
+ async function decrypt(encryptedValue) {
1201
+ const cryptoKey = await webcrypto.subtle.importKey('raw', key, { name: algorithm }, false, [
1202
+ 'decrypt',
1203
+ ]);
1204
+ const encryptedBuffer = Buffer.from(encryptedValue, 'hex');
1205
+ const decryptedBuffer = await webcrypto.subtle.decrypt({ name: algorithm, iv: iv }, cryptoKey, encryptedBuffer);
1206
+ const decoder = new TextDecoder();
1207
+ return decoder.decode(decryptedBuffer);
1208
+ }
1209
+ /** --- v2: AES-CBC with a random IV per encryption --- */
1210
+ /**
1211
+ * Encrypts a value using AES-CBC with a random IV per encryption
1212
+ * @param value - The plaintext to encrypt
1213
+ * @returns The encrypted string with IV prepended (iv:ciphertext format)
1214
+ */
1215
+ async function encryptV2(value) {
1216
+ const gen_iv = webcrypto.getRandomValues(new Uint8Array(16));
1217
+ const cryptoKey = await webcrypto.subtle.importKey('raw', key, { name: algorithm }, false, [
1218
+ 'encrypt',
1219
+ ]);
1220
+ const encoder = new TextEncoder();
1221
+ const data = encoder.encode(value);
1222
+ const encryptedBuffer = await webcrypto.subtle.encrypt({ name: algorithm, iv: gen_iv }, cryptoKey, data);
1223
+ return Buffer.from(gen_iv).toString('hex') + ':' + Buffer.from(encryptedBuffer).toString('hex');
1224
+ }
1225
+ /**
1226
+ * Decrypts an encrypted value using AES-CBC with random IV
1227
+ * @param encryptedValue - The encrypted string in iv:ciphertext format
1228
+ * @returns The decrypted plaintext
1229
+ */
1230
+ async function decryptV2(encryptedValue) {
1231
+ var _a;
1232
+ const parts = encryptedValue.split(':');
1233
+ if (parts.length === 1) {
1234
+ return parts[0];
1235
+ }
1236
+ const gen_iv = Buffer.from((_a = parts.shift()) !== null && _a !== void 0 ? _a : '', 'hex');
1237
+ const encrypted = parts.join(':');
1238
+ const cryptoKey = await webcrypto.subtle.importKey('raw', key, { name: algorithm }, false, [
1239
+ 'decrypt',
1240
+ ]);
1241
+ const encryptedBuffer = Buffer.from(encrypted, 'hex');
1242
+ const decryptedBuffer = await webcrypto.subtle.decrypt({ name: algorithm, iv: gen_iv }, cryptoKey, encryptedBuffer);
1243
+ const decoder = new TextDecoder();
1244
+ return decoder.decode(decryptedBuffer);
1245
+ }
1246
+ /** --- v3: AES-256-CTR using Node's crypto functions --- */
1247
+ const algorithm_v3 = 'aes-256-ctr';
1248
+ /**
1249
+ * Encrypts a value using AES-256-CTR.
1250
+ * Note: AES-256 requires a 32-byte key. Ensure that process.env.CREDS_KEY is a 64-character hex string.
1251
+ * @param value - The plaintext to encrypt.
1252
+ * @returns The encrypted string with a "v3:" prefix.
1253
+ */
1254
+ function encryptV3(value) {
1255
+ if (key.length !== 32) {
1256
+ throw new Error(`Invalid key length: expected 32 bytes, got ${key.length} bytes`);
1257
+ }
1258
+ const iv_v3 = crypto.randomBytes(16);
1259
+ const cipher = crypto.createCipheriv(algorithm_v3, key, iv_v3);
1260
+ const encrypted = Buffer.concat([cipher.update(value, 'utf8'), cipher.final()]);
1261
+ return `v3:${iv_v3.toString('hex')}:${encrypted.toString('hex')}`;
1262
+ }
1263
+ /**
1264
+ * Decrypts an encrypted value using AES-256-CTR.
1265
+ * @param encryptedValue - The encrypted string with "v3:" prefix.
1266
+ * @returns The decrypted plaintext.
1267
+ */
1268
+ function decryptV3(encryptedValue) {
1269
+ const parts = encryptedValue.split(':');
1270
+ if (parts[0] !== 'v3') {
1271
+ throw new Error('Not a v3 encrypted value');
1272
+ }
1273
+ const iv_v3 = Buffer.from(parts[1], 'hex');
1274
+ const encryptedText = Buffer.from(parts.slice(2).join(':'), 'hex');
1275
+ const decipher = crypto.createDecipheriv(algorithm_v3, key, iv_v3);
1276
+ const decrypted = Buffer.concat([decipher.update(encryptedText), decipher.final()]);
1277
+ return decrypted.toString('utf8');
1278
+ }
1279
+ /**
1280
+ * Generates random values as a hex string
1281
+ * @param length - The number of random bytes to generate
1282
+ * @returns The random values as a hex string
1283
+ */
1284
+ async function getRandomValues(length) {
1285
+ if (!Number.isInteger(length) || length <= 0) {
1286
+ throw new Error('Length must be a positive integer');
1287
+ }
1288
+ const randomValues = new Uint8Array(length);
1289
+ webcrypto.getRandomValues(randomValues);
1290
+ return Buffer.from(randomValues).toString('hex');
1291
+ }
1292
+ /**
1293
+ * Computes SHA-256 hash for the given input.
1294
+ * @param input - The input to hash.
1295
+ * @returns The SHA-256 hash of the input.
1296
+ */
1297
+ async function hashBackupCode(input) {
1298
+ const encoder = new TextEncoder();
1299
+ const data = encoder.encode(input);
1300
+ const hashBuffer = await webcrypto.subtle.digest('SHA-256', data);
1301
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
1302
+ return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
1303
+ }
1010
1304
 
1011
1305
  // Define the Auth sub-schema with type-safety.
1012
1306
  const AuthSchema = new mongoose.Schema({
@@ -1162,10 +1456,17 @@ const agentSchema = new mongoose.Schema({
1162
1456
  default: false,
1163
1457
  index: true,
1164
1458
  },
1459
+ /** MCP server names extracted from tools for efficient querying */
1460
+ mcpServerNames: {
1461
+ type: [String],
1462
+ default: [],
1463
+ index: true,
1464
+ },
1165
1465
  }, {
1166
1466
  timestamps: true,
1167
1467
  });
1168
1468
  agentSchema.index({ updatedAt: -1, _id: 1 });
1469
+ agentSchema.index({ 'edges.to': 1 });
1169
1470
 
1170
1471
  const agentCategorySchema = new mongoose.Schema({
1171
1472
  value: {
@@ -1301,6 +1602,10 @@ const bannerSchema = new mongoose.Schema({
1301
1602
  type: Boolean,
1302
1603
  default: false,
1303
1604
  },
1605
+ persistable: {
1606
+ type: Boolean,
1607
+ default: false,
1608
+ },
1304
1609
  }, { timestamps: true });
1305
1610
 
1306
1611
  const categoriesSchema = new mongoose.Schema({
@@ -1344,7 +1649,6 @@ conversationTag.index({ tag: 1, user: 1 }, { unique: true });
1344
1649
 
1345
1650
  // @ts-ignore
1346
1651
  const conversationPreset = {
1347
- // endpoint: [azureOpenAI, openAI, anthropic, chatGPTBrowser]
1348
1652
  endpoint: {
1349
1653
  type: String,
1350
1654
  default: null,
@@ -1353,7 +1657,7 @@ const conversationPreset = {
1353
1657
  endpointType: {
1354
1658
  type: String,
1355
1659
  },
1356
- // for azureOpenAI, openAI, chatGPTBrowser only
1660
+ // for azureOpenAI, openAI only
1357
1661
  model: {
1358
1662
  type: String,
1359
1663
  required: false,
@@ -1518,9 +1822,6 @@ const convoSchema = new mongoose.Schema({
1518
1822
  meiliIndex: true,
1519
1823
  },
1520
1824
  messages: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Message' }],
1521
- agentOptions: {
1522
- type: mongoose.Schema.Types.Mixed,
1523
- },
1524
1825
  ...conversationPreset,
1525
1826
  agent_id: {
1526
1827
  type: String,
@@ -1540,6 +1841,8 @@ const convoSchema = new mongoose.Schema({
1540
1841
  convoSchema.index({ expiredAt: 1 }, { expireAfterSeconds: 0 });
1541
1842
  convoSchema.index({ createdAt: 1, updatedAt: 1 });
1542
1843
  convoSchema.index({ conversationId: 1, user: 1 }, { unique: true });
1844
+ // index for MeiliSearch sync operations
1845
+ convoSchema.index({ _meiliIndex: 1, expiredAt: 1 });
1543
1846
 
1544
1847
  const file = new mongoose.Schema({
1545
1848
  user: {
@@ -1736,25 +2039,6 @@ const messageSchema = new mongoose.Schema({
1736
2039
  default: false,
1737
2040
  },
1738
2041
  files: { type: [{ type: mongoose.Schema.Types.Mixed }], default: undefined },
1739
- plugin: {
1740
- type: {
1741
- latest: {
1742
- type: String,
1743
- required: false,
1744
- },
1745
- inputs: {
1746
- type: [mongoose.Schema.Types.Mixed],
1747
- required: false,
1748
- default: undefined,
1749
- },
1750
- outputs: {
1751
- type: String,
1752
- required: false,
1753
- },
1754
- },
1755
- default: undefined,
1756
- },
1757
- plugins: { type: [{ type: mongoose.Schema.Types.Mixed }], default: undefined },
1758
2042
  content: {
1759
2043
  type: [{ type: mongoose.Schema.Types.Mixed }],
1760
2044
  default: undefined,
@@ -1794,10 +2078,16 @@ const messageSchema = new mongoose.Schema({
1794
2078
  expiredAt: {
1795
2079
  type: Date,
1796
2080
  },
2081
+ addedConvo: {
2082
+ type: Boolean,
2083
+ default: undefined,
2084
+ },
1797
2085
  }, { timestamps: true });
1798
2086
  messageSchema.index({ expiredAt: 1 }, { expireAfterSeconds: 0 });
1799
2087
  messageSchema.index({ createdAt: 1 });
1800
2088
  messageSchema.index({ messageId: 1, user: 1 }, { unique: true });
2089
+ // index for MeiliSearch sync operations
2090
+ messageSchema.index({ _meiliIndex: 1, expiredAt: 1 });
1801
2091
 
1802
2092
  const pluginAuthSchema = new mongoose.Schema({
1803
2093
  authField: {
@@ -1840,10 +2130,6 @@ const presetSchema = new mongoose.Schema({
1840
2130
  type: Number,
1841
2131
  },
1842
2132
  ...conversationPreset,
1843
- agentOptions: {
1844
- type: mongoose.Schema.Types.Mixed,
1845
- default: null,
1846
- },
1847
2133
  }, { timestamps: true });
1848
2134
 
1849
2135
  const projectSchema = new mongoose.Schema({
@@ -2002,6 +2288,11 @@ const rolePermissionsSchema = new mongoose.Schema({
2002
2288
  [librechatDataProvider.PermissionTypes.FILE_CITATIONS]: {
2003
2289
  [librechatDataProvider.Permissions.USE]: { type: Boolean },
2004
2290
  },
2291
+ [librechatDataProvider.PermissionTypes.MCP_SERVERS]: {
2292
+ [librechatDataProvider.Permissions.USE]: { type: Boolean },
2293
+ [librechatDataProvider.Permissions.CREATE]: { type: Boolean },
2294
+ [librechatDataProvider.Permissions.SHARE]: { type: Boolean },
2295
+ },
2005
2296
  }, { _id: false });
2006
2297
  const roleSchema = new mongoose.Schema({
2007
2298
  name: { type: String, required: true, unique: true, index: true },
@@ -2145,6 +2436,7 @@ const transactionSchema = new mongoose.Schema({
2145
2436
  },
2146
2437
  model: {
2147
2438
  type: String,
2439
+ index: true,
2148
2440
  },
2149
2441
  context: {
2150
2442
  type: String,
@@ -2292,6 +2584,17 @@ const userSchema = new mongoose.Schema({
2292
2584
  },
2293
2585
  default: {},
2294
2586
  },
2587
+ favorites: {
2588
+ type: [
2589
+ {
2590
+ _id: false,
2591
+ agentId: String, // for agent
2592
+ model: String, // for model
2593
+ endpoint: String, // for model
2594
+ },
2595
+ ],
2596
+ default: [],
2597
+ },
2295
2598
  /** Field for external source identification (for consistency with TPrincipal schema) */
2296
2599
  idOnTheSource: {
2297
2600
  type: String,
@@ -2511,26 +2814,6 @@ const getSyncConfig = () => ({
2511
2814
  batchSize: parseInt(process.env.MEILI_SYNC_BATCH_SIZE || '100', 10),
2512
2815
  delayMs: parseInt(process.env.MEILI_SYNC_DELAY_MS || '100', 10),
2513
2816
  });
2514
- /**
2515
- * Local implementation of parseTextParts to avoid dependency on librechat-data-provider
2516
- * Extracts text content from an array of content items
2517
- */
2518
- const parseTextParts = (content) => {
2519
- if (!Array.isArray(content)) {
2520
- return '';
2521
- }
2522
- return content
2523
- .filter((item) => item.type === 'text' && typeof item.text === 'string')
2524
- .map((item) => item.text)
2525
- .join(' ')
2526
- .trim();
2527
- };
2528
- /**
2529
- * Local implementation to handle Bing convoId conversion
2530
- */
2531
- const cleanUpPrimaryKeyValue = (value) => {
2532
- return value.replace(/--/g, '|');
2533
- };
2534
2817
  /**
2535
2818
  * Validates the required options for configuring the mongoMeili plugin.
2536
2819
  */
@@ -2592,7 +2875,8 @@ const createMeiliMongooseModel = ({ index, attributesToIndex, syncOptions, }) =>
2592
2875
  const { batchSize, delayMs } = syncConfig;
2593
2876
  logger.info(`[syncWithMeili] Starting sync for ${primaryKey === 'messageId' ? 'messages' : 'conversations'} with batch size ${batchSize}`);
2594
2877
  // Build query with resume capability
2595
- const query = {};
2878
+ // Do not sync TTL documents
2879
+ const query = { expiredAt: null };
2596
2880
  if (options === null || options === void 0 ? void 0 : options.resumeFromId) {
2597
2881
  query._id = { $gt: options.resumeFromId };
2598
2882
  }
@@ -2720,7 +3004,7 @@ const createMeiliMongooseModel = ({ index, attributesToIndex, syncOptions, }) =>
2720
3004
  const data = await index.search(q, params);
2721
3005
  if (populate) {
2722
3006
  const query = {};
2723
- query[primaryKey] = _.map(data.hits, (hit) => cleanUpPrimaryKeyValue(hit[primaryKey]));
3007
+ query[primaryKey] = _.map(data.hits, (hit) => hit[primaryKey]);
2724
3008
  const projection = Object.keys(this.schema.obj).reduce((results, key) => {
2725
3009
  if (!key.startsWith('$')) {
2726
3010
  results[key] = 1;
@@ -2754,7 +3038,7 @@ const createMeiliMongooseModel = ({ index, attributesToIndex, syncOptions, }) =>
2754
3038
  object.conversationId = object.conversationId.replace(/\|/g, '--');
2755
3039
  }
2756
3040
  if (object.content && Array.isArray(object.content)) {
2757
- object.text = parseTextParts(object.content);
3041
+ object.text = librechatDataProvider.parseTextParts(object.content);
2758
3042
  delete object.content;
2759
3043
  }
2760
3044
  return object;
@@ -2763,6 +3047,10 @@ const createMeiliMongooseModel = ({ index, attributesToIndex, syncOptions, }) =>
2763
3047
  * Adds the current document to the MeiliSearch index with retry logic
2764
3048
  */
2765
3049
  async addObjectToMeili(next) {
3050
+ // If this conversation or message has a TTL, don't index it
3051
+ if (!_.isNil(this.expiredAt)) {
3052
+ return next();
3053
+ }
2766
3054
  const object = this.preprocessObjectForIndex();
2767
3055
  const maxRetries = 3;
2768
3056
  let retryCount = 0;
@@ -3077,7 +3365,38 @@ function createAgentModel(mongoose) {
3077
3365
  * Creates or returns the AgentCategory model using the provided mongoose instance and schema
3078
3366
  */
3079
3367
  function createAgentCategoryModel(mongoose) {
3080
- return mongoose.models.AgentCategory || mongoose.model('AgentCategory', agentCategorySchema);
3368
+ return (mongoose.models.AgentCategory ||
3369
+ mongoose.model('AgentCategory', agentCategorySchema));
3370
+ }
3371
+
3372
+ const mcpServerSchema = new mongoose.Schema({
3373
+ serverName: {
3374
+ type: String,
3375
+ index: true,
3376
+ unique: true,
3377
+ required: true,
3378
+ },
3379
+ config: {
3380
+ type: mongoose.Schema.Types.Mixed,
3381
+ required: true,
3382
+ // Config contains: title, description, url, oauth, etc.
3383
+ },
3384
+ author: {
3385
+ type: mongoose.Schema.Types.ObjectId,
3386
+ ref: 'User',
3387
+ required: true,
3388
+ index: true,
3389
+ },
3390
+ }, {
3391
+ timestamps: true,
3392
+ });
3393
+ mcpServerSchema.index({ updatedAt: -1, _id: 1 });
3394
+
3395
+ /**
3396
+ * Creates or returns the MCPServer model using the provided mongoose instance and schema
3397
+ */
3398
+ function createMCPServerModel(mongoose) {
3399
+ return (mongoose.models.MCPServer || mongoose.model('MCPServer', mcpServerSchema));
3081
3400
  }
3082
3401
 
3083
3402
  /**
@@ -3205,7 +3524,7 @@ const accessRoleSchema = new mongoose.Schema({
3205
3524
  description: String,
3206
3525
  resourceType: {
3207
3526
  type: String,
3208
- enum: ['agent', 'project', 'file', 'promptGroup'],
3527
+ enum: ['agent', 'project', 'file', 'promptGroup', 'mcpServer'],
3209
3528
  required: true,
3210
3529
  default: 'agent',
3211
3530
  },
@@ -3306,6 +3625,7 @@ function createModels(mongoose) {
3306
3625
  Message: createMessageModel(mongoose),
3307
3626
  Agent: createAgentModel(mongoose),
3308
3627
  AgentCategory: createAgentCategoryModel(mongoose),
3628
+ MCPServer: createMCPServerModel(mongoose),
3309
3629
  Role: createRoleModel(mongoose),
3310
3630
  Action: createActionModel(mongoose),
3311
3631
  Assistant: createAssistantModel(mongoose),
@@ -3328,7 +3648,6 @@ function createModels(mongoose) {
3328
3648
  };
3329
3649
  }
3330
3650
 
3331
- var _a;
3332
3651
  class SessionError extends Error {
3333
3652
  constructor(message, code = 'SESSION_ERROR') {
3334
3653
  super(message);
@@ -3336,22 +3655,24 @@ class SessionError extends Error {
3336
3655
  this.code = code;
3337
3656
  }
3338
3657
  }
3339
- const { REFRESH_TOKEN_EXPIRY } = (_a = process.env) !== null && _a !== void 0 ? _a : {};
3340
- const expires = REFRESH_TOKEN_EXPIRY ? eval(REFRESH_TOKEN_EXPIRY) : 1000 * 60 * 60 * 24 * 7; // 7 days default
3658
+ /** Default refresh token expiry: 7 days in milliseconds */
3659
+ const DEFAULT_REFRESH_TOKEN_EXPIRY = 1000 * 60 * 60 * 24 * 7;
3341
3660
  // Factory function that takes mongoose instance and returns the methods
3342
3661
  function createSessionMethods(mongoose) {
3343
3662
  /**
3344
3663
  * Creates a new session for a user
3345
3664
  */
3346
3665
  async function createSession(userId, options = {}) {
3666
+ var _a;
3347
3667
  if (!userId) {
3348
3668
  throw new SessionError('User ID is required', 'INVALID_USER_ID');
3349
3669
  }
3670
+ const expiresIn = (_a = options.expiresIn) !== null && _a !== void 0 ? _a : DEFAULT_REFRESH_TOKEN_EXPIRY;
3350
3671
  try {
3351
3672
  const Session = mongoose.models.Session;
3352
3673
  const currentSession = new Session({
3353
3674
  user: userId,
3354
- expiration: options.expiration || new Date(Date.now() + expires),
3675
+ expiration: options.expiration || new Date(Date.now() + expiresIn),
3355
3676
  });
3356
3677
  const refreshToken = await generateRefreshToken(currentSession);
3357
3678
  return { session: currentSession, refreshToken };
@@ -3405,14 +3726,16 @@ function createSessionMethods(mongoose) {
3405
3726
  /**
3406
3727
  * Updates session expiration
3407
3728
  */
3408
- async function updateExpiration(session, newExpiration) {
3729
+ async function updateExpiration(session, newExpiration, options = {}) {
3730
+ var _a;
3731
+ const expiresIn = (_a = options.expiresIn) !== null && _a !== void 0 ? _a : DEFAULT_REFRESH_TOKEN_EXPIRY;
3409
3732
  try {
3410
3733
  const Session = mongoose.models.Session;
3411
3734
  const sessionDoc = typeof session === 'string' ? await Session.findById(session) : session;
3412
3735
  if (!sessionDoc) {
3413
3736
  throw new SessionError('Session not found', 'SESSION_NOT_FOUND');
3414
3737
  }
3415
- sessionDoc.expiration = newExpiration || new Date(Date.now() + expires);
3738
+ sessionDoc.expiration = newExpiration || new Date(Date.now() + expiresIn);
3416
3739
  return await sessionDoc.save();
3417
3740
  }
3418
3741
  catch (error) {
@@ -3483,7 +3806,9 @@ function createSessionMethods(mongoose) {
3483
3806
  throw new SessionError('Invalid session object', 'INVALID_SESSION');
3484
3807
  }
3485
3808
  try {
3486
- const expiresIn = session.expiration ? session.expiration.getTime() : Date.now() + expires;
3809
+ const expiresIn = session.expiration
3810
+ ? session.expiration.getTime()
3811
+ : Date.now() + DEFAULT_REFRESH_TOKEN_EXPIRY;
3487
3812
  if (!session.expiration) {
3488
3813
  session.expiration = new Date(expiresIn);
3489
3814
  }
@@ -3689,6 +4014,8 @@ function createRoleMethods(mongoose) {
3689
4014
  };
3690
4015
  }
3691
4016
 
4017
+ /** Default JWT session expiry: 15 minutes in milliseconds */
4018
+ const DEFAULT_SESSION_EXPIRY = 1000 * 60 * 15;
3692
4019
  /** Factory function that takes mongoose instance and returns the methods */
3693
4020
  function createUserMethods(mongoose) {
3694
4021
  /**
@@ -3814,23 +4141,14 @@ function createUserMethods(mongoose) {
3814
4141
  }
3815
4142
  /**
3816
4143
  * Generates a JWT token for a given user.
4144
+ * @param user - The user object
4145
+ * @param expiresIn - Optional expiry time in milliseconds. Default: 15 minutes
3817
4146
  */
3818
- async function generateToken(user) {
4147
+ async function generateToken(user, expiresIn) {
3819
4148
  if (!user) {
3820
4149
  throw new Error('No user provided');
3821
4150
  }
3822
- let expires = 1000 * 60 * 15;
3823
- if (process.env.SESSION_EXPIRY !== undefined && process.env.SESSION_EXPIRY !== '') {
3824
- try {
3825
- const evaluated = eval(process.env.SESSION_EXPIRY);
3826
- if (evaluated) {
3827
- expires = evaluated;
3828
- }
3829
- }
3830
- catch (error) {
3831
- console.warn('Invalid SESSION_EXPIRY expression, using default:', error);
3832
- }
3833
- }
4151
+ const expires = expiresIn !== null && expiresIn !== void 0 ? expiresIn : DEFAULT_SESSION_EXPIRY;
3834
4152
  return await signPayload({
3835
4153
  payload: {
3836
4154
  id: user._id,
@@ -3924,6 +4242,26 @@ function createUserMethods(mongoose) {
3924
4242
  return userWithoutScore;
3925
4243
  });
3926
4244
  };
4245
+ /**
4246
+ * Updates the plugins for a user based on the action specified (install/uninstall).
4247
+ * @param userId - The user ID whose plugins are to be updated
4248
+ * @param plugins - The current plugins array
4249
+ * @param pluginKey - The key of the plugin to install or uninstall
4250
+ * @param action - The action to perform, 'install' or 'uninstall'
4251
+ * @returns The result of the update operation or null if action is invalid
4252
+ */
4253
+ async function updateUserPlugins(userId, plugins, pluginKey, action) {
4254
+ const userPlugins = plugins !== null && plugins !== void 0 ? plugins : [];
4255
+ if (action === 'install') {
4256
+ return updateUser(userId, { plugins: [...userPlugins, pluginKey] });
4257
+ }
4258
+ if (action === 'uninstall') {
4259
+ return updateUser(userId, {
4260
+ plugins: userPlugins.filter((plugin) => plugin !== pluginKey),
4261
+ });
4262
+ }
4263
+ return null;
4264
+ }
3927
4265
  return {
3928
4266
  findUser,
3929
4267
  countUsers,
@@ -3933,10 +4271,356 @@ function createUserMethods(mongoose) {
3933
4271
  getUserById,
3934
4272
  generateToken,
3935
4273
  deleteUserById,
4274
+ updateUserPlugins,
3936
4275
  toggleUserMemories,
3937
4276
  };
3938
4277
  }
3939
4278
 
4279
+ /** Factory function that takes mongoose instance and returns the key methods */
4280
+ function createKeyMethods(mongoose) {
4281
+ /**
4282
+ * Retrieves and decrypts the key value for a given user identified by userId and identifier name.
4283
+ * @param params - The parameters object
4284
+ * @param params.userId - The unique identifier for the user
4285
+ * @param params.name - The name associated with the key
4286
+ * @returns The decrypted key value
4287
+ * @throws Error if the key is not found or if there is a problem during key retrieval
4288
+ * @description This function searches for a user's key in the database using their userId and name.
4289
+ * If found, it decrypts the value of the key and returns it. If no key is found, it throws
4290
+ * an error indicating that there is no user key available.
4291
+ */
4292
+ async function getUserKey(params) {
4293
+ const { userId, name } = params;
4294
+ const Key = mongoose.models.Key;
4295
+ const keyValue = (await Key.findOne({ userId, name }).lean());
4296
+ if (!keyValue) {
4297
+ throw new Error(JSON.stringify({
4298
+ type: librechatDataProvider.ErrorTypes.NO_USER_KEY,
4299
+ }));
4300
+ }
4301
+ return await decrypt(keyValue.value);
4302
+ }
4303
+ /**
4304
+ * Retrieves, decrypts, and parses the key values for a given user identified by userId and name.
4305
+ * @param params - The parameters object
4306
+ * @param params.userId - The unique identifier for the user
4307
+ * @param params.name - The name associated with the key
4308
+ * @returns The decrypted and parsed key values
4309
+ * @throws Error if the key is invalid or if there is a problem during key value parsing
4310
+ * @description This function retrieves a user's encrypted key using their userId and name, decrypts it,
4311
+ * and then attempts to parse the decrypted string into a JSON object. If the parsing fails,
4312
+ * it throws an error indicating that the user key is invalid.
4313
+ */
4314
+ async function getUserKeyValues(params) {
4315
+ const { userId, name } = params;
4316
+ const userValues = await getUserKey({ userId, name });
4317
+ try {
4318
+ return JSON.parse(userValues);
4319
+ }
4320
+ catch (e) {
4321
+ logger$1.error('[getUserKeyValues]', e);
4322
+ throw new Error(JSON.stringify({
4323
+ type: librechatDataProvider.ErrorTypes.INVALID_USER_KEY,
4324
+ }));
4325
+ }
4326
+ }
4327
+ /**
4328
+ * Retrieves the expiry information of a user's key identified by userId and name.
4329
+ * @param params - The parameters object
4330
+ * @param params.userId - The unique identifier for the user
4331
+ * @param params.name - The name associated with the key
4332
+ * @returns The expiry date of the key or null if the key doesn't exist
4333
+ * @description This function fetches a user's key from the database using their userId and name and
4334
+ * returns its expiry date. If the key is not found, it returns null for the expiry date.
4335
+ */
4336
+ async function getUserKeyExpiry(params) {
4337
+ const { userId, name } = params;
4338
+ const Key = mongoose.models.Key;
4339
+ const keyValue = (await Key.findOne({ userId, name }).lean());
4340
+ if (!keyValue) {
4341
+ return { expiresAt: null };
4342
+ }
4343
+ return { expiresAt: keyValue.expiresAt || 'never' };
4344
+ }
4345
+ /**
4346
+ * Updates or inserts a new key for a given user identified by userId and name, with a specified value and expiry date.
4347
+ * @param params - The parameters object
4348
+ * @param params.userId - The unique identifier for the user
4349
+ * @param params.name - The name associated with the key
4350
+ * @param params.value - The value to be encrypted and stored as the key's value
4351
+ * @param params.expiresAt - The expiry date for the key [optional]
4352
+ * @returns The updated or newly inserted key document
4353
+ * @description This function either updates an existing user key or inserts a new one into the database,
4354
+ * after encrypting the provided value. It sets the provided expiry date for the key (or unsets for no expiry).
4355
+ */
4356
+ async function updateUserKey(params) {
4357
+ const { userId, name, value, expiresAt = null } = params;
4358
+ const Key = mongoose.models.Key;
4359
+ const encryptedValue = await encrypt(value);
4360
+ const updateObject = {
4361
+ userId,
4362
+ name,
4363
+ value: encryptedValue,
4364
+ };
4365
+ const updateQuery = {
4366
+ $set: updateObject,
4367
+ };
4368
+ if (expiresAt) {
4369
+ updateObject.expiresAt = new Date(expiresAt);
4370
+ }
4371
+ else {
4372
+ updateQuery.$unset = { expiresAt: '' };
4373
+ }
4374
+ return await Key.findOneAndUpdate({ userId, name }, updateQuery, {
4375
+ upsert: true,
4376
+ new: true,
4377
+ }).lean();
4378
+ }
4379
+ /**
4380
+ * Deletes a key or all keys for a given user identified by userId, optionally based on a specified name.
4381
+ * @param params - The parameters object
4382
+ * @param params.userId - The unique identifier for the user
4383
+ * @param params.name - The name associated with the key to delete. If not provided and all is true, deletes all keys
4384
+ * @param params.all - Whether to delete all keys for the user
4385
+ * @returns The result of the deletion operation
4386
+ * @description This function deletes a specific key or all keys for a user from the database.
4387
+ * If a name is provided and all is false, it deletes only the key with that name.
4388
+ * If all is true, it ignores the name and deletes all keys for the user.
4389
+ */
4390
+ async function deleteUserKey(params) {
4391
+ const { userId, name, all = false } = params;
4392
+ const Key = mongoose.models.Key;
4393
+ if (all) {
4394
+ return await Key.deleteMany({ userId });
4395
+ }
4396
+ return await Key.findOneAndDelete({ userId, name }).lean();
4397
+ }
4398
+ return {
4399
+ getUserKey,
4400
+ updateUserKey,
4401
+ deleteUserKey,
4402
+ getUserKeyValues,
4403
+ getUserKeyExpiry,
4404
+ };
4405
+ }
4406
+
4407
+ /** Factory function that takes mongoose instance and returns the file methods */
4408
+ function createFileMethods(mongoose) {
4409
+ /**
4410
+ * Finds a file by its file_id with additional query options.
4411
+ * @param file_id - The unique identifier of the file
4412
+ * @param options - Query options for filtering, projection, etc.
4413
+ * @returns A promise that resolves to the file document or null
4414
+ */
4415
+ async function findFileById(file_id, options = {}) {
4416
+ const File = mongoose.models.File;
4417
+ return File.findOne({ file_id, ...options }).lean();
4418
+ }
4419
+ /**
4420
+ * Retrieves files matching a given filter, sorted by the most recently updated.
4421
+ * @param filter - The filter criteria to apply
4422
+ * @param _sortOptions - Optional sort parameters
4423
+ * @param selectFields - Fields to include/exclude in the query results. Default excludes the 'text' field
4424
+ * @param options - Additional query options (userId, agentId for ACL)
4425
+ * @returns A promise that resolves to an array of file documents
4426
+ */
4427
+ async function getFiles(filter, _sortOptions, selectFields) {
4428
+ const File = mongoose.models.File;
4429
+ const sortOptions = { updatedAt: -1, ..._sortOptions };
4430
+ const query = File.find(filter);
4431
+ if (selectFields != null) {
4432
+ query.select(selectFields);
4433
+ }
4434
+ else {
4435
+ query.select({ text: 0 });
4436
+ }
4437
+ return await query.sort(sortOptions).lean();
4438
+ }
4439
+ /**
4440
+ * Retrieves tool files (files that are embedded or have a fileIdentifier) from an array of file IDs
4441
+ * @param fileIds - Array of file_id strings to search for
4442
+ * @param toolResourceSet - Optional filter for tool resources
4443
+ * @returns Files that match the criteria
4444
+ */
4445
+ async function getToolFilesByIds(fileIds, toolResourceSet) {
4446
+ var _a, _b, _c;
4447
+ if (!fileIds || !fileIds.length || !(toolResourceSet === null || toolResourceSet === void 0 ? void 0 : toolResourceSet.size)) {
4448
+ return [];
4449
+ }
4450
+ try {
4451
+ const filter = {
4452
+ file_id: { $in: fileIds },
4453
+ $or: [],
4454
+ };
4455
+ if (toolResourceSet.has(librechatDataProvider.EToolResources.context)) {
4456
+ (_a = filter.$or) === null || _a === void 0 ? void 0 : _a.push({ text: { $exists: true, $ne: null }, context: librechatDataProvider.FileContext.agents });
4457
+ }
4458
+ if (toolResourceSet.has(librechatDataProvider.EToolResources.file_search)) {
4459
+ (_b = filter.$or) === null || _b === void 0 ? void 0 : _b.push({ embedded: true });
4460
+ }
4461
+ if (toolResourceSet.has(librechatDataProvider.EToolResources.execute_code)) {
4462
+ (_c = filter.$or) === null || _c === void 0 ? void 0 : _c.push({ 'metadata.fileIdentifier': { $exists: true } });
4463
+ }
4464
+ const selectFields = { text: 0 };
4465
+ const sortOptions = { updatedAt: -1 };
4466
+ const results = await getFiles(filter, sortOptions, selectFields);
4467
+ return results !== null && results !== void 0 ? results : [];
4468
+ }
4469
+ catch (error) {
4470
+ logger$1.error('[getToolFilesByIds] Error retrieving tool files:', error);
4471
+ throw new Error('Error retrieving tool files');
4472
+ }
4473
+ }
4474
+ /**
4475
+ * Creates a new file with a TTL of 1 hour.
4476
+ * @param data - The file data to be created, must contain file_id
4477
+ * @param disableTTL - Whether to disable the TTL
4478
+ * @returns A promise that resolves to the created file document
4479
+ */
4480
+ async function createFile(data, disableTTL) {
4481
+ const File = mongoose.models.File;
4482
+ const fileData = {
4483
+ ...data,
4484
+ expiresAt: new Date(Date.now() + 3600 * 1000),
4485
+ };
4486
+ if (disableTTL) {
4487
+ delete fileData.expiresAt;
4488
+ }
4489
+ return File.findOneAndUpdate({ file_id: data.file_id }, fileData, {
4490
+ new: true,
4491
+ upsert: true,
4492
+ }).lean();
4493
+ }
4494
+ /**
4495
+ * Updates a file identified by file_id with new data and removes the TTL.
4496
+ * @param data - The data to update, must contain file_id
4497
+ * @returns A promise that resolves to the updated file document
4498
+ */
4499
+ async function updateFile(data) {
4500
+ const File = mongoose.models.File;
4501
+ const { file_id, ...update } = data;
4502
+ const updateOperation = {
4503
+ $set: update,
4504
+ $unset: { expiresAt: '' },
4505
+ };
4506
+ return File.findOneAndUpdate({ file_id }, updateOperation, {
4507
+ new: true,
4508
+ }).lean();
4509
+ }
4510
+ /**
4511
+ * Increments the usage of a file identified by file_id.
4512
+ * @param data - The data to update, must contain file_id and the increment value for usage
4513
+ * @returns A promise that resolves to the updated file document
4514
+ */
4515
+ async function updateFileUsage(data) {
4516
+ const File = mongoose.models.File;
4517
+ const { file_id, inc = 1 } = data;
4518
+ const updateOperation = {
4519
+ $inc: { usage: inc },
4520
+ $unset: { expiresAt: '', temp_file_id: '' },
4521
+ };
4522
+ return File.findOneAndUpdate({ file_id }, updateOperation, {
4523
+ new: true,
4524
+ }).lean();
4525
+ }
4526
+ /**
4527
+ * Deletes a file identified by file_id.
4528
+ * @param file_id - The unique identifier of the file to delete
4529
+ * @returns A promise that resolves to the deleted file document or null
4530
+ */
4531
+ async function deleteFile(file_id) {
4532
+ const File = mongoose.models.File;
4533
+ return File.findOneAndDelete({ file_id }).lean();
4534
+ }
4535
+ /**
4536
+ * Deletes a file identified by a filter.
4537
+ * @param filter - The filter criteria to apply
4538
+ * @returns A promise that resolves to the deleted file document or null
4539
+ */
4540
+ async function deleteFileByFilter(filter) {
4541
+ const File = mongoose.models.File;
4542
+ return File.findOneAndDelete(filter).lean();
4543
+ }
4544
+ /**
4545
+ * Deletes multiple files identified by an array of file_ids.
4546
+ * @param file_ids - The unique identifiers of the files to delete
4547
+ * @param user - Optional user ID to filter by
4548
+ * @returns A promise that resolves to the result of the deletion operation
4549
+ */
4550
+ async function deleteFiles(file_ids, user) {
4551
+ const File = mongoose.models.File;
4552
+ let deleteQuery = { file_id: { $in: file_ids } };
4553
+ if (user) {
4554
+ deleteQuery = { user: user };
4555
+ }
4556
+ return File.deleteMany(deleteQuery);
4557
+ }
4558
+ /**
4559
+ * Batch updates files with new signed URLs in MongoDB
4560
+ * @param updates - Array of updates in the format { file_id, filepath }
4561
+ */
4562
+ async function batchUpdateFiles(updates) {
4563
+ if (!updates || updates.length === 0) {
4564
+ return;
4565
+ }
4566
+ const File = mongoose.models.File;
4567
+ const bulkOperations = updates.map((update) => ({
4568
+ updateOne: {
4569
+ filter: { file_id: update.file_id },
4570
+ update: { $set: { filepath: update.filepath } },
4571
+ },
4572
+ }));
4573
+ const result = await File.bulkWrite(bulkOperations);
4574
+ logger$1.info(`Updated ${result.modifiedCount} files with new S3 URLs`);
4575
+ }
4576
+ /**
4577
+ * Updates usage tracking for multiple files.
4578
+ * Processes files and optional fileIds, updating their usage count in the database.
4579
+ *
4580
+ * @param files - Array of file objects to process
4581
+ * @param fileIds - Optional array of file IDs to process
4582
+ * @returns Array of updated file documents (with null results filtered out)
4583
+ */
4584
+ async function updateFilesUsage(files, fileIds) {
4585
+ const promises = [];
4586
+ const seen = new Set();
4587
+ for (const file of files) {
4588
+ const { file_id } = file;
4589
+ if (seen.has(file_id)) {
4590
+ continue;
4591
+ }
4592
+ seen.add(file_id);
4593
+ promises.push(updateFileUsage({ file_id }));
4594
+ }
4595
+ if (!fileIds) {
4596
+ const results = await Promise.all(promises);
4597
+ return results.filter((result) => result != null);
4598
+ }
4599
+ for (const file_id of fileIds) {
4600
+ if (seen.has(file_id)) {
4601
+ continue;
4602
+ }
4603
+ seen.add(file_id);
4604
+ promises.push(updateFileUsage({ file_id }));
4605
+ }
4606
+ const results = await Promise.all(promises);
4607
+ return results.filter((result) => result != null);
4608
+ }
4609
+ return {
4610
+ findFileById,
4611
+ getFiles,
4612
+ getToolFilesByIds,
4613
+ createFile,
4614
+ updateFile,
4615
+ updateFileUsage,
4616
+ deleteFile,
4617
+ deleteFiles,
4618
+ deleteFileByFilter,
4619
+ batchUpdateFiles,
4620
+ updateFilesUsage,
4621
+ };
4622
+ }
4623
+
3940
4624
  /**
3941
4625
  * Formats a date in YYYY-MM-DD format
3942
4626
  */
@@ -4284,6 +4968,258 @@ function createAgentCategoryMethods(mongoose) {
4284
4968
  };
4285
4969
  }
4286
4970
 
4971
+ const NORMALIZED_LIMIT_DEFAULT = 20;
4972
+ const MAX_CREATE_RETRIES = 5;
4973
+ const RETRY_BASE_DELAY_MS = 25;
4974
+ /**
4975
+ * Helper to check if an error is a MongoDB duplicate key error.
4976
+ * Since serverName is the only unique index on MCPServer, any E11000 error
4977
+ * during creation is necessarily a serverName collision.
4978
+ */
4979
+ function isDuplicateKeyError(error) {
4980
+ if (error && typeof error === 'object' && 'code' in error) {
4981
+ const mongoError = error;
4982
+ return mongoError.code === 11000;
4983
+ }
4984
+ return false;
4985
+ }
4986
+ /**
4987
+ * Escapes special regex characters in a string so they are treated literally.
4988
+ */
4989
+ function escapeRegex(str) {
4990
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
4991
+ }
4992
+ /**
4993
+ * Generates a URL-friendly server name from a title.
4994
+ * Converts to lowercase, replaces spaces with hyphens, removes special characters.
4995
+ */
4996
+ function generateServerNameFromTitle(title) {
4997
+ const slug = title
4998
+ .toLowerCase()
4999
+ .trim()
5000
+ .replace(/[^a-z0-9\s-]/g, '') // Remove special chars except spaces and hyphens
5001
+ .replace(/\s+/g, '-') // Replace spaces with hyphens
5002
+ .replace(/-+/g, '-') // Remove consecutive hyphens
5003
+ .replace(/^-|-$/g, ''); // Trim leading/trailing hyphens
5004
+ return slug || 'mcp-server'; // Fallback if empty
5005
+ }
5006
+ function createMCPServerMethods(mongoose) {
5007
+ /**
5008
+ * Finds the next available server name by checking for duplicates.
5009
+ * If baseName exists, returns baseName-2, baseName-3, etc.
5010
+ */
5011
+ async function findNextAvailableServerName(baseName) {
5012
+ const MCPServer = mongoose.models.MCPServer;
5013
+ // Find all servers with matching base name pattern (baseName or baseName-N)
5014
+ const escapedBaseName = escapeRegex(baseName);
5015
+ const existing = await MCPServer.find({
5016
+ serverName: { $regex: `^${escapedBaseName}(-\\d+)?$` },
5017
+ })
5018
+ .select('serverName')
5019
+ .lean();
5020
+ if (existing.length === 0) {
5021
+ return baseName;
5022
+ }
5023
+ // Extract numbers from existing names
5024
+ const numbers = existing.map((s) => {
5025
+ const match = s.serverName.match(/-(\d+)$/);
5026
+ return match ? parseInt(match[1], 10) : 1;
5027
+ });
5028
+ const maxNumber = Math.max(...numbers);
5029
+ return `${baseName}-${maxNumber + 1}`;
5030
+ }
5031
+ /**
5032
+ * Create a new MCP server with retry logic for handling race conditions.
5033
+ * When multiple requests try to create servers with the same title simultaneously,
5034
+ * they may get the same serverName from findNextAvailableServerName() before any
5035
+ * creates the record (TOCTOU race condition). This is handled by retrying with
5036
+ * exponential backoff when a duplicate key error occurs.
5037
+ * @param data - Object containing config (with title, description, url, etc.) and author
5038
+ * @returns The created MCP server document
5039
+ */
5040
+ async function createMCPServer(data) {
5041
+ const MCPServer = mongoose.models.MCPServer;
5042
+ let lastError;
5043
+ for (let attempt = 0; attempt < MAX_CREATE_RETRIES; attempt++) {
5044
+ try {
5045
+ // Generate serverName from title, with fallback to nanoid if no title
5046
+ // Important: regenerate on each attempt to get fresh available name
5047
+ let serverName;
5048
+ if (data.config.title) {
5049
+ const baseSlug = generateServerNameFromTitle(data.config.title);
5050
+ serverName = await findNextAvailableServerName(baseSlug);
5051
+ }
5052
+ else {
5053
+ serverName = `mcp-${nanoid.nanoid(16)}`;
5054
+ }
5055
+ const newServer = await MCPServer.create({
5056
+ serverName,
5057
+ config: data.config,
5058
+ author: data.author,
5059
+ });
5060
+ return newServer.toObject();
5061
+ }
5062
+ catch (error) {
5063
+ lastError = error;
5064
+ // Only retry on duplicate key errors (serverName collision)
5065
+ if (isDuplicateKeyError(error) && attempt < MAX_CREATE_RETRIES - 1) {
5066
+ // Exponential backoff: 10ms, 20ms, 40ms
5067
+ const delay = RETRY_BASE_DELAY_MS * Math.pow(2, attempt);
5068
+ logger$1.debug(`[createMCPServer] Duplicate serverName detected, retrying (attempt ${attempt + 2}/${MAX_CREATE_RETRIES}) after ${delay}ms`);
5069
+ await new Promise((resolve) => setTimeout(resolve, delay));
5070
+ continue;
5071
+ }
5072
+ // Not a duplicate key error or out of retries - throw immediately
5073
+ throw error;
5074
+ }
5075
+ }
5076
+ // Should not reach here, but TypeScript requires a return
5077
+ throw lastError;
5078
+ }
5079
+ /**
5080
+ * Find an MCP server by serverName
5081
+ * @param serverName - The MCP server ID
5082
+ * @returns The MCP server document or null
5083
+ */
5084
+ async function findMCPServerById(serverName) {
5085
+ const MCPServer = mongoose.models.MCPServer;
5086
+ return await MCPServer.findOne({ serverName }).lean();
5087
+ }
5088
+ /**
5089
+ * Find an MCP server by MongoDB ObjectId
5090
+ * @param _id - The MongoDB ObjectId
5091
+ * @returns The MCP server document or null
5092
+ */
5093
+ async function findMCPServerByObjectId(_id) {
5094
+ const MCPServer = mongoose.models.MCPServer;
5095
+ return await MCPServer.findById(_id).lean();
5096
+ }
5097
+ /**
5098
+ * Find MCP servers by author
5099
+ * @param authorId - The author's ObjectId or string
5100
+ * @returns Array of MCP server documents
5101
+ */
5102
+ async function findMCPServersByAuthor(authorId) {
5103
+ const MCPServer = mongoose.models.MCPServer;
5104
+ return await MCPServer.find({ author: authorId }).sort({ updatedAt: -1 }).lean();
5105
+ }
5106
+ /**
5107
+ * Get a paginated list of MCP servers by IDs with filtering and search
5108
+ * @param ids - Array of ObjectIds to include
5109
+ * @param otherParams - Additional filter parameters (e.g., search)
5110
+ * @param limit - Page size limit (null for no pagination)
5111
+ * @param after - Cursor for pagination
5112
+ * @returns Paginated list of MCP servers
5113
+ */
5114
+ async function getListMCPServersByIds({ ids = [], otherParams = {}, limit = null, after = null, }) {
5115
+ const MCPServer = mongoose.models.MCPServer;
5116
+ const isPaginated = limit !== null && limit !== undefined;
5117
+ const normalizedLimit = isPaginated
5118
+ ? Math.min(Math.max(1, parseInt(String(limit)) || NORMALIZED_LIMIT_DEFAULT), 100)
5119
+ : null;
5120
+ // Build base query combining accessible servers with other filters
5121
+ const baseQuery = { ...otherParams, _id: { $in: ids } };
5122
+ // Add cursor condition
5123
+ if (after) {
5124
+ try {
5125
+ const cursor = JSON.parse(Buffer.from(after, 'base64').toString('utf8'));
5126
+ const { updatedAt, _id } = cursor;
5127
+ const cursorCondition = {
5128
+ $or: [
5129
+ { updatedAt: { $lt: new Date(updatedAt) } },
5130
+ { updatedAt: new Date(updatedAt), _id: { $gt: new mongoose.Types.ObjectId(_id) } },
5131
+ ],
5132
+ };
5133
+ // Merge cursor condition with base query
5134
+ if (Object.keys(baseQuery).length > 0) {
5135
+ baseQuery.$and = [{ ...baseQuery }, cursorCondition];
5136
+ // Remove the original conditions from baseQuery to avoid duplication
5137
+ Object.keys(baseQuery).forEach((key) => {
5138
+ if (key !== '$and') {
5139
+ delete baseQuery[key];
5140
+ }
5141
+ });
5142
+ }
5143
+ }
5144
+ catch (error) {
5145
+ // Invalid cursor, ignore
5146
+ logger$1.warn('[getListMCPServersByIds] Invalid cursor provided', error);
5147
+ }
5148
+ }
5149
+ if (normalizedLimit === null) {
5150
+ // No pagination - return all matching servers
5151
+ const servers = await MCPServer.find(baseQuery).sort({ updatedAt: -1, _id: 1 }).lean();
5152
+ return {
5153
+ data: servers,
5154
+ has_more: false,
5155
+ after: null,
5156
+ };
5157
+ }
5158
+ // Paginated query - assign to const to help TypeScript
5159
+ const servers = await MCPServer.find(baseQuery)
5160
+ .sort({ updatedAt: -1, _id: 1 })
5161
+ .limit(normalizedLimit + 1)
5162
+ .lean();
5163
+ const hasMore = servers.length > normalizedLimit;
5164
+ const data = hasMore ? servers.slice(0, normalizedLimit) : servers;
5165
+ let nextCursor = null;
5166
+ if (hasMore && data.length > 0) {
5167
+ const lastItem = data[data.length - 1];
5168
+ nextCursor = Buffer.from(JSON.stringify({
5169
+ updatedAt: lastItem.updatedAt,
5170
+ _id: lastItem._id,
5171
+ })).toString('base64');
5172
+ }
5173
+ return {
5174
+ data,
5175
+ has_more: hasMore,
5176
+ after: nextCursor,
5177
+ };
5178
+ }
5179
+ /**
5180
+ * Update an MCP server
5181
+ * @param serverName - The MCP server ID
5182
+ * @param updateData - Object containing config to update
5183
+ * @returns The updated MCP server document or null
5184
+ */
5185
+ async function updateMCPServer(serverName, updateData) {
5186
+ const MCPServer = mongoose.models.MCPServer;
5187
+ return await MCPServer.findOneAndUpdate({ serverName }, { $set: updateData }, { new: true, runValidators: true }).lean();
5188
+ }
5189
+ /**
5190
+ * Delete an MCP server
5191
+ * @param serverName - The MCP server ID
5192
+ * @returns The deleted MCP server document or null
5193
+ */
5194
+ async function deleteMCPServer(serverName) {
5195
+ const MCPServer = mongoose.models.MCPServer;
5196
+ return await MCPServer.findOneAndDelete({ serverName }).lean();
5197
+ }
5198
+ /**
5199
+ * Get MCP servers by their serverName strings
5200
+ * @param names - Array of serverName strings to fetch
5201
+ * @returns Object containing array of MCP server documents
5202
+ */
5203
+ async function getListMCPServersByNames({ names = [] }) {
5204
+ if (names.length === 0) {
5205
+ return { data: [] };
5206
+ }
5207
+ const MCPServer = mongoose.models.MCPServer;
5208
+ const servers = await MCPServer.find({ serverName: { $in: names } }).lean();
5209
+ return { data: servers };
5210
+ }
5211
+ return {
5212
+ createMCPServer,
5213
+ findMCPServerById,
5214
+ findMCPServerByObjectId,
5215
+ findMCPServersByAuthor,
5216
+ getListMCPServersByIds,
5217
+ getListMCPServersByNames,
5218
+ updateMCPServer,
5219
+ deleteMCPServer,
5220
+ };
5221
+ }
5222
+
4287
5223
  // Factory function that takes mongoose instance and returns the methods
4288
5224
  function createPluginAuthMethods(mongoose) {
4289
5225
  /**
@@ -4511,6 +5447,27 @@ function createAccessRoleMethods(mongoose) {
4511
5447
  resourceType: librechatDataProvider.ResourceType.PROMPTGROUP,
4512
5448
  permBits: exports.RoleBits.OWNER,
4513
5449
  },
5450
+ {
5451
+ accessRoleId: librechatDataProvider.AccessRoleIds.MCPSERVER_VIEWER,
5452
+ name: 'com_ui_mcp_server_role_viewer',
5453
+ description: 'com_ui_mcp_server_role_viewer_desc',
5454
+ resourceType: librechatDataProvider.ResourceType.MCPSERVER,
5455
+ permBits: exports.RoleBits.VIEWER,
5456
+ },
5457
+ {
5458
+ accessRoleId: librechatDataProvider.AccessRoleIds.MCPSERVER_EDITOR,
5459
+ name: 'com_ui_mcp_server_role_editor',
5460
+ description: 'com_ui_mcp_server_role_editor_desc',
5461
+ resourceType: librechatDataProvider.ResourceType.MCPSERVER,
5462
+ permBits: exports.RoleBits.EDITOR,
5463
+ },
5464
+ {
5465
+ accessRoleId: librechatDataProvider.AccessRoleIds.MCPSERVER_OWNER,
5466
+ name: 'com_ui_mcp_server_role_owner',
5467
+ description: 'com_ui_mcp_server_role_owner_desc',
5468
+ resourceType: librechatDataProvider.ResourceType.MCPSERVER,
5469
+ permBits: exports.RoleBits.OWNER,
5470
+ },
4514
5471
  ];
4515
5472
  const result = {};
4516
5473
  for (const role of defaultRoles) {
@@ -5091,6 +6048,49 @@ function createAclEntryMethods(mongoose$1) {
5091
6048
  }
5092
6049
  return effectiveBits;
5093
6050
  }
6051
+ /**
6052
+ * Get effective permissions for multiple resources in a single query (BATCH)
6053
+ * Returns a map of resourceId → effectivePermissionBits
6054
+ *
6055
+ * @param principalsList - List of principals (user + groups + public)
6056
+ * @param resourceType - The type of resource ('MCPSERVER', 'AGENT', etc.)
6057
+ * @param resourceIds - Array of resource IDs to check
6058
+ * @returns {Promise<Map<string, number>>} Map of resourceId → permission bits
6059
+ *
6060
+ * @example
6061
+ * const principals = await getUserPrincipals({ userId, role });
6062
+ * const serverIds = [id1, id2, id3];
6063
+ * const permMap = await getEffectivePermissionsForResources(
6064
+ * principals,
6065
+ * ResourceType.MCPSERVER,
6066
+ * serverIds
6067
+ * );
6068
+ * // permMap.get(id1.toString()) → 7 (VIEW|EDIT|DELETE)
6069
+ */
6070
+ async function getEffectivePermissionsForResources(principalsList, resourceType, resourceIds) {
6071
+ if (!Array.isArray(resourceIds) || resourceIds.length === 0) {
6072
+ return new Map();
6073
+ }
6074
+ const AclEntry = mongoose$1.models.AclEntry;
6075
+ const principalsQuery = principalsList.map((p) => ({
6076
+ principalType: p.principalType,
6077
+ ...(p.principalType !== librechatDataProvider.PrincipalType.PUBLIC && { principalId: p.principalId }),
6078
+ }));
6079
+ // Batch query for all resources at once
6080
+ const aclEntries = await AclEntry.find({
6081
+ $or: principalsQuery,
6082
+ resourceType,
6083
+ resourceId: { $in: resourceIds },
6084
+ }).lean();
6085
+ // Compute effective permissions per resource
6086
+ const permissionsMap = new Map();
6087
+ for (const entry of aclEntries) {
6088
+ const rid = entry.resourceId.toString();
6089
+ const currentBits = permissionsMap.get(rid) || 0;
6090
+ permissionsMap.set(rid, currentBits | entry.permBits);
6091
+ }
6092
+ return permissionsMap;
6093
+ }
5094
6094
  /**
5095
6095
  * Grant permission to a principal for a resource
5096
6096
  * @param principalType - The type of principal ('user', 'group', 'public')
@@ -5231,6 +6231,7 @@ function createAclEntryMethods(mongoose$1) {
5231
6231
  findEntriesByPrincipalsAndResource,
5232
6232
  hasPermission,
5233
6233
  getEffectivePermissions,
6234
+ getEffectivePermissionsForResources,
5234
6235
  grantPermission,
5235
6236
  revokePermission,
5236
6237
  modifyPermissionBits,
@@ -5706,6 +6707,7 @@ function createShareMethods(mongoose) {
5706
6707
 
5707
6708
  /**
5708
6709
  * Creates all database methods for all collections
6710
+ * @param mongoose - Mongoose instance
5709
6711
  */
5710
6712
  function createMethods(mongoose) {
5711
6713
  return {
@@ -5713,8 +6715,11 @@ function createMethods(mongoose) {
5713
6715
  ...createSessionMethods(mongoose),
5714
6716
  ...createTokenMethods(mongoose),
5715
6717
  ...createRoleMethods(mongoose),
6718
+ ...createKeyMethods(mongoose),
6719
+ ...createFileMethods(mongoose),
5716
6720
  ...createMemoryMethods(mongoose),
5717
6721
  ...createAgentCategoryMethods(mongoose),
6722
+ ...createMCPServerMethods(mongoose),
5718
6723
  ...createAccessRoleMethods(mongoose),
5719
6724
  ...createUserGroupMethods(mongoose),
5720
6725
  ...createAclEntryMethods(mongoose),
@@ -5724,6 +6729,8 @@ function createMethods(mongoose) {
5724
6729
  }
5725
6730
 
5726
6731
  exports.AppService = AppService;
6732
+ exports.DEFAULT_REFRESH_TOKEN_EXPIRY = DEFAULT_REFRESH_TOKEN_EXPIRY;
6733
+ exports.DEFAULT_SESSION_EXPIRY = DEFAULT_SESSION_EXPIRY;
5727
6734
  exports.actionSchema = Action;
5728
6735
  exports.agentCategorySchema = agentCategorySchema;
5729
6736
  exports.agentSchema = agentSchema;
@@ -5736,10 +6743,19 @@ exports.conversationTagSchema = conversationTag;
5736
6743
  exports.convoSchema = convoSchema;
5737
6744
  exports.createMethods = createMethods;
5738
6745
  exports.createModels = createModels;
6746
+ exports.decrypt = decrypt;
6747
+ exports.decryptV2 = decryptV2;
6748
+ exports.decryptV3 = decryptV3;
6749
+ exports.defaultVertexModels = defaultVertexModels;
6750
+ exports.encrypt = encrypt;
6751
+ exports.encryptV2 = encryptV2;
6752
+ exports.encryptV3 = encryptV3;
5739
6753
  exports.fileSchema = file;
6754
+ exports.getRandomValues = getRandomValues;
5740
6755
  exports.getTransactionSupport = getTransactionSupport;
5741
6756
  exports.getWebSearchKeys = getWebSearchKeys;
5742
6757
  exports.groupSchema = groupSchema;
6758
+ exports.hashBackupCode = hashBackupCode;
5743
6759
  exports.hashToken = hashToken;
5744
6760
  exports.keySchema = keySchema;
5745
6761
  exports.loadDefaultInterface = loadDefaultInterface;
@@ -5764,6 +6780,8 @@ exports.tokenSchema = tokenSchema;
5764
6780
  exports.toolCallSchema = toolCallSchema;
5765
6781
  exports.transactionSchema = transactionSchema;
5766
6782
  exports.userSchema = userSchema;
6783
+ exports.validateVertexConfig = validateVertexConfig;
6784
+ exports.vertexConfigSetup = vertexConfigSetup;
5767
6785
  exports.webSearchAuth = webSearchAuth;
5768
6786
  exports.webSearchKeys = webSearchKeys;
5769
6787
  //# sourceMappingURL=index.cjs.map