whisper-id 0.6.0 → 0.7.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
@@ -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
@@ -165,14 +165,15 @@ transparently inside `withEgress`.
165
165
 
166
166
  Zero dependencies. The **graph read verbs**, the **keyless** checks, and the **control plane** are
167
167
  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.
168
+ (or pass `{ apiKey }`) for the control plane (`whisper.agents`), which always needs it. Graph calls
169
+ never require a key from this SDK: they go up with one or without, and the graph decides what to
170
+ answer. A key lifts the keyless rate limit. `verify` and `rdap` need no key either.
170
171
 
171
172
  Only **egress** and the CLI-backed calls (`register`, `egress`, `withEgress`, `ip`) need the
172
173
  `whisper` CLI on your `PATH`:
173
174
 
174
175
  ```sh
175
- curl get.whisper.online | sh
176
+ curl -fsSL https://get.whisper.online | sh
176
177
  ```
177
178
 
178
179
  (`$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
@@ -157,7 +157,7 @@ export function agent(opts?: AgentSelectOptions): Promise<AgentItem | null>;
157
157
  // KEYLESS - no key at all, a rate-limited taste with real answers; a key ({ apiKey } or
158
158
  // WHISPER_API_KEY) lifts the limit. Raw Cypher (`graph.query`), the 15 multi-step `flow`
159
159
  // 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
160
+ // write are proxied up the same way. A present key rides only in `X-API-Key`; an absent one sends no auth
161
161
  // header at all. The method surface below is GENERATED from the Whisper query catalog by
162
162
  // scripts/gen-graph.mjs; every method's docs live at the `@see` URL and are also on the
163
163
  // function itself as `.docs`, and `graph.recipes()` lists the whole catalog.
@@ -181,7 +181,7 @@ export interface GraphRecipe {
181
181
  docsUrl: string;
182
182
  }
183
183
 
184
- /** Result of a keyed graph query. `rows` are objects keyed by column name. */
184
+ /** Result of a graph query. `rows` are objects keyed by column name. */
185
185
  export interface GraphResult {
186
186
  columns: string[];
187
187
  rows: Record<string, unknown>[];
@@ -237,15 +237,18 @@ export interface GraphFlowResult {
237
237
  }
238
238
 
239
239
  /**
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`.
240
+ * The graph namespace: one typed method per catalog entry (13 direct Cypher reads,
241
+ * `submit`, and 15 workflow-runner flows), plus a raw `query` escape hatch and
242
+ * `recipes()` discovery. Every verb is proxied to the graph with a key if you have one
243
+ * and without if you do not: the graph owns the keyless policy and decides what to
244
+ * answer, so the SDK never refuses a call on your behalf. A key (`{ apiKey }` or
245
+ * `WHISPER_API_KEY`) lifts the keyless rate limit. The control plane (`whisper.agents`:
246
+ * agents, policy, logs, identity) is a separate surface and always requires a key.
247
+ * Every method carries its canonical docs URL as `.docs`.
245
248
  */
246
249
  export interface GraphNamespace {
247
250
  /**
248
- * Raw Cypher escape hatch (KEYED): run arbitrary Cypher with `params` bound
251
+ * Raw Cypher escape hatch: run arbitrary Cypher with `params` bound
249
252
  * server-side as $-parameters. Returns the full `{columns,rows,statistics}`.
250
253
  * @see https://www.whisper.security/docs/cypher-api
251
254
  */
@@ -268,7 +271,7 @@ export interface GraphNamespace {
268
271
  * letters have in-country DNS_ROOT_INSTANCE anycast nodes, the Global-vs-Local split, the BGP
269
272
  * origin ASNs hosting them, and a resilience grade.
270
273
  *
271
- * KEYED flow: streams Server-Sent Events from the workflow runner (slug "anycast-dns-root-sovereignty")
274
+ * Flow: streams Server-Sent Events from the workflow runner (slug "anycast-dns-root-sovereignty")
272
275
  * and resolves to the collected { slug, steps, complete, events }.
273
276
  * Inputs: country. Params: instanceType.
274
277
  * @see https://www.whisper.security/docs/recipes/compliance
@@ -281,7 +284,7 @@ export interface GraphNamespace {
281
284
  * From a starting foothold, finds shared dependencies whose compromise reaches furthest; for
282
285
  * any two indicators, traces how they are actually connected.
283
286
  *
284
- * KEYED flow: streams Server-Sent Events from the workflow runner (slug "attack-path")
287
+ * Flow: streams Server-Sent Events from the workflow runner (slug "attack-path")
285
288
  * and resolves to the collected { slug, steps, complete, events }.
286
289
  * Inputs: value, other. Params: level.
287
290
  * @see https://www.whisper.security/docs/recipes/attack-path
@@ -294,7 +297,7 @@ export interface GraphNamespace {
294
297
  * Maps the full external footprint - subdomains, name/mail servers, registrant, third-party
295
298
  * services, connected web - and scores the exposure.
296
299
  *
297
- * KEYED flow: streams Server-Sent Events from the workflow runner (slug "attack-surface")
300
+ * Flow: streams Server-Sent Events from the workflow runner (slug "attack-surface")
298
301
  * and resolves to the collected { slug, steps, complete, events }.
299
302
  * Inputs: domain. Params: level.
300
303
  * @see https://www.whisper.security/docs/recipes/pentest-recon
@@ -307,7 +310,7 @@ export interface GraphNamespace {
307
310
  * Grades a network on the conflicts/gaps that make route hijacking possible, then traces any
308
311
  * conflict to the specific domains and organisations exposed on the affected blocks.
309
312
  *
310
- * KEYED flow: streams Server-Sent Events from the workflow runner (slug "bgp-hijack-exposure")
313
+ * Flow: streams Server-Sent Events from the workflow runner (slug "bgp-hijack-exposure")
311
314
  * and resolves to the collected { slug, steps, complete, events }.
312
315
  * Inputs: value.
313
316
  * @see https://www.whisper.security/docs/recipes/bgp-routing
@@ -320,7 +323,7 @@ export interface GraphNamespace {
320
323
  * Maps dependencies in both directions: everything that breaks if the asset fails (SPOFs) and
321
324
  * everything it relies on (its own DNS/mail/hosting/network supply chain).
322
325
  *
323
- * KEYED flow: streams Server-Sent Events from the workflow runner (slug "blast-radius")
326
+ * Flow: streams Server-Sent Events from the workflow runner (slug "blast-radius")
324
327
  * and resolves to the collected { slug, steps, complete, events }.
325
328
  * Inputs: indicator. Params: depth.
326
329
  * @see https://www.whisper.security/docs/recipes/soc
@@ -333,7 +336,7 @@ export interface GraphNamespace {
333
336
  * One-pass takedown package: reputation verdict, owner (WHOIS), abuse-list listings, and
334
337
  * surrounding infrastructure, laid out ready to hand to a registrar/host.
335
338
  *
336
- * KEYED flow: streams Server-Sent Events from the workflow runner (slug "build-takedown-evidence-package")
339
+ * Flow: streams Server-Sent Events from the workflow runner (slug "build-takedown-evidence-package")
337
340
  * and resolves to the collected { slug, steps, complete, events }.
338
341
  * Inputs: domain.
339
342
  * @see https://www.whisper.security/docs/recipes/threat-intel
@@ -346,7 +349,7 @@ export interface GraphNamespace {
346
349
  * Maps externally visible AI/agent hosts (API, model, agent) from the outside via heuristic
347
350
  * hostname patterns (api./mcp./ai./vector./llm./agent./chat./copilot.). Best-effort leads.
348
351
  *
349
- * KEYED flow: streams Server-Sent Events from the workflow runner (slug "discover-ai-agent-infrastructure")
352
+ * Flow: streams Server-Sent Events from the workflow runner (slug "discover-ai-agent-infrastructure")
350
353
  * and resolves to the collected { slug, steps, complete, events }.
351
354
  * Inputs: value.
352
355
  * @see https://www.whisper.security/docs/recipes/pentest-recon
@@ -360,7 +363,7 @@ export interface GraphNamespace {
360
363
  * CDN, neighbouring infra), threat-checks each node, never inherits maliciousness from shared
361
364
  * infra; labelled posture, no score.
362
365
  *
363
- * KEYED flow: streams Server-Sent Events from the workflow runner (slug "indicator")
366
+ * Flow: streams Server-Sent Events from the workflow runner (slug "indicator")
364
367
  * and resolves to the collected { slug, steps, complete, events }.
365
368
  * Inputs: indicator.
366
369
  * @see https://www.whisper.security/docs/recipes/soc
@@ -373,7 +376,7 @@ export interface GraphNamespace {
373
376
  * Fills the picture for one indicator: registrant (WHOIS), hosting + country, mail/name
374
377
  * servers, network behind it, reputation read.
375
378
  *
376
- * KEYED flow: streams Server-Sent Events from the workflow runner (slug "indicator-enrichment")
379
+ * Flow: streams Server-Sent Events from the workflow runner (slug "indicator-enrichment")
377
380
  * and resolves to the collected { slug, steps, complete, events }.
378
381
  * Inputs: value.
379
382
  * @see https://www.whisper.security/docs/recipes/dns-email
@@ -386,7 +389,7 @@ export interface GraphNamespace {
386
389
  * Works out the true operator (even behind privacy WHOIS), de-cloaks CDN-fronted sites to real
387
390
  * servers, pivots to the rest of that owner's estate across every layer.
388
391
  *
389
- * KEYED flow: streams Server-Sent Events from the workflow runner (slug "infrastructure-mapping")
392
+ * Flow: streams Server-Sent Events from the workflow runner (slug "infrastructure-mapping")
390
393
  * and resolves to the collected { slug, steps, complete, events }.
391
394
  * Inputs: value. Params: level.
392
395
  * @see https://www.whisper.security/docs/recipes/compliance
@@ -399,7 +402,7 @@ export interface GraphNamespace {
399
402
  * Grades how concentrated infra is - too much riding on one provider/region/data-centre/cable
400
403
  * landing - surfacing SPOFs for resilience and DORA/NIS2 fourth-party risk.
401
404
  *
402
- * KEYED flow: streams Server-Sent Events from the workflow runner (slug "map-supply-chain-concentration")
405
+ * Flow: streams Server-Sent Events from the workflow runner (slug "map-supply-chain-concentration")
403
406
  * and resolves to the collected { slug, steps, complete, events }.
404
407
  * Inputs: domain.
405
408
  * @see https://www.whisper.security/docs/recipes/compliance
@@ -412,7 +415,7 @@ export interface GraphNamespace {
412
415
  * Audits delegation, flags stale/mismatched/lame nameservers, sizes each provider's share,
413
416
  * surfaces registry facts - catches delegation weakness before exploit.
414
417
  *
415
- * KEYED flow: streams Server-Sent Events from the workflow runner (slug "nameserver-hijack-dns-consistency")
418
+ * Flow: streams Server-Sent Events from the workflow runner (slug "nameserver-hijack-dns-consistency")
416
419
  * and resolves to the collected { slug, steps, complete, events }.
417
420
  * Inputs: value.
418
421
  * @see https://www.whisper.security/docs/recipes/dns-email
@@ -425,7 +428,7 @@ export interface GraphNamespace {
425
428
  * Health card for a network/block: what it announces, peers/transit, single-upstream lean,
426
429
  * RPKI protection - the one-look reach & resilience report.
427
430
  *
428
- * KEYED flow: streams Server-Sent Events from the workflow runner (slug "route-health")
431
+ * Flow: streams Server-Sent Events from the workflow runner (slug "route-health")
429
432
  * and resolves to the collected { slug, steps, complete, events }.
430
433
  * Inputs: target.
431
434
  * @see https://www.whisper.security/docs/recipes/bgp-routing
@@ -439,7 +442,7 @@ export interface GraphNamespace {
439
442
  * no longer resolves) so you can reclaim/remove before someone else does. CNAME layer is
440
443
  * prod-ahead.
441
444
  *
442
- * KEYED flow: streams Server-Sent Events from the workflow runner (slug "subdomain-takeover")
445
+ * Flow: streams Server-Sent Events from the workflow runner (slug "subdomain-takeover")
443
446
  * and resolves to the collected { slug, steps, complete, events }.
444
447
  * Inputs: value.
445
448
  * @see https://www.whisper.security/docs/recipes/pentest-recon
@@ -452,7 +455,7 @@ export interface GraphNamespace {
452
455
  * Finds registered impersonations across misspellings/risky TLDs, separates your own defensive
453
456
  * domains from strangers', flags fresh/privacy/abuse-listed ones into a prioritised list.
454
457
  *
455
- * KEYED flow: streams Server-Sent Events from the workflow runner (slug "typosquat")
458
+ * Flow: streams Server-Sent Events from the workflow runner (slug "typosquat")
456
459
  * and resolves to the collected { slug, steps, complete, events }.
457
460
  * Inputs: domain.
458
461
  * @see https://www.whisper.security/docs/recipes/brand-protection
@@ -465,7 +468,7 @@ export interface GraphNamespace {
465
468
  * Resolves a host/IP to its canonical vendor, category and operator roles (DNS_OPERATOR / CDN
466
469
  * / ORIGIN_AS / MAIL_RECEIVER) with a confidence band.
467
470
  *
468
- * KEYLESS direct read: runs with NO key at all (a rate-limited taste, real
471
+ * Direct read: answers with NO key at all (a rate-limited taste, real
469
472
  * answers); pass a key ({ apiKey } or WHISPER_API_KEY) to lift the limit.
470
473
  * Cypher: CALL whisper.identify([$v]) YIELD host, vendor_id, canonical_name, category, roles, host_class, band
471
474
  * Returns columns: host, vendor_id, canonical_name, category, roles, host_class, band.
@@ -479,7 +482,7 @@ export interface GraphNamespace {
479
482
  * Returns a posture label + severity band + sub-labels + coverage + evidence for an indicator;
480
483
  * benign-allowlisted vs malicious-evidenced, never a bare score.
481
484
  *
482
- * KEYLESS direct read: runs with NO key at all (a rate-limited taste, real
485
+ * Direct read: answers with NO key at all (a rate-limited taste, real
483
486
  * answers); pass a key ({ apiKey } or WHISPER_API_KEY) to lift the limit.
484
487
  * Cypher: CALL whisper.assess([$v]) YIELD host, label, band, sub_labels, coverage, evidence
485
488
  * Returns columns: host, label, band, sub_labels, coverage, evidence.
@@ -493,7 +496,7 @@ export interface GraphNamespace {
493
496
  * Enumerates permutations (omission, bitsquatting, ...) of a domain, flags which exist, with a
494
497
  * per-variant confidence.
495
498
  *
496
- * 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
497
500
  * answers); pass a key ({ apiKey } or WHISPER_API_KEY) to lift the limit.
498
501
  * Cypher: CALL whisper.variants($v) YIELD variant, method, exists, confidence
499
502
  * Returns columns: variant, method, exists, confidence.
@@ -508,7 +511,7 @@ export interface GraphNamespace {
508
511
  * ORIGIN_AS) and confidence, plus siblings and coverage - explains WHY a host maps to a
509
512
  * vendor.
510
513
  *
511
- * KEYLESS direct read: runs with NO key at all (a rate-limited taste, real
514
+ * Direct read: answers with NO key at all (a rate-limited taste, real
512
515
  * answers); pass a key ({ apiKey } or WHISPER_API_KEY) to lift the limit.
513
516
  * Cypher: CALL whisper.walk($v) YIELD coverage, host, nearest_known_vendors, no_atlas_match, siblings
514
517
  * Returns columns: coverage, host, nearest_known_vendors, no_atlas_match, siblings.
@@ -523,7 +526,7 @@ export interface GraphNamespace {
523
526
  * weight, firstSeen/lastSeen). Two live forms: bare CALL explain($v) (full columns) and CALL
524
527
  * whisper.explain($v) (restricted YIELD).
525
528
  *
526
- * KEYLESS direct read: runs with NO key at all (a rate-limited taste, real
529
+ * Direct read: answers with NO key at all (a rate-limited taste, real
527
530
  * answers); pass a key ({ apiKey } or WHISPER_API_KEY) to lift the limit.
528
531
  * Cypher: CALL whisper.explain($v) YIELD indicator, score, level, explanation, sources
529
532
  * Returns columns: indicator, score, level, explanation, sources.
@@ -537,7 +540,7 @@ export interface GraphNamespace {
537
540
  * PSL-correct eTLD+1: www.foo.co.uk -> foo.co.uk. The right way to group hosts by the thing
538
541
  * someone actually registered.
539
542
  *
540
- * KEYLESS direct read: runs with NO key at all (a rate-limited taste, real
543
+ * Direct read: answers with NO key at all (a rate-limited taste, real
541
544
  * answers); pass a key ({ apiKey } or WHISPER_API_KEY) to lift the limit.
542
545
  * Cypher: CALL whisper.psl.tldPlusOne($v) YIELD apex
543
546
  * Returns columns: apex.
@@ -551,7 +554,7 @@ export interface GraphNamespace {
551
554
  * For a PSL private-section suffix, returns the submitting org/login and evidence kind +
552
555
  * confidence; found=false for ordinary registrable domains.
553
556
  *
554
- * 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
555
558
  * answers); pass a key ({ apiKey } or WHISPER_API_KEY) to lift the limit.
556
559
  * Cypher: CALL whisper.psl.affiliation($v) YIELD found, suffix, submitterOrg, submitterLogin, evidenceKind, confidence
557
560
  * Returns columns: found, suffix, submitterOrg, submitterLogin, evidenceKind, confidence.
@@ -565,7 +568,7 @@ export interface GraphNamespace {
565
568
  * Candidate origin IPs behind a CDN with confidence, the methods that found them (e.g.
566
569
  * links_to), and the hosting ASN/name - de-cloaks to the real server.
567
570
  *
568
- * 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
569
572
  * answers); pass a key ({ apiKey } or WHISPER_API_KEY) to lift the limit.
570
573
  * Cypher: CALL whisper.origins($v) YIELD ip, confidence, methods, asn, asnName, kind
571
574
  * Returns columns: ip, confidence, methods, asn, asnName, kind.
@@ -579,7 +582,7 @@ export interface GraphNamespace {
579
582
  * Every observed WHOIS snapshot over time: create/update/expiry, registrar, registrant,
580
583
  * country, nameservers - the ownership/registration story.
581
584
  *
582
- * 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
583
586
  * answers); pass a key ({ apiKey } or WHISPER_API_KEY) to lift the limit.
584
587
  * Cypher: CALL whisper.history($v)
585
588
  * Returns columns: indicator, type, queryTime, createDate, updateDate, expiryDate, registrar, registrant, country, nameServers, cached.
@@ -593,7 +596,7 @@ export interface GraphNamespace {
593
596
  * The registration-record projection of the history timeline: create/update/expiry, registrar,
594
597
  * registrant, country, nameservers per snapshot.
595
598
  *
596
- * 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
597
600
  * answers); pass a key ({ apiKey } or WHISPER_API_KEY) to lift the limit.
598
601
  * Cypher: CALL whisper.history.whois($v) YIELD queryTime, createDate, updateDate, expiryDate, registrar, registrant, country, nameServers
599
602
  * Returns columns: queryTime, createDate, updateDate, expiryDate, registrar, registrant, country, nameServers.
@@ -608,7 +611,7 @@ export interface GraphNamespace {
608
611
  * STRING as-set name, not an integer ASN. (Membership is sparsely populated in the current
609
612
  * graph.)
610
613
  *
611
- * KEYLESS direct read: runs with NO key at all (a rate-limited taste, real
614
+ * Direct read: answers with NO key at all (a rate-limited taste, real
612
615
  * answers); pass a key ({ apiKey } or WHISPER_API_KEY) to lift the limit.
613
616
  * Cypher: CALL whisper.asSet($v) YIELD asSetName, memberAsn, sourceRir
614
617
  * Returns columns: asSetName, memberAsn, sourceRir.
@@ -622,7 +625,7 @@ export interface GraphNamespace {
622
625
  * Live Tor exit-node check: found + fingerprint + exit-address count + source + ingest time
623
626
  * for an IPv4.
624
627
  *
625
- * KEYLESS direct read: runs with NO key at all (a rate-limited taste, real
628
+ * Direct read: answers with NO key at all (a rate-limited taste, real
626
629
  * answers); pass a key ({ apiKey } or WHISPER_API_KEY) to lift the limit.
627
630
  * Cypher: CALL whisper.lookupTorRelay($v) YIELD indicator, found, fingerprint, exitAddressCount, source, ingestedAt
628
631
  * Returns columns: indicator, found, fingerprint, exitAddressCount, source, ingestedAt.
@@ -636,7 +639,7 @@ export interface GraphNamespace {
636
639
  * The self-describing schema: node/relationship types with counts, descriptions, examples,
637
640
  * source/target labels and query best-practices - the map of what the graph holds.
638
641
  *
639
- * 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
640
643
  * answers); pass a key ({ apiKey } or WHISPER_API_KEY) to lift the limit.
641
644
  * Cypher: CALL db.schema()
642
645
  * Returns columns: type, name, count, description, example, sourceLabels, targetLabels, fastPatterns, slowPatterns, bestPractices.
@@ -647,10 +650,11 @@ export interface GraphNamespace {
647
650
  /**
648
651
  * Contribute an indicator observation or feedback back into the graph (requires an API key).
649
652
  *
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.
653
+ * The write channel: submit an indicator/feedback with an attributable API key. The graph
654
+ * requires a key carrying the submit:write scope and answers anonymous callers with a clear
655
+ * 400, to preserve K-anonymity.
652
656
  *
653
- * KEYED write: CALL whisper.submit({...}) with the payload fields bound as $-parameters.
657
+ * Write: CALL whisper.submit({...}) with the payload fields bound as $-parameters.
654
658
  * Fields: kind, identifier_kind, value, observation_id, confidence, first_seen, provenance, query, results, comment, severity, v (kind required).
655
659
  * Returns columns: observation_id, kind, accepted, idempotent, promotion_state, k_bucket, advisory, v.
656
660
  * @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
 
@@ -35,7 +36,7 @@ async function run(args, { timeout = 120000, check = true } = {}) {
35
36
  if (err && err.code === "ENOENT") {
36
37
  throw new WhisperError(
37
38
  "the `whisper` CLI was not found on PATH. Install it with " +
38
- "`curl get.whisper.online | sh` (see https://whisper.online)."
39
+ "`curl -fsSL https://get.whisper.online | sh` (see https://whisper.online)."
39
40
  );
40
41
  }
41
42
  const out = { stdout: err.stdout || "", stderr: err.stderr || "", code: typeof err.code === "number" ? err.code : 1 };
@@ -203,7 +204,7 @@ export async function ip({ timeout = 60000 } = {}) {
203
204
  // read from opts.apiKey or $WHISPER_API_KEY and sent ONLY in the X-API-Key header.
204
205
 
205
206
  function controlUrl() {
206
- return process.env.WHISPER_CONTROL_URL || "https://graph.whisper.security/api/query";
207
+ return process.env.WHISPER_CONTROL_URL || "https://graph.whisper.online/api/query";
207
208
  }
208
209
 
209
210
  // Conservative-emit (RFC 761): render a value as a Cypher literal so no input - however
@@ -463,8 +464,8 @@ export async function agent(sel = {}, maybeOpts) {
463
464
  // origins, history, historyWhois, asset, lookupTorRelay, dbSchema) run KEYLESS - no key
464
465
  // at all, a rate-limited taste (~100/window) with real answers; a key (opts.apiKey or
465
466
  // 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
467
- // control plane uses (https://graph.whisper.security/api/query), reusing resolveKey() so
467
+ // and the submit write channel are proxied up the same way. Everything POSTs to the SAME endpoint the
468
+ // control plane uses (https://graph.whisper.online/api/query), reusing resolveKey() so
468
469
  // there is one auth path, not two: a present key rides ONLY in the X-API-Key header, an
469
470
  // absent one sends NO auth header at all. It does NOT go through the whisper.agents()
470
471
  // control wrapper: the graph contract is different (body {query,parameters}; reply
@@ -508,17 +509,12 @@ function normalizeGraph(body) {
508
509
  // (plus .status/.retryAfter). Two-tier auth (Postel: liberal in what we accept): a keyless
509
510
  // method (`keyless: true`) NEVER throws for a missing key - with a key it runs unlimited,
510
511
  // 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.
512
+ // rate-limited keyless taste. No method is refused here; the graph decides what to answer.
512
513
  // 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) {
514
+ async function callGraph(cypher, params, opts = {}) {
515
+ // Proxy up with whatever key there is, or none: the graph owns the keyless policy and
516
+ // decides what to answer. The SDK adds no gate the server did not ask for.
514
517
  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
518
  const url = graphUrl();
523
519
  const timeout = opts.timeout || 60000;
524
520
  const ctrl = new AbortController();
@@ -619,14 +615,14 @@ function graphValue(name, value) {
619
615
  }
620
616
 
621
617
  /**
622
- * Run raw Cypher against the Whisper Security graph (keyed). The escape hatch under every
618
+ * Run raw Cypher against the Whisper Security graph. The escape hatch under every
623
619
  * typed method: `graph.query("MATCH (n) RETURN n LIMIT 1", {}, { apiKey })`. Returns the
624
620
  * full `{ columns, rows, statistics }` (rows are objects keyed by column name).
625
621
  * @see https://www.whisper.security/docs/cypher-api
626
622
  */
627
623
  async function graphQuery(cypher, params, opts = {}) {
628
624
  if (!cypher || !String(cypher).trim()) throw new WhisperError("graph.query() needs a Cypher string");
629
- return callGraph(String(cypher), params || {}, opts, "query");
625
+ return callGraph(String(cypher), params || {}, opts);
630
626
  }
631
627
 
632
628
  /**
@@ -650,7 +646,7 @@ function graphRecipes() {
650
646
 
651
647
  // ── Flow runner (gallery/run, Server-Sent Events) ───────────────────────────────────
652
648
  // 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
649
+ // POST {slug, value, paramValues} to the gallery/run endpoint (X-API-Key when set) and consume
654
650
  // the SSE stream. Each `step` event carries a step's bound Cypher + rows; the terminal
655
651
  // `complete` event carries the aggregate context. callFlow() collects the whole stream and
656
652
  // resolves to { slug, steps, complete, events } - one call, the full flow.
@@ -732,19 +728,15 @@ function parseSseRecord(record) {
732
728
  return { event, data };
733
729
  }
734
730
 
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
731
+ // The one place that talks to the workflow runner. Sends the key when there is one (shared
732
+ // resolveKey) and goes without when there is not, POSTs {slug, value, paramValues},
733
+ // consumes the SSE stream, and resolves to the collected
737
734
  // { slug, steps, complete, events }. Flows are multi-step, so the default timeout is 5
738
735
  // minutes (override with opts.timeout). On an HTTP error, an `error` event, or a stream
739
736
  // that ends with no result at all, throws a WhisperError with the server's clear detail.
740
737
  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
- }
738
+ // Flows proxy up like every other graph verb; the runner decides on the key.
739
+ const key = resolveKey(opts) || "";
748
740
  const body = flowWireBody(m, input);
749
741
  const url = flowRunUrl();
750
742
  const timeout = opts.timeout || 300000;
@@ -753,14 +745,17 @@ async function callFlow(m, input, opts = {}) {
753
745
  try {
754
746
  let res;
755
747
  try {
748
+ const headers = {
749
+ "Content-Type": "application/json",
750
+ Accept: "text/event-stream",
751
+ "User-Agent": UA,
752
+ };
753
+ // Present => keyed. ABSENT => keyless: an empty credential is not the same as no
754
+ // credential, and a server handed "" refuses instead of applying keyless policy.
755
+ if (key) headers["X-API-Key"] = key;
756
756
  res = await fetch(url, {
757
757
  method: "POST",
758
- headers: {
759
- "Content-Type": "application/json",
760
- Accept: "text/event-stream",
761
- "X-API-Key": key,
762
- "User-Agent": UA,
763
- },
758
+ headers,
764
759
  body: JSON.stringify(body),
765
760
  signal: ctrl.signal,
766
761
  });
@@ -868,7 +863,7 @@ function makeMapCall(m) {
868
863
  }
869
864
  const keys = Object.keys(bound).sort();
870
865
  const cypher = `CALL ${m.proc}({` + keys.map((k) => `${k}:$${k}`).join(", ") + "})";
871
- return callGraph(cypher, bound, opts, m.name);
866
+ return callGraph(cypher, bound, opts);
872
867
  };
873
868
  }
874
869
 
@@ -886,9 +881,9 @@ for (const m of GRAPH_METHODS) {
886
881
  } else if (m.fields) {
887
882
  graph[m.name] = makeMapCall(m);
888
883
  } 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);
884
+ graph[m.name] = async (value, opts = {}) => callGraph(m.cypher, { [m.input]: graphValue(m.name, value) }, opts);
890
885
  } else {
891
- graph[m.name] = async (opts = {}) => callGraph(m.cypher, {}, opts, m.name, m.keyless);
886
+ graph[m.name] = async (opts = {}) => callGraph(m.cypher, {}, opts);
892
887
  }
893
888
  graph[m.name].docs = m.docs;
894
889
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "whisper-id",
3
- "version": "0.6.0",
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.0",
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": {