Skip to content

v1.38.11 — sign out everywhere means everywhere - #937

Merged
MBombeck merged 2 commits into
mainfrom
release/v1.38.11
Sep 8, 2026
Merged

v1.38.11 — sign out everywhere means everywhere#937
MBombeck merged 2 commits into
mainfrom
release/v1.38.11

Conversation

@MBombeck

@MBombeck MBombeck commented Sep 8, 2026

Copy link
Copy Markdown
Owner

Three fixes on the paths that decide who is still signed in, and one README sentence that had outlived the code. Nothing on the wire changes; a device that has been signed out learns it at once instead of at expiry.

"Sign out everywhere" signs out the other phones, not the one pressing it. A native login is two rows, a refresh token and its paired access token, and the Bearer check consults only the second. The button revoked the refresh tokens alone, so every other device kept working until its access token ran out, up to a day on the default policy. A phone pressing the button also described itself as a browser session, so the exception meant to spare the caller matched nothing and the phone revoked its own refresh token. destroyOtherSessions now revokes the paired access tokens of every other device inside the same transaction and reports the count; the route names a Bearer caller by its access-token hash. Programmatic hlk_ tokens are untouched.

A refresh that overlaps a revocation loses. The consume step guarded on usedAt: null only, so a revocation landing between the read and the write was overtaken and a pair minted after the family ended stayed live. The guard now includes revokedAt: null; on a lost race the minted pair is retired, and the reason distinguishes revoked from already_used.

The idempotency cache is keyed by the authority a request carries. The grant was consulted at replay time and only for its existence, and the narrow-token check ran only on the delegated arm. Both move into the cell key: delegated cells carry the grant's id, level and scope fingerprint; a narrow token's cells carry the token; a request without a live grant neither reads nor claims a cell. Cookie and wildcard own-record cells are keyed byte-for-byte as before, which the existing unit tests pin.

Tests: two integration cases for the paired access tokens (cookie caller and Bearer caller), a deterministic revoke-mid-rotation race hooked between mint and consume against real Postgres, unit coverage for the new cell keys, and the structural guard moved to the new invariant. Full unit suite and full integration suite pass locally.

Version 1.38.11, changelog, OpenAPI document and service-worker anchor are in the release commit, along with the README correction on immunization records.

…, key the replay cache by authority

"Sign out everywhere" revoked every other refresh token and nothing else.
A native login is two rows, and the Bearer check consults only the
paired access token, so the other phones kept working until that token
expired, up to a day on the default policy. The route also handed
destroyOtherSessions a session-kind credential for a Bearer caller, whose
session.id is the ApiToken row id; the exception matched nothing and the
calling phone revoked its own refresh token. destroyOtherSessions now
finds the paired access tokens through the refresh rows and revokes them
inside the same transaction, reporting the count, and the route names a
Bearer caller by its access-token hash. Programmatic tokens are untouched.

rotateRefreshToken marked the consumed row under usedAt: null alone, so a
revocation landing between the read and the write was overtaken and a
pair minted after the family ended stayed live. The guard now includes
revokedAt: null; on a lost race the minted pair is retired and the reason
distinguishes a revoked family from a double consume.

withIdempotency consulted the grant only at replay time, and only for its
existence; a narrow-scoped token was checked only on the delegated arm.
Both move into the cell key: a delegated cell carries the grant's id,
level and scope fingerprint, a narrow token's cells carry the token, and
a request with no live grant neither reads nor claims a cell. Cookie and
wildcard own-record cells are keyed byte-for-byte as before.

Tests: integration for the paired access tokens (cookie and Bearer
caller), a deterministic revoke-mid-rotation race against Postgres, unit
coverage for the new cell keys, and the structural guard moved to the
new invariant.
Version anchors, the OpenAPI document, the changelog entry, and the
README sentence that still declared immunization records out of scope
while the vaccination record has shipped since v1.37.3.
@MBombeck
MBombeck merged commit c2bd573 into main Sep 8, 2026
24 checks passed
@MBombeck
MBombeck deleted the release/v1.38.11 branch September 8, 2026 19:26
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