fix(jwt): handle CORS preflight on /jwt/* endpoints#84
Merged
Conversation
Browser CORS preflight (OPTIONS /jwt/login) was being routed through
JwtEndpoint::process(), which only accepts POST and returned 405 with
no CORS headers. Browsers then blocked the actual POST, breaking JWT
login for any same-page web client — including Shipyard 2.0+ in JWT
mode and any kyte-api-js v2 JWT consumer.
Root cause: Api::cors() runs inside validateRequest(), which is
downstream of the /jwt dispatch in Api::route(). So /jwt/* skipped
CORS entirely. Same theoretical problem exists for /mcp, but MCP is
server-to-server and never triggers browser preflight.
Fix:
- JwtEndpoint::handle() now emits CORS headers on every response
(Access-Control-Allow-Origin from the request's Origin/Referer,
Allow-Credentials true, Content-Type application/json; charset=utf-8)
- OPTIONS requests get an early 204 + preflight-specific headers
(Allow-Methods POST,OPTIONS; Allow-Headers echoing the request's
Access-Control-Request-Headers)
- Mirrors the permissive Origin policy in Api::cors()
Regression test exercises handle() end-to-end with $_SERVER mocked
for an OPTIONS request, output-buffers the response, and asserts
http_response_code() === 204 + empty body.
All 12 JwtEndpoint tests pass (11 existing + 1 new).
Bumps to v4.4.1 — patch-level fix, no schema changes, composer
upgrade is sufficient. Required for Shipyard 2.0+ deployments.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
OPTIONS /jwt/login) was returning405 method_not_allowedwith no CORS headers, blocking the actualPOST /jwt/loginand breaking JWT login from any same-page web client (Shipyard 2.0+ in JWT mode, kyte-api-js v2 JWT consumers).JwtEndpoint::handle()now emits CORS headers on every response and replies to OPTIONS with204 No Content+ preflight headers before falling through toprocess().Api::cors(). The /mcp endpoint has the same theoretical gap but never sees a preflight (server-to-server only) so it's left alone for this point release.Root cause
Api::cors()runs insidevalidateRequest(), which lives downstream of the/jwtdispatch inApi::route(). So/jwt/*skipped CORS entirely. The fix is contained toJwtEndpoint— no changes to the route dispatcher orApi::cors().Test plan
JwtEndpointTest::testHandleAnswersOptionsPreflightWith204exerciseshandle()end-to-end with$_SERVERmocked for an OPTIONS request, output-buffers the response, and assertshttp_response_code() === 204+ empty bodyOPTIONS /jwt/loginreturns 204 withAccess-Control-Allow-Origin/Allow-Credentials/Allow-Methods/Allow-Headers;POST /jwt/loginreturns 400 (empty body) with full CORS headersRollout
🤖 Generated with Claude Code