Шлюз HTTP/JSON, API-ключи, сессии, чтения с реплик, миграции builtin://
Руководство к функциям 1.0 «дешёвого» списка (ответ заказчика 1, сессия 6) [С §12.4, §12.8, §13.2, §16.3]; решения — D-8200..D-8249 в decisions.md.
Шлюз HTTP/JSON к pub(api)-сервисам#
Тот же порт, что у gRPC (expose): gRPC определяется по content-type, шлюз — по префиксу /api/. Слушатель — HTTP/2 (h2c или TLS); HTTP/1.1 на нём не поддерживается.
POST /api/<Сервис>/<rpc> Content-Type: application/json
{"id": 7} -> 200 {"result": {"id": "7", "name": "Hammer", ...}}- Обслуживаются унарные rpc сервисов
pub(api), не@internal.<Сервис>— короткое имя или полное (shop.catalog.Catalog). Потоковые rpc —501. - Тело — JSON-объект с параметрами (канонический protobuf JSON: имена
lowerCamelCaseили как в OxL; 64-битные целые принимаются числом или строкой и пишутся строкой). Пустое тело = нет параметров. Параметр без значения по умолчанию обязателен: пропущенный —400с именем поля. - Ответ — JSON ответного сообщения; значение — в
result(всегда, нулевое тоже:{"result": "0"},false,""). - Вызов идёт тем же обработчиком, что gRPC: аутентификация, политики, идемпотентность (
idempotency-key), дедлайн (grpc-timeout),@ratelimit. - Ошибка — тело
{"code": 5, "status": "NOT_FOUND", "message": "...", "details": [{"@type": "type.googleapis.com/pkg.Missing", "value": {...}}]}; типизированная ошибка лежит вdetailsкак JSON.
| gRPC | HTTP | gRPC | HTTP |
|---|---|---|---|
| INVALID_ARGUMENT, FAILED_PRECONDITION, OUT_OF_RANGE | 400 | UNAUTHENTICATED | 401 |
| PERMISSION_DENIED | 403 | NOT_FOUND / нет такого rpc | 404 |
| ALREADY_EXISTS, ABORTED | 409 | RESOURCE_EXHAUSTED | 429 |
| CANCELLED | 499 | UNKNOWN, INTERNAL, DATA_LOSS | 500 |
| UNIMPLEMENTED | 501 | UNAVAILABLE (с Retry-After) | 503 |
| DEADLINE_EXCEEDED | 504 | — | — |
Не-POST — 405 (Allow: POST), тело не application/json — 415, тело больше предела — 413.
Аутентификация: apikey и session#
cluster shop {
auth { jwt(issuer: "https://id.example"); apikey; session(cookie: "sid") }
role all { serves Shop; expose 443 tls(...) }
}Порядок проверки: Authorization: Bearer (jwt) → ключ API → cookie сессии. Предъявленный и неверный документ — UNAUTHENTICATED; отсутствие всех — Anonymous.
apikey / apikey(header: "x-shop-key"). Ключ — в заголовке (по умолчанию x-api-key) или Authorization: ApiKey <ключ>. Сервер хранит только SHA-256 ключа в файле
(OXL_AUTH_APIKEYS или --auth-apikeys; без файла все ключи отвергаются):
{"keys": [{"id": "ci", "sha256": "<hex sha256 ключа>", "sub": "svc-ci", "roles": ["Admin"], "tenant": "acme", "mfa": false,
"claims": {"team": "x"}, "expires": 1893456000}]}Файл перечитывается при изменении (не чаще раза в секунду): ключи добавляются и отзываются без перезапуска; испорченный файл оставляет прежние ключи. Ключ даёт Principal.User(sub, roles, ...),
claims["apikey"] — id ключа. Новый ключ: python3 -c "import secrets,hashlib; k='oxl_'+secrets.token_urlsafe(32); print(k, hashlib.sha256(k.encode()).hexdigest())".
session(cookie: "sid"). Серверная сессия: std.rpc.createSession(user, roles, ttl, tenant?, mfa?) -> str | SessionError возвращает непрозрачный идентификатор (256 бит) — значение cookie
(Set-Cookie: sid=<id>; HttpOnly; Secure; SameSite=Lax; Path=/); endSession(id) завершает сессию. Сессии лежат в реплицируемом KV _oxl_sessions как SHA-256 идентификатора с
принципалом и сроком (до 30 суток). Отзыв виден на всех узлах не позже чем через секунду (кэш). Хранилище недоступно — UNAUTHENTICATED (закрыто по умолчанию).
Защита от CSRF — на приложении: шлюз принимает только application/json (требует preflight в браузере), cookie ставьте SameSite=Lax или строже.
Чтения с реплик (std.kv)#
let fast = kv.reading(.Follower) // Consistency: Linearizable (по умолчанию) | Follower | Stale
let v = fast.get("k")
let s = kv.reading(.Stale, Duration.ofSeconds(2)).scan("user/", 0)Linearizable— лидер шарда после ReadIndex: прежнее поведение.Follower— реплика этого узла (если она не лидер) спрашивает у лидера индекс чтения (лидер подтверждает лидерство и отвечает индексом применения), ждёт его у себя и читает свою копию. Остаётся линеаризуемым, читающий видит все записи, завершённые до чтения; лидер не отдаёт данные. Нет реплики на узле или узел — лидер: обычный путь.Stale(maxStale)— реплика без обращения к лидеру, пока слышала лидера не дольшеmaxStale(и группа не «спит»): ограниченная устарелость, монотонно в пределах процесса (не старше прочитанного им раньше), может не содержать записей последнихmaxStaleи записей самого процесса; иначе — обычный путь.- Записи и транзакции идут через лидера. Вне кластера режимы не меняют ничего.
replicaReads()— счётчик чтений, обслуженных репликой. - ORM-таблицы
builtin://читаются только на лидере (чтения с реплик для SQL — после 1.0).
Миграции builtin://#
oxl db diff не меняется. oxl db migrate|rollback|status --db builtin://имя[?shards=N] [--data-dir каталог] применяют migrations/NNNN_*.sql к файлам шардов остановленного узла
(имя.db или имя.shard<i>.db): ко всем шардам или ни к одному, в каждом своя таблица _oxl_migrations; шард без таблицы миграции (таблицы создаются по мере поступления строк) пропускается с сообщением;
расходящиеся шарды — ошибка. Уже работающий или кластерный (Raft-реплики) стор этой командой не меняется: остановите узлы либо выполните db.execute("ALTER ...") из программы — DDL реплицируется по шардам.
oxl gen --go и --kotlin#
См. interfaces.md. Клиенты Go и Kotlin принимают ключ API и токены как метаданные: oxlrt.WithMetadata("x-api-key", key), OxlChannel.forTarget(addr, metadata = mapOf("x-api-key" to key)).
Исходник: docs/gateway.md