Security
khotan mounts a single catch-all route at /api/khotan/[...all] that powers the dashboard, CLI, and runtime. You secure its management endpoints with the authorize hook.
That route exposes management endpoints for plugs, flows, variables, wires, webhook handlers, runs, mappings, caches, and resources.
Unauthenticated by default
On a deployed app, anyone who can reach/api/khotan/* can read and write plug credentials, trigger flows, and edit mappings until you add an authorize hook.The `authorize` hook
Pass an authorize function to the khotan() factory. It receives the raw Request and returns true to allow the request or false to reject it with 401.
export type KhotanAuthorize = (
request: Request,
) => boolean | Promise<boolean>;The hook runs inside the catch-all handler on every management request, before any route logic. It is not middleware — there is nothing to add to middleware.ts, and it works on any host.
const khotanData = khotan({
adapter: drizzleAdapter(db),
authorize: async (request) => {
const session = await auth.api.getSession({ headers: request.headers });
return Boolean(session?.user);
},
});Because you get the raw Request, you can authorize however your app already does — session cookies, bearer tokens, API keys, or role checks.
// Restrict the management API to admins only
authorize: async (request) => {
const session = await auth.api.getSession({ headers: request.headers });
return session?.user?.role === "admin";
},If the hook throws, the request is treated as unauthorized and rejected with 401.
What it gates
When authorize is set, every management route must pass it: plugs, flows, variables, wires, webhook-handlers, runs, mappings, caches, and resources.
What is exempt
Three route families skip the authorize hook because they carry their own protection:
- Inbound webhooks (
POST .../webhook/:plug) — verified per-plug via the wire'sonVerifysignature check. - The cron dispatcher (
.../cron) — protected byCRON_SECRET. - Debug routes (
.../debug...) — gated byKHOTAN_DEBUGand disabled in production.
If you want those behind your own auth too, that is a config decision — they are intentionally exempt by default so external callers (webhook providers, platform cron) can reach them.
The 401 response
A rejected request returns:
{ "error": "Unauthorized" }with HTTP status 401. The scaffolded dashboard components (Hub, debug, logs, mappings, panels) route every API call through a shared khotanFetch helper and render a simple ApiErrorStateon failure — so an unauthenticated user sees a clean "Access denied" state instead of a blank screen. The same component handles 403, 404, 429, 5xx, and network errors. It lives in components/khotan/api-state.tsx and is yours to restyle.
CLI access
The khotan CLI talks to your running dev server over /api/khotan/*, so it passes through the same authorize gate. To avoid forcing a terminal login, the CLI authenticates with a token derived from KHOTAN_SECRET — it never sends the raw secret.
- It sends a timestamped HMAC of
KHOTAN_SECRETasAuthorization: KhotanCLI <timestamp>.<hmac>. Because it's a one-way hash, the encryption key never travels over the wire. - The factory accepts this token only when NODE_ENV !== 'production' and only within a ~60s freshness window, so a token captured from a dev log can't be replayed.
- So
khotan plug,plug vars,flows,mappings, andwirework against your local dev server as long asKHOTAN_SECRETis set in your environment or.env.local.
Commands that only touch the filesystem or run migrations (init, add, generate, migrate) never call the API and are unaffected. In production the CLI token is rejected, so the deployed management API is governed solely by your authorize hook.
Secrets and hardening
KHOTAN_SECRET— encrypts plug credentials and wire metadata at rest. Without it, those values are stored unencrypted. Treat it like any other production secret; if you lose it you lose access to encrypted vars.CRON_SECRET— protects the/api/khotan/crondispatcher. It fails closed in production: if it is unset on a deployed app, the dispatcher is rejected. In development it stays open for local testing.KHOTAN_DEBUG— enables the debug routes. They are disabled in production regardless of this flag, so the credentialed debug proxy can never be reached on a deployed app.
Startup warnings
The factory logs a one-time warning at construction if authorize is not configured (the management API is public) or if no KHOTAN_SECRET is set (credentials are unencrypted). Treat both as required for any deployed app.
Production checklist
- Set an
authorizehook that reflects who should manage khotan. - Set
KHOTAN_SECRETto a random 32+ byte value. - Set
CRON_SECRETand have your platform cron sendAuthorization: Bearer <CRON_SECRET>. - Leave
KHOTAN_DEBUGunset in production. - Confirm unauthenticated requests to
/api/khotan/plugsreturn401.