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 anywhereclaim_id— a canonical (uppercase)Ankusa.ULID. Its first 48 bits are the pack's creation time and pick the pack'sdt=folder; its next 5 bits (the id's eleventh character) pick the pack'sshard=folder under the:shardedlayout (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 ofclaim_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.
Summary
Types
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.
Functions
The id of the claim at index in pack pack_id.
The fixed storage prefix every claim object lives under.
The storage key, under layout, of the pack this ref points into.
Split a claim id into its pack's id and its position in that pack.
A fresh pack id, stamped with the current time.
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.
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.
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 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.
The gateway path that serves this ref's bytes: with ?exp=<exp_unix>&sig=<signature>
when the ref is signed. Neither value needs encoding.
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.
ref as a v2 ref that expires at the Unix second expires_at, signed with key.
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.
The URN form: v2 when the ref is signed, else v1.
Is tenant_id a valid tenant name?
:ok if pack_id is a ULID whose position bits are all zero.
:ok if tenant_id is a valid tenant name.
Check a ref against the signing keys, current first. A ref signed with any of
them is :ok; now is a Unix second.
Types
@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.
@type t() :: %Ankusa.ClaimCheck.Ref{ claim_id: String.t(), expires_at: pos_integer() | nil, signature: String.t() | nil, tenant_id: String.t() }
Functions
@spec claim_id(String.t(), non_neg_integer()) :: String.t()
The id of the claim at index in pack pack_id.
@spec claims_prefix() :: String.t()
The fixed storage prefix every claim object lives under.
The storage key, under layout, of the pack this ref points into.
@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.
@spec new_pack_id() :: String.t()
A fresh pack id, stamped with the current time.
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>,cbeingshard/1of 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 abovedt=so a partition formed on a shard stays warm across days, and belowtenant=so one tenant is still one prefix to delete.
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.
@spec pack_id(non_neg_integer(), <<_::64>>) :: 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 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.
The gateway path that serves this ref's bytes: with ?exp=<exp_unix>&sig=<signature>
when the ref is signed. Neither value needs encoding.
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.
@spec sign(t(), binary(), pos_integer()) :: t()
ref as a v2 ref that expires at the Unix second expires_at, signed with key.
@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.
The URN form: v2 when the ref is signed, else v1.
Is tenant_id a valid tenant name?
@spec validate_pack_id(term()) :: :ok | {:error, :invalid_id}
:ok if pack_id is a ULID whose position bits are all zero.
@spec validate_tenant(term()) :: :ok | {:error, :invalid_tenant}
:ok if tenant_id is a valid tenant name.
@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, andnowis pastexpires_at