{"id":51648790,"url":"https://github.com/whisper-sec/whisper-edge","last_synced_at":"2026-07-14T01:32:55.519Z","repository":{"id":368784156,"uuid":"1286361629","full_name":"whisper-sec/whisper-edge","owner":"whisper-sec","description":"whisper-edge — dependency-free Whisper agent-identity SDK for serverless \u0026 edge runtimes (Cloudflare Workers, Vercel, Deno, Netlify, Lambda, Supabase). Keyless verify/resolve/RDAP + control plane with an API key.","archived":false,"fork":false,"pushed_at":"2026-07-02T06:21:49.000Z","size":153,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-07-02T08:16:32.206Z","etag":null,"topics":["agent-identity","aws-lambda","cloudflare-workers","dane","deno","edge","ipv6","netlify","rdap","serverless","supabase","typescript","vercel","whisper","zero-dependency"],"latest_commit_sha":null,"homepage":"https://whisper.online","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/whisper-sec.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-07-01T17:45:44.000Z","updated_at":"2026-07-02T06:21:50.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/whisper-sec/whisper-edge","commit_stats":null,"previous_names":["whisper-sec/whisper-edge"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/whisper-sec/whisper-edge","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/whisper-sec%2Fwhisper-edge","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/whisper-sec%2Fwhisper-edge/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/whisper-sec%2Fwhisper-edge/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/whisper-sec%2Fwhisper-edge/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/whisper-sec","download_url":"https://codeload.github.com/whisper-sec/whisper-edge/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/whisper-sec%2Fwhisper-edge/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35442828,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-13T02:00:06.543Z","response_time":119,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["agent-identity","aws-lambda","cloudflare-workers","dane","deno","edge","ipv6","netlify","rdap","serverless","supabase","typescript","vercel","whisper","zero-dependency"],"created_at":"2026-07-14T01:32:55.392Z","updated_at":"2026-07-14T01:32:55.514Z","avatar_url":"https://github.com/whisper-sec.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n \u003cimg src=\"logo.png\" alt=\"Whisper\" width=\"96\" height=\"96\"\u003e\n\u003c/p\u003e\n\n\u003ch1 align=\"center\"\u003ewhisper-edge\u003c/h1\u003e\n\n\u003cp align=\"center\"\u003e\n Agent identity \u003cb\u003eand real egress\u003c/b\u003e for serverless \u0026 edge runtimes — \u003cb\u003ezero dependencies\u003c/b\u003e, one \u003ccode\u003efetch\u003c/code\u003e.\u003cbr\u003e\n Verify who an agent is with \u003cb\u003eno key\u003c/b\u003e; run the full control plane and route traffic from an agent's routable /128 \u003cb\u003ewith\u003c/b\u003e one.\n\u003c/p\u003e\n\n```ts\nimport { verify, agentEgress } from \"whisper-edge\";\n\nif (await verify(addr)) { /* keyless — it's a real, cryptographically-verifiable Whisper agent */ }\n\nconst egress = await agentEgress(process.env.WHISPER_API_KEY!); // with a key — real egress\nconst who = await egress.fetch(\"https://api.example.com/whoami\"); // this request LEAVES from your agent's /128\n```\n\nRuns unchanged on **Cloudflare Workers, Vercel, Deno, Netlify, AWS Lambda, and Supabase Edge\nFunctions**. No Node built-ins to install, no CLI, no local proxy process — the egress transport\nruns **in-process** on each runtime's own socket primitive.\n\n---\n\n## Install\n\n```bash\nnpm i whisper-edge # Node / Cloudflare / Vercel / Netlify / Lambda\n```\n\n```ts\nimport { verify, resolve, control, agentEgress } from \"npm:whisper-edge@^0.3.0\"; // Deno / Supabase\n```\n\n## Two tiers (+ egress)\n\nWhisper is **Postel-shaped**: with **no key** you can already verify and resolve any agent\nidentity (the same public facts RDAP exposes); with **your key** the full control plane unlocks —\nmint agents, set policy, read logs, revoke — **and** you can route real traffic out through an\nagent's routable `/128`.\n\n### Keyless — verify / resolve / RDAP\n\n```ts\nimport { verify, verifyDetails, resolve, rdap, rdapDomain } from \"whisper-edge\";\n\nawait verify(\"2a04:2a01::1\"); // → boolean: is this a Whisper agent?\nawait verifyDetails(\"2a04:2a01::1\"); // → full verdict (dane_ok, jws_ok, …) or null\nawait resolve(\"2a04:2a01::1\"); // → { fqdn, operator, tenant, daneOk, jwsOk, … } or null\nawait rdap(\"2a04:2a01::1\"); // → the public RDAP object (RFC 9083) or null\nawait rdapDomain(\"scout.agents.whisper.online\"); // → the forward-name RDAP object or null\n```\n\n`verify` runs the **whole trust chain server-side** — reverse-DNS PTR, forward-confirm AAAA, the\n**DANE-EE TLSA pin** (DNSSEC-anchored — the trust anchor for an agent cert, not a public CA), and\nthe JWS identity doc — and folds it into one answer. `daneOk` is the load-bearing field.\n\n### Control — with your API key\n\n```ts\nimport { control } from \"whisper-edge\";\n\nconst c = control(process.env.WHISPER_API_KEY!); // never hard-code it — read it from a secret\n\nconst created = await c.register({ name: \"scout\", email: \"ops@acme.co\" });\n// created.records[0].address → your new routable /128 ·.api_key → the new agent's key (shown once)\n\nawait c.list(); // your agents (confined to your tenant)\nawait c.policy({ block: [\"ads.example\"], default: \"allow\" }); // per-tenant DNS policy\nawait c.logs({ kind: \"dns\", from: \"-1h\", limit: 200 }); // recent activity\nawait c.revoke(\"scout\"); // withdraw the /128, PTR, tokens, and key\n```\n\n### Egress — route traffic out through an agent's /128\n\n```ts\nimport { agentEgress } from \"whisper-edge\";\n\nconst egress = await agentEgress(process.env.WHISPER_API_KEY!); // omit the arg → reuse most-recent agent\n// agentEgress(key, \"\u003cagent-id or /128\u003e\") // …or pick a specific agent\n\nconst res = await egress.fetch(\"https://rdap.whisper.online/egress-ip\"); // keyless source-IP echo\nconst seen = (await res.json()).ip; // what the internet saw\n\nseen === egress.transport.address; // true → the request left from YOUR agent's routable /128\negress.close(); // release pooled transport resources when done\n```\n\n`egress.fetch` is a drop-in for the global `fetch`: every request is source-bound to the agent's\n`/128`, so a peer's reverse-DNS resolves the request back to the agent's identity. `agentEgress`\ndetects the runtime and picks the best transport it has — **no CLI, no local proxy, ever**:\n\n| Runtime | Egress transport | Bearer on the wire |\n|---|---|---|\n| **Node** — AWS Lambda, Vercel (Node), Netlify Functions | `node:net` + `node:tls` CONNECT tunnel (or an undici `ProxyAgent` when undici is present) | **encrypted** to the proxy (nested TLS) |\n| **Deno** — Deno Deploy, Supabase Edge | `Deno.connect` + `Deno.startTls` CONNECT tunnel | to the proxy on the clear leg¹ |\n| **Cloudflare Workers** | `cloudflare:sockets` `connect()` CONNECT tunnel | to the proxy on the clear leg¹ |\n| **Fetch-only sandboxes** — Vercel *Edge*, Netlify *Edge*, and anything else with no raw-socket API | **fetch-forward gateway** — one HTTPS hop through `forward.whisper.online/forward` | encrypted (plain HTTPS to the gateway) |\n\n`egress.transport` is **secret-free** — `{ tier, address, fqdn, runtime, tokenProtected, mechanism }`.\nThe per-agent egress bearer is used in-process to authenticate the transport and is **never**\nreturned, logged, or persisted. For advanced use, `egress.connect(host, port)` opens a raw,\nsource-bound tunnel socket — except on the fetch-forward transport, which has no raw-socket\nequivalent (the gateway relays whole HTTP requests, not an arbitrary byte stream) and throws a\nclear error if you call it.\n\n¹ Deno and Cloudflare Workers cannot layer TLS-inside-TLS, so the CONNECT preamble (which carries\nthe bearer) rides the clear leg to the proxy on those runtimes — reflected as\n`tokenProtected: false`. Node nests TLS and keeps the bearer encrypted end-to-end.\n\n### Fetch-forward — egress on fetch-only sandboxes\n\nA raw CONNECT tunnel needs a raw socket. Runtimes that don't expose one — the *Edge* flavors of\nVercel and Netlify, and anything else `detectRuntime()` can't place on Node/Deno/Workers — can't\nopen one, full stop. `agentEgress` detects this and **auto-selects the fetch-forward transport**:\nevery request is instead POSTed (method mirrored) to `https://forward.whisper.online/forward` with\n`Authorization: Basic base64(\"w:\"+bearer)` and `X-Whisper-Target: \u003cthe real URL\u003e`; the gateway\negresses server-side from the agent's `/128` and streams the target's response straight back,\nstamped `X-Whisper-Egress-Source: \u003cthe /128\u003e`. One HTTPS hop, zero raw sockets required — this is\nthe path that works in *every* fetch runtime that will ever exist, including ones that will never\ngrow a socket API. Force it explicitly with `{ transport: \"forward\" }` (or force the raw-socket\npath with `{ transport: \"socket\" }`); the default, `\"auto\"`, follows the table above.\n\n**Retry on 407.²** A freshly-minted egress token needs a short window (up to ~45s) to\npropagate to every gateway node — a 407 in that window means \"not recognised on *this* node yet\",\nnot \"bad token\". `agentEgress`/`forwardFetch` retry a 407 automatically: a handful of attempts\n(default 4), ~1.5s apart, capped — enough hops to very likely land on a node that already knows\nthe token, without ever making a call hang for the full 45s. Tune it with `{ retries,\nretryDelayMs }`; a persistent 407 after the whole budget throws a clear `WhisperError` telling you\nto wait a moment or mint a fresh token.\n\n```ts\nimport { agentEgress } from \"whisper-edge\";\n\n// On Vercel Edge / Netlify Edge / any other fetch-only sandbox this is automatic — no config.\nconst egress = await agentEgress(process.env.WHISPER_API_KEY!, undefined, {\n transport: \"forward\", // force it anywhere, e.g. to test the gateway path from Node\n retries: 5, retryDelayMs: 1000,\n});\nconst res = await egress.fetch(\"https://api.example.com/whoami\");\n```\n\n² On Node, forwarding runs over `node:http`/`node:https` rather than the global `fetch`: Node's\nbuilt-in `fetch` (undici) turns a direct, non-proxied HTTP `407` into an opaque network error\ninstead of a normal response ([nodejs/undici#2896](https://github.com/nodejs/undici/issues/2896)),\nwhich would otherwise swallow the retry-on-407 behaviour above. This is internal and transparent —\n`{ transport: \"forward\" }` behaves identically on every runtime, including Node.\n\n## Runtime examples\n\nCopy-paste, deploy, done. Each sample is three-tier (`?addr=` keyless, `?egress` and `?op=list`\nwith a key).\n\n| Runtime | Example | Egress transport |\n|---|---|---|\n| Cloudflare Workers | [`examples/cloudflare`](examples/cloudflare) | `cloudflare:sockets` CONNECT tunnel |\n| Vercel (Node) | [`examples/vercel`](examples/vercel) | `node:net`/`node:tls` CONNECT tunnel |\n| Vercel Edge | [`examples/vercel-edge`](examples/vercel-edge) | fetch-forward gateway (auto) |\n| Netlify Functions | [`examples/netlify`](examples/netlify) | `node:net`/`node:tls` CONNECT tunnel |\n| Deno Deploy | [`examples/deno`](examples/deno) | `Deno.connect` CONNECT tunnel |\n| AWS Lambda | [`examples/lambda`](examples/lambda) | `node:net`/`node:tls` CONNECT tunnel |\n| Supabase Edge | [`examples/supabase`](examples/supabase) | `Deno.connect` CONNECT tunnel |\n\nReal deploys of the `examples/*` samples onto live Vercel/Netlify/Cloudflare accounts are pending\n(account-gated) — every transport above **is** proven end-to-end against the live gateway from a\nplain Node process (see the SDK's own test suite + the e2e run in the release notes), which is\nthe same code path each example calls into.\n\n## Errors, timeouts, config\n\n- Failures throw a **`WhisperError`** carrying the server's exact, secret-free message\n (`.status`, `.detail`, `.title`). A \"not an agent\" is **not** an error — `verify` returns\n `false`, `resolve`/`rdap` return `null`.\n- Every call takes optional `{ timeoutMs, signal, fetch, endpoints }`. The default timeout never\n hangs, even if the runtime's `fetch` ignores `AbortSignal`.\n- Inject `fetch` for tests or custom transports; override `endpoints` for pre-prod/self-host.\n\n```ts\nawait verify(addr, { timeoutMs: 3000 });\nawait agentEgress(key, undefined, { tier: \"socks5\", timeoutMs: 20000 });\n```\n\n## API\n\nKeyless: `verify` · `verifyDetails` · `resolve` · `rdap` · `rdapDomain`\nControl: `control(apiKey)` → `register` · `identity` · `list` · `agent` · `policy` · `logs` · `connect` · `revoke` · `agents(op, args)` · `query(cypher)`\nEgress: `agentEgress(apiKey, selector?, opts?)` → `{ fetch, transport, connect, close }` · `detectRuntime()`\nLow-level: `buildAgentsQuery` · `escapeCypherString` · `decodeEnvelope` · `WhisperError`\n\nFull types ship with the package (`whisper-edge` is written in TypeScript).\n\n---\n\nGet a key and learn more at **[whisper.online](https://whisper.online)** · MIT licensed.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwhisper-sec%2Fwhisper-edge","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fwhisper-sec%2Fwhisper-edge","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwhisper-sec%2Fwhisper-edge/lists"}