The :claim_check role's HTTP API: read-only byte transport for claim-check
refs. Deliberately its own listener (claim_check.port, default 4001),
separate from Ankusa.Edge.Router: the edge is internet-facing, this is
internal-network-only.
Every request but GET /health is authenticated (claim_check.auth, see
Ankusa.Admin.Auth) and authorized: 401 without a credential, 403
unless the caller holds claims:read and tenant:<tenant_id> (or
tenant:*) for the tenant in the path — or, with claim_check.authorize
set to {module, fun, args}, unless apply(module, fun, [context | args])
returns :ok, context being %{principal, tenant_id, claim_id, conn}
(the path's two segments as given, not yet validated). The hook replaces
the built-in check and runs before anything is read. The default
mode: :none is accepted on loopback only; anywhere else configure :jwt,
:mtls or :token, or say with allow_insecure: true that a mesh or proxy
in front of the port authenticates every caller. A shared cache must sit
behind that layer, never in front of it.
API
GET /v1/claims/:tenant_id/:claim_id— the claim's exact bytes,application/octet-stream,Cache-Control: private, max-age=31536000, immutable(claims are written once and never rewritten;privatebecause the path is a capability, and a shared cache would serve it to whoever asks),Accept-Ranges: bytes.400malformed,404no such claim,503 store_unavailable+Retry-After(a random 1–3 seconds, so clients a throttled store turned away don't all come back at once) when the store is unavailable, throttling, or slower than the deadline, or this node is shedding load,503 store_forbidden+Retry-After: 60when the store refused this node's credential. A 503 body never carries the store's error; it is logged.502 integrity_mismatchwhen the bytes read (or the pack's manifest) are not what the manifest records.- The same
GETwith oneRange: bytes=a-b,bytes=a-orbytes=-n—206with those bytes andContent-Range: bytes a-b/length(bclamped to the claim's end), read from the store as one ranged read; a range that starts past the end (or a zero-length claim, orbytes=-0) is416withContent-Range: bytes */length. Several ranges, another unit, a malformed spec, or anyIf-Rangeare ignored:200with the whole claim. HEAD /v1/claims/:tenant_id/:claim_id— the same status and headers,content-lengthincluded, without the body; it reads only the pack's index, never the claim or its manifest.Rangeis ignored.GET /health— liveness.
Signed refs
A v2 ref (Ankusa.ClaimCheck.Ref) is redeemed at the same path with
?exp=<exp_unix>&sig=<signature>. After authorization and before any read
the gateway holds it to Ankusa.ClaimCheck.verify_ref/2: 400 bad_signature
when no claim_check.ref_signing_keys entry signed this tenant, claim and
expiry (or exp/sig are missing one, or out of grammar), 410 ref_expired when it did and the expiry has passed, 400 unsigned_ref for a
path with neither while claim_check.accept_unsigned_refs is false. All
three are permanent. Past claim_check.max_concurrent_reads requests in
flight on this node a request is 503 overloaded + Retry-After (random
1–3 seconds) before anything is read.
Integrity
With claim_check.integrity_check: :manifest (the default) a whole-claim
GET is checked against the sha256 the pack's manifest records for that
claim (Ankusa.ClaimCheck.digest/3: read once per pack and cached with the
pack's index, shared by every concurrent request): the bytes that don't
match are 502 integrity_mismatch, never sent. A 200 or 206 carries the
digest as x-ankusa-sha256 (lowercase hex of the whole claim), so a client
reassembling ranges can check what it assembled; HEAD reads only the
index, so it sends none. :off serves the store's bytes as they are, with
no header. The reader still holds the bytes to the sha256 its queue message
carried (Ankusa.ClaimCheck.redeem/3 does this in-process): the manifest
is the store's own record of the bytes, the message's digest is the
writer's. The whole response is one binary, not a stream: a claim is at
most max_body_bytes.
Under load
Every claim request answers within claim_check.read_deadline_ms. Its
reads go through Ankusa.ClaimCheck.Cache (pack indexes and claims already
read) and Ankusa.ClaimCheck.Reader (coalescing, a per-store adaptive
limit on store requests, a bounded queue, and the deadline). Each authorized
request emits [:ankusa, :claim_check, :gateway] with its method, status and
duration; a 401 or 403 is refused before any of it and emits nothing.
Summary
Functions
Callback implementation for Plug.call/2.
Callback implementation for Plug.init/1.