Whisper · Docs
SDKs & CLI

Edge SDK (serverless)

whisper-edge is the zero-dependency, fetch-only SDK for runtimes with no child_process, no sockets, and no filesystem — Cloudflare Workers, Vercel Edge, Deno, Netlify, Lambda, Supabase. Verify any agent keylessly; egress through a real /128 with one secret.

npm i whisper-id won't work on these targets — it shells out to the whisper CLI, and there's no binary to spawn on a V8 isolate. What every one of them does have is a global fetch, so whisper-edge is written to need nothing else.

Why the Node SDK doesn't fit

whisper-id (npm, whisper-sec/whisper-node) wraps the whisper Go binary and Node's dns/tls/child_process modules. All three are unavailable on an edge isolate:

Node SDK needs Cloudflare Workers Vercel Edge Deno Deploy Netlify Functions Lambda (Node 18+) Supabase Edge
child_process (spawn whisper) no no no no yes, but cold-start-hostile no
dns/tls sockets no (cloudflare:sockets is HTTP-CONNECT-only) no no no yes no
global fetch yes yes yes yes yes yes

Every one of those runtimes ships a global fetch. So whisper-edge (npm, source whisper-sec/whisper-edge) is written to need nothing else: zero runtime dependencies, pure fetch, TypeScript types included. Same two-tier shape as every other Whisper surface — keyless verification for anyone, the full control plane for a key-holder — just ported to a runtime with no built-ins.

npm i whisper-edge
// Deno / Supabase can skip the install entirely:
import { verify, resolve, control } from "npm:whisper-edge@^0.3.0";

Keyless tier: verify, verifyDetails, resolve, rdap, rdapDomain

No API key required — this is the same public trust surface RDAP and whisper verify expose, just callable from an isolate. Given the public demo agent 2a04:2a01:eb5a:ca74:cef2:2a:323d:40d4:

On an isolate there's no dig and no socket — the "raw" way is a single fetch at the keyless endpoint. The SDK is that call, typed and null-safe:

// raw — one fetch, no package
const addr = "2a04:2a01:eb5a:ca74:cef2:2a:323d:40d4";
const r = await fetch(`https://rdap.whisper.online/verify-identity?ip=${addr}`);
const { is_whisper_agent, dane_ok, jws_ok } = await r.json();

The full chain behind that verdict (PTR → forward-confirm → DANE-EE TLSA → the JWS identity doc), and how to re-derive it from a terminal, is in Verify an agent. With whisper-edge:

import { verify, verifyDetails, resolve, rdap, rdapDomain } from "whisper-edge";

const addr = "2a04:2a01:eb5a:ca74:cef2:2a:323d:40d4";

await verify(addr);                 // → true/false, "is this a real Whisper agent"
await verifyDetails(addr);          // → { is_whisper_agent, fqdn, dane_ok, jws_ok, evidence, ... }
await resolve(addr);                // → { fqdn, operator, tenant, daneOk, jwsOk, rdapUrl } | null
await rdap(addr);                   // → the raw RDAP object (RFC 9083)
await rdapDomain("acef2002a323d40d4.demo.agents.whisper.online"); // RDAP by name

dane_ok is the field that matters: DANE (RFC 6698) is the trust anchor here — a DNSSEC-signed TLSA record pins the agent's certificate, not a public CA. jws_ok additionally checks the agent's signed identity document. A 404 from the verify endpoint means "not a Whisper agent" and both verify/resolve fold that into false/null — it is never a thrown error. Anything else wrong (bad address, transport failure, a real server fault) throws a WhisperError with the server's exact RFC 7807 problem detail, never an opaque 500 — see /docs/rdap for the full response shape.

Control tier: control(apiKey)

The same one control-plane verb every Whisper surface uses — CALL whisper.agents({op, args}) over HTTPS — reachable from an isolate with your key in the X-API-Key header, never a URL parameter, never logged.

Raw, it's one HTTPS POST; with whisper-edge the Cypher stays internal and you call methods:

// raw — one fetch
await fetch("https://graph.whisper.security/api/query", {
  method: "POST",
  headers: { "X-API-Key": apiKey, "content-type": "application/json" },
  body: JSON.stringify({ query: "CALL whisper.agents({op:'list', args:{}})" }),
});

With whisper-edge:

import { control } from "whisper-edge";

const c = control(process.env.WHISPER_API_KEY!);   // read from a secret, never hard-coded

const created = await c.register({ name: "scout", email: "ops@acme.co" });
// created.records[0].address → the new routable /128 · .api_key → shown once

await c.list();                                       // your tenant's agents
await c.policy({ block: ["ads.example"], default: "allow" });
await c.logs({ kind: "dns", from: "-1h", limit: 200 });
await c.revoke("scout");                              // withdraws /128, PTR, tokens, key

Every method returns a normalised ControlResult{ columns, rows, records, raw, status }records being the column-keyed view you'll actually read, raw the verbatim JSON body for when you need it.

Egress at the edge: transport: "forward"

Verifying identity is half the point; the other half is an agent's traffic actually sourcing from its routable /128. On a normal host that's a WireGuard interface or a SOCKS5 proxy (see /docs/connect) — but Workers-class runtimes give you neither a TUN device nor a raw outbound TCP socket you fully control. cloudflare:sockets gets you a connect() that opens a CONNECT-style tunnel, but workerd's startTls() pins the TLS server name to the proxy you called connect() on — it cannot then layer a second TLS handshake for the actual https:// target inside that tunnel. Concretely: SNI (RFC 6066 §3) gets set once, to the wrong host, and the target's certificate never matches.

whisper-edge's answer is agentEgress(apiKey, agent, { transport: "forward" }): a single HTTPS hop to the fetch-forward gateway (forward.whisper.online) that performs the outbound request server-side, sourced from the agent's /128, and streams the response back — no nested TLS, no raw socket, and the egress credential rides inside the outer TLS session rather than as a plaintext CONNECT header. See Forward gateway for the full mechanism.

import { verify, rdap, agentEgress } from "whisper-edge";

const egress = await agentEgress(env.WHISPER_API_KEY, "scout", { transport: "forward" });
const upstream = await egress.fetch("https://v6.ident.me/");
console.log(await upstream.text());        // → 2a04:2a01:eb5a:ca74:cef2:2a:323d:40d4
egress.close();

The full Worker — keyless ?addr= verification plus keyed ?egress=1 proof that seen_ip equals the agent's address — is in whisper-examples/cloudflare, including an MCP-tool build (agents-sdk/) that exposes the same two tiers as tools on a Durable-Object McpAgent.

Six runtimes, one package

Runtime Example
Cloudflare Workers examples/cloudflare
Vercel (Edge) examples/vercel
Netlify Functions examples/netlify
Deno Deploy examples/deno
AWS Lambda examples/lambda
Supabase Edge examples/supabase

Every sample is two-tier by construction: ?addr= works with no key at all; ?op=list (or the runtime's egress query) needs the key. Copy one, deploy it, done — see the full catalog on /integrations.

Errors, timeouts, no surprises

await verify(addr, { timeoutMs: 3000 });
await resolve(addr, { fetch: myFetch, endpoints: { verify: "https://rdap.example" } });

Full API surface: keyless verify · verifyDetails · resolve · rdap · rdapDomain; control register · identity · list · agent · policy · logs · connect · agentEgress · revoke · agents(op, args) · query(cypher); low-level buildAgentsQuery · escapeCypherString · decodeEnvelope · WhisperError. Types ship with the package — no @types/ install.


Next: Cloudflare integration for the full worker + MCP walkthrough, or DANE for the TLSA mechanism verify() checks under the hood.