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

Optional periodic housekeeping `GenServer` that deletes expired rows from the
Ecto-backed authorization-code, refresh-token, DPoP-nonce, DPoP-replay,
pushed-authorization-request, client-id-metadata-cache, and consent-grant
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 `nonce` the resource or
    authorization server requires the client to echo is time-bounded).
  * DPoP proof `jti` replay records - RFC 9449 §11.1 (a `jti` need only be
    remembered for the proof `iat` acceptance window; past that window the
    record is dead weight).
  * pushed authorization requests - RFC 9126 §2.2 (a `request_uri` reference is
    short-lived; past its expiry it can resolve nothing).
  * cached Client ID Metadata Documents -
    `draft-ietf-oauth-client-id-metadata-document-01` §6 / RFC 9111 (a cached
    document is fresh only until its `expires_at`; past that it is re-fetched).
  * consent grants - RFC 6749 §4.1.1 / §4.1.2 (consent precedes a short-lived
    authorization code; a grant past its `expires_at` can authorize nothing,
    and `consume/2` already rejects it on read).
  * back-channel-logout sessions - OpenID Connect Back-Channel Logout 1.0 (a
    recorded `(session, RP)` delivery row past its `expires_at` belongs to an
    abandoned session and is no longer a logout target).

## 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` - the `Ecto.Repo` the deletes run against (required by
    `AttestoPhoenix.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/1` raises 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.

# `start_link`

```elixir
@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.

# `sweep_now`

```elixir
@spec sweep_now(GenServer.server()) :: %{optional(String.t()) =&gt; 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.

---

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