@didis/client 0.2.0 → 0.2.1
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 +46 -0
- package/README.md +11 -2
- package/dist/guard.d.ts +16 -4
- package/dist/guard.js +37 -31
- package/package.json +3 -2
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
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.1] - 2026-10-14
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- 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).
|
|
12
|
+
- `onDecision` events and `DidisGuardError.detail` now include `capability`.
|
|
13
|
+
- Examples for the MCP TypeScript SDK (`mcp-client.mjs`), LangChain (`langchain-tool.mjs`) and the OpenAI Agents SDK (`openai-agents-tool.mjs`).
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- A capability containing `*`, empty segments or whitespace is refused with `GUARD_MISCONFIGURED` before any network call. Wildcards belong in delegations, not in tool calls.
|
|
18
|
+
|
|
19
|
+
## [0.2.0] - 2026-10-13
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- 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/).
|
|
24
|
+
- Tool definition pinning: `canonicalJson` (RFC 8785 JCS), `toolDefinitionSha256`, `mcpInventoryHash` and `pinToolDefinitions`. Hashes are identical to the `definitionSha256` and `inventoryHash` values DID.is reports.
|
|
25
|
+
- `attestMcpPins(client, endpoint, pins)` compares a public MCP server, as observed by DID.is, with your pins.
|
|
26
|
+
- `verifyCredential` options `expectedNonce` and `trustedIssuerCertificates` (SD-JWT VC key binding and caller-chosen X.509 anchors), with the `CredentialVerifyOptions` type.
|
|
27
|
+
- Response types `IssuerTrust` and `SdJwtEvidence`, and the optional `issuerTrust` and `sdJwt` fields on `CredentialVerification`.
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
- 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`.
|
|
32
|
+
|
|
33
|
+
## [0.1.0] - 2026-10-05
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
|
|
37
|
+
- First public release as `@didis/client`. Zero dependencies, ESM, Node 18+, Deno, Bun, browsers and edge runtimes.
|
|
38
|
+
- `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`.
|
|
39
|
+
- Self-hosted monitoring methods that need an admin token: `watch`, `watches`, `unwatch` and `events`.
|
|
40
|
+
- `DidisError` carrying the RFC 9457 problem body and `Retry-After`.
|
|
41
|
+
- Webhook helpers `verifyWebhookSignature` and `verifyCustomerMonitorSignature`.
|
|
42
|
+
|
|
43
|
+
[Unreleased]: https://github.com/did-is/didis-js/compare/v0.2.1...HEAD
|
|
44
|
+
[0.2.1]: https://github.com/did-is/didis-js/compare/v0.2.0...v0.2.1
|
|
45
|
+
[0.2.0]: https://github.com/did-is/didis-js/compare/v0.1.0...v0.2.0
|
|
46
|
+
[0.1.0]: https://github.com/did-is/didis-js/releases/tag/v0.1.0
|
package/README.md
CHANGED
|
@@ -83,15 +83,24 @@ const guard = createDidisGuard({
|
|
|
83
83
|
});
|
|
84
84
|
|
|
85
85
|
const createIssue = guard.wrap(
|
|
86
|
-
{
|
|
86
|
+
{
|
|
87
|
+
tool: "github_create_issue", // the name in tools/list (pin key)
|
|
88
|
+
capability: "github:create_issue", // what the delegation must grant
|
|
89
|
+
definition: () => currentTools.get("github_create_issue"),
|
|
90
|
+
},
|
|
87
91
|
async (args) => github.createIssue(args),
|
|
88
92
|
);
|
|
89
93
|
```
|
|
90
94
|
|
|
95
|
+
**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.
|
|
96
|
+
|
|
97
|
+
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).
|
|
98
|
+
|
|
91
99
|
- The definition check is local (RFC 8785 JCS + SHA-256, the same `definitionSha256` DID.is reports) and makes no network call.
|
|
92
100
|
- 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.
|
|
93
101
|
- Error codes: `TOOL_DRIFT`, `TOOL_DEFINITION_MISSING`, `TOOL_NOT_AUTHORIZED`, `GUARD_UNAVAILABLE`, `GUARD_MISCONFIGURED`.
|
|
94
|
-
- One tool only: `withDidisGuard(handler, { client, agent, root, tool, pin, definition })`.
|
|
102
|
+
- One tool only: `withDidisGuard(handler, { client, agent, root, tool, capability, pin, definition })`.
|
|
103
|
+
- `onDecision` and `DidisGuardError.detail` include both `tool` and `capability`.
|
|
95
104
|
- `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.
|
|
96
105
|
|
|
97
106
|
## Webhooks
|
package/dist/guard.d.ts
CHANGED
|
@@ -6,8 +6,14 @@
|
|
|
6
6
|
* hashes to the SHA-256 you approved (same RFC 8785 JCS hash DID.is reports as
|
|
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
|
-
* delegation whose leaf capabilities grant the tool. ALLOW is cached briefly;
|
|
10
|
-
* errors are never cached.
|
|
9
|
+
* delegation whose leaf capabilities grant the tool's capability. ALLOW is cached briefly;
|
|
10
|
+
* DENY and errors are never cached.
|
|
11
|
+
*
|
|
12
|
+
* Tool name vs capability: MCP tool names may only use `A-Z a-z 0-9 _ - .` (SEP-986), while
|
|
13
|
+
* DID.is capabilities are namespaced with `:` (e.g. `weather:forecast`). Pass both when they
|
|
14
|
+
* differ: `{ tool: "weather_forecast", capability: "weather:forecast" }`. Pins are keyed by the
|
|
15
|
+
* tool name (what `tools/list` returns); DID.is is asked about the capability. When
|
|
16
|
+
* `capability` is omitted, the tool name is used as the capability.
|
|
11
17
|
*
|
|
12
18
|
* Any failure (drift, DENY, network error, 429, malformed answer) throws `DidisGuardError`
|
|
13
19
|
* and the handler is not called. `attestMcpPins` is an optional, separate startup check that
|
|
@@ -50,6 +56,7 @@ export interface DidisGuardOptions {
|
|
|
50
56
|
/** Called after every decision (for audit logs). Must not throw. */
|
|
51
57
|
onDecision?: (event: {
|
|
52
58
|
tool: string;
|
|
59
|
+
capability: string;
|
|
53
60
|
allowed: boolean;
|
|
54
61
|
code?: DidisGuardErrorCode;
|
|
55
62
|
reason: string;
|
|
@@ -57,8 +64,13 @@ export interface DidisGuardOptions {
|
|
|
57
64
|
}) => void;
|
|
58
65
|
}
|
|
59
66
|
export interface GuardCallOptions {
|
|
60
|
-
/** The tool name
|
|
67
|
+
/** The tool name the agent sees (MCP `tools/list` name, pin key), e.g. `github_create_issue`. */
|
|
61
68
|
tool: string;
|
|
69
|
+
/**
|
|
70
|
+
* The DID.is capability this tool needs, e.g. `github:create_issue`. Defaults to `tool`.
|
|
71
|
+
* Must be an exact capability (no `*`): wildcards belong in delegations, not in tool calls.
|
|
72
|
+
*/
|
|
73
|
+
capability?: string;
|
|
62
74
|
/** The definition the agent received, or a function returning the current one. */
|
|
63
75
|
definition?: Definition;
|
|
64
76
|
}
|
|
@@ -72,7 +84,7 @@ export interface DidisGuard {
|
|
|
72
84
|
}
|
|
73
85
|
export declare function createDidisGuard(options: DidisGuardOptions): DidisGuard;
|
|
74
86
|
/**
|
|
75
|
-
* One-line form: `withDidisGuard(handler, { client, agent, root, tool, pin, definition })`.
|
|
87
|
+
* One-line form: `withDidisGuard(handler, { client, agent, root, tool, capability, pin, definition })`.
|
|
76
88
|
* For several tools, create one guard with `createDidisGuard` so the ALLOW cache is shared.
|
|
77
89
|
*/
|
|
78
90
|
export declare function withDidisGuard<A extends unknown[], R>(handler: (...args: A) => R | Promise<R>, options: Omit<DidisGuardOptions, "pins"> & GuardCallOptions & {
|
package/dist/guard.js
CHANGED
|
@@ -70,6 +70,8 @@ export async function pinToolDefinitions(tools) {
|
|
|
70
70
|
return pins;
|
|
71
71
|
}
|
|
72
72
|
const DID = /^did:[a-z0-9]+:\S+$/;
|
|
73
|
+
/** An exact DID.is capability: `:`-separated non-empty segments, no whitespace, no `*`. */
|
|
74
|
+
const CAPABILITY = /^[^\s:*]+(?::[^\s:*]+)*$/;
|
|
73
75
|
export function createDidisGuard(options) {
|
|
74
76
|
const { client, agent, root, pins = {}, requirePins = false, onDecision } = options;
|
|
75
77
|
const allowCacheMs = options.allowCacheMs ?? 30_000;
|
|
@@ -87,23 +89,23 @@ export function createDidisGuard(options) {
|
|
|
87
89
|
}
|
|
88
90
|
const pinFor = (tool) => (Object.prototype.hasOwnProperty.call(pins, tool) ? pins[tool] : undefined);
|
|
89
91
|
const allowed = new Map();
|
|
90
|
-
const report = (tool, ok, reason, cached, code) => {
|
|
92
|
+
const report = (tool, capability, ok, reason, cached, code) => {
|
|
91
93
|
try {
|
|
92
|
-
onDecision?.({ tool, allowed: ok, code, reason, cached });
|
|
94
|
+
onDecision?.({ tool, capability, allowed: ok, code, reason, cached });
|
|
93
95
|
}
|
|
94
96
|
catch {
|
|
95
97
|
/* audit hooks must not change the decision */
|
|
96
98
|
}
|
|
97
99
|
};
|
|
98
|
-
const fail = (code, tool, reason, detail) => {
|
|
99
|
-
report(tool, false, reason, false, code);
|
|
100
|
-
throw new DidisGuardError(code, tool, reason, detail);
|
|
100
|
+
const fail = (code, tool, capability, reason, detail) => {
|
|
101
|
+
report(tool, capability, false, reason, false, code);
|
|
102
|
+
throw new DidisGuardError(code, tool, reason, { ...detail, capability });
|
|
101
103
|
};
|
|
102
|
-
async function checkDefinition(tool, definition) {
|
|
104
|
+
async function checkDefinition(tool, capability, definition) {
|
|
103
105
|
const pin = pinFor(tool);
|
|
104
106
|
if (pin === undefined) {
|
|
105
107
|
if (requirePins)
|
|
106
|
-
fail("TOOL_DEFINITION_MISSING", tool, `no pinned definition for '${tool}'`);
|
|
108
|
+
fail("TOOL_DEFINITION_MISSING", tool, capability, `no pinned definition for '${tool}'`);
|
|
107
109
|
return;
|
|
108
110
|
}
|
|
109
111
|
let def;
|
|
@@ -111,60 +113,64 @@ export function createDidisGuard(options) {
|
|
|
111
113
|
def = typeof definition === "function" ? await definition() : definition;
|
|
112
114
|
}
|
|
113
115
|
catch {
|
|
114
|
-
return fail("TOOL_DEFINITION_MISSING", tool, `could not read the current definition of '${tool}'`);
|
|
116
|
+
return fail("TOOL_DEFINITION_MISSING", tool, capability, `could not read the current definition of '${tool}'`);
|
|
115
117
|
}
|
|
116
118
|
if (def === undefined || def === null)
|
|
117
|
-
fail("TOOL_DEFINITION_MISSING", tool, `'${tool}' is pinned but no definition was supplied`);
|
|
119
|
+
fail("TOOL_DEFINITION_MISSING", tool, capability, `'${tool}' is pinned but no definition was supplied`);
|
|
118
120
|
let actual;
|
|
119
121
|
try {
|
|
120
122
|
actual = await toolDefinitionSha256(def);
|
|
121
123
|
}
|
|
122
124
|
catch {
|
|
123
|
-
return fail("TOOL_DRIFT", tool, `the definition of '${tool}' is not valid JSON`);
|
|
125
|
+
return fail("TOOL_DRIFT", tool, capability, `the definition of '${tool}' is not valid JSON`);
|
|
124
126
|
}
|
|
125
127
|
if (def.name !== tool)
|
|
126
|
-
fail("TOOL_DRIFT", tool, `definition name does not match '${tool}'`, { expected: pin, actual });
|
|
128
|
+
fail("TOOL_DRIFT", tool, capability, `definition name does not match '${tool}'`, { expected: pin, actual });
|
|
127
129
|
if (actual !== pin)
|
|
128
|
-
fail("TOOL_DRIFT", tool, `definition of '${tool}' changed since it was approved`, { expected: pin, actual });
|
|
130
|
+
fail("TOOL_DRIFT", tool, capability, `definition of '${tool}' changed since it was approved`, { expected: pin, actual });
|
|
129
131
|
}
|
|
130
|
-
async function checkDelegation(tool) {
|
|
132
|
+
async function checkDelegation(tool, capability) {
|
|
131
133
|
const now = Date.now();
|
|
132
|
-
const until = allowed.get(
|
|
134
|
+
const until = allowed.get(capability);
|
|
133
135
|
if (until !== undefined && until > now) {
|
|
134
|
-
report(tool, true, "cached ALLOW", true);
|
|
136
|
+
report(tool, capability, true, "cached ALLOW", true);
|
|
135
137
|
return;
|
|
136
138
|
}
|
|
137
|
-
allowed.delete(
|
|
139
|
+
allowed.delete(capability);
|
|
138
140
|
let auth;
|
|
139
141
|
try {
|
|
140
|
-
auth = await client.verifyTool(agent,
|
|
142
|
+
auth = await client.verifyTool(agent, capability, root);
|
|
141
143
|
}
|
|
142
144
|
catch (e) {
|
|
143
145
|
const status = e.status;
|
|
144
|
-
return fail("GUARD_UNAVAILABLE", tool, `DID.is verify-tool failed${status ? ` (HTTP ${status})` : ""}`, { status });
|
|
146
|
+
return fail("GUARD_UNAVAILABLE", tool, capability, `DID.is verify-tool failed${status ? ` (HTTP ${status})` : ""}`, { status });
|
|
145
147
|
}
|
|
146
|
-
const exact = auth && auth.decision === "ALLOW" && auth.authorized === true && auth.agent === agent && auth.tool ===
|
|
148
|
+
const exact = auth && auth.decision === "ALLOW" && auth.authorized === true && auth.agent === agent && auth.tool === capability;
|
|
147
149
|
if (!exact) {
|
|
148
150
|
if (auth && (auth.decision === "DENY" || auth.authorized === false)) {
|
|
149
|
-
fail("TOOL_NOT_AUTHORIZED", tool, auth.reason || "delegation does not grant this
|
|
151
|
+
fail("TOOL_NOT_AUTHORIZED", tool, capability, auth.reason || "delegation does not grant this capability", { matchedCapability: auth.matchedCapability });
|
|
150
152
|
}
|
|
151
|
-
fail("GUARD_UNAVAILABLE", tool, "DID.is returned an unexpected authorization answer");
|
|
153
|
+
fail("GUARD_UNAVAILABLE", tool, capability, "DID.is returned an unexpected authorization answer");
|
|
152
154
|
}
|
|
153
155
|
if (root !== undefined && auth.root !== root)
|
|
154
|
-
fail("TOOL_NOT_AUTHORIZED", tool, "delegation root does not match the configured root");
|
|
156
|
+
fail("TOOL_NOT_AUTHORIZED", tool, capability, "delegation root does not match the configured root");
|
|
155
157
|
if (allowCacheMs > 0) {
|
|
156
158
|
const expires = auth.expires ? Date.parse(auth.expires) : NaN;
|
|
157
159
|
const until2 = Math.min(now + allowCacheMs, Number.isFinite(expires) ? expires : now + allowCacheMs);
|
|
158
160
|
if (until2 > now)
|
|
159
|
-
allowed.set(
|
|
161
|
+
allowed.set(capability, until2);
|
|
160
162
|
}
|
|
161
|
-
report(tool, true, auth.reason, false);
|
|
163
|
+
report(tool, capability, true, auth.reason, false);
|
|
162
164
|
}
|
|
163
|
-
async function check({ tool, definition }) {
|
|
165
|
+
async function check({ tool, capability, definition }) {
|
|
164
166
|
if (typeof tool !== "string" || !tool.trim() || tool !== tool.trim())
|
|
165
|
-
fail("GUARD_MISCONFIGURED", String(tool), "tool must be a non-empty, trimmed name");
|
|
166
|
-
|
|
167
|
-
|
|
167
|
+
fail("GUARD_MISCONFIGURED", String(tool), String(capability ?? tool), "tool must be a non-empty, trimmed name");
|
|
168
|
+
const cap = capability === undefined ? tool : capability;
|
|
169
|
+
if (typeof cap !== "string" || !CAPABILITY.test(cap)) {
|
|
170
|
+
fail("GUARD_MISCONFIGURED", tool, String(cap), "capability must be exact: ':'-separated non-empty segments, no spaces, no '*'");
|
|
171
|
+
}
|
|
172
|
+
await checkDefinition(tool, cap, definition);
|
|
173
|
+
await checkDelegation(tool, cap);
|
|
168
174
|
}
|
|
169
175
|
return {
|
|
170
176
|
check,
|
|
@@ -182,13 +188,13 @@ export function createDidisGuard(options) {
|
|
|
182
188
|
};
|
|
183
189
|
}
|
|
184
190
|
/**
|
|
185
|
-
* One-line form: `withDidisGuard(handler, { client, agent, root, tool, pin, definition })`.
|
|
191
|
+
* One-line form: `withDidisGuard(handler, { client, agent, root, tool, capability, pin, definition })`.
|
|
186
192
|
* For several tools, create one guard with `createDidisGuard` so the ALLOW cache is shared.
|
|
187
193
|
*/
|
|
188
194
|
export function withDidisGuard(handler, options) {
|
|
189
|
-
const { tool, definition, pin, ...rest } = options;
|
|
195
|
+
const { tool, capability, definition, pin, ...rest } = options;
|
|
190
196
|
const guard = createDidisGuard({ ...rest, pins: pin ? { [tool]: pin } : undefined });
|
|
191
|
-
return guard.wrap({ tool, definition }, handler);
|
|
197
|
+
return guard.wrap({ tool, capability, definition }, handler);
|
|
192
198
|
}
|
|
193
199
|
/**
|
|
194
200
|
* Optional startup check: asks DID.is to inspect a public MCP endpoint and compares the observed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@didis/client",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Official TypeScript/JavaScript SDK for the DID.is public API: DID resolution with evidence, credentials, policies and agent trust",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
"files": [
|
|
16
16
|
"dist",
|
|
17
17
|
"README.md",
|
|
18
|
-
"LICENSE"
|
|
18
|
+
"LICENSE",
|
|
19
|
+
"CHANGELOG.md"
|
|
19
20
|
],
|
|
20
21
|
"sideEffects": false,
|
|
21
22
|
"engines": {
|