Skip to content

id(web): registered identities are kept forever — no delete, expiry or cap at any layer, and the register handler is unthrottled #1766

Open

Description

@dezren39

Problem

Every POST /api/identity/register creates a client identity that is kept forever, everywhere it is stored: there is no delete, eviction, expiry, or cap for identities anywhere in the web identity store — and the registration handler has no rate limiting. Tokens expire after 30 days, but the identity record itself does not, and /api/identity/me refreshes a presented token indefinitely, so neither side of the store ever shrinks.

Evidence (developing-today/code @ 9874fe5, pkgs/id/src/web/identity.rs unless noted)

  • register (:180-214) persists the identity first (IdentityDb::create, :541), then inserts it into the in-memory clients map (:198-201) and creates a watch::channel inserted into name_watchers (:205-209). Nothing bounds the count.
  • IdentityDb offers only open / load / read_all / create / update (:483, :516, :529, :541, :556) — there is no delete operation at the storage layer, and IdentityStore has no removal method (grep for clients.remove / retain / any deregister path across pkgs/id/src returns nothing).
  • Token expiry is not identity expiry: decode_token rejects tokens older than TOKEN_MAX_AGE_SECS (30 days, :65, :379-383), but verify_and_refresh (used by GET /api/identity/me, routes.rs:2886) mints a fresh token on every visit, and the underlying ClientIdentity row is never aged out regardless.
  • Startup cost grows with the total ever registered: IdentityStore::new loads every record and builds both maps plus one watch channel per identity (:140-148).
  • The handler (web/routes.rs:2858-2881) calls register directly — no per-client rate limit, counter, or cap (compare SaveRateLimiter for file saves). Registration is reachable by any client that can reach the web UI: the default bind is loopback, but --bind to a non-loopback address is a supported, documented mode (cli.rs:333-339) where the only protection is the optional --web-token (absent it, serve.rs:1138 merely warns).

Verification (this run)

  • Grep audit (above): no deletion/eviction path exists at any layer — handler, store, or DB.
  • Laptop probe ~/git/fleet/probe-2026-10-28-laneA-identity-growth/ mirroring register's insert shapes (clients map + watcher slot per identity, sealed-record size as nonce + JSON plaintext + GCM tag): 100,000 registrations → both maps hold 100,000 entries and ≥13.8 MB of persisted sealed records, monotonically — nothing in the code can ever reduce any of the three.

Why it matters

A long-lived id serve accumulates one permanent identity per browser profile, device, test run, or script that ever registered — plus one encrypted on-disk record and a startup reload of all of them — with no operator-visible count, no cleanup verb, and no bound. On a non-loopback bind without a web token, a remote caller can grow the store and the on-disk .identity.redb at request speed. This is the same unbounded-growth family as #1732 (directory_auth codes), #1713 (SaveRateLimiter) and #1726 (outbox), in the one store that also persists to disk.

Acceptance criteria

  • Identities have a bounded lifecycle — triage pins the policy (e.g. expiry of identities whose tokens have all aged out, an operator delete verb, a registration cap, and/or rate limiting on the register handler) — and the pinned bound is implemented at the store and DB layers (a real IdentityDb delete).
  • Startup no longer retains records the pinned policy considers dead (they are pruned on load or by a sweep), and clients / name_watchers stay consistent with the DB after pruning.
  • The register handler's abuse surface is addressed per the pinned policy (rate limit and/or cap), without breaking first-visit registration for a legitimate new browser.
  • Regression tests: registering past the bound / past the expiry leaves the store at or under the bound, in memory and on disk; a still-active identity is never pruned.
  • Existing identity tests pass (laptop or CI).

Activity

  1. developing-today commented on Oct 28, 2026

    @developing-today
    Contributor
  2. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    task/triageQueue packet kind: verify, reproduce, and route this item
    source/creatorFiled by the new-issue-creator job
    priority/normalOrdinary priority (the default)
    queue/queuedHas a queued task in the Supabase agent queue
    app/idPackage/app: id (pkgs/ or apps/ in this repo)
    effort/sSmall: expected under 2 hours of work
    on Oct 28, 2026
  3. dezren39 commented on Oct 28, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

    • Agent: new-issue-creator job (new-issue-creator-code-a)
    • Session / ID: new-issue-creator-code-a run 2026-10-28 13:05 CDT
    • Found via: filed this run
    • Task: Filed after verifying on current main (9874fe5) that web identity registrations have no delete/expiry/cap at any layer (handler, IdentityStore, IdentityDb) and the register handler is unthrottled; laptop probe (probe-2026-10-28-laneA-identity-growth) quantified monotone growth. Triage packet enqueued to repo-health-code-pilot.
    • Expectation: effort/s — triage pins the lifecycle policy (expiry / delete verb / cap / rate limit), then the bounded fix + regression tests in the issue's acceptance criteria.
    • Links: evidence + probe details in the issue body
  4. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 1, 2026
  5. 204 remaining items

  6. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up (OpenChamber scheduled lane)

  7. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done (OpenChamber scheduled lane)

  8. dezren39 commented on Nov 8, 2026

    @dezren39
    MemberAuthor

    Partial verification — staying open (2026-11-07 done-task-only audit, tracking: #7016).

    Queue task(s) [1996, 4795] were marked done, and some work did land:

    No merged change closes this issue's full scope, so the done status is not treated as completion. Remainder re-queued as triage task 11576.

  9. dezren39 commented on Nov 8, 2026

    @dezren39
    MemberAuthor

    agent-note:v1
    event: picked-up
    task: 11963 (repo-health-code-pilot, OpenChamber scheduled lane)
    state: claimed
    verified: #1766 is OPEN. PR #4143 is MERGED (2026-11-02, merge 2b7b246); no open competing PR found. Issue has no held/user-owned/do-not-merge/do-not-auto-edit labels. Existing user/escalated label retained pending fresh triage of its reason.
    next: compare the original issue scope with merged implementation and current main; record the remaining actionable lifecycle/throttling work and enqueue a scoped fix packet, or close only if the full scope is already satisfied.

  10. added
    queue/blockedQueue or user escalation state
    status/blockedQueue or user escalation state
    and removed
    queue/claimedQueue worker lifecycle state
    on Nov 8, 2026
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)queue/blockedQueue or user escalation statesource/creatorFiled by the new-issue-creator jobstatus/blockedQueue or user escalation statetask/triageQueue packet kind: verify, reproduce, and route this itemuser/escalatedQueue or user escalation statework/assignedQueue worker lifecycle state

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions