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

Ecto schema for a single-use, request-bound consent grant (RFC 6749 §4.1.1).

Backs `AttestoPhoenix.Store.EctoConsentGrantStore`. The host consent screen
mints one row per Authorize click, keyed on an unguessable `token` and
carrying a `binding_hash` over the exact request the user saw (subject +
client_id + redirect_uri + the requested scope set + code_challenge +
code_challenge_method, built by `AttestoPhoenix.ConsentGrant`). The
authorization-server `:consent` callback recomputes the hash from the live
request, looks the row up by token, verifies the hash matches, and consumes it
(single use) before a code is issued.

## Columns

  * `token` - the opaque, unguessable grant token (the PRIMARY KEY), so the
    consume's conditional `UPDATE` and the disambiguation read both hit the
    primary key directly. Never the plaintext of any other credential; the
    token is the grant's only secret.
  * `binding_hash` - the canonical SHA-256 hash over the bound request fields
    (`AttestoPhoenix.ConsentGrant.binding_hash/1`). The consume matches on it,
    so a grant only ever approves the request it was minted for.
  * `subject` - the OIDC `sub` of the resource owner who consented
    (diagnostic; the subject is also folded into `binding_hash`).
  * `consumed_at` - NULL until the single-use claim stamps it; a non-NULL value
    means the grant was already spent.
  * `expires_at` - the grant's short TTL (RFC 6749 §4.1.2: codes — and the
    consent that precedes them — are short-lived). The consume rejects an
    expired row, so an unswept expired grant is never honored.

# `t`

```elixir
@type t() :: %AttestoPhoenix.Schema.ConsentGrant{
  __meta__: term(),
  binding_hash: String.t() | nil,
  consumed_at: DateTime.t() | nil,
  expires_at: DateTime.t() | nil,
  inserted_at: DateTime.t() | nil,
  subject: String.t() | nil,
  token: String.t() | nil,
  updated_at: DateTime.t() | nil
}
```

A persisted consent-grant row.

# `changeset`

```elixir
@spec changeset(
  t()
  | %AttestoPhoenix.Schema.ConsentGrant{
      __meta__: term(),
      binding_hash: term(),
      consumed_at: term(),
      expires_at: term(),
      inserted_at: term(),
      subject: term(),
      token: term(),
      updated_at: term()
    },
  map()
) :: Ecto.Changeset.t()
```

Changeset for storing a freshly minted consent grant.

Requires the `token`, the `binding_hash`, the `subject`, and the `expires_at`.
A grant with no expiry would never fail closed, so a missing `:expires_at` is a
hard validation error rather than a silently unlimited grant. The
`unique_constraint/3` on the primary key surfaces a duplicate `token` as a
changeset error rather than a raised exception.

---

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