A third listener that serves one fullscreen map (#146) - #147
Merged
Conversation
A LISTENER, not a path on the public site. `WebConfig` already carries a port and an enabled flag per surface, and following that shape here is not about symmetry: a separate port is what lets an operator hand the map to people who must not reach the shop or the panel, with a firewall rule rather than with trust. "Yönetici kısıtlayabilmeli" has to mean something at the network layer, not only in a template. Not a fourth map. The page pastes `MAP_CSS`, `MAP_HTML` and `MAP_JS` from `@shared/mapUi` and restyles the wrapper - the canvas fills the window instead of sitting in a card, and the controls float over it. #129 was about three maps that had drifted apart; writing another one here would undo it. The one thing the engine needed was a hook for the areas URL, because this page has neither an admin id nor the public site's routes and would have 404ed on both branches of the guess. Three access modes: open, a shared passphrase, or a signed-in player. The gate refuses the DATA, not just the HTML - a page that gates only its markup is a page whose feed anyone can fetch directly. `mapPageAllows` is the single answer, called by the state route and every feed, so the door and the data cannot disagree. The passphrase is stored in the clear on purpose. It is a shared doorcode an operator has to be able to read back and tell people, not a credential belonging to a person; hashing it would only stop them looking it up, while anyone who can read the config can already change it. The COOKIE is a hash of it and an in-memory salt, so restarting MSMS signs everyone out - a cheap way to shut a leaked link without changing the code and telling everybody the new one. What the page may show is eleven separate operator decisions, and every one of them is enforced server-side: `world: false` makes the tile route a 404, not a hidden layer. Heads are refused with names off, because a face identifies a player exactly as well as a name does (#116). Verified: 12/12 gates. The gate proved failable - disabling it answers "protected /api/map answered 200". One test defect fixed: the route-coverage check isolated `handlePanel` by slicing to `startWebServer`, which held only while `handlePanel` was the last thing before it. `handleMapPage` landed in between and its routes - a different listener, deliberately outside the /api/v1 surface - were read as undocumented panel routes. It now slices to the next top-level function, and asserts the slice is big enough to be the real router, because a slice that is too short reads exactly like success.
Three access modes shipped; two had tests. The untested one did not work. `players` read the public site's session, which lives in `localStorage` under `msms_ptoken`. localStorage is per ORIGIN, and a different port IS a different origin - so the map page on 8724 could never read what the site on 8723 wrote, whatever the two agreed to call it. The mode was permanently shut, and nothing said so: the state route answered `allowed: false` exactly as it would for a visitor who simply had not signed in yet. The page signs players in itself now, against the same `playerAuth` accounts, and holds the session in a cookie on its own origin. Cookies are not isolated by port, which is a weakness elsewhere and the mechanism here. Session-length rather than the passphrase cookie's thirty days: this one stands for a person. The test signs a real account in and reads the map with the cookie a browser would send. Restoring the old cookie name fails it with "a signed-in player still could not read the map". It also asserts the NEGATIVE - that the public site's cookie name is not accepted - because reading that was the bug. Each mode's login route now 404s in the other's mode. An unused door left open is a door. Also: the page title is operator text interpolated inside a script block, and `JSON.stringify` escapes quotes while leaving `<` alone - a title containing a closing script tag ended the block and the rest of the page became markup. Escaped, and the smoke serves a page titled `evil</script><img src=x>` and checks it did not break out. The two HTML positions were already escaped; this one had been missed. And a port collision with the panel or the site is now said on screen. It used to be an EADDRINUSE in the log and a card that said "stopped". Verified: 12/12 gates.
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.
Closes #146.
A third web surface whose entire design is the map: canvas edge to edge, controls floating over it, nothing else on the screen.
A listener, not a route
WebConfigalready carries a port and an enabled flag per surface, and following that shape here is not about symmetry. A separate port is what lets an operator hand the map to people who must not reach the shop or the panel — with a firewall rule rather than with trust. "Yönetici kısıtlayabilmeli" has to mean something at the network layer, not only in a template.Not a fourth map
It pastes
MAP_CSS/MAP_HTML/MAP_JSfrom@shared/mapUiand restyles the wrapper. #129 was about three maps that had drifted apart; writing another here would undo it. The one thing the engine needed was a hook for the areas URL — this page has neither an admin id nor the public site's routes, so the existing guess would have 404ed on both branches.The gate refuses the data, not the markup
mapPageAllowsis the single answer, called by the state route and every feed. A page that gates only its HTML is a page whose feed anyone can fetch directly.The passphrase is stored in the clear on purpose: it is a shared doorcode an operator has to read back and hand out, not a credential belonging to a person. Hashing it would only stop them looking it up, while anyone who can read the config can already change it. The cookie is a hash of it and an in-memory salt, so restarting MSMS signs everyone out — a cheap way to shut a leaked link without changing the code and telling everybody the new one.
Eleven settings for what the page may show, every one enforced server-side:
world: falsemakes the tile route a 404, not a hidden layer.What the self-review found
The
playersmode was a door that never opened. It reached for the public site's session, which lives inlocalStorageundermsms_ptoken— and localStorage is per origin, so a different port could never read it. Three modes shipped, two had tests, and the untested one did not work. The page signs players in itself now, against the same accounts, with a cookie on its own origin.Also: the page title is operator text interpolated inside a
<script>block, andJSON.stringifyescapes quotes while leaving<alone — a title containing</script>ended the block. The two HTML positions were already escaped; this one had been missed.Verification
12/12 gates. Three checks proved failable:
protected /api/map answered 200a signed-in player still could not read the mapOne test defect fixed on the way: the route-coverage check isolated
handlePanelby slicing tostartWebServer, which held only whilehandlePanelwas the last thing before it.handleMapPagelanded in between and its routes — a different listener, deliberately outside the/api/v1surface — were read as undocumented panel routes. It now slices to the next top-level function, and asserts the slice is big enough to be the real router, because a slice that is too short reads exactly like success.