172 lines
5.5 KiB
Markdown
172 lines
5.5 KiB
Markdown
# 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:<client_id>`
|
||
- `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:<id>|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`.
|