Repository navigation
fix(club): keep club devices in their club past a day between refreshes - #145
Draft
ShuhaoZQGG wants to merge 1 commit into
Draft
ShuhaoZQGG wants to merge 1 commit into
ShuhaoZQGG wants to merge 1 commit into
Conversation
A club device refreshes at most once a day and its club token lives 24 h, so the token it sends to GET /api/club is nearly always already expired. The route answered 401, and the client treats 401 as "credential spent" and leaves the club. A club that meets weekly lost every device every week. GET /api/club now takes a correctly signed token up to 180 days past its exp (CLUB_TOKEN_REFRESH_GRACE_MS) and re-mints it. Nothing is minted until the club and device records are read, so a revoked or departed device still gets 403 and a deleted club 404. Every other club route keeps strict expiry, so a revoked device's residual reads stay bounded by 24 h. The route also answers 503 rather than 401 when SESSION_SIGNING_KEY is missing, so a misconfigured deploy cannot empty every club. The client still leaves on 401: after this change a 401 is a token the server will never renew, and holding it would keep Pro on with no refresh able to lapse it. Adds club-card-sets CC-10 (passed on the web device against dev 766d77cf; not yet run on the Zoom device, whose token is fresh) and covers worker/club.js and worker/club-token.js from that spec.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No issue. Found live in the pro-launch test run (
test-runs/2026-11-13-pro-launch/run.md, local).Why
A club device leaves its club the first time it opens the app more than 24 hours after its last club refresh. A club that meets weekly would lose its club on every device every week. That defeats the main point of "one Pro account for your whole club". The bug has been on master since 792efa1 (2026-09-27). Production has no clubs yet, so nobody is hurt today. It has to land before clubs exist.
Cause:
CLUB_TOKEN_TTL_MS). Only activate/create andGET /api/clubmint one.CLUB_REFRESH_INTERVAL_MS), so the token it sends is nearly always expired.GET /api/clubverified the token strictly and answered 401.refreshClubtreats 401 as "credential spent" and callsleaveClub().Seen live on dev:
GET /api/club 401thenPOST /api/club/leave 401at 02:25:09Z on 2026-11-14, 25 h after that Zoom device's last refresh.What changes
GET /api/clubaccepts a correctly signed token for up to 180 days past itsexpand re-mints it (CLUB_TOKEN_REFRESH_GRACE_MS). No other route does. This is safe on this route because it reads the club record and the device record before it mints anything, so:GET /api/clubanswers 503, not 401, whenSESSION_SIGNING_KEYis missing. Without the key every token reads as forged, and a 401 would make every device in every club leave.exp). Keeping it would leave the cached Pro plan on with no refresh ever able to lapse it. The comment inrefreshClubnow says this.verifyClubToken(token, secret, now, { graceMs })andreadClub(request, env, { graceMs, now }). Both default to strict.Security
GET /api/club. The signature check is unchanged, so a forged or foreign-key token is still refused inside the grace (unit-tested, and checked live: a tampered signature gets 401).Production impact
Testing
npm test: 116 files, 2288 tests passed.npm run test:e2e: 44 passed.npm run test:specs: 8 specs valid. The pre-push drift check passed.worker/club-token.test.js: grace accepts an expired token only when asked; refuses at the end of the window; still refuses forged and foreign-key tokens; nonsense grace values count as none.worker/club.test.js: a token a week old is renewed and re-minted with the right claims; 200 just inside the grace window and 401 at its edge; a revoked device gets 403, a deleted club 404, a departed device 403, all with an expired token; leave and the speech upload refuse the expired token; 503 without a signing key.packages/shared/__tests__/club.test.js: a device a week past its last refresh stays in the club on the renewed token; 401 and 404 still leave; a 503 keeps the club.worker/club.test.jstests fail. I checked this before committing.Evidence (real Zoom client)
Run log:
test-runs/2026-11-13-club-token-refresh-grace/run.md(local). This branch was deployed to dev as version766d77cf, withFLAGS_FORCE "1"set locally andENTITLEMENT_ENFORCE "1"as committed.lastRefreshAt25.35 h old. That is the state that made the Zoom device leave in the pro-launch run./timer/appsentGET /api/club, which answered 200. There was noPOST /api/club/leave.toastmaster_club.clubTokenwas replaced by a re-minted token for the same device (iat 02:53:09Z, exp +24 h).club-card-sets.mdnow alsocoversworker/club.jsandworker/club-token.js. No other spec covered them, and pro-club is not migrated yet.Not verified
status: staleuntil that run. To run it, keep dev on this fix and open the timer on A after that time.expis unit-tested only.Look here first
worker/club.jshandleClubState: the grace is applied here and nowhere else, and the 503 guard runs before it.worker/club-token.js:CLUB_TOKEN_REFRESH_GRACE_MSand theexp + gracecheck inverifyClubToken.packages/shared/club.jsrefreshClub: why the client still leaves on 401.Related, not fixed here:
drainOutboxruns at app start alongsiderefreshClub, so its first upload still carries the old, expired token.