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

Ecto schema for a single server-issued DPoP nonce (RFC 9449 §8).

Each row records one nonce, the instant it was issued, and the instant it
was consumed (`nil` while still unused). The single-use guarantee of
`Attesto.DPoP.NonceStore` is implemented at the storage layer by a
conditional update against `used_at`; this schema only describes the row
shape and does not embed any consumption policy.

## Columns

  * `nonce` - the opaque, unpredictable value returned to the client in the
    `DPoP-Nonce` response header (RFC 9449 §8.1). A unique index on this
    column is required so a nonce can be issued at most once.
  * `issued_at` - issuance instant. Combined with a caller-supplied TTL at
    consume time it defines the freshness window (RFC 9449 §8).
  * `expires_at` - precomputed expiry (`issued_at + ttl` at issuance) so a
    stateless freshness check has no TTL argument to supply.
  * `used_at` - consumption instant, or `nil` while unused. The transition
    from `nil` to non-`nil` happens exactly once.

# `t`

```elixir
@type t() :: %AttestoPhoenix.Schema.DPoPNonce{
  __meta__: term(),
  expires_at: DateTime.t() | nil,
  id: Ecto.UUID.t() | nil,
  issued_at: DateTime.t() | nil,
  nonce: String.t() | nil,
  used_at: DateTime.t() | nil
}
```

A persisted DPoP nonce row.

# `consume_changeset`

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

Changeset that marks an issued nonce as consumed at `used_at`.

Single-use acceptance (RFC 9449 §8): a nonce may be spent exactly once. The
caller performs the load-and-stamp atomically (a conditional `update_all`
guarded on `used_at IS NULL`) so two concurrent nodes cannot both observe the
same nonce as unused; this changeset only describes the field write.

# `issue_changeset`

```elixir
@spec issue_changeset(
  t()
  | %AttestoPhoenix.Schema.DPoPNonce{
      __meta__: term(),
      expires_at: term(),
      id: term(),
      issued_at: term(),
      nonce: term(),
      used_at: term()
    },
  map()
) :: Ecto.Changeset.t()
```

Changeset for inserting a freshly issued nonce.

Requires the opaque `:nonce` value and both bounding instants. A nonce with no
expiry would never fail closed, so a missing `:expires_at` (or `:issued_at`) is
a hard validation error rather than a silently issued unlimited nonce. The
`unique_constraint/3` on `:nonce` surfaces a duplicate issuance as a changeset
error rather than a raised exception, so a caller can treat a collision as a
generation retry.

---

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