fix(rebate-indexer): CORS preflight missing Allow-Methods/Allow-Headers broke every signed POST#567
Merged
Merged
Conversation
…rs broke every signed POST
ROOT CAUSE of the partner dashboard "Could not load the dashboard" error (and a
silent break of the whole signed referral flow): the CORS onRequest hook set
ONLY Access-Control-Allow-Origin, and app.options('*') replied a bare 204. The
affiliate/partner pages POST /partner, /ref/bind, /ref/codes with
`content-type: application/json` (a non-safelisted header), which forces a
browser CORS preflight. With no Access-Control-Allow-Methods and no
Access-Control-Allow-Headers on the preflight response, the browser blocks the
real POST and fetch() rejects with a TypeError -> the page falls through to its
generic error state. Simple GETs (/affiliate, /tier, /leaderboard) need no
preflight, so the public read path kept working: exactly the observed asymmetry.
curl is not bound by CORS, which is why direct POSTs reached the real 401/403.
Fix: for allowed origins, also set Access-Control-Allow-Methods (GET, POST,
OPTIONS), Access-Control-Allow-Headers (content-type, accept), Access-Control-
Max-Age, and a proper Vary. Adds 4 regression tests (allowed-origin preflight on
/partner + /ref/bind + /ref/codes echoes the headers; disallowed origin gets none).
Found via a multi-agent audit. Data integrity verified healthy (partner 'san'
= 12% rate, referredCount:0 is correct, not stale). Frontend error-masking
(parseJson drops the server message; generic catch-all) is a separate follow-up.
typecheck green; 16/16 unit tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
san-npm
added a commit
that referenced
this pull request
Jun 12, 2026
…I timeout (audit follow-up) (#568) The CORS-preflight bug (fixed server-side in #567) surfaced as an opaque "Could not load the dashboard / Something went wrong" because the frontend collapsed every non-403/401 failure into one generic state and discarded the server's error message. These fixes make the next failure diagnosable (no behavior change to the happy path): - ophisAffiliateApi.parseJson: read the server's `{error}` body into AffiliateApiError.message (was thrown away, leaving a bare status). - ophisAffiliateApi: wire the defined-but-unused AFFILIATE_API_TIMEOUT_MS into the signed POSTs (/partner, /ref/bind, /ref/codes) via AbortSignal.timeout, so a hung preflight/backend fails fast instead of spinning forever. - Partner.container: distinguish a network/CORS/timeout failure (TypeError / AbortError) into a dedicated "Could not reach the partner service" state, log the status+message for real server errors, and console.error transport failures. - OphisAffiliateDashboard: render the rate from stats.rateOfNetFeePct (8 only as a pre-load fallback) instead of a hardcoded "8%", so the view tracks the backend FEE_SHARE_BPS. - RefCodeCaptureUpdater: console.debug -> console.warn for transient bind failures so a persistent backend/transport break is visible in logs. Data integrity verified healthy in the audit (partner rates 8%/12% correct; referredCount:0 correct). typecheck green; 949/951 tests pass (2 pre-existing unrelated OP-router snapshots). Co-authored-by: Claude Opus 4.8 <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.
Root cause of the Partner dashboard failure
The Partner dashboard (
swap.ophis.fi/#/partner) showed "Could not load the dashboard / Something went wrong". A multi-agent audit traced it to the rebate-indexer CORS preflight:onRequesthook set onlyAccess-Control-Allow-Origin(+ Vary);app.options('*')returned a bare204./partner,/ref/bind,/ref/codes) withcontent-type: application/json— a non-safelisted header that forces a CORS preflight.Access-Control-Allow-Methodsand noAccess-Control-Allow-Headerson the preflight, the browser blocks the real POST;fetch()rejects with aTypeError, which the page can't classify → generic error./affiliate,/tier,/leaderboard) need no preflight → kept working. That asymmetry (reads OK, every signed POST dead) is exactly what was observed.curlisn't bound by CORS, so direct POSTs reached the real401/403.This silently broke the entire signed referral flow (partner dashboard and referral code bind/mint), not just the dashboard.
Fix
For allowed origins, the preflight now also sets
Access-Control-Allow-Methods: GET, POST, OPTIONS,Access-Control-Allow-Headers: content-type, accept,Access-Control-Max-Age: 600, and a correctVary. 4 regression tests added (preflight on/partner+/ref/bind+/ref/codesechoes the headers; disallowed origin gets none) — the hand-rolled CORS path had zero coverage.Audit notes (separate)
san=kind:'partner', 12% rate,activeCodes:['san'];referredCount:0/currentCycleVolumeUsd:0are correct (no wallets bound yet), not stale. Rates (8%/12%) verified correct. No data/money bugs.parseJsondiscards the server{error}; the page collapses TypeError/4xx/5xx into the generic message; the definedAFFILIATE_API_TIMEOUT_MSis never wired in) is a follow-up PR — not required to fix the outage, but makes the next failure diagnosable.Verification
tsc --noEmitgreen;vitest16/16 pass.curl -i -X OPTIONS https://rebates.ophis.fi/partner -H 'Origin: https://swap.ophis.fi' -H 'Access-Control-Request-Method: POST' -H 'Access-Control-Request-Headers: content-type'must showaccess-control-allow-methods(incl POST) +access-control-allow-headers(incl content-type).🤖 Generated with Claude Code