sync: unlinking a device now revokes its token on the server (issue 2110)
CI & Build / Python lint (push) Successful in 2s
CI & Build / TypeScript typecheck (push) Successful in 6s
CI & Build / Python tests (push) Successful in 8s
CI & Build / Build & push image (push) Successful in 40s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 2m13s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 4m9s
Desktop (Tauri) / Update manifest (push) Successful in 4s

Unlink was local-only. It cleared the server URL, token and cursor from the
device, and left the bearer token valid on the server indefinitely — so someone
who unlinked because the laptop was being sold or handed on believed they had
revoked access when they hadn't.

The blocker was identification, not intent: a token pasted from the web app
never carried a device id, and /api/auth/me describes the user, not the device
row, so DELETE /devices/<id> could only ever have worked for one of the two ways
this app can be linked. DELETE /api/auth/devices/self keys off the token in the
Authorization header instead, which the caller always holds — one route that
works for both paths, owner-scoped like the rest, and no local schema change.

Unlinking is never blocked on the network. Wanting to stop syncing is a local
decision, so the revoke is attempted first, its outcome carried back, and the
link cleared either way. When the token survives — server unreachable, or older
than the route — the Sync screen says so in place, with where to revoke it. A
toast would have been the wrong shape for that: it disappears, and this is
exactly what someone returns to the screen to check.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-16 10:31:06 -04:00
co-authored by Claude Opus 5
parent edf52da97f
commit 2cfe049f9c
7 changed files with 237 additions and 12 deletions
+10
View File
@@ -89,6 +89,16 @@ shown once at creation.
device}`. They paste it into the native app.
- **Manage:** `GET /api/auth/devices` (list), `DELETE /api/auth/devices/<id>`
(revoke). A revoked token stops authenticating immediately.
- **Self-revoke:** `DELETE /api/auth/devices/self` retires the token presented in
the `Authorization` header. This is what a native client calls when the user
unlinks. It exists because a client can't use the id-keyed route: a token pasted
from the web app arrives without a device id, and `/api/auth/me` describes the
user, not the device row. A caller authenticated by session cookie gets `400`
it holds no device token, so there is nothing for it to mean.
Unlinking is never blocked on this call. If the server is unreachable or too old
to have the route, the client still unlinks locally and tells the user the token
is still live and where to revoke it.
Every authenticated request (sync or otherwise) accepts the bearer token in
place of the session cookie.