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

A claim-check reference: one string that names a claim's bytes.

    urn:ankusa:claim:v1:<tenant>:<claim_id>
    urn:ankusa:claim:v2:<tenant>:<claim_id>:<exp_unix>:<signature>

  * `tenant` — `[A-Za-z0-9_-]{1,64}`; the same string in the URN, the gateway
    path, and the storage key, with no encoding anywhere
  * `claim_id` — a canonical (uppercase) `Ankusa.ULID`. Its first 48 bits
    are the pack's creation time and pick the pack's `dt=` folder; its next
    5 bits (the id's eleventh character) pick the pack's `shard=` folder
    under the `:sharded` layout (`object_key/3`); its last 16 bits are the
    claim's position in the pack. The pack's own id is the claim id with
    those 16 bits zeroed, so the ref locates the pack without an index
    lookup.
  * `exp_unix` (v2) — the Unix second after which the gateway refuses the
    ref (`410`), decimal, no leading zero.
  * `signature` (v2) — the unpadded base64url (43 characters) HMAC-SHA256,
    under one of `claim_check.ref_signing_keys`, of the URN up to and
    excluding the last colon: `urn:ankusa:claim:v2:<tenant>:<claim_id>:<exp_unix>`.

A v1 ref is an unexpiring bearer string; v2 binds the tenant, the claim and
an expiry to a key only the operator holds, so a leaked queue message stops
granting reads when it expires and a guessed claim id grants nothing. This
module parses and verifies the grammar; `Ankusa.ClaimCheck.verify_ref/2`
applies the node's policy (which keys, whether a v1 ref is still accepted).

The digest isn't part of the ref: a queue message carries it next to the
ref, as `sha256`, and the reader checks it.

A ref maps to a gateway path by dropping the prefix:
`GET /v1/claims/<tenant>/<claim_id>` for v1, and the same path with
`?exp=<exp_unix>&sig=<signature>` for v2 — see `path/1`.

# `layout`

```elixir
@type layout() :: :flat | :sharded
```

How a pack's storage key is laid out: `:flat` is
`claims/tenant=<t>/dt=<d>/<pack>`, `:sharded` adds `shard=<c>/` between the
tenant and the date — see `object_key/3`.

# `t`

```elixir
@type t() :: %Ankusa.ClaimCheck.Ref{
  claim_id: String.t(),
  expires_at: pos_integer() | nil,
  signature: String.t() | nil,
  tenant_id: String.t()
}
```

# `claim_id`

```elixir
@spec claim_id(String.t(), non_neg_integer()) :: String.t()
```

The id of the claim at `index` in pack `pack_id`.

# `claims_prefix`

```elixir
@spec claims_prefix() :: String.t()
```

The fixed storage prefix every claim object lives under.

# `key`

```elixir
@spec key(t(), layout()) :: String.t()
```

The storage key, under `layout`, of the pack this ref points into.

# `locate`

```elixir
@spec locate(term()) :: {:ok, String.t(), non_neg_integer()} | {:error, :invalid_id}
```

Split a claim id into its pack's id and its position in that pack.

# `new_pack_id`

```elixir
@spec new_pack_id() :: String.t()
```

A fresh pack id, stamped with the current time.

# `object_key`

```elixir
@spec object_key(String.t(), String.t(), layout()) :: String.t()
```

The storage key for a pack: Hive-style partition folders, so Spark,
BigQuery, and Athena discover `tenant` and `dt` as columns. `dt` is the UTC
date of the pack id's timestamp, so the key is a pure function of the ref.
Only call this with a validated tenant and pack id.

  * `:flat` — `claims/tenant=<t>/dt=<yyyy-mm-dd>/<pack_id>`.
  * `:sharded` — `claims/tenant=<t>/shard=<c>/dt=<yyyy-mm-dd>/<pack_id>`,
    `c` being `shard/1` of the pack id: one of 32 prefixes per tenant, so a
    busy tenant's writes spread over 32 object-store partitions instead of
    landing on the tail of one. `shard=` sits above `dt=` so a partition
    formed on a shard stays warm across days, and below `tenant=` so one
    tenant is still one prefix to delete.

# `object_keys`

```elixir
@spec object_keys(String.t(), String.t(), layout()) :: [String.t()]
```

Both storage keys a pack may live under, `preferred`'s first: a reader tries
the configured layout, then the other one, so a pack written before the
layout changed is still found.

# `pack_id`

```elixir
@spec pack_id(non_neg_integer(), &lt;&lt;_::64&gt;&gt;) :: String.t()
```

The pack id for a Unix millisecond timestamp and 64 bits of entropy: the
timestamp, the entropy, and 16 zero bits that each claim's id fills with
its position in the pack.

# `parse`

```elixir
@spec parse(String.t()) :: {:ok, t()} | {:error, :invalid_ref}
```

Parse a URN. Rejects anything that isn't exactly the v1 or the v2 grammar.
The signature of a v2 ref is checked for shape only; `verify/3` checks it.

# `path`

```elixir
@spec path(t()) :: String.t()
```

The gateway path that serves this ref's bytes: with `?exp=<exp_unix>&sig=<signature>`
when the ref is signed. Neither value needs encoding.

# `shard`

```elixir
@spec shard(String.t()) :: String.t()
```

The pack's shard under the `:sharded` layout: the id's eleventh character,
i.e. the first five of its 64 random bits (the first ten characters are the
48-bit timestamp), one of the 32 Crockford characters. Every claim id of a
pack shares it, so a reader computes the key from a claim id as is.

# `sign`

```elixir
@spec sign(t(), binary(), pos_integer()) :: t()
```

`ref` as a v2 ref that expires at the Unix second `expires_at`, signed with `key`.

# `signed`

```elixir
@spec signed(term(), term(), term(), term()) ::
  {:ok, t()} | {:error, :invalid_tenant | :invalid_id | :bad_signature}
```

A v2 ref from its four parts as strings (the gateway's path segments and its
`exp` and `sig` query parameters), checked for shape only. `{:error,
:invalid_tenant | :invalid_id}` for a bad tenant or claim id, `{:error,
:bad_signature}` for an `exp` or `sig` that is not in the grammar.

# `to_string`

```elixir
@spec to_string(t()) :: String.t()
```

The URN form: v2 when the ref is signed, else v1.

# `valid_tenant?`

```elixir
@spec valid_tenant?(term()) :: boolean()
```

Is `tenant_id` a valid tenant name?

# `validate_pack_id`

```elixir
@spec validate_pack_id(term()) :: :ok | {:error, :invalid_id}
```

`:ok` if `pack_id` is a ULID whose position bits are all zero.

# `validate_tenant`

```elixir
@spec validate_tenant(term()) :: :ok | {:error, :invalid_tenant}
```

`:ok` if `tenant_id` is a valid tenant name.

# `verify`

```elixir
@spec verify(t(), [binary()], integer()) ::
  :ok | {:error, :unsigned_ref | :bad_signature | :ref_expired}
```

Check a ref against the signing keys, current first. A ref signed with any of
them is `:ok`; `now` is a Unix second.

  * `{:error, :unsigned_ref}` — a v1 ref: it carries nothing to check
  * `{:error, :bad_signature}` — no key produced this signature; checked
    before the expiry, so a forged expiry is never reported as a real one
  * `{:error, :ref_expired}` — signed, and `now` is past `expires_at`

---

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