@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.
Maintainers
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 loginNode 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 withnpm i -g @duct-sdk/sdkand useduct <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.yaml4. Push:
npx @duct-sdk/sdk push5. 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:
- List all routes where a user lands with identity-specific state (order ID, document ID, profile slug).
- Mark which of those a conversation could meaningfully route to.
- Declare only those in
deeplinks[]with matchingstateFields.
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_KEYserver-side only — never in client bundles orNEXT_PUBLIC_*/VITE_*vars - Use least privilege when marking actions
agentAccessible: true sideEffects: trueon any action that creates, updates, deletes, or charges- Rotate keys in the dashboard if credentials are exposed
