@didis/client 0.2.3 → 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 +16 -1
- package/README.md +5 -0
- package/dist/index.d.ts +52 -1
- package/dist/index.js +70 -2
- package/dist/webcrypto.d.ts +2 -0
- package/dist/webcrypto.js +10 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,19 @@ 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
|
+
|
|
14
|
+
## [0.2.4] - 2026-10-19
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- Automatic wait-and-retry for `429` (request budget used up) and `503` (temporarily overloaded) responses that carry `Retry-After`. By default the client retries up to 2 times and never waits longer than 10 seconds per attempt; a short random delay is added so clients refused together do not return together. Both statuses are refused before the request is processed, so retrying is safe for every call. Tune with `new DidisClient({ retry: { maxRetries, maxWaitMs } })` or turn off with `retry: false`. When retries run out, the last `DidisError` is thrown as before, with `problem.retryAfter`.
|
|
19
|
+
|
|
7
20
|
## [0.2.3] - 2026-10-18
|
|
8
21
|
|
|
9
22
|
### Added
|
|
@@ -56,7 +69,9 @@ All notable changes to `@didis/client` are listed here. The format follows [Keep
|
|
|
56
69
|
- `DidisError` carrying the RFC 9457 problem body and `Retry-After`.
|
|
57
70
|
- Webhook helpers `verifyWebhookSignature` and `verifyCustomerMonitorSignature`.
|
|
58
71
|
|
|
59
|
-
[Unreleased]: https://github.com/did-is/didis-js/compare/v0.2.
|
|
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
|
|
74
|
+
[0.2.4]: https://github.com/did-is/didis-js/compare/v0.2.3...v0.2.4
|
|
60
75
|
[0.2.3]: https://github.com/did-is/didis-js/compare/v0.2.2...v0.2.3
|
|
61
76
|
[0.2.2]: https://github.com/did-is/didis-js/compare/v0.2.1...v0.2.2
|
|
62
77
|
[0.2.1]: https://github.com/did-is/didis-js/compare/v0.2.0...v0.2.1
|
package/README.md
CHANGED
|
@@ -63,6 +63,10 @@ try {
|
|
|
63
63
|
|
|
64
64
|
Identifiers are percent-encoded exactly once by the client. Non-2xx responses throw `DidisError` with the RFC 9457 problem body (including the resolution `trace` where available).
|
|
65
65
|
|
|
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
|
+
|
|
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
|
+
|
|
66
70
|
## Guard agent tools
|
|
67
71
|
|
|
68
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).
|
|
@@ -146,6 +150,7 @@ const ok = await verifyWebhookSignature(
|
|
|
146
150
|
| `inspectMcp(endpoint)` / `inspectA2a(url)` | `GET /v1/mcp/inspect`, `/v1/a2a/inspect` |
|
|
147
151
|
| `verifyDelegation(chain, opts)` / `authorize(chain, tool, opts)` / `verifyTool(agent, tool, root?)` | `/v1/agents/*` |
|
|
148
152
|
| `fastVerifyDid(did)` / `fastVerifyJws(jws, relationship?)` | `/v1/fast-verify/*` |
|
|
153
|
+
| `usage()` (with `apiKey`) | `GET /v1/customer/usage` |
|
|
149
154
|
| `watch` / `watches` / `unwatch` / `events` | `/v1/monitoring/*` (admin) |
|
|
150
155
|
|
|
151
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,20 +682,55 @@ 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;
|
|
694
|
+
/**
|
|
695
|
+
* Automatic wait-and-retry when the API answers 429 (request budget used up) or 503
|
|
696
|
+
* (temporarily overloaded) with a `Retry-After` header. Such refusals happen before the
|
|
697
|
+
* request is processed, so retrying is safe for every call. Default: up to 2 retries,
|
|
698
|
+
* never waiting longer than 10 s for one retry. Pass `false` to turn it off.
|
|
699
|
+
*/
|
|
700
|
+
retry?: false | {
|
|
701
|
+
maxRetries?: number;
|
|
702
|
+
maxWaitMs?: number;
|
|
703
|
+
};
|
|
675
704
|
}
|
|
705
|
+
/** Milliseconds to wait from a `Retry-After` value (delta-seconds or HTTP-date); null if unusable. */
|
|
706
|
+
export declare function retryAfterMs(value: string | null | undefined, now?: number): number | null;
|
|
676
707
|
/** Public DID.is API. Point `baseUrl` at your own deployment when self-hosting. */
|
|
677
708
|
export declare const DEFAULT_BASE_URL = "https://did.is/api";
|
|
678
709
|
/** Package version, kept equal to package.json (checked by the test suite). */
|
|
679
|
-
export declare const VERSION = "0.2.
|
|
710
|
+
export declare const VERSION = "0.2.5";
|
|
680
711
|
export declare class DidisClient {
|
|
681
712
|
private readonly baseUrl;
|
|
682
713
|
private readonly adminToken?;
|
|
714
|
+
private readonly apiKey?;
|
|
683
715
|
private readonly timeoutMs;
|
|
684
716
|
private readonly fetchImpl;
|
|
717
|
+
private readonly maxRetries;
|
|
718
|
+
private readonly maxWaitMs;
|
|
685
719
|
constructor(options?: string | DidisClientOptions);
|
|
720
|
+
/**
|
|
721
|
+
* Waits out a 429/503 refusal when the server says how long, within the configured bounds.
|
|
722
|
+
* A small random delay spreads out many clients that were refused at the same moment.
|
|
723
|
+
*/
|
|
724
|
+
private backoff;
|
|
686
725
|
private request;
|
|
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>;
|
|
687
734
|
private admin;
|
|
688
735
|
health(): Promise<HealthStatus>;
|
|
689
736
|
/** Enriched resolution: verdict, evidence dimensions, graph, telemetry. */
|
|
@@ -709,6 +756,10 @@ export declare class DidisClient {
|
|
|
709
756
|
}, credential?: unknown): Promise<PolicyResponse>;
|
|
710
757
|
inspectMcp(endpoint: string): Promise<McpInspection>;
|
|
711
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
|
+
*/
|
|
712
763
|
verifyDelegation(chain: string[], opts?: {
|
|
713
764
|
trustedRoots?: string[];
|
|
714
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
|
// ---------------------------------------------------------------------------------------------
|
|
@@ -12,10 +12,21 @@ export class DidisError extends Error {
|
|
|
12
12
|
this.problem = problem;
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
|
+
/** Milliseconds to wait from a `Retry-After` value (delta-seconds or HTTP-date); null if unusable. */
|
|
16
|
+
export function retryAfterMs(value, now = Date.now()) {
|
|
17
|
+
if (!value)
|
|
18
|
+
return null;
|
|
19
|
+
const v = value.trim();
|
|
20
|
+
if (/^\d+$/.test(v))
|
|
21
|
+
return Number(v) * 1000;
|
|
22
|
+
const at = Date.parse(v);
|
|
23
|
+
return Number.isNaN(at) ? null : Math.max(0, at - now);
|
|
24
|
+
}
|
|
25
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
15
26
|
/** Public DID.is API. Point `baseUrl` at your own deployment when self-hosting. */
|
|
16
27
|
export const DEFAULT_BASE_URL = "https://did.is/api";
|
|
17
28
|
/** Package version, kept equal to package.json (checked by the test suite). */
|
|
18
|
-
export const VERSION = "0.2.
|
|
29
|
+
export const VERSION = "0.2.5";
|
|
19
30
|
/**
|
|
20
31
|
* Identifies the SDK to the API so DID.is can count SDK use apart from other HTTP clients.
|
|
21
32
|
* Only the product name and version are sent. Browsers are left alone: there the browser
|
|
@@ -26,16 +37,46 @@ const enc = (s) => encodeURIComponent(s.trim());
|
|
|
26
37
|
export class DidisClient {
|
|
27
38
|
baseUrl;
|
|
28
39
|
adminToken;
|
|
40
|
+
apiKey;
|
|
29
41
|
timeoutMs;
|
|
30
42
|
fetchImpl;
|
|
43
|
+
maxRetries;
|
|
44
|
+
maxWaitMs;
|
|
31
45
|
constructor(options = {}) {
|
|
32
46
|
const o = typeof options === "string" ? { baseUrl: options } : options;
|
|
33
47
|
this.baseUrl = (o.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
34
48
|
this.adminToken = o.adminToken;
|
|
49
|
+
this.apiKey = o.apiKey || undefined;
|
|
35
50
|
this.timeoutMs = o.timeoutMs ?? 30_000;
|
|
36
51
|
this.fetchImpl = o.fetch ?? fetch;
|
|
52
|
+
this.maxRetries = o.retry === false ? 0 : Math.max(0, Math.floor(o.retry?.maxRetries ?? 2));
|
|
53
|
+
this.maxWaitMs = o.retry === false ? 0 : Math.max(0, o.retry?.maxWaitMs ?? 10_000);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Waits out a 429/503 refusal when the server says how long, within the configured bounds.
|
|
57
|
+
* A small random delay spreads out many clients that were refused at the same moment.
|
|
58
|
+
*/
|
|
59
|
+
async backoff(attempt, status, retryAfter) {
|
|
60
|
+
if (attempt >= this.maxRetries || (status !== 429 && status !== 503))
|
|
61
|
+
return false;
|
|
62
|
+
const wait = retryAfterMs(retryAfter);
|
|
63
|
+
if (wait === null || wait > this.maxWaitMs)
|
|
64
|
+
return false;
|
|
65
|
+
await sleep(wait + Math.floor(Math.random() * 250));
|
|
66
|
+
return true;
|
|
37
67
|
}
|
|
38
68
|
async request(method, path, body, headers = {}) {
|
|
69
|
+
for (let attempt = 0;; attempt++) {
|
|
70
|
+
try {
|
|
71
|
+
return await this.once(method, path, body, headers);
|
|
72
|
+
}
|
|
73
|
+
catch (e) {
|
|
74
|
+
if (!(e instanceof DidisError) || !(await this.backoff(attempt, e.status, e.problem.retryAfter ?? null)))
|
|
75
|
+
throw e;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
async once(method, path, body, headers = {}) {
|
|
39
80
|
const controller = new AbortController();
|
|
40
81
|
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
41
82
|
try {
|
|
@@ -65,6 +106,20 @@ export class DidisClient {
|
|
|
65
106
|
clearTimeout(timer);
|
|
66
107
|
}
|
|
67
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
|
+
}
|
|
68
123
|
admin() {
|
|
69
124
|
if (!this.adminToken)
|
|
70
125
|
throw new Error("adminToken is required for monitoring routes");
|
|
@@ -152,15 +207,28 @@ export class DidisClient {
|
|
|
152
207
|
return this.request("POST", "/v1/policies/evaluate", { did, policy, credential });
|
|
153
208
|
}
|
|
154
209
|
inspectMcp(endpoint) {
|
|
210
|
+
if (this.apiKey)
|
|
211
|
+
return this.paid("POST", "mcp/inspect", { endpoint });
|
|
155
212
|
return this.request("GET", `/v1/mcp/inspect?endpoint=${encodeURIComponent(endpoint)}`);
|
|
156
213
|
}
|
|
157
214
|
inspectA2a(url) {
|
|
215
|
+
if (this.apiKey)
|
|
216
|
+
return this.paid("POST", "a2a/inspect", { url });
|
|
158
217
|
return this.request("GET", `/v1/a2a/inspect?url=${encodeURIComponent(url)}`);
|
|
159
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
|
+
*/
|
|
160
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
|
+
}
|
|
161
227
|
return this.request("POST", "/v1/agents/verify-delegation", { chain, ...opts });
|
|
162
228
|
}
|
|
163
229
|
authorize(chain, tool, opts = {}) {
|
|
230
|
+
if (this.apiKey)
|
|
231
|
+
return this.paid("POST", "agents/authorize", { chain, tool, ...(opts.trustedRoots ? { trustedRoots: opts.trustedRoots } : {}) });
|
|
164
232
|
return this.request("POST", "/v1/agents/authorize", { chain, tool, ...opts });
|
|
165
233
|
}
|
|
166
234
|
verifyTool(agent, tool, root) {
|
package/dist/webcrypto.d.ts
CHANGED
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.
|
|
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",
|