# `AttestoPhoenix.Schema.RefreshToken`
[🔗](https://github.com/XukuLLC/attesto_phoenix/blob/v0.19.0/lib/attesto_phoenix/schema/refresh_token.ex#L1)

Ecto schema for the refresh-token records that back an Ecto-backed
`Attesto.RefreshStore`.

Refresh tokens are rotated single-use credentials (RFC 6749 §6, §10.4;
OAuth 2.0 Security BCP §4.13). Presenting a token consumes it and mints a
successor in the same *family*; re-presenting an already-consumed token is
the captured-token signal that revokes the whole family. Only the hash of
each token is persisted, never the plaintext, so a leaked store yields no
usable credentials.

## Columns

  * `:token_hash` - `Attesto.Secret.hash/1` of the token. The lookup key;
    a unique index enforces one row per token.
  * `:family_id` - groups every token descended from one authorization
    grant. Revoked together on reuse detection.
  * `:generation` - rotation generation within the family (`0` for the
    first token).
  * `:client_id` - the OAuth client the token was issued to (RFC 6749 §10.4
    requires rotation to be confined to the issuing client). `nil` for a
    token with no client binding.
  * `:subject` - the resource owner the token authorizes.
  * `:scope` - the granted scope as a list of strings (RFC 6749 §3.3); a
    successor's scope MUST be a subset of its predecessor's.
  * `:cnf` - the RFC 7800 confirmation claim binding the token to a proof of
    possession (e.g. `%{"jkt" => thumbprint}` for a DPoP key, RFC 9449;
    `%{"x5t#S256" => thumbprint}` for an mTLS certificate, RFC 8705). `nil`
    for a bearer token.
  * `:claims` - opaque issuer context round-tripped into the next access
    token. A map; never `nil`.
  * `:consumed` - whether the token has already been rotated. The atomic
    transition of this flag (see `claim_changeset/1`) is what makes reuse
    detection reliable.
  * `:consumed_at` - when the token was rotated, used for the short
    idempotency window on honest refresh retries.
  * `:successor` - encrypted already-minted successor returned during an
    idempotent retry. The plaintext successor token is never stored directly
    in the database.
  * `:family_revoked` - whether the token's family has been revoked. A
    revoked family fails closed: no row in it may be rotated, and no
    successor may be inserted into it (sticky revocation).
  * `:expires_at` - absolute expiry. A token at or past its expiry is
    refused without being consumed.
  * `:parent_hash` - the `:token_hash` of the predecessor that minted this
    token, or `nil` for the first token in a family. Diagnostic lineage; it
    is never used as a lookup key.
  * `:inserted_at` - issuance time, set on insert.

## Confirmation translation

`Attesto.RefreshToken` carries the proof-of-possession binding as a
`:dpop_jkt` thumbprint inside its opaque context map. This schema persists
the binding as a structured `:cnf` confirmation so the same column can hold
any RFC 7800 member. `from_store_record/2` folds a `:dpop_jkt` into a `cnf`,
and `to_store_record/1` unfolds it back, so the protocol layer continues to
speak `:dpop_jkt` while storage stays confirmation-shaped.

# `t`

```elixir
@type t() :: %AttestoPhoenix.Schema.RefreshToken{
  __meta__: term(),
  acr: term(),
  auth_time: term(),
  claims: term(),
  client_id: term(),
  cnf: term(),
  consumed: term(),
  consumed_at: term(),
  expires_at: term(),
  family_id: term(),
  family_revoked: term(),
  generation: term(),
  id: term(),
  inserted_at: term(),
  parent_hash: term(),
  resource: term(),
  scope: term(),
  subject: term(),
  successor: term(),
  token_hash: term()
}
```

# `claim_changeset`

```elixir
@spec claim_changeset(t(), DateTime.t()) :: Ecto.Changeset.t()
```

Changeset that atomically claims (consumes) an unconsumed token.

The atomic primitive on which reuse detection depends (see
`Attesto.RefreshStore`) is `UPDATE ... SET consumed = true WHERE
token_hash = $1 AND consumed = false`. An Ecto-backed store runs this
changeset inside such a guarded update so that two concurrent rotations
cannot both observe the token as unconsumed: exactly one update affects a
row, the other affects none and is reported as reuse.

# `from_store_record`

```elixir
@spec from_store_record(
  map(),
  keyword()
) :: map()
```

Build the insert attributes for a store record handed in by
`Attesto.RefreshToken`.

The protocol layer's record is `%{token_hash, family_id, generation, data,
expires_at, consumed}` where `data` is the opaque context
(`%{subject, scope, client_id, dpop_jkt, claims}`). This flattens `data`
into the schema's columns, translating `:dpop_jkt` into an RFC 7800 `:cnf`
confirmation, and renders `:expires_at` (unix seconds in the contract) as a
`DateTime`. `:parent_hash` is taken from `opts[:parent_hash]` when the store
threads predecessor lineage; the contract does not carry it.

# `insert_changeset`

```elixir
@spec insert_changeset(t(), map()) :: Ecto.Changeset.t()
```

Changeset for inserting a new (unconsumed) refresh-token record.

Validates the columns the store contract requires and enforces single-use
storage via the unique constraint on `:token_hash`. A new record is always
unconsumed and never starts revoked; passing either flag as true is refused
so an insert cannot smuggle a token into a consumed or revoked state.

# `to_store_record`

```elixir
@spec to_store_record(t()) :: map()
```

Render a persisted row back into the `Attesto.RefreshStore` record shape the
protocol layer expects.

Inverse of `from_store_record/2`: it rebuilds the opaque `:data` context
(unfolding the `:cnf` confirmation back into `:dpop_jkt`) and renders
`:expires_at` back to unix seconds. `:generation` is not stored as a column;
it is reported as `0` so the contract's record stays well-formed without the
schema asserting lineage it does not track.

---

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