GET /events answers today with no security headers at all, and the socket in #9 would be upgraded
through a same-origin rule that a non-browser client passes by saying nothing. Both are fixed before
the game's live data goes through them.
What is true today, checked against this branch running on a free port:
/ and the 404 path carry the full floor. /events returns content-type: text/event-stream,
cache-control: no-store, x-accel-buffering: no — and nothing else. No CSP, no
x-content-type-options, no referrer-policy. The one streaming door is the one door outside the
floor, because events() in src/live.ts builds its own Response.
The CSP is default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; ... — no
script-src, no connect-src, and inline styles allowed.
sameOrigin() returns true when both sec-fetch-site and origin are absent. That is right for a
script judged by its identity and wrong for a websocket upgrade.
Done looks like
[ ] events() returns SECURITY on its response like every other door. The floor is defined once in
src/http.ts and streaming is not an exception to it.
[ ] SECURITY names script-src 'self' and connect-src 'self' explicitly, so the canvas's socket
is permitted by a stated rule rather than by whatever default-src happens to cover; the comment
above SECURITY says what was decided about ws:/wss: and why.
[ ] style-src 'unsafe-inline' is gone, or the pull names the one inline style that still needs it.
#7 rewrites the page's CSS — coordinate with it rather than fight it.
[ ] A socketOrigin(request) rule beside sameOrigin in src/http.ts that an upgrade must pass:
Origin present and equal to publicOrigin. A missing Origin is refused for an upgrade even
though sameOrigin allows it for a POST — the two rules differ on purpose and the comment says
why. #9 calls this and does not invent its own.
[ ] /events is rationed: a cap on concurrent streams per identity, per address for a stranger, over
the cap answered with 429 rather than a stream held open.
[ ] A listener count is exported from src/live.ts so a test can prove the subscribe set does not
grow: open N streams, abort them, assert zero listeners.
[ ] A non-numeric Last-Event-ID or ?since= replays from the start instead of silently replaying
nothing (Number("x") is NaN and every id > NaN is false today).
[ ] Tests: a table over every route asserting each response carries every floor header, /events and
/static/* included, so a door added later without the floor fails; an upgrade with a foreign
Origin and one with no Origin are both refused; the listener-leak test above.
Risks #9 builds the socket on socketOrigin, so this lands before it. The unsafe-inline removal
is the one line that touches #7's work — do it last and check the page still styles. Do not weaken the
floor to make a test pass: the header table is the test, and a route that cannot carry the floor is
the bug.
Fits Sentinel's brief: this is a floor issue, and the test that guards it matters more than the fix.
GET /eventsanswers today with no security headers at all, and the socket in #9 would be upgraded through a same-origin rule that a non-browser client passes by saying nothing. Both are fixed before the game's live data goes through them.What is true today, checked against this branch running on a free port:
/and the 404 path carry the full floor./eventsreturnscontent-type: text/event-stream,cache-control: no-store,x-accel-buffering: no— and nothing else. No CSP, nox-content-type-options, noreferrer-policy. The one streaming door is the one door outside the floor, becauseevents()insrc/live.tsbuilds its ownResponse.default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; ...— noscript-src, noconnect-src, and inline styles allowed.sameOrigin()returns true when bothsec-fetch-siteandoriginare absent. That is right for ascript judged by its identity and wrong for a websocket upgrade.
Done looks like
events()returnsSECURITYon its response like every other door. The floor is defined once insrc/http.tsand streaming is not an exception to it.SECURITYnamesscript-src 'self'andconnect-src 'self'explicitly, so the canvas's socketis permitted by a stated rule rather than by whatever
default-srchappens to cover; the comment aboveSECURITYsays what was decided aboutws:/wss:and why.style-src 'unsafe-inline'is gone, or the pull names the one inline style that still needs it.#7 rewrites the page's CSS — coordinate with it rather than fight it.
socketOrigin(request)rule besidesameOrigininsrc/http.tsthat an upgrade must pass:Originpresent and equal topublicOrigin. A missingOriginis refused for an upgrade even thoughsameOriginallows it for a POST — the two rules differ on purpose and the comment says why. #9 calls this and does not invent its own./eventsis rationed: a cap on concurrent streams per identity, per address for a stranger, overthe cap answered with 429 rather than a stream held open.
src/live.tsso a test can prove thesubscribeset does notgrow: open N streams, abort them, assert zero listeners.
Last-Event-IDor?since=replays from the start instead of silently replayingnothing (
Number("x")isNaNand everyid > NaNis false today)./eventsand/static/*included, so a door added later without the floor fails; an upgrade with a foreignOriginand one with noOriginare both refused; the listener-leak test above.Files
src/http.ts,src/live.ts,src/server.ts,src/server.test.ts,src/live.test.ts(new).Risks #9 builds the socket on
socketOrigin, so this lands before it. Theunsafe-inlineremoval is the one line that touches #7's work — do it last and check the page still styles. Do not weaken the floor to make a test pass: the header table is the test, and a route that cannot carry the floor is the bug.Fits Sentinel's brief: this is a floor issue, and the test that guards it matters more than the fix.