- 
                Notifications
    You must be signed in to change notification settings 
- Fork 634
Add hostToken generation and authentication #2320
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
| WalkthroughAdds host-token support end-to-end: GameServer stores a generated 16‑char hex host token; Worker includes the token in  Changes
 Sequence Diagram(s)sequenceDiagram
    participant Client
    participant HostModal as HostLobbyModal (client)
    participant Worker as Worker (HTTP)
    participant Game
    participant Server as GameServer
    rect rgb(235,245,255)
    Note over Client,HostModal: Create lobby
    Client->>HostModal: createLobby()
    HostModal->>Worker: POST /create_game
    Worker->>Game: new Game(...)
    Game->>Server: createHostToken()
    Server-->>Game: hostToken (16‑char hex)
    Game-->>Worker: gameInfo + hostToken
    Worker-->>HostModal: { gameInfo, hostToken }
    HostModal->>Client: set cookie (hostToken, Max‑Age=6h, Path=/, SameSite=Strict)
    end
    rect rgb(235,255,235)
    Note over Client,Worker: Start private game (host-only)
    Client->>HostModal: startGame()
    HostModal->>HostModal: read hostToken cookie
    HostModal->>Worker: POST /start_game { hostToken }
    Worker->>Game: getHostToken()
    alt tokens match
        Worker-->>HostModal: 200 OK
        HostModal->>Client: delete hostToken cookie
    else mismatch or missing
        Worker-->>HostModal: 403 Forbidden
    end
    end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes 
 Suggested labels
 Suggested reviewers
 Poem
 Pre-merge checks❌ Failed checks (1 warning)
 ✅ Passed checks (3 passed)
 Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment  | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️  Outside diff range comments (1)
src/server/Worker.ts (1)
161-185: Authorization is broken and early returns hang requests—both server and client need fixes.The issues are confirmed:
- Route
/api/start_game/:idhas no:hostTokenparameter, soreq.params.hostTokenis always undefined.- Early
returnstatements at lines 165 and 172 send no response, hanging the request.- Client code at
src/client/HostLobbyModal.ts:798-810sends nohostTokenin the body at all.Apply server changes in
src/server/Worker.ts(161–185):+import { timingSafeEqual } from "node:crypto"; app.post("/api/start_game/:id", async (req, res) => { log.info(`starting private lobby with id ${req.params.id}`); const game = gm.game(req.params.id); if (!game) { - return; + return res.status(404).json({ error: "Game not found" }); } if (game.isPublic()) { const clientIP = req.ip || req.socket.remoteAddress || "unknown"; log.info( `cannot start public game ${game.id}, game is public, ip: ${ipAnonymize(clientIP)}`, ); - return; + return res.status(400).json({ error: "Cannot start public game" }); } - if ( - !("hostToken" in req.params) || - req.params.hostToken !== game.getHostToken() - ) { - log.info("cannot start private game, requestor is not host"); - res.status(403); - return; - } + const tokenSchema = z.object({ + hostToken: z.string().regex(/^[A-F0-9]{16}$/), + }); + const parsed = tokenSchema.safeParse(req.body); + if (!parsed.success) { + const error = z.prettifyError(parsed.error); + return res.status(400).json({ error }); + } + const expected = game.getHostToken(); + if (!expected) { + return res.status(409).json({ error: "Host token not set for this game" }); + } + const suppliedBuf = Buffer.from(parsed.data.hostToken, "utf8"); + const expectedBuf = Buffer.from(expected, "utf8"); + if ( + suppliedBuf.length !== expectedBuf.length || + !timingSafeEqual(suppliedBuf, expectedBuf) + ) { + log.info("cannot start private game, requester is not host"); + return res.status(403).json({ error: "Forbidden" }); + } game.start(); res.status(200).json({ success: true }); });Update client at
src/client/HostLobbyModal.ts:798-810to send the token in body. The fetch call needs a body withhostToken. Where doesthis.lobbyIdor similar contain or reference the host token? Please confirm the token source so the client code can be corrected.
🧹 Nitpick comments (2)
src/server/Worker.ts (1)
151-153: Only include hostToken for private games; avoid spreading an object wrapper.Return the token only for private lobbies and keep the response shape simple.
Apply:
- const token = game.createHostToken(); - const hostToken = { hostToken: token }; + const hostToken = game.isPublic() ? undefined : game.createHostToken(); ... - res.json({ ...game.gameInfo(), ...hostToken }); + res.json( + hostToken ? { ...game.gameInfo(), hostToken } : game.gameInfo(), + );This avoids leaking an unnecessary secret for public games and keeps the JSON compact.
Also applies to: 157-157
src/server/GameServer.ts (1)
145-148: Add an explicit return type and consider null‑clearing semantics.Small polish and safety:
- Add return type for clarity.
- Consider clearing the token after it’s used (e.g., right after
start()).Apply:
- public getHostToken() { - return this.hostToken ?? null; - } + public getHostToken(): string | null { + return this.hostToken ?? null; + }If you decide to clear the token on start:
// At the end of start(): this.hostToken = null;
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
- src/server/GameServer.ts(2 hunks)
- src/server/Worker.ts(2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/server/Worker.ts (1)
src/server/GameManager.ts (1)
game(24-26)
🪛 GitHub Check: 🔍 ESLint
src/server/GameServer.ts
[failure] 134-134:
'tokenLength' is assigned a value but never used.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 4
🧹 Nitpick comments (5)
src/client/HostLobbyModal.ts (3)
801-809: Cookie parsing: simplify and harden.Consider a small helper to read cookies; reduces bugs and improves readability.
Example:
+function getCookie(name: string): string | null { + const m = document.cookie.match(new RegExp(`(?:^|; )${name}=([^;]*)`)); + return m ? decodeURIComponent(m[1]) : null; +} ... - const cookies = document.cookie.split(";"); - let hostToken = ""; - for (let i = 0; i < cookies.length; i++) { - const cookie = cookies[i]; - if (cookie.trim().startsWith("hostToken=")) { - hostToken = cookie.split("=")[1]; - break; - } - } + const hostToken = getCookie("hostToken") ?? "";
870-873: Type shape consistency for createLobby response.You re-wrap server data into
{ gameInfo, hostToken }. Good. If TypeScript ever complains at call sites (e.g., “property hostToken missing”), ensure this function’s return type is used by callers and consider validating the server payload.Optional hardening:
+type CreateLobbyResponse = { gameInfo: GameInfo; hostToken: string }; +const CreateLobbySchema = z.object({ + gameInfo: z.any(), // or stricter GameInfo schema if available + hostToken: z.string().min(1), +}); ... -): Promise<{ gameInfo: GameInfo; hostToken: string }> { +): Promise<CreateLobbyResponse> { ... - const data = await response.json(); + const data = await response.json(); + // If server returns flat { ...gameInfo, hostToken }, normalize here: + const normalized = { gameInfo: data, hostToken: data.hostToken }; + // Optionally validate: + // CreateLobbySchema.parse(normalized); - return { gameInfo: data, hostToken: data.hostToken }; + return normalized;Also applies to: 896-896
60-63: Timer typings cross‑env.For browser code, prefer
ReturnType<typeof setInterval/setTimeout>to avoid NodeJS.Timeout vs number mismatches.- private playersInterval: NodeJS.Timeout | null = null; + private playersInterval: ReturnType<typeof setInterval> | null = null; - private botsUpdateTimer: number | null = null; + private botsUpdateTimer: ReturnType<typeof setTimeout> | null = null;src/server/Worker.ts (2)
174-181: Validate request body and tighten responses.Add schema validation for
hostToken; keep responses explicit.- const hostToken = req.body.hostToken ?? ""; + const body = z.object({ hostToken: z.string().min(1) }).safeParse(req.body); + if (!body.success) { + return res.status(400).json({ error: "Invalid body" }); + } + const { hostToken } = body.data; ... - log.info(`cannot start private game ${game.id}, requestor is not host`); - res.status(403).json({ success: false }); - return; + log.info(`cannot start private game ${game.id}, requester is not host`); + return res.status(403).json({ success: false });Also consider responding 404 when game is missing:
- if (!game) { - return; - } + if (!game) { + return res.status(404).json({ error: "Game not found" }); + }
151-157: Response shape: return{ gameInfo, hostToken }instead of flattening.This avoids mixing concerns and matches the client’s natural usage.
- const hostToken = { hostToken: game.createHostToken() }; ... - res.json({ ...game.gameInfo(), ...hostToken }); + const hostToken = game.createHostToken(); + res.json({ gameInfo: game.gameInfo(), hostToken });Please confirm no other clients depend on the flat shape.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
- src/client/HostLobbyModal.ts(4 hunks)
- src/server/GameServer.ts(3 hunks)
- src/server/Worker.ts(2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- src/server/GameServer.ts
🧰 Additional context used
🧬 Code graph analysis (2)
src/server/Worker.ts (1)
src/server/GameManager.ts (1)
game(24-26)
src/client/HostLobbyModal.ts (1)
src/core/Schemas.ts (1)
GameInfo(125-131)
🪛 Biome (2.1.2)
src/server/Worker.ts
[error] 178-178: expected , but instead found ;
Remove ;
(parse)
🪛 GitHub Actions: 🧪 CI
src/server/Worker.ts
[error] 178-178: SyntaxError: ')' expected while running 'npx prettier --check .' (likely missing closing parenthesis or semicolon in response. Add ); to properly close res.status(403).json({ success: false }).
🪛 GitHub Check: 🔍 ESLint
src/server/Worker.ts
[failure] 178-178:
Parsing error: ')' expected.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
♻️ Duplicate comments (2)
src/client/HostLobbyModal.ts (2)
595-600: Fix cookie typo and add security attributes.The cookie setting still has "Max-Axe" instead of "Max-Age", and is missing URL encoding and security flags (SameSite, Secure).
This issue was already flagged in a previous review at lines 597-600.
821-821: Cookie deletion needs matching attributes.The cookie deletion is missing the Path, SameSite, and Secure attributes used when setting the cookie. This can cause the deletion to fail.
This issue was already flagged in a previous review at line 821.
🧹 Nitpick comments (1)
src/client/HostLobbyModal.ts (1)
818-818: Use object shorthand.Simplify
{ hostToken: hostToken }to{ hostToken }.- body: JSON.stringify({ hostToken: hostToken }), + body: JSON.stringify({ hostToken }),
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
- src/client/HostLobbyModal.ts(4 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/client/HostLobbyModal.ts (1)
src/core/Schemas.ts (1)
GameInfo(125-131)
🔇 Additional comments (1)
src/client/HostLobbyModal.ts (1)
868-899: API returns hostToken—no validation needed.The backend endpoint (Worker.ts line 156) spreads
hostTokeninto every successful response:res.json({ ...game.gameInfo(), ...hostToken }). The assumption in the client code is valid and safe.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️  Outside diff range comments (1)
src/server/Worker.ts (1)
160-173: Bug: early returns without HTTP response.Both “game not found” and “game is public” paths return without sending a response; callers will hang. Return proper status codes.
- if (!game) { - return; - } + if (!game) { + return res.status(404).json({ error: "Game not found" }); + } if (game.isPublic()) { // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const clientIP = req.ip || req.socket.remoteAddress || "unknown"; - log.info( + log.warn( `cannot start public game ${game.id}, game is public, ip: ${ipAnonymize(clientIP)}`, ); - return; + return res.status(400).json({ error: "Cannot start a public game" }); }
♻️ Duplicate comments (4)
src/server/Worker.ts (1)
178-179: Syntax fix acknowledged.The missing parenthesis previously flagged here is now fixed; CI should pass this spot.
src/client/HostLobbyModal.ts (3)
801-810: Make cookie parsing robust and URL‑decode the value.Handle values with “=” and decode.
- const cookies = document.cookie.split(";"); - let hostToken = ""; - for (let i = 0; i < cookies.length; i++) { - const cookie = cookies[i]; - if (cookie.trim().startsWith("hostToken=")) { - hostToken = cookie.split("=")[1]; - break; - } - } + const cookies = document.cookie.split(";"); + let hostToken = ""; + for (const cookie of cookies) { + const trimmed = cookie.trim(); + if (trimmed.startsWith("hostToken=")) { + hostToken = decodeURIComponent(trimmed.substring("hostToken=".length)); + break; + } + } + if (!hostToken) { + console.warn("startGame aborted: missing hostToken cookie"); + return; + }
595-600: Harden cookie: URL‑encode and add security attributes; remove stray comment.Encode the token and set SameSite and Secure (when HTTPS). The inline “getting error…” comment should be removed.
- const cookieDurationSec = 60 * 60 * 6; //Store cookie for max 6 hours - // getting error here from lobby.hostToken - document.cookie = `hostToken=${lobby.hostToken}; Max-Age=${cookieDurationSec}; Path=/`; + const cookieDurationSec = 60 * 60 * 6; // store cookie for max 6 hours + const secure = location.protocol === "https:" ? "; Secure" : ""; + document.cookie = `hostToken=${encodeURIComponent(lobby.hostToken)}; Max-Age=${cookieDurationSec}; Path=/; SameSite=Strict${secure}`;
892-896: Do not log tokens (response includes hostToken).
console.log("Success:", data)prints the token. Remove or log only non‑sensitive fields.- const data = await response.json(); - console.log("Success:", data); - - return { gameInfo: data, hostToken: data.hostToken }; + const data = await response.json(); + // Optional: debug minimal info + // console.debug("Lobby created", { gameID: data.gameID }); + return { gameInfo: data, hostToken: data.hostToken };
🧹 Nitpick comments (3)
src/server/Worker.ts (1)
174-183: Validate request body and align error shape.Parse
hostTokenwith Zod, reject missing/empty values, and keep error shape consistent across APIs. Optionally invalidate the host token after a successful start to prevent reuse.+ const StartGameSchema = z.object({ hostToken: z.string().min(1) }); + const parsed = StartGameSchema.safeParse(req.body); + if (!parsed.success) { + const error = z.prettifyError ? z.prettifyError(parsed.error) : "Invalid body"; + return res.status(400).json({ error }); + } - const hostToken = req.body.hostToken ?? ""; + const { hostToken } = parsed.data; if (hostToken !== game.getHostToken()) { - log.info(`cannot start private game ${game.id}, requestor is not host`); - res.status(403).json({ success: false }); - return; + log.warn(`cannot start private game ${game.id}, requestor is not host`); + return res.status(403).json({ error: "Forbidden" }); } game.start(); - res.status(200).json({ success: true }); + // Optional: make token one-time. If supported: + // game.invalidateHostToken?.(); + return res.status(200).json({ success: true });src/client/HostLobbyModal.ts (2)
818-819: Tiny cleanup: shorthand property.Use
{ hostToken }for brevity.- body: JSON.stringify({ hostToken: hostToken }), + body: JSON.stringify({ hostToken }),
851-854: Noisy console log in production path.Logging full
GameInfoeach second is noisy and may include usernames. Consider removing or guard behind a debug flag.- console.log(`got game info response: ${JSON.stringify(data)}`); + // console.debug("got game info response");
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
- src/client/HostLobbyModal.ts(4 hunks)
- src/server/Worker.ts(2 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/server/Worker.ts (1)
src/server/GameManager.ts (1)
game(24-26)
src/client/HostLobbyModal.ts (1)
src/core/Schemas.ts (1)
GameInfo(125-131)
🔇 Additional comments (1)
src/client/HostLobbyModal.ts (1)
821-823: Good: delete cookie with matching attributes.Deletion mirrors Path and SameSite and adds Secure when applicable.
| async function createLobby( | ||
| creatorClientID: string, | ||
| ): Promise<{ gameInfo: GameInfo; hostToken: string }> { | ||
| const config = await getServerConfigFromClient(); | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Nice: typed return for createLobby.
Clear contract { gameInfo, hostToken } simplifies callers. Consider adding a unit/integration test to ensure server response always includes hostToken.
🏁 Script executed:
#!/bin/bash
# Quick check of all call sites for the new shape
rg -n --type=ts 'createLobby\(' -C2Length of output: 568
🏁 Script executed:
#!/bin/bash
# Search more comprehensively for createLobby usage across all TypeScript files
rg -n 'createLobby' --type=ts -B1 -A3Length of output: 633
🏁 Script executed:
#!/bin/bash
# Check if there are existing tests for HostLobbyModal or createLobby
fd -t f '\.(test|spec)\.' --type=f | xargs rg -l 'createLobby' 2>/dev/null || echo "No test files found with createLobby"Length of output: 36
🏁 Script executed:
#!/bin/bash
# Extract the full implementation of createLobby to verify it always provides both fields
ast-grep --pattern $'async function createLobby($_) {
  $$$
}'Length of output: 0
🏁 Script executed:
#!/bin/bash
# Extract the full createLobby function implementation
sed -n '869,/^}/p' src/client/HostLobbyModal.ts | head -50Length of output: 979
🏁 Script executed:
#!/bin/bash
# Look for how the response is built and returned in createLobby
rg -n --type=ts 'hostToken' src/client/HostLobbyModal.ts -B2 -A2Length of output: 1446
Add tests to validate server response includes hostToken field.
The function promises hostToken: string in its return type, but the implementation at line 895 directly returns data.hostToken without verifying the server response includes it. If the server response is missing hostToken, the code will return undefined instead of the promised string, breaking the type contract. The call site at line 599 accesses lobby.hostToken directly in a cookie, assuming it always exists.
Add unit/integration tests that verify the server API endpoint always includes hostToken in its response payload.
🤖 Prompt for AI Agents
In src/client/HostLobbyModal.ts around lines 869 to 872, the createLobby
function promises a hostToken but returns data.hostToken without asserting it
exists; add tests that call the server API (unit or integration) and assert the
response payload always contains a non-empty string hostToken and that
createLobby throws or fails the test when hostToken is missing. Implement at
least one positive test (valid response includes hostToken string) and one
negative test (mock server response missing hostToken should cause createLobby
to reject or throw), and ensure the test framework mocks the HTTP response shape
used by createLobby so the call site’s assumption that lobby.hostToken exists is
validated.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️  Outside diff range comments (2)
src/client/HostLobbyModal.ts (2)
789-824: Validate hostToken presence before starting game.If the cookie expired or was deleted,
hostTokenwill be an empty string (line 803). The request will proceed and likely fail server-side without clear user feedback. Consider checking that the token exists before calling the API.Add validation before the fetch:
} + if (!hostToken) { + console.error("Host token missing - cannot start private lobby"); + alert("Session expired. Please create a new lobby."); + return; + } const response = await fetch(
869-895: Validate hostToken at response time and update caller to handle null case.Your review identifies a real bug. The code has a type-caller mismatch:
- createLobby returns
hostToken: string | nullwith no validation (line 895)- Caller at line 599 uses
encodeURIComponent(lobby.hostToken)directly without checking for null- If the server omits
hostToken, the code silently sets the cookie tohostToken=null(the string), which breaks authenticationFix with validation + type change (preferred):
async function createLobby( creatorClientID: string, -): Promise<{ gameInfo: GameInfo; hostToken: string | null }> { +): Promise<{ gameInfo: GameInfo; hostToken: string }> { // ... fetch logic ... const data = await response.json(); console.log("Success:", data); + if (!data.hostToken || typeof data.hostToken !== "string") { + throw new Error("Server returned invalid hostToken"); + } return { gameInfo: data, hostToken: data.hostToken };This approach is cleaner because:
- You validate once at the source, not at every call site
- Type system now prevents null
- Errors are explicit, not silent cookie corruption
- Caller at line 599 works safely without changes
Add tests to verify the server always returns valid
hostTokenin the response.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
- src/client/HostLobbyModal.ts(4 hunks)
- src/server/GameServer.ts(3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- src/server/GameServer.ts
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-10-21T20:06:04.823Z
Learnt from: Saphereye
PR: openfrontio/OpenFrontIO#2233
File: src/client/HostLobbyModal.ts:891-891
Timestamp: 2025-10-21T20:06:04.823Z
Learning: For the HumansVsNations game mode in `src/client/HostLobbyModal.ts` and related files, the implementation strategy is to generate all nations and adjust their strength for balancing, rather than limiting lobby size based on the number of available nations on the map.
Applied to files:
- src/client/HostLobbyModal.ts
📚 Learning: 2025-06-09T02:20:43.637Z
Learnt from: VariableVince
PR: openfrontio/OpenFrontIO#1110
File: src/client/Main.ts:293-295
Timestamp: 2025-06-09T02:20:43.637Z
Learning: In src/client/Main.ts, during game start in the handleJoinLobby callback, UI elements are hidden using direct DOM manipulation with classList.add("hidden") for consistency. This includes modals, buttons, and error divs. The codebase follows this pattern rather than using component APIs for hiding elements during game transitions.
Applied to files:
- src/client/HostLobbyModal.ts
🧬 Code graph analysis (1)
src/client/HostLobbyModal.ts (1)
src/core/Schemas.ts (1)
GameInfo(125-131)
🪛 GitHub Actions: 🧪 CI
src/client/HostLobbyModal.ts
[error] 599-599: TS2345: Argument of type 'string | null' is not assignable to parameter of type 'string | number | boolean'. Type 'null' is not assignable to type 'string | number | boolean'.
🔇 Additional comments (1)
src/client/HostLobbyModal.ts (1)
801-810: Cookie parsing looks good.The manual parsing correctly handles URL-encoded tokens and
=characters within the value usingslice(1).join("=")anddecodeURIComponent.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️  Outside diff range comments (1)
src/client/HostLobbyModal.ts (1)
869-895: Silent failure: validate server response includes hostToken.Line 895 returns an empty string if
data.hostTokenis missing (??operator), but the function signature promiseshostToken: string. An empty string satisfies the type contract but isn't a valid token. This silent failure means the call site at line 593 will store an invalid cookie without realizing the server didn't provide a token.Apply this diff to fail fast when the server doesn't return a token:
const data = await response.json(); console.log("Success:", data); - return { gameInfo: data, hostToken: data.hostToken ?? "" }; + if (!data.hostToken || typeof data.hostToken !== "string") { + throw new Error("Server did not return a valid hostToken"); + } + return { gameInfo: data, hostToken: data.hostToken };
♻️ Duplicate comments (2)
src/client/HostLobbyModal.ts (2)
800-810: Cookie parsing is fragile and missing URL decoding.The manual parsing at line 807 uses
cookie.split("=")[1], which breaks if the token contains=characters. It also lacksdecodeURIComponentto match the encoding that should be applied when the cookie is set.Apply this diff:
// Parse cookies for hostToken const cookies = document.cookie.split(";"); let hostToken = ""; - for (let i = 0; i < cookies.length; i++) { - const cookie = cookies[i]; - if (cookie.trim().startsWith("hostToken=")) { - hostToken = cookie.split("=")[1]; + for (const cookie of cookies) { + const trimmed = cookie.trim(); + if (trimmed.startsWith("hostToken=")) { + hostToken = decodeURIComponent(trimmed.split("=").slice(1).join("=")); break; } }
593-600: Guard against null hostToken and fix cookie attributes.The code has two critical issues:
Type error:
lobby.hostTokenis typed asstring | null(per the return type at line 871), but line 599 doesn't check for null before using it. If the server fails to return a token, this will cause a runtime error.
Cookie security mismatch: Line 599 sets the cookie without URL encoding,
SameSite, or conditionalSecureflag, but line 822 includes these when deleting. This mismatch will cause the deletion to fail, leaving the token in the browser.Apply this diff to fix both issues:
createLobby(this.lobbyCreatorClientID) .then((lobby) => { + if (!lobby.hostToken) { + throw new Error("Server did not return hostToken for private lobby"); + } this.lobbyId = lobby.gameInfo.gameID; // join lobby const cookieDurationSec = 60 * 60 * 6; //Store cookie for max 6 hours - // getting error here from lobby.hostToken - document.cookie = `hostToken=${lobby.hostToken}; Max-Age=${cookieDurationSec}; Path=/`; + const secure = location.protocol === "https:" ? "; Secure" : ""; + document.cookie = `hostToken=${encodeURIComponent(lobby.hostToken)}; Max-Age=${cookieDurationSec}; Path=/; SameSite=Strict${secure}`; })
🧹 Nitpick comments (2)
src/client/HostLobbyModal.ts (2)
818-818: Use object shorthand.Simplify
{ hostToken: hostToken }to{ hostToken }.- body: JSON.stringify({ hostToken: hostToken }), + body: JSON.stringify({ hostToken }),
869-895: Add tests for server response contract.The function assumes the server will include
hostTokenin the response, but there's no test coverage verifying this contract. Add unit or integration tests that validate:
- Server response includes a non-empty
hostTokenstring- Function throws when
hostTokenis missing or invalid
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
- src/client/HostLobbyModal.ts(4 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-06-09T02:20:43.637Z
Learnt from: VariableVince
PR: openfrontio/OpenFrontIO#1110
File: src/client/Main.ts:293-295
Timestamp: 2025-06-09T02:20:43.637Z
Learning: In src/client/Main.ts, during game start in the handleJoinLobby callback, UI elements are hidden using direct DOM manipulation with classList.add("hidden") for consistency. This includes modals, buttons, and error divs. The codebase follows this pattern rather than using component APIs for hiding elements during game transitions.
Applied to files:
- src/client/HostLobbyModal.ts
📚 Learning: 2025-10-21T20:06:04.823Z
Learnt from: Saphereye
PR: openfrontio/OpenFrontIO#2233
File: src/client/HostLobbyModal.ts:891-891
Timestamp: 2025-10-21T20:06:04.823Z
Learning: For the HumansVsNations game mode in `src/client/HostLobbyModal.ts` and related files, the implementation strategy is to generate all nations and adjust their strength for balancing, rather than limiting lobby size based on the number of available nations on the map.
Applied to files:
- src/client/HostLobbyModal.ts
🧬 Code graph analysis (1)
src/client/HostLobbyModal.ts (1)
src/core/Schemas.ts (1)
GameInfo(125-131)
🔇 Additional comments (1)
src/client/HostLobbyModal.ts (1)
821-822: Cookie deletion looks correct.The deletion properly includes matching attributes (
Path,SameSite, conditionalSecure) and usesMax-Age=0.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️  Outside diff range comments (1)
src/client/HostLobbyModal.ts (1)
1-1: Fix code formatting.The pipeline detected code style issues. Run Prettier to fix:
npx prettier --write src/client/HostLobbyModal.ts
🧹 Nitpick comments (3)
src/client/HostLobbyModal.ts (3)
804-813: Validate hostToken before sending request.If the cookie is missing or expired,
hostTokenwill be an empty string and still sent to the server. The server will likely reject it, but making this explicit on the client improves UX.Add a check before the fetch:
// Parse cookies for hostToken const cookies = document.cookie.split(";"); let hostToken = ""; for (const cookie of cookies) { const trimmed = cookie.trim(); if (trimmed.startsWith("hostToken=")) { hostToken = decodeURIComponent(trimmed.split("=").slice(1).join("=")); break; } } + + if (!hostToken) { + console.error("No hostToken found - cannot start game"); + alert(translateText("errors.host_token_missing")); + return; + } + const response = await fetch(
821-821: Use shorthand property syntax.- body: JSON.stringify({ hostToken: hostToken }), + body: JSON.stringify({ hostToken }),
895-898: Consider validating server response shape.The return statement assumes
dataincludeshostToken, but if the server omits it,nullis returned silently. While the null check at line 595 catches this later, validating here would fail faster and provide clearer error context.Optional improvement:
const data = await response.json(); console.log("Success:", data); + if (typeof data.hostToken !== "string") { + throw new Error("Server response missing or invalid hostToken field"); + } + - return { gameInfo: data, hostToken: data.hostToken ?? null }; + return { gameInfo: data, hostToken: data.hostToken };This makes the function's contract stricter (always returns string) and clarifies where validation fails.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
- src/client/HostLobbyModal.ts(4 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-06-09T02:20:43.637Z
Learnt from: VariableVince
PR: openfrontio/OpenFrontIO#1110
File: src/client/Main.ts:293-295
Timestamp: 2025-06-09T02:20:43.637Z
Learning: In src/client/Main.ts, during game start in the handleJoinLobby callback, UI elements are hidden using direct DOM manipulation with classList.add("hidden") for consistency. This includes modals, buttons, and error divs. The codebase follows this pattern rather than using component APIs for hiding elements during game transitions.
Applied to files:
- src/client/HostLobbyModal.ts
📚 Learning: 2025-10-21T20:06:04.823Z
Learnt from: Saphereye
PR: openfrontio/OpenFrontIO#2233
File: src/client/HostLobbyModal.ts:891-891
Timestamp: 2025-10-21T20:06:04.823Z
Learning: For the HumansVsNations game mode in `src/client/HostLobbyModal.ts` and related files, the implementation strategy is to generate all nations and adjust their strength for balancing, rather than limiting lobby size based on the number of available nations on the map.
Applied to files:
- src/client/HostLobbyModal.ts
🧬 Code graph analysis (1)
src/client/HostLobbyModal.ts (1)
src/core/Schemas.ts (1)
GameInfo(125-131)
🪛 GitHub Actions: 🧪 CI
src/client/HostLobbyModal.ts
[warning] 1-1: Code style issues found in the above file. Run Prettier with --write to fix.
🔇 Additional comments (3)
src/client/HostLobbyModal.ts (3)
598-598: LGTM!Correctly accesses
gameIDfrom the nestedgameInfoobject after the return type change.
600-602: Cookie settings look good.The cookie attributes (Max-Age, Path, SameSite, conditional Secure) are properly set. SameSite=Strict is appropriate for preventing CSRF, though if the app needs to work in cross-site contexts (e.g., embeds), consider SameSite=Lax instead.
872-874: Typed return contract is clear.The explicit return type
{ gameInfo: GameInfo; hostToken: string | null }makes the contract clear. However, the past review comment about adding tests to validate the server response is still relevant.Consider adding tests that:
- Verify the server API endpoint includes
hostTokenin responses- Test the null check at line 595 triggers when server returns null
- Mock various server response shapes to ensure type safety
This would prevent runtime issues if the server response format changes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️  Outside diff range comments (1)
src/client/HostLobbyModal.ts (1)
1-1: Run prettier to fix formatting.The pipeline detected code style issues. Please run:
npx prettier --write src/client/HostLobbyModal.ts
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
- resources/lang/en.json(1 hunks)
- src/client/HostLobbyModal.ts(5 hunks)
🧰 Additional context used
🧠 Learnings (5)
📚 Learning: 2025-08-16T10:52:08.292Z
Learnt from: TheGiraffe3
PR: openfrontio/OpenFrontIO#884
File: resources/lang/en.json:456-461
Timestamp: 2025-08-16T10:52:08.292Z
Learning: In OpenFrontIO, translation files in resources/lang/*.json (except en.json) should not be updated in regular PRs. Only dedicated translation PRs titled "mls" and made by Aotumori should update non-English locale files. Regular PRs should only update en.json when adding or modifying translation keys.
Applied to files:
- resources/lang/en.json
📚 Learning: 2025-06-09T02:20:43.637Z
Learnt from: VariableVince
PR: openfrontio/OpenFrontIO#1110
File: src/client/Main.ts:293-295
Timestamp: 2025-06-09T02:20:43.637Z
Learning: In src/client/Main.ts, during game start in the handleJoinLobby callback, UI elements are hidden using direct DOM manipulation with classList.add("hidden") for consistency. This includes modals, buttons, and error divs. The codebase follows this pattern rather than using component APIs for hiding elements during game transitions.
Applied to files:
- src/client/HostLobbyModal.ts
📚 Learning: 2025-10-21T20:06:04.823Z
Learnt from: Saphereye
PR: openfrontio/OpenFrontIO#2233
File: src/client/HostLobbyModal.ts:891-891
Timestamp: 2025-10-21T20:06:04.823Z
Learning: For the HumansVsNations game mode in `src/client/HostLobbyModal.ts` and related files, the implementation strategy is to generate all nations and adjust their strength for balancing, rather than limiting lobby size based on the number of available nations on the map.
Applied to files:
- src/client/HostLobbyModal.ts
📚 Learning: 2025-08-17T20:48:49.411Z
Learnt from: woodydrn
PR: openfrontio/OpenFrontIO#1836
File: src/client/Main.ts:482-482
Timestamp: 2025-08-17T20:48:49.411Z
Learning: In PR #1836, user woodydrn prefers to keep changes minimal and focused on the specific issue (clientID persistence) rather than refactoring redundant code in JoinLobbyEvent dispatchers. They want to avoid scope creep in focused bug fix PRs.
Applied to files:
- src/client/HostLobbyModal.ts
📚 Learning: 2025-08-12T00:31:50.144Z
Learnt from: scottanderson
PR: openfrontio/OpenFrontIO#1752
File: src/core/game/Game.ts:750-752
Timestamp: 2025-08-12T00:31:50.144Z
Learning: In the OpenFrontIO codebase, changes to the PlayerInteraction interface (like adding canDonateGold and canDonateTroops flags) do not require corresponding updates to src/core/Schemas.ts or server serialization code.
Applied to files:
- src/client/HostLobbyModal.ts
🧬 Code graph analysis (1)
src/client/HostLobbyModal.ts (2)
src/client/Utils.ts (1)
translateText(92-147)src/core/Schemas.ts (1)
GameInfo(125-131)
🪛 GitHub Actions: 🧪 CI
src/client/HostLobbyModal.ts
[warning] 1-1: Code style issues found in the file. Run 'npx prettier --write' to fix.
🔇 Additional comments (5)
resources/lang/en.json (1)
219-220: LGTM! Translation key added correctly.The new
creation_errorkey provides user feedback when lobby creation fails, and the comma fix maintains valid JSON syntax.src/client/HostLobbyModal.ts (4)
595-602: Good: null check and secure cookie attributes.The guard prevents
nullfrom reachingencodeURIComponent, and the cookie now includes properMax-Age,Path,SameSite, and conditionalSecureflags.
812-821: Cookie parsing handles edge cases correctly.The implementation properly URL-decodes the token and handles
=characters within the value usingslice(1).join("=").
832-833: Cookie deletion uses matching attributes.The deletion correctly uses
Max-Age=0with the samePath,SameSite, andSecureflags as creation.
880-882: Clear type contract with null handling.The function signature explicitly returns
string | nullforhostToken, and the implementation uses??to coalesceundefinedtonull, matching the contract.Also applies to: 906-906
| headers: { | ||
| "Content-Type": "application/json", | ||
| }, | ||
| body: JSON.stringify({ hostToken: hostToken }), | 
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Validate hostToken before sending.
If the cookie is missing, hostToken will be an empty string. The server might reject it, but there's no user feedback or validation here.
Consider validating and using shorthand syntax:
+    if (!hostToken) {
+      console.error("No hostToken found in cookies");
+      alert(translateText("errors.missing_host_token"));
+      return;
+    }
     const response = await fetch(
       `${window.location.origin}/${config.workerPath(this.lobbyId)}/api/start_game/${this.lobbyId}`,
       {
         method: "POST",
         headers: {
           "Content-Type": "application/json",
         },
-        body: JSON.stringify({ hostToken: hostToken }),
+        body: JSON.stringify({ hostToken }),
       },
     );There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
♻️ Duplicate comments (1)
src/client/HostLobbyModal.ts (1)
616-623: Remove popup from DOM to prevent memory leak.Each failed lobby creation appends a new
divtodocument.bodythat is never removed. On repeated failures, orphan elements accumulate.Add cleanup logic:
.catch((err) => { console.error(`Failed to create lobby: ${err}`); const popup = document.createElement("div"); - popup.className = "setting-popup"; // TODO: Change to general popup class? + popup.className = "error-popup"; popup.textContent = translateText("private_lobby.creation_error"); document.body.appendChild(popup); + setTimeout(() => { + popup.remove(); + }, 3000); this.close(); });Alternatively, use an existing toast/notification system if available in the codebase.
🧹 Nitpick comments (1)
src/client/HostLobbyModal.ts (1)
829-829: Validate hostToken before sending.If the cookie is missing,
hostTokenwill be an empty string. The server will reject it, but the user sees no feedback until after the network round-trip.Add validation and use shorthand syntax:
+ if (!hostToken) { + console.error("No hostToken found in cookies"); + alert(translateText("errors.missing_host_token")); + return; + } const response = await fetch( `${window.location.origin}/${config.workerPath(this.lobbyId)}/api/start_game/${this.lobbyId}`, { method: "POST", headers: { "Content-Type": "application/json", }, - body: JSON.stringify({ hostToken: hostToken }), + body: JSON.stringify({ hostToken }), }, );
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
- src/client/HostLobbyModal.ts(5 hunks)
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2025-06-09T02:20:43.637Z
Learnt from: VariableVince
PR: openfrontio/OpenFrontIO#1110
File: src/client/Main.ts:293-295
Timestamp: 2025-06-09T02:20:43.637Z
Learning: In src/client/Main.ts, during game start in the handleJoinLobby callback, UI elements are hidden using direct DOM manipulation with classList.add("hidden") for consistency. This includes modals, buttons, and error divs. The codebase follows this pattern rather than using component APIs for hiding elements during game transitions.
Applied to files:
- src/client/HostLobbyModal.ts
📚 Learning: 2025-10-21T20:06:04.823Z
Learnt from: Saphereye
PR: openfrontio/OpenFrontIO#2233
File: src/client/HostLobbyModal.ts:891-891
Timestamp: 2025-10-21T20:06:04.823Z
Learning: For the HumansVsNations game mode in `src/client/HostLobbyModal.ts` and related files, the implementation strategy is to generate all nations and adjust their strength for balancing, rather than limiting lobby size based on the number of available nations on the map.
Applied to files:
- src/client/HostLobbyModal.ts
📚 Learning: 2025-08-17T20:48:49.411Z
Learnt from: woodydrn
PR: openfrontio/OpenFrontIO#1836
File: src/client/Main.ts:482-482
Timestamp: 2025-08-17T20:48:49.411Z
Learning: In PR #1836, user woodydrn prefers to keep changes minimal and focused on the specific issue (clientID persistence) rather than refactoring redundant code in JoinLobbyEvent dispatchers. They want to avoid scope creep in focused bug fix PRs.
Applied to files:
- src/client/HostLobbyModal.ts
📚 Learning: 2025-08-12T00:31:50.144Z
Learnt from: scottanderson
PR: openfrontio/OpenFrontIO#1752
File: src/core/game/Game.ts:750-752
Timestamp: 2025-08-12T00:31:50.144Z
Learning: In the OpenFrontIO codebase, changes to the PlayerInteraction interface (like adding canDonateGold and canDonateTroops flags) do not require corresponding updates to src/core/Schemas.ts or server serialization code.
Applied to files:
- src/client/HostLobbyModal.ts
🧬 Code graph analysis (1)
src/client/HostLobbyModal.ts (2)
src/client/Utils.ts (1)
translateText(92-147)src/core/Schemas.ts (1)
GameInfo(125-131)
🔇 Additional comments (4)
src/client/HostLobbyModal.ts (4)
595-602: Nice: null check prevents type error.The guard correctly narrows the type and ensures
encodeURIComponentreceives a string. Cookie attributes (Max-Age, Path, SameSite, Secure) are properly set.
812-821: LGTM!Cookie parsing correctly handles tokens containing "=" characters and applies URL decoding.
832-833: LGTM!Cookie deletion uses matching attributes (Path, SameSite, Secure) to ensure removal.
880-882: LGTM!The function signature clearly promises both
gameInfoandhostToken(nullable). The return statement correctly coalesces missinghostTokentonull.Also applies to: 906-906
Description:
Fixes #344.
Creates a new hostToken upon lobby creation, saves it as a GameServer attribute and sends it to the host, who then stores it as a cookie. Afterwards, when the host starts the game, it sends the hostToken in the request's body. If the hostToken does not match, the lobby does not start.
The algorithm for creating the hostToken is very simple and not cryptographically secure. The requests also aren't secure exclusive. I figured this isn't a very sensitive piece of information so it doesn't have to be protected too hard.
This PR does nothing for validation of changing private lobby settings, as those are overwritten when the host starts the game anyways.
I couldn't figure out how to do cookies through Express (I think we lack a middleware, and it seems silly to get it for just a single purpose), so technically, the hostToken is sent twice in the request (in cookie and in body), but I feel like this is so minor it doesn't matter too much.
Please complete the following:
Please put your Discord username so you can be contacted if a bug or regression is found:
Lavodan