import { type ESTree, parse } from "meriyah"; import { traverse, VisitorOption } from "estraverse"; import { z } from "zod"; type Candidate = { key: number; prio: number }; const HOOK = `(() => { if (globalThis.__secretHookInstalled) return; globalThis.__secretHookInstalled = true; globalThis.__captures = []; globalThis.__validUntil = null; Object.defineProperty(Object.prototype, "secret", { configurable: true, set: function (v) { __captures.push(this); Object.defineProperty(this, "secret", { value: v, writable: true, configurable: true, enumerable: true, }); }, }); })(); `; const MOD_LOADER = ` const modEnv = {}; let currentlyImporting = null; function n(id) { if (modEnv[id]) { return modEnv[id]; } if (__webpack_modules__[id]) { modEnv[id] = {}; currentlyImporting = id __webpack_modules__[id]({id}, modEnv[id], n); console.error("imported", id) currentlyImporting = null; return modEnv[id]; } console.error(\`failed to import \${id} (during import of \${currentlyImporting})\`); return {}; } n.d = () => {}; `; const READOUT = ` globalThis.__captures.filter((c) => c.secret && c.version) `; const outputSchema = z .array(z.object({ secret: z.string().nonempty(), version: z.int().positive() })) .nonempty(); type SpotifySecrets = z.infer; type SpotifySecretDict = { [key: number]: number[] }; type SpotifySecretBytes = { version: number; secret: number[] }[]; function extractWebpackModules(playerJs: string): ESTree.ObjectExpression { const ast = parse(playerJs, { ranges: true }); let wpm = null; traverse(ast, { enter: (node) => { if (node.type === "VariableDeclaration") { const found = node.declarations.find( (d) => d.id.type === "Identifier" && d.id.name === "__webpack_modules__" ); if (found?.init && found.init.type === "ObjectExpression") { wpm = found.init; console.error("__webpack_modules__ found at", found.init.range); return VisitorOption.Break; } } }, }); if (!wpm) throw new Error("could not find __webpack_modules__"); return wpm; } function findOtpModule(playerJs: string, wpm: ESTree.ObjectExpression): Candidate[] { const searchPatterns = [ "Hash#digest()", ".validUntil", ".secrets", '"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/="', ]; const candidates: { key: number; prio: number }[] = []; wpm.properties.forEach((p) => { if ( p.type === "Property" && p.key.type === "Literal" && typeof p.key.value === "number" && (p.value.type === "ArrowFunctionExpression" || p.value.type === "FunctionExpression") && p.value.body ) { const body_js = playerJs.substring(p.value.body.start!, p.value.body.end!); const prio = searchPatterns.findIndex((x) => body_js.includes(x)); if (prio !== -1) { candidates.push({ key: p.key.value, prio }); } } return false; }); if (candidates.length === 0) throw new Error("could not find OTP module"); candidates.sort((a, b) => b.prio - a.prio); return candidates; } function getWpmString(playerJs: string, wpm: ESTree.ObjectExpression): string { return "const __webpack_modules__ = " + playerJs.substring(wpm.start!, wpm.end!); } function buildEvalScript(wpmStr: string, otpCandidates: Candidate[]): string { let otpCode = "\n\n"; otpCandidates.forEach((c) => { otpCode += `n(${c.key});\n`; }); return HOOK + MOD_LOADER + wpmStr + otpCode + READOUT; } function runEvalScript(evalScript: string): SpotifySecrets { const res = (() => eval(evalScript))(); const spotifySecrets = outputSchema.parse(res); spotifySecrets.sort((a, b) => a.version - b.version); return spotifySecrets; } function secretsToBytes(secrets: SpotifySecrets): SpotifySecretBytes { return secrets.map(({ version, secret }) => { return { version, secret: Array.from(secret).map((c) => c.charCodeAt(0)) }; }); } function secretsToDict(secrets: SpotifySecrets): SpotifySecretDict { return Object.fromEntries( secrets.map(({ version, secret }) => [ String(version), Array.from(secret).map((c) => c.charCodeAt(0)), ]) ); } const HTTP_OPTIONS = { headers: { "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36", }, }; function isOk(status: number) { if (status < 200 || status > 299) throw new Error(`HTTP status code ${status}`); } async function fetchPlayerJsUrl(): Promise { const htmlResp = await fetch("https://open.spotify.com/", HTTP_OPTIONS); isOk(htmlResp.status); const html = await htmlResp.text(); const rePlayerJs = /"(https:\/\/[^" ]+\/web-player\.[0-9a-f]+\.js)"/; const m = html.match(rePlayerJs); if (!m) throw new Error("Player JS URL not found"); const playerJsUrl = m[1]; return playerJsUrl; } async function fetchPlayerJs(playerJsUrl: string): Promise { const jsResp = await fetch(playerJsUrl, HTTP_OPTIONS); isOk(jsResp.status); const ct = jsResp.headers.get("content-type"); if (!ct?.match(/text\/javascript\b/)) throw new Error(`Invalid content type: ${ct}`); const playerJs = await jsResp.text(); return playerJs; } async function main() { let playerJs; if (Deno.args[0]) { playerJs = Deno.readTextFileSync(Deno.args[0]); } else { const playerJsUrl = await fetchPlayerJsUrl(); console.error("Player JS URL", playerJsUrl); let lastPlayerJsUrl = ""; try { lastPlayerJsUrl = Deno.readTextFileSync("tmp/playerUrl.txt").trim(); // deno-lint-ignore no-empty } catch {} if (playerJsUrl === lastPlayerJsUrl) { console.error("no player updates"); return; } playerJs = await fetchPlayerJs(playerJsUrl); Deno.writeTextFileSync("tmp/playerUrl.txt", playerJsUrl); } const wpm = extractWebpackModules(playerJs); const otpCandidates = findOtpModule(playerJs, wpm); const wpmString = getWpmString(playerJs, wpm); const evalScript = buildEvalScript(wpmString, otpCandidates); const spotifySecrets = runEvalScript(evalScript); const spotifySecretBytes = secretsToBytes(spotifySecrets); const spotifySecretDict = secretsToDict(spotifySecrets); console.log(spotifySecrets); Deno.writeTextFileSync( "secrets/secrets.json", JSON.stringify(spotifySecrets, null, 2) ); Deno.writeTextFileSync( "secrets/secretBytes.json", JSON.stringify(spotifySecretBytes) ); Deno.writeTextFileSync("secrets/secretDict.json", JSON.stringify(spotifySecretDict)); } if (import.meta.main) { main() }