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 theDPoP-Nonceresponse 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 + ttlat issuance) so a stateless freshness check has no TTL argument to supply.used_at- consumption instant, ornilwhile unused. The transition fromnilto non-nilhappens exactly once.
Summary
Functions
Changeset that marks an issued nonce as consumed at used_at.
Changeset for inserting a freshly issued nonce.
Types
@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.
Functions
@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.
@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.