# Node SDK

`whisper-id` on npm gives a Node agent a real, routable IPv6 identity and a one-line proof of who it is — the keyless half in pure `fetch`, zero dependencies.

A shared NAT or cloud IP has no identity to give: every request looks like it came from your laptop, or your whole fleet from one exit. A Whisper agent gets its own `/128` out of `2a04:2a01::/32` (announced by AS219419) — DNSSEC-signed, DANE-anchored, RDAP-registered — and `whisper-id` is the Node-runtime SDK that gets you there in one `npm i`.

## Two tiers, one package

`whisper-id` follows Postel's Law end to end: liberal about who can use it (no key needed for the checks anyone should be able to run), conservative about what it exposes (the control plane only unlocks with your key, and the key never touches argv or logs).

| Tier | Needs | What you get |
|---|---|---|
| **Keyless** | Nothing — pure `fetch` | `verify`, `verifyDetails`, `rdap`, `egressIp` |
| **Control plane** | `WHISPER_API_KEY` (pure HTTP, still no CLI) | `list`, `identity`, `agent`, `policy`, `logs`, `revoke` |
| **Egress + CLI-backed** | the `whisper` binary on `PATH` | `register`, `egress`, `withEgress`, `ip` |

```sh
npm i whisper-id
```

```js
import { verify } from "whisper-id";
await verify("2a04:2a01:eb5a:ca74:cef2:2a:323d:40d4");   // → true, no key, no CLI
```

```js
import { identity, withEgress } from "whisper-id";        // WHISPER_API_KEY in env
const me = await identity({ label: "my-bot" });            // allocate a routable /128
await withEgress(async (e) => {
  console.log("egress on", e.proxyUrl);                    // curl/axios/got now leave from me.address
});
```

## `whisper-id` vs `whisper-edge` — pick by runtime, not by preference

This is the one distinction people get wrong, so get it right up front:

- **`whisper-id`** (this page) is the **Node-runtime** SDK. Its keyless and control-plane calls are pure `fetch` (Node ≥18's global `fetch`), but `register`, `egress`, `withEgress`, and `ip` **shell out to the `whisper` CLI** to bring up the local egress proxy. It needs a real process and a filesystem — a long-running server, a worker, a CLI tool, a container.
- **[`whisper-edge`](/docs/sdk-edge)** is a **dependency-free, fetch-only** TypeScript SDK for runtimes that have no `child_process` at all — Cloudflare Workers, Vercel Edge, Deno Deploy, Netlify Edge, Lambda, Supabase Edge Functions. It talks the same public HTTP API but never touches a binary.

If your agent runs as a plain Node/Bun process (PM2, systemd, Docker, a Kubernetes pod, a LangGraph/LangChain agent loop, an MCP server), use `whisper-id`. If it runs on someone else's edge runtime, use `whisper-edge`. Both share the same `verify`/`verifyDetails`/`rdap` keyless surface and the same control-plane shape — the only difference is how (or whether) egress gets wired up under the hood.

## The API, in full

### Keyless — no key, no CLI

```js
import { verify, verifyDetails, rdap, egressIp } from "whisper-id";

await verify(addr);          // boolean — server-side DANE + DNSSEC + reverse-DNS + JWS
await verifyDetails(addr);   // { is_whisper_agent, fqdn, operator, dane_ok, jws_ok, ... } | null
await rdap(addr);            // the public RDAP object for the /128, or null
await egressIp();            // the IP this process currently leaves from
```

Under the hood `verify`/`verifyDetails` hit the same keyless gateway you can curl directly (see below) — `verify-identity/<addr>` on `rdap.whisper.online`. `rdap(addr)` fetches the [RDAP](/docs/rdap) document for the `/128`, an [RFC 9082/9083](https://www.rfc-editor.org/rfc/rfc9083)-shaped JSON `ip network` object, the same one `whois`-style tooling resolves via [RFC 7484](https://www.rfc-editor.org/rfc/rfc7484) bootstrap.

### Control plane — with a key, still no CLI

Every call here sends your key only in the `X-API-Key` request header (never argv, never a log line) to `graph.whisper.security`, and throws a `WhisperError` (with `.status`/`.retryAfter`) on failure instead of returning `undefined`:

```js
import { list, identity, agent, policy, logs, revoke } from "whisper-id";
// WHISPER_API_KEY=whisper_live_... in the environment

await policy({ default: "deny", allow: ["api.openai.com", "*.githubusercontent.com"] });
for (const a of await list()) console.log(a.address, a.label);
const recent = await logs({ kind: "dns", limit: 100 });
await revoke(me.agent);   // irreversible — needs the admin:dns scope
```

`identity({ label, contact_email })` is the one-call path to a routable `/128`: it returns `{ agent, address, fqdn, ptr, state }` where `fqdn` looks like `acef2002a323d40d4.<tenant>.agents.whisper.online` and `ptr` is the matching `ip6.arpa` delegation — both live in [authoritative DNS](/docs/dns) the moment the call returns.

### Egress + CLI-backed

```js
import { register, egress, withEgress, ip } from "whisper-id";

const a = await register("checkout-bot", { newKey: true });  // mints agent + its own API key
const e = await egress({ agent: a, tier: "wireguard" });       // routed /128, sets proxy env
console.log(await ip());                                       // → a.address
e.close();                                                      // restores the environment
```

`egress()` sets `HTTP_PROXY`/`HTTPS_PROXY`/`ALL_PROXY` for you, so any proxy-env-aware client — `curl`, `axios`, `got`, most CLI tools spawned from your process — leaves from the `/128` with zero further code. Node's own global `fetch` (undici) does **not** read those env vars, so pass a dispatcher explicitly:

```js
import { egress } from "whisper-id";
import { ProxyAgent } from "undici";

const e = await egress();
const res = await fetch("https://api64.ipify.org", { dispatcher: new ProxyAgent(e.proxyUrl) });
console.log(await res.text());   // ← your /128, not your host's
```

## Verifying an agent — raw fetch vs the typed SDK

Verification is keyless. In Node the "raw" way is a single `fetch`; the SDK is the same call, typed and null-safe:

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

// with whisper-id — typed, and returns null (not a throw) for a non-agent
import { verifyDetails } from "whisper-id";
const v = await verifyDetails("2a04:2a01:eb5a:ca74:cef2:2a:323d:40d4");
console.log(v?.is_whisper_agent, v?.dane_ok, v?.jws_ok);   // true true true
```

Both ask Whisper's server to run the chain. To re-derive it yourself from the DNSSEC root, trusting no server, that's a terminal job (`dig`, `openssl`) or `whisper verify --trustless` — see [Verify an agent](/docs/verify) and [DNSSEC](/docs/dnssec).

## Requirements

Zero runtime dependencies. Keyless checks and the control plane need only Node ≥18 (global `fetch`) and, for the control plane, `WHISPER_API_KEY` in the environment or `{ apiKey }` on each call. Only `register`, `egress`, `withEgress`, and `ip` need the `whisper` binary on `PATH`:

```sh
curl -fsSL https://get.whisper.online | sh
```

`$WHISPER_BIN` overrides the CLI path; `$WHISPER_CONTROL_URL` / `$WHISPER_RDAP_URL` override the endpoints for self-hosted or staging setups. Source: [github.com/whisper-sec/whisper-node](https://github.com/whisper-sec/whisper-node), MIT licensed. Every other language and runtime combination — Python (`pip install whisper-id`), edge/serverless, n8n, MCP — is catalogued on [/docs/integrations](/docs/integrations).

---

**Next:** [`whisper-edge`](/docs/sdk-edge) for fetch-only edge runtimes · [`whisper` CLI](/docs/cli) for the trustless verification chain and local egress daemon.
