🔬 ci: Add TypeScript Type Checks to Backend Workflow and Fix All Type Errors (#12451)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run

* 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)
This commit is contained in:
Danny Avila 2026-03-28 21:06:39 -04:00 committed by GitHub
parent d5c7d9f525
commit fda1bfc3cc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 406 additions and 233 deletions

View file

@ -7,7 +7,7 @@ import type {
DeleteResult,
Model,
} from 'mongoose';
import type { IAclEntry } from '~/types';
import type { AclEntry, IAclEntry } from '~/types';
import { tenantSafeBulkWrite } from '~/utils/tenantBulkWrite';
export function createAclEntryMethods(mongoose: typeof import('mongoose')) {
@ -375,11 +375,11 @@ export function createAclEntryMethods(mongoose: typeof import('mongoose')) {
* @param options - Optional query options (e.g., { session })
*/
async function bulkWriteAclEntries(
ops: AnyBulkWriteOperation<IAclEntry>[],
ops: AnyBulkWriteOperation<AclEntry>[],
options?: { session?: ClientSession },
) {
const AclEntry = mongoose.models.AclEntry as Model<IAclEntry>;
return tenantSafeBulkWrite(AclEntry, ops, options || {});
return tenantSafeBulkWrite(AclEntry, ops as AnyBulkWriteOperation[], options || {});
}
/**

View file

@ -70,7 +70,7 @@ describe('upsertConfig', () => {
PrincipalType.ROLE,
'admin',
PrincipalModel.ROLE,
{ a: 1 },
{ interface: { endpointsMenu: true } },
10,
);
@ -78,7 +78,7 @@ describe('upsertConfig', () => {
PrincipalType.ROLE,
'admin',
PrincipalModel.ROLE,
{ a: 2 },
{ interface: { endpointsMenu: false } },
10,
);
@ -88,7 +88,7 @@ describe('upsertConfig', () => {
it('normalizes ObjectId principalId to string', async () => {
const oid = new Types.ObjectId();
await methods.upsertConfig(PrincipalType.USER, oid, PrincipalModel.USER, { test: true }, 100);
await methods.upsertConfig(PrincipalType.USER, oid, PrincipalModel.USER, { cache: true }, 100);
const found = await methods.findConfigByPrincipal(PrincipalType.USER, oid.toString());
expect(found).toBeTruthy();
@ -98,7 +98,13 @@ describe('upsertConfig', () => {
describe('findConfigByPrincipal', () => {
it('finds an active config', async () => {
await methods.upsertConfig(PrincipalType.ROLE, 'admin', PrincipalModel.ROLE, { x: 1 }, 10);
await methods.upsertConfig(
PrincipalType.ROLE,
'admin',
PrincipalModel.ROLE,
{ cache: true },
10,
);
const result = await methods.findConfigByPrincipal(PrincipalType.ROLE, 'admin');
expect(result).toBeTruthy();
@ -111,7 +117,13 @@ describe('findConfigByPrincipal', () => {
});
it('does not find inactive configs', async () => {
await methods.upsertConfig(PrincipalType.ROLE, 'admin', PrincipalModel.ROLE, { x: 1 }, 10);
await methods.upsertConfig(
PrincipalType.ROLE,
'admin',
PrincipalModel.ROLE,
{ cache: true },
10,
);
await methods.toggleConfigActive(PrincipalType.ROLE, 'admin', false);
const result = await methods.findConfigByPrincipal(PrincipalType.ROLE, 'admin');
@ -155,7 +167,13 @@ describe('listAllConfigs', () => {
describe('getApplicableConfigs', () => {
it('always includes the __base__ config', async () => {
await methods.upsertConfig(PrincipalType.ROLE, '__base__', PrincipalModel.ROLE, { a: 1 }, 0);
await methods.upsertConfig(
PrincipalType.ROLE,
'__base__',
PrincipalModel.ROLE,
{ cache: true },
0,
);
const configs = await methods.getApplicableConfigs([]);
expect(configs).toHaveLength(1);
@ -163,9 +181,27 @@ describe('getApplicableConfigs', () => {
});
it('returns base + matching principals', async () => {
await methods.upsertConfig(PrincipalType.ROLE, '__base__', PrincipalModel.ROLE, { a: 1 }, 0);
await methods.upsertConfig(PrincipalType.ROLE, 'admin', PrincipalModel.ROLE, { b: 2 }, 10);
await methods.upsertConfig(PrincipalType.ROLE, 'user', PrincipalModel.ROLE, { c: 3 }, 10);
await methods.upsertConfig(
PrincipalType.ROLE,
'__base__',
PrincipalModel.ROLE,
{ cache: true },
0,
);
await methods.upsertConfig(
PrincipalType.ROLE,
'admin',
PrincipalModel.ROLE,
{ version: '2' },
10,
);
await methods.upsertConfig(
PrincipalType.ROLE,
'user',
PrincipalModel.ROLE,
{ version: '3' },
10,
);
const configs = await methods.getApplicableConfigs([
{ principalType: PrincipalType.ROLE, principalId: 'admin' },

View file

@ -285,12 +285,12 @@ describe('updateAccessPermissions', () => {
const updatedRole = await getRoleByName(SystemRoles.USER);
// SHARED_GLOBAL=true → SHARE=true (inherited)
expect(updatedRole.permissions[PermissionTypes.PROMPTS].SHARE).toBe(true);
expect(updatedRole.permissions[PermissionTypes.PROMPTS]!.SHARE).toBe(true);
// SHARED_GLOBAL=false → SHARE=false (inherited)
expect(updatedRole.permissions[PermissionTypes.AGENTS].SHARE).toBe(false);
expect(updatedRole.permissions[PermissionTypes.AGENTS]!.SHARE).toBe(false);
// SHARED_GLOBAL cleaned up
expect(updatedRole.permissions[PermissionTypes.PROMPTS].SHARED_GLOBAL).toBeUndefined();
expect(updatedRole.permissions[PermissionTypes.AGENTS].SHARED_GLOBAL).toBeUndefined();
expect(updatedRole.permissions[PermissionTypes.PROMPTS]).not.toHaveProperty('SHARED_GLOBAL');
expect(updatedRole.permissions[PermissionTypes.AGENTS]).not.toHaveProperty('SHARED_GLOBAL');
});
it('should respect explicit SHARE in update payload and not override it with SHARED_GLOBAL', async () => {
@ -309,8 +309,8 @@ describe('updateAccessPermissions', () => {
const updatedRole = await getRoleByName(SystemRoles.USER);
expect(updatedRole.permissions[PermissionTypes.PROMPTS].SHARE).toBe(false);
expect(updatedRole.permissions[PermissionTypes.PROMPTS].SHARED_GLOBAL).toBeUndefined();
expect(updatedRole.permissions[PermissionTypes.PROMPTS]!.SHARE).toBe(false);
expect(updatedRole.permissions[PermissionTypes.PROMPTS]).not.toHaveProperty('SHARED_GLOBAL');
});
it('should migrate SHARED_GLOBAL to SHARE even when the permType is not in the update payload', async () => {
@ -336,13 +336,13 @@ describe('updateAccessPermissions', () => {
const updatedRole = await getRoleByName(SystemRoles.USER);
// SHARE should have been inherited from SHARED_GLOBAL, not silently dropped
expect(updatedRole.permissions[PermissionTypes.PROMPTS].SHARE).toBe(true);
expect(updatedRole.permissions[PermissionTypes.PROMPTS]!.SHARE).toBe(true);
// SHARED_GLOBAL should be removed
expect(updatedRole.permissions[PermissionTypes.PROMPTS].SHARED_GLOBAL).toBeUndefined();
expect(updatedRole.permissions[PermissionTypes.PROMPTS]).not.toHaveProperty('SHARED_GLOBAL');
// Original USE should be untouched
expect(updatedRole.permissions[PermissionTypes.PROMPTS].USE).toBe(true);
expect(updatedRole.permissions[PermissionTypes.PROMPTS]!.USE).toBe(true);
// The actual update should have applied
expect(updatedRole.permissions[PermissionTypes.MULTI_CONVO].USE).toBe(true);
expect(updatedRole.permissions[PermissionTypes.MULTI_CONVO]!.USE).toBe(true);
});
it('should remove orphaned SHARED_GLOBAL when SHARE already exists and permType is not in update', async () => {
@ -366,9 +366,9 @@ describe('updateAccessPermissions', () => {
const updatedRole = await getRoleByName(SystemRoles.USER);
expect(updatedRole.permissions[PermissionTypes.PROMPTS].SHARED_GLOBAL).toBeUndefined();
expect(updatedRole.permissions[PermissionTypes.PROMPTS].SHARE).toBe(true);
expect(updatedRole.permissions[PermissionTypes.MULTI_CONVO].USE).toBe(true);
expect(updatedRole.permissions[PermissionTypes.PROMPTS]).not.toHaveProperty('SHARED_GLOBAL');
expect(updatedRole.permissions[PermissionTypes.PROMPTS]!.SHARE).toBe(true);
expect(updatedRole.permissions[PermissionTypes.MULTI_CONVO]!.USE).toBe(true);
});
it('should not update MULTI_CONVO permissions when no changes are needed', async () => {

View file

@ -69,7 +69,7 @@ export function createRoleMethods(mongoose: typeof import('mongoose'), deps: Rol
limit?: number;
offset?: number;
}): Promise<Pick<IRole, '_id' | 'name' | 'description'>[]> {
const Role = mongoose.models.Role;
const Role = mongoose.models.Role as Model<IRole>;
const limit = options?.limit ?? 50;
const offset = options?.offset ?? 0;
return await Role.find({})

View file

@ -864,8 +864,8 @@ describe('spendTokens', () => {
const expectedCompletionCost = tokenUsage.completionTokens * premiumCompletionRate;
expect(result).not.toBeNull();
expect(result!.prompt.prompt).toBeCloseTo(-expectedPromptCost, 0);
expect(result!.completion.completion).toBeCloseTo(-expectedCompletionCost, 0);
expect(result!.prompt!.prompt).toBeCloseTo(-expectedPromptCost, 0);
expect(result!.completion!.completion).toBeCloseTo(-expectedCompletionCost, 0);
});
it('should charge standard rates for structured tokens when below threshold', async () => {
@ -907,8 +907,8 @@ describe('spendTokens', () => {
const expectedCompletionCost = tokenUsage.completionTokens * standardCompletionRate;
expect(result).not.toBeNull();
expect(result!.prompt.prompt).toBeCloseTo(-expectedPromptCost, 0);
expect(result!.completion.completion).toBeCloseTo(-expectedCompletionCost, 0);
expect(result!.prompt!.prompt).toBeCloseTo(-expectedPromptCost, 0);
expect(result!.completion!.completion).toBeCloseTo(-expectedCompletionCost, 0);
});
it('should charge standard rates for gemini-3.1-pro-preview when prompt tokens are below threshold', async () => {
@ -937,7 +937,7 @@ describe('spendTokens', () => {
completionTokens * tokenValues['gemini-3.1'].completion;
const balance = await Balance.findOne({ user: userId });
expect(balance.tokenCredits).toBeCloseTo(initialBalance - expectedCost, 0);
expect(balance!.tokenCredits).toBeCloseTo(initialBalance - expectedCost, 0);
});
it('should charge premium rates for gemini-3.1-pro-preview when prompt tokens exceed threshold', async () => {
@ -966,7 +966,7 @@ describe('spendTokens', () => {
completionTokens * premiumTokenValues['gemini-3.1'].completion;
const balance = await Balance.findOne({ user: userId });
expect(balance.tokenCredits).toBeCloseTo(initialBalance - expectedCost, 0);
expect(balance!.tokenCredits).toBeCloseTo(initialBalance - expectedCost, 0);
});
it('should charge premium rates for gemini-3.1-pro-preview-customtools when prompt tokens exceed threshold', async () => {
@ -995,7 +995,7 @@ describe('spendTokens', () => {
completionTokens * premiumTokenValues['gemini-3.1'].completion;
const balance = await Balance.findOne({ user: userId });
expect(balance.tokenCredits).toBeCloseTo(initialBalance - expectedCost, 0);
expect(balance!.tokenCredits).toBeCloseTo(initialBalance - expectedCost, 0);
});
it('should charge premium rates for structured gemini-3.1 tokens when total input exceeds threshold', async () => {
@ -1032,13 +1032,13 @@ describe('spendTokens', () => {
const expectedPromptCost =
tokenUsage.promptTokens.input * premiumPromptRate +
tokenUsage.promptTokens.write * writeRate +
tokenUsage.promptTokens.read * readRate;
tokenUsage.promptTokens.write * writeRate! +
tokenUsage.promptTokens.read * readRate!;
const expectedCompletionCost = tokenUsage.completionTokens * premiumCompletionRate;
expect(result).not.toBeNull();
expect(result!.prompt.prompt).toBeCloseTo(-expectedPromptCost, 0);
expect(result!.completion.completion).toBeCloseTo(-expectedCompletionCost, 0);
expect(result!.prompt!.prompt).toBeCloseTo(-expectedPromptCost, 0);
expect(result!.completion!.completion).toBeCloseTo(-expectedCompletionCost, 0);
});
it('should not apply premium pricing to non-premium models regardless of prompt size', async () => {

View file

@ -387,7 +387,7 @@ export function createTxMethods(_mongoose: typeof import('mongoose'), txDeps: Tx
function getPremiumRate(
valueKey: string,
tokenType: string,
inputTokenCount?: number,
inputTokenCount?: number | null,
): number | null {
if (inputTokenCount == null) {
return null;

View file

@ -18,7 +18,7 @@ describe('User Methods', () => {
describe('generateToken', () => {
const mockUser = {
_id: 'user123',
_id: new mongoose.Types.ObjectId('aaaaaaaaaaaaaaaaaaaaaaaa'),
username: 'testuser',
provider: 'local',
email: 'test@example.com',

View file

@ -35,26 +35,26 @@ export function createUserMethods(mongoose: typeof import('mongoose')) {
searchCriteria: FilterQuery<IUser>,
fieldsToSelect?: string | string[] | null,
): Promise<IUser | null> {
const User = mongoose.models.User;
const User = mongoose.models.User as mongoose.Model<IUser>;
const normalizedCriteria = normalizeEmailInCriteria(searchCriteria);
const query = User.findOne(normalizedCriteria);
if (fieldsToSelect) {
query.select(fieldsToSelect);
}
return (await query.lean()) as IUser | null;
return await query.lean();
}
async function findUsers(
searchCriteria: FilterQuery<IUser>,
fieldsToSelect?: string | string[] | null,
): Promise<IUser[]> {
const User = mongoose.models.User;
const User = mongoose.models.User as mongoose.Model<IUser>;
const normalizedCriteria = normalizeEmailInCriteria(searchCriteria);
const query = User.find(normalizedCriteria);
if (fieldsToSelect) {
query.select(fieldsToSelect);
}
return (await query.lean()) as IUser[];
return await query.lean();
}
/**
@ -301,8 +301,6 @@ export function createUserMethods(mongoose: typeof import('mongoose')) {
.sort((a, b) => b._searchScore - a._searchScore)
.slice(0, limit)
.map((user) => {
// Remove the search score from final results
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { _searchScore, ...userWithoutScore } = user;
return userWithoutScore;
});

View file

@ -496,7 +496,7 @@ describe('userGroup methods', () => {
it('returns the updated user document', async () => {
const user = await createTestUser({ idOnTheSource: 'user-ext-1' });
const { user: updatedUser } = await methods.syncUserEntraGroups(user._id, []);
expect(updatedUser._id.toString()).toBe(user._id.toString());
expect((updatedUser._id as Types.ObjectId).toString()).toBe(user._id.toString());
});
});

View file

@ -265,13 +265,14 @@ export function createUserGroupMethods(mongoose: typeof import('mongoose')) {
role?: string | null;
},
session?: ClientSession,
): Promise<Array<{ principalType: string; principalId?: string | Types.ObjectId }>> {
): Promise<Array<{ principalType: PrincipalType; principalId?: string | Types.ObjectId }>> {
const { userId, role } = params;
/** `userId` must be an `ObjectId` for USER principal since ACL entries store `ObjectId`s */
const userObjectId = typeof userId === 'string' ? new Types.ObjectId(userId) : userId;
const principals: Array<{ principalType: string; principalId?: string | Types.ObjectId }> = [
{ principalType: PrincipalType.USER, principalId: userObjectId },
];
const principals: Array<{
principalType: PrincipalType;
principalId?: string | Types.ObjectId;
}> = [{ principalType: PrincipalType.USER, principalId: userObjectId }];
// If role is not provided, query user to get it
let userRole = role;

View file

@ -18,7 +18,7 @@ export async function dropSupersededPromptGroupIndexes(
let collection;
try {
collection = connection.db.collection(collectionName);
collection = connection.db!.collection(collectionName);
} catch {
result.skipped.push(
...SUPERSEDED_PROMPT_GROUP_INDEXES.map(

View file

@ -1,4 +1,4 @@
import mongoose, { Schema } from 'mongoose';
import mongoose from 'mongoose';
import { MongoMemoryServer } from 'mongodb-memory-server';
import { dropSupersededTenantIndexes, SUPERSEDED_INDEXES } from './tenantIndexes';
@ -24,7 +24,7 @@ afterAll(async () => {
describe('dropSupersededTenantIndexes', () => {
describe('with pre-existing single-field unique indexes (simulates upgrade)', () => {
beforeAll(async () => {
const db = mongoose.connection.db;
const db = mongoose.connection.db!;
await db.createCollection('users');
const users = db.collection('users');
@ -133,7 +133,7 @@ describe('dropSupersededTenantIndexes', () => {
});
it('old unique indexes are actually gone from users collection', async () => {
const indexes = await mongoose.connection.db.collection('users').indexes();
const indexes = await mongoose.connection.db!.collection('users').indexes();
const indexNames = indexes.map((idx) => idx.name);
expect(indexNames).not.toContain('email_1');
@ -143,14 +143,14 @@ describe('dropSupersededTenantIndexes', () => {
});
it('old unique indexes are actually gone from roles collection', async () => {
const indexes = await mongoose.connection.db.collection('roles').indexes();
const indexes = await mongoose.connection.db!.collection('roles').indexes();
const indexNames = indexes.map((idx) => idx.name);
expect(indexNames).not.toContain('name_1');
});
it('old compound unique indexes are gone from conversations collection', async () => {
const indexes = await mongoose.connection.db.collection('conversations').indexes();
const indexes = await mongoose.connection.db!.collection('conversations').indexes();
const indexNames = indexes.map((idx) => idx.name);
expect(indexNames).not.toContain('conversationId_1_user_1');
@ -159,7 +159,7 @@ describe('dropSupersededTenantIndexes', () => {
describe('multi-tenant writes after migration', () => {
beforeAll(async () => {
const db = mongoose.connection.db;
const db = mongoose.connection.db!;
const users = db.collection('users');
await users.createIndex(
@ -169,7 +169,7 @@ describe('dropSupersededTenantIndexes', () => {
});
it('allows same email in different tenants after old index is dropped', async () => {
const users = mongoose.connection.db.collection('users');
const users = mongoose.connection.db!.collection('users');
await users.insertOne({
email: 'shared@example.com',
@ -196,7 +196,7 @@ describe('dropSupersededTenantIndexes', () => {
});
it('still rejects duplicate email within same tenant', async () => {
const users = mongoose.connection.db.collection('users');
const users = mongoose.connection.db!.collection('users');
await users.insertOne({
email: 'unique-within@example.com',
@ -247,7 +247,7 @@ describe('dropSupersededTenantIndexes', () => {
partialConnection = mongoose.createConnection(partialServer.getUri());
await partialConnection.asPromise();
const db = partialConnection.db;
const db = partialConnection.db!;
await db.createCollection('users');
await db.collection('users').createIndex({ email: 1 }, { unique: true, name: 'email_1' });
});

View file

@ -55,7 +55,7 @@ export async function dropSupersededTenantIndexes(
const result: MigrationResult = { dropped: [], skipped: [], errors: [] };
for (const [collectionName, indexNames] of Object.entries(SUPERSEDED_INDEXES)) {
const collection = connection.db.collection(collectionName);
const collection = connection.db!.collection(collectionName);
let existingIndexes: Array<{ name?: string }>;
try {

View file

@ -1,10 +1,4 @@
import type {
PrincipalType,
PrincipalModel,
TCustomConfig,
z,
configSchema,
} from 'librechat-data-provider';
import type { PrincipalType, PrincipalModel, TCustomConfig } from 'librechat-data-provider';
import type { SystemCapabilities } from '~/admin/capabilities';
/* ── Capability types ───────────────────────────────────────────────── */
@ -16,7 +10,7 @@ export type BaseSystemCapability = (typeof SystemCapabilities)[keyof typeof Syst
export type ConfigAssignTarget = 'user' | 'group' | 'role';
/** Top-level keys of the configSchema from librechat.yaml. */
export type ConfigSection = keyof z.infer<typeof configSchema>;
export type ConfigSection = string & keyof TCustomConfig;
/** Section-level config capabilities derived from configSchema keys. */
type ConfigSectionCapability = `manage:configs:${ConfigSection}` | `read:configs:${ConfigSection}`;

View file

@ -2,6 +2,7 @@ import type { Document, Types } from 'mongoose';
import { CursorPaginationParams } from '~/common';
export interface IUser extends Document {
_id: Types.ObjectId;
name?: string;
username?: string;
email: string;
@ -50,6 +51,15 @@ export interface IUser extends Document {
/** Field for external source identification (for consistency with TPrincipal schema) */
idOnTheSource?: string;
tenantId?: string;
federatedTokens?: OIDCTokens;
openidTokens?: OIDCTokens;
}
export interface OIDCTokens {
access_token?: string;
id_token?: string;
refresh_token?: string;
expires_at?: number;
}
export interface BalanceConfig {

View file

@ -0,0 +1,34 @@
import type TransportStream from 'winston-transport';
/**
* Module augmentation for winston's transports namespace.
*
* `winston-daily-rotate-file` ships its own augmentation targeting
* `'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). This local
* declaration bridges the gap so `tsc --noEmit` passes.
*/
declare module 'winston/lib/winston/transports' {
interface Transports {
DailyRotateFile: new (
opts?: {
level?: string;
filename?: string;
datePattern?: string;
zippedArchive?: boolean;
maxSize?: string | number;
maxFiles?: string | number;
dirname?: string;
stream?: NodeJS.WritableStream;
frequency?: string;
utc?: boolean;
extension?: string;
createSymlink?: boolean;
symlinkName?: string;
auditFile?: string;
format?: import('logform').Format;
} & TransportStream.TransportStreamOptions,
) => TransportStream;
}
}