# HTTP API Base path: `/v1`. Transport is JSON. Requests and responses are `transport.Envelope` objects (see `docs/PROTOCOL.md`). ## Authentication Read endpoints require a session token. Obtain one with the challenge/assert handshake (AuthAssertion bound to the server's `audience`): ``` POST /v1/auth/challenge -> { "challenge": "" } POST /v1/auth/assert (envelope) -> { "session_token", "identity", "scope" } ``` Send the token as `Authorization: Bearer ` or `?token=`. A session is valid for 30 minutes; each challenge is single-use and expires after 5 minutes. Scopes: `read:claims`, `read:requests`, `read:responses`, `read:revocations`, plus the generic `read` / `*`. Rate limit: `POST /v1/auth/challenge` is capped at **30/min per IP** (`429`). ## Endpoints | Method | Path | Auth | Query / body | Purpose | |--------|--------------------------|-------------|-----------------------------|------------------------------------------| | POST | `/v1/objects` | none | `Envelope` body | Store a signed object (claim/request/...) | | GET | `/v1/objects/{id}` | none | — | Fetch one object by content id | | GET | `/v1/objects?ids=a,b,c` | none | ≤100 ids | Batch fetch; missing ids are omitted | | GET | `/v1/claims` | session | `subject`, `limit`,`offset` | Claims about a subject | | GET | `/v1/requests` | session | `recipient`,`limit`,`offset`| Approval requests to a recipient | | GET | `/v1/responses` | session | `request`,`limit`,`offset` | Responses to a request | | GET | `/v1/revocations` | session | `claim`,`limit`,`offset` | Revocations targeting a claim | | GET | `/v1/config` | none | — | `{ "audience": "..." }` | | GET | `/v1/metrics` | none | — | Prometheus-style metrics | | GET | `/v1/ws` | session | WebSocket control frames | Live stream of newly stored envelopes | | GET | `/v1/healthz` | none | — | Liveness (`200`) | | GET | `/v1/readyz` | none | — | Readiness (`200` / `503`) | ## WebSocket streaming (`GET /v1/ws`) Authenticate like any read endpoint (bearer token; browsers may use `?token=`). The token's scopes gate channels exactly as the REST list endpoints do: `read:claims`, `read:requests`, `read:responses`, `read:revocations`, or the generic `read`/`*`. After connecting, send JSON text frames: ```json { "op": "subscribe", "channel": "claims", "key": "" } { "op": "unsubscribe", "channel": "claims", "key": "..." } ``` Channels mirror the list endpoints: `claims` (by subject), `requests` (by recipient), `responses` (by request hash), `revocations` (by claim id). Every newly stored object on a subscribed channel arrives as: ```json { "event": "object", "channel": "...", "key": "...", "object_id": "...", "envelope": { "tce": "...", "signature": "..." } } ``` The envelope is raw and unverified by design: verify it locally exactly as if you had fetched it yourself. Limits: ≤16 subscriptions per connection; a client that cannot keep up is disconnected rather than allowed to stall broadcasts; the session's 30-minute lifetime applies mid-stream. ## Storing objects (`POST /v1/objects`) - The relay does **not** verify the signature (see `docs/TRUST-MODEL.md`); verification is the verifier's job. Any well-formed envelope is stored. - **Idempotent**: replaying the same object returns the same `object_id` with `200` and creates no duplicate. - **Body cap**: the request body is limited to `MaxClaimTCE*2 + 1024` bytes (~9 KiB). Larger bodies get `413`. - **Per-subject quota**: a subject may hold at most **1000** claims. The 1001st distinct claim returns `422` (`subject quota exceeded`). - **One response per request**: a second `ApprovalResponse` for an already answered request returns `422` (`request already answered`). - **Rate limit**: `60/min` per IP (`429`). Request / response shape: ``` POST { "tce": "", "signature": "" } 200 { "object_id": "" } 422 { "error": "server: subject ... quota exceeded" } 413 { "error": "payload too large" } 429 { "error": "rate limited" } ``` ## Listing endpoints (claims/requests/responses/revocations) All require a session with the matching `read:*` scope and return a JSON object with a single list key (`claims`, `requests`, `responses`, `revocations`). Each entry is a raw `Envelope` (`{ "tce": "", "signature": "" }`); decode the `TCE` to read the issuer/subject/fields. - `subject` / `recipient` / `request` / `claim` is required; omitting it is `400`. - Pagination: results are ordered lexicographically by content id. - `limit` (max items, `0` = no cap) and `offset` (skip) query params. Example: `?subject=trust1def&limit=10&offset=20`. - `after=` is the stable cursor: only ids greater than it are returned. Prefer it over `offset` — offset shifts when objects are inserted concurrently, a cursor does not. - Missing/invalid scope is `403`; missing token is `401`. ## Health & metrics - `GET /v1/healthz` → `200 { "status": "ok" }`. - `GET /v1/readyz` → `200 { "status": "ready" }`, or `503` if not ready. - `GET /v1/metrics` → `text/plain` Prometheus exposition with at least: `trust_objects_stored`, `trust_objects_rejected`, `trust_challenges_issued`, `trust_assertions_ok`, `trust_assertions_failed`, `trust_sessions_active`, `trust_ws_connections`, `trust_ws_messages_sent`, and one `trust_objects_stored_` counter per object type. ## Status code summary | Code | Meaning | |------|-------------------------------------------| | 200 | OK / stored / fetched | | 400 | Malformed request (bad envelope, missing param) | | 401 | Missing/invalid auth token or assertion | | 403 | Token present but insufficient scope | | 404 | Unknown object id | | 413 | Request body exceeds the size cap | | 422 | Well-formed but rejected (quota / already answered) | | 429 | Rate limit exceeded | | 503 | Server not ready | ## Configuration The server is configured from a YAML file (default `config.yaml`), with command-line flags overriding individual keys. A missing `config.yaml` is not an error: the server runs with built-in defaults. Copy `config.yaml.example` to `get started`. ```yaml listen_addr: ":8080" # HTTP listen address audience: "trust.n1ko.dev" # audience bound into auth assertions data_dir: "" # "" = in-memory; a path persists objects to disk log_level: "info" # debug | info | warn | error put_limit: 60 # POST /v1/objects per window, per IP put_window: "1m" challenge_limit: 30 # POST /v1/auth/challenge per window, per IP challenge_window: "1m" max_per_subject: 1000 # claim cap per subject address challenge_ttl: "5m" # auth challenge validity session_ttl: "30m" # verified session validity max_body_bytes: 9216 # hard request-body cap ``` Flags: `-config ` (default `config.yaml`), `-addr`, `-audience`, `-data` (any of these overrides the file).