{"version":3,"file":"challenge.cjs","names":["solveProofOfWork","DEFAULT_TIME_BUDGET_MS"],"sources":["../../src/anubis/challenge.ts"],"sourcesContent":["import { DEFAULT_TIME_BUDGET_MS, solveProofOfWork } from './proof-of-work';\n\n// Anubis (BotStopper by Techaro) is a proof-of-work anti-bot interstitial:\n// instead of the page it serves an HTML challenge that a browser solves in\n// JavaScript. This module replicates the protocol so a plain `fetch` can earn\n// the auth cookie. See: https://github.com/TecharoHQ/anubis\n\nconst AUTH_COOKIE_NAME = 'techaro.lol-anubis-auth';\nconst VERIFY_COOKIE_NAME = 'techaro.lol-anubis-cookie-verification';\nconst PASS_CHALLENGE_PATH = '/.within.website/x/cmd/anubis/api/pass-challenge';\n\n// Anubis picks a challenge method per request. The SHA-256 ones make the client\n// burn CPU; `metarefresh` instead makes it sit out a declared delay. Anything\n// else (e.g. `preact`) needs a real JS runtime and must fail immediately rather\n// than burn the whole time budget computing a hash nobody asked for.\nconst PROOF_OF_WORK_ALGORITHMS = ['fast', 'slow'];\nconst METAREFRESH_ALGORITHM = 'metarefresh';\n\n// Anubis states the wait in a `Refresh` header or its `<meta>` twin. This is\n// only the fallback for a page that omits both, where the exchange URL has to\n// be rebuilt from the challenge anyway.\nconst DEFAULT_METAREFRESH_DELAY_MS = 2000;\n\n// Structural markers, deliberately not the localised body text: the\n// interstitial is translated, so matching prose would both miss locales and\n// risk false positives on user-generated content that quotes it.\nconst CHALLENGE_MARKERS = ['id=\"anubis_challenge\"', '/.within.website/x/cmd/anubis/'];\n\nexport type FetchLike = (input: string, init?: RequestInit) => Promise<Response>;\n\ninterface ParsedChallenge {\n  rules: { algorithm: string; difficulty: number };\n  challenge: { id: string; randomData: string };\n}\n\nexport interface ChallengeResult {\n  /** Cookie to replay, or `null` when the runtime's own cookie jar holds it. */\n  cookie: string | null;\n  /** True when Set-Cookie was hidden and the runtime now owns the cookie. */\n  platformCookieJar: boolean;\n}\n\nexport interface PassChallengeParams {\n  /** Body of the interstitial page that was served instead of the content. */\n  html: string;\n  /** Response headers that came with it, carrying the verification cookie. */\n  headers: Headers;\n  /** The URL that was blocked; used as the redirect target after passing. */\n  url: string;\n  /** Headers to reuse, so the exchange looks like the original request. */\n  requestHeaders?: Headers;\n  fetch: FetchLike;\n  timeBudgetMs?: number;\n}\n\nexport const isAnubisChallenge = (html: string): boolean =>\n  CHALLENGE_MARKERS.some((marker) => html.includes(marker));\n\nconst parseChallenge = (html: string): ParsedChallenge | null => {\n  const match = html.match(\n    /<script id=\"anubis_challenge\" type=\"application\\/json\">([\\s\\S]*?)<\\/script>/\n  );\n  if (!match) {\n    return null;\n  }\n  try {\n    return JSON.parse(match[1].trim()) as ParsedChallenge;\n  } catch {\n    return null;\n  }\n};\n\nconst readCookie = (headers: Headers, name: string): string | null => {\n  if (typeof headers.getSetCookie !== 'function') {\n    return null;\n  }\n  const cookie = headers\n    .getSetCookie()\n    .map((entry) => entry.split(';', 1)[0])\n    .find((pair) => pair.startsWith(`${name}=`) && pair.length > name.length + 1);\n  return cookie ?? null;\n};\n\n// Set-Cookie is a forbidden response header outside Node, so seeing none on a\n// response that certainly carried them means the runtime (browser, React\n// Native) is hiding them and keeping the cookies in its own jar instead.\nconst hidesSetCookie = (headers: Headers): boolean =>\n  typeof headers.getSetCookie !== 'function' || headers.getSetCookie().length === 0;\n\nconst REFRESH_HEADER_DIRECTIVE = /^\\s*(\\d+)\\s*;\\s*url=(.+)$/i;\nconst REFRESH_META_DIRECTIVE =\n  /<meta[^>]+http-equiv=[\"']?refresh[\"']?[^>]*content=[\"'](\\d+)[^;]*;\\s*url=([^\"'>]+)/i;\n\ninterface RefreshDirective {\n  delayMs: number;\n  url: string;\n}\n\n/**\n * The `<delay>; url=<target>` directive Anubis serves with a metarefresh\n * challenge. It arrives as a `Refresh` header on some responses and as its\n * `<meta http-equiv>` equivalent on others, so both are read.\n */\nconst readRefreshDirective = (html: string, headers: Headers): RefreshDirective | null => {\n  const directive =\n    headers.get('refresh')?.match(REFRESH_HEADER_DIRECTIVE) ?? html.match(REFRESH_META_DIRECTIVE);\n  if (!directive) {\n    return null;\n  }\n  return {\n    delayMs: Number(directive[1]) * 1000,\n    // Inside a meta attribute the query separators arrive HTML-escaped.\n    url: directive[2].trim().replace(/&amp;/g, '&')\n  };\n};\n\nconst wait = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms));\n\nconst proofOfWorkPassUrl = async (\n  url: string,\n  { id, randomData }: ParsedChallenge['challenge'],\n  difficulty: number,\n  timeBudgetMs: number\n): Promise<string | null> => {\n  const startedAt = Date.now();\n  const solution = await solveProofOfWork(randomData, difficulty, timeBudgetMs);\n  if (!solution) {\n    return null;\n  }\n\n  const passUrl = new URL(PASS_CHALLENGE_PATH, url);\n  passUrl.searchParams.set('id', id);\n  passUrl.searchParams.set('response', solution.hash);\n  passUrl.searchParams.set('nonce', String(solution.nonce));\n  passUrl.searchParams.set('redir', url);\n  passUrl.searchParams.set('elapsedTime', String(Date.now() - startedAt));\n  return passUrl.toString();\n};\n\n/**\n * Metarefresh asks for patience rather than hashes: Anubis hands over the\n * exchange URL up front but answers it with 403 until the delay it declared has\n * actually elapsed, so the wait is the whole proof.\n */\nconst metarefreshPassUrl = async (\n  url: string,\n  { id, randomData }: ParsedChallenge['challenge'],\n  directive: RefreshDirective | null,\n  timeBudgetMs: number\n): Promise<string | null> => {\n  const delayMs = directive?.delayMs ?? DEFAULT_METAREFRESH_DELAY_MS;\n  // Waiting longer than the caller allowed is worse than not passing at all.\n  if (delayMs > timeBudgetMs) {\n    return null;\n  }\n\n  // Anubis' own URL is authoritative, so it is preferred over rebuilding one.\n  let passUrl: URL;\n  if (directive) {\n    passUrl = new URL(directive.url, url);\n  } else {\n    passUrl = new URL(PASS_CHALLENGE_PATH, url);\n    passUrl.searchParams.set('challenge', randomData);\n    passUrl.searchParams.set('id', id);\n    passUrl.searchParams.set('redir', url);\n  }\n\n  await wait(delayMs);\n  return passUrl.toString();\n};\n\n/**\n * Solve the challenge on an interstitial page and exchange it for an Anubis\n * auth cookie. Returns `null` if the challenge could not be passed.\n */\nexport const passChallenge = async ({\n  html: challengeHtml,\n  headers: challengeHeaders,\n  url,\n  requestHeaders,\n  fetch,\n  timeBudgetMs = DEFAULT_TIME_BUDGET_MS\n}: PassChallengeParams): Promise<ChallengeResult | null> => {\n  let html = challengeHtml;\n  let headers = challengeHeaders;\n  let platformCookieJar = false;\n\n  // On a cookie-jar runtime the first request was made without credentials, so\n  // the jar never stored Anubis' verification cookie. Ask for a fresh challenge\n  // with credentials enabled and let the jar keep it this time.\n  if (hidesSetCookie(headers)) {\n    platformCookieJar = true;\n    const reissued = await fetch(url, {\n      credentials: 'include',\n      redirect: 'manual',\n      headers: requestHeaders\n    });\n    const reissuedHtml = await reissued.text();\n\n    // Credentials change the answer: the jar may already hold a valid auth\n    // cookie, in which case this sails straight past Anubis. There is then no\n    // challenge left to solve — only a request worth retrying with the jar.\n    if (!isAnubisChallenge(reissuedHtml)) {\n      return { cookie: null, platformCookieJar };\n    }\n\n    html = reissuedHtml;\n    headers = reissued.headers;\n  }\n\n  const parsed = parseChallenge(html);\n  if (!parsed) {\n    return null;\n  }\n\n  const { challenge, rules } = parsed;\n  let passUrl: string | null = null;\n  if (PROOF_OF_WORK_ALGORITHMS.includes(rules.algorithm)) {\n    passUrl = await proofOfWorkPassUrl(url, challenge, rules.difficulty, timeBudgetMs);\n  } else if (rules.algorithm === METAREFRESH_ALGORITHM) {\n    passUrl = await metarefreshPassUrl(\n      url,\n      challenge,\n      readRefreshDirective(html, headers),\n      timeBudgetMs\n    );\n  }\n  if (!passUrl) {\n    return null;\n  }\n\n  // Anubis requires the verification cookie it set on the interstitial as proof\n  // that cookies work; on a jar runtime the runtime itself attaches it.\n  const passHeaders = new Headers(requestHeaders);\n  const verifyCookie = readCookie(headers, VERIFY_COOKIE_NAME);\n  if (verifyCookie) {\n    passHeaders.set('Cookie', verifyCookie);\n  }\n\n  // `redirect: 'manual'` stops fetch from following the 302 to `redir`, which\n  // would discard the Set-Cookie we need to read off this very response.\n  const response = await fetch(passUrl, {\n    method: 'GET',\n    credentials: platformCookieJar ? 'include' : 'omit',\n    redirect: 'manual',\n    headers: passHeaders\n  });\n\n  const authCookie = readCookie(response.headers, AUTH_COOKIE_NAME);\n  if (authCookie) {\n    return { cookie: authCookie, platformCookieJar };\n  }\n\n  // No cookie in hand. A runtime that lets us read Set-Cookie would have shown\n  // it, so this is a failed exchange; only a jar runtime can have passed while\n  // keeping the cookie to itself.\n  if (!platformCookieJar) {\n    return null;\n  }\n\n  // A 302 to `redir` is Anubis' success signal, and the jar has just stored the\n  // cookie off it. Browsers report the unfollowed redirect as `opaqueredirect`.\n  if (response.status === 302 || response.type === 'opaqueredirect') {\n    return { cookie: null, platformCookieJar };\n  }\n\n  // React Native ignores `redirect: 'manual'` and follows the 302 itself, so\n  // what we hold is the page we were after — proof enough, unless Anubis is\n  // still challenging us.\n  if (response.ok) {\n    const body = await response.text();\n    return body && !isAnubisChallenge(body) ? { cookie: null, platformCookieJar } : null;\n  }\n\n  return null;\n};\n"],"mappings":"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"}