Indexium/docs/api-spec.md

11 KiB
Raw Blame History

Public REST API - Indexium v1

Base URL: https://api.indexium.example.com/api/v1 (локально http://localhost:3000/api/v1)

Все ответы - application/json. Пагинация - page/limit (MVP) → cursor позже. Кэш - Cache-Control: public, max-age=60, ETag.


Health

GET /health

Проверка живости + БД + Redis.

200

{ "status": "ok", "db": "up", "redis": "up", "version": "0.1.0" }

Webhooks (internal)

POST /webhooks/github

Принимает GitHub Webhook release.

Headers:

  • X-GitHub-Delivery: uuid
  • X-Hub-Signature-256: sha256=...
  • X-GitHub-Event: release

Body: raw JSON от GitHub.

202 - принято в очередь

{ "status": "accepted", "delivery_id": "..." }

401 - неверная подпись 409 - уже обработано (идемпотентность)

Логика: HMAC проверка → дедуп по delivery_id → push в Redis Streams → 202.


Mods

GET /mods

Query params:

param type описание
query string FTS по name/summary/README
gameVersion string фильтр 1.20.1
loader string fabric | quilt | neoforge | forge
page int default 1
limit int default 20, max 50
sort string relevance | newest | popular | active_servers

200

{
  "data": [
    {
      "slug": "sodium-extra",
      "name": "Sodium Extra",
      "summary": "Extra optimizations",
      "author": "flashy",
      "icon_url": "https://...",
      "game_versions": ["1.20.1"],
      "loaders": ["fabric"],
      "latest_version": "1.2.3",
      "download_url": "https://github.com/.../releases/download/...",
      "updated_at": "2026-09-01T12:00:00Z"
    }
  ],
  "pagination": { "page": 1, "limit": 20, "total": 142, "pages": 8 }
}

Кэшируется в Redis по ключу mods:query=...:gv=...:loader=...:page=... TTL 60s.

GET /mods/:slug

200

{
  "slug": "sodium-extra",
  "name": "Sodium Extra",
  "summary": "...",
  "description": "... (markdown)",
  "github_repo": "owner/repo",
  "author": { "login": "flashy", "avatar_url": "https://..." },
  "icon_url": "https://...",
  "verified": true,
  "versions": [
    {
      "version_number": "1.2.3",
      "game_versions": ["1.20.1"],
      "loaders": ["fabric"],
      "download_url": "https://github.com/.../releases/download/v1.2.3/sodium-extra-1.2.3.jar",
      "file_sha256": "abc...",
      "file_size": 123456,
      "published_at": "2026-09-01T12:00:00Z"
    }
  ]
}

404 - {"error":"mod_not_found"}

GET /mods/:slug/icon

Отдаёт иконку мода. Воркер при индексации извлекает assets/<modid>/icon.png (или icon из fabric.mod.json → путь внутри jar) → сохраняет в кэш/проксирует.

  • 200 - image/png / image/webp с Cache-Control: public, max-age=86400, ETag. Если иконки нет → 302 на raw.githubusercontent.com fallback или дефолтная заглушка.
  • 404 - мод не найден.

Альтернатива на MVP: не хранить иконку у себя, а отдавать icon_url как прямую ссылку https://raw.githubusercontent.com/<owner>/<repo>/<branch>/src/main/resources/assets/.... Эндпоинт /icon тогда - 302 редирект + кэш заголовков.

POST /mods/resolve - пакетный резолв для лаунчеров

Принимает список модов + окружение, возвращает дерево прямых скачиваний и зависимостей (для Prism / Modrinth-compatible клиентов).

Request

{
  "game_version": "1.20.1",
  "loader": "fabric",
  "mods": [
    { "slug": "sodium-extra", "version": "1.2.3" },
    { "slug": "lithium", "version": "latest" }
  ]
}

200

{
  "resolved": [
    {
      "slug": "sodium-extra",
      "version_number": "1.2.3",
      "download_url": "https://github.com/.../releases/download/.../sodium-extra-1.2.3.jar",
      "file_sha256": "abc...",
      "file_size": 123456,
      "dependencies": [{ "slug": "sodium", "version_range": ">=0.5.0", "resolved_version": "0.5.8" }]
    },
    {
      "slug": "sodium",
      "version_number": "0.5.8",
      "download_url": "https://github.com/.../sodium-0.5.8.jar",
      "file_sha256": "def...",
      "file_size": 654321,
      "dependencies": []
    }
  ],
  "unresolved": []
}
  • version: "latest" → резолвит последнюю совместимую с game_version + loader.
  • Транзитивные зависимости резолвятся рекурсивно (BFS, max depth 20, защита от циклов).
  • 422 - несовместимая комбинация game_version/loader.
  • Кэшируется по ключу resolve:gv:loader:hash(mods) TTL 60s.

GET /mods/:slug/versions/:version

Детали конкретной версии. Аналогично элементу массива выше + зависимости:

{
  "mod_slug": "sodium-extra",
  "version_number": "1.2.3",
  "game_versions": ["1.20.1"],
  "loaders": ["fabric"],
  "dependencies": [{ "mod_id": "sodium", "version_range": ">=0.5.0" }],
  "download_url": "https://github.com/...",
  "file_sha256": "...",
  "file_size": 123456,
  "published_at": "..."
}

POST /mods/import (auth required)

Импорт репозитория по GitHub OAuth.

Headers: Authorization: Bearer <github_token>

Body:

{ "repo": "owner/repo" }

Логика: проверить что токен имеет доступ к репо → fetch fabric.mod.json из default branch → создать запись mods → повесить webhook.

201 - создан 409 - уже импортирован 422 - манифест не найден


Auth

GET /auth/github → 302 redirect на GitHub OAuth

POST /auth/tokens (auth) - PAT creation

Body: { "name": "ci-token", "scopes": ["read:mods","write:mods"], "expires_in_days": 30 } → 201 { token: "idx_...", id, expires_at } (токен показывается 1 раз, храним hash). Authorization: Bearer idx_... для API.

GET /auth/tokens / DELETE /auth/tokens/:id - список/отзыв.

Device Flow (Phase 2, RFC 8628)

  • POST /oauth/device/code → { device_code, user_code: "ABCD-1234", verification_uri: "https://indexium.example.com/activate", expires_in: 600 }
  • GET /activate (frontend) - ввод user_code → consent → POST /oauth/device/verify { user_code }
  • POST /oauth/token grant_type=urn:ietf:params:oauth:grant-type:device_code → { access_token, refresh_token }
  • Лаунчер поллит /oauth/token до получения токена.

GET /auth/discord → линк Discord (linked_account, не логин). GET /auth/discord/callback → запись в linked_accounts.

Profiles & Social

GET /u/:login / GET /org/:login - публичный профиль (кэш 60s, ISR)

POST /mods/:slug/star / DELETE /mods/:slug/star - звезда (auth)

POST /u/:login/follow / DELETE /u/:login/follow - подписка на автора с опционально ?game_version=1.20.1&loader=fabric

GET /collections / POST /collections (auth, body: { title, description, mods: [{slug, version}] })

GET /collections/:slug / GET /collections/:slug/export?format=prism|packwiz

GET /v1/badges/:slug/downloads.svg / GET /v1/badges/:slug/version.svg - SVG виджет для README (public, кэш 1h)

Analytics (bStats аналог, см. docs/analytics.md)

POST /api/v1/analytics/submit - пинг от мода (gzip опционально)

Headers: Content-Type: application/json, Content-Encoding: gzip (optional) Body:

{
  "mod_slug": "sodium-extra",
  "server_uuid": "e8d9a0f1-4b2c-...",
  "metrics": {
    "mc_version": "1.20.1",
    "loader": "fabric",
    "java_version": "21.0.2",
    "os": "Linux",
    "player_count": 12,
    "custom_charts": { "gui_theme": "dark" }
  }
}
  • Валидация: mod_slug exists, allow-list версий/лоадеров, custom_charts ≤5 ключей.
  • Анонимизация: server_hash = sha256(server_uuid + daily_salt) - IP не храним.
  • Rate limit: 1 пинг / 15 мин per server_hash+mod_slug (Redis SET NX EX 900) → 429.
  • Opt-Out: respect -Dindexium.analytics.disable=true на клиенте.
  • 200 { "status": "ok" } 400 validation 429 rate_limited

GET /api/v1/mods/:slug/analytics?range=7d|30d|90d

Public, кэш public, max-age=300.

{
  "mod_slug": "sodium-extra",
  "range": "30d",
  "daily": [
    { "date": "2026-09-01", "active_servers": 450, "active_players": 3200 },
    { "date": "2026-09-02", "active_servers": 470, "active_players": 3400 }
  ],
  "breakdown": {
    "mc_versions": { "1.20.1": 400, "1.21": 50 },
    "loaders": { "fabric": 420, "neoforge": 30 },
    "os": { "Linux": 200, "Windows": 250 },
    "java": { "21": 300, "17": 150 },
    "custom": { "gui_theme": { "dark": 120, "light": 30 } }
  }
}

404 mod_not_found. Источник: mod_daily_stats.

GET /api/v1/badges/:slug/servers.svg - бейдж активных серверов (как downloads.svg, кэш 1h)

SVG Active Servers: 1.2k из mod_daily_stats за вчера.

Sponsors & Badges

  • GET /u/:login отдаёт sponsors: { github, patreon, kofi, bmc } и badges: ["early_adopter","verified"]
  • Бейджи выдаются воркером (badges таблица), SVG - динамически.

Ошибки

Единый формат:

{ "error": "validation_error", "message": "gameVersion must be semver", "details": {...} }

Коды:

  • 400 validation_error
  • 401 unauthorized
  • 404 not_found
  • 429 rate_limited (headers Retry-After)
  • 500 internal_error

Rate limiting

  • Public API: 60 req/min per IP (Redis).
  • Webhook: без лимита, но HMAC обязателен.

Версионирование

  • URL версионирование /api/v1.
  • Breaking changes → /api/v2 + 6 мес поддержка v1.

OpenAPI

Спека будет жить в indexium-backend/openapi.yaml (генерировать из Axum через utoipa когда созреет). На MVP - этот markdown как источник правды.