From 172945bbfca51327ef12e99c75c6dd3bf72316a3 Mon Sep 17 00:00:00 2001 From: FloPinguin <25036848+FloPinguin@users.noreply.github.com> Date: Tue, 3 Feb 2026 14:21:17 +0100 Subject: [PATCH 01/20] Missing translation: "host_modal.crowded" (#3102) ## Description: Join Lobby Modal will show: Crowded modifier Enabled ## Please complete the following: - [X] I have added screenshots for all UI updates - [X] I process any text displayed to the user through translateText() and I've added it to the en.json file - [X] I have added relevant tests to the test directory - [X] 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: FloPinguin --- resources/lang/en.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/resources/lang/en.json b/resources/lang/en.json index a25b0fb6d..44ebe941a 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -447,7 +447,8 @@ "gold_multiplier": "Gold multiplier", "gold_multiplier_placeholder": "2.0x", "starting_gold": "Starting gold", - "starting_gold_placeholder": "5000000" + "starting_gold_placeholder": "5000000", + "crowded": "Crowded modifier" }, "team_colors": { "red": "Red", From 8851a110a32bb60e16673b69447fb2a906a3f777 Mon Sep 17 00:00:00 2001 From: Aotumuri Date: Tue, 3 Feb 2026 22:22:27 +0900 Subject: [PATCH 02/20] fix: align leaderboard modal background with other modals (#3100) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description: Aligned the LeaderboardModal background styling with other modals (e.g., HelpModal). Uses the same inline background/blur/border treatment to improve visual consistency. before leaderboard スクリーンショット 2026-02-03 22 07 58 other スクリーンショット 2026-02-03 22 08 07 after leaderboard スクリーンショット 2026-02-03 22 08 20 other スクリーンショット 2026-02-03 22 08 35 ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] 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: aotumuri --- src/client/LeaderboardModal.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/client/LeaderboardModal.ts b/src/client/LeaderboardModal.ts index e88460e03..33c4c809f 100644 --- a/src/client/LeaderboardModal.ts +++ b/src/client/LeaderboardModal.ts @@ -79,7 +79,9 @@ export class LeaderboardModal extends BaseModal { const content = html`
${modalHeader({ titleContent: html` From 3429c9d8e9731ae022bcf906c984c069bed5df53 Mon Sep 17 00:00:00 2001 From: Aotumuri Date: Tue, 3 Feb 2026 22:24:50 +0900 Subject: [PATCH 03/20] fix: show full flag names in selector (#3103) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description: Flag selector country names no longer truncate with ellipses; they wrap normally for readability. before スクリーンショット 2026-02-03 22 21 01 after スクリーンショット 2026-02-03 22 21 11 ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] 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: aotumuri Co-authored-by: Ryan <7389646+ryanbarlow97@users.noreply.github.com> --- src/client/FlagInputModal.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/FlagInputModal.ts b/src/client/FlagInputModal.ts index fe2ea7c6b..4b864bb15 100644 --- a/src/client/FlagInputModal.ts +++ b/src/client/FlagInputModal.ts @@ -73,7 +73,7 @@ export class FlagInputModal extends BaseModal { }} /> ${country.name} From f5cba9a495acf867c774328adeeb1fe775af8978 Mon Sep 17 00:00:00 2001 From: Aotumuri Date: Tue, 3 Feb 2026 22:26:47 +0900 Subject: [PATCH 04/20] fix: remove incorrect host display in JoinLobbyModal (Public game) (#3099) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description: Hide the host badge when joining public lobbies before スクリーンショット 2026-02-03 21 45 45 after スクリーンショット 2026-02-03 21 44 00 ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] 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: aotumuri --- src/client/JoinLobbyModal.ts | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/client/JoinLobbyModal.ts b/src/client/JoinLobbyModal.ts index 20e0e79b0..59ee5b48a 100644 --- a/src/client/JoinLobbyModal.ts +++ b/src/client/JoinLobbyModal.ts @@ -55,6 +55,10 @@ export class JoinLobbyModal extends BaseModal { private countdownTimerId: number | null = null; private handledJoinTimeout = false; + private isPrivateLobby(): boolean { + return this.gameConfig?.gameType === GameType.Private; + } + private readonly handleLobbyInfo = (event: LobbyInfoEvent) => { const lobby = event.lobby; if (!this.currentLobbyId || lobby.gameID !== this.currentLobbyId) { @@ -91,6 +95,9 @@ export class JoinLobbyModal extends BaseModal { : translateText("public_lobby.started"); const maxPlayers = this.gameConfig?.maxPlayers ?? 0; const playerCount = this.playerCount; + const hostClientID = this.isPrivateLobby() + ? (this.lobbyCreatorClientID ?? "") + : ""; const content = html`
this.closeAndLeave(), ariaLabel: translateText("common.close"), rightContent: - this.currentLobbyId && - this.gameConfig?.gameType === GameType.Private + this.currentLobbyId && this.isPrivateLobby() ? html` ` @@ -129,7 +135,7 @@ export class JoinLobbyModal extends BaseModal { class="mt-6" .gameMode=${this.gameConfig?.gameMode ?? GameMode.FFA} .clients=${this.players} - .lobbyCreatorClientID=${this.lobbyCreatorClientID} + .lobbyCreatorClientID=${hostClientID} .currentClientID=${this.currentClientID} .teamCount=${this.gameConfig?.playerTeams ?? 2} .nationCount=${this.nationCount} @@ -143,7 +149,7 @@ export class JoinLobbyModal extends BaseModal { `}
- ${this.gameConfig?.gameType === GameType.Private + ${this.isPrivateLobby() ? html`
Date: Tue, 3 Feb 2026 13:42:12 +0000 Subject: [PATCH 05/20] fix: validate base username length separately from clan tag (#3098) ## Description: fix: validate base username length separately from clan tag) ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] 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: w.o.n --- src/client/UsernameInput.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/UsernameInput.ts b/src/client/UsernameInput.ts index cfff06a1f..11889cd67 100644 --- a/src/client/UsernameInput.ts +++ b/src/client/UsernameInput.ts @@ -147,8 +147,8 @@ export class UsernameInput extends LitElement { } private validateAndStore() { - // Prevent empty username even if clan tag is present - if (!this.baseUsername.trim()) { + // Validate base username meets minimum length (clan tag doesn't count) + if (this.baseUsername.trim().length < MIN_USERNAME_LENGTH) { this._isValid = false; this.validationError = translateText("username.too_short", { min: MIN_USERNAME_LENGTH, From 144442a99b7dbb86e4373be835aeb8c5f3397826 Mon Sep 17 00:00:00 2001 From: Aotumuri Date: Wed, 4 Feb 2026 08:02:07 +0900 Subject: [PATCH 06/20] fix: prevent button content from overflowing in translated labels (#3112) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description: Prevents translated button text from overflowing its container by constraining layout and clipping excess content. before スクリーンショット 2026-02-04 6 46 35 after スクリーンショット 2026-02-04 6 46 46 ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] 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: aotumuri --- src/client/components/baseComponents/Button.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/client/components/baseComponents/Button.ts b/src/client/components/baseComponents/Button.ts index dd0818c5a..e3dd03898 100644 --- a/src/client/components/baseComponents/Button.ts +++ b/src/client/components/baseComponents/Button.ts @@ -13,7 +13,7 @@ export class OButton extends LitElement { @property({ type: Boolean }) disable = false; @property({ type: Boolean }) fill = false; private static readonly BASE_CLASS = - "bg-blue-600 hover:bg-blue-700 text-white font-bold uppercase tracking-wider px-4 py-3 rounded-xl transition-all duration-300 transform hover:-translate-y-px outline-none border border-transparent text-center text-base lg:text-lg"; + "bg-blue-600 hover:bg-blue-700 text-white font-bold uppercase tracking-wider px-4 py-3 rounded-xl transition-all duration-300 transform hover:-translate-y-px outline-none border border-transparent text-center text-base lg:text-lg whitespace-normal break-words leading-tight overflow-hidden relative"; createRenderRoot() { return this; @@ -39,9 +39,11 @@ export class OButton extends LitElement { class=${classMap(this.getButtonClasses())} ?disabled=${this.disable} > - ${this.translationKey === "" - ? this.title - : translateText(this.translationKey)} + + ${this.translationKey === "" + ? this.title + : translateText(this.translationKey)} + `; } From c9d8ed767ce9b72cf42460201986d6d9238dafc1 Mon Sep 17 00:00:00 2001 From: Ryan <7389646+ryanbarlow97@users.noreply.github.com> Date: Tue, 3 Feb 2026 23:07:12 +0000 Subject: [PATCH 07/20] Add getClientIDForGame for consistent client IDs per game session (#3108) ## Description: - Add getClientIDForGame function to Auth.ts that generates and stores a consistent clientID per gameID using sessionStorage - Update HostLobbyModal to use getClientIDForGame for lobby creation - Update Matchmaking to use getClientIDForGame when joining games - Update PublicLobby to use getClientIDForGame when joining lobbies This enables reconnection support by ensuring the same clientID is used when rejoining a game session. ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] 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: w.o.n --------- Co-authored-by: Evan Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/client/Auth.ts | 20 ++++++++++++++++++++ src/client/HostLobbyModal.ts | 14 +++++++++----- src/client/JoinLobbyModal.ts | 5 ++--- src/client/Matchmaking.ts | 5 ++--- 4 files changed, 33 insertions(+), 11 deletions(-) diff --git a/src/client/Auth.ts b/src/client/Auth.ts index c5fdef981..6dd5f4992 100644 --- a/src/client/Auth.ts +++ b/src/client/Auth.ts @@ -2,12 +2,16 @@ import { decodeJwt } from "jose"; import { z } from "zod"; import { TokenPayload, TokenPayloadSchema } from "../core/ApiSchemas"; import { base64urlToUuid } from "../core/Base64"; +import { ID } from "../core/Schemas"; +import { generateID } from "../core/Util"; import { getApiBase, getAudience } from "./Api"; import { generateCryptoRandomUUID } from "./Utils"; export type UserAuth = { jwt: string; claims: TokenPayload } | false; const PERSISTENT_ID_KEY = "player_persistent_id"; +const CLIENT_ID_KEY = "client_join_id"; +const CLIENT_GAME_ID_KEY = "client_join_game_id"; let __jwt: string | null = null; @@ -209,6 +213,22 @@ export function getPersistentID(): string { return base64urlToUuid(sub); } +export function getClientIDForGame(gameID: string): string { + const storedGameID = sessionStorage.getItem(CLIENT_GAME_ID_KEY); + const storedClientID = sessionStorage.getItem(CLIENT_ID_KEY); + if ( + storedGameID === gameID && + storedClientID && + ID.safeParse(storedClientID).success + ) { + return storedClientID; + } + const newID = generateID(); + sessionStorage.setItem(CLIENT_GAME_ID_KEY, gameID); + sessionStorage.setItem(CLIENT_ID_KEY, newID); + return newID; +} + // WARNING: DO NOT EXPOSE THIS ID function getPersistentIDFromLocalStorage(): string { // Try to get existing localStorage diff --git a/src/client/HostLobbyModal.ts b/src/client/HostLobbyModal.ts index 8b3e314f4..c086354e6 100644 --- a/src/client/HostLobbyModal.ts +++ b/src/client/HostLobbyModal.ts @@ -21,6 +21,7 @@ import { isValidGameID, } from "../core/Schemas"; import { generateID } from "../core/Util"; +import { getClientIDForGame } from "./Auth"; import "./components/baseComponents/Modal"; import { BaseModal } from "./components/BaseModal"; import "./components/CopyButton"; @@ -635,9 +636,10 @@ export class HostLobbyModal extends BaseModal { } protected onOpen(): void { - this.lobbyCreatorClientID = generateID(); + this.lobbyId = generateID(); + this.lobbyCreatorClientID = getClientIDForGame(this.lobbyId); - createLobby(this.lobbyCreatorClientID) + createLobby(this.lobbyCreatorClientID, this.lobbyId) .then(async (lobby) => { this.lobbyId = lobby.gameID; if (!isValidGameID(this.lobbyId)) { @@ -1080,12 +1082,14 @@ export class HostLobbyModal extends BaseModal { } } -async function createLobby(creatorClientID: string): Promise { +async function createLobby( + creatorClientID: string, + gameID: string, +): Promise { const config = await getServerConfigFromClient(); try { - const id = generateID(); const response = await fetch( - `/${config.workerPath(id)}/api/create_game/${id}?creatorClientID=${encodeURIComponent(creatorClientID)}`, + `/${config.workerPath(gameID)}/api/create_game/${gameID}?creatorClientID=${encodeURIComponent(creatorClientID)}`, { method: "POST", headers: { diff --git a/src/client/JoinLobbyModal.ts b/src/client/JoinLobbyModal.ts index 59ee5b48a..d2653c0e6 100644 --- a/src/client/JoinLobbyModal.ts +++ b/src/client/JoinLobbyModal.ts @@ -17,7 +17,6 @@ import { GameRecordSchema, LobbyInfoEvent, } from "../core/Schemas"; -import { generateID } from "../core/Util"; import { getServerConfigFromClient } from "../core/configuration/ConfigLoader"; import { GameMapSize, @@ -26,8 +25,8 @@ import { HumansVsNations, } from "../core/game/Game"; import { getApiBase } from "./Api"; +import { getClientIDForGame } from "./Auth"; import { crazyGamesSDK } from "./CrazyGamesSDK"; -import { JoinLobbyEvent } from "./Main"; import { terrainMapFileLoader } from "./TerrainMapFileLoader"; import { BaseModal } from "./components/BaseModal"; import "./components/CopyButton"; @@ -346,7 +345,7 @@ export class JoinLobbyModal extends BaseModal { private startTrackingLobby(lobbyId: string, lobbyInfo?: GameInfo) { this.currentLobbyId = lobbyId; - this.currentClientID = generateID(); + this.currentClientID = getClientIDForGame(lobbyId); this.gameConfig = null; this.players = []; this.playerCount = 0; diff --git a/src/client/Matchmaking.ts b/src/client/Matchmaking.ts index c29931896..4e53ea6ab 100644 --- a/src/client/Matchmaking.ts +++ b/src/client/Matchmaking.ts @@ -2,9 +2,8 @@ import { html, LitElement } from "lit"; import { customElement, query, state } from "lit/decorators.js"; import { UserMeResponse } from "../core/ApiSchemas"; import { getServerConfigFromClient } from "../core/configuration/ConfigLoader"; -import { generateID } from "../core/Util"; import { getUserMe, hasLinkedAccount } from "./Api"; -import { getPlayToken } from "./Auth"; +import { getClientIDForGame, getPlayToken } from "./Auth"; import { BaseModal } from "./components/BaseModal"; import "./components/Difficulties"; import "./components/PatternButton"; @@ -231,7 +230,7 @@ export class MatchmakingModal extends BaseModal { new CustomEvent("join-lobby", { detail: { gameID: this.gameID, - clientID: generateID(), + clientID: getClientIDForGame(this.gameID), } as JoinLobbyEvent, bubbles: true, composed: true, From 596bc90134280d906e3ad4527aee836b9a21d2f1 Mon Sep 17 00:00:00 2001 From: FloPinguin <25036848+FloPinguin@users.noreply.github.com> Date: Wed, 4 Feb 2026 00:16:17 +0100 Subject: [PATCH 08/20] =?UTF-8?q?Fix=20the=20new=20mobile=20styles=20a=20b?= =?UTF-8?q?it=20=F0=9F=96=8C=EF=B8=8F=20(#3106)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description: ### 1. PlayerInfoOverlay had less margin top than the leaderboard and the game controls. Fixed. ### 2. PlayerInfoOverlay did overlap with HeadsUpMessage. Fixed. Previous: Screenshot 2026-02-03 184835 Now: Screenshot 2026-02-03 185117 ### 3. Fixed border radius of the lower panels Previous: Screenshot 2026-02-03 184938 Screenshot 2026-02-03 185014 Now: Screenshot 2026-02-03 184953 Screenshot 2026-02-03 185004 ### 4. Give PlayerInfoOverlay the same gap between gold icon and gold text as in ControlPanel Previous: Screenshot 2026-02-03 191150 Now: Screenshot 2026-02-03 191203 ## Please complete the following: - [X] I have added screenshots for all UI updates - [X] I process any text displayed to the user through translateText() and I've added it to the en.json file - [X] I have added relevant tests to the test directory - [X] 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: FloPinguin --- src/client/graphics/layers/ControlPanel.ts | 2 +- src/client/graphics/layers/EventsDisplay.ts | 6 ++++-- src/client/graphics/layers/HeadsUpMessage.ts | 2 +- src/client/graphics/layers/PlayerInfoOverlay.ts | 4 ++-- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/client/graphics/layers/ControlPanel.ts b/src/client/graphics/layers/ControlPanel.ts index c6f000405..c45a31b00 100644 --- a/src/client/graphics/layers/ControlPanel.ts +++ b/src/client/graphics/layers/ControlPanel.ts @@ -261,7 +261,7 @@ export class ControlPanel extends LitElement implements Layer { return html`
e.preventDefault()} > diff --git a/src/client/graphics/layers/EventsDisplay.ts b/src/client/graphics/layers/EventsDisplay.ts index db68fe987..3d0bad609 100644 --- a/src/client/graphics/layers/EventsDisplay.ts +++ b/src/client/graphics/layers/EventsDisplay.ts @@ -1038,7 +1038,9 @@ export class EventsDisplay extends LitElement implements Layer { class="relative w-full min-[1200px]:bottom-4 min-[1200px]:right-4 z-50 min-[1200px]:w-96 backdrop-blur-sm" > -
+
${this.renderToggleButton( @@ -1081,7 +1083,7 @@ export class EventsDisplay extends LitElement implements Layer {
@@ -452,7 +452,7 @@ export class PlayerInfoOverlay extends LitElement implements Layer { return html`
e.preventDefault()} >
Date: Wed, 4 Feb 2026 00:09:06 +0000 Subject: [PATCH 09/20] add import, somehow missing?? (#3113) If this PR fixes an issue, link it below. If not, delete these two lines. Resolves #(issue number) ## Description: add import, somehow missing?? ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] 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: w.o.n --- src/client/JoinLobbyModal.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/client/JoinLobbyModal.ts b/src/client/JoinLobbyModal.ts index d2653c0e6..8a1bf281d 100644 --- a/src/client/JoinLobbyModal.ts +++ b/src/client/JoinLobbyModal.ts @@ -27,6 +27,7 @@ import { import { getApiBase } from "./Api"; import { getClientIDForGame } from "./Auth"; import { crazyGamesSDK } from "./CrazyGamesSDK"; +import { JoinLobbyEvent } from "./Main"; import { terrainMapFileLoader } from "./TerrainMapFileLoader"; import { BaseModal } from "./components/BaseModal"; import "./components/CopyButton"; From 294a1b478446715025e679cf0fb5c77cce598d83 Mon Sep 17 00:00:00 2001 From: Evan Date: Tue, 3 Feb 2026 18:26:38 -0800 Subject: [PATCH 10/20] move lobby websockets to worker (#2974) ## Description: Currently only the master process sends public lobby updates to clients. This is not scalable since it could overload the master process. In this PR, the master uses IPC to send public lobby info to all workers. Then clients connect to a random worker to get public lobby updates via websocket. This way clients never connect directly to the master websocket. The flow looks like this: Every 100ms: 1. Master schedules a public game on a random worker if new games are needed 2. Master broadcasts public lobby info to all workers (all public games & num clients connected to each game) 3. Each worker responds to that update with the number of clients connected to its own public games 4. Master then updates its public lobby state so it knows how many clients are connected to each public game ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] 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: evan --- nginx.conf | 19 --- src/client/JoinLobbyModal.ts | 31 ++--- src/client/LobbySocket.ts | 71 +++-------- src/client/Main.ts | 12 +- src/client/PublicLobby.ts | 35 ++++-- src/core/Schemas.ts | 25 ++-- src/server/GameManager.ts | 8 ++ src/server/GamePreviewBuilder.ts | 3 +- src/server/GameServer.ts | 26 ++-- src/server/IPCBridgeSchema.ts | 56 +++++++++ src/server/Master.ts | 208 ++----------------------------- src/server/MasterLobbyService.ts | 135 ++++++++++++++++++++ src/server/Worker.ts | 31 +++-- src/server/WorkerLobbyService.ts | 136 ++++++++++++++++++++ tests/client/LobbySocket.test.ts | 113 ----------------- 15 files changed, 437 insertions(+), 472 deletions(-) create mode 100644 src/server/IPCBridgeSchema.ts create mode 100644 src/server/MasterLobbyService.ts create mode 100644 src/server/WorkerLobbyService.ts delete mode 100644 tests/client/LobbySocket.test.ts diff --git a/nginx.conf b/nginx.conf index 12e7282f1..2da48c6af 100644 --- a/nginx.conf +++ b/nginx.conf @@ -102,25 +102,6 @@ server { add_header Cache-Control "public, max-age=86400"; # 24 hours } - # /api/public_lobbies endpoint - Cache for 1 second to handle high request volume - location = /api/public_lobbies { - proxy_pass http://127.0.0.1:3000; - proxy_http_version 1.1; - - # Cache configuration - proxy_cache API_CACHE; - proxy_cache_valid 200 1s; - proxy_cache_use_stale updating error timeout http_500 http_502 http_503 http_504; - proxy_cache_lock on; - add_header X-Cache-Status $upstream_cache_status; - - # Standard proxy headers - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } - # /api/env endpoint - Cache for 1 hour location = /api/env { proxy_pass http://127.0.0.1:3000; diff --git a/src/client/JoinLobbyModal.ts b/src/client/JoinLobbyModal.ts index 8a1bf281d..7cd330d64 100644 --- a/src/client/JoinLobbyModal.ts +++ b/src/client/JoinLobbyModal.ts @@ -70,7 +70,7 @@ export class JoinLobbyModal extends BaseModal { } this.updateFromLobby({ ...lobby, - msUntilStart: lobby.msUntilStart ?? undefined, + startsAt: lobby.startsAt ?? undefined, }); }; @@ -94,7 +94,7 @@ export class JoinLobbyModal extends BaseModal { }) : translateText("public_lobby.started"); const maxPlayers = this.gameConfig?.maxPlayers ?? 0; - const playerCount = this.playerCount; + const playerCount = this.players?.length ?? 0; const hostClientID = this.isPrivateLobby() ? (this.lobbyCreatorClientID ?? "") : ""; @@ -283,13 +283,13 @@ export class JoinLobbyModal extends BaseModal { `; } - public open(lobbyId: string = "", lobbyInfo?: GameInfo) { + public open(lobbyId: string = "", isPublic: boolean = false) { super.open(); if (lobbyId) { - this.startTrackingLobby(lobbyId, lobbyInfo); + this.startTrackingLobby(lobbyId); // If opened with lobbyInfo (public lobby case), auto-join the lobby - if (lobbyInfo) { - this.joinPublicLobby(lobbyId, lobbyInfo); + if (isPublic) { + this.joinPublicLobby(lobbyId); } else { // If opened with lobbyId but no lobbyInfo (URL join case), check if active and join this.handleUrlJoin(lobbyId); @@ -329,14 +329,13 @@ export class JoinLobbyModal extends BaseModal { } } - private joinPublicLobby(lobbyId: string, lobbyInfo: GameInfo) { + private joinPublicLobby(lobbyId: string) { // Dispatch join-lobby event to actually connect to the lobby this.dispatchEvent( new CustomEvent("join-lobby", { detail: { gameID: lobbyId, clientID: this.currentClientID, - publicLobbyInfo: lobbyInfo, } as JoinLobbyEvent, bubbles: true, composed: true, @@ -349,7 +348,6 @@ export class JoinLobbyModal extends BaseModal { this.currentClientID = getClientIDForGame(lobbyId); this.gameConfig = null; this.players = []; - this.playerCount = 0; this.nationCount = 0; this.lobbyStartAt = null; this.lobbyCreatorClientID = null; @@ -397,7 +395,6 @@ export class JoinLobbyModal extends BaseModal { if (this.lobbyIdInput) this.lobbyIdInput.value = ""; this.gameConfig = null; this.players = []; - this.playerCount = 0; this.currentLobbyId = ""; this.currentClientID = ""; this.nationCount = 0; @@ -536,18 +533,8 @@ export class JoinLobbyModal extends BaseModal { // --- Lobby event handling --- private updateFromLobby(lobby: GameInfo) { - if (lobby.clients) { - this.players = lobby.clients; - this.playerCount = lobby.clients.length; - } else { - this.players = []; - this.playerCount = lobby.numClients ?? 0; - } - if (lobby.msUntilStart !== undefined) { - this.lobbyStartAt = lobby.msUntilStart + Date.now(); - } else { - this.lobbyStartAt = null; - } + this.players = lobby.clients ?? []; + this.lobbyStartAt = lobby.startsAt ?? null; this.syncCountdownTimer(); if (lobby.gameConfig) { const mapChanged = this.gameConfig?.gameMap !== lobby.gameConfig.gameMap; diff --git a/src/client/LobbySocket.ts b/src/client/LobbySocket.ts index f398da054..7b71dc21a 100644 --- a/src/client/LobbySocket.ts +++ b/src/client/LobbySocket.ts @@ -1,6 +1,5 @@ -import { GameInfo } from "../core/Schemas"; - -type LobbyUpdateHandler = (lobbies: GameInfo[]) => void; +import { getServerConfigFromClient } from "../core/configuration/ConfigLoader"; +import { PublicGames, PublicGamesSchema } from "../core/Schemas"; interface LobbySocketOptions { reconnectDelay?: number; @@ -8,36 +7,39 @@ interface LobbySocketOptions { pollIntervalMs?: number; } +function getRandomWorkerPath(numWorkers: number): string { + const workerIndex = Math.floor(Math.random() * numWorkers); + return `/w${workerIndex}`; +} + export class PublicLobbySocket { private ws: WebSocket | null = null; private wsReconnectTimeout: number | null = null; - private fallbackPollInterval: number | null = null; private wsConnectionAttempts = 0; private wsAttemptCounted = false; + private workerPath: string = ""; private readonly reconnectDelay: number; private readonly maxWsAttempts: number; - private readonly pollIntervalMs: number; - private readonly onLobbiesUpdate: LobbyUpdateHandler; constructor( - onLobbiesUpdate: LobbyUpdateHandler, + private onLobbiesUpdate: (data: PublicGames) => void, options?: LobbySocketOptions, ) { - this.onLobbiesUpdate = onLobbiesUpdate; this.reconnectDelay = options?.reconnectDelay ?? 3000; this.maxWsAttempts = options?.maxWsAttempts ?? 3; - this.pollIntervalMs = options?.pollIntervalMs ?? 1000; } - start() { + async start() { this.wsConnectionAttempts = 0; + // Get config to determine number of workers, then pick a random one + const config = await getServerConfigFromClient(); + this.workerPath = getRandomWorkerPath(config.numWorkers()); this.connectWebSocket(); } stop() { this.disconnectWebSocket(); - this.stopFallbackPolling(); } private connectWebSocket() { @@ -49,7 +51,7 @@ export class PublicLobbySocket { } const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; - const wsUrl = `${protocol}//${window.location.host}/lobbies`; + const wsUrl = `${protocol}//${window.location.host}${this.workerPath}/lobbies`; this.ws = new WebSocket(wsUrl); this.wsAttemptCounted = false; @@ -70,15 +72,14 @@ export class PublicLobbySocket { clearTimeout(this.wsReconnectTimeout); this.wsReconnectTimeout = null; } - this.stopFallbackPolling(); } private handleMessage(event: MessageEvent) { try { - const message = JSON.parse(event.data as string); - if (message.type === "lobbies_update") { - this.onLobbiesUpdate(message.data?.lobbies ?? []); - } + const publicGames = PublicGamesSchema.parse( + JSON.parse(event.data as string), + ); + this.onLobbiesUpdate(publicGames); } catch (error) { console.error("Error parsing WebSocket message:", error); if (this.ws && this.ws.readyState === WebSocket.OPEN) { @@ -101,10 +102,7 @@ export class PublicLobbySocket { this.wsConnectionAttempts++; } if (this.wsConnectionAttempts >= this.maxWsAttempts) { - console.log( - "Max WebSocket attempts reached, falling back to HTTP polling", - ); - this.startFallbackPolling(); + console.error("Max WebSocket attempts reached"); } else { this.scheduleReconnect(); } @@ -121,7 +119,7 @@ export class PublicLobbySocket { this.wsConnectionAttempts++; } if (this.wsConnectionAttempts >= this.maxWsAttempts) { - this.startFallbackPolling(); + alert("error connecting to game service"); } else { this.scheduleReconnect(); } @@ -145,33 +143,4 @@ export class PublicLobbySocket { this.wsReconnectTimeout = null; } } - - private startFallbackPolling() { - if (this.fallbackPollInterval !== null) return; - console.log("Starting HTTP fallback polling"); - this.fetchLobbiesHTTP(); - this.fallbackPollInterval = window.setInterval(() => { - this.fetchLobbiesHTTP(); - }, this.pollIntervalMs); - } - - private stopFallbackPolling() { - if (this.fallbackPollInterval !== null) { - clearInterval(this.fallbackPollInterval); - this.fallbackPollInterval = null; - } - } - - private async fetchLobbiesHTTP() { - try { - const response = await fetch(`/api/public_lobbies`); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - const data = await response.json(); - this.onLobbiesUpdate(data.lobbies as GameInfo[]); - } catch (error) { - console.error("Error fetching lobbies via HTTP:", error); - } - } } diff --git a/src/client/Main.ts b/src/client/Main.ts index c48d0f06f..ba6569889 100644 --- a/src/client/Main.ts +++ b/src/client/Main.ts @@ -1,12 +1,7 @@ import version from "resources/version.txt?raw"; import { UserMeResponse } from "../core/ApiSchemas"; import { EventBus } from "../core/EventBus"; -import { - GAME_ID_REGEX, - GameInfo, - GameRecord, - GameStartInfo, -} from "../core/Schemas"; +import { GAME_ID_REGEX, GameRecord, GameStartInfo } from "../core/Schemas"; import { GameEnv } from "../core/configuration/Config"; import { getServerConfigFromClient } from "../core/configuration/ConfigLoader"; import { GameType } from "../core/game/Game"; @@ -223,7 +218,6 @@ export interface JoinLobbyEvent { // GameRecord exists when replaying an archived game. gameRecord?: GameRecord; source?: "public" | "private" | "host" | "matchmaking" | "singleplayer"; - publicLobbyInfo?: GameInfo; } class Client { @@ -773,7 +767,7 @@ class Client { } const config = await getServerConfigFromClient(); // Only update URL immediately for private lobbies, not public ones - if (!lobby.publicLobbyInfo && lobby.source !== "public") { + if (lobby.source !== "public") { this.updateJoinUrlForShare(lobby.gameID, config); } @@ -908,7 +902,7 @@ class Client { // Open the join lobby modal page and pass the lobby info window.showPage?.("page-join-lobby"); - this.joinModal?.open(lobby.gameID, lobby); + this.joinModal?.open(lobby.gameID, true); } private async handleLeaveLobby(/* event: CustomEvent */) { diff --git a/src/client/PublicLobby.ts b/src/client/PublicLobby.ts index dc60087fd..ef9729c93 100644 --- a/src/client/PublicLobby.ts +++ b/src/client/PublicLobby.ts @@ -1,7 +1,7 @@ import { html, LitElement } from "lit"; import { customElement, state } from "lit/decorators.js"; import { GameMapType } from "../core/game/Game"; -import { GameID, GameInfo } from "../core/Schemas"; +import { GameID, PublicGameInfo, PublicGames } from "../core/Schemas"; import { PublicLobbySocket } from "./LobbySocket"; import { terrainMapFileLoader } from "./TerrainMapFileLoader"; import { @@ -13,16 +13,19 @@ import { } from "./Utils"; export interface ShowPublicLobbyModalEvent { - lobby: GameInfo; + lobby: PublicGameInfo; } @customElement("public-lobby") export class PublicLobby extends LitElement { - @state() private lobbies: GameInfo[] = []; + @state() private publicGames: PublicGames | null = null; + @state() public isLobbyHighlighted: boolean = false; @state() private mapImages: Map = new Map(); + private lobbyIDToStart = new Map(); - private lobbySocket = new PublicLobbySocket((lobbies) => - this.handleLobbiesUpdate(lobbies), + private serverTimeOffset = 0; + private lobbySocket = new PublicLobbySocket((data) => + this.handleLobbiesUpdate(data), ); createRenderRoot() { @@ -39,12 +42,18 @@ export class PublicLobby extends LitElement { this.lobbySocket.stop(); } - private handleLobbiesUpdate(lobbies: GameInfo[]) { - this.lobbies = lobbies; - this.lobbies.forEach((l) => { + private handleLobbiesUpdate(publicGames: PublicGames) { + this.publicGames = publicGames; + + // Calculate offset between server time and client time + if (this.publicGames) { + this.serverTimeOffset = this.publicGames.serverTime - Date.now(); + } + this.publicGames.games.forEach((l) => { if (!this.lobbyIDToStart.has(l.gameID)) { - const msUntilStart = l.msUntilStart ?? 0; - this.lobbyIDToStart.set(l.gameID, msUntilStart + Date.now()); + // Convert server's startsAt to client time by subtracting offset + const startsAt = l.startsAt ?? Date.now(); + this.lobbyIDToStart.set(l.gameID, startsAt - this.serverTimeOffset); } if (l.gameConfig && !this.mapImages.has(l.gameID)) { @@ -66,9 +75,9 @@ export class PublicLobby extends LitElement { } render() { - if (this.lobbies.length === 0) return html``; + if (!this.publicGames) return html``; - const lobby = this.lobbies[0]; + const lobby = this.publicGames.games[0]; if (!lobby?.gameConfig) return html``; const start = this.lobbyIDToStart.get(lobby.gameID) ?? 0; @@ -200,7 +209,7 @@ export class PublicLobby extends LitElement { this.lobbySocket.stop(); } - private lobbyClicked(lobby: GameInfo) { + private lobbyClicked(lobby: PublicGameInfo) { // Validate username before opening the modal const usernameInput = document.querySelector("username-input") as any; if ( diff --git a/src/core/Schemas.ts b/src/core/Schemas.ts index dc257151b..0aa06a316 100644 --- a/src/core/Schemas.ts +++ b/src/core/Schemas.ts @@ -136,6 +136,9 @@ export type PlayerPattern = z.infer; export type PlayerColor = z.infer; export type Flag = z.infer; export type GameStartInfo = z.infer; +export type GameInfo = z.infer; +export type PublicGames = z.infer; +export type PublicGameInfo = z.infer; const ClientInfoSchema = z.object({ clientID: z.string(), @@ -145,18 +148,22 @@ const ClientInfoSchema = z.object({ export const GameInfoSchema = z.object({ gameID: z.string(), clients: z.array(ClientInfoSchema).optional(), - numClients: z.number().optional(), - msUntilStart: z.number().optional(), + startsAt: z.number().optional(), + serverTime: z.number(), gameConfig: z.lazy(() => GameConfigSchema).optional(), }); -export interface GameInfo { - gameID: GameID; - clients?: ClientInfo[]; - numClients?: number; - msUntilStart?: number; - gameConfig?: GameConfig; -} +export const PublicGameInfoSchema = z.object({ + gameID: z.string(), + numClients: z.number(), + startsAt: z.number(), + gameConfig: z.lazy(() => GameConfigSchema).optional(), +}); + +export const PublicGamesSchema = z.object({ + serverTime: z.number(), + games: PublicGameInfoSchema.array(), +}); export class LobbyInfoEvent implements GameEvent { constructor(public lobby: GameInfo) {} diff --git a/src/server/GameManager.ts b/src/server/GameManager.ts index d2588ae00..7b7e4358a 100644 --- a/src/server/GameManager.ts +++ b/src/server/GameManager.ts @@ -26,6 +26,12 @@ export class GameManager { return this.games.get(id) ?? null; } + public publicLobbies(): GameServer[] { + return Array.from(this.games.values()).filter( + (g) => g.phase() === GamePhase.Lobby && g.isPublic(), + ); + } + joinClient(client: Client, gameID: GameID): boolean { const game = this.games.get(gameID); if (game) { @@ -52,6 +58,7 @@ export class GameManager { id: GameID, gameConfig: GameConfig | undefined, creatorClientID?: string, + startsAt?: number, ) { const game = new GameServer( id, @@ -77,6 +84,7 @@ export class GameManager { ...gameConfig, }, creatorClientID, + startsAt, ); this.games.set(id, game); return game; diff --git a/src/server/GamePreviewBuilder.ts b/src/server/GamePreviewBuilder.ts index a68c335c2..ea9916025 100644 --- a/src/server/GamePreviewBuilder.ts +++ b/src/server/GamePreviewBuilder.ts @@ -147,8 +147,7 @@ export function buildPreview( activePlayers = countActivePlayers(players); } else { activePlayers = - countActivePlayers(players) || - (lobby?.numClients ?? lobby?.clients?.length ?? 0); + countActivePlayers(players) || (lobby?.clients?.length ?? 0); } const map = lobby?.gameConfig?.gameMap ?? config.gameMap; let mode = lobby?.gameConfig?.gameMode ?? config.gameMode ?? GameMode.FFA; diff --git a/src/server/GameServer.ts b/src/server/GameServer.ts index ef476f1ec..f8b183dce 100644 --- a/src/server/GameServer.ts +++ b/src/server/GameServer.ts @@ -88,6 +88,7 @@ export class GameServer { private config: ServerConfig, public gameConfig: GameConfig, private lobbyCreatorID?: string, + private startsAt?: number, ) { this.log = log_.child({ gameID: id }); } @@ -506,15 +507,6 @@ export class GameServer { return this.activeClients.length; } - public startTime(): number { - if (this._startTime !== null && this._startTime > 0) { - return this._startTime; - } else { - //game hasn't started yet, only works for public games - return this.createdAt + this.config.gameCreationRate(); - } - } - public prestart() { if (this.hasStarted()) { return; @@ -787,8 +779,9 @@ export class GameServer { } } - const msSinceCreation = now - this.createdAt; - const lessThanLifetime = msSinceCreation < this.config.gameCreationRate(); + // Public Games + + const lessThanLifetime = Date.now() < this.startsAt!; const notEnoughPlayers = this.gameConfig.gameType === GameType.Public && this.gameConfig.maxPlayers && @@ -796,8 +789,7 @@ export class GameServer { if (lessThanLifetime && notEnoughPlayers) { return GamePhase.Lobby; } - const warmupOver = - now > this.createdAt + this.config.gameCreationRate() + 30 * 1000; + const warmupOver = now > this.startsAt! + 30 * 1000; if (noActive && warmupOver && noRecentPings) { return GamePhase.Finished; } @@ -817,15 +809,11 @@ export class GameServer { clientID: c.clientID, })), gameConfig: this.gameConfig, - msUntilStart: this.isPublic() ? this.getMsUntilStart() : undefined, + startsAt: this.startsAt, + serverTime: Date.now(), }; } - private getMsUntilStart(): number { - const startTime = this.createdAt + this.config.gameCreationRate(); - return Math.max(0, startTime - Date.now()); - } - public isPublic(): boolean { return this.gameConfig.gameType === GameType.Public; } diff --git a/src/server/IPCBridgeSchema.ts b/src/server/IPCBridgeSchema.ts new file mode 100644 index 000000000..e6034091e --- /dev/null +++ b/src/server/IPCBridgeSchema.ts @@ -0,0 +1,56 @@ +import { z } from "zod"; +import { + GameConfigSchema, + PublicGameInfoSchema, + PublicGamesSchema, +} from "../core/Schemas"; + +export type WorkerLobbyList = z.infer; +export type WorkerReady = z.infer; +export type MasterLobbiesBroadcast = z.infer< + typeof MasterLobbiesBroadcastSchema +>; +export type MasterCreateGame = z.infer; +export type WorkerMessage = z.infer; +export type MasterMessage = z.infer; + +// --- Worker Messages --- + +// Worker tells the master about its lobbies. +const WorkerLobbyListSchema = z.object({ + type: z.literal("lobbyList"), + lobbies: z.array(PublicGameInfoSchema), +}); + +const WorkerReadySchema = z.object({ + type: z.literal("workerReady"), + workerId: z.number(), +}); + +export const WorkerMessageSchema = z.discriminatedUnion("type", [ + WorkerLobbyListSchema, + WorkerReadySchema, +]); + +// --- Master Messages --- + +// Broadcasts all public game info to all workers. +// Workers need information on all public lobbies so +// it can send it to the client. +const MasterLobbiesBroadcastSchema = z.object({ + type: z.literal("lobbiesBroadcast"), + publicGames: PublicGamesSchema, +}); + +// Master sends a message to worker to schedule a new public game/lobby. +const MasterCreateGameSchema = z.object({ + type: z.literal("createGame"), + gameID: z.string(), + gameConfig: GameConfigSchema, + startsAt: z.number(), +}); + +export const MasterMessageSchema = z.discriminatedUnion("type", [ + MasterLobbiesBroadcastSchema, + MasterCreateGameSchema, +]); diff --git a/src/server/Master.ts b/src/server/Master.ts index c2b131c2d..a9322d8ff 100644 --- a/src/server/Master.ts +++ b/src/server/Master.ts @@ -5,19 +5,16 @@ import rateLimit from "express-rate-limit"; import http from "http"; import path from "path"; import { fileURLToPath } from "url"; -import { WebSocket, WebSocketServer } from "ws"; import { GameEnv } from "../core/configuration/Config"; import { getServerConfigFromServer } from "../core/configuration/ConfigLoader"; -import { GameInfo } from "../core/Schemas"; -import { generateID } from "../core/Util"; import { logger } from "./Logger"; import { MapPlaylist } from "./MapPlaylist"; -import { startPolling } from "./PollingLoop"; +import { MasterLobbyService } from "./MasterLobbyService"; import { renderHtml } from "./RenderHtml"; const config = getServerConfigFromServer(); const playlist = new MapPlaylist(); -const readyWorkers = new Set(); +let lobbyService: MasterLobbyService; const app = express(); const server = http.createServer(app); @@ -68,33 +65,6 @@ app.use( }), ); -let publicLobbiesData: { lobbies: GameInfo[] } = { lobbies: [] }; - -const publicLobbyIDs: Set = new Set(); -const connectedClients: Set = new Set(); - -// Broadcast lobbies to all connected clients -function broadcastLobbies() { - const message = JSON.stringify({ - type: "lobbies_update", - data: publicLobbiesData, - }); - - const clientsToRemove: WebSocket[] = []; - - connectedClients.forEach((client) => { - if (client.readyState === WebSocket.OPEN) { - client.send(message); - } else { - clientsToRemove.push(client); - } - }); - - clientsToRemove.forEach((client) => { - connectedClients.delete(client); - }); -} - // Start the master process export async function startMaster() { if (!cluster.isPrimary) { @@ -106,36 +76,7 @@ export async function startMaster() { log.info(`Primary ${process.pid} is running`); log.info(`Setting up ${config.numWorkers()} workers...`); - // Setup WebSocket server for clients - const wss = new WebSocketServer({ server, path: "/lobbies" }); - - wss.on("connection", (ws: WebSocket) => { - connectedClients.add(ws); - - // Send current lobbies immediately (always send, even if empty) - ws.send( - JSON.stringify({ type: "lobbies_update", data: publicLobbiesData }), - ); - - ws.on("close", () => { - connectedClients.delete(ws); - }); - - ws.on("error", (error) => { - log.error(`WebSocket error:`, error); - connectedClients.delete(ws); - try { - if ( - ws.readyState === WebSocket.OPEN || - ws.readyState === WebSocket.CONNECTING - ) { - ws.close(1011, "WebSocket internal error"); - } - } catch (closeError) { - log.error("Error while closing WebSocket after error:", closeError); - } - }); - }); + lobbyService = new MasterLobbyService(config, playlist, log); // Generate admin token for worker authentication const ADMIN_TOKEN = crypto.randomBytes(16).toString("hex"); @@ -157,44 +98,21 @@ export async function startMaster() { INSTANCE_ID, }); + lobbyService.registerWorker(i, worker); log.info(`Started worker ${i} (PID: ${worker.process.pid})`); } - cluster.on("message", (worker, message) => { - if (message.type === "WORKER_READY") { - const workerId = message.workerId; - readyWorkers.add(workerId); - log.info( - `Worker ${workerId} is ready. (${readyWorkers.size}/${config.numWorkers()} ready)`, - ); - // Start scheduling when all workers are ready - if (readyWorkers.size === config.numWorkers()) { - log.info("All workers ready, starting game scheduling"); - - const scheduleLobbies = () => { - schedulePublicGame(playlist).catch((error) => { - log.error("Error scheduling public game:", error); - }); - }; - - startPolling(async () => { - const lobbies = await fetchLobbies(); - if (lobbies === 0) { - scheduleLobbies(); - } - }, 100); - } - } - }); - // Handle worker crashes cluster.on("exit", (worker, code, signal) => { const workerId = (worker as any).process?.env?.WORKER_ID; - if (!workerId) { + if (workerId === undefined) { log.error(`worker crashed could not find id`); return; } + const workerIdNum = parseInt(workerId); + lobbyService.removeWorker(workerIdNum); + log.warn( `Worker ${workerId} (PID: ${worker.process.pid}) died with code: ${code} and signal: ${signal}`, ); @@ -207,6 +125,7 @@ export async function startMaster() { INSTANCE_ID, }); + lobbyService.registerWorker(workerIdNum, newWorker); log.info( `Restarted worker ${workerId} (New PID: ${newWorker.process.pid})`, ); @@ -226,115 +145,6 @@ app.get("/api/env", async (req, res) => { res.json(envConfig); }); -// Add lobbies endpoint to list public games for this worker -app.get("/api/public_lobbies", async (req, res) => { - res.json(publicLobbiesData); -}); - -async function fetchLobbies(): Promise { - const fetchPromises: Promise[] = []; - - for (const gameID of new Set(publicLobbyIDs)) { - const controller = new AbortController(); - setTimeout(() => controller.abort(), 5000); // 5 second timeout - const port = config.workerPort(gameID); - const promise = fetch(`http://localhost:${port}/api/game/${gameID}`, { - headers: { [config.adminHeader()]: config.adminToken() }, - signal: controller.signal, - }) - .then((resp) => resp.json()) - .then((json) => { - return json as GameInfo; - }) - .catch((error) => { - log.error(`Error fetching game ${gameID}:`, error); - // Return null or a placeholder if fetch fails - publicLobbyIDs.delete(gameID); - return null; - }); - - fetchPromises.push(promise); - } - - // Wait for all promises to resolve - const results = await Promise.all(fetchPromises); - - // Filter out any null results from failed fetches - const lobbyInfos: GameInfo[] = results - .filter((result) => result !== null) - .map((gi: GameInfo) => { - return { - gameID: gi.gameID, - numClients: gi?.clients?.length ?? 0, - gameConfig: gi.gameConfig, - msUntilStart: gi.msUntilStart, - } as GameInfo; - }); - - lobbyInfos.forEach((l) => { - if ( - "msUntilStart" in l && - l.msUntilStart !== undefined && - l.msUntilStart <= 250 - ) { - publicLobbyIDs.delete(l.gameID); - return; - } - - if ( - "gameConfig" in l && - l.gameConfig !== undefined && - "maxPlayers" in l.gameConfig && - l.gameConfig.maxPlayers !== undefined && - "numClients" in l && - l.numClients !== undefined && - l.gameConfig.maxPlayers <= l.numClients - ) { - publicLobbyIDs.delete(l.gameID); - return; - } - }); - - // Update the lobbies data - publicLobbiesData = { - lobbies: lobbyInfos, - }; - - broadcastLobbies(); - - return publicLobbyIDs.size; -} - -// Function to schedule a new public game -async function schedulePublicGame(playlist: MapPlaylist) { - const gameID = generateID(); - publicLobbyIDs.add(gameID); - - const workerPath = config.workerPath(gameID); - - // Send request to the worker to start the game - try { - const response = await fetch( - `http://localhost:${config.workerPort(gameID)}/api/create_game/${gameID}`, - { - method: "POST", - headers: { - "Content-Type": "application/json", - [config.adminHeader()]: config.adminToken(), - }, - body: JSON.stringify(await playlist.gameConfig()), - }, - ); - - if (!response.ok) { - throw new Error(`Failed to schedule public game: ${response.statusText}`); - } - } catch (error) { - log.error(`Failed to schedule public game on worker ${workerPath}:`, error); - throw error; - } -} - // SPA fallback route app.get("*", async function (_req, res) { try { diff --git a/src/server/MasterLobbyService.ts b/src/server/MasterLobbyService.ts new file mode 100644 index 000000000..c82684cd9 --- /dev/null +++ b/src/server/MasterLobbyService.ts @@ -0,0 +1,135 @@ +import { Worker } from "cluster"; +import winston from "winston"; +import { ServerConfig } from "../core/configuration/Config"; +import { PublicGameInfo } from "../core/Schemas"; +import { generateID } from "../core/Util"; +import { + MasterCreateGame, + MasterLobbiesBroadcast, + WorkerMessageSchema, +} from "./IPCBridgeSchema"; +import { logger } from "./Logger"; +import { MapPlaylist } from "./MapPlaylist"; +import { startPolling } from "./PollingLoop"; + +export interface MasterLobbyServiceOptions { + config: ServerConfig; + playlist: MapPlaylist; + log: typeof logger; +} + +export class MasterLobbyService { + private readonly workers = new Map(); + // Worker id => the lobbies it owns. + private readonly workerLobbies = new Map(); + private readonly readyWorkers = new Set(); + private started = false; + + constructor( + private config: ServerConfig, + private playlist: MapPlaylist, + private log: winston.Logger, + ) {} + + registerWorker(workerId: number, worker: Worker) { + this.workers.set(workerId, worker); + + worker.on("message", (raw: unknown) => { + const result = WorkerMessageSchema.safeParse(raw); + if (!result.success) { + this.log.error("Invalid IPC message from worker:", raw); + return; + } + + const msg = result.data; + switch (msg.type) { + case "workerReady": + this.handleWorkerReady(msg.workerId); + break; + case "lobbyList": + this.workerLobbies.set(workerId, msg.lobbies); + break; + } + }); + } + + removeWorker(workerId: number) { + this.workers.delete(workerId); + this.workerLobbies.delete(workerId); + this.readyWorkers.delete(workerId); + } + + private handleWorkerReady(workerId: number) { + this.readyWorkers.add(workerId); + this.log.info( + `Worker ${workerId} is ready. (${this.readyWorkers.size}/${this.config.numWorkers()} ready)`, + ); + if (this.readyWorkers.size === this.config.numWorkers() && !this.started) { + this.started = true; + this.log.info("All workers ready, starting game scheduling"); + startPolling(async () => this.broadcastLobbies(), 250); + startPolling(async () => await this.maybeScheduleLobby(), 1000); + } + } + + private getAllLobbies(): PublicGameInfo[] { + const lobbies = Array.from(this.workerLobbies.values()) + .flat() + .sort((a, b) => a.startsAt! - b.startsAt); + return lobbies; + } + + private broadcastLobbies() { + const msg = { + type: "lobbiesBroadcast", + publicGames: { + serverTime: Date.now(), + games: this.getAllLobbies(), + }, + } satisfies MasterLobbiesBroadcast; + for (const worker of this.workers.values()) { + worker.send(msg, (e) => { + if (e) { + this.log.error("Failed to send lobbies broadcast to worker:", e); + } + }); + } + } + + private async maybeScheduleLobby() { + const lobbies = this.getAllLobbies(); + if (lobbies.length >= 2) { + return; + } + + const lastStart = lobbies.reduce( + (max, pb) => Math.max(max, pb.startsAt), + Date.now(), + ); + + const gameID = generateID(); + const workerId = this.config.workerIndex(gameID); + + const gameConfig = await this.playlist.gameConfig(); + const worker = this.workers.get(workerId); + if (!worker) { + this.log.error(`Worker ${workerId} not found`); + return; + } + + worker.send( + { + type: "createGame", + gameID, + gameConfig, + startsAt: lastStart + this.config.gameCreationRate(), + } satisfies MasterCreateGame, + (e) => { + if (e) { + this.log.error("Failed to schedule lobby on worker:", e); + } + }, + ); + this.log.info(`Scheduled public game ${gameID} on worker ${workerId}`); + } +} diff --git a/src/server/Worker.ts b/src/server/Worker.ts index e9f340c27..2d8d8dcea 100644 --- a/src/server/Worker.ts +++ b/src/server/Worker.ts @@ -30,6 +30,7 @@ import { MapPlaylist } from "./MapPlaylist"; import { startPolling } from "./PollingLoop"; import { PrivilegeRefresher } from "./PrivilegeRefresher"; import { verifyTurnstileToken } from "./Turnstile"; +import { WorkerLobbyService } from "./WorkerLobbyService"; import { initWorkerMetrics } from "./WorkerMetrics"; const config = getServerConfigFromServer(); @@ -42,6 +43,18 @@ const playlist = new MapPlaylist(true); export async function startWorker() { log.info(`Worker starting...`); + const __filename = fileURLToPath(import.meta.url); + const __dirname = path.dirname(__filename); + + const app = express(); + const server = http.createServer(app); + const wss = new WebSocketServer({ noServer: true }); + + const gm = new GameManager(config, log); + + // Initialize lobby service (handles WebSocket upgrade routing) + const lobbyService = new WorkerLobbyService(server, wss, gm, log); + setTimeout( () => { startMatchmakingPolling(gm); @@ -49,15 +62,6 @@ export async function startWorker() { 1000 + Math.random() * 2000, ); - const __filename = fileURLToPath(import.meta.url); - const __dirname = path.dirname(__filename); - - const app = express(); - const server = http.createServer(app); - const wss = new WebSocketServer({ server }); - - const gm = new GameManager(config, log); - if (config.otelEnabled()) { initWorkerMetrics(gm); } @@ -459,13 +463,8 @@ export async function startWorker() { log.info(`running on http://localhost:${PORT}`); log.info(`Handling requests with path prefix /w${workerId}/`); // Signal to the master process that this worker is ready - if (process.send) { - process.send({ - type: "WORKER_READY", - workerId: workerId, - }); - log.info(`signaled ready state to master`); - } + lobbyService.sendReady(workerId); + log.info(`signaled ready state to master`); }); // Global error handler diff --git a/src/server/WorkerLobbyService.ts b/src/server/WorkerLobbyService.ts new file mode 100644 index 000000000..d06609294 --- /dev/null +++ b/src/server/WorkerLobbyService.ts @@ -0,0 +1,136 @@ +import http from "http"; +import { WebSocket, WebSocketServer } from "ws"; +import { PublicGameInfo, PublicGames } from "../core/Schemas"; +import { GameManager } from "./GameManager"; +import { + MasterMessageSchema, + WorkerLobbyList, + WorkerReady, +} from "./IPCBridgeSchema"; +import { logger } from "./Logger"; + +export class WorkerLobbyService { + private readonly lobbiesWss: WebSocketServer; + private readonly lobbyClients: Set = new Set(); + + constructor( + private readonly server: http.Server, + private readonly gameWss: WebSocketServer, + private readonly gm: GameManager, + private readonly log: typeof logger, + ) { + this.lobbiesWss = new WebSocketServer({ noServer: true }); + this.setupUpgradeHandler(); + this.setupLobbiesWebSocket(); + this.setupIPCListener(); + } + + private setupIPCListener() { + process.on("message", (raw: unknown) => { + const result = MasterMessageSchema.safeParse(raw); + if (!result.success) { + this.log.error("Invalid IPC message from master:", raw); + return; + } + + const msg = result.data; + switch (msg.type) { + case "lobbiesBroadcast": + // Forward message to all clients + this.broadcastLobbiesToClients(msg.publicGames); + // Update master with my lobby info + this.sendMyLobbiesToMaster(); + break; + case "createGame": + if (this.gm.game(msg.gameID) !== null) { + this.log.warn(`Game ${msg.gameID} already exists, skipping create`); + return; + } + this.log.info(`Creating public game ${msg.gameID} from master`); + this.gm.createGame( + msg.gameID, + msg.gameConfig, + undefined, + msg.startsAt, + ); + break; + } + }); + } + + sendReady(workerId: number) { + const msg: WorkerReady = { type: "workerReady", workerId }; + process.send?.(msg); + } + + private sendMyLobbiesToMaster() { + const lobbies = this.gm + .publicLobbies() + .map((g) => g.gameInfo()) + .map((gi) => { + return { + gameID: gi.gameID, + numClients: gi.clients?.length ?? 0, + startsAt: gi.startsAt!, + gameConfig: gi.gameConfig, + } satisfies PublicGameInfo; + }); + process.send?.({ type: "lobbyList", lobbies } satisfies WorkerLobbyList); + } + + private setupUpgradeHandler() { + this.server.on("upgrade", (request, socket, head) => { + const pathname = request.url ?? ""; + if (pathname === "/lobbies" || pathname.endsWith("/lobbies")) { + this.lobbiesWss.handleUpgrade(request, socket, head, (ws) => { + this.lobbiesWss.emit("connection", ws, request); + }); + } else { + this.gameWss.handleUpgrade(request, socket, head, (ws) => { + this.gameWss.emit("connection", ws, request); + }); + } + }); + } + + private setupLobbiesWebSocket() { + this.lobbiesWss.on("connection", (ws: WebSocket) => { + this.lobbyClients.add(ws); + ws.on("close", () => { + this.lobbyClients.delete(ws); + }); + + ws.on("error", (error) => { + this.log.error(`Lobbies WebSocket error:`, error); + this.lobbyClients.delete(ws); + try { + if ( + ws.readyState === WebSocket.OPEN || + ws.readyState === WebSocket.CONNECTING + ) { + ws.close(1011, "WebSocket internal error"); + } + } catch (closeError) { + this.log.error("Error closing lobbies WebSocket:", closeError); + } + }); + }); + } + + private broadcastLobbiesToClients(publicGames: PublicGames) { + const message = JSON.stringify(publicGames); + + const clientsToRemove: WebSocket[] = []; + this.lobbyClients.forEach((client) => { + if (client.readyState === WebSocket.OPEN) { + client.send(message); + } else { + clientsToRemove.push(client); + } + }); + + clientsToRemove.forEach((client) => { + this.lobbyClients.delete(client); + }); + } +} diff --git a/tests/client/LobbySocket.test.ts b/tests/client/LobbySocket.test.ts deleted file mode 100644 index 3e6e8d901..000000000 --- a/tests/client/LobbySocket.test.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { PublicLobbySocket } from "../../src/client/LobbySocket"; - -class MockWebSocket extends EventTarget { - static instances: MockWebSocket[] = []; - static readonly OPEN = 1; - static readonly CLOSED = 3; - - readonly url: string; - readyState = MockWebSocket.OPEN; - - constructor(url: string) { - super(); - this.url = url; - MockWebSocket.instances.push(this); - } - - addEventListener( - type: string, - listener: EventListenerOrEventListenerObject, - options?: boolean | AddEventListenerOptions, - ): void { - super.addEventListener(type, listener, options); - } - - close(code?: number, reason?: string) { - this.readyState = MockWebSocket.CLOSED; - this.dispatchEvent(new CloseEvent("close", { code, reason })); - } - - send(_data: unknown) {} -} - -describe("PublicLobbySocket", () => { - const originalWebSocket = globalThis.WebSocket; - const originalFetch = globalThis.fetch; - - beforeEach(() => { - MockWebSocket.instances = []; - // @ts-expect-error assign test mock - globalThis.WebSocket = MockWebSocket; - }); - - afterEach(() => { - globalThis.WebSocket = originalWebSocket; - globalThis.fetch = originalFetch; - vi.useRealTimers(); - }); - - it("delivers lobby updates from websocket messages", () => { - const updates: unknown[][] = []; - const socket = new PublicLobbySocket((lobbies) => updates.push(lobbies)); - - socket.start(); - const ws = MockWebSocket.instances.at(-1); - expect(ws?.url).toContain("/lobbies"); - - ws?.dispatchEvent( - new MessageEvent("message", { - data: JSON.stringify({ - type: "lobbies_update", - data: { - lobbies: [ - { - gameID: "g1", - numClients: 1, - gameConfig: { - maxPlayers: 2, - gameMode: 0, - gameMap: "Earth", - }, - }, - ], - }, - }), - }), - ); - - expect(updates).toHaveLength(1); - expect((updates[0][0] as { gameID: string }).gameID).toBe("g1"); - - socket.stop(); - }); - - it("falls back to HTTP polling after max websocket attempts", async () => { - vi.useFakeTimers(); - - const fetchMock = vi.fn().mockResolvedValue({ - ok: true, - json: async () => ({ lobbies: [] }), - }); - - globalThis.fetch = fetchMock as unknown as typeof fetch; - - const socket = new PublicLobbySocket(() => {}, { - maxWsAttempts: 1, - reconnectDelay: 0, - pollIntervalMs: 50, - }); - - socket.start(); - const ws = MockWebSocket.instances.at(-1); - ws?.dispatchEvent(new CloseEvent("close")); - - await Promise.resolve(); - expect(fetchMock).toHaveBeenCalledTimes(1); - - vi.advanceTimersByTime(60); - await Promise.resolve(); - expect(fetchMock).toHaveBeenCalledTimes(2); - - socket.stop(); - }); -}); From 9a4742f378567f01c8b8c723484a70d60f037bc1 Mon Sep 17 00:00:00 2001 From: evanpelle Date: Tue, 3 Feb 2026 20:30:27 -0800 Subject: [PATCH 11/20] update package-lock.json --- package-lock.json | 1419 +++++++++++++++++++++++---------------------- 1 file changed, 718 insertions(+), 701 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8169f6eec..45bbe6db1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -348,595 +348,628 @@ } }, "node_modules/@aws-sdk/client-s3": { - "version": "3.830.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.830.0.tgz", - "integrity": "sha512-Cti+zj1lqvQIScXFQv8/t1xo3pvcvk/ObmGIbyLzfgcYpKMHaIWhzhi6aN+z4dYEv1EwrukC9tNoqScyShc5tw==", + "version": "3.982.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.982.0.tgz", + "integrity": "sha512-k0ANYAtPiON9BwLXcDgJXkmmCAGEuSk2pZOvrMej2kNhs3xTXoPshIUR5UMCD9apYiWtXJJfXMZSgaME+iWNaQ==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.826.0", - "@aws-sdk/credential-provider-node": "3.830.0", - "@aws-sdk/middleware-bucket-endpoint": "3.830.0", - "@aws-sdk/middleware-expect-continue": "3.821.0", - "@aws-sdk/middleware-flexible-checksums": "3.826.0", - "@aws-sdk/middleware-host-header": "3.821.0", - "@aws-sdk/middleware-location-constraint": "3.821.0", - "@aws-sdk/middleware-logger": "3.821.0", - "@aws-sdk/middleware-recursion-detection": "3.821.0", - "@aws-sdk/middleware-sdk-s3": "3.826.0", - "@aws-sdk/middleware-ssec": "3.821.0", - "@aws-sdk/middleware-user-agent": "3.828.0", - "@aws-sdk/region-config-resolver": "3.821.0", - "@aws-sdk/signature-v4-multi-region": "3.826.0", - "@aws-sdk/types": "3.821.0", - "@aws-sdk/util-endpoints": "3.828.0", - "@aws-sdk/util-user-agent-browser": "3.821.0", - "@aws-sdk/util-user-agent-node": "3.828.0", - "@aws-sdk/xml-builder": "3.821.0", - "@smithy/config-resolver": "^4.1.4", - "@smithy/core": "^3.5.3", - "@smithy/eventstream-serde-browser": "^4.0.4", - "@smithy/eventstream-serde-config-resolver": "^4.1.2", - "@smithy/eventstream-serde-node": "^4.0.4", - "@smithy/fetch-http-handler": "^5.0.4", - "@smithy/hash-blob-browser": "^4.0.4", - "@smithy/hash-node": "^4.0.4", - "@smithy/hash-stream-node": "^4.0.4", - "@smithy/invalid-dependency": "^4.0.4", - "@smithy/md5-js": "^4.0.4", - "@smithy/middleware-content-length": "^4.0.4", - "@smithy/middleware-endpoint": "^4.1.11", - "@smithy/middleware-retry": "^4.1.12", - "@smithy/middleware-serde": "^4.0.8", - "@smithy/middleware-stack": "^4.0.4", - "@smithy/node-config-provider": "^4.1.3", - "@smithy/node-http-handler": "^4.0.6", - "@smithy/protocol-http": "^5.1.2", - "@smithy/smithy-client": "^4.4.3", - "@smithy/types": "^4.3.1", - "@smithy/url-parser": "^4.0.4", - "@smithy/util-base64": "^4.0.0", - "@smithy/util-body-length-browser": "^4.0.0", - "@smithy/util-body-length-node": "^4.0.0", - "@smithy/util-defaults-mode-browser": "^4.0.19", - "@smithy/util-defaults-mode-node": "^4.0.19", - "@smithy/util-endpoints": "^3.0.6", - "@smithy/util-middleware": "^4.0.4", - "@smithy/util-retry": "^4.0.5", - "@smithy/util-stream": "^4.2.2", - "@smithy/util-utf8": "^4.0.0", - "@smithy/util-waiter": "^4.0.5", + "@aws-sdk/core": "^3.973.6", + "@aws-sdk/credential-provider-node": "^3.972.5", + "@aws-sdk/middleware-bucket-endpoint": "^3.972.3", + "@aws-sdk/middleware-expect-continue": "^3.972.3", + "@aws-sdk/middleware-flexible-checksums": "^3.972.4", + "@aws-sdk/middleware-host-header": "^3.972.3", + "@aws-sdk/middleware-location-constraint": "^3.972.3", + "@aws-sdk/middleware-logger": "^3.972.3", + "@aws-sdk/middleware-recursion-detection": "^3.972.3", + "@aws-sdk/middleware-sdk-s3": "^3.972.6", + "@aws-sdk/middleware-ssec": "^3.972.3", + "@aws-sdk/middleware-user-agent": "^3.972.6", + "@aws-sdk/region-config-resolver": "^3.972.3", + "@aws-sdk/signature-v4-multi-region": "3.982.0", + "@aws-sdk/types": "^3.973.1", + "@aws-sdk/util-endpoints": "3.982.0", + "@aws-sdk/util-user-agent-browser": "^3.972.3", + "@aws-sdk/util-user-agent-node": "^3.972.4", + "@smithy/config-resolver": "^4.4.6", + "@smithy/core": "^3.22.0", + "@smithy/eventstream-serde-browser": "^4.2.8", + "@smithy/eventstream-serde-config-resolver": "^4.3.8", + "@smithy/eventstream-serde-node": "^4.2.8", + "@smithy/fetch-http-handler": "^5.3.9", + "@smithy/hash-blob-browser": "^4.2.9", + "@smithy/hash-node": "^4.2.8", + "@smithy/hash-stream-node": "^4.2.8", + "@smithy/invalid-dependency": "^4.2.8", + "@smithy/md5-js": "^4.2.8", + "@smithy/middleware-content-length": "^4.2.8", + "@smithy/middleware-endpoint": "^4.4.12", + "@smithy/middleware-retry": "^4.4.29", + "@smithy/middleware-serde": "^4.2.9", + "@smithy/middleware-stack": "^4.2.8", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/node-http-handler": "^4.4.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/smithy-client": "^4.11.1", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.28", + "@smithy/util-defaults-mode-node": "^4.2.31", + "@smithy/util-endpoints": "^3.2.8", + "@smithy/util-middleware": "^4.2.8", + "@smithy/util-retry": "^4.2.8", + "@smithy/util-stream": "^4.5.10", + "@smithy/util-utf8": "^4.2.0", + "@smithy/util-waiter": "^4.2.8", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/client-sso": { - "version": "3.830.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.830.0.tgz", - "integrity": "sha512-5zCEpfI+zwX2SIa258L+TItNbBoAvQQ6w74qdFM6YJufQ1F9tvwjTX8T+eSTT9nsFIvfYnUaGalWwJVfmJUgVQ==", + "version": "3.982.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.982.0.tgz", + "integrity": "sha512-qJrIiivmvujdGqJ0ldSUvhN3k3N7GtPesoOI1BSt0fNXovVnMz4C/JmnkhZihU7hJhDvxJaBROLYTU+lpild4w==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.826.0", - "@aws-sdk/middleware-host-header": "3.821.0", - "@aws-sdk/middleware-logger": "3.821.0", - "@aws-sdk/middleware-recursion-detection": "3.821.0", - "@aws-sdk/middleware-user-agent": "3.828.0", - "@aws-sdk/region-config-resolver": "3.821.0", - "@aws-sdk/types": "3.821.0", - "@aws-sdk/util-endpoints": "3.828.0", - "@aws-sdk/util-user-agent-browser": "3.821.0", - "@aws-sdk/util-user-agent-node": "3.828.0", - "@smithy/config-resolver": "^4.1.4", - "@smithy/core": "^3.5.3", - "@smithy/fetch-http-handler": "^5.0.4", - "@smithy/hash-node": "^4.0.4", - "@smithy/invalid-dependency": "^4.0.4", - "@smithy/middleware-content-length": "^4.0.4", - "@smithy/middleware-endpoint": "^4.1.11", - "@smithy/middleware-retry": "^4.1.12", - "@smithy/middleware-serde": "^4.0.8", - "@smithy/middleware-stack": "^4.0.4", - "@smithy/node-config-provider": "^4.1.3", - "@smithy/node-http-handler": "^4.0.6", - "@smithy/protocol-http": "^5.1.2", - "@smithy/smithy-client": "^4.4.3", - "@smithy/types": "^4.3.1", - "@smithy/url-parser": "^4.0.4", - "@smithy/util-base64": "^4.0.0", - "@smithy/util-body-length-browser": "^4.0.0", - "@smithy/util-body-length-node": "^4.0.0", - "@smithy/util-defaults-mode-browser": "^4.0.19", - "@smithy/util-defaults-mode-node": "^4.0.19", - "@smithy/util-endpoints": "^3.0.6", - "@smithy/util-middleware": "^4.0.4", - "@smithy/util-retry": "^4.0.5", - "@smithy/util-utf8": "^4.0.0", + "@aws-sdk/core": "^3.973.6", + "@aws-sdk/middleware-host-header": "^3.972.3", + "@aws-sdk/middleware-logger": "^3.972.3", + "@aws-sdk/middleware-recursion-detection": "^3.972.3", + "@aws-sdk/middleware-user-agent": "^3.972.6", + "@aws-sdk/region-config-resolver": "^3.972.3", + "@aws-sdk/types": "^3.973.1", + "@aws-sdk/util-endpoints": "3.982.0", + "@aws-sdk/util-user-agent-browser": "^3.972.3", + "@aws-sdk/util-user-agent-node": "^3.972.4", + "@smithy/config-resolver": "^4.4.6", + "@smithy/core": "^3.22.0", + "@smithy/fetch-http-handler": "^5.3.9", + "@smithy/hash-node": "^4.2.8", + "@smithy/invalid-dependency": "^4.2.8", + "@smithy/middleware-content-length": "^4.2.8", + "@smithy/middleware-endpoint": "^4.4.12", + "@smithy/middleware-retry": "^4.4.29", + "@smithy/middleware-serde": "^4.2.9", + "@smithy/middleware-stack": "^4.2.8", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/node-http-handler": "^4.4.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/smithy-client": "^4.11.1", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.28", + "@smithy/util-defaults-mode-node": "^4.2.31", + "@smithy/util-endpoints": "^3.2.8", + "@smithy/util-middleware": "^4.2.8", + "@smithy/util-retry": "^4.2.8", + "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/core": { - "version": "3.826.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.826.0.tgz", - "integrity": "sha512-BGbQYzWj3ps+dblq33FY5tz/SsgJCcXX0zjQlSC07tYvU1jHTUvsefphyig+fY38xZ4wdKjbTop+KUmXUYrOXw==", + "version": "3.973.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.6.tgz", + "integrity": "sha512-pz4ZOw3BLG0NdF25HoB9ymSYyPbMiIjwQJ2aROXRhAzt+b+EOxStfFv8s5iZyP6Kiw7aYhyWxj5G3NhmkoOTKw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.821.0", - "@aws-sdk/xml-builder": "3.821.0", - "@smithy/core": "^3.5.3", - "@smithy/node-config-provider": "^4.1.3", - "@smithy/property-provider": "^4.0.4", - "@smithy/protocol-http": "^5.1.2", - "@smithy/signature-v4": "^5.1.2", - "@smithy/smithy-client": "^4.4.3", - "@smithy/types": "^4.3.1", - "@smithy/util-base64": "^4.0.0", - "@smithy/util-body-length-browser": "^4.0.0", - "@smithy/util-middleware": "^4.0.4", - "@smithy/util-utf8": "^4.0.0", - "fast-xml-parser": "4.4.1", + "@aws-sdk/types": "^3.973.1", + "@aws-sdk/xml-builder": "^3.972.4", + "@smithy/core": "^3.22.0", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/property-provider": "^4.2.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/signature-v4": "^5.3.8", + "@smithy/smithy-client": "^4.11.1", + "@smithy/types": "^4.12.0", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-middleware": "^4.2.8", + "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/crc64-nvme": { + "version": "3.972.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/crc64-nvme/-/crc64-nvme-3.972.0.tgz", + "integrity": "sha512-ThlLhTqX68jvoIVv+pryOdb5coP1cX1/MaTbB9xkGDCbWbsqQcLqzPxuSoW1DCnAAIacmXCWpzUNOB9pv+xXQw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.826.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.826.0.tgz", - "integrity": "sha512-DK3pQY8+iKK3MGDdC3uOZQ2psU01obaKlTYhEwNu4VWzgwQL4Vi3sWj4xSWGEK41vqZxiRLq6fOq7ysRI+qEZA==", + "version": "3.972.4", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.4.tgz", + "integrity": "sha512-/8dnc7+XNMmViEom2xsNdArQxQPSgy4Z/lm6qaFPTrMFesT1bV3PsBhb19n09nmxHdrtQskYmViddUIjUQElXg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.826.0", - "@aws-sdk/types": "3.821.0", - "@smithy/property-provider": "^4.0.4", - "@smithy/types": "^4.3.1", + "@aws-sdk/core": "^3.973.6", + "@aws-sdk/types": "^3.973.1", + "@smithy/property-provider": "^4.2.8", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.826.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.826.0.tgz", - "integrity": "sha512-N+IVZBh+yx/9GbMZTKO/gErBi/FYZQtcFRItoLbY+6WU+0cSWyZYfkoeOxHmQV3iX9k65oljERIWUmL9x6OSQg==", + "version": "3.972.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.6.tgz", + "integrity": "sha512-5ERWqRljiZv44AIdvIRQ3k+EAV0Sq2WeJHvXuK7gL7bovSxOf8Al7MLH7Eh3rdovH4KHFnlIty7J71mzvQBl5Q==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.826.0", - "@aws-sdk/types": "3.821.0", - "@smithy/fetch-http-handler": "^5.0.4", - "@smithy/node-http-handler": "^4.0.6", - "@smithy/property-provider": "^4.0.4", - "@smithy/protocol-http": "^5.1.2", - "@smithy/smithy-client": "^4.4.3", - "@smithy/types": "^4.3.1", - "@smithy/util-stream": "^4.2.2", + "@aws-sdk/core": "^3.973.6", + "@aws-sdk/types": "^3.973.1", + "@smithy/fetch-http-handler": "^5.3.9", + "@smithy/node-http-handler": "^4.4.8", + "@smithy/property-provider": "^4.2.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/smithy-client": "^4.11.1", + "@smithy/types": "^4.12.0", + "@smithy/util-stream": "^4.5.10", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.830.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.830.0.tgz", - "integrity": "sha512-zeQenzvh8JRY5nULd8izdjVGoCM1tgsVVsrLSwDkHxZTTW0hW/bmOmXfvdaE0wDdomXW7m2CkQDSmP7XdvNXZg==", + "version": "3.972.4", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.4.tgz", + "integrity": "sha512-eRUg+3HaUKuXWn/lEMirdiA5HOKmEl8hEHVuszIDt2MMBUKgVX5XNGmb3XmbgU17h6DZ+RtjbxQpjhz3SbTjZg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.826.0", - "@aws-sdk/credential-provider-env": "3.826.0", - "@aws-sdk/credential-provider-http": "3.826.0", - "@aws-sdk/credential-provider-process": "3.826.0", - "@aws-sdk/credential-provider-sso": "3.830.0", - "@aws-sdk/credential-provider-web-identity": "3.830.0", - "@aws-sdk/nested-clients": "3.830.0", - "@aws-sdk/types": "3.821.0", - "@smithy/credential-provider-imds": "^4.0.6", - "@smithy/property-provider": "^4.0.4", - "@smithy/shared-ini-file-loader": "^4.0.4", - "@smithy/types": "^4.3.1", + "@aws-sdk/core": "^3.973.6", + "@aws-sdk/credential-provider-env": "^3.972.4", + "@aws-sdk/credential-provider-http": "^3.972.6", + "@aws-sdk/credential-provider-login": "^3.972.4", + "@aws-sdk/credential-provider-process": "^3.972.4", + "@aws-sdk/credential-provider-sso": "^3.972.4", + "@aws-sdk/credential-provider-web-identity": "^3.972.4", + "@aws-sdk/nested-clients": "3.982.0", + "@aws-sdk/types": "^3.973.1", + "@smithy/credential-provider-imds": "^4.2.8", + "@smithy/property-provider": "^4.2.8", + "@smithy/shared-ini-file-loader": "^4.4.3", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login": { + "version": "3.972.4", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.4.tgz", + "integrity": "sha512-nLGjXuvWWDlQAp505xIONI7Gam0vw2p7Qu3P6on/W2q7rjJXtYjtpHbcsaOjJ/pAju3eTvEQuSuRedcRHVQIAQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.6", + "@aws-sdk/nested-clients": "3.982.0", + "@aws-sdk/types": "^3.973.1", + "@smithy/property-provider": "^4.2.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/shared-ini-file-loader": "^4.4.3", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.830.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.830.0.tgz", - "integrity": "sha512-X/2LrTgwtK1pkWrvofxQBI8VTi6QVLtSMpsKKPPnJQ0vgqC0e4czSIs3ZxiEsOkCBaQ2usXSiKyh0ccsQ6k2OA==", + "version": "3.972.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.5.tgz", + "integrity": "sha512-VWXKgSISQCI2GKN3zakTNHSiZ0+mux7v6YHmmbLQp/o3fvYUQJmKGcLZZzg2GFA+tGGBStplra9VFNf/WwxpYg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/credential-provider-env": "3.826.0", - "@aws-sdk/credential-provider-http": "3.826.0", - "@aws-sdk/credential-provider-ini": "3.830.0", - "@aws-sdk/credential-provider-process": "3.826.0", - "@aws-sdk/credential-provider-sso": "3.830.0", - "@aws-sdk/credential-provider-web-identity": "3.830.0", - "@aws-sdk/types": "3.821.0", - "@smithy/credential-provider-imds": "^4.0.6", - "@smithy/property-provider": "^4.0.4", - "@smithy/shared-ini-file-loader": "^4.0.4", - "@smithy/types": "^4.3.1", + "@aws-sdk/credential-provider-env": "^3.972.4", + "@aws-sdk/credential-provider-http": "^3.972.6", + "@aws-sdk/credential-provider-ini": "^3.972.4", + "@aws-sdk/credential-provider-process": "^3.972.4", + "@aws-sdk/credential-provider-sso": "^3.972.4", + "@aws-sdk/credential-provider-web-identity": "^3.972.4", + "@aws-sdk/types": "^3.973.1", + "@smithy/credential-provider-imds": "^4.2.8", + "@smithy/property-provider": "^4.2.8", + "@smithy/shared-ini-file-loader": "^4.4.3", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.826.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.826.0.tgz", - "integrity": "sha512-kURrc4amu3NLtw1yZw7EoLNEVhmOMRUTs+chaNcmS+ERm3yK0nKjaJzmKahmwlTQTSl3wJ8jjK7x962VPo+zWw==", + "version": "3.972.4", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.4.tgz", + "integrity": "sha512-TCZpWUnBQN1YPk6grvd5x419OfXjHvhj5Oj44GYb84dOVChpg/+2VoEj+YVA4F4E/6huQPNnX7UYbTtxJqgihw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.826.0", - "@aws-sdk/types": "3.821.0", - "@smithy/property-provider": "^4.0.4", - "@smithy/shared-ini-file-loader": "^4.0.4", - "@smithy/types": "^4.3.1", + "@aws-sdk/core": "^3.973.6", + "@aws-sdk/types": "^3.973.1", + "@smithy/property-provider": "^4.2.8", + "@smithy/shared-ini-file-loader": "^4.4.3", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.830.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.830.0.tgz", - "integrity": "sha512-+VdRpZmfekzpySqZikAKx6l5ndnLGluioIgUG4ZznrButgFD/iogzFtGmBDFB3ZLViX1l4pMXru0zFwJEZT21Q==", + "version": "3.972.4", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.4.tgz", + "integrity": "sha512-wzsGwv9mKlwJ3vHLyembBvGE/5nPUIwRR2I51B1cBV4Cb4ql9nIIfpmHzm050XYTY5fqTOKJQnhLj7zj89VG8g==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/client-sso": "3.830.0", - "@aws-sdk/core": "3.826.0", - "@aws-sdk/token-providers": "3.830.0", - "@aws-sdk/types": "3.821.0", - "@smithy/property-provider": "^4.0.4", - "@smithy/shared-ini-file-loader": "^4.0.4", - "@smithy/types": "^4.3.1", + "@aws-sdk/client-sso": "3.982.0", + "@aws-sdk/core": "^3.973.6", + "@aws-sdk/token-providers": "3.982.0", + "@aws-sdk/types": "^3.973.1", + "@smithy/property-provider": "^4.2.8", + "@smithy/shared-ini-file-loader": "^4.4.3", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.830.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.830.0.tgz", - "integrity": "sha512-hPYrKsZeeOdLROJ59T6Y8yZ0iwC/60L3qhZXjapBFjbqBtMaQiMTI645K6xVXBioA6vxXq7B4aLOhYqk6Fy/Ww==", + "version": "3.972.4", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.4.tgz", + "integrity": "sha512-hIzw2XzrG8jzsUSEatehmpkd5rWzASg5IHUfA+m01k/RtvfAML7ZJVVohuKdhAYx+wV2AThLiQJVzqn7F0khrw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.826.0", - "@aws-sdk/nested-clients": "3.830.0", - "@aws-sdk/types": "3.821.0", - "@smithy/property-provider": "^4.0.4", - "@smithy/types": "^4.3.1", + "@aws-sdk/core": "^3.973.6", + "@aws-sdk/nested-clients": "3.982.0", + "@aws-sdk/types": "^3.973.1", + "@smithy/property-provider": "^4.2.8", + "@smithy/shared-ini-file-loader": "^4.4.3", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/middleware-bucket-endpoint": { - "version": "3.830.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.830.0.tgz", - "integrity": "sha512-ElVeCReZSH5Ds+/pkL5ebneJjuo8f49e9JXV1cYizuH0OAOQfYaBU9+M+7+rn61pTttOFE8W//qKzrXBBJhfMg==", + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.972.3.tgz", + "integrity": "sha512-fmbgWYirF67YF1GfD7cg5N6HHQ96EyRNx/rDIrTF277/zTWVuPI2qS/ZHgofwR1NZPe/NWvoppflQY01LrbVLg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.821.0", - "@aws-sdk/util-arn-parser": "3.804.0", - "@smithy/node-config-provider": "^4.1.3", - "@smithy/protocol-http": "^5.1.2", - "@smithy/types": "^4.3.1", - "@smithy/util-config-provider": "^4.0.0", + "@aws-sdk/types": "^3.973.1", + "@aws-sdk/util-arn-parser": "^3.972.2", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/types": "^4.12.0", + "@smithy/util-config-provider": "^4.2.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/middleware-expect-continue": { - "version": "3.821.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.821.0.tgz", - "integrity": "sha512-zAOoSZKe1njOrtynvK6ZORU57YGv5I7KP4+rwOvUN3ZhJbQ7QPf8gKtFUCYAPRMegaXCKF/ADPtDZBAmM+zZ9g==", + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.972.3.tgz", + "integrity": "sha512-4msC33RZsXQpUKR5QR4HnvBSNCPLGHmB55oDiROqqgyOc+TOfVu2xgi5goA7ms6MdZLeEh2905UfWMnMMF4mRg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.821.0", - "@smithy/protocol-http": "^5.1.2", - "@smithy/types": "^4.3.1", + "@aws-sdk/types": "^3.973.1", + "@smithy/protocol-http": "^5.3.8", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/middleware-flexible-checksums": { - "version": "3.826.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.826.0.tgz", - "integrity": "sha512-Fz9w8CFYPfSlHEB6feSsi06hdS+s+FB8k5pO4L7IV0tUa78mlhxF/VNlAJaVWYyOkZXl4HPH2K48aapACSQOXw==", + "version": "3.972.4", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.972.4.tgz", + "integrity": "sha512-xOxsUkF3O3BtIe3tf54OpPo94eZepjFm3z0Dd2TZKbsPxMiRTFXurC04wJ58o/wPW9YHVO9VqZik3MfoPfrKlw==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/crc32": "5.2.0", "@aws-crypto/crc32c": "5.2.0", "@aws-crypto/util": "5.2.0", - "@aws-sdk/core": "3.826.0", - "@aws-sdk/types": "3.821.0", - "@smithy/is-array-buffer": "^4.0.0", - "@smithy/node-config-provider": "^4.1.3", - "@smithy/protocol-http": "^5.1.2", - "@smithy/types": "^4.3.1", - "@smithy/util-middleware": "^4.0.4", - "@smithy/util-stream": "^4.2.2", - "@smithy/util-utf8": "^4.0.0", + "@aws-sdk/core": "^3.973.6", + "@aws-sdk/crc64-nvme": "3.972.0", + "@aws-sdk/types": "^3.973.1", + "@smithy/is-array-buffer": "^4.2.0", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/types": "^4.12.0", + "@smithy/util-middleware": "^4.2.8", + "@smithy/util-stream": "^4.5.10", + "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/middleware-host-header": { - "version": "3.821.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.821.0.tgz", - "integrity": "sha512-xSMR+sopSeWGx5/4pAGhhfMvGBHioVBbqGvDs6pG64xfNwM5vq5s5v6D04e2i+uSTj4qGa71dLUs5I0UzAK3sw==", + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.3.tgz", + "integrity": "sha512-aknPTb2M+G3s+0qLCx4Li/qGZH8IIYjugHMv15JTYMe6mgZO8VBpYgeGYsNMGCqCZOcWzuf900jFBG5bopfzmA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.821.0", - "@smithy/protocol-http": "^5.1.2", - "@smithy/types": "^4.3.1", + "@aws-sdk/types": "^3.973.1", + "@smithy/protocol-http": "^5.3.8", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/middleware-location-constraint": { - "version": "3.821.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.821.0.tgz", - "integrity": "sha512-sKrm80k0t3R0on8aA/WhWFoMaAl4yvdk+riotmMElLUpcMcRXAd1+600uFVrxJqZdbrKQ0mjX0PjT68DlkYXLg==", + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.972.3.tgz", + "integrity": "sha512-nIg64CVrsXp67vbK0U1/Is8rik3huS3QkRHn2DRDx4NldrEFMgdkZGI/+cZMKD9k4YOS110Dfu21KZLHrFA/1g==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.821.0", - "@smithy/types": "^4.3.1", + "@aws-sdk/types": "^3.973.1", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/middleware-logger": { - "version": "3.821.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.821.0.tgz", - "integrity": "sha512-0cvI0ipf2tGx7fXYEEN5fBeZDz2RnHyb9xftSgUsEq7NBxjV0yTZfLJw6Za5rjE6snC80dRN8+bTNR1tuG89zA==", + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.3.tgz", + "integrity": "sha512-Ftg09xNNRqaz9QNzlfdQWfpqMCJbsQdnZVJP55jfhbKi1+FTWxGuvfPoBhDHIovqWKjqbuiew3HuhxbJ0+OjgA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.821.0", - "@smithy/types": "^4.3.1", + "@aws-sdk/types": "^3.973.1", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.821.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.821.0.tgz", - "integrity": "sha512-efmaifbhBoqKG3bAoEfDdcM8hn1psF+4qa7ykWuYmfmah59JBeqHLfz5W9m9JoTwoKPkFcVLWZxnyZzAnVBOIg==", + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.3.tgz", + "integrity": "sha512-PY57QhzNuXHnwbJgbWYTrqIDHYSeOlhfYERTAuc16LKZpTZRJUjzBFokp9hF7u1fuGeE3D70ERXzdbMBOqQz7Q==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.821.0", - "@smithy/protocol-http": "^5.1.2", - "@smithy/types": "^4.3.1", + "@aws-sdk/types": "^3.973.1", + "@aws/lambda-invoke-store": "^0.2.2", + "@smithy/protocol-http": "^5.3.8", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/middleware-sdk-s3": { - "version": "3.826.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.826.0.tgz", - "integrity": "sha512-8F0qWaYKfvD/de1AKccXuigM+gb/IZSncCqxdnFWqd+TFzo9qI9Hh+TpUhWOMYSgxsMsYQ8ipmLzlD/lDhjrmA==", + "version": "3.972.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.6.tgz", + "integrity": "sha512-Xq7wM6kbgJN1UO++8dvH/efPb1nTwWqFCpZCR7RCLOETP7xAUAhVo7JmsCnML5Di/iC4Oo5VrJ4QmkYcMZniLw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.826.0", - "@aws-sdk/types": "3.821.0", - "@aws-sdk/util-arn-parser": "3.804.0", - "@smithy/core": "^3.5.3", - "@smithy/node-config-provider": "^4.1.3", - "@smithy/protocol-http": "^5.1.2", - "@smithy/signature-v4": "^5.1.2", - "@smithy/smithy-client": "^4.4.3", - "@smithy/types": "^4.3.1", - "@smithy/util-config-provider": "^4.0.0", - "@smithy/util-middleware": "^4.0.4", - "@smithy/util-stream": "^4.2.2", - "@smithy/util-utf8": "^4.0.0", + "@aws-sdk/core": "^3.973.6", + "@aws-sdk/types": "^3.973.1", + "@aws-sdk/util-arn-parser": "^3.972.2", + "@smithy/core": "^3.22.0", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/signature-v4": "^5.3.8", + "@smithy/smithy-client": "^4.11.1", + "@smithy/types": "^4.12.0", + "@smithy/util-config-provider": "^4.2.0", + "@smithy/util-middleware": "^4.2.8", + "@smithy/util-stream": "^4.5.10", + "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/middleware-ssec": { - "version": "3.821.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.821.0.tgz", - "integrity": "sha512-YYi1Hhr2AYiU/24cQc8HIB+SWbQo6FBkMYojVuz/zgrtkFmALxENGF/21OPg7f/QWd+eadZJRxCjmRwh5F2Cxg==", + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.972.3.tgz", + "integrity": "sha512-dU6kDuULN3o3jEHcjm0c4zWJlY1zWVkjG9NPe9qxYLLpcbdj5kRYBS2DdWYD+1B9f910DezRuws7xDEqKkHQIg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.821.0", - "@smithy/types": "^4.3.1", + "@aws-sdk/types": "^3.973.1", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.828.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.828.0.tgz", - "integrity": "sha512-nixvI/SETXRdmrVab4D9LvXT3lrXkwAWGWk2GVvQvzlqN1/M/RfClj+o37Sn4FqRkGH9o9g7Fqb1YqZ4mqDAtA==", + "version": "3.972.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.6.tgz", + "integrity": "sha512-TehLN8W/kivl0U9HcS+keryElEWORROpghDXZBLfnb40DXM7hx/i+7OOjkogXQOF3QtUraJVRkHQ07bPhrWKlw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.826.0", - "@aws-sdk/types": "3.821.0", - "@aws-sdk/util-endpoints": "3.828.0", - "@smithy/core": "^3.5.3", - "@smithy/protocol-http": "^5.1.2", - "@smithy/types": "^4.3.1", + "@aws-sdk/core": "^3.973.6", + "@aws-sdk/types": "^3.973.1", + "@aws-sdk/util-endpoints": "3.982.0", + "@smithy/core": "^3.22.0", + "@smithy/protocol-http": "^5.3.8", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/nested-clients": { - "version": "3.830.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.830.0.tgz", - "integrity": "sha512-5N5YTlBr1vtxf7+t+UaIQ625KEAmm7fY9o1e3MgGOi/paBoI0+axr3ud24qLIy0NSzFlAHEaxUSWxcERNjIoZw==", + "version": "3.982.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.982.0.tgz", + "integrity": "sha512-VVkaH27digrJfdVrT64rjkllvOp4oRiZuuJvrylLXAKl18ujToJR7AqpDldL/LS63RVne3QWIpkygIymxFtliQ==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.826.0", - "@aws-sdk/middleware-host-header": "3.821.0", - "@aws-sdk/middleware-logger": "3.821.0", - "@aws-sdk/middleware-recursion-detection": "3.821.0", - "@aws-sdk/middleware-user-agent": "3.828.0", - "@aws-sdk/region-config-resolver": "3.821.0", - "@aws-sdk/types": "3.821.0", - "@aws-sdk/util-endpoints": "3.828.0", - "@aws-sdk/util-user-agent-browser": "3.821.0", - "@aws-sdk/util-user-agent-node": "3.828.0", - "@smithy/config-resolver": "^4.1.4", - "@smithy/core": "^3.5.3", - "@smithy/fetch-http-handler": "^5.0.4", - "@smithy/hash-node": "^4.0.4", - "@smithy/invalid-dependency": "^4.0.4", - "@smithy/middleware-content-length": "^4.0.4", - "@smithy/middleware-endpoint": "^4.1.11", - "@smithy/middleware-retry": "^4.1.12", - "@smithy/middleware-serde": "^4.0.8", - "@smithy/middleware-stack": "^4.0.4", - "@smithy/node-config-provider": "^4.1.3", - "@smithy/node-http-handler": "^4.0.6", - "@smithy/protocol-http": "^5.1.2", - "@smithy/smithy-client": "^4.4.3", - "@smithy/types": "^4.3.1", - "@smithy/url-parser": "^4.0.4", - "@smithy/util-base64": "^4.0.0", - "@smithy/util-body-length-browser": "^4.0.0", - "@smithy/util-body-length-node": "^4.0.0", - "@smithy/util-defaults-mode-browser": "^4.0.19", - "@smithy/util-defaults-mode-node": "^4.0.19", - "@smithy/util-endpoints": "^3.0.6", - "@smithy/util-middleware": "^4.0.4", - "@smithy/util-retry": "^4.0.5", - "@smithy/util-utf8": "^4.0.0", + "@aws-sdk/core": "^3.973.6", + "@aws-sdk/middleware-host-header": "^3.972.3", + "@aws-sdk/middleware-logger": "^3.972.3", + "@aws-sdk/middleware-recursion-detection": "^3.972.3", + "@aws-sdk/middleware-user-agent": "^3.972.6", + "@aws-sdk/region-config-resolver": "^3.972.3", + "@aws-sdk/types": "^3.973.1", + "@aws-sdk/util-endpoints": "3.982.0", + "@aws-sdk/util-user-agent-browser": "^3.972.3", + "@aws-sdk/util-user-agent-node": "^3.972.4", + "@smithy/config-resolver": "^4.4.6", + "@smithy/core": "^3.22.0", + "@smithy/fetch-http-handler": "^5.3.9", + "@smithy/hash-node": "^4.2.8", + "@smithy/invalid-dependency": "^4.2.8", + "@smithy/middleware-content-length": "^4.2.8", + "@smithy/middleware-endpoint": "^4.4.12", + "@smithy/middleware-retry": "^4.4.29", + "@smithy/middleware-serde": "^4.2.9", + "@smithy/middleware-stack": "^4.2.8", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/node-http-handler": "^4.4.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/smithy-client": "^4.11.1", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.28", + "@smithy/util-defaults-mode-node": "^4.2.31", + "@smithy/util-endpoints": "^3.2.8", + "@smithy/util-middleware": "^4.2.8", + "@smithy/util-retry": "^4.2.8", + "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/region-config-resolver": { - "version": "3.821.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.821.0.tgz", - "integrity": "sha512-t8og+lRCIIy5nlId0bScNpCkif8sc0LhmtaKsbm0ZPm3sCa/WhCbSZibjbZ28FNjVCV+p0D9RYZx0VDDbtWyjw==", + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.3.tgz", + "integrity": "sha512-v4J8qYAWfOMcZ4MJUyatntOicTzEMaU7j3OpkRCGGFSL2NgXQ5VbxauIyORA+pxdKZ0qQG2tCQjQjZDlXEC3Ow==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.821.0", - "@smithy/node-config-provider": "^4.1.3", - "@smithy/types": "^4.3.1", - "@smithy/util-config-provider": "^4.0.0", - "@smithy/util-middleware": "^4.0.4", + "@aws-sdk/types": "^3.973.1", + "@smithy/config-resolver": "^4.4.6", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/signature-v4-multi-region": { - "version": "3.826.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.826.0.tgz", - "integrity": "sha512-3fEi/zy6tpMzomYosksGtu7jZqGFcdBXoL7YRsG7OEeQzBbOW9B+fVaQZ4jnsViSjzA/yKydLahMrfPnt+iaxg==", + "version": "3.982.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.982.0.tgz", + "integrity": "sha512-AWqjMAH848aNwnLCtIKM3WO00eHuUoYVfQMP4ccrUHhnEduGOusVgdHQ5mLNQZZNZzREuBwnPPhIP55cy0gFSg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-sdk-s3": "3.826.0", - "@aws-sdk/types": "3.821.0", - "@smithy/protocol-http": "^5.1.2", - "@smithy/signature-v4": "^5.1.2", - "@smithy/types": "^4.3.1", + "@aws-sdk/middleware-sdk-s3": "^3.972.6", + "@aws-sdk/types": "^3.973.1", + "@smithy/protocol-http": "^5.3.8", + "@smithy/signature-v4": "^5.3.8", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/token-providers": { - "version": "3.830.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.830.0.tgz", - "integrity": "sha512-aJ4guFwj92nV9D+EgJPaCFKK0I3y2uMchiDfh69Zqnmwfxxxfxat6F79VA7PS0BdbjRfhLbn+Ghjftnomu2c1g==", + "version": "3.982.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.982.0.tgz", + "integrity": "sha512-v3M0KYp2TVHYHNBT7jHD9lLTWAdS9CaWJ2jboRKt0WAB65bA7iUEpR+k4VqKYtpQN4+8kKSc4w+K6kUNZkHKQw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.826.0", - "@aws-sdk/nested-clients": "3.830.0", - "@aws-sdk/types": "3.821.0", - "@smithy/property-provider": "^4.0.4", - "@smithy/shared-ini-file-loader": "^4.0.4", - "@smithy/types": "^4.3.1", + "@aws-sdk/core": "^3.973.6", + "@aws-sdk/nested-clients": "3.982.0", + "@aws-sdk/types": "^3.973.1", + "@smithy/property-provider": "^4.2.8", + "@smithy/shared-ini-file-loader": "^4.4.3", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/types": { - "version": "3.821.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.821.0.tgz", - "integrity": "sha512-Znroqdai1a90TlxGaJ+FK1lwC0fHpo97Xjsp5UKGR5JODYm7f9+/fF17ebO1KdoBr/Rm0UIFiF5VmI8ts9F1eA==", + "version": "3.973.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.1.tgz", + "integrity": "sha512-DwHBiMNOB468JiX6+i34c+THsKHErYUdNQ3HexeXZvVn4zouLjgaS4FejiGSi2HyBuzuyHg7SuOPmjSvoU9NRg==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.3.1", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/util-arn-parser": { - "version": "3.804.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.804.0.tgz", - "integrity": "sha512-wmBJqn1DRXnZu3b4EkE6CWnoWMo1ZMvlfkqU5zPz67xx1GMaXlDCchFvKAXMjk4jn/L1O3tKnoFDNsoLV1kgNQ==", + "version": "3.972.2", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.972.2.tgz", + "integrity": "sha512-VkykWbqMjlSgBFDyrY3nOSqupMc6ivXuGmvci6Q3NnLq5kC+mKQe2QBZ4nrWRE/jqOxeFP2uYzLtwncYYcvQDg==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/util-endpoints": { - "version": "3.828.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.828.0.tgz", - "integrity": "sha512-RvKch111SblqdkPzg3oCIdlGxlQs+k+P7Etory9FmxPHyPDvsP1j1c74PmgYqtzzMWmoXTjd+c9naUHh9xG8xg==", + "version": "3.982.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.982.0.tgz", + "integrity": "sha512-M27u8FJP7O0Of9hMWX5dipp//8iglmV9jr7R8SR8RveU+Z50/8TqH68Tu6wUWBGMfXjzbVwn1INIAO5lZrlxXQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.821.0", - "@smithy/types": "^4.3.1", - "@smithy/util-endpoints": "^3.0.6", + "@aws-sdk/types": "^3.973.1", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", + "@smithy/util-endpoints": "^3.2.8", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/util-locate-window": { @@ -952,31 +985,31 @@ } }, "node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.821.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.821.0.tgz", - "integrity": "sha512-irWZHyM0Jr1xhC+38OuZ7JB6OXMLPZlj48thElpsO1ZSLRkLZx5+I7VV6k3sp2yZ7BYbKz/G2ojSv4wdm7XTLw==", + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.3.tgz", + "integrity": "sha512-JurOwkRUcXD/5MTDBcqdyQ9eVedtAsZgw5rBwktsPTN7QtPiS2Ld1jkJepNgYoCufz1Wcut9iup7GJDoIHp8Fw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.821.0", - "@smithy/types": "^4.3.1", + "@aws-sdk/types": "^3.973.1", + "@smithy/types": "^4.12.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.828.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.828.0.tgz", - "integrity": "sha512-LdN6fTBzTlQmc8O8f1wiZN0qF3yBWVGis7NwpWK7FUEzP9bEZRxYfIkV9oV9zpt6iNRze1SedK3JQVB/udxBoA==", + "version": "3.972.4", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.972.4.tgz", + "integrity": "sha512-3WFCBLiM8QiHDfosQq3Py+lIMgWlFWwFQliUHUqwEiRqLnKyhgbU3AKa7AWJF7lW2Oc/2kFNY4MlAYVnVc0i8A==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-user-agent": "3.828.0", - "@aws-sdk/types": "3.821.0", - "@smithy/node-config-provider": "^4.1.3", - "@smithy/types": "^4.3.1", + "@aws-sdk/middleware-user-agent": "^3.972.6", + "@aws-sdk/types": "^3.973.1", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" }, "peerDependencies": { "aws-crt": ">=1.0.0" @@ -988,14 +1021,24 @@ } }, "node_modules/@aws-sdk/xml-builder": { - "version": "3.821.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.821.0.tgz", - "integrity": "sha512-DIIotRnefVL6DiaHtO6/21DhJ4JZnnIwdNbpwiAhdt/AVbttcE4yw925gsjur0OGv5BTYXQXU3YnANBYnZjuQA==", + "version": "3.972.4", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.4.tgz", + "integrity": "sha512-0zJ05ANfYqI6+rGqj8samZBFod0dPPousBjLEqg8WdxSgbMAkRgLyn81lP215Do0rFJ/17LIXwr7q0yK24mP6Q==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.3.1", + "@smithy/types": "^4.12.0", + "fast-xml-parser": "5.3.4", "tslib": "^2.6.2" }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws/lambda-invoke-store": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.3.tgz", + "integrity": "sha512-oLvsaPMTBejkkmHhjf09xTgk71mOqyr/409NKhRIL08If7AhVfUsJhVsx386uJaqNd42v9kWamQ9lFbkoC2dYw==", + "license": "Apache-2.0", "engines": { "node": ">=18.0.0" } @@ -1189,7 +1232,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -1233,7 +1275,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -2014,9 +2055,9 @@ } }, "node_modules/@isaacs/brace-expansion": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", - "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.1.tgz", + "integrity": "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2167,7 +2208,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=8.0.0" } @@ -3000,12 +3040,12 @@ } }, "node_modules/@smithy/abort-controller": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.0.4.tgz", - "integrity": "sha512-gJnEjZMvigPDQWHrW3oPrFhQtkrgqBkyjj3pCIdF3A5M6vsZODG93KNlfJprv6bp4245bdT32fsHK4kkH3KYDA==", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.8.tgz", + "integrity": "sha512-peuVfkYHAmS5ybKxWcfraK7WBBP0J+rkfUcbHJJKQ4ir3UAUNQI+Y4Vt/PqSzGqgloJ5O1dk7+WzNL8wcCSXbw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.3.1", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { @@ -3013,9 +3053,9 @@ } }, "node_modules/@smithy/chunked-blob-reader": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader/-/chunked-blob-reader-5.0.0.tgz", - "integrity": "sha512-+sKqDBQqb036hh4NPaUiEkYFkTUGYzRsn3EuFhyfQfMy6oGHEUJDurLP9Ufb5dasr/XiAmPNMr6wa9afjQB+Gw==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader/-/chunked-blob-reader-5.2.0.tgz", + "integrity": "sha512-WmU0TnhEAJLWvfSeMxBNe5xtbselEO8+4wG0NtZeL8oR21WgH1xiO37El+/Y+H/Ie4SCwBy3MxYWmOYaGgZueA==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -3025,12 +3065,12 @@ } }, "node_modules/@smithy/chunked-blob-reader-native": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader-native/-/chunked-blob-reader-native-4.0.0.tgz", - "integrity": "sha512-R9wM2yPmfEMsUmlMlIgSzOyICs0x9uu7UTHoccMyt7BWw8shcGM8HqB355+BZCPBcySvbTYMs62EgEQkNxz2ig==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader-native/-/chunked-blob-reader-native-4.2.1.tgz", + "integrity": "sha512-lX9Ay+6LisTfpLid2zZtIhSEjHMZoAR5hHCR4H7tBz/Zkfr5ea8RcQ7Tk4mi0P76p4cN+Btz16Ffno7YHpKXnQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/util-base64": "^4.0.0", + "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" }, "engines": { @@ -3038,16 +3078,16 @@ } }, "node_modules/@smithy/config-resolver": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.5.tgz", - "integrity": "sha512-HAGoUAFYsUkoSckuKbCPayECeMim8pOu+yLy1zOxt1sifzEbrsRpYa+mKcMdiHKMeiqOibyPG0sFJnmaV/OGEg==", + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.6.tgz", + "integrity": "sha512-qJpzYC64kaj3S0fueiu3kXm8xPrR3PcXDPEgnaNMRn0EjNSZFoFjvbUp0YUDsRhN1CB90EnHJtbxWKevnH99UQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.7", - "@smithy/types": "^4.11.0", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/types": "^4.12.0", "@smithy/util-config-provider": "^4.2.0", - "@smithy/util-endpoints": "^3.2.7", - "@smithy/util-middleware": "^4.2.7", + "@smithy/util-endpoints": "^3.2.8", + "@smithy/util-middleware": "^4.2.8", "tslib": "^2.6.2" }, "engines": { @@ -3055,19 +3095,20 @@ } }, "node_modules/@smithy/core": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.5.3.tgz", - "integrity": "sha512-xa5byV9fEguZNofCclv6v9ra0FYh5FATQW/da7FQUVTic94DfrN/NvmKZjrMyzbpqfot9ZjBaO8U1UeTbmSLuA==", + "version": "3.22.1", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.22.1.tgz", + "integrity": "sha512-x3ie6Crr58MWrm4viHqqy2Du2rHYZjwu8BekasrQx4ca+Y24dzVAwq3yErdqIbc2G3I0kLQA13PQ+/rde+u65g==", "license": "Apache-2.0", "dependencies": { - "@smithy/middleware-serde": "^4.0.8", - "@smithy/protocol-http": "^5.1.2", - "@smithy/types": "^4.3.1", - "@smithy/util-base64": "^4.0.0", - "@smithy/util-body-length-browser": "^4.0.0", - "@smithy/util-middleware": "^4.0.4", - "@smithy/util-stream": "^4.2.2", - "@smithy/util-utf8": "^4.0.0", + "@smithy/middleware-serde": "^4.2.9", + "@smithy/protocol-http": "^5.3.8", + "@smithy/types": "^4.12.0", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-middleware": "^4.2.8", + "@smithy/util-stream": "^4.5.11", + "@smithy/util-utf8": "^4.2.0", + "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" }, "engines": { @@ -3075,15 +3116,15 @@ } }, "node_modules/@smithy/credential-provider-imds": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.0.6.tgz", - "integrity": "sha512-hKMWcANhUiNbCJouYkZ9V3+/Qf9pteR1dnwgdyzR09R4ODEYx8BbUysHwRSyex4rZ9zapddZhLFTnT4ZijR4pw==", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.8.tgz", + "integrity": "sha512-FNT0xHS1c/CPN8upqbMFP83+ul5YgdisfCfkZ86Jh2NSmnqw/AJ6x5pEogVCTVvSm7j9MopRU89bmDelxuDMYw==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.1.3", - "@smithy/property-provider": "^4.0.4", - "@smithy/types": "^4.3.1", - "@smithy/url-parser": "^4.0.4", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/property-provider": "^4.2.8", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", "tslib": "^2.6.2" }, "engines": { @@ -3091,14 +3132,14 @@ } }, "node_modules/@smithy/eventstream-codec": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.0.4.tgz", - "integrity": "sha512-7XoWfZqWb/QoR/rAU4VSi0mWnO2vu9/ltS6JZ5ZSZv0eovLVfDfu0/AX4ub33RsJTOth3TiFWSHS5YdztvFnig==", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.2.8.tgz", + "integrity": "sha512-jS/O5Q14UsufqoGhov7dHLOPCzkYJl9QDzusI2Psh4wyYx/izhzvX9P4D69aTxcdfVhEPhjK+wYyn/PzLjKbbw==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/crc32": "5.2.0", - "@smithy/types": "^4.3.1", - "@smithy/util-hex-encoding": "^4.0.0", + "@smithy/types": "^4.12.0", + "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" }, "engines": { @@ -3106,13 +3147,13 @@ } }, "node_modules/@smithy/eventstream-serde-browser": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.0.4.tgz", - "integrity": "sha512-3fb/9SYaYqbpy/z/H3yIi0bYKyAa89y6xPmIqwr2vQiUT2St+avRt8UKwsWt9fEdEasc5d/V+QjrviRaX1JRFA==", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.2.8.tgz", + "integrity": "sha512-MTfQT/CRQz5g24ayXdjg53V0mhucZth4PESoA5IhvaWVDTOQLfo8qI9vzqHcPsdd2v6sqfTYqF5L/l+pea5Uyw==", "license": "Apache-2.0", "dependencies": { - "@smithy/eventstream-serde-universal": "^4.0.4", - "@smithy/types": "^4.3.1", + "@smithy/eventstream-serde-universal": "^4.2.8", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { @@ -3120,12 +3161,12 @@ } }, "node_modules/@smithy/eventstream-serde-config-resolver": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.1.2.tgz", - "integrity": "sha512-JGtambizrWP50xHgbzZI04IWU7LdI0nh/wGbqH3sJesYToMi2j/DcoElqyOcqEIG/D4tNyxgRuaqBXWE3zOFhQ==", + "version": "4.3.8", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.3.8.tgz", + "integrity": "sha512-ah12+luBiDGzBruhu3efNy1IlbwSEdNiw8fOZksoKoWW1ZHvO/04MQsdnws/9Aj+5b0YXSSN2JXKy/ClIsW8MQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.3.1", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { @@ -3133,13 +3174,13 @@ } }, "node_modules/@smithy/eventstream-serde-node": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.0.4.tgz", - "integrity": "sha512-RD6UwNZ5zISpOWPuhVgRz60GkSIp0dy1fuZmj4RYmqLVRtejFqQ16WmfYDdoSoAjlp1LX+FnZo+/hkdmyyGZ1w==", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.2.8.tgz", + "integrity": "sha512-cYpCpp29z6EJHa5T9WL0KAlq3SOKUQkcgSoeRfRVwjGgSFl7Uh32eYGt7IDYCX20skiEdRffyDpvF2efEZPC0A==", "license": "Apache-2.0", "dependencies": { - "@smithy/eventstream-serde-universal": "^4.0.4", - "@smithy/types": "^4.3.1", + "@smithy/eventstream-serde-universal": "^4.2.8", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { @@ -3147,13 +3188,13 @@ } }, "node_modules/@smithy/eventstream-serde-universal": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.0.4.tgz", - "integrity": "sha512-UeJpOmLGhq1SLox79QWw/0n2PFX+oPRE1ZyRMxPIaFEfCqWaqpB7BU9C8kpPOGEhLF7AwEqfFbtwNxGy4ReENA==", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.2.8.tgz", + "integrity": "sha512-iJ6YNJd0bntJYnX6s52NC4WFYcZeKrPUr1Kmmr5AwZcwCSzVpS7oavAmxMR7pMq7V+D1G4s9F5NJK0xwOsKAlQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/eventstream-codec": "^4.0.4", - "@smithy/types": "^4.3.1", + "@smithy/eventstream-codec": "^4.2.8", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { @@ -3161,15 +3202,15 @@ } }, "node_modules/@smithy/fetch-http-handler": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.0.4.tgz", - "integrity": "sha512-AMtBR5pHppYMVD7z7G+OlHHAcgAN7v0kVKEpHuTO4Gb199Gowh0taYi9oDStFeUhetkeP55JLSVlTW1n9rFtUw==", + "version": "5.3.9", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.9.tgz", + "integrity": "sha512-I4UhmcTYXBrct03rwzQX1Y/iqQlzVQaPxWjCjula++5EmWq9YGBrx6bbGqluGc1f0XEfhSkiY4jhLgbsJUMKRA==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.1.2", - "@smithy/querystring-builder": "^4.0.4", - "@smithy/types": "^4.3.1", - "@smithy/util-base64": "^4.0.0", + "@smithy/protocol-http": "^5.3.8", + "@smithy/querystring-builder": "^4.2.8", + "@smithy/types": "^4.12.0", + "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" }, "engines": { @@ -3177,14 +3218,14 @@ } }, "node_modules/@smithy/hash-blob-browser": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-4.0.4.tgz", - "integrity": "sha512-WszRiACJiQV3QG6XMV44i5YWlkrlsM5Yxgz4jvsksuu7LDXA6wAtypfPajtNTadzpJy3KyJPoWehYpmZGKUFIQ==", + "version": "4.2.9", + "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-4.2.9.tgz", + "integrity": "sha512-m80d/iicI7DlBDxyQP6Th7BW/ejDGiF0bgI754+tiwK0lgMkcaIBgvwwVc7OFbY4eUzpGtnig52MhPAEJ7iNYg==", "license": "Apache-2.0", "dependencies": { - "@smithy/chunked-blob-reader": "^5.0.0", - "@smithy/chunked-blob-reader-native": "^4.0.0", - "@smithy/types": "^4.3.1", + "@smithy/chunked-blob-reader": "^5.2.0", + "@smithy/chunked-blob-reader-native": "^4.2.1", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { @@ -3192,14 +3233,14 @@ } }, "node_modules/@smithy/hash-node": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.0.4.tgz", - "integrity": "sha512-qnbTPUhCVnCgBp4z4BUJUhOEkVwxiEi1cyFM+Zj6o+aY8OFGxUQleKWq8ltgp3dujuhXojIvJWdoqpm6dVO3lQ==", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.8.tgz", + "integrity": "sha512-7ZIlPbmaDGxVoxErDZnuFG18WekhbA/g2/i97wGj+wUBeS6pcUeAym8u4BXh/75RXWhgIJhyC11hBzig6MljwA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.3.1", - "@smithy/util-buffer-from": "^4.0.0", - "@smithy/util-utf8": "^4.0.0", + "@smithy/types": "^4.12.0", + "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { @@ -3207,13 +3248,13 @@ } }, "node_modules/@smithy/hash-stream-node": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@smithy/hash-stream-node/-/hash-stream-node-4.0.4.tgz", - "integrity": "sha512-wHo0d8GXyVmpmMh/qOR0R7Y46/G1y6OR8U+bSTB4ppEzRxd1xVAQ9xOE9hOc0bSjhz0ujCPAbfNLkLrpa6cevg==", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/hash-stream-node/-/hash-stream-node-4.2.8.tgz", + "integrity": "sha512-v0FLTXgHrTeheYZFGhR+ehX5qUm4IQsjAiL9qehad2cyjMWcN2QG6/4mSwbSgEQzI7jwfoXj7z4fxZUx/Mhj2w==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.3.1", - "@smithy/util-utf8": "^4.0.0", + "@smithy/types": "^4.12.0", + "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { @@ -3221,12 +3262,12 @@ } }, "node_modules/@smithy/invalid-dependency": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.0.4.tgz", - "integrity": "sha512-bNYMi7WKTJHu0gn26wg8OscncTt1t2b8KcsZxvOv56XA6cyXtOAAAaNP7+m45xfppXfOatXF3Sb1MNsLUgVLTw==", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.8.tgz", + "integrity": "sha512-N9iozRybwAQ2dn9Fot9kI6/w9vos2oTXLhtK7ovGqwZjlOcxu6XhPlpLpC+INsxktqHinn5gS2DXDjDF2kG5sQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.3.1", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { @@ -3234,9 +3275,9 @@ } }, "node_modules/@smithy/is-array-buffer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.0.0.tgz", - "integrity": "sha512-saYhF8ZZNoJDTvJBEWgeBccCg+yvp1CX+ed12yORU3NilJScfc6gfch2oVb4QgxZrGUx3/ZJlb+c/dJbyupxlw==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.0.tgz", + "integrity": "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -3246,13 +3287,13 @@ } }, "node_modules/@smithy/md5-js": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-4.0.4.tgz", - "integrity": "sha512-uGLBVqcOwrLvGh/v/jw423yWHq/ofUGK1W31M2TNspLQbUV1Va0F5kTxtirkoHawODAZcjXTSGi7JwbnPcDPJg==", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-4.2.8.tgz", + "integrity": "sha512-oGMaLj4tVZzLi3itBa9TCswgMBr7k9b+qKYowQ6x1rTyTuO1IU2YHdHUa+891OsOH+wCsH7aTPRsTJO3RMQmjQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.3.1", - "@smithy/util-utf8": "^4.0.0", + "@smithy/types": "^4.12.0", + "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { @@ -3260,13 +3301,13 @@ } }, "node_modules/@smithy/middleware-content-length": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.0.4.tgz", - "integrity": "sha512-F7gDyfI2BB1Kc+4M6rpuOLne5LOcEknH1n6UQB69qv+HucXBR1rkzXBnQTB2q46sFy1PM/zuSJOB532yc8bg3w==", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.8.tgz", + "integrity": "sha512-RO0jeoaYAB1qBRhfVyq0pMgBoUK34YEJxVxyjOWYZiOKOq2yMZ4MnVXMZCUDenpozHue207+9P5ilTV1zeda0A==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.1.2", - "@smithy/types": "^4.3.1", + "@smithy/protocol-http": "^5.3.8", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { @@ -3274,18 +3315,18 @@ } }, "node_modules/@smithy/middleware-endpoint": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.1.11.tgz", - "integrity": "sha512-zDogwtRLzKl58lVS8wPcARevFZNBOOqnmzWWxVe9XiaXU2CADFjvJ9XfNibgkOWs08sxLuSr81NrpY4mgp9OwQ==", + "version": "4.4.13", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.13.tgz", + "integrity": "sha512-x6vn0PjYmGdNuKh/juUJJewZh7MoQ46jYaJ2mvekF4EesMuFfrl4LaW/k97Zjf8PTCPQmPgMvwewg7eNoH9n5w==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.5.3", - "@smithy/middleware-serde": "^4.0.8", - "@smithy/node-config-provider": "^4.1.3", - "@smithy/shared-ini-file-loader": "^4.0.4", - "@smithy/types": "^4.3.1", - "@smithy/url-parser": "^4.0.4", - "@smithy/util-middleware": "^4.0.4", + "@smithy/core": "^3.22.1", + "@smithy/middleware-serde": "^4.2.9", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/shared-ini-file-loader": "^4.4.3", + "@smithy/types": "^4.12.0", + "@smithy/url-parser": "^4.2.8", + "@smithy/util-middleware": "^4.2.8", "tslib": "^2.6.2" }, "engines": { @@ -3293,46 +3334,33 @@ } }, "node_modules/@smithy/middleware-retry": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.1.12.tgz", - "integrity": "sha512-wvIH70c4e91NtRxdaLZF+mbLZ/HcC6yg7ySKUiufL6ESp6zJUSnJucZ309AvG9nqCFHSRB5I6T3Ez1Q9wCh0Ww==", + "version": "4.4.30", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.30.tgz", + "integrity": "sha512-CBGyFvN0f8hlnqKH/jckRDz78Snrp345+PVk8Ux7pnkUCW97Iinse59lY78hBt04h1GZ6hjBN94BRwZy1xC8Bg==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.1.3", - "@smithy/protocol-http": "^5.1.2", - "@smithy/service-error-classification": "^4.0.5", - "@smithy/smithy-client": "^4.4.3", - "@smithy/types": "^4.3.1", - "@smithy/util-middleware": "^4.0.4", - "@smithy/util-retry": "^4.0.5", - "tslib": "^2.6.2", - "uuid": "^9.0.1" + "@smithy/node-config-provider": "^4.3.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/service-error-classification": "^4.2.8", + "@smithy/smithy-client": "^4.11.2", + "@smithy/types": "^4.12.0", + "@smithy/util-middleware": "^4.2.8", + "@smithy/util-retry": "^4.2.8", + "@smithy/uuid": "^1.1.0", + "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@smithy/middleware-retry/node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/@smithy/middleware-serde": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.0.8.tgz", - "integrity": "sha512-iSSl7HJoJaGyMIoNn2B7czghOVwJ9nD7TMvLhMWeSB5vt0TnEYyRRqPJu/TqW76WScaNvYYB8nRoiBHR9S1Ddw==", + "version": "4.2.9", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.9.tgz", + "integrity": "sha512-eMNiej0u/snzDvlqRGSN3Vl0ESn3838+nKyVfF2FKNXFbi4SERYT6PR392D39iczngbqqGG0Jl1DlCnp7tBbXQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.1.2", - "@smithy/types": "^4.3.1", + "@smithy/protocol-http": "^5.3.8", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { @@ -3340,12 +3368,12 @@ } }, "node_modules/@smithy/middleware-stack": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.0.4.tgz", - "integrity": "sha512-kagK5ggDrBUCCzI93ft6DjteNSfY8Ulr83UtySog/h09lTIOAJ/xUSObutanlPT0nhoHAkpmW9V5K8oPyLh+QA==", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.8.tgz", + "integrity": "sha512-w6LCfOviTYQjBctOKSwy6A8FIkQy7ICvglrZFl6Bw4FmcQ1Z420fUtIhxaUZZshRe0VCq4kvDiPiXrPZAe8oRA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.3.1", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { @@ -3353,14 +3381,14 @@ } }, "node_modules/@smithy/node-config-provider": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.7.tgz", - "integrity": "sha512-7r58wq8sdOcrwWe+klL9y3bc4GW1gnlfnFOuL7CXa7UzfhzhxKuzNdtqgzmTV+53lEp9NXh5hY/S4UgjLOzPfw==", + "version": "4.3.8", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.8.tgz", + "integrity": "sha512-aFP1ai4lrbVlWjfpAfRSL8KFcnJQYfTl5QxLJXY32vghJrDuFyPZ6LtUL+JEGYiFRG1PfPLHLoxj107ulncLIg==", "license": "Apache-2.0", "dependencies": { - "@smithy/property-provider": "^4.2.7", - "@smithy/shared-ini-file-loader": "^4.4.2", - "@smithy/types": "^4.11.0", + "@smithy/property-provider": "^4.2.8", + "@smithy/shared-ini-file-loader": "^4.4.3", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { @@ -3368,15 +3396,15 @@ } }, "node_modules/@smithy/node-http-handler": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.0.6.tgz", - "integrity": "sha512-NqbmSz7AW2rvw4kXhKGrYTiJVDHnMsFnX4i+/FzcZAfbOBauPYs2ekuECkSbtqaxETLLTu9Rl/ex6+I2BKErPA==", + "version": "4.4.9", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.9.tgz", + "integrity": "sha512-KX5Wml5mF+luxm1szW4QDz32e3NObgJ4Fyw+irhph4I/2geXwUy4jkIMUs5ZPGflRBeR6BUkC2wqIab4Llgm3w==", "license": "Apache-2.0", "dependencies": { - "@smithy/abort-controller": "^4.0.4", - "@smithy/protocol-http": "^5.1.2", - "@smithy/querystring-builder": "^4.0.4", - "@smithy/types": "^4.3.1", + "@smithy/abort-controller": "^4.2.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/querystring-builder": "^4.2.8", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { @@ -3384,12 +3412,12 @@ } }, "node_modules/@smithy/property-provider": { - "version": "4.2.7", - "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.7.tgz", - "integrity": "sha512-jmNYKe9MGGPoSl/D7JDDs1C8b3dC8f/w78LbaVfoTtWy4xAd5dfjaFG9c9PWPihY4ggMQNQSMtzU77CNgAJwmA==", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.8.tgz", + "integrity": "sha512-EtCTbyIveCKeOXDSWSdze3k612yCPq1YbXsbqX3UHhkOSW8zKsM9NOJG5gTIya0vbY2DIaieG8pKo1rITHYL0w==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.11.0", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { @@ -3397,12 +3425,12 @@ } }, "node_modules/@smithy/protocol-http": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.1.2.tgz", - "integrity": "sha512-rOG5cNLBXovxIrICSBm95dLqzfvxjEmuZx4KK3hWwPFHGdW3lxY0fZNXfv2zebfRO7sJZ5pKJYHScsqopeIWtQ==", + "version": "5.3.8", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.8.tgz", + "integrity": "sha512-QNINVDhxpZ5QnP3aviNHQFlRogQZDfYlCkQT+7tJnErPQbDhysondEjhikuANxgMsZrkGeiAxXy4jguEGsDrWQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.3.1", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { @@ -3410,13 +3438,13 @@ } }, "node_modules/@smithy/querystring-builder": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.0.4.tgz", - "integrity": "sha512-SwREZcDnEYoh9tLNgMbpop+UTGq44Hl9tdj3rf+yeLcfH7+J8OXEBaMc2kDxtyRHu8BhSg9ADEx0gFHvpJgU8w==", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.8.tgz", + "integrity": "sha512-Xr83r31+DrE8CP3MqPgMJl+pQlLLmOfiEUnoyAlGzzJIrEsbKsPy1hqH0qySaQm4oWrCBlUqRt+idEgunKB+iw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.3.1", - "@smithy/util-uri-escape": "^4.0.0", + "@smithy/types": "^4.12.0", + "@smithy/util-uri-escape": "^4.2.0", "tslib": "^2.6.2" }, "engines": { @@ -3424,12 +3452,12 @@ } }, "node_modules/@smithy/querystring-parser": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.0.4.tgz", - "integrity": "sha512-6yZf53i/qB8gRHH/l2ZwUG5xgkPgQF15/KxH0DdXMDHjesA9MeZje/853ifkSY0x4m5S+dfDZ+c4x439PF0M2w==", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.8.tgz", + "integrity": "sha512-vUurovluVy50CUlazOiXkPq40KGvGWSdmusa3130MwrR1UNnNgKAlj58wlOe61XSHRpUfIIh6cE0zZ8mzKaDPA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.3.1", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { @@ -3437,24 +3465,24 @@ } }, "node_modules/@smithy/service-error-classification": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.0.5.tgz", - "integrity": "sha512-LvcfhrnCBvCmTee81pRlh1F39yTS/+kYleVeLCwNtkY8wtGg8V/ca9rbZZvYIl8OjlMtL6KIjaiL/lgVqHD2nA==", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.8.tgz", + "integrity": "sha512-mZ5xddodpJhEt3RkCjbmUQuXUOaPNTkbMGR0bcS8FE0bJDLMZlhmpgrvPNCYglVw5rsYTpSnv19womw9WWXKQQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.3.1" + "@smithy/types": "^4.12.0" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/shared-ini-file-loader": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.2.tgz", - "integrity": "sha512-M7iUUff/KwfNunmrgtqBfvZSzh3bmFgv/j/t1Y1dQ+8dNo34br1cqVEqy6v0mYEgi0DkGO7Xig0AnuOaEGVlcg==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.3.tgz", + "integrity": "sha512-DfQjxXQnzC5UbCUPeC3Ie8u+rIWZTvuDPAGU/BxzrOGhRvgUanaP68kDZA+jaT3ZI+djOf+4dERGlm9mWfFDrg==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.11.0", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { @@ -3462,18 +3490,18 @@ } }, "node_modules/@smithy/signature-v4": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.1.2.tgz", - "integrity": "sha512-d3+U/VpX7a60seHziWnVZOHuEgJlclufjkS6zhXvxcJgkJq4UWdH5eOBLzHRMx6gXjsdT9h6lfpmLzbrdupHgQ==", + "version": "5.3.8", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.8.tgz", + "integrity": "sha512-6A4vdGj7qKNRF16UIcO8HhHjKW27thsxYci+5r/uVRkdcBEkOEiY8OMPuydLX4QHSrJqGHPJzPRwwVTqbLZJhg==", "license": "Apache-2.0", "dependencies": { - "@smithy/is-array-buffer": "^4.0.0", - "@smithy/protocol-http": "^5.1.2", - "@smithy/types": "^4.3.1", - "@smithy/util-hex-encoding": "^4.0.0", - "@smithy/util-middleware": "^4.0.4", - "@smithy/util-uri-escape": "^4.0.0", - "@smithy/util-utf8": "^4.0.0", + "@smithy/is-array-buffer": "^4.2.0", + "@smithy/protocol-http": "^5.3.8", + "@smithy/types": "^4.12.0", + "@smithy/util-hex-encoding": "^4.2.0", + "@smithy/util-middleware": "^4.2.8", + "@smithy/util-uri-escape": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { @@ -3481,17 +3509,17 @@ } }, "node_modules/@smithy/smithy-client": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.4.3.tgz", - "integrity": "sha512-xxzNYgA0HD6ETCe5QJubsxP0hQH3QK3kbpJz3QrosBCuIWyEXLR/CO5hFb2OeawEKUxMNhz3a1nuJNN2np2RMA==", + "version": "4.11.2", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.11.2.tgz", + "integrity": "sha512-SCkGmFak/xC1n7hKRsUr6wOnBTJ3L22Qd4e8H1fQIuKTAjntwgU8lrdMe7uHdiT2mJAOWA/60qaW9tiMu69n1A==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.5.3", - "@smithy/middleware-endpoint": "^4.1.11", - "@smithy/middleware-stack": "^4.0.4", - "@smithy/protocol-http": "^5.1.2", - "@smithy/types": "^4.3.1", - "@smithy/util-stream": "^4.2.2", + "@smithy/core": "^3.22.1", + "@smithy/middleware-endpoint": "^4.4.13", + "@smithy/middleware-stack": "^4.2.8", + "@smithy/protocol-http": "^5.3.8", + "@smithy/types": "^4.12.0", + "@smithy/util-stream": "^4.5.11", "tslib": "^2.6.2" }, "engines": { @@ -3499,9 +3527,9 @@ } }, "node_modules/@smithy/types": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.11.0.tgz", - "integrity": "sha512-mlrmL0DRDVe3mNrjTcVcZEgkFmufITfUAPBEA+AHYiIeYyJebso/He1qLbP3PssRe22KUzLRpQSdBPbXdgZ2VA==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.12.0.tgz", + "integrity": "sha512-9YcuJVTOBDjg9LWo23Qp0lTQ3D7fQsQtwle0jVfpbUHy9qBwCEgKuVH4FqFB3VYu0nwdHKiEMA+oXz7oV8X1kw==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -3511,13 +3539,13 @@ } }, "node_modules/@smithy/url-parser": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.0.4.tgz", - "integrity": "sha512-eMkc144MuN7B0TDA4U2fKs+BqczVbk3W+qIvcoCY6D1JY3hnAdCuhCZODC+GAeaxj0p6Jroz4+XMUn3PCxQQeQ==", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.8.tgz", + "integrity": "sha512-NQho9U68TGMEU639YkXnVMV3GEFFULmmaWdlu1E9qzyIePOHsoSnagTGSDv1Zi8DCNN6btxOSdgmy5E/hsZwhA==", "license": "Apache-2.0", "dependencies": { - "@smithy/querystring-parser": "^4.0.4", - "@smithy/types": "^4.3.1", + "@smithy/querystring-parser": "^4.2.8", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { @@ -3525,13 +3553,13 @@ } }, "node_modules/@smithy/util-base64": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.0.0.tgz", - "integrity": "sha512-CvHfCmO2mchox9kjrtzoHkWHxjHZzaFojLc8quxXY7WAAMAg43nuxwv95tATVgQFNDwd4M9S1qFzj40Ul41Kmg==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.0.tgz", + "integrity": "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/util-buffer-from": "^4.0.0", - "@smithy/util-utf8": "^4.0.0", + "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { @@ -3539,9 +3567,9 @@ } }, "node_modules/@smithy/util-body-length-browser": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.0.0.tgz", - "integrity": "sha512-sNi3DL0/k64/LO3A256M+m3CDdG6V7WKWHdAiBBMUN8S3hK3aMPhwnPik2A/a2ONN+9doY9UxaLfgqsIRg69QA==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.0.tgz", + "integrity": "sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -3551,9 +3579,9 @@ } }, "node_modules/@smithy/util-body-length-node": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.0.0.tgz", - "integrity": "sha512-q0iDP3VsZzqJyje8xJWEJCNIu3lktUGVoSy1KB0UWym2CL1siV3artm+u1DFYTLejpsrdGyCSWBdGNjJzfDPjg==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.1.tgz", + "integrity": "sha512-h53dz/pISVrVrfxV1iqXlx5pRg3V2YWFcSQyPyXZRrZoZj4R4DeWRDo1a7dd3CPTcFi3kE+98tuNyD2axyZReA==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -3563,12 +3591,12 @@ } }, "node_modules/@smithy/util-buffer-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.0.0.tgz", - "integrity": "sha512-9TOQ7781sZvddgO8nxueKi3+yGvkY35kotA0Y6BWRajAv8jjmigQ1sBwz0UX47pQMYXJPahSKEKYFgt+rXdcug==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.0.tgz", + "integrity": "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==", "license": "Apache-2.0", "dependencies": { - "@smithy/is-array-buffer": "^4.0.0", + "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" }, "engines": { @@ -3588,15 +3616,14 @@ } }, "node_modules/@smithy/util-defaults-mode-browser": { - "version": "4.0.19", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.0.19.tgz", - "integrity": "sha512-mvLMh87xSmQrV5XqnUYEPoiFFeEGYeAKIDDKdhE2ahqitm8OHM3aSvhqL6rrK6wm1brIk90JhxDf5lf2hbrLbQ==", + "version": "4.3.29", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.29.tgz", + "integrity": "sha512-nIGy3DNRmOjaYaaKcQDzmWsro9uxlaqUOhZDHQed9MW/GmkBZPtnU70Pu1+GT9IBmUXwRdDuiyaeiy9Xtpn3+Q==", "license": "Apache-2.0", "dependencies": { - "@smithy/property-provider": "^4.0.4", - "@smithy/smithy-client": "^4.4.3", - "@smithy/types": "^4.3.1", - "bowser": "^2.11.0", + "@smithy/property-provider": "^4.2.8", + "@smithy/smithy-client": "^4.11.2", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { @@ -3604,17 +3631,17 @@ } }, "node_modules/@smithy/util-defaults-mode-node": { - "version": "4.0.19", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.0.19.tgz", - "integrity": "sha512-8tYnx+LUfj6m+zkUUIrIQJxPM1xVxfRBvoGHua7R/i6qAxOMjqR6CpEpDwKoIs1o0+hOjGvkKE23CafKL0vJ9w==", + "version": "4.2.32", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.32.tgz", + "integrity": "sha512-7dtFff6pu5fsjqrVve0YMhrnzJtccCWDacNKOkiZjJ++fmjGExmmSu341x+WU6Oc1IccL7lDuaUj7SfrHpWc5Q==", "license": "Apache-2.0", "dependencies": { - "@smithy/config-resolver": "^4.1.4", - "@smithy/credential-provider-imds": "^4.0.6", - "@smithy/node-config-provider": "^4.1.3", - "@smithy/property-provider": "^4.0.4", - "@smithy/smithy-client": "^4.4.3", - "@smithy/types": "^4.3.1", + "@smithy/config-resolver": "^4.4.6", + "@smithy/credential-provider-imds": "^4.2.8", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/property-provider": "^4.2.8", + "@smithy/smithy-client": "^4.11.2", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { @@ -3622,13 +3649,13 @@ } }, "node_modules/@smithy/util-endpoints": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.2.7.tgz", - "integrity": "sha512-s4ILhyAvVqhMDYREeTS68R43B1V5aenV5q/V1QpRQJkCXib5BPRo4s7uNdzGtIKxaPHCfU/8YkvPAEvTpxgspg==", + "version": "3.2.8", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.2.8.tgz", + "integrity": "sha512-8JaVTn3pBDkhZgHQ8R0epwWt+BqPSLCjdjXXusK1onwJlRuN69fbvSK66aIKKO7SwVFM6x2J2ox5X8pOaWcUEw==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.7", - "@smithy/types": "^4.11.0", + "@smithy/node-config-provider": "^4.3.8", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { @@ -3636,9 +3663,9 @@ } }, "node_modules/@smithy/util-hex-encoding": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.0.0.tgz", - "integrity": "sha512-Yk5mLhHtfIgW2W2WQZWSg5kuMZCVbvhFmC7rV4IO2QqnZdbEFPmQnCcGMAX2z/8Qj3B9hYYNjZOhWym+RwhePw==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.0.tgz", + "integrity": "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -3648,12 +3675,12 @@ } }, "node_modules/@smithy/util-middleware": { - "version": "4.2.7", - "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.7.tgz", - "integrity": "sha512-i1IkpbOae6NvIKsEeLLM9/2q4X+M90KV3oCFgWQI4q0Qz+yUZvsr+gZPdAEAtFhWQhAHpTsJO8DRJPuwVyln+w==", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.8.tgz", + "integrity": "sha512-PMqfeJxLcNPMDgvPbbLl/2Vpin+luxqTGPpW3NAQVLbRrFRzTa4rNAASYeIGjRV9Ytuhzny39SpyU04EQreF+A==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.11.0", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { @@ -3661,13 +3688,13 @@ } }, "node_modules/@smithy/util-retry": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.0.5.tgz", - "integrity": "sha512-V7MSjVDTlEt/plmOFBn1762Dyu5uqMrV2Pl2X0dYk4XvWfdWJNe9Bs5Bzb56wkCuiWjSfClVMGcsuKrGj7S/yg==", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.8.tgz", + "integrity": "sha512-CfJqwvoRY0kTGe5AkQokpURNCT1u/MkRzMTASWMPPo2hNSnKtF1D45dQl3DE2LKLr4m+PW9mCeBMJr5mCAVThg==", "license": "Apache-2.0", "dependencies": { - "@smithy/service-error-classification": "^4.0.5", - "@smithy/types": "^4.3.1", + "@smithy/service-error-classification": "^4.2.8", + "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "engines": { @@ -3675,18 +3702,18 @@ } }, "node_modules/@smithy/util-stream": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.2.2.tgz", - "integrity": "sha512-aI+GLi7MJoVxg24/3J1ipwLoYzgkB4kUfogZfnslcYlynj3xsQ0e7vk4TnTro9hhsS5PvX1mwmkRqqHQjwcU7w==", + "version": "4.5.11", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.11.tgz", + "integrity": "sha512-lKmZ0S/3Qj2OF5H1+VzvDLb6kRxGzZHq6f3rAsoSu5cTLGsn3v3VQBA8czkNNXlLjoFEtVu3OQT2jEeOtOE2CA==", "license": "Apache-2.0", "dependencies": { - "@smithy/fetch-http-handler": "^5.0.4", - "@smithy/node-http-handler": "^4.0.6", - "@smithy/types": "^4.3.1", - "@smithy/util-base64": "^4.0.0", - "@smithy/util-buffer-from": "^4.0.0", - "@smithy/util-hex-encoding": "^4.0.0", - "@smithy/util-utf8": "^4.0.0", + "@smithy/fetch-http-handler": "^5.3.9", + "@smithy/node-http-handler": "^4.4.9", + "@smithy/types": "^4.12.0", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-hex-encoding": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { @@ -3694,9 +3721,9 @@ } }, "node_modules/@smithy/util-uri-escape": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.0.0.tgz", - "integrity": "sha512-77yfbCbQMtgtTylO9itEAdpPXSog3ZxMe09AEhm0dU0NLTalV70ghDZFR+Nfi1C60jnJoh/Re4090/DuZh2Omg==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.0.tgz", + "integrity": "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -3706,12 +3733,12 @@ } }, "node_modules/@smithy/util-utf8": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.0.0.tgz", - "integrity": "sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.0.tgz", + "integrity": "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==", "license": "Apache-2.0", "dependencies": { - "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" }, "engines": { @@ -3719,13 +3746,25 @@ } }, "node_modules/@smithy/util-waiter": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.0.5.tgz", - "integrity": "sha512-4QvC49HTteI1gfemu0I1syWovJgPvGn7CVUoN9ZFkdvr/cCFkrEL7qNCdx/2eICqDWEGnnr68oMdSIPCLAriSQ==", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.2.8.tgz", + "integrity": "sha512-n+lahlMWk+aejGuax7DPWtqav8HYnWxQwR+LCG2BgCUmaGcTe9qZCFsmw8TMg9iG75HOwhrJCX9TCJRLH+Yzqg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.2.8", + "@smithy/types": "^4.12.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/uuid": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.0.tgz", + "integrity": "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw==", "license": "Apache-2.0", "dependencies": { - "@smithy/abort-controller": "^4.0.4", - "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, "engines": { @@ -4562,7 +4601,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.32.tgz", "integrity": "sha512-3jigKqgSjsH6gYZv2nEsqdXfZqIFGAV36XYYjf9KGZ3PSG+IhLecqPnI310RvjutyMwifE2hhhNEklOUrvx/wA==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -4728,7 +4766,6 @@ "integrity": "sha512-4O3idHxhyzjClSMJ0a29AcoK0+YwnEqzI6oz3vlRf3xw0zbzt15MzXwItOlnr5nIth6zlY2RENLsOPvhyrKAQA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.34.1", "@typescript-eslint/types": "8.34.1", @@ -5208,7 +5245,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5561,9 +5597,9 @@ "license": "ISC" }, "node_modules/bowser": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz", - "integrity": "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==", + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.13.1.tgz", + "integrity": "sha512-OHawaAbjwx6rqICCKgSG0SAnT05bzd7ppyKLVUITZpANBaaMFBAsaNkto3LoQ31tyFP5kNujE8Cdx85G9VzOkw==", "license": "MIT" }, "node_modules/brace-expansion": { @@ -5608,7 +5644,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001718", "electron-to-chromium": "^1.5.160", @@ -5762,7 +5797,6 @@ "integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", @@ -6684,7 +6718,6 @@ "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "dev": true, "license": "ISC", - "peer": true, "engines": { "node": ">=12" } @@ -7254,7 +7287,6 @@ "integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -7696,22 +7728,18 @@ "license": "MIT" }, "node_modules/fast-xml-parser": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz", - "integrity": "sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==", + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.3.4.tgz", + "integrity": "sha512-EFd6afGmXlCx8H8WTZHhAoDaWaGyuIBoZJ2mknrNxug+aZKjkp0a0dlars9Izl+jF+7Gu1/5f/2h68cQpe0IiA==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/NaturalIntelligence" - }, - { - "type": "paypal", - "url": "https://paypal.me/naturalintelligence" } ], "license": "MIT", "dependencies": { - "strnum": "^1.0.5" + "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" @@ -8589,7 +8617,6 @@ "integrity": "sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@acemir/cssom": "^0.9.28", "@asamuzakjp/dom-selector": "^6.7.6", @@ -10185,7 +10212,6 @@ "integrity": "sha512-dyuThzncsgEgJZnvd/A/5x6IkUERbK+phXqUQrI+0C6WE+8xqGH5VChRTLecemhgZF0kQ+gZOM3tJTX9937xpg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@pixi/colord": "^2.9.6", "@types/css-font-loading-module": "^0.0.12", @@ -10230,7 +10256,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -10333,7 +10358,6 @@ "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -11156,7 +11180,6 @@ "integrity": "sha512-Z0NVCW45W8Mg5oC/27/+fCqIHFnW8kpkFOq0j9XJIev4Ld0mKmERaZv5DMLAb9fGCevjKwaEeIQz5+MBXfZcDw==", "dev": true, "license": "BSD-3-Clause", - "peer": true, "dependencies": { "@sinonjs/commons": "^3.0.1", "@sinonjs/fake-timers": "^15.1.0", @@ -11329,9 +11352,9 @@ } }, "node_modules/strnum": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz", - "integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.2.tgz", + "integrity": "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ==", "funding": [ { "type": "github", @@ -11578,7 +11601,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -11821,7 +11843,6 @@ "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.3.tgz", "integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==", "license": "MIT", - "peer": true, "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" @@ -11890,7 +11911,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -12037,7 +12057,6 @@ "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -12787,7 +12806,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -12801,7 +12819,6 @@ "integrity": "sha512-E4t7DJ9pESL6E3I8nFjPa4xGUd3PmiWDLsDztS2qXSJWfHtbQnwAWylaBvSNY48I3vr8PTqIZlyK8TE3V3CA4Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vitest/expect": "4.0.16", "@vitest/mocker": "4.0.16", From 904425cab0df60a3d0dc68aa49d439aefa93b5e1 Mon Sep 17 00:00:00 2001 From: FloPinguin <25036848+FloPinguin@users.noreply.github.com> Date: Wed, 4 Feb 2026 12:38:49 +0100 Subject: [PATCH 12/20] =?UTF-8?q?Prevent=20players=20from=20nuking=20their?= =?UTF-8?q?=20teammates=20structures=20=F0=9F=92=A5=20(#3105)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description: It is possible to hit your teammates while throwing a nuke onto water or enemies. This PR blocks the nuking entirely if you would hit a teammates structure. Because they are valuable. Feature requested by Wonder :) https://github.com/user-attachments/assets/448a3444-cc3d-4e76-acaf-595decab1634 ## Please complete the following: - [X] I have added screenshots for all UI updates - [X] I process any text displayed to the user through translateText() and I've added it to the en.json file - [X] I have added relevant tests to the test directory - [X] 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: FloPinguin --------- Co-authored-by: Ryan <7389646+ryanbarlow97@users.noreply.github.com> --- src/core/game/PlayerImpl.ts | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/src/core/game/PlayerImpl.ts b/src/core/game/PlayerImpl.ts index fae23df8e..64cdf70c8 100644 --- a/src/core/game/PlayerImpl.ts +++ b/src/core/game/PlayerImpl.ts @@ -20,6 +20,7 @@ import { ColoredTeams, Embargo, EmojiMessage, + GameMode, Gold, MessageType, MutableAlliance, @@ -29,6 +30,7 @@ import { PlayerProfile, PlayerType, Relation, + StructureTypes, Team, TerraNullius, Tick, @@ -994,10 +996,10 @@ export class PlayerImpl implements Player { if (!this.mg.hasOwner(targetTile)) { return false; } - return this.nukeSpawn(targetTile); + return this.nukeSpawn(targetTile, unitType); case UnitType.AtomBomb: case UnitType.HydrogenBomb: - return this.nukeSpawn(targetTile); + return this.nukeSpawn(targetTile, unitType); case UnitType.MIRVWarhead: return targetTile; case UnitType.Port: @@ -1024,7 +1026,7 @@ export class PlayerImpl implements Player { } } - nukeSpawn(tile: TileRef): TileRef | false { + nukeSpawn(tile: TileRef, nukeType: UnitType): TileRef | false { if (this.mg.isSpawnImmunityActive()) { return false; } @@ -1034,6 +1036,24 @@ export class PlayerImpl implements Player { return false; } } + + // Prevent launching nukes that would hit teammate structures (only in team games) + if ( + this.mg.config().gameConfig().gameMode === GameMode.Team && + nukeType !== UnitType.MIRV + ) { + const magnitude = this.mg.config().nukeMagnitudes(nukeType); + const wouldHitTeammate = this.mg.anyUnitNearby( + tile, + magnitude.outer, + StructureTypes, + (unit) => unit.owner().isPlayer() && this.isOnSameTeam(unit.owner()), + ); + if (wouldHitTeammate) { + return false; + } + } + // only get missilesilos that are not on cooldown and not under construction const spawns = this.units(UnitType.MissileSilo) .filter((silo) => { From a8836f76d33937dbf52f96af177d972f696eb0ee Mon Sep 17 00:00:00 2001 From: FloPinguin <25036848+FloPinguin@users.noreply.github.com> Date: Wed, 4 Feb 2026 12:39:35 +0100 Subject: [PATCH 13/20] Reduce bot farming problem (#2895) ## Description: Explanation of the bot farming strategy in the discord: https://discord.com/channels/1359946986937258015/1359949371956789289/1460928540575928478 "the result is that a player can build unlimited factories for 125 000 gold discount, trade with themselves with each train being worth 50 000 gold. First the 25 000 for neutral trade and then another 25 000 when the bot is harvested." "If you have a minute and ally people around you it should be trivial to get 10 of both cities and factories for 1.25mil" It's debatable if we want to let people do that (close this PR) or see it as an abusive mechanic. Here is the fix, bots try to delete all structures now. You can simply retake them to stop the deletion: https://github.com/user-attachments/assets/ac1ca846-50bd-42fa-8e25-5ac25a6d627e ## Please complete the following: - [X] I have added screenshots for all UI updates - [X] I process any text displayed to the user through translateText() and I've added it to the en.json file - [X] I have added relevant tests to the test directory - [X] 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: FloPinguin Co-authored-by: Ryan <7389646+ryanbarlow97@users.noreply.github.com> --- src/core/execution/BotExecution.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/core/execution/BotExecution.ts b/src/core/execution/BotExecution.ts index 4f9f178f9..85fabb34a 100644 --- a/src/core/execution/BotExecution.ts +++ b/src/core/execution/BotExecution.ts @@ -1,7 +1,8 @@ -import { Execution, Game, Player } from "../game/Game"; +import { Execution, Game, isStructureType, Player } from "../game/Game"; import { PseudoRandom } from "../PseudoRandom"; import { simpleHash } from "../Util"; import { AllianceExtensionExecution } from "./alliance/AllianceExtensionExecution"; +import { DeleteUnitExecution } from "./DeleteUnitExecution"; import { AiAttackBehavior } from "./utils/AiAttackBehavior"; export class BotExecution implements Execution { @@ -58,6 +59,7 @@ export class BotExecution implements Execution { } this.acceptAllAllianceRequests(); + this.deleteAllStructures(); this.maybeAttack(); } @@ -80,6 +82,14 @@ export class BotExecution implements Execution { } } + private deleteAllStructures() { + for (const unit of this.bot.units()) { + if (isStructureType(unit.type()) && this.bot.canDeleteUnit()) { + this.mg.addExecution(new DeleteUnitExecution(this.bot, unit.id())); + } + } + } + private maybeAttack() { if (this.attackBehavior === null) { throw new Error("not initialized"); From 41a9bb80c0c5f7d0561e84ef9d98c0724472709b Mon Sep 17 00:00:00 2001 From: Ryan <7389646+ryanbarlow97@users.noreply.github.com> Date: Wed, 4 Feb 2026 18:01:05 +0000 Subject: [PATCH 14/20] Added source for join context (#3116) ## Description: Added source for join context ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] 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: w.o.n --- src/client/HostLobbyModal.ts | 1 + src/client/JoinLobbyModal.ts | 3 +++ src/client/Matchmaking.ts | 1 + src/client/SinglePlayerModal.ts | 1 + 4 files changed, 6 insertions(+) diff --git a/src/client/HostLobbyModal.ts b/src/client/HostLobbyModal.ts index c086354e6..6bca2ca8c 100644 --- a/src/client/HostLobbyModal.ts +++ b/src/client/HostLobbyModal.ts @@ -655,6 +655,7 @@ export class HostLobbyModal extends BaseModal { detail: { gameID: this.lobbyId, clientID: this.lobbyCreatorClientID, + source: "host", } as JoinLobbyEvent, bubbles: true, composed: true, diff --git a/src/client/JoinLobbyModal.ts b/src/client/JoinLobbyModal.ts index 7cd330d64..0f1c5e2da 100644 --- a/src/client/JoinLobbyModal.ts +++ b/src/client/JoinLobbyModal.ts @@ -336,6 +336,7 @@ export class JoinLobbyModal extends BaseModal { detail: { gameID: lobbyId, clientID: this.currentClientID, + source: "public", } as JoinLobbyEvent, bubbles: true, composed: true, @@ -776,6 +777,7 @@ export class JoinLobbyModal extends BaseModal { detail: { gameID: lobbyId, clientID: this.currentClientID, + source: "private", } as JoinLobbyEvent, bubbles: true, composed: true, @@ -834,6 +836,7 @@ export class JoinLobbyModal extends BaseModal { gameID: lobbyId, gameRecord: parsed.data, clientID: this.currentClientID, + source: "private", } as JoinLobbyEvent, bubbles: true, composed: true, diff --git a/src/client/Matchmaking.ts b/src/client/Matchmaking.ts index 4e53ea6ab..3fa4af738 100644 --- a/src/client/Matchmaking.ts +++ b/src/client/Matchmaking.ts @@ -231,6 +231,7 @@ export class MatchmakingModal extends BaseModal { detail: { gameID: this.gameID, clientID: getClientIDForGame(this.gameID), + source: "matchmaking", } as JoinLobbyEvent, bubbles: true, composed: true, diff --git a/src/client/SinglePlayerModal.ts b/src/client/SinglePlayerModal.ts index 7a10f21aa..1623d996e 100644 --- a/src/client/SinglePlayerModal.ts +++ b/src/client/SinglePlayerModal.ts @@ -967,6 +967,7 @@ export class SinglePlayerModal extends BaseModal { }, lobbyCreatedAt: Date.now(), // ms; server should be authoritative in MP }, + source: "singleplayer", } satisfies JoinLobbyEvent, bubbles: true, composed: true, From c2663944e59396968549cfe30a6f4689eeff58ef Mon Sep 17 00:00:00 2001 From: FloPinguin <25036848+FloPinguin@users.noreply.github.com> Date: Wed, 4 Feb 2026 23:12:30 +0100 Subject: [PATCH 15/20] =?UTF-8?q?Stop=20getting=20gold=20from=20conquering?= =?UTF-8?q?=20inactive=20players=20=F0=9F=94=A7=20(#3020)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description: Maybe for v29. In the 5M starting gold modifier games you can conquer a inactive player (spawned but didn't do anything) and get their 5M gold. Huge unfair advantage. I think that even without the starting gold modifier you should not get the gold of inactive players because its unfair. I identify inactive players (spawned but didn't do anything) by checking the attack stats. I added a translation for the displayMessage "Conquered {name}, received {gold} gold". Why was that not translated? I added a new message "Conquered {name} (Inactive player, received no gold)". ## Please complete the following: - [X] I have added screenshots for all UI updates - [X] I process any text displayed to the user through translateText() and I've added it to the en.json file - [X] I have added relevant tests to the test directory - [X] 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: FloPinguin --------- Co-authored-by: Ryan <7389646+ryanbarlow97@users.noreply.github.com> --- resources/lang/en.json | 2 ++ src/core/game/GameImpl.ts | 49 ++++++++++++++++++++++++++++----------- tests/AttackStats.test.ts | 25 ++++++++++++++++++++ 3 files changed, 63 insertions(+), 13 deletions(-) diff --git a/resources/lang/en.json b/resources/lang/en.json index 44ebe941a..6f06528dc 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -771,6 +771,8 @@ "attack_cancelled_retreat": "Attack cancelled, {troops} soldiers killed during retreat", "received_gold_from_captured_ship": "Received {gold} gold from ship captured from {name}", "received_gold_from_trade": "Received {gold} gold from trade with {name}", + "received_gold_from_conquest": "Conquered {name}, received {gold} gold", + "conquered_no_gold": "Conquered {name} (didn't play, no gold awarded)", "missile_intercepted": "Missile intercepted {unit}", "mirv_warheads_intercepted": "{count, plural, one {{count} MIRV warhead intercepted} other {{count} MIRV warheads intercepted}}", "sent_troops_to_player": "Sent {troops} troops to {name}", diff --git a/src/core/game/GameImpl.ts b/src/core/game/GameImpl.ts index 6d409fc58..1f6a97add 100644 --- a/src/core/game/GameImpl.ts +++ b/src/core/game/GameImpl.ts @@ -7,6 +7,7 @@ import { import { AStarWaterHierarchical } from "../pathfinding/algorithms/AStar.WaterHierarchical"; import { PathFinder } from "../pathfinding/types"; import { AllPlayersStats, ClientID, Winner } from "../Schemas"; +import { ATTACK_INDEX_SENT } from "../StatsSchemas"; import { simpleHash } from "../Util"; import { AllianceImpl } from "./AllianceImpl"; import { AllianceRequestImpl } from "./AllianceRequestImpl"; @@ -1097,26 +1098,48 @@ export class GameImpl implements Game { } } - const gold = conquered.gold(); - this.displayMessage( - `Conquered ${conquered.displayName()} received ${renderNumber( + // Don't transfer gold when the conquered player didn't play (never attacked anyone) + // This is especially important when starting gold is enabled + const stats = this._stats.getPlayerStats(conquered); + const attacksSent = stats?.attacks?.[ATTACK_INDEX_SENT] ?? 0n; + const skipGoldTransfer = + attacksSent === 0n && conquered.type() === PlayerType.Human; + const gold = skipGoldTransfer ? 0n : conquered.gold(); + + if (skipGoldTransfer) { + this.displayMessage( + "events_display.conquered_no_gold", + MessageType.CONQUERED_PLAYER, + conqueror.id(), + undefined, + { + name: conquered.displayName(), + }, + ); + } else { + this.displayMessage( + "events_display.received_gold_from_conquest", + MessageType.CONQUERED_PLAYER, + conqueror.id(), gold, - )} gold`, - MessageType.CONQUERED_PLAYER, - conqueror.id(), - gold, - ); - conqueror.addGold(gold); - conquered.removeGold(gold); + { + gold: renderNumber(gold), + name: conquered.displayName(), + }, + ); + conqueror.addGold(gold); + conquered.removeGold(gold); + + // Record stats + this.stats().goldWar(conqueror, conquered, gold); + } + this.addUpdate({ type: GameUpdateType.ConquestEvent, conquerorId: conqueror.id(), conqueredId: conquered.id(), gold, }); - - // Record stats - this.stats().goldWar(conqueror, conquered, gold); } } diff --git a/tests/AttackStats.test.ts b/tests/AttackStats.test.ts index 10cb236c3..23e0ef98f 100644 --- a/tests/AttackStats.test.ts +++ b/tests/AttackStats.test.ts @@ -34,11 +34,36 @@ describe("AttackStats", () => { test("should increase war gold stat when a player is eliminated", () => { expect(player1.sharesBorderWith(player2)).toBeTruthy(); + // Player2 must attack to be considered active (otherwise gold won't transfer) + game.addExecution( + new AttackExecution(1, player2, game.terraNullius().id()), + ); + game.executeNextTick(); performAttack(game, player1, player2); expectWarGoldStatIsIncreasedAfterKill(game, player1, player2); }); + test("should NOT increase war gold stat when a inactive player is eliminated", () => { + expect(player1.sharesBorderWith(player2)).toBeTruthy(); + + const attackerStatsBefore = game.stats().stats()[player1.clientID()!]; + const warGoldBefore = attackerStatsBefore?.gold?.[GOLD_INDEX_WAR] ?? 0n; + + performAttack(game, player1, player2); + + const attackerStatsAfter = game.stats().stats()[player1.clientID()!]; + const warGoldAfter = attackerStatsAfter?.gold?.[GOLD_INDEX_WAR] ?? 0n; + + expect(warGoldAfter).toBe(warGoldBefore); + }); + test("should increase war gold stat when elimination occurs via territory annexation", () => { + // Player2 must attack to be considered active (otherwise gold won't transfer) + game.addExecution( + new AttackExecution(1, player2, game.terraNullius().id()), + ); + game.executeNextTick(); + // Mark every tile on the map as owned by player1 for (let x = 0; x < game.map().width(); x++) { for (let y = 0; y < game.map().height(); y++) { From ec5fb4fa221a47e0f57b5898b3f396f84db07637 Mon Sep 17 00:00:00 2001 From: scamiv <6170744+scamiv@users.noreply.github.com> Date: Thu, 5 Feb 2026 05:18:25 +0100 Subject: [PATCH 16/20] Pr fxlayer viewport culling (#3123) ## Description: Reduce FX layer rendering cost by: - Updating the offscreen FX buffer only when needed (and clearing it once when FX ends). - Drawing only the visible portion of the FX buffer to the main canvas (viewport culling). - Reusing `TransformHandler.screenBoundingRect()` as the single source of truth for viewport bounds. - ## Changes - `FxLayer`: - Track buffered frames and skip work when there are no active FX. - Use `performance.now()` for refresh timing. - Draw only the visible map rect (clamp + small pad) instead of blitting the full map-sized FX canvas. - Compute the visible rect via `TransformHandler.screenBoundingRect()`. - `GameRenderer`: - Thread `TransformHandler` into `FxLayer` construction. ## 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: DISCORD_USERNAME --- src/client/graphics/GameRenderer.ts | 2 +- src/client/graphics/layers/FxLayer.ts | 81 ++++++++++++++++++++------- 2 files changed, 62 insertions(+), 21 deletions(-) diff --git a/src/client/graphics/GameRenderer.ts b/src/client/graphics/GameRenderer.ts index d5d1cdce8..0a0e8b5cf 100644 --- a/src/client/graphics/GameRenderer.ts +++ b/src/client/graphics/GameRenderer.ts @@ -269,7 +269,7 @@ export function createRenderer( structureLayer, samRadiusLayer, new UnitLayer(game, eventBus, transformHandler), - new FxLayer(game), + new FxLayer(game, transformHandler), new UILayer(game, eventBus, transformHandler), new NukeTrajectoryPreviewLayer(game, eventBus, transformHandler, uiState), new StructureIconsLayer(game, eventBus, uiState, transformHandler), diff --git a/src/client/graphics/layers/FxLayer.ts b/src/client/graphics/layers/FxLayer.ts index ed3602c15..b8ca35c8c 100644 --- a/src/client/graphics/layers/FxLayer.ts +++ b/src/client/graphics/layers/FxLayer.ts @@ -13,20 +13,25 @@ import { Fx, FxType } from "../fx/Fx"; import { nukeFxFactory, ShockwaveFx } from "../fx/NukeFx"; import { SpriteFx } from "../fx/SpriteFx"; import { UnitExplosionFx } from "../fx/UnitExplosionFx"; +import { TransformHandler } from "../TransformHandler"; import { Layer } from "./Layer"; export class FxLayer implements Layer { private canvas: HTMLCanvasElement; private context: CanvasRenderingContext2D; - private lastRefresh: number = 0; + private lastRefreshMs: number = 0; private refreshRate: number = 10; private theme: Theme; private animatedSpriteLoader: AnimatedSpriteLoader = new AnimatedSpriteLoader(); private allFx: Fx[] = []; + private hasBufferedFrame = false; - constructor(private game: GameView) { + constructor( + private game: GameView, + private transformHandler: TransformHandler, + ) { this.theme = this.game.config().theme(); } @@ -254,28 +259,64 @@ export class FxLayer implements Layer { } renderLayer(context: CanvasRenderingContext2D) { - const now = Date.now(); - if (this.game.config().userSettings()?.fxLayer()) { - if (now > this.lastRefresh + this.refreshRate) { - const delta = now - this.lastRefresh; - this.renderAllFx(context, delta); - this.lastRefresh = now; + const nowMs = performance.now(); + + const hasFx = this.allFx.length > 0; + if (!this.game.config().userSettings()?.fxLayer() || !hasFx) { + if (this.hasBufferedFrame) { + // Clear stale pixels once when fx ends/disabled so re-enabling doesn't + // flash old frames. + this.context.clearRect(0, 0, this.canvas.width, this.canvas.height); + this.hasBufferedFrame = false; } - context.drawImage( - this.canvas, - -this.game.width() / 2, - -this.game.height() / 2, - this.game.width(), - this.game.height(), - ); + this.lastRefreshMs = nowMs; + return; } + + const needsRefresh = + !this.hasBufferedFrame || nowMs > this.lastRefreshMs + this.refreshRate; + if (needsRefresh) { + const delta = this.hasBufferedFrame ? nowMs - this.lastRefreshMs : 0; + this.renderAllFx(delta); + this.lastRefreshMs = nowMs; + this.hasBufferedFrame = true; + } + + this.drawVisibleFx(context); } - renderAllFx(context: CanvasRenderingContext2D, delta: number) { - if (this.allFx.length > 0) { - this.context.clearRect(0, 0, this.canvas.width, this.canvas.height); - this.renderContextFx(delta); - } + private drawVisibleFx(context: CanvasRenderingContext2D) { + const mapW = this.game.width(); + const mapH = this.game.height(); + + const [topLeft, bottomRight] = this.transformHandler.screenBoundingRect(); + const pad = 2; + + const left = Math.max(0, Math.floor(topLeft.x - pad)); + const top = Math.max(0, Math.floor(topLeft.y - pad)); + const right = Math.min(mapW, Math.ceil(bottomRight.x + pad)); + const bottom = Math.min(mapH, Math.ceil(bottomRight.y + pad)); + + const width = Math.max(0, right - left); + const height = Math.max(0, bottom - top); + if (width === 0 || height === 0) return; + + context.drawImage( + this.canvas, + left, + top, + width, + height, + -mapW / 2 + left, + -mapH / 2 + top, + width, + height, + ); + } + + private renderAllFx(delta: number) { + this.context.clearRect(0, 0, this.canvas.width, this.canvas.height); + this.renderContextFx(delta); } renderContextFx(duration: number) { From d40923fc182a2228a46a76fdd22b2df4f9ca3255 Mon Sep 17 00:00:00 2001 From: scamiv <6170744+scamiv@users.noreply.github.com> Date: Thu, 5 Feb 2026 05:19:50 +0100 Subject: [PATCH 17/20] perf(game): optimize border tile updates (#3124) ## Description Optimize border-tile maintenance in `GameImpl` to reduce per-conquest overhead. Border tiles are updated whenever ownership changes; this PR trims allocations and avoids unnecessary iteration in the hot path. ## Changes - `src/core/game/GameImpl.ts` - `updateBorders(tile)` no longer allocates an array of tiles; it updates the changed tile and its 4-neighbors directly via `forEachNeighbor`. - `calcIsBorder(tile)` no longer calls `neighbors(tile)` / loops an array; it checks the four cardinal neighbors via `x/y` bounds and `ownerID`. ## Affected Functions - `GameImpl.updateBorders(tile: TileRef)` - `GameImpl.calcIsBorder(tile: TileRef): boolean` - Call sites impacted by behavior/perf: - `GameImpl.conquer(owner, tile)` - `GameImpl.relinquish(tile)` ## 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: DISCORD_USERNAME --- src/core/game/GameImpl.ts | 43 ++++++++++++++++++++++++++------------- 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/src/core/game/GameImpl.ts b/src/core/game/GameImpl.ts index 1f6a97add..6dd76beff 100644 --- a/src/core/game/GameImpl.ts +++ b/src/core/game/GameImpl.ts @@ -622,31 +622,46 @@ export class GameImpl implements Game { } private updateBorders(tile: TileRef) { - const tiles: TileRef[] = []; - tiles.push(tile); - this.neighbors(tile).forEach((t) => tiles.push(t)); - - for (const t of tiles) { + const updateBorderStatus = (t: TileRef) => { if (!this.hasOwner(t)) { - continue; + return; } + const owner = this.owner(t) as PlayerImpl; if (this.calcIsBorder(t)) { - (this.owner(t) as PlayerImpl)._borderTiles.add(t); + owner._borderTiles.add(t); } else { - (this.owner(t) as PlayerImpl)._borderTiles.delete(t); + owner._borderTiles.delete(t); } - } + }; + + updateBorderStatus(tile); + this.forEachNeighbor(tile, updateBorderStatus); } private calcIsBorder(tile: TileRef): boolean { if (!this.hasOwner(tile)) { return false; } - for (const neighbor of this.neighbors(tile)) { - const bordersEnemy = this.owner(tile) !== this.owner(neighbor); - if (bordersEnemy) { - return true; - } + const ownerId = this.ownerID(tile); + const x = this.x(tile); + const y = this.y(tile); + if (x > 0 && this.ownerID(this._map.ref(x - 1, y)) !== ownerId) { + return true; + } + if ( + x + 1 < this._width && + this.ownerID(this._map.ref(x + 1, y)) !== ownerId + ) { + return true; + } + if (y > 0 && this.ownerID(this._map.ref(x, y - 1)) !== ownerId) { + return true; + } + if ( + y + 1 < this._height && + this.ownerID(this._map.ref(x, y + 1)) !== ownerId + ) { + return true; } return false; } From b68de96c6e91d2f5f77b9b5e8a37e69b11c360d0 Mon Sep 17 00:00:00 2001 From: scamiv <6170744+scamiv@users.noreply.github.com> Date: Thu, 5 Feb 2026 19:07:17 +0100 Subject: [PATCH 18/20] Perf clusters (#3127) ## Description: This PR reduces server/client tick CPU spent on territory cluster maintenance by: - Cutting redundant work in `PlayerExecution.removeClusters()` (largest-cluster bounding box reuse, fewer allocations in `isSurrounded()` and `removeCluster()`). - Making `calculateBoundingBox()` allocation-free per tile by switching from `gm.cell(tile)` to `gm.x(tile)`/`gm.y(tile)` (it now only allocates the two result `Cell`s, instead of 1 per tile). ## Commits - `51de0a1b` core: reduce PlayerExecution cluster overhead - `6d9d85c5` core: avoid Cell allocations in PlayerExecution isSurrounded - `346f6a8c` core(util): speed up calculateBoundingBox by avoiding Cell allocations ## Notes - This PR is intended to be behavior-preserving; changes are limited to hot-path micro-optimizations. - Follow-up opportunity: `calculateClusters`/flood-fill is now the top hotspot; further wins likely come from reducing traversal work or caching. ## 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: DISCORD_USERNAME --- src/core/Util.ts | 15 +++++---- src/core/execution/PlayerExecution.ts | 47 +++++++++++++++++---------- 2 files changed, 37 insertions(+), 25 deletions(-) diff --git a/src/core/Util.ts b/src/core/Util.ts index 330359021..a79dda1a8 100644 --- a/src/core/Util.ts +++ b/src/core/Util.ts @@ -80,13 +80,14 @@ export function calculateBoundingBox( maxX = -Infinity, maxY = -Infinity; - borderTiles.forEach((tile: TileRef) => { - const cell = gm.cell(tile); - minX = Math.min(minX, cell.x); - minY = Math.min(minY, cell.y); - maxX = Math.max(maxX, cell.x); - maxY = Math.max(maxY, cell.y); - }); + for (const tile of borderTiles) { + const x = gm.x(tile); + const y = gm.y(tile); + minX = Math.min(minX, x); + minY = Math.min(minY, y); + maxX = Math.max(maxX, x); + maxY = Math.max(maxY, y); + } return { min: new Cell(minX, minY), max: new Cell(maxX, maxY) }; } diff --git a/src/core/execution/PlayerExecution.ts b/src/core/execution/PlayerExecution.ts index c7e452e5d..e1d80d0b4 100644 --- a/src/core/execution/PlayerExecution.ts +++ b/src/core/execution/PlayerExecution.ts @@ -1,5 +1,5 @@ import { Config } from "../configuration/Config"; -import { Execution, Game, Player, UnitType } from "../game/Game"; +import { Cell, Execution, Game, Player, UnitType } from "../game/Game"; import { TileRef } from "../game/GameMap"; import { calculateBoundingBox, getMode, inscribed, simpleHash } from "../Util"; @@ -139,11 +139,12 @@ export class PlayerExecution implements Execution { const largestCluster = clusters[largestIndex]; if (largestCluster === undefined) throw new Error("No clusters"); - this.player.largestClusterBoundingBox = calculateBoundingBox( - this.mg, + const largestClusterBox = calculateBoundingBox(this.mg, largestCluster); + this.player.largestClusterBoundingBox = largestClusterBox; + const surroundedBy = this.surroundedBySamePlayer( largestCluster, + largestClusterBox, ); - const surroundedBy = this.surroundedBySamePlayer(largestCluster); if (surroundedBy && !surroundedBy.isFriendly(this.player)) { this.removeCluster(largestCluster); } @@ -158,7 +159,10 @@ export class PlayerExecution implements Execution { } } - private surroundedBySamePlayer(cluster: Set): false | Player { + private surroundedBySamePlayer( + cluster: Set, + clusterBox: { min: Cell; max: Cell }, + ): false | Player { const enemies = new Set(); for (const tile of cluster) { let hasUnownedNeighbor = false; @@ -187,7 +191,6 @@ export class PlayerExecution implements Execution { } const enemy = this.mg.playerBySmallID(Array.from(enemies)[0]) as Player; const enemyBox = calculateBoundingBox(this.mg, enemy.borderTiles()); - const clusterBox = calculateBoundingBox(this.mg, cluster); if (inscribed(enemyBox, clusterBox)) { return enemy; } @@ -195,7 +198,11 @@ export class PlayerExecution implements Execution { } private isSurrounded(cluster: Set): boolean { - const enemyTiles = new Set(); + let hasEnemy = false; + let minX = Infinity, + minY = Infinity, + maxX = -Infinity, + maxY = -Infinity; for (const tr of cluster) { if (this.mg.isShore(tr) || this.mg.isOnEdgeOfMap(tr)) { return false; @@ -203,27 +210,31 @@ export class PlayerExecution implements Execution { this.mg.forEachNeighbor(tr, (n) => { const owner = this.mg.owner(n); if (owner.isPlayer() && this.mg.ownerID(n) !== this.player.smallID()) { - enemyTiles.add(n); + hasEnemy = true; + const x = this.mg.x(n); + const y = this.mg.y(n); + minX = Math.min(minX, x); + minY = Math.min(minY, y); + maxX = Math.max(maxX, x); + maxY = Math.max(maxY, y); } }); } - if (enemyTiles.size === 0) { + if (!hasEnemy) { return false; } - const enemyBox = calculateBoundingBox(this.mg, enemyTiles); const clusterBox = calculateBoundingBox(this.mg, cluster); + const enemyBox = { min: new Cell(minX, minY), max: new Cell(maxX, maxY) }; return inscribed(enemyBox, clusterBox); } private removeCluster(cluster: Set) { - if ( - Array.from(cluster).some( - (t) => this.mg?.ownerID(t) !== this.player?.smallID(), - ) - ) { - // Other removeCluster operations could change tile owners, - // so double check. - return; + for (const t of cluster) { + if (this.mg?.ownerID(t) !== this.player?.smallID()) { + // Other removeCluster operations could change tile owners, + // so double check. + return; + } } const capturing = this.getCapturingPlayer(cluster); From 59e808b63b47da5172b84f5c00fe68907b8df179 Mon Sep 17 00:00:00 2001 From: Wraith <54374743+wraith4081@users.noreply.github.com> Date: Thu, 5 Feb 2026 22:35:49 +0300 Subject: [PATCH 19/20] fix(leaderboards): update & move reached_limit check (#3128) If this PR fixes an issue, link it below. If not, delete these two lines. Resolves #3126 ## Description: update & move reached_limit check ## Please complete the following: - [X] I have added screenshots for all UI updates - [X] I process any text displayed to the user through translateText() and I've added it to the en.json file - [X] I have added relevant tests to the test directory - [X] 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: wraith4081 --------- Co-authored-by: Ryan <7389646+ryanbarlow97@users.noreply.github.com> --- src/client/Api.ts | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/client/Api.ts b/src/client/Api.ts index d128ad2f2..7a18dea8a 100644 --- a/src/client/Api.ts +++ b/src/client/Api.ts @@ -235,13 +235,6 @@ export async function fetchPlayerLeaderboard( }); if (!res.ok) { - // Handle "Page must be between X and Y" error as end of list - if (res.status === 400) { - const errorJson = await res.json().catch(() => null); - if (errorJson?.message?.includes("Page must be between")) { - return "reached_limit"; - } - } console.warn( "fetchPlayerLeaderboard: unexpected status", res.status, @@ -253,6 +246,10 @@ export async function fetchPlayerLeaderboard( const json = await res.json(); const parsed = RankedLeaderboardResponseSchema.safeParse(json); if (!parsed.success) { + // Handle "Page must be between X and Y" error as end of list + if (json?.message?.includes?.("Page must be between")) { + return "reached_limit"; + } console.warn( "fetchPlayerLeaderboard: Zod validation failed", parsed.error.toString(), From 8cc6c2c2aa8b7cc30d0a598ab499b9c13ff484b9 Mon Sep 17 00:00:00 2001 From: scamiv <6170744+scamiv@users.noreply.github.com> Date: Thu, 5 Feb 2026 21:16:59 +0100 Subject: [PATCH 20/20] Perf spawn train (#3130) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description: Train spawning hot-path optimization (trade destination selection) ## Summary This PR reduces per-tick overhead in train spawning by removing temporary allocations and reducing work in the destination-selection path. The change focuses on `Cluster` trade destination lookup and how `TrainStationExecution` picks a destination. ## What changed ### 1) Maintain a “trade-capable” station subset per cluster `src/core/game/TrainStation.ts` - `Cluster` now maintains: - `stations`: all stations in the cluster (unchanged) - `tradeStations`: maintained subset of stations that can act as trade endpoints (`City` or `Port`) - `tradeStations` is kept in sync in: - `addStation()` - `removeStation()` - `clear()` Impact: - Trade queries no longer scan every station in the cluster; they only scan `tradeStations`. ### 2) Add cheap eligibility helpers `src/core/game/TrainStation.ts` - `hasAnyTradeDestination(player)`: - Fast early-exit check: returns as soon as it finds any eligible trade destination. - `randomTradeDestination(player, random)`: - Picks a random eligible trade destination directly without materializing an intermediate `Set`. ### 3) Use reservoir sampling for single-pass random choice `src/core/game/TrainStation.ts` `Cluster.randomTradeDestination()` uses reservoir sampling: - Iterates `tradeStations` once. - Maintains a running count of eligible stations (`eligibleSeen`). - Replaces the selected station with probability `1/eligibleSeen`. Properties: - Uniform selection among eligible stations. - One pass instead of “count then pick by index” (two pass). - Allocation-free. - Returns `null` when no eligible destination exists. ### 4) Update train spawning to avoid temporary sets `src/core/execution/TrainStationExecution.ts` - Previously: `spawnTrain()` called `cluster.availableForTrade()` and then `random.randFromSet(...)`. - This built a new `Set` on the hot path. - Now: - Early-exit via `cluster.hasAnyTradeDestination(owner)`. - Destination via `cluster.randomTradeDestination(owner, random)`. Net effect: - Less per-tick work and no per-spawn temporary `Set` allocations. ## Why this helps Train spawning happens frequently and can become a hot path in large games / large rail clusters. Avoiding repeated allocations and reducing work inside `tick()` helps keep frame/update time predictable. ## notes - Trade rules are unchanged (`tradeAvailable(player)` still gates eligibility). - Destination selection remains random-uniform over eligible `City`/`Port` stations that satisfy `tradeAvailable(player)`. - `TrainStationExecution` now avoids calling `spawnTrain()` entirely when `spawnTrains` is falsy (it was already guarded inside). ## 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: DISCORD_USERNAME --- src/core/execution/TrainStationExecution.ts | 36 ++++++++-------- src/core/game/TrainStation.ts | 48 ++++++++++++++++++--- tests/core/game/Cluster.test.ts | 4 ++ tests/core/game/RailNetwork.test.ts | 3 +- 4 files changed, 67 insertions(+), 24 deletions(-) diff --git a/src/core/execution/TrainStationExecution.ts b/src/core/execution/TrainStationExecution.ts index af8a5bd3f..5f029a0cc 100644 --- a/src/core/execution/TrainStationExecution.ts +++ b/src/core/execution/TrainStationExecution.ts @@ -45,7 +45,9 @@ export class TrainStationExecution implements Execution { this.active = false; return; } - this.spawnTrain(this.station, ticks); + if (this.spawnTrains) { + this.spawnTrain(this.station, ticks); + } } private shouldSpawnTrain(): boolean { @@ -69,8 +71,8 @@ export class TrainStationExecution implements Execution { if (cluster === null) { return; } - const availableForTrade = cluster.availableForTrade(this.unit.owner()); - if (availableForTrade.size === 0) { + const owner = this.unit.owner(); + if (!cluster.hasAnyTradeDestination(owner)) { return; } if (!this.shouldSpawnTrain()) { @@ -79,20 +81,20 @@ export class TrainStationExecution implements Execution { // Pick a destination randomly. // Could be improved to pick a lucrative trip - const destination: TrainStation = - this.random.randFromSet(availableForTrade); - if (destination !== station) { - this.mg.addExecution( - new TrainExecution( - this.mg.railNetwork(), - this.unit.owner(), - station, - destination, - this.numCars, - ), - ); - this.lastSpawnTick = currentTick; - } + const destination = cluster.randomTradeDestination(owner, this.random); + if (destination === null) return; + if (destination === station) return; + + this.mg.addExecution( + new TrainExecution( + this.mg.railNetwork(), + owner, + station, + destination, + this.numCars, + ), + ); + this.lastSpawnTick = currentTick; } activeDuringSpawnPhase(): boolean { diff --git a/src/core/game/TrainStation.ts b/src/core/game/TrainStation.ts index 9917737d9..6e9b57f0e 100644 --- a/src/core/game/TrainStation.ts +++ b/src/core/game/TrainStation.ts @@ -155,6 +155,12 @@ export class TrainStation { */ export class Cluster { public stations: Set = new Set(); + private tradeStations: Set = new Set(); + + private isTradeStation(station: TrainStation): boolean { + const type = station.unit.type(); + return type === UnitType.City || type === UnitType.Port; + } has(station: TrainStation) { return this.stations.has(station); @@ -162,11 +168,15 @@ export class Cluster { addStation(station: TrainStation) { this.stations.add(station); + if (this.isTradeStation(station)) { + this.tradeStations.add(station); + } station.setCluster(this); } removeStation(station: TrainStation) { this.stations.delete(station); + this.tradeStations.delete(station); } addStations(stations: Set) { @@ -181,14 +191,39 @@ export class Cluster { } } + hasAnyTradeDestination(player: Player): boolean { + for (const station of this.tradeStations) { + if (station.tradeAvailable(player)) { + return true; + } + } + return false; + } + + randomTradeDestination( + player: Player, + random: PseudoRandom, + ): TrainStation | null { + let selected: TrainStation | null = null; + let eligibleSeen = 0; + + for (const station of this.tradeStations) { + if (!station.tradeAvailable(player)) continue; + eligibleSeen++; + + // Reservoir sampling: keep each eligible station with probability 1/eligibleSeen. + if (random.nextInt(0, eligibleSeen) === 0) { + selected = station; + } + } + + return selected; + } + availableForTrade(player: Player): Set { const tradingStations = new Set(); - for (const station of this.stations) { - if ( - (station.unit.type() === UnitType.City || - station.unit.type() === UnitType.Port) && - station.tradeAvailable(player) - ) { + for (const station of this.tradeStations) { + if (station.tradeAvailable(player)) { tradingStations.add(station); } } @@ -201,6 +236,7 @@ export class Cluster { clear() { this.stations.clear(); + this.tradeStations.clear(); } } diff --git a/tests/core/game/Cluster.test.ts b/tests/core/game/Cluster.test.ts index 138e3968e..f64d73d09 100644 --- a/tests/core/game/Cluster.test.ts +++ b/tests/core/game/Cluster.test.ts @@ -1,9 +1,13 @@ import { vi, type Mocked } from "vitest"; +import { UnitType } from "../../../src/core/game/Game"; import { Cluster, TrainStation } from "../../../src/core/game/TrainStation"; const createMockStation = (id: string): Mocked => { return { id, + unit: { + type: vi.fn(() => UnitType.City), + } as any, setCluster: vi.fn(), getCluster: vi.fn(() => null), } as any; diff --git a/tests/core/game/RailNetwork.test.ts b/tests/core/game/RailNetwork.test.ts index fc39db81d..70be4febd 100644 --- a/tests/core/game/RailNetwork.test.ts +++ b/tests/core/game/RailNetwork.test.ts @@ -1,4 +1,4 @@ -import { Unit } from "../../../src/core/game/Game"; +import { Unit, UnitType } from "../../../src/core/game/Game"; import { RailNetworkImpl, StationManagerImpl, @@ -14,6 +14,7 @@ const createMockStation = (unitId: number): any => { unit: { id: unitId, setTrainStation: vi.fn(), + type: vi.fn(() => UnitType.City), }, tile: vi.fn(), neighbors: vi.fn(() => []),