fix(auth): Convex auth readiness + JWT audience fallback for Clerk (#2638)

* fix(auth): wait for Convex auth readiness, handle missing JWT aud claim

Root cause of "Authentication required" Convex errors and "Failed to
load notification settings": ConvexClient sends mutations before
WebSocket auth handshake completes, and validateBearerToken rejects
standard Clerk session tokens that lack an `aud` claim.

Fixes:
- convex-client.ts: expose waitForConvexAuth() using setAuth's onChange
  callback, so callers can wait for the server to confirm auth
- App.ts: claimSubscription now awaits waitForConvexAuth before mutation
- checkout.ts: createCheckout awaits waitForConvexAuth before action
- auth-session.ts: try jwtVerify with audience first (convex template),
  fall back without audience (standard Clerk session tokens have no aud)
- DeckGLMap.ts: guard this.maplibreMap.style null before getLayer
  (Sentry WORLDMONITOR-JW, iOS Safari background tab kill)

* fix(notifications): log error instead of silently swallowing it

The catch block discarded the error entirely (no console, no Sentry).
401s and other failures were invisible. Now logs the actual error.

* fix(auth): correct jose aud error check, guard checkout auth timeout

P1: jose error message is '"aud" claim check failed', not 'audience'.
The fallback for standard Clerk tokens was never triggering.

P2: waitForConvexAuth result was ignored in startCheckout. Now falls
back to pricing page on auth timeout instead of sending unauthenticated.

* fix(auth): reset authReadyPromise on sign-out so re-auth waits properly

* fix(auth): only fallback for missing aud, not wrong aud; reset auth on sign-out; guard checkout timeout

- auth-session.ts: check 'missing required "aud"' not just '"aud"' so
  tokens with wrong audience are still rejected (fixes test)
- convex-client.ts: reset authReadyPromise on sign-out (onChange false)
- checkout.ts: bail on waitForConvexAuth timeout instead of proceeding

* fix(sentry): filter Clerk removeChild DOM reconciliation noise (JV)

Clerk SDK's internal Preact renderer throws 'The node to be removed is
not a child of this node' with zero frames from our code. 9 events
across 8 users, all inside clerk-*.js bundle. Cannot fix without SDK
update.

* fix(checkout): skip auth wait for signed-out users to avoid 10s stall

waitForConvexAuth blocks until timeout for unauthenticated users since
the promise can never resolve. Now only waits when getCurrentClerkUser()
returns a signed-in user. Signed-out upgrade clicks fall through
immediately to the pricing page.

* test(auth): add test for missing-aud JWT fallback (standard Clerk tokens)

Covers the case where a Clerk standard session token (no aud claim)
is accepted by the audience fallback path, while tokens with a wrong
aud are still rejected.
This commit is contained in:
Elie Habib
2026-04-03 08:43:45 +04:00
committed by GitHub
parent 50626a40c7
commit 6517af5314
8 changed files with 93 additions and 25 deletions

View File

@@ -215,6 +215,20 @@ describe('validateBearerToken (with JWKS)', () => {
assert.equal(result.valid, false);
});
it('accepts a standard Clerk token with no aud claim (fallback path)', async () => {
const token = await new SignJWT({ sub: 'user_noaud', plan: 'pro' })
.setProtectedHeader({ alg: 'RS256', kid: 'test-key-1' })
.setIssuer(`http://127.0.0.1:${jwksPort}`)
.setSubject('user_noaud')
.setIssuedAt()
.setExpirationTime('1h')
.sign(privateKey);
const result = await validateBearerToken(token);
assert.equal(result.valid, true, 'standard Clerk tokens without aud should be accepted');
assert.equal(result.userId, 'user_noaud');
});
it('rejects a token with wrong issuer', async () => {
const token = await new SignJWT({ sub: 'user_wrongiss', plan: 'pro' })
.setProtectedHeader({ alg: 'RS256', kid: 'test-key-1' })