LibreChat/packages/data-schemas/src/methods/message.ts

401 lines
12 KiB
TypeScript
Raw Normal View History

📦 refactor: Consolidate DB models, encapsulating Mongoose usage in `data-schemas` (#11830) * chore: move database model methods to /packages/data-schemas * chore: add TypeScript ESLint rule to warn on unused variables * refactor: model imports to streamline access - Consolidated model imports across various files to improve code organization and reduce redundancy. - Updated imports for models such as Assistant, Message, Conversation, and others to a unified import path. - Adjusted middleware and service files to reflect the new import structure, ensuring functionality remains intact. - Enhanced test files to align with the new import paths, maintaining test coverage and integrity. * chore: migrate database models to packages/data-schemas and refactor all direct Mongoose Model usage outside of data-schemas * test: update agent model mocks in unit tests - Added `getAgent` mock to `client.test.js` to enhance test coverage for agent-related functionality. - Removed redundant `getAgent` and `getAgents` mocks from `openai.spec.js` and `responses.unit.spec.js` to streamline test setup and reduce duplication. - Ensured consistency in agent mock implementations across test files. * fix: update types in data-schemas * refactor: enhance type definitions in transaction and spending methods - Updated type definitions in `checkBalance.ts` to use specific request and response types. - Refined `spendTokens.ts` to utilize a new `SpendTxData` interface for better clarity and type safety. - Improved transaction handling in `transaction.ts` by introducing `TransactionResult` and `TxData` interfaces, ensuring consistent data structures across methods. - Adjusted unit tests in `transaction.spec.ts` to accommodate new type definitions and enhance robustness. * refactor: streamline model imports and enhance code organization - Consolidated model imports across various controllers and services to a unified import path, improving code clarity and reducing redundancy. - Updated multiple files to reflect the new import structure, ensuring all functionalities remain intact. - Enhanced overall code organization by removing duplicate import statements and optimizing the usage of model methods. * feat: implement loadAddedAgent and refactor agent loading logic - Introduced `loadAddedAgent` function to handle loading agents from added conversations, supporting multi-convo parallel execution. - Created a new `load.ts` file to encapsulate agent loading functionalities, including `loadEphemeralAgent` and `loadAgent`. - Updated the `index.ts` file to export the new `load` module instead of the deprecated `loadAgent`. - Enhanced type definitions and improved error handling in the agent loading process. - Adjusted unit tests to reflect changes in the agent loading structure and ensure comprehensive coverage. * refactor: enhance balance handling with new update interface - Introduced `IBalanceUpdate` interface to streamline balance update operations across the codebase. - Updated `upsertBalanceFields` method signatures in `balance.ts`, `transaction.ts`, and related tests to utilize the new interface for improved type safety. - Adjusted type imports in `balance.spec.ts` to include `IBalanceUpdate`, ensuring consistency in balance management functionalities. - Enhanced overall code clarity and maintainability by refining type definitions related to balance operations. * feat: add unit tests for loadAgent functionality and enhance agent loading logic - Introduced comprehensive unit tests for the `loadAgent` function, covering various scenarios including null and empty agent IDs, loading of ephemeral agents, and permission checks. - Enhanced the `initializeClient` function by moving `getConvoFiles` to the correct position in the database method exports, ensuring proper functionality. - Improved test coverage for agent loading, including handling of non-existent agents and user permissions. * chore: reorder memory method exports for consistency - Moved `deleteAllUserMemories` to the correct position in the exported memory methods, ensuring a consistent and logical order of method exports in `memory.ts`.
2026-02-17 18:23:44 -05:00
import type { DeleteResult, FilterQuery, Model } from 'mongoose';
import logger from '~/config/winston';
import { createTempChatExpirationDate } from '~/utils/tempChatRetention';
🏗️ feat: bulkWrite isolation, pre-auth context, strict-mode fixes (#12445) * fix: wrap seedDatabase() in runAsSystem() for strict tenant mode seedDatabase() was called without tenant context at startup, causing every Mongoose operation inside it to throw when TENANT_ISOLATION_STRICT=true. Wrapping in runAsSystem() gives it the SYSTEM_TENANT_ID sentinel so the isolation plugin skips filtering, matching the pattern already used for performStartupChecks and updateInterfacePermissions. * fix: chain tenantContextMiddleware in optionalJwtAuth optionalJwtAuth populated req.user but never established ALS tenant context, unlike requireJwtAuth which chains tenantContextMiddleware after successful auth. Authenticated users hitting routes with optionalJwtAuth (e.g. /api/banner) had no tenant isolation. * feat: tenant-safe bulkWrite wrapper and call-site migration Mongoose's bulkWrite() does not trigger schema-level middleware hooks, so the applyTenantIsolation plugin cannot intercept it. This adds a tenantSafeBulkWrite() utility that injects the current ALS tenant context into every operation's filter/document before delegating to native bulkWrite. Migrates all 8 runtime bulkWrite call sites: - agentCategory (seedCategories, ensureDefaultCategories) - conversation (bulkSaveConvos) - message (bulkSaveMessages) - file (batchUpdateFiles) - conversationTag (updateTagsForConversation, bulkIncrementTagCounts) - aclEntry (bulkWriteAclEntries) systemGrant.seedSystemGrants is intentionally not migrated — it uses explicit tenantId: { $exists: false } filters and is exempt from the isolation plugin. * feat: pre-auth tenant middleware and tenant-scoped config cache Adds preAuthTenantMiddleware that reads X-Tenant-Id from the request header and wraps downstream in tenantStorage ALS context. Wired onto /oauth, /api/auth, /api/config, and /api/share — unauthenticated routes that need tenant scoping before JWT auth runs. The /api/config cache key is now tenant-scoped (STARTUP_CONFIG:${tenantId}) so multi-tenant deployments serve the correct login page config per tenant. The middleware is intentionally minimal — no subdomain parsing, no OIDC claim extraction. The private fork's reverse proxy or auth gateway sets the header. * feat: accept optional tenantId in updateInterfacePermissions When tenantId is provided, the function re-enters inside tenantStorage.run({ tenantId }) so all downstream Mongoose queries target that tenant's roles instead of the system context. This lets the private fork's tenant provisioning flow call updateInterfacePermissions per-tenant after creating tenant-scoped ADMIN/USER roles. * fix: tenant-filter $lookup in getPromptGroup aggregation The $lookup stage in getPromptGroup() queried the prompts collection without tenant filtering. While the outer PromptGroup aggregate is protected by the tenantIsolation plugin's pre('aggregate') hook, $lookup runs as an internal MongoDB operation that bypasses Mongoose hooks entirely. Converts from simple field-based $lookup to pipeline-based $lookup with an explicit tenantId match when tenant context is active. * fix: replace field-level unique indexes with tenant-scoped compounds Field-level unique:true creates a globally-unique single-field index in MongoDB, which would cause insert failures across tenants sharing the same ID values. - agent.id: removed field-level unique, added { id, tenantId } compound - convo.conversationId: removed field-level unique (compound at line 50 already exists: { conversationId, user, tenantId }) - message.messageId: removed field-level unique (compound at line 165 already exists: { messageId, user, tenantId }) - preset.presetId: removed field-level unique, added { presetId, tenantId } compound * fix: scope MODELS_CONFIG, ENDPOINT_CONFIG, PLUGINS, TOOLS caches by tenant These caches store per-tenant configuration (available models, endpoint settings, plugin availability, tool definitions) but were using global cache keys. In multi-tenant mode, one tenant's cached config would be served to all tenants. Appends :${tenantId} to cache keys when tenant context is active. Falls back to the unscoped key when no tenant context exists (backward compatible for single-tenant OSS deployments). Covers all read, write, and delete sites: - ModelController.js: get/set MODELS_CONFIG - PluginController.js: get/set PLUGINS, get/set TOOLS - getEndpointsConfig.js: get/set/delete ENDPOINT_CONFIG - app.js: delete ENDPOINT_CONFIG (clearEndpointConfigCache) - mcp.js: delete TOOLS (updateMCPTools, mergeAppTools) - importers.js: get ENDPOINT_CONFIG * fix: add getTenantId to PluginController spec mock The data-schemas mock was missing getTenantId, causing all PluginController tests to throw when the controller calls getTenantId() for tenant-scoped cache keys. * fix: address review findings — migration, strict-mode, DRY, types Addresses all CRITICAL, MAJOR, and MINOR review findings: F1 (CRITICAL): Add agents, conversations, messages, presets to SUPERSEDED_INDEXES in tenantIndexes.ts so dropSupersededTenantIndexes() drops the old single-field unique indexes that block multi-tenant inserts. F2 (CRITICAL): Unknown bulkWrite op types now throw in strict mode instead of silently passing through without tenant injection. F3 (MAJOR): Replace wildcard export with named export for tenantSafeBulkWrite, hiding _resetBulkWriteStrictCache from the public package API. F5 (MAJOR): Restore AnyBulkWriteOperation<IAclEntry>[] typing on bulkWriteAclEntries — the unparameterized wrapper accepts parameterized ops as a subtype. F7 (MAJOR): Fix config.js tenant precedence — JWT-derived req.user.tenantId now takes priority over the X-Tenant-Id header for authenticated requests. F8 (MINOR): Extract scopedCacheKey() helper into tenantContext.ts and replace all 11 inline occurrences across 7 files. F9 (MINOR): Use simple localField/foreignField $lookup for the non-tenant getPromptGroup path (more efficient index seeks). F12 (NIT): Remove redundant BulkOp type alias. F13 (NIT): Remove debug log that leaked raw tenantId. * fix: add new superseded indexes to tenantIndexes test fixture The test creates old indexes to verify the migration drops them. Missing fixture entries for agents.id_1, conversations.conversationId_1, messages.messageId_1, and presets.presetId_1 caused the count assertion to fail (expected 22, got 18). * fix: restore logger.warn for unknown bulk op types in non-strict mode * fix: block SYSTEM_TENANT_ID sentinel from external header input CRITICAL: preAuthTenantMiddleware accepted any string as X-Tenant-Id, including '__SYSTEM__'. The tenantIsolation plugin treats SYSTEM_TENANT_ID as an explicit bypass — skipping ALL query filters. A client sending X-Tenant-Id: __SYSTEM__ to pre-auth routes (/api/share, /api/config, /api/auth, /oauth) would execute Mongoose operations without tenant isolation. Fixes: - preAuthTenantMiddleware rejects SYSTEM_TENANT_ID in header - scopedCacheKey returns the base key (not key:__SYSTEM__) in system context, preventing stale cache entries during runAsSystem() - updateInterfacePermissions guards tenantId against SYSTEM_TENANT_ID - $lookup pipeline separates $expr join from constant tenantId match for better index utilization - Regression test for sentinel rejection in preAuthTenant.spec.ts - Remove redundant getTenantId() call in config.js * test: add missing deleteMany/replaceOne coverage, fix vacuous ALS assertions bulkWrite spec: - deleteMany: verifies tenant-scoped deletion leaves other tenants untouched - replaceOne: verifies tenantId injected into both filter and replacement - replaceOne overwrite: verifies a conflicting tenantId in the replacement document is overwritten by the ALS tenant (defense-in-depth) - empty ops array: verifies graceful handling preAuthTenant spec: - All negative-case tests now use the capturedNext pattern to verify getTenantId() inside the middleware's execution context, not the test runner's outer frame (which was always undefined regardless) * feat: tenant-isolate MESSAGES cache, FLOWS cache, and GenerationJobManager MESSAGES cache (streamAudio.js): - Cache key now uses scopedCacheKey(messageId) to prefix with tenantId, preventing cross-tenant message content reads during TTS streaming. FLOWS cache (FlowStateManager): - getFlowKey() now generates ${type}:${tenantId}:${flowId} when tenant context is active, isolating OAuth flow state per tenant. GenerationJobManager: - tenantId added to SerializableJobData and GenerationJobMetadata - createJob() captures the current ALS tenant context (excluding SYSTEM_TENANT_ID) and stores it in job metadata - SSE subscription endpoint validates job.metadata.tenantId matches req.user.tenantId, blocking cross-tenant stream access - Both InMemoryJobStore and RedisJobStore updated to accept tenantId * fix: add getTenantId and SYSTEM_TENANT_ID to MCP OAuth test mocks FlowStateManager.getFlowKey() now calls getTenantId() for tenant-scoped flow keys. The 4 MCP OAuth test files mock @librechat/data-schemas without these exports, causing TypeError at runtime. * fix: correct import ordering per AGENTS.md conventions Package imports sorted shortest to longest line length, local imports sorted longest to shortest — fixes ordering violations introduced by our new imports across 8 files. * fix: deserialize tenantId in RedisJobStore — cross-tenant SSE guard was no-op in Redis mode serializeJob() writes tenantId to the Redis hash via Object.entries, but deserializeJob() manually enumerates fields and omitted tenantId. Every getJob() from Redis returned tenantId: undefined, causing the SSE route's cross-tenant guard to short-circuit (undefined && ... → false). * test: SSE tenant guard, FlowStateManager key consistency, ALS scope docs SSE stream tenant tests (streamTenant.spec.js): - Cross-tenant user accessing another tenant's stream → 403 - Same-tenant user accessing own stream → allowed - OSS mode (no tenantId on job) → tenant check skipped FlowStateManager tenant tests (manager.tenant.spec.ts): - completeFlow finds flow created under same tenant context - completeFlow does NOT find flow under different tenant context - Unscoped flows are separate from tenant-scoped flows Documentation: - JSDoc on getFlowKey documenting ALS context consistency requirement - Comment on streamAudio.js scopedCacheKey capture site * fix: SSE stream tests hang on success path, remove internal fork references The success-path tests entered the SSE streaming code which never closes, causing timeout. Mock subscribe() to end the response immediately. Restructured assertions to verify non-403/non-404. Removed "private fork" and "OSS" references from code and test descriptions — replaced with "deployment layer", "multi-tenant deployments", and "single-tenant mode". * fix: address review findings — test rigor, tenant ID validation, docs F1: SSE stream tests now mock subscribe() with correct signature (streamId, writeEvent, onDone, onError) and assert 200 status, verifying the tenant guard actually allows through same-tenant users. F2: completeFlow logs the attempted key and ALS tenantId when flow is not found, so reverse proxy misconfiguration (missing X-Tenant-Id on OAuth callback) produces an actionable warning. F3/F10: preAuthTenantMiddleware validates tenant ID format — rejects colons, special characters, and values exceeding 128 chars. Trims whitespace. Prevents cache key collisions via crafted headers. F4: Documented cache invalidation scope limitation in clearEndpointConfigCache — only the calling tenant's key is cleared; other tenants expire via TTL. F7: getFlowKey JSDoc now lists all 8 methods requiring consistent ALS context. F8: Added dedicated scopedCacheKey unit tests — base key without context, base key in system context, scoped key with tenant, no ALS leakage across scope boundaries. * fix: revert flow key tenant scoping, fix SSE test timing FlowStateManager: Reverts tenant-scoped flow keys. OAuth callbacks arrive without tenant ALS context (provider redirects don't carry X-Tenant-Id), so completeFlow/failFlow would never find flows created under tenant context. Flow IDs are random UUIDs with no collision risk, and flow data is ephemeral (TTL-bounded). SSE tests: Use process.nextTick for onDone callback so Express response headers are flushed before res.write/res.end are called. * fix: restore getTenantId import for completeFlow diagnostic log * fix: correct completeFlow warning message, add missing flow test The warning referenced X-Tenant-Id header consistency which was only relevant when flow keys were tenant-scoped (since reverted). Updated to list actual causes: TTL expiry, missing flow, or routing to a different instance without shared Keyv storage. Removed the getTenantId() call and import — no longer needed since flow keys are unscoped. Added test for the !flowState branch in completeFlow — verifies return false and logger.warn on nonexistent flow ID. * fix: add explicit return type to recursive updateInterfacePermissions The recursive call (tenantId branch calls itself without tenantId) causes TypeScript to infer circular return type 'any'. Adding explicit Promise<void> satisfies the rollup typescript plugin. * fix: update MCPOAuthRaceCondition test to match new completeFlow warning * fix: clearEndpointConfigCache deletes both scoped and unscoped keys Unauthenticated /api/endpoints requests populate the unscoped ENDPOINT_CONFIG key. Admin config mutations clear only the tenant-scoped key, leaving the unscoped entry stale indefinitely. Now deletes both when in tenant context. * fix: tenant guard on abort/status endpoints, warn logs, test coverage F1: Add tenant guard to /chat/status/:conversationId and /chat/abort matching the existing guard on /chat/stream/:streamId. The status endpoint exposes aggregatedContent (AI response text) which requires tenant-level access control. F2: preAuthTenantMiddleware now logs warn for rejected __SYSTEM__ sentinel and malformed tenant IDs, providing observability for bypass probing attempts. F3: Abort fallback path (getActiveJobIdsForUser) now has tenant check after resolving the job. F4: Test for strict mode + SYSTEM_TENANT_ID — verifies runAsSystem bypasses tenantSafeBulkWrite without throwing in strict mode. F5: Test for job with tenantId + user without tenantId → 403. F10: Regex uses idiomatic hyphen-at-start form. F11: Test descriptions changed from "rejects" to "ignores" since middleware calls next() (not 4xx). Also fixes MCPOAuthRaceCondition test assertion to match updated completeFlow warning message. * fix: test coverage for logger.warn, status/abort guards, consistency A: preAuthTenant spec now mocks logger and asserts warn calls for __SYSTEM__ sentinel, malformed characters, and oversized headers. B: streamTenant spec expanded with status and abort endpoint tests — cross-tenant status returns 403, same-tenant returns 200 with body, cross-tenant abort returns 403. C: Abort endpoint uses req.user.tenantId (not req.user?.tenantId) matching stream/status pattern — requireJwtAuth guarantees req.user. D: Malformed header warning now includes ip in log metadata, matching the sentinel warning for consistent SOC correlation. * fix: assert ip field in malformed header warn tests * fix: parallelize cache deletes, document tenant guard, fix import order - clearEndpointConfigCache uses Promise.all for independent cache deletes instead of sequential awaits - SSE stream tenant guard has inline comment explaining backward-compat behavior for untenanted legacy jobs - conversation.ts local imports reordered longest-to-shortest per AGENTS.md * fix: tenant-qualify userJobs keys, document tenant guard backward-compat Job store userJobs keys now include tenantId when available: - Redis: stream:user:{tenantId:userId}:jobs (falls back to stream:user:{userId}:jobs when no tenant) - InMemory: composite key tenantId:userId in userJobMap getActiveJobIdsByUser/getActiveJobIdsForUser accept optional tenantId parameter, threaded through from req.user.tenantId at all call sites (/chat/active and /chat/abort fallback). Added inline comments on all three SSE tenant guards explaining the backward-compat design: untenanted legacy jobs remain accessible when the userId check passes. * fix: parallelize cache deletes, document tenant guard, fix import order Fix InMemoryJobStore.getActiveJobIdsByUser empty-set cleanup to use the tenant-qualified userKey instead of bare userId — prevents orphaned empty Sets accumulating in userJobMap for multi-tenant users. Document cross-tenant staleness in clearEndpointConfigCache JSDoc — other tenants' scoped keys expire via TTL, not active invalidation. * fix: cleanup userJobMap leak, startup warning, DRY tenant guard, docs F1: InMemoryJobStore.cleanup() now removes entries from userJobMap before calling deleteJob, preventing orphaned empty Sets from accumulating with tenant-qualified composite keys. F2: Startup warning when TENANT_ISOLATION_STRICT is active — reminds operators to configure reverse proxy to control X-Tenant-Id header. F3: mergeAppTools JSDoc documents that tenant-scoped TOOLS keys are not actively invalidated (matching clearEndpointConfigCache pattern). F5: Abort handler getActiveJobIdsForUser call uses req.user.tenantId (not req.user?.tenantId) — consistent with stream/status handlers. F6: updateInterfacePermissions JSDoc clarifies SYSTEM_TENANT_ID behavior — falls through to caller's ALS context. F7: Extracted hasTenantMismatch() helper, replacing three identical inline tenant guard blocks across stream/status/abort endpoints. F9: scopedCacheKey JSDoc documents both passthrough cases (no context and SYSTEM_TENANT_ID context). * fix: clean userJobMap in evictOldest — same leak as cleanup()
2026-03-28 16:43:50 -04:00
import { tenantSafeBulkWrite } from '~/utils/tenantBulkWrite';
📦 refactor: Consolidate DB models, encapsulating Mongoose usage in `data-schemas` (#11830) * chore: move database model methods to /packages/data-schemas * chore: add TypeScript ESLint rule to warn on unused variables * refactor: model imports to streamline access - Consolidated model imports across various files to improve code organization and reduce redundancy. - Updated imports for models such as Assistant, Message, Conversation, and others to a unified import path. - Adjusted middleware and service files to reflect the new import structure, ensuring functionality remains intact. - Enhanced test files to align with the new import paths, maintaining test coverage and integrity. * chore: migrate database models to packages/data-schemas and refactor all direct Mongoose Model usage outside of data-schemas * test: update agent model mocks in unit tests - Added `getAgent` mock to `client.test.js` to enhance test coverage for agent-related functionality. - Removed redundant `getAgent` and `getAgents` mocks from `openai.spec.js` and `responses.unit.spec.js` to streamline test setup and reduce duplication. - Ensured consistency in agent mock implementations across test files. * fix: update types in data-schemas * refactor: enhance type definitions in transaction and spending methods - Updated type definitions in `checkBalance.ts` to use specific request and response types. - Refined `spendTokens.ts` to utilize a new `SpendTxData` interface for better clarity and type safety. - Improved transaction handling in `transaction.ts` by introducing `TransactionResult` and `TxData` interfaces, ensuring consistent data structures across methods. - Adjusted unit tests in `transaction.spec.ts` to accommodate new type definitions and enhance robustness. * refactor: streamline model imports and enhance code organization - Consolidated model imports across various controllers and services to a unified import path, improving code clarity and reducing redundancy. - Updated multiple files to reflect the new import structure, ensuring all functionalities remain intact. - Enhanced overall code organization by removing duplicate import statements and optimizing the usage of model methods. * feat: implement loadAddedAgent and refactor agent loading logic - Introduced `loadAddedAgent` function to handle loading agents from added conversations, supporting multi-convo parallel execution. - Created a new `load.ts` file to encapsulate agent loading functionalities, including `loadEphemeralAgent` and `loadAgent`. - Updated the `index.ts` file to export the new `load` module instead of the deprecated `loadAgent`. - Enhanced type definitions and improved error handling in the agent loading process. - Adjusted unit tests to reflect changes in the agent loading structure and ensure comprehensive coverage. * refactor: enhance balance handling with new update interface - Introduced `IBalanceUpdate` interface to streamline balance update operations across the codebase. - Updated `upsertBalanceFields` method signatures in `balance.ts`, `transaction.ts`, and related tests to utilize the new interface for improved type safety. - Adjusted type imports in `balance.spec.ts` to include `IBalanceUpdate`, ensuring consistency in balance management functionalities. - Enhanced overall code clarity and maintainability by refining type definitions related to balance operations. * feat: add unit tests for loadAgent functionality and enhance agent loading logic - Introduced comprehensive unit tests for the `loadAgent` function, covering various scenarios including null and empty agent IDs, loading of ephemeral agents, and permission checks. - Enhanced the `initializeClient` function by moving `getConvoFiles` to the correct position in the database method exports, ensuring proper functionality. - Improved test coverage for agent loading, including handling of non-existent agents and user permissions. * chore: reorder memory method exports for consistency - Moved `deleteAllUserMemories` to the correct position in the exported memory methods, ensuring a consistent and logical order of method exports in `memory.ts`.
2026-02-17 18:23:44 -05:00
import type { AppConfig, IMessage } from '~/types';
/** Simple UUID v4 regex to replace zod validation */
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
export interface MessageMethods {
saveMessage(
ctx: { userId: string; isTemporary?: boolean; interfaceConfig?: AppConfig['interfaceConfig'] },
params: Partial<IMessage> & { newMessageId?: string },
metadata?: { context?: string },
): Promise<IMessage | null | undefined>;
bulkSaveMessages(
messages: Array<Partial<IMessage>>,
overrideTimestamp?: boolean,
): Promise<unknown>;
recordMessage(params: {
user: string;
endpoint?: string;
messageId: string;
conversationId?: string;
parentMessageId?: string;
[key: string]: unknown;
}): Promise<IMessage | null>;
updateMessageText(userId: string, params: { messageId: string; text: string }): Promise<void>;
updateMessage(
userId: string,
message: Partial<IMessage> & { newMessageId?: string },
metadata?: { context?: string },
): Promise<Partial<IMessage>>;
deleteMessagesSince(
userId: string,
params: { messageId: string; conversationId: string },
): Promise<DeleteResult>;
getMessages(filter: FilterQuery<IMessage>, select?: string): Promise<IMessage[]>;
getMessage(params: { user: string; messageId: string }): Promise<IMessage | null>;
getMessagesByCursor(
filter: FilterQuery<IMessage>,
options?: {
sortField?: string;
sortOrder?: 1 | -1;
limit?: number;
cursor?: string | null;
},
): Promise<{ messages: IMessage[]; nextCursor: string | null }>;
searchMessages(
query: string,
searchOptions: Partial<IMessage>,
hydrate?: boolean,
): Promise<unknown>;
deleteMessages(filter: FilterQuery<IMessage>): Promise<DeleteResult>;
}
export function createMessageMethods(mongoose: typeof import('mongoose')): MessageMethods {
/**
* Saves a message in the database.
*/
async function saveMessage(
{
userId,
isTemporary,
interfaceConfig,
}: {
userId: string;
isTemporary?: boolean;
interfaceConfig?: AppConfig['interfaceConfig'];
},
params: Partial<IMessage> & { newMessageId?: string },
metadata?: { context?: string },
) {
if (!userId) {
throw new Error('User not authenticated');
}
const conversationId = params.conversationId as string | undefined;
if (!conversationId || !UUID_REGEX.test(conversationId)) {
logger.warn(`Invalid conversation ID: ${conversationId}`);
logger.info(`---\`saveMessage\` context: ${metadata?.context}`);
logger.info(`---Invalid conversation ID Params: ${JSON.stringify(params, null, 2)}`);
return;
}
try {
const Message = mongoose.models.Message as Model<IMessage>;
const update: Record<string, unknown> = {
...params,
user: userId,
messageId: params.newMessageId || params.messageId,
};
if (isTemporary) {
try {
update.expiredAt = createTempChatExpirationDate(interfaceConfig);
} catch (err) {
logger.error('Error creating temporary chat expiration date:', err);
logger.info(`---\`saveMessage\` context: ${metadata?.context}`);
update.expiredAt = null;
}
} else {
update.expiredAt = null;
}
if (update.tokenCount != null && isNaN(update.tokenCount as number)) {
logger.warn(
`Resetting invalid \`tokenCount\` for message \`${params.messageId}\`: ${update.tokenCount}`,
);
logger.info(`---\`saveMessage\` context: ${metadata?.context}`);
update.tokenCount = 0;
}
const message = await Message.findOneAndUpdate(
{ messageId: params.messageId, user: userId },
update,
{ upsert: true, new: true },
);
return message.toObject();
} catch (err: unknown) {
logger.error('Error saving message:', err);
logger.info(`---\`saveMessage\` context: ${metadata?.context}`);
const mongoErr = err as { code?: number; message?: string };
if (mongoErr.code === 11000 && mongoErr.message?.includes('duplicate key error')) {
logger.warn(`Duplicate messageId detected: ${params.messageId}. Continuing execution.`);
try {
const Message = mongoose.models.Message as Model<IMessage>;
const existingMessage = await Message.findOne({
messageId: params.messageId,
user: userId,
});
if (existingMessage) {
return existingMessage.toObject();
}
return undefined;
} catch (findError) {
logger.warn(
`Could not retrieve existing message with ID ${params.messageId}: ${(findError as Error).message}`,
);
return undefined;
}
}
throw err;
}
}
/**
* Saves multiple messages in bulk.
*/
async function bulkSaveMessages(
messages: Array<Record<string, unknown>>,
overrideTimestamp = false,
) {
try {
const Message = mongoose.models.Message as Model<IMessage>;
🛡️ refactor: Self-Healing Tenant Isolation Update Guard (#12506) * refactor: self-healing tenant isolation update guard Replace the strict throw-on-any-tenantId guard with a strip-or-throw approach: - $set/$setOnInsert: strip when value matches current tenant or no context is active; throw only on cross-tenant mutations - $unset/$rename: always strip (unsetting/renaming tenantId is never valid) - Top-level tenantId: same logic as $set This eliminates the entire class of "tenantId in update payload" bugs at the plugin level while preserving the cross-tenant security invariant. * test: update mutation guard tests for self-healing behavior - Convert same-tenant $set/$setOnInsert tests to expect silent stripping instead of throws - Convert $unset test to expect silent stripping - Add cross-tenant throw tests for $set, $setOnInsert, top-level - Add same-tenant stripping tests for $set, $setOnInsert, top-level - Add $rename stripping test - Add no-context stripping test - Update error message assertions to match new cross-tenant message * revert: remove call-site tenantId stripping patches Revert the per-call-site tenantId stripping from #12498 and the excludedKeys patch from #12501. These are no longer needed since the self-healing guard handles tenantId in update payloads at the plugin level. Reverted patches: - conversation.ts: delete update.tenantId in saveConvo(), tenantId destructuring in bulkSaveConvos() - message.ts: delete update.tenantId in saveMessage() and recordMessage(), tenantId destructuring in bulkSaveMessages() and updateMessage() - config.ts: tenantId in excludedKeys Set - config.spec.ts: tenantId in excludedKeys test assertion * fix: strip tenantId from update documents in tenantSafeBulkWrite Mongoose middleware does not fire for bulkWrite, so the plugin-level guard never sees update payloads in bulk operations. Extend injectTenantId() to strip tenantId from update documents for updateOne/updateMany operations, preventing cross-tenant overwrites. * refactor: rename guard, add empty-op cleanup and strict-mode warning - Rename assertNoTenantIdMutation to sanitizeTenantIdMutation - Remove empty operator objects after stripping to avoid MongoDB errors - Log warning in strict mode when stripping tenantId without context - Fix $setOnInsert test to use upsert:true with non-matching filter * test: fix bulk-save tests and add negative excludedKeys assertion - Wrap bulkSaveConvos/bulkSaveMessages tests in tenantStorage.run() to exercise the actual multi-tenant stripping path - Assert tenantId equals the real tenant, not undefined - Add negative assertion: excludedKeys must NOT contain tenantId * fix: type-safe tenantId stripping in tenantSafeBulkWrite - Fix TS2345 error: replace conditional type inference with UpdateQuery<Record<string, unknown>> for stripTenantIdFromUpdate - Handle empty updates after stripping (e.g., $set: { tenantId } as sole field) by filtering null ops from the bulk array - Add 4 tests for bulk update tenantId stripping: plain-object update, $set stripping, $unset stripping, and sole-field-in-$set edge case * fix: resolve TS2345 in stripTenantIdFromUpdate parameter type Use Record<string, unknown> instead of UpdateQuery<> to avoid type incompatibility with Mongoose's AnyObject-based UpdateQuery resolution in CI. * fix: strip tenantId from bulk updates unconditionally Separate sanitization from injection in tenantSafeBulkWrite: tenantId is now stripped from all update documents before any tenant-context checks, closing the gap where no-context and system-context paths passed caller-supplied tenantId through to MongoDB unmodified. * refactor: address review findings in tenant isolation - Fix early-return gap in stripTenantIdFromUpdate that skipped operator-level tenantId when top-level was also present - Lazy-allocate copy in stripTenantIdFromUpdate (no allocation when no tenantId is present) - Document behavioral asymmetry: plugin throws on cross-tenant, bulkWrite strips silently (intentional, documented in JSDoc) - Remove double JSDoc on injectTenantId - Remove redundant cast in stripTenantIdFromUpdate - Use shared frozen EMPTY_BULK_RESULT constant - Remove Record<string, unknown> annotation in recordMessage - Isolate bulkSave* tests: pre-create docs then update with cross-tenant payload, read via runAsSystem to prove stripping is independent of filter injection * fix: no-op empty updates after tenantId sanitization When tenantId is the sole field in an update (e.g., { $set: { tenantId } }), sanitization leaves an empty update object that would fail with "Update document requires atomic operators." The updateGuard now detects this and short-circuits the query by adding an unmatchable filter condition and disabling upsert, matching the bulk-write handling that filters out null ops. * refactor: remove dead logger.warn branches, add mixed-case test - Remove unreachable logger.warn calls in sanitizeTenantIdMutation: queryMiddleware throws before updateGuard in strict+no-context, and isStrict() is false in non-strict+no-context - Add test for combined top-level + operator-level tenantId stripping to lock in the early-return fix * feat: ESLint rule to ban raw bulkWrite and collection.* in data-schemas Add no-restricted-syntax rules to the data-schemas ESLint config that flag direct Model.bulkWrite() and Model.collection.* calls. These bypass Mongoose middleware and the tenant isolation plugin — all bulk writes must use tenantSafeBulkWrite() instead. Test files are excluded since they intentionally use raw driver calls for fixture setup. Also migrate the one remaining raw bulkWrite in seedSystemGrants() to use tenantSafeBulkWrite() for consistency. * test: add findByIdAndUpdate coverage to mutation guard tests * fix: keep tenantSafeBulkWrite in seedSystemGrants, fix ESLint config - Revert to tenantSafeBulkWrite in seedSystemGrants (always runs under runAsSystem, so the wrapper passes through correctly) - Split data-schemas ESLint config: shared TS rules for all files, no-restricted-syntax only for production non-wrapper files - Fix unused destructure vars to use _tenantId pattern
2026-04-01 19:07:52 -04:00
const bulkOps = messages.map((message) => ({
updateOne: {
filter: { messageId: message.messageId },
update: message,
timestamps: !overrideTimestamp,
upsert: true,
},
}));
🏗️ feat: bulkWrite isolation, pre-auth context, strict-mode fixes (#12445) * fix: wrap seedDatabase() in runAsSystem() for strict tenant mode seedDatabase() was called without tenant context at startup, causing every Mongoose operation inside it to throw when TENANT_ISOLATION_STRICT=true. Wrapping in runAsSystem() gives it the SYSTEM_TENANT_ID sentinel so the isolation plugin skips filtering, matching the pattern already used for performStartupChecks and updateInterfacePermissions. * fix: chain tenantContextMiddleware in optionalJwtAuth optionalJwtAuth populated req.user but never established ALS tenant context, unlike requireJwtAuth which chains tenantContextMiddleware after successful auth. Authenticated users hitting routes with optionalJwtAuth (e.g. /api/banner) had no tenant isolation. * feat: tenant-safe bulkWrite wrapper and call-site migration Mongoose's bulkWrite() does not trigger schema-level middleware hooks, so the applyTenantIsolation plugin cannot intercept it. This adds a tenantSafeBulkWrite() utility that injects the current ALS tenant context into every operation's filter/document before delegating to native bulkWrite. Migrates all 8 runtime bulkWrite call sites: - agentCategory (seedCategories, ensureDefaultCategories) - conversation (bulkSaveConvos) - message (bulkSaveMessages) - file (batchUpdateFiles) - conversationTag (updateTagsForConversation, bulkIncrementTagCounts) - aclEntry (bulkWriteAclEntries) systemGrant.seedSystemGrants is intentionally not migrated — it uses explicit tenantId: { $exists: false } filters and is exempt from the isolation plugin. * feat: pre-auth tenant middleware and tenant-scoped config cache Adds preAuthTenantMiddleware that reads X-Tenant-Id from the request header and wraps downstream in tenantStorage ALS context. Wired onto /oauth, /api/auth, /api/config, and /api/share — unauthenticated routes that need tenant scoping before JWT auth runs. The /api/config cache key is now tenant-scoped (STARTUP_CONFIG:${tenantId}) so multi-tenant deployments serve the correct login page config per tenant. The middleware is intentionally minimal — no subdomain parsing, no OIDC claim extraction. The private fork's reverse proxy or auth gateway sets the header. * feat: accept optional tenantId in updateInterfacePermissions When tenantId is provided, the function re-enters inside tenantStorage.run({ tenantId }) so all downstream Mongoose queries target that tenant's roles instead of the system context. This lets the private fork's tenant provisioning flow call updateInterfacePermissions per-tenant after creating tenant-scoped ADMIN/USER roles. * fix: tenant-filter $lookup in getPromptGroup aggregation The $lookup stage in getPromptGroup() queried the prompts collection without tenant filtering. While the outer PromptGroup aggregate is protected by the tenantIsolation plugin's pre('aggregate') hook, $lookup runs as an internal MongoDB operation that bypasses Mongoose hooks entirely. Converts from simple field-based $lookup to pipeline-based $lookup with an explicit tenantId match when tenant context is active. * fix: replace field-level unique indexes with tenant-scoped compounds Field-level unique:true creates a globally-unique single-field index in MongoDB, which would cause insert failures across tenants sharing the same ID values. - agent.id: removed field-level unique, added { id, tenantId } compound - convo.conversationId: removed field-level unique (compound at line 50 already exists: { conversationId, user, tenantId }) - message.messageId: removed field-level unique (compound at line 165 already exists: { messageId, user, tenantId }) - preset.presetId: removed field-level unique, added { presetId, tenantId } compound * fix: scope MODELS_CONFIG, ENDPOINT_CONFIG, PLUGINS, TOOLS caches by tenant These caches store per-tenant configuration (available models, endpoint settings, plugin availability, tool definitions) but were using global cache keys. In multi-tenant mode, one tenant's cached config would be served to all tenants. Appends :${tenantId} to cache keys when tenant context is active. Falls back to the unscoped key when no tenant context exists (backward compatible for single-tenant OSS deployments). Covers all read, write, and delete sites: - ModelController.js: get/set MODELS_CONFIG - PluginController.js: get/set PLUGINS, get/set TOOLS - getEndpointsConfig.js: get/set/delete ENDPOINT_CONFIG - app.js: delete ENDPOINT_CONFIG (clearEndpointConfigCache) - mcp.js: delete TOOLS (updateMCPTools, mergeAppTools) - importers.js: get ENDPOINT_CONFIG * fix: add getTenantId to PluginController spec mock The data-schemas mock was missing getTenantId, causing all PluginController tests to throw when the controller calls getTenantId() for tenant-scoped cache keys. * fix: address review findings — migration, strict-mode, DRY, types Addresses all CRITICAL, MAJOR, and MINOR review findings: F1 (CRITICAL): Add agents, conversations, messages, presets to SUPERSEDED_INDEXES in tenantIndexes.ts so dropSupersededTenantIndexes() drops the old single-field unique indexes that block multi-tenant inserts. F2 (CRITICAL): Unknown bulkWrite op types now throw in strict mode instead of silently passing through without tenant injection. F3 (MAJOR): Replace wildcard export with named export for tenantSafeBulkWrite, hiding _resetBulkWriteStrictCache from the public package API. F5 (MAJOR): Restore AnyBulkWriteOperation<IAclEntry>[] typing on bulkWriteAclEntries — the unparameterized wrapper accepts parameterized ops as a subtype. F7 (MAJOR): Fix config.js tenant precedence — JWT-derived req.user.tenantId now takes priority over the X-Tenant-Id header for authenticated requests. F8 (MINOR): Extract scopedCacheKey() helper into tenantContext.ts and replace all 11 inline occurrences across 7 files. F9 (MINOR): Use simple localField/foreignField $lookup for the non-tenant getPromptGroup path (more efficient index seeks). F12 (NIT): Remove redundant BulkOp type alias. F13 (NIT): Remove debug log that leaked raw tenantId. * fix: add new superseded indexes to tenantIndexes test fixture The test creates old indexes to verify the migration drops them. Missing fixture entries for agents.id_1, conversations.conversationId_1, messages.messageId_1, and presets.presetId_1 caused the count assertion to fail (expected 22, got 18). * fix: restore logger.warn for unknown bulk op types in non-strict mode * fix: block SYSTEM_TENANT_ID sentinel from external header input CRITICAL: preAuthTenantMiddleware accepted any string as X-Tenant-Id, including '__SYSTEM__'. The tenantIsolation plugin treats SYSTEM_TENANT_ID as an explicit bypass — skipping ALL query filters. A client sending X-Tenant-Id: __SYSTEM__ to pre-auth routes (/api/share, /api/config, /api/auth, /oauth) would execute Mongoose operations without tenant isolation. Fixes: - preAuthTenantMiddleware rejects SYSTEM_TENANT_ID in header - scopedCacheKey returns the base key (not key:__SYSTEM__) in system context, preventing stale cache entries during runAsSystem() - updateInterfacePermissions guards tenantId against SYSTEM_TENANT_ID - $lookup pipeline separates $expr join from constant tenantId match for better index utilization - Regression test for sentinel rejection in preAuthTenant.spec.ts - Remove redundant getTenantId() call in config.js * test: add missing deleteMany/replaceOne coverage, fix vacuous ALS assertions bulkWrite spec: - deleteMany: verifies tenant-scoped deletion leaves other tenants untouched - replaceOne: verifies tenantId injected into both filter and replacement - replaceOne overwrite: verifies a conflicting tenantId in the replacement document is overwritten by the ALS tenant (defense-in-depth) - empty ops array: verifies graceful handling preAuthTenant spec: - All negative-case tests now use the capturedNext pattern to verify getTenantId() inside the middleware's execution context, not the test runner's outer frame (which was always undefined regardless) * feat: tenant-isolate MESSAGES cache, FLOWS cache, and GenerationJobManager MESSAGES cache (streamAudio.js): - Cache key now uses scopedCacheKey(messageId) to prefix with tenantId, preventing cross-tenant message content reads during TTS streaming. FLOWS cache (FlowStateManager): - getFlowKey() now generates ${type}:${tenantId}:${flowId} when tenant context is active, isolating OAuth flow state per tenant. GenerationJobManager: - tenantId added to SerializableJobData and GenerationJobMetadata - createJob() captures the current ALS tenant context (excluding SYSTEM_TENANT_ID) and stores it in job metadata - SSE subscription endpoint validates job.metadata.tenantId matches req.user.tenantId, blocking cross-tenant stream access - Both InMemoryJobStore and RedisJobStore updated to accept tenantId * fix: add getTenantId and SYSTEM_TENANT_ID to MCP OAuth test mocks FlowStateManager.getFlowKey() now calls getTenantId() for tenant-scoped flow keys. The 4 MCP OAuth test files mock @librechat/data-schemas without these exports, causing TypeError at runtime. * fix: correct import ordering per AGENTS.md conventions Package imports sorted shortest to longest line length, local imports sorted longest to shortest — fixes ordering violations introduced by our new imports across 8 files. * fix: deserialize tenantId in RedisJobStore — cross-tenant SSE guard was no-op in Redis mode serializeJob() writes tenantId to the Redis hash via Object.entries, but deserializeJob() manually enumerates fields and omitted tenantId. Every getJob() from Redis returned tenantId: undefined, causing the SSE route's cross-tenant guard to short-circuit (undefined && ... → false). * test: SSE tenant guard, FlowStateManager key consistency, ALS scope docs SSE stream tenant tests (streamTenant.spec.js): - Cross-tenant user accessing another tenant's stream → 403 - Same-tenant user accessing own stream → allowed - OSS mode (no tenantId on job) → tenant check skipped FlowStateManager tenant tests (manager.tenant.spec.ts): - completeFlow finds flow created under same tenant context - completeFlow does NOT find flow under different tenant context - Unscoped flows are separate from tenant-scoped flows Documentation: - JSDoc on getFlowKey documenting ALS context consistency requirement - Comment on streamAudio.js scopedCacheKey capture site * fix: SSE stream tests hang on success path, remove internal fork references The success-path tests entered the SSE streaming code which never closes, causing timeout. Mock subscribe() to end the response immediately. Restructured assertions to verify non-403/non-404. Removed "private fork" and "OSS" references from code and test descriptions — replaced with "deployment layer", "multi-tenant deployments", and "single-tenant mode". * fix: address review findings — test rigor, tenant ID validation, docs F1: SSE stream tests now mock subscribe() with correct signature (streamId, writeEvent, onDone, onError) and assert 200 status, verifying the tenant guard actually allows through same-tenant users. F2: completeFlow logs the attempted key and ALS tenantId when flow is not found, so reverse proxy misconfiguration (missing X-Tenant-Id on OAuth callback) produces an actionable warning. F3/F10: preAuthTenantMiddleware validates tenant ID format — rejects colons, special characters, and values exceeding 128 chars. Trims whitespace. Prevents cache key collisions via crafted headers. F4: Documented cache invalidation scope limitation in clearEndpointConfigCache — only the calling tenant's key is cleared; other tenants expire via TTL. F7: getFlowKey JSDoc now lists all 8 methods requiring consistent ALS context. F8: Added dedicated scopedCacheKey unit tests — base key without context, base key in system context, scoped key with tenant, no ALS leakage across scope boundaries. * fix: revert flow key tenant scoping, fix SSE test timing FlowStateManager: Reverts tenant-scoped flow keys. OAuth callbacks arrive without tenant ALS context (provider redirects don't carry X-Tenant-Id), so completeFlow/failFlow would never find flows created under tenant context. Flow IDs are random UUIDs with no collision risk, and flow data is ephemeral (TTL-bounded). SSE tests: Use process.nextTick for onDone callback so Express response headers are flushed before res.write/res.end are called. * fix: restore getTenantId import for completeFlow diagnostic log * fix: correct completeFlow warning message, add missing flow test The warning referenced X-Tenant-Id header consistency which was only relevant when flow keys were tenant-scoped (since reverted). Updated to list actual causes: TTL expiry, missing flow, or routing to a different instance without shared Keyv storage. Removed the getTenantId() call and import — no longer needed since flow keys are unscoped. Added test for the !flowState branch in completeFlow — verifies return false and logger.warn on nonexistent flow ID. * fix: add explicit return type to recursive updateInterfacePermissions The recursive call (tenantId branch calls itself without tenantId) causes TypeScript to infer circular return type 'any'. Adding explicit Promise<void> satisfies the rollup typescript plugin. * fix: update MCPOAuthRaceCondition test to match new completeFlow warning * fix: clearEndpointConfigCache deletes both scoped and unscoped keys Unauthenticated /api/endpoints requests populate the unscoped ENDPOINT_CONFIG key. Admin config mutations clear only the tenant-scoped key, leaving the unscoped entry stale indefinitely. Now deletes both when in tenant context. * fix: tenant guard on abort/status endpoints, warn logs, test coverage F1: Add tenant guard to /chat/status/:conversationId and /chat/abort matching the existing guard on /chat/stream/:streamId. The status endpoint exposes aggregatedContent (AI response text) which requires tenant-level access control. F2: preAuthTenantMiddleware now logs warn for rejected __SYSTEM__ sentinel and malformed tenant IDs, providing observability for bypass probing attempts. F3: Abort fallback path (getActiveJobIdsForUser) now has tenant check after resolving the job. F4: Test for strict mode + SYSTEM_TENANT_ID — verifies runAsSystem bypasses tenantSafeBulkWrite without throwing in strict mode. F5: Test for job with tenantId + user without tenantId → 403. F10: Regex uses idiomatic hyphen-at-start form. F11: Test descriptions changed from "rejects" to "ignores" since middleware calls next() (not 4xx). Also fixes MCPOAuthRaceCondition test assertion to match updated completeFlow warning message. * fix: test coverage for logger.warn, status/abort guards, consistency A: preAuthTenant spec now mocks logger and asserts warn calls for __SYSTEM__ sentinel, malformed characters, and oversized headers. B: streamTenant spec expanded with status and abort endpoint tests — cross-tenant status returns 403, same-tenant returns 200 with body, cross-tenant abort returns 403. C: Abort endpoint uses req.user.tenantId (not req.user?.tenantId) matching stream/status pattern — requireJwtAuth guarantees req.user. D: Malformed header warning now includes ip in log metadata, matching the sentinel warning for consistent SOC correlation. * fix: assert ip field in malformed header warn tests * fix: parallelize cache deletes, document tenant guard, fix import order - clearEndpointConfigCache uses Promise.all for independent cache deletes instead of sequential awaits - SSE stream tenant guard has inline comment explaining backward-compat behavior for untenanted legacy jobs - conversation.ts local imports reordered longest-to-shortest per AGENTS.md * fix: tenant-qualify userJobs keys, document tenant guard backward-compat Job store userJobs keys now include tenantId when available: - Redis: stream:user:{tenantId:userId}:jobs (falls back to stream:user:{userId}:jobs when no tenant) - InMemory: composite key tenantId:userId in userJobMap getActiveJobIdsByUser/getActiveJobIdsForUser accept optional tenantId parameter, threaded through from req.user.tenantId at all call sites (/chat/active and /chat/abort fallback). Added inline comments on all three SSE tenant guards explaining the backward-compat design: untenanted legacy jobs remain accessible when the userId check passes. * fix: parallelize cache deletes, document tenant guard, fix import order Fix InMemoryJobStore.getActiveJobIdsByUser empty-set cleanup to use the tenant-qualified userKey instead of bare userId — prevents orphaned empty Sets accumulating in userJobMap for multi-tenant users. Document cross-tenant staleness in clearEndpointConfigCache JSDoc — other tenants' scoped keys expire via TTL, not active invalidation. * fix: cleanup userJobMap leak, startup warning, DRY tenant guard, docs F1: InMemoryJobStore.cleanup() now removes entries from userJobMap before calling deleteJob, preventing orphaned empty Sets from accumulating with tenant-qualified composite keys. F2: Startup warning when TENANT_ISOLATION_STRICT is active — reminds operators to configure reverse proxy to control X-Tenant-Id header. F3: mergeAppTools JSDoc documents that tenant-scoped TOOLS keys are not actively invalidated (matching clearEndpointConfigCache pattern). F5: Abort handler getActiveJobIdsForUser call uses req.user.tenantId (not req.user?.tenantId) — consistent with stream/status handlers. F6: updateInterfacePermissions JSDoc clarifies SYSTEM_TENANT_ID behavior — falls through to caller's ALS context. F7: Extracted hasTenantMismatch() helper, replacing three identical inline tenant guard blocks across stream/status/abort endpoints. F9: scopedCacheKey JSDoc documents both passthrough cases (no context and SYSTEM_TENANT_ID context). * fix: clean userJobMap in evictOldest — same leak as cleanup()
2026-03-28 16:43:50 -04:00
const result = await tenantSafeBulkWrite(Message, bulkOps);
📦 refactor: Consolidate DB models, encapsulating Mongoose usage in `data-schemas` (#11830) * chore: move database model methods to /packages/data-schemas * chore: add TypeScript ESLint rule to warn on unused variables * refactor: model imports to streamline access - Consolidated model imports across various files to improve code organization and reduce redundancy. - Updated imports for models such as Assistant, Message, Conversation, and others to a unified import path. - Adjusted middleware and service files to reflect the new import structure, ensuring functionality remains intact. - Enhanced test files to align with the new import paths, maintaining test coverage and integrity. * chore: migrate database models to packages/data-schemas and refactor all direct Mongoose Model usage outside of data-schemas * test: update agent model mocks in unit tests - Added `getAgent` mock to `client.test.js` to enhance test coverage for agent-related functionality. - Removed redundant `getAgent` and `getAgents` mocks from `openai.spec.js` and `responses.unit.spec.js` to streamline test setup and reduce duplication. - Ensured consistency in agent mock implementations across test files. * fix: update types in data-schemas * refactor: enhance type definitions in transaction and spending methods - Updated type definitions in `checkBalance.ts` to use specific request and response types. - Refined `spendTokens.ts` to utilize a new `SpendTxData` interface for better clarity and type safety. - Improved transaction handling in `transaction.ts` by introducing `TransactionResult` and `TxData` interfaces, ensuring consistent data structures across methods. - Adjusted unit tests in `transaction.spec.ts` to accommodate new type definitions and enhance robustness. * refactor: streamline model imports and enhance code organization - Consolidated model imports across various controllers and services to a unified import path, improving code clarity and reducing redundancy. - Updated multiple files to reflect the new import structure, ensuring all functionalities remain intact. - Enhanced overall code organization by removing duplicate import statements and optimizing the usage of model methods. * feat: implement loadAddedAgent and refactor agent loading logic - Introduced `loadAddedAgent` function to handle loading agents from added conversations, supporting multi-convo parallel execution. - Created a new `load.ts` file to encapsulate agent loading functionalities, including `loadEphemeralAgent` and `loadAgent`. - Updated the `index.ts` file to export the new `load` module instead of the deprecated `loadAgent`. - Enhanced type definitions and improved error handling in the agent loading process. - Adjusted unit tests to reflect changes in the agent loading structure and ensure comprehensive coverage. * refactor: enhance balance handling with new update interface - Introduced `IBalanceUpdate` interface to streamline balance update operations across the codebase. - Updated `upsertBalanceFields` method signatures in `balance.ts`, `transaction.ts`, and related tests to utilize the new interface for improved type safety. - Adjusted type imports in `balance.spec.ts` to include `IBalanceUpdate`, ensuring consistency in balance management functionalities. - Enhanced overall code clarity and maintainability by refining type definitions related to balance operations. * feat: add unit tests for loadAgent functionality and enhance agent loading logic - Introduced comprehensive unit tests for the `loadAgent` function, covering various scenarios including null and empty agent IDs, loading of ephemeral agents, and permission checks. - Enhanced the `initializeClient` function by moving `getConvoFiles` to the correct position in the database method exports, ensuring proper functionality. - Improved test coverage for agent loading, including handling of non-existent agents and user permissions. * chore: reorder memory method exports for consistency - Moved `deleteAllUserMemories` to the correct position in the exported memory methods, ensuring a consistent and logical order of method exports in `memory.ts`.
2026-02-17 18:23:44 -05:00
return result;
} catch (err) {
logger.error('Error saving messages in bulk:', err);
throw err;
}
}
/**
* Records a message in the database (no UUID validation).
*/
async function recordMessage({
user,
endpoint,
messageId,
conversationId,
parentMessageId,
...rest
}: {
user: string;
endpoint?: string;
messageId: string;
conversationId?: string;
parentMessageId?: string;
[key: string]: unknown;
}) {
try {
const Message = mongoose.models.Message as Model<IMessage>;
🛡️ refactor: Self-Healing Tenant Isolation Update Guard (#12506) * refactor: self-healing tenant isolation update guard Replace the strict throw-on-any-tenantId guard with a strip-or-throw approach: - $set/$setOnInsert: strip when value matches current tenant or no context is active; throw only on cross-tenant mutations - $unset/$rename: always strip (unsetting/renaming tenantId is never valid) - Top-level tenantId: same logic as $set This eliminates the entire class of "tenantId in update payload" bugs at the plugin level while preserving the cross-tenant security invariant. * test: update mutation guard tests for self-healing behavior - Convert same-tenant $set/$setOnInsert tests to expect silent stripping instead of throws - Convert $unset test to expect silent stripping - Add cross-tenant throw tests for $set, $setOnInsert, top-level - Add same-tenant stripping tests for $set, $setOnInsert, top-level - Add $rename stripping test - Add no-context stripping test - Update error message assertions to match new cross-tenant message * revert: remove call-site tenantId stripping patches Revert the per-call-site tenantId stripping from #12498 and the excludedKeys patch from #12501. These are no longer needed since the self-healing guard handles tenantId in update payloads at the plugin level. Reverted patches: - conversation.ts: delete update.tenantId in saveConvo(), tenantId destructuring in bulkSaveConvos() - message.ts: delete update.tenantId in saveMessage() and recordMessage(), tenantId destructuring in bulkSaveMessages() and updateMessage() - config.ts: tenantId in excludedKeys Set - config.spec.ts: tenantId in excludedKeys test assertion * fix: strip tenantId from update documents in tenantSafeBulkWrite Mongoose middleware does not fire for bulkWrite, so the plugin-level guard never sees update payloads in bulk operations. Extend injectTenantId() to strip tenantId from update documents for updateOne/updateMany operations, preventing cross-tenant overwrites. * refactor: rename guard, add empty-op cleanup and strict-mode warning - Rename assertNoTenantIdMutation to sanitizeTenantIdMutation - Remove empty operator objects after stripping to avoid MongoDB errors - Log warning in strict mode when stripping tenantId without context - Fix $setOnInsert test to use upsert:true with non-matching filter * test: fix bulk-save tests and add negative excludedKeys assertion - Wrap bulkSaveConvos/bulkSaveMessages tests in tenantStorage.run() to exercise the actual multi-tenant stripping path - Assert tenantId equals the real tenant, not undefined - Add negative assertion: excludedKeys must NOT contain tenantId * fix: type-safe tenantId stripping in tenantSafeBulkWrite - Fix TS2345 error: replace conditional type inference with UpdateQuery<Record<string, unknown>> for stripTenantIdFromUpdate - Handle empty updates after stripping (e.g., $set: { tenantId } as sole field) by filtering null ops from the bulk array - Add 4 tests for bulk update tenantId stripping: plain-object update, $set stripping, $unset stripping, and sole-field-in-$set edge case * fix: resolve TS2345 in stripTenantIdFromUpdate parameter type Use Record<string, unknown> instead of UpdateQuery<> to avoid type incompatibility with Mongoose's AnyObject-based UpdateQuery resolution in CI. * fix: strip tenantId from bulk updates unconditionally Separate sanitization from injection in tenantSafeBulkWrite: tenantId is now stripped from all update documents before any tenant-context checks, closing the gap where no-context and system-context paths passed caller-supplied tenantId through to MongoDB unmodified. * refactor: address review findings in tenant isolation - Fix early-return gap in stripTenantIdFromUpdate that skipped operator-level tenantId when top-level was also present - Lazy-allocate copy in stripTenantIdFromUpdate (no allocation when no tenantId is present) - Document behavioral asymmetry: plugin throws on cross-tenant, bulkWrite strips silently (intentional, documented in JSDoc) - Remove double JSDoc on injectTenantId - Remove redundant cast in stripTenantIdFromUpdate - Use shared frozen EMPTY_BULK_RESULT constant - Remove Record<string, unknown> annotation in recordMessage - Isolate bulkSave* tests: pre-create docs then update with cross-tenant payload, read via runAsSystem to prove stripping is independent of filter injection * fix: no-op empty updates after tenantId sanitization When tenantId is the sole field in an update (e.g., { $set: { tenantId } }), sanitization leaves an empty update object that would fail with "Update document requires atomic operators." The updateGuard now detects this and short-circuits the query by adding an unmatchable filter condition and disabling upsert, matching the bulk-write handling that filters out null ops. * refactor: remove dead logger.warn branches, add mixed-case test - Remove unreachable logger.warn calls in sanitizeTenantIdMutation: queryMiddleware throws before updateGuard in strict+no-context, and isStrict() is false in non-strict+no-context - Add test for combined top-level + operator-level tenantId stripping to lock in the early-return fix * feat: ESLint rule to ban raw bulkWrite and collection.* in data-schemas Add no-restricted-syntax rules to the data-schemas ESLint config that flag direct Model.bulkWrite() and Model.collection.* calls. These bypass Mongoose middleware and the tenant isolation plugin — all bulk writes must use tenantSafeBulkWrite() instead. Test files are excluded since they intentionally use raw driver calls for fixture setup. Also migrate the one remaining raw bulkWrite in seedSystemGrants() to use tenantSafeBulkWrite() for consistency. * test: add findByIdAndUpdate coverage to mutation guard tests * fix: keep tenantSafeBulkWrite in seedSystemGrants, fix ESLint config - Revert to tenantSafeBulkWrite in seedSystemGrants (always runs under runAsSystem, so the wrapper passes through correctly) - Split data-schemas ESLint config: shared TS rules for all files, no-restricted-syntax only for production non-wrapper files - Fix unused destructure vars to use _tenantId pattern
2026-04-01 19:07:52 -04:00
const message = {
📦 refactor: Consolidate DB models, encapsulating Mongoose usage in `data-schemas` (#11830) * chore: move database model methods to /packages/data-schemas * chore: add TypeScript ESLint rule to warn on unused variables * refactor: model imports to streamline access - Consolidated model imports across various files to improve code organization and reduce redundancy. - Updated imports for models such as Assistant, Message, Conversation, and others to a unified import path. - Adjusted middleware and service files to reflect the new import structure, ensuring functionality remains intact. - Enhanced test files to align with the new import paths, maintaining test coverage and integrity. * chore: migrate database models to packages/data-schemas and refactor all direct Mongoose Model usage outside of data-schemas * test: update agent model mocks in unit tests - Added `getAgent` mock to `client.test.js` to enhance test coverage for agent-related functionality. - Removed redundant `getAgent` and `getAgents` mocks from `openai.spec.js` and `responses.unit.spec.js` to streamline test setup and reduce duplication. - Ensured consistency in agent mock implementations across test files. * fix: update types in data-schemas * refactor: enhance type definitions in transaction and spending methods - Updated type definitions in `checkBalance.ts` to use specific request and response types. - Refined `spendTokens.ts` to utilize a new `SpendTxData` interface for better clarity and type safety. - Improved transaction handling in `transaction.ts` by introducing `TransactionResult` and `TxData` interfaces, ensuring consistent data structures across methods. - Adjusted unit tests in `transaction.spec.ts` to accommodate new type definitions and enhance robustness. * refactor: streamline model imports and enhance code organization - Consolidated model imports across various controllers and services to a unified import path, improving code clarity and reducing redundancy. - Updated multiple files to reflect the new import structure, ensuring all functionalities remain intact. - Enhanced overall code organization by removing duplicate import statements and optimizing the usage of model methods. * feat: implement loadAddedAgent and refactor agent loading logic - Introduced `loadAddedAgent` function to handle loading agents from added conversations, supporting multi-convo parallel execution. - Created a new `load.ts` file to encapsulate agent loading functionalities, including `loadEphemeralAgent` and `loadAgent`. - Updated the `index.ts` file to export the new `load` module instead of the deprecated `loadAgent`. - Enhanced type definitions and improved error handling in the agent loading process. - Adjusted unit tests to reflect changes in the agent loading structure and ensure comprehensive coverage. * refactor: enhance balance handling with new update interface - Introduced `IBalanceUpdate` interface to streamline balance update operations across the codebase. - Updated `upsertBalanceFields` method signatures in `balance.ts`, `transaction.ts`, and related tests to utilize the new interface for improved type safety. - Adjusted type imports in `balance.spec.ts` to include `IBalanceUpdate`, ensuring consistency in balance management functionalities. - Enhanced overall code clarity and maintainability by refining type definitions related to balance operations. * feat: add unit tests for loadAgent functionality and enhance agent loading logic - Introduced comprehensive unit tests for the `loadAgent` function, covering various scenarios including null and empty agent IDs, loading of ephemeral agents, and permission checks. - Enhanced the `initializeClient` function by moving `getConvoFiles` to the correct position in the database method exports, ensuring proper functionality. - Improved test coverage for agent loading, including handling of non-existent agents and user permissions. * chore: reorder memory method exports for consistency - Moved `deleteAllUserMemories` to the correct position in the exported memory methods, ensuring a consistent and logical order of method exports in `memory.ts`.
2026-02-17 18:23:44 -05:00
user,
endpoint,
messageId,
conversationId,
parentMessageId,
...rest,
};
return await Message.findOneAndUpdate({ user, messageId }, message, {
upsert: true,
new: true,
});
} catch (err) {
logger.error('Error recording message:', err);
throw err;
}
}
/**
* Updates the text of a message.
*/
async function updateMessageText(
userId: string,
{ messageId, text }: { messageId: string; text: string },
) {
try {
const Message = mongoose.models.Message as Model<IMessage>;
await Message.updateOne({ messageId, user: userId }, { text });
} catch (err) {
logger.error('Error updating message text:', err);
throw err;
}
}
/**
* Updates a message and returns sanitized fields.
*/
async function updateMessage(
userId: string,
message: { messageId: string; [key: string]: unknown },
metadata?: { context?: string },
) {
try {
const Message = mongoose.models.Message as Model<IMessage>;
🛡️ refactor: Self-Healing Tenant Isolation Update Guard (#12506) * refactor: self-healing tenant isolation update guard Replace the strict throw-on-any-tenantId guard with a strip-or-throw approach: - $set/$setOnInsert: strip when value matches current tenant or no context is active; throw only on cross-tenant mutations - $unset/$rename: always strip (unsetting/renaming tenantId is never valid) - Top-level tenantId: same logic as $set This eliminates the entire class of "tenantId in update payload" bugs at the plugin level while preserving the cross-tenant security invariant. * test: update mutation guard tests for self-healing behavior - Convert same-tenant $set/$setOnInsert tests to expect silent stripping instead of throws - Convert $unset test to expect silent stripping - Add cross-tenant throw tests for $set, $setOnInsert, top-level - Add same-tenant stripping tests for $set, $setOnInsert, top-level - Add $rename stripping test - Add no-context stripping test - Update error message assertions to match new cross-tenant message * revert: remove call-site tenantId stripping patches Revert the per-call-site tenantId stripping from #12498 and the excludedKeys patch from #12501. These are no longer needed since the self-healing guard handles tenantId in update payloads at the plugin level. Reverted patches: - conversation.ts: delete update.tenantId in saveConvo(), tenantId destructuring in bulkSaveConvos() - message.ts: delete update.tenantId in saveMessage() and recordMessage(), tenantId destructuring in bulkSaveMessages() and updateMessage() - config.ts: tenantId in excludedKeys Set - config.spec.ts: tenantId in excludedKeys test assertion * fix: strip tenantId from update documents in tenantSafeBulkWrite Mongoose middleware does not fire for bulkWrite, so the plugin-level guard never sees update payloads in bulk operations. Extend injectTenantId() to strip tenantId from update documents for updateOne/updateMany operations, preventing cross-tenant overwrites. * refactor: rename guard, add empty-op cleanup and strict-mode warning - Rename assertNoTenantIdMutation to sanitizeTenantIdMutation - Remove empty operator objects after stripping to avoid MongoDB errors - Log warning in strict mode when stripping tenantId without context - Fix $setOnInsert test to use upsert:true with non-matching filter * test: fix bulk-save tests and add negative excludedKeys assertion - Wrap bulkSaveConvos/bulkSaveMessages tests in tenantStorage.run() to exercise the actual multi-tenant stripping path - Assert tenantId equals the real tenant, not undefined - Add negative assertion: excludedKeys must NOT contain tenantId * fix: type-safe tenantId stripping in tenantSafeBulkWrite - Fix TS2345 error: replace conditional type inference with UpdateQuery<Record<string, unknown>> for stripTenantIdFromUpdate - Handle empty updates after stripping (e.g., $set: { tenantId } as sole field) by filtering null ops from the bulk array - Add 4 tests for bulk update tenantId stripping: plain-object update, $set stripping, $unset stripping, and sole-field-in-$set edge case * fix: resolve TS2345 in stripTenantIdFromUpdate parameter type Use Record<string, unknown> instead of UpdateQuery<> to avoid type incompatibility with Mongoose's AnyObject-based UpdateQuery resolution in CI. * fix: strip tenantId from bulk updates unconditionally Separate sanitization from injection in tenantSafeBulkWrite: tenantId is now stripped from all update documents before any tenant-context checks, closing the gap where no-context and system-context paths passed caller-supplied tenantId through to MongoDB unmodified. * refactor: address review findings in tenant isolation - Fix early-return gap in stripTenantIdFromUpdate that skipped operator-level tenantId when top-level was also present - Lazy-allocate copy in stripTenantIdFromUpdate (no allocation when no tenantId is present) - Document behavioral asymmetry: plugin throws on cross-tenant, bulkWrite strips silently (intentional, documented in JSDoc) - Remove double JSDoc on injectTenantId - Remove redundant cast in stripTenantIdFromUpdate - Use shared frozen EMPTY_BULK_RESULT constant - Remove Record<string, unknown> annotation in recordMessage - Isolate bulkSave* tests: pre-create docs then update with cross-tenant payload, read via runAsSystem to prove stripping is independent of filter injection * fix: no-op empty updates after tenantId sanitization When tenantId is the sole field in an update (e.g., { $set: { tenantId } }), sanitization leaves an empty update object that would fail with "Update document requires atomic operators." The updateGuard now detects this and short-circuits the query by adding an unmatchable filter condition and disabling upsert, matching the bulk-write handling that filters out null ops. * refactor: remove dead logger.warn branches, add mixed-case test - Remove unreachable logger.warn calls in sanitizeTenantIdMutation: queryMiddleware throws before updateGuard in strict+no-context, and isStrict() is false in non-strict+no-context - Add test for combined top-level + operator-level tenantId stripping to lock in the early-return fix * feat: ESLint rule to ban raw bulkWrite and collection.* in data-schemas Add no-restricted-syntax rules to the data-schemas ESLint config that flag direct Model.bulkWrite() and Model.collection.* calls. These bypass Mongoose middleware and the tenant isolation plugin — all bulk writes must use tenantSafeBulkWrite() instead. Test files are excluded since they intentionally use raw driver calls for fixture setup. Also migrate the one remaining raw bulkWrite in seedSystemGrants() to use tenantSafeBulkWrite() for consistency. * test: add findByIdAndUpdate coverage to mutation guard tests * fix: keep tenantSafeBulkWrite in seedSystemGrants, fix ESLint config - Revert to tenantSafeBulkWrite in seedSystemGrants (always runs under runAsSystem, so the wrapper passes through correctly) - Split data-schemas ESLint config: shared TS rules for all files, no-restricted-syntax only for production non-wrapper files - Fix unused destructure vars to use _tenantId pattern
2026-04-01 19:07:52 -04:00
const { messageId, ...update } = message;
📦 refactor: Consolidate DB models, encapsulating Mongoose usage in `data-schemas` (#11830) * chore: move database model methods to /packages/data-schemas * chore: add TypeScript ESLint rule to warn on unused variables * refactor: model imports to streamline access - Consolidated model imports across various files to improve code organization and reduce redundancy. - Updated imports for models such as Assistant, Message, Conversation, and others to a unified import path. - Adjusted middleware and service files to reflect the new import structure, ensuring functionality remains intact. - Enhanced test files to align with the new import paths, maintaining test coverage and integrity. * chore: migrate database models to packages/data-schemas and refactor all direct Mongoose Model usage outside of data-schemas * test: update agent model mocks in unit tests - Added `getAgent` mock to `client.test.js` to enhance test coverage for agent-related functionality. - Removed redundant `getAgent` and `getAgents` mocks from `openai.spec.js` and `responses.unit.spec.js` to streamline test setup and reduce duplication. - Ensured consistency in agent mock implementations across test files. * fix: update types in data-schemas * refactor: enhance type definitions in transaction and spending methods - Updated type definitions in `checkBalance.ts` to use specific request and response types. - Refined `spendTokens.ts` to utilize a new `SpendTxData` interface for better clarity and type safety. - Improved transaction handling in `transaction.ts` by introducing `TransactionResult` and `TxData` interfaces, ensuring consistent data structures across methods. - Adjusted unit tests in `transaction.spec.ts` to accommodate new type definitions and enhance robustness. * refactor: streamline model imports and enhance code organization - Consolidated model imports across various controllers and services to a unified import path, improving code clarity and reducing redundancy. - Updated multiple files to reflect the new import structure, ensuring all functionalities remain intact. - Enhanced overall code organization by removing duplicate import statements and optimizing the usage of model methods. * feat: implement loadAddedAgent and refactor agent loading logic - Introduced `loadAddedAgent` function to handle loading agents from added conversations, supporting multi-convo parallel execution. - Created a new `load.ts` file to encapsulate agent loading functionalities, including `loadEphemeralAgent` and `loadAgent`. - Updated the `index.ts` file to export the new `load` module instead of the deprecated `loadAgent`. - Enhanced type definitions and improved error handling in the agent loading process. - Adjusted unit tests to reflect changes in the agent loading structure and ensure comprehensive coverage. * refactor: enhance balance handling with new update interface - Introduced `IBalanceUpdate` interface to streamline balance update operations across the codebase. - Updated `upsertBalanceFields` method signatures in `balance.ts`, `transaction.ts`, and related tests to utilize the new interface for improved type safety. - Adjusted type imports in `balance.spec.ts` to include `IBalanceUpdate`, ensuring consistency in balance management functionalities. - Enhanced overall code clarity and maintainability by refining type definitions related to balance operations. * feat: add unit tests for loadAgent functionality and enhance agent loading logic - Introduced comprehensive unit tests for the `loadAgent` function, covering various scenarios including null and empty agent IDs, loading of ephemeral agents, and permission checks. - Enhanced the `initializeClient` function by moving `getConvoFiles` to the correct position in the database method exports, ensuring proper functionality. - Improved test coverage for agent loading, including handling of non-existent agents and user permissions. * chore: reorder memory method exports for consistency - Moved `deleteAllUserMemories` to the correct position in the exported memory methods, ensuring a consistent and logical order of method exports in `memory.ts`.
2026-02-17 18:23:44 -05:00
const updatedMessage = await Message.findOneAndUpdate({ messageId, user: userId }, update, {
new: true,
});
if (!updatedMessage) {
throw new Error('Message not found or user not authorized.');
}
return {
messageId: updatedMessage.messageId,
conversationId: updatedMessage.conversationId,
parentMessageId: updatedMessage.parentMessageId,
sender: updatedMessage.sender,
text: updatedMessage.text,
isCreatedByUser: updatedMessage.isCreatedByUser,
tokenCount: updatedMessage.tokenCount,
feedback: updatedMessage.feedback,
};
} catch (err) {
logger.error('Error updating message:', err);
if (metadata?.context) {
logger.info(`---\`updateMessage\` context: ${metadata.context}`);
}
throw err;
}
}
/**
* Deletes messages in a conversation since a specific message.
*/
async function deleteMessagesSince(
userId: string,
{ messageId, conversationId }: { messageId: string; conversationId: string },
) {
try {
const Message = mongoose.models.Message as Model<IMessage>;
const message = await Message.findOne({ messageId, user: userId }).lean();
if (message) {
const query = Message.find({ conversationId, user: userId });
return await query.deleteMany({
createdAt: { $gt: message.createdAt },
});
}
return undefined;
} catch (err) {
logger.error('Error deleting messages:', err);
throw err;
}
}
/**
* Retrieves messages from the database.
*/
async function getMessages(filter: FilterQuery<IMessage>, select?: string) {
try {
const Message = mongoose.models.Message as Model<IMessage>;
if (select) {
return await Message.find(filter).select(select).sort({ createdAt: 1 }).lean();
}
return await Message.find(filter).sort({ createdAt: 1 }).lean();
} catch (err) {
logger.error('Error getting messages:', err);
throw err;
}
}
/**
* Retrieves a single message from the database.
*/
async function getMessage({ user, messageId }: { user: string; messageId: string }) {
try {
const Message = mongoose.models.Message as Model<IMessage>;
return await Message.findOne({ user, messageId }).lean();
} catch (err) {
logger.error('Error getting message:', err);
throw err;
}
}
/**
* Deletes messages from the database.
*/
async function deleteMessages(filter: FilterQuery<IMessage>) {
try {
const Message = mongoose.models.Message as Model<IMessage>;
return await Message.deleteMany(filter);
} catch (err) {
logger.error('Error deleting messages:', err);
throw err;
}
}
/**
* Retrieves paginated messages with custom sorting and cursor support.
*/
async function getMessagesByCursor(
filter: FilterQuery<IMessage>,
options: {
sortField?: string;
sortOrder?: 1 | -1;
limit?: number;
cursor?: string | null;
} = {},
) {
const Message = mongoose.models.Message as Model<IMessage>;
const { sortField = 'createdAt', sortOrder = -1, limit = 25, cursor } = options;
const queryFilter = { ...filter };
if (cursor) {
queryFilter[sortField] = sortOrder === 1 ? { $gt: cursor } : { $lt: cursor };
}
const messages = await Message.find(queryFilter)
.sort({ [sortField]: sortOrder })
.limit(limit + 1)
.lean();
let nextCursor: string | null = null;
if (messages.length > limit) {
messages.pop();
const last = messages[messages.length - 1] as Record<string, unknown>;
nextCursor = String(last[sortField] ?? '');
}
return { messages, nextCursor };
}
/**
* Performs a MeiliSearch query on the Message collection.
* Requires the meilisearch plugin to be registered on the Message model.
*/
async function searchMessages(
query: string,
searchOptions: Record<string, unknown>,
hydrate?: boolean,
) {
const Message = mongoose.models.Message as Model<IMessage> & {
meiliSearch?: (q: string, opts: Record<string, unknown>, h?: boolean) => Promise<unknown>;
};
if (typeof Message.meiliSearch !== 'function') {
throw new Error('MeiliSearch plugin not registered on Message model');
}
return Message.meiliSearch(query, searchOptions, hydrate);
}
return {
saveMessage,
bulkSaveMessages,
recordMessage,
updateMessageText,
updateMessage,
deleteMessagesSince,
getMessages,
getMessage,
getMessagesByCursor,
searchMessages,
deleteMessages,
};
}