# 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`](https://github.com/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](https://www.npmjs.com/package/whisper-edge), source [`whisper-sec/whisper-edge`](https://github.com/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.

```bash
npm i whisper-edge
```

```ts
// 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](/docs/rdap) and [`whisper verify`](/docs/verifiable-identity) 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:

```ts
// 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](/docs/verify). With `whisper-edge`:

```ts
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](https://www.rfc-editor.org/rfc/rfc6698)) 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`](/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:

```ts
// 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`:

```ts
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`](/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](https://www.rfc-editor.org/rfc/rfc6066)) 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](/docs/forward-gateway) for the full mechanism.

```ts
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`](https://github.com/whisper-sec/whisper-examples/tree/main/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`](https://github.com/whisper-sec/whisper-edge/tree/main/examples/cloudflare) |
| Vercel (Edge) | [`examples/vercel`](https://github.com/whisper-sec/whisper-edge/tree/main/examples/vercel) |
| Netlify Functions | [`examples/netlify`](https://github.com/whisper-sec/whisper-edge/tree/main/examples/netlify) |
| Deno Deploy | [`examples/deno`](https://github.com/whisper-sec/whisper-edge/tree/main/examples/deno) |
| AWS Lambda | [`examples/lambda`](https://github.com/whisper-sec/whisper-edge/tree/main/examples/lambda) |
| Supabase Edge | [`examples/supabase`](https://github.com/whisper-sec/whisper-edge/tree/main/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`](/integrations).

## Errors, timeouts, no surprises

- A failure throws `WhisperError` with 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: `verify` returns `false`, `resolve`/`rdap` return `null`.
- Every call accepts `{ timeoutMs, signal, fetch, endpoints }`. Default timeout is 10s and is enforced internally — it never hangs even on a runtime whose `fetch` ignores `AbortSignal`.
- `endpoints` lets you point at a self-hosted or pre-prod deployment; `fetch` lets you inject a mock for tests.

```ts
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](/docs/cloudflare) for the full worker + MCP walkthrough, or [DANE](/docs/dane) for the TLSA mechanism `verify()` checks under the hood.
