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

@w6w/sdk

v0.9.13

Published

Typed client for the w6w HTTP API. Thin transport, auth and error mapping — nothing else.

Readme

@w6w/sdk

The TypeScript client for the w6w HTTP API.

It is a thin client: transport, auth, error mapping, and typed request/response shapes. No business logic, no client-side polling, no hidden retries. If something useful needs two calls composed, that composition belongs in the API, not here.

The package is authored as runtime-neutral TypeScript against Web standards (fetch, Headers, URL, AbortController), so the same build runs under Node 18+, Deno and Bun.

Install

npm (compiled ESM + type declarations):

npm install @w6w/sdk

JSR (the TypeScript source, published verbatim):

deno add jsr:@w6w/sdk

Both registries publish the same version at the same time — @w6w/sdk, @w6w/cli, @w6w/react and the Python w6w package share one version number and are released together. A given version means the same set of operations in every language.

Quick start

import { isActionRun, isFunctionRun, isWorkflowRun, W6WClient } from "@w6w/sdk";

const client = new W6WClient(); // reads W6W_BASE_URL and W6W_TOKEN

// Who am I? (`me.versions.wrapper` is this SDK's version.)
const me = await client.me();

// What can I run? `run` is addressed by a `conn_…` / `fn_…` / `ep_…` / `wf_…`
// id — these are how you discover one.
const connections = await client.connections.list();
const workflows = await client.workflows.list();

Run a connection action

A conn_… id resolves to an app action. run returns a kind-tagged envelope — narrow it with isActionRun / isFunctionRun / isWorkflowRun before reading a field, since the three arms use different fields (output for an action or a function, runId+status for a workflow):

const env = await client.run({
  urn: connections[0].id, // "conn_…"
  action: "send_message", // which of the app's actions to invoke
  payload: { channel: "#general", text: "Hello from @w6w/sdk" },
});

if (isActionRun(env)) console.log(env.output); // the action's return value

Run a function or an endpoint

fn_… and ep_… ids run the same way — no action, since a function or endpoint has exactly one operation:

const env = await client.run({
  urn: "fn_normalize_address",
  payload: { address: "1 Infinite Loop" },
});
if (isFunctionRun(env)) console.log(env.output);

Run a workflow

Two ways to start a wf_… run. client.run() dispatches it like anything else and always answers 202 (queued) — use it when you're treating workflows the same as any other runnable URN:

const env = await client.run({ urn: workflows[0].id, payload: { customerId: "cus_1" } });
if (isWorkflowRun(env)) console.log(env.runId, env.status); // "queued"

workflows.run is the typed path, and the only one that can wait for the result server-side:

const run = await client.workflows.run(workflows[0].id, {
  wait: true,
  variables: { customerId: "cus_1" },
});

// A failed run is data, not a thrown error — `run.terminal` tells you it's done.
if (run.terminal && run.status === "failed") console.error(run.error);
else if (run.terminal) console.log(run.output);

Documents and vars

const doc = await client.documents.create({ key: "welcome", content: "# Hi" });
await client.documents.update(doc.id, { content: "# Hello" });
await client.documents.delete(doc.id);

const v = await client.vars.create({ name: "sender_email", type: "string", value: "[email protected]" });
await client.vars.update(v.id, { value: "[email protected]" });

Documents belong to a project: pass { project: "prj_…" } as the last argument, or set project on the client. Without either, your account's default project is used. Vars belong to the account.

Functions, Endpoints and workflow definitions

// Run by key or id. `functions.run` returns the Function's output directly.
const out = await client.functions.run("send-welcome-email", { payload: { to: "[email protected]" } });
const res = await client.endpoints.run("signup", { payload: { email: "[email protected]" } });

// Cancel a run. The answer is the run's status at that moment.
await client.workflows.cancel("run_01H…");

// Read, edit and save a definition, refusing to overwrite someone else's save.
const { workflow, updatedAt } = await client.workflows.get("wf_01H…");
await client.workflows.update("wf_01H…", { ...workflow, description: "Sends the welcome email" }, {
  ifUnmodifiedSince: updatedAt,
});

functions.list/get/create/update/delete and workflows.create/archive/delete complete the lifecycle. A Function or workflow must be archived before it can be deleted (409 function_not_archived / 409 workflow_not_archived).

Team

const members = await client.team.members();
const { redemptionLink } = await client.team.invite({ email: "[email protected]", role: "member" });

Reading the roster and open invites works for any member. invite, revokeInvite, setRole and removeMember need the owner or admin role.

A route the SDK doesn't wrap

client.request() sends any request with the client's own base URL, token and headers, and raises the same ApiError. Build the path with the path tag, which percent-encodes each value:

import { path } from "@w6w/sdk";

const { status, body } = await client.request<{ run: { status: string } }>({
  method: "GET",
  path: path`/runs/${runId}`,
});

Every call also takes an AbortSignal as signal; an aborted call raises ApiError with status 0 and code cancelled.

Configuration

Two environment variables, both read only at client construction:

| Variable | Meaning | Required | | -------------- | -------------------------------------------------------------------------------------------------------------------------------------- | -------- | | W6W_BASE_URL | The origin of your w6w server, e.g. https://api.example.com. The API is served at the root of that host, so nothing is appended. | Yes | | W6W_TOKEN | The bearer token, sent as Authorization: Bearer <token> on every request. | Yes |

Explicit constructor arguments always win over the environment, and credentials are instance state: two clients in one process can hold different tokens and point at different servers with no interference. There is no default base URL — a client built without one and without W6W_BASE_URL raises a configuration error naming the variable.

import { W6WClient } from "@w6w/sdk";

// Everything from the environment.
const client = new W6WClient();

// Explicit arguments override W6W_BASE_URL and W6W_TOKEN.
const other = new W6WClient({ baseUrl: "https://api.example.com", token: "…" });

The four options a host embedding w6w needs

Both variables stay plain strings. These four constructor options cover the cases a host — a partner embedding w6w inside its own product, a backend juggling tenants — actually runs into, and none of them has an environment-variable spelling:

| Option | Meaning | | ----------------------- | -------------------------------------------------------------------------------------------------- | | token | A bearer token, or a supplier function called per request (see below). | | refreshOnUnauthorized | Opt in to a single recovery retry when a request fails 401 / unauthorized. false by default. | | onUnauthorized | Called with the terminal 401 ApiError, at most once per call. | | headers | Default headers sent with every request. |

const client = new W6WClient({
  baseUrl: "https://api.example.com",
  // Called (and awaited) per request — never cached from construction.
  token: () => readTenantToken(),
  refreshOnUnauthorized: true, // one retry when the server answers 401
  onUnauthorized: (err) => redirectToLogin(err), // …and what to do when that did not help
  headers: { "X-W6W-Tenant": tenantId }, // your own gateway/authorizer header
});

A token supplier is how a host mints or rotates a credential out of band. token takes a string — sent verbatim on every call, exactly as before — or a TokenProvider function returning one, which is called per request rather than resolved once at construction, so a token that changes between calls is picked up without rebuilding the client. Requests in flight concurrently on one client share ONE supplier call (and its result); nothing is cached once it settles, so memoize the supplier yourself if minting is expensive. A nullish or blank result is treated exactly like a missing static token: a ConfigError naming W6W_TOKEN, never a request sent with no credential.

refreshOnUnauthorized opts in to one recovery. Off by default, and the behaviour before the option existed. When a request fails 401 with {code: "unauthorized"} and token is a function, the supplier is called once more with {forceRefresh: true} and, if that yields a usable value, the same request is re-sent once. Nothing else is ever retried — not a second time, not another status, and never a requireAuth: false request. onUnauthorized receives the terminal 401 error — after a failed recovery, or immediately when recovery is off or the token is a plain string — at most once per call, and never on success.

headers is the base every per-request headers option builds on. A per-request header with the same name wins, and neither can displace the authorization bearer or the content-type the client sets for a JSON body.

W6W_BASE_URL is an origin

The API is served at the root of its own host — https://api.example.com/vars, not …/api/vars — so the client appends nothing. Trailing slashes never matter, and any path you configure is preserved verbatim, because it is indistinguishable from a real gateway prefix:

| W6W_BASE_URL | Requests go to | | ---------------------------- | ------------------------------ | | https://api.example.com | https://api.example.com/… | | https://api.example.com/ | https://api.example.com/… | | https://api.example.com/// | https://api.example.com/… | | https://api.example.com/gw | https://api.example.com/gw/… |

Breaking in 0.2.0. The client used to append /api. If your W6W_BASE_URL ends in /api because of that, drop the suffix — the path is now preserved rather than deduplicated, so a stale one 404s on every call. It is not stripped for you: a configured path is exactly how a deployment behind a gateway prefix is addressed, and silently removing it would break those.

A variable that is set but empty — or whitespace-only — counts as unset, in both variables. W6W_BASE_URL= is how a shell or a Dockerfile spells "I meant to set this and did not", so it raises the same configuration error as an absent one rather than quietly producing a relative URL. An empty string passed explicitly to the constructor is likewise an error, and does not fall back to the environment.

W6W_TOKEN

Sent as Authorization: Bearer <token> on every request — there are no anonymous operations on this API. A client with no token can still be constructed (so tools that only print help or a version work offline); the configuration error naming W6W_TOKEN surfaces on the first request.

The variable is a plain string only. A credential that has to change between requests is spelled in the constructor — token: () => …, above — never in the environment.

Errors

Two error types, and the difference between them is diagnostic:

  • ConfigError — the client never got as far as a request: no base URL, or no token.
  • ApiError — carries status, code, message and raw (the parsed response body, kept because an error body carries fields the message drops). It covers exactly three cases: a transport failure (status: 0, code network_error), a non-JSON body on a failing status (code bad_response, with a snippet of what actually came back — usually a proxy's HTML error page), and the server's own error envelope (its code and message, verbatim).

Classify by status, and by a prefix of code (unknown_*, invalid_*, *_exists) — never by an exhaustive list of codes, which the server extends freely. Note that a 424 means the target app or its upstream vendor failed, not that w6w did; it is passed through untouched.

Nothing is retried, no token is refreshed, and a 401 has no side effect beyond the raised error — unless you opted in with refreshOnUnauthorized, which retries the failed request exactly once (see the four options a host embedding w6w needs).

Embedding for enterprise tenants

If you are a partner embedding w6w inside your OWN product, your backend mints a short-lived, per-user, account-scoped w6w session with createEmbedSessionClient, from a separate entry point, @w6w/sdk/server, never from @w6w/sdk itself. This is the recommended embed path; the host route and walkthrough are in Embed w6w in your product.

import { createEmbedSessionClient } from "@w6w/sdk/server";

const sessions = createEmbedSessionClient({
  baseUrl: process.env.W6W_BASE_URL!,
  clientId: process.env.W6W_TENANT_CLIENT_ID!,
  clientSecret: process.env.W6W_TENANT_CLIENT_SECRET!, // never sent to the browser
});

// `accountRef` is required: YOUR reference for the company the signed-in user is acting in.
const { token, expiresAt, account } = await sessions.create({ subject: userId, accountRef });

create() returns { token, expiresIn, expiresAt, subject, tenant, account, accountRef }, where account is the server's resolved acct_… id. The factory throws in a browser-like runtime.

The lower-level exchangeToken, with an optional account, stays available from the same entry point:

import { exchangeToken } from "@w6w/sdk/server";

This is a backend-only function: it takes your tenant's clientId/clientSecret, which must never reach a browser or a mobile client. A typical route on your OWN backend — never called directly from your frontend — looks like this:

// Your backend, e.g. POST /api/w6w-token
app.post("/api/w6w-token", async (req, res) => {
  const session = await readPartnerSession(req); // YOUR auth, not w6w's

  // Exchange trusts whatever `account` it is given — w6w has no way to verify
  // it against your own data, so YOU derive it from YOUR OWN membership data,
  // never from an unauthenticated client input (a query param, a request body
  // field a browser could set).
  const account = await yourOwnMembershipLookup(session.userId);

  const { token, expiresIn } = await exchangeToken({
    baseUrl: process.env.W6W_BASE_URL!,
    clientId: process.env.W6W_TENANT_CLIENT_ID!,
    clientSecret: process.env.W6W_TENANT_CLIENT_SECRET!, // never sent to the browser
    subject: session.userId,
    account,
  });

  res.json({ token, expiresAt: Date.now() + expiresIn * 1000 });
});

Your frontend calls /api/w6w-token, gets { token, expiresAt }, and uses token as W6W_TOKEN/a bearer for the rest of the session — the tenant secret itself never leaves your backend. Credentials travel only as an HTTP Authorization: Basic base64(clientId:clientSecret) header — never in the request body, the URL or a query string — and every malformed input (a blank clientId/clientSecret/subject, a clientId containing :, a non-latin-1 clientId/clientSecret) raises a local ConfigError before any network call. The embedding guide walks through token caching and refresh end to end; ../docs/sdk-surface.md "Server-only subpath — @w6w/sdk/server" pins the contract in both languages.

The surface

What operations exist is not decided in this repo. It is defined by the shared machine-readable contract endpoints.json, which every contract lane (this SDK, the CLI and the Python client) implements and which each one's conformance test reads directly — the same file, never a vendored copy. An operation missing from a wrapper is a failing test, not a preference.

An operation marked planned in that contract is implemented and unit-tested here ahead of its server route. The marker records server readiness, not wrapper completeness.

Versioning

The version is a shared fact across all four packages, so a release may go out for this package even when nothing in it changed — a divergent version is far more expensive than an empty release.

Below 1.0.0, breaking changes may land in a minor bump. That grace ends at 1.0.0; do not plan around it.

Development

The gates are Deno tasks. They need no network and no server:

deno task check      # typecheck mod.ts, src/ and tests/
deno task lint
deno task fmt:check
deno task test       # every test runs against a mocked transport
deno task cov        # test with coverage, then report it

Tests never require a live server. The npm dist/ build (npm run build, i.e. tsc -p tsconfig.build.json) runs in CI only; src/*.ts is the single authored source for both registries.