Record a failed refresh on the identity row so another process does not re-present the token #8

Merged
heimdall merged 8 commits from heimdall/failed-refresh-marker into main 2026-11-09 10:31:03 +00:00

2026-11-09

Heimdall
Tests: rotate fixtures carry consumedRefreshToken
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-09 10:27:35 +00:00
Heimdall
Blank the refresh token in the fallback write only when it is spent
A provider that keeps its refresh token valid (Twitch) keeps it when the
rotation cannot be committed: the marker is left, the token stays, and the
next caller after the backoff presents it. Blanking stays for a revoke and
for a provider that rotates (the rotate decision now carries
consumedRefreshToken). Live-Postgres test for Twitch written first and seen
failing; the contract states the rule inline.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-09 10:22:14 +00:00
Heimdall
Blank the consumed refresh token in the fallback marker write
When the rotation or revocation the provider's answer called for cannot be
committed, the fallback write now clears refresh_token_encrypted as well as
stamping the marker, so the end of the 60 s backoff cannot replay the consumed
token. Tests (live Postgres, trigger-refused revoke) assert no replayable token
and no second provider call. Docs: the marker is a best-effort write on a
second connection after rollback, the skew cap, the blanking write's residual.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-09 10:07:24 +00:00

2026-11-08

Heimdall
Leave the failed-refresh marker when the revoke a rotating provider's answer called for cannot be written
A rotating provider that answered without a new refresh token has consumed the old one; the revoke
that follows is rolled back if its UPDATE fails, and no marker remained. The fallback marker now
covers revoke as well as rotate. The residual-risk paragraph says the window opens when the request
is sent, cites the steward ruling, and ends its sentences.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-08 21:56:40 +00:00
Heimdall
Docs: name the process-death residual in the refresh contract
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-08 21:53:12 +00:00
Heimdall
Make the failed-refresh marker durable and stamp it with the failure time
The marker carried the time the attempt began, so a slow failure ate the backoff; it now carries the
time the provider's answer (or failure) was known. A throw after the provider answered (custody
encrypt) records the marker instead of rolling it back, and a rotation Postgres refuses to write
leaves the marker in a transaction of its own. A stamp from a writer's clock ahead of the reader's by
the backoff or more is treated as skew, not a fresh failure.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-08 21:48:13 +00:00
Heimdall
Count the new linked_identities column in the schema inventory test
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-08 21:40:39 +00:00
Heimdall
Record a failed refresh on the linked identity row so another process does not re-present the token
A failed or timed-out refresh attempt now writes refresh_failed_at under the row lock; a process
that takes the lock inside a 60 s backoff answers managed_credential_refresh_failed without calling
the provider. Rotation, revocation and re-link clear the marker. Closes the cross-process residual
that refresh-once-per-token left to a follow-up, and pins it on the two-pool rig.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-08 21:35:13 +00:00