diff --git a/src/client/LocalServer.ts b/src/client/LocalServer.ts index 73e4961d2..e8ec51d25 100644 --- a/src/client/LocalServer.ts +++ b/src/client/LocalServer.ts @@ -1,3 +1,4 @@ +import { z } from "zod/v4"; import { EventBus } from "../core/EventBus"; import { AllPlayersStats, @@ -8,7 +9,7 @@ import { Intent, PlayerRecord, ServerMessage, - ServerStartGameMessageSchema, + ServerStartGameMessage, Turn, } from "../core/Schemas"; import { createGameRecord, decompressGameRecord, replacer } from "../core/Util"; @@ -75,14 +76,11 @@ export class LocalServer { if (this.lobbyConfig.gameStartInfo === undefined) { throw new Error("missing gameStartInfo"); } - this.clientMessage( - ServerStartGameMessageSchema.parse({ - type: "start", - gameID: this.lobbyConfig.gameStartInfo.gameID, - gameStartInfo: this.lobbyConfig.gameStartInfo, - turns: [], - }), - ); + this.clientMessage({ + type: "start", + gameStartInfo: this.lobbyConfig.gameStartInfo, + turns: [], + } satisfies ServerStartGameMessage); } pause() { @@ -94,9 +92,14 @@ export class LocalServer { } onMessage(message: string) { - const clientMsg: ClientMessage = ClientMessageSchema.parse( - JSON.parse(message), - ); + const result = ClientMessageSchema.safeParse(JSON.parse(message)); + if (!result.success) { + const error = z.prettifyError(result.error); + console.error("Error parsing client message", error); + return; + } + + const clientMsg: ClientMessage = result.data; if (clientMsg.type === "intent") { if (this.lobbyConfig.gameRecord) { // If we are replaying a game, we don't want to process intents @@ -211,12 +214,15 @@ export class LocalServer { record.turns = []; } // For unload events, sendBeacon is the only reliable method - const blob = new Blob( - [JSON.stringify(GameRecordSchema.parse(record), replacer)], - { - type: "application/json", - }, - ); + const result = GameRecordSchema.safeParse(record); + if (!result.success) { + const error = z.prettifyError(result.error); + console.error("Error parsing game record", error); + return; + } + const blob = new Blob([JSON.stringify(result.data, replacer)], { + type: "application/json", + }); const workerPath = this.lobbyConfig.serverConfig.workerPath( this.lobbyConfig.gameStartInfo.gameID, ); diff --git a/src/client/Transport.ts b/src/client/Transport.ts index f4b732f8d..314a63423 100644 --- a/src/client/Transport.ts +++ b/src/client/Transport.ts @@ -1,3 +1,4 @@ +import { z } from "zod/v4"; import { EventBus, GameEvent } from "../core/EventBus"; import { AllPlayers, @@ -309,14 +310,13 @@ export class Transport { onconnect(); }; this.socket.onmessage = (event: MessageEvent) => { - try { - const serverMsg = ServerMessageSchema.parse(JSON.parse(event.data)); - this.onmessage(serverMsg); - } catch (error) { - console.error( - `Failed to process server message ${event.data}: ${error}, ${error.stack}`, - ); + const result = ServerMessageSchema.safeParse(JSON.parse(event.data)); + if (!result.success) { + const error = z.prettifyError(result.error); + console.error("Error parsing server message", error); + return; } + this.onmessage(result.data); }; this.socket.onerror = (err) => { console.error("Socket encountered error: ", err, "Closing socket"); diff --git a/src/core/ApiSchemas.ts b/src/core/ApiSchemas.ts index aec1ae506..f82d8b1e0 100644 --- a/src/core/ApiSchemas.ts +++ b/src/core/ApiSchemas.ts @@ -1,4 +1,4 @@ -import { z } from "zod"; +import { z } from "zod/v4"; import { base64urlToUuid } from "./Base64"; export const RefreshResponseSchema = z.object({ diff --git a/src/core/Schemas.ts b/src/core/Schemas.ts index dcc5d7699..0f9d7d860 100644 --- a/src/core/Schemas.ts +++ b/src/core/Schemas.ts @@ -1,4 +1,4 @@ -import { z } from "zod"; +import { z } from "zod/v4"; import quickChatData from "../../resources/QuickChat.json" with { type: "json" }; import { AllPlayers, @@ -76,14 +76,14 @@ export type ClientMessage = | ClientLogMessage | ClientHashMessage; export type ServerMessage = - | ServerSyncMessage + | ServerTurnMessage | ServerStartGameMessage | ServerPingMessage | ServerDesyncMessage | ServerPrestartMessage | ServerErrorMessage; -export type ServerSyncMessage = z.infer; +export type ServerTurnMessage = z.infer; export type ServerStartGameMessage = z.infer< typeof ServerStartGameMessageSchema >; @@ -101,7 +101,7 @@ export type ClientHashMessage = z.infer; export type AllPlayersStats = z.infer; export type Player = z.infer; export type GameStartInfo = z.infer; -const PlayerTypeSchema = z.nativeEnum(PlayerType); +const PlayerTypeSchema = z.enum(PlayerType); export interface GameInfo { gameID: GameID; @@ -127,18 +127,18 @@ export enum LogSeverity { // export const GameConfigSchema = z.object({ - gameMap: z.nativeEnum(GameMapType), - difficulty: z.nativeEnum(Difficulty), - gameType: z.nativeEnum(GameType), - gameMode: z.nativeEnum(GameMode), + gameMap: z.enum(GameMapType), + difficulty: z.enum(Difficulty), + gameType: z.enum(GameType), + gameMode: z.enum(GameMode), disableNPCs: z.boolean(), bots: z.number().int().min(0).max(400), infiniteGold: z.boolean(), infiniteTroops: z.boolean(), instantBuild: z.boolean(), maxPlayers: z.number().optional(), - disabledUnits: z.nativeEnum(UnitType).array().optional(), - playerTeams: z.union([z.number().optional(), z.literal(Duos)]), + disabledUnits: z.enum(UnitType).array().optional(), + playerTeams: z.union([z.number(), z.literal(Duos)]).optional(), }); export const TeamSchema = z.string(); @@ -150,8 +150,8 @@ const SafeString = z ) .max(1000); -export const PersistentIdSchema = z.string().uuid(); -const JwtTokenSchema = z.string().jwt(); +export const PersistentIdSchema = z.uuid(); +const JwtTokenSchema = z.jwt(); const TokenSchema = z .string() .refine( @@ -268,14 +268,14 @@ export const TargetTroopRatioIntentSchema = BaseIntentSchema.extend({ export const BuildUnitIntentSchema = BaseIntentSchema.extend({ type: z.literal("build_unit"), - unit: z.nativeEnum(UnitType), + unit: z.enum(UnitType), x: z.number(), y: z.number(), }); export const UpgradeStructureIntentSchema = BaseIntentSchema.extend({ type: z.literal("upgrade_structure"), - unit: z.nativeEnum(UnitType), + unit: z.enum(UnitType), unitId: z.number(), }); @@ -426,7 +426,7 @@ export const ClientHashSchema = z.object({ export const ClientLogMessageSchema = z.object({ type: z.literal("log"), - severity: z.nativeEnum(LogSeverity), + severity: z.enum(LogSeverity), log: ID, }); diff --git a/src/core/StatsSchemas.ts b/src/core/StatsSchemas.ts index e9a643766..e6e6fa519 100644 --- a/src/core/StatsSchemas.ts +++ b/src/core/StatsSchemas.ts @@ -1,4 +1,4 @@ -import { z } from "zod"; +import { z } from "zod/v4"; import { UnitType } from "./game/Game"; export const BombUnitSchema = z.union([ diff --git a/src/core/WorkerSchemas.ts b/src/core/WorkerSchemas.ts index 0a06b1571..aa53eaa9d 100644 --- a/src/core/WorkerSchemas.ts +++ b/src/core/WorkerSchemas.ts @@ -1,4 +1,4 @@ -import { z } from "zod"; +import { z } from "zod/v4"; import { GameConfigSchema } from "./Schemas"; export const CreateGameInputSchema = GameConfigSchema.or( diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts index 102b50a32..d9c54bfdc 100644 --- a/src/core/configuration/DefaultConfig.ts +++ b/src/core/configuration/DefaultConfig.ts @@ -1,5 +1,5 @@ import { JWK } from "jose"; -import { z } from "zod"; +import { z } from "zod/v4"; import { Difficulty, Duos, @@ -98,8 +98,13 @@ export abstract class DefaultServerConfig implements ServerConfig { const jwksUrl = this.jwtIssuer() + "/.well-known/jwks.json"; console.log(`Fetching JWKS from ${jwksUrl}`); const response = await fetch(jwksUrl); - const jwks = JwksSchema.parse(await response.json()); - this.publicKey = jwks.keys[0]; + const result = JwksSchema.safeParse(await response.json()); + if (!result.success) { + const error = z.prettifyError(result.error); + console.error("Error parsing JWKS", error); + throw new Error("Invalid JWKS"); + } + this.publicKey = result.data.keys[0]; return this.publicKey; } otelEnabled(): boolean { diff --git a/src/server/GameServer.ts b/src/server/GameServer.ts index 37c51c7f1..19439a089 100644 --- a/src/server/GameServer.ts +++ b/src/server/GameServer.ts @@ -15,8 +15,8 @@ import { ServerDesyncSchema, ServerErrorMessage, ServerPrestartMessageSchema, - ServerStartGameMessageSchema, - ServerTurnMessageSchema, + ServerStartGameMessage, + ServerTurnMessage, Turn, } from "../core/Schemas"; import { createGameRecord } from "../core/Util"; @@ -327,7 +327,7 @@ export class GameServer { // if no client connects/pings. this.lastPingUpdate = Date.now(); - this.gameStartInfo = GameStartInfoSchema.parse({ + const result = GameStartInfoSchema.safeParse({ gameID: this.id, config: this.gameConfig, players: this.activeClients.map((c) => ({ @@ -335,7 +335,13 @@ export class GameServer { clientID: c.clientID, flag: c.flag, })), - } satisfies GameStartInfo); + }); + if (!result.success) { + const error = z.prettifyError(result.error); + this.log.error("Error parsing game start info", { message: error }); + return; + } + this.gameStartInfo = result.data satisfies GameStartInfo; this.endTurnIntervalID = setInterval( () => this.endTurn(), @@ -357,13 +363,11 @@ export class GameServer { private sendStartGameMsg(ws: WebSocket, lastTurn: number) { try { ws.send( - JSON.stringify( - ServerStartGameMessageSchema.parse({ - type: "start", - turns: this.turns.slice(lastTurn), - gameStartInfo: this.gameStartInfo, - }), - ), + JSON.stringify({ + type: "start", + turns: this.turns.slice(lastTurn), + gameStartInfo: this.gameStartInfo, + } satisfies ServerStartGameMessage), ); } catch (error) { throw new Error( @@ -386,22 +390,10 @@ export class GameServer { this.handleSynchronization(); this.checkDisconnectedStatus(); - let msg = ""; - try { - msg = JSON.stringify( - ServerTurnMessageSchema.parse({ - type: "turn", - turn: pastTurn, - }), - ); - } catch (error) { - this.log.info( - `error sending message for game: ${error.substring(0, 250)}`, - {}, - ); - return; - } - + const msg = JSON.stringify({ + type: "turn", + turn: pastTurn, + } satisfies ServerTurnMessage); this.activeClients.forEach((c) => { c.ws.send(msg); }); diff --git a/src/server/jwt.ts b/src/server/jwt.ts index d056b8445..de2767b70 100644 --- a/src/server/jwt.ts +++ b/src/server/jwt.ts @@ -1,4 +1,5 @@ import { jwtVerify } from "jose"; +import { z } from "zod/v4"; import { TokenPayload, TokenPayloadSchema, @@ -32,7 +33,13 @@ export async function verifyClientToken( audience, maxTokenAge: "6 days", }); - const claims = TokenPayloadSchema.parse(payload); + const result = TokenPayloadSchema.safeParse(payload); + if (!result.success) { + const error = z.prettifyError(result.error); + console.warn("Error parsing token payload", error); + return false; + } + const claims = result.data; const persistentId = claims.sub; return { persistentId, claims }; } catch (e) {