- BLAKE3 keyed proof-of-work on object storage and auth challenges, with frozen vectors cross-checked against an independent Python reference implementing the single-block hash it needs. - Sparse Merkle trie over object IDs: order-independent roots, inclusion and absence proofs (internal/smt). - Signed checkpoint chain per relay: transport key amendment to INV-1, /v1/checkpoint/* and inclusion/absence proof endpoints, restart-safe epoch continuity (internal/checkpoint). - Head gossip with TOFU pinning and equivocation detection; light node (cmd/lightnode) that stores no history: quorum of pinned relays, every served object proven against the agreed root, LRU disk cache. - WebSocket streaming on relay and light node (coder/websocket): scoped channels mirroring REST, raw envelopes verified client-side; light node marks streamed objects unproven until checkpoint coverage. - Protocol v1 additions: DelegationClaim tag 0x07 with deterministic chain resolution in verify.Graph, KeyRotationRequest/Confirm tags 0x08/0x09 with hash-bound two-sided consent and Policy.RotationMaxAge; spec sections, frozen vectors appended byte-identically, Python reference extended. - Optional permissioned BFT finality over gossip (internal/bft): prevote/precommit with quorum certificates verifiable offline. - Quick wins: Policy.TrustedIssuers, per-type stored metrics, batch fetch, lexicographic lists with stable cursor pagination. - Security review of the network layer (docs/SECURITY-REVIEW.md) with findings F-01..F-09; hub send/close race and unstable pagination fixed under review. 12 packages green, vet/gofmt clean, protocol fuzzing stable.
148 lines
7.8 KiB
Markdown
148 lines
7.8 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/pow/challenge {"purpose":"auth"} -> { "key", "difficulty", "ttl" }
|
|
POST /v1/auth/challenge ({"pow":{...}}) -> { "challenge": "<hex>" }
|
|
POST /v1/auth/assert (envelope) -> { "session_token", "identity", "scope" }
|
|
```
|
|
|
|
When `-pow-auth-bits` is enabled, `/v1/auth/challenge` requires a solved
|
|
challenge first; see [POW.md](POW.md).
|
|
|
|
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` + optional `pow` | 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 |
|
|
| POST | `/v1/pow/challenge` | none | `{"purpose":"put"\|"auth"}` | Issue a single-use PoW challenge key |
|
|
| 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": "...", "relay_pubkey": "..." }` |
|
|
| GET | `/v1/metrics` | none | — | Prometheus-style metrics |
|
|
| GET | `/v1/checkpoint/latest` | none | — | Newest signed object-set commitment |
|
|
| GET | `/v1/checkpoint/{epoch}` | none | — | A specific historical head |
|
|
| GET | `/v1/proof/object/{id}` | none | — | Inclusion proof against the current root |
|
|
| GET | `/v1/proof/absent/{id}` | none | — | Absence proof against the current root |
|
|
| POST | `/v1/gossip/checkpoint` | none | peer head announcement | Record another relay's signed head |
|
|
| GET | `/v1/peers/heads` | none | — | Observed peer heads |
|
|
| 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.
|
|
|
|
Checkpoint and proof endpoints are specified in
|
|
[CHECKPOINT.md](CHECKPOINT.md); the proof-of-work scheme in [POW.md](POW.md).
|
|
|
|
## 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>",
|
|
"pow": { "key": "<hex>", "counter": <uint> } }
|
|
200 { "object_id": "<content-id>" }
|
|
422 { "error": "server: subject ... quota exceeded" }
|
|
413 { "error": "payload too large" }
|
|
429 { "error": "proof of work required" | "invalid proof of work" | "rate limited" }
|
|
```
|
|
|
|
The `pow` field is required when `-pow-put-bits` > 0 (the default); see
|
|
[POW.md](POW.md) for the scheme.
|
|
|
|
## 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_pow_ok`, `trust_pow_failed`.
|
|
|
|
## 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 |
|