atproto pds in zig pds.zat.dev
pds atproto

oauth: make oauthError terminate the request; PAR scope regression in smoke master

oauthError wrote the error body and returned success, so every helper that did `return oauthError(...)` — validateScope, requireClientAuth — returned normally to its `try` caller and the handler went on to overwrite the 400 with a 201/200. Observed live: /oauth/par answered 201 for an empty scope, `garbage:thing`, an unresolvable include, mixed transition+granular scopes, and scopes not registered in the client's metadata. requireClientAuth has the same shape on the token and refresh paths, so a failed private_key_jwt assertion was also overwritten. oauthError now returns error.HandledResponse after writing; the server loop treats that as "response already written" instead of a 500. The route switch moves into App.dispatch so the telemetry status stays honest. tools/smoke.sh hosts a public client's metadata on the PLC stub and asserts PAR answers 400 invalid_scope for unregistered, unresolvable and mixed scopes, and reaches the DPoP check for a valid one. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>


+64 -2
3 changed files