Files
iistwin/server/utils/upload-tickets.ts
Ильяс Султанов affbdeee1e Upload-тикеты: загрузка файлов через REST без знания API-ключа
- MCP create_upload_ticket → короткоживущий uploadUrl (10 мин, in-memory)
- POST /api/upload/ticket/:token — тот же пайплайн (processUploadRequest вынесен из /api/upload), авторизация тикетом, проверки скоупов
- get_api_guide: флоу без ключа — ticket → curl → привязка по fileUrl
2026-07-23 15:53:17 +03:00

62 lines
2.5 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
}