oauth_payloads grows without bound — no sweep for expired rows #167
Labels
No labels
bug
duplicate
enhancement
help wanted
invalid
question
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
lz/agent-nexus#167
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Spun out of #151, flagged independently by two reviewers.
The gap
nexus/src/lib/server/oauth/adapter.tsfilters expiry at read time (revive()returnsundefinedfor a row pastexpires_at) and never deletes. Nothing sweeps the table. The only deletions arerevokeByGrantId(reachable only from a logout or revocation flow, andconfig.tsleaves bothfeatures.revocationandfeatures.introspectiondisabled) andoidc-provider's ownInteraction.destroy()on a successful resume.Why the existing precedent does not transfer
Read-time filtering is the established pattern here —
api_tokens(AGENTS.md fact #24) and the quota store (fact #19) both do it, and #151 followed them deliberately. But those tables hold one row per durable entity: a token the operator deliberately minted, a quota window that is overwritten.oauth_payloadsgains a new row per grant, per authorization code, per access token, per refresh token, and per interaction ever issued. WithrotateRefreshToken: true, a single long-lived MCP connection inserts a new row on every refresh and keeps the consumed predecessor. An interaction the operator abandons — closing the tab — leaks its row permanently, becausedestroy()only runs on the success path.So it grows from ordinary protocol traffic with no operator action, and there is no visibility: no count surfaced anywhere, no log line, no periodic job.
Shape of a fix
src/server.tsalready has the pattern for this, for the skills-installer staging sweep:A sweep deleting rows where
expires_at IS NOT NULL AND expires_at < now, run at boot and on an interval, would cover it. Two things to be careful of:consume()marks rather than deletes precisely so a replayed authorization code is distinguishable from an unknown one — deleting early would downgrade the provider's response from "revoke the whole grant" to a plain refusal.server.ts, which has no test coverage by design.Not urgent
No correctness impact, and the growth rate on a single-operator instance is slow. Filed so it is not rediscovered as a mystery when a long-lived instance's SQLite file becomes conspicuous.