OxL Reference

HTTP/JSON gateway, API keys, sessions, replica reads, builtin:// migrations

English translation of docs/gateway.md (the Russian text is the original). A guide to the "cheap" features of 1.0 (customer answer 1, session 6) [S §12.4, §12.8, §13.2, §16.3]; the decisions are D-8200..D-8249 in decisions.md.

The HTTP/JSON gateway of pub(api) services#

The same port as gRPC (expose): gRPC is told apart by content-type, the gateway by the prefix /api/. The listener is HTTP/2 (h2c or TLS); HTTP/1.1 is not supported on it.

oxl
POST /api/<Service>/<rpc>           Content-Type: application/json
{"id": 7}                           -> 200 {"result": {"id": "7", "name": "Hammer", ...}}
  • Unary rpcs of pub(api) services that are not @internal are served. <Service> is the short or the qualified name (shop.catalog.Catalog). Stream rpcs answer 501.
  • The body is a JSON object with the parameters (canonical protobuf JSON: lowerCamelCase or the OxL names; 64-bit integers are accepted as numbers or strings and written as strings). An empty body means no parameters. A parameter without a default is required: a missing one is 400 naming the field.
  • The response is the JSON of the response message; the value is in result (always, a zero value too: {"result": "0"}, false, "").
  • The call runs through the same handler as gRPC: authentication, policies, idempotency (idempotency-key), deadline (grpc-timeout), @ratelimit.
  • An error has the body {"code": 5, "status": "NOT_FOUND", "message": "...", "details": [{"@type": "type.googleapis.com/pkg.Missing", "value": {...}}]}; a typed error is in details as JSON.
gRPCHTTPgRPCHTTP
INVALID_ARGUMENT, FAILED_PRECONDITION, OUT_OF_RANGE400UNAUTHENTICATED401
PERMISSION_DENIED403NOT_FOUND / no such rpc404
ALREADY_EXISTS, ABORTED409RESOURCE_EXHAUSTED429
CANCELLED499UNKNOWN, INTERNAL, DATA_LOSS500
UNIMPLEMENTED501UNAVAILABLE (with Retry-After)503
DEADLINE_EXCEEDED504——

A method other than POST is 405 (Allow: POST), a body that is not application/json is 415, a body above the limit is 413.

Authentication: apikey and session#

oxl
cluster shop {
  auth { jwt(issuer: "https://id.example"); apikey; session(cookie: "sid") }
  role all { serves Shop; expose 443 tls(...) }
}

Order of checks: Authorization: Bearer (jwt), then API key, then session cookie. A credential that is presented and wrong is UNAUTHENTICATED; none at all is Anonymous.

apikey / apikey(header: "x-shop-key"). The key is in the header (default x-api-key) or in Authorization: ApiKey <key>. The server stores only the SHA-256 of a key, in a file (OXL_AUTH_APIKEYS or --auth-apikeys; without a file every key is rejected):

json
{"keys": [{"id": "ci", "sha256": "<hex sha256 of the key>", "sub": "svc-ci", "roles": ["Admin"], "tenant": "acme", "mfa": false,
           "claims": {"team": "x"}, "expires": 1893456000}]}

The file is re-read when it changes (at most once a second): keys are added and revoked without a restart; a broken file keeps the old keys. A key gives Principal.User(sub, roles, ...), claims["apikey"] is the key id. A new key: python3 -c "import secrets,hashlib; k='oxl_'+secrets.token_urlsafe(32); print(k, hashlib.sha256(k.encode()).hexdigest())".

session(cookie: "sid"). A server-side session: std.rpc.createSession(user, roles, ttl, tenant?, mfa?) -> str | SessionError returns an opaque id (256 bits) - the cookie value (Set-Cookie: sid=<id>; HttpOnly; Secure; SameSite=Lax; Path=/); endSession(id) ends it. Sessions live in the replicated KV store _oxl_sessions as the SHA-256 of the id with the principal and an expiry (up to 30 days). A revocation is seen on every node within a second (cache). When the store is unavailable the call is UNAUTHENTICATED (fail closed). CSRF is the application's business: the gateway takes only application/json (a preflight in browsers); set cookies SameSite=Lax or stricter.

Reads from replicas (std.kv)#

oxl
let fast = kv.reading(.Follower)            // Consistency: Linearizable (default) | Follower | Stale
let v = fast.get("k")
let s = kv.reading(.Stale, Duration.ofSeconds(2)).scan("user/", 0)
  • Linearizable - the shard leader after ReadIndex: the behaviour so far.
  • Follower - a replica of this node (if it is not the leader) asks the leader for a read index (the leader confirms its leadership and answers its applied index), waits for it locally and reads its own copy. Still linearizable: the read sees every write that completed before it; the leader does not serve the data. No replica on the node, or the node leads: the ordinary path.
  • Stale(maxStale) - a replica without asking the leader, while it heard from the leader within maxStale (and the group is not quiesced): bounded staleness, monotonic within the process (never older than what it read before), may miss the writes of the last maxStale and the process's own writes; otherwise the ordinary path.
  • Writes and transactions go through the leader. Outside a cluster the modes change nothing. replicaReads() counts the reads a replica served.
  • builtin:// ORM tables are read on the leader only (replica reads for SQL come after 1.0).

builtin:// migrations#

oxl db diff is unchanged. oxl db migrate|rollback|status --db builtin://name[?shards=N] [--data-dir dir] apply migrations/NNNN_*.sql to the shard files of a stopped node (name.db or name.shard<i>.db): to all shards or to none, each with its own _oxl_migrations table; a shard without the table of a migration (tables are created as rows arrive) is skipped with a message; shards that disagree are an error. A running or clustered (Raft replicas) store is not changed by this command: stop the nodes, or run db.execute("ALTER ...") from the program - DDL is replicated over the shards.

oxl gen --go and --kotlin#

See interfaces.md. The Go and Kotlin clients take API keys and tokens as metadata: oxlrt.WithMetadata("x-api-key", key), OxlChannel.forTarget(addr, metadata = mapOf("x-api-key" to key)).

Source: docs/gateway.en.md

OxL 1.0.0-beta · Documentation version 1.0.0-beta© OxwaldLink · generated from the Standard, the std sources and the guides