Skip to content

Revoke the server session at logout and show synced home paths as ~ - #46

Merged
starkdmi merged 7 commits into
mainfrom
privacy-hardening
Oct 8, 2026
Merged

starkdmi merged 7 commits into
mainfrom
privacy-hardening

Conversation

@starkdmi

@starkdmi starkdmi commented Oct 8, 2026

Copy link
Copy Markdown
Owner

statsai auth logout now revokes the device session on the server before
clearing local credentials, by posting the stored refresh token to
/api/devices/logout. It holds the auth refresh lock from reading the
credentials through revocation and cleanup, so a concurrent sync cannot
rotate the token in between. If an earlier refresh was interrupted after
the server rotated the token, logout replays that rotation with its saved
rotationId and revokes the successor. When revocation fails, logout warns
that the session may still be active and how to revoke it, then clears
local credentials anyway. --local-only skips the server call.

Synced project path labels under the home directory are sent as ~/....
Only the sync payload changes: local storage, local display, path hashes
and every identity key stay the same. Summaries, sessions, task work items
and task spans are covered. Each acknowledged task bucket now records the
sanitizer version it was sent under (store migration 031), so the first
incremental sync after upgrading re-sends affected buckets once; deleted
buckets carry no labels and are not re-sent. Sync status and selection share
one predicate for pending buckets. Summaries and sessions re-send once on
their own because their payload hash changes.

The --include-projects, --include-sessions, --include-tasks and
--include-activity help text, the README and docs/sync-contract.md now
say what each flag sends.

statsai auth logout now posts the stored refresh token to
/api/devices/logout before clearing local credentials. The call is best
effort: a network error or non-2xx response prints a warning and local
credentials are still cleared. --local-only skips the server call.
Sync payloads now rewrite a project path_label under the home directory
to ~/... (the home directory itself to ~) for summaries, sessions, and
task work items and spans. Only the outgoing label changes: the local
store keeps the full path, path_hash and other identities are untouched,
and pending-sync selection hashes the same payload that is sent.
The --include-projects, --include-tasks, --include-sessions, and
--include-activity help now names the fields each one adds to hosted
sync. The README lists the same, and the sync contract no longer says
session rows carry no paths.
Task buckets are re-sent only when they change, so buckets a target
already held kept their full home paths after the sanitizer started
showing them as ~. Each acknowledged bucket now records the sanitizer
version it was sent under (schema 31), and incremental sync treats a
bucket acknowledged under an older version as pending. Bumping
TASK_BUCKET_SYNC_SANITIZER_VERSION re-sends them again for any future
sanitizer change. Summaries and sessions already re-send because their
payload hash changes; a test now covers that.
A sync refreshing at the same time could rotate the refresh token after
logout read it. The server answers 204 for an already-revoked token, so
logout reported success while the rotated session stayed active.
Logout now takes the same lock token refresh uses before reading
credentials and holds it through the server revocation and local
cleanup, including with --local-only.
After the schema 31 migration, an acknowledged deletion tombstone read
as sanitizer version 0. Sync status counted it as pending while sync
selection, which only re-sends deleted buckets when dirty, never chose
it, so task_bucket_backlog stayed above zero. A tombstone is an empty
snapshot and no sanitizer change affects it, so only local buckets are
re-sent for an older sanitizer version. Selection and status now share
task_bucket_needs_sync and the same tracked-bucket query.
If a refresh reached the server but its response was lost, the stored
refresh token is already rotated away and its successor is live. The
logout endpoint answers 204 for the old token without touching the
successor, so logout reported success while a session stayed active.
Under the refresh lock, logout now replays the pending rotation with
its rotationId, as the next refresh would, and revokes the successor.
If that exchange fails, logout warns that the session may still be
active and how to revoke it, then clears local credentials.
@starkdmi
starkdmi merged commit 34b8ac9 into main Oct 8, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant