niko_trust/docs/API.md
Niko Marmeladkov 79df689f7a refactor: drop PoW, checkpoint chain, gossip, light node and BFT; keep WS
The decentralization stack overcomplicated the project. Removed:
internal/pow, internal/smt, internal/checkpoint, internal/bft,
internal/lightnode, cmd/lightnode, relay gossip/checkpoint/proof/BFT
endpoints, their docs, vectors and the blake3 dependency.

Kept: WebSocket streaming on the relay, the full protocol v1 object set
including DelegationClaim (0x07) and KeyRotation request/confirm
(0x08/0x09) with chain resolution in verify.Graph, TrustedIssuers,
batch fetch, stable cursor pagination, per-type metrics.

INV-1 reverts to its original form: the relay holds no keys again.
Everything removed remains reachable at commit 20cc52c.
2026-08-26 00:49:25 +03:00

160 lines
7.5 KiB
Markdown

# 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": "<hex>" }
POST /v1/auth/assert (envelope) -> { "session_token", "identity", "scope" }
```
Send the token as `Authorization: Bearer <token>` or `?token=<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": "<subject address>" }
{ "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": "<base64>", "signature": "<base64>" }
200 { "object_id": "<content-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": "<base64>", "signature": "<base64>" }`);
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=<object_id>` 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_<type>` 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 <path>` (default `config.yaml`), `-addr`, `-audience`, `-data`
(any of these overrides the file).