match app-password errors on the typed lex error, not raw message equality
isErrorMaybeAppPasswordPermissions compared getErrorName against
'TokenInvalid', a code the PDS never sends - the string fallback was
doing all the work. The real wire shape is the GENERIC 'InvalidToken'
code (also used for malformed/expired tokens) with the app-password
specifics only in the message, so the typed path now matches code AND
message ('Bad token scope' / 'Bad token method').
Deactivated and DeactivateAccountDialog used raw switch(e.message)
equality; both now go through the helper. Tests updated to pin the
actual PDS wire shape (and reject plain malformed-token errors).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -854,9 +854,6 @@
|
|||||||
},
|
},
|
||||||
"typescript/no-misused-promises": {
|
"typescript/no-misused-promises": {
|
||||||
"count": 1
|
"count": 1
|
||||||
},
|
|
||||||
"typescript/no-unsafe-member-access": {
|
|
||||||
"count": 1
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"src/screens/E2E/SharedPreferencesTesterScreen.tsx": {
|
"src/screens/E2E/SharedPreferencesTesterScreen.tsx": {
|
||||||
@@ -1140,9 +1137,6 @@
|
|||||||
},
|
},
|
||||||
"typescript/no-misused-promises": {
|
"typescript/no-misused-promises": {
|
||||||
"count": 1
|
"count": 1
|
||||||
},
|
|
||||||
"typescript/no-unsafe-member-access": {
|
|
||||||
"count": 1
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"src/screens/Settings/components/DeleteAccountDialog.tsx": {
|
"src/screens/Settings/components/DeleteAccountDialog.tsx": {
|
||||||
|
|||||||
@@ -18,12 +18,13 @@ function lexError(
|
|||||||
status: number,
|
status: number,
|
||||||
error: string,
|
error: string,
|
||||||
headers?: Record<string, string>,
|
headers?: Record<string, string>,
|
||||||
|
message = `${error} message`,
|
||||||
) {
|
) {
|
||||||
const response = new Response(null, {status, headers})
|
const response = new Response(null, {status, headers})
|
||||||
const method = {} as Procedure | Query
|
const method = {} as Procedure | Query
|
||||||
return new XrpcResponseError(method, response, {
|
return new XrpcResponseError(method, response, {
|
||||||
encoding: 'application/json',
|
encoding: 'application/json',
|
||||||
body: {error, message: `${error} message`},
|
body: {error, message},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,13 +74,33 @@ describe('getErrorHeader', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('isErrorMaybeAppPasswordPermissions', () => {
|
describe('isErrorMaybeAppPasswordPermissions', () => {
|
||||||
it('matches a TokenInvalid error', () => {
|
/*
|
||||||
|
* The PDS wire shape: generic error code `InvalidToken`, with the
|
||||||
|
* app-password specifics only in the message ('Bad token scope' from
|
||||||
|
* auth-verifier, 'Bad token method' from pipethrough).
|
||||||
|
*/
|
||||||
|
it('matches the InvalidToken + bad-token-message wire shape', () => {
|
||||||
expect(
|
expect(
|
||||||
isErrorMaybeAppPasswordPermissions(lexError(400, 'TokenInvalid')),
|
isErrorMaybeAppPasswordPermissions(
|
||||||
|
lexError(400, 'InvalidToken', undefined, 'Bad token scope'),
|
||||||
|
),
|
||||||
|
).toBe(true)
|
||||||
|
expect(
|
||||||
|
isErrorMaybeAppPasswordPermissions(
|
||||||
|
lexError(400, 'InvalidToken', undefined, 'Bad token method'),
|
||||||
|
),
|
||||||
).toBe(true)
|
).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('still matches the string-based bad-token signals', () => {
|
it('does not match other InvalidToken errors (malformed/expired tokens)', () => {
|
||||||
|
expect(
|
||||||
|
isErrorMaybeAppPasswordPermissions(
|
||||||
|
lexError(400, 'InvalidToken', undefined, 'Malformed token'),
|
||||||
|
),
|
||||||
|
).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('still matches the string-based bad-token signals on plain errors', () => {
|
||||||
expect(
|
expect(
|
||||||
isErrorMaybeAppPasswordPermissions(new Error('Bad token scope')),
|
isErrorMaybeAppPasswordPermissions(new Error('Bad token scope')),
|
||||||
).toBe(true)
|
).toBe(true)
|
||||||
|
|||||||
@@ -67,9 +67,22 @@ export function isNetworkError(e: unknown) {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True when an error looks like an App Password hitting an endpoint it lacks
|
||||||
|
* permission for. The PDS reports this under the GENERIC `InvalidToken` error
|
||||||
|
* code (which also covers malformed/expired tokens), with the app-password
|
||||||
|
* specifics only in the message: 'Bad token scope' (auth-verifier) or 'Bad
|
||||||
|
* token method' (pipethrough). So the typed path matches the code AND the
|
||||||
|
* message. (The old check compared against 'TokenInvalid', which the PDS never
|
||||||
|
* sends - the string fallback was doing all the work.)
|
||||||
|
*/
|
||||||
export function isErrorMaybeAppPasswordPermissions(e: unknown) {
|
export function isErrorMaybeAppPasswordPermissions(e: unknown) {
|
||||||
if (isXrpcError(e) && getErrorName(e) === 'TokenInvalid') {
|
if (isXrpcError(e)) {
|
||||||
return true
|
return (
|
||||||
|
getErrorName(e) === 'InvalidToken' &&
|
||||||
|
(e.message.includes('Bad token scope') ||
|
||||||
|
e.message.includes('Bad token method'))
|
||||||
|
)
|
||||||
}
|
}
|
||||||
const str = String(e)
|
const str = String(e)
|
||||||
return str.includes('Bad token scope') || str.includes('Bad token method')
|
return str.includes('Bad token scope') || str.includes('Bad token method')
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {Trans} from '@lingui/react/macro'
|
|||||||
import {useQueryClient} from '@tanstack/react-query'
|
import {useQueryClient} from '@tanstack/react-query'
|
||||||
|
|
||||||
import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher'
|
import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher'
|
||||||
|
import {isErrorMaybeAppPasswordPermissions} from '#/lib/strings/errors'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {
|
import {
|
||||||
type SessionAccount,
|
type SessionAccount,
|
||||||
@@ -75,17 +76,14 @@ export function Deactivated() {
|
|||||||
await queryClient.resetQueries()
|
await queryClient.resetQueries()
|
||||||
await refreshSession()
|
await refreshSession()
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
switch (e.message) {
|
if (isErrorMaybeAppPasswordPermissions(e)) {
|
||||||
case 'Bad token scope':
|
setError(
|
||||||
setError(
|
_(
|
||||||
_(
|
msg`You're signed in with an App Password. Please sign in with your main password to continue deactivating your account.`,
|
||||||
msg`You're signed in with an App Password. Please sign in with your main password to continue deactivating your account.`,
|
),
|
||||||
),
|
)
|
||||||
)
|
} else {
|
||||||
break
|
setError(_(msg`Something went wrong, please try again`))
|
||||||
default:
|
|
||||||
setError(_(msg`Something went wrong, please try again`))
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.error(e, {
|
logger.error(e, {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {msg} from '@lingui/core/macro'
|
|||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {Trans} from '@lingui/react/macro'
|
import {Trans} from '@lingui/react/macro'
|
||||||
|
|
||||||
|
import {isErrorMaybeAppPasswordPermissions} from '#/lib/strings/errors'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {usePdsClient, useSessionApi} from '#/state/session'
|
import {usePdsClient, useSessionApi} from '#/state/session'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
@@ -48,17 +49,14 @@ function DeactivateAccountDialogInner({
|
|||||||
logoutCurrentAccount('Deactivated')
|
logoutCurrentAccount('Deactivated')
|
||||||
})
|
})
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
switch (e.message) {
|
if (isErrorMaybeAppPasswordPermissions(e)) {
|
||||||
case 'Bad token scope':
|
setError(
|
||||||
setError(
|
_(
|
||||||
_(
|
msg`You're signed in with an App Password. Please sign in with your main password to continue deactivating your account.`,
|
||||||
msg`You're signed in with an App Password. Please sign in with your main password to continue deactivating your account.`,
|
),
|
||||||
),
|
)
|
||||||
)
|
} else {
|
||||||
break
|
setError(_(msg`Something went wrong, please try again`))
|
||||||
default:
|
|
||||||
setError(_(msg`Something went wrong, please try again`))
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.error(e, {
|
logger.error(e, {
|
||||||
|
|||||||
Reference in New Issue
Block a user