# `Ankusa.ClaimCheck.Router`
[🔗](https://github.com/jamescarr/ankusa/blob/ankusa-v0.7.0/packages/ankusa/lib/ankusa/claim_check/router.ex#L1)

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; `private`
    because the path is a capability, and a shared cache would serve it to
    whoever asks), `Accept-Ranges: bytes`. `400` malformed, `404` no 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: 60`
    when the store refused this node's credential. A 503 body never carries
    the store's error; it is logged. `502 integrity_mismatch` when the bytes
    read (or the pack's manifest) are not what the manifest records.
  * The same `GET` with one `Range: bytes=a-b`, `bytes=a-` or `bytes=-n` —
    `206` with those bytes and `Content-Range: bytes a-b/length` (`b`
    clamped 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, or `bytes=-0`)
    is `416` with `Content-Range: bytes */length`. Several ranges, another
    unit, a malformed spec, or any `If-Range` are ignored: `200` with the
    whole claim.
  * `HEAD /v1/claims/:tenant_id/:claim_id` — the same status and headers,
    `content-length` included, without the body; it reads only the pack's
    index, never the claim or its manifest. `Range` is 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.

# `call`

# `init`

---

*Consult [api-reference.md](api-reference.md) for complete listing*
