Thanks to the Colibri developers for the report and for finding the line it came from.
response_mode (OAuth 2.0 Multiple Response Type Encoding Practices 2.1) was
not declared on ParInput at all, so serde dropped it. Every pushed
authorization request was staged as though it had asked for query, and the
consent page built redirect_uri?code=...&state=...&iss=... unconditionally.
@atproto/oauth-client-browser sets responseMode: 'fragment' by default and
reads its callback out of location.hash, so every single-page client built on
it was returned to its own redirect URI carrying the code in a place it does
not look. The browser navigated, the client found nothing, and no error was
raised at either end -- sign-in simply never completed, with nothing in a log
on either side to say why.
The parameter is now read from both PAR intake shapes, stored on the staged
request, and honoured everywhere an authorization response is written. Absent
still means query, which is the default for response_type=code and what
every request was answered with before.
oauth_par carries it as a nullable column because the mode is decided at PAR
and needed at a later request, against a row that may outlive the process. A
row staged before the column existed reads back as query -- not an absent
answer, but the mode it was actually answered in, which matters for the flows
in flight across an upgrade.
Two paths write the response and both were changed. The consent page assembles
the parameters once and then either sets them on the fragment or on the query,
reading the mode the server settled at PAR rather than guessing at the client.
The delegated callback is the one place this server builds the redirect itself,
in a 303; its success and its access_denied now go through the same builder,
which also retires delegation::redirect_with in favour of one renderer for
both modes.
form_post is refused at PAR rather than downgraded. The reference server
offers it and this one cannot: it would need this origin to render an
auto-submitting form aimed at the client, and HTML_CSP carries
form-action 'self', so the browser would block the submission and the flow
would die on a blank page. Accepting it and answering with a query would
reproduce exactly the failure above. response_modes_supported is built from
the same constant PAR validates against, so the metadata document cannot
promise a mode the endpoint refuses.
Nine tests hold it: the PAR-to-authorize fragment path, the unstated default,
the form_post refusal naming the mode, the advertised list, the round trip
through both the consuming and non-consuming reads of the SQL store, the
consent script's fragment branch, and a delegated redirect landing in the
fragment with the client's own query component untouched.