@didis/client 0.2.0 → 0.2.2

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 ADDED
@@ -0,0 +1,53 @@
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.2] - 2026-10-18
8
+
9
+ ### Added
10
+
11
+ - `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.
12
+
13
+ ## [0.2.1] - 2026-10-14
14
+
15
+ ### Added
16
+
17
+ - 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).
18
+ - `onDecision` events and `DidisGuardError.detail` now include `capability`.
19
+ - Examples for the MCP TypeScript SDK (`mcp-client.mjs`), LangChain (`langchain-tool.mjs`) and the OpenAI Agents SDK (`openai-agents-tool.mjs`).
20
+
21
+ ### Changed
22
+
23
+ - A capability containing `*`, empty segments or whitespace is refused with `GUARD_MISCONFIGURED` before any network call. Wildcards belong in delegations, not in tool calls.
24
+
25
+ ## [0.2.0] - 2026-10-13
26
+
27
+ ### Added
28
+
29
+ - 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/).
30
+ - Tool definition pinning: `canonicalJson` (RFC 8785 JCS), `toolDefinitionSha256`, `mcpInventoryHash` and `pinToolDefinitions`. Hashes are identical to the `definitionSha256` and `inventoryHash` values DID.is reports.
31
+ - `attestMcpPins(client, endpoint, pins)` compares a public MCP server, as observed by DID.is, with your pins.
32
+ - `verifyCredential` options `expectedNonce` and `trustedIssuerCertificates` (SD-JWT VC key binding and caller-chosen X.509 anchors), with the `CredentialVerifyOptions` type.
33
+ - Response types `IssuerTrust` and `SdJwtEvidence`, and the optional `issuerTrust` and `sdJwt` fields on `CredentialVerification`.
34
+
35
+ ### Fixed
36
+
37
+ - 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`.
38
+
39
+ ## [0.1.0] - 2026-10-05
40
+
41
+ ### Added
42
+
43
+ - First public release as `@didis/client`. Zero dependencies, ESM, Node 18+, Deno, Bun, browsers and edge runtimes.
44
+ - `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`.
45
+ - Self-hosted monitoring methods that need an admin token: `watch`, `watches`, `unwatch` and `events`.
46
+ - `DidisError` carrying the RFC 9457 problem body and `Retry-After`.
47
+ - Webhook helpers `verifyWebhookSignature` and `verifyCustomerMonitorSignature`.
48
+
49
+ [Unreleased]: https://github.com/did-is/didis-js/compare/v0.2.2...HEAD
50
+ [0.2.2]: https://github.com/did-is/didis-js/compare/v0.2.1...v0.2.2
51
+ [0.2.1]: https://github.com/did-is/didis-js/compare/v0.2.0...v0.2.1
52
+ [0.2.0]: https://github.com/did-is/didis-js/compare/v0.1.0...v0.2.0
53
+ [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
- { tool: "github:create_issue", definition: () => currentTools.get("github:create_issue") },
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; DENY and
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 as granted by capabilities, e.g. `github:create_issue`. */
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(tool);
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(tool);
139
+ allowed.delete(capability);
138
140
  let auth;
139
141
  try {
140
- auth = await client.verifyTool(agent, tool, root);
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 === 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 tool", { matchedCapability: auth.matchedCapability });
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(tool, until2);
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
- await checkDefinition(tool, definition);
167
- await checkDelegation(tool);
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/dist/index.d.ts CHANGED
@@ -254,6 +254,24 @@ export interface CredentialVerifyOptions {
254
254
  expectedNonce?: string;
255
255
  /** Caller-chosen X.509 anchors (PEM or base64 DER) for SD-JWT VC `x5c`. Without them `x5c` is never VALID. */
256
256
  trustedIssuerCertificates?: string[];
257
+ /**
258
+ * Conditions checked without returning claim values, e.g. `[{ path: "/age_over_18", equals: true }]`.
259
+ * Paths are JSON Pointers into the issuer-signed claims; `equals` is a boolean, integer or string.
260
+ * Evaluated only for a VALID credential; see `CredentialVerification.requirements`.
261
+ */
262
+ requiredClaims?: ClaimRequirement[];
263
+ }
264
+ export interface ClaimRequirement {
265
+ path: string;
266
+ equals: boolean | number | string;
267
+ }
268
+ export interface RequirementsOutcome {
269
+ /** True only when the credential is VALID and every condition is SATISFIED. */
270
+ met: boolean;
271
+ results: {
272
+ path: string;
273
+ result: "SATISFIED" | "NOT_SATISFIED" | "NOT_DISCLOSED" | "NOT_EVALUATED";
274
+ }[];
257
275
  }
258
276
  export interface CredentialVerification {
259
277
  status: string;
@@ -304,6 +322,8 @@ export interface CredentialVerification {
304
322
  issuerTrust?: IssuerTrust;
305
323
  /** SD-JWT VC only. No disclosed claim values are ever returned. */
306
324
  sdJwt?: SdJwtEvidence;
325
+ /** Present only when `requiredClaims` was sent. Never contains claim values. */
326
+ requirements?: RequirementsOutcome;
307
327
  /** For SD-JWT VC: disclosed claim paths and digests only, never values. */
308
328
  claims: unknown;
309
329
  credential: unknown;
package/dist/index.js CHANGED
@@ -137,6 +137,7 @@ export class DidisClient {
137
137
  expectedAudience: options.expectedAudience,
138
138
  expectedNonce: options.expectedNonce,
139
139
  trustedIssuerCertificates: options.trustedIssuerCertificates,
140
+ requiredClaims: options.requiredClaims,
140
141
  });
141
142
  }
142
143
  evaluatePolicy(did, policy, credential) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@didis/client",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
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": {