
Research
/Security News
GlassWASM: WebAssembly Malware Found in Trojanized Open VSX Extensions
The trojanized extensions use TinyGo-compiled WebAssembly and Solana transaction memos to resolve command-and-control infrastructure.
tls-client-node
Advanced tools
Node.js client for bogdanfinn/tls-client with native shared-library loading and optional managed runtime support.
Managed-first Node.js wrapper for browser-like TLS profiles.
Explicit lifecycle, upstream-aligned payloads, and published package distribution without singleton-style API state.
Browser-like TLS profiles are not just about the user-agent header. Servers inspect the full handshake,
HTTP/2 behavior, and related transport traits. tls-client-node gives Node.js a cleaner wrapper around that
upstream capability while keeping the lifecycle explicit instead of hiding it behind singleton state.
tls-client-node is a source-available Node.js client for bogdanfinn/tls-client. It uses managed tls-client-api mode by default for predictable async concurrency, keeps lifecycle control explicit through TLSClient and Session, and can also run through direct shared-library loading when native mode is explicitly selected.
| Focus | What you get |
|---|---|
| Managed-first local runtime | Uses the tls-client-api sidecar process by default for more predictable concurrent async behavior. |
| Explicit lifecycle | TLSClient and Session keep ownership obvious, instead of hiding everything behind global init and destroy calls. |
| Upstream alignment | Custom TLS payloads and profile identifiers are kept close to Bogdan Finn's tls-client contract. |
| Migration practicality | Common node-tls-client aliases such as ja3string, timeout, hostOverride, and randomTlsExtensionOrder are supported. |
| Modern package surface | Published npm package with strict TypeScript types, named ESM imports, and CommonJS require support. |
require support.tls-client-api.runtimeMode: "native".npm install tls-client-node
# or
yarn add tls-client-node
# or
pnpm add tls-client-node
During postinstall, the package tries to download the matching upstream shared library for the current platform. If that step is skipped or fails, the required local asset is downloaded lazily on first startup.
Environment variables:
TLS_CLIENT_SKIP_DOWNLOAD=1 disables install-time downloads.TLS_CLIENT_VERSION=1.14.0 pins the upstream asset version.TLS_CLIENT_API_VERSION=1.14.0 is also recognized as an alias for TLS_CLIENT_VERSION.TLS_CLIENT_RUNTIME_SLOTS=128 caps how many managed tls-client-api runtimes can run at once on a host (default 128). The per-client runtimeSlots option overrides it. See Runtime Modes.ESM named imports work directly:
import {
ClientIdentifier,
Emulation,
MultipartForm,
TLSClient,
createMultipartForm,
} from "tls-client-node";
const client = new TLSClient();
const session = client.session({
clientIdentifier: Emulation.chrome_136,
});
CommonJS is supported too:
const { ClientIdentifier, Emulation, MultipartForm, TLSClient, createMultipartForm } = require("tls-client-node");
import {
ClientIdentifier,
TLSClient,
} from "tls-client-node";
async function main() {
const client = new TLSClient();
const session = client.session({
clientIdentifier: ClientIdentifier.chrome_136,
});
const response = await session.get("https://tls.peet.ws/api/all");
console.log(response.status, await response.text());
await session.close();
await client.stop();
}
main().catch(console.error);
import { ClientIdentifier, TLSClient } from "tls-client-node";
const client = new TLSClient();
const session = client.session({
clientIdentifier: ClientIdentifier.chrome_136,
timeoutSeconds: 30,
headers: {
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36",
accept: "*/*",
"accept-language": "en-US,en;q=0.9",
"accept-encoding": "gzip, deflate, br",
},
});
const response = await session.get("https://tls.peet.ws/api/all");
console.log(response.status, response.usedProtocol);
await session.close();
await client.stop();
import { ClientIdentifier, fetch } from "tls-client-node";
const response = await fetch("https://example.com", {
clientIdentifier: ClientIdentifier.chrome_136,
headers: {
accept: "text/html",
},
});
console.log(await response.text());
import { MultipartForm, TLSClient, createMultipartForm } from "tls-client-node";
const client = new TLSClient();
const form = createMultipartForm({
title: "example",
file: {
data: "hello world",
filename: "hello.txt",
contentType: "text/plain",
},
});
const builder = new MultipartForm()
.append("kind", "builder")
.appendJson("meta", { ok: true });
const response = await client.request("https://example.com/upload", {
method: "POST",
body: form,
});
console.log(response.status);
await client.request("https://example.com/upload-builder", {
method: "POST",
body: builder,
});
await client.stop();
import { TLSClient } from "tls-client-node";
const client = new TLSClient();
const session = client.session({
redirect: "follow",
});
await session.get("https://example.com/start", {
redirect: "manual",
});
await client.stop();
redirect is a higher-level alias for followRedirects.
redirect: "follow" maps to followRedirects: trueredirect: "manual" maps to followRedirects: falseredirect: true and redirect: false are also acceptedDefault local mode is managed tls-client-api.
Use native mode only when you explicitly want direct shared-library loading:
import { TLSClient } from "tls-client-node";
const client = new TLSClient({
runtimeMode: "native",
});
If you already host tls-client-api yourself, use remote mode:
import { TLSClient } from "tls-client-node";
const client = new TLSClient({
baseUrl: "http://127.0.0.1:8080",
apiKey: "my-auth-key-1",
});
In managed mode each started TLSClient claims an isolated runtime slot — its own runtime directory, lock file, and tls-client-api child process — so multiple clients (and multiple OS processes) never collide on the same files or ports. A single runtime serves many concurrent sessions and requests, so you normally need one slot per long-lived process, not one per request.
Slots live under the runtime cache, keyed by upstream version:
| Platform | Location |
|---|---|
| Linux | ${XDG_CACHE_HOME:-~/.cache}/tls-client-node/runtime/<version>/slot-N/ |
| macOS | ~/Library/Caches/tls-client-node/runtime/<version>/slot-N/ |
| Windows | single managed runtime under %LOCALAPPDATA%\tls-client-node\runtime (the slot pool applies only when you set runtimeDir) |
The slot ceiling is resolved as runtimeSlots option → TLS_CLIENT_RUNTIME_SLOTS env → default 128:
const client = new TLSClient({
runtimeSlots: 256, // overrides TLS_CLIENT_RUNTIME_SLOTS and the default
});
The cap is a ceiling, not a reservation: slots are spawned only as they are claimed, so low-concurrency apps pay nothing for a high cap. Raise it only if you genuinely run that many concurrent managed runtimes on one host; each slot is a full native process with its own memory, file handles, and port. Invalid values (non-integers, zero, or negatives) fall back to the next source in the chain.
Reuse one client instead of creating a new one per request — and always release the slot when done:
const client = new TLSClient();
try {
const session = client.session();
// ...many concurrent requests share this one runtime slot...
} finally {
await client.stop(); // releases the slot and stops the child process
}
The top-level
fetch()helper spins up an isolated temporary client (one slot) per call when you do not pass an explicitclientorsession. For hot paths, pass a shared client.
ERR_RUNTIME_SLOT_UNAVAILABLEUnable to allocate a tls-client runtime slot after checking N slots means every slot is taken. Common causes:
TLS_CLIENT_RUNTIME_SLOTS.SIGKILL, kill -9, abrupt container stop) before client.stop() ran, leaving a stale .lock behind. Stale locks are reclaimed automatically only when the recorded PID is no longer alive, so on busy hosts where PIDs get recycled a dead owner can keep a slot pinned.To clear stale slots when no runtimes should be active:
# Linux/macOS
pkill -f tls-client-api
rm -f ~/.cache/tls-client-node/runtime/*/slot-*/.lock # Linux
rm -f ~/Library/Caches/tls-client-node/runtime/*/slot-*/.lock # macOS
Calling await client.stop() (ideally in a finally, and on SIGINT/SIGTERM handlers) prevents leaks in the first place.
import { TLSClient } from "tls-client-node";
const client = new TLSClient();
const response = await client.request("https://example.com/", {
proxyUrl: "http://user:pass@proxy.example:5959",
followRedirects: true,
headers: {
"user-agent": "Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Mobile Safari/537.36",
accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"accept-language": "en-US,en;q=0.9",
"accept-encoding": "gzip, deflate, br",
},
customTlsClient: {
ja3String: "771,2570-4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,2570-0-23-65281-10-11-35-16-5-13-18-51-45-43-27-17513-2570-21,2570-29-23-24,0",
h2Settings: {
HEADER_TABLE_SIZE: 65536,
MAX_CONCURRENT_STREAMS: 1000,
INITIAL_WINDOW_SIZE: 6291456,
MAX_HEADER_LIST_SIZE: 262144,
},
h2SettingsOrder: [
"HEADER_TABLE_SIZE",
"MAX_CONCURRENT_STREAMS",
"INITIAL_WINDOW_SIZE",
"MAX_HEADER_LIST_SIZE",
],
supportedSignatureAlgorithms: [
"ECDSAWithP256AndSHA256",
"PSSWithSHA256",
"PKCS1WithSHA256",
"ECDSAWithP384AndSHA384",
"PSSWithSHA384",
"PKCS1WithSHA384",
"PSSWithSHA512",
"PKCS1WithSHA512",
],
supportedVersions: ["GREASE", "1.3", "1.2"],
keyShareCurves: ["GREASE", "X25519"],
certCompressionAlgos: ["brotli"],
pseudoHeaderOrder: [":method", ":authority", ":scheme", ":path"],
connectionFlow: 15663105,
headerOrder: ["accept", "user-agent", "accept-encoding", "accept-language"],
priorityFrames: [
{
streamID: 1,
priorityParam: {
streamDep: 1,
exclusive: true,
weight: 1,
},
},
],
headerPriority: {
streamDep: 1,
exclusive: true,
weight: 1,
},
alpnProtocols: ["h2", "http/1.1"],
alpsProtocols: ["h2"],
},
headerOrder: [":method", ":authority", ":scheme", ":path"],
});
TLSClient, create one or more Session instances, and stop the client when finished.Session keeps a tough-cookie jar in sync with request and response cookies. You can inspect URL cookies with session.cookies(url) or serialize the jar with session.exportCookies().Emulation is exported as a higher-level alias for ClientIdentifier, so Emulation.chrome_136 and ClientIdentifier.chrome_136 are equivalent.byteResponse: true is enabled, matching upstream behavior.FormData, MultipartForm, and createMultipartForm() can all be used for multipart uploads, with the generated boundary preserved in the content-type header.redirect is a higher-level alias over followRedirects; it improves call-site clarity without changing upstream redirect semantics.ClientIdentifier.tls: illegal parameter or unknown ClientHelloID: Custom-1 throw ERR_CUSTOM_TLS_REJECTED instead of falling back silently.certCompressionAlgo is provided, it is normalized to the upstream certCompressionAlgos field before the request is sent.new TLSClient() is the primary lifecycle model. The top-level fetch() helper uses an isolated temporary client when you do not pass an explicit client or session.This project is distributed under Apache License 2.0 with Commons Clause.
tls-client-node, without separate permission.NOTICE.This is source-available, not OSI open source.
This product includes software developed by Bogdan Finn and contributors via bogdanfinn/tls-client and bogdanfinn/tls-client-api.
FAQs
Node.js client for bogdanfinn/tls-client with native shared-library loading and optional managed runtime support.
We found that tls-client-node demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Research
/Security News
The trojanized extensions use TinyGo-compiled WebAssembly and Solana transaction memos to resolve command-and-control infrastructure.

Security News
Anthropic says the directive cited national security concerns over a narrow jailbreak, but offered no specific technical details.

Security News
A network of 152 Chrome live wallpaper extensions hid ad tracking and made extension-driven traffic look like Google search clicks.