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

@workstudio-inc/sdk

v0.2.0

Published

Official JavaScript/TypeScript SDK for Workstudio. Headless and framework-agnostic. Covers the embeddable integration marketplace today; the surface will grow to the rest of the API.

Readme

@workstudio-inc/sdk

Headless TypeScript SDK for the WorkStudio embeddable integration marketplace.

It wraps the /embed API with typed methods and carries no UI — build the catalog, activation, and connection experience however you want (React, Vue, Svelte, plain DOM). Your product team owns the pixels; the SDK owns the protocol.

npm install @workstudio-inc/sdk

Quick start

import { IntegrationsClient } from '@workstudio-inc/sdk';

const client = new IntegrationsClient({
  apiKey: process.env.WS_EMBED_KEY,   // svx_ik_...  (server-side secret)
  tenantId: 'a8737fb0-0d10-4aab-809b-d54ef71dc8f1',
  scopeKey: 'customer:acme',          // which of YOUR customers this is for
});

// 1. List what's available
const { integrations } = await client.listIntegrations();

// 2. Activate one
const activation = await client.activate('servicenow');

// 3. Anything the customer still needs to do is in pendingRequirements
for (const req of activation.pendingRequirements ?? []) {
  console.log(req.type, req.connectorName, req.authorizationUrl);
}

Authentication & scoping

There are two ways to authenticate, and they differ in how the customer scope is trusted:

| Mode | Use it | How to set the customer scope | |---|---|---| | API key (apiKey) | Server-side / backend-for-frontend | Pass scopeKey (sent as X-Embed-Scope). Trusted because the API key is a secret only your server holds — like Stripe's Stripe-Account. | | Custom JWT (token) | Directly in the browser | The scope is a claim inside the signed token and cannot be forged. scopeKey in the config is ignored. |

Never ship an API key to a browser. For browser embeds, mint a short-lived custom JWT per customer on your server and pass it as token.

// Browser embed — scope is inside the signed JWT
const client = new IntegrationsClient({ token: shortLivedCustomerJwt, tenantId });

You can also override the scope per call:

await client.activate('powerbi', { scopeKey: 'customer:globex' });

Secure browser embeds — session tokens (recommended)

The cleanest way to auth a browser embed: your backend exchanges the API key for a short-lived, single-customer-scoped session token (svx_st_…), and only that token reaches the browser. The API key never leaves your server.

// 1. Server (your backend) — mint a scoped, short-lived token
const server = new IntegrationsClient({ apiKey: process.env.WS_KEY, tenantId });
const { token, expiresAt } = await server.mintSessionToken('customer:acme', { ttlSeconds: 900 });
// send `token` to the browser (it expires in ~15 min and is scoped to this one customer)

// 2. Browser — use the session token, never the API key
const client = new IntegrationsClient({ token, tenantId });
await client.listIntegrations();

The web component / iframe accept the same token (<svx-integration-catalog token="svx_st_…">), and the web component injects it over postMessage so it never appears in the URL, history, or logs.


API

All methods return typed promises and throw IntegrationsApiError on non-2xx responses.

// Catalog
client.listIntegrations({ category?, search? }): Promise<Catalog>
client.getIntegration(idOrSlug): Promise<IntegrationDetail>

// Activations
client.listActivations(): Promise<Activation[]>
client.getActivation(activationId): Promise<Activation>
client.activate(integrationIdOrSlug, { config?, metadata?, scopeKey? }): Promise<Activation>
client.deactivate(activationId): Promise<void>

// Connections
client.bindConnection(activationId, connectorGlobalId, connectionInstanceId): Promise<Activation>
client.disconnectConnection(activationId, connectorGlobalId): Promise<Activation>

// OAuth
client.initiateOAuth(activationId, connectorGlobalId, { redirectUri?, state? }): Promise<OAuthInitResult>
client.completeOAuth({ code, state, activationId?, connectorGlobalId?, connectionInstanceId? }): Promise<OAuthCallbackResult>

// The customer's connections, independent of any App
client.listConnections(): Promise<ConnectionsList>          // offered connectors + status + MCP access
client.disconnect(connectorGlobalId): Promise<{ removed }>

// Call a connector with the customer's own connection
client.listOperations(connectorGlobalId): Promise<ConnectorOperation[]>
client.execute(connectorGlobalId, operation, input?): Promise<ExecuteResult>

// The customer's AI assistant (MCP)
client.getMcpAccess(): Promise<McpAccess>
client.createMcpUrl(): Promise<McpUrl>                      // url is returned once
client.revokeMcpUrl(): Promise<void>

Activation & connector state

Activation.status is pending | active | failed | disabled. Connector-level state lives in:

  • activation.connectors[] — connected connectors (status: connected | expired | error)
  • activation.pendingRequirements[] — connectors still needing OAuth/config
const total = integration.connectorCount;
const connected = activation.connectors?.filter(c => c.status === 'connected').length ?? 0;
render(`${connected}/${total} connected`);

Connecting a connector (browser)

The browser entry point adds a popup-based OAuth helper so a "Connect" button is one call:

import { connectConnector } from '@workstudio-inc/sdk/browser';

async function onConnectClick(activationId: string, connectorGlobalId: string) {
  const updated = await connectConnector(client, activationId, connectorGlobalId);
  // `updated` is the refreshed activation with the connector now connected
}

Prefer to drive the flow yourself? Use the primitives:

import { openOAuthPopup } from '@workstudio-inc/sdk/browser';

const { authorizationUrl } = await client.initiateOAuth(activationId, connectorGlobalId);
await openOAuthPopup(authorizationUrl);
const activation = await client.getActivation(activationId);

Your customer's AI assistant

A customer who has connected their systems can use them from Claude, ChatGPT or Cursor. Create a private MCP URL for them and show it once; the assistant then sees their connections as tools, with their own credentials, and nothing of anyone else's.

const { url } = await client.createMcpUrl();   // show this to the customer, once
// later
await client.revokeMcpUrl();                   // the URL stops working immediately

The embedded catalog's Connections tab does this for you ("Use with AI assistants"). Turn it off for every customer in Designer → Marketplace → Embed.


Example: a tiny React hook

The SDK ships no React dependency — a hook is a few lines on top of it:

import { useEffect, useState, useMemo } from 'react';
import { IntegrationsClient, type Catalog } from '@workstudio-inc/sdk';

export function useCatalog(config) {
  const client = useMemo(() => new IntegrationsClient(config), [config]);
  const [catalog, setCatalog] = useState<Catalog | null>(null);
  const [error, setError] = useState<unknown>(null);

  useEffect(() => {
    client.listIntegrations().then(setCatalog).catch(setError);
  }, [client]);

  return { client, catalog, error };
}

Configuration reference

new IntegrationsClient({
  apiKey?:   string,   // svx_ik_...  (server-side)
  token?:    string,   // JWT (browser)
  tenantId?: string,   // X-Embed-Tenant (required for API-key auth)
  envId?:    string,   // X-Embed-Env
  scopeKey?: string,   // X-Embed-Scope, e.g. "customer:acme" (API-key mode)
  origin?:   string,   // X-Embed-Origin (origin allowlist)
  baseUrl?:  string,   // default 'https://api.work.studio'
  basePath?: string,   // default '/api/v1/workflow/embed'
  fetch?:    typeof fetch, // custom fetch on older runtimes
});

If your gateway routes the embed API under a different prefix, override basePath (e.g. /api/v1/workflow/catalog/embed).


Error handling

import { IntegrationsApiError } from '@workstudio-inc/sdk';

try {
  await client.activate('servicenow');
} catch (e) {
  if (e instanceof IntegrationsApiError) {
    if (e.isAuthError) { /* 401/403 — bad key or missing scope */ }
    console.error(e.status, e.body);
  }
}

Build

npm install
npm run build      # emits ESM + CJS + .d.ts to dist/
npm run typecheck