fix(ST-1007): listen for SSE 'state' event so approve-during-connect resolves

De SSE-route in ST-1004 stuurt de catch-up payload als `event: state\ndata: …`
om een race te dichten: tussen pair/start en SSE-open kan de mobiel approven,
de pg_notify fired vóór onze LISTEN actief is en gaat verloren (Postgres
queuet niet). De server compenseert door direct na connect een `state`-event
te sturen met de huidige status uit de DB.

Maar de client luisterde alleen op 'message'. EventSource routeert events met
`event: <name>` enkel naar listeners voor die exacte naam — het catch-up event
werd dus genegeerd. Gevolg bij een (zeldzame) race: QR blijft hangen tot
expiry omdat noch de notify noch de catch-up doorkomt.

Fix: dezelfde onMessage-handler ook aan 'state' binden (en netjes
unsubscriben bij cleanup). Geen server-side wijziging nodig — protocol bleef
bewust om de semantische scheiding 'initial state' vs 'live notify' te
behouden voor toekomstige clients die er onderscheid in willen maken.

Severity: middel-laag — kleine race-window, geen data/security-impact, alleen
"QR doet niks" tot user op Vernieuwen klikt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Janpeter Visser 2026-04-27 23:53:50 +02:00
parent 5cbf543c16
commit d6e71f915c

View file

@ -106,11 +106,20 @@ export function QrLoginButton() {
// Geen actie nodig tenzij we definitief willen falen.
}
// De server stuurt direct na connect een `event: state`-payload met de
// huidige pairing-status (catch-up voor de race tussen pair/start en de
// SSE-open: als de mobiel net daarvoor approvet komt de notify door
// vóórdat onze LISTEN actief is en wordt 'ie verloren). EventSource
// routeert events met `event: <name>` alleen naar listeners voor die
// naam — niet naar 'message'. Dezelfde handler aan beide hangen vangt
// de catch-up én reguliere notifies op.
es.addEventListener('message', onMessage)
es.addEventListener('state', onMessage as unknown as EventListener)
es.addEventListener('error', onError)
return () => {
es.removeEventListener('message', onMessage)
es.removeEventListener('state', onMessage as unknown as EventListener)
es.removeEventListener('error', onError)
es.close()
sseRef.current = null