npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

sprntrl

v0.1.3

Published

Supernatural Node SDK — stealth browser-as-a-service client.

Readme

Supernatural Node SDK

Official Node.js / TypeScript client for the Supernatural stealth browser-as-a-service API.

Installation

npm install sprntrl
# Optional — pick whichever browser automation library you use:
npm install playwright
npm install puppeteer

Quick start

import { Sprntrl } from "sprntrl";
import type { Browser } from "playwright";

const client = new Sprntrl(); // reads SPRNTRL_API_KEY from env

const session = await client.sessions.create({ os: "macos", location: "America/New_York" });

// browserSession waits for the session, connects Playwright, and hands you
// a disposable handle. autoWhitelist registers your IP (CDP is IP-gated).
const handle = await client.sessions.browserSession(session.id, { autoWhitelist: true });
try {
  const browser = handle.browser as Browser;
  const page = await browser.contexts()[0].newPage();
  await page.goto("https://bot.sannysoft.com");
  await page.screenshot({ path: "out.png" });
} finally {
  await handle.close();
  await client.sessions.stop(session.id);
}

With await using (Node 24+ / TS 5.2+)

{
  await using handle = await client.sessions.browserSession(session.id, { autoWhitelist: true });
  const page = await handle.browser.contexts()[0].newPage();
  await page.goto("https://example.com");
} // browser auto-closes here

Puppeteer

const handle = await client.sessions.browserSession(session.id, {
  framework: "puppeteer",
  autoWhitelist: true,
});

Lower-level connect() and cdpUrl()

If you want to manage the browser lifecycle yourself:

const browser = await client.sessions.connect(session.id, { autoWhitelist: true });
// ...
await browser.close();

Or get the raw CDP WebSocket URL for any CDP client (chrome-remote-interface, raw ws, etc.):

const url = client.sessions.cdpUrl(session.id);
// wss://api.supernatural.sh/api/v1/sessions/<id>/cdp

Session options

sessions.create takes the full option set:

const session = await client.sessions.create({
  os: "macos",                  // "macos" | "windows"
  location: "America/New_York", // IANA timezone (see Locations below)
  label: "Kentucky, US",        // pin the pool match to a specific labeled slice (ignored for BYO proxies)
  persistent: true,             // keep the profile for later resume (default false)
  session_name: "my-profile",   // name for the persistent profile
  captcha_solver: true,         // auto-solves hCaptcha, Turnstile, reCAPTCHA and more (charged per solve)
  isolated_world: true,         // default true — keep on for stealth; set false only to access
                                // page-defined JS globals (main-world execution is detectable)
  headless: false,              // default TRUE for API/SDK callers; set false to get the live
                                // browser view in the dashboard
  block_images: true,           // default false; cuts bandwidth and speeds up loads
  proxy: "http://user:pass@host:8080", // string URL or { protocol, host, port, username, password };
                                       // HTTP / HTTPS / SOCKS5
});

Locations

const { options, accepts_iana } = await client.sessions.listLocations();
// options: [{ location: "America/New_York", label: "Kentucky, US" }, ...]

Pool-proxy sessions must pick a location (and optionally label) from options. accepts_iana: true means BYO-proxy sessions may instead pass any IANA timezone as location.

Persistent sessions (profiles)

const session = await client.sessions.create({
  os: "macos",
  location: "America/New_York",
  persistent: true,
  session_name: "my-profile",
});
// ... automate ...
await client.sessions.stop(session.id); // profile is kept

// Later — resume, optionally overriding any create-time option:
const resumed = await client.sessions.resume(session.id, { headless: false });

// Done with the profile entirely:
await client.sessions.deletePersistent(session.id);

Every resume option is an override; omitted fields keep their stored values. Changing os or location rebuilds the profile's pinned fingerprint (an intentional one-time identity drift); changing location on a pool session also re-assigns the pool proxy for the new region. Supplying proxy switches a pool session to BYO — switching BYO back to pool is not supported (delete and recreate instead).

Files

const files = await client.sessions.files.list(session.id);
const data = await client.sessions.files.download(session.id, "report.csv");
await client.sessions.files.upload(session.id, "input.json", JSON.stringify(payload));

Uploads are capped at 100 MB per request.

Extensions

Ephemeral sessions load extensions inline at create — each entry uses exactly one of webstoreUrl, crxUrl, or uploadB64:

await client.sessions.create({
  os: "macos",
  location: "America/New_York",
  extensions: [{ webstoreUrl: "https://chromewebstore.google.com/detail/..." }],
});

Persistent profiles manage extensions through the dedicated resource instead:

const ext = await client.sessions.extensions.add(session.id, { crxUrl: "https://example.com/my.crx" });
await client.sessions.extensions.list(session.id);
await client.sessions.extensions.setEnabled(session.id, ext.id, false);
await client.sessions.extensions.remove(session.id, ext.id);

Manifest V3 only (Chromium 148 dropped MV2). Max 16 extensions per profile, uploads capped at 50 MiB. Changes take effect at the next session start — stop and resume to apply.

Configuration

| Env var | Default | |---------------------|----------------------------| | SPRNTRL_API_KEY | — | | SPRNTRL_BASE_URL | https://api.supernatural.sh |

Or per client:

const client = new Sprntrl({
  apiKey: "sk_...",
  baseURL: "https://api.supernatural.sh",
  timeout: 30_000,
  maxRetries: 2,
});

Resources

  • client.sessionscreate, list, listActive, listHistory, listResumable, listPersistent, listLocations, get, stop, resume, deletePersistent, waitUntilReady, connect, browserSession, cdpUrl
  • client.sessions.fileslist, download, upload
  • client.sessions.extensionsadd, list, setEnabled, remove
  • client.profilescreate, list, get, update, duplicate, delete
  • client.templates.list()
  • client.ipWhitelistlist, add, remove
  • client.usagecurrent, history
  • client.userme, update, updateSettings, changePassword
  • client.apiKeyslist, create (full key returned ONCE), revoke

Error handling

import { Sprntrl, APIError, RateLimitError, AuthenticationError } from "sprntrl";

try {
  await client.sessions.create({ os: "macos", location: "America/New_York" });
} catch (err) {
  if (err instanceof RateLimitError) console.log("rate limited", err.status);
  else if (err instanceof AuthenticationError) console.log("bad API key");
  else if (err instanceof APIError) console.log("api error", err.status, err.body);
  else throw err;
}

Transient errors (5xx, 429, 408, connection errors) are retried automatically up to maxRetries with exponential backoff.

Gotchas

  • CDP access is IP-whitelist gated. The WebSocket at /api/v1/sessions/:id/cdp does not accept bearer auth — instead your public IP (as Cloudflare sees it) must be in your account's whitelist. Use client.ipWhitelist.add("current") or pass { autoWhitelist: true } to sessions.connect.
  • Sessions start async. sessions.create returns immediately with status: "creating". Call sessions.waitUntilReady(id) before connecting, or just use sessions.connect() which waits for you.
  • API key is shown only once. apiKeys.create() returns the full key field exactly once — store it immediately.

License

MIT