From f9af701bb9eaa634ecc4c9c51c0620034cd35449 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Sat, 26 Sep 2026 16:17:29 +0200 Subject: [PATCH] fix: keep the Lite session when the token endpoint is only rate limiting Bulwark Lite deleted the remembered refresh token on any non-5xx error from `/auth/token`, so a 429 or 408 during a busy moment signed the user out for good. Like the server build, it now clears the token only when the server refuses it (400, 401, 403) and treats everything else as an outage. --- lib/__tests__/lite-tokens.test.ts | 7 +++++++ lib/auth/lite-tokens.ts | 7 ++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/__tests__/lite-tokens.test.ts b/lib/__tests__/lite-tokens.test.ts index 382842744..5913319f2 100644 --- a/lib/__tests__/lite-tokens.test.ts +++ b/lib/__tests__/lite-tokens.test.ts @@ -233,6 +233,13 @@ describe('liteRefreshTokens', () => { expect(readLiteRefreshToken(0)?.refreshToken).toBe('RT'); }); + it.each([429, 408])('keeps the token on a %i, which is an outage and not a sign-out', async (status) => { + saveLiteRefreshToken(0, { serverUrl: SERVER, username: 'a', refreshToken: 'RT' }, true); + fetchMock.mockResolvedValue(new Response('', { status })); + await expect(liteRefreshTokens(0)).rejects.toMatchObject({ code: 'token_exchange_failed', status }); + expect(readLiteRefreshToken(0)?.refreshToken).toBe('RT'); + }); + it('propagates network errors and keeps the token', async () => { saveLiteRefreshToken(0, { serverUrl: SERVER, username: 'a', refreshToken: 'RT' }, true); fetchMock.mockRejectedValue(new TypeError('offline')); diff --git a/lib/auth/lite-tokens.ts b/lib/auth/lite-tokens.ts index c31575ea3..b6610ba82 100644 --- a/lib/auth/lite-tokens.ts +++ b/lib/auth/lite-tokens.ts @@ -356,7 +356,12 @@ export async function liteRefreshTokens(slot: number, clientIdOverride?: string) }).toString(), }); - if (response.status >= 500) { + // Only a definitive refusal (400/401/403) ends the session, as in the + // server build. A rate limit (429), a request timeout (408) or a 5xx is an + // outage: the refresh token stays, so the session resumes once the server + // answers again. + const refused = response.status === 400 || response.status === 401 || response.status === 403; + if (!response.ok && !refused) { throw new LiteLoginError('token_exchange_failed', response.status); } const tokens = (await response.json().catch(() => ({}))) as TokenResponse;