reCAPTCHA in Bun lösen mit .env-Dateien und bun test

solve recaptcha in bun - How to Solve reCAPTCHA in Bun With .env Files and bun test

Um reCAPTCHA in Bun zu lösen, fügen Sie das CapSkip Node.js SDK mit bun add hinzu. Bauen Sie den Client aus Werten, die Bun bereits aus Ihrer .env-Datei geladen hat, rufen Sie dann den Solver auf und senden Sie den Token ab. CapSkip läuft auf Ihrem eigenen Windows-PC, daher gibt es keine Rechnung pro Lösung. Der Code ist kurz, und das SDK funktioniert unter Bun unverändert. Drei Gewohnheiten von Bun entsprechen nicht dem, was ein Node-Entwickler erwartet. Bun liest .env-Dateien, ohne dass man es verlangt, und nur aus dem Ordner, in dem Sie es ausführen. Ein Timeout auf einem hängenden Socket führt zwar zu einem abgelehnten Promise, beendet den Prozess aber nicht. Und bun test bricht jeden Test nach fünf Sekunden ab, was kürzer ist als ein echter Lösevorgang. Dieser Leitfaden behandelt jeden dieser Punkte.

Was Sie brauchen

  • CapSkip läuft auf einem Windows-PC, mit eingeschaltetem reCAPTCHA-Löser. Die Einrichtungsanleitung führt durch den ersten Start.
  • Bun für Windows. Alles unten lief unter Windows 11 mit Bun 1.4.3 und capskip 1.3.0 von npm. Ältere Bun-Versionen wurden nicht getestet.
  • Eine PowerShell-Eingabeaufforderung. Die Befehle verwenden PowerShell-Syntax.
  • Eine Seite mit einer reCAPTCHA-v2-Checkbox in einem Formular. Das Beispiel meldet sich an einem Formular im Django-Stil an, das einen CSRF-Token trägt. Passen Sie die Feldnamen an Ihre Website an.
  • Eine Adresse für den Solver. Im Local-Modus antwortet CapSkip auf 127.0.0.1 nur für dieses Gerät. Im Server-Modus lauscht es auf Ihrer Netzwerkadresse oder öffentlichen IP, damit ein Skript auf einem anderen Rechner es über die API aufrufen kann. Eingestellt werden beide Modi in den Verbindungseinstellungen.

Die Messungen unten stammen von einem Mock-CapSkip und einer Mock-Login-Seite (nachgebaute Testattrappen) auf demselben PC. Bun und das SDK sind die echten. Bun und das SDK sollten sich bei Ihrer eigenen Website genauso verhalten, es wurde aber nur ein Mock verwendet. Die Lösezeiten und die Regeln der Login-Seite zum Annehmen und Ablehnen sind die des Mocks.

Schritt 1: Bun installieren

Auf der Installationsseite von Bun steht der Befehl unten, dort ist auch npm install -g bun aufgeführt. Die Tests für diesen Leitfaden verwendeten Bun 1.4.3, per npm in einen Projektordner installiert. Der Befehl des Installers selbst wurde hier nicht ausgeführt. Öffnen Sie danach ein neues PowerShell-Fenster, damit PATH aktualisiert wird.

# PowerShell: Bun's official installer (or: npm install -g bun)
powershell -c "irm bun.sh/install.ps1|iex"

# Open a new PowerShell window so PATH is refreshed, then check:
bun --version

Schritt 2: Zuerst das Projekt anlegen

Führen Sie bun init vor bun add aus. Hat der Ordner keine package.json, sucht bun add weiter oben nach der nächsten und ändert stattdessen diese Datei. In einem Testordner innerhalb eines zu Testzwecken angelegten übergeordneten Ordners (decoy parent) ließ es die anderen Pakete des übergeordneten Ordners unverändert und fügte capskip diesem hinzu. Ihr eigener übergeordneter Ordner könnte ein Profilordner oder eine Monorepo-Wurzel sein. Legen Sie die Projektdatei zuerst an, dann landet die Installation dort, wo Sie sie erwarten.

# Make the folder, then create package.json BEFORE bun add.
mkdir captcha-bun
cd captcha-bun
bun init -y
bun add capskip

Der Befehl init schreibt eine strikte tsconfig.json, eine index.ts, eine README.md, eine .gitignore und eine package.json mit gesetztem Modultyp. Er installiert außerdem TypeScript und die Typdefinitionen von Bun. Der Befehl add speichert capskip als Version 1.3.0 oder neuer und schreibt eine Lockfile im Textformat namens bun.lock. Das SDK ist CommonJS. Sowohl der unten verwendete benannte Import als auch require funktionieren, anders als bei Deno, wo require scheitert.

Das Auslassen von init hat einen zweiten Preis. Bun löst einen Import auf, indem es die Ordner nach oben durchläuft. Ohne node_modules in Ihrem Projekt lud es auf dem Test-PC ein älteres capskip 1.0.2 aus einem node_modules in einem übergeordneten Ordner. Keine Fehlermeldung wies darauf hin.

Schritt 3: Die Einstellungen in eine .env-Datei legen

Legen Sie im Projektordner eine Datei namens .env an. Bun lädt sie, bevor Ihr Code läuft, daher stehen die Werte in Bun.env und process.env, und beide stimmen immer überein. Das SDK liest selbst keine Umgebungsvariable. Ihr Skript liest die drei Werte und übergibt sie, das ist der nächste Schritt. Die Zeile LOGIN_URL ist für den Live-Test weiter unten.

# .env in the folder you run bun from (bun init already git-ignores it)
CAPSKIP_HOST=127.0.0.1
CAPSKIP_PORT=8080
CAPSKIP_API_KEY=capskip
LOGIN_URL=https://example.com/login

Kennen Sie die Regeln, denn eine verirrte .env-Datei kann Sie überraschen:

  • Bun liest .env-Dateien nur aus dem Ordner, in dem Sie es ausführen. Wenn Sie ein Skript per Pfad aus einem anderen Ordner starten, wird nichts gelesen, und auch eine .env einen Ordner höher wird nicht gelesen.
  • Eine in der Shell gesetzte Variable gewinnt. Danach kommt .env.local, dann die Datei für den Modus, also .env.development unter bun run, sofern NODE_ENV nicht gesetzt ist, und schließlich die einfache .env.
  • Die .gitignore von bun init deckt .env und .env.local ab. Sie deckt .env.production, .env.test und .env.development nicht ab, tragen Sie diese daher ein, bevor Sie committen.
  • Anführungszeichen, ein Kommentar am Zeilenende und die Expansion ${OTHER} funktionieren in der Datei.

Schritt 4: Den CapSkip-Client bauen

Speichern Sie die nächsten Blöcke nacheinander in einer Datei namens solve.ts. Die Schritte 4 bis 9 gehören alle in diese eine Datei, und die ganze Datei steht gegen Ende. Mit den Standardwerten läuft das Skript ohne jede Einrichtung. CAPSKIP_API_KEY spielt nur eine Rolle, wenn Sie in CapSkip die Validierung des API-Schlüssels eingeschaltet haben.

import {
  ApiException,
  CapSkip,
  NetworkException,
  TimeoutException,
} from "capskip";

// Bun has already loaded .env into Bun.env. The SDK reads no
// environment variable itself, so pass the three values in.
const solver = new CapSkip({
  host: Bun.env.CAPSKIP_HOST ?? "127.0.0.1",
  port: Number(Bun.env.CAPSKIP_PORT ?? 8080),
  apiKey: Bun.env.CAPSKIP_API_KEY ?? "capskip",
});

Schritt 5: Cookies selbst verwalten

Das Login-Formular braucht das Cookie der Seite, die es angezeigt hat, und fetch in Bun behält keine Cookies. Lesen Sie jede Set-Cookie-Zeile mit getSetCookie aus. Der übliche Aufruf headers.get verbindet die Zeilen mit einem Komma, und das Komma im Expires-Datum bricht dann das Aufteilen.

// fetch keeps no cookies, so collect every Set-Cookie line.
// (headers.get("set-cookie") would glue them into one string.)
const jar = new Map<string, string>();

function keepCookies(response: Response): void {
  for (const line of response.headers.getSetCookie()) {
    const pair = line.split(";")[0] ?? "";
    const eq = pair.indexOf("=");
    jar.set(pair.slice(0, eq).trim(), pair.slice(eq + 1).trim());
  }
}

const cookies = () =>
  Array.from(jar, ([name, value]) => `${name}=${value}`).join("; ");

Schritt 6: Den sitekey und den CSRF-Token auslesen

Dieser Teil ist Ihrer, nicht der von CapSkip. Er öffnet die Login-Seite und zieht die zwei Werte heraus. Passen Sie die Muster an Ihr Markup an.

// Load the login page, keep its cookies, and read the sitekey and
// the CSRF token. Adjust the two patterns to your page's markup.
async function loadForm(pageUrl: string) {
  const page = await fetch(pageUrl);
  keepCookies(page);
  const html = await page.text();

  const sitekey = html.match(/data-sitekey="([^"]+)"/)?.[1];
  const csrf = html.match(/name="csrfmiddlewaretoken" value="([^"]+)"/)?.[1];
  if (!sitekey || !csrf) throw new Error("sitekey or CSRF token not found");
  return { sitekey, csrf };
}

Schritt 7: Ein Timeout einbauen, das funktioniert

Das SDK setzt kein Socket-Timeout. Ein CapSkip, das einen Aufruf annimmt und nie antwortet, lässt das Skript hängen. Im Test gab Bun nichts aus und beendete sich nie, es wartete auch nach 400 Sekunden noch. Die Timeout-Option des SDK wird nur zwischen zwei Abfragen geprüft, kann also nicht helfen, solange eine einzelne Anfrage feststeckt. Der Block unten lässt den Lösevorgang gegen einen Timer antreten.

// The SDK sets no socket timeout, so a CapSkip that accepts the call
// and never answers would hang the script. Race the solve against a
// timer. In Bun an AbortSignal.timeout does not keep the process alive.
function withTimeout<T>(work: Promise<T>, ms: number): Promise<T> {
  const signal = AbortSignal.timeout(ms);
  const limit = new Promise<never>((_, reject) => {
    signal.addEventListener("abort", () =>
      reject(new TimeoutException(`no answer from CapSkip in ${ms} ms`)),
    );
  });
  return Promise.race([work, limit]);
}

// The SDK gives up after 300 s by itself, so 330 s only fires when
// CapSkip goes silent. code is typed "string or object", hence String().
async function getToken(sitekey: string, pageUrl: string): Promise<string> {
  const solving = solver.recaptcha(sitekey, pageUrl);
  const { code } = await withTimeout(solving, 330_000);
  return String(code);
}

Bun unterscheidet sich hier von Deno, zu Ihren Gunsten. Ein Timer aus AbortSignal.timeout hält Bun nicht am Leben, sodass ein normaler Lösevorgang endet, sobald er fertig ist. Im Test endete ein Skript mit einem Zeitbudget von 330 Sekunden um einen Lösevorgang von 1,8 Sekunden nach etwa 1,8 Sekunden. Ein einfaches setTimeout, das Sie nie löschen, ist etwas anderes. Es hielt Bun am Leben, bis es auslöste: 20,04 Sekunden bei einem Zeitbudget von 20 Sekunden. Löschen Sie einen solchen Timer in einem finally-Block oder verwenden Sie das Signal.

Der Haken liegt in der anderen Richtung. Läuft das Zeitbudget bei einem hängenden Socket ab, wird das Promise sofort abgelehnt, in einem Test mit 3 Sekunden Zeitbudget nach 3,0 Sekunden. Danach bleibt Bun am Leben, weil der festhängende Socket es offen hält. Deshalb endet Schritt 9 mit process.exit. Dasselbe passiert, wenn das Rennen verloren ist, während CapSkip noch mit „nicht bereit“ antwortet. Das SDK pollt im Hintergrund weiter, und der Prozess lebt bis zum eigenen Timeout des SDK, das bei einem Limit von 12 bei 12,8 Sekunden lag.

Wählen Sie ein kleineres Zeitbudget, wenn Ihr Skript schneller scheitern soll. Die Tests ließen den Block mit 4 Sekunden laufen, mit einem CapSkip, das beim Übermitteln verstummte, und dann bei der Abfrage. Beide gaben die Meldung aus Schritt 9 aus und endeten nach etwa 4,04 Sekunden mit Exit-Code 1. Ein normaler Lösevorgang endete nach etwa 1,8 Sekunden mit Exit-Code 0.

Schritt 8: Lösen, absenden und das Ergebnis prüfen

Der nächste Block ist der Login. Ein reCAPTCHA-Token funktioniert nur einmal, daher löst jeder Login für sich. Zwei Details sind wichtig. Eine strenge Website wie Django über HTTPS lehnt einen Formular-POST ohne Referer ab, und fetch sendet keinen, daher setzt der Block ihn. Und fetch folgt standardmäßig einer 302 und verliert dabei das neue Cookie. Deshalb setzt der Block redirect auf manual und liest das Cookie aus der 302. Passen Sie die Feldnamen, den Benutzernamen und das Merkmal Welcome back an Ihre Website an.

// fetch sends no Referer, and a strict site rejects the post without
// one. redirect "manual" keeps the 302 so we can read its new cookie.
export async function login(pageUrl: string): Promise<boolean> {
  jar.clear();
  const { sitekey, csrf } = await loadForm(pageUrl);
  const token = await getToken(sitekey, pageUrl);

  const response = await fetch(pageUrl, {
    method: "POST",
    redirect: "manual",
    headers: { Cookie: cookies(), Referer: pageUrl },
    body: new URLSearchParams({
      csrfmiddlewaretoken: csrf,
      username: "YOUR_USERNAME",
      password: "YOUR_PASSWORD",
      "g-recaptcha-response": token,
    }),
  });
  keepCookies(response);
  if (response.status !== 302) {
    const body = await response.text();
    throw new Error(`login rejected: ${response.status} ${body}`);
  }

  // Swap "Welcome back" for a marker your own site shows after login.
  const next = new URL(response.headers.get("location") ?? "/", pageUrl);
  const home = await fetch(next, { headers: { Cookie: cookies() } });
  return (await home.text()).includes("Welcome back");
}

Schritt 9: Fehler melden und das Skript ausführen

Ein geschlossener Port, ein Fehler von CapSkip und ein langsamer Lösevorgang sind drei verschiedene Exception-Klassen, und instanceof unterscheidet sie unter Bun. Ein geschlossener Port löste in etwa 10 Millisekunden eine NetworkException mit der Meldung connect ECONNREFUSED 127.0.0.1 und dem Port aus. Der Hostname localhost war fast so schnell, ließ die Meldung aber leer, mit dem Fehlercode in der Ursache (cause). Der Block greift auf diesen Code zurück.

// A closed port, a bad reply and a slow solve are three different
// exception classes. A host NAME can leave the message empty, so fall
// back to the error code on .cause.
function explain(err: unknown): string {
  if (err instanceof NetworkException) {
    const code = (err.cause as { code?: string } | undefined)?.code;
    return `CapSkip is not reachable: ${err.message || code}`;
  }
  if (err instanceof ApiException) return `CapSkip replied: ${err.message}`;
  if (err instanceof TimeoutException) return `Too slow: ${err.message}`;
  return err instanceof Error ? (err.stack ?? err.message) : String(err);
}

// import.meta.main is true only when this file is run directly, so
// the tests can import login() without starting a login.
if (import.meta.main) {
  try {
    const ok = await login(Bun.argv[2] ?? "https://example.com/login");
    console.log(ok ? "logged in" : "NOT logged in");
    process.exit(ok ? 0 : 2);
  } catch (err) {
    console.error(explain(err));
    // A hung socket would keep Bun alive, so leave explicitly.
    process.exit(1);
  }
}

Ersetzen Sie die Beispiel-URL durch Ihre eigene Login-Seite und führen Sie das Skript im Projektordner aus. Dort übernimmt Bun die .env-Datei. Eine in PowerShell gesetzte Variable schlägt denselben Namen in der Datei und bleibt für den Rest dieser PowerShell-Sitzung gesetzt, daher löscht der Block sie wieder. Die letzten beiden Befehle schalten das automatische Laden aus oder lesen stattdessen eine Datei Ihrer Wahl. Legen Sie diese Datei zuerst an. Eine fehlende Datei wird ohne Meldung übersprungen, und die .env wird dann auch nicht gelesen.

# Run it. Bun reads .env from the folder you run this in.
bun run solve.ts https://example.com/login

# A variable set in PowerShell beats the same name in .env.
$env:CAPSKIP_API_KEY = "my-key"
bun run solve.ts https://example.com/login
Remove-Item Env:CAPSKIP_API_KEY

# Ignore .env completely, or read a different file instead. The second
# file must exist: a missing one is skipped and .env is not read either.
bun run --no-env-file solve.ts https://example.com/login
bun run --env-file=.env.office solve.ts https://example.com/login

Ein guter Lauf gibt logged in aus und endet mit Exit-Code 0. Gegen den Mock brauchte ein Lösevorgang, den der Mock eine Sekunde lang zurückhielt, 1,77 Sekunden. Das SDK pollt sofort, wartet dann 0,25; 0,5; 1; 2 und 4 Sekunden und danach jeweils 5 Sekunden. Ein kurzer Lösevorgang wird nicht pauschal auf fünf Sekunden aufgerundet.

Mit bun test testen

Bun bringt einen eigenen Test-Runner mit. Er führt Dateien aus, die wie solve.test.ts heißen, und sein Standardlimit liegt bei 5000 Millisekunden pro Test. Ein echter reCAPTCHA-Lösevorgang kann länger dauern. Ein Test, der auf den Mock mit 8 Sekunden wartete, scheiterte hiermit, und der Lauf endete mit Exit-Code 1:

  • (fail) logs in through CapSkip [5001.15ms]
  • this test timed out after 5000ms.

Heben Sie das Limit auf einem von drei Wegen an: mit dem dritten Argument von test, mit setDefaultTimeout wie unten oder mit dem Schalter timeout von bun test. Alle drei bestanden gegen den Mock mit 8 Sekunden. Ein Schlüssel timeout unter dem Abschnitt test der bunfig.toml funktionierte nicht. Der Test scheiterte weiterhin nach 5000 Millisekunden, und die bunfig-Dokumentation von Bun führt keinen solchen Schlüssel auf. Der Live-Test liest LOGIN_URL aus der .env-Datei, setzen Sie sie daher auf Ihre Login-Seite. Speichern Sie diesen als solve.test.ts.

import { expect, setDefaultTimeout, test } from "bun:test";
import { login } from "./solve";

// bun test stops every test after 5 s, and a real solve can take
// longer. Raise the limit for the whole file (or pass a number as the
// third argument of test(), or run: bun test --timeout 120000).
setDefaultTimeout(120_000);

test("logs in through CapSkip", async () => {
  const ok = await login(Bun.env.LOGIN_URL ?? "https://example.com/login");
  expect(ok).toBe(true);
});

Der Großteil Ihrer Suite sollte gar keinen Solver brauchen. Der zweite Test ersetzt das SDK durch einen Fake und läuft gegen eine kleine Stellvertreter-Website, daher besteht er bei geschlossenem CapSkip. Zwei Dinge macht man leicht falsch. Der Import von solve.ts muss dynamisch sein, weil ein statischer Import vor mock.module läuft. Und der Fake leckt: Das Mock-Modul bleibt für die anderen Testdateien in Kraft. Ein einfaches bun test ließ solve.test.ts mit login rejected: 403 scheitern, weil es nun den Fake-Solver verwendete. Speichern Sie diesen als fake.test.ts.

import { expect, mock, test } from "bun:test";

// Replace the SDK before solve.ts loads it. The import must be dynamic,
// because a static import runs before mock.module() does.
mock.module("capskip", () => ({
  CapSkip: class {
    async recaptcha() {
      return { captchaId: "1", code: "fake-token" };
    }
  },
  ApiException: class extends Error {},
  NetworkException: class extends Error {},
  TimeoutException: class extends Error {},
}));
const { login } = await import("./solve");

// A tiny stand-in login site: it only accepts the fake token.
const site = Bun.serve({
  port: 0,
  async fetch(req) {
    const url = new URL(req.url);
    if (req.method === "GET" && url.pathname === "/login") {
      return new Response(
        '<div data-sitekey="K"></div>' +
          '<input name="csrfmiddlewaretoken" value="C">',
      );
    }
    if (req.method === "POST") {
      const form = await req.formData();
      if (form.get("g-recaptcha-response") !== "fake-token")
        return new Response("bad token", { status: 403 });
      return new Response(null, {
        status: 302,
        headers: { Location: "/home" },
      });
    }
    return new Response("Welcome back");
  },
});

test("login posts the token CapSkip returned", async () => {
  expect(await login(`http://127.0.0.1:${site.port}/login`)).toBe(true);
  await site.stop();
});

Die Lösung ist der Schalter isolate, der jeder Datei ein frisches globales Objekt gibt. Damit bestanden beide Dateien. Bun setzt außerdem NODE_ENV während bun test auf test, lädt also .env.test und überspringt .env.local.

# Run every *.test.ts file. --isolate gives each file a fresh global
# object, so the fake SDK in fake.test.ts cannot leak into the others.
bun test --isolate

# The default limit is 5 s per test. Raise it for all tests (ms).
bun test --isolate --timeout 120000

Eine .exe-Datei bauen

Der Befehl compile packt die Laufzeitumgebung von Bun, Ihr Skript und das SDK in eine einzige Datei. Er brauchte hier 0,3 Sekunden und erzeugte eine Datei von 96,4 MB, der Größe von Bun selbst. Die exe lief mit einem PATH, in dem Bun nicht vorkam. Die .env-Datei ist nicht fest eingebaut. Die exe liest die .env des Ordners, aus dem sie gestartet wird, zur Laufzeit. Sie verwendet weder die aus dem Build-Ordner noch die neben der exe. Wird sie aus einem Ordner ohne .env gestartet, kommt der Port als undefined zurück, und die Standardwerte aus Schritt 4 greifen. Setzen Sie im Task Scheduler den Ordner Start in auf den mit Ihrer .env. Fügen Sie dem Build-Befehl den Schalter no-compile-autoload-dotenv hinzu, dann ignoriert die exe die Datei. Echte Variablen funktionieren weiterhin.

# One .exe with the Bun runtime and the SDK inside. Run it from the
# folder that holds your .env (Task Scheduler: set "Start in").
bun build --compile --outfile solve.exe solve.ts
.\solve.exe https://example.com/login

Die Typen bei Bedarf prüfen

Der Befehl bun run entfernt die Typen und prüft sie nie. Eine Datei mit einem Typfehler lief trotzdem und meldete sich an. Bun 1.4.3 hat einen Befehl bun check, der TypeScript über das Projekt laufen lässt, und beide Testdateien waren sauber. Drei Typisierungsdetails des SDK sind gut zu wissen. Das Feld code ist als String oder Objekt deklariert, weshalb Schritt 7 es in String einpackt. Das Feld token ist optional. Und der Optionstyp des Aufrufs recaptcha akzeptiert jeden Namen, sodass eine falsch geschriebene Option kompiliert. Eine falsch geschriebene Konstruktor-Option wie hots ist dagegen ein Fehler.

# bun run never type checks. Check on demand (TypeScript from bun init):
bun check

Das Skript gegen CapSkip auf einem anderen Rechner ausführen

Das Skript und der Solver müssen sich keinen PC teilen. Schalten Sie CapSkip in den Verbindungseinstellungen in den Server-Modus, dann lauscht es auf Ihrer Netzwerkadresse oder öffentlichen IP. Tragen Sie diese Adresse in die .env des Rechners ein, der das Skript ausführt. Für stabilen Fernzugriff wird eine statische öffentliche IP empfohlen. Der Solver bleibt Ihr eigener Windows-Rechner, und abgerechnet wird pro Lösung weiterhin nichts.

# .env on the PC that runs the script, with CapSkip in Server mode
# on another PC. Use that PC's address, and an API key if you set one.
CAPSKIP_HOST=192.168.1.50
CAPSKIP_PORT=8080
CAPSKIP_API_KEY=your-long-random-key

Beachten Sie zwei Dinge. Das SDK spricht mit CapSkip über reines HTTP, daher wandert der API-Schlüssel im Klartext. Halten Sie diese Verbindung in einem privaten Netzwerk oder einem VPN, und aktivieren Sie die Validierung des API-Schlüssels. Und CapSkip öffnet seinen Port beim Start des Server-Modus für jede Adresse. Um den Zugriff auf die Rechner zu beschränken, die Sie erwarten, fügen Sie in der Windows Firewall eine Blockierregel für alle anderen hinzu. Das Remote-Setup wurde für diesen Leitfaden nicht ausgeführt. Es wurde nur die .env-Datei eingelesen.

Die vollständige solve.ts

Das sind die Schritte 4 bis 9 in einer Datei, für einen schnellen Einstieg. Wenn Sie die Blöcke einzeln gespeichert haben, haben Sie sie bereits. Fügen Sie die Datei nicht zweimal ein, sonst meldet Bun, dass ein Name bereits deklariert ist.

import {
  ApiException,
  CapSkip,
  NetworkException,
  TimeoutException,
} from "capskip";

// Bun has already loaded .env into Bun.env. The SDK reads no
// environment variable itself, so pass the three values in.
const solver = new CapSkip({
  host: Bun.env.CAPSKIP_HOST ?? "127.0.0.1",
  port: Number(Bun.env.CAPSKIP_PORT ?? 8080),
  apiKey: Bun.env.CAPSKIP_API_KEY ?? "capskip",
});

// fetch keeps no cookies, so collect every Set-Cookie line.
// (headers.get("set-cookie") would glue them into one string.)
const jar = new Map<string, string>();

function keepCookies(response: Response): void {
  for (const line of response.headers.getSetCookie()) {
    const pair = line.split(";")[0] ?? "";
    const eq = pair.indexOf("=");
    jar.set(pair.slice(0, eq).trim(), pair.slice(eq + 1).trim());
  }
}

const cookies = () =>
  Array.from(jar, ([name, value]) => `${name}=${value}`).join("; ");

// Load the login page, keep its cookies, and read the sitekey and
// the CSRF token. Adjust the two patterns to your page's markup.
async function loadForm(pageUrl: string) {
  const page = await fetch(pageUrl);
  keepCookies(page);
  const html = await page.text();

  const sitekey = html.match(/data-sitekey="([^"]+)"/)?.[1];
  const csrf = html.match(/name="csrfmiddlewaretoken" value="([^"]+)"/)?.[1];
  if (!sitekey || !csrf) throw new Error("sitekey or CSRF token not found");
  return { sitekey, csrf };
}

// The SDK sets no socket timeout, so a CapSkip that accepts the call
// and never answers would hang the script. Race the solve against a
// timer. In Bun an AbortSignal.timeout does not keep the process alive.
function withTimeout<T>(work: Promise<T>, ms: number): Promise<T> {
  const signal = AbortSignal.timeout(ms);
  const limit = new Promise<never>((_, reject) => {
    signal.addEventListener("abort", () =>
      reject(new TimeoutException(`no answer from CapSkip in ${ms} ms`)),
    );
  });
  return Promise.race([work, limit]);
}

// The SDK gives up after 300 s by itself, so 330 s only fires when
// CapSkip goes silent. code is typed "string or object", hence String().
async function getToken(sitekey: string, pageUrl: string): Promise<string> {
  const solving = solver.recaptcha(sitekey, pageUrl);
  const { code } = await withTimeout(solving, 330_000);
  return String(code);
}

// fetch sends no Referer, and a strict site rejects the post without
// one. redirect "manual" keeps the 302 so we can read its new cookie.
export async function login(pageUrl: string): Promise<boolean> {
  jar.clear();
  const { sitekey, csrf } = await loadForm(pageUrl);
  const token = await getToken(sitekey, pageUrl);

  const response = await fetch(pageUrl, {
    method: "POST",
    redirect: "manual",
    headers: { Cookie: cookies(), Referer: pageUrl },
    body: new URLSearchParams({
      csrfmiddlewaretoken: csrf,
      username: "YOUR_USERNAME",
      password: "YOUR_PASSWORD",
      "g-recaptcha-response": token,
    }),
  });
  keepCookies(response);
  if (response.status !== 302) {
    const body = await response.text();
    throw new Error(`login rejected: ${response.status} ${body}`);
  }

  // Swap "Welcome back" for a marker your own site shows after login.
  const next = new URL(response.headers.get("location") ?? "/", pageUrl);
  const home = await fetch(next, { headers: { Cookie: cookies() } });
  return (await home.text()).includes("Welcome back");
}

// A closed port, a bad reply and a slow solve are three different
// exception classes. A host NAME can leave the message empty, so fall
// back to the error code on .cause.
function explain(err: unknown): string {
  if (err instanceof NetworkException) {
    const code = (err.cause as { code?: string } | undefined)?.code;
    return `CapSkip is not reachable: ${err.message || code}`;
  }
  if (err instanceof ApiException) return `CapSkip replied: ${err.message}`;
  if (err instanceof TimeoutException) return `Too slow: ${err.message}`;
  return err instanceof Error ? (err.stack ?? err.message) : String(err);
}

// import.meta.main is true only when this file is run directly, so
// the tests can import login() without starting a login.
if (import.meta.main) {
  try {
    const ok = await login(Bun.argv[2] ?? "https://example.com/login");
    console.log(ok ? "logged in" : "NOT logged in");
    process.exit(ok ? 0 : 2);
  } catch (err) {
    console.error(explain(err));
    // A hung socket would keep Bun alive, so leave explicitly.
    process.exit(1);
  }
}

Häufige Fehler beim Lösen von reCAPTCHA in Bun

Was Sie sehenUrsacheBeheben
this test timed out after 5000msbun test bricht jeden Test nach fünf Sekunden absetDefaultTimeout oder ein drittes Argument für test
login rejected: 403 nur in solve.test.tsmock.module in einer anderen Datei gilt weiterFühren Sie bun test mit dem Schalter isolate aus
NetworkException mit leerer MeldungDer Host ist ein Name wie localhost, daher steht der Fehler in der UrsacheGeben Sie err.cause.code aus oder verwenden Sie 127.0.0.1
NetworkException: connect ECONNREFUSED 127.0.0.1CapSkip läuft nicht, oder der Port ist falschStarten Sie CapSkip und prüfen Sie den Port in den Verbindungseinstellungen
Object is not a constructorEin Default-Import liefert das gesamte exports-ObjektImportieren Sie CapSkip per Name, in geschweiften Klammern
Das Skript hängt und gibt nichts ausCapSkip hat den Aufruf angenommen und ist verstummtVerwenden Sie das Rennen mit withTimeout und beenden Sie mit process.exit
Die falsche Version von capskip wird geladenKein node_modules im Projekt, daher fand Bun weiter oben eine ältere KopieFühren Sie bun init und bun add im Projektordner aus
Die Einstellungen kommen in der exe als undefined zurückDie exe liest die .env ihres ArbeitsordnersStarten Sie sie aus diesem Ordner oder setzen Sie Start in
fetch an 127.0.0.1 scheitert mit ECONNREFUSEDHTTP_PROXY zeigt auf einen Proxy, der nicht läuft, und fetch verwendet ihnSetzen Sie NO_PROXY auf 127.0.0.1,localhost
ApiException: ERROR_CAPTCHA_UNSOLVABLECapSkip konnte diesen Fall nicht lösenWiederholen Sie den Lösevorgang

FAQ

Funktioniert das CapSkip Node.js SDK in Bun?

Ja, mit Bun 1.4.3 und capskip 1.3.0. Der Import, ein vollständiger Lösevorgang, die vier Exception-Klassen und das Polling verhielten sich alle wie unter Node. Der Lösevorgang dauerte gegen denselben Mock in Bun 1.4.3 und in Node 25 jeweils etwa 1,8 Sekunden. Zur einfachen Node-Version dieses Setups siehe reCAPTCHA v2 in Node.js lösen.

Wie unterscheidet sich das von Deno?

Deno blockiert Netzwerk- und Dateizugriff, bis Sie ihn erlauben, und ein Timer aus AbortSignal hält Deno am Leben. Bun hat keine Berechtigungs-Flags, daher gibt es nichts zu erlauben. Die Fallen sind das automatische Laden der .env, der hängende Socket, der ein Timeout überlebt, und das Testlimit von fünf Sekunden. Der Deno-Leitfaden behandelt die Berechtigungsseite.

Kann ich mehrere Lösungen gleichzeitig laufen lassen?

Ja, mit Promise.all über mehrere Aufrufe des Solvers. Im Node-SDK ist AsyncCapSkip nur ein anderer Name für CapSkip und bringt gegenüber dem einfachen Client nichts. Beachten Sie das eigene Limit von CapSkip: Die reCAPTCHA-Einstellung Max. Threads steht standardmäßig auf 10, und zusätzliche Aufgaben warten auf einen freien Thread. Die Nebenläufigkeit wurde für diesen Leitfaden unter Bun nicht gemessen.

Kann ein Skript auf einer gehosteten Plattform mein CapSkip verwenden?

Eine gehostete Plattform läuft auf dem Rechner eines Fremden, kann also 127.0.0.1 auf Ihrem PC nicht erreichen. Betreiben Sie CapSkip im Server-Modus auf einem Windows-Rechner, den Sie kontrollieren, mit einer statischen öffentlichen IP. Tragen Sie diese Adresse in die .env oder in den Secret Store der Plattform ein. CapSkip erlaubt auf seinem Port bereits jede Adresse, fügen Sie daher eine Blockierregel für alle übrigen hinzu, und denken Sie daran, dass der Schlüssel im Klartext über reines HTTP wandert. Für diesen Leitfaden wurde keine gehostete Plattform getestet.

Die Kurzfassung

Um reCAPTCHA in Bun zu lösen, führen Sie bun init vor bun add aus. Halten Sie die CapSkip-Einstellungen in einer .env-Datei und übergeben Sie sie selbst an den Client. Lassen Sie jeden Lösevorgang gegen einen Timer antreten und beenden Sie das Skript mit process.exit, damit ein hängender Socket es nicht am Leben halten kann. Heben Sie das Testlimit von fünf Sekunden an und halten Sie neben dem Live-Test einen Test mit Fake-Solver bereit.

Ein Skript, das einige hundert Mal pro Nacht löst, summiert sich schnell. Wenn Sie es mithilfe unbegrenzter Captcha-Löser auf Ihrem eigenen Windows-Rechner laufen lassen, werden diese Lösungen nie zu einer Rechnung.