Optional periodic housekeeping GenServer that deletes expired rows from the
Ecto-backed authorization-code, refresh-token, DPoP-nonce, and DPoP-replay
tables.
Each of these tables carries an expires_at column whose semantics are fixed
by the relevant RFC:
- authorization codes - RFC 6749 §4.1.2 ("The authorization code MUST expire shortly after it is issued") and §10.5 (codes are short-lived, single-use).
- refresh tokens - RFC 6749 §1.5 / §6 (refresh tokens MAY expire); the stored expiry bounds the credential's lifetime.
- server-issued DPoP nonces - RFC 9449 §8 / §9 (the
noncethe resource or authorization server requires the client to echo is time-bounded). - DPoP proof
jtireplay records - RFC 9449 §11.1 (ajtineed only be remembered for the proofiatacceptance window; past that window the record is dead weight).
Correctness vs. housekeeping
Sweeping is not required for correctness. Every store re-validates
expires_at against the current time on read, so an expired row that has not
yet been swept is never honored: an expired authorization code is rejected, an
expired nonce is rejected, and an expired replay record no longer blocks a
fresh jti. The sweeper exists only to bound table growth by reclaiming rows
that can no longer affect any decision. It is therefore safe to run on any
interval, or not at all.
This is generic TTL housekeeping: it issues a single
DELETE ... WHERE expires_at < $now per swept table and makes no assumption
about how, where, or by which process the host deploys it.
Comparison boundary (fail-closed)
Deletion uses a strict < comparison against a single DateTime captured
once per sweep (DateTime.utc_now/0) and reused across every table, so a
sweep applies one consistent boundary. A row whose expires_at equals "now"
is retained, never deleted, so the sweeper can only ever remove rows that the
stores themselves already treat as expired. The sweeper widens no acceptance
window.
Configuration
All policy is read from AttestoPhoenix.Config; nothing is hardcoded here.
:repo- theEcto.Repothe deletes run against (required byAttestoPhoenix.Config).:sweep_interval_ms- how often a sweep runs, in milliseconds. When this key is unset the sweeper MUST NOT be placed in the supervision tree;start_link/1raises rather than silently choosing an interval, so a missing interval is a configuration error, not a default.:table_prefix- optional Ecto schema/table prefix applied to every delete so a host that installed the generated tables under a non-default prefix sweeps the same tables it created.
The set of swept tables is fixed by the generated schema and is not
host-configurable: every Ecto-backed store the library generates carries an
expires_at column and is swept.
Summary
Functions
Starts the sweeper.
Runs a single sweep synchronously and returns the number of rows deleted per table. Test- and diagnostic-facing; the supervised process drives sweeps via the configured interval, not this call.
Functions
@spec start_link(keyword()) :: GenServer.on_start()
Starts the sweeper.
Requires a %AttestoPhoenix.Config{} under the :config key. The config's
:sweep_interval_ms MUST be a positive integer; a missing or non-positive
interval raises ArgumentError so a misconfigured host fails at boot instead
of starting a process that never sweeps.
@spec sweep_now(GenServer.server()) :: %{optional(String.t()) => non_neg_integer()}
Runs a single sweep synchronously and returns the number of rows deleted per table. Test- and diagnostic-facing; the supervised process drives sweeps via the configured interval, not this call.