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

@duct-sdk/sdk

v0.3.11

Published

Duct SDK — A shell (like terminal) that knows when to talk, when to show, and when to hand off.

Readme

@duct-sdk/sdk

Let agents use your product safely.

The Duct SDK scans your codebase, generates a manifest, and provides embed components so humans and agents can call your existing APIs through one permissioned surface.

  • Humans — chat or use an embedded shell; Duct runs actions or hands off via signed deeplinks
  • Agents — structured, permissioned API access from the same manifest — no scraping, no screen automation

Credentials and shell provisioning are managed by the Duct dashboard.


What integration looks like

Duct does not replace your API or rewrite your product. You wire four things in your codebase, and Duct does the rest:

| What you add | Where | |---|---| | Token minting endpoint | POST /api/duct/token — one server route | | Auth middleware fallback | verifyDuctToken added to your existing Bearer-token check | | Deeplink receiver | POST /api/duct/receive — verifies signed nav tokens | | Widget embed | <DuctShell> once in your root layout |

The manifest (duct.config.ts) declares which routes, actions, and deeplinks exist. duct push uploads it to Duct. Your API stays unchanged.


Quickstart

1. Create your shell in the Duct dashboard and copy your API key.

2. Authenticate the CLI:

npx @duct-sdk/sdk login

Node 22+ note: if the CLI exits with an ERR_REQUIRE_ESM error, run with node --experimental-require-module $(npx --no which duct) <command> or install the package globally with npm i -g @duct-sdk/sdk and use duct <command>.

3. Generate a manifest:

# Recommended first run — instant editable template, no LLM required
npx @duct-sdk/sdk init --manual

# AI-assisted scan of your codebase once your API surface is stable
npx @duct-sdk/sdk init

# From an existing OpenAPI / Swagger spec
npx @duct-sdk/sdk init --api-spec openapi.yaml

4. Push:

npx @duct-sdk/sdk push

5. Embed the shell:

// Next.js App Router (root layout)
import { DuctShell } from '@duct-sdk/sdk/next';

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        {children}
        <DuctShell
          shellId={process.env.NEXT_PUBLIC_DUCT_SHELL_ID!}
          shellHost={process.env.NEXT_PUBLIC_DUCT_SHELL_HOST}
          onTokenRequest={async () => {
            const res = await fetch('/api/duct/token', { method: 'POST', credentials: 'include' });
            return (await res.json()).token ?? '';
          }}
          preset="tall"           // "tall" | "square" | "large"
          position="bottom-right" // starting corner; bubble + panel are draggable
          theme="system"          // "system" | "light" | "dark"
        />
      </body>
    </html>
  );
}

Import guide — use the right subpath

| Import | Use when | |---|---| | @duct-sdk/sdk/server | Any Node.js / Edge backend — token helpers, config, introspect. No React. Safe in Express, Fastify, Next.js API routes, Remix loaders, SvelteKit hooks. | | @duct-sdk/sdk/next | Next.js App Router frontend (requires React) | | @duct-sdk/sdk/react | Plain React or non-Next frameworks (requires React) | | @duct-sdk/sdk/vite | Vite + Express (requires React) | | @duct-sdk/sdk/remix | Remix (requires React) | | @duct-sdk/sdk/sveltekit | SvelteKit | | @duct-sdk/sdk (root) | Full bundle — only in React SSR frontends where React is already installed. Crashes plain Express/Fastify backends. |

// ✅ Correct — server route (Express, Next.js API, Remix loader, etc.)
import { generateDuctToken, verifyDuctToken } from '@duct-sdk/sdk/server';

// ❌ Wrong — pulls in React adapters, crashes if React is not installed
import { generateDuctToken } from '@duct-sdk/sdk';

CLI reference

| Command | Description | |---|---| | duct login | Authenticate with your Duct API key | | duct init | AI-assisted scan — generates config + shell files | | duct init --manual | Instant template without LLM (recommended first run) | | duct init --update | Re-scan only files changed since last push | | duct init --api-spec <path> | Generate manifest from OpenAPI / Swagger spec | | duct init --no-agents | Skip AI scan; blank template | | duct push | Push duct.config.ts to Duct | | duct pull <hash> | Restore a manifest version from push history | | duct promote --from <id> --to <id> | Copy manifest to another environment shell | | duct scan | Detect drift between live API and manifest | | duct checkup | Check env config and service connectivity |

Install globally to drop the npx: npm i -g @duct-sdk/sdk.


Framework support

| Framework | Import | Client env vars | |---|---|---| | Next.js (App Router) | @duct-sdk/sdk/next | NEXT_PUBLIC_DUCT_SHELL_ID, NEXT_PUBLIC_DUCT_SHELL_HOST | | Vite | @duct-sdk/sdk/vite | VITE_DUCT_SHELL_ID, VITE_DUCT_SHELL_HOST | | Remix | @duct-sdk/sdk/remix | VITE_DUCT_SHELL_ID, VITE_DUCT_SHELL_HOST | | SvelteKit | @duct-sdk/sdk/sveltekit | PUBLIC_DUCT_SHELL_ID, PUBLIC_DUCT_SHELL_HOST | | React (generic) | @duct-sdk/sdk/react | any | | Express / Fastify / other Node | @duct-sdk/sdk/server | — (server only) |

Vite + Express

Keep environment variables in three files:

/.env              → DUCT_SECRET_KEY, DUCT_SHELL_ID  (server, never commit)
/backend/.env      → same values for the Express process if run separately
/frontend/.env     → VITE_DUCT_SHELL_ID, VITE_DUCT_SHELL_HOST  (public)

Express token route:

import express from 'express';
import { generateDuctToken } from '@duct-sdk/sdk/server'; // ← /server, not root

const router = express.Router();
router.post('/api/duct/token', express.json(), async (req, res) => {
  if (!req.user) return res.status(401).json({ error: 'unauthenticated' });
  const { token } = await generateDuctToken({
    secretKey: process.env.DUCT_SECRET_KEY!,
    shellId: process.env.DUCT_SHELL_ID!,
    userSession: { id: req.user.id, email: req.user.email },
    expirySeconds: 300,
  });
  res.json({ token });
});

API auth middleware:

import { verifyDuctToken } from '@duct-sdk/sdk/server';

app.use(async (req, res, next) => {
  const header = req.headers.authorization ?? '';
  if (!header.startsWith('Bearer ')) return res.status(401).end();
  const raw = header.slice(7);
  // Try your own JWT first
  const mine = await verifyMyJwt(raw).catch(() => null);
  if (mine) { req.user = mine; return next(); }
  // Fall back to Duct delegated token
  const duct = await verifyDuctToken(raw, { secretKey: process.env.DUCT_SECRET_KEY! });
  if (duct.valid) { req.user = duct.payload!.session; return next(); }
  res.status(401).end();
});

Webhook configuration

Webhook URLs (deeplink callback, event stream) and the signing secret are not part of duct.config.ts and are not pushed via duct push. Configure them in the Duct dashboard → Shell → Settings → Webhooks.

Your endpoint receives a POST with an X-Duct-Signature HMAC-SHA256 header for verification. Webhooks are optional — the shell, widget, and action flow work without them.


Deeplinks — route inventory first

Before declaring deeplinks, inventory every navigable page in your app:

  1. List all routes where a user lands with identity-specific state (order ID, document ID, profile slug).
  2. Mark which of those a conversation could meaningfully route to.
  3. Declare only those in deeplinks[] with matching stateFields.

A targetRoute must match an entry in routes[]. The deeplink receiver must verify the signed token server-side before using any state value — never trust raw query params.


Security

  • Keep DUCT_SECRET_KEY server-side only — never in client bundles or NEXT_PUBLIC_* / VITE_* vars
  • Use least privilege when marking actions agentAccessible: true
  • sideEffects: true on any action that creates, updates, deletes, or charges
  • Rotate keys in the dashboard if credentials are exposed