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
This commit is contained in:
61
server/utils/upload-tickets.ts
Normal file
61
server/utils/upload-tickets.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user