# `AttestoPhoenix.Store.EctoConsentGrantStore`
[🔗](https://github.com/XukuLLC/attesto_phoenix/blob/v0.19.0/lib/attesto_phoenix/store/ecto_consent_grant_store.ex#L1)

Postgres-backed `AttestoPhoenix.ConsentGrantStore` (RFC 6749 §4.1.1).

A consent grant ties one consent decision to the exact authorization request
the resource owner saw, so a single Authorize click cannot approve a different
client, redirect URI, scope set, or PKCE challenge. The host consent screen
`mint/2`s a grant when the user authorizes; the host `:consent` callback
`consume/2`s it before a code is issued. The single-use guarantee is enforced
here, at the database, not advisory: it must hold against two concurrent
presentations of one token, on any node sharing the database.

## Behaviour callbacks

  * `mint/2` inserts a grant keyed on an unguessable token, with the canonical
    `binding_hash` and a TTL-derived `expires_at`, and returns the token.
  * `consume/2` issues one conditional `UPDATE ... WHERE token AND binding_hash
    AND consumed_at IS NULL AND expires_at > now` that stamps `consumed_at`.
    Postgres serialises the update on the row, so exactly one of any number of
    concurrent callers observes an affected-row count of 1 and gets `:ok`. A
    count of 0 is disambiguated into a precise `{:error, reason}` by reading
    the row back — fail closed: every reason still refuses consent.

The repository module is supplied by the host application (`:repo` under the
`:attesto_phoenix` app) and read at call time; a store with no backing
repository cannot enforce single use, so it fails closed rather than silently
no-opping.

# `consume`

```elixir
@spec consume(String.t() | nil, AttestoPhoenix.ConsentGrant.binding()) ::
  :ok | {:error, AttestoPhoenix.ConsentGrantStore.consume_error()}
```

Atomically consumes the grant for `token` iff it matches `binding`, is
unconsumed, and is unexpired.

Returns `:ok` to the single winning caller and `{:error, reason}` to every
other (`:not_found`, `:binding_mismatch`, `:expired`, `:consumed`). A `nil` or
blank token short-circuits to `{:error, :not_found}` without touching the
database.

# `mint`

```elixir
@spec mint(AttestoPhoenix.ConsentGrant.binding(), pos_integer()) ::
  {:ok, String.t()} | {:error, Ecto.Changeset.t()}
```

Mints a single-use consent grant bound to `binding`, valid for `ttl_seconds`.

Returns `{:ok, token}` with the opaque token the consent screen carries
forward to the authorization endpoint, or `{:error, changeset}` if the row
could not be persisted (e.g. an astronomically unlikely token collision).

---

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