Skip to content

Fix/challenge - #217

Merged
bartholomej merged 6 commits into
masterfrom
fix/challenge
Aug 3, 2026
Merged

Fix/challenge#217
bartholomej merged 6 commits into
masterfrom
fix/challenge

Conversation

@bartholomej

@bartholomej bartholomej commented Aug 3, 2026

Copy link
Copy Markdown
Owner

Description

Resolve challenge

Type of change

  • Bug fix
  • New feature
  • Refactoring (no functional changes)
  • Code style update
  • Build / CI related changes
  • Documentation update
  • Tests
  • Other

Summary by CodeRabbit

  • New Features

    • Added automatic handling of Anubis verification challenges, including proof-of-work and cookie management.
    • Added typed request errors for blocked, missing, network, and HTTP failures.
    • Added public controls for managing verification cookies.
    • Improved challenge retries and compatibility across runtime cookie environments.
  • Bug Fixes

    • 404 responses now report structured, actionable errors instead of placeholder content.
  • Chores

    • Updated Yarn to version 4.18.0.

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds Anubis challenge detection and solving with portable SHA-256 proof-of-work support. Integrates cookie handling and retries into page fetching. Adds typed request errors, public exports, Yarn updates, and comprehensive tests.

Changes

Anubis integration

Layer / File(s) Summary
Portable hashing and proof-of-work
src/anubis/sha256.ts, src/anubis/proof-of-work.ts
Adds a portable SHA-256 implementation and an asynchronous proof-of-work solver with time limits and event-loop yielding.
Challenge parsing and exchange
src/anubis/challenge.ts, tests/anubis.test.ts
Detects and parses Anubis challenges, handles proof-of-work and metarefresh flows, exchanges challenge passes, validates cookies and redirects, and tests runtime-specific behavior.
Client, fetch, and error integration
src/anubis/client.ts, src/fetchers/index.ts, src/errors.ts, src/anubis/index.ts, src/index.ts, tests/fetchers.test.ts, .yarnrc.yml, package.json
Adds shared Anubis client state, typed CSFD errors, challenge retries, cookie helpers, public exports, updated fetcher assertions, and Yarn configuration updates.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant fetchPage
  participant passChallenge
  participant AnubisEndpoint
  fetchPage->>passChallenge: Detect challenge and pass request data
  passChallenge->>AnubisEndpoint: Submit proof-of-work or metarefresh exchange
  AnubisEndpoint-->>passChallenge: Return authentication cookie or redirect
  passChallenge-->>fetchPage: Return challenge result
  fetchPage->>AnubisEndpoint: Retry page request with authentication
Loading

Possibly related PRs

Suggested labels: enhancement

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description includes a brief summary and change type but omits the Related Issues section and the entire Checklist section. Add the Related Issues and Checklist sections, and confirm self-review, warnings, and test coverage.
Title check ❓ Inconclusive The title identifies a challenge fix but is too vague to describe the Anubis challenge resolution implemented by the pull request. Use a specific title, such as "Implement Anubis challenge resolution and fetch error handling."
✅ Passed checks (3 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/challenge

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

package.json

Oops! Something went wrong! :(

ESLint: 10.6.0

TypeError [ERR_IMPORT_ATTRIBUTE_MISSING]: Module "file:///.eslintrc.json?mtime=1785785496802" needs an import attribute of "type: json"
at validateAttributes (node:internal/modules/esm/assert:88:15)
at defaultLoadSync (node:internal/modules/esm/load:164:3)
at #loadAndMaybeBlockOnLoaderThread (node:internal/modules/esm/loader:776:12)
at #loadSync (node:internal/modules/esm/loader:796:49)
at ModuleLoader.load (node:internal/modules/esm/loader:762:26)
at ModuleLoader.loadAndTranslate (node:internal/modules/esm/loader:504:31)
at #getOrCreateModuleJobAfterResolve (node:internal/modules/esm/loader:555:36)
at afterResolve (node:internal/modules/esm/loader:603:52)
at ModuleLoader.getOrCreateModuleJob (node:internal/modules/esm/loader:609:12)
at node:internal/modules/esm/loader:628:32

src/anubis/challenge.ts

Oops! Something went wrong! :(

ESLint: 10.6.0

TypeError [ERR_IMPORT_ATTRIBUTE_MISSING]: Module "file:///.eslintrc.json?mtime=1785785496802" needs an import attribute of "type: json"
at validateAttributes (node:internal/modules/esm/assert:88:15)
at defaultLoadSync (node:internal/modules/esm/load:164:3)
at #loadAndMaybeBlockOnLoaderThread (node:internal/modules/esm/loader:776:12)
at #loadSync (node:internal/modules/esm/loader:796:49)
at ModuleLoader.load (node:internal/modules/esm/loader:762:26)
at ModuleLoader.loadAndTranslate (node:internal/modules/esm/loader:504:31)
at #getOrCreateModuleJobAfterResolve (node:internal/modules/esm/loader:555:36)
at afterResolve (node:internal/modules/esm/loader:603:52)
at ModuleLoader.getOrCreateModuleJob (node:internal/modules/esm/loader:609:12)
at node:internal/modules/esm/loader:628:32

src/anubis/client.ts

Oops! Something went wrong! :(

ESLint: 10.6.0

TypeError [ERR_IMPORT_ATTRIBUTE_MISSING]: Module "file:///.eslintrc.json?mtime=1785785496802" needs an import attribute of "type: json"
at validateAttributes (node:internal/modules/esm/assert:88:15)
at defaultLoadSync (node:internal/modules/esm/load:164:3)
at #loadAndMaybeBlockOnLoaderThread (node:internal/modules/esm/loader:776:12)
at #loadSync (node:internal/modules/esm/loader:796:49)
at ModuleLoader.load (node:internal/modules/esm/loader:762:26)
at ModuleLoader.loadAndTranslate (node:internal/modules/esm/loader:504:31)
at #getOrCreateModuleJobAfterResolve (node:internal/modules/esm/loader:555:36)
at afterResolve (node:internal/modules/esm/loader:603:52)
at ModuleLoader.getOrCreateModuleJob (node:internal/modules/esm/loader:609:12)
at node:internal/modules/esm/loader:628:32

  • 8 others

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov-commenter

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.79715% with 9 lines in your changes missing coverage. Please review.
✅ Project coverage is 98.46%. Comparing base (8fe98f8) to head (b19cb8e).

Files with missing lines Patch % Lines
src/anubis/challenge.ts 96.07% 4 Missing ⚠️
src/fetchers/index.ts 91.89% 3 Missing ⚠️
src/anubis/proof-of-work.ts 92.59% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #217      +/-   ##
==========================================
- Coverage   98.71%   98.46%   -0.26%     
==========================================
  Files          34       39       +5     
  Lines         781     1044     +263     
  Branches      202      244      +42     
==========================================
+ Hits          771     1028     +257     
- Misses         10       16       +6     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (2)
src/anubis/sha256.ts (1)

23-25: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Hoist the TextEncoder instance out of sha256.

sha256 is called once per nonce in the proof-of-work loop. Difficulty 4 needs roughly 65k calls, and difficulty 5 far more. Each call allocates a new TextEncoder. Create one module-level encoder and reuse it.

♻️ Proposed refactor
 const HEX = '0123456789abcdef';
+
+// Reused across calls: the PoW loop hashes tens of thousands of strings.
+const encoder = new TextEncoder();
 
 /** SHA-256 digest of `text` (UTF-8) as 32 raw bytes. */
 export const sha256 = (text: string): Uint8Array => {
-  const bytes = new TextEncoder().encode(text);
+  const bytes = encoder.encode(text);
   const length = bytes.length;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/anubis/sha256.ts` around lines 23 - 25, Hoist the TextEncoder allocation
out of sha256 by creating a module-level encoder, then reuse that instance when
encoding text inside sha256. Keep the hashing behavior unchanged while avoiding
per-call encoder creation in the proof-of-work loop.
src/anubis/challenge.ts (1)

145-170: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Restrict the metarefresh exchange URL to the challenge origin.

directive.url is read out of the response body or the Refresh header. new URL(directive.url, url) accepts an absolute URL to any host. The exchange request then sends requestHeaders and the techaro.lol-anubis-cookie-verification cookie to that host. Compare the resolved origin with the origin of url and refuse a cross-origin target.

🛡️ Proposed guard
   let passUrl: URL;
   if (directive) {
     passUrl = new URL(directive.url, url);
+    // The directive is page-supplied; never hand the verification cookie to
+    // another origin.
+    if (passUrl.origin !== new URL(url).origin) {
+      return null;
+    }
   } else {

Also applies to: 232-247

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/anubis/challenge.ts` around lines 145 - 170, Update metarefreshPassUrl to
validate directive.url after resolving it against url: compare the resulting
URL’s origin with the origin of url and return null for cross-origin targets
before waiting or returning the exchange URL. Preserve same-origin directive
handling and the existing fallback URL construction.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.yarnrc.yml:
- Around line 7-8: Update the npmMinimalAgeGate setting in .yarnrc.yml to use
the default or another non-zero delay, and add a brief comment explaining the
rationale if configuring it explicitly; do not leave it set to 0.

In `@package.json`:
- Line 129: Update the packageManager entry to reference a published
`@yarnpkg/cli-dist` release instead of the unavailable yarn@4.18.0 value, ensuring
Corepack can enable it during immutable installs.

In `@src/anubis/challenge.ts`:
- Around line 59-71: Validate the parsed result in parseChallenge before
returning it, requiring rules.algorithm and challenge.id/challenge.randomData to
be strings and rules.difficulty to be a number; return null for missing or
invalid fields. In src/anubis/proof-of-work.ts lines 33-41, ensure the solver
rejects difficulty values that are not non-negative integers before beginning
the search.
- Around line 84-88: Update hidesSetCookie and the passChallenge state handling
to distinguish runtimes that cannot expose Set-Cookie from responses where Node
explicitly exposes no cookies. Only set platformCookieJar when the runtime lacks
getSetCookie; preserve the credential-less fetch behavior when getSetCookie
exists but returns an empty array, including the later fetchPage flow.

In `@src/anubis/client.ts`:
- Around line 52-55: Update the client state around reset() and pass() so each
reset advances a generation counter, and each in-flight pass() captures its
generation before awaiting. Ignore stale results when the exchange resolves,
preventing older requests from restoring cookie or platformCookieJar after
reset(), while preserving normal current-generation behavior.

---

Nitpick comments:
In `@src/anubis/challenge.ts`:
- Around line 145-170: Update metarefreshPassUrl to validate directive.url after
resolving it against url: compare the resulting URL’s origin with the origin of
url and return null for cross-origin targets before waiting or returning the
exchange URL. Preserve same-origin directive handling and the existing fallback
URL construction.

In `@src/anubis/sha256.ts`:
- Around line 23-25: Hoist the TextEncoder allocation out of sha256 by creating
a module-level encoder, then reuse that instance when encoding text inside
sha256. Keep the hashing behavior unchanged while avoiding per-call encoder
creation in the proof-of-work loop.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e2e05dbb-8dbe-4b7d-95af-91e7fdeafb15

📥 Commits

Reviewing files that changed from the base of the PR and between 8fe98f8 and b19cb8e.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (12)
  • .yarnrc.yml
  • package.json
  • src/anubis/challenge.ts
  • src/anubis/client.ts
  • src/anubis/index.ts
  • src/anubis/proof-of-work.ts
  • src/anubis/sha256.ts
  • src/errors.ts
  • src/fetchers/index.ts
  • src/index.ts
  • tests/anubis.test.ts
  • tests/fetchers.test.ts

Comment thread .yarnrc.yml
Comment on lines +7 to +8

npmMinimalAgeGate: 0

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Yarn npmMinimalAgeGate configuration default value

💡 Result:

The current default value for the Yarn npmMinimalAgeGate configuration is 1d (one day) [1][2]. This setting, introduced in Yarn 4.12, restricts the installation of packages to those published to the npm registry at least the specified duration ago, serving as a security measure to mitigate the risk of installing newly published, potentially malicious packages [1][2]. While the default value has undergone adjustments—including a period where it was set to 3 days (4320m) [3]—it is currently established as 1d [1][2]. Users can override this behavior by setting npmMinimalAgeGate to 0 in their .yarnrc.yml file, or by using the --no-time-gate flag during specific installation commands [1][3][2]. Additionally, the npmPreapprovedPackages setting allows for specific exceptions to this rule [1][2].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Repository files matching .yarnrc.yml or yarnrc:"
fd -a '(^|/)yarnrc(\.yml|\.yaml)?$' . | sed 's#^\./##' || true

if [ -f .yarnrc.yml ]; then
  echo
  echo ".yarnrc.yml excerpt:"
  nl -ba .yarnrc.yml | sed -n '1,40p'
fi

echo
echo "Package manager declarations:"
if [ -f package.json ]; then
  node -e "const p=require('./package.json'); console.log(JSON.stringify({packageManager:p.packageManager,engines:p.engines},null,2))"
fi

echo
echo "Search for npmMinimalAgeGate / no-time-gate:"
rg -n "npmMinimalAgeGate|no-time-gate|time-gate|Minimal.*Age|AgeGate" . || true

Repository: bartholomej/node-csfd-api

Length of output: 275


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Repository files matching yarnrc:"
find . -type f \( -name '.yarnrc.yml' -o -name '.yarnrc.yaml' -o -name 'yarnrc.yml' -o -name 'yarnrc.yaml' \) -print

if [ -f .yarnrc.yml ]; then
  echo
  echo ".yarnrc.yml excerpt:"
  i=1
  while IFS= read -r line || [ -n "$line" ]; do
    printf '%5d  %s\n' "$i" "$line"
    [ "$i" -ge 80 ] && break
    i=$((i+1))
  done < .yarnrc.yml
fi

echo
echo "Package manager declarations:"
if [ -f package.json ]; then
  node -e "const p=require('./package.json'); console.log(JSON.stringify({packageManager:p.packageManager,engines:p.engines},null,2))"
fi

echo
echo "Search for npmMinimalAgeGate / no-time-gate:"
grep -RInE 'npmMinimalAgeGate|no-time-gate|time-gate|Minimal.*Age|AgeGate' . || true

Repository: bartholomej/node-csfd-api

Length of output: 584


Keep npmMinimalAgeGate above 0.

This project pins Yarn 4.18.0, where the default npm age-gate delay is active. Setting npmMinimalAgeGate: 0 removes that protection for every install path. Use the default or set a non-zero value with a short comment explaining the rationale.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.yarnrc.yml around lines 7 - 8, Update the npmMinimalAgeGate setting in
.yarnrc.yml to use the default or another non-zero delay, and add a brief
comment explaining the rationale if configuring it explicitly; do not leave it
set to 0.

Comment thread package.json
},
"sideEffects": false,
"packageManager": "yarn@4.14.1"
"packageManager": "yarn@4.18.0"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm the published Yarn version and check CI/workflow pins.
curl -s https://registry.npmjs.org/@yarnpkg/cli-dist | jq -r '.versions | keys | map(select(startswith("4."))) | .[-8:]'
fd -H -t f -e yml -e yaml . .github --exec sh -c 'echo "== $1"; rg -n "yarn|corepack|node-version" "$1" || true' _ {}
rg -n '"__metadata"|version:' yarn.lock | head -20

Repository: bartholomej/node-csfd-api

Length of output: 2033


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== packageManager =="
python3 - <<'PY'
import json
from pathlib import Path
p = json.loads(Path("package.json").read_text())
print(p.get("packageManager"))
PY

echo "== yarn.lock metadata/header candidates =="
rg -n '^[#] yarn.lock|^__metadata|^[^#].*:|  yarn_version:' yarn.lock | head -40

echo "== lock first 30 lines =="
sed -n '1,35p' yarn.lock

Repository: bartholomej/node-csfd-api

Length of output: 1926


🌐 Web query:

Yarn 4.18.0 @yarnpkg/cli-dist npm versions

💡 Result:

As of August 3, 2026, the version 4.18.0 of @yarnpkg/cli was published on GitHub on July 29, 2026 [1][2]. However, the corresponding npm package @yarnpkg/cli-dist does not currently list 4.18.0 as an available version [3][4]. The latest version published to the @yarnpkg/cli-dist npm registry is 4.17.0 [3][4]. While GitHub repositories and npm distribution packages for Yarn Berry (v4+) are linked, there can sometimes be a delay or specific build process requirements for the @yarnpkg/cli-dist package to be updated on npm after a release is tagged on GitHub [1]. Users are advised to check the official npm registry page for updates [3].

Citations:


Set packageManager to a published @yarnpkg/cli-dist version.

yarn@4.18.0 is not listed on npm, so Corepack will fail to enable this version during yarn install --immutable. Pin packageManager to an available @yarnpkg/cli-dist release or publish/update the artifact before use.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@package.json` at line 129, Update the packageManager entry to reference a
published `@yarnpkg/cli-dist` release instead of the unavailable yarn@4.18.0
value, ensuring Corepack can enable it during immutable installs.

Comment thread src/anubis/challenge.ts
Comment on lines +59 to +71
const parseChallenge = (html: string): ParsedChallenge | null => {
const match = html.match(
/<script id="anubis_challenge" type="application\/json">([\s\S]*?)<\/script>/
);
if (!match) {
return null;
}
try {
return JSON.parse(match[1].trim()) as ParsedChallenge;
} catch {
return null;
}
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Server-supplied challenge fields reach the solver without validation. parseChallenge casts arbitrary JSON to ParsedChallenge, so rules.algorithm, rules.difficulty, challenge.id, and challenge.randomData can be missing or of the wrong type. A missing rules throws a TypeError at src/anubis/challenge.ts line 218. A NaN or negative difficulty makes the solver return nonce 0 with a digest that satisfies nothing.

  • src/anubis/challenge.ts#L59-L71: check the four fields after JSON.parse and return null when any field has the wrong type.
  • src/anubis/proof-of-work.ts#L33-L41: reject a difficulty that is not a non-negative integer before the search starts.
📍 Affects 2 files
  • src/anubis/challenge.ts#L59-L71 (this comment)
  • src/anubis/proof-of-work.ts#L33-L41
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/anubis/challenge.ts` around lines 59 - 71, Validate the parsed result in
parseChallenge before returning it, requiring rules.algorithm and
challenge.id/challenge.randomData to be strings and rules.difficulty to be a
number; return null for missing or invalid fields. In
src/anubis/proof-of-work.ts lines 33-41, ensure the solver rejects difficulty
values that are not non-negative integers before beginning the search.

Comment thread src/anubis/challenge.ts
Comment on lines +84 to +88
// Set-Cookie is a forbidden response header outside Node, so seeing none on a
// response that certainly carried them means the runtime (browser, React
// Native) is hiding them and keeping the cookies in its own jar instead.
const hidesSetCookie = (headers: Headers): boolean =>
typeof headers.getSetCookie !== 'function' || headers.getSetCookie().length === 0;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

hidesSetCookie conflates a missing cookie with a hidden cookie.

On Node, getSetCookie exists. If an interstitial arrives without any Set-Cookie header, getSetCookie() returns an empty array and this function reports true. passChallenge then sets platformCookieJar = true, and the client keeps that state. Every later fetchPage call sends credentials: 'include', which removes the credential-less default described in src/fetchers/index.ts lines 119-122. Separate the capability check from the cookie-presence check.

♻️ Proposed change
-const hidesSetCookie = (headers: Headers): boolean =>
-  typeof headers.getSetCookie !== 'function' || headers.getSetCookie().length === 0;
+// A runtime without `getSetCookie` cannot expose Set-Cookie at all. A runtime
+// that has it but returned nothing simply got a response without cookies.
+const hidesSetCookie = (headers: Headers): boolean =>
+  typeof headers.getSetCookie !== 'function';

Also applies to: 191-209

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/anubis/challenge.ts` around lines 84 - 88, Update hidesSetCookie and the
passChallenge state handling to distinguish runtimes that cannot expose
Set-Cookie from responses where Node explicitly exposes no cookies. Only set
platformCookieJar when the runtime lacks getSetCookie; preserve the
credential-less fetch behavior when getSetCookie exists but returns an empty
array, including the later fetchPage flow.

Comment thread src/anubis/client.ts
Comment on lines +52 to +55
reset: () => {
cookie = null;
pending = null;
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

reset() does not stop an in-flight exchange from restoring the cookie.

reset() clears cookie and pending, but the promise created by an earlier pass() call keeps running. When it resolves, lines 76-77 write cookie and platformCookieJar again. A caller that resets after a block then keeps using the cookie it asked to discard. Track a generation counter and ignore results from an older generation.

♻️ Proposed change
   let cookie: string | null = null;
   let platformCookieJar = false;
   let pending: Promise<ChallengeResult | null> | null = null;
+  let generation = 0;
 
   return {
     isChallenge: isAnubisChallenge,
     getCookie: () => cookie,
     setCookie: (value) => {
       cookie = value;
     },
     reset: () => {
       cookie = null;
       pending = null;
+      generation++;
     },
     usesPlatformCookieJar: () => platformCookieJar,
 
     pass: async (html, headers, url, requestHeaders) => {
+      const startedAt = generation;
       if (!pending) {
       const result = await pending;
       if (!result) {
         return false;
       }
+      if (startedAt !== generation) {
+        return false;
+      }
       cookie = result.cookie;

Also applies to: 58-79

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/anubis/client.ts` around lines 52 - 55, Update the client state around
reset() and pass() so each reset advances a generation counter, and each
in-flight pass() captures its generation before awaiting. Ignore stale results
when the exchange resolves, preventing older requests from restoring cookie or
platformCookieJar after reset(), while preserving normal current-generation
behavior.

@bartholomej
bartholomej merged commit b19cb8e into master Aug 3, 2026
2 checks passed
@bartholomej
bartholomej deleted the fix/challenge branch August 3, 2026 19:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants