@didis/client 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,22 @@ All notable changes to `@didis/client` are listed here. The format follows [Keep
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.2.4] - 2026-10-19
8
+
9
+ ### Added
10
+
11
+ - 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`.
12
+
13
+ ## [0.2.3] - 2026-10-18
14
+
15
+ ### Added
16
+
17
+ - 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`.
18
+
19
+ - 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.
20
+
21
+ - 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.
22
+
7
23
  ## [0.2.2] - 2026-10-18
8
24
 
9
25
  ### Added
@@ -46,7 +62,9 @@ All notable changes to `@didis/client` are listed here. The format follows [Keep
46
62
  - `DidisError` carrying the RFC 9457 problem body and `Retry-After`.
47
63
  - Webhook helpers `verifyWebhookSignature` and `verifyCustomerMonitorSignature`.
48
64
 
49
- [Unreleased]: https://github.com/did-is/didis-js/compare/v0.2.2...HEAD
65
+ [Unreleased]: https://github.com/did-is/didis-js/compare/v0.2.4...HEAD
66
+ [0.2.4]: https://github.com/did-is/didis-js/compare/v0.2.3...v0.2.4
67
+ [0.2.3]: https://github.com/did-is/didis-js/compare/v0.2.2...v0.2.3
50
68
  [0.2.2]: https://github.com/did-is/didis-js/compare/v0.2.1...v0.2.2
51
69
  [0.2.1]: https://github.com/did-is/didis-js/compare/v0.2.0...v0.2.1
52
70
  [0.2.0]: https://github.com/did-is/didis-js/compare/v0.1.0...v0.2.0
package/README.md CHANGED
@@ -63,6 +63,8 @@ try {
63
63
 
64
64
  Identifiers are percent-encoded exactly once by the client. Non-2xx responses throw `DidisError` with the RFC 9457 problem body (including the resolution `trace` where available).
65
65
 
66
+ **Rate limits.** When the public budget is used up the API answers `429` with `Retry-After`. The client waits that long and retries (up to 2 times, at most 10 s per wait), so a loop of calls slows down instead of failing. Tune with `new DidisClient({ retry: { maxRetries: 3, maxWaitMs: 5000 } })` or turn it off with `retry: false`.
67
+
66
68
  ## Guard agent tools
67
69
 
68
70
  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).
package/dist/index.d.ts CHANGED
@@ -128,6 +128,19 @@ export interface DomainBindingEvidence {
128
128
  credentials: LinkageCredentialEvidence[];
129
129
  detail: string;
130
130
  }
131
+ /** One `alsoKnownAs` value checked for a reciprocal link back to the DID. */
132
+ export interface LinkedAccountEvidence {
133
+ /** The `alsoKnownAs` value exactly as declared. */
134
+ value: string;
135
+ /** `WEB_ORIGIN` | `WEB_PAGE` | `SAME_ORIGIN` | `DID` | `OTHER` */
136
+ kind: string;
137
+ origin?: string;
138
+ /** `CONFIRMED` | `NOT_CONFIRMED` | `FAILED` | `UNSUPPORTED` | `UNREACHABLE` | `NOT_CHECKABLE` */
139
+ status: string;
140
+ configurationStatus?: string;
141
+ configurationSha256?: string;
142
+ detail: string;
143
+ }
131
144
  export interface LogEntryEvidence {
132
145
  versionId: string;
133
146
  versionNumber: number;
@@ -195,6 +208,8 @@ export interface Evidence {
195
208
  keys: KeyEvidence[];
196
209
  services: ServiceEvidence[];
197
210
  domainBinding?: DomainBindingEvidence;
211
+ /** Reciprocal checks of `alsoKnownAs`; absent when nothing is declared. */
212
+ linkedAccounts?: LinkedAccountEvidence[];
198
213
  tls?: TlsEvidence;
199
214
  history?: HistoryEvidence;
200
215
  }
@@ -552,6 +567,8 @@ export interface PolicyRules {
552
567
  requireKeyRelationship?: string;
553
568
  domainBinding?: "required" | "optional";
554
569
  tlsMinDaysRemaining?: number;
570
+ /** 1–4 bare https origins that must be declared in `alsoKnownAs` and link back to the DID. */
571
+ linkedAccounts?: string[];
555
572
  verifiableHistory?: "required";
556
573
  maxCacheAgeSeconds?: number;
557
574
  credentialStatus?: "active";
@@ -655,16 +672,38 @@ export interface DidisClientOptions {
655
672
  adminToken?: string;
656
673
  timeoutMs?: number;
657
674
  fetch?: typeof fetch;
675
+ /**
676
+ * Automatic wait-and-retry when the API answers 429 (request budget used up) or 503
677
+ * (temporarily overloaded) with a `Retry-After` header. Such refusals happen before the
678
+ * request is processed, so retrying is safe for every call. Default: up to 2 retries,
679
+ * never waiting longer than 10 s for one retry. Pass `false` to turn it off.
680
+ */
681
+ retry?: false | {
682
+ maxRetries?: number;
683
+ maxWaitMs?: number;
684
+ };
658
685
  }
686
+ /** Milliseconds to wait from a `Retry-After` value (delta-seconds or HTTP-date); null if unusable. */
687
+ export declare function retryAfterMs(value: string | null | undefined, now?: number): number | null;
659
688
  /** Public DID.is API. Point `baseUrl` at your own deployment when self-hosting. */
660
689
  export declare const DEFAULT_BASE_URL = "https://did.is/api";
690
+ /** Package version, kept equal to package.json (checked by the test suite). */
691
+ export declare const VERSION = "0.2.4";
661
692
  export declare class DidisClient {
662
693
  private readonly baseUrl;
663
694
  private readonly adminToken?;
664
695
  private readonly timeoutMs;
665
696
  private readonly fetchImpl;
697
+ private readonly maxRetries;
698
+ private readonly maxWaitMs;
666
699
  constructor(options?: string | DidisClientOptions);
700
+ /**
701
+ * Waits out a 429/503 refusal when the server says how long, within the configured bounds.
702
+ * A small random delay spreads out many clients that were refused at the same moment.
703
+ */
704
+ private backoff;
667
705
  private request;
706
+ private once;
668
707
  private admin;
669
708
  health(): Promise<HealthStatus>;
670
709
  /** Enriched resolution: verdict, evidence dimensions, graph, telemetry. */
package/dist/index.js CHANGED
@@ -12,29 +12,76 @@ export class DidisError extends Error {
12
12
  this.problem = problem;
13
13
  }
14
14
  }
15
+ /** Milliseconds to wait from a `Retry-After` value (delta-seconds or HTTP-date); null if unusable. */
16
+ export function retryAfterMs(value, now = Date.now()) {
17
+ if (!value)
18
+ return null;
19
+ const v = value.trim();
20
+ if (/^\d+$/.test(v))
21
+ return Number(v) * 1000;
22
+ const at = Date.parse(v);
23
+ return Number.isNaN(at) ? null : Math.max(0, at - now);
24
+ }
25
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
15
26
  /** Public DID.is API. Point `baseUrl` at your own deployment when self-hosting. */
16
27
  export const DEFAULT_BASE_URL = "https://did.is/api";
28
+ /** Package version, kept equal to package.json (checked by the test suite). */
29
+ export const VERSION = "0.2.4";
30
+ /**
31
+ * Identifies the SDK to the API so DID.is can count SDK use apart from other HTTP clients.
32
+ * Only the product name and version are sent. Browsers are left alone: there the browser
33
+ * controls the user agent and an extra header would add a CORS preflight to every call.
34
+ */
35
+ const SDK_HEADERS = typeof globalThis.document === "undefined" ? { "user-agent": `didis-client/${VERSION}` } : {};
17
36
  const enc = (s) => encodeURIComponent(s.trim());
18
37
  export class DidisClient {
19
38
  baseUrl;
20
39
  adminToken;
21
40
  timeoutMs;
22
41
  fetchImpl;
42
+ maxRetries;
43
+ maxWaitMs;
23
44
  constructor(options = {}) {
24
45
  const o = typeof options === "string" ? { baseUrl: options } : options;
25
46
  this.baseUrl = (o.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
26
47
  this.adminToken = o.adminToken;
27
48
  this.timeoutMs = o.timeoutMs ?? 30_000;
28
49
  this.fetchImpl = o.fetch ?? fetch;
50
+ this.maxRetries = o.retry === false ? 0 : Math.max(0, Math.floor(o.retry?.maxRetries ?? 2));
51
+ this.maxWaitMs = o.retry === false ? 0 : Math.max(0, o.retry?.maxWaitMs ?? 10_000);
52
+ }
53
+ /**
54
+ * Waits out a 429/503 refusal when the server says how long, within the configured bounds.
55
+ * A small random delay spreads out many clients that were refused at the same moment.
56
+ */
57
+ async backoff(attempt, status, retryAfter) {
58
+ if (attempt >= this.maxRetries || (status !== 429 && status !== 503))
59
+ return false;
60
+ const wait = retryAfterMs(retryAfter);
61
+ if (wait === null || wait > this.maxWaitMs)
62
+ return false;
63
+ await sleep(wait + Math.floor(Math.random() * 250));
64
+ return true;
29
65
  }
30
66
  async request(method, path, body, headers = {}) {
67
+ for (let attempt = 0;; attempt++) {
68
+ try {
69
+ return await this.once(method, path, body, headers);
70
+ }
71
+ catch (e) {
72
+ if (!(e instanceof DidisError) || !(await this.backoff(attempt, e.status, e.problem.retryAfter ?? null)))
73
+ throw e;
74
+ }
75
+ }
76
+ }
77
+ async once(method, path, body, headers = {}) {
31
78
  const controller = new AbortController();
32
79
  const timer = setTimeout(() => controller.abort(), this.timeoutMs);
33
80
  try {
34
81
  const res = await this.fetchImpl(`${this.baseUrl}${path}`, {
35
82
  method,
36
83
  signal: controller.signal,
37
- headers: { accept: "application/json", ...(body !== undefined ? { "content-type": "application/json" } : {}), ...headers },
84
+ headers: { ...SDK_HEADERS, accept: "application/json", ...(body !== undefined ? { "content-type": "application/json" } : {}), ...headers },
38
85
  body: body !== undefined ? JSON.stringify(body) : undefined,
39
86
  });
40
87
  const text = await res.text();
@@ -79,7 +126,7 @@ export class DidisClient {
79
126
  /** Live resolution over server-sent events. */
80
127
  async *stream(did, opts = { noCache: true }) {
81
128
  const res = await this.fetchImpl(`${this.baseUrl}/v1/stream/${enc(did)}${opts.noCache ? "?noCache=true" : ""}`, {
82
- headers: { accept: "text/event-stream" },
129
+ headers: { ...SDK_HEADERS, accept: "text/event-stream" },
83
130
  });
84
131
  if (!res.ok || !res.body)
85
132
  throw new DidisError(res.status, { type: "about:blank", title: `HTTP ${res.status}`, detail: "stream failed" });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@didis/client",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
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",