@zadq/x402 0.2.0 → 0.3.0

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.
@@ -17,44 +17,131 @@
17
17
  // transaction hash and height the chain published anyway — never the
18
18
  // revocation value, never the proof.
19
19
  //
20
- // CONTRACT POINT: the relay's path, answer shapes and reason
21
- // codes handled in `submitSelfRevocation`, and the name of the material the
22
- // issuer serves with a witness answer (`self_revoke_context`, holder.ts),
23
- // are restated from MintID's relay contract v0 and holder-library rule and
24
- // are to be confirmed against MintID's self-contained attachment on the
25
- // card that tracks this integration, before the first live run.
20
+ // The contracts, as MintID delivered them on 2026-10-07 (cited by name):
21
+ // the self-revocation privacy relay v0, provisional 0.1.0 (`POST
22
+ // /v1/self-revocations`, anonymous, no CORS, the closed reason vocabulary,
23
+ // `Retry-After` per answer); the holder library's `self_revoke` op (the
24
+ // relay's body is its `message`, unchanged); the chain's issuer-scoped
25
+ // `RecordedNullifiers` read (chain.ts); and the published effect bound per
26
+ // path (the network's revocation rule): a recorded self-revocation is
27
+ // effective everywhere within G + A + K = 245 s of the chain recording it.
26
28
  import { HolderError, HolderStepError } from "./errors.js";
27
29
  import { readRecordedNullifiers } from "./chain.js";
28
30
  /** The relay's submission path (relay contract v0). */
29
31
  export const RELAY_SUBMIT_PATH = '/v1/self-revocations';
32
+ /** The relay contract this module implements (`X-Relay-Api-Version` on every answer). */
33
+ export const RELAY_API_VERSION = '0.1.0';
34
+ /** The relay's body cap: 16 KiB. A larger body is refused here, before it travels. */
35
+ export const RELAY_BODY_CAP_BYTES = 16 * 1024;
30
36
  /** Environment variables the middleware host is configured with. */
31
37
  export const RELAY_URL_ENV = 'MINTID_RELAY_URL';
32
38
  export const CHAIN_RPC_ENV = 'MINTID_CHAIN_RPC';
33
39
  export const CHAIN_ID_ENV = 'MINTID_CHAIN_ID';
40
+ /**
41
+ * The relay's closed reason vocabulary (relay contract v0 0.1.0): its own
42
+ * codes, the shape and lookup codes, the proof-check codes and the chain's
43
+ * `selfrevoke` codes (any other chain code arrives as `chain_refused`). An
44
+ * answer naming anything else is read as `unexpected_answer`.
45
+ */
46
+ export const RELAY_REASON_CODES = [
47
+ // the relay's own
48
+ 'request_too_large',
49
+ 'malformed_json',
50
+ 'duplicate_submission',
51
+ 'busy',
52
+ 'chain_unavailable',
53
+ 'inclusion_timeout',
54
+ 'chain_refused',
55
+ 'proof_refused',
56
+ 'not_found',
57
+ 'method_not_allowed',
58
+ 'internal_error',
59
+ // shape and lookups
60
+ 'proof_system_version',
61
+ 'msg_shape',
62
+ 'nullifier',
63
+ 'nullifier_recorded',
64
+ 'unknown_root',
65
+ // the proof check
66
+ 'wire_version',
67
+ 'schema',
68
+ 'wire_shape',
69
+ 'path_depth',
70
+ 'anchor_definition',
71
+ 'anchor_encryption_key',
72
+ 'definition_shape',
73
+ 'encoding',
74
+ 'identity_point',
75
+ 'bbs_schnorr',
76
+ 'bbs_pairing',
77
+ 'acc_schnorr',
78
+ 'acc_pairing',
79
+ 've_c1',
80
+ 've_c2',
81
+ // the chain (codespace selfrevoke, codes 30–36)
82
+ 'selfrevoke_disabled',
83
+ 'block_cap',
84
+ 'issuer_state',
85
+ 'issuer_cap',
86
+ 'pending_cap',
87
+ 'tx_shape',
88
+ 'params',
89
+ ];
90
+ /** True for a code of the relay's closed vocabulary. */
91
+ export function isRelayReasonCode(value) {
92
+ return typeof value === 'string' && RELAY_REASON_CODES.includes(value);
93
+ }
94
+ /**
95
+ * The contract's `Retry-After` table, by code — used only when an answer
96
+ * that should carry the header does not (the header, when present, wins):
97
+ * `429 busy` 2 or 5 s, `503 chain_unavailable` 5 or 10 s, `503 block_cap` /
98
+ * `issuer_cap` 10 s, `503 busy` 10 s, `503 pending_cap` 60 s. The fallback
99
+ * takes the longer value of each row.
100
+ */
101
+ export const RELAY_RETRY_AFTER_SECONDS = {
102
+ busy: 10,
103
+ chain_unavailable: 10,
104
+ block_cap: 10,
105
+ issuer_cap: 10,
106
+ pending_cap: 60,
107
+ };
108
+ /** A relay that cannot be reached is tried again after this long. */
109
+ export const RELAY_UNREACHABLE_RETRY_SECONDS = 5;
110
+ /** The kill switch's submission attempts at one relay (the primitive's default is one). */
111
+ export const DEFAULT_RELAY_ATTEMPTS = 3;
112
+ /** No single wait is longer than the contract's longest `Retry-After` (pending_cap). */
113
+ export const MAX_RETRY_AFTER_MS = 60_000;
34
114
  /** A random delay before the revocation is built and sent: up to a minute by default. */
35
115
  export const DEFAULT_JITTER_MS = 60_000;
36
116
  /** The confirmation read: about one block, in a few short reads. */
37
117
  export const CONFIRM_ATTEMPTS = 4;
38
118
  export const CONFIRM_INTERVAL_MS = 750;
39
119
  /**
40
- * The published effect bound, restated from the substrate's operational
41
- * rules: a status root every 30 s, a root accepted for at most 180 s (a
42
- * presentation against an older root is refused, so a recorded revocation
43
- * is effective everywhere within that bound once the issuer folds it at
44
- * its next root); a revoked principal's agents follow over the next roots,
45
- * about four minutes.
120
+ * The published effect bound (the network's revocation rule, per path; the
121
+ * same parameters on production and the public testnet): a holder
122
+ * self-revocation — the kill switch — is effective everywhere within
123
+ * G + A + K = 245 s of the chain recording it (fold grace 60 s, a root
124
+ * accepted for at most 180 s, 5 s of future skew; a root every 30 s). A
125
+ * revoked principal's agents follow within 7·H + A + K = 395 s of the root
126
+ * that carries the trigger. The bound is the guarantee: a measured figure
127
+ * is only ever quoted next to it.
46
128
  */
47
129
  export const EFFECT_BOUND = {
48
- statusRootSeconds: 30,
49
- rootAcceptedSeconds: 180,
50
- agentCascadeSeconds: 240,
130
+ boundSeconds: 245,
131
+ countedFrom: 'recorded_on_chain',
132
+ principalCascadeSeconds: 395,
133
+ rootPeriodSeconds: 30,
134
+ acceptanceWindowSeconds: 180,
135
+ futureSkewSeconds: 5,
136
+ foldGraceSeconds: 60,
51
137
  };
52
- /** A relay refusal: the contract's bare reason code and stage, nothing of the message. */
138
+ /** A relay refusal: the bare reason code and stage, nothing of the message. */
53
139
  export class RelayRefusedError extends HolderError {
54
140
  status;
55
141
  code;
142
+ /** `client`: decided on this side (unreachable, outside the contract, refused before sending). */
56
143
  stage;
57
- /** Worth a retry, here or at another relay (`busy`, `chain_unavailable`, caps). */
144
+ /** Worth a retry, here or at another relay (`busy`, `chain_unavailable`, caps, unreachable). */
58
145
  retryable;
59
146
  retryAfterMs;
60
147
  constructor(status, code, stage, retryAfterMs) {
@@ -62,22 +149,57 @@ export class RelayRefusedError extends HolderError {
62
149
  this.status = status;
63
150
  this.code = code;
64
151
  this.stage = stage;
65
- this.retryable = status === 429 || status === 503;
152
+ this.retryable = status === 429 || status === 503 || code === 'relay_unreachable';
66
153
  this.retryAfterMs = retryAfterMs;
67
154
  }
68
155
  }
156
+ const HEX = /^([0-9a-f]{2})*$/;
157
+ const hexOf = (v, min, max) => typeof v === 'string' && HEX.test(v) && v.length >= min && v.length <= max;
158
+ /** The relay's `Message` schema (five fields), checked before anything travels. */
159
+ function messageShapeOk(m) {
160
+ return (hexOf(m.issuer_id, 64, 64) &&
161
+ hexOf(m.nullifier, 64, 64) &&
162
+ !/^0+$/.test(m.nullifier) &&
163
+ Number.isSafeInteger(m.status_epoch) &&
164
+ m.status_epoch >= 0 &&
165
+ hexOf(m.zk_authorization_proof, 2, 9216) &&
166
+ m.proof_system_version === '01');
167
+ }
69
168
  /**
70
169
  * Hands the self-revocation to the privacy relay: `{message}` with the five
71
170
  * message fields only, no header beyond the content type. The relay checks
72
- * the proof with the chain's rules, pays for the transaction and submits
73
- * it; this side signs nothing and holds no account. `RelayRefusedError` on
74
- * a refusal; every accepted answer (recorded, queued, already recorded,
75
- * already in flight, broadcast but not yet seen) still needs the chain read.
171
+ * the proof against the issuer's anchored root, pays for the transaction
172
+ * and submits it; this side signs nothing and holds no account.
173
+ * `RelayRefusedError` on a refusal (the code from the closed vocabulary);
174
+ * every accepted answer (recorded, queued, already recorded, already in
175
+ * flight, broadcast but not yet seen) still needs the chain read. With
176
+ * `attempts` above one, a retryable refusal (429, 503, unreachable) is
177
+ * tried again after the relay's `Retry-After` (or the contract's table).
76
178
  */
77
179
  export async function submitSelfRevocation(relayUrl, revocation, options = {}) {
180
+ const attempts = Math.max(1, options.attempts ?? 1);
181
+ const sleep = options.sleep ?? defaultSleep;
182
+ for (let attempt = 1;; attempt++) {
183
+ try {
184
+ return await submitOnce(relayUrl, revocation, options);
185
+ }
186
+ catch (err) {
187
+ if (!(err instanceof RelayRefusedError) || !err.retryable || attempt >= attempts)
188
+ throw err;
189
+ await sleep(Math.min(MAX_RETRY_AFTER_MS, err.retryAfterMs ??
190
+ 1000 *
191
+ (err.code === 'relay_unreachable'
192
+ ? RELAY_UNREACHABLE_RETRY_SECONDS
193
+ : (RELAY_RETRY_AFTER_SECONDS[err.code] ?? RELAY_UNREACHABLE_RETRY_SECONDS))));
194
+ }
195
+ }
196
+ }
197
+ async function submitOnce(relayUrl, revocation, options) {
78
198
  const fetchImpl = options.fetch ?? fetch;
79
199
  const m = revocation.message;
80
- const body = {
200
+ if (!messageShapeOk(m))
201
+ throw new RelayRefusedError(0, 'msg_shape', 'client');
202
+ const body = JSON.stringify({
81
203
  message: {
82
204
  issuer_id: m.issuer_id,
83
205
  nullifier: m.nullifier,
@@ -85,28 +207,33 @@ export async function submitSelfRevocation(relayUrl, revocation, options = {}) {
85
207
  zk_authorization_proof: m.zk_authorization_proof,
86
208
  proof_system_version: m.proof_system_version,
87
209
  },
88
- };
210
+ });
211
+ if (Buffer.byteLength(body) > RELAY_BODY_CAP_BYTES) {
212
+ throw new RelayRefusedError(0, 'request_too_large', 'client');
213
+ }
89
214
  let response;
90
215
  try {
91
216
  response = await fetchImpl(relayUrl.replace(/\/+$/, '') + RELAY_SUBMIT_PATH, {
92
217
  method: 'POST',
93
218
  headers: { 'content-type': 'application/json' },
94
- body: JSON.stringify(body),
219
+ body,
95
220
  signal: AbortSignal.timeout(options.timeoutMs ?? 60_000),
96
221
  });
97
222
  }
98
223
  catch {
99
- throw new RelayRefusedError(0, 'relay_unreachable', 'relay');
224
+ throw new RelayRefusedError(0, 'relay_unreachable', 'client');
100
225
  }
101
226
  let answer = {};
102
227
  try {
103
228
  const text = await response.text();
104
- answer = text ? JSON.parse(text) : {};
229
+ const parsed = text ? JSON.parse(text) : {};
230
+ if (typeof parsed === 'object' && parsed !== null)
231
+ answer = parsed;
105
232
  }
106
233
  catch {
107
234
  answer = {};
108
235
  }
109
- const error = typeof answer['error'] === 'string' ? answer['error'] : '';
236
+ const error = isRelayReasonCode(answer['error']) ? answer['error'] : undefined;
110
237
  switch (response.status) {
111
238
  case 200:
112
239
  if (answer['status'] === 'recorded') {
@@ -115,29 +242,40 @@ export async function submitSelfRevocation(relayUrl, revocation, options = {}) {
115
242
  out.txHash = answer['tx_hash'];
116
243
  if (typeof answer['height'] === 'number')
117
244
  out.height = answer['height'];
245
+ const state = answer['nullifier_state'];
246
+ if (state === 'pending' || state === 'folded')
247
+ out.relayNullifierState = state;
118
248
  return out;
119
249
  }
120
250
  break;
121
251
  case 202:
122
- return { outcome: 'queued' };
252
+ if (answer['status'] === 'queued')
253
+ return { outcome: 'queued' };
254
+ break;
123
255
  case 409:
124
256
  if (error === 'nullifier_recorded' || error === 'duplicate_submission') {
125
257
  return { outcome: error };
126
258
  }
127
259
  break;
128
260
  case 504:
129
- return { outcome: 'inclusion_timeout' };
261
+ if (error === 'inclusion_timeout')
262
+ return { outcome: 'inclusion_timeout' };
263
+ break;
130
264
  }
131
265
  const retryAfter = Number(response.headers.get('retry-after') ?? '');
132
- throw new RelayRefusedError(response.status, error || 'unexpected_answer', typeof answer['stage'] === 'string' ? answer['stage'] : 'relay', Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter * 1000 : undefined);
266
+ const stage = answer['stage'];
267
+ if (!error) {
268
+ throw new RelayRefusedError(response.status, 'unexpected_answer', 'client');
269
+ }
270
+ throw new RelayRefusedError(response.status, error, stage === 'precheck' || stage === 'chain' ? stage : 'relay', Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter * 1000 : undefined);
133
271
  }
134
272
  /**
135
- * Asks a node of the chain whether the revocation is there — for about one
136
- * block — reading the issuer's recorded set and finding the holder's own
137
- * value locally, so the node never learns which one. `pending` once
138
- * recorded, `folded` once the issuer's next root has folded it; otherwise
139
- * `unconfirmed` with the reason (no node named, node unreachable, not
140
- * recorded yet).
273
+ * Asks a node of the chain whether the revocation is there, reading the
274
+ * issuer's recorded set and finding the holder's own value locally, so the
275
+ * node never learns which one. `pending` once recorded, `folded` once the
276
+ * issuer has folded it (or, after an earlier sighting, once the chain has
277
+ * pruned it); otherwise `unconfirmed` with the reason (no node named, node
278
+ * unreachable, not recorded yet). The relay's answer never enters here.
141
279
  */
142
280
  export async function confirmSelfRevocation(rpcUrl, revocation, options = {}) {
143
281
  if (!rpcUrl)
@@ -146,6 +284,7 @@ export async function confirmSelfRevocation(rpcUrl, revocation, options = {}) {
146
284
  const interval = options.intervalMs ?? CONFIRM_INTERVAL_MS;
147
285
  const sleep = options.sleep ?? defaultSleep;
148
286
  const value = revocation.nullifier || revocation.message.nullifier;
287
+ let seen = options.seen;
149
288
  let reason = 'chain_unreachable';
150
289
  for (let attempt = 0; attempt < attempts; attempt++) {
151
290
  if (attempt > 0)
@@ -161,32 +300,55 @@ export async function confirmSelfRevocation(rpcUrl, revocation, options = {}) {
161
300
  continue;
162
301
  }
163
302
  const state = recorded.get(value);
164
- if (state)
303
+ if (state === 'folded')
165
304
  return { state };
305
+ if (state === 'pending') {
306
+ seen = 'pending';
307
+ if (options.until !== 'folded')
308
+ return { state };
309
+ continue;
310
+ }
311
+ if (seen)
312
+ return { state: 'folded', pruned: true };
166
313
  reason = 'not_recorded';
167
314
  }
315
+ if (seen === 'pending')
316
+ return { state: 'pending' };
168
317
  return { state: 'unconfirmed', reason };
169
318
  }
319
+ /**
320
+ * What the seller path answers for an agent credential: agent credentials
321
+ * are revoked through the verification service's agent kill route (the
322
+ * operator console's kill action), which keeps the operator view's state —
323
+ * never through this path.
324
+ */
325
+ export const AGENT_CREDENTIAL_REFUSAL = 'an agent credential: revoke it with the operator console kill action ' +
326
+ '(POST /v0/agent-operators/{ref}/agents/{id}/kill), not with the seller kill switch';
170
327
  /**
171
328
  * The product kill switch, end to end, where the key is:
172
329
  *
173
330
  * 1. a random delay, so the moment the seller decided is not the moment
174
- * the chain records (the relay shuffles what it holds; this adds the
175
- * seller's own slack);
176
- * 2. a fresh witness from the issuer, so the self-revocation is built
177
- * against the root the chain currently holds (the material comes with
178
- * the witness answer; no other call);
179
- * 3. the self-revocation, built by the holder library — from that moment
180
- * the holder refuses to present, also after a reload from custody;
181
- * 4. the submission through the privacy relay; a refusal for a root the
182
- * chain no longer holds rebuilds against the current one, once;
183
- * 5. the confirmation read on the chain, issuer-scoped.
331
+ * the chain records (the relay adds its own 0–6 s and shuffles each
332
+ * block's batch; this adds the seller's slack);
333
+ * 2. a fresh witness from the issuer, whose answer carries the root's
334
+ * `self_revoke_context` (no other call);
335
+ * 3. the holder library's `self_revoke` — `context = {chain_id,
336
+ * status_root, anchoring}` with `anchoring` the served context without
337
+ * `status_root` — and from that moment the holder refuses to present,
338
+ * also after a reload from custody;
339
+ * 4. the submission through the privacy relay, `Retry-After` honoured; a
340
+ * refusal for a root the chain no longer holds (`unknown_root`)
341
+ * rebuilds the context once from a fresh witness;
342
+ * 5. the confirmation read on the chain, issuer-scoped — the relay's
343
+ * answer is never the receipt.
184
344
  *
185
- * Configuration: `relayUrl` (or `MINTID_RELAY_URL`), `chainRpc` (or
186
- * `MINTID_CHAIN_RPC`; without it the report is `unconfirmed`,
187
- * `no_chain_endpoint`), `chainId` (or `MINTID_CHAIN_ID`, else the issuer's
188
- * public build information). The client never bypasses the relay to the
189
- * chain: it holds no account and signs nothing.
345
+ * The seller's own credential only: an agent credential is refused
346
+ * (`AGENT_CREDENTIAL_REFUSAL`). Configuration: `relayUrl` (or
347
+ * `MINTID_RELAY_URL`), `chainRpc` (or `MINTID_CHAIN_RPC`; without it the
348
+ * report is `unconfirmed`, `no_chain_endpoint`), `chainId` (or
349
+ * `MINTID_CHAIN_ID`, else the issuer's public build information). The
350
+ * client never bypasses the relay to the chain: it holds no account and
351
+ * signs nothing.
190
352
  */
191
353
  export async function revokeCredential(holder, options = {}) {
192
354
  const relayUrl = options.relayUrl ?? process.env[RELAY_URL_ENV];
@@ -195,6 +357,8 @@ export async function revokeCredential(holder, options = {}) {
195
357
  const chainRpc = options.chainRpc ?? process.env[CHAIN_RPC_ENV];
196
358
  const sleep = options.sleep ?? defaultSleep;
197
359
  const random = options.random ?? secureRandom;
360
+ if ((await holder.info()).profile === 'agent_v1')
361
+ throw new HolderError(AGENT_CREDENTIAL_REFUSAL);
198
362
  const jitter = options.jitterMs ?? DEFAULT_JITTER_MS;
199
363
  if (jitter > 0)
200
364
  await sleep(Math.floor(random() * jitter));
@@ -203,7 +367,12 @@ export async function revokeCredential(holder, options = {}) {
203
367
  await holder.refreshWitness();
204
368
  return holder.selfRevoke({ chain_id: chainId });
205
369
  };
206
- const submitOptions = options.relayFetch ? { fetch: options.relayFetch } : {};
370
+ const submitOptions = {
371
+ attempts: options.relayAttempts ?? DEFAULT_RELAY_ATTEMPTS,
372
+ sleep,
373
+ };
374
+ if (options.relayFetch)
375
+ submitOptions.fetch = options.relayFetch;
207
376
  let revocation = await build();
208
377
  let relay;
209
378
  try {
@@ -213,7 +382,7 @@ export async function revokeCredential(holder, options = {}) {
213
382
  if (!(err instanceof RelayRefusedError) || err.code !== 'unknown_root')
214
383
  throw err;
215
384
  // The root the message names has left the chain's ring: once more,
216
- // against the root it holds now.
385
+ // against the root the issuer serves now.
217
386
  revocation = await build();
218
387
  relay = await submitSelfRevocation(relayUrl, revocation, submitOptions);
219
388
  }
@@ -224,7 +393,18 @@ export async function revokeCredential(holder, options = {}) {
224
393
  confirmOptions.attempts = options.confirmAttempts;
225
394
  if (options.confirmIntervalMs !== undefined)
226
395
  confirmOptions.intervalMs = options.confirmIntervalMs;
227
- const confirmation = await confirmSelfRevocation(chainRpc, revocation, confirmOptions);
396
+ let confirmation = await confirmSelfRevocation(chainRpc, revocation, confirmOptions);
397
+ const waitMs = options.waitForFoldMs ?? 0;
398
+ if (confirmation.state === 'pending' && waitMs > 0) {
399
+ const interval = options.confirmIntervalMs ?? EFFECT_BOUND.rootPeriodSeconds * 1000;
400
+ confirmation = await confirmSelfRevocation(chainRpc, revocation, {
401
+ ...confirmOptions,
402
+ intervalMs: interval,
403
+ attempts: Math.max(1, Math.ceil(waitMs / interval)),
404
+ until: 'folded',
405
+ seen: 'pending',
406
+ });
407
+ }
228
408
  const report = {
229
409
  state: confirmation.state === 'folded'
230
410
  ? 'revoked'
@@ -1,5 +1,14 @@
1
1
  /** Environment variable locating the binary when no path is given. */
2
2
  export declare const HOLDER_BINARY_ENV = "MINTID_HOLDER_BIN";
3
+ /**
4
+ * The identity network's public release this module is built against, and
5
+ * what its holder binary's `version` op answers (the binary is built from
6
+ * that public tag; no release artefact is published).
7
+ */
8
+ export declare const HOLDER_RELEASE: {
9
+ readonly tag: "public-v2026-10-07.1";
10
+ readonly holder: "mintid-holder 0.4.0";
11
+ };
3
12
  /** The holder process protocol this module was written against (`version` op). */
4
13
  export declare const HOLDER_PROTOCOL: {
5
14
  readonly holder: "mintid-holder";
@@ -12,6 +12,15 @@ import { createInterface } from 'node:readline';
12
12
  import { HolderLibraryError } from "./errors.js";
13
13
  /** Environment variable locating the binary when no path is given. */
14
14
  export const HOLDER_BINARY_ENV = 'MINTID_HOLDER_BIN';
15
+ /**
16
+ * The identity network's public release this module is built against, and
17
+ * what its holder binary's `version` op answers (the binary is built from
18
+ * that public tag; no release artefact is published).
19
+ */
20
+ export const HOLDER_RELEASE = {
21
+ tag: 'public-v2026-10-07.1',
22
+ holder: 'mintid-holder 0.4.0',
23
+ };
15
24
  /** The holder process protocol this module was written against (`version` op). */
16
25
  export const HOLDER_PROTOCOL = { holder: 'mintid-holder', storageVersion: 1 };
17
26
  export class HolderProcess {
@@ -58,30 +58,66 @@ export type WitnessState = {
58
58
  statusRoot: string | null;
59
59
  };
60
60
  /**
61
- * The issuer's self-revocation material for a finalized root the credential
62
- * is a member of, exactly as the issuer serves it with every usable witness
63
- * (`self_revoke_context`): its anchored definition with the digest-leaf
64
- * witness, the profile's self-revocation encryption key with its witness,
65
- * and the root they name. Public material, the same for every holder of
66
- * the issuer — nothing per credential.
61
+ * One anchored leaf of the issuer's anchoring material, with its path to the
62
+ * served root (identity-status ring): the definition's digest leaf, or the
63
+ * self-revocation encryption key's. Public material.
67
64
  */
68
- export type SelfRevokeMaterial = Record<string, unknown> & {
69
- status_root?: string;
65
+ export type AnchoredLeafWitness = {
66
+ status: string;
67
+ ref: string;
68
+ value: string;
69
+ status_epoch: number;
70
+ root: string;
71
+ index: number;
72
+ path: string[];
73
+ };
74
+ /**
75
+ * The issuer's anchoring material a self-revocation is built against — the
76
+ * holder library's `self_revoke` `context.anchoring`: the anchored issuer
77
+ * definition with its digest-leaf witness, and the profile's
78
+ * self-revocation encryption key with its witness. Public, the same for
79
+ * every holder of the issuer — nothing per credential.
80
+ */
81
+ export type SelfRevokeAnchoring = {
82
+ issuer_definition: string;
83
+ definition_witness: AnchoredLeafWitness;
84
+ selfrevoke_encryption_key: {
85
+ version: number;
86
+ public_key: string;
87
+ };
88
+ selfrevoke_encryption_key_witness: AnchoredLeafWitness;
89
+ };
90
+ /**
91
+ * `self_revoke_context` exactly as the issuer serves it with a witness
92
+ * answer (`GET /v1/credentials/{id}/witness`): the anchoring plus the root
93
+ * it names (`status_root`, the served root). The issuer serves `null`
94
+ * unless the witness is usable and the anchoring names exactly that root;
95
+ * for an agent credential it is the agent anchoring, for an identity
96
+ * credential the identity anchoring — the route picks the right one.
97
+ */
98
+ export type SelfRevokeMaterial = SelfRevokeAnchoring & {
99
+ status_root: string;
70
100
  };
71
101
  /**
72
- * What a self-revocation needs: the chain id and the issuer's anchoring
73
- * material for a finalized root the credential is a member of. Omitted, the
74
- * material cached by the last witness refresh is used.
102
+ * What a self-revocation needs: the network's chain id and the issuer's
103
+ * anchoring for a finalized root the credential is a member of. Omitted,
104
+ * the material cached by the last witness refresh is used. A served
105
+ * `self_revoke_context` may be passed as is: its `status_root` is moved out
106
+ * of the anchoring, where the holder library does not take it.
75
107
  */
76
108
  export type SelfRevokeContext = {
77
109
  chain_id: string;
78
- anchoring?: SelfRevokeMaterial;
110
+ anchoring?: SelfRevokeAnchoring | SelfRevokeMaterial;
111
+ /** Optional; when given it must equal the root both witnesses name. */
79
112
  status_root?: string;
80
113
  };
81
114
  /**
82
- * The self-revocation message the holder library builds for a privacy
83
- * relay: it names neither the holder nor the credential. Building it
84
- * revokes nothing by itself — a relay submits it and the chain records it.
115
+ * The holder library's `self_revoke` output: `message` is the relay's body
116
+ * (`MsgHolderSelfRevoke` fields 1–5) and names neither the holder nor the
117
+ * credential; `nullifier` and `ciphertext` are what the holder keeps (the
118
+ * nullifier is how the holder finds its own record in the chain read).
119
+ * Building it revokes nothing by itself — a relay submits it and the chain
120
+ * records it — but from then on the holder refuses to present.
85
121
  */
86
122
  export type SelfRevocation = {
87
123
  message: {
@@ -89,6 +125,7 @@ export type SelfRevocation = {
89
125
  nullifier: string;
90
126
  status_epoch: number;
91
127
  zk_authorization_proof: string;
128
+ /** `"01"` in relay contract v0 0.1.0. */
92
129
  proof_system_version: string;
93
130
  };
94
131
  nullifier: string;
package/dist/index.d.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  export { BRAND, VERDICT_EXTENSION_KEY } from './brand.js';
2
- export { CHAINS, DEFAULT_TIMEOUT_MS, DEPOSIT_DECLARATION_STATES, DEPOSIT_LEVELS, canonicalPayTo, canonicalSubject, canonicalVerdictPayload, fetchVerdict, freshness, isChain, keyIdFor, readDepositDeclaration, verifyVerdictSignature, type Chain, type DepositDeclaration, type DepositDeclarationState, type DepositLevel, type Freshness, type SignatureCheck, type Verdict, type VerdictOutcome, type VerdictReason, type VerdictSubject, } from './verdict.js';
3
- export { IDENTITY_POLICY_EXTENSION, MINTID_IDENTITY_EXTENSION, PERIMETER_EXTENSION, readIdentityPolicy, readMintidIdentity, readPerimeterInvitation, withIdentityPolicy, withMintidIdentity, withPerimeterInvitation, withSellerIdentity, type ClaimPolicyEntry, type CredentialState, type IdentityPolicy, type MintidIdentityPolicy, type PaymentRequiredBody, type PerimeterInvitation, type SellerIdentity, } from './extension.js';
4
- export { X_PAYMENT_HEADER, X_PAYMENT_IDENTITY_HEADER, attachIdentityProof, readIdentityProof, type IdentityProof, } from './header.js';
2
+ export { BADGE_CLIENT, CLIENT_CLASSES, CLIENT_HEADER, SDK_CLIENT, SDK_VERSION, clientHeaders, clientUserAgent, runtimeDeclaresClient, type ClientClass, type ClientDeclaration, } from './client.js';
3
+ export { CHAINS, DEFAULT_TIMEOUT_MS, DEPOSIT_DECLARATION_STATES, DEPOSIT_LEVELS, VERDICT_OUTCOMES, VERDICT_REASONS, canonicalPayTo, canonicalSubject, canonicalVerdictPayload, fetchVerdict, freshness, isChain, isVerdict, keyIdFor, readDepositDeclaration, subjectMatches, verifyVerdictSignature, type Chain, type DepositDeclaration, type DepositDeclarationState, type DepositLevel, type Freshness, type SignatureCheck, type Verdict, type VerdictOutcome, type VerdictReason, type VerdictSubject, } from './verdict.js';
4
+ export { IDENTITY_POLICY_EXTENSION, canonicalInlineVerdict, MINTID_IDENTITY_EXTENSION, PERIMETER_EXTENSION, readIdentityPolicy, readMintidIdentity, readPerimeterInvitation, withIdentityPolicy, withMintidIdentity, withPerimeterInvitation, withSellerIdentity, type ClaimPolicyEntry, type CredentialState, type IdentityPolicy, type MintidIdentityPolicy, type PaymentRequiredBody, type PerimeterInvitation, type SellerIdentity, } from './extension.js';
5
+ export { X_PAYMENT_HEADER, X_PAYMENT_IDENTITY_HEADER, attachIdentityProof, identityProofHeaderValue, readIdentityProof, type IdentityProof, } from './header.js';
5
6
  export { ATTESTATION_RECEIPT_FORMAT, attestationReceiptProof, canonicalJson, isReceipt, readAttestationReceipt, receiptSigningInput, verifyAttestationReceipt, type AttestationReceipt, type ReceiptCheck, } from './receipt.js';
6
7
  export { UNKNOWN_SIGNAL, createVerificationClient, toSignal, type VerificationClient, type VerificationClientOptions, type VerificationSignal, } from './explorer.js';
7
- export { fetchVerificationKeys, findKey, type VerificationKey } from './keys.js';
8
+ export { fetchVerificationKeys, findKey, keyFor, type VerificationKey } from './keys.js';
9
+ export { NO_SIGNAL_TITLE, badgeLabel, badgeState, badgeTitle, embedVerificationBadge, renderBadge, type BadgeOptions, type BadgeState, } from './badge.js';
8
10
  export { SIGNATURE_HEADER, SIGNATURE_KEY_HEADER, WEBHOOK_VERSION, WEBHOOK_VERSION_HEADER, parseVerdictEvent, verifyWebhookSignature, webhookKeyId, type VerdictEvent, } from './webhook.js';
9
11
  export * from './holder/index.js';
package/dist/index.js CHANGED
@@ -7,12 +7,14 @@
7
7
  // (cited by name): identity policy in a `PaymentRequired` extension, proof alongside
8
8
  // the `X-PAYMENT` header.
9
9
  export { BRAND, VERDICT_EXTENSION_KEY } from "./brand.js";
10
- export { CHAINS, DEFAULT_TIMEOUT_MS, DEPOSIT_DECLARATION_STATES, DEPOSIT_LEVELS, canonicalPayTo, canonicalSubject, canonicalVerdictPayload, fetchVerdict, freshness, isChain, keyIdFor, readDepositDeclaration, verifyVerdictSignature, } from "./verdict.js";
11
- export { IDENTITY_POLICY_EXTENSION, MINTID_IDENTITY_EXTENSION, PERIMETER_EXTENSION, readIdentityPolicy, readMintidIdentity, readPerimeterInvitation, withIdentityPolicy, withMintidIdentity, withPerimeterInvitation, withSellerIdentity, } from "./extension.js";
12
- export { X_PAYMENT_HEADER, X_PAYMENT_IDENTITY_HEADER, attachIdentityProof, readIdentityProof, } from "./header.js";
10
+ export { BADGE_CLIENT, CLIENT_CLASSES, CLIENT_HEADER, SDK_CLIENT, SDK_VERSION, clientHeaders, clientUserAgent, runtimeDeclaresClient, } from "./client.js";
11
+ export { CHAINS, DEFAULT_TIMEOUT_MS, DEPOSIT_DECLARATION_STATES, DEPOSIT_LEVELS, VERDICT_OUTCOMES, VERDICT_REASONS, canonicalPayTo, canonicalSubject, canonicalVerdictPayload, fetchVerdict, freshness, isChain, isVerdict, keyIdFor, readDepositDeclaration, subjectMatches, verifyVerdictSignature, } from "./verdict.js";
12
+ export { IDENTITY_POLICY_EXTENSION, canonicalInlineVerdict, MINTID_IDENTITY_EXTENSION, PERIMETER_EXTENSION, readIdentityPolicy, readMintidIdentity, readPerimeterInvitation, withIdentityPolicy, withMintidIdentity, withPerimeterInvitation, withSellerIdentity, } from "./extension.js";
13
+ export { X_PAYMENT_HEADER, X_PAYMENT_IDENTITY_HEADER, attachIdentityProof, identityProofHeaderValue, readIdentityProof, } from "./header.js";
13
14
  export { ATTESTATION_RECEIPT_FORMAT, attestationReceiptProof, canonicalJson, isReceipt, readAttestationReceipt, receiptSigningInput, verifyAttestationReceipt, } from "./receipt.js";
14
15
  export { UNKNOWN_SIGNAL, createVerificationClient, toSignal, } from "./explorer.js";
15
- export { fetchVerificationKeys, findKey } from "./keys.js";
16
+ export { fetchVerificationKeys, findKey, keyFor } from "./keys.js";
17
+ export { NO_SIGNAL_TITLE, badgeLabel, badgeState, badgeTitle, embedVerificationBadge, renderBadge, } from "./badge.js";
16
18
  export { SIGNATURE_HEADER, SIGNATURE_KEY_HEADER, WEBHOOK_VERSION, WEBHOOK_VERSION_HEADER, parseVerdictEvent, verifyWebhookSignature, webhookKeyId, } from "./webhook.js";
17
19
  // The seller-side holder module (Node-only: it spawns MintID's holder
18
20
  // library process). Import it from './holder/index.ts' directly in a
package/dist/keys.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import type { ClientDeclaration } from './client.js';
2
+ import { type Verdict } from './verdict.js';
1
3
  /** One published verification key. */
2
4
  export type VerificationKey = {
3
5
  /** First 8 bytes of the public key's SHA-256, hex — matches `Verdict.keyId`. */
@@ -5,6 +7,12 @@ export type VerificationKey = {
5
7
  algorithm: 'Ed25519';
6
8
  /** Raw 32-byte public key, base64. */
7
9
  publicKey: string;
10
+ /**
11
+ * `current` signs new verdicts; `previous` keys stay published for at least
12
+ * the re-check window after a rotation, so verdicts signed before it keep
13
+ * verifying. Select by `keyId` (`findKey`), never by position or status.
14
+ */
15
+ status?: 'current' | 'previous';
8
16
  };
9
17
  /**
10
18
  * Fetches the published verification keys. Resolves to `undefined` — never
@@ -12,6 +20,19 @@ export type VerificationKey = {
12
20
  */
13
21
  export declare function fetchVerificationKeys(baseUrl: string, options?: {
14
22
  timeoutMs?: number;
23
+ client?: ClientDeclaration;
15
24
  }): Promise<VerificationKey[] | undefined>;
16
- /** Picks the key a verdict was signed with, by `keyId`. */
25
+ /**
26
+ * Picks the key a verdict was signed with, by `keyId` — never by position:
27
+ * after a rotation the `previous` key is still the right one for a verdict
28
+ * signed before it.
29
+ */
17
30
  export declare function findKey(keys: readonly VerificationKey[] | undefined, keyId: string | undefined): VerificationKey | undefined;
31
+ /**
32
+ * The published key a verdict names (`verdict.keyId`), as its base64 public
33
+ * key — `undefined` when the verdict names none or a key that is not
34
+ * published. A positive claim that names an unpublished key cannot be
35
+ * checked against the published set: a consumer that holds the set treats it
36
+ * as `invalid` (the explorer client does).
37
+ */
38
+ export declare function keyFor(verdict: Pick<Verdict, 'keyId'> | undefined, keys: readonly VerificationKey[] | undefined): string | undefined;