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

@tapi-dev/sdk

v0.1.40

Published

Official JavaScript and TypeScript client for TAPI developer APIs.

Readme

TAPI JavaScript SDK

Official JavaScript and TypeScript client for TAPI developer APIs.

Install

npm install @tapi-dev/sdk

This package is ESM-first and works in runtimes with fetch, including modern Node.js and browser-like server runtimes.

The CLI command is tapi. If your package manager only installed the SDK locally and tapi is not on PATH, use npx tapi as a fallback.

Developer Flow

Tapi Studio is launched from the signed-in developer account. Running tapi studio lists the Tapps available to that account, asks which one to open, then starts Studio in that Tapp context. The current directory can still provide a default via .tapi/project.json, but it is no longer required for Studio to know which Tapp to show.

npm install @tapi-dev/sdk
tapi login
tapi tapp create brokerage
tapi studio

Useful Studio launch forms:

tapi studio                    # ask which Tapp to open
tapi studio --tapp brokerage   # open one Tapp directly
tapi studio --select           # force the picker
tapi studio --last             # reuse the last selected Tapp

tapi studio checks the required local tapi-service, installs it when needed, downloads the portable Studio server release when the channel manifest uses installerKind: portable-server, starts Studio locally, and opens the browser. If the channel still publishes a legacy NSIS desktop Studio manifest and no Studio executable is installed yet, tapi studio runs the installer first, then opens Studio. The normal developer path is the portable server launched by the CLI.

By default, Studio downloads and extracted portable server releases live under the user's local app data directory. On Windows that is usually %LOCALAPPDATA%\Tapi\Studio. To keep the whole Studio install on another drive, set TAPI_STUDIO_HOME before launching:

$env:TAPI_STUDIO_HOME="F:\Tapi\Studio"
tapi studio

For separate paths, use --cache-dir for downloaded zip artifacts and --install-dir for extracted portable Studio server releases.

After a successful install, the CLI prunes older Tapi-managed download artifacts from the cache and older SDK-marked release folders from the install directory. The current artifact and current release are kept.

Useful commands:

tapi login
tapi init --project brokerage
tapi link --project brokerage
tapi studio
tapi studio --tapp brokerage
tapi service install --channel pilot
tapi service status
tapi services describe schwab.place_order
tapi tapp create brokerage
tapi queue create "John"
tapi tapp queue add brokerage queue_abc123
tapi runner setup --runner-id john-laptop --project brokerage
tapi runner slot set john-laptop 1 queue_abc123 --project brokerage
tapi tapp service add brokerage schwab.place_order --service-map sm_123 --entry place_order
tapi triggers sync
tapi sessions

Local project identity lives in the app repo:

.tapi/project.json
.tapi/services/catalog.json

ServiceMaps and service-run contracts are saved in the bound Tapi server project. Runtime SDK calls read the service catalog from that project.

tapi tapp create brokerage --name "Brokerage"
tapi tapp service add brokerage schwab.place_order \
  --service-map sm_123 \
  --entry place_order

Create a queue, attach it to the Tapp, and point one or more runner slots at that queue:

tapi queue create "John" --project brokerage
tapi tapp queue add brokerage queue_abc123

tapi runner slot set john-laptop 1 queue_abc123 \
  --project brokerage

For users setting up a local runner, tapi runner setup opens a localhost setup page that can create or accept a queue id, attach that queue to the Tapp, and assign one or more slots on the runner:

tapi runner setup \
  --runner-id john-laptop \
  --project brokerage

Quick Start

Create one TAPI client in server-side app code. Use a Firebase ID token for the developer identity that owns and pays for the Tapp, the Tapp id that owns the service call, and optionally the same project id from .tapi/project.json.

import { TapiClient } from "@tapi-dev/sdk";

export const tapi = new TapiClient({
  baseUrl: process.env.TAPI_BASE_URL!,
  authToken: process.env.TAPI_FIREBASE_ID_TOKEN!,
  tappId: process.env.TAPI_TAPP_ID!,
  projectId: process.env.TAPI_PROJECT_ID!,
});

Production is the default mode. Unknown website states fail the run instead of holding a browser open:

const tapi = new TapiClient({
  baseUrl: process.env.TAPI_BASE_URL!,
  authToken: process.env.TAPI_FIREBASE_ID_TOKEN!,
  tappId: process.env.TAPI_TAPP_ID!,
  projectId: process.env.TAPI_PROJECT_ID!,
  dev: false,
});

During development, set dev: true. If a website reaches an unknown state, Tapi preserves the browser session for takeover instead of turning it into a production failure:

const tapi = new TapiClient({
  baseUrl: process.env.TAPI_BASE_URL!,
  authToken: process.env.TAPI_FIREBASE_ID_TOKEN!,
  tappId: process.env.TAPI_TAPP_ID!,
  projectId: process.env.TAPI_PROJECT_ID!,
  dev: true,
});

Inspect preserved sessions from the repo:

tapi sessions
tapi sessions --json
tapi sessions open <session-id>

tapi sessions open reuses the matching Studio window for the current repo when one is already running; otherwise it installs/starts Studio and opens directly to that session.

Service runs are authored visually in Tapi Studio by setting ServiceMap bounds and selecting inputs/outputs. The SDK sees the public service-run name:

<namespace>.<operation>

A service run is routed through a queue. The queue id is the only routing handle: the Tapp must attach the queue, and one or more runner slots must be configured with the same queue id.

const operation = await tapi.services.describe("schwab.place_order");

const run = await tapi.services.run("schwab.place_order", {
  queueId: "queue_abc123",
  inputs: {
    symbol: "AAPL",
    side: "buy",
    quantity: 10,
    orderType: "limit",
    limitPrice: 190,
  },
});

const completedRun = await tapi.runs.wait(run.id);
console.log(completedRun.status, completedRun.result);

Late Inputs

Most service inputs are known before the run starts. Use lateInput() when the workflow needs a value later, after earlier browser steps have triggered work somewhere else.

For example, state 4 might submit a login form and trigger a one-time code by email or SMS. State 5 needs that code, but your app can only retrieve it after state 4 runs. Declare the input up front with the service run, then resolve it from your own code:

import { lateInput } from "@tapi-dev/sdk";

const verificationCode = lateInput<string>({
  resolve: async () => {
    return await waitForVerificationCodeFromEmail();
  },
  timeoutMs: 180_000,
});

const run = await tapi.services.run("walmart.login", {
  inputs: {
    email: "[email protected]",
    password: process.env.WALMART_PASSWORD!,
    verificationCode,
  },
});

await verificationCode.waitForProvides();

const completedRun = await tapi.runs.wait(run.id);

You can also provide the value manually. This is useful when another callback, worker, webhook, or polling loop finds the value:

const verificationCode = lateInput<string>({ timeoutMs: 180_000 });

const run = await tapi.services.run("walmart.login", {
  inputs: { email: "[email protected]", verificationCode },
});

const code = await waitForVerificationCodeFromWebhook();
await verificationCode.set(code);

const completedRun = await tapi.runs.wait(run.id);

Tapi only waits when the runner reaches the action bound to that input. If your app resolves the value early, Tapi stores it and uses it later. If the value is not provided before timeoutMs, the run fails instead of guessing or using the recorded default.

Late inputs are for values produced outside the website flow. Values read from the website itself should stay as normal service outputs.

Service Triggers

Triggers are scheduled service runs. Keep trigger definitions in the app repo and sync them with the service catalog from that repo:

// tapi.config.ts
export default {
  triggers: {
    nightlyBalance: {
      serviceRun: "schwab.get_balance",
      schedule: { cron: "0 9 * * MON-FRI" },
      inputs: { accountId: "main" },
      runtime: { profileRef: "perm_default" },
    },
  },
};
tapi triggers sync

triggers sync upserts by trigger name for the current .tapi/project.json project. Schedules support standard five-field cron strings or intervals as numbers of seconds / strings like 30s, 15m, 2h, and 1d.

You can also manage triggers directly:

await tapi.triggers.create({
  name: "nightlyBalance",
  serviceRun: "schwab.get_balance",
  schedule: { cron: "0 9 * * MON-FRI" },
  inputs: { accountId: "main" },
  runtime: { profileRef: "perm_default" },
});

await tapi.triggers.fire("act_123");
await tapi.triggers.disable("act_123");

You can inspect the same input/output contract from the CLI:

tapi services describe schwab.place_order \
  --api-base-url "$TAPI_BASE_URL" \
  --project "$TAPI_PROJECT_ID"

Do not expose developer Firebase ID tokens in public browser bundles. Put the SDK behind your own backend route, server action, or job worker.

Runtime Profiles and Proxies

Browser identity is selected with a runner-local profileRef, not by passing Chrome folder paths through the cloud service-run request.

Use PermProfile for a long-lived account identity. It owns a persistent Chrome user-data directory and should usually use the runner's normal home IP:

const perm = await tapi.runtime.permProfiles.create({
  displayName: "Mom Walmart",
  originPolicy: { type: "home" },
});

await tapi.runtime.permProfiles.launchSetup(perm.profile.profileRef);
// The user logs in, adds passkeys, then closes setup Chrome.

await tapi.services.run("walmart.reorder", {
  queueId: "queue_abc123",
  inputs: { item: "paper towels" },
  runtime: { profileRef: perm.profile.profileRef },
});

Use TempProfile for throwaway work. Temp profiles can be cloned from a template profile and assigned different proxies:

const temp = await tapi.runtime.tempProfiles.provisionMany({
  count: 3,
  displayNamePrefix: "checkout",
  proxies: [
    "http://user1:[email protected]:8080",
    "http://user2:[email protected]:8080",
    "http://user3:[email protected]:8080",
  ],
  destroyOnRelease: true,
});

await Promise.all(
  temp.profiles.map((profile) =>
    tapi.services.run("walmart.lookupItem", {
      queueId: "queue_abc123",
      inputs: { sku: "123" },
      runtime: {
        profileRef: profile.profileRef,
        destroyOnRelease: profile.destroyOnRelease,
      },
    }),
  ),
);

runtime.profileRef is the only value sent to the TAPI cloud run request. Proxy URLs, proxy credentials, and Chrome profile folders stay on the local runner and are handled through the local control WebSocket at ws://127.0.0.1:8765. When the same PermProfile is used concurrently for multiple different sites, the runner creates runtime clones so Chrome does not open the same --user-data-dir twice. You can also create a clone explicitly with tapi.runtime.profiles.clone({ profileRef, site }), but most apps should pass the base profileRef and let the runner decide. If your JavaScript runtime does not provide WebSocket, pass one:

const tapi = new TapiClient({
  baseUrl: process.env.TAPI_BASE_URL!,
  authToken: process.env.TAPI_FIREBASE_ID_TOKEN!,
  tappId: process.env.TAPI_TAPP_ID!,
  webSocket: MyWebSocketImplementation,
});

Configuration

TAPI_BASE_URL=https://your-tapi-api-host
TAPI_FIREBASE_ID_TOKEN=firebase_id_token_for_the_tapp_owner
TAPI_TAPP_ID=brokerage
TAPI_PROJECT_ID=brokerage

tappId is required. Service calls are made for that Tapp, and the developer who owns the Tapp pays for the service delivery. If provided, projectId is sent as the X-Tapi-Project header and the server rejects mismatches.

Common Project Setup

A typical application keeps the client in one small module:

src/
  lib/
    tapi.ts
// src/lib/tapi.ts
import { TapiClient } from "@tapi-dev/sdk";

export const tapi = new TapiClient({
  baseUrl: process.env.TAPI_BASE_URL!,
  authToken: process.env.TAPI_FIREBASE_ID_TOKEN!,
  tappId: process.env.TAPI_TAPP_ID!,
  projectId: process.env.TAPI_PROJECT_ID!,
});

Application code should import this shared client instead of constructing a new client in every file.

Available Resources

await tapi.catalog.get();
await tapi.runners.list();
await tapi.runtime.requirements();
await tapi.runtime.profiles.list();
await tapi.runtime.profiles.clone({ profileRef: "perm_default", site: "walmart" });
await tapi.runtime.permProfiles.create({ displayName: "Default", originPolicy: { type: "home" } });
await tapi.runtime.tempProfiles.provisionMany({ proxies: ["http://user:pass@host:8080"] });
await tapi.services.describe("schwab.place_order");

const run = await tapi.services.run("serviceName.serviceKey", {
  queueId: "queue_abc123",
  inputs: { example: true },
  runtime: { profileRef: "perm_default" },
  priority: 5,
  idempotencyKey: "request-123",
});

await tapi.runs.get(run.id);
await tapi.runs.wait(run.id, { intervalMs: 1000, timeoutMs: 300000 });
await tapi.runs.provideInput(run.id, "verificationCode", "123456");
await tapi.runs.cancel(run.id);

Cloud Batch Runs

Cloud runs are requested from the SDK, but VM provisioning, service installation, worker leases, browser internals, and AWS cleanup stay on the Tapi server. The SDK is only the front door.

Keep this code server-side. Do not put developer Firebase ID tokens in a browser bundle and do not wire AWS or Stripe from the developer's app. The app sends its own user identity to Tapi, and Tapi handles prepaid Stripe checkout, credit accounting, AWS worker provisioning, and cleanup.

The minimum developer flow is:

  1. App user clicks a button.
  2. The developer's backend calls runCloudBatch with user.externalUserId.
  3. If the returned run has status: "payment_required", redirect the app user to run.checkoutUrl.
  4. Stripe calls the Tapi webhook, Tapi credits that same externalUserId, and the app retries the cloud batch.
  5. If credit is available, Tapi reserves the balance and starts cloud workers.
// backend route or server action
const quote = await tapi.services.quoteCloud("schwab.place_order", {
  inputs: [
    { symbol: "AAPL", quantity: 1 },
    { symbol: "MSFT", quantity: 2 },
  ],
  user: {
    externalUserId: appUser.id,
    email: appUser.email,
  },
  cloud: {
    windows: true,
    maxVms: 1,
    chromePerVm: 10,
    maxCostUsd: 20,
  },
});

if (!quote.withinMaxCost) {
  throw new Error("Cloud batch exceeds the configured cost cap");
}

const run = await tapi.services.runCloudBatch("schwab.place_order", {
  inputs: [
    { symbol: "AAPL", quantity: 1 },
    { symbol: "MSFT", quantity: 2 },
  ],
  user: {
    externalUserId: appUser.id,
    email: appUser.email,
  },
  payment: {
    successUrl: `https://your-app.example/cloud/success`,
    cancelUrl: `https://your-app.example/cloud/cancel`,
  },
  cloud: {
    windows: true,
    maxVms: 1,
    chromePerVm: 10,
    maxCostUsd: 20,
  },
});

if (run.status === "payment_required") {
  return { redirectTo: run.checkoutUrl };
}

for await (const event of tapi.cloudRuns.stream(run.id)) {
  console.log(event.status, event.traceId);
}

const results = await tapi.cloudRuns.results(run.id);

When CLOUD_BILLING_ENABLED=true, the server refuses to launch AWS workers unless prepaid cloud credit is available and reserved first. If balance is too low, runCloudBatch converts the server's HTTP 402 into a normal CloudBatchRun object with status: "payment_required", checkoutUrl, availableBalanceCents, and requiredBalanceCents. That keeps the button handler simple.

Credits are scoped under the authenticated Firebase owner, Tapp id, and user.externalUserId. Use the same externalUserId for quote, balance, checkout, and run calls. If you omit externalUserId but provide email, Tapi uses the normalized email as the external user id.

Applications can also create a top-up checkout explicitly:

const user = {
  externalUserId: appUser.id,
  email: appUser.email,
};

const balance = await tapi.cloudRuns.balance(user);

if (balance.balanceCents < balance.minimumBalanceCents) {
  const checkout = await tapi.cloudRuns.checkout({
    amountCents: balance.minimumBalanceCents,
    user,
    successUrl: "https://your-app.example/cloud/success",
    cancelUrl: "https://your-app.example/cloud/cancel",
  });
  console.log(checkout.checkoutUrl);
}

The developer application does not need VM provisioning logic, installer deployment logic, worker command interpretation, browser runner internals, or builder logic. Those stay behind the Tapi server API.

Service runs are addressed as <serviceName>.<serviceKey>. The service name comes from the ServiceMap name in Studio. The service key is the name the developer types into the SDK name textbox at the service boundary.

describe() returns the public SDK contract: input controls such as textbox, radio, select, checkbox, conditional requirements such as requiredWhen, and the output schema. It does not expose workflow ids, state ids, action ids, selectors, runner ids, or other Studio internals.

Errors

Failed HTTP responses throw TapiError:

import { TapiError } from "@tapi-dev/sdk";

try {
  await tapi.catalog.get();
} catch (error) {
  if (error instanceof TapiError) {
    console.error(error.status, error.code, error.details);
  }
  throw error;
}

TypeScript

The package includes generated TypeScript declarations. Common exported types include:

import type {
  LateInput,
  RuntimeRequirements,
  RuntimeProfile,
  RuntimeRunOptions,
  SdkCatalog,
  ServiceOperation,
  ServiceRunRequest,
  TapiRun,
  TapiRunner,
} from "@tapi-dev/sdk";

Local Development

From this SDK directory:

npm ci
npm test
npm run build
npm pack --dry-run

npm pack --dry-run shows the exact files that will be published.