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
- A failure throws
WhisperErrorwith the server's exact, secret-free.detail/.title/.status— an RFC 7807 problem object, never a stack trace or a bare 500. - "Not an agent" is not an error:
verifyreturnsfalse,resolve/rdapreturnnull. - Every call accepts
{ timeoutMs, signal, fetch, endpoints }. Default timeout is 10s and is enforced internally — it never hangs even on a runtime whosefetchignoresAbortSignal. endpointslets you point at a self-hosted or pre-prod deployment;fetchlets you inject a mock for tests.
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.