# E6 API-контракт: egress identity (IP + country) Дата: 2026-03-10 Статус: draft (E6.1 freeze) Владелец: Engineering ## 1) Цель - Ввести единый backend-контракт для определения текущей egress-идентичности для любого движка. - Избежать дублирования логики в desktop/web/mobile: UI только читает готовые поля из Go API. - Дать унифицированные поля для показа `IP + страна` и построения флага в UI из `country_code`. ## 2) Область и ограничения - Источник истины: только Go-ядро. - UI не делает внешние IP/Geo запросы напрямую. - Поддерживаемые scope: - `adguardvpn` - `transport:` - `system` - Первичный формат ответа: `HTTP 200 + ok/message` (совместимо с текущим API-паттерном). ## 3) Модель данных ### 3.1 EgressIdentity ```json { "scope": "transport:sg-realnetns", "source": "transport", "source_id": "sg-realnetns", "ip": "203.0.113.10", "country_code": "SG", "country_name": "Singapore", "updated_at": "2026-03-10T07:20:00Z", "stale": false, "refresh_in_progress": false, "last_error": "", "next_retry_at": "" } ``` Пояснения: - `scope`: нормализованный ключ области. - `source`: `adguardvpn|transport|system`. - `source_id`: для `transport` это `client_id`, иначе пусто. - `ip`: egress IP для выбранного scope. - `country_code`: ISO-2 uppercase (например `US`, `SG`). - `country_name`: человекочитаемое имя страны. - `updated_at/stale/refresh_in_progress/last_error/next_retry_at`: SWR-метаданные. ### 3.2 EgressIdentityResponse ```json { "ok": true, "message": "ok", "item": { "scope": "adguardvpn", "source": "adguardvpn", "source_id": "", "ip": "198.51.100.5", "country_code": "NL", "country_name": "Netherlands", "updated_at": "2026-03-10T07:20:00Z", "stale": false, "refresh_in_progress": false, "last_error": "", "next_retry_at": "" } } ``` ### 3.3 EgressIdentityRefreshRequest ```json { "scopes": ["adguardvpn", "transport:sg-realnetns"], "force": false } ``` ### 3.4 EgressIdentityRefreshResponse ```json { "ok": true, "message": "refresh queued", "count": 2, "queued": 1, "skipped": 1, "items": [ { "scope": "adguardvpn", "status": "queued", "queued": true, "reason": "" }, { "scope": "transport:sg-realnetns", "status": "skipped", "queued": false, "reason": "throttled or already fresh" } ] } ``` ## 4) Endpoint-ы ### 4.1 `GET /api/v1/egress/identity` Назначение: - Получить текущий snapshot egress identity для одного scope. Query: - `scope` (required): `adguardvpn|transport:|system` - `refresh=1` (optional): best-effort trigger фонового refresh перед возвратом snapshot. Response: - `200 + EgressIdentityResponse`. Ошибки запроса: - `400` при невалидном `scope`. ### 4.2 `POST /api/v1/egress/identity/refresh` Назначение: - Поставить refresh в очередь для одного или нескольких scope без блокировки UI. Request body: - `scopes[]` optional (если пусто -> refresh всех известных scope); - `force` optional (`true` игнорирует freshness TTL, но уважает single-flight lock). Response: - `200 + EgressIdentityRefreshResponse`. Ошибки запроса: - `400` bad json / invalid scope format. ## 5) Правила freshness/SWR - Refresh делается в фоне, UI получает последний cache snapshot мгновенно. - Для каждого scope: - single-flight (не запускать параллельные одинаковые refresh); - backoff при ошибках; - `stale=true` если snapshot устарел или нет новых данных; - `next_retry_at` выставляется при backoff. - Рекомендуемая стратегия UI: - сразу рисовать cache (`GET`), - отдельным действием триггерить `POST .../refresh`, - обновляться по SSE/ручному poll. ## 6) SSE событие Событие для подписчиков: - `egress_identity_changed` Payload (минимум): ```json { "scope": "transport:sg-realnetns", "ip": "203.0.113.10", "country_code": "SG", "country_name": "Singapore", "updated_at": "2026-03-10T07:20:00Z", "stale": false, "last_error": "" } ``` ## 7) Отрисовка флага - Backend возвращает только `country_code`. - Флаг рендерится в UI из `country_code` (desktop/web/mobile одинаково). - Если `country_code` пустой/невалидный: UI показывает `N/A` без флага. ## 8) Минимальные критерии готовности E6.1 - Документирован единый контракт `GET/POST` для egress identity. - Зафиксированы scope и поля snapshot. - Зафиксированы правила SWR и SSE-событие. - Явно указано, что флаг строится в UI из `country_code`.