Skip to content

id(directory_auth): pending email codes are never evicted — the codes map keeps every expired code whose address never comes back #1732

Closed

Description

@dezren39

Problem

DirectoryAuth's pending email-code map grows forever: an entry is only ever removed when that same address comes back to check_code (or re-issues after the resend minute). A code that is issued and never checked — the normal case for a mistyped address, an abandoned sign-in, or a bot probing many addresses — stays in the map for the life of the process, long past its 15-minute expiry.

Evidence (verified this run on main 8c8d1bbb, line trace)

  • pkgs/id/src/directory_auth.rs:112 — codes: HashMap<String, PendingCode>, keyed by address.
  • pkgs/id/src/directory_auth.rs:157-179 — issue_code inserts/overwrites one entry per address; it never sweeps expired entries for other addresses.
  • pkgs/id/src/directory_auth.rs:186-200 — check_code is the only place an entry is removed (on success, on expiry, or on exhausted attempts) — and only for the address being checked.
  • pkgs/id/src/directory_auth.rs:27 — CODE_TTL_MS = 15 * 60 * 1000: entries are dead after 15 minutes but are retained indefinitely.
  • The neighbouring maps in the same file DO sweep, so the omission is an asymmetry, not a design rule: sessions are swept in open_session (directory_auth.rs:118, retain(|_, record| record.expires_at > now)), and the signed-request replay guard sweeps in accept (directory_auth.rs:250, retain on SIGNED_WINDOW_MS).
  • Callers: pkgs/id/src/directory_view.rs:1219 (confirm) and :1244 (sign-in) call issue_code for an address supplied in the request, so distinct addresses accumulate entries at request rate; nothing else touches the map (grep over src/ for issue_code/check_code/.codes — only directory_auth.rs and those two call sites).

Why it matters

This is a long-lived server process (a hosted world). The map grows by one entry per distinct address that ever requests a code and does not complete the flow, with no bound and no eviction — the same unbounded-growth class as #1713 (SaveRateLimiter) and #1730 (WaitingPool arrived set), but in the auth surface, where an unauthenticated caller controls the key (the email address). It also contradicts the module's own framing (directory_auth.rs:5-7): pending codes are supposed to live only until "its time is up".

Suggested direction (for triage to pin)

Mirror the session sweep: retain only unexpired entries when issue_code runs (it already receives now), and/or sweep at the top of check_code. Keep behaviour otherwise identical: the resend-minute rule, the 5-attempt cap, and the digest-only storage all stay.

Acceptance criteria

  • After a code's 15-minute expiry, its entry is evicted without that address ever calling check_code (unit test: issue for address A, advance the clock past expiry, issue/check for address B, assert A's entry is gone — e.g. via the map's observable size or a re-issue behaving as a fresh entry).
  • Unexpired codes still verify, the 60-second resend refusal still applies, and the 5-attempt exhaustion still removes the entry.
  • Issue/check behaviour for live codes is unchanged; existing directory_auth tests still pass (laptop or CI).

Dedup

Searched open + closed issues and code search for DirectoryAuth, issue_code, and pending-code/expiry terms before filing: no filing covers this map. #1713/#1726/#1730 are the same unbounded-growth class in other components (web save limiter, outbox, waiting pool), not this one.

Activity

  1. developing-today commented on Oct 28, 2026

    @developing-today
    Contributor
  2. dezren39 commented on Oct 28, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

    • Agent: new-issue-creator-code-a
    • Session / ID: scheduled creator run 2026-10-28 11:39 CDT
    • Found via: filed this run
    • Task: Pending email-code map in DirectoryAuth never evicts expired entries for addresses that never return; verified by line trace on main 8c8d1bb (directory_auth.rs:112,157-200; sweep asymmetry vs sessions :118 and replay guard :250).
    • Links: queue packet enqueued this run (repo-health-code-pilot)
  3. added
    queue/queuedHas a queued task in the Supabase agent queue
    source/creatorFiled by the new-issue-creator job
    task/triageQueue packet kind: verify, reproduce, and route this item
    priority/normalOrdinary priority (the default)
    app/idPackage/app: id (pkgs/ or apps/ in this repo)
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    effort/sSmall: expected under 2 hours of work
    on Oct 28, 2026
  4. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed on Nov 1, 2026
  5. 5 remaining items

  6. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done / handed off

    • Agent: openchamber-scheduled-lane
    • Session / ID: triage queue task 1957
    • Outcome: Triage complete; implementation queued as task 4789. The issue remains open pending the fix.
  7. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  8. added a commit that references this issue on Nov 1, 2026
  9. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done

    Outcome (task 4789): DirectoryAuth::issue_code now sweeps expired pending codes first (retain(expires_at > now), mirroring the open_session session sweep). Unexpired codes — including attempt-exhausted ones — are retained, so resend throttling and the 5-attempt limit are unchanged; the sweep's opportunistic nature and O(n) retain cost are documented on issue_code per the triage's guidance (no idle cleanup or hard bound claimed). Two new tests pin the TTL boundary (kept at TTL−1ms, evicted at exactly TTL) and exhausted-attempt retention.

    Verification: cargo test --lib directory_auth — 15 passed, 0 failed, on a combined tree (main + the #3751 compile repair + this change). The first run surfaced a pre-existing pkgs/id compile break on main from PR #3738, filed as issue #3751; an independent lane's PR #3754 landed the repair on main in parallel, and the duplicate repair PR #3757 was closed after a salvage diff found nothing unique.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

app/idPackage/app: id (pkgs/ or apps/ in this repo)effort/sSmall: expected under 2 hours of workkind/bugSomething isn't working (prefixed form of the classic bug label)priority/normalOrdinary priority (the default)source/creatorFiled by the new-issue-creator jobstatus:donetask/triageQueue packet kind: verify, reproduce, and route this item

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions