Полный реестр всех HTTP-эндпоинтов проекта (src/app/api/**/route.ts). Документ составлен построчным чтением
всех 90 файлов route.ts, найденных в репозитории на дату составления (2026-09-01), плюс src/middleware.ts
и модулей авторизации src/lib/session.ts, src/lib/telegram-auth.ts.
Ничего не выдумано: если в коде нет проверки авторизации — так и написано ("Auth: нет"). Если поведение неочевидно из кода — помечено как "требует уточнения у владельца".
В проекте используется несколько независимых механизмов авторизации — единого middleware-гейта для /api/* нет.
middleware.ts оборачивает только next-intl (роутинг локалей) и подчистку Location-заголовка при редиректах.
config.matcher явно исключает /api:
export const config = {
matcher: [
"/(ru|en|kz)/:path*",
"/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt|manifest.json|.*\\.(?:svg|png|jpg|jpeg|gif|webp|ico|css|js)).*)",
],
};Следствие: каждый route.ts обязан сам проверять авторизацию. Ни один запрос к /api/** не проходит через
middleware вообще (ни для локали, ни для чего-либо ещё).
- Кука
perricheno_session— JWT (HS256), подписан ключомSESSION_SECRET(env, обязателен — при отсутствии процесс падает на старте). - JWT содержит
{ sessionId, userId };verifySession()дополнительно проверяет, чтоsessionIdсуществует в таблице сессий в БД (отзыв сессий работает через удаление записи в БД, не только через куку). Возвращаетnumber | null(id пользователя) — без выброса исключений наружу. - Срок жизни сессии — 10 лет ("immortal"),
httpOnly,secureв проде,sameSite: lax. - Большинство пользовательских эндпоинтов (
agent/*,space/*,citations/*,billing/checkout,billing/stats,r/*,tikz/*,chat/history,auth/me,auth/delete,telegram/send,webhook-proxy,pdf-proxyчастично) вызываютverifySession()и возвращают401при отсутствии/невалидности сессии.
Используется только в POST /api/auth/login. Проверяет HMAC-SHA256 подписи данных, присланных Telegram Login
Widget: секретный ключ — SHA256(TELEGRAM_BOT_TOKEN), строка проверки — отсортированные key=value пары
(auth_date, first_name, id, last_name, photo_url, username), сравнение с полем hash. Дополнительно проверяется
auth_date (не старше 24 часов).
Все эндпоинты src/app/api/internal/bot/** защищены сравнением заголовка запроса с process.env.WEBHOOK_SECRET:
const secret = req.headers.get("x-bot-secret"); // или "X-Bot-Secret" — HTTP-заголовки регистронезависимы
if (secret !== WEBHOOK_SECRET) return NextResponse.json({ error: "Unauthorized" }, { status: 403 /* или 401 */ });Это единственная защита этих роутов — предполагается, что вызывать их может только контейнер telegram-bot
внутри приватной docker-сети. WEBHOOK_DOMAIN/публичного доступа к ним быть не должно, но код сам по себе
не ограничивает источник запроса по IP/сети — только по секрету.
Тот же секрет WEBHOOK_SECRET используется в POST /api/webhook/telegram (там сравнивается с заголовком
x-telegram-bot-api-secret-token, который Telegram присылает по настройке secret_token вебхука) — это другой
канал сравнения, но тот же env-секрет.
Явно (по коду) не проверяют ни сессию, ни секрет, ни подпись:
POST /api/auth/link— генерация одноразового токена для Telegram deep-link (по дизайну, это pre-auth шаг).POST /api/auth/login— но проверяет отдельную HMAC-подпись Telegram Login Widget (см. 0.3).GET /api/auth/poll— принимает произвольныйtokenв query и создаёт сессию, если бот подтвердил его (риск ограничен только угадыванием UUID-токена).GET /api/billing/receipt/[id]иGET /api/billing/receipt/[id]/verify— публичные страницы чека/протокола верификации, отдают частично маскированные PII (username/telegram_idв замаскированном виде) любому, кто знает/угадаетidчека.POST /api/billing/webhook— авторизация не сессионная, а через проверку HMAC/MD5-подписи CryptoCloud (CRYPTOCLOUD_SECRET), см. соответствующий раздел.GET /api/health— публичный health-check.POST /api/pdf-proxy— конвертация файлов через внешний Stirling PDF API идёт без проверки сессии;verifySession()вызывается только опционально внутри, чтобы решить, слать ли результат в Telegram.
Ниже — вещи, обнаруженные при построчном чтении кода, которые выглядят как потенциальные проблемы или несогласованности, но не являются "придумками" — все взяты дословно из исходников:
src/app/api/tasks/route.tsиsrc/app/api/tasks/parse/route.tsиспользуют отдельный, захардкоженный JWT-секрет вместоverifySession()изsrc/lib/session.ts:Они читают ту же кукуconst JWT_SECRET = new TextEncoder().encode("super-secret-key-change-this-in-env-938210");
perricheno_session, но верифицируют её собственнымjwt.jwtVerifyс этим литералом вместоprocess.env.SESSION_SECRET. ЕслиSESSION_SECRETв окружении не равен буквально этой строке, подпись не совпадёт иverifyAuth()будет всегда возвращатьnull(эндпоинты задач окажутся постоянно "неавторизованными" для реальных пользовательских сессий). Требует уточнения у владельца — либо это мёртвый код, либоSESSION_SECRETнамеренно был выставлен равным этому значению для обратной совместимости.- Захардкоженный API-ключ Stirling PDF встречается в открытом виде в трёх файлах:
src/app/api/pdf-proxy/route.ts,src/app/api/internal/bot/extract-text/route.ts(как дефолт),src/app/api/internal/bot/history/files/route.ts(как дефолтPDF_API_KEY) — везде значение"0a69f4b4-0210-47c0-a2a9-946e3e894c4c". Требует уточнения — ключ явно не секретен в текущем виде (лежит в исходниках, попадающих в git). src/app/api/space/[id]/ws-token/route.tsимеет захардкоженный fallback-секрет для подписи WS JWT:process.env.WS_JWT_SECRET || 'fallback-secret-key-at-least-thirty-two-chars-long'. ЕслиWS_JWT_SECRETне задан в окружении, все WS-токены подписываются публично известной строкой.POST /api/agent/scholar/searchпроверяетverifySession(), но не проверяет, чтоsession.user_id === userIdдля найденнойAgentSession— теоретически можно передать чужойsessionId(UUID) и прочитать/перезаписать чужую сессию литературного поиска. Риск ограничен угадыванием UUID.- Многие AI-эндпоинты (
agent/visualize/edit,agent/visualize/recommend,agent/visualize/generate-code,space/[id]/ai-edit,space/[id]/ai-fix) проверяют толькоverifySession(), но не вызываютcheckAndDeductUsage— то есть не списывают квоту символов/визуалов за вызов OpenAI, в отличие от соседних похожих роутов (agent/visualize(POST, action=generate),agent/generate,r/generate), где списание есть. Возможно, это осознанный дизайн (эти операции дешёвые/вспомогательные), но стоит уточнить у владельца.
Файл: src/app/api/auth/delete/route.ts
Удаляет аккаунт текущего пользователя и завершает сессию.
- Auth: требуется (
verifySession()). - Request: тело не требуется.
- Response:
401 { error: "Unauthorized" }— нет сессии.200 { success: true }— успех (вызываетdeleteUser(userId)иdeleteSession(); ошибкиdeleteUserне обрабатываются — вызов неawait-ится).
- Пример:
DELETE /api/auth/delete(с валидной курой) →{"success":true}.
Файл: src/app/api/auth/link/route.ts
Генерирует одноразовый токен для входа через Telegram deep-link (AuthRequest со статусом pending), заодно
чистит записи старше 10 минут.
- Auth: нет (публичный, pre-auth эндпоинт).
- Request: тело не требуется.
- Response:
200 { token: string, deepLink: "https://t.me/<TELEGRAM_BOT_USERNAME>?start=<token>" }.500 { error: string }— ошибка БД.
- Пример:
POST /api/auth/link→{"token":"5b1e...","deepLink":"https://t.me/PerrichenoBot?start=5b1e..."}.
Файл: src/app/api/auth/login/route.ts
Логин через Telegram Login Widget: проверяет HMAC-подпись, апсертит пользователя, создаёт сессию (кука).
- Auth: нет сессии на входе; вместо этого — проверка
verifyTelegramAuth(data)(см. §0.3). - Request (JSON body — набор полей Telegram Login Widget):
{ id, first_name, last_name?, username?, photo_url?, auth_date, hash }. - Response:
401 { error: "Invalid signature or expired data" }— подпись не совпала илиauth_dateпротух.200 { success: true, user }— успех, кукаperricheno_sessionвыставлена.500 { error, details, stack }— включаетe.stackв ответ (отладочная утечка деталей исключения).
- Пример:
POST /api/auth/login {"id":123,"first_name":"Ivan","auth_date":1690000000,"hash":"..."}→{"success":true,"user":{...}}.
Файл: src/app/api/auth/logout/route.ts
- Auth: не требуется явной проверки — просто удаляет текущую сессию/куку, если она есть (
deleteSession()сама no-op-ит, если куки нет). - Request: без тела.
- Response:
200 { success: true }всегда.
Файл: src/app/api/auth/me/route.ts
Возвращает профиль текущего пользователя + лимиты плана + использование PDF-стейджинга. Попутно "пингует"
checkAndDeductUsage(userId, 'chars', 0) — нулевое списание, нужное только чтобы триггернуть сброс
дневных/недельных счётчиков при смене даты.
- Auth: требуется.
- Request: query/body не требуются.
- Response:
401 { user: null }— нет сессии.404 { user: null }— сессия валидна, но пользователь не найден в БД.403 { user: null, error: "Account suspended" }—user.is_banned === true.200 { user: {...user, isAdmin: boolean}, limits: PLAN_LIMITS, stagingUsed: number, stagingCap: number }.isAdmin— жёстко:user.telegram_id === '1153844209'.
Файл: src/app/api/auth/poll/route.ts
Клиент опрашивает по токену, завершил ли пользователь вход через бота; при завершении создаёт сессию и шлёт Telegram-уведомление о новом входе.
- Auth: нет (сам создаёт сессию по результату).
- Request: query
?token=<uuid>(обязателен). - Response:
400 { error: "Missing token" }.200 { status: "expired" }— токен не найден вAuthRequest.200 { status: "pending" }— ещё не подтверждён ботом.200 { status: "completed", user }— сессия создана (кука выставлена), записьAuthRequestудалена.500 { error }.
Все роуты этого раздела (кроме внутренних деталей ниже) требуют verifySession() → 401 { error: "Auth required" }
/{ error: "Authentication required." }/{ error: "Unauthorized" } при отсутствии сессии (текст сообщения
отличается от файла к файлу, но код всегда 401).
Файл: src/app/api/agent/analytics/generate/route.ts
Ставит в очередь BullMQ (job.name: 'analytics-generate', [Фаза 6a]: раньше был fire-and-forget промис прямо в этом процессе, теперь задачу исполняет отдельный worker) пайплайн аналитики данных (runAnalyticsPipeline, src/lib/jobs/analyticsGenerate.ts): парсит загруженные файлы,
генерирует графики через R/Python-компиляторы, пишет прогресс в agent_sessions.stage_json.
- Auth: требуется. Плюс проверка квоты
checkAndDeductUsage(userId, 'visuals', 0)до старта. - Request (JSON body):
{ prompt: string, runtime?: 'Python'|'R' = 'Python', uploadIds?: string[] = [] }. - Response:
401 { error: "Auth required" }.500 { error: "OpenAI API Key not configured" }— нетOPENAI_API_KEY.402 { error: "LIMIT_REACHED", details: "Visual tokens limit reached." }— квота исчерпана.400 { error: "Prompt is required" }.400 { error: "No data files uploaded. Please upload CSV, Excel, or other data files." }.404 { error: "Uploads not found" }.200 { sessionId: string }— сессия создана, пайплайн запущен в фоне; итог опрашивается черезGET /api/agent/sessions/[id].500 { error: string }— прочие ошибки при старте.
- Пример:
POST { "prompt": "Покажи динамику продаж", "uploadIds": ["u1"] }→{"sessionId":"a1b2..."}.
Файл: src/app/api/agent/attach/route.ts
Универсальная загрузка вложения для чата (PDF / изображение / файл данных CSV-Excel-JSON-TSV-TXT) в
agent_uploads. Большие файлы данных (>1 МБ) уходят в Supabase Storage вместо БД.
- Auth: требуется (
401 { error: "Unauthorized" }). - Request (POST):
multipart/form-dataс полямиfile(Blob, обязателен) и опциональноfilename. Лимиты: PDF ≤ 40 МБ, изображение (png/jpeg/webp) ≤ 10 МБ, данные (csv/xlsx/json/tsv/txt) ≤ 20 МБ (при1 МБ файл кладётся в Storage).
- Response (POST):
400 { error: "Invalid multipart body" }/{ error: "Missing 'file' field" }.- PDF-ветка:
413 { error: "PDF too large (...)" };415 { error: "File does not look like a valid PDF" };502 { error: "Failed to parse PDF", details };413 { error: "TOTAL_CHAR_CAP", message, fileChars, alreadyUsed, stagingCap, remaining, planTier }(превышен план-зависимый лимит стейджинга);422 { error: "EMPTY_PDF", message }; успех —200 { uploadId, kind: "pdf", filename, charCount, imageCount, pageCount, ocrUsed }. - Image-ветка:
413 { error: "Image too large (...)" }; успех —200 { uploadId, kind: "image", filename, charCount: 0, imageCount: 1, pageCount: 0, ocrUsed: false }. - Data-ветка (Storage, >1 МБ):
200 { uploadId, kind: "data_storage", filename, fileSize, storagePath, ... }или500 { error: "Storage upload failed", details }. - Data-ветка (БД, ≤1 МБ):
500 { error: "Failed to read file", details }(например, битый xlsx) или200 { uploadId, kind: "data", filename, charCount, imageCount: 0, pageCount: 1, ocrUsed: false }. - Неподдерживаемый тип:
415 { error: "Unsupported file type. Accepted: PDF, PNG, JPEG, WebP, CSV, Excel, JSON, TSV, TXT." }.
- Request (DELETE): query
?id=<uploadId>(обязателен). - Response (DELETE):
400 { error: "Missing id" }или200 { ok: true }.
Файл: src/app/api/agent/chat/route.ts
Стриминговый (SSE, text/event-stream) чат с GPT (gpt-5-mini-2025-08-07), поддерживает режимы отправки,
повтора и редактирования сообщения, прикрепление PDF/картинок (по uploadId), списывает символы посимвольно
по факту использованных токенов × 3.
- Auth: требуется.
- Request (JSON body):
{ sessionId: string (обязателен), mode?: 'send'|'retry'|'edit' = 'send', editIndex?: number, message?: string, uploadIds?: string[] }. Дляsend/editmessageобязателен (макс. 100 000 символов); дляeditобязателенeditIndex >= 0. - Response:
401 { error: 'Auth required' };500 { error: 'OPENAI_API_KEY not configured' }.400 { error: 'sessionId required' }/'message required'/'editIndex required for edit mode' }.413 { error: 'Message is too large (>100k characters).' }.402 { error: 'LIMIT_REACHED' }— предпроверка квоты символов.404 { error: 'Session not found' }— сессия чужая или не существует.400 { error: 'No user message to retry' }(retry без истории),{ error: 'editIndex out of range' },{ error: 'editIndex must point to a user message' }.- Успех:
200поток SSE-событийdata: {"delta": "..."}(по мере генерации) и финальныйdata: {"done": true, aborted, charsBilled, promptTokens, completionTokens}. Ошибка апстрима — событиеdata: {"error": "..."}.
- Пример:
POST {"sessionId":"s1","message":"Привет"}→ SSE-поток дельт, завершение{"done":true,...}.
Файл: src/app/api/agent/chat/sessions/route.ts
Создание/список чат-сессий (doc_type: 'chat').
- Auth: требуется.
- POST Request:
{ initialMessage?: string }. Response:200 { sessionId }или500 { error }. - GET Response:
200 { sessions: AgentSession[] }(только сdoc_type === 'chat').
Файл: src/app/api/agent/compile-pdf/route.ts
Проксирует ZIP-архив LaTeX-проекта во внешний компилятор (LATEX_COMPILER_URL, заголовок x-api-key: LATEX_COMPILER_KEY) и возвращает готовый PDF.
- Auth: требуется.
- Request:
multipart/form-data, полеfile— ZIP. - Response:
500 { error: "Compiler Config (URL/KEY) missing..." }— не настроены env-переменные.400 { error: "No physical ZIP file payload provided." }.<compilerStatus> { error: "Compiler Error (<status>): ..." }— компилятор вернул ошибку.500 { error: "Compiler returned text/json: ..." }— компилятор ответил не PDF.200— бинарный PDF (Content-Type: application/pdf,Content-Disposition: attachment; filename="compiled_document.pdf").500 { error }— прочее.
Файл: src/app/api/agent/data-analytics/route.ts
Комбинированный action-эндпоинт: action: 'suggest' (AI подсказывает типы графиков) или action: 'generate'
(default) — SSE-стрим последовательной генерации до 8 графиков через R/Python-компиляторы.
- Auth: требуется + предпроверка
checkAndDeductUsage(userId, 'visuals', 0)→402 { error: "LIMIT_REACHED", details: "Visual tokens limit reached." }. - Request:
{ action?: 'suggest'|'generate', prompt: string, contextFiles?: {name,content}[], charts?: string[], runtime?: 'Python'|'R' }. - Response (
suggest):200 { charts: string[], count: number, reasoning: string }(с фоллбэком["bar","scatter","line"]при ошибке парсинга ответа модели) либо500 { error }(в т.ч. hard-stop при "Failed to extract"/"[Failed" в контексте). - Response (
generate):200SSE-поток событийstatus,code_chunk,chart_done({index, chartType, image, code, runtime}),chart_error,all_done,error. - Списание квоты (
chars,visuals) происходит внутри цикла генерации, per-chart.
Файл: src/app/api/agent/generate/route.ts
Главный оркестратор генерации документа (5-стадийный пайплайн runPipeline), фоновая генерация с прогрессом
в stage_json, Telegram-уведомления о старте/завершении/ошибке. Также поддерживает "legacy"-путь редактирования/
исправления ошибок компиляции существующей сессии одним вызовом LLM.
- Auth: требуется. Предпроверка квоты символов (
402 { error: 'LIMIT_REACHED', details: 'Characters limit reached.' }) и лимит одновременных генераций — не более 3 активных сессий на пользователя (429 { error: 'Достигнут лимит одновременных генераций (макс. 3)...' }). - Request (основной путь, JSON):
{ prompt, type?: DocType = 'research', style?='medium', wordCount? (300..30000, default 2000), columns?: 1|2 = 2, useTemplate?=true, templateId?, customTemplatePreamble?, useReferences?=false, language?='en', authorName?, courseName?, dateStr?, groupName?, supervisorName?, taskDescription?, taskFileText?, referenceLinks?: string[], uploadIds?: string[], visualCount?: 0..10, dataUploadIds?: string[], dataRuntime?: 'R'|'Python' }. - Request (legacy-путь — если тело содержит
currentTexилиerrorLog):{ sessionId (обязателен), currentTex?, currentBib?, errorLog?, prompt?, language? }. - Response:
401 { error: 'Authentication required.' };500 { error: 'OpenAI API Key is not configured.' }.400 { error: 'Prompt is required.' }(основной путь) /{ error: 'Legacy path requires currentTex.' }/{ error: 'sessionId required for edit' }(legacy).200 { sessionId }— в обоих путях; фактический результат пишется в фоне, клиент опрашиваетGET /api/agent/sessions/[id].
Файл: src/app/api/agent/ingest-pdf/route.ts
То же, что PDF-ветка agent/attach, но отдельным эндпоинтом только для PDF (используется, например, в R Studio).
- Auth: требуется.
- POST Request:
multipart/form-data { file (обязателен, ≤40 МБ), filename? }. - POST Response:
400/415/413/502/413 TOTAL_CHAR_CAP/422 EMPTY_PDF(аналогичноagent/attach); успех —200 { uploadId, filename, charCount, imageCount, pageCount, ocrUsed, remaining }. - DELETE: query
?id=,400/200 { ok: true }.
Файл: src/app/api/agent/python-compile/route.ts
Прокси кода Python во внутренний сервис-компилятор.
- Auth: требуется.
- Request:
{ code (обязателен), files?: any[] }. - Response:
400 { error: "Python code is required" };500 { error: "Python compiler error" }; успех —200 <результат компилятора as-is>(обычно{ success, image?, log? }).
Файл: src/app/api/agent/r-compile/route.ts
Симметричный прокси для R-кода (таймаут 35 c).
- Auth: требуется. Request/Response — как у
python-compile, но с"R code is required".
Файл: src/app/api/agent/scholar/search/route.ts
Ищет научные статьи через внутренний Go-микросервис (RESEARCH_API_URL, arXiv/OpenAlex), с автопереводом
не-латинских запросов через MyMemory API.
- Auth: требуется (проверяется после чтения
sessionIdиз тела, но до похода в БД).⚠️ Не проверяется, чтоsession.user_id === userId(см. §0.6, п.4). - Request:
{ sessionId (обязателен) }— сам запрос читается изAgentSession.stream_textэтой сессии. - Response:
401 { error: "Unauthorized" };404 { error: "Session not found" };400 { error: "Empty query" };500 { error }(в т.ч. при ошибке Go-сервиса); успех —200 { articles: ScholarArticle[], query, originalQuery }, попутно обновляетAgentSession.status='completed'иvisuals_json.
Файл: src/app/api/agent/scholar/sessions/[id]/route.ts
- Auth: требуется. Удаляет
AgentSessionтолько еслиidиuser_idсовпадают (deleteManyс фильтром). - Response:
404 { error: 'Not found' }(0 удалённых строк) или200 { ok: true }.
Файл: src/app/api/agent/scholar/sessions/route.ts
Создаёт AgentSession с doc_type: 'literature_search', списывает фиксированные 2500 символов за операцию.
- Auth: требуется.
- Request:
{ prompt, settings }. - Response:
402 { error: "Insufficient characters on balance" };500 { error }; успех —200 { sessionId }.
Файл: src/app/api/agent/sessions/[id]/route.ts
Универсальный CRUD над одной AgentSession — базовый способ поллинга прогресса генерации/чата/визуализации.
- Auth: требуется во всех трёх методах.
- GET Response:
404 { error: "Session not found" };403 { error: "Unauthorized" }(чужая сессия); успех —200 { session: { id, status, stream_text, stage_json, error_msg, main_tex, references_bib, visuals_json, settings_json, doc_type, title, updated_at } }. - PUT Request: частичное обновление — принимает только
main_tex,references_bib,settings_json,title,visuals_json(прочие поля игнорируются). Response:404/403как выше;400 { error: "No valid fields to update" }; успех —200 { success: true }. - DELETE Response:
404 { error: "Session not found or unauthorized" }или200 { success: true }.
Файл: src/app/api/agent/sessions/[id]/share/route.ts
Переключает публичную ссылку на сессию (toggleAgentSessionShare — генерирует/убирает share_id).
- Auth: требуется.
- Response:
200 { shared: boolean, share_id: string|null, share_url: "/agent/shared/<id>"|null }.
Файл: src/app/api/agent/sessions/route.ts
- Auth: требуется.
- GET Response:
200 { sessions: AgentSession[] }(все сессии пользователя, без фильтра поdoc_type). - POST Request:
{ title (обязателен), doc_type?='research', settings_json?, main_tex?, references_bib?, visuals_json? }. Response:400 { error: "Title is required" }; успех —201 { session }.
Файл: src/app/api/agent/templates/upload/route.ts
Извлекает LaTeX-преамбулу (\usepackage... до \begin{document}) из загруженного Overleaf-ZIP — без записи в БД,
клиент передаёт результат обратно в agent/generate как customTemplatePreamble.
- Auth: требуется.
- Request:
multipart/form-data { file }, ограничение по размеру — 10 МБ (проверяется поContent-Lengthдо чтения тела). - Response:
413 { error: "ZIP file too large (max 10 MB)." };400 { error: "Invalid multipart body." }/{ error: "No file provided." };422 { error: "Could not read ZIP file..." }/{ error: "No .tex file found in the ZIP." }/{ error: "Could not extract a valid preamble from main.tex." }(преамбула < 20 символов); успех —200 { preamble, mainFile, filesFound: string[], preambleLength }.
Файл: src/app/api/agent/uploads/route.ts
Метаданные загруженных файлов (без text_content) по списку id — для отображения TTL/статуса в staging-трее.
- Auth: требуется.
- Request: query
?ids=id1,id2,...(обязателен). - Response:
400 { error: 'ids parameter required' };200 { uploads: [] }при пустом списке; иначе200 { uploads: [{id, user_id, filename, char_count, image_count, page_count, ocr_used, storage_path, expires_at, created_at}] }(только принадлежащие текущему пользователю — фильтрuser_id: userIdв запросе);500 { error }.
Файл: src/app/api/agent/visualize/edit/route.ts
AI-редактирование существующего R/Python кода визуализации по текстовому запросу пользователя (без компиляции).
- Auth: требуется.
⚠️ Квота не списывается (см. §0.6, п.5). - Request:
{ currentCode, editPrompt, language?: 'R'|'Python' = 'R' }. - Response:
500 { error: "Failed" }(или сообщение исключения) при ошибке AI; успех —200 { code: string }.
Файл: src/app/api/agent/visualize/generate-code/route.ts
Стримит (обычный текстовый поток, не SSE JSON) сырой R/Python-код визуализации по теме без компиляции.
- Auth: требуется.
⚠️ Квота не списывается. - Request:
{ topic, chartType, palette?='viridis', language?='en', dataContext?='', engine?: 'r'|'python' ='r' }. - Response:
500 { error: "OpenAI API Key not configured" };500 { error: "AI error: ..." }; успех —200потокContent-Type: text/plainс сырыми чанками кода.
Файл: src/app/api/agent/visualize/recommend/route.ts
AI подбирает ровно 3 (максимум 4 после slice) типа графиков под тему + контекст данных.
- Auth: требуется.
⚠️ Квота не списывается. - Request:
{ topic, dataContext, availableTypes: string[] }. - Response:
500 { error: "Recommendation failed" }/ прочее сообщение исключения; успех —200 { recommended: string[] }(фоллбэк["bar","scatter","wordcloud"]при сбое парсинга).
Файл: src/app/api/agent/visualize/route.ts
Комбинированный: action: "compile" (компилирует пользовательский код) или action: "generate" (AI генерирует +
сразу компилирует, синхронно, без стрима — в отличие от data-analytics).
- Auth: требуется + предпроверка
checkAndDeductUsage(userId, 'visuals', 0)→402 { error: "LIMIT_REACHED", details: "Visual tokens limit reached." }. - Request:
{ topic, chartType, palette?='viridis', language?='en', dataContext?='', action: 'compile'| 'generate', code? (для compile), previousError?, previousCode? (для retry внутри generate), runtime?: 'R'| 'Python' = 'R' }. - Response:
action="compile":500 { error: "<runtime> Compilation server error" };500 { error: log, code }при неуспехе компиляции; успех —200 { success: true, image, code, chart_type }.action="generate":500 { error: "AI Generation failed: ..." };500 { error: log, code: generatedCode }при неуспехе компиляции; успех —200 { success: true, image, code, chart_type }(здесь квота списывается —charsиvisuals).400 { error: "Invalid action. Use 'generate' or 'compile'." }.
Файл: src/app/api/r/generate/route.ts
Три режима в одном эндпоинте: suggest (подбор типов графиков), multi (фоновая генерация нескольких графиков,
план зависит от тарифа пользователя, с автоповтором при ошибке компиляции — [Фаза 6a] исполняется задачей
BullMQ r-multi-generate в отдельном процессе worker, src/lib/jobs/rMultiGenerate.ts, а не промисом в этом
процессе), одиночная синхронная генерация (по умолчанию, без action).
- Auth: требуется + предпроверка
checkAndDeductUsage(userId, "visuals", 0)→402 { error: "LIMIT_REACHED", details: "Visual limit reached." }. Также500 { error: "OpenAI API key not configured." }. - Request:
{ action?: 'suggest'|'multi'|(generate по умолчанию), prompt, chartType?='', chartTypes?: string[], contextFiles?: {name,content,rFileName?,fileData?,images?}[], previousCode?, previousError? }. - Response:
400 { error: "Invalid JSON" }/{ error: "Prompt is required." }.suggest:500 { error: "Suggestion failed." }или200 { charts: string[], reasoning }(фоллбэк на ошибке парсинга).multi: план графиков ограничен тарифом (free:1, plus:3, pro:10, ultra:15); ответ сразу200 { sessionId, chartsPlanned: string[] }— реальная генерация идёт в фоне, прогресс — черезGET /api/r/sessions/[id].- default (одиночный, синхронный):
500 { error: <текст ошибки AI/компилятора> };422 { error: log, code }— R упал на выполнении; успех —200 { image, code, chartType, sessionId }.
Файл: src/app/api/r/sessions/[id]/route.ts
- Auth: требуется (
401 { error: 'Authentication required.' }). - GET:
404 { error: 'Not found.' }(чужая или несуществующая); успех —200 { session: { id, title, prompt, status, results: RResultItem[], created_at, updated_at } }. - DELETE:
404 { error: 'Not found.' }или200 { success: true }.
Файл: src/app/api/r/sessions/route.ts
- Auth: требуется. Response:
200 { sessions: RSession[] }(только сессии текущего пользователя).
Файл: src/app/api/r/upload/route.ts
Прокси извлечения текста/изображений из файла (PDF/Office/CSV/TXT) через внутренний
/api/internal/bot/extract-text (тот же процесс, вызов по HTTP на INTERNAL_EXTRACT_URL, по умолчанию
http://localhost:3000/api/internal/bot/extract-text), с подстановкой x-bot-secret: WEBHOOK_SECRET.
- Auth: требуется (
401). Плюс500 { error: "Extract service not configured." }, еслиWEBHOOK_SECRETне задан на этом сервисе. - Request:
multipart/form-data { file }, лимит 20 МБ. - Response:
400 { error: "No file provided." }/{ error: "File too large (max 20 MB)." };500 { error: "Extraction failed: ..." }; успех —200 { name, content, images, chars, pages }.
Файл: src/app/api/tikz/generate/route.ts
Генерация TikZ-диаграммы по одному из ~28 заготовленных "blueprint"-типов (mind_map, flowchart, er_diagram,
swot, gantt и т.д.), с автоповтором до 3 попыток компиляции (ошибки LaTeX скармливаются обратно модели).
- Auth: требуется. Предпроверка
checkAndDeductUsage(userId, "visuals", 0)→402 { error: "LIMIT_REACHED", details: "Visual limit reached." }. - Request:
{ prompt (обязателен), visualType?='concept_map', language?='en' }. - Response:
400 { error: "Invalid JSON" }/{ error: "Prompt is required." }; успех —200 { tikzCode, pdfBase64, type }(квота списывается по факту потраченных токенов); при исчерпании 3 попыток —422 { error: "TikZ compilation failed after 3 attempts.", log, tikzCode }.
Фича удалена целиком по прямому запросу владельца (src/app/[locale]/canvas/page.tsx, src/app/api/canvas/generate/route.ts, src/lib/json-canvas.ts — все удалены; пункт меню и i18n-строки тоже убраны). До удаления не имела зависимостей от БД/Prisma — удаление не потребовало миграции схемы.
Файл: src/app/api/chat/history/route.ts
CRUD над ChatSession/ChatMessage (отдельная от agent/chat модель — здесь своя таблица истории чата).
- Auth: требуется во всех методах (
401 { error: "Auth required" }). - GET Request: query
?sessionId=(опционально). Без него — список сессий пользователя:200 { sessions: [{id,title,created_at,createdAt}] }. С ним — сообщения сессии:200 { messages: [{id,role,text,timestamp}] }.500 { error: String(err) }. - POST Request:
{ action: 'create_session'|'save_message', sessionId, title?, message?: {id,role,text, metadata?} }.create_session→200 { success: true }.save_message→ создаёт сессию, если её ещё нет, затем транзакционно пишет сообщение и обновляетupdated_at→200 { success: true }. Неизвестныйaction→400 { error: "Invalid action" }. - DELETE Request: query
?sessionId=(обязателен).400 { error: "Missing sessionId" }; удаляет только сессии текущего пользователя (deleteManyс фильтромuser_id);200 { success: true }независимо от того, была ли реально удалена строка.
Все роуты требуют verifySession() → 401 { error: 'Unauthorized' } при отсутствии.
Файл: src/app/api/citations/[id]/route.ts
- GET:
404 { error: 'Not found' }или200 { citation }. - PATCH Request: JSON, разрешённые к обновлению поля — белый список:
title, authors, year, venue, abstract, url, bibtex, cite_key, tags, notes, starred, doi, arxiv_id, isbn(прочие поля тела молча игнорируются). Response:404 { error: 'Not found' }/409 { error: 'cite_key already in use' }([Фаза 5] — переименование вcite_key, уже занятый другой цитатой этого пользователя, теперь отклоняется DB-констрейнтом@@unique([user_id, cite_key])вместо тихого дублирования) /200 { citation }. - DELETE:
404 { error: 'Not found' }или200 { ok: true }.
Файл: src/app/api/citations/collections/route.ts
- GET:
200 { collections }. - POST Request:
{ name (обязателен, string), color? }. Response:400 { error: 'name is required' }или200 { collection }.
Файл: src/app/api/citations/collections/[id]/route.ts
- DELETE: удаляет коллекцию.
404 { error: 'Not found' }или200 { ok: true }. - POST: добавляет/убирает цитату из коллекции. Request:
{ citationId (обязателен), remove?: boolean }—remove: trueубирает, иначе добавляет. Response:400 { error: 'citationId required' };400 { error: <message> }при ошибкеaddCitationToCollection/removeCitationFromCollection; успех —200 { ok: true }.
Файл: src/app/api/citations/route.ts
- GET Request: query
search?,tag?,starred? ('1'),collection?. Response:200 { citations }. - POST Request:
{ title, bibtex, cite_key (все три обязательны), doi?, arxiv_id?, isbn?, authors?: string[], year?: number, venue?, abstract?, url?, tags?: string[], notes?, starred?: boolean }. Response:400 { error: 'Invalid JSON' }/{ error: 'title, bibtex, cite_key are required' };500 { error: <message> }; успех —200 { citation }.
Файл: src/app/api/citations/search/route.ts
Поиск во внешних академических индексах: CrossRef (DOI/полнотекстовый поиск) и arXiv, с авто-конвертацией в
BibTeX и генерацией cite_key.
- Auth: требуется.
- Request: query
?q=<строка> (обязателен),&source=auto|crossref|arxiv|doi(defaultauto). Приauto— еслиqпохож на DOI (10.xxxx/...) → резолвится напрямую; если похож на arXiv ID (\d{4}\.\d{4,5}) → берётся одна статья; иначе — параллельный поиск в CrossRef (до 8) и arXiv (до 5). - Response:
400 { error: 'Query parameter "q" is required' };500 { error: <message> }; успех —200 { results: [{source, doi, arxiv_id, title, authors, year, venue, abstract, url, bibtex, cite_key}] }.
Единый паттерн авторизации: verifySession() → 401, затем getUserRoleInSpace(id, userId) возвращает
'owner'|'editor'|'viewer'|null. null → как правило 404 { error: 'Not found' } (чтобы не раскрывать
существование чужого приватного проекта); viewer не может изменять содержимое (403 { error: 'Forbidden' }
на write-эндпоинтах); только owner может менять настройки/удалять/приглашать/убирать участников
(403 { error: '...' }).
Файл: src/app/api/space/route.ts
- GET:
401; успех —200 { spaces: Space[] }(все пространства пользователя). - POST Request:
{ title?='Untitled Project' (обрезается до 120 симв.), template?='blank', compiler?: 'pdflatex'|'xelatex'|'lualatex' = 'pdflatex' }. Response:201 { space }.
Файл: src/app/api/space/[id]/route.ts
- GET:
404 { error: 'Not found' }или200 { space }. - PATCH: только
owner(404если роли нет,403 { error: 'Only owner can edit settings' }иначе). Request:{ title?, description?, compiler?, main_file?, auto_compile?, is_public? }. Response:400 { error: 'Invalid JSON' }или200 { ok: true }. - DELETE:
401;200 { ok: true }(удаление ограничено внутриdeleteSpace(id, userId)).
Файл: src/app/api/space/invite/[token]/route.ts
- GET (превью инвайта, без авторизации):
404 { error: 'Invite not found or expired' }или200 { invite: { space_id, role } }(email не раскрывается). - POST (принять инвайт): требуется сессия.
404 { error: 'Invite not found or expired' }или200 { ok: true, spaceId }.
Файл: src/app/api/space/[id]/invite/route.ts
Только owner. Request: { role?: 'editor'|'viewer' = 'editor' }. Response:
403 { error: 'Only owner can invite' }; успех — 201 { ok: true, inviteUrl, token }.
Файл: src/app/api/space/[id]/share/route.ts
Включает/выключает публичную read-only ссылку на проект. enableSpaceSharing/disableSpaceSharing(id, userId) (не прочитан
отдельно, поведение для не-owner — требует уточнения у владельца/см. src/lib/space-db.ts).
- Request:
{ enabled?: false }— приfalseвыключает; иначе включает. Response:200 { ok: true, enabled: false }или200 { ok: true, enabled: true, shareId, url }.
Файл: src/app/api/space/[id]/collaborators/route.ts
Любая роль (включая viewer) может смотреть список. 404 { error: 'Not found' } без роли; успех —
200 { collaborators }.
Файл: src/app/api/space/[id]/collaborators/[uid]/route.ts
- Response:
404 { error: 'Not found' }(нет роли);403 { error: 'Owner cannot leave; transfer ownership first' }(owner пытается удалить сам себя);403 { error: 'Forbidden' }(не-owner пытается удалить кого-то кроме себя); успех —200 { ok: true }.
Файл: src/app/api/space/[id]/compile/route.ts
Собирает все файлы пространства в ZIP и шлёт на внешний LaTeX-компилятор (LATEX_COMPILER_URL +
x-api-key: LATEX_COMPILER_KEY), передавая движок (pdflatex/xelatex/lualatex через заголовок
X-Compiler) и точку входа (X-Main-File).
- Response:
404 { error: 'Not found' };503 { error: 'Compiler not configured' };400 { error: 'No files to compile' }; успех при компиляции —200бинарный PDF (Content-Disposition: inline); при ошибке компиляции — тоже HTTP200, но JSON{ ok: false, log: string (обрезан до 12000 симв.) }— статус-код не отражает неуспех компиляции, различать нужно поContent-Type/телу.
Файл: src/app/api/space/[id]/duplicate/route.ts
404 { error: 'Not found' } или 201 { space }.
Файл: src/app/api/space/[id]/export/route.ts
Экспорт всех файлов проекта одним ZIP. Доступно любой роли (owner/editor/viewer).
- Response:
403 { error: 'Forbidden' }(нет роли);404 { error: 'Not found' }; успех —200бинарный ZIP (Content-Disposition: attachment; filename="<slug>.zip").
Файл: src/app/api/space/[id]/ai-edit/route.ts
AI-редактирование выделенного куска LaTeX по одному из пресетов действий (fix, rewrite, translate,
explain, shorten, expand) либо по произвольному customPrompt.
- Auth: требуется + роль в пространстве (
404без роли; редактор/владелец могут —viewerтоже технически проходит проверкуif (!role), т.е. viewer может вызывать AI-правки текста, хотя не может сохранить их файлом — стоит уточнить, ожидаемое ли это поведение).⚠️ Квота не списывается. - Request:
{ action?: keyof ACTIONS, text (обязателен), customPrompt? }. Response:503 { error: 'AI not configured' };400 { error: 'text required' }/{ error: 'action or customPrompt required' };502 { error: 'AI request failed' }; успех —200 { result: string }.
Файл: src/app/api/space/[id]/ai-fix/route.ts
AI диагностирует ошибку компиляции LaTeX по логу + исходнику, возвращает объяснение и патч.
- Auth: как у
ai-edit.⚠️ Квота не списывается. - Request:
{ log (обязателен), code?, filename? }. Response:503/400 { error: 'log required' }/502 { error: 'AI request failed' }; успех —200 { explanation?, fix? }(модель должна вернуть JSON; при сбое парсинга —{ explanation: <сырой ответ>, fix: '' }).
Файл: src/app/api/space/[id]/ws-token/route.ts
Выдаёт короткоживущий (2 часа) JWT для подключения к WebSocket-серверу совместного редактирования.
- Response:
404 { error: 'Not found' }(нет роли); успех —200 { token }(payload{spaceId, userId, role}, подписанWS_JWT_SECRET, см. предупреждение о fallback-секрете в §0.6, п.3).
Файл: src/app/api/space/[id]/files/route.ts
- GET: список файлов без содержимого (
{id, path, mime_type, size_bytes, updated_at, is_binary}).404без роли; успех —200 { files }. - POST: создаёт новый текстовый файл. Роль
viewerзапрещена (403). Request:{ path (обязателен), content?='' }. Response:400 { error: 'path required' }; успех —201 { ok: true }.
Файл: src/app/api/space/[id]/files/[...path]/route.ts
Работа с содержимым одного файла по произвольному вложенному пути ([...path] — каталоги через /).
- GET:
404 { error: 'Not found' }(нет роли) /404 { error: 'File not found' }; успех —200 { file }. - PUT:
400 { error: 'Invalid path' }(path пуст, начинается с/, содержит../или NUL-байт — защита от path traversal);403 { error: 'Forbidden' }(viewer);413 { error: 'File too large (max 2 MB)' }; успех —200 { ok: true }. - DELETE:
403для viewer;200 { ok: true }.
Файл: src/app/api/space/[id]/files/import-zip/route.ts
Импортирует содержимое ZIP-архива как файлы проекта (текстовые по whitelist-расширениям идут как текст, прочие — как base64/бинарные), с авто-детектом общей корневой папки (Overleaf-style экспорт).
- Request:
multipart/form-data { file }(ZIP, ≤20 МБ; отдельные файлы внутри ≤5 МБ — превышающие пропускаются и попадают вskipped). Response:403для viewer;413 { error: 'ZIP too large (max 20 MB)' };400 { error: 'Invalid ZIP file' }; успех —200 { ok: true, imported: string[], skipped: string[] }.
Файл: src/app/api/space/[id]/files/rename/route.ts
Request: { from (обязателен), to (обязателен) }. Response: 403 для viewer; 400 { error: 'from and to required' }; 200 { ok: true } (если from === to — no-op, тоже 200 { ok: true }).
Файл: src/app/api/space/[id]/files/upload/route.ts
Загрузка одного бинарного файла (изображение и т.п.), лимит 5 МБ, путь по умолчанию images/<filename>.
Request: multipart/form-data { file, path? }. Response: 403 для viewer; 413 { error: 'File too large (max 5 MB)' }; успех — 201 { ok: true, path }.
Файл: src/app/api/space/[id]/versions/route.ts
- GET:
200 { versions }. - POST (ручное сохранение снапшота):
403для viewer. Request:{ label?='Manual save' (до 100 симв.), message? (до 500 симв.) }. Response:201 { version }.
Файл: src/app/api/space/[id]/versions/[vid]/route.ts
404 { error: 'Not found' } (нет роли, либо версия не найдена, либо version.space_id !== id); успех —
200 { version }.
Файл: src/app/api/space/[id]/versions/[vid]/restore/route.ts
403 для viewer; успех — 200 { ok: true }.
Файл: src/app/api/billing/checkout/route.ts
Создаёт платёжную ссылку через CryptoCloud (USD), с фоллбэком на статичный POS-терминал.
Шлёт пользователю Telegram-уведомление со ссылкой на оплату (если есть TELEGRAM_BOT_TOKEN и известен
telegram_id).
⚠️ До 2026-09-03 этот роут также поддерживал Kaspi Pay (KZT) черезcurrency: 'kzt'. Интеграция была заброшена (подтверждено владельцем, никогда не была подключена в CI) и код удалён — см.docs/integrations.md, раздел 3. Полеcurrencyв запросе больше ни на что не влияет.
- Auth: требуется (
401 { error: 'Auth required' }). - Request:
{ planId|packId (id пакета из PLANS) }. - Response:
400 { error: 'Invalid package selected' }.- Нет ключей CryptoCloud →
200 { fallback_url: POS_FALLBACK }(статичная ссылка на терминал). 502 { error: 'Gateway error', fallback_url }— CryptoCloud вернул невалидный JSON.500 { error: <message>, fallback_url }— CryptoCloud вернул ошибку/не дал ссылку.- Успех:
200 { url: <ссылка на оплату> }.
Файл src/app/api/billing/kaspi-webhook/route.ts больше не существует (Kaspi Pay заброшена, подтверждено
владельцем). Запрос на этот путь теперь возвращает 404. Сохранено здесь только для истории: раньше принимал
{ txn_id, order_id, status, amount, currency, sign } с HMAC-SHA256-подписью и отвечал в специфичном для
Kaspi формате { result: 0 | 1 }.
Файл: src/app/api/billing/receipt/[id]/route.ts
Отдаёт готовый PDF-чек (хранится как base64 в Receipt.pdf_base64).
- Auth: нет.
- Response:
400(невалидный id);202HTML-заглушка "чек генерируется..." (авто-refresh через 5 c), если запись есть, ноpdf_base64ещё пуст;404— записи нет вообще;500текстовый лог ошибки LaTeX, если вместо PDF внутри лежит текст компиляции (магический байт-чек%PDFне совпал); успех —200бинарный PDF (Content-Disposition: inline). Умеет распаковывать ZIP, если хранившийся blob оказался ZIP-архивом с PDF внутри.
Файл: src/app/api/billing/receipt/[id]/verify/route.ts
Публичная HTML-страница "верификации" чека (стилизованная под фискальный протокол), маскирует username/
telegram_id пользователя частично (например, @iv***).
- Auth: нет.
- Request: query
?hash=,?sig=(опциональны — используются только для отображения, не проверяются). - Response:
400(невалидный id);404HTML "Чек не найден"; успех —200HTML-страница.
Файл: src/app/api/billing/stats/route.ts
Последние 20 транзакций + все чеки текущего пользователя (для страницы биллинга).
- Auth: требуется. Response:
500 { error }; успех —200 { transactions, receipts, hourlyData: [] }(hourlyDataзахардкожен пустым).
Файл: src/app/api/billing/webhook/route.ts
Основной вебхук CryptoCloud. Auth: не сессионная — MD5-подпись
(sign = MD5(status + order_id + amount_crypto + currency_crypto + CRYPTOCLOUD_SECRET)), плюс идемпотентность
через ProcessedPayment. Принимает тело как application/x-www-form-urlencoded либо JSON (автоопределение по
первому символу).
- Response:
200 'OK'для неуспешных/уже обработанных статусов;500 'Server misconfiguration'безCRYPTOCLOUD_SECRET;403 'Missing signature'/'Invalid signature';400 'Invalid order ID'/'Bad package data';200 'Already processed'; успех —200 'OK'(начисление ресурсов/подписки в транзакции, генерация чека с live-курсом USD→KZT/RUB черезopen.er-api.com, Telegram-уведомление).
Единая проверка во всех трёх файлах — функция verifyAdmin(): требует валидную сессию и
user.telegram_id === '1153844209' (единственный захардкоженный супер-админ ID). Несоответствие →
403 { error: "Forbidden" } (не 401, даже если сессии вообще нет).
Файл: src/app/api/admin/promos/route.ts
- GET: список до 100 промокодов.
200 { promos }. - PUT Request:
{ promoId (обязателен), action: 'toggle_active'|'delete', is_active? }. Response:400 { error: "Missing promoId" }/{ error: "Invalid action" }; успех —200 { success: true }(deleteсначала чиститPromoUsage, затемPromoCode).
Файл: src/app/api/admin/stats/route.ts
Глобальная статистика: число пользователей, суммарные токены использования, число покупок, график за 7 дней.
- Response:
200 { stats: { totalUsers, totalUsage, totalTransactions }, weekData: [{label, value}] }.
Файл: src/app/api/admin/users/route.ts
- GET: query
?search=(поtelegram_id/username/first_name,insensitive); до 50 записей.200 { users }. - PUT Request:
{ targetUserId, action: 'grant_chars'|'grant_reports'|'grant_subscription'|'set_tier'| 'toggle_ban', amount?, tier? }. Response:404 { error: "User not found" };400 { error: "Invalid action" };toggle_ban→200 { success: true, newBanStatus }; прочие действия →200 { success: true }(гранты символов/отчётов/подписки дополнительно создаютTransactionи фоново генерируют чек черезgenerateAndStoreReceipt).
Служебные эндпоинты для контейнера telegram-bot. Все защищены только сравнением заголовка
x-bot-secret/X-Bot-Secret с process.env.WEBHOOK_SECRET (см. §0.4) — при несовпадении 401 или 403
{ error: "Unauthorized" } (код варьируется по файлу — см. ниже).
Файл: src/app/api/internal/bot/billing/route.ts
Мульти-action эндпоинт для биллинга из бота: status, packages, checkout, transactions, promo,
receipt.
- Auth:
x-bot-secret→403при несовпадении. - Request:
{ action, telegram_id (обязателен кроме action="packages"), ...специфичные поля }. - Response (по
action):status/без action:404 { error: "User not found" }или200 { billing: {...} }.packages:200 { packages: [...] }(опционально фильтрcategory: 'chars'|'reports'|'combo'|'all').checkout:404/400 { error: "Invalid package" }; успех —200 { success: true, pay_url, order_id?, pack, fallback? }.transactions:200 { transactions, receipts }(limitпо умолчанию 10).promo:400 { error: "Missing code" };404 { error: "Промокод не найден." };410 { error: "Промокод деактивирован." }/"Лимит активаций исчерпан." };403 { error: "Вы уже использовали этот промокод." }(уникальныйPromoUsage); успех —200 { success, type, amount, receipt_url, message }.receipt:404 { error: "Transaction not found" }; успех —200 { receipt: {...} }.- Неизвестный
action:400 { error: "Unknown action" }. Прочие ошибки —500 { error }.
Файл: src/app/api/internal/bot/compile/route.ts
Компилирует R/Python-код от имени пользователя бота, списывая символы по длине кода до компиляции.
- Request:
{ code (обязателен), type?: 'python'|'r' = 'python', telegramId (обязателен), files?: any[] }. - Response:
403(секрет);400 { error: "Code and Telegram ID are required" };404 { error: "User not found" };402 { error: "Insufficient balance. Need <N> symbols, but you have <M>." };500 { error: "Compiler error: ..." }; успех —200 <результат компилятора>.
Файл: src/app/api/internal/bot/extract-text/route.ts
Универсальный экстрактор текста/изображений: PDF → внутренний pdf-extractor сервис (PDF_EXTRACTOR_URL);
Office-форматы (xlsx/xls/docx/doc/pptx/ppt) сначала конвертируются в PDF через внешний Stirling API
(PDF_API_BASE + захардкоженный PDF_API_KEY, см. §0.6, п.2); текстовые форматы (txt/csv/tsv/json/md/xml)
парсятся локально (для CSV — выдержка схемы: заголовок + первые 10 строк, лимит чтения 5 МБ).
- Request: тело — сырые байты файла (не multipart!), заголовки
x-file-name(имя файла),x-bot-secret. - Response:
403(секрет);200 { text: "", error: "Unsupported file type" }для неизвестных расширений (обратите внимание —200, не4xx); успех —200 { text, images, fileName, chars, pages };500 { error, text: "", images: [] }при внутренней ошибке.
Файл: src/app/api/internal/bot/history/files/route.ts
Отдаёт содержимое сессии агента в разных форматах для скачивания из бота: zip (исходники + изображения),
pdf (LaTeX → компилятор, либо markdown → PDF фоллбэк через Stirling), images (список графиков), code
(исходный код визуализации/LaTeX).
- Request:
{ sessionId (обязателен), type: 'zip'|'pdf'|'images'|'code' }. - Response:
403(секрет);404 { error: "Session not found" };type='zip'→200бинарный ZIP;type='pdf'→200бинарный PDF или404 { error: "Нет содержимого для генерации PDF." }/500 { error: "Не удалось скомпилировать PDF. Используйте ZIP." };type='images'→200 { visuals: [...] };type='code'→200текстовый файл или ZIP из нескольких, либо404 { error: "Код для этой сессии отсутствует." };400 { error: "Invalid type" }для прочих значений.
Файл: src/app/api/internal/bot/history/route.ts
- POST: три режима по составу тела — просмотр одной сессии (
{sessionId}), обновлениеvisuals_jsonсессии ({sessionId, updateVisuals}), пагинированный список сессий пользователя ({telegram_id, page?=1, limit?=5}). Response:400 { error: "Missing telegram_id" };404 { error: "User not found" }; успех —200 { session }/200 { success: true }/200 { sessions, total, totalPages, currentPage }. - DELETE: query
?sessionId=.400 { error: "Missing sessionId" }; успех —200 { success: true }(⚠️ удалениеAgentSessionпоidбез проверки, что она принадлежит переданномуtelegram_id— но эндпоинт защищён только общимWEBHOOK_SECRET, доверенный вызывающий — сам бот).
Файл: src/app/api/internal/bot/referral/apply/route.ts
Начисляет 100 000 символов рефереру за нового приглашённого пользователя (только если приглашённый ещё не существовал в БД).
- Auth: заголовок
X-Bot-Secret→401(не403, в отличие от большинства других bot-роутов) при несовпадении. - Request:
{ invitee: {id, username?, first_name?}, referrerId }. Response:404 { error: "Referrer not found" };200 { success: true, awarded: true }(новый юзер, бонус начислен) или200 { success: true, awarded: false, message: "User already registered" }.
Файл: src/app/api/internal/bot/referral/route.ts
Возвращает реферальную статистику и ссылку.
- Auth:
X-Bot-Secret→401. - Request:
{ telegram_id }. Response:404 { error: "User not found" }; успех —200 { success: true, stats, referralLink: "https://t.me/perrichenobot?start=ref_<telegram_id>" }.
Файл: src/app/api/internal/bot/session/route.ts
Хранилище "сессии диалога" бота (мастер текущего шага сценария) + управление активными веб-сессиями пользователя.
- Auth:
x-bot-secret→403. - GET Request: query
?telegramId=(список веб-сессий пользователя черезgetSessionsByUserId) либо?userId=(одна bot-сессия черезgetBotSession).400 { error: "Missing userId" }, если ни один параметр не подошёл под первую ветку иuserIdне передан. - POST Request:
{ action: 'terminate_others', telegramId }— убивает все веб-сессии пользователя кроме текущей; либо{ userId, session }— апдейт bot-сессии. Response:400 { error: "Missing data" }; успех —200 { success: true }.
Файл: src/app/api/internal/bot/tasks/route.ts
Просмотр/сброс активных задач генерации (AgentSession со статусом generating|processing|extracting)
пользователя бота.
- Auth:
X-Bot-Secret→401. - Request:
{ telegram_id, action?: 'reset', sessionId? }. Response:404 { error: "User not found" };action='reset'→200 { success: true, message: "Task reset complete." }(помечает сессиюfailed); иначе →200 { success: true, tasks: [{...,timeAgo (минут)}] }.
Файл: src/app/api/internal/bot/user-info/route.ts
- Auth:
x-bot-secret→403. - Request:
{ telegram_id (обязателен) }. Response:400 { error: "Missing telegram_id" };404 { error: "User not found" }; успех —200 { user }(полная записьUser, без фильтрации полей).
Файл: src/app/api/internal/bot/verify/route.ts
Вызывается ботом, когда пользователь ввёл /start <token> — подтверждает AuthRequest, привязывая Telegram-
данные пользователя; автопромоутит в админы, если user.id входит в захардкоженный список
SUPER_ADMINS = ['1153844209', '5934503762'].
- Auth:
x-bot-secret→403. - Request:
{ token (обязателен), user: {id (обязателен), username?, first_name?, ...} }. - Response:
400 { error: "Missing token or user data" };404 { error: "Token not found or expired" };409 { error: "Token already used" }; успех —200 { success: true };500 { error }.
Файл: src/app/api/internal/bot/visual/compile/route.ts
Простой прокси компиляции R/Python-кода без биллинга/сессий (в отличие от internal/bot/compile).
- Auth:
x-bot-secret→403. - Request:
{ code (обязателен), language?: 'python'|<other, трактуется как R> = 'python' }. - Response:
400 { error: "No code provided" };502 { error: "Compiler Error: ..." }; успех —200 <результат компилятора>.
Файл: src/app/api/internal/bot/visual/generate/route.ts
Самый сложный bot-эндпоинт: принимает извлечённый текст/изображения документа из бота, генерирует и
компилирует визуализацию (с автоисправлением ошибки — 1 повтор), пишет прогресс через колбэки на сам бот
(BOT_INTERNAL_URL = "http://telegram-bot:3001/bot-internal", эндпоинты /update-visual,
/complete-visual, тоже защищённые тем же X-Bot-Secret). Есть кэш по заголовку сессии с тем же title.
- Auth:
x-bot-secret→403.500 { error: "OpenAI API Key not configured" }. - Request:
{ context: { text_data? }, language?: 'python'|'r', telegramId, title?, chartType?='auto', chatId?, messageId?, images?: string[], instruction?, attachedFiles?: any[] }. - Response:
400 { error: "No context provided" };422 { error: "⚠️ В документе недостаточно данных для анализа..." }(текст < 400 символов и нет изображений — защита от галлюцинаций);403 { error: "Ваш аккаунт заморожен администрацией." }(user.is_banned);402 { error: "Insufficient balance..." }; успех —200 { success: true, sessionId, cached?: true }— фактическая генерация асинхронна ([Фаза 6a] задача BullMQbot-visual-generate, исполняется вworker,src/lib/jobs/botVisualGenerate.ts), статус/результат доступны черезinternal/bot/history(sessionId) или push-колбэки в бот.
src/lib/session.ts проверку авторизации (см. критичную
находку в §0.6, п.1) — тот же cookie perricheno_session, но верифицируемый захардкоженным JWT-секретом
"super-secret-key-change-this-in-env-938210" вместо process.env.SESSION_SECRET.
Файл: src/app/api/tasks/parse/route.ts
Парсит естественно-языковой текст ("напомни мне в 18:00 позвонить маме") в задачу с точной датой через
OpenAI (gpt-4.1-nano-2025-04-14), сразу сохраняет как Task.
- Auth: своя
verifyAuth()(см. выше) →401 { error: 'Unauthorized' }. - Request:
{ text (обязателен), timezoneOffset, currentTime }. - Response:
500 { error: 'OpenAI API key is missing' };400 { error: 'Text input is required' };500 { error: 'Failed to parse task from OpenAI' }/{ error: 'Failed to parse model response' }/{ error: 'Model returned incomplete data' }; успех —200 { success: true, task }.
Файл: src/app/api/tasks/route.ts
Обычный CRUD над задачами напоминаний.
- Auth: та же
verifyAuth()(дублирует код изtasks/parse). - GET:
200 <Task[]>(массив напрямую, без обёртки в объект). - POST Request:
{ text (обязателен), remindAt (обязателен) }. Response:400 { error: 'Text and remindAt are required' };500 { error: 'Failed to create task' }; успех —200 <Task>. - PUT Request:
{ taskId (обязателен), status?, text?, remindAt? }— если переданtext/remindAt, идёт прямойprisma.task.update(без проверки владения задачей текущим пользователем⚠️ ); иначе —updateTaskStatus(taskId, status). Response:400 { error: 'taskId is required' };500 { error: 'Failed to update task' }; успех —200 { success: true }. - DELETE Request: query
?id=(обязателен). Response:400 { error: 'id parameter is required' };500 { error: 'Failed to delete task' }; успех —200 { success: true }(⚠️ deleteTask(Number(taskId))тоже без явной проверки, что задача принадлежит текущему пользователю).
Файл: src/app/api/telegram/send/route.ts
Отправляет документ пользователю в Telegram через Bot API (sendDocument), используется как внутренний шаг
после конвертации файлов (pdf-proxy).
- Auth: требуется (
verifySession(),401 { error: "Unauthorized" }). - Request:
multipart/form-data { document (файл), chat_id }. - Response:
500 { error: "Server configuration error" }(нетTELEGRAM_BOT_TOKEN);400 { error: "Missing file or chat_id" };<telegramStatus> { error: "Telegram send failed", details }; успех —200 { success: true }.
Файл: src/app/api/health/route.ts
Публичный health-check с проверкой соединения с БД (SELECT 1).
- Auth: нет.
- Response:
200 { status: 'UP', timestamp, database: 'CONNECTED' }или503 { status: 'DOWN', timestamp, database: 'DISCONNECTED', error }.
Файл: src/app/api/pdf-proxy/route.ts
Единая точка входа для ~90 операций Stirling PDF (конвертации, merge/split, безопасность, OCR, метаданные и
т.д. — полный список эндпоинтов зашит в объект ENDPOINTS). Ключ Stirling API захардкожен в файле (см. §0.6,
п.2). После успешной конвертации, если у вызывающего есть валидная сессия и известен его telegram_id,
результат дополнительно фоново отправляется в Telegram через /api/telegram/send.
- Auth: нет для самой конвертации — любой может вызвать эндпоинт без сессии, авторизация проверяется только опционально внутри, чтобы решить, слать ли файл в Telegram.
- Request: query
?type=<ключ из ENDPOINTS>(по умолчаниюfile-to-pdf),multipart/form-dataсfileInputилиurlInput(плюс служебное полеoriginalName, удаляется перед проксированием) и прочими полями, специфичными для конкретной Stirling-операции. - Response:
400 { error: "Invalid conversion type" };400 { error: "No input provided" };<upstreamStatus> { error: "API Error: <status>", details }; успех —200бинарный файл с оригинальнымиContent-Type/Content-Dispositionот Stirling;500 { error: String(e) }.
Файл: src/app/api/webhook-proxy/route.ts
Серверный прокси для вызова внешних webhook'ов (n8n) в обход CORS, с базовой SSRF-защитой (только https:,
опциональный allowlist хостов через ALLOWED_WEBHOOK_HOSTS).
- Auth: требуется (
verifySession(),401 { error: "Unauthorized" }). - Request:
multipart/form-dataс полемwebhookUrl(обязателен, будет удалён из формы перед проксированием) + произвольные прочие поля, пересылаемые как есть. - Response:
400 { error: "Missing webhookUrl" }/{ error: "Invalid webhookUrl" };400 { error: "Only HTTPS webhooks allowed" };403 { error: "Webhook host not allowed" }(если задан allowlist);504 { error: "Webhook timed out (120s)" };502 { error: <message> }; успех —200(JSON или text, проксируется от n8n как есть).
Файл: src/app/api/webhook/telegram/route.ts
Публичная точка приёма вебхуков Telegram Bot API — проксирует апдейты во внутренний контейнер бота
(BOT_CONTAINER_URL, по умолчанию http://telegram-bot:3001).
- Auth: Telegram-специфичный секретный токен вебхука — заголовок
x-telegram-bot-api-secret-tokenсравнивается сprocess.env.WEBHOOK_SECRET(тот же env-секрет, что иx-bot-secretв internal/bot-роутах, но другой канал/заголовок — настраивается при регистрации вебхука через Telegram Bot APIsetWebhook(secret_token=...)). - Request: сырое тело от Telegram (JSON Update-объект) — пробрасывается как текст без парсинга.
- Response:
403 { error: "Forbidden" }при несовпадении секрета; иначе — статус ответа от контейнера бота пробрасывается 1:1 (тело не передаётся обратно, только код);502 { error: "Proxy failed" }при сетевой ошибке до бота.
| Путь | Метод | Auth | Назначение |
|---|---|---|---|
/api/admin/promos |
GET | Session + admin (telegram_id=1153844209) |
Список промокодов |
/api/admin/promos |
PUT | Session + admin | Toggle/удалить промокод |
/api/admin/stats |
GET | Session + admin | Глобальная статистика (юзеры/использование/график 7д) |
/api/admin/users |
GET | Session + admin | Поиск/список пользователей |
/api/admin/users |
PUT | Session + admin | Гранты символов/отчётов/подписки, бан, тир |
/api/agent/analytics/generate |
POST | Session | Фоновый пайплайн аналитики данных (графики) |
/api/agent/attach |
POST | Session | Универсальная загрузка вложения (PDF/img/data) |
/api/agent/attach |
DELETE | Session | Удалить вложение |
/api/agent/chat |
POST | Session | SSE-чат с GPT (с attach-контекстом) |
/api/agent/chat/sessions |
POST | Session | Создать чат-сессию |
/api/agent/chat/sessions |
GET | Session | Список чат-сессий |
/api/agent/compile-pdf |
POST | Session | Прокси ZIP → внешний LaTeX-компилятор → PDF |
/api/agent/data-analytics |
POST | Session | suggest/generate графиков (SSE) |
/api/agent/generate |
POST | Session | Оркестратор 5-стадийной генерации документа + legacy edit/fix |
/api/agent/ingest-pdf |
POST | Session | Загрузка и парсинг PDF |
/api/agent/ingest-pdf |
DELETE | Session | Удалить upload |
/api/agent/python-compile |
POST | Session | Прокси Python-кода в компилятор |
/api/agent/r-compile |
POST | Session | Прокси R-кода в компилятор |
/api/agent/scholar/search |
POST | Session (без проверки владения сессией) | Поиск статей arXiv/OpenAlex/CrossRef |
/api/agent/scholar/sessions/[id] |
DELETE | Session | Удалить сессию лит. поиска |
/api/agent/scholar/sessions |
POST | Session | Создать сессию лит. поиска (спишет 2500 симв.) |
/api/agent/sessions/[id] |
GET | Session (owner) | Получить сессию агента |
/api/agent/sessions/[id] |
PUT | Session (owner) | Частично обновить сессию |
/api/agent/sessions/[id] |
DELETE | Session (owner) | Удалить сессию |
/api/agent/sessions/[id]/share |
POST | Session | Вкл/выкл публичную ссылку на сессию |
/api/agent/sessions |
GET | Session | Список всех сессий пользователя |
/api/agent/sessions |
POST | Session | Создать произвольную сессию |
/api/agent/templates/upload |
POST | Session | Извлечь LaTeX-преамбулу из ZIP-шаблона |
/api/agent/uploads |
GET | Session | Метаданные вложений по id |
/api/agent/visualize/edit |
POST | Session (без списания квоты) | AI-правка кода визуализации |
/api/agent/visualize/generate-code |
POST | Session (без списания квоты) | Стрим сырого кода визуализации |
/api/agent/visualize/recommend |
POST | Session (без списания квоты) | AI подбирает 3 типа графика |
/api/agent/visualize |
POST | Session | compile/generate одного графика (синхронно) |
/api/auth/delete |
DELETE | Session | Удалить аккаунт |
/api/auth/link |
POST | Нет | Сгенерировать deep-link токен для Telegram-логина |
/api/auth/login |
POST | Подпись Telegram Login Widget | Логин через Telegram, создать сессию |
/api/auth/logout |
POST | Нет (no-op без сессии) | Выйти |
/api/auth/me |
GET | Session | Профиль + лимиты плана |
/api/auth/poll |
GET | Нет (по одноразовому token) | Опрос статуса Telegram-логина |
/api/billing/checkout |
POST | Session | Создать ссылку оплаты (CryptoCloud) |
/api/billing/kaspi-webhook |
— | — | Удалён 2026-09-03, теперь 404 (Kaspi Pay заброшена) |
/api/billing/receipt/[id] |
GET | Нет | Скачать PDF-чек |
/api/billing/receipt/[id]/verify |
GET | Нет | HTML-страница верификации чека |
/api/billing/stats |
GET | Session | Транзакции и чеки пользователя |
/api/billing/webhook |
POST | MD5-подпись CryptoCloud | Вебхук подтверждения оплаты CryptoCloud |
/api/chat/history |
GET | Session | Список чат-сессий / сообщения сессии |
/api/chat/history |
POST | Session | Создать сессию / сохранить сообщение |
/api/chat/history |
DELETE | Session | Удалить чат-сессию |
/api/citations/[id] |
GET | Session | Получить цитату |
/api/citations/[id] |
PATCH | Session | Частично обновить цитату |
/api/citations/[id] |
DELETE | Session | Удалить цитату |
/api/citations/collections |
GET | Session | Список коллекций |
/api/citations/collections |
POST | Session | Создать коллекцию |
/api/citations/collections/[id] |
DELETE | Session | Удалить коллекцию |
/api/citations/collections/[id] |
POST | Session | Добавить/убрать цитату из коллекции |
/api/citations |
GET | Session | Список/поиск цитат пользователя |
/api/citations |
POST | Session | Создать цитату |
/api/citations/search |
GET | Session | Поиск в CrossRef/arXiv/DOI |
/api/health |
GET | Нет | Health-check БД |
/api/internal/bot/billing |
POST | x-bot-secret |
Биллинг-операции для бота (status/packages/checkout/promo/...) |
/api/internal/bot/compile |
POST | x-bot-secret |
Компиляция кода от имени юзера бота + списание квоты |
/api/internal/bot/extract-text |
POST | x-bot-secret |
Извлечение текста/изображений из файла |
/api/internal/bot/history/files |
POST | x-bot-secret |
Скачивание ZIP/PDF/images/code сессии из бота |
/api/internal/bot/history |
POST | x-bot-secret |
Просмотр/список сессий, апдейт visuals_json |
/api/internal/bot/history |
DELETE | x-bot-secret |
Удалить сессию |
/api/internal/bot/referral/apply |
POST | X-Bot-Secret |
Начислить реферальный бонус |
/api/internal/bot/referral |
POST | X-Bot-Secret |
Реферальная статистика/ссылка |
/api/internal/bot/session |
GET | x-bot-secret |
Список веб-сессий / bot-сессия пользователя |
/api/internal/bot/session |
POST | x-bot-secret |
Апдейт bot-сессии / терминировать другие веб-сессии |
/api/internal/bot/tasks |
POST | X-Bot-Secret |
Активные задачи генерации / сброс задачи |
/api/internal/bot/user-info |
POST | x-bot-secret |
Получить полную запись User по telegram_id |
/api/internal/bot/verify |
POST | x-bot-secret |
Подтвердить /start <token>, создать/промоутнуть юзера |
/api/internal/bot/visual/compile |
POST | x-bot-secret |
Прямая компиляция кода без биллинга |
/api/internal/bot/visual/generate |
POST | x-bot-secret |
Генерация+компиляция визуализации из бота |
/api/pdf-proxy |
POST | Нет (опционально сессия для Telegram-доставки) | Прокси ~90 Stirling PDF операций |
/api/r/generate |
POST | Session | suggest/multi/single генерация R-графиков |
/api/r/sessions/[id] |
GET | Session (owner) | Получить R-сессию с результатами |
/api/r/sessions/[id] |
DELETE | Session (owner) | Удалить R-сессию |
/api/r/sessions |
GET | Session | Список R-сессий пользователя |
/api/r/upload |
POST | Session | Извлечение текста/картинок из файла (прокси bot/extract-text) |
/api/space |
GET | Session | Список пространств пользователя |
/api/space |
POST | Session | Создать пространство |
/api/space/[id] |
GET | Session + роль | Получить пространство |
/api/space/[id] |
PATCH | Session + owner | Обновить настройки пространства |
/api/space/[id] |
DELETE | Session | Удалить пространство |
/api/space/invite/[token] |
GET | Нет | Превью инвайта |
/api/space/invite/[token] |
POST | Session | Принять инвайт |
/api/space/[id]/invite |
POST | Session + owner | Создать инвайт-ссылку |
/api/space/[id]/share |
POST | Session (роль не проверена в роуте) | Вкл/выкл публичный доступ к проекту |
/api/space/[id]/collaborators |
GET | Session + роль | Список участников |
/api/space/[id]/collaborators/[uid] |
DELETE | Session + роль | Удалить участника / выйти самому |
/api/space/[id]/compile |
POST | Session + роль | Собрать проект в ZIP и скомпилировать LaTeX |
/api/space/[id]/duplicate |
POST | Session | Дублировать пространство |
/api/space/[id]/export |
GET | Session + роль | Экспорт всех файлов как ZIP |
/api/space/[id]/ai-edit |
POST | Session + роль (без списания квоты) | AI-правка выделенного LaTeX |
/api/space/[id]/ai-fix |
POST | Session + роль (без списания квоты) | AI-диагностика ошибки компиляции |
/api/space/[id]/ws-token |
GET | Session + роль | JWT для WebSocket совместного редактирования |
/api/space/[id]/files |
GET | Session + роль | Список файлов проекта (без содержимого) |
/api/space/[id]/files |
POST | Session + роль (не viewer) | Создать текстовый файл |
/api/space/[id]/files/[...path] |
GET | Session + роль | Содержимое файла |
/api/space/[id]/files/[...path] |
PUT | Session + роль (не viewer) | Создать/обновить содержимое файла |
/api/space/[id]/files/[...path] |
DELETE | Session + роль (не viewer) | Удалить файл |
/api/space/[id]/files/import-zip |
POST | Session + роль (не viewer) | Импорт файлов из ZIP |
/api/space/[id]/files/rename |
POST | Session + роль (не viewer) | Переименовать файл |
/api/space/[id]/files/upload |
POST | Session + роль (не viewer) | Загрузить бинарный файл |
/api/space/[id]/versions |
GET | Session + роль | Список версий |
/api/space/[id]/versions |
POST | Session + роль (не viewer) | Создать снапшот версии |
/api/space/[id]/versions/[vid] |
GET | Session + роль | Получить версию |
/api/space/[id]/versions/[vid]/restore |
POST | Session + роль (не viewer) | Восстановить версию |
/api/tasks/parse |
POST | Кастомный JWT (хардкод-секрет, см. §0.6.1) | Распарсить NL-текст в задачу-напоминание |
/api/tasks |
GET | Кастомный JWT (хардкод-секрет) | Список задач пользователя |
/api/tasks |
POST | Кастомный JWT (хардкод-секрет) | Создать задачу |
/api/tasks |
PUT | Кастомный JWT (хардкод-секрет) | Обновить задачу (без проверки владения) |
/api/tasks |
DELETE | Кастомный JWT (хардкод-секрет) | Удалить задачу (без проверки владения) |
/api/telegram/send |
POST | Session | Отправить документ пользователю в Telegram |
/api/tikz/generate |
POST | Session | Генерация+компиляция TikZ-диаграммы (retry x3) |
/api/webhook-proxy |
POST | Session | Серверный прокси произвольного https-вебхука (n8n) с SSRF-фильтром |
/api/webhook/telegram |
POST | x-telegram-bot-api-secret-token |
Публичный приём вебхуков Telegram → прокси в bot-контейнер |
Итого: 90 файлов route.ts, 119 экспортированных HTTP-методов (эндпоинтов).