@duct-sdk/sdk
v0.5.13
Published
Duct SDK — manifest, CLI, and embed components for permissioned human and agent access to your product APIs.
Maintainers
Readme
██████╗ ██╗ ██╗ ╔██████ ████████╗
██╔══██╗ ██║ ██║ ╚════╗██ ╚══██╔══╝
██║ ██║ ██║ ██║ ║██ ██║
██║ ██║ ██║ ██║ ║██ ██║
██████╔╝ ╚██████╔╝ ╔██████╝ ██║
╚═════╝ ╚═════╝ ╚═════╝ ╚═╝@duct-sdk/sdk
Let agents use your product safely.
The official Duct SDK and CLI. Scan your codebase, declare a manifest, push it to Duct, and embed a permissioned chat shell — so humans and AI agents call your existing APIs through one audited surface.
| Audience | What you get | | ------------- | ----------------------------------------------------------------------------------------------- | | Humans | Embedded chat shell, structured results, signed deeplink handoff into your app | | Agents | Permissioned API access from the same manifest — no scraping or screen automation | | Operators | Shell credentials, action tiers, and usage from the configured Duct dashboard |
Documentation
| Resource | Description |
| --- | --- |
| Developer docs (SITE_URL/docs) | Quickstart, CLI, frameworks, agents, threat model |
| AI integration prompt (SITE_URL/docs/integration-prompt) | Full DUCT_CONFIG_PROMPT.md — copy into Cursor, Claude Code, Codex, or any agentic IDE |
| TOKEN_SPEC.md | Token prefixes, claims, TTLs, and cross-language verify snippets |
| Changelog (SITE_URL/changelog) | Platform release notes (includes SDK) |
AI-assisted integration
Duct ships a single integration prompt (DUCT_CONFIG_PROMPT.md) for greenfield setup or auditing an existing duct.config.ts. Paste it into your coding assistant with the product repo open; the agent generates or reviews manifest fields, the four wiring points, env vars, and a ship checklist.
Get the prompt: open SITE_URL/docs/integration-prompt on your configured deployment. The page is expandable and copy-ready, with platform URLs resolved for that environment.
Typical flow:
- Run
duct initorduct init --manualfor a first draft. - Paste
DUCT_CONFIG_PROMPT.mdfrom the docs page to audit wiring and permissions. duct push, embed<DuctShell>, smoke-test in the dashboard playground.
Install
npm install @duct-sdk/sdk
# or run without installing:
npx @duct-sdk/sdk <command>Global CLI (optional): npm i -g @duct-sdk/sdk → duct <command>.
Node 22+: if the CLI exits with
ERR_REQUIRE_ESM, usenode --experimental-require-module $(npx --no which duct) <command>or install globally.
Quickstart
1. Create a shell in the Duct dashboard and copy your API key.
2. Log in:
npx @duct-sdk/sdk login3. Generate a manifest:
npx @duct-sdk/sdk init --manual # instant template (recommended first run)
npx @duct-sdk/sdk init # AI-assisted scan when your API is stable
npx @duct-sdk/sdk init --api-spec openapi.yaml4. Push:
npx @duct-sdk/sdk push5. Embed (Next.js App Router example):
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"
position="bottom-right"
theme="system"
/>
</body>
</html>
);
}Four wiring points
Duct does not replace your API. You add four integration points in your codebase:
| # | What | Where |
| --- | ----------------- | ----------------------------------------------------------------- |
| 1 | Token minting | POST /api/duct/token → generateDuctToken |
| 2 | API auth | Existing middleware also accepts verifyDuctToken delegated JWTs |
| 3 | Deeplink receiver | POST /api/duct/receive → verifyDeeplinkToken |
| 4 | Widget embed | <DuctShell> in root layout with onTokenRequest |
duct.config.ts declares routes, actions, deeplinks, and agent permissions. duct push uploads it; your business logic stays unchanged.
Import guide
Use the subpath that matches your runtime. The root export pulls React adapters and will crash plain Node servers.
| Import | Use when |
| ------------------------- | ------------------------------------------------------------------- |
| @duct-sdk/sdk/server | Any Node / Edge backend — tokens, config, introspect. No React. |
| @duct-sdk/sdk/next | Next.js App Router (React) |
| @duct-sdk/sdk/react | React without Next |
| @duct-sdk/sdk/vite | Vite + Express (React) |
| @duct-sdk/sdk/remix | Remix (React) |
| @duct-sdk/sdk/sveltekit | SvelteKit |
| @duct-sdk/sdk | React SSR frontends only — not Express/Fastify APIs |
// Server route (Express, Next.js route handler, Remix loader, …)
import { generateDuctToken, verifyDuctToken } from '@duct-sdk/sdk/server';CLI
| Command | Description |
| ------------------------------------ | ---------------------------------------- |
| duct login | Authenticate with your Duct API key |
| duct init | AI-assisted scan → config + shell files |
| duct init --manual | Template without LLM |
| duct init --update | Re-scan files changed since last push |
| duct init --api-spec <path> | Manifest from OpenAPI / Swagger |
| duct push | Upload duct.config.ts to Duct |
| duct pull <hash> | Restore a manifest version from history |
| duct promote --from <id> --to <id> | Copy manifest between environment shells |
| duct scan | Drift between live API and manifest |
| duct checkup | Env config and service connectivity |
Frameworks
| 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 | framework-specific |
| Express / Fastify / Node | @duct-sdk/sdk/server | — (server only) |
Vite + Express
import express from 'express';
import { generateDuctToken, verifyDuctToken } from '@duct-sdk/sdk/server';
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 });
});Keep secrets in server .env only (DUCT_SECRET_KEY, DUCT_SHELL_ID). Client bundles use public VITE_* shell ID / host vars.
Deeplinks & replay protection
Declare deeplinks only for routes you have inventoried in routes[]. The receiver must verify signed tokens server-side — never trust raw query params.
Deeplink JWTs include a jti claim. Pass a replayStore to verifyDeeplinkToken so each token is single-use:
import {
verifyDeeplinkToken,
createDevReplayStore,
RedisReplayStore,
} from '@duct-sdk/sdk/server';
const replayStore = createDevReplayStore(); // dev / single instance
// const replayStore = new RedisReplayStore(redisClient, 'duct:jti:'); // production
const result = await verifyDeeplinkToken(token, {
secretKey: process.env.DUCT_SECRET_KEY!,
expectedShellId: ductConfig.shellId,
replayStore,
});Details: TOKEN_SPEC.md.
Webhooks
Webhook URLs and signing secrets are configured in the dashboard → Shell → Settings → Webhooks, not in duct.config.ts. Your endpoint verifies X-Duct-Signature (HMAC-SHA256). Webhooks are optional for chat and actions.
Security
- Keep
DUCT_SECRET_KEYserver-side — never inNEXT_PUBLIC_*,VITE_*, or client bundles - Default mutations to
agentAccessible: falseandsideEffects: truewhere appropriate - Rotate keys in the dashboard if credentials are exposed
- Review agent-accessible actions before production — use the integration prompt at
SITE_URL/docs/integration-prompt
Support
- Docs:
SITE_URL/docson your configured deployment - npm: @duct-sdk/sdk
