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(); // Ленивая очистка просроченных тикетов (вызывается при 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; }