mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-10-25 19:52:29 +00:00
* fix(data-schemas): resolve TypeScript strict type check errors in source files - Constrain ConfigSection to string keys via `string & keyof TCustomConfig` - Replace broken `z` import from data-provider with TCustomConfig derivation - Add `_id: Types.ObjectId` to IUser matching other Document interfaces - Add `federatedTokens` and `openidTokens` optional fields to IUser - Type mongoose model accessors as `Model<IRole>` and `Model<IUser>` - Widen `getPremiumRate` param to accept `number | null` - Widen `bulkWriteAclEntries` ops to untyped `AnyBulkWriteOperation[]` - Fix `getUserPrincipals` return type to use `PrincipalType` enum - Add non-null assertions for `connection.db` in migration files - Import DailyRotateFile constructor directly instead of relying on broken module augmentation across mismatched node_modules trees - Add winston-daily-rotate-file as devDependency for type resolution * fix(data-schemas): resolve TypeScript type errors in test files - Replace arbitrary test keys with valid TCustomConfig properties in config.spec - Use non-null assertions for permission objects in role.methods.spec - Replace `.SHARED_GLOBAL` access with `.not.toHaveProperty()` for legacy field - Add non-null assertions for balance, writeRate, readRate in spendTokens.spec - Update mock user _id to use ObjectId in user.test - Remove unused Schema import in tenantIndexes.spec * fix(api): resolve TypeScript strict type check errors across source and test files - Widen getUserPrincipals dep type in capabilities middleware - Fix federatedTokens type in createSafeUser return - Use proper mock req type for read-only properties in preAuthTenant.spec - Replace `as IUser` casts with ObjectId-typed mocks in openid/oidc specs - Use TokenExchangeMethodEnum values instead of string literals in MCP specs - Fix SessionStore type compatibility in sessionCache specs - Replace `catch (error: any)` with `(error as Error)` in redis specs - Remove invalid properties from test data in initialize and MCP specs - Add String.prototype.isWellFormed declaration for sanitizeTitle spec * fix(client): resolve TypeScript type errors in shared client components - Add default values for destructured bindings in OGDialogTemplate - Replace broken ExtendedFile import with inline type in FileIcon * ci: add TypeScript type-check job to backend review workflow Add a `typecheck` job that runs `tsc --noEmit` on all four TypeScript workspaces (data-provider, data-schemas, @librechat/api, @librechat/client) after the build step. Catches type errors that rollup builds may miss. * fix(data-schemas): add local type declaration for DailyRotateFile transport The `winston-daily-rotate-file` package ships a module augmentation for `winston/lib/winston/transports`, but it fails when winston and winston-daily-rotate-file resolve from different node_modules trees (which happens in this monorepo due to npm hoisting). Add a local `.d.ts` declaration that augments the same module path from within data-schemas' compilation unit, so `tsc --noEmit` passes while keeping the original runtime pattern (`new winston.transports.DailyRotateFile`). * fix: address code review findings from PR #12451 - Restore typed `AnyBulkWriteOperation<AclEntry>[]` on bulkWriteAclEntries, cast to untyped only at the tenantSafeBulkWrite call site (Finding 1) - Type `findUser` model accessor consistently with `findUsers` (Finding 2) - Replace inline `import('mongoose').ClientSession` with top-level import type - Use `toHaveLength` for spy assertions in playwright-expect spec file - Replace numbered Record casts with `.not.toHaveProperty()` in role.methods.spec for SHARED_GLOBAL assertions - Use per-test ObjectIds instead of shared testUserId in openid.spec - Replace inline `import()` type annotations with top-level SessionData import in sessionCache spec - Remove extraneous blank line in user.ts searchUsers * refactor: address remaining review findings (4–7) - Extract OIDCTokens interface in user.ts; deduplicate across IUser fields and oidc.ts FederatedTokens (Finding 4) - Move String.isWellFormed declaration from spec file to project-level src/types/es2024-string.d.ts (Finding 5) - Replace verbose `= undefined` defaults in OGDialogTemplate with null coalescing pattern (Finding 6) - Replace `Record<string, unknown>` TestConfig with named interface containing explicit test fields (Finding 7)
194 lines
6.1 KiB
TypeScript
194 lines
6.1 KiB
TypeScript
import { isMainThread } from 'node:worker_threads';
|
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
import {
|
|
logger,
|
|
configCapability,
|
|
SystemCapabilities,
|
|
readConfigCapability,
|
|
} from '@librechat/data-schemas';
|
|
import type { PrincipalType } from 'librechat-data-provider';
|
|
import type { SystemCapability, ConfigSection } from '@librechat/data-schemas';
|
|
import type { NextFunction, Response } from 'express';
|
|
import type { Types, ClientSession } from 'mongoose';
|
|
import type { ServerRequest } from '~/types/http';
|
|
|
|
interface ResolvedPrincipal {
|
|
principalType: PrincipalType;
|
|
principalId?: string | Types.ObjectId;
|
|
}
|
|
|
|
interface CapabilityDeps {
|
|
getUserPrincipals: (
|
|
params: { userId: string | Types.ObjectId; role?: string | null },
|
|
session?: ClientSession,
|
|
) => Promise<ResolvedPrincipal[]>;
|
|
hasCapabilityForPrincipals: (params: {
|
|
principals: ResolvedPrincipal[];
|
|
capability: SystemCapability;
|
|
tenantId?: string;
|
|
}) => Promise<boolean>;
|
|
}
|
|
|
|
export interface CapabilityUser {
|
|
id: string;
|
|
role: string;
|
|
tenantId?: string;
|
|
}
|
|
|
|
interface CapabilityStore {
|
|
principals: Map<string, ResolvedPrincipal[]>;
|
|
results: Map<string, boolean>;
|
|
}
|
|
|
|
export type HasCapabilityFn = (
|
|
user: CapabilityUser,
|
|
capability: SystemCapability,
|
|
) => Promise<boolean>;
|
|
|
|
export type RequireCapabilityFn = (
|
|
capability: SystemCapability,
|
|
) => (req: ServerRequest, res: Response, next: NextFunction) => Promise<void>;
|
|
|
|
export type HasConfigCapabilityFn = (
|
|
user: CapabilityUser,
|
|
section: ConfigSection | null,
|
|
verb?: 'manage' | 'read',
|
|
) => Promise<boolean>;
|
|
|
|
/**
|
|
* Per-request store for caching resolved principals and capability check results.
|
|
* When running inside an Express request (via `capabilityContextMiddleware`),
|
|
* duplicate `hasCapability` calls within the same request are served from
|
|
* the in-memory Map instead of hitting the database again.
|
|
* Outside a request context (background jobs, tests), the store is undefined
|
|
* and every check falls through to the database — correct behavior.
|
|
*/
|
|
export const capabilityStore = new AsyncLocalStorage<CapabilityStore>();
|
|
|
|
export function capabilityContextMiddleware(
|
|
_req: ServerRequest,
|
|
_res: Response,
|
|
next: NextFunction,
|
|
): void {
|
|
if (!isMainThread) {
|
|
logger.error(
|
|
'[capabilityContextMiddleware] Mounted in a worker thread — ' +
|
|
'ALS context will not propagate to the main thread or other workers. ' +
|
|
'This middleware should only run in the main Express process.',
|
|
);
|
|
}
|
|
capabilityStore.run({ principals: new Map(), results: new Map() }, next);
|
|
}
|
|
|
|
/**
|
|
* Factory that creates `hasCapability` and `requireCapability` with injected
|
|
* database methods. Follows the same dependency-injection pattern as
|
|
* `generateCheckAccess`.
|
|
*/
|
|
export function generateCapabilityCheck(deps: CapabilityDeps): {
|
|
hasCapability: HasCapabilityFn;
|
|
requireCapability: RequireCapabilityFn;
|
|
hasConfigCapability: HasConfigCapabilityFn;
|
|
} {
|
|
const { getUserPrincipals, hasCapabilityForPrincipals } = deps;
|
|
|
|
let workerWarned = false;
|
|
|
|
async function hasCapability(
|
|
user: CapabilityUser,
|
|
capability: SystemCapability,
|
|
): Promise<boolean> {
|
|
if (!isMainThread && !workerWarned) {
|
|
workerWarned = true;
|
|
logger.warn(
|
|
'[hasCapability] Called from a worker thread — ALS context is unavailable. ' +
|
|
'Capability checks will hit the database on every call (no per-request caching). ' +
|
|
'If this is intentional, no action needed.',
|
|
);
|
|
}
|
|
|
|
const store = capabilityStore.getStore();
|
|
|
|
const resultKey = `${user.id}:${user.tenantId ?? ''}:${capability}`;
|
|
const cached = store?.results.get(resultKey);
|
|
if (cached !== undefined) {
|
|
return cached;
|
|
}
|
|
|
|
const principalKey = `${user.id}:${user.role}:${user.tenantId ?? ''}`;
|
|
let principals: ResolvedPrincipal[];
|
|
const cachedPrincipals = store?.principals.get(principalKey);
|
|
if (cachedPrincipals) {
|
|
principals = cachedPrincipals;
|
|
} else {
|
|
principals = await getUserPrincipals({ userId: user.id, role: user.role });
|
|
store?.principals.set(principalKey, principals);
|
|
}
|
|
|
|
const result = await hasCapabilityForPrincipals({
|
|
principals,
|
|
capability,
|
|
tenantId: user.tenantId,
|
|
});
|
|
store?.results.set(resultKey, result);
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Checks if a user can manage or read a specific config section.
|
|
* First checks the broad capability (manage:configs / read:configs),
|
|
* then falls back to the section-specific capability (manage:configs:<section>).
|
|
*/
|
|
async function hasConfigCapability(
|
|
user: CapabilityUser,
|
|
section: ConfigSection | null,
|
|
verb: 'manage' | 'read' = 'manage',
|
|
): Promise<boolean> {
|
|
const broadCap =
|
|
verb === 'manage' ? SystemCapabilities.MANAGE_CONFIGS : SystemCapabilities.READ_CONFIGS;
|
|
if (section == null) {
|
|
return hasCapability(user, broadCap);
|
|
}
|
|
if (await hasCapability(user, broadCap)) {
|
|
return true;
|
|
}
|
|
const sectionCap =
|
|
verb === 'manage' ? configCapability(section) : readConfigCapability(section);
|
|
return hasCapability(user, sectionCap);
|
|
}
|
|
|
|
function requireCapability(capability: SystemCapability) {
|
|
return async (req: ServerRequest, res: Response, next: NextFunction) => {
|
|
try {
|
|
if (!req.user) {
|
|
res.status(401).json({ message: 'Authentication required' });
|
|
return;
|
|
}
|
|
|
|
const id = req.user.id ?? req.user._id?.toString();
|
|
if (!id) {
|
|
res.status(401).json({ message: 'Authentication required' });
|
|
return;
|
|
}
|
|
|
|
const user: CapabilityUser = {
|
|
id,
|
|
role: req.user.role ?? '',
|
|
tenantId: (req.user as CapabilityUser).tenantId,
|
|
};
|
|
|
|
if (await hasCapability(user, capability)) {
|
|
next();
|
|
return;
|
|
}
|
|
|
|
res.status(403).json({ message: 'Forbidden' });
|
|
} catch (err) {
|
|
logger.error(`[requireCapability] Error checking capability: ${capability}`, err);
|
|
res.status(500).json({ message: 'Internal Server Error' });
|
|
}
|
|
};
|
|
}
|
|
|
|
return { hasCapability, requireCapability, hasConfigCapability };
|
|
}
|