{"version":3,"file":"teleporter.luau","names":[],"sources":["..\\src\\teleporter.ts"],"sourcesContent":["import { catcher } from \"@lisachandra/core/utils/main\";\nimport { typeAssertIs } from \"@lisachandra/core/utils/type\";\nimport { Boba } from \"@rbxts/boba\";\nimport Log from \"@rbxts/log\";\nimport HashLib from \"@rbxts/rbxts-hashlib\";\nimport { TeleportService } from \"@rbxts/services\";\n\n// ─── Runtime Configuration ──────────────────────────────────────────────────\n\n/** Configuration for teleport behavior including expiration, retry attempts, and flood delay. */\nexport interface TeleportConfig {\n\t/** Max teleport retry attempts (default: 3). */\n\tattempts?: number;\n\t/** Teleport data expiration in seconds (default: 300). */\n\texpiration?: number;\n\t/** Delay when teleport is flooded in seconds (default: 5). */\n\tflood_delay?: number;\n\t/** Delay between retries in seconds (default: 1). */\n\tretry_delay?: number;\n}\n\nconst defaultConfig: Required<TeleportConfig> = {\n\tattempts: 3,\n\texpiration: 300,\n\tflood_delay: 5,\n\tretry_delay: 1,\n};\n\nlet teleportConfig = { ...defaultConfig };\nlet teleportSecret = \"\";\n\n/** Configures teleport behavior (expiration, retry, flood delay). */\nexport function configureTeleport(config: TeleportConfig): void {\n\tteleportConfig = { ...defaultConfig, ...config };\n}\n\n/** Configures the secret used for teleport hash verification. */\nexport function configureTeleportSecret(secret: string): void {\n\tteleportSecret = secret;\n}\n\nconst checkTeleportData = Boba.ExhaustiveStruct({\n\thash: Boba.String,\n\tstamp: Boba.Number,\n}) satisfies Boba<PossibleTeleportData>;\n\nconst teleportAsync = Promise.promisify(\n\t(placeId: number, players: Array<Player>, options?: TeleportOptions) => {\n\t\treturn TeleportService.TeleportAsync(placeId, players, options);\n\t},\n);\n\n/** Reasons why a teleport may be considered invalid. */\nexport enum TeleportReason {\n\tTeleportInvalidData,\n\tTeleportInvalidHash,\n\tTeleportOldHash,\n}\n\n/** Defines the structure for teleport data that can be serialized and sent with a teleport. */\nexport interface PossibleTeleportData {\n\t/** A hash value used to verify the integrity of the teleport data. */\n\thash: string;\n\t/** A timestamp indicating when the teleport data was generated. */\n\tstamp: number;\n}\n\nfunction generateTeleportHash(\n\t{ stamp }: Omit<PossibleTeleportData, \"hash\">,\n\tsecret = teleportSecret,\n): string {\n\treturn HashLib.sha256(`${stamp}${secret}`);\n}\n\n/**\n * Serializes teleport data into a TeleportOptions object, adding a hash and timestamp for security.\n *\n * @param data - The teleport data to serialize. It should not include `hash` or `stamp`.\n * @returns A TeleportOptions object with serialized data, hash, and timestamp.\n */\nexport function serializeTeleportData(\n\tdata: Omit<PossibleTeleportData, \"hash\" | \"stamp\">,\n): TeleportOptions {\n\tconst options = new Instance(\"TeleportOptions\");\n\tconst stamp = os.time(os.date(\"!*t\"));\n\tconst hash = generateTeleportHash({ ...data, stamp });\n\n\tconst teleportData: PossibleTeleportData = { ...data, hash, stamp };\n\toptions.SetTeleportData(teleportData);\n\n\treturn options;\n}\n\n/* Logs invalid teleports. */\nfunction logInvalidTeleport(\n\t{ format, success, unexpired, validData, validHash }: ReturnType<typeof isValidTeleport>,\n\tuserId: number,\n\t_teleportData?: unknown,\n): void {\n\tif (success || validHash === true || unexpired === true) {\n\t\treturn;\n\t}\n\n\tLog.Warn(\n\t\t\"Invalid teleport detected for user {UserId}: validData={ValidData}, validHash={ValidHash}, unexpired={Unexpired} {Format}\",\n\t\tuserId,\n\t\tvalidData,\n\t\tvalidHash,\n\t\tunexpired,\n\t\tformat,\n\t);\n}\n\n/**\n * Validates the teleport data received by a player upon joining a game instance.\n *\n * @param player - The player whose teleport data needs to be validated.\n * @param logInvalid - Whether it should log invalid teleports.\n * @returns An object indicating the success of the validation and the validity of the hash and\n *   expiration status.\n */\nexport function isValidTeleport(\n\tplayer: Player,\n\tlogInvalid = true,\n): {\n\tformat?: string;\n\tsuccess: boolean;\n\tunexpired?: boolean;\n\tvalidData?: boolean;\n\tvalidHash?: boolean;\n} {\n\tconst joinData = player.GetJoinData();\n\tlet teleportData = joinData.TeleportData as unknown;\n\tconst validData = checkTeleportData.match(teleportData);\n\tif (!validData.ok) {\n\t\tconst returned = { format: validData.format(), success: false, validData: false };\n\t\tif (logInvalid) {\n\t\t\tlogInvalidTeleport(returned, player.UserId, teleportData);\n\t\t}\n\n\t\treturn returned;\n\t}\n\n\ttypeAssertIs<PossibleTeleportData>(teleportData);\n\n\tconst stamp = os.time(os.date(\"!*t\"));\n\tconst hash = generateTeleportHash(teleportData);\n\n\tconst validHash = teleportData.hash === hash;\n\tconst unexpired = stamp < teleportData.stamp + teleportConfig.expiration;\n\tconst success = validHash && unexpired;\n\n\tconst returned = { success, unexpired, validData: true, validHash };\n\tif (!success) {\n\t\tlogInvalidTeleport(returned, player.UserId, teleportData);\n\t}\n\n\treturn returned;\n}\n\n/**\n * Teleports a group of players to a specified place, retrying upon failure.\n *\n * @param placeId - The ID of the target place.\n * @param players - An array of players to teleport.\n * @param options - Optional TeleportOptions to include with the teleport request.\n * @returns A promise resolving to a tuple indicating success and the result of the teleport\n *   attempt.\n */\nexport async function teleport(\n\tplaceId: number,\n\tplayers: Array<Player>,\n\toptions?: TeleportOptions,\n): Promise<[success: false, result: unknown] | [success: true, result: TeleportAsyncResult]> {\n\tlet attemptIndex = 0;\n\tlet success = false;\n\tlet result: unknown;\n\n\twhile (!success && attemptIndex < teleportConfig.attempts) {\n\t\t[success, result] = teleportAsync(placeId, players, options).await();\n\t\tattemptIndex++;\n\t\tif (!success) {\n\t\t\ttask.wait(teleportConfig.retry_delay);\n\t\t}\n\t}\n\n\tif (!success) {\n\t\tLog.Warn(\"Teleport unsuccessful: {$Result}\", result);\n\t\treturn [false, result];\n\t}\n\n\treturn [true, result as TeleportAsyncResult];\n}\n\nTeleportService.TeleportInitFailed.Connect(\n\t(player, teleportResult, errorMessage, targetPlaceId, teleportOptions) => {\n\t\tif (teleportResult === Enum.TeleportResult.Flooded) {\n\t\t\ttask.wait(teleportConfig.flood_delay);\n\t\t} else if (teleportResult === Enum.TeleportResult.Failure) {\n\t\t\ttask.wait(teleportConfig.retry_delay);\n\t\t} else {\n\t\t\tLog.Fatal(`Invalid teleport [${teleportResult.Name}]: ${errorMessage}`);\n\t\t}\n\n\t\tteleport(targetPlaceId, [player], teleportOptions).catch(catcher());\n\t},\n);\n"],"mappings":";;AAAA;AACA;AACA;AACA;AACA;AACA;;;AAgBA;;;;;;AAOA;AAAA;AAAA;AACA;;AAGA;AACC;AAAA;AAAA;;AAAgD;AAAhD;AACD;;AAGA;AACC;AACD;AAEA;;;;AAKA;;;;AAOA;AAAA;;;;;;;;;;;AAPA;;AAqBA;;;;;AAIC;AACD;;;;;;;;;AAQA;AAGC;AACA;AACA;AAAA;AAAA;AAAA;AAEA;AAAA;AAAA;AAAA;AAAA;AACA;AAEA;AACD;;AAGA;;;;;;AAKC;AACC;AACD;AAEA;AAQD;;;;;;;;;;;AAUA;;;;AAUC;AACA;AACA;AACA;AACC;;;;;AACA;AACC;AACD;AAEA;AACD;AAEA;AAEA;AACA;AAEA;AACA;AACA;AAEA;;;;;;AACA;AACC;AACD;AAEA;AACD;;;;;;;;;;;;AAWA;;;;;;;;;;;;;;;;;AAyBA;AAEE;AACC;;AAEA;;AAEA;AACD;AAEA;AAED","ignoreList":[]}