LibreChat/packages/api/src/apiKeys/service.ts
Danny Avila 6bc75d24c8
️ refactor: Migrate @librechat/api build to tsdown (#13595)
* ️ refactor: Migrate @librechat/api build to tsdown

Replace Rollup with tsdown (rolldown + oxc isolated-declarations) for the
@librechat/api package build, mirroring the merged data-schemas migration.

- Add tsdown.config.mjs (cjs output, oxc dts, externalize all bare deps,
  bundle first-party `~/` + relative imports)
- Annotate exports for isolatedDeclarations (codefix-driven). Collapse the
  tokens.ts model->token maps to Record<string, Record<string, number>> and
  switch validation.ts's runtime `files` field from z.any() to z.unknown()
  so no explicit `any` is introduced
- Repoint package.json main/types/exports to tsdown's .cjs/.d.cts output
- Add src/telemetry.ts entry shim so the two index.ts entries don't collide
  in oxc's flat dts output (stable dist/telemetry.{cjs,d.cts})
- Delete rollup.config.js

Build time ~36s -> ~0.5s. No runtime behavior change: 5712 unit tests pass,
both entries load via require(), legacy /api consumes them unchanged.

* 👷 ci: Hash packages/api/tsdown.config.mjs in build-api cache keys

The build-api cache keys hashed `packages/api/server-rollup.config.js`,
which never existed (api used `rollup.config.js`, now removed) — a copy-paste
artifact from the data-provider key that matched no file. Replace it with the
new `packages/api/tsdown.config.mjs` so edits to the build config (entry,
format, externals) bust the api build cache, matching the data-schemas key.
2026-06-08 10:54:48 -04:00

157 lines
4.4 KiB
TypeScript

import { createMethods } from '@librechat/data-schemas';
import { ResourceType, PermissionBits, hasPermissions } from 'librechat-data-provider';
import type {
AgentApiKeyListItem,
AgentApiKeyCreateResult,
AllMethods,
IUser,
} from '@librechat/data-schemas';
import type { Types } from 'mongoose';
export interface ApiKeyServiceDependencies {
validateAgentApiKey: AllMethods['validateAgentApiKey'];
createAgentApiKey: AllMethods['createAgentApiKey'];
listAgentApiKeys: AllMethods['listAgentApiKeys'];
deleteAgentApiKey: AllMethods['deleteAgentApiKey'];
getAgentApiKeyById: AllMethods['getAgentApiKeyById'];
findUser: (query: { _id: string | Types.ObjectId }) => Promise<IUser | null>;
}
export interface RemoteAgentAccessResult {
hasAccess: boolean;
permissions: number;
agent: { _id: Types.ObjectId; [key: string]: unknown } | null;
}
export class AgentApiKeyService {
private deps: ApiKeyServiceDependencies;
constructor(deps: ApiKeyServiceDependencies) {
this.deps = deps;
}
async validateApiKey(apiKey: string): Promise<{
userId: Types.ObjectId;
keyId: Types.ObjectId;
} | null> {
return this.deps.validateAgentApiKey(apiKey);
}
async createApiKey(params: {
userId: string | Types.ObjectId;
name: string;
expiresAt?: Date | null;
}): Promise<AgentApiKeyCreateResult> {
return this.deps.createAgentApiKey(params);
}
async listApiKeys(userId: string | Types.ObjectId): Promise<AgentApiKeyListItem[]> {
return this.deps.listAgentApiKeys(userId);
}
async deleteApiKey(
keyId: string | Types.ObjectId,
userId: string | Types.ObjectId,
): Promise<boolean> {
return this.deps.deleteAgentApiKey(keyId, userId);
}
async getApiKeyById(
keyId: string | Types.ObjectId,
userId: string | Types.ObjectId,
): Promise<AgentApiKeyListItem | null> {
return this.deps.getAgentApiKeyById(keyId, userId);
}
async getUserFromApiKey(apiKey: string): Promise<IUser | null> {
const keyValidation = await this.validateApiKey(apiKey);
if (!keyValidation) {
return null;
}
return this.deps.findUser({ _id: keyValidation.userId });
}
}
export function createApiKeyServiceDependencies(
mongoose: typeof import('mongoose'),
): ApiKeyServiceDependencies {
const methods = createMethods(mongoose);
return {
validateAgentApiKey: methods.validateAgentApiKey,
createAgentApiKey: methods.createAgentApiKey,
listAgentApiKeys: methods.listAgentApiKeys,
deleteAgentApiKey: methods.deleteAgentApiKey,
getAgentApiKeyById: methods.getAgentApiKeyById,
findUser: methods.findUser,
};
}
export interface GetRemoteAgentPermissionsDeps {
getEffectivePermissions: (params: {
userId: string;
role?: string;
resourceType: ResourceType;
resourceId: string | Types.ObjectId;
}) => Promise<number>;
}
/** AGENT owners automatically have full REMOTE_AGENT permissions */
export async function getRemoteAgentPermissions(
deps: GetRemoteAgentPermissionsDeps,
userId: string,
role: string | undefined,
resourceId: string | Types.ObjectId,
): Promise<number> {
const agentPerms = await deps.getEffectivePermissions({
userId,
role,
resourceType: ResourceType.AGENT,
resourceId,
});
if (hasPermissions(agentPerms, PermissionBits.SHARE)) {
return PermissionBits.VIEW | PermissionBits.EDIT | PermissionBits.DELETE | PermissionBits.SHARE;
}
return deps.getEffectivePermissions({
userId,
role,
resourceType: ResourceType.REMOTE_AGENT,
resourceId,
});
}
export async function checkRemoteAgentAccess(params: {
userId: string;
role?: string;
agentId: string;
getAgent: (query: {
id: string;
}) => Promise<{ _id: Types.ObjectId; [key: string]: unknown } | null>;
getEffectivePermissions: (params: {
userId: string;
role?: string;
resourceType: ResourceType;
resourceId: string | Types.ObjectId;
}) => Promise<number>;
}): Promise<RemoteAgentAccessResult> {
const { userId, role, agentId, getAgent, getEffectivePermissions } = params;
const agent = await getAgent({ id: agentId });
if (!agent) {
return { hasAccess: false, permissions: 0, agent: null };
}
const permissions = await getRemoteAgentPermissions(
{ getEffectivePermissions },
userId,
role,
agent._id,
);
const hasAccess = hasPermissions(permissions, PermissionBits.VIEW);
return { hasAccess, permissions, agent };
}