/// import { z } from "zod"; import { Config, ConfigSchema } from "../helpers/config.ts"; import { BG, buildURL, GOOG_API_KEY, USER_AGENT } from "bgutils"; import type { WebPoSignalOutput } from "bgutils"; import { JSDOM } from "jsdom"; import { Innertube } from "youtubei.js"; import { PLAYER_ID } from "../../constants.ts"; let getFetchClientLocation = "getFetchClient"; if (Deno.env.get("GET_FETCH_CLIENT_LOCATION")) { if (Deno.env.has("DENO_COMPILED")) { getFetchClientLocation = Deno.mainModule.replace("src/main.ts", "") + Deno.env.get("GET_FETCH_CLIENT_LOCATION"); } else { getFetchClientLocation = Deno.env.get( "GET_FETCH_CLIENT_LOCATION", ) as string; } } type FetchFunction = typeof fetch; const { getFetchClient }: { getFetchClient: (config: Config) => Promise; } = await import(getFetchClientLocation); // ---- Messages to send to the webworker ---- const InputInitialiseSchema = z.object({ type: z.literal("initialise"), config: ConfigSchema, }).strict(); const InputContentTokenSchema = z.object({ type: z.literal("content-token-request"), videoId: z.string(), requestId: z.string().uuid(), }).strict(); export type InputInitialise = z.infer; export type InputContentToken = z.infer; const InputMessageSchema = z.union([ InputInitialiseSchema, InputContentTokenSchema, ]); export type InputMessage = z.infer; // ---- Messages that the webworker sends to the parent ---- const OutputReadySchema = z.object({ type: z.literal("ready"), }).strict(); const OutputInitialiseSchema = z.object({ type: z.literal("initialised"), sessionPoToken: z.string(), visitorData: z.string(), }).strict(); const OutputContentTokenSchema = z.object({ type: z.literal("content-token"), contentToken: z.string(), requestId: InputContentTokenSchema.shape.requestId, }).strict(); const OutputErrorSchema = z.object({ type: z.literal("error"), error: z.any(), }).strict(); export const OutputMessageSchema = z.union([ OutputReadySchema, OutputInitialiseSchema, OutputContentTokenSchema, OutputErrorSchema, ]); type OutputMessage = z.infer; const IntegrityTokenResponse = z.tuple([z.string()]).rest(z.any()); const isWorker = typeof WorkerGlobalScope !== "undefined" && self instanceof WorkerGlobalScope; if (isWorker) { // helper function to force type-checking const untypedPostmessage = self.postMessage.bind(self); const postMessage = (message: OutputMessage) => { untypedPostmessage(message); }; let minter: BG.WebPoMinter; onmessage = async (event) => { const message = InputMessageSchema.parse(event.data); if (message.type === "initialise") { const fetchImpl: typeof fetch = await getFetchClient( message.config, ); try { const { sessionPoToken, visitorData, generatedMinter, } = await setup({ fetchImpl, innertubeClientCookies: message.config.youtube_session.cookies, }); minter = generatedMinter; postMessage({ type: "initialised", sessionPoToken, visitorData, }); } catch (err) { postMessage({ type: "error", error: err }); } } // this is called every time a video needs a content token if (message.type === "content-token-request") { if (!minter) { throw new Error( "Minter not yet ready, must initialise first", ); } const contentToken = await minter.mintAsWebsafeString( message.videoId, ); postMessage({ type: "content-token", contentToken, requestId: message.requestId, }); } }; postMessage({ type: "ready" }); } async function setup( { fetchImpl, innertubeClientCookies }: { fetchImpl: FetchFunction; innertubeClientCookies: string; }, ) { const innertubeClient = await Innertube.create({ enable_session_cache: false, fetch: fetchImpl, user_agent: USER_AGENT, retrieve_player: false, cookie: innertubeClientCookies || undefined, player_id: PLAYER_ID, }); const visitorData = innertubeClient.session.context.client.visitorData; if (!visitorData) { throw new Error("Could not get visitor data"); } const dom = new JSDOM( '', { url: "https://www.youtube.com/", referrer: "https://www.youtube.com/", userAgent: USER_AGENT, }, ); Object.assign(globalThis, { window: dom.window, document: dom.window.document, // location: dom.window.location, // --- doesn't seem to be necessary and the Web Worker doesn't like it origin: dom.window.origin, }); if (!Reflect.has(globalThis, "navigator")) { Object.defineProperty(globalThis, "navigator", { value: dom.window.navigator, }); } const challengeResponse = await innertubeClient.getAttestationChallenge( "ENGAGEMENT_TYPE_UNBOUND", ); if (!challengeResponse.bg_challenge) { throw new Error("Could not get challenge"); } // Mock HTMLCanvasElement.prototype.getContext to silence "Not implemented" error // and prevent unnecessary noise in logs. if (dom.window.HTMLCanvasElement) { dom.window.HTMLCanvasElement.prototype.getContext = (( _contextId: string, _options?: any, ) => { return new Proxy({}, { get: (_target, _prop) => { return () => { }; }, }); }) as any; dom.window.HTMLCanvasElement.prototype.toDataURL = () => ""; } const interpreterUrl = challengeResponse.bg_challenge.interpreter_url .private_do_not_access_or_else_trusted_resource_url_wrapped_value; const bgScriptResponse = await fetchImpl( `https:${interpreterUrl}`, ); const interpreterJavascript = await bgScriptResponse.text(); if (interpreterJavascript) { new Function(interpreterJavascript)(); } else throw new Error("Could not load VM"); const botguard = await BG.BotGuardClient.create({ program: challengeResponse.bg_challenge.program, globalName: challengeResponse.bg_challenge.global_name, globalObj: globalThis, }); const webPoSignalOutput: WebPoSignalOutput = []; const botguardResponse = await botguard.snapshot({ webPoSignalOutput }); const requestKey = "O43z0dpjhgX20SCx4KAo"; const integrityTokenResponse = await fetchImpl( buildURL("GenerateIT", true), { method: "POST", headers: { "content-type": "application/json+protobuf", "x-goog-api-key": GOOG_API_KEY, "x-user-agent": "grpc-web-javascript/0.1", "user-agent": USER_AGENT, }, body: JSON.stringify([requestKey, botguardResponse]), }, ); const integrityTokenBody = IntegrityTokenResponse.parse( await integrityTokenResponse.json(), ); const integrityTokenBasedMinter = await BG.WebPoMinter.create({ integrityToken: integrityTokenBody[0], }, webPoSignalOutput); const sessionPoToken = await integrityTokenBasedMinter.mintAsWebsafeString( visitorData, ); return { sessionPoToken, visitorData, generatedMinter: integrityTokenBasedMinter, }; }