@didis/client 0.2.5 → 0.2.7

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
@@ -1,79 +1,111 @@
1
- # Changelog
2
-
3
- All notable changes to `@didis/client` are listed here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the package uses [Semantic Versioning](https://semver.org/). Before 1.0.0, a minor version (0.x.0) may add features; breaking changes are called out under **Changed** with a migration note.
4
-
5
- ## [Unreleased]
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
-
20
- ## [0.2.3] - 2026-10-18
21
-
22
- ### Added
23
-
24
- - Requests now carry `user-agent: didis-client/<version>` (Node, Deno, Bun and other non-browser runtimes; browsers keep their own user agent). Only the package name and version are sent, so DID.is can count SDK use separately from other HTTP clients. New export `VERSION`.
25
-
26
- - Type `LinkedAccountEvidence` and `Evidence.linkedAccounts`: each `alsoKnownAs` value with whether that website links back to the DID (`CONFIRMED`, `NOT_CONFIRMED`, `FAILED`, `UNSUPPORTED`, `UNREACHABLE`, `NOT_CHECKABLE`). A confirmation proves the same party controls the website origin and the DID; it does not prove who that party is.
27
-
28
- - Policy rule `linkedAccounts` in `PolicyRules`: 1–4 https origins (for example `["https://shop.example"]`) that must be listed in the DID's `alsoKnownAs` and confirm the DID back. Use it to require "this DID belongs to our official website" in one policy call.
29
-
30
- ## [0.2.2] - 2026-10-18
31
-
32
- ### Added
33
-
34
- - `verifyCredential` option `requiredClaims` (`ClaimRequirement[]`): check conditions such as `{ path: "/age_over_18", equals: true }` without receiving claim values. The response field `requirements` (`RequirementsOutcome`) reports `met` and a per-path result; conditions are evaluated only for a `VALID` credential.
35
-
36
- ## [0.2.1] - 2026-10-14
37
-
38
- ### Added
39
-
40
- - Guard option `capability` (on `check`, `wrap` and `withDidisGuard`): the DID.is capability a tool needs when it differs from the tool name. MCP and OpenAI tool names cannot contain `:`, so use `{ tool: "weather_forecast", capability: "weather:forecast" }`. Pins stay keyed by the tool name; DID.is is asked about the capability; the ALLOW cache is keyed by capability. Without `capability`, behaviour is unchanged (the tool name is the capability).
41
- - `onDecision` events and `DidisGuardError.detail` now include `capability`.
42
- - Examples for the MCP TypeScript SDK (`mcp-client.mjs`), LangChain (`langchain-tool.mjs`) and the OpenAI Agents SDK (`openai-agents-tool.mjs`).
43
-
44
- ### Changed
45
-
46
- - A capability containing `*`, empty segments or whitespace is refused with `GUARD_MISCONFIGURED` before any network call. Wildcards belong in delegations, not in tool calls.
47
-
48
- ## [0.2.0] - 2026-10-13
49
-
50
- ### Added
51
-
52
- - Fail-closed tool guard: `createDidisGuard`, `withDidisGuard` and `DidisGuardError`. A guarded handler runs only when the tool definition still matches its pin and DID.is `verifyTool` answers `ALLOW` for the configured agent (and root, if set). Drift, `DENY`, network errors, `429` and unexpected answers all throw, and the handler does not run. See [Guard agent tools](https://docs.did.is/build/guard-tools/).
53
- - Tool definition pinning: `canonicalJson` (RFC 8785 JCS), `toolDefinitionSha256`, `mcpInventoryHash` and `pinToolDefinitions`. Hashes are identical to the `definitionSha256` and `inventoryHash` values DID.is reports.
54
- - `attestMcpPins(client, endpoint, pins)` compares a public MCP server, as observed by DID.is, with your pins.
55
- - `verifyCredential` options `expectedNonce` and `trustedIssuerCertificates` (SD-JWT VC key binding and caller-chosen X.509 anchors), with the `CredentialVerifyOptions` type.
56
- - Response types `IssuerTrust` and `SdJwtEvidence`, and the optional `issuerTrust` and `sdJwt` fields on `CredentialVerification`.
57
-
58
- ### Fixed
59
-
60
- - Node 18: WebCrypto is loaded from `node:crypto` when there is no global `crypto`. Before this, `verifyWebhookSignature` and `verifyCustomerMonitorSignature` failed on Node 18 with `crypto is not defined`.
61
-
62
- ## [0.1.0] - 2026-10-05
63
-
64
- ### Added
65
-
66
- - First public release as `@didis/client`. Zero dependencies, ESM, Node 18+, Deno, Bun, browsers and edge runtimes.
67
- - `DidisClient` for the DID.is public API (default base URL `https://did.is/api`): `health`, `resolve`, `resolveW3c`, `dereference`, `stream` (server-sent events), `history`, `diff`, `graph`, `verifyCredential`, `evaluatePolicy`, `inspectMcp`, `inspectA2a`, `verifyDelegation`, `authorize`, `verifyTool`, `fastVerifyDid` and `fastVerifyJws`.
68
- - Self-hosted monitoring methods that need an admin token: `watch`, `watches`, `unwatch` and `events`.
69
- - `DidisError` carrying the RFC 9457 problem body and `Retry-After`.
70
- - Webhook helpers `verifyWebhookSignature` and `verifyCustomerMonitorSignature`.
71
-
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
75
- [0.2.3]: https://github.com/did-is/didis-js/compare/v0.2.2...v0.2.3
76
- [0.2.2]: https://github.com/did-is/didis-js/compare/v0.2.1...v0.2.2
77
- [0.2.1]: https://github.com/did-is/didis-js/compare/v0.2.0...v0.2.1
78
- [0.2.0]: https://github.com/did-is/didis-js/compare/v0.1.0...v0.2.0
79
- [0.1.0]: https://github.com/did-is/didis-js/releases/tag/v0.1.0
1
+ # Changelog
2
+
3
+ All notable changes to `@didis/client` are listed here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and release numbering follows the project’s pre-1.0 policy. A third-component increment can contain breaking changes; read **Changed** and migration notes before upgrading. 0.2.6 is not a backward-compatible patch.
4
+
5
+ ## [Unreleased]
6
+
7
+ ## [0.2.7] - 2026-11-03
8
+
9
+ ### Fixed
10
+
11
+ - Correct first-party license attribution to DID.is; MIT permission and disclaimer terms are unchanged.
12
+
13
+ ### Migration
14
+
15
+ - This release includes the execution-guard hardening and response types described under 0.2.6 below. **Upgrading from 0.2.5 is not a backward-compatible patch:** configure an application-owned trusted root for every guard; TypeScript requires `root` statically and Python enforces it at runtime with `GUARD_MISCONFIGURED`. Never derive the trusted root from submitted chains or model output. Expired approval, awaited-definition and immutable-deadline checks remain enforced; definition pins do not authorize callers or guarantee tool behavior.
16
+ - npm 0.2.6 was not published. 0.2.7 is the first npm release after 0.2.5 to deliver these changes; the 0.2.6 entry documents the earlier source release.
17
+
18
+ ## [0.2.6] - 2026-11-03
19
+
20
+ ### Fixed
21
+
22
+ - `createDidisGuard` / `withDidisGuard`: reject an ALLOW whose parseable `expires` is at or before execution time with the existing `TOOL_NOT_AUTHORIZED` code, including first use and disabled caching. Re-read pinned definitions after authorization and recheck each call's immutable deadline after async work and immediately before handler invocation; concurrent refreshes cannot extend another call's approval. Cached ALLOW is still bounded by both configured TTL and delegation expiry; failed/DENY/expired refreshes never fall back to stale approval (`GUARD_UNAVAILABLE` for transport/unexpected-answer errors). Missing/unparseable expiry remains optional and keeps the existing TTL-only behavior. Pin providers may now run twice per check. Each call retains the checked approval pin and synchronously compares it with the current pin immediately before execution; changes during asynchronous definition reads fail with `TOOL_DRIFT`. This is a pre-execution boundary, not per-stream reauthorization, resource binding, replay prevention, or an atomic remote side-effect guarantee.
23
+
24
+ ### Changed
25
+
26
+ - **Breaking in 0.2.6:** execution guards require a nonempty, trimmed application-owned trusted root DID. Missing/null/empty/whitespace roots raise `GUARD_MISCONFIGURED` at construction, before network or handler work. Migrate every guard and one-line wrapper by passing the root from trusted application configuration, not from a submitted chain, authorization response, or model output. Rootless advisory verification APIs remain available; signature-valid does not mean application-authorized. 0.2.5 accepts rootless guards; 0.2.6 rejects them. TypeScript requires `root` statically; Python retains its Optional annotation but enforces it at runtime.
27
+
28
+ - Correct delegation response fields to required nullable wire values; correct fast-verification explicit null fields. Wire-type corrections only; these corrections do not alter HTTP routing.
29
+
30
+ ### Added
31
+
32
+ - Add optional tools-only inventory completeness/evidence (missing means unknown); scoped requested-pin attestation preserves partial-result pin checks and rejects duplicate ambiguity. Missing completeness remains unknown, not complete.
33
+
34
+ - `diffObservations()` selects exact public history observations, including domain-only changes with identical document hashes. Semantic diff adds optional `fromObservation`/`toObservation` references. Legacy hash selectors remain available but ambiguous hashes return `INVALID_REQUEST` instead of silently selecting a different state. No change to private usage/billing routes.
35
+
36
+ ## [0.2.5] - 2026-10-19
37
+
38
+ ### Added
39
+
40
+ - 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.
41
+ - `usage()` returns this month's team allowance (`CustomerUsage`: used, reserved, limit and remaining per pool). Requires the API key.
42
+
43
+ ## [0.2.4] - 2026-10-19
44
+
45
+ ### Added
46
+
47
+ - 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`.
48
+
49
+ ## [0.2.3] - 2026-10-18
50
+
51
+ ### Added
52
+
53
+ - Requests now carry `user-agent: didis-client/<version>` (Node, Deno, Bun and other non-browser runtimes; browsers keep their own user agent). Only the package name and version are sent, so DID.is can count SDK use separately from other HTTP clients. New export `VERSION`.
54
+
55
+ - Type `LinkedAccountEvidence` and `Evidence.linkedAccounts`: each `alsoKnownAs` value with whether that website links back to the DID (`CONFIRMED`, `NOT_CONFIRMED`, `FAILED`, `UNSUPPORTED`, `UNREACHABLE`, `NOT_CHECKABLE`). A confirmation proves the same party controls the website origin and the DID; it does not prove who that party is.
56
+
57
+ - Policy rule `linkedAccounts` in `PolicyRules`: 1–4 https origins (for example `["https://shop.example"]`) that must be listed in the DID's `alsoKnownAs` and confirm the DID back. Use it to require "this DID belongs to our official website" in one policy call.
58
+
59
+ ## [0.2.2] - 2026-10-18
60
+
61
+ ### Added
62
+
63
+ - `verifyCredential` option `requiredClaims` (`ClaimRequirement[]`): check conditions such as `{ path: "/age_over_18", equals: true }` without receiving claim values. The response field `requirements` (`RequirementsOutcome`) reports `met` and a per-path result; conditions are evaluated only for a `VALID` credential.
64
+
65
+ ## [0.2.1] - 2026-10-14
66
+
67
+ ### Added
68
+
69
+ - Guard option `capability` (on `check`, `wrap` and `withDidisGuard`): the DID.is capability a tool needs when it differs from the tool name. MCP and OpenAI tool names cannot contain `:`, so use `{ tool: "weather_forecast", capability: "weather:forecast" }`. Pins stay keyed by the tool name; DID.is is asked about the capability; the ALLOW cache is keyed by capability. Without `capability`, behaviour is unchanged (the tool name is the capability).
70
+ - `onDecision` events and `DidisGuardError.detail` now include `capability`.
71
+ - Examples for the MCP TypeScript SDK (`mcp-client.mjs`), LangChain (`langchain-tool.mjs`) and the OpenAI Agents SDK (`openai-agents-tool.mjs`).
72
+
73
+ ### Changed
74
+
75
+ - A capability containing `*`, empty segments or whitespace is refused with `GUARD_MISCONFIGURED` before any network call. Wildcards belong in delegations, not in tool calls.
76
+
77
+ ## [0.2.0] - 2026-10-13
78
+
79
+ ### Added
80
+
81
+ - Fail-closed tool guard: `createDidisGuard`, `withDidisGuard` and `DidisGuardError`. A guarded handler runs only when the tool definition still matches its pin and DID.is `verifyTool` answers `ALLOW` for the configured agent (and root, if set). Drift, `DENY`, network errors, `429` and unexpected answers all throw, and the handler does not run. See [Guard agent tools](https://docs.did.is/build/guard-tools/).
82
+ - Tool definition pinning: `canonicalJson` (RFC 8785 JCS), `toolDefinitionSha256`, `mcpInventoryHash` and `pinToolDefinitions`. Hashes are identical to the `definitionSha256` and `inventoryHash` values DID.is reports.
83
+ - `attestMcpPins(client, endpoint, pins)` compares a public MCP server, as observed by DID.is, with your pins.
84
+ - `verifyCredential` options `expectedNonce` and `trustedIssuerCertificates` (SD-JWT VC key binding and caller-chosen X.509 anchors), with the `CredentialVerifyOptions` type.
85
+ - Response types `IssuerTrust` and `SdJwtEvidence`, and the optional `issuerTrust` and `sdJwt` fields on `CredentialVerification`.
86
+
87
+ ### Fixed
88
+
89
+ - Node 18: WebCrypto is loaded from `node:crypto` when there is no global `crypto`. Before this, `verifyWebhookSignature` and `verifyCustomerMonitorSignature` failed on Node 18 with `crypto is not defined`.
90
+
91
+ ## [0.1.0] - 2026-10-05
92
+
93
+ ### Added
94
+
95
+ - First public release as `@didis/client`. Zero dependencies, ESM, Node 18+, Deno, Bun, browsers and edge runtimes.
96
+ - `DidisClient` for the DID.is public API (default base URL `https://did.is/api`): `health`, `resolve`, `resolveW3c`, `dereference`, `stream` (server-sent events), `history`, `diff`, `graph`, `verifyCredential`, `evaluatePolicy`, `inspectMcp`, `inspectA2a`, `verifyDelegation`, `authorize`, `verifyTool`, `fastVerifyDid` and `fastVerifyJws`.
97
+ - Self-hosted monitoring methods that need an admin token: `watch`, `watches`, `unwatch` and `events`.
98
+ - `DidisError` carrying the RFC 9457 problem body and `Retry-After`.
99
+ - Webhook helpers `verifyWebhookSignature` and `verifyCustomerMonitorSignature`.
100
+
101
+ [Unreleased]: https://github.com/did-is/didis-js/compare/v0.2.7...HEAD
102
+ [0.2.6]: https://github.com/did-is/didis-js/compare/v0.2.5...v0.2.6
103
+ [0.2.5]: https://github.com/did-is/didis-js/compare/v0.2.4...v0.2.5
104
+ [0.2.4]: https://github.com/did-is/didis-js/compare/v0.2.3...v0.2.4
105
+ [0.2.3]: https://github.com/did-is/didis-js/compare/v0.2.2...v0.2.3
106
+ [0.2.2]: https://github.com/did-is/didis-js/compare/v0.2.1...v0.2.2
107
+ [0.2.1]: https://github.com/did-is/didis-js/compare/v0.2.0...v0.2.1
108
+ [0.2.0]: https://github.com/did-is/didis-js/compare/v0.1.0...v0.2.0
109
+ [0.1.0]: https://github.com/did-is/didis-js/releases/tag/v0.1.0
110
+
111
+ [0.2.7]: https://github.com/did-is/didis-js/compare/v0.2.6...v0.2.7
package/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 ledgerbingo
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 DID.is
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,160 +1,180 @@
1
- # @didis/client
2
-
3
- Official TypeScript/JavaScript SDK for the [DID.is](https://did.is) public API: DID resolution with evidence, W3C DID Resolution, verifiable credentials, explicit policies, and MCP/A2A agent trust. Works anywhere `fetch` exists: Node 18+, Deno, Bun, browsers and edge runtimes. Zero dependencies, fully typed, ESM.
4
-
5
- ## Install
6
-
7
- ```bash
8
- npm install @didis/client
9
- ```
10
-
11
- ## Usage
12
-
13
- ```ts
14
- import { DidisClient, DidisError } from "@didis/client";
15
-
16
- // Defaults to the public API at https://did.is/api.
17
- // Self-hosting? new DidisClient({ baseUrl: "https://your-host/api" })
18
- const didis = new DidisClient();
19
-
20
- // Enriched resolution: verdict, evidence dimensions, graph, telemetry.
21
- const r = await didis.resolve("did:web:identity.foundation");
22
- console.log(r.verdict.headline);
23
- for (const d of r.dimensions) console.log(d.label, d.state);
24
-
25
- // DID Resolution v1 (W3C CR Draft HTTPS binding, unmodified).
26
- const w3c = await didis.resolveW3c("did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK");
27
-
28
- // Live resolution stream (server-sent events).
29
- for await (const ev of didis.stream("did:web:identity.foundation")) {
30
- if (ev.event === "stage") console.log(ev.data.label, ev.data.status);
31
- if (ev.event === "result") console.log(ev.data.verdict.outcome);
32
- }
33
-
34
- // Credentials: VC DM 2.0 / 1.1 with Data Integrity (eddsa-jcs-2022, ecdsa-jcs-2019) or VC-JOSE/VC-JWT.
35
- // RDFC suites, ecdsa-sd-2023 and bbs-2023 are reported as UNSUPPORTED, never as valid.
36
- const credential = {
37
- "@context": ["https://www.w3.org/ns/credentials/v2"],
38
- type: ["VerifiableCredential"],
39
- issuer: "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
40
- validFrom: "2026-01-01T00:00:00Z",
41
- credentialSubject: { id: "did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLdmQR63zLFKfvUwi3" },
42
- };
43
- const vc = await didis.verifyCredential(credential);
44
- console.log(vc.status, vc.headline);
45
-
46
- // Policies are explicit rules, never scores.
47
- const { evaluation } = await didis.evaluatePolicy("did:web:example.com", {
48
- rules: { allowedMethods: ["web", "webvh"], domainBinding: "required", tlsMinDaysRemaining: 14 },
49
- });
50
-
51
- // Agents.
52
- const mcp = await didis.inspectMcp("https://mcp.example.com/mcp");
53
- const card = await didis.inspectA2a("https://agent.example.com");
54
- const chain: string[] = []; // delegation receipt JWS strings, root first
55
- const { authorization } = await didis.authorize(chain, "payments.refund", { trustedRoots: ["did:web:example.com"] });
56
-
57
- try {
58
- await didis.resolve("did:web:does-not-exist.invalid");
59
- } catch (e) {
60
- if (e instanceof DidisError) console.log(e.status, e.problem.code, e.problem.detail);
61
- }
62
- ```
63
-
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
-
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
-
70
- ## Guard agent tools
71
-
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).
73
-
74
- ```ts
75
- import { DidisClient, createDidisGuard, pinToolDefinitions } from "@didis/client";
76
-
77
- const didis = new DidisClient();
78
- // Once, after reviewing the server's tools/list: store these pins in your config.
79
- const pins = await pinToolDefinitions(reviewedTools);
80
-
81
- const guard = createDidisGuard({
82
- client: didis,
83
- agent: process.env.AGENT_DID!, // from configuration, never from model output
84
- root: process.env.ROOT_DID, // require this delegation root
85
- pins,
86
- requirePins: true,
87
- });
88
-
89
- const createIssue = guard.wrap(
90
- {
91
- tool: "github_create_issue", // the name in tools/list (pin key)
92
- capability: "github:create_issue", // what the delegation must grant
93
- definition: () => currentTools.get("github_create_issue"),
94
- },
95
- async (args) => github.createIssue(args),
96
- );
97
- ```
98
-
99
- **Tool name vs capability.** MCP and OpenAI tool names may only contain letters, digits, `_`, `-` and `.`, so a tool cannot be called `github:create_issue`. DID.is capabilities use `:` namespaces so that `github:*` can grant a whole group. Pass both: `tool` is used for the pin and error messages, `capability` is what DID.is is asked about. Without `capability`, the tool name is used. A capability must be exact (no `*`, no empty segments, no spaces); an invalid one throws `GUARD_MISCONFIGURED` before any network call. Since 0.2.1.
100
-
101
- Guarding MCP clients, LangChain and the OpenAI Agents SDK: see [`examples/`](examples/) and [Guard agent tools](https://docs.did.is/build/guard-tools/#use-with-mcp-and-agent-frameworks).
102
-
103
- - The definition check is local (RFC 8785 JCS + SHA-256, the same `definitionSha256` DID.is reports) and makes no network call.
104
- - The delegation check calls `verifyTool`. An ALLOW is reused for 30 s by default (`allowCacheMs`), never past the delegation expiry; DENY and errors are never cached.
105
- - Error codes: `TOOL_DRIFT`, `TOOL_DEFINITION_MISSING`, `TOOL_NOT_AUTHORIZED`, `GUARD_UNAVAILABLE`, `GUARD_MISCONFIGURED`.
106
- - One tool only: `withDidisGuard(handler, { client, agent, root, tool, capability, pin, definition })`.
107
- - `onDecision` and `DidisGuardError.detail` include both `tool` and `capability`.
108
- - `attestMcpPins(client, endpoint, pins)` asks DID.is to inspect a public MCP server and compares its hashes with your pins. Run it at startup or deploy time, not per call: it is an outbound, rate-limited request, and servers that need login or run on a private network cannot be inspected.
109
-
110
- ## Webhooks
111
-
112
- Console project monitor events use `verifyCustomerMonitorSignature`:
113
-
114
- ```ts
115
- import { verifyCustomerMonitorSignature } from "@didis/client";
116
-
117
- const ok = await verifyCustomerMonitorSignature(
118
- secret,
119
- rawBody,
120
- req.headers["x-didis-signature"],
121
- { tenantId, projectId, eventId: req.headers["x-didis-event-id"] },
122
- );
123
- ```
124
-
125
- For self-hosted admin daemon monitoring deliveries, use `verifyWebhookSignature`:
126
-
127
- ```ts
128
- import { verifyWebhookSignature } from "@didis/client";
129
-
130
- const ok = await verifyWebhookSignature(
131
- secret,
132
- req.headers["x-didis-timestamp"],
133
- rawBody,
134
- req.headers["x-didis-signature-256"],
135
- );
136
- ```
137
-
138
- ## Surface
139
-
140
- | Method | Route |
141
- | --- | --- |
142
- | `health()` | `GET /health` |
143
- | `resolve(did, { noCache })` | `GET /v1/resolve/{did}` |
144
- | `resolveW3c(did)` | `GET /1.0/identifiers/{did}` |
145
- | `dereference(didUrl)` | `GET /v1/dereference/{didUrl}` |
146
- | `stream(did)` | `GET /v1/stream/{did}` |
147
- | `history(did)` / `diff(did, from?, to?)` / `graph(did)` | `GET /v1/history`, `/v1/diff`, `/v1/graph` |
148
- | `verifyCredential(vc)` | `POST /v1/credentials/verify` |
149
- | `evaluatePolicy(did, policy, vc?)` | `POST /v1/policies/evaluate` |
150
- | `inspectMcp(endpoint)` / `inspectA2a(url)` | `GET /v1/mcp/inspect`, `/v1/a2a/inspect` |
151
- | `verifyDelegation(chain, opts)` / `authorize(chain, tool, opts)` / `verifyTool(agent, tool, root?)` | `/v1/agents/*` |
152
- | `fastVerifyDid(did)` / `fastVerifyJws(jws, relationship?)` | `/v1/fast-verify/*` |
153
- | `usage()` (with `apiKey`) | `GET /v1/customer/usage` |
154
- | `watch` / `watches` / `unwatch` / `events` | `/v1/monitoring/*` (admin) |
155
-
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.
157
-
158
- ## License
159
-
160
- MIT
1
+ # @didis/client
2
+
3
+ Official TypeScript/JavaScript SDK for the [DID.is](https://did.is) public API: DID resolution with evidence, W3C DID Resolution, verifiable credentials, explicit policies, and MCP/A2A agent trust. Works anywhere `fetch` exists: Node 18+, Deno, Bun, browsers and edge runtimes. Zero dependencies, fully typed, ESM.
4
+
5
+
6
+ Current release: **0.2.7**. See [CHANGELOG.md](CHANGELOG.md) for root-required guard migration and corrected first-party license attribution.
7
+
8
+ ## Install
9
+
10
+ ```bash
11
+ npm install @didis/client
12
+ ```
13
+
14
+ ## Runnable MCP examples
15
+
16
+ Start with [DID.is MCP examples](https://github.com/did-is/mcp-examples) for three separate jobs:
17
+
18
+ - [Connect your MCP client to DID.is](https://github.com/did-is/mcp-examples/tree/main/examples/00-connect-didis) — an opt-in round trip to the existing read-only server; connecting it does not protect your other tools.
19
+ - [Inspect a public MCP server](https://github.com/did-is/mcp-examples/tree/main/examples/01-inspect-public-server) — offline evidence first, then an explicit live inspection of a server you own or have permission to inspect.
20
+ - [Block calls after approved definitions change](https://github.com/did-is/mcp-examples/tree/main/examples/02-block-definition-drift) — an offline TypeScript execution boundary using the public SDK fingerprint helpers, not delegation authorization or the full SDK guard.
21
+
22
+ Default demos need no DID.is account, API key, or LLM key. The inspection example also has a [Python mirror](https://github.com/did-is/mcp-examples/tree/main/python/01-inspect-public-server). Host subscriptions and optional live calls are separate. [See the actual recorded run and the code boundary](https://github.com/did-is/mcp-examples/blob/main/docs/demo.md).
23
+
24
+ ## Usage
25
+
26
+ ```ts
27
+ import { DidisClient, DidisError } from "@didis/client";
28
+
29
+ // Defaults to the public API at https://did.is/api.
30
+ // Self-hosting? new DidisClient({ baseUrl: "https://your-host/api" })
31
+ const didis = new DidisClient();
32
+
33
+ // Enriched resolution: verdict, evidence dimensions, graph, telemetry.
34
+ const r = await didis.resolve("did:web:identity.foundation");
35
+ console.log(r.verdict.headline);
36
+ for (const d of r.dimensions) console.log(d.label, d.state);
37
+
38
+ // DID Resolution v1 (W3C CR Draft HTTPS binding, unmodified).
39
+ const w3c = await didis.resolveW3c("did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK");
40
+
41
+ // Live resolution stream (server-sent events).
42
+ for await (const ev of didis.stream("did:web:identity.foundation")) {
43
+ if (ev.event === "stage") console.log(ev.data.label, ev.data.status);
44
+ if (ev.event === "result") console.log(ev.data.verdict.outcome);
45
+ }
46
+
47
+ // Credentials: VC DM 2.0 / 1.1 with Data Integrity (eddsa-jcs-2022, ecdsa-jcs-2019) or VC-JOSE/VC-JWT.
48
+ // RDFC suites, ecdsa-sd-2023 and bbs-2023 are reported as UNSUPPORTED, never as valid.
49
+ const credential = {
50
+ "@context": ["https://www.w3.org/ns/credentials/v2"],
51
+ type: ["VerifiableCredential"],
52
+ issuer: "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK",
53
+ validFrom: "2026-01-01T00:00:00Z",
54
+ credentialSubject: { id: "did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLdmQR63zLFKfvUwi3" },
55
+ };
56
+ const vc = await didis.verifyCredential(credential);
57
+ console.log(vc.status, vc.headline);
58
+
59
+ // Policies are explicit rules, never scores.
60
+ const { evaluation } = await didis.evaluatePolicy("did:web:example.com", {
61
+ rules: { allowedMethods: ["web", "webvh"], domainBinding: "required", tlsMinDaysRemaining: 14 },
62
+ });
63
+
64
+ // Agents.
65
+ const mcp = await didis.inspectMcp("https://mcp.example.com/mcp");
66
+ const card = await didis.inspectA2a("https://agent.example.com");
67
+ const chain: string[] = []; // delegation receipt JWS strings, root first
68
+ const { authorization } = await didis.authorize(chain, "payments.refund", { trustedRoots: ["did:web:example.com"] });
69
+
70
+ try {
71
+ await didis.resolve("did:web:does-not-exist.invalid");
72
+ } catch (e) {
73
+ if (e instanceof DidisError) console.log(e.status, e.problem.code, e.problem.detail);
74
+ }
75
+ ```
76
+
77
+ 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).
78
+
79
+ **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`.
80
+
81
+ **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.
82
+
83
+ ## Guard agent tools
84
+
85
+ **0.2.7 migration from 0.2.5 (breaking; introduced in the 0.2.6 source release):** every execution guard, including standalone `check` calls and one-line wrappers, requires a nonempty, trimmed trusted root DID from application-owned configuration. Missing/null/empty/whitespace roots fail with `GUARD_MISCONFIGURED` at construction, before any network call or handler. TypeScript requires `root` statically; Python retains its Optional annotation but validates it at runtime. Do not obtain the trusted root from model output, a submitted delegation, or an ALLOW response. The configured root is sent to verification and compared with the returned root. Definition pins do not establish issuer authority. Rootless chain/tool verification remains advisory: signature-valid alone is not application authorization. 0.2.5 accepts rootless guards; 0.2.7 rejects them, so this is not a backward-compatible patch.
86
+
87
+ 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).
88
+
89
+ ```ts
90
+ import { DidisClient, createDidisGuard, pinToolDefinitions } from "@didis/client";
91
+
92
+ const didis = new DidisClient();
93
+ // Once, after reviewing the server's tools/list: store these pins in your config.
94
+ const pins = await pinToolDefinitions(reviewedTools);
95
+
96
+ const guard = createDidisGuard({
97
+ client: didis,
98
+ agent: process.env.AGENT_DID!, // from configuration, never from model output
99
+ root: process.env.ROOT_DID!, // trusted application configuration; required in 0.2.7
100
+ pins,
101
+ requirePins: true,
102
+ });
103
+
104
+ const createIssue = guard.wrap(
105
+ {
106
+ tool: "github_create_issue", // the name in tools/list (pin key)
107
+ capability: "github:create_issue", // what the delegation must grant
108
+ definition: () => currentTools.get("github_create_issue"),
109
+ },
110
+ async (args) => github.createIssue(args),
111
+ );
112
+ ```
113
+
114
+ **Tool name vs capability.** MCP and OpenAI tool names may only contain letters, digits, `_`, `-` and `.`, so a tool cannot be called `github:create_issue`. DID.is capabilities use `:` namespaces so that `github:*` can grant a whole group. Pass both: `tool` is used for the pin and error messages, `capability` is what DID.is is asked about. Without `capability`, the tool name is used. A capability must be exact (no `*`, no empty segments, no spaces); an invalid one throws `GUARD_MISCONFIGURED` before any network call. Since 0.2.1.
115
+
116
+ Guarding MCP clients, LangChain and the OpenAI Agents SDK: see [`examples/`](examples/) and [Guard agent tools](https://docs.did.is/build/guard-tools/#use-with-mcp-and-agent-frameworks).
117
+
118
+ - The definition check is local (RFC 8785 JCS + SHA-256, the same `definitionSha256` DID.is reports) and makes no network call.
119
+ - The delegation check calls `verifyTool`. An ALLOW is reused for 30 s by default (`allowCacheMs`), never past a parseable delegation expiry. Expired approvals are refused even on first use or with caching disabled. Approval deadlines and pins are rechecked after awaited work and immediately before execution; DENY and errors never fall back to stale approvals. Missing/unparseable expiry retains TTL-only behavior. This is a pre-execution boundary, not caller authentication, replay prevention, resource binding or per-stream reauthorization.
120
+ - Error codes: `TOOL_DRIFT`, `TOOL_DEFINITION_MISSING`, `TOOL_NOT_AUTHORIZED`, `GUARD_UNAVAILABLE`, `GUARD_MISCONFIGURED`.
121
+ - One tool only: `withDidisGuard(handler, { client, agent, root, tool, capability, pin, definition })`.
122
+ - `onDecision` and `DidisGuardError.detail` include both `tool` and `capability`.
123
+ - `attestMcpPins(client, endpoint, pins)` asks DID.is to inspect a public MCP server and compares its hashes with your pins. Run it at startup or deploy time, not per call: it is an outbound, rate-limited request, and servers that need login or run on a private network cannot be inspected.
124
+
125
+ ## Webhooks
126
+
127
+ Console project monitor events use `verifyCustomerMonitorSignature`:
128
+
129
+ ```ts
130
+ import { verifyCustomerMonitorSignature } from "@didis/client";
131
+
132
+ const ok = await verifyCustomerMonitorSignature(
133
+ secret,
134
+ rawBody,
135
+ req.headers["x-didis-signature"],
136
+ { tenantId, projectId, eventId: req.headers["x-didis-event-id"] },
137
+ );
138
+ ```
139
+
140
+ For self-hosted admin daemon monitoring deliveries, use `verifyWebhookSignature`:
141
+
142
+ ```ts
143
+ import { verifyWebhookSignature } from "@didis/client";
144
+
145
+ const ok = await verifyWebhookSignature(
146
+ secret,
147
+ req.headers["x-didis-timestamp"],
148
+ rawBody,
149
+ req.headers["x-didis-signature-256"],
150
+ );
151
+ ```
152
+
153
+ ## Surface
154
+
155
+ | Method | Route |
156
+ | --- | --- |
157
+ | `health()` | `GET /health` |
158
+ | `resolve(did, { noCache })` | `GET /v1/resolve/{did}` |
159
+ | `resolveW3c(did)` | `GET /1.0/identifiers/{did}` |
160
+ | `dereference(didUrl)` | `GET /v1/dereference/{didUrl}` |
161
+ | `stream(did)` | `GET /v1/stream/{did}` |
162
+ | `history(did)` / `diff(did, from?, to?)` / `graph(did)` | `GET /v1/history`, `/v1/diff`, `/v1/graph` |
163
+ | `verifyCredential(vc)` | `POST /v1/credentials/verify` |
164
+ | `evaluatePolicy(did, policy, vc?)` | `POST /v1/policies/evaluate` |
165
+ | `inspectMcp(endpoint)` / `inspectA2a(url)` | `GET /v1/mcp/inspect`, `/v1/a2a/inspect` |
166
+ | `verifyDelegation(chain, opts)` / `authorize(chain, tool, opts)` / `verifyTool(agent, tool, root?)` | `/v1/agents/*` |
167
+ | `fastVerifyDid(did)` / `fastVerifyJws(jws, relationship?)` | `/v1/fast-verify/*` |
168
+ | `usage()` (with `apiKey`) | `GET /v1/customer/usage` |
169
+ | `watch` / `watches` / `unwatch` / `events` | `/v1/monitoring/*` (admin) |
170
+
171
+ 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.
172
+
173
+ ### Tools-only inventory completeness
174
+
175
+ `inventoryHash` fingerprints the observed tools, not an assumed entire catalog. Require `inventoryComplete === true` for whole-inventory acceptance. Optional missing completeness is unknown, not true. `inventoryEvidence` reports tools-only page/entry/drop counts, remaining pages and reasons; prompts/resources are not inspected. Pin attestation `ok` checks requested reviewed pins only and can succeed on partial inventory; it does not attest unpinned tools or establish safety. Duplicate ambiguous names cannot satisfy pins.
176
+
177
+
178
+ ## License
179
+
180
+ MIT
package/dist/guard.d.ts CHANGED
@@ -7,7 +7,14 @@
7
7
  * `definitionSha256`). Catches MCP "rug-pull" drift, including servers DID.is cannot reach.
8
8
  * 2. Delegation (DID.is `verify-tool`): the configured agent DID holds a registered, VALID
9
9
  * delegation whose leaf capabilities grant the tool's capability. ALLOW is cached briefly;
10
- * DENY and errors are never cached.
10
+ * DENY and errors are never cached. A parseable `expires` is an exclusive deadline:
11
+ * stale ALLOW is refused with TOOL_NOT_AUTHORIZED, even with caching disabled.
12
+ * Absent/unparseable expiry keeps the legacy optional-field behavior (TTL-only cache).
13
+ *
14
+ * Pins are re-read after authorization awaits, and the per-call deadline is checked again
15
+ * immediately before invoking a wrapped handler. Pinned providers may run twice per check.
16
+ * This is a pre-execution check, not ongoing
17
+ * stream authorization or an atomic transaction with a remote tool's side effects.
11
18
  *
12
19
  * Tool name vs capability: MCP tool names may only use `A-Z a-z 0-9 _ - .` (SEP-986), while
13
20
  * DID.is capabilities are namespaced with `:` (e.g. `weather:forecast`). Pass both when they
@@ -45,8 +52,8 @@ export interface DidisGuardOptions {
45
52
  client: DidisClient;
46
53
  /** The agent DID from your runtime configuration. Never take it from model output. */
47
54
  agent: string;
48
- /** Require the delegation chain to start at this root DID. Strongly recommended. */
49
- root?: string;
55
+ /** Application-owned trusted root DID. Required; never derive it from a chain or model output. */
56
+ root: string;
50
57
  /** Approved `{ toolName: definitionSha256 }`. A pinned tool must be given its definition. */
51
58
  pins?: Record<string, string>;
52
59
  /** Refuse tools that have no pin. Default `false`. */
@@ -71,7 +78,7 @@ export interface GuardCallOptions {
71
78
  * Must be an exact capability (no `*`): wildcards belong in delegations, not in tool calls.
72
79
  */
73
80
  capability?: string;
74
- /** The definition the agent received, or a function returning the current one. */
81
+ /** The definition the agent received, or a function returning the current one (may be read twice). */
75
82
  definition?: Definition;
76
83
  }
77
84
  export interface DidisGuard {
@@ -91,6 +98,10 @@ export declare function withDidisGuard<A extends unknown[], R>(handler: (...args
91
98
  pin?: string;
92
99
  }): (...args: A) => Promise<Awaited<R>>;
93
100
  export interface McpAttestation {
101
+ /** Requested pins only, never an all-tools or safety assertion. */
102
+ inventoryComplete?: boolean;
103
+ inventoryEvidence?: McpInspection["inventoryEvidence"];
104
+ scope: "requested-pins";
94
105
  ok: boolean;
95
106
  inventoryHash: string;
96
107
  /** Pinned tools whose DID.is-observed hash differs, or that DID.is did not see. */
@@ -106,6 +117,7 @@ export interface McpAttestation {
106
117
  /**
107
118
  * Optional startup check: asks DID.is to inspect a public MCP endpoint and compares the observed
108
119
  * `definitionSha256` values with your pins. Use at deploy/startup, never per tool call.
120
+ * Matching requested pins can succeed on partial inventory; extras and tool safety are not accepted.
109
121
  * `AUTH_REQUIRED` or any non-INSPECTED status returns `ok: false`.
110
122
  */
111
123
  export declare function attestMcpPins(client: DidisClient, endpoint: string, pins: Record<string, string>): Promise<McpAttestation>;
package/dist/guard.js CHANGED
@@ -79,8 +79,8 @@ export function createDidisGuard(options) {
79
79
  throw new DidisGuardError("GUARD_MISCONFIGURED", "", "a DidisClient is required");
80
80
  if (typeof agent !== "string" || !DID.test(agent))
81
81
  throw new DidisGuardError("GUARD_MISCONFIGURED", "", "agent must be a DID");
82
- if (root !== undefined && (typeof root !== "string" || !DID.test(root)))
83
- throw new DidisGuardError("GUARD_MISCONFIGURED", "", "root must be a DID");
82
+ if (typeof root !== "string" || root !== root.trim() || !DID.test(root))
83
+ throw new DidisGuardError("GUARD_MISCONFIGURED", "", "an application-owned trusted root DID is required");
84
84
  if (!Number.isFinite(allowCacheMs) || allowCacheMs < 0)
85
85
  throw new DidisGuardError("GUARD_MISCONFIGURED", "", "allowCacheMs must be >= 0");
86
86
  for (const [name, pin] of Object.entries(pins)) {
@@ -128,13 +128,20 @@ export function createDidisGuard(options) {
128
128
  fail("TOOL_DRIFT", tool, capability, `definition name does not match '${tool}'`, { expected: pin, actual });
129
129
  if (actual !== pin)
130
130
  fail("TOOL_DRIFT", tool, capability, `definition of '${tool}' changed since it was approved`, { expected: pin, actual });
131
+ assertPinCurrent(tool, capability, pin);
132
+ return pin;
133
+ }
134
+ function assertPinCurrent(tool, capability, checkedPin) {
135
+ if (pinFor(tool) !== checkedPin) {
136
+ fail("TOOL_DRIFT", tool, capability, `approved pin for '${tool}' changed before execution`);
137
+ }
131
138
  }
132
139
  async function checkDelegation(tool, capability) {
133
140
  const now = Date.now();
134
141
  const until = allowed.get(capability);
135
142
  if (until !== undefined && until > now) {
136
143
  report(tool, capability, true, "cached ALLOW", true);
137
- return;
144
+ return until;
138
145
  }
139
146
  allowed.delete(capability);
140
147
  let auth;
@@ -152,17 +159,27 @@ export function createDidisGuard(options) {
152
159
  }
153
160
  fail("GUARD_UNAVAILABLE", tool, capability, "DID.is returned an unexpected authorization answer");
154
161
  }
155
- if (root !== undefined && auth.root !== root)
162
+ if (auth.root !== root)
156
163
  fail("TOOL_NOT_AUTHORIZED", tool, capability, "delegation root does not match the configured root");
164
+ // Expiry is optional in the existing wire contract. Never turn malformed/missing expiry
165
+ // into a new mandatory field; when parseable, enforce it on first use as well as reuse.
166
+ const parsed = typeof auth.expires === "string" ? Date.parse(auth.expires) : NaN;
167
+ const deadline = Number.isFinite(parsed) ? parsed : Infinity;
168
+ assertFresh(tool, capability, deadline);
157
169
  if (allowCacheMs > 0) {
158
- const expires = auth.expires ? Date.parse(auth.expires) : NaN;
159
- const until2 = Math.min(now + allowCacheMs, Number.isFinite(expires) ? expires : now + allowCacheMs);
160
- if (until2 > now)
170
+ const until2 = Math.min(now + allowCacheMs, deadline);
171
+ if (until2 > Date.now())
161
172
  allowed.set(capability, until2);
162
173
  }
163
174
  report(tool, capability, true, auth.reason, false);
175
+ return deadline;
176
+ }
177
+ function assertFresh(tool, capability, deadline) {
178
+ if (deadline <= Date.now()) {
179
+ fail("TOOL_NOT_AUTHORIZED", tool, capability, "authorization expired before execution");
180
+ }
164
181
  }
165
- async function check({ tool, capability, definition }) {
182
+ async function prepare({ tool, capability, definition }) {
166
183
  if (typeof tool !== "string" || !tool.trim() || tool !== tool.trim())
167
184
  fail("GUARD_MISCONFIGURED", String(tool), String(capability ?? tool), "tool must be a non-empty, trimmed name");
168
185
  const cap = capability === undefined ? tool : capability;
@@ -170,15 +187,27 @@ export function createDidisGuard(options) {
170
187
  fail("GUARD_MISCONFIGURED", tool, String(cap), "capability must be exact: ':'-separated non-empty segments, no spaces, no '*'");
171
188
  }
172
189
  await checkDefinition(tool, cap, definition);
173
- await checkDelegation(tool, cap);
190
+ // Keep this call's deadline, not a later cache entry written by a concurrent call.
191
+ const deadline = await checkDelegation(tool, cap);
192
+ const pin = await checkDefinition(tool, cap, definition);
193
+ assertFresh(tool, cap, deadline);
194
+ assertPinCurrent(tool, cap, pin);
195
+ return { tool, capability: cap, deadline, pin };
174
196
  }
175
197
  return {
176
- check,
198
+ async check(call) {
199
+ const ticket = await prepare(call);
200
+ assertFresh(ticket.tool, ticket.capability, ticket.deadline);
201
+ assertPinCurrent(ticket.tool, ticket.capability, ticket.pin);
202
+ },
177
203
  wrap(call, handler) {
178
204
  if (typeof handler !== "function")
179
205
  throw new DidisGuardError("GUARD_MISCONFIGURED", call?.tool ?? "", "handler must be a function");
180
206
  return async (...args) => {
181
- await check(call);
207
+ const ticket = await prepare(call);
208
+ // No await between the final deadline/pin checks and calling the handler.
209
+ assertFresh(ticket.tool, ticket.capability, ticket.deadline);
210
+ assertPinCurrent(ticket.tool, ticket.capability, ticket.pin);
182
211
  return await handler(...args);
183
212
  };
184
213
  },
@@ -199,11 +228,21 @@ export function withDidisGuard(handler, options) {
199
228
  /**
200
229
  * Optional startup check: asks DID.is to inspect a public MCP endpoint and compares the observed
201
230
  * `definitionSha256` values with your pins. Use at deploy/startup, never per tool call.
231
+ * Matching requested pins can succeed on partial inventory; extras and tool safety are not accepted.
202
232
  * `AUTH_REQUIRED` or any non-INSPECTED status returns `ok: false`.
203
233
  */
204
234
  export async function attestMcpPins(client, endpoint, pins) {
205
235
  const inspection = await client.inspectMcp(endpoint);
206
- const seen = new Map(inspection.tools.map((t) => [t.name, t.definitionSha256]));
236
+ const seen = new Map();
237
+ const ambiguous = new Set();
238
+ for (const t of inspection.tools) {
239
+ if (seen.has(t.name) || ambiguous.has(t.name)) {
240
+ seen.delete(t.name);
241
+ ambiguous.add(t.name);
242
+ }
243
+ else
244
+ seen.set(t.name, t.definitionSha256);
245
+ }
207
246
  const mismatched = Object.entries(pins)
208
247
  .filter(([tool, expected]) => seen.get(tool) !== expected)
209
248
  .map(([tool, expected]) => ({ tool, expected, observed: seen.get(tool) }));
@@ -211,6 +250,9 @@ export async function attestMcpPins(client, endpoint, pins) {
211
250
  return {
212
251
  ok: inspection.status === "INSPECTED" && mismatched.length === 0,
213
252
  inventoryHash: inspection.inventoryHash,
253
+ inventoryComplete: inspection.inventoryComplete,
254
+ inventoryEvidence: inspection.inventoryEvidence,
255
+ scope: "requested-pins",
214
256
  mismatched,
215
257
  unpinned,
216
258
  status: inspection.status,
package/dist/index.d.ts CHANGED
@@ -372,6 +372,15 @@ export interface CustomerUsage {
372
372
  expensiveLimit: number;
373
373
  expensiveRemaining: number;
374
374
  }
375
+ /** Bounded tools/list evidence only; prompts and resources are not inspected. */
376
+ export interface InventoryEvidence {
377
+ scope: string;
378
+ pagesFetched: number;
379
+ entriesSeen: number;
380
+ entriesDropped: number;
381
+ hasMore: boolean;
382
+ reasons: string[];
383
+ }
375
384
  export interface McpInspection {
376
385
  endpoint: string;
377
386
  status: "INSPECTED" | "AUTH_REQUIRED" | string;
@@ -383,6 +392,9 @@ export interface McpInspection {
383
392
  instructions?: string;
384
393
  tools: ToolEvidence[];
385
394
  inventoryHash: string;
395
+ /** Missing means unknown, not complete. Hash covers observed tools only. */
396
+ inventoryComplete?: boolean;
397
+ inventoryEvidence?: InventoryEvidence;
386
398
  jcsProfile?: string;
387
399
  classCounts: Record<string, number>;
388
400
  drift: {
@@ -463,11 +475,11 @@ export interface DelegationLink {
463
475
  issuer: string;
464
476
  audience: string;
465
477
  capabilities: string[];
466
- notBefore?: string;
467
- expires?: string;
468
- jti?: string;
469
- kid?: string;
470
- alg?: string;
478
+ notBefore: string | null;
479
+ expires: string | null;
480
+ jti: string | null;
481
+ kid: string | null;
482
+ alg: string | null;
471
483
  digest: string;
472
484
  signatureValid: boolean;
473
485
  status: string;
@@ -478,11 +490,11 @@ export interface DelegationChainResult {
478
490
  status: string;
479
491
  headline: string;
480
492
  format: string;
481
- root?: string;
482
- leaf?: string;
493
+ root: string | null;
494
+ leaf: string | null;
483
495
  effectiveCapabilities: string[];
484
- notAfter?: string;
485
- trustedRoot?: boolean;
496
+ notAfter: string | null;
497
+ trustedRoot: boolean | null;
486
498
  links: DelegationLink[];
487
499
  checks: Check[];
488
500
  limitations: string[];
@@ -531,6 +543,8 @@ export interface ObservationRecord {
531
543
  }
532
544
  export interface SemanticDiff {
533
545
  did: string;
546
+ fromObservation?: string;
547
+ toObservation?: string;
534
548
  fromHash: string;
535
549
  toHash: string;
536
550
  fromObservedAt: string;
@@ -629,13 +643,13 @@ export interface FastVerifyDidResult {
629
643
  }[];
630
644
  keys: {
631
645
  id: string;
632
- curve?: string;
646
+ curve: string | null;
633
647
  status: KeyEvidence["status"];
634
648
  relationships: string[];
635
- jwkThumbprint?: string;
649
+ jwkThumbprint: string | null;
636
650
  }[];
637
651
  domainBinding: string | null;
638
- documentSha256?: string;
652
+ documentSha256: string | null;
639
653
  observedAt: string;
640
654
  cached: boolean;
641
655
  }
@@ -643,9 +657,9 @@ export interface FastVerifyJwsResult {
643
657
  status: "VALID" | "INVALID" | "INDETERMINATE" | "UNSUPPORTED" | "MALFORMED";
644
658
  valid: boolean;
645
659
  headline: string;
646
- alg?: string;
647
- kid?: string;
648
- typ?: string;
660
+ alg?: string | null;
661
+ kid?: string | null;
662
+ typ?: string | null;
649
663
  /** Decoded JSON payload, or base64url text when the payload is not JSON. */
650
664
  payload?: unknown;
651
665
  signingInputSha256?: string;
@@ -707,7 +721,7 @@ export declare function retryAfterMs(value: string | null | undefined, now?: num
707
721
  /** Public DID.is API. Point `baseUrl` at your own deployment when self-hosting. */
708
722
  export declare const DEFAULT_BASE_URL = "https://did.is/api";
709
723
  /** Package version, kept equal to package.json (checked by the test suite). */
710
- export declare const VERSION = "0.2.5";
724
+ export declare const VERSION = "0.2.7";
711
725
  export declare class DidisClient {
712
726
  private readonly baseUrl;
713
727
  private readonly adminToken?;
@@ -747,6 +761,8 @@ export declare class DidisClient {
747
761
  history(did: string): Promise<HistoryResponse>;
748
762
  /** Semantic diff; defaults to the latest two distinct observations. */
749
763
  diff(did: string, from?: string, to?: string): Promise<SemanticDiff>;
764
+ /** Exact public history rows, including domain-only changes with identical document hashes. */
765
+ diffObservations(did: string, fromObservation: string, toObservation: string): Promise<SemanticDiff>;
750
766
  graph(did: string): Promise<GraphResponse>;
751
767
  /** Audience is checked only when an expected recipient is supplied. Pass raw JSON as a string to preserve member identity. */
752
768
  verifyCredential(credential: unknown, options?: CredentialVerifyOptions): Promise<CredentialVerification>;
package/dist/index.js CHANGED
@@ -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.5";
29
+ export const VERSION = "0.2.7";
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
@@ -190,6 +190,10 @@ export class DidisClient {
190
190
  const q = from && to ? `?from=${encodeURIComponent(from)}&to=${encodeURIComponent(to)}` : "";
191
191
  return this.request("GET", `/v1/diff/${enc(did)}${q}`);
192
192
  }
193
+ /** Exact public history rows, including domain-only changes with identical document hashes. */
194
+ diffObservations(did, fromObservation, toObservation) {
195
+ return this.request("GET", `/v1/diff/${enc(did)}?fromObservation=${encodeURIComponent(fromObservation)}&toObservation=${encodeURIComponent(toObservation)}`);
196
+ }
193
197
  graph(did) {
194
198
  return this.request("GET", `/v1/graph/${enc(did)}`);
195
199
  }
package/package.json CHANGED
@@ -1,55 +1,55 @@
1
- {
2
- "name": "@didis/client",
3
- "version": "0.2.5",
4
- "description": "Official TypeScript/JavaScript SDK for the DID.is public API: DID resolution with evidence, credentials, policies and agent trust",
5
- "type": "module",
6
- "main": "./dist/index.js",
7
- "types": "./dist/index.d.ts",
8
- "exports": {
9
- ".": {
10
- "types": "./dist/index.d.ts",
11
- "default": "./dist/index.js"
12
- },
13
- "./package.json": "./package.json"
14
- },
15
- "files": [
16
- "dist",
17
- "README.md",
18
- "LICENSE",
19
- "CHANGELOG.md"
20
- ],
21
- "sideEffects": false,
22
- "engines": {
23
- "node": ">=18"
24
- },
25
- "scripts": {
26
- "build": "tsc",
27
- "test": "npm run build && node --test test/*.test.js",
28
- "prepublishOnly": "npm run build"
29
- },
30
- "keywords": [
31
- "did",
32
- "w3c",
33
- "verifiable-credentials",
34
- "identity",
35
- "mcp",
36
- "a2a",
37
- "resolver"
38
- ],
39
- "author": "DID.is",
40
- "license": "MIT",
41
- "homepage": "https://did.is/developers",
42
- "repository": {
43
- "type": "git",
44
- "url": "git+https://github.com/did-is/didis-js.git"
45
- },
46
- "bugs": {
47
- "url": "https://github.com/did-is/didis-js/issues"
48
- },
49
- "publishConfig": {
50
- "access": "public"
51
- },
52
- "devDependencies": {
53
- "typescript": "^5.7.0"
54
- }
55
- }
1
+ {
2
+ "name": "@didis/client",
3
+ "version": "0.2.7",
4
+ "description": "Official TypeScript/JavaScript SDK for the DID.is public API: DID resolution with evidence, credentials, policies and agent trust",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ },
13
+ "./package.json": "./package.json"
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "README.md",
18
+ "LICENSE",
19
+ "CHANGELOG.md"
20
+ ],
21
+ "sideEffects": false,
22
+ "engines": {
23
+ "node": ">=18"
24
+ },
25
+ "scripts": {
26
+ "build": "tsc",
27
+ "test": "npm run build && node --test test/*.test.js",
28
+ "prepublishOnly": "npm run build"
29
+ },
30
+ "keywords": [
31
+ "did",
32
+ "w3c",
33
+ "verifiable-credentials",
34
+ "identity",
35
+ "mcp",
36
+ "a2a",
37
+ "resolver"
38
+ ],
39
+ "author": "DID.is",
40
+ "license": "MIT",
41
+ "homepage": "https://did.is/developers",
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "git+https://github.com/did-is/didis-js.git"
45
+ },
46
+ "bugs": {
47
+ "url": "https://github.com/did-is/didis-js/issues"
48
+ },
49
+ "publishConfig": {
50
+ "access": "public"
51
+ },
52
+ "devDependencies": {
53
+ "typescript": "^5.7.0"
54
+ }
55
+ }