Documentation
¶
Overview ¶
Package challenge owns single-use, model-bound recovery credentials. Typed password-reset and email-verification flows share one atomic persistence contract. No challenge produces an authentication proof.
Index ¶
- Constants
- func ValidateLifetime(lifetime time.Duration) error
- type Address
- type Backend
- type Binding
- type Config
- type Digest
- type EmailVerification
- type Flow
- func (f *Flow[M, K, P]) Consume(ctx context.Context, token Token[M, P], ...) (M, error)
- func (f *Flow[M, K, P]) Issue(ctx context.Context, reference model.Reference[M, K]) (Issued[M, P], error)
- func (f *Flow[M, K, P]) Prune(ctx context.Context, limit int) (uint64, error)
- func (f *Flow[M, K, P]) Revoke(ctx context.Context, reference model.Reference[M, K]) (bool, error)
- func (f *Flow[M, K, P]) Validate() error
- type Issued
- type Issuer
- type Kind
- type Model
- type PasswordReset
- type Purpose
- type Record
- type RequestCallbacks
- type Requests
- type Revision
- type RevisionOwner
- type Store
- type Token
- func (Token[M, P]) Format(s fmt.State, _ rune)
- func (Token[M, P]) JSONContract() contract.JSON[Token[M, P]]
- func (Token[M, P]) LogValue() slog.Value
- func (Token[M, P]) MarshalJSON() ([]byte, error)
- func (t Token[M, P]) Secret() secret.String
- func (t *Token[M, P]) UnmarshalJSON(data []byte) error
- func (t Token[M, P]) Validate() error
Constants ¶
const MaxLifetime = 7 * 24 * time.Hour
const MaxPrune = 128
Variables ¶
This section is empty.
Functions ¶
func ValidateLifetime ¶
Types ¶
type Address ¶
type Address struct {
Namespace keyspace.Namespace
Provider auth.ProviderName
Model string
Purpose Kind
}
Address is an infrastructure boundary. Models retain their concrete key type in public flows; only persistence serializes identity.
type Backend ¶
type Backend interface {
Issue(context.Context, Address, model.Identity, Digest, time.Duration, func(context.Context, *database.Tx) (Binding, error)) (Record, error)
Consume(context.Context, Address, Digest, func(context.Context, *database.Tx, Record) error) (value.Optional[Record], error)
Revoke(context.Context, Address, model.Identity) (bool, error)
Prune(context.Context, Address, int) (uint64, error)
}
Backend owns a read-committed transaction for each operation, without retries. Issue locks the stable purpose/subject row, then invokes prepare exactly once in that transaction before replacing the previous token. Consume finds the hash, acquires the same subject lock, rechecks the current hash/expiry, invokes apply once, then removes the token in that SAME transaction. Before-commit errors/panic/Goexit/cancellation roll back model changes and consumption together. After-commit failures preserve a Committed database outcome; they cannot undo persistence. Never retry based only on the presence of an error. Callbacks must use only the supplied transaction for writes; no external I/O, detached work, nested challenge operations or additional pool acquisitions. Lock order is challenge subject, then application model. Stable subject rows remain after consumption. An uncertain commit returns an error and no result.
type Binding ¶
type Binding struct {
// contains filtered or unexported fields
}
Binding fingerprints the exact account state relevant to a link. Password reset includes the current email AND stored hash; verification includes the current email. Recovery flows also include a persisted Revision, so restoring an old address cannot revive old links. Bind alone compares current values. Bindings are not passwords or standalone authenticators. Keep them private.
func BindRevision ¶
BindRevision binds current values AND their persisted history generation. A zero generation fails closed; consumers must initialize existing rows before enabling a recovery flow. The revision remains independent of link issuance.
func ParseBinding ¶
type Config ¶
func DefaultConfig ¶
type Digest ¶
type Digest struct {
// contains filtered or unexported fields
}
Digest is a nominal challenge hash, never interchangeable with access tokens.
func ParseDigest ¶
type EmailVerification ¶
type EmailVerification struct{}
type Flow ¶
type Flow[M model.Identifiable, K any, P Purpose] struct { // contains filtered or unexported fields }
func (*Flow[M, K, P]) Consume ¶
func (f *Flow[M, K, P]) Consume(ctx context.Context, token Token[M, P], apply func(context.Context, *database.Tx, M) (M, error)) (M, error)
Consume executes an identity-preserving model mutation inside the consumption transaction. Missing, expired, replaced and stale-state links return the same Unauthenticated error. No proof/session/token is issued by consuming a link.
func (*Flow[M, K, P]) Issue ¶
func (f *Flow[M, K, P]) Issue(ctx context.Context, reference model.Reference[M, K]) (Issued[M, P], error)
Issue replaces the previous link for this model/provider/purpose. The reference selects a model; it does not authenticate the caller. Public request handlers must throttle, use a uniform response and deliver only to the stored address.
type Issued ¶
Issued keeps delivery information private to ordinary formatting and JSON. Use the locked Subject snapshot's email when constructing a delivery message. No built-in email delivery is performed here, nor is publication crash durable.
type Issuer ¶
type Issuer[M model.Identifiable, K any, P Purpose] interface { Validate() error Issue(context.Context, model.Reference[M, K]) (Issued[M, P], error) }
Issuer is the model/purpose contract implemented by Flow, passwordreset.Reset and emailverification.Verification. A requester borrows the configured issuer.
type Model ¶
type Model[M model.Identifiable, K any] struct { Lock func(context.Context, *database.Tx, K) (value.Optional[M], error) Binding func(M) (Binding, error) // Eligible optionally replaces the provider's login eligibility for this // flow, for example so email verification works before a provider that // requires verified addresses admits the account. Identity checks remain. // False rejects like a stale link. Omit it to reuse provider eligibility. Eligible func(context.Context, M) (bool, error) }
Model owns only domain mapping. Lock must use a generated ForUpdate query on the supplied transaction and return an owned full model. Binding uses stored fields, never read getters or a stale model passed from an HTTP request.
type PasswordReset ¶
type PasswordReset struct{}
type Purpose ¶
type Purpose interface {
PasswordReset | EmailVerification
}
Purpose is closed to the supported recovery operations. It is preserved in token and issuer types; a verification token cannot reset a password.
type Record ¶
type RequestCallbacks ¶
type RequestCallbacks[M model.Identifiable, K, I any, P Purpose] struct { Lookup func(context.Context, I) (value.Optional[model.Reference[M, K]], error) Deliver func(context.Context, Issued[M, P]) error }
RequestCallbacks maps a canonical submitted identifier to a model reference and delivers ONLY to the committed issuance's stored subject address. Lookup is domain-specific; it grants no authority. Issue locks and checks the current model again. Deliver runs after commit, outside database locks, at most once. Both run in the requester's owned background dispatch, not in the caller's request. Deliver's result is not crash durable: an email/outbox adapter owns durable dispatch (for example enqueue a job carrying the issued link).
type Requests ¶
type Requests[M model.Identifiable, K, I any, P Purpose] struct { // contains filtered or unexported fields }
Requests owns public recovery-request orchestration and bounds all callbacks. It returns no token/model/existence flag. Request performs only the identifier's rate-limit admission synchronously, identical for existing and missing accounts, then hands lookup, issuance and delivery to a bounded background dispatch owned by the requester, so response latency does not depend on whether the account exists. Operational failures are logged safely. Close drains the owned dispatches at shutdown.
func NewRequests ¶
func (*Requests[M, K, I, P]) Close ¶
Close stops accepting requests and waits for owned dispatches. When ctx ends first, remaining dispatches are canceled and Close returns ctx.Err(); a callback that ignores cancellation keeps its slot until it actually exits. It is idempotent. Register it with the application's shutdown hooks.
func (*Requests[M, K, I, P]) Request ¶
Request uses the same canonical identifier for quota and lookup. Validate and normalize it before this call. Quota denial is also an acknowledgement, without lookup or token replacement. Rate-authority failures, dispatch capacity (fault.Overloaded after a short bounded wait) and a closed requester are reported before any lookup, so they never depend on account existence. Ingress/IP limiting is separately required at the transport. There are no implicit retries, automatic login or plaintext recovery result. A lost/failed delivery can leave an undisclosed active link.
type Revision ¶
type Revision[M any] = model.ID[RevisionOwner[M]]
Revision is a persisted recovery-state generation, using the existing typed UUID codec. It is not a secret, account ID or authentication capability. Keep it on the model and replace it transactionally when protected state changes; never restore an earlier revision when restoring an earlier field value.
func NewRevision ¶
type RevisionOwner ¶
type RevisionOwner[M any] struct { // contains filtered or unexported fields }
RevisionOwner keeps a recovery-state generation separate from a model's primary key, while retaining its model owner. It is a type marker, not a stored model.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store borrows its backend and bounds callback capacity across every flow.
type Token ¶
Token retains its model and recovery purpose. Parsing validates syntax only; storage and current model state decide whether it may be consumed. Ordinary JSON/logging redacts it. Secret is the explicit delivery/transport boundary.
func (Token[M, P]) JSONContract ¶
JSONContract lets generated request DTOs retain both model and purpose. Its wire value is a string, with native parsing and redacted output. A contract's model/purpose name describes the Go declaration, not data claimed by a client.
func (Token[M, P]) MarshalJSON ¶
func (*Token[M, P]) UnmarshalJSON ¶
UnmarshalJSON accepts only a canonical opaque challenge. It resets a previous value on failure, never exposes input in errors, and does not establish model existence, purpose, validity or authority. Consumption checks those in storage.