fix(desktop): restore native OAuth tokens after restart

This commit is contained in:
Doud-FR 2026-07-25 20:53:58 +02:00 committed by Austin Pickett
parent 539e9b5c1b
commit df1f825ce7
3 changed files with 56 additions and 2 deletions

View file

@ -145,6 +145,7 @@ import {
import {
nativeRefreshUrl,
type NativeTokenSet,
parseStoredTokenSet,
parseTokenResponse,
resolveLoginStrategy,
tokenNeedsRefresh
@ -6276,11 +6277,15 @@ function _loadNativeTokens(baseUrl: string): NativeTokenSet | null {
return null
}
const tokens = parseTokenResponse(JSON.parse(plaintext))
const tokens = parseStoredTokenSet(JSON.parse(plaintext))
_nativeTokens.set(baseUrl, tokens)
return tokens
} catch {
} catch (error) {
rememberLog(
`[native-oauth] failed to load stored tokens for ${baseUrl}: ${(error as Error).message}`
)
return null
}
}

View file

@ -20,6 +20,7 @@ import {
nativeRefreshUrl,
nativeTokenUrl,
parseLoopbackCallback,
parseStoredTokenSet,
parseTokenResponse,
resolveLoginStrategy,
statusSupportsNativeFlow,
@ -176,6 +177,29 @@ test('parseTokenResponse tolerates an absent refresh token / expiry', () => {
assert.equal(t.expiresAt, 0)
})
test('parseStoredTokenSet maps the encrypted on-disk camelCase shape', () => {
const t = parseStoredTokenSet({
accessToken: 'AT-stored',
refreshToken: 'RT-stored',
expiresAt: 1893456000,
provider: 'self-hosted',
userId: 'u-stored'
})
assert.equal(t.accessToken, 'AT-stored')
assert.equal(t.refreshToken, 'RT-stored')
assert.equal(t.expiresAt, 1893456000)
assert.equal(t.provider, 'self-hosted')
assert.equal(t.userId, 'u-stored')
})
test('parseStoredTokenSet rejects a non-normalized server response', () => {
assert.throws(
() => parseStoredTokenSet({ access_token: 'AT-server' }),
/missing accessToken/i
)
})
// --- refresh timing ---
test('tokenNeedsRefresh respects the skew window', () => {

View file

@ -193,6 +193,31 @@ export function parseTokenResponse(body: any): NativeTokenSet {
}
}
/**
* Validate a token set loaded from the encrypted local store.
*
* The stored representation is already normalized as NativeTokenSet and
* therefore uses camelCase. Gateway token responses use snake_case and
* remain handled separately by parseTokenResponse().
*/
export function parseStoredTokenSet(body: any): NativeTokenSet {
const accessToken = String(body?.accessToken || '')
if (!accessToken) {
throw new Error('Stored token set missing accessToken')
}
const expiresAt = Number(body?.expiresAt)
return {
accessToken,
refreshToken: String(body?.refreshToken || ''),
expiresAt: Number.isFinite(expiresAt) ? expiresAt : 0,
provider: String(body?.provider || ''),
userId: String(body?.userId || '')
}
}
/**
* True when a stored token set is at/near expiry and should be refreshed
* before use. `skewSeconds` refreshes slightly early to avoid a race where