whisper-id 0.7.0 → 0.7.2
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 +5 -2
- package/index.d.ts +31 -3
- package/index.js +66 -6
- package/package.json +1 -1
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
|
+
Three things for any Node agent, in one dependency-free package: query the **Whisper security graph** (billions of nodes and tens of billions of edges joining the physical, network, naming, ownership and threat layers, queried with Cypher; live totals at nic.whisper.online/stats), 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
|
|
@@ -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
|
|
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
|
|------|------|
|
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
|
-
|
|
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
|
|
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
|
-
/**
|
|
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}`. */
|
package/index.js
CHANGED
|
@@ -16,7 +16,11 @@ const HOSTPORT_RE = /127\.0\.0\.1:(\d{2,5})/;
|
|
|
16
16
|
const PROXY_VARS = ["HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", "http_proxy", "https_proxy", "all_proxy"];
|
|
17
17
|
const UA = "whisper-id-node"; // client identifier for the keyless + control HTTP calls
|
|
18
18
|
|
|
19
|
-
/**
|
|
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
|
+
*/
|
|
20
24
|
export class WhisperError extends Error {
|
|
21
25
|
constructor(message) {
|
|
22
26
|
super(message);
|
|
@@ -24,6 +28,41 @@ export class WhisperError extends Error {
|
|
|
24
28
|
}
|
|
25
29
|
}
|
|
26
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
|
+
|
|
27
66
|
function bin() {
|
|
28
67
|
return process.env.WHISPER_BIN || "whisper";
|
|
29
68
|
}
|
|
@@ -309,7 +348,8 @@ function resolveKey(opts) {
|
|
|
309
348
|
|
|
310
349
|
// The one place that talks to the control plane. Builds the injection-proof query, POSTs
|
|
311
350
|
// it with the key in X-API-Key, decodes either envelope shape, and throws WhisperError
|
|
312
|
-
// (carrying the server's
|
|
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
|
|
313
353
|
// is never logged, never placed on argv, never returned.
|
|
314
354
|
async function callControl(op, args, opts = {}) {
|
|
315
355
|
const key = resolveKey(opts);
|
|
@@ -350,8 +390,13 @@ async function callControl(op, args, opts = {}) {
|
|
|
350
390
|
const env = decodeEnvelope(body, res.status);
|
|
351
391
|
if (!env.ok) {
|
|
352
392
|
const e = env.error || {};
|
|
353
|
-
|
|
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);
|
|
354
397
|
werr.status = env.status;
|
|
398
|
+
if (e.code) werr.code = String(e.code);
|
|
399
|
+
if (typeof e.retryable === "boolean") werr.retryable = e.retryable;
|
|
355
400
|
if (env.retryAfter != null) werr.retryAfter = env.retryAfter;
|
|
356
401
|
if (e.suggestions) werr.suggestions = e.suggestions;
|
|
357
402
|
throw werr;
|
|
@@ -403,15 +448,30 @@ export async function logs(opts = {}) {
|
|
|
403
448
|
|
|
404
449
|
/**
|
|
405
450
|
* Fully revoke an agent - IRREVERSIBLE (op:revoke; needs admin:dns). Liberal input:
|
|
406
|
-
* `revoke("agent-id")`, `revoke("2a04:2a01:...")`,
|
|
407
|
-
* → `{ 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.
|
|
408
458
|
*/
|
|
409
459
|
export async function revoke(target, maybeOpts) {
|
|
410
460
|
const o = typeof target === "string" ? maybeOpts || {} : target || {};
|
|
411
461
|
const sel = typeof target === "string" ? target : o.agent;
|
|
412
462
|
if (!sel || !String(sel).trim()) throw new WhisperError("revoke() needs an agent id or /128 address");
|
|
413
|
-
const
|
|
463
|
+
const selector = String(sel).trim();
|
|
464
|
+
const recs = await callControl("revoke", { agent: selector }, o);
|
|
414
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
|
+
}
|
|
415
475
|
return { status: rec.status || rec.state || "submitted", ...rec };
|
|
416
476
|
}
|
|
417
477
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "whisper-id",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
4
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": {
|