From e1e6f3d06469ae1f3a47d8a41b358f3d420e6303 Mon Sep 17 00:00:00 2001 From: Priveetee Date: Sat, 22 Aug 2026 17:35:24 +0200 Subject: [PATCH 1/4] chore: separate TypeScript 7 production type checks --- .github/workflows/ci.yml | 3 ++ bun.lock | 43 ++++++++++++++++++++++++++++ package.json | 6 +++- src/botguard-page.ts | 5 +++- src/innertube.ts | 2 +- src/remote-login-browser.ts | 4 +-- src/remote-login-input.ts | 5 ++-- src/remote-login-session.ts | 1 + src/subtitle-routes.ts | 5 +++- src/youtube-caption-tracks.ts | 2 +- src/youtube-innertube-session.ts | 2 +- src/youtube-player-evaluator.ts | 5 ++-- src/youtube-sabr-adaptive-format.ts | 22 +++++++------- src/youtube-sabr-session.ts | 16 +++++------ tests/botguard-challenge.test.ts | 3 +- tests/index.test.ts | 2 +- tests/po-token-routes.test.ts | 2 ++ tests/subtitles.test.ts | 8 ++++-- tests/token-service-refresh.test.ts | 3 +- tests/token-service.test.ts | 3 +- tests/youtube-caption-tracks.test.ts | 3 ++ tests/youtube-fetch.test.ts | 3 +- tests/youtube-mweb-config.test.ts | 2 +- tsconfig.json | 5 ++-- tsconfig.tests.json | 12 ++++++++ 25 files changed, 125 insertions(+), 42 deletions(-) create mode 100644 tsconfig.tests.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 28abd13..ee4d76c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,6 +39,9 @@ jobs: - name: Lint run: bun run lint + - name: Typecheck + run: bun run typecheck + - name: Test with coverage run: bun test --coverage --coverage-reporter=lcov diff --git a/bun.lock b/bun.lock index 5d87ae1..dc90e06 100644 --- a/bun.lock +++ b/bun.lock @@ -13,6 +13,7 @@ "devDependencies": { "@biomejs/biome": "^2.5.7", "bun-types": "^1.3.14", + "typescript": "~7.0.2", }, }, }, @@ -42,6 +43,46 @@ "@types/node": ["@types/node@25.4.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-9wLpoeWuBlcbBpOY3XmzSTG3oscB6xjBEEtn+pYXTfhyXhIxC5FsBer2KTopBlvKEiW9l13po9fq+SJY/5lkhw=="], + "@typescript/typescript-aix-ppc64": ["@typescript/typescript-aix-ppc64@7.0.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ=="], + + "@typescript/typescript-darwin-arm64": ["@typescript/typescript-darwin-arm64@7.0.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA=="], + + "@typescript/typescript-darwin-x64": ["@typescript/typescript-darwin-x64@7.0.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA=="], + + "@typescript/typescript-freebsd-arm64": ["@typescript/typescript-freebsd-arm64@7.0.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ=="], + + "@typescript/typescript-freebsd-x64": ["@typescript/typescript-freebsd-x64@7.0.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw=="], + + "@typescript/typescript-linux-arm": ["@typescript/typescript-linux-arm@7.0.2", "", { "os": "linux", "cpu": "arm" }, "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ=="], + + "@typescript/typescript-linux-arm64": ["@typescript/typescript-linux-arm64@7.0.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ=="], + + "@typescript/typescript-linux-loong64": ["@typescript/typescript-linux-loong64@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ=="], + + "@typescript/typescript-linux-mips64el": ["@typescript/typescript-linux-mips64el@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA=="], + + "@typescript/typescript-linux-ppc64": ["@typescript/typescript-linux-ppc64@7.0.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA=="], + + "@typescript/typescript-linux-riscv64": ["@typescript/typescript-linux-riscv64@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ=="], + + "@typescript/typescript-linux-s390x": ["@typescript/typescript-linux-s390x@7.0.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw=="], + + "@typescript/typescript-linux-x64": ["@typescript/typescript-linux-x64@7.0.2", "", { "os": "linux", "cpu": "x64" }, "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A=="], + + "@typescript/typescript-netbsd-arm64": ["@typescript/typescript-netbsd-arm64@7.0.2", "", { "os": "none", "cpu": "arm64" }, "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA=="], + + "@typescript/typescript-netbsd-x64": ["@typescript/typescript-netbsd-x64@7.0.2", "", { "os": "none", "cpu": "x64" }, "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA=="], + + "@typescript/typescript-openbsd-arm64": ["@typescript/typescript-openbsd-arm64@7.0.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ=="], + + "@typescript/typescript-openbsd-x64": ["@typescript/typescript-openbsd-x64@7.0.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg=="], + + "@typescript/typescript-sunos-x64": ["@typescript/typescript-sunos-x64@7.0.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g=="], + + "@typescript/typescript-win32-arm64": ["@typescript/typescript-win32-arm64@7.0.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ=="], + + "@typescript/typescript-win32-x64": ["@typescript/typescript-win32-x64@7.0.2", "", { "os": "win32", "cpu": "x64" }, "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g=="], + "bgutils-js": ["bgutils-js@4.0.3", "", {}, "sha512-nvhrzqRYqFcC3AJbf1fENc/jzyMk95wH9UC9ruQzTZCq+itYWuHi2a9leK/bC1lLfi+R4a28wJZxpD5KiTFf+w=="], "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], @@ -58,6 +99,8 @@ "playwright-core": ["playwright-core@1.62.1", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw=="], + "typescript": ["typescript@7.0.2", "", { "optionalDependencies": { "@typescript/typescript-aix-ppc64": "7.0.2", "@typescript/typescript-darwin-arm64": "7.0.2", "@typescript/typescript-darwin-x64": "7.0.2", "@typescript/typescript-freebsd-arm64": "7.0.2", "@typescript/typescript-freebsd-x64": "7.0.2", "@typescript/typescript-linux-arm": "7.0.2", "@typescript/typescript-linux-arm64": "7.0.2", "@typescript/typescript-linux-loong64": "7.0.2", "@typescript/typescript-linux-mips64el": "7.0.2", "@typescript/typescript-linux-ppc64": "7.0.2", "@typescript/typescript-linux-riscv64": "7.0.2", "@typescript/typescript-linux-s390x": "7.0.2", "@typescript/typescript-linux-x64": "7.0.2", "@typescript/typescript-netbsd-arm64": "7.0.2", "@typescript/typescript-netbsd-x64": "7.0.2", "@typescript/typescript-openbsd-arm64": "7.0.2", "@typescript/typescript-openbsd-x64": "7.0.2", "@typescript/typescript-sunos-x64": "7.0.2", "@typescript/typescript-win32-arm64": "7.0.2", "@typescript/typescript-win32-x64": "7.0.2" }, "bin": { "tsc": "bin/tsc" } }, "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA=="], + "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], "youtubei.js": ["youtubei.js@17.2.0", "", { "dependencies": { "@bufbuild/protobuf": "^2.0.0", "fflate": "^0.8.2", "meriyah": "^6.1.4" } }, "sha512-XLNsgRKO1h7t4i9tIMWSQSeWdD7Ujkk5v1m5YCaumaHMhu/xuLqtO3M0Hq7CXNup9HlJ1NGrT1Y+HLIHnL6Ujg=="], diff --git a/package.json b/package.json index 9a48aa8..6237550 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,9 @@ "start": "bun run src/index.ts", "build": "bun build src/index.ts --outfile dist/index.js --target bun --external playwright", "test": "bun test", + "typecheck": "bun run typecheck:src && bun run typecheck:tests", + "typecheck:src": "tsc -p tsconfig.json", + "typecheck:tests": "tsc -p tsconfig.tests.json", "lint": "biome check src tests", "format": "biome format --write src tests" }, @@ -18,7 +21,8 @@ }, "devDependencies": { "@biomejs/biome": "^2.5.7", - "bun-types": "^1.3.14" + "bun-types": "^1.3.14", + "typescript": "~7.0.2" }, "trustedDependencies": [ "@biomejs/biome" diff --git a/src/botguard-page.ts b/src/botguard-page.ts index 42876a3..08bf9e9 100644 --- a/src/botguard-page.ts +++ b/src/botguard-page.ts @@ -56,7 +56,10 @@ export async function executeBotGuard( } config.EVENT_ID = args.eventId; new Function(args.script)(); - const g = globalThis as Record unknown>>; + const g = globalThis as unknown as Record< + string, + Record unknown> + >; const vm = g[args.name]; if (!vm?.a) throw new Error("BotGuard VM not found"); diff --git a/src/innertube.ts b/src/innertube.ts index c27ecf6..3ddd8a4 100644 --- a/src/innertube.ts +++ b/src/innertube.ts @@ -1,4 +1,4 @@ -import type { IntegrityTokenData } from "bgutils-js"; +import type { IntegrityTokenData } from "bgutils-js/shared-types"; import { WEB_CLIENT_VERSION } from "./botguard-challenge.ts"; import { youtubeFetch } from "./youtube-fetch.ts"; diff --git a/src/remote-login-browser.ts b/src/remote-login-browser.ts index c2e63be..ecadc18 100644 --- a/src/remote-login-browser.ts +++ b/src/remote-login-browser.ts @@ -60,8 +60,8 @@ function launchOptions(config: RemoteLoginConfig): Parameters { ); } try { - return new Response(await fetchSubtitleContent(rawUrl), { + const content = await fetchSubtitleContent(rawUrl); + const body = new ArrayBuffer(content.byteLength); + new Uint8Array(body).set(content); + return new Response(body, { headers: { "cache-control": "private, max-age=300", "content-type": "text/vtt; charset=utf-8", diff --git a/src/youtube-caption-tracks.ts b/src/youtube-caption-tracks.ts index 17f5ec1..899ae4a 100644 --- a/src/youtube-caption-tracks.ts +++ b/src/youtube-caption-tracks.ts @@ -113,7 +113,7 @@ function toRawCaptionTrack(track: CaptionTrackData, client: YoutubeSabrClient): ).toString(), name: { simpleText: track.name.toString() }, languageCode: track.language_code, - kind: track.kind, + ...(track.kind ? { kind: track.kind } : {}), vssId: track.vss_id, }; } diff --git a/src/youtube-innertube-session.ts b/src/youtube-innertube-session.ts index 70186f8..0c95b3c 100644 --- a/src/youtube-innertube-session.ts +++ b/src/youtube-innertube-session.ts @@ -50,7 +50,7 @@ const sessions = new YoutubeInnertubeSessions(async (client, v const innertube = await Innertube.create({ cache: new UniversalCache(true), client_type: client === "MWEB" ? ClientType.MWEB : ClientType.WEB, - fetch: youtubeFetch, + fetch: youtubeFetch as typeof fetch, visitor_data: visitorData, }); if (client === "MWEB") { diff --git a/src/youtube-player-evaluator.ts b/src/youtube-player-evaluator.ts index 16ad8b5..028f379 100644 --- a/src/youtube-player-evaluator.ts +++ b/src/youtube-player-evaluator.ts @@ -1,7 +1,6 @@ -import type { EvalResult } from "youtubei.js"; -import { Platform } from "youtubei.js"; +import { Platform, type Types } from "youtubei.js"; -export function evaluateYoutubePlayerScript(output: string): EvalResult { +export function evaluateYoutubePlayerScript(output: string): Types.EvalResult { return new Function(output)(); } diff --git a/src/youtube-sabr-adaptive-format.ts b/src/youtube-sabr-adaptive-format.ts index 0fe865e..ad59487 100644 --- a/src/youtube-sabr-adaptive-format.ts +++ b/src/youtube-sabr-adaptive-format.ts @@ -37,20 +37,20 @@ export function toYoutubeSabrAdaptiveFormat(format: SourceFormat): YoutubeSabrAd return { itag: format.itag, lastModified: format.last_modified_ms, - xtags: format.xtags, mimeType: format.mime_type, - audioTrack, - qualityLabel: format.quality_label, - audioQuality: format.audio_quality, isDrc: format.is_drc ?? false, - width: format.width, - height: format.height, bitrate: format.bitrate, - contentLength: format.content_length, approxDurationMs: format.approx_duration_ms, - url: format.url, - signatureCipher: format.signature_cipher, - initRange: format.init_range, - indexRange: format.index_range, + ...(format.xtags ? { xtags: format.xtags } : {}), + ...(audioTrack ? { audioTrack } : {}), + ...(format.quality_label ? { qualityLabel: format.quality_label } : {}), + ...(format.audio_quality ? { audioQuality: format.audio_quality } : {}), + ...(format.width !== undefined ? { width: format.width } : {}), + ...(format.height !== undefined ? { height: format.height } : {}), + ...(format.content_length !== undefined ? { contentLength: format.content_length } : {}), + ...(format.url ? { url: format.url } : {}), + ...(format.signature_cipher ? { signatureCipher: format.signature_cipher } : {}), + ...(format.init_range ? { initRange: format.init_range } : {}), + ...(format.index_range ? { indexRange: format.index_range } : {}), }; } diff --git a/src/youtube-sabr-session.ts b/src/youtube-sabr-session.ts index 08d6fe0..b7552bf 100644 --- a/src/youtube-sabr-session.ts +++ b/src/youtube-sabr-session.ts @@ -1,5 +1,5 @@ import { buildSabrFormat } from "googlevideo/utils"; -import { YTNodes } from "youtubei.js"; +import { type IPlayerResponse, YTNodes } from "youtubei.js"; import { KeyedSingleFlight } from "./keyed-single-flight.ts"; import { fetchPoToken } from "./token-service.ts"; import { findYoutubeChannelAvatarUrl } from "./youtube-channel-avatar.ts"; @@ -80,7 +80,7 @@ async function fetchYoutubeResponses( const nextEndpoint = new YTNodes.NavigationEndpoint({ watchNextEndpoint: { videoId } }); const cachedChannelAvatarUrl = getCachedYoutubeChannelAvatar(videoId); const [videoInfo, nextResponse] = await Promise.all([ - endpoint.call(innertube.actions, { + endpoint.call(innertube.actions, { ...buildYoutubeSabrPlayerRequest( innertube.session.player?.signature_timestamp, poToken, @@ -100,12 +100,12 @@ async function buildYoutubeSabrSession( client: YoutubeSabrClient, tokens: Awaited>, innertube: YoutubeInnertube, - videoInfo: Awaited>, + videoInfo: IPlayerResponse, channelAvatarUrl: string, ): Promise { - const decipheredServerAbrStreamingUrl = await innertube.session.player?.decipher( - videoInfo.streaming_data?.server_abr_streaming_url, - ); + const rawServerAbrStreamingUrl = videoInfo.streaming_data?.server_abr_streaming_url; + const decipheredServerAbrStreamingUrl = + await innertube.session.player?.decipher(rawServerAbrStreamingUrl); const serverAbrStreamingUrl = decipheredServerAbrStreamingUrl ? withYoutubeClientVersion( decipheredServerAbrStreamingUrl, @@ -116,7 +116,7 @@ async function buildYoutubeSabrSession( videoInfo.player_config?.media_common_config.media_ustreamer_request_config ?.video_playback_ustreamer_config; - if (!serverAbrStreamingUrl) { + if (!serverAbrStreamingUrl || !rawServerAbrStreamingUrl) { throw new Error("serverAbrStreamingUrl missing from YouTube player response"); } if (!videoPlaybackUstreamerConfig) { @@ -151,7 +151,7 @@ async function buildYoutubeSabrSession( poToken: tokens.visitorBoundPoToken, streamingPot: tokens.streamingPot, serverAbrStreamingUrl, - rawServerAbrStreamingUrl: videoInfo.streaming_data.server_abr_streaming_url, + rawServerAbrStreamingUrl, hlsManifestUrl: videoInfo.streaming_data?.hls_manifest_url ?? null, videoPlaybackUstreamerConfig, durationMs: metadata.durationMs || null, diff --git a/tests/botguard-challenge.test.ts b/tests/botguard-challenge.test.ts index fb79ac5..a38e83b 100644 --- a/tests/botguard-challenge.test.ts +++ b/tests/botguard-challenge.test.ts @@ -25,9 +25,10 @@ describe("fetchChallenge", () => { ``, ); - }) as typeof fetch; + }) as unknown as typeof fetch; const { fetchChallenge, WEB_CLIENT_VERSION } = await import( + // @ts-expect-error Bun uses the query to isolate this module instance. "../src/botguard-challenge.ts?contract-test" ); const challenge = await fetchChallenge("visitor-session"); diff --git a/tests/index.test.ts b/tests/index.test.ts index 5f83803..775da42 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -3,7 +3,7 @@ import type { SubtitleTrack } from "../src/subtitles.ts"; import type { TokenResult } from "../src/token-service.ts"; import type { RawCaptionTrack } from "../src/youtube-caption-tracks.ts"; import type { YoutubePlayerDecodeResponse } from "../src/youtube-player-decoder.ts"; -import type { YoutubeSabrSession } from "../src/youtube-sabr-session.ts"; +import type { YoutubeSabrSession } from "../src/youtube-sabr-types.ts"; const mockFetchPoToken = mock( async (videoId: string, _forceRefresh = false, _refreshVideo = false): Promise => ({ diff --git a/tests/po-token-routes.test.ts b/tests/po-token-routes.test.ts index 486d924..1296f90 100644 --- a/tests/po-token-routes.test.ts +++ b/tests/po-token-routes.test.ts @@ -23,6 +23,7 @@ mock.module("../src/token-service.ts", () => ({ describe("POST /potoken/session", () => { it("returns tokens bound by one internal request", async () => { + // @ts-expect-error Bun uses the query to isolate this module instance. const { handlePoTokenRequest } = await import("../src/po-token-routes.ts?session-route-test"); const request = new Request("http://localhost:8081/potoken/session", { method: "POST", @@ -40,6 +41,7 @@ describe("POST /potoken/session", () => { }); it("rejects incomplete and oversized bindings", async () => { + // @ts-expect-error Bun uses the query to isolate this module instance. const { handlePoTokenRequest } = await import("../src/po-token-routes.ts?session-route-test"); const callsBefore = fetchSessionPoTokens.mock.calls.length; const request = new Request("http://localhost:8081/potoken/session", { diff --git a/tests/subtitles.test.ts b/tests/subtitles.test.ts index 2fe840c..9e58c7e 100644 --- a/tests/subtitles.test.ts +++ b/tests/subtitles.test.ts @@ -6,7 +6,7 @@ const VISITOR_DATA = "visitor-data-test-123"; const INTEGRITY_TOKEN = "integrity-token-test-xyz"; const PO_TOKEN = `pot-${VISITOR_DATA}`; const mockYoutubeFetch = mock( - async (_input: RequestInfo | URL): Promise => + async (_input: RequestInfo | URL, _init?: RequestInit): Promise => new Response("WEBVTT\n\n00:00.000 --> 00:01.000\nHello", { status: 200, headers: { "content-type": "text/vtt" }, @@ -105,7 +105,11 @@ describe("fetchSubtitles", () => { it("skips tracks with missing baseUrl or languageCode", async () => { mockFetchCaptionTracks.mockImplementation(async () => [ - { name: { simpleText: "Bad" } }, + { + baseUrl: undefined as unknown as string, + languageCode: undefined as unknown as string, + name: { simpleText: "Bad" }, + }, frTrack, ]); const { fetchSubtitles } = await import("../src/subtitles.ts"); diff --git a/tests/token-service-refresh.test.ts b/tests/token-service-refresh.test.ts index c5c807d..2f41a61 100644 --- a/tests/token-service-refresh.test.ts +++ b/tests/token-service-refresh.test.ts @@ -1,5 +1,5 @@ import { beforeAll, describe, expect, it, mock } from "bun:test"; -import type { IntegrityTokenData } from "bgutils-js"; +import type { IntegrityTokenData } from "bgutils-js/shared-types"; import type { TokenResult } from "../src/token-service.ts"; let markVisitorDataStarted: (() => void) | undefined; @@ -85,6 +85,7 @@ let fetchPoToken: ( describe("token refresh concurrency", () => { beforeAll(async () => { + // @ts-expect-error Bun uses the query to isolate this module instance. const module = await import("../src/token-service.ts?refresh-race-test"); fetchPoToken = module.fetchPoToken; }); diff --git a/tests/token-service.test.ts b/tests/token-service.test.ts index 8f58008..75ab665 100644 --- a/tests/token-service.test.ts +++ b/tests/token-service.test.ts @@ -1,5 +1,5 @@ import { beforeAll, describe, expect, it, mock } from "bun:test"; -import type { IntegrityTokenData } from "bgutils-js"; +import type { IntegrityTokenData } from "bgutils-js/shared-types"; import type { SessionTokenResult, TokenResult } from "../src/token-service.ts"; const VISITOR_DATA = "visitor-data-test-123"; @@ -51,6 +51,7 @@ let fetchSessionPoTokens: ( describe("fetchPoToken", () => { beforeAll(async () => { + // @ts-expect-error Bun uses the query to isolate this module instance. const module = await import("../src/token-service.ts?token-service-test"); fetchPoToken = module.fetchPoToken; fetchSessionPoTokens = module.fetchSessionPoTokens; diff --git a/tests/youtube-caption-tracks.test.ts b/tests/youtube-caption-tracks.test.ts index 394e860..d4ea51b 100644 --- a/tests/youtube-caption-tracks.test.ts +++ b/tests/youtube-caption-tracks.test.ts @@ -7,6 +7,7 @@ const secondSession = { id: "second" } as unknown as YoutubeInnertube; describe("YouTube caption tracks", () => { it("loads and normalizes tracks through the WEB player contract", async () => { const { fetchCaptionTracks } = await import( + // @ts-expect-error Bun uses the query to isolate this module instance. "../src/youtube-caption-tracks.ts?caption-contract" ); const getInnertube = mock(async () => firstSession); @@ -48,6 +49,7 @@ describe("YouTube caption tracks", () => { it("falls back to MWEB when WEB does not return caption tracks", async () => { const { fetchCaptionTracks } = await import( + // @ts-expect-error Bun uses the query to isolate this module instance. "../src/youtube-caption-tracks.ts?caption-fallback" ); const getInnertube = mock(async (client: "WEB" | "MWEB") => @@ -83,6 +85,7 @@ describe("YouTube caption tracks", () => { it("recreates a rejected anonymous WEB session only once", async () => { const { fetchCaptionTracks } = await import( + // @ts-expect-error Bun uses the query to isolate this module instance. "../src/youtube-caption-tracks.ts?caption-contract" ); const getInnertube = mock(async () => diff --git a/tests/youtube-fetch.test.ts b/tests/youtube-fetch.test.ts index cee0ac7..ce6924a 100644 --- a/tests/youtube-fetch.test.ts +++ b/tests/youtube-fetch.test.ts @@ -19,7 +19,8 @@ describe("youtubeFetch", () => { if (init?.proxy !== undefined) throw new Error("proxy unavailable"); return new Response("direct response"); }); - globalThis.fetch = fetcher as typeof fetch; + globalThis.fetch = fetcher as unknown as typeof fetch; + // @ts-expect-error Bun uses the query to isolate this module instance. const { youtubeFetch } = await import("../src/youtube-fetch.ts?fail-closed-test"); await expect(youtubeFetch("https://www.youtube.com")).rejects.toThrow("proxy unavailable"); diff --git a/tests/youtube-mweb-config.test.ts b/tests/youtube-mweb-config.test.ts index 774d85c..55ed2c6 100644 --- a/tests/youtube-mweb-config.test.ts +++ b/tests/youtube-mweb-config.test.ts @@ -31,7 +31,7 @@ describe("YouTube MWEB config", () => { return new Response(configBody("2.20260723.05.00")); }; - await expect(fetchYoutubeMwebConfig(fetcher)).resolves.toEqual({ + await expect(fetchYoutubeMwebConfig(fetcher as unknown as typeof fetch)).resolves.toEqual({ clientVersion: "2.20260723.05.00", }); expect(calls[0]?.url).toBe("https://m.youtube.com/sw.js_data"); diff --git a/tsconfig.json b/tsconfig.json index 30f7145..251f675 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,8 +11,9 @@ "exactOptionalPropertyTypes": true, "allowImportingTsExtensions": true, "noEmit": true, - "lib": ["ESNext"], + "lib": ["ESNext", "DOM", "DOM.Iterable"], + "skipLibCheck": true, "types": ["bun-types"] }, - "include": ["src/**/*", "tests/**/*"] + "include": ["src/**/*"] } diff --git a/tsconfig.tests.json b/tsconfig.tests.json new file mode 100644 index 0000000..f3e0ff6 --- /dev/null +++ b/tsconfig.tests.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "strict": false, + "noImplicitAny": false, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noImplicitReturns": false, + "exactOptionalPropertyTypes": false + }, + "include": ["src/**/*", "tests/**/*"] +} From 8b3ba728e2061c9681e32e07766bb7ad577e89c8 Mon Sep 17 00:00:00 2001 From: Priveetee Date: Sat, 22 Aug 2026 18:59:16 +0200 Subject: [PATCH 2/4] chore: update biome --- bun.lock | 20 ++++++++++---------- package.json | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/bun.lock b/bun.lock index dc90e06..82b1de3 100644 --- a/bun.lock +++ b/bun.lock @@ -11,7 +11,7 @@ "youtubei.js": "^17.2.0", }, "devDependencies": { - "@biomejs/biome": "^2.5.7", + "@biomejs/biome": "^2.5.10", "bun-types": "^1.3.14", "typescript": "~7.0.2", }, @@ -21,23 +21,23 @@ "@biomejs/biome", ], "packages": { - "@biomejs/biome": ["@biomejs/biome@2.5.7", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.5.7", "@biomejs/cli-darwin-x64": "2.5.7", "@biomejs/cli-linux-arm64": "2.5.7", "@biomejs/cli-linux-arm64-musl": "2.5.7", "@biomejs/cli-linux-x64": "2.5.7", "@biomejs/cli-linux-x64-musl": "2.5.7", "@biomejs/cli-win32-arm64": "2.5.7", "@biomejs/cli-win32-x64": "2.5.7" }, "bin": { "biome": "bin/biome" } }, "sha512-zr8K/DcY5tYsQOQwqMJ0AWElo6QgmgNI7idXgXLhevVszlt8RGVpesEJPqx3ThazLaOwjJ5Y8fz3BtH5fGZNsw=="], + "@biomejs/biome": ["@biomejs/biome@2.5.10", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.5.10", "@biomejs/cli-darwin-x64": "2.5.10", "@biomejs/cli-linux-arm64": "2.5.10", "@biomejs/cli-linux-arm64-musl": "2.5.10", "@biomejs/cli-linux-x64": "2.5.10", "@biomejs/cli-linux-x64-musl": "2.5.10", "@biomejs/cli-win32-arm64": "2.5.10", "@biomejs/cli-win32-x64": "2.5.10" }, "bin": { "biome": "bin/biome" } }, "sha512-WRKXARA3kTuiV5sxqTpobJ/I0MVd4vk3pOL6wnp5az4LntFIhWTj1RWZq3DI9PCEN3lXcqy7p5aqUHzvq8AXyQ=="], - "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.5.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-vxo/Ls3/PYdQWyLhYYcgMOCzQypAjcY+iihS8M0wW03l16TCLW4zqZzGo75gm1VdCMj38hTVZ31KBWrZ4G9dJw=="], + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.5.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ItCrxKK6SXVT6flYs0qIuBd4AA3TTTl4d66Re6YI2FuGZnN85NmuYNzkiTJUyYw8qBLv69L5zTUB6uyWd++h3Q=="], - "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.5.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-Cd3Ga61amT/Yl/0x8elP5hhGYaFy4bw6WuysTgf7oo8TA5tJ5A1k+DkVoJ2BHbTVil51gTX9VPzArnrlLJ3Kyg=="], + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.5.10", "", { "os": "darwin", "cpu": "x64" }, "sha512-yLsPU9pAmtChXDu8vhKAzErqe+LeeYuwuUB2FZMkRitsmdodxsYRa9KHrFispsUHzzOu+9HB3nP/TQxyia+Sjw=="], - "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.5.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-rR2QE0yF2GYSuYuKIa7pKvODGJqnOH+2eDREAM8wV+mWKSkMQKdAp4zXEZfTaxY8PMoNONnpgSWcBCyLDPDOKg=="], + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.5.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-VG8uQW/86a1roLaIFvtIbEigxIdzdJ190oGyg1tV7VYeQtOS+x10sflk7WbuXgw91EtZX5DlIIIej1YqkNLlcg=="], - "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.5.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-xPI5yB6XlpDbNkS+bm1t42olw5c4l3UrlOmLg7KtLJvjvkNF/1V4tnUgfkylGIeb3u/T+BzMGYqgQhzjAoJzuQ=="], + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.5.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-t1QAKZwQJRB4dvgJSgFiQ4BNfNPChg69BNonz854qLVxnjT3UvDzQg9mbkTJRu35ZqU0Rw10A73J8Urgbg2RPw=="], - "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.5.7", "", { "os": "linux", "cpu": "x64" }, "sha512-FQgqJhscrqJUFptGaRSUJWlXAExwWcDwLuK49dvKfkQ1bB5SEEyFssnsxQY83Xm6jR0EbbX3+8+D5bfvYqUG2Q=="], + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.5.10", "", { "os": "linux", "cpu": "x64" }, "sha512-4O6T0eq2heoHZN0a9UX+rWQoxXEBaKf+lRi2hbsGlHneUz9BWXM76nEWMK7Eeq8gzMxR1khQB6BFpAASpeXqGg=="], - "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.5.7", "", { "os": "linux", "cpu": "x64" }, "sha512-rE5VZi+qtmPgQH+l7jVxYoZ18b/TiHEhulhMpjmCZH1PltSbjRcxNWywC3HZ9tYottG7ORkeTtoscBilKSBm0g=="], + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.5.10", "", { "os": "linux", "cpu": "x64" }, "sha512-pgDDqp9JybHm2I0KRgzN6i4+lt8xu4iqxUwLzglUMmOmyRTU1AYBGKzh9sNMOtIjah7xoWvKHlLVetvyifzoiQ=="], - "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.5.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-Oq4x0CCwP4jirrcTywXs5kOGZ4v5vuEP+gWrbtjApOA2CL9F3F9GlIdQIci8AKSCa/zURanMRpX/4wQ7Am6hHg=="], + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.5.10", "", { "os": "win32", "cpu": "arm64" }, "sha512-pxAbxduPO4xq/Cvgaa2lOrs9BB0hEXmmDqfMNP4ZOffGOkUrD1/QGw9UAMpFQpX2P8MqTIIRuQKcmetum4Oa6A=="], - "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.5.7", "", { "os": "win32", "cpu": "x64" }, "sha512-V+0wu/nrj2S+MhP4EQ0uHNolP0IALEsz45pg0WoKkHfDeh0+ItHwP/p7bX5RPoMOl9NkpHYWdYPhIcy2mACHvQ=="], + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.5.10", "", { "os": "win32", "cpu": "x64" }, "sha512-M+2dgBsl3lXRiTfgPVc2p3anS4Tocojke4rzFLScZ2Y/wmF+36dRb1iHCLiyGqOzQGyTplZH1HnEYviiAqi3nA=="], "@bufbuild/protobuf": ["@bufbuild/protobuf@2.12.1", "", {}, "sha512-BvAMfS6LrgZiryOAZ4pBYucu4wG/Ei/9o9DZ9akbREnMLbPJiom2i8b9C8IsKErQoiKqVhrerzt3kOT/RrzLHg=="], diff --git a/package.json b/package.json index 6237550..c3e0e6e 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "youtubei.js": "^17.2.0" }, "devDependencies": { - "@biomejs/biome": "^2.5.7", + "@biomejs/biome": "^2.5.10", "bun-types": "^1.3.14", "typescript": "~7.0.2" }, From 2e24b516426e06fdbe98f7b8a942f29608d1bdf3 Mon Sep 17 00:00:00 2001 From: Priveetee Date: Wed, 26 Aug 2026 22:55:25 +0200 Subject: [PATCH 3/4] fix: serialize remote login input events --- src/remote-login-session.ts | 44 +++++++++++++++- tests/remote-login-session-input.test.ts | 64 ++++++++++++++++++++++++ 2 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 tests/remote-login-session-input.test.ts diff --git a/src/remote-login-session.ts b/src/remote-login-session.ts index 7213f42..624d91c 100644 --- a/src/remote-login-session.ts +++ b/src/remote-login-session.ts @@ -5,6 +5,7 @@ import { applyRemoteLoginInput } from "./remote-login-input.ts"; import { errorMessage, parseRemoteLoginInput, + type RemoteLoginInput, type RemoteLoginPhase, statusMessage, } from "./remote-login-messages.ts"; @@ -32,6 +33,8 @@ export class RemoteLoginSession { private expiryTimer: ReturnType; private frameTimer: ReturnType | null = null; private loginTimer: ReturnType | null = null; + private readonly inputQueue: RemoteLoginInput[] = []; + private inputDrainRunning = false; constructor(options: RemoteLoginSessionOptions) { this.sessionId = options.sessionId; this.userId = options.userId; @@ -68,7 +71,7 @@ export class RemoteLoginSession { if (this.closed || typeof raw !== "string") return; const message = parseRemoteLoginInput(raw); if (!message) return; - void this.applyInput(message); + this.enqueueInput(message); } disconnect(): void { this.fail("WebSocket disconnected"); @@ -82,6 +85,42 @@ export class RemoteLoginSession { if (!page) return; if ((await applyRemoteLoginInput(page, message)) === "cancelled") this.cancel(); } + private enqueueInput(message: RemoteLoginInput): void { + if (message.type === "cancel") { + this.inputQueue.length = 0; + this.inputQueue.unshift(message); + } else if (message.type === "pointer" && message.event === "move") { + const last = this.inputQueue.at(-1); + if (last?.type === "pointer" && last.event === "move") { + this.inputQueue[this.inputQueue.length - 1] = message; + } else if (this.inputQueue.length < MAX_INPUT_QUEUE) { + this.inputQueue.push(message); + } + } else { + if (this.inputQueue.length >= MAX_INPUT_QUEUE) { + const moveIndex = this.inputQueue.findIndex( + (input) => input.type === "pointer" && input.event === "move", + ); + if (moveIndex >= 0) this.inputQueue.splice(moveIndex, 1); + } + if (this.inputQueue.length < MAX_INPUT_QUEUE) this.inputQueue.push(message); + } + void this.drainInputQueue(); + } + private async drainInputQueue(): Promise { + if (this.inputDrainRunning) return; + this.inputDrainRunning = true; + try { + while (!this.closed) { + const message = this.inputQueue.shift(); + if (!message) return; + await this.applyInput(message); + } + } finally { + this.inputDrainRunning = false; + if (!this.closed && this.inputQueue.length > 0) void this.drainInputQueue(); + } + } private scheduleLoginCheck(): void { if (this.closed || this.captureStarted) return; this.loginTimer = setTimeout(() => void this.checkLogin(), 1000); @@ -157,6 +196,7 @@ export class RemoteLoginSession { private finish(code: number, reason: string): void { if (this.closed) return; this.closed = true; + this.inputQueue.length = 0; clearTimeout(this.expiryTimer); if (this.frameTimer) clearTimeout(this.frameTimer); if (this.loginTimer) clearTimeout(this.loginTimer); @@ -165,3 +205,5 @@ export class RemoteLoginSession { this.onDone(this.sessionId, this.userId); } } + +const MAX_INPUT_QUEUE = 128; diff --git a/tests/remote-login-session-input.test.ts b/tests/remote-login-session-input.test.ts new file mode 100644 index 0000000..60cec0b --- /dev/null +++ b/tests/remote-login-session-input.test.ts @@ -0,0 +1,64 @@ +import { describe, expect, it } from "bun:test"; +import { RemoteLoginSession } from "../src/remote-login-session.ts"; +import { + fakeRemoteLoginPage, + remoteLoginTarget, + remoteLoginTestConfig, +} from "./remote-login-fixtures.ts"; + +function waitFor(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +describe("RemoteLoginSession input handling", () => { + it("keeps a click's press and release in order", async () => { + const calls: string[] = []; + let releaseMove: (() => void) | null = null; + const page = fakeRemoteLoginPage({ + page: { + goto: async () => undefined, + setViewportSize: async () => undefined, + screenshot: async () => Buffer.from("frame"), + mouse: { + move: async () => { + calls.push("move"); + if (calls.length === 1) await new Promise((resolve) => (releaseMove = resolve)); + }, + down: async () => { + calls.push("down"); + }, + up: async () => { + calls.push("up"); + }, + wheel: async () => undefined, + }, + keyboard: { + down: async () => undefined, + up: async () => undefined, + insertText: async () => undefined, + }, + }, + }); + const session = new RemoteLoginSession({ + sessionId: "session", + userId: "user", + expiresAt: Date.now() + 300_000, + target: remoteLoginTarget(), + config: remoteLoginTestConfig(), + createPage: async () => page, + onDone: () => undefined, + }); + + await session.start(); + session.handleMessage(JSON.stringify({ type: "pointer", event: "down", x: 10, y: 20 })); + session.handleMessage(JSON.stringify({ type: "pointer", event: "up", x: 10, y: 20 })); + await waitFor(0); + expect(calls).toEqual(["move"]); + + releaseMove?.(); + await waitFor(0); + await waitFor(0); + expect(calls).toEqual(["move", "down", "move", "up"]); + session.cancel(); + }); +}); From b5e60b1f24097640bc0d39640b1b3424f031cc3b Mon Sep 17 00:00:00 2001 From: Priveetee Date: Fri, 28 Aug 2026 16:41:14 +0200 Subject: [PATCH 4/4] chore: prepare token 1.7.0 --- Dockerfile | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 993caef..7a6ada3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,7 +13,7 @@ ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 RUN bun install --frozen-lockfile --production FROM mcr.microsoft.com/playwright:v1.62.1-noble AS runner -ARG BUILD_VERSION=1.6.0-dev +ARG BUILD_VERSION=1.7.0-dev ARG BUILD_REVISION=development ARG BUILD_TIME=unknown RUN apt-get update \ diff --git a/package.json b/package.json index c3e0e6e..f2851b8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "typetype-token", - "version": "1.6.0", + "version": "1.7.0", "private": true, "license": "MIT", "scripts": {