* fix(mcp): grace window + idempotent refresh-token replay for OAuth (#710)
OAuth refresh rotated BOTH the refresh token and the access key in one
zero-grace CAS. Claude Code's MCP OAuth client fails to persist the rotated
refresh token (or fires concurrent refreshes), re-presents the stale one, the
CAS matches 0 rows, and the grant dies with invalid_grant — forcing a full
re-authorization roughly every 60s in a loop. Regression from #392.
Keep rotation (RFC 9700 §4.14.2 requires it for public clients) but add a
bounded grace window with idempotent replay, atomic in one SECURITY DEFINER RPC:
- Migration adds previous_key_hash / previous_refresh_token_hash (+ *_expires_at)
shadow columns. validate_and_increment_api_key accepts the current OR an
unexpired previous key_hash, with the rate-limit increment keyed off the
resolved row id.
- New rotate_mcp_refresh_token RPC: rotated | replayed | reuse_revoked |
revoked | invalid. In-grace replay re-issues a fresh pair and slides the
window so an actively-refreshing client that cannot persist the rotated token
keeps working; reuse after the window revokes the grant family (RFC 9700
4.14.2 reuse detection preserved).
- The refresh grant now calls the one RPC, closing the old SELECT-then-CAS
TOCTOU gap.
All previous_* columns default NULL, so existing keys are unaffected and the
RPC return shape is unchanged (callers untouched).
Tests: rewired the token-route unit tests to the RPC and replaced the test that
codified the bug with a #710 regression (in-grace replay returns 200, not 400);
added tests/pg/mcp-oauth-rotation-grace.pg.test.ts (grace accept/expire,
revoke-never-graced, rotate->demote, idempotent replay, reuse-after-grace->revoke).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* ci: retrigger checks for #714
No code change — re-running CI. The Supabase Preview check fails on a
pre-existing main-branch migration-history drift ("Remote migration versions
not found in local migrations directory"), not this PR; pg-real (full migration
replay) passes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>