@didis/client 0.2.4 → 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 +111 -71
- package/LICENSE +21 -21
- package/README.md +180 -157
- package/dist/guard.d.ts +16 -4
- package/dist/guard.js +54 -12
- package/dist/index.d.ts +63 -16
- package/dist/index.js +35 -2
- package/dist/webcrypto.d.ts +2 -0
- package/dist/webcrypto.js +10 -0
- package/package.json +55 -55
package/CHANGELOG.md
CHANGED
|
@@ -1,71 +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
|
|
4
|
-
|
|
5
|
-
## [Unreleased]
|
|
6
|
-
|
|
7
|
-
## [0.2.
|
|
8
|
-
|
|
9
|
-
###
|
|
10
|
-
|
|
11
|
-
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
- `
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
- `
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
###
|
|
52
|
-
|
|
53
|
-
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
-
|
|
64
|
-
|
|
65
|
-
[
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
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,157 +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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
console.log(
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
);
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
/**
|
|
49
|
-
root
|
|
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 !==
|
|
83
|
-
throw new DidisGuardError("GUARD_MISCONFIGURED", "", "
|
|
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 (
|
|
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
|
|
159
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
@@ -360,6 +360,27 @@ 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
|
+
}
|
|
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
|
+
}
|
|
363
384
|
export interface McpInspection {
|
|
364
385
|
endpoint: string;
|
|
365
386
|
status: "INSPECTED" | "AUTH_REQUIRED" | string;
|
|
@@ -371,6 +392,9 @@ export interface McpInspection {
|
|
|
371
392
|
instructions?: string;
|
|
372
393
|
tools: ToolEvidence[];
|
|
373
394
|
inventoryHash: string;
|
|
395
|
+
/** Missing means unknown, not complete. Hash covers observed tools only. */
|
|
396
|
+
inventoryComplete?: boolean;
|
|
397
|
+
inventoryEvidence?: InventoryEvidence;
|
|
374
398
|
jcsProfile?: string;
|
|
375
399
|
classCounts: Record<string, number>;
|
|
376
400
|
drift: {
|
|
@@ -451,11 +475,11 @@ export interface DelegationLink {
|
|
|
451
475
|
issuer: string;
|
|
452
476
|
audience: string;
|
|
453
477
|
capabilities: string[];
|
|
454
|
-
notBefore
|
|
455
|
-
expires
|
|
456
|
-
jti
|
|
457
|
-
kid
|
|
458
|
-
alg
|
|
478
|
+
notBefore: string | null;
|
|
479
|
+
expires: string | null;
|
|
480
|
+
jti: string | null;
|
|
481
|
+
kid: string | null;
|
|
482
|
+
alg: string | null;
|
|
459
483
|
digest: string;
|
|
460
484
|
signatureValid: boolean;
|
|
461
485
|
status: string;
|
|
@@ -466,11 +490,11 @@ export interface DelegationChainResult {
|
|
|
466
490
|
status: string;
|
|
467
491
|
headline: string;
|
|
468
492
|
format: string;
|
|
469
|
-
root
|
|
470
|
-
leaf
|
|
493
|
+
root: string | null;
|
|
494
|
+
leaf: string | null;
|
|
471
495
|
effectiveCapabilities: string[];
|
|
472
|
-
notAfter
|
|
473
|
-
trustedRoot
|
|
496
|
+
notAfter: string | null;
|
|
497
|
+
trustedRoot: boolean | null;
|
|
474
498
|
links: DelegationLink[];
|
|
475
499
|
checks: Check[];
|
|
476
500
|
limitations: string[];
|
|
@@ -519,6 +543,8 @@ export interface ObservationRecord {
|
|
|
519
543
|
}
|
|
520
544
|
export interface SemanticDiff {
|
|
521
545
|
did: string;
|
|
546
|
+
fromObservation?: string;
|
|
547
|
+
toObservation?: string;
|
|
522
548
|
fromHash: string;
|
|
523
549
|
toHash: string;
|
|
524
550
|
fromObservedAt: string;
|
|
@@ -617,13 +643,13 @@ export interface FastVerifyDidResult {
|
|
|
617
643
|
}[];
|
|
618
644
|
keys: {
|
|
619
645
|
id: string;
|
|
620
|
-
curve
|
|
646
|
+
curve: string | null;
|
|
621
647
|
status: KeyEvidence["status"];
|
|
622
648
|
relationships: string[];
|
|
623
|
-
jwkThumbprint
|
|
649
|
+
jwkThumbprint: string | null;
|
|
624
650
|
}[];
|
|
625
651
|
domainBinding: string | null;
|
|
626
|
-
documentSha256
|
|
652
|
+
documentSha256: string | null;
|
|
627
653
|
observedAt: string;
|
|
628
654
|
cached: boolean;
|
|
629
655
|
}
|
|
@@ -631,9 +657,9 @@ export interface FastVerifyJwsResult {
|
|
|
631
657
|
status: "VALID" | "INVALID" | "INDETERMINATE" | "UNSUPPORTED" | "MALFORMED";
|
|
632
658
|
valid: boolean;
|
|
633
659
|
headline: string;
|
|
634
|
-
alg?: string;
|
|
635
|
-
kid?: string;
|
|
636
|
-
typ?: string;
|
|
660
|
+
alg?: string | null;
|
|
661
|
+
kid?: string | null;
|
|
662
|
+
typ?: string | null;
|
|
637
663
|
/** Decoded JSON payload, or base64url text when the payload is not JSON. */
|
|
638
664
|
payload?: unknown;
|
|
639
665
|
signingInputSha256?: string;
|
|
@@ -670,6 +696,13 @@ export interface DidisClientOptions {
|
|
|
670
696
|
baseUrl?: string;
|
|
671
697
|
/** Bearer token for admin-only monitoring routes. Never ship it to browsers. */
|
|
672
698
|
adminToken?: string;
|
|
699
|
+
/**
|
|
700
|
+
* Project API key from the DID.is console (optional). With a key, `inspectMcp`, `inspectA2a`,
|
|
701
|
+
* `authorize` and `verifyDelegation` use the paid routes and count against your team's monthly
|
|
702
|
+
* allowance instead of the anonymous per-IP limit. Results are identical. The key is sent only
|
|
703
|
+
* to `/v1/customer/*` routes, never to public routes. Keep it server-side.
|
|
704
|
+
*/
|
|
705
|
+
apiKey?: string;
|
|
673
706
|
timeoutMs?: number;
|
|
674
707
|
fetch?: typeof fetch;
|
|
675
708
|
/**
|
|
@@ -688,10 +721,11 @@ export declare function retryAfterMs(value: string | null | undefined, now?: num
|
|
|
688
721
|
/** Public DID.is API. Point `baseUrl` at your own deployment when self-hosting. */
|
|
689
722
|
export declare const DEFAULT_BASE_URL = "https://did.is/api";
|
|
690
723
|
/** Package version, kept equal to package.json (checked by the test suite). */
|
|
691
|
-
export declare const VERSION = "0.2.
|
|
724
|
+
export declare const VERSION = "0.2.7";
|
|
692
725
|
export declare class DidisClient {
|
|
693
726
|
private readonly baseUrl;
|
|
694
727
|
private readonly adminToken?;
|
|
728
|
+
private readonly apiKey?;
|
|
695
729
|
private readonly timeoutMs;
|
|
696
730
|
private readonly fetchImpl;
|
|
697
731
|
private readonly maxRetries;
|
|
@@ -704,6 +738,13 @@ export declare class DidisClient {
|
|
|
704
738
|
private backoff;
|
|
705
739
|
private request;
|
|
706
740
|
private once;
|
|
741
|
+
/**
|
|
742
|
+
* Paid route call: the project API key plus one fresh Idempotency-Key. Automatic retries of a
|
|
743
|
+
* 429/503 reuse the same key, so a request is never billed twice.
|
|
744
|
+
*/
|
|
745
|
+
private paid;
|
|
746
|
+
/** Team allowance for this month (requires `apiKey`). Same numbers as the RateLimit headers. */
|
|
747
|
+
usage(): Promise<CustomerUsage>;
|
|
707
748
|
private admin;
|
|
708
749
|
health(): Promise<HealthStatus>;
|
|
709
750
|
/** Enriched resolution: verdict, evidence dimensions, graph, telemetry. */
|
|
@@ -720,6 +761,8 @@ export declare class DidisClient {
|
|
|
720
761
|
history(did: string): Promise<HistoryResponse>;
|
|
721
762
|
/** Semantic diff; defaults to the latest two distinct observations. */
|
|
722
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>;
|
|
723
766
|
graph(did: string): Promise<GraphResponse>;
|
|
724
767
|
/** Audience is checked only when an expected recipient is supplied. Pass raw JSON as a string to preserve member identity. */
|
|
725
768
|
verifyCredential(credential: unknown, options?: CredentialVerifyOptions): Promise<CredentialVerification>;
|
|
@@ -729,6 +772,10 @@ export declare class DidisClient {
|
|
|
729
772
|
}, credential?: unknown): Promise<PolicyResponse>;
|
|
730
773
|
inspectMcp(endpoint: string): Promise<McpInspection>;
|
|
731
774
|
inspectA2a(url: string): Promise<A2aInspection>;
|
|
775
|
+
/**
|
|
776
|
+
* `register: true` stores the chain for `verifyTool`; it always uses the public route (the paid
|
|
777
|
+
* route never writes shared state), so the API key is not sent for it.
|
|
778
|
+
*/
|
|
732
779
|
verifyDelegation(chain: string[], opts?: {
|
|
733
780
|
trustedRoots?: string[];
|
|
734
781
|
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.
|
|
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
|
|
@@ -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");
|
|
@@ -174,6 +190,10 @@ export class DidisClient {
|
|
|
174
190
|
const q = from && to ? `?from=${encodeURIComponent(from)}&to=${encodeURIComponent(to)}` : "";
|
|
175
191
|
return this.request("GET", `/v1/diff/${enc(did)}${q}`);
|
|
176
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
|
+
}
|
|
177
197
|
graph(did) {
|
|
178
198
|
return this.request("GET", `/v1/graph/${enc(did)}`);
|
|
179
199
|
}
|
|
@@ -191,15 +211,28 @@ export class DidisClient {
|
|
|
191
211
|
return this.request("POST", "/v1/policies/evaluate", { did, policy, credential });
|
|
192
212
|
}
|
|
193
213
|
inspectMcp(endpoint) {
|
|
214
|
+
if (this.apiKey)
|
|
215
|
+
return this.paid("POST", "mcp/inspect", { endpoint });
|
|
194
216
|
return this.request("GET", `/v1/mcp/inspect?endpoint=${encodeURIComponent(endpoint)}`);
|
|
195
217
|
}
|
|
196
218
|
inspectA2a(url) {
|
|
219
|
+
if (this.apiKey)
|
|
220
|
+
return this.paid("POST", "a2a/inspect", { url });
|
|
197
221
|
return this.request("GET", `/v1/a2a/inspect?url=${encodeURIComponent(url)}`);
|
|
198
222
|
}
|
|
223
|
+
/**
|
|
224
|
+
* `register: true` stores the chain for `verifyTool`; it always uses the public route (the paid
|
|
225
|
+
* route never writes shared state), so the API key is not sent for it.
|
|
226
|
+
*/
|
|
199
227
|
verifyDelegation(chain, opts = {}) {
|
|
228
|
+
if (this.apiKey && !opts.register) {
|
|
229
|
+
return this.paid("POST", "agents/verify-delegation", { chain, ...(opts.trustedRoots ? { trustedRoots: opts.trustedRoots } : {}) });
|
|
230
|
+
}
|
|
200
231
|
return this.request("POST", "/v1/agents/verify-delegation", { chain, ...opts });
|
|
201
232
|
}
|
|
202
233
|
authorize(chain, tool, opts = {}) {
|
|
234
|
+
if (this.apiKey)
|
|
235
|
+
return this.paid("POST", "agents/authorize", { chain, tool, ...(opts.trustedRoots ? { trustedRoots: opts.trustedRoots } : {}) });
|
|
203
236
|
return this.request("POST", "/v1/agents/authorize", { chain, tool, ...opts });
|
|
204
237
|
}
|
|
205
238
|
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,55 +1,55 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@didis/client",
|
|
3
|
-
"version": "0.2.
|
|
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
|
+
}
|