whisper-id 0.6.1 → 0.7.1

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
@@ -1,6 +1,6 @@
1
1
  # whisper-id
2
2
 
3
- Three things for any Node agent, in one dependency-free package: query the **Whisper security graph** (3.6B nodes / 30B relationships, Cypher), give the agent a routable **IPv6 identity** with safe **egress**, and drive the full **control plane**.
3
+ Three things for any Node agent, in one dependency-free package: query the **Whisper security graph** (7.48B nodes and 39.5B edges joining the physical, network, naming, ownership and threat layers; live totals at nic.whisper.online/stats, Cypher), give the agent a routable **IPv6 identity** with safe **egress**, and drive the full **control plane**.
4
4
 
5
5
  ```sh
6
6
  npm i whisper-id
@@ -8,7 +8,7 @@ npm i whisper-id
8
8
 
9
9
  ## The security graph (keyless, zero setup)
10
10
 
11
- The Whisper graph knows who operates a host, its threat posture, its look-alikes, the real origins behind a CDN, WHOIS history, and 20+ named investigations. The **direct read verbs run with no key at all** (rate-limited taste, ~100/window). One import, real answers:
11
+ The Whisper graph knows who operates a host, its threat posture, its look-alikes, the real origins behind a CDN, WHOIS history, and 15 named investigations. The **direct read verbs run with no key at all** (rate-limited taste, ~100/window). One import, real answers:
12
12
 
13
13
  ```js
14
14
  import { graph } from "whisper-id"; // no key needed for the read verbs
@@ -19,7 +19,7 @@ await graph.origins("cloudflare.com"); // the real origin IPs behind
19
19
  await graph.explain("paypal.com"); // threat-feed score + why
20
20
  ```
21
21
 
22
- Set `WHISPER_API_KEY` (or pass `{ apiKey }`) to lift the rate limit and unlock **raw Cypher** and the **multi-step flows**:
22
+ Set `WHISPER_API_KEY` (or pass `{ apiKey }`) to lift the keyless rate limit. Every verb is sent either way; the graph decides what to answer:
23
23
 
24
24
  ```js
25
25
  // raw Cypher, your own query, parameters bound as $-params (never string-built):
@@ -35,7 +35,7 @@ for (const r of graph.recipes()) {
35
35
  }
36
36
  ```
37
37
 
38
- Every verb maps to a catalog entry with its own docs page under [whisper.security/docs](https://www.whisper.security/docs) (e.g. [`assess`](https://www.whisper.security/docs/whisper-graph/procedures), [`identify`](https://www.whisper.security/docs/whisper-graph/procedures/identify)); `recipes()` carries the exact `docsUrl` for each, and the same URL sits on every method as `graph.identify.docs` and on its `@see` tag in your editor. The 13 direct reads are keyless; the 15 multi-step flows and the `submit` write channel are keyed. Full query reference: [whisper-catalog](https://github.com/whisper-sec/whisper-catalog).
38
+ Every verb maps to a catalog entry with its own docs page under [whisper.security/docs](https://www.whisper.security/docs) (e.g. [`assess`](https://www.whisper.security/docs/whisper-graph/procedures), [`identify`](https://www.whisper.security/docs/whisper-graph/procedures/identify)); `recipes()` carries the exact `docsUrl` for each, and the same URL sits on every method as `graph.identify.docs` and on its `@see` tag in your editor. The 13 direct reads answer keyless; the 15 multi-step flows and the `submit` write channel are proxied up the same way and the graph decides. Full query reference: [whisper-catalog](https://github.com/whisper-sec/whisper-catalog).
39
39
 
40
40
  ## Identity + egress
41
41
 
@@ -78,12 +78,12 @@ if (await verify(addr)) console.log((await rdap(addr)).name); // keyless, pure
78
78
  | `graph.asset(value)` | yes | Member ASNs of an AS-SET macro |
79
79
  | `graph.lookupTorRelay(value)` | yes | Is an IP a known Tor exit relay |
80
80
  | `graph.dbSchema()` | yes | Every node + relationship type in the graph, with counts and examples |
81
- | `graph.query(cypher, params)` | key | Raw Cypher escape hatch. -> the full `{ columns, rows, statistics }` |
81
+ | `graph.query(cypher, params)` | yes | Raw Cypher escape hatch. -> the full `{ columns, rows, statistics }` |
82
82
  | `graph.submit({ kind, ...fields })` | key | Contribute an indicator observation or feedback (the write channel) |
83
83
  | `graph.recipes()` | yes (offline) | The whole catalog: `{ method, keyless, mode, summary, params, docsUrl }` per verb |
84
84
 
85
85
  Fifteen multi-step **workflow** methods (`attackPath`, `attackSurface`, `blastRadius`, `typosquat`,
86
- `indicator`, `routeHealth`, and 9 more) execute keyed on the Whisper workflow runner and stream
86
+ `indicator`, `routeHealth`, and 9 more) execute on the Whisper workflow runner and stream
87
87
  Server-Sent Events; one call collects the whole run into `{ slug, steps, complete, events }`:
88
88
 
89
89
  ```js
@@ -114,7 +114,10 @@ Run anywhere `fetch` runs (Cloudflare Workers, Vercel, Deno, Lambda, the browser
114
114
  Pure `fetch` to the control endpoint, with no `whisper` CLI and no child process. The key comes from
115
115
  `opts.apiKey` or `WHISPER_API_KEY` and is sent only in the `X-API-Key` header (never on argv,
116
116
  never logged). Each also takes `{ apiKey, timeout }`. On failure they throw a `WhisperError`
117
- carrying the server's `detail` (plus `.status` / `.retryAfter`).
117
+ carrying the server's message (plus `.code` / `.status` / `.retryable` / `.retryAfter`). A
118
+ `revoke` that matches nothing of yours (revoked already, or not in your account) throws
119
+ `NotFoundError` (`.code` `NOT_FOUND`, `.status` 404, not retryable): nothing was revoked, so
120
+ do not retry it.
118
121
 
119
122
  | Call | Does |
120
123
  |------|------|
@@ -165,14 +168,15 @@ transparently inside `withEgress`.
165
168
 
166
169
  Zero dependencies. The **graph read verbs**, the **keyless** checks, and the **control plane** are
167
170
  pure HTTP (Node ≥ 18's global `fetch`), with no CLI and nothing to install. Set `WHISPER_API_KEY`
168
- (or pass `{ apiKey }`) for the control plane and the keyed graph (raw Cypher + flows + `submit`);
169
- the graph read verbs, `verify`, and `rdap` need no key.
171
+ (or pass `{ apiKey }`) for the control plane (`whisper.agents`), which always needs it. Graph calls
172
+ never require a key from this SDK: they go up with one or without, and the graph decides what to
173
+ answer. A key lifts the keyless rate limit. `verify` and `rdap` need no key either.
170
174
 
171
175
  Only **egress** and the CLI-backed calls (`register`, `egress`, `withEgress`, `ip`) need the
172
176
  `whisper` CLI on your `PATH`:
173
177
 
174
178
  ```sh
175
- curl get.whisper.online | sh
179
+ curl -fsSL https://get.whisper.online | sh
176
180
  ```
177
181
 
178
182
  (`$WHISPER_BIN` overrides the CLI path; `$WHISPER_CONTROL_URL` / `$WHISPER_GRAPH_URL` /
package/SECURITY.md ADDED
@@ -0,0 +1,26 @@
1
+ # Security Policy
2
+
3
+ ## Reporting a vulnerability
4
+
5
+ Please report security issues privately to **security@whisper.security**. Do not
6
+ open a public issue for a suspected vulnerability.
7
+
8
+ Include what you found, how to reproduce it, and the impact you expect. We aim to
9
+ acknowledge within a few business days and will keep you updated as we work on a
10
+ fix. Coordinated disclosure is appreciated.
11
+
12
+ ## Scope
13
+
14
+ This repository is the **Node client** for Whisper: the control plane
15
+ (`whisper.agents`), the keyless public checks, and the Whisper security graph. It
16
+ talks only to Whisper's public endpoints and holds no server-side secrets.
17
+
18
+ Your API key travels in one place and one place only: the `X-API-Key` request
19
+ header. It is never written to a URL, a request body, a log line, or argv. A call
20
+ made without a key sends no auth header at all rather than an empty one. Any
21
+ deviation from that is a security bug and is in scope, as are the input validation
22
+ and the redaction of keys from error text.
23
+
24
+ ## Supported versions
25
+
26
+ The latest published release receives security fixes. Older versions may not.
package/index.d.ts CHANGED
@@ -1,6 +1,30 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  // Copyright (c) 2026 viaGraph B.V. (Whisper Security)
3
- export declare class WhisperError extends Error {}
3
+ /**
4
+ * Thrown when a `whisper` invocation fails, the CLI is missing, or the control plane refuses
5
+ * a call. A control-plane refusal carries the server's `code`, `status` and `retryable`.
6
+ */
7
+ export declare class WhisperError extends Error {
8
+ /** The server's error code, e.g. "NOT_FOUND" or "FORBIDDEN", when it gave one. */
9
+ code?: string;
10
+ /** The effective HTTP/envelope status of a control-plane refusal. */
11
+ status?: number;
12
+ /** Whether the server says retrying could succeed. */
13
+ retryable?: boolean;
14
+ /** Seconds the server asked the caller to wait (rate limits). */
15
+ retryAfter?: number;
16
+ suggestions?: string[];
17
+ }
18
+
19
+ /**
20
+ * Nothing of yours matched: from `revoke()`, nothing was revoked (revoked already, or not in
21
+ * your account). An answer, not a fault: never retry it.
22
+ */
23
+ export declare class NotFoundError extends WhisperError {
24
+ code: "NOT_FOUND";
25
+ status: 404;
26
+ retryable: false;
27
+ }
4
28
 
5
29
  export interface Agent {
6
30
  /** The routable IPv6 /128 - both the identity and the auth. */
@@ -55,7 +79,8 @@ export function firstAddr(obj: unknown): string | null;
55
79
  // The governance half of the two-tier surface: pure `fetch` to the control endpoint, so
56
80
  // it runs anywhere (serverless, edge, a plain server). The key is read from `opts.apiKey`
57
81
  // or `WHISPER_API_KEY` and sent only in the `X-API-Key` header. On failure these throw a
58
- // `WhisperError` carrying the server's `detail` (and `.status` / `.retryAfter`).
82
+ // `WhisperError` carrying the server's message (and `.code` / `.status` / `.retryable` /
83
+ // `.retryAfter`); a `NotFoundError` when the server's code is NOT_FOUND.
59
84
 
60
85
  /** Common options for every keyed control call. */
61
86
  export interface ControlOptions {
@@ -142,7 +167,10 @@ export function list(opts?: ListOptions): Promise<AgentItem[]>;
142
167
  export function policy(opts?: PolicyOptions): Promise<PolicyEntry[]>;
143
168
  /** Query recent DNS/conn/alloc activity from warm storage. → event records. */
144
169
  export function logs(opts?: LogsOptions): Promise<LogEvent[]>;
145
- /** Fully revoke an agent - IRREVERSIBLE (admin:dns). → `{ status, ... }`. */
170
+ /**
171
+ * Fully revoke an agent - IRREVERSIBLE (admin:dns). → `{ status, ... }`. Throws
172
+ * `NotFoundError` when nothing was revoked (revoked already, or not in your account).
173
+ */
146
174
  export function revoke(agent: string, opts?: ControlOptions): Promise<{ status: string; [k: string]: unknown }>;
147
175
  export function revoke(opts: RevokeOptions): Promise<{ status: string; [k: string]: unknown }>;
148
176
  /** Allocate your own /128 (or release with `{release:true,address}`). → `{agent,address,fqdn,ptr,state}`. */
@@ -157,7 +185,7 @@ export function agent(opts?: AgentSelectOptions): Promise<AgentItem | null>;
157
185
  // KEYLESS - no key at all, a rate-limited taste with real answers; a key ({ apiKey } or
158
186
  // WHISPER_API_KEY) lifts the limit. Raw Cypher (`graph.query`), the 15 multi-step `flow`
159
187
  // methods (workflow runner: POST gallery/run, Server-Sent Events), and the `submit`
160
- // write stay KEYED. A present key rides only in `X-API-Key`; an absent one sends no auth
188
+ // write are proxied up the same way. A present key rides only in `X-API-Key`; an absent one sends no auth
161
189
  // header at all. The method surface below is GENERATED from the Whisper query catalog by
162
190
  // scripts/gen-graph.mjs; every method's docs live at the `@see` URL and are also on the
163
191
  // function itself as `.docs`, and `graph.recipes()` lists the whole catalog.
@@ -181,7 +209,7 @@ export interface GraphRecipe {
181
209
  docsUrl: string;
182
210
  }
183
211
 
184
- /** Result of a keyed graph query. `rows` are objects keyed by column name. */
212
+ /** Result of a graph query. `rows` are objects keyed by column name. */
185
213
  export interface GraphResult {
186
214
  columns: string[];
187
215
  rows: Record<string, unknown>[];
@@ -237,15 +265,18 @@ export interface GraphFlowResult {
237
265
  }
238
266
 
239
267
  /**
240
- * The graph namespace: one typed method per catalog entry (14 direct Cypher reads +
241
- * 15 workflow-runner flows), plus a raw `query` escape hatch and `recipes()` discovery.
242
- * Two-tier: the 13 direct read verbs run keyless (no key needed, rate-limited; a key
243
- * lifts the limit); `query`, the flows, and `submit` need a key (`{ apiKey }` or
244
- * `WHISPER_API_KEY`). Every method carries its canonical docs URL as `.docs`.
268
+ * The graph namespace: one typed method per catalog entry (13 direct Cypher reads,
269
+ * `submit`, and 15 workflow-runner flows), plus a raw `query` escape hatch and
270
+ * `recipes()` discovery. Every verb is proxied to the graph with a key if you have one
271
+ * and without if you do not: the graph owns the keyless policy and decides what to
272
+ * answer, so the SDK never refuses a call on your behalf. A key (`{ apiKey }` or
273
+ * `WHISPER_API_KEY`) lifts the keyless rate limit. The control plane (`whisper.agents`:
274
+ * agents, policy, logs, identity) is a separate surface and always requires a key.
275
+ * Every method carries its canonical docs URL as `.docs`.
245
276
  */
246
277
  export interface GraphNamespace {
247
278
  /**
248
- * Raw Cypher escape hatch (KEYED): run arbitrary Cypher with `params` bound
279
+ * Raw Cypher escape hatch: run arbitrary Cypher with `params` bound
249
280
  * server-side as $-parameters. Returns the full `{columns,rows,statistics}`.
250
281
  * @see https://www.whisper.security/docs/cypher-api
251
282
  */
@@ -268,7 +299,7 @@ export interface GraphNamespace {
268
299
  * letters have in-country DNS_ROOT_INSTANCE anycast nodes, the Global-vs-Local split, the BGP
269
300
  * origin ASNs hosting them, and a resilience grade.
270
301
  *
271
- * KEYED flow: streams Server-Sent Events from the workflow runner (slug "anycast-dns-root-sovereignty")
302
+ * Flow: streams Server-Sent Events from the workflow runner (slug "anycast-dns-root-sovereignty")
272
303
  * and resolves to the collected { slug, steps, complete, events }.
273
304
  * Inputs: country. Params: instanceType.
274
305
  * @see https://www.whisper.security/docs/recipes/compliance
@@ -281,7 +312,7 @@ export interface GraphNamespace {
281
312
  * From a starting foothold, finds shared dependencies whose compromise reaches furthest; for
282
313
  * any two indicators, traces how they are actually connected.
283
314
  *
284
- * KEYED flow: streams Server-Sent Events from the workflow runner (slug "attack-path")
315
+ * Flow: streams Server-Sent Events from the workflow runner (slug "attack-path")
285
316
  * and resolves to the collected { slug, steps, complete, events }.
286
317
  * Inputs: value, other. Params: level.
287
318
  * @see https://www.whisper.security/docs/recipes/attack-path
@@ -294,7 +325,7 @@ export interface GraphNamespace {
294
325
  * Maps the full external footprint - subdomains, name/mail servers, registrant, third-party
295
326
  * services, connected web - and scores the exposure.
296
327
  *
297
- * KEYED flow: streams Server-Sent Events from the workflow runner (slug "attack-surface")
328
+ * Flow: streams Server-Sent Events from the workflow runner (slug "attack-surface")
298
329
  * and resolves to the collected { slug, steps, complete, events }.
299
330
  * Inputs: domain. Params: level.
300
331
  * @see https://www.whisper.security/docs/recipes/pentest-recon
@@ -307,7 +338,7 @@ export interface GraphNamespace {
307
338
  * Grades a network on the conflicts/gaps that make route hijacking possible, then traces any
308
339
  * conflict to the specific domains and organisations exposed on the affected blocks.
309
340
  *
310
- * KEYED flow: streams Server-Sent Events from the workflow runner (slug "bgp-hijack-exposure")
341
+ * Flow: streams Server-Sent Events from the workflow runner (slug "bgp-hijack-exposure")
311
342
  * and resolves to the collected { slug, steps, complete, events }.
312
343
  * Inputs: value.
313
344
  * @see https://www.whisper.security/docs/recipes/bgp-routing
@@ -320,7 +351,7 @@ export interface GraphNamespace {
320
351
  * Maps dependencies in both directions: everything that breaks if the asset fails (SPOFs) and
321
352
  * everything it relies on (its own DNS/mail/hosting/network supply chain).
322
353
  *
323
- * KEYED flow: streams Server-Sent Events from the workflow runner (slug "blast-radius")
354
+ * Flow: streams Server-Sent Events from the workflow runner (slug "blast-radius")
324
355
  * and resolves to the collected { slug, steps, complete, events }.
325
356
  * Inputs: indicator. Params: depth.
326
357
  * @see https://www.whisper.security/docs/recipes/soc
@@ -333,7 +364,7 @@ export interface GraphNamespace {
333
364
  * One-pass takedown package: reputation verdict, owner (WHOIS), abuse-list listings, and
334
365
  * surrounding infrastructure, laid out ready to hand to a registrar/host.
335
366
  *
336
- * KEYED flow: streams Server-Sent Events from the workflow runner (slug "build-takedown-evidence-package")
367
+ * Flow: streams Server-Sent Events from the workflow runner (slug "build-takedown-evidence-package")
337
368
  * and resolves to the collected { slug, steps, complete, events }.
338
369
  * Inputs: domain.
339
370
  * @see https://www.whisper.security/docs/recipes/threat-intel
@@ -346,7 +377,7 @@ export interface GraphNamespace {
346
377
  * Maps externally visible AI/agent hosts (API, model, agent) from the outside via heuristic
347
378
  * hostname patterns (api./mcp./ai./vector./llm./agent./chat./copilot.). Best-effort leads.
348
379
  *
349
- * KEYED flow: streams Server-Sent Events from the workflow runner (slug "discover-ai-agent-infrastructure")
380
+ * Flow: streams Server-Sent Events from the workflow runner (slug "discover-ai-agent-infrastructure")
350
381
  * and resolves to the collected { slug, steps, complete, events }.
351
382
  * Inputs: value.
352
383
  * @see https://www.whisper.security/docs/recipes/pentest-recon
@@ -360,7 +391,7 @@ export interface GraphNamespace {
360
391
  * CDN, neighbouring infra), threat-checks each node, never inherits maliciousness from shared
361
392
  * infra; labelled posture, no score.
362
393
  *
363
- * KEYED flow: streams Server-Sent Events from the workflow runner (slug "indicator")
394
+ * Flow: streams Server-Sent Events from the workflow runner (slug "indicator")
364
395
  * and resolves to the collected { slug, steps, complete, events }.
365
396
  * Inputs: indicator.
366
397
  * @see https://www.whisper.security/docs/recipes/soc
@@ -373,7 +404,7 @@ export interface GraphNamespace {
373
404
  * Fills the picture for one indicator: registrant (WHOIS), hosting + country, mail/name
374
405
  * servers, network behind it, reputation read.
375
406
  *
376
- * KEYED flow: streams Server-Sent Events from the workflow runner (slug "indicator-enrichment")
407
+ * Flow: streams Server-Sent Events from the workflow runner (slug "indicator-enrichment")
377
408
  * and resolves to the collected { slug, steps, complete, events }.
378
409
  * Inputs: value.
379
410
  * @see https://www.whisper.security/docs/recipes/dns-email
@@ -386,7 +417,7 @@ export interface GraphNamespace {
386
417
  * Works out the true operator (even behind privacy WHOIS), de-cloaks CDN-fronted sites to real
387
418
  * servers, pivots to the rest of that owner's estate across every layer.
388
419
  *
389
- * KEYED flow: streams Server-Sent Events from the workflow runner (slug "infrastructure-mapping")
420
+ * Flow: streams Server-Sent Events from the workflow runner (slug "infrastructure-mapping")
390
421
  * and resolves to the collected { slug, steps, complete, events }.
391
422
  * Inputs: value. Params: level.
392
423
  * @see https://www.whisper.security/docs/recipes/compliance
@@ -399,7 +430,7 @@ export interface GraphNamespace {
399
430
  * Grades how concentrated infra is - too much riding on one provider/region/data-centre/cable
400
431
  * landing - surfacing SPOFs for resilience and DORA/NIS2 fourth-party risk.
401
432
  *
402
- * KEYED flow: streams Server-Sent Events from the workflow runner (slug "map-supply-chain-concentration")
433
+ * Flow: streams Server-Sent Events from the workflow runner (slug "map-supply-chain-concentration")
403
434
  * and resolves to the collected { slug, steps, complete, events }.
404
435
  * Inputs: domain.
405
436
  * @see https://www.whisper.security/docs/recipes/compliance
@@ -412,7 +443,7 @@ export interface GraphNamespace {
412
443
  * Audits delegation, flags stale/mismatched/lame nameservers, sizes each provider's share,
413
444
  * surfaces registry facts - catches delegation weakness before exploit.
414
445
  *
415
- * KEYED flow: streams Server-Sent Events from the workflow runner (slug "nameserver-hijack-dns-consistency")
446
+ * Flow: streams Server-Sent Events from the workflow runner (slug "nameserver-hijack-dns-consistency")
416
447
  * and resolves to the collected { slug, steps, complete, events }.
417
448
  * Inputs: value.
418
449
  * @see https://www.whisper.security/docs/recipes/dns-email
@@ -425,7 +456,7 @@ export interface GraphNamespace {
425
456
  * Health card for a network/block: what it announces, peers/transit, single-upstream lean,
426
457
  * RPKI protection - the one-look reach & resilience report.
427
458
  *
428
- * KEYED flow: streams Server-Sent Events from the workflow runner (slug "route-health")
459
+ * Flow: streams Server-Sent Events from the workflow runner (slug "route-health")
429
460
  * and resolves to the collected { slug, steps, complete, events }.
430
461
  * Inputs: target.
431
462
  * @see https://www.whisper.security/docs/recipes/bgp-routing
@@ -439,7 +470,7 @@ export interface GraphNamespace {
439
470
  * no longer resolves) so you can reclaim/remove before someone else does. CNAME layer is
440
471
  * prod-ahead.
441
472
  *
442
- * KEYED flow: streams Server-Sent Events from the workflow runner (slug "subdomain-takeover")
473
+ * Flow: streams Server-Sent Events from the workflow runner (slug "subdomain-takeover")
443
474
  * and resolves to the collected { slug, steps, complete, events }.
444
475
  * Inputs: value.
445
476
  * @see https://www.whisper.security/docs/recipes/pentest-recon
@@ -452,7 +483,7 @@ export interface GraphNamespace {
452
483
  * Finds registered impersonations across misspellings/risky TLDs, separates your own defensive
453
484
  * domains from strangers', flags fresh/privacy/abuse-listed ones into a prioritised list.
454
485
  *
455
- * KEYED flow: streams Server-Sent Events from the workflow runner (slug "typosquat")
486
+ * Flow: streams Server-Sent Events from the workflow runner (slug "typosquat")
456
487
  * and resolves to the collected { slug, steps, complete, events }.
457
488
  * Inputs: domain.
458
489
  * @see https://www.whisper.security/docs/recipes/brand-protection
@@ -465,7 +496,7 @@ export interface GraphNamespace {
465
496
  * Resolves a host/IP to its canonical vendor, category and operator roles (DNS_OPERATOR / CDN
466
497
  * / ORIGIN_AS / MAIL_RECEIVER) with a confidence band.
467
498
  *
468
- * KEYLESS direct read: runs with NO key at all (a rate-limited taste, real
499
+ * Direct read: answers with NO key at all (a rate-limited taste, real
469
500
  * answers); pass a key ({ apiKey } or WHISPER_API_KEY) to lift the limit.
470
501
  * Cypher: CALL whisper.identify([$v]) YIELD host, vendor_id, canonical_name, category, roles, host_class, band
471
502
  * Returns columns: host, vendor_id, canonical_name, category, roles, host_class, band.
@@ -479,7 +510,7 @@ export interface GraphNamespace {
479
510
  * Returns a posture label + severity band + sub-labels + coverage + evidence for an indicator;
480
511
  * benign-allowlisted vs malicious-evidenced, never a bare score.
481
512
  *
482
- * KEYLESS direct read: runs with NO key at all (a rate-limited taste, real
513
+ * Direct read: answers with NO key at all (a rate-limited taste, real
483
514
  * answers); pass a key ({ apiKey } or WHISPER_API_KEY) to lift the limit.
484
515
  * Cypher: CALL whisper.assess([$v]) YIELD host, label, band, sub_labels, coverage, evidence
485
516
  * Returns columns: host, label, band, sub_labels, coverage, evidence.
@@ -493,7 +524,7 @@ export interface GraphNamespace {
493
524
  * Enumerates permutations (omission, bitsquatting, ...) of a domain, flags which exist, with a
494
525
  * per-variant confidence.
495
526
  *
496
- * KEYLESS direct read: runs with NO key at all (a rate-limited taste, real
527
+ * Direct read: answers with NO key at all (a rate-limited taste, real
497
528
  * answers); pass a key ({ apiKey } or WHISPER_API_KEY) to lift the limit.
498
529
  * Cypher: CALL whisper.variants($v) YIELD variant, method, exists, confidence
499
530
  * Returns columns: variant, method, exists, confidence.
@@ -508,7 +539,7 @@ export interface GraphNamespace {
508
539
  * ORIGIN_AS) and confidence, plus siblings and coverage - explains WHY a host maps to a
509
540
  * vendor.
510
541
  *
511
- * KEYLESS direct read: runs with NO key at all (a rate-limited taste, real
542
+ * Direct read: answers with NO key at all (a rate-limited taste, real
512
543
  * answers); pass a key ({ apiKey } or WHISPER_API_KEY) to lift the limit.
513
544
  * Cypher: CALL whisper.walk($v) YIELD coverage, host, nearest_known_vendors, no_atlas_match, siblings
514
545
  * Returns columns: coverage, host, nearest_known_vendors, no_atlas_match, siblings.
@@ -523,7 +554,7 @@ export interface GraphNamespace {
523
554
  * weight, firstSeen/lastSeen). Two live forms: bare CALL explain($v) (full columns) and CALL
524
555
  * whisper.explain($v) (restricted YIELD).
525
556
  *
526
- * KEYLESS direct read: runs with NO key at all (a rate-limited taste, real
557
+ * Direct read: answers with NO key at all (a rate-limited taste, real
527
558
  * answers); pass a key ({ apiKey } or WHISPER_API_KEY) to lift the limit.
528
559
  * Cypher: CALL whisper.explain($v) YIELD indicator, score, level, explanation, sources
529
560
  * Returns columns: indicator, score, level, explanation, sources.
@@ -537,7 +568,7 @@ export interface GraphNamespace {
537
568
  * PSL-correct eTLD+1: www.foo.co.uk -> foo.co.uk. The right way to group hosts by the thing
538
569
  * someone actually registered.
539
570
  *
540
- * KEYLESS direct read: runs with NO key at all (a rate-limited taste, real
571
+ * Direct read: answers with NO key at all (a rate-limited taste, real
541
572
  * answers); pass a key ({ apiKey } or WHISPER_API_KEY) to lift the limit.
542
573
  * Cypher: CALL whisper.psl.tldPlusOne($v) YIELD apex
543
574
  * Returns columns: apex.
@@ -551,7 +582,7 @@ export interface GraphNamespace {
551
582
  * For a PSL private-section suffix, returns the submitting org/login and evidence kind +
552
583
  * confidence; found=false for ordinary registrable domains.
553
584
  *
554
- * KEYLESS direct read: runs with NO key at all (a rate-limited taste, real
585
+ * Direct read: answers with NO key at all (a rate-limited taste, real
555
586
  * answers); pass a key ({ apiKey } or WHISPER_API_KEY) to lift the limit.
556
587
  * Cypher: CALL whisper.psl.affiliation($v) YIELD found, suffix, submitterOrg, submitterLogin, evidenceKind, confidence
557
588
  * Returns columns: found, suffix, submitterOrg, submitterLogin, evidenceKind, confidence.
@@ -565,7 +596,7 @@ export interface GraphNamespace {
565
596
  * Candidate origin IPs behind a CDN with confidence, the methods that found them (e.g.
566
597
  * links_to), and the hosting ASN/name - de-cloaks to the real server.
567
598
  *
568
- * KEYLESS direct read: runs with NO key at all (a rate-limited taste, real
599
+ * Direct read: answers with NO key at all (a rate-limited taste, real
569
600
  * answers); pass a key ({ apiKey } or WHISPER_API_KEY) to lift the limit.
570
601
  * Cypher: CALL whisper.origins($v) YIELD ip, confidence, methods, asn, asnName, kind
571
602
  * Returns columns: ip, confidence, methods, asn, asnName, kind.
@@ -579,7 +610,7 @@ export interface GraphNamespace {
579
610
  * Every observed WHOIS snapshot over time: create/update/expiry, registrar, registrant,
580
611
  * country, nameservers - the ownership/registration story.
581
612
  *
582
- * KEYLESS direct read: runs with NO key at all (a rate-limited taste, real
613
+ * Direct read: answers with NO key at all (a rate-limited taste, real
583
614
  * answers); pass a key ({ apiKey } or WHISPER_API_KEY) to lift the limit.
584
615
  * Cypher: CALL whisper.history($v)
585
616
  * Returns columns: indicator, type, queryTime, createDate, updateDate, expiryDate, registrar, registrant, country, nameServers, cached.
@@ -593,7 +624,7 @@ export interface GraphNamespace {
593
624
  * The registration-record projection of the history timeline: create/update/expiry, registrar,
594
625
  * registrant, country, nameservers per snapshot.
595
626
  *
596
- * KEYLESS direct read: runs with NO key at all (a rate-limited taste, real
627
+ * Direct read: answers with NO key at all (a rate-limited taste, real
597
628
  * answers); pass a key ({ apiKey } or WHISPER_API_KEY) to lift the limit.
598
629
  * Cypher: CALL whisper.history.whois($v) YIELD queryTime, createDate, updateDate, expiryDate, registrar, registrant, country, nameServers
599
630
  * Returns columns: queryTime, createDate, updateDate, expiryDate, registrar, registrant, country, nameServers.
@@ -608,7 +639,7 @@ export interface GraphNamespace {
608
639
  * STRING as-set name, not an integer ASN. (Membership is sparsely populated in the current
609
640
  * graph.)
610
641
  *
611
- * KEYLESS direct read: runs with NO key at all (a rate-limited taste, real
642
+ * Direct read: answers with NO key at all (a rate-limited taste, real
612
643
  * answers); pass a key ({ apiKey } or WHISPER_API_KEY) to lift the limit.
613
644
  * Cypher: CALL whisper.asSet($v) YIELD asSetName, memberAsn, sourceRir
614
645
  * Returns columns: asSetName, memberAsn, sourceRir.
@@ -622,7 +653,7 @@ export interface GraphNamespace {
622
653
  * Live Tor exit-node check: found + fingerprint + exit-address count + source + ingest time
623
654
  * for an IPv4.
624
655
  *
625
- * KEYLESS direct read: runs with NO key at all (a rate-limited taste, real
656
+ * Direct read: answers with NO key at all (a rate-limited taste, real
626
657
  * answers); pass a key ({ apiKey } or WHISPER_API_KEY) to lift the limit.
627
658
  * Cypher: CALL whisper.lookupTorRelay($v) YIELD indicator, found, fingerprint, exitAddressCount, source, ingestedAt
628
659
  * Returns columns: indicator, found, fingerprint, exitAddressCount, source, ingestedAt.
@@ -636,7 +667,7 @@ export interface GraphNamespace {
636
667
  * The self-describing schema: node/relationship types with counts, descriptions, examples,
637
668
  * source/target labels and query best-practices - the map of what the graph holds.
638
669
  *
639
- * KEYLESS direct read: runs with NO key at all (a rate-limited taste, real
670
+ * Direct read: answers with NO key at all (a rate-limited taste, real
640
671
  * answers); pass a key ({ apiKey } or WHISPER_API_KEY) to lift the limit.
641
672
  * Cypher: CALL db.schema()
642
673
  * Returns columns: type, name, count, description, example, sourceLabels, targetLabels, fastPatterns, slowPatterns, bestPractices.
@@ -647,10 +678,11 @@ export interface GraphNamespace {
647
678
  /**
648
679
  * Contribute an indicator observation or feedback back into the graph (requires an API key).
649
680
  *
650
- * The write channel: submit an indicator/feedback with an attributable API key (anonymous
651
- * submits are refused to preserve K-anonymity). Keyed-only by design.
681
+ * The write channel: submit an indicator/feedback with an attributable API key. The graph
682
+ * requires a key carrying the submit:write scope and answers anonymous callers with a clear
683
+ * 400, to preserve K-anonymity.
652
684
  *
653
- * KEYED write: CALL whisper.submit({...}) with the payload fields bound as $-parameters.
685
+ * Write: CALL whisper.submit({...}) with the payload fields bound as $-parameters.
654
686
  * Fields: kind, identifier_kind, value, observation_id, confidence, first_seen, provenance, query, results, comment, severity, v (kind required).
655
687
  * Returns columns: observation_id, kind, accepted, idempotent, promotion_state, k_bucket, advisory, v.
656
688
  * @see https://www.whisper.security/docs/cypher-api
package/index.js CHANGED
@@ -3,8 +3,9 @@
3
3
  // whisper-id - the Whisper security graph, a routable IPv6 identity, and safe egress for
4
4
  // any Node agent. Dependency-free and two-tier (Postel): the graph's direct read verbs and
5
5
  // the identity checks run KEYLESS over plain fetch() (graph.assess("8.8.8.8") needs no key
6
- // at all); the control plane, raw Cypher, and the flows unlock with WHISPER_API_KEY; only
7
- // register/egress/ip lean on the `whisper` CLI (https://whisper.online).
6
+ // at all); raw Cypher and the flows are sent the same way and the graph decides. Only the
7
+ // control plane requires WHISPER_API_KEY, and only register/egress/ip lean on the
8
+ // `whisper` CLI (https://whisper.online).
8
9
  import { execFile } from "node:child_process";
9
10
  import { promisify } from "node:util";
10
11
 
@@ -15,7 +16,11 @@ const HOSTPORT_RE = /127\.0\.0\.1:(\d{2,5})/;
15
16
  const PROXY_VARS = ["HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", "http_proxy", "https_proxy", "all_proxy"];
16
17
  const UA = "whisper-id-node"; // client identifier for the keyless + control HTTP calls
17
18
 
18
- /** Error thrown when a `whisper` invocation fails, or the CLI is not installed. */
19
+ /**
20
+ * Error thrown when a `whisper` invocation fails, the CLI is not installed, or the control
21
+ * plane refuses a call. A control-plane refusal carries the server's own `.code` (for example
22
+ * "NOT_FOUND" or "FORBIDDEN"), `.status` and `.retryable` when the server said them.
23
+ */
19
24
  export class WhisperError extends Error {
20
25
  constructor(message) {
21
26
  super(message);
@@ -23,6 +28,41 @@ export class WhisperError extends Error {
23
28
  }
24
29
  }
25
30
 
31
+ /**
32
+ * The control plane understood the call and found nothing of yours to act on. From
33
+ * revoke() it means nothing was revoked: the agent was revoked already, or it is not in your
34
+ * account. That is an answer, not a fault, so never retry it. `.code` is "NOT_FOUND",
35
+ * `.status` is 404, `.retryable` is false, and the message is the server's own.
36
+ */
37
+ export class NotFoundError extends WhisperError {
38
+ constructor(message) {
39
+ super(message);
40
+ this.name = "NotFoundError";
41
+ this.code = "NOT_FOUND";
42
+ this.status = 404;
43
+ this.retryable = false;
44
+ }
45
+ }
46
+
47
+ // NOT_FOUND is read off the code, on a 404 or with no error status; never off a bare 404,
48
+ // which is a mistyped control URL or a proxy in the way rather than a statement about the agent.
49
+ function isNotFound(e, status) {
50
+ if (!e || typeof e !== "object") return false;
51
+ if (String(e.code || "").trim().toUpperCase() !== "NOT_FOUND") return false;
52
+ return status === 404 || !status || status < 400;
53
+ }
54
+
55
+ // An older plane's ok:true revoke row whose status is not_found (directly or under item).
56
+ // Only the first of status/state present is read.
57
+ function revokeRowNotFound(rec) {
58
+ if (!rec || typeof rec !== "object") return false;
59
+ const row = rec.item && typeof rec.item === "object" ? rec.item : rec;
60
+ for (const k of ["status", "state"]) {
61
+ if (typeof row[k] === "string") return row[k].trim().toLowerCase() === "not_found";
62
+ }
63
+ return false;
64
+ }
65
+
26
66
  function bin() {
27
67
  return process.env.WHISPER_BIN || "whisper";
28
68
  }
@@ -35,7 +75,7 @@ async function run(args, { timeout = 120000, check = true } = {}) {
35
75
  if (err && err.code === "ENOENT") {
36
76
  throw new WhisperError(
37
77
  "the `whisper` CLI was not found on PATH. Install it with " +
38
- "`curl get.whisper.online | sh` (see https://whisper.online)."
78
+ "`curl -fsSL https://get.whisper.online | sh` (see https://whisper.online)."
39
79
  );
40
80
  }
41
81
  const out = { stdout: err.stdout || "", stderr: err.stderr || "", code: typeof err.code === "number" ? err.code : 1 };
@@ -308,7 +348,8 @@ function resolveKey(opts) {
308
348
 
309
349
  // The one place that talks to the control plane. Builds the injection-proof query, POSTs
310
350
  // it with the key in X-API-Key, decodes either envelope shape, and throws WhisperError
311
- // (carrying the server's helpful `detail`, plus .status/.retryAfter) on ok:false. The key
351
+ // (carrying the server's message, plus .code/.status/.retryable/.retryAfter) on ok:false,
352
+ // a NotFoundError when the code is NOT_FOUND. The key
312
353
  // is never logged, never placed on argv, never returned.
313
354
  async function callControl(op, args, opts = {}) {
314
355
  const key = resolveKey(opts);
@@ -349,8 +390,13 @@ async function callControl(op, args, opts = {}) {
349
390
  const env = decodeEnvelope(body, res.status);
350
391
  if (!env.ok) {
351
392
  const e = env.error || {};
352
- const werr = new WhisperError(e.detail || e.title || e.type || `control plane returned HTTP ${env.status}`);
393
+ // The plane's own errors are {code, message, retryable}; RFC 7807 ones use detail/title.
394
+ const msg = e.detail || e.message || e.title || e.type || `control plane returned HTTP ${env.status}`;
395
+ if (isNotFound(e, env.status)) throw new NotFoundError(msg);
396
+ const werr = new WhisperError(msg);
353
397
  werr.status = env.status;
398
+ if (e.code) werr.code = String(e.code);
399
+ if (typeof e.retryable === "boolean") werr.retryable = e.retryable;
354
400
  if (env.retryAfter != null) werr.retryAfter = env.retryAfter;
355
401
  if (e.suggestions) werr.suggestions = e.suggestions;
356
402
  throw werr;
@@ -402,15 +448,30 @@ export async function logs(opts = {}) {
402
448
 
403
449
  /**
404
450
  * Fully revoke an agent - IRREVERSIBLE (op:revoke; needs admin:dns). Liberal input:
405
- * `revoke("agent-id")`, `revoke("2a04:2a01:...")`, or `revoke({ agent, apiKey })`.
406
- * → `{ status, ... }`.
451
+ * `revoke("agent-id")`, `revoke("2a04:2a01:...")`, `revoke("my-label")`, or
452
+ * `revoke({ agent, apiKey })`. → `{ status, ... }`.
453
+ *
454
+ * Throws NotFoundError when nothing was revoked: no agent of yours matches, because it was
455
+ * revoked already or is not in your account. Do not retry that; list() shows what is left.
456
+ * Both ways the plane says it throw it: the NOT_FOUND error of current servers, and the
457
+ * `status: not_found` row older ones answered with ok:true.
407
458
  */
408
459
  export async function revoke(target, maybeOpts) {
409
460
  const o = typeof target === "string" ? maybeOpts || {} : target || {};
410
461
  const sel = typeof target === "string" ? target : o.agent;
411
462
  if (!sel || !String(sel).trim()) throw new WhisperError("revoke() needs an agent id or /128 address");
412
- const recs = await callControl("revoke", { agent: String(sel).trim() }, o);
463
+ const selector = String(sel).trim();
464
+ const recs = await callControl("revoke", { agent: selector }, o);
413
465
  const rec = recs.length ? recs[0] : {};
466
+ if (revokeRowNotFound(rec)) {
467
+ const row = rec.item && typeof rec.item === "object" ? rec.item : rec;
468
+ const said = [row.message, row.detail].find((v) => typeof v === "string" && v.trim());
469
+ throw new NotFoundError(
470
+ said ||
471
+ `nothing was revoked: no agent of yours matches ${JSON.stringify(selector)}. ` +
472
+ "If you revoked it before, there is nothing left to revoke; list() shows what is left"
473
+ );
474
+ }
414
475
  return { status: rec.status || rec.state || "submitted", ...rec };
415
476
  }
416
477
 
@@ -463,7 +524,7 @@ export async function agent(sel = {}, maybeOpts) {
463
524
  // origins, history, historyWhois, asset, lookupTorRelay, dbSchema) run KEYLESS - no key
464
525
  // at all, a rate-limited taste (~100/window) with real answers; a key (opts.apiKey or
465
526
  // WHISPER_API_KEY) lifts the limit. Raw Cypher (graph.query), the 15 multi-step flows,
466
- // and the submit write channel stay KEYED. Everything POSTs to the SAME endpoint the
527
+ // and the submit write channel are proxied up the same way. Everything POSTs to the SAME endpoint the
467
528
  // control plane uses (https://graph.whisper.online/api/query), reusing resolveKey() so
468
529
  // there is one auth path, not two: a present key rides ONLY in the X-API-Key header, an
469
530
  // absent one sends NO auth header at all. It does NOT go through the whisper.agents()
@@ -508,17 +569,12 @@ function normalizeGraph(body) {
508
569
  // (plus .status/.retryAfter). Two-tier auth (Postel: liberal in what we accept): a keyless
509
570
  // method (`keyless: true`) NEVER throws for a missing key - with a key it runs unlimited,
510
571
  // without one the X-API-Key header is omitted entirely and the server serves the
511
- // rate-limited keyless taste. A keyed method with no key fails fast with a helpful error.
572
+ // rate-limited keyless taste. No method is refused here; the graph decides what to answer.
512
573
  // The key is only ever sent in X-API-Key: never logged, never on argv, never returned.
513
- async function callGraph(cypher, params, opts = {}, methodName = "query", keyless = false) {
574
+ async function callGraph(cypher, params, opts = {}) {
575
+ // Proxy up with whatever key there is, or none: the graph owns the keyless policy and
576
+ // decides what to answer. The SDK adds no gate the server did not ask for.
514
577
  const key = resolveKey(opts) || "";
515
- if (!key && !keyless) {
516
- throw new WhisperError(
517
- `\`graph.${methodName}\` needs a Whisper API key: pass { apiKey } or set WHISPER_API_KEY. ` +
518
- "(The direct read verbs - assess, identify, explain, origins, and friends - run keyless " +
519
- "with no key at all; raw Cypher, the flows, and submit are keyed. See graph.recipes().)"
520
- );
521
- }
522
578
  const url = graphUrl();
523
579
  const timeout = opts.timeout || 60000;
524
580
  const ctrl = new AbortController();
@@ -619,14 +675,14 @@ function graphValue(name, value) {
619
675
  }
620
676
 
621
677
  /**
622
- * Run raw Cypher against the Whisper Security graph (keyed). The escape hatch under every
678
+ * Run raw Cypher against the Whisper Security graph. The escape hatch under every
623
679
  * typed method: `graph.query("MATCH (n) RETURN n LIMIT 1", {}, { apiKey })`. Returns the
624
680
  * full `{ columns, rows, statistics }` (rows are objects keyed by column name).
625
681
  * @see https://www.whisper.security/docs/cypher-api
626
682
  */
627
683
  async function graphQuery(cypher, params, opts = {}) {
628
684
  if (!cypher || !String(cypher).trim()) throw new WhisperError("graph.query() needs a Cypher string");
629
- return callGraph(String(cypher), params || {}, opts, "query");
685
+ return callGraph(String(cypher), params || {}, opts);
630
686
  }
631
687
 
632
688
  /**
@@ -650,7 +706,7 @@ function graphRecipes() {
650
706
 
651
707
  // ── Flow runner (gallery/run, Server-Sent Events) ───────────────────────────────────
652
708
  // A flow entry is a multi-step read workflow orchestrated by the Whisper workflow runner:
653
- // POST {slug, value, paramValues} to the gallery/run endpoint (keyed, X-API-Key) and consume
709
+ // POST {slug, value, paramValues} to the gallery/run endpoint (X-API-Key when set) and consume
654
710
  // the SSE stream. Each `step` event carries a step's bound Cypher + rows; the terminal
655
711
  // `complete` event carries the aggregate context. callFlow() collects the whole stream and
656
712
  // resolves to { slug, steps, complete, events } - one call, the full flow.
@@ -732,19 +788,15 @@ function parseSseRecord(record) {
732
788
  return { event, data };
733
789
  }
734
790
 
735
- // The one place that talks to the workflow runner. Requires a key (shared resolveKey),
736
- // POSTs {slug, value, paramValues}, consumes the SSE stream, and resolves to the collected
791
+ // The one place that talks to the workflow runner. Sends the key when there is one (shared
792
+ // resolveKey) and goes without when there is not, POSTs {slug, value, paramValues},
793
+ // consumes the SSE stream, and resolves to the collected
737
794
  // { slug, steps, complete, events }. Flows are multi-step, so the default timeout is 5
738
795
  // minutes (override with opts.timeout). On an HTTP error, an `error` event, or a stream
739
796
  // that ends with no result at all, throws a WhisperError with the server's clear detail.
740
797
  async function callFlow(m, input, opts = {}) {
741
- const key = resolveKey(opts);
742
- if (!key) {
743
- throw new WhisperError(
744
- `\`graph.${m.name}\` needs a Whisper API key: pass { apiKey } or set WHISPER_API_KEY. ` +
745
- "(The graph's direct read verbs run keyless; flows run keyed on the workflow runner.)"
746
- );
747
- }
798
+ // Flows proxy up like every other graph verb; the runner decides on the key.
799
+ const key = resolveKey(opts) || "";
748
800
  const body = flowWireBody(m, input);
749
801
  const url = flowRunUrl();
750
802
  const timeout = opts.timeout || 300000;
@@ -753,14 +805,17 @@ async function callFlow(m, input, opts = {}) {
753
805
  try {
754
806
  let res;
755
807
  try {
808
+ const headers = {
809
+ "Content-Type": "application/json",
810
+ Accept: "text/event-stream",
811
+ "User-Agent": UA,
812
+ };
813
+ // Present => keyed. ABSENT => keyless: an empty credential is not the same as no
814
+ // credential, and a server handed "" refuses instead of applying keyless policy.
815
+ if (key) headers["X-API-Key"] = key;
756
816
  res = await fetch(url, {
757
817
  method: "POST",
758
- headers: {
759
- "Content-Type": "application/json",
760
- Accept: "text/event-stream",
761
- "X-API-Key": key,
762
- "User-Agent": UA,
763
- },
818
+ headers,
764
819
  body: JSON.stringify(body),
765
820
  signal: ctrl.signal,
766
821
  });
@@ -868,7 +923,7 @@ function makeMapCall(m) {
868
923
  }
869
924
  const keys = Object.keys(bound).sort();
870
925
  const cypher = `CALL ${m.proc}({` + keys.map((k) => `${k}:$${k}`).join(", ") + "})";
871
- return callGraph(cypher, bound, opts, m.name);
926
+ return callGraph(cypher, bound, opts);
872
927
  };
873
928
  }
874
929
 
@@ -886,9 +941,9 @@ for (const m of GRAPH_METHODS) {
886
941
  } else if (m.fields) {
887
942
  graph[m.name] = makeMapCall(m);
888
943
  } else if (m.input) {
889
- graph[m.name] = async (value, opts = {}) => callGraph(m.cypher, { [m.input]: graphValue(m.name, value) }, opts, m.name, m.keyless);
944
+ graph[m.name] = async (value, opts = {}) => callGraph(m.cypher, { [m.input]: graphValue(m.name, value) }, opts);
890
945
  } else {
891
- graph[m.name] = async (opts = {}) => callGraph(m.cypher, {}, opts, m.name, m.keyless);
946
+ graph[m.name] = async (opts = {}) => callGraph(m.cypher, {}, opts);
892
947
  }
893
948
  graph[m.name].docs = m.docs;
894
949
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "whisper-id",
3
- "version": "0.6.1",
4
- "description": "Give any Node agent the Whisper security graph (keyless reads + keyed Cypher), a real routable Whisper IPv6 identity, safe egress, and the full control plane.",
3
+ "version": "0.7.1",
4
+ "description": "Give any Node agent the Whisper security graph (keyless reads, no client-side gate), a real routable Whisper IPv6 identity, safe egress, and the full control plane.",
5
5
  "type": "module",
6
6
  "exports": {
7
7
  ".": {
@@ -14,6 +14,7 @@
14
14
  "index.js",
15
15
  "index.d.ts",
16
16
  "README.md",
17
+ "SECURITY.md",
17
18
  "LICENSE"
18
19
  ],
19
20
  "engines": {