Repository navigation
fix(club): keep club devices in their club past a day between refreshes - #145
ShuhaoZQGG wants to merge 4 commits 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.
|
zoom — Waiting, not run (daily run, 2026-11-14 22:05Z). The only Zoom-client step this change affects is club-card-sets CC-10 on A (Zoom desktop). It needs A's club token past its 24 h, and A last refreshed at 2026-11-14 02:28:59Z, so it qualifies only after 2026-11-15 02:29Z. The diff changes only GET /api/club (the refresh), so CC-01…CC-09 (verified on 905d8d1 on 11-13) aren't affected and weren't rerun. Opening A now could refresh it and push CC-10 back another day. Nothing was deployed and Zoom wasn't opened. |
…-grace # Conflicts: # e2e/zoom-client/INDEX.md
…nst the refresh grace The fix only rewrites the comment on refreshClub's 401/403/404 branch in packages/shared/club.js; neither the Pro gate nor the renewal reminder changes. The behaviour it does change is club-card-sets CC-10.
A (Zoom desktop) opened 25 h after its last club refresh with an expired token: GET /api/club 200, no leave, club badge and card set kept, and its next requests carried the re-minted token. Only CC-10 ran, so verified_at stays at the last full run.
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: 120 files, 2357 tests passed (on the branch merged with deve28d733).npm run test:e2e: 56 passed.npm run test:specs: 13 specs valid; the pre-push drift check passed.worker/club-token.test.js: the 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 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). New step club-card-sets CC-10 passed on both devices.5b9a1545=3262d37,FLAGS_FORCE "1"set locally,ENTITLEMENT_ENFORCE "1"as committed):GET /api/clubanswered 200, and there was noPOST /api/club/leave.766d77cf):GET /api/clubanswered 200 and B stayed in the club on a re-minted token.club-card-sets.mdnow alsocoversworker/club.jsandworker/club-token.js. No other spec covered them, and pro-club is not migrated yet.club-pro-gateandclub-renewal-reminderalso coverpackages/shared/club.js. This PR only rewrites a comment there; I reviewed both specs and noted it in them.e28d733(version15071666).Not verified
club-card-setsstaysstale, withverified_atat the last full run (905d8d1), because of dev's own feat(pro): pre-launch — announcement banner, locked Pro button, "coming soon" Sounds; renewal reminder only in the last 60 days #147 and club-pro-gate changes. Those are unrelated to this PR.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.