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

@driftstack/sdk

v0.5.0

Published

Official TypeScript SDK for the Driftstack API

Readme

@driftstack/sdk

Official TypeScript SDK for the Driftstack API.

Status: pre-1.0. Stable surface for the API contract; the SDK API may shift before 1.0. While the SDK is 0.x, a minor version can change the surface and a patch never does.

Install

npm install @driftstack/sdk
# or
pnpm add @driftstack/sdk
# or
yarn add @driftstack/sdk

Requires Node.js ≥ 18 (uses native fetch). Works in any modern runtime that exposes fetch and node:crypto.

Both module systems work: import gives you the ESM build and require() gives you the CommonJS one, each with its own type declarations. The package carries its own copy of the API contract types, so there is nothing else to install — zod is the single runtime dependency, and it is shared rather than duplicated, so a ZodError the SDK throws is the same class your own zod catches.

Versions

Install normally. The line above writes a caret range (^0.2.0), and for a 0.x package npm resolves a caret to PATCH releases only — which is what you want, because while the SDK is 0.x a minor version can change the surface and a patch never does. Read the CHANGELOG before moving to a new minor. Pin an exact version only if you need a byte-for-byte reproducible build, and note that your lockfile already gives you one.

Quickstart

An agent session is an iPhone Safari browser in the cloud that an AI drives for you. Every agent session goes out through one of your saved proxies, so the create names its id (list them with client.egress.listProxies(), using a key with the account_owner scope).

import { randomUUID } from 'node:crypto';
import { Driftstack } from '@driftstack/sdk';

const client = new Driftstack({ apiKey: process.env.DRIFTSTACK_API_KEY! });

const agent = await client.agentSessions.create({ mode: 'ai', proxy_id: '<proxy id>' });
try {
  while ((await client.agentSessions.get(agent.id)).status === 'provisioning') {
    await new Promise((resolve) => setTimeout(resolve, 2_000));
  }
  const reply = await client.agentSessions.message(
    agent.id,
    'Open https://example.com and tell me the page title.',
    { idempotencyKey: randomUUID() },
  );
  console.log(reply.kind, reply.kind === 'plan-executed' ? reply.answer : undefined);
} finally {
  await client.agentSessions.close(agent.id);
}

Configuration

new Driftstack({
  apiKey: 'ds_live_…', // required
  baseUrl: 'https://api.driftstack.dev', // optional override
  timeoutMs: 30_000, // per-request timeout
  retry: {
    // retry policy (defaults shown)
    maxAttempts: 3,
    initialDelayMs: 200,
    maxDelayMs: 10_000,
  },
});

Resources

client.sessions.create(body?)
client.sessions.list(query?)
client.sessions.navigate(id, body)
client.sessions.interact(id, body)
client.sessions.wait(id, body)
client.sessions.getState(id)
client.sessions.capture(id, body)
client.sessions.extract(id, body)
client.sessions.search(id, body) // not available yet — 503 on every deployment, not in the API reference
client.sessions.login(id, body)  // not available yet — 503 on every deployment, not in the API reference
client.sessions.destroy(id)

client.egress.attachToSession(sessionId, config)  // retired — 410 on every deployment; pass proxy_id at create instead
client.egress.getSessionProxy(sessionId)          // not available yet — 404 on every deployment
client.egress.listProxies()             // saved reusable proxy library (metadata only)
client.egress.createProxy(body)         // secret fields are write-only (never echoed back)
client.egress.updateProxy(id, body)
client.egress.deleteProxy(id)
client.egress.testProxy(id)             // server-side reachability probe (SSRF-guarded)

client.agentSessions.create(body, opts?)   // start an AI session; body.proxy_id is required; opts: idempotencyKey, byokApiKey
client.agentSessions.get(id)
client.agentSessions.list(query?)       // cursor-paginated, newest first
client.agentSessions.iterate(opts?)
client.agentSessions.message(id, userMessage, opts?)  // send a task; returns plan-executed | clarify | refuse | stopped | logged-manual — branch on kind
client.agentSessions.getCapture(id, captureId)  // a screenshot the agent took: { contentType, bytes }
client.agentSessions.transcript(id, opts?)  // async generator: the conversation so far, then live; opts: lastEventId, signal, timeoutMs
client.agentSessions.stop(id)           // stop the running task; its message() returns kind 'stopped'
client.agentSessions.close(id)
client.agentSessions.livekitToken(id)   // subscriber token for the live view (LiveKitInfo); the create response carries one too
client.agentSessions.setEgress(id, proxyId, applyPoint?)  // not available yet — returns status 'unavailable'
client.agentSessions.resume(id, body?)  // resume after a resolved bot-challenge

client.profiles.create(body)
client.profiles.list(query?)
client.profiles.iterate(opts?)
client.profiles.get(id)
client.profiles.update(id, body)
client.profiles.delete(id)
client.profiles.clone(id, body?)        // auto-derives "(copy)" name when omitted

client.profileSnapshots.capture(profileId, body)            // immutable point-in-time copy
client.profileSnapshots.listForProfile(profileId, query?)
client.profileSnapshots.list(query?)                        // cross-account
client.profileSnapshots.iterate(opts?)
client.profileSnapshots.get(id)
client.profileSnapshots.restore(id, body)                   // creates a NEW profile
client.profileSnapshots.delete(id)

client.recipes.create(body)             // snapshot an agent-session's intent_log
client.recipes.list(query?)             // cursor-paginated, newest first
client.recipes.iterate(opts?)
client.recipes.get(id)                  // recipe + public intent_log; secret type values omitted
client.recipes.delete(id)               // recipe management only; no execute method

client.webhooks.create(body)            // requires admin scope
client.webhooks.list()
client.webhooks.get(id)
client.webhooks.delete(id)
client.webhooks.listDeliveries(id, query?)
client.webhooks.iterateDeliveries(id, opts?)
client.webhooks.replayDelivery(id)      // re-fire a failed/DLQ delivery

client.account.whoami()                 // the account, key, tier and scopes behind this key
client.account.rateLimits()             // per-bucket limits
client.account.getBundledLlmStatus()    // month-to-date Driftstack-provided AI spend

client.usage.current()

API keys, team members, billing and orders, sign-in, two-factor, email preferences and legal acceptance are managed in the Driftstack dashboard, not through the SDK. The API refuses the dashboard-only actions when they are asked for with an API key (DashboardOnlyError, a 403).

Every method is fully typed against the public OpenAPI 3.1 contract. Those types ship inside this package, so hover works in your editor with nothing else installed.

Errors

Every error thrown by the SDK extends DriftstackError. Catch the base class to handle anything, or specific subclasses for granular logic:

import {
  Driftstack,
  RateLimitError,
  ConcurrencyLimitError,
  ValidationError,
  DriftstackError,
} from '@driftstack/sdk';

try {
  const session = await client.sessions.create();
  // ...
} catch (err) {
  if (err instanceof RateLimitError) {
    console.warn(`Rate limited; retry after ${err.retryAfterSeconds}s`);
  } else if (err instanceof ConcurrencyLimitError) {
    console.warn(`At ${err.currentSessions}/${err.limit} concurrent sessions`);
  } else if (err instanceof ValidationError) {
    console.error('Validation failed:', err.issues);
  } else if (err instanceof DriftstackError) {
    console.error(`API error ${err.status}: ${err.title}`, err.detail);
  } else {
    throw err;
  }
}

Retry behaviour

By default the SDK retries automatically on:

  • network failures (TransportError)
  • 5xx server errors
  • 429 rate-limit responses (honours Retry-After header / retry_after_seconds body field)

It does not retry 4xx responses other than 429. Backoff is exponential with full jitter, capped at maxDelayMs. Set retry: { maxAttempts: 0 } to disable retries entirely.

Idempotency on retried writes. Because the SDK retries network failures and 5xx, and a request can reach the server, be processed, and then have its response lost (or a 500 fire after the write commits), an automatically-retried create or charge can execute twice. Pass an idempotencyKey on non-idempotent calls — e.g. client.agentSessions.create(body, { idempotencyKey }) — and the server collapses the retry onto the first request.

Agent browser turns need the same protection even though they stream: pass client.agentSessions.message(id, text, { idempotencyKey }) and reuse that key only for an ambiguous retry of the exact same session/message/approvals/BYOK request. A completed turn is replayed without executing its browser actions again; changed or still-running turns fail closed.

A turn is never retried automatically. A refusal raised before the turn did any work gives the key back, so the same key runs the turn once the cause is gone: a 409 turnInProgress, a 429, a 402, a 403 about the plan's AI or the model (requiresOwnKey), and a 502 whose keyRejected is false. So does a ConflictError whose idempotencyStatus is 'in_progress' — the first attempt is still being resolved. Every other answer is final for its key: a completed turn, a failure after the turn started, a rejected own key (keyRejected), a 500, a 'refuse' result, and the 409 for a closed or paused session. Those need a new key.

Run an AI task

import { randomUUID } from 'node:crypto';

// Every agent session goes out through one of your saved proxies: pass its id.
// `client.egress.listProxies()` lists them. Without it the create is refused
// with a ProxyRequiredError and nothing is created.
const session = await client.agentSessions.create(
  { mode: 'ai', proxy_id: process.env.DRIFTSTACK_PROXY_ID! },
  { idempotencyKey: randomUUID() },
);
try {
  // Poll get(id) while session.status is 'provisioning' before sending.
  const resp = await client.agentSessions.message(
    session.id,
    'Open https://example.com and tell me the main heading.',
    {
      idempotencyKey: randomUUID(),
      onStep: ({ index, result }) => console.log(index, result.kind), // live progress
    },
  );
  if (resp.kind === 'plan-executed') {
    console.log(resp.answer, resp.notice); // notice set = the task is not finished yet
    const paused = resp.results.filter((r) => r.kind === 'confirmation_required');
    // To approve a purchase / payment / account deletion the agent stopped on,
    // send the next message with { approveConsequentialActions: paused }.
  }
} finally {
  await client.agentSessions.close(session.id);
}

onEvent receives the other progress events (phase, plan, step_start, answer, notice; ignore names you do not recognise). When you asked for information and none could be produced, resp.answer_unavailable says why.

When a turn ends before the task is finished, resp.notice is the sentence to show a person and resp.notice_reason is the one word to branch on: step_limit, time_limit, budget_low, no_progress, repeated_step, ai_unavailable, question or declined — or, from a newer server, a value this SDK has never heard of, so show notice for anything you do not know.

A capture step's result carries a captureId; fetch the image as soon as the turn ends, because screenshots are kept only briefly:

import { writeFile } from 'node:fs/promises';

for (const r of resp.results) {
  if (r.kind === 'success' && r.captureId !== undefined) {
    const shot = await client.agentSessions.getCapture(session.id, r.captureId);
    await writeFile(shot.contentType === 'image/jpeg' ? 'shot.jpg' : 'shot.png', shot.bytes);
  }
}

transcript(id) yields the conversation so far and then follows it live, so leave the loop when you have what you need (that closes the connection). To read only what is there now:

const { transcript_length } = await client.agentSessions.get(session.id);
if (transcript_length > 0) {
  for await (const { index, entry } of client.agentSessions.transcript(session.id)) {
    console.log(index, entry.role, entry.body);
    if (index === transcript_length - 1) break;
  }
}

Pass { lastEventId } (the last index you saw) to carry on from there.

AI refusals are typed: ForbiddenError.requiresOwnKey (an Opus model needs your own Anthropic key), ConflictError.turnInProgress / .sessionStatus / .closedReason, RateLimitError (the message rate, or too many AI turns running at once — wait retryAfterSeconds, then send the same request again, the same idempotency key and all), BundledLlmBudgetExhaustedError, BundledLlmConsentRequiredError and ByokAnthropicRequiredError (.keyRejected, .keySource, .keyRejectedReason when Anthropic refused your own key). On stop(), FeatureUnavailableError.stopUnconfirmed means the stop could not be confirmed: call stop() again. See examples/agent-chat.ts for the complete flow.

Webhook signature verification

When you wire up Driftstack webhooks, verify each delivery before processing:

import { verifyWebhookSignature } from '@driftstack/sdk';

app.post('/driftstack-webhook', express.raw({ type: 'application/json' }), async (req, res) => {
  const ok = await verifyWebhookSignature({
    body: req.body, // raw bytes — DO NOT use a parsed body
    header: req.headers['x-driftstack-signature'],
    secret: process.env.DRIFTSTACK_WEBHOOK_SECRET!,
  });
  if (!ok) return res.status(401).end();
  // ... process the event ...
  res.status(204).end();
});

The verifier uses HMAC-SHA256 with constant-time comparison and rejects timestamps older than 5 minutes (configurable via toleranceSec). It's browser-isomorphic — verifyWebhookSignature works the same way in Node 20+, every modern browser, Tauri WebViews, Cloudflare Workers, Deno, and Bun (uses Web Crypto API under the hood, hence the await).

Examples

See examples/ for complete runnable demos:

  • quickstart.ts — happiest path, end-to-end
  • agent-chat.ts — run an AI task: create, wait until ready, send a task with live progress, handle each result kind (answer, notice, approvals), close
  • profile-management.ts — persistent profiles: create, update, clone, iterate, delete
  • pagination.ts — cursor pagination over list endpoints
  • egress-flow.ts — per-session SOCKS5 proxy config
  • egress-openvpn.ts — OpenVPN egress variant
  • webhook-receiver.ts — signature-verified webhook receiver
  • error-handling.ts — every documented error class
  • rate-limit-handling.ts — explicit retry-after honouring on top of the built-in retry

License

MIT