Skip to content

Conversation

@Lavodan
Copy link
Contributor

@Lavodan Lavodan commented Oct 29, 2025

Description:

Fixes #344.
Uses the host's persistentID upon lobby creation, saves it as a GameServer attribute. Afterwards, when the host starts the game, it sends its persistentID in the request's body. If the ID does not match, the lobby does not start.
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.
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:

  • I have added screenshots for all UI updates
  • I process any text displayed to the user through translateText() and I've added it to the en.json file
  • I have added relevant tests to the test directory
  • I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced

Please put your Discord username so you can be contacted if a bug or regression is found:

Lavodan

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 29, 2025

Walkthrough

Adds optional hostPersistentID end-to-end: GameServer stores it; Worker parses and enforces it for private-game create/start/update flows; client HostLobbyModal sends it on create/start/update and surfaces an error on create failure; schemas and localization updated to accept and surface the field.

Changes

Cohort / File(s) Summary
Server — GameServer
src/server/GameServer.ts
Add `private _hostPersistentID: string
Server — Worker (game lifecycle)
src/server/Worker.ts
Parse optional hostPersistentID on create_game; warn if private game created without it; set game's hostPersistentID when provided. For start_game and update, require matching hostPersistentID against game.getHostPersistentID() and return 403 on mismatch (enforced only when a hostPersistentID exists). Includes type casts to GameConfig where applied.
Client — Host lobby UI
src/client/HostLobbyModal.ts
Import getPersistentID; include hostPersistentID in createLobby, start_game, and PUT /api/game payloads; add error popup and modal-close on lobby creation failure.
Schema — Worker input validation
src/core/WorkerSchemas.ts
Add StartGameSchema with optional UUID hostPersistentID; update CreateGameInputSchema and GameInputSchema to include/merge start-game fields; export type GameConfig = z.infer<typeof GameConfigSchema>.
Localization
resources/lang/en.json
Add private_lobby.creation_error: "Could not create lobby." and adjust punctuation.

Sequence Diagram(s)

sequenceDiagram
    participant C as Client
    participant UI as HostLobbyModal (client)
    participant W as Worker (HTTP)
    participant G as GameServer.Instance

    rect rgba(235,245,255,0.9)
    Note over C,UI: Create lobby — include hostPersistentID when available
    C->>UI: createLobby()
    UI->>W: POST /create_game { ..., hostPersistentID? }
    W->>G: create game instance
    alt hostPersistentID provided
        G->>G: setHostPersistentID(hostPersistentID)
    else not provided and game is private
        W-->>UI: 200 OK + warning logged
    end
    W-->>UI: 200 OK { gameInfo }
    end

    rect rgba(235,255,235,0.9)
    Note over C,W: Start/update private game — host check
    C->>UI: startGame()/updateGame()
    UI->>W: POST /start_game or PUT /api/game { hostPersistentID? }
    W->>G: getHostPersistentID()
    alt game has hostPersistentID
        alt provided && matches
            W-->>UI: 200 OK (proceed)
        else
            W-->>UI: 403 Forbidden
        end
    else
        W-->>UI: proceed (no host check)
    end
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Review focus:
    • Authorization branches and early-return behavior in src/server/Worker.ts.
    • Correctness of setHostPersistentID duplicate detection in src/server/GameServer.ts.
    • Zod schema merges and type exports in src/core/WorkerSchemas.ts.
    • Client error-handling path in src/client/HostLobbyModal.ts.

Possibly related PRs

Suggested labels

Feature - Backend, Feature - Frontend

Suggested reviewers

  • scottanderson

Poem

A tiny token guards the door,
Sent with lobbies, kept in store.
Server checks the holder's key,
Client reports when things go free.
Quiet guard for games once more. 🎮✨

Pre-merge checks

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Linked Issues check ❓ Inconclusive The PR implements host persistentID validation for lobby start (issue #344), but does not fully address pre-start parameter modification restrictions mentioned in the issue. Clarify whether pre-start game parameter modifications by non-hosts are being enforced or deferred for a future update.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding host persistentID validation for private lobbies, directly addressing the PR's core objective.
Description check ✅ Passed The description is related to the changeset and explains the host persistentID validation mechanism for private lobbies that matches the code changes.
Out of Scope Changes check ✅ Passed All changes directly support host persistentID validation: GameServer stores/exposes the ID, Worker enforces authorization, HostLobbyModal sends it, schemas define the contract, and translations support UI messages.

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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/:id has no :hostToken parameter, so req.params.hostToken is always undefined.
  • Early return statements at lines 165 and 172 send no response, hanging the request.
  • Client code at src/client/HostLobbyModal.ts:798-810 sends no hostToken in 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-810 to send the token in body. The fetch call needs a body with hostToken. Where does this.lobbyId or 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

📥 Commits

Reviewing files that changed from the base of the PR and between ecced3c and ee760fc.

📒 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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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

📥 Commits

Reviewing files that changed from the base of the PR and between ee760fc and f9b4c1e.

📒 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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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

📥 Commits

Reviewing files that changed from the base of the PR and between f9b4c1e and 32900db.

📒 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 hostToken into every successful response: res.json({ ...game.gameInfo(), ...hostToken }). The assumption in the client code is valid and safe.

@Lavodan Lavodan changed the title Add token host generation authentication Add hostToken generation and authentication Oct 29, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a 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 hostToken with 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 GameInfo each 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

📥 Commits

Reviewing files that changed from the base of the PR and between 32900db and 1ec4d93.

📒 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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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, hostToken will 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 | null with 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 to hostToken=null (the string), which breaks authentication

Fix 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 hostToken in the response.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1ec4d93 and c74226a.

📒 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 using slice(1).join("=") and decodeURIComponent.

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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.hostToken is missing (?? operator), but the function signature promises hostToken: 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 lacks decodeURIComponent to 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:

  1. Type error: lobby.hostToken is typed as string | 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.

  2. Cookie security mismatch: Line 599 sets the cookie without URL encoding, SameSite, or conditional Secure flag, 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 hostToken in the response, but there's no test coverage verifying this contract. Add unit or integration tests that validate:

  • Server response includes a non-empty hostToken string
  • Function throws when hostToken is missing or invalid
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c74226a and dc1950f.

📒 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, conditional Secure) and uses Max-Age=0.

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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, hostToken will 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 data includes hostToken, but if the server omits it, null is 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

📥 Commits

Reviewing files that changed from the base of the PR and between dc1950f and 0230678.

📒 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 gameID from the nested gameInfo object 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 hostToken in 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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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

📥 Commits

Reviewing files that changed from the base of the PR and between 172b38a and 92b945e.

📒 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_error key 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 null from reaching encodeURIComponent, and the cookie now includes proper Max-Age, Path, SameSite, and conditional Secure flags.


812-821: Cookie parsing handles edge cases correctly.

The implementation properly URL-decodes the token and handles = characters within the value using slice(1).join("=").


832-833: Cookie deletion uses matching attributes.

The deletion correctly uses Max-Age=0 with the same Path, SameSite, and Secure flags as creation.


880-882: Clear type contract with null handling.

The function signature explicitly returns string | null for hostToken, and the implementation uses ?? to coalesce undefined to null, matching the contract.

Also applies to: 906-906

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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 div to document.body that 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, hostToken will 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

📥 Commits

Reviewing files that changed from the base of the PR and between 92b945e and 6d890d1.

📒 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 encodeURIComponent receives 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 gameInfo and hostToken (nullable). The return statement correctly coalesces missing hostToken to null.

Also applies to: 906-906

@Lavodan Lavodan marked this pull request as ready for review October 30, 2025 09:14
@Lavodan Lavodan requested a review from a team as a code owner October 30, 2025 09:14
Copy link
Contributor

@coderabbitai coderabbitai bot left a 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 (5)
src/server/Worker.ts (3)

157-165: Critical: Fix logic error in hostPersistentID validation.

Line 161 checks !hostPersistentID, but hostPersistentID is the object { hostPersistentID: ... }, which is always truthy. The warning never logs even when the ID is null, and the response incorrectly sends hostPersistentID: null.

Apply this fix:

   // Pass creatorClientID to createGame
   const game = gm.createGame(id, gc, creatorClientID);
 
   log.info(
     `Worker ${workerId}: IP ${ipAnonymize(clientIP)} creating ${game.isPublic() ? "Public" : "Private"}${gc?.gameMode ? ` ${gc.gameMode}` : ""} game with id ${id}${creatorClientID ? `, creator: ${creatorClientID}` : ""}`,
   );
 
-  const hostPersistentID = {
-    hostPersistentID: game.setHostPersistentID(req.body.hostPersistentID),
-  };
+  const hostPersistentID = game.setHostPersistentID(req.body.hostPersistentID);
 
   if (!hostPersistentID) {
     log.warn(
       `Worker ${workerId}: IP ${ipAnonymize(clientIP)} id ${id} did not receive hostPersistentID, game may be started by anyone`,
     );
   }
 
-  res.json({ ...game.gameInfo(), ...hostPersistentID });
+  res.json({ ...game.gameInfo(), hostPersistentID });

186-194: Fix indentation: use spaces instead of tabs.

The authorization block uses tabs for indentation while the rest of the file uses spaces. This breaks Prettier formatting.

Apply this diff:

-
-    const hostPersistentID = req.body.hostPersistentID ?? "";
-	const gameHostPersistentID = game.getHostPersistentID();
-	
-	// If gameHostPersistentID is null, no auth is done
-    if (gameHostPersistentID && hostPersistentID !== gameHostPersistentID) {
-      log.info(`cannot start private game ${game.id}, requestor is not host`);
-      res.status(403).json({ success: false });
-      return;
-    }
+
+  const hostPersistentID = req.body.hostPersistentID ?? "";
+  const gameHostPersistentID = game.getHostPersistentID();
+
+  // If gameHostPersistentID is null, no auth is done
+  if (gameHostPersistentID && hostPersistentID !== gameHostPersistentID) {
+    log.info(`cannot start private game ${game.id}, requestor is not host`);
+    res.status(403).json({ success: false });
+    return;
+  }

123-123: Clarify or resolve the TODO comment.

The TODO states "this destroys hostPersistentID requests, but also I cannot put hostPersistentID in configschema". This is unclear. If CreateGameInputSchema strips hostPersistentID, extract it before validation:

   const clientIP = req.ip || req.socket.remoteAddress || "unknown";
+  // Extract hostPersistentID before schema validation
+  const hostPersistentIDFromClient = req.body.hostPersistentID;
-  // TODO: this destroys hostPersistentID requests, but also I cannot put hostPersistentID in configschema
   const result = CreateGameInputSchema.safeParse(req.body);

Then use hostPersistentIDFromClient at line 157. Remove or update the TODO once resolved.

src/server/GameServer.ts (1)

132-142: Fix indentation and consider throwing on duplicate set.

Two issues:

  1. Indentation: Uses tabs instead of spaces (inconsistent with file)
  2. Silent failure: Returning null when already set hides bugs. Worker.ts line 157 doesn't check the return value.

Apply this diff:

-  public setHostPersistentID(id: string): string | null {
-    if (this.getHostPersistentID()) {
-		return null;
-	}
-	this._hostPersistentID = id;
-	return this.getHostPersistentID();
-  }
-
-  public getHostPersistentID(): string | null {
-    return this._hostPersistentID ?? null;
-  }
+  public setHostPersistentID(id: string): string | null {
+    if (this.getHostPersistentID()) {
+      this.log.warn(`hostPersistentID already set for game ${this.id}`);
+      return null;
+    }
+    this._hostPersistentID = id;
+    return this.getHostPersistentID();
+  }
+
+  public getHostPersistentID(): string | null {
+    return this._hostPersistentID ?? null;
+  }

Alternatively, throw an error instead of returning null to make failures explicit. If keeping null return, update Worker.ts to check and log the return value.

src/client/HostLobbyModal.ts (1)

818-818: Validate getPersistentID() returns non-empty string before sending.

Based on the code in src/client/Main.ts lines 628-632, getPersistentID() delegates to getPersistentIDFromCookie() which can return an empty string if a cookie exists with an empty value. Sending an empty hostPersistentID to the server bypasses authorization silently.

Add validation before both requests:

   private async startGame() {
     if (this.useRandomMap) {
       this.selectedMap = this.getRandomMap();
     }
 
     await this.putGameConfig();
     console.log(
       `Starting private game with map: ${GameMapType[this.selectedMap as keyof typeof GameMapType]} ${this.useRandomMap ? " (Randomly selected)" : ""}`,
     );
     this.close();
     const config = await getServerConfigFromClient();
+
+    const hostPersistentID = getPersistentID();
+    if (!hostPersistentID) {
+      console.error("Cannot start game without persistent ID");
+      alert(translateText("errors.missing_persistent_id"));
+      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({ hostPersistentID: getPersistentID() }),
+        body: JSON.stringify({ hostPersistentID }),
       },
     );
     return response;
   }

Apply the same pattern at line 880 in createLobby:

     const id = generateID();
+    const hostPersistentID = getPersistentID();
+    if (!hostPersistentID) {
+      throw new Error("Cannot create lobby without persistent ID");
+    }
     const response = await fetch(
       `/${config.workerPath(id)}/api/create_game/${id}?creatorClientID=${encodeURIComponent(creatorClientID)}`,
       {
         method: "POST",
         headers: {
           "Content-Type": "application/json",
         },
-        body: JSON.stringify({ hostPersistentID: getPersistentID() }),
+        body: JSON.stringify({ hostPersistentID }),
       },
     );
🧹 Nitpick comments (1)
src/client/HostLobbyModal.ts (1)

867-893: Consider narrowing return type after validation.

The return type allows hostPersistentID: string | null, but the caller at line 595 immediately validates and throws if null. Consider either:

  1. Having createLobby throw if the server returns null (fail-fast), or
  2. Narrow the return type to string and document that it throws on null

This makes the type contract clearer:

async function createLobby(
  creatorClientID: string,
): Promise<{ gameInfo: GameInfo; hostPersistentID: string }> {
  // ... existing code ...
  
  const data = await response.json();
  console.log("Success:", data);
  
  const hostPersistentID = data.hostPersistentID ?? null;
  if (!hostPersistentID) {
    throw new Error("Server did not return hostPersistentID");
  }
  
  return { gameInfo: data, hostPersistentID };
}

This eliminates the null check at line 595 since the type guarantees a string.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between eef3d54 and 38741b4.

📒 Files selected for processing (3)
  • src/client/HostLobbyModal.ts (7 hunks)
  • src/server/GameServer.ts (2 hunks)
  • src/server/Worker.ts (3 hunks)
🧰 Additional context used
🧠 Learnings (7)
📚 Learning: 2025-10-08T17:14:49.369Z
Learnt from: Foorack
Repo: openfrontio/OpenFrontIO PR: 2141
File: src/client/ClientGameRunner.ts:228-234
Timestamp: 2025-10-08T17:14:49.369Z
Learning: In `ClientGameRunner.ts`, the `myPlayer` field is always set when `shouldPreventWindowClose()` is called, so the null check in that method is sufficient without needing to fetch it again from `gameView.playerByClientID()`.

Applied to files:

  • src/server/GameServer.ts
  • src/client/HostLobbyModal.ts
📚 Learning: 2025-06-09T02:20:43.637Z
Learnt from: VariableVince
Repo: openfrontio/OpenFrontIO PR: 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
Repo: openfrontio/OpenFrontIO PR: 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
Repo: openfrontio/OpenFrontIO PR: 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
Repo: openfrontio/OpenFrontIO PR: 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
📚 Learning: 2025-10-08T17:14:49.369Z
Learnt from: Foorack
Repo: openfrontio/OpenFrontIO PR: 2141
File: src/client/ClientGameRunner.ts:228-234
Timestamp: 2025-10-08T17:14:49.369Z
Learning: For the window close confirmation feature in `ClientGameRunner.ts`, the troop count requirement (>10,000 troops) from issue #2137 was intentionally removed because it was arbitrary and troop count can be reported as low despite having significant land. The confirmation now triggers for any alive player regardless of troop count.

Applied to files:

  • src/client/HostLobbyModal.ts
📚 Learning: 2025-07-30T19:46:23.797Z
Learnt from: scottanderson
Repo: openfrontio/OpenFrontIO PR: 1436
File: src/server/Worker.ts:93-93
Timestamp: 2025-07-30T19:46:23.797Z
Learning: Input validation at API boundaries is critical for security. All client-provided data, especially parameters used for authorization decisions like creatorClientID in the kick player functionality, must be validated for type, format, length, and business logic constraints before processing.

Applied to files:

  • src/client/HostLobbyModal.ts
🧬 Code graph analysis (1)
src/client/HostLobbyModal.ts (3)
src/client/Utils.ts (1)
  • translateText (92-147)
src/client/Main.ts (1)
  • getPersistentID (628-632)
src/core/Schemas.ts (1)
  • GameInfo (125-131)

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 41f8063 and 5ef91f7.

📒 Files selected for processing (4)
  • src/client/HostLobbyModal.ts (7 hunks)
  • src/core/WorkerSchemas.ts (1 hunks)
  • src/server/GameServer.ts (2 hunks)
  • src/server/Worker.ts (2 hunks)
🧰 Additional context used
🧠 Learnings (10)
📚 Learning: 2025-08-12T00:31:50.144Z
Learnt from: scottanderson
Repo: openfrontio/OpenFrontIO PR: 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/core/WorkerSchemas.ts
  • src/client/HostLobbyModal.ts
📚 Learning: 2025-05-21T04:10:33.435Z
Learnt from: scottanderson
Repo: openfrontio/OpenFrontIO PR: 784
File: src/core/game/StatsImpl.ts:34-38
Timestamp: 2025-05-21T04:10:33.435Z
Learning: In the codebase, PlayerStats is defined as `z.infer<typeof PlayerStatsSchema>` where PlayerStatsSchema has `.optional()` applied at the object level, making PlayerStats a union type that already includes undefined (PlayerStats | undefined).

Applied to files:

  • src/core/WorkerSchemas.ts
📚 Learning: 2025-05-21T04:10:59.706Z
Learnt from: scottanderson
Repo: openfrontio/OpenFrontIO PR: 784
File: src/core/game/StatsImpl.ts:44-53
Timestamp: 2025-05-21T04:10:59.706Z
Learning: In Zod, when a schema has `.optional()` applied at the object level, the TypeScript type inferred using `z.infer` already includes `undefined` in the union type. Therefore, when using such a type as a return type annotation, there's no need to explicitly add `| undefined`.

Applied to files:

  • src/core/WorkerSchemas.ts
📚 Learning: 2025-05-21T04:10:33.435Z
Learnt from: scottanderson
Repo: openfrontio/OpenFrontIO PR: 784
File: src/core/game/StatsImpl.ts:34-38
Timestamp: 2025-05-21T04:10:33.435Z
Learning: In the codebase, PlayerStats is defined as a type inferred from a Zod schema that is marked as optional, which means PlayerStats already includes undefined as a possible type (PlayerStats | undefined).

Applied to files:

  • src/core/WorkerSchemas.ts
📚 Learning: 2025-06-09T02:20:43.637Z
Learnt from: VariableVince
Repo: openfrontio/OpenFrontIO PR: 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
Repo: openfrontio/OpenFrontIO PR: 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
Repo: openfrontio/OpenFrontIO PR: 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-10-08T17:14:49.369Z
Learnt from: Foorack
Repo: openfrontio/OpenFrontIO PR: 2141
File: src/client/ClientGameRunner.ts:228-234
Timestamp: 2025-10-08T17:14:49.369Z
Learning: For the window close confirmation feature in `ClientGameRunner.ts`, the troop count requirement (>10,000 troops) from issue #2137 was intentionally removed because it was arbitrary and troop count can be reported as low despite having significant land. The confirmation now triggers for any alive player regardless of troop count.

Applied to files:

  • src/client/HostLobbyModal.ts
📚 Learning: 2025-10-08T17:14:49.369Z
Learnt from: Foorack
Repo: openfrontio/OpenFrontIO PR: 2141
File: src/client/ClientGameRunner.ts:228-234
Timestamp: 2025-10-08T17:14:49.369Z
Learning: In `ClientGameRunner.ts`, the `myPlayer` field is always set when `shouldPreventWindowClose()` is called, so the null check in that method is sufficient without needing to fetch it again from `gameView.playerByClientID()`.

Applied to files:

  • src/client/HostLobbyModal.ts
  • src/server/GameServer.ts
📚 Learning: 2025-07-30T19:46:23.797Z
Learnt from: scottanderson
Repo: openfrontio/OpenFrontIO PR: 1436
File: src/server/Worker.ts:93-93
Timestamp: 2025-07-30T19:46:23.797Z
Learning: Input validation at API boundaries is critical for security. All client-provided data, especially parameters used for authorization decisions like creatorClientID in the kick player functionality, must be validated for type, format, length, and business logic constraints before processing.

Applied to files:

  • src/client/HostLobbyModal.ts
🧬 Code graph analysis (1)
src/client/HostLobbyModal.ts (4)
src/client/Utils.ts (1)
  • translateText (92-147)
src/client/LangSelector.ts (1)
  • translateText (254-274)
src/client/Main.ts (1)
  • getPersistentID (628-632)
src/core/Schemas.ts (1)
  • GameInfo (125-131)
🪛 GitHub Check: 🔍 ESLint
src/server/Worker.ts

[failure] 156-156:
'hostPersistentID' is assigned a value but never used.

🔇 Additional comments (9)
src/server/GameServer.ts (1)

35-35: LGTM: Clean field declaration.

The private field follows TypeScript conventions with the underscore prefix and nullable typing.

src/server/Worker.ts (2)

160-164: Good: Helpful warning log for missing host ID.

This warning helps identify when private lobbies are created without authorization tokens, which is useful for debugging and security monitoring.


185-193: Authorization logic is sound.

The check correctly:

  • Defaults missing hostPersistentID to empty string
  • Only enforces authorization when gameHostPersistentID is set
  • Returns 403 on mismatch

The empty string handling is consistent with the fallback behavior, though it relies on the earlier GameServer validation (see GameServer.ts review comment).

src/core/WorkerSchemas.ts (1)

4-9: Good: UUID validation rejects invalid formats.

The .uuid() validator ensures hostPersistentID is a valid UUID when present, rejecting empty strings and malformed values at the schema level. The .transform(() => undefined) pattern allows the schema to accept requests containing only hostPersistentID (without full game config), which the Worker extracts separately from req.body.

This addresses empty string concerns in the GameServer/Worker flow.

src/client/HostLobbyModal.ts (5)

29-29: LGTM: Import added for host authentication.

The getPersistentID import supports the new host-based authorization flow.


593-617: Error handling follows codebase patterns.

The updated code:

  • Correctly accesses lobby.gameInfo.gameID per the new return type
  • Displays user feedback on failure using the established popup pattern
  • Follows precedent from UserSettingModal.ts for popup behavior (CSS animation handles disappearance)

Clean error flow.


813-813: Server-side validation handles edge cases.

While getPersistentID() could theoretically return an empty string for anonymous users, the server's UUID validation in WorkerSchemas.ts will reject invalid formats with a 400 error. This is acceptable—server-side validation provides the security boundary.

Optional improvement: add client-side validation to provide earlier feedback, but not required.


862-888: Return type change is consistent with persistentID approach.

The function now returns { gameInfo: GameInfo } instead of GameInfo directly. Since the flow switched from cookie-based tokens to using getPersistentID() (per the conversation with evanpelle), the client doesn't need the server to return hostPersistentID—it reads it from the session when calling start_game.

The wrapping provides a clear API shape, though it adds nesting at the call site (line 595).


875-875: Consistent with start_game flow.

Sending hostPersistentID during lobby creation mirrors the pattern at line 813. Server-side validation ensures format correctness.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (1)
src/client/HostLobbyModal.ts (1)

813-813: Validate persistentID before sending to prevent empty string bypass.

getPersistentID() can return an empty string (when getPersistentIDFromCookie() returns empty). If the client sends an empty hostPersistentID, and the server also stored empty/null during lobby creation, the authorization check at Worker.ts line 185 will pass, allowing anyone to start the game.

Add validation before the fetch:

   await this.putGameConfig();
   console.log(
     `Starting private game with map: ${GameMapType[this.selectedMap as keyof typeof GameMapType]} ${this.useRandomMap ? " (Randomly selected)" : ""}`,
   );
   this.close();
   const config = await getServerConfigFromClient();
+
+  const hostPersistentID = getPersistentID();
+  if (!hostPersistentID) {
+    console.error("Cannot start game: missing persistent ID");
+    alert(translateText("errors.missing_persistent_id"));
+    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({ hostPersistentID: getPersistentID() }),
+      body: JSON.stringify({ hostPersistentID }),
     },
   );
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5ef91f7 and 1dcbc73.

📒 Files selected for processing (2)
  • src/client/HostLobbyModal.ts (4 hunks)
  • src/server/Worker.ts (2 hunks)
🧰 Additional context used
🧠 Learnings (7)
📚 Learning: 2025-10-08T17:14:49.369Z
Learnt from: Foorack
Repo: openfrontio/OpenFrontIO PR: 2141
File: src/client/ClientGameRunner.ts:228-234
Timestamp: 2025-10-08T17:14:49.369Z
Learning: In `ClientGameRunner.ts`, the `myPlayer` field is always set when `shouldPreventWindowClose()` is called, so the null check in that method is sufficient without needing to fetch it again from `gameView.playerByClientID()`.

Applied to files:

  • src/server/Worker.ts
  • src/client/HostLobbyModal.ts
📚 Learning: 2025-06-09T02:20:43.637Z
Learnt from: VariableVince
Repo: openfrontio/OpenFrontIO PR: 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
Repo: openfrontio/OpenFrontIO PR: 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
Repo: openfrontio/OpenFrontIO PR: 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
Repo: openfrontio/OpenFrontIO PR: 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
📚 Learning: 2025-10-08T17:14:49.369Z
Learnt from: Foorack
Repo: openfrontio/OpenFrontIO PR: 2141
File: src/client/ClientGameRunner.ts:228-234
Timestamp: 2025-10-08T17:14:49.369Z
Learning: For the window close confirmation feature in `ClientGameRunner.ts`, the troop count requirement (>10,000 troops) from issue #2137 was intentionally removed because it was arbitrary and troop count can be reported as low despite having significant land. The confirmation now triggers for any alive player regardless of troop count.

Applied to files:

  • src/client/HostLobbyModal.ts
📚 Learning: 2025-07-30T19:46:23.797Z
Learnt from: scottanderson
Repo: openfrontio/OpenFrontIO PR: 1436
File: src/server/Worker.ts:93-93
Timestamp: 2025-07-30T19:46:23.797Z
Learning: Input validation at API boundaries is critical for security. All client-provided data, especially parameters used for authorization decisions like creatorClientID in the kick player functionality, must be validated for type, format, length, and business logic constraints before processing.

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/client/Main.ts (1)
  • getPersistentID (628-632)
🔇 Additional comments (1)
src/client/HostLobbyModal.ts (1)

610-617: Error handling looks good.

The catch block properly logs the error, shows user feedback via popup, and closes the modal. This follows the established codebase pattern from UserSettingModal.ts.

@Lavodan Lavodan marked this pull request as ready for review November 5, 2025 20:23
@Lavodan Lavodan changed the title Add hostToken generation and authentication Validate private lobby creation and modification with host's persistentID Nov 7, 2025
coderabbitai[bot]
coderabbitai bot previously approved these changes Nov 7, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a 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

🧹 Nitpick comments (1)
src/client/HostLobbyModal.ts (1)

771-799: Type safety looks good; consider extracting for readability.

The satisfies Partial<GameConfig> clause ensures type safety for the game config portion while allowing hostPersistentID in the outer object. The nested parentheses are required syntax.

For slightly better readability, you could extract the game config:

 private async putGameConfig() {
   const config = await getServerConfigFromClient();
+  const gameConfig: Partial<GameConfig> = {
+    gameMap: this.selectedMap,
+    gameMapSize: this.compactMap ? GameMapSize.Compact : GameMapSize.Normal,
+    difficulty: this.selectedDifficulty,
+    bots: this.bots,
+    infiniteGold: this.infiniteGold,
+    donateGold: this.donateGold,
+    infiniteTroops: this.infiniteTroops,
+    donateTroops: this.donateTroops,
+    instantBuild: this.instantBuild,
+    randomSpawn: this.randomSpawn,
+    gameMode: this.gameMode,
+    disabledUnits: this.disabledUnits,
+    playerTeams: this.teamCount,
+    ...(this.gameMode === GameMode.Team && this.teamCount === HumansVsNations
+      ? { disableNPCs: false }
+      : { disableNPCs: this.disableNPCs }),
+    maxTimerValue: this.maxTimer === true ? this.maxTimerValue : undefined,
+  };
   const response = await fetch(
     `${window.location.origin}/${config.workerPath(this.lobbyId)}/api/game/${this.lobbyId}`,
     {
       method: "PUT",
       headers: {
         "Content-Type": "application/json",
       },
       body: JSON.stringify({
         hostPersistentID: getPersistentID(),
-        ...({
-          gameMap: this.selectedMap,
-          gameMapSize: this.compactMap ? GameMapSize.Compact : GameMapSize.Normal,
-          difficulty: this.selectedDifficulty,
-          bots: this.bots,
-          infiniteGold: this.infiniteGold,
-          donateGold: this.donateGold,
-          infiniteTroops: this.infiniteTroops,
-          donateTroops: this.donateTroops,
-          instantBuild: this.instantBuild,
-          randomSpawn: this.randomSpawn,
-          gameMode: this.gameMode,
-          disabledUnits: this.disabledUnits,
-          playerTeams: this.teamCount,
-          ...(this.gameMode === GameMode.Team && this.teamCount === HumansVsNations
-            ? { disableNPCs: false }
-            : { disableNPCs: this.disableNPCs }),
-          maxTimerValue: this.maxTimer === true ? this.maxTimerValue : undefined,
-        } satisfies Partial<GameConfig>),
+        ...gameConfig,
       }),
     },
   );
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between de8d21f and 92ba79c.

📒 Files selected for processing (3)
  • resources/lang/en.json (1 hunks)
  • src/client/HostLobbyModal.ts (5 hunks)
  • src/server/GameServer.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • resources/lang/en.json
  • src/server/GameServer.ts
🧰 Additional context used
🧠 Learnings (8)
📓 Common learnings
Learnt from: woodydrn
Repo: openfrontio/OpenFrontIO PR: 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.
📚 Learning: 2025-06-09T02:20:43.637Z
Learnt from: VariableVince
Repo: openfrontio/OpenFrontIO PR: 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
Repo: openfrontio/OpenFrontIO PR: 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
Repo: openfrontio/OpenFrontIO PR: 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
Repo: openfrontio/OpenFrontIO PR: 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
📚 Learning: 2025-10-08T17:14:49.369Z
Learnt from: Foorack
Repo: openfrontio/OpenFrontIO PR: 2141
File: src/client/ClientGameRunner.ts:228-234
Timestamp: 2025-10-08T17:14:49.369Z
Learning: For the window close confirmation feature in `ClientGameRunner.ts`, the troop count requirement (>10,000 troops) from issue #2137 was intentionally removed because it was arbitrary and troop count can be reported as low despite having significant land. The confirmation now triggers for any alive player regardless of troop count.

Applied to files:

  • src/client/HostLobbyModal.ts
📚 Learning: 2025-10-08T17:14:49.369Z
Learnt from: Foorack
Repo: openfrontio/OpenFrontIO PR: 2141
File: src/client/ClientGameRunner.ts:228-234
Timestamp: 2025-10-08T17:14:49.369Z
Learning: In `ClientGameRunner.ts`, the `myPlayer` field is always set when `shouldPreventWindowClose()` is called, so the null check in that method is sufficient without needing to fetch it again from `gameView.playerByClientID()`.

Applied to files:

  • src/client/HostLobbyModal.ts
📚 Learning: 2025-07-30T19:46:23.797Z
Learnt from: scottanderson
Repo: openfrontio/OpenFrontIO PR: 1436
File: src/server/Worker.ts:93-93
Timestamp: 2025-07-30T19:46:23.797Z
Learning: Input validation at API boundaries is critical for security. All client-provided data, especially parameters used for authorization decisions like creatorClientID in the kick player functionality, must be validated for type, format, length, and business logic constraints before processing.

Applied to files:

  • src/client/HostLobbyModal.ts
🧬 Code graph analysis (1)
src/client/HostLobbyModal.ts (3)
src/client/Utils.ts (1)
  • translateText (92-147)
src/client/Main.ts (1)
  • getPersistentID (628-632)
src/core/game/Game.ts (1)
  • HumansVsNations (56-56)
🔇 Additional comments (4)
src/client/HostLobbyModal.ts (4)

29-29: Clean import addition.

The getPersistentID import is necessary and well-placed alongside the existing JoinLobbyEvent import.


627-634: Good error handling for lobby creation.

The catch block properly surfaces creation failures to the user with a localized message, following the established pattern from UserSettingModal.ts.


839-839: Consistent persistentID usage.

The startGame request properly includes hostPersistentID, consistent with the validation approach used in putGameConfig and createLobby. Server-side UUID validation will handle empty or invalid IDs.


899-899: Lobby creation properly includes host validation.

The createLobby function correctly sends hostPersistentID to establish the host identity at creation time. This is consistent with the validation approach used throughout the file.

maxTimerValue:
this.maxTimer === true ? this.maxTimerValue : undefined,
} satisfies Partial<GameConfig>),
hostPersistentID: getPersistentID(),
Copy link
Collaborator

Choose a reason for hiding this comment

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

instead of the body, let's put it as x-persistent-id header.

headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ hostPersistentID: getPersistentID() }),
Copy link
Collaborator

Choose a reason for hiding this comment

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

same here, put in the header

Comment on lines +4 to +6
export const StartGameSchema = z.object({
hostPersistentID: z.string().uuid().optional(),
});
Copy link
Collaborator

Choose a reason for hiding this comment

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

this can be removed now that we are using headers.


export const GameInputSchema = GameConfigSchema.partial();
export const GameInputSchema =
GameConfigSchema.partial().merge(StartGameSchema);
Copy link
Collaborator

Choose a reason for hiding this comment

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

merging the persistent id in to the game config could make us accidently leak persistent id if we're not careful.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Instead lets do
{
persistentID: ...
config: GameConfig
}

@Lavodan
Copy link
Contributor Author

Lavodan commented Nov 16, 2025

If someone else wants to take this I'm down

@Lavodan Lavodan marked this pull request as draft November 17, 2025 09:58
@ryanbarlow97 ryanbarlow97 modified the milestones: v27, v28 Nov 23, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a 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

🧹 Nitpick comments (1)
src/server/GameServer.ts (1)

133-142: Consider adding input validation for defense in depth.

The setHostPersistentID method accepts any string without validation. While server-side Zod validation should prevent empty or malformed IDs at the API boundary, adding a defensive check here would catch programming errors earlier and make the intent clearer.

Apply this diff to add validation:

 public setHostPersistentID(id: string): void {
   if (this.getHostPersistentID()) {
     throw new Error("hostPersistentID is already set");
   }
+  if (!id || !id.trim()) {
+    throw new Error("hostPersistentID cannot be empty");
+  }
   this._hostPersistentID = id;
 }

Based on learnings, server-side validation is the authoritative check, so this is optional defensive coding rather than a critical fix.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 92ba79c and 6c3ebea.

📒 Files selected for processing (3)
  • resources/lang/en.json (1 hunks)
  • src/client/HostLobbyModal.ts (5 hunks)
  • src/server/GameServer.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • resources/lang/en.json
🧰 Additional context used
🧠 Learnings (8)
📓 Common learnings
Learnt from: woodydrn
Repo: openfrontio/OpenFrontIO PR: 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.
📚 Learning: 2025-10-08T17:14:49.369Z
Learnt from: Foorack
Repo: openfrontio/OpenFrontIO PR: 2141
File: src/client/ClientGameRunner.ts:228-234
Timestamp: 2025-10-08T17:14:49.369Z
Learning: In `ClientGameRunner.ts`, the `myPlayer` field is always set when `shouldPreventWindowClose()` is called, so the null check in that method is sufficient without needing to fetch it again from `gameView.playerByClientID()`.

Applied to files:

  • src/server/GameServer.ts
  • src/client/HostLobbyModal.ts
📚 Learning: 2025-06-09T02:20:43.637Z
Learnt from: VariableVince
Repo: openfrontio/OpenFrontIO PR: 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
Repo: openfrontio/OpenFrontIO PR: 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-12T00:31:50.144Z
Learnt from: scottanderson
Repo: openfrontio/OpenFrontIO PR: 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
📚 Learning: 2025-08-17T20:48:49.411Z
Learnt from: woodydrn
Repo: openfrontio/OpenFrontIO PR: 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-10-08T17:14:49.369Z
Learnt from: Foorack
Repo: openfrontio/OpenFrontIO PR: 2141
File: src/client/ClientGameRunner.ts:228-234
Timestamp: 2025-10-08T17:14:49.369Z
Learning: For the window close confirmation feature in `ClientGameRunner.ts`, the troop count requirement (>10,000 troops) from issue #2137 was intentionally removed because it was arbitrary and troop count can be reported as low despite having significant land. The confirmation now triggers for any alive player regardless of troop count.

Applied to files:

  • src/client/HostLobbyModal.ts
📚 Learning: 2025-07-30T19:46:23.797Z
Learnt from: scottanderson
Repo: openfrontio/OpenFrontIO PR: 1436
File: src/server/Worker.ts:93-93
Timestamp: 2025-07-30T19:46:23.797Z
Learning: Input validation at API boundaries is critical for security. All client-provided data, especially parameters used for authorization decisions like creatorClientID in the kick player functionality, must be validated for type, format, length, and business logic constraints before processing.

Applied to files:

  • src/client/HostLobbyModal.ts
🧬 Code graph analysis (1)
src/client/HostLobbyModal.ts (6)
src/client/LangSelector.ts (1)
  • translateText (258-278)
src/client/Utils.ts (1)
  • translateText (92-147)
src/client/Main.ts (1)
  • getPersistentID (614-618)
src/core/game/Game.ts (1)
  • HumansVsNations (56-56)
src/core/Schemas.ts (1)
  • GameConfig (84-84)
src/core/WorkerSchemas.ts (1)
  • GameConfig (15-15)
🔇 Additional comments (6)
src/server/GameServer.ts (1)

35-35: Clean addition of the host persistent ID field.

The nullable string field properly represents that a hostPersistentID may not be set initially. The initialization to null is explicit and clear.

src/client/HostLobbyModal.ts (5)

30-30: Good import of the persistent ID helper.

Importing getPersistentID from Main enables the persistent ID validation flow. The import is correctly placed with other Main imports.


614-621: Error handling follows codebase precedent.

The .catch() block properly logs the error, displays a user-facing popup using setting-popup class (consistent with UserSettingModal.ts:221), and closes the modal. The popup disappears via CSS animation as designed.


758-786: Consider moving hostPersistentID to request header.

The implementation correctly includes hostPersistentID in the request body before spreading the Partial<GameConfig>. However, a past comment from evanpelle suggested using an x-persistent-id header instead of the body for better separation of authorization data from game configuration.

If the header approach is preferred, coordinate with server-side changes to parse the header. Otherwise, the current body approach works correctly.

Based on past review comments, this design choice may have been discussed elsewhere.


826-826: Persistent ID correctly included in start game request.

The hostPersistentID is properly sent in the request body to authorize the game start. The same header vs. body consideration from putGameConfig applies here.


886-886: Persistent ID correctly included in lobby creation.

The hostPersistentID is properly sent when creating the lobby, establishing the authorization token at creation time. The structure is clean and straightforward.

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.

API has no permission restrictions on private lobbies

3 participants