slyspoof
v1.2.0
Published
HTTP client with fully controlled TLS fingerprints (JA3/JA4) for Node.js, Bun and Deno
Maintainers
Readme
slySpoof
HTTP client with fully controlled TLS fingerprints (JA3/JA4) for Node.js, Bun and Deno.
slyspoof mimics real browser TLS/HTTP2 fingerprints so detectors see a genuine
Chrome handshake instead of a Node runtime client. The core is a Zig shared
library built on libcurl-impersonate, loaded from JavaScript with bun:ffi.
Positioning: TLS fingerprint management + browser-consistent networking, not a magic "Cloudflare bypass". Headers, IP reputation and JS behaviour still matter.
Install
bun add slyspoofThe published package ships a prebuilt lib/ binary. Building from source is
only needed for other platforms or custom profiles.
Usage
import { rq, rqSync, create } from "slyspoof";
// async (runs in a worker, does not block the event loop)
const res = await rq("https://tls.peet.ws/api/all", { profile: "chrome120" });
// res: { status, ok, error, encoding: "utf8" | "base64", headers, body }
// blocking
const sync = rqSync("https://example.com", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ a: 1 }),
});
// small axios-like facade
const client = create({ profile: "chrome120" });
const page = await client.get("https://example.com");
// session keeps cookies between requests
const session = createSession({ profile: "chrome120" });
await session.fetch("https://example.com/login", { method: "POST", body: "user=x" });
const res = await session.fetch("https://example.com/me");
// route through a proxy
const viaProxy = await rq("https://example.com", { proxy: "http://127.0.0.1:8080" });axios-compatible API
import slySpoof, { create, slySpoofAdapter } from "slyspoof";
const res = await slySpoof.get("https://example.com", { params: { q: 1 } });
// res: { data, status, statusText, headers, config }
const client = create({ baseURL: "https://example.com" });
client.interceptors.request.use((config) => config);
client.interceptors.response.use((response) => response);
await client.post("/users", { data: { name: "x" } });
// drop-in axios migration (axios is an optional peer)
import axios from "axios";
const ax = axios.create({ adapter: slySpoofAdapter });
const page = await ax.get("https://example.com");Browser layer
solveChallenge drives a real Chrome to clear JS challenges and hands the
resulting cookies to the native client.
import { solveChallenge, createSession, LOCATIONS } from "slyspoof";
const solved = await solveChallenge({
url: "https://example.com",
backend: "auto", // "real" | "core" | "auto"
location: LOCATIONS.NETHERLANDS_AMSTERDAM,
waitForCookie: ["cf_clearance"],
verify: true, // adds a leak report
});
// solved: { backend, url, cookies, clearance, version, solved, afpApplied, leaks?, reason? }
const session = createSession();
await session.solve("https://example.com", { backend: "core" });
await session.fetch("https://example.com/api");Anti-fingerprinting patches (puppeteer-afp) are applied by default on both
backends. Disable with afp: false, or pass an AfpOptions object to reuse a
stable canvas/WebGL/font/audio fingerprint. If puppeteer-afp is not
installed, requests still work and afpApplied is false.
Pass verify: true to run an in-page leak check after emulation. The result
then includes leaks — a LeakReport with probed values (webdriver,
plugins, languages, timezone, userAgent, webglVendor,
webglRenderer, chrome) and a leaks array naming any exposed signals.
API
| Export | Description |
| --- | --- |
| rq(url, opts?) | async request, executed in a Worker pool |
| rqSync(url, opts?) | blocking request |
| create(defaults?) | axios-style client instance (request/get/post/…, interceptors, defaults) |
| slySpoof | default client instance |
| slySpoofAdapter | axios adapter for drop-in migration |
| SlySpoofError | error type (config, code, response) |
| createSession(opts?) | cookie session (fetch, cookies, close) |
| rqVersion | native library version string |
| libraryPath | resolved path to the native library |
| poolSize | number of worker threads used by rq() |
RqOptions: method, headers, body, profile, proxy, cookies.
RqResponse: status, ok, error, encoding, headers, setCookies,
body. Non-UTF8 bodies are returned base64-encoded.
Profiles
libcurl-impersonate 1.5.6: chrome99..120, edge99..101, ff91..117,
safari15_3, safari15_5. Default chrome120.
Build from source
Requires Zig and libcurl-impersonate installed system-wide.
bun install
bun run build # zig build + copies the artifact into lib/
bun testNotes
bun:ffistrings must be passed asBuffer(seecstr()insrc/rq.ts); passing a raw JS string ascstringthrows.rqSyncblocks the calling thread. Userq()to keep the event loop free.- Only TLS/HTTP2 fingerprints are imitated. Headers, IP and JS checks are not.
License
MIT
