Skip to content

A third listener that serves one fullscreen map (#146) - #147

Merged
CaYatur merged 2 commits into
mainfrom
feat/map-page
Jul 29, 2026
Merged

A third listener that serves one fullscreen map (#146)#147
CaYatur merged 2 commits into
mainfrom
feat/map-page

Conversation

@CaYatur

@CaYatur CaYatur commented Jul 29, 2026

Copy link
Copy Markdown
Owner

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

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

It pastes MAP_CSS / MAP_HTML / MAP_JS from @shared/mapUi and 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

mapPageAllows is 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: false makes the tile route a 404, not a hidden layer.

What the self-review found

The players mode was a door that never opened. It reached for the public site's session, which lives in localStorage under msms_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, and JSON.stringify escapes 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:

Reintroduced Gate says
the data gate removed protected /api/map answered 200
the old cookie name a signed-in player still could not read the map

One test defect fixed on the way: 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.

CaYatur added 2 commits July 29, 2026 16:47
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.
Copilot AI review requested due to automatic review settings July 29, 2026 13:59

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@CaYatur
CaYatur merged commit 8fac002 into main Jul 29, 2026
1 check passed
@CaYatur
CaYatur deleted the feat/map-page branch July 29, 2026 13:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A third page: a dedicated fullscreen map

2 participants