- MCP create_upload_ticket → короткоживущий uploadUrl (10 мин, in-memory) - POST /api/upload/ticket/:token — тот же пайплайн (processUploadRequest вынесен из /api/upload), авторизация тикетом, проверки скоупов - get_api_guide: флоу без ключа — ticket → curl → привязка по fileUrl
62 lines
2.5 KiB
TypeScript
62 lines
2.5 KiB
TypeScript
import crypto from 'crypto';
|
||
import type { ApiKeyScopes } from '@shared/schema';
|
||
|
||
// Короткоживущие тикеты загрузки файлов.
|
||
// Сценарий: ИИ-агент подключён по MCP, но значение API-ключа зашито в конфиге
|
||
// его MCP-клиента и агенту недоступно — выполнить curl на POST /api/upload
|
||
// с заголовком X-Api-Key он не может. MCP-инструмент create_upload_ticket
|
||
// выдаёт тикет, а REST POST /api/upload/ticket/:token принимает файл БЕЗ ключа.
|
||
|
||
// TTL тикета: 10 минут
|
||
const TICKET_TTL_MS = 10 * 60 * 1000;
|
||
|
||
export interface UploadTicket {
|
||
token: string;
|
||
organizationId: number;
|
||
apiKeyId: number | null;
|
||
botId: number | null;
|
||
createdBy: number;
|
||
scopes: ApiKeyScopes;
|
||
expiresAt: Date;
|
||
}
|
||
|
||
// Хранилище в памяти процесса: тикеты короткоживущие, персистентность не нужна
|
||
const tickets = new Map<string, UploadTicket>();
|
||
|
||
// Ленивая очистка просроченных тикетов (вызывается при create/consume)
|
||
function cleanupExpired(): void {
|
||
const now = Date.now();
|
||
for (const [token, ticket] of tickets) {
|
||
if (ticket.expiresAt.getTime() <= now) tickets.delete(token);
|
||
}
|
||
}
|
||
|
||
// Создаёт тикет загрузки. Тикет многоразовый в пределах TTL (несколько файлов)
|
||
// и жёстко привязан к организации ключа.
|
||
export function createUploadTicket(data: {
|
||
organizationId: number;
|
||
apiKeyId: number | null;
|
||
botId: number | null;
|
||
createdBy: number;
|
||
scopes: ApiKeyScopes;
|
||
}): { token: string; expiresAt: Date } {
|
||
cleanupExpired();
|
||
const token = crypto.randomBytes(24).toString('base64url');
|
||
const expiresAt = new Date(Date.now() + TICKET_TTL_MS);
|
||
tickets.set(token, { token, expiresAt, ...data });
|
||
return { token, expiresAt };
|
||
}
|
||
|
||
// Возвращает данные тикета или null (неизвестный/просроченный).
|
||
// Тикет НЕ удаляется при использовании — он валиден до конца TTL.
|
||
export function consumeUploadTicket(token: string): UploadTicket | null {
|
||
cleanupExpired();
|
||
const ticket = tickets.get(token);
|
||
if (!ticket) return null;
|
||
if (ticket.expiresAt.getTime() <= Date.now()) {
|
||
tickets.delete(token);
|
||
return null;
|
||
}
|
||
return ticket;
|
||
}
|