mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-06-26 09:21:33 +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)
185 lines
5.2 KiB
TypeScript
185 lines
5.2 KiB
TypeScript
import { logger } from '@librechat/data-schemas';
|
|
import type { IUser, OIDCTokens } from '@librechat/data-schemas';
|
|
|
|
export interface OpenIDTokenInfo {
|
|
accessToken?: string;
|
|
idToken?: string;
|
|
expiresAt?: number;
|
|
userId?: string;
|
|
userEmail?: string;
|
|
userName?: string;
|
|
claims?: Record<string, unknown>;
|
|
}
|
|
|
|
function isFederatedTokens(obj: unknown): obj is OIDCTokens {
|
|
if (!obj || typeof obj !== 'object') {
|
|
return false;
|
|
}
|
|
return 'access_token' in obj || 'id_token' in obj || 'expires_at' in obj;
|
|
}
|
|
|
|
const OPENID_TOKEN_FIELDS = [
|
|
'ACCESS_TOKEN',
|
|
'ID_TOKEN',
|
|
'USER_ID',
|
|
'USER_EMAIL',
|
|
'USER_NAME',
|
|
'EXPIRES_AT',
|
|
] as const;
|
|
|
|
/**
|
|
* Placeholder for Microsoft Graph API access token.
|
|
* This placeholder is resolved asynchronously via OBO (On-Behalf-Of) flow
|
|
* and requires special handling outside the synchronous processMCPEnv pipeline.
|
|
*/
|
|
export const GRAPH_TOKEN_PLACEHOLDER = '{{LIBRECHAT_GRAPH_ACCESS_TOKEN}}';
|
|
|
|
/**
|
|
* Default Microsoft Graph API scopes for OBO token exchange.
|
|
* Can be overridden via GRAPH_API_SCOPES environment variable.
|
|
*/
|
|
export const DEFAULT_GRAPH_SCOPES = 'https://graph.microsoft.com/.default';
|
|
|
|
export function extractOpenIDTokenInfo(
|
|
user: Partial<IUser> | null | undefined,
|
|
): OpenIDTokenInfo | null {
|
|
if (!user) {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
if (user.provider !== 'openid' && !user.openidId) {
|
|
return null;
|
|
}
|
|
|
|
const tokenInfo: OpenIDTokenInfo = {};
|
|
|
|
const federated = user.federatedTokens;
|
|
const openid = user.openidTokens;
|
|
|
|
if (federated && isFederatedTokens(federated)) {
|
|
logger.debug('[extractOpenIDTokenInfo] Found federatedTokens:', {
|
|
has_access_token: !!federated.access_token,
|
|
has_id_token: !!federated.id_token,
|
|
has_refresh_token: !!federated.refresh_token,
|
|
expires_at: federated.expires_at,
|
|
});
|
|
tokenInfo.accessToken = federated.access_token;
|
|
tokenInfo.idToken = federated.id_token;
|
|
tokenInfo.expiresAt = federated.expires_at;
|
|
} else if (openid && isFederatedTokens(openid)) {
|
|
logger.debug('[extractOpenIDTokenInfo] Found openidTokens');
|
|
tokenInfo.accessToken = openid.access_token;
|
|
tokenInfo.idToken = openid.id_token;
|
|
tokenInfo.expiresAt = openid.expires_at;
|
|
}
|
|
|
|
tokenInfo.userId = user.openidId || user.id;
|
|
tokenInfo.userEmail = user.email;
|
|
tokenInfo.userName = user.name || user.username;
|
|
|
|
if (tokenInfo.idToken) {
|
|
try {
|
|
const payload = JSON.parse(
|
|
Buffer.from(tokenInfo.idToken.split('.')[1], 'base64').toString(),
|
|
);
|
|
tokenInfo.claims = payload;
|
|
|
|
if (payload.sub) tokenInfo.userId = payload.sub;
|
|
if (payload.email) tokenInfo.userEmail = payload.email;
|
|
if (payload.name) tokenInfo.userName = payload.name;
|
|
if (payload.exp) tokenInfo.expiresAt = payload.exp;
|
|
} catch (jwtError) {
|
|
logger.warn('Could not parse ID token claims:', jwtError);
|
|
}
|
|
}
|
|
|
|
return tokenInfo;
|
|
} catch (error) {
|
|
logger.error('Error extracting OpenID token info:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export function isOpenIDTokenValid(tokenInfo: OpenIDTokenInfo | null): boolean {
|
|
if (!tokenInfo || !tokenInfo.accessToken) {
|
|
return false;
|
|
}
|
|
|
|
if (tokenInfo.expiresAt) {
|
|
const now = Math.floor(Date.now() / 1000);
|
|
if (now >= tokenInfo.expiresAt) {
|
|
logger.warn('OpenID token has expired');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
export function processOpenIDPlaceholders(
|
|
value: string,
|
|
tokenInfo: OpenIDTokenInfo | null,
|
|
): string {
|
|
if (!tokenInfo || typeof value !== 'string') {
|
|
return value;
|
|
}
|
|
|
|
let processedValue = value;
|
|
|
|
for (const field of OPENID_TOKEN_FIELDS) {
|
|
const placeholder = `{{LIBRECHAT_OPENID_${field}}}`;
|
|
if (!processedValue.includes(placeholder)) {
|
|
continue;
|
|
}
|
|
|
|
let replacementValue = '';
|
|
|
|
switch (field) {
|
|
case 'ACCESS_TOKEN':
|
|
replacementValue = tokenInfo.accessToken || '';
|
|
break;
|
|
case 'ID_TOKEN':
|
|
replacementValue = tokenInfo.idToken || '';
|
|
break;
|
|
case 'USER_ID':
|
|
replacementValue = tokenInfo.userId || '';
|
|
break;
|
|
case 'USER_EMAIL':
|
|
replacementValue = tokenInfo.userEmail || '';
|
|
break;
|
|
case 'USER_NAME':
|
|
replacementValue = tokenInfo.userName || '';
|
|
break;
|
|
case 'EXPIRES_AT':
|
|
replacementValue = tokenInfo.expiresAt ? String(tokenInfo.expiresAt) : '';
|
|
break;
|
|
}
|
|
|
|
processedValue = processedValue.replace(new RegExp(placeholder, 'g'), replacementValue);
|
|
}
|
|
|
|
const genericPlaceholder = '{{LIBRECHAT_OPENID_TOKEN}}';
|
|
if (processedValue.includes(genericPlaceholder)) {
|
|
const replacementValue = tokenInfo.accessToken || '';
|
|
processedValue = processedValue.replace(new RegExp(genericPlaceholder, 'g'), replacementValue);
|
|
}
|
|
|
|
return processedValue;
|
|
}
|
|
|
|
export function createBearerAuthHeader(tokenInfo: OpenIDTokenInfo | null): string {
|
|
if (!tokenInfo || !tokenInfo.accessToken) {
|
|
return '';
|
|
}
|
|
|
|
return `Bearer ${tokenInfo.accessToken}`;
|
|
}
|
|
|
|
export function isOpenIDAvailable(): boolean {
|
|
const openidClientId = process.env.OPENID_CLIENT_ID;
|
|
const openidClientSecret = process.env.OPENID_CLIENT_SECRET;
|
|
const openidIssuer = process.env.OPENID_ISSUER;
|
|
|
|
return !!(openidClientId && openidClientSecret && openidIssuer);
|
|
}
|