Code: locked in hosted/server/relay-auth.ts runs BEGIN, then pg_advisory_xact_lock. admit in hosted/server/relay-api.ts guards its insert with WHERE <expiry> > now(), and the enrollment poll's redeem UPDATE … WHERE "expiresAt" > now() runs inside locked too. In Postgres, now() is the transaction's start, which here is the BEGIN before the lock wait. The after() helper's comment in relay-api.ts already records this for TTL inserts and switched those to clock_timestamp(). The instant-expiry branch and the redeem did not get the same change.
Failure path (restored setup token): finish is refused and restoreSetupToken passes its JS check (expiresAt > Date.now()). admit then waits on setup_tokens:<burrowId> while another write holds the lock. The token's expiry passes during the wait. The statement's WHERE $expiry > now() still compares against the BEGIN time, so the dead token is reinserted. Because the trimmed CTE always runs, a Burrow at MAX_TOKENS_PER_BURROW also loses its oldest live token to make room for it. This breaks hosted.md's "Must restore a token … never once that expiry has passed" and the function's own "never evicts a live one". The window is only the length of a lock wait that straddles the expiry, and every spend rechecks liveness in its own statement, so the dead row itself is never accepted.
Failure path (poll): an approval that expires while its poll waits on burrows:<userId> is still redeemed. The impact is negligible, but this has the same cause.
Suggested fix: use clock_timestamp() instead of now() in every comparison that runs after pg_advisory_xact_lock, for both admit's instant-expiry WHERE and its pruned/trimmed CTEs, and for the poll's redeem UPDATE. Better still, give locked a single helper for "now, after the lock". To test it, hold the advisory lock from a second connection across a token's expiry, then assert that the restore inserts nothing and evicts nothing.
Found while trimming #904, which had parked this as a spec "Known gap".
🤖 Generated with Claude Code
Code:
lockedinhosted/server/relay-auth.tsrunsBEGIN, thenpg_advisory_xact_lock.admitinhosted/server/relay-api.tsguards its insert withWHERE <expiry> > now(), and the enrollment poll's redeemUPDATE … WHERE "expiresAt" > now()runs insidelockedtoo. In Postgres,now()is the transaction's start, which here is theBEGINbefore the lock wait. Theafter()helper's comment inrelay-api.tsalready records this for TTL inserts and switched those toclock_timestamp(). The instant-expiry branch and the redeem did not get the same change.Failure path (restored setup token):
finishis refused andrestoreSetupTokenpasses its JS check (expiresAt > Date.now()).admitthen waits onsetup_tokens:<burrowId>while another write holds the lock. The token's expiry passes during the wait. The statement'sWHERE $expiry > now()still compares against theBEGINtime, so the dead token is reinserted. Because thetrimmedCTE always runs, a Burrow atMAX_TOKENS_PER_BURROWalso loses its oldest live token to make room for it. This breaks hosted.md's "Must restore a token … never once that expiry has passed" and the function's own "never evicts a live one". The window is only the length of a lock wait that straddles the expiry, and every spend rechecks liveness in its own statement, so the dead row itself is never accepted.Failure path (poll): an approval that expires while its poll waits on
burrows:<userId>is still redeemed. The impact is negligible, but this has the same cause.Suggested fix: use
clock_timestamp()instead ofnow()in every comparison that runs afterpg_advisory_xact_lock, for bothadmit's instant-expiryWHEREand itspruned/trimmedCTEs, and for the poll's redeemUPDATE. Better still, givelockeda single helper for "now, after the lock". To test it, hold the advisory lock from a second connection across a token's expiry, then assert that the restore inserts nothing and evicts nothing.Found while trimming #904, which had parked this as a spec "Known gap".
🤖 Generated with Claude Code