From 1b39d93a8a672eef394903b992e897aa0e774308 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sun, 25 Oct 2026 15:21:10 +0100 Subject: [PATCH] docs(auth): say that credentials from before a deletion answer 401 The deletion revokes every session, refresh token and API token, so they answer like any revoked credential; only new sign-ins get the 403 auth.account.deletion_pending, through the shared login response. --- docs/api/openapi.yaml | 6 ++++-- src/lib/account-deletion/pending.ts | 9 +++++---- src/lib/openapi/routes/auth.ts | 2 +- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/docs/api/openapi.yaml b/docs/api/openapi.yaml index 1c6adb12f..7f5523d68 100644 --- a/docs/api/openapi.yaml +++ b/docs/api/openapi.yaml @@ -2211,8 +2211,10 @@ paths: Also 403 `auth.account.deletion_pending`, after the password was proved and before a second factor is asked for: the account is pending deletion and locked. `meta.purgeAt` is when it is erased; an admin can undo the - deletion until then. Every other sign-in surface (passkey, second-factor completion, refresh, the native - token exchanges) refuses with the same code. + deletion until then. Every other sign-in surface (passkey, second-factor completion, the native token + exchanges) refuses with the same code. A session, refresh token or API token issued before the deletion is + revoked by it, so those answer 401 (`auth.refresh.revoked` on refresh), the same as for a signed-out + session. content: application/json: schema: diff --git a/src/lib/account-deletion/pending.ts b/src/lib/account-deletion/pending.ts index bf3ae6528..3fd18dbb3 100644 --- a/src/lib/account-deletion/pending.ts +++ b/src/lib/account-deletion/pending.ts @@ -32,10 +32,11 @@ export async function pendingDeletionOf( * * Call it only after the credential was proved, never before: the refusal * says the account exists and is pending, which is not something to tell a - * caller who has not shown they could sign in to it. Every primitive that - * mints a sign-in (`createSession`, `issueApiToken`) calls it too, so a - * surface that forgets is still refused; the explicit calls in the sign-in - * routes are there to refuse before a second factor is asked for. + * caller who has not shown they could sign in to it. Every sign-in route ends + * in the shared login response (`src/lib/auth/login-response.ts`), which + * refuses a pending account, and the second-factor challenge refuses before + * a code is asked for. Credentials issued before the deletion are not checked + * here: the deletion revokes them, so they answer 401 like any revoked one. */ export async function assertAccountNotPendingDeletion( userId: string, diff --git a/src/lib/openapi/routes/auth.ts b/src/lib/openapi/routes/auth.ts index 90e3f004d..78a4217fb 100644 --- a/src/lib/openapi/routes/auth.ts +++ b/src/lib/openapi/routes/auth.ts @@ -939,7 +939,7 @@ export const authPaths: NonNullable = { "403": { description: "Password login is disabled — the operator runs `OIDC_ONLY=true`. `meta.errorCode` = `oidc_only`; sign in through SSO instead. The one exception is a browser the SSO callback sent back with a pending link to confirm an existing account: it may sign in with that one account's password, which links the account. A sign-in to any other account on that browser is refused before its password is checked, so the answer is the same whether the password was right or wrong.\n\n" + - "Also 403 `auth.account.deletion_pending`, after the password was proved and before a second factor is asked for: the account is pending deletion and locked. `meta.purgeAt` is when it is erased; an admin can undo the deletion until then. Every other sign-in surface (passkey, second-factor completion, refresh, the native token exchanges) refuses with the same code.", + "Also 403 `auth.account.deletion_pending`, after the password was proved and before a second factor is asked for: the account is pending deletion and locked. `meta.purgeAt` is when it is erased; an admin can undo the deletion until then. Every other sign-in surface (passkey, second-factor completion, the native token exchanges) refuses with the same code. A session, refresh token or API token issued before the deletion is revoked by it, so those answer 401 (`auth.refresh.revoked` on refresh), the same as for a signed-out session.", content: { "application/json": { schema: errorEnvelope } }, }, ...stdResponses,