whisper-id 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -25,7 +25,21 @@ await withEgress(async (e) => {
25
25
  | `egress({ agent, tier, setEnv })` | Bring up egress bound to your `/128`. → `Egress { port, proxyUrl, socksUrl, proxies, close() }`. `tier: "wireguard"` for a routed `/128`. Sets proxy env unless `setEnv:false`; call `close()` to restore. |
26
26
  | `withEgress([opts], fn)` | Run `fn(egress)` with egress up, restoring the environment afterwards. |
27
27
  | `verify(address)` | Keyless — is `address` a real Whisper agent? (DANE + DNSSEC + reverse-DNS + JWS) → `boolean` |
28
- | `ip()` | Your current egress IP, proving it's your `/128`. → `string` |
28
+ | `verifyDetails(address)` | Keyless — the full verdict (`is_whisper_agent`, `fqdn`, `operator`, `dane_ok`, `jws_ok`, …) or `null` |
29
+ | `rdap(address)` | Keyless — the public RDAP record for a `/128`, or `null` |
30
+ | `egressIp()` | Keyless — the IP this process leaves from (a `/128` when routed, else the platform's) → `string` |
31
+ | `ip()` | Your current egress IP via the CLI, proving it's your `/128`. → `string` |
32
+
33
+ ### Keyless / serverless (no CLI)
34
+
35
+ `verify`, `verifyDetails`, `rdap`, and `egressIp` are **keyless** — pure HTTPS via `fetch`, **no `whisper` CLI and no key** — so they run in Cloudflare Workers, Vercel, Deno, Lambda, etc.:
36
+
37
+ ```js
38
+ import { verify, rdap } from "whisper-id";
39
+ if (await verify(addr)) console.log((await rdap(addr)).name);
40
+ ```
41
+
42
+ `register`, `egress`, `withEgress`, and `ip` need the CLI on the host (egress needs the local proxy; register needs your key).
29
43
 
30
44
  ### Using it with `fetch`
31
45
 
package/index.d.ts CHANGED
@@ -26,6 +26,27 @@ export function register(name: string, opts?: RegisterOptions): Promise<Agent>;
26
26
  export function egress(opts?: EgressOptions): Promise<Egress>;
27
27
  export function withEgress<T>(fn: (e: Egress) => Promise<T> | T): Promise<T>;
28
28
  export function withEgress<T>(opts: EgressOptions, fn: (e: Egress) => Promise<T> | T): Promise<T>;
29
+ /** Full server-side verify verdict (keyless). */
30
+ export interface VerifyVerdict {
31
+ is_whisper_agent: boolean;
32
+ fqdn?: string;
33
+ operator?: string;
34
+ tenant?: string;
35
+ dane_ok?: boolean;
36
+ jws_ok?: boolean;
37
+ verified_at?: number;
38
+ evidence?: unknown;
39
+ [k: string]: unknown;
40
+ }
41
+
42
+ /** Keyless, CLI-free: is `address` a real Whisper agent? (server-side DANE+DNSSEC+reverse-DNS+JWS) */
29
43
  export function verify(address: string, opts?: { timeout?: number }): Promise<boolean>;
44
+ /** Keyless, CLI-free: the full verify verdict, or null if not an agent. */
45
+ export function verifyDetails(address: string, opts?: { timeout?: number }): Promise<VerifyVerdict | null>;
46
+ /** Keyless, CLI-free: the public RDAP record for a /128, or null. */
47
+ export function rdap(address: string, opts?: { timeout?: number }): Promise<Record<string, unknown> | null>;
48
+ /** Keyless, CLI-free: the caller's current egress IP (what this process leaves from). */
49
+ export function egressIp(opts?: { timeout?: number }): Promise<string>;
50
+ /** Your current egress IP via the CLI (proves your /128). */
30
51
  export function ip(opts?: { timeout?: number }): Promise<string>;
31
52
  export function firstAddr(obj: unknown): string | null;
package/index.js CHANGED
@@ -127,11 +127,63 @@ export async function withEgress(optsOrFn, maybeFn) {
127
127
  }
128
128
  }
129
129
 
130
- /** Return true iff `address` is a real Whisper agent (DANE+DNSSEC+reverse-DNS+JWS). Keyless. */
130
+ // Keyless public endpoint base (the same surface the CLI uses, server-side). No key, no `whisper`
131
+ // CLI — these work in any runtime that can fetch() (serverless, edge, browser). Override via env.
132
+ function rdapBase() {
133
+ return (process.env.WHISPER_RDAP_URL || "https://rdap.whisper.online").replace(/\/+$/, "");
134
+ }
135
+
136
+ async function httpGetJson(url, { timeout = 60000 } = {}) {
137
+ const ctrl = new AbortController();
138
+ const t = setTimeout(() => ctrl.abort(), timeout);
139
+ let res;
140
+ try {
141
+ res = await fetch(url, { headers: { Accept: "application/rdap+json, application/json, */*", "User-Agent": "whisper-id-node/0.2.0" }, signal: ctrl.signal });
142
+ } catch (err) {
143
+ throw new WhisperError(`could not reach ${url}: ${err && err.message ? err.message : err}`);
144
+ } finally {
145
+ clearTimeout(t);
146
+ }
147
+ let body = null;
148
+ try {
149
+ body = await res.json();
150
+ } catch {
151
+ body = null;
152
+ }
153
+ return { status: res.status, body };
154
+ }
155
+
156
+ /**
157
+ * Return true iff `address` is a real Whisper agent. **Keyless and CLI-free** — one HTTPS GET to
158
+ * the public /verify-identity endpoint (server-side DANE+DNSSEC+reverse-DNS+JWS chain). Works in
159
+ * any runtime. 200 ⇒ agent, 404 ⇒ not. Throws only if the endpoint is unreachable.
160
+ */
131
161
  export async function verify(address, { timeout = 60000 } = {}) {
162
+ return (await verifyDetails(address, { timeout })) !== null;
163
+ }
164
+
165
+ /** Full verify verdict for a real agent (is_whisper_agent, fqdn, operator, tenant, dane_ok, jws_ok, …), else null. Keyless, CLI-free. */
166
+ export async function verifyDetails(address, { timeout = 60000 } = {}) {
132
167
  if (!address || !address.trim()) throw new WhisperError("verify() needs an address");
133
- const { code } = await run(["verify", address], { timeout, check: false });
134
- return code === 0;
168
+ const url = `${rdapBase()}/verify-identity?ip=${encodeURIComponent(address.trim())}`;
169
+ const { status, body } = await httpGetJson(url, { timeout });
170
+ if (status !== 200 || !body || !body.is_whisper_agent) return null;
171
+ return body;
172
+ }
173
+
174
+ /** Public RDAP record for a Whisper /128 (handle, name, status, entities, …), or null. Keyless, CLI-free. */
175
+ export async function rdap(address, { timeout = 60000 } = {}) {
176
+ if (!address || !address.trim()) throw new WhisperError("rdap() needs an address");
177
+ const url = `${rdapBase()}/ip/${encodeURI(address.trim())}`; // path segment — keep the literal colons
178
+ const { status, body } = await httpGetJson(url, { timeout });
179
+ return status === 200 ? body : null;
180
+ }
181
+
182
+ /** The caller's current egress IP as seen by Whisper (what this process leaves from). Keyless, CLI-free. */
183
+ export async function egressIp({ timeout = 60000 } = {}) {
184
+ const { status, body } = await httpGetJson(`${rdapBase()}/egress-ip`, { timeout });
185
+ if (status !== 200) throw new WhisperError(`egress-ip endpoint returned HTTP ${status}`);
186
+ return (body && body.ip) || "";
135
187
  }
136
188
 
137
189
  /** Return the current egress IP, proving it is your Whisper /128. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whisper-id",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "A routable Whisper IPv6 identity and safe egress for any Node agent.",
5
5
  "type": "module",
6
6
  "exports": { ".": { "types": "./index.d.ts", "import": "./index.js" } },