fix(relay): address OAuth token endpoint code review — security and completeness
## Critical Fixes
**C-1/C-2: Prevent TOCTOU in token consumption**
- Split `consume_authorization_code` into `get_authorization_code` (SELECT) and
`delete_authorization_code` (DELETE). The handler now validates client_id,
redirect_uri, and PKCE before consuming. Serialization via max_connections(1)
prevents TOCTOU races.
- Split `consume_oauth_refresh_token` into `get_oauth_refresh_token` and
`delete_oauth_refresh_token` with the same pattern. Validation of client_id
and DPoP binding happens before deletion.
**C-3: Remove nonce header panics**
- Replace `nonce.parse().unwrap()` with `HeaderValue::from_str()` in
`OAuthTokenError::into_response()` and success response handlers. Log warnings
on parse failure rather than panicking.
**C-4: Add iss and aud claims to access tokens (RFC 9068 §2.2)**
- AccessTokenClaims now includes `iss` (public_url) and `aud` (public_url) fields.
- `issue_access_token()` accepts `public_url` parameter to populate these claims.
**C-5: Reject multiple DPoP headers (RFC 9449 §11.1)**
- Both `handle_authorization_code` and `handle_refresh_token` now check
`headers.get_all("DPoP").iter().count() > 1` and return `invalid_dpop_proof`
error if multiple headers are present.
## Important Fixes
**I-1: Remove AC references from source code**
- Removed ~45 AC-style comments from oauth_token.rs, oauth.rs, and auth/mod.rs
per CLAUDE.md policy.
**I-2: Add aud claim for token validation**
- Included in C-4 above.
**I-3: Normalize scope values to AT Protocol standards**
- Authorization code handler now returns `scope: "com.atproto.access"` in token
response (mapped from stored "atproto" value).
- Refresh token handler returns `scope: "com.atproto.refresh"` in token response.
**I-4: Return correct HTTP status codes**
- `OAuthTokenError::into_response()` now returns 500 (INTERNAL_SERVER_ERROR)
for `error == "server_error"`, and 400 (BAD_REQUEST) for all other errors.
**I-5: Restrict DPoP algorithm choices**
- `dpop_alg_from_str()` now only accepts ES256 and ES384 (elliptic curve).
Removed RS256/384/512 and PS256/384/512 entries to match server metadata
advertising only ES256.
**I-6: Remove base64url decode silent fallback**
- Both handlers now use explicit error handling for base64url decode failures.
Invalid codes/tokens return `invalid_grant` immediately rather than silently
hashing raw bytes.
**I-7: Add logging for nonce rejection**
- `validate_and_consume_nonce()` now logs debug messages distinguishing:
- Nonce expired (rejected)
- Nonce unknown (possible replay or server restart)
**I-8: Clarify dead_code lint on OAuthSigningKey**
- Updated doc comment explaining Axum's State<AppState> extractor hides
field usage from dead code analyzer.
**I-9: Document atomicity precondition**
- `get_authorization_code()` and `get_oauth_refresh_token()` doc comments
already noted that SELECT+DELETE serialization relies on max_connections(1).
**I-10: Ephemeral key logging**
- No changes needed; startup warning is sufficient.
## Stale Comment Fixes
**SC-1: Update nonce deduplication comment**
- Removed "not yet implemented" from jti doc; now references the nonce
validation mechanism in the token endpoint.
**SC-2: Update token endpoint reference**
- Removed "not yet implemented" from `store_authorization_code()` doc;
token endpoint is now live.
## Test Improvements
**T-1: Assert scope claim value**
- Authorization code happy path now asserts `json["scope"] == "com.atproto.access"`.
**T-2: Assert rotated refresh token length**
- Refresh token happy path now asserts rotated token length is 43 characters.
**T-3/T-4/T-5: Additional test coverage deferred**
- These require new test functions, handled separately from code fixes.
## Verification
- All 22 oauth_token tests pass.
- `cargo clippy -p relay -- -D warnings` passes cleanly.
- `cargo fmt --all --check` passes.