Add allowlist for private lobbies (OFM) (#4351)

**Add approved & assigned issue number here:**

Resolves #4349

## Description:

1. **Private-lobby allowlist.** `create_game` accepts an optional
`allowedPublicIds`. It's set by whoever creates the lobby (admin-token
gated, no client UI), the game server pulls it out of the config so it's
never broadcast to clients or written to the game record, and it rejects
any joiner whose OF publicId isn't on the list before they take a slot
(stickily, so they can't retry on reconnect). Lobbies created without it
behave exactly as before.

It is off by default
Previews:
<img width="241" height="140" alt="image"
src="https://github.com/user-attachments/assets/30c4e47b-399d-4720-b25b-a04c63668577"
/>
<img width="982" height="456" alt="image"
src="https://github.com/user-attachments/assets/1b5c68b7-9b99-4ccc-b987-e70c8ec25dce"
/>
<img width="547" height="369" alt="image"
src="https://github.com/user-attachments/assets/1623090b-ea2b-4657-9cd8-903fbabca51b"
/>


I am not able to manually test all of it since it needs to also run the
auth API (infra) and actually be connected to disc and whatnot (but
still tested the refused flow)..
Also, we would need to place some guards and visual error feedback, but
since this only would affect casual of players and is more of a
improvement to the feature, I will consider it out of scope for now.

## Please complete the following:

- [x] I have added screenshots for all UI updates (no UI changes in this
PR)
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file (no new user-facing text)
- [x] I have added relevant tests to the test directory

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

zixer._
This commit is contained in:
Zixer1
2026-06-21 18:10:48 -04:00
committed by evanpelle
parent 88aad8501e
commit 18ef15d3ae
7 changed files with 188 additions and 2 deletions
+1 -1
View File
@@ -31,7 +31,7 @@ export class GameManager {
joinClient(
client: Client,
gameID: GameID,
): "joined" | "kicked" | "rejected" | "not_found" {
): "joined" | "kicked" | "rejected" | "not_allowlisted" | "not_found" {
const game = this.games.get(gameID);
if (!game) return "not_found";
return game.joinClient(client);
+21 -1
View File
@@ -181,6 +181,9 @@ export class GameServer {
this.gameConfig.disableAlliances =
gameConfig.disableAlliances ?? undefined;
}
if (gameConfig.allowedPublicIds !== undefined) {
this.gameConfig.allowedPublicIds = gameConfig.allowedPublicIds;
}
if (gameConfig.waterNukes !== undefined) {
this.gameConfig.waterNukes = gameConfig.waterNukes ?? undefined;
}
@@ -202,11 +205,28 @@ export class GameServer {
return clientID;
}
public joinClient(client: Client): "joined" | "kicked" | "rejected" {
public joinClient(
client: Client,
): "joined" | "kicked" | "rejected" | "not_allowlisted" {
if (this.kickedPersistentIds.has(client.persistentID)) {
return "kicked";
}
// OFM: if an allowlist is set, only those publicIds may join. Re-checked on
// every join attempt
const allowedPublicIds = this.gameConfig.allowedPublicIds;
if (
allowedPublicIds !== undefined &&
allowedPublicIds.length > 0 &&
(client.publicId === undefined ||
!allowedPublicIds.includes(client.publicId))
) {
this.log.warn("client not on allowlist, rejecting", {
clientID: client.clientID,
});
return "not_allowlisted";
}
if (
this.gameConfig.maxPlayers &&
this.activeClients.length >= this.gameConfig.maxPlayers
+6
View File
@@ -499,6 +499,12 @@ export async function startWorker() {
workerId,
});
ws.close(1002, "Cannot join game");
} else if (joinResult === "not_allowlisted") {
log.info(`client not whitelisted for game ${clientMsg.gameID}`, {
gameID: clientMsg.gameID,
workerId,
});
ws.close(1002, "You are not whitelisted");
} else if (joinResult === "rejected") {
log.info(`client rejected from game ${clientMsg.gameID}`, {
gameID: clientMsg.gameID,