- 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.
7.8 KiB
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.
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:
{ "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:
{ "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; the proof-of-work scheme in 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_idwith200and creates no duplicate. - Body cap: the request body is limited to
MaxClaimTCE*2 + 1024bytes (~9 KiB). Larger bodies get413. - 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
ApprovalResponsefor an already answered request returns422(request already answered). - Rate limit:
60/minper 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 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/claimis required; omitting it is400.- Pagination: results are ordered lexicographically by content id.
limit(max items,0= no cap) andoffset(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 overoffset— offset shifts when objects are inserted concurrently, a cursor does not.
- Missing/invalid scope is
403; missing token is401.
Health & metrics
GET /v1/healthz→200 { "status": "ok" }.GET /v1/readyz→200 { "status": "ready" }, or503if not ready.GET /v1/metrics→text/plainPrometheus 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 |