@didis/client 0.2.4 → 0.2.5

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/CHANGELOG.md CHANGED
@@ -4,6 +4,13 @@ All notable changes to `@didis/client` are listed here. The format follows [Keep
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.2.5] - 2026-10-19
8
+
9
+ ### Added
10
+
11
+ - Optional project API key: `new DidisClient({ apiKey })`. With a key, MCP inspection, A2A inspection, tool-call authorization and delegation-chain checks use the paid `/v1/customer/*` routes and count against your team's monthly allowance instead of the anonymous per-IP limit. Results are identical to the public routes. Each call sends one fresh `Idempotency-Key`; automatic `429`/`503` retries reuse it, so a request is never billed twice. The key is never sent to public routes, and `register: true` delegation checks always use the public route. Without a key nothing changes.
12
+ - `usage()` returns this month's team allowance (`CustomerUsage`: used, reserved, limit and remaining per pool). Requires the API key.
13
+
7
14
  ## [0.2.4] - 2026-10-19
8
15
 
9
16
  ### Added
@@ -62,7 +69,8 @@ All notable changes to `@didis/client` are listed here. The format follows [Keep
62
69
  - `DidisError` carrying the RFC 9457 problem body and `Retry-After`.
63
70
  - Webhook helpers `verifyWebhookSignature` and `verifyCustomerMonitorSignature`.
64
71
 
65
- [Unreleased]: https://github.com/did-is/didis-js/compare/v0.2.4...HEAD
72
+ [Unreleased]: https://github.com/did-is/didis-js/compare/v0.2.5...HEAD
73
+ [0.2.5]: https://github.com/did-is/didis-js/compare/v0.2.4...v0.2.5
66
74
  [0.2.4]: https://github.com/did-is/didis-js/compare/v0.2.3...v0.2.4
67
75
  [0.2.3]: https://github.com/did-is/didis-js/compare/v0.2.2...v0.2.3
68
76
  [0.2.2]: https://github.com/did-is/didis-js/compare/v0.2.1...v0.2.2
package/README.md CHANGED
@@ -65,6 +65,8 @@ Identifiers are percent-encoded exactly once by the client. Non-2xx responses th
65
65
 
66
66
  **Rate limits.** When the public budget is used up the API answers `429` with `Retry-After`. The client waits that long and retries (up to 2 times, at most 10 s per wait), so a loop of calls slows down instead of failing. Tune with `new DidisClient({ retry: { maxRetries: 3, maxWaitMs: 5000 } })` or turn it off with `retry: false`.
67
67
 
68
+ **Project API key (optional).** Every check works without a key. A team that needs more than the anonymous per-IP limit can create a key in the DID.is console (Free plan included) and pass it: `new DidisClient({ apiKey: process.env.DIDIS_API_KEY })`. Then `inspectMcp`, `inspectA2a`, `authorize` and `verifyDelegation` use the `/v1/customer/*` routes and count against the team's monthly allowance; results are identical. Each call sends one fresh `Idempotency-Key` (retries reuse it, so nothing is billed twice). `await didis.usage()` shows the allowance left. Keep the key server-side; it is never sent to public routes.
69
+
68
70
  ## Guard agent tools
69
71
 
70
72
  Wrap a tool handler so it runs only when (1) the tool definition the agent received still matches the one you approved and (2) DID.is says the agent's registered delegation grants the tool. Every failure throws `DidisGuardError` and the handler does not run (fail-closed).
@@ -148,6 +150,7 @@ const ok = await verifyWebhookSignature(
148
150
  | `inspectMcp(endpoint)` / `inspectA2a(url)` | `GET /v1/mcp/inspect`, `/v1/a2a/inspect` |
149
151
  | `verifyDelegation(chain, opts)` / `authorize(chain, tool, opts)` / `verifyTool(agent, tool, root?)` | `/v1/agents/*` |
150
152
  | `fastVerifyDid(did)` / `fastVerifyJws(jws, relationship?)` | `/v1/fast-verify/*` |
153
+ | `usage()` (with `apiKey`) | `GET /v1/customer/usage` |
151
154
  | `watch` / `watches` / `unwatch` / `events` | `/v1/monitoring/*` (admin) |
152
155
 
153
156
  Public API reference: <https://did.is/developers>. Monitoring routes (`watch`, `watches`, `unwatch`, `events`) need an admin token and are only available on self-hosted deployments; they are not exposed by the public API.
package/dist/index.d.ts CHANGED
@@ -360,6 +360,18 @@ export interface ToolEvidence {
360
360
  drift: string;
361
361
  inputSchema: unknown;
362
362
  }
363
+ /** `GET /v1/customer/usage`: this month's team allowance (all of the team's projects share it). */
364
+ export interface CustomerUsage {
365
+ period: string;
366
+ standardUsed: number;
367
+ standardReserved: number;
368
+ standardLimit: number;
369
+ standardRemaining: number;
370
+ expensiveUsed: number;
371
+ expensiveReserved: number;
372
+ expensiveLimit: number;
373
+ expensiveRemaining: number;
374
+ }
363
375
  export interface McpInspection {
364
376
  endpoint: string;
365
377
  status: "INSPECTED" | "AUTH_REQUIRED" | string;
@@ -670,6 +682,13 @@ export interface DidisClientOptions {
670
682
  baseUrl?: string;
671
683
  /** Bearer token for admin-only monitoring routes. Never ship it to browsers. */
672
684
  adminToken?: string;
685
+ /**
686
+ * Project API key from the DID.is console (optional). With a key, `inspectMcp`, `inspectA2a`,
687
+ * `authorize` and `verifyDelegation` use the paid routes and count against your team's monthly
688
+ * allowance instead of the anonymous per-IP limit. Results are identical. The key is sent only
689
+ * to `/v1/customer/*` routes, never to public routes. Keep it server-side.
690
+ */
691
+ apiKey?: string;
673
692
  timeoutMs?: number;
674
693
  fetch?: typeof fetch;
675
694
  /**
@@ -688,10 +707,11 @@ export declare function retryAfterMs(value: string | null | undefined, now?: num
688
707
  /** Public DID.is API. Point `baseUrl` at your own deployment when self-hosting. */
689
708
  export declare const DEFAULT_BASE_URL = "https://did.is/api";
690
709
  /** Package version, kept equal to package.json (checked by the test suite). */
691
- export declare const VERSION = "0.2.4";
710
+ export declare const VERSION = "0.2.5";
692
711
  export declare class DidisClient {
693
712
  private readonly baseUrl;
694
713
  private readonly adminToken?;
714
+ private readonly apiKey?;
695
715
  private readonly timeoutMs;
696
716
  private readonly fetchImpl;
697
717
  private readonly maxRetries;
@@ -704,6 +724,13 @@ export declare class DidisClient {
704
724
  private backoff;
705
725
  private request;
706
726
  private once;
727
+ /**
728
+ * Paid route call: the project API key plus one fresh Idempotency-Key. Automatic retries of a
729
+ * 429/503 reuse the same key, so a request is never billed twice.
730
+ */
731
+ private paid;
732
+ /** Team allowance for this month (requires `apiKey`). Same numbers as the RateLimit headers. */
733
+ usage(): Promise<CustomerUsage>;
707
734
  private admin;
708
735
  health(): Promise<HealthStatus>;
709
736
  /** Enriched resolution: verdict, evidence dimensions, graph, telemetry. */
@@ -729,6 +756,10 @@ export declare class DidisClient {
729
756
  }, credential?: unknown): Promise<PolicyResponse>;
730
757
  inspectMcp(endpoint: string): Promise<McpInspection>;
731
758
  inspectA2a(url: string): Promise<A2aInspection>;
759
+ /**
760
+ * `register: true` stores the chain for `verifyTool`; it always uses the public route (the paid
761
+ * route never writes shared state), so the API key is not sent for it.
762
+ */
732
763
  verifyDelegation(chain: string[], opts?: {
733
764
  trustedRoots?: string[];
734
765
  register?: boolean;
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { subtle } from "./webcrypto.js";
1
+ import { randomId, subtle } from "./webcrypto.js";
2
2
  // ---------------------------------------------------------------------------------------------
3
3
  // Client
4
4
  // ---------------------------------------------------------------------------------------------
@@ -26,7 +26,7 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
26
26
  /** Public DID.is API. Point `baseUrl` at your own deployment when self-hosting. */
27
27
  export const DEFAULT_BASE_URL = "https://did.is/api";
28
28
  /** Package version, kept equal to package.json (checked by the test suite). */
29
- export const VERSION = "0.2.4";
29
+ export const VERSION = "0.2.5";
30
30
  /**
31
31
  * Identifies the SDK to the API so DID.is can count SDK use apart from other HTTP clients.
32
32
  * Only the product name and version are sent. Browsers are left alone: there the browser
@@ -37,6 +37,7 @@ const enc = (s) => encodeURIComponent(s.trim());
37
37
  export class DidisClient {
38
38
  baseUrl;
39
39
  adminToken;
40
+ apiKey;
40
41
  timeoutMs;
41
42
  fetchImpl;
42
43
  maxRetries;
@@ -45,6 +46,7 @@ export class DidisClient {
45
46
  const o = typeof options === "string" ? { baseUrl: options } : options;
46
47
  this.baseUrl = (o.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
47
48
  this.adminToken = o.adminToken;
49
+ this.apiKey = o.apiKey || undefined;
48
50
  this.timeoutMs = o.timeoutMs ?? 30_000;
49
51
  this.fetchImpl = o.fetch ?? fetch;
50
52
  this.maxRetries = o.retry === false ? 0 : Math.max(0, Math.floor(o.retry?.maxRetries ?? 2));
@@ -104,6 +106,20 @@ export class DidisClient {
104
106
  clearTimeout(timer);
105
107
  }
106
108
  }
109
+ /**
110
+ * Paid route call: the project API key plus one fresh Idempotency-Key. Automatic retries of a
111
+ * 429/503 reuse the same key, so a request is never billed twice.
112
+ */
113
+ async paid(method, path, body) {
114
+ const headers = { authorization: `Bearer ${this.apiKey}`, ...(method === "POST" ? { "idempotency-key": await randomId() } : {}) };
115
+ return this.request(method, `/v1/customer/${path}`, body, headers);
116
+ }
117
+ /** Team allowance for this month (requires `apiKey`). Same numbers as the RateLimit headers. */
118
+ usage() {
119
+ if (!this.apiKey)
120
+ return Promise.reject(new Error("apiKey is required for usage()"));
121
+ return this.paid("GET", "usage");
122
+ }
107
123
  admin() {
108
124
  if (!this.adminToken)
109
125
  throw new Error("adminToken is required for monitoring routes");
@@ -191,15 +207,28 @@ export class DidisClient {
191
207
  return this.request("POST", "/v1/policies/evaluate", { did, policy, credential });
192
208
  }
193
209
  inspectMcp(endpoint) {
210
+ if (this.apiKey)
211
+ return this.paid("POST", "mcp/inspect", { endpoint });
194
212
  return this.request("GET", `/v1/mcp/inspect?endpoint=${encodeURIComponent(endpoint)}`);
195
213
  }
196
214
  inspectA2a(url) {
215
+ if (this.apiKey)
216
+ return this.paid("POST", "a2a/inspect", { url });
197
217
  return this.request("GET", `/v1/a2a/inspect?url=${encodeURIComponent(url)}`);
198
218
  }
219
+ /**
220
+ * `register: true` stores the chain for `verifyTool`; it always uses the public route (the paid
221
+ * route never writes shared state), so the API key is not sent for it.
222
+ */
199
223
  verifyDelegation(chain, opts = {}) {
224
+ if (this.apiKey && !opts.register) {
225
+ return this.paid("POST", "agents/verify-delegation", { chain, ...(opts.trustedRoots ? { trustedRoots: opts.trustedRoots } : {}) });
226
+ }
200
227
  return this.request("POST", "/v1/agents/verify-delegation", { chain, ...opts });
201
228
  }
202
229
  authorize(chain, tool, opts = {}) {
230
+ if (this.apiKey)
231
+ return this.paid("POST", "agents/authorize", { chain, tool, ...(opts.trustedRoots ? { trustedRoots: opts.trustedRoots } : {}) });
203
232
  return this.request("POST", "/v1/agents/authorize", { chain, tool, ...opts });
204
233
  }
205
234
  verifyTool(agent, tool, root) {
@@ -1 +1,3 @@
1
1
  export declare function subtle(): Promise<SubtleCrypto>;
2
+ /** Random UUID v4 for idempotency keys; Node 18 has no global `crypto`. */
3
+ export declare function randomId(): Promise<string>;
package/dist/webcrypto.js CHANGED
@@ -14,3 +14,13 @@ export function subtle() {
14
14
  }
15
15
  return cached;
16
16
  }
17
+ /** Random UUID v4 for idempotency keys; Node 18 has no global `crypto`. */
18
+ export async function randomId() {
19
+ const g = globalThis.crypto;
20
+ if (g?.randomUUID)
21
+ return g.randomUUID();
22
+ const m = (await import("node:crypto"));
23
+ if (!m.randomUUID)
24
+ throw new Error("crypto.randomUUID is not available in this runtime");
25
+ return m.randomUUID();
26
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@didis/client",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
4
4
  "description": "Official TypeScript/JavaScript SDK for the DID.is public API: DID resolution with evidence, credentials, policies and agent trust",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",