Summary
- Adds POST /xrpc/com.atproto.server.requestPasswordReset — accepts an email, generates a 1-hour single-use reset token (stored as SHA-256 hash, never plaintext), and always returns 200 regardless of whether the email exists to prevent account enumeration. Email delivery is stubbed as tracing::info! for v0.1.
- Adds POST /xrpc/com.atproto.server.resetPassword — validates the token, hashes the new password with argon2id, and atomically marks the token used and updates accounts.password_hash. Returns 401 InvalidToken for unknown tokens and 400 ExpiredToken for expired or already-used tokens.
- Adds ErrorCode::ExpiredToken (HTTP 400, serialised as PascalCase "ExpiredToken") to match the ATProto resetPassword lexicon exactly.
- Adds V014 migration for the password_reset_tokens table (token_hash PK, did FK, expires_at, used_at nullable, created_at).
Test plan
- cargo test -p relay -- request_password_reset reset_password — 14 tests covering happy path, anti-enumeration, token expiry, already-used token, malformed token, and DB side effects
- cargo test -p common — verifies ExpiredToken serialises as "ExpiredToken" (PascalCase) and maps to HTTP 400
- Bruno: open request_password_reset.bru against a local relay, confirm 200 response and token logged to stdout
- Bruno: copy token from logs into reset_password.bru, confirm 200 and that a second call with the same token returns 400 ExpiredToken