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

@fabric-platform/sdk

v0.19.1

Published

TypeScript SDK for the Fabric AI workflow platform

Readme

@fabric-platform/sdk

TypeScript SDK for the Fabric AI workflow platform. Designed for Next.js (server components, API routes, client-side) with zero runtime dependencies.

Install

npm install @fabric-platform/sdk

Requires Node.js 18+.

Quick Start

import { FabricClient } from '@fabric-platform/sdk';

const fabric = new FabricClient({
  baseUrl: 'https://api.fabric.ai',
  auth: { type: 'api-key', key: 'fab_...' },
  organizationId: 'your-org-id',
});

// Submit a workflow and stream live progress
const run = await fabric.workflows.runs.submitAndWait('research/deep_research', {
  input: { query: 'AI trends in 2026' },
  onEvent(event) {
    console.log(`${event.kind}: ${event.node_key}`);
  },
});

console.log(run.status); // "completed"
console.log(run.output); // workflow result

Authentication

The SDK is stateless — it does not store tokens. Your application owns the session.

API Key (server-side)

const fabric = new FabricClient({
  auth: { type: 'api-key', key: process.env.FABRIC_API_KEY! },
});

Bearer Token (client-side)

const fabric = new FabricClient({
  auth: { type: 'bearer', token: supabaseSession.access_token },
});

Dynamic Token (Next.js server components)

import { cookies } from 'next/headers';

const fabric = new FabricClient({
  auth: async () => {
    const token = cookies().get('fabric_access_token')?.value;
    if (!token) throw new Error('Not authenticated');
    return token;
  },
});

OAuth Client Credentials (service-to-service)

const fabric = new FabricClient({
  auth: { type: 'oauth', clientId: '...', clientSecret: '...' },
});

The OAuth strategy auto-exchanges credentials for an access token and caches it until expiry.

Auth Endpoints

The auth resource exposes login/signup methods that return tokens without storing them:

// No auth needed for these
const fabric = new FabricClient({ baseUrl: 'https://api.fabric.ai' });

const { access_token, refresh_token, user } = await fabric.auth.login('[email protected]', 'password');
const { access_token: newToken } = await fabric.auth.refresh(refresh_token);

await fabric.auth.signup('[email protected]', 'password');
await fabric.auth.magicLink('[email protected]');
await fabric.auth.forgotPassword('[email protected]');
await fabric.auth.resetPassword(recoveryToken, 'new-password');

// Social login — returns a redirect URL
const url = fabric.auth.socialLoginUrl('google');

// MFA
const factor = await fabric.auth.mfa.enroll('totp');
const challenge = await fabric.auth.mfa.challenge(factor.id);
const verified = await fabric.auth.mfa.verify(factor.id, challenge.id, '123456');

Configuration

interface FabricConfig {
  /** Base URL of the Fabric API. Default: http://localhost:3001 */
  baseUrl?: string;
  /** Authentication strategy or dynamic token provider. */
  auth?: AuthStrategy | (() => Promise<string>);
  /** Default organization ID for scoped requests. */
  organizationId?: string;
  /** Default team ID for scoped requests. */
  teamId?: string;
  /** Request timeout in milliseconds. Default: 30000 */
  timeout?: number;
  /** Custom fetch implementation (e.g., Next.js enhanced fetch). */
  fetch?: typeof globalThis.fetch;
}

Next.js Fetch Caching

Pass Next.js's extended fetch for cache control:

const fabric = new FabricClient({
  auth: { type: 'api-key', key: process.env.FABRIC_API_KEY! },
  fetch: fetch, // Next.js enhanced fetch
});

Workflows

Registry (create, list, manage)

// Create a workflow definition
const entry = await fabric.workflows.registry.create({
  name: 'my-pipeline',
  language: 'python',
  source: 'custom',
  entry_point: 'main.py',
  description: 'My custom pipeline',
  input_schema: { type: 'object', properties: { query: { type: 'string' } } },
});

// List workflows (hierarchical: team > org > global)
const { items, pagination } = await fabric.workflows.registry.list();
const { items: orgWorkflows } = await fabric.workflows.registry.list({
  organization_id: 'org-uuid',
  limit: 50,
});

// Get a specific workflow by name
const workflow = await fabric.workflows.registry.get('research/deep_research');

// Delete a workflow
await fabric.workflows.registry.delete('my-pipeline');

// Compose multiple workflows into a pipeline
const composed = await fabric.workflows.compose({
  name: 'research-to-hooks',
  steps: ['research/deep_research', 'hooks/generate'],
  description: 'Research a topic then generate hooks',
});

Discovering a workflow's schema

Fabric derives JSON Schemas from each workflow's Pydantic task types at server boot and stores them in the registry. Use the dedicated getSchemas method to fetch just the contract:

const schemas = await fabric.workflows.registry.getSchemas('research/trends');

// Workflow-level pair — use these to render a submit form.
if (schemas.input_schema) {
  renderForm(schemas.input_schema);
}
if (schemas.output_schema) {
  console.log('This workflow returns:', schemas.output_schema);
}

// Per-task breakdown (excludes internal :capture / :finalize shims).
for (const task of schemas.task_schemas ?? []) {
  console.log(task.task_id, task.description, task.source);
}

Workflows that don't declare Pydantic task types come back with null schemas — the call still succeeds, the consumer just learns there's no machine-readable contract yet. See the Typed Workflows guide for the full pattern.

Runs (execute, monitor, control)

// Submit a run (returns immediately)
const run = await fabric.workflows.runs.submit('research/deep_research', {
  input: { query: 'AI trends' },
  metadata: { source: 'dashboard' },
  priority: 75,
  idempotencyKey: 'unique-request-id', // prevent double-submissions
  validate: true, // opt in to server-side JSON Schema validation
});

// Get run status
const status = await fabric.workflows.runs.get(run.id);
console.log(status.status);   // "pending" | "running" | "completed" | "failed" | ...
console.log(status.progress); // { total_tasks: 5, completed_tasks: 3, percentage: 60, ... }

// List runs
const { items: runs } = await fabric.workflows.runs.list({ limit: 20 });

// Cancel a run
await fabric.workflows.runs.cancel(run.id, 'No longer needed');

// Pause / Resume
await fabric.workflows.runs.pause(run.id, 'Waiting for approval');
await fabric.workflows.runs.resume(run.id);

Submit and Wait

// Submit and block until completion, with live progress callbacks
const result = await fabric.workflows.runs.submitAndWait('research/deep_research', {
  input: { query: 'AI trends' },
  timeoutMs: 300_000, // 5 minutes
  onEvent(event) {
    if (event.kind === 'workflow.node.completed') {
      console.log(`Step done: ${event.node_key}`);
    }
  },
});

console.log(result.output);

Real-Time Events (SSE)

Stream events for a workflow run

const stream = fabric.workflows.runs.streamEvents(run.id, {
  onEvent(event) {
    switch (event.kind) {
      case 'workflow.run.started':
        console.log('Workflow started');
        break;
      case 'workflow.node.started':
        console.log(`Node ${event.node_key} running...`);
        break;
      case 'workflow.node.progress':
        console.log(`Progress: ${JSON.stringify(event.payload)}`);
        break;
      case 'workflow.node.completed':
        console.log(`Node ${event.node_key} done`);
        break;
      case 'workflow.run.completed':
        console.log('Workflow finished!');
        break;
      case 'workflow.run.failed':
        console.error(`Failed: ${event.payload?.error}`);
        break;
    }
  },
  onError(err) {
    console.error('Stream error:', err);
  },
});

// Cancel the stream
stream.abort();

// Wait for the stream to finish
await stream.done;

Features:

  • Replays existing events on connect (no missed events)
  • Auto-reconnects via Last-Event-ID header
  • Auto-closes on terminal events (completed, failed, cancelled)
  • Works in Node.js, browsers, and Edge Runtime

Stream all events

const stream = fabric.events.stream({
  onEvent(event) { console.log(event.kind); },
});

WebSocket (for dashboards)

const ws = fabric.events.connectWebSocket({
  onEvent(event) { console.log(event); },
  onError(err) { console.error(err); },
  onClose() { console.log('Disconnected'); },
});

ws.subscribeToRun('run-uuid');  // Filter to a specific run
ws.unsubscribe();               // Receive all events
ws.close();                     // Disconnect

Built-in event logger

import { logEvents } from '@fabric-platform/sdk';

// Pretty-prints events to the console with timing
fabric.workflows.runs.streamEvents(run.id, { onEvent: logEvents });

React Component Example

'use client';
import { useEffect, useState } from 'react';
import { FabricClient, type DomainEvent } from '@fabric-platform/sdk';

function WorkflowProgress({ runId, token }: { runId: string; token: string }) {
  const [events, setEvents] = useState<DomainEvent[]>([]);

  useEffect(() => {
    const fabric = new FabricClient({
      baseUrl: process.env.NEXT_PUBLIC_FABRIC_URL,
      auth: { type: 'bearer', token },
    });
    const stream = fabric.workflows.runs.streamEvents(runId, {
      onEvent(event) {
        setEvents(prev => [...prev, event]);
      },
    });
    return () => stream.abort(); // Cleanup on unmount
  }, [runId, token]);

  return (
    <ul>
      {events.map((e) => (
        <li key={e.id}>{e.kind}: {e.node_key}</li>
      ))}
    </ul>
  );
}

Streaming workflow runs in the browser

The SDK's SSE layer uses fetch + ReadableStream, not the browser's native EventSource, which means watch() and streamEvents() work directly from the browser with custom Authorization headers — no proxy required.

Auth: use a dynamic token provider, never ship an API key

Browsers must never carry a long-lived fab_... API key. Configure the client with a dynamic token provider that fetches a short-lived token from your own auth backend:

'use client';
import { FabricClient } from '@fabric-platform/sdk';

export const fabric = new FabricClient({
  baseUrl: 'https://api.fabric.example.com',
  auth: async () => {
    // Your server issues a short-lived JWT scoped to this user.
    const res = await fetch('/api/fabric-token', { credentials: 'include' });
    const { token } = await res.json();
    return token;
  },
});

The provider is re-invoked on every fetch call, so tokens refresh naturally on reconnect. Your backend only needs to implement one route: mint a short-lived Fabric token for the logged-in user.

useWorkflowRun — React hook with typed state

@fabric-platform/sdk/react exports a hook that wraps watch() and tracks node progress for you. It accepts the FabricClient you constructed above, and a nullable runId so you can render it before the run is submitted:

'use client';
import { useState } from 'react';
import { useWorkflowRun } from '@fabric-platform/sdk/react';
import { fabric } from './fabric-client';

export function RunTrigger({ workflowName }: { workflowName: string }) {
  const [runId, setRunId] = useState<string | null>(null);
  const run = useWorkflowRun(fabric, runId);

  return (
    <div>
      <button
        disabled={runId !== null && !run.isComplete}
        onClick={async () => {
          // submit() is a normal POST — works in the browser with the same
          // dynamic token provider, no proxy needed.
          const r = await fabric.workflows.runs.submit(workflowName, {
            input: { topic: 'quarterly trends' },
          });
          setRunId(r.id);
        }}
      >
        Run workflow
      </button>

      <progress value={run.progressPercent} max={100} />
      <p>{run.currentStep ?? (runId ? 'waiting…' : 'idle')}</p>

      {run.error && <p>Error: {run.error.message}</p>}
      {run.isComplete && <p>Done: {run.runStatus}</p>}
    </div>
  );
}

State surface:

| Field | Type | Description | |-------------------|---------------------------------------------------|-----------------------------------------------| | nodes | PipelineNode[] | All nodes seen so far with current status | | currentStep | string \| null | Key of the node currently running | | progressPercent | number | 0–100 based on terminal node count | | isComplete | boolean | true once a terminal run event arrives | | runStatus | 'completed' \| 'failed' \| 'cancelled' \| null | Final status, null while running | | connected | boolean | Whether the SSE stream is currently connected | | error | Error \| null | Most recent stream error | | abort | () => void | Manually tear down the stream |

The hook cleans up on unmount automatically. Authorization errors (403 on connect, expired token, etc.) surface on error rather than throwing.

Direct usage without the hook

If you prefer to drive watch() yourself:

const watcher = fabric.workflows.runs.watch(run.id, {
  onNodeStarted:   (key)          => console.log(`${key} started`),
  onNodeCompleted: (key, payload) => console.log(`${key} done`, payload),
  onRunCompleted:  (payload)      => console.log('run done', payload),
  onError:         (err)          => console.error(err),
});

// Later — e.g. on route change:
watcher.abort();

Choosing between the two React hooks

| Hook | Transport | Auth | Use when | |-------------------------|--------------------------------|--------------------------------------------------|-------------------------------------------------------------------------| | useWorkflowRun | fetch + ReadableStream | Bearer token from dynamic provider | You want direct browser → Fabric streaming, no proxy hop | | usePipelineProgress | Native EventSource | Cookies / same-origin only (no headers possible) | You already proxy SSE through your own server via createSSEResponse() |

Both are legitimate. useWorkflowRun is strictly newer; reach for it when starting fresh.

Deployment checklist

  • CORS: your Fabric API must include the caller's origin in CORS_ALLOWED_ORIGINS, and the allow_headers list must cover Authorization, Cache-Control, and Last-Event-ID. The default tower_http::cors config that ships with fabric serve already does this.
  • Token endpoint: implement GET /api/fabric-token (or equivalent) on your own backend that returns { token: "..." } scoped to the logged-in user. Keep token TTL short (default is 2 hours).
  • Long-running streams: if a stream stays open past the token's lifetime without disconnecting, the server may or may not terminate it depending on your Fabric deployment's auth policy. On reconnect the provider is re-invoked, so the token refreshes automatically after any drop.

Node Logs

Fetch stdout/stderr excerpts for completed workflow nodes:

const attempts = await fabric.workflows.runs.getNodeAttempts(run.id, 'my-node-key');
for (const attempt of attempts) {
  console.log('stdout:', attempt.stdout_excerpt);
  console.log('stderr:', attempt.stderr_excerpt);
  console.log('duration:', attempt.duration_ms, 'ms');
}

Organizations

const org = await fabric.organizations.create({ name: 'Acme Corp' });
const { items: orgs } = await fabric.organizations.list({ limit: 20 });
const details = await fabric.organizations.get(org.id);

// Members and teams
const { items: members } = await fabric.organizations.members(org.id);
const { items: teams } = await fabric.organizations.teams(org.id);

// Usage and audit
const usage = await fabric.organizations.usage(org.id);
const { items: logs } = await fabric.organizations.auditLogs(org.id);

// Budget
const budget = await fabric.organizations.getBudget(org.id);
await fabric.organizations.setBudget(org.id, 500.00);

// Secrets
await fabric.organizations.createSecret(org.id, { name: 'OPENAI_KEY', value: 'sk-...' });
const secrets = await fabric.organizations.listSecrets(org.id);
await fabric.organizations.deleteSecret(org.id, 'OPENAI_KEY');

Teams

const team = await fabric.teams.create({
  organization_id: org.id,
  name: 'Engineering',
});
const details = await fabric.teams.get(team.id);

Assets

// Upload a file
const asset = await fabric.assets.upload(fileBlob, {
  contentType: 'image/png',
  filename: 'avatar.png',
});

// List assets
const { items: assets } = await fabric.assets.list();

// Get a signed download URL
const { url } = await fabric.assets.signedUrl(asset.id, 3600); // 1 hour expiry

Galleries

const gallery = await fabric.galleries.create({
  name: 'Portraits',
  kind: 'portrait',
  tags: ['professional'],
});
const { items: galleries } = await fabric.galleries.list();

// Add/list/remove items
const item = await fabric.galleries.addItem(gallery.id, {
  asset_id: 'asset-uuid',
  label: 'Professional Woman #1',
  tags: ['female', 'business'],
});
const { items } = await fabric.galleries.listItems(gallery.id);
await fabric.galleries.removeItem(item.id);
await fabric.galleries.delete(gallery.id);

API Keys

const { id, secret } = await fabric.apiKeys.create({
  description: 'CI/CD key',
  scopes: ['workflows:execute', 'assets:read'],
});
// `secret` is only returned at creation time

const { items: keys } = await fabric.apiKeys.list();
const key = await fabric.apiKeys.get(id);

await fabric.apiKeys.disable(id);
const rotated = await fabric.apiKeys.rotate(id); // new secret issued
await fabric.apiKeys.delete(id);

Invitations

const invite = await fabric.invitations.create({
  organization_id: org.id,
  email: '[email protected]',
  role: 'member',
});
await fabric.invitations.accept(invite.id);
await fabric.invitations.revoke(invite.id);

Permissions

// Check a single permission
const { allowed } = await fabric.permissions.check({
  resource: `organization:${org.id}`,
  action: 'update',
});

// Batch check
const results = await fabric.permissions.checkBatch([
  { resource: `organization:${org.id}`, action: 'read' },
  { resource: `organization:${org.id}`, action: 'delete' },
]);

// Grant / revoke
const perm = await fabric.permissions.grant({
  principal_id: 'user-uuid',
  resource_type: 'organization',
  resource_id: org.id,
  action: 'update',
});
await fabric.permissions.revoke(perm.id);

Webhooks

const webhook = await fabric.webhooks.create(org.id, {
  url: 'https://example.com/webhook',
  event_filter: ['workflow.run.completed', 'workflow.run.failed'],
});
const { items: webhooks } = await fabric.webhooks.list(org.id);
await fabric.webhooks.update(webhook.id, { active: false });
const { items: deliveries } = await fabric.webhooks.deliveries(webhook.id);
await fabric.webhooks.delete(webhook.id);

Service Accounts

const sa = await fabric.serviceAccounts.create({ name: 'ci-bot' });
const { items: accounts } = await fabric.serviceAccounts.list();

const apiKey = await fabric.serviceAccounts.createApiKey(sa.id);
const keys = await fabric.serviceAccounts.listApiKeys(sa.id);
await fabric.serviceAccounts.rotateApiKey(sa.id, apiKey.id);
await fabric.serviceAccounts.revokeApiKey(sa.id, apiKey.id);

await fabric.serviceAccounts.disable(sa.id);
await fabric.serviceAccounts.enable(sa.id);

OAuth Clients

const client = await fabric.oauth.createClient({ client_name: 'My App' });
const clients = await fabric.oauth.listClients();

const tokens = await fabric.oauth.token(client.client_id, 'client-secret');
const refreshed = await fabric.oauth.refresh(tokens.refresh_token);
await fabric.oauth.revoke(tokens.access_token);

await fabric.oauth.deleteClient(client.id);

Current User

const me = await fabric.me.get();
const myOrgs = await fabric.me.organizations();
const myTeams = await fabric.me.teams();
const myPerms = await fabric.me.permissions();

Admin

// Dev bootstrap (creates initial org + admin)
await fabric.admin.bootstrap();

// Concurrency limits
const limits = await fabric.admin.listConcurrencyLimits();
await fabric.admin.setConcurrencyLimit('org:uuid', 10);

Pagination

All list endpoints return PaginatedResponse<T>:

const { items, pagination } = await fabric.organizations.list({ limit: 20 });
// pagination: { count: 20, has_more: true, next_cursor: "20" }

Auto-pagination

import { paginate, paginateAll } from '@fabric-platform/sdk';

// Async iterator — page by page
for await (const page of paginate((params) => fabric.organizations.list(params))) {
  for (const org of page) {
    console.log(org.name);
  }
}

// Collect everything into one array
const allOrgs = await paginateAll((params) => fabric.organizations.list(params));

Error Handling

All errors are typed with status code, error code, request ID, and trace ID:

import {
  FabricError,
  FabricAuthError,
  FabricNotFoundError,
  FabricConflictError,
  FabricRateLimitError,
} from '@fabric-platform/sdk';

try {
  await fabric.organizations.create({ name: 'Acme' });
} catch (err) {
  if (err instanceof FabricConflictError) {
    console.log('Slug already taken');
  } else if (err instanceof FabricAuthError) {
    console.log('Re-authenticate');
  } else if (err instanceof FabricRateLimitError) {
    console.log(`Retry after ${err.retryAfterMs}ms`);
  } else if (err instanceof FabricNotFoundError) {
    console.log('Not found');
  } else if (err instanceof FabricError) {
    console.log(`${err.code}: ${err.message} (request: ${err.requestId})`);
  }
}

Error hierarchy:

| Class | Status | |-------|--------| | FabricValidationError | 400, 422 | | FabricAuthError | 401 | | FabricForbiddenError | 403 | | FabricNotFoundError | 404 | | FabricConflictError | 409 | | FabricRateLimitError | 429 | | FabricServerError | 5xx |

All extend FabricError which extends Error.

Event Types

type EventKind =
  // Run lifecycle
  | 'workflow.run.created' | 'workflow.run.queued' | 'workflow.run.promoted'
  | 'workflow.run.started' | 'workflow.run.completed'
  | 'workflow.run.failed'  | 'workflow.run.cancelled'
  // Node lifecycle
  | 'workflow.node.ready'    | 'workflow.node.claimed'
  | 'workflow.node.started'  | 'workflow.node.progress'
  | 'workflow.node.completed'| 'workflow.node.failed'
  | 'workflow.node.retried'  | 'workflow.node.skipped'
  | 'workflow.node.cancelled'| 'workflow.node.waiting_for_event'
  | 'workflow.node.resumed';

Next.js Integration Patterns

Server-side client factory

// lib/fabric.ts
import { cookies } from 'next/headers';
import { FabricClient, FabricAuthError } from '@fabric-platform/sdk';

export function getFabricClient() {
  return new FabricClient({
    baseUrl: process.env.FABRIC_URL,
    auth: async () => {
      const token = cookies().get('fabric_access_token')?.value;
      if (!token) throw new FabricAuthError({
        code: 'no_session', status: 401, message: 'Not authenticated',
      });
      return token;
    },
  });
}

Login API route

// app/api/auth/login/route.ts
import { FabricClient } from '@fabric-platform/sdk';
import { cookies } from 'next/headers';

export async function POST(req: Request) {
  const { email, password } = await req.json();
  const fabric = new FabricClient({ baseUrl: process.env.FABRIC_URL });
  const auth = await fabric.auth.login(email, password);

  cookies().set('fabric_access_token', auth.access_token, {
    httpOnly: true,
    secure: true,
    maxAge: auth.expires_in,
  });
  cookies().set('fabric_refresh_token', auth.refresh_token, {
    httpOnly: true,
    secure: true,
    maxAge: 30 * 86400,
  });

  return Response.json({ user: auth.user });
}

SSE proxy route

// app/api/workflows/[runId]/events/route.ts
import { cookies } from 'next/headers';

export async function GET(req: Request, { params }: { params: { runId: string } }) {
  const token = cookies().get('fabric_access_token')?.value;
  const upstream = await fetch(
    `${process.env.FABRIC_URL}/v1/workflow-runs/${params.runId}/events`,
    { headers: { Authorization: `Bearer ${token}`, Accept: 'text/event-stream' } },
  );
  return new Response(upstream.body, {
    headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache' },
  });
}

Development

npm run build       # Build with tsup (ESM + CJS + declarations)
npm run typecheck   # Type-check without emitting
npm test            # Run tests with vitest
npm run dev         # Watch mode