OxL Справочник OxL
РусскийEnglish

Шлюз 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 на нём не поддерживается.

oxl
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.
gRPCHTTPgRPCHTTP
INVALID_ARGUMENT, FAILED_PRECONDITION, OUT_OF_RANGE400UNAUTHENTICATED401
PERMISSION_DENIED403NOT_FOUND / нет такого rpc404
ALREADY_EXISTS, ABORTED409RESOURCE_EXHAUSTED429
CANCELLED499UNKNOWN, INTERNAL, DATA_LOSS500
UNIMPLEMENTED501UNAVAILABLE (с Retry-After)503
DEADLINE_EXCEEDED504——

Не-POST — 405 (Allow: POST), тело не application/json — 415, тело больше предела — 413.

Аутентификация: apikey и session#

oxl
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; без файла все ключи отвергаются):

json
{"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)#

oxl
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

OxL 1.0.0-beta · Версия документации 1.0.0-beta© OxwaldLink · сгенерировано из Стандарта, исходников std и руководства