# Supabase Edge

**Supabase Edge Functions run on Deno across a global edge network, so "just curl the target API" is a trap — no dedicated egress IP, no static address to allowlist, a different edge node every invocation.**

The moment a partner API wants IP allowlisting, or you want to prove *which* deployment made a call, you're bolting on a third-party proxy or giving up. Whisper solves both from inside the function: verify who's calling you with zero setup, and — with one key — route the function's outbound calls through an agent's permanent, routable `/128`, so the far end sees a stable, DNSSEC-anchored identity instead of a rotating edge IP.

## The shape: two tiers, one package

`whisper-edge` is a dependency-free TypeScript SDK built for exactly this class of runtime — Cloudflare Workers, Vercel, Netlify, AWS Lambda, Deno Deploy, and Supabase Edge Functions — anywhere a global `fetch` exists and no Node built-ins are guaranteed. It ships two tiers, Postel-shaped:

- **Keyless** — `verify` / `verifyDetails` / `resolve` / `rdap` / `rdapDomain`. Pure HTTPS GETs against `rdap.whisper.online`. No account, no key, no CLI.
- **Control (with a key)** — `control(apiKey)` wraps the single control-plane verb, `CALL whisper.agents({op, args})`, POSTed to `graph.whisper.security`. Mint agents, list them, set policy, read logs, revoke — and `agentEgress(apiKey)` hands you a `fetch`-shaped object whose requests physically leave from that agent's `/128`.

Supabase Edge Functions run on the open-source `edge-runtime` (a Deno-compatible sandbox), so anything written for Deno works unmodified — including `Deno.connect`/`Deno.startTls`, which is exactly the primitive `whisper-edge` needs for real egress on this runtime (more on the wire mechanics below).

## Keyless: verify a caller, no setup

### The raw way — one `fetch`

A Supabase Edge function runs Deno: no `dig`, just `fetch`. The keyless verify is a single GET:

```ts
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, fqdn } = await r.json();
```

That verdict folds the whole chain server-side — PTR, forward-confirm AAAA, the DANE-EE TLSA pin under DNSSEC (RFC 6698 + 4035), and the JWS identity document. `dane_ok` is the one that matters: the certificate pin traces to the IANA root with no CA in the chain. The full terminal walk is in [Verify an agent](/docs/verify).

### With Whisper — a Supabase function

```ts
// supabase/functions/whisper/index.ts
// SPDX-License-Identifier: MIT
//   supabase functions deploy whisper
//   KEYLESS:  GET /whisper?addr=<agent /128 address>
//   CONTROL:  GET /whisper?op=list      (supabase secrets set WHISPER_API_KEY=<your key>)
import { resolve, rdap, control } from "npm:whisper-edge@^0.3.0";

Deno.serve(async (req: Request) => {
  const url = new URL(req.url);
  const op = url.searchParams.get("op");

  if (op) {
    const key = Deno.env.get("WHISPER_API_KEY");
    if (!key) return Response.json({ error: "set WHISPER_API_KEY to use the control plane" }, { status: 401 });
    const res = op === "list" ? await control(key).list() : await control(key).agents(op, {});
    return Response.json({ op, records: res.records });
  }

  const addr = url.searchParams.get("addr");
  if (!addr) return new Response("usage: ?addr=<agent /128 address>   |   ?op=list (needs a key)\n", { status: 400 });
  const identity = await resolve(addr);
  return Response.json({ address: addr, identity, rdap: identity ? await rdap(addr) : null });
});
```

```bash
supabase functions deploy whisper
supabase secrets set WHISPER_API_KEY=whisper_live_...   # only needed for ?op=

curl "https://<project>.functions.supabase.co/whisper?addr=2a04:2a01:eb5a:ca74:cef2:2a:323d:40d4"
curl "https://<project>.functions.supabase.co/whisper?op=list"
```

`resolve()` returns `null` — never an exception — for anything that isn't a real Whisper agent, so this endpoint is safe to expose publicly as a "verify this address" utility with no auth at all.

## Egress: leave from the agent's own `/128`

This is the part that's actually hard to DIY. A Supabase Edge Function's outbound `fetch` sources from whatever edge node happened to run that invocation — you cannot pin it, and it changes under you. `agentEgress` fixes the source address instead of chasing the PoP.

### With stock tools

You *can* hand-roll an HTTP CONNECT tunnel over a raw TCP socket in Deno — `Deno.connect()` gets you the socket, `Deno.startTls()` upgrades it — but you'd be reimplementing the CONNECT preamble, the bearer handshake, and 407-retry timing yourself:

```ts
// the raw mechanism, for reference — this is what whisper-edge does for you
const raw = await Deno.connect({ hostname: "egress.whisper.online", port: 443 });
const tls = await Deno.startTls(raw, { hostname: "egress.whisper.online" });
const enc = new TextEncoder();
await tls.write(enc.encode(
  `CONNECT api.example.com:443 HTTP/1.1\r\nHost: api.example.com\r\n` +
  `Proxy-Authorization: Basic ${btoa("w:" + egressBearer)}\r\n\r\n`
));
// read the "200 Connection established" reply, then TLS-wrap `tls` again for the real request
```

### With Whisper

```ts
import { agentEgress } from "npm:whisper-edge@^0.3.0";

const egress = await agentEgress(Deno.env.get("WHISPER_API_KEY")!);
const res = await egress.fetch("https://api.example.com/whoami");

const seen = (await res.json()).ip;
seen === egress.transport.address;   // true — the request left from YOUR agent's /128
egress.close();
```

On Deno-family runtimes (Deno Deploy and Supabase Edge), `agentEgress` auto-detects and opens a `Deno.connect` + `Deno.startTls` HTTP-CONNECT tunnel to the egress proxy, source-bound to the agent's `/128` via `IP_FREEBIND` over `2a04:2a01::/32`. One caveat worth knowing before you rely on it: Deno can't layer TLS-inside-TLS, so the CONNECT preamble carrying the bearer rides the clear leg to the proxy — reflected as `egress.transport.tokenProtected === false`. The bearer is still short-lived, per-agent, and never logged; it just isn't double-wrapped in TLS on this specific runtime (Node gets the fully-nested form). If a fetch-only sandbox somewhere in your stack has no raw-socket API at all, the same call transparently falls back to the [fetch-forward gateway](/docs/forward-gateway) (`forward.whisper.online/forward`) — same call site, `egress.transport.tier` tells you which path was actually used.

### Without the SDK: proving egress with curl alone

Whatever minted the agent, you can independently prove where its traffic actually exits using only `curl` and the keyless echo endpoint — never trust a client-side claim:

```bash
curl -x <http_proxy_from_connect> -s https://whisper.online/egress-ip
# -> {"ip":"2a04:2a01:eb5a:ca74:cef2:2a:323d:40d4"}   <- must equal the agent's own /128
```

## Minting the agent behind the key

```bash
# stock tools — the control plane is one HTTPS POST, no SDK required
curl -s https://graph.whisper.security/api/query \
  -H "X-API-Key: whisper_live_..." -H "content-type: application/json" \
  -d '{"query":"CALL whisper.agents({op:'"'"'register'"'"', args:{label:'"'"'edge-fn'"'"'}})"}' | jq .
```

```ts
// with Whisper — same call, typed
import { control } from "npm:whisper-edge@^0.3.0";
const created = await control(Deno.env.get("WHISPER_API_KEY")!).register({ name: "edge-fn" });
// created.records[0].address -> the new routable /128 · .api_key -> shown once, store it as a secret
```

## Reference

- SDK internals, full API surface, and the runtime-detection table (Node / Deno / Workers / fetch-only): [Edge SDK (serverless)](/docs/sdk-edge)
- The mechanism `agentEgress` falls back to when a runtime has no raw socket at all: [The forward gateway](/docs/forward-gateway)
- DANE/TLSA verification in depth: [DANE & TLSA](/docs/dane)
- Package: [npm i whisper-edge](https://www.npmjs.com/package/whisper-edge) · source: [github.com/whisper-sec/whisper-edge](https://github.com/whisper-sec/whisper-edge) (`examples/supabase`)

> Every distribution channel — including this one — is two-tier by construction: no key still verifies any Whisper agent against the DNSSEC root; your key unlocks the control plane and real egress. See [Integrations](/docs/integrations) for the full list.

**Next:** [Edge SDK (serverless)](/docs/sdk-edge) · [Vercel / Netlify / Deno](/docs/serverless)
