@zadq/x402 0.1.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.
@@ -1,5 +1,7 @@
1
1
  /** The chain query path of the issuer-scoped read. */
2
2
  export declare const RECORDED_NULLIFIERS_QUERY = "/identity.v1.selfrevoke.Query/RecordedNullifiers";
3
+ /** After how long the chain prunes a record (`retention_seconds`, both networks). */
4
+ export declare const RECORD_RETENTION_SECONDS = 3600;
3
5
  /** A recorded self-revocation's state on the chain, by revocation value (hex). */
4
6
  export type RecordedState = 'pending' | 'folded';
5
7
  export type ChainReadOptions = {
@@ -12,7 +14,10 @@ export declare function bytesField(field: number, value: Uint8Array): Uint8Array
12
14
  export declare function varintField(field: number, value: number): Uint8Array;
13
15
  export declare function hexToBytes(hex: string): Uint8Array;
14
16
  export declare function bytesToHex(bytes: Uint8Array): string;
15
- /** `QueryRecordedNullifiersRequest{issuer_id = 1}` — names no revocation value. */
17
+ /**
18
+ * `QueryRecordedNullifiersRequest{issuer_id = 1}` — `0a20‖issuer_id`; names
19
+ * no revocation value. The issuer id is exactly 32 bytes.
20
+ */
16
21
  export declare function encodeIssuerQuery(issuerIdHex: string): Uint8Array;
17
22
  /**
18
23
  * `QueryRecordedNullifiersResponse{repeated RecordedNullifier records = 1}`
@@ -11,15 +11,25 @@
11
11
  // two protobuf shapes involved are small enough to encode and decode by
12
12
  // hand (varints and length-delimited fields only).
13
13
  //
14
- // CONTRACT POINT: the query path and the field numbers below
15
- // (request: issuer id = field 1; each record: version = 1, value = 2,
16
- // folded-in epoch = 3) are restated from MintID's chain query contract and
17
- // are to be confirmed against MintID's self-contained attachment on the
18
- // card that tracks this integration, before the first live run; they are
19
- // the only place to change.
14
+ // The contract, as MintID delivered it on 2026-10-07 (the chain's
15
+ // issuer-scoped recorded-revocations query, cited by name): service
16
+ // `identity.v1.selfrevoke.Query`, method `RecordedNullifiers`, over CometBFT
17
+ // `abci_query` with `prove: false` (no proven variant exists: the read is as
18
+ // trustworthy as the node — run your own for more). Request
19
+ // `{bytes issuer_id = 1}`, exactly 32 bytes, so `data` is `0a20‖issuer_id`;
20
+ // the answer's `result.response.value` is base64 of
21
+ // `{repeated RecordedNullifier records = 1; uint64 version = 2; uint64 acked = 3}`
22
+ // with `RecordedNullifier{uint64 version = 1; bytes nullifier = 2;
23
+ // uint64 folded_in_epoch = 3}` (0 while pending). Every unpruned record of
24
+ // the issuer comes back; a record is pruned after `retention_seconds`
25
+ // (3 600 s on both networks), so a value absent after it was seen is folded
26
+ // and pruned, not lost. The chain's `Nullifier` query would
27
+ // name the value to the node, and is not used here.
20
28
  import { HolderStepError } from "./errors.js";
21
29
  /** The chain query path of the issuer-scoped read. */
22
30
  export const RECORDED_NULLIFIERS_QUERY = '/identity.v1.selfrevoke.Query/RecordedNullifiers';
31
+ /** After how long the chain prunes a record (`retention_seconds`, both networks). */
32
+ export const RECORD_RETENTION_SECONDS = 3_600;
23
33
  function varint(value) {
24
34
  const out = [];
25
35
  let v = value;
@@ -46,9 +56,15 @@ export function hexToBytes(hex) {
46
56
  export function bytesToHex(bytes) {
47
57
  return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
48
58
  }
49
- /** `QueryRecordedNullifiersRequest{issuer_id = 1}` — names no revocation value. */
59
+ /**
60
+ * `QueryRecordedNullifiersRequest{issuer_id = 1}` — `0a20‖issuer_id`; names
61
+ * no revocation value. The issuer id is exactly 32 bytes.
62
+ */
50
63
  export function encodeIssuerQuery(issuerIdHex) {
51
- return bytesField(1, hexToBytes(issuerIdHex));
64
+ const id = hexToBytes(issuerIdHex);
65
+ if (id.length !== 32)
66
+ throw new HolderStepError('chain', 0, { error: 'issuer id is not 32 bytes' });
67
+ return bytesField(1, id);
52
68
  }
53
69
  function readVarint(data, pos) {
54
70
  let result = 0;
@@ -8,6 +8,8 @@ export declare const CUSTODY_KEY_ENV = "CREDENTIAL_CUSTODY_KEY";
8
8
  export declare const DASHBOARD_URL_ENV = "VERIFICATION_BASE_URL";
9
9
  export declare const DASHBOARD_SELLER_ENV = "VERIFICATION_SELLER_ID";
10
10
  export declare const DASHBOARD_SESSION_ENV = "VERIFICATION_SESSION";
11
+ /** `--wait`: the fold grace and two root periods — the issuer has folded by then. */
12
+ export declare const DEFAULT_WAIT_FOR_FOLD_MS: number;
11
13
  /** The credential state the seller's record takes at the dashboard service. */
12
14
  export declare const DASHBOARD_REVOKED_STATE = "self_revoked";
13
15
  export type KillSwitchEnv = Record<string, string | undefined>;
@@ -14,20 +14,25 @@
14
14
  //
15
15
  // Flags: --yes (required: the revocation is irreversible), --no-jitter or
16
16
  // --jitter-ms <n> (the random delay before the submission, up to a minute
17
- // by default), --report (tell the dashboard, with VERIFICATION_BASE_URL,
17
+ // by default), --wait or --wait-ms <n> (keep reading the chain after the
18
+ // record shows, until the issuer has folded it: two minutes by default),
19
+ // --report (tell the dashboard, with VERIFICATION_BASE_URL,
18
20
  // VERIFICATION_SELLER_ID and VERIFICATION_SESSION — the dashboard's short-lived session
19
21
  // credential — set). Nothing here logs; stdout carries the report only.
20
22
  import { pathToFileURL } from 'node:url';
23
+ import { clientHeaders } from "../client.js";
21
24
  import { custodyKeyFromHex, fileCustodyStore, sealedCustodyStore, } from "./custody.js";
22
25
  import { HolderError } from "./errors.js";
23
26
  import { SellerHolder } from "./holder.js";
24
- import { CHAIN_ID_ENV, CHAIN_RPC_ENV, RELAY_URL_ENV, RelayRefusedError, revokeCredential, } from "./killswitch.js";
27
+ import { CHAIN_ID_ENV, CHAIN_RPC_ENV, EFFECT_BOUND, RELAY_URL_ENV, RelayRefusedError, revokeCredential, } from "./killswitch.js";
25
28
  import { HOLDER_BINARY_ENV, HolderProcess } from "./process.js";
26
29
  export const CUSTODY_FILE_ENV = 'CREDENTIAL_CUSTODY_FILE';
27
30
  export const CUSTODY_KEY_ENV = 'CREDENTIAL_CUSTODY_KEY';
28
31
  export const DASHBOARD_URL_ENV = 'VERIFICATION_BASE_URL';
29
32
  export const DASHBOARD_SELLER_ENV = 'VERIFICATION_SELLER_ID';
30
33
  export const DASHBOARD_SESSION_ENV = 'VERIFICATION_SESSION';
34
+ /** `--wait`: the fold grace and two root periods — the issuer has folded by then. */
35
+ export const DEFAULT_WAIT_FOR_FOLD_MS = (EFFECT_BOUND.foldGraceSeconds + 2 * EFFECT_BOUND.rootPeriodSeconds) * 1000;
31
36
  /** The credential state the seller's record takes at the dashboard service. */
32
37
  export const DASHBOARD_REVOKED_STATE = 'self_revoked';
33
38
  const NEXT_STEP = 'Open the dashboard and press "Revoke my credential" → "My middleware revoked it" ' +
@@ -47,7 +52,8 @@ function value(argv, name) {
47
52
  */
48
53
  export async function runKillSwitch(argv, env, io, deps = {}) {
49
54
  if (!flag(argv, '--yes')) {
50
- io.err('This revokes your credential for good: from the next status root on it presents nowhere, ' +
55
+ io.err('This revokes your credential for good: within ' +
56
+ `${EFFECT_BOUND.boundSeconds} s of the chain recording it, it presents nowhere, ` +
51
57
  'and only a new verification gives you a fresh one. Run again with --yes.');
52
58
  return 1;
53
59
  }
@@ -90,6 +96,17 @@ export async function runKillSwitch(argv, env, io, deps = {}) {
90
96
  }
91
97
  options.jitterMs = ms;
92
98
  }
99
+ if (flag(argv, '--wait'))
100
+ options.waitForFoldMs = DEFAULT_WAIT_FOR_FOLD_MS;
101
+ const wait = value(argv, '--wait-ms');
102
+ if (wait !== undefined) {
103
+ const ms = Number(wait);
104
+ if (!Number.isFinite(ms) || ms < 0) {
105
+ io.err('--wait-ms takes a non-negative number of milliseconds.');
106
+ return 1;
107
+ }
108
+ options.waitForFoldMs = ms;
109
+ }
93
110
  const rpc = deps.rpc ?? HolderProcess.start(env[HOLDER_BINARY_ENV]);
94
111
  const fetchImpl = deps.fetch ?? fetch;
95
112
  let report;
@@ -152,7 +169,11 @@ export async function reportToDashboard(fetchImpl, baseUrl, sellerId, session) {
152
169
  try {
153
170
  const response = await fetchImpl(`${baseUrl.replace(/\/+$/, '')}/v0/sellers/${encodeURIComponent(sellerId)}/credential`, {
154
171
  method: 'POST',
155
- headers: { 'content-type': 'application/json', authorization: `Bearer ${session}` },
172
+ headers: {
173
+ ...clientHeaders(),
174
+ 'content-type': 'application/json',
175
+ authorization: `Bearer ${session}`,
176
+ },
156
177
  body: JSON.stringify({ state: DASHBOARD_REVOKED_STATE }),
157
178
  signal: AbortSignal.timeout(15_000),
158
179
  });
@@ -106,6 +106,14 @@ export declare class SellerHolder {
106
106
  /** Forget the credential inside the process (its secret with it). The custody store is untouched. */
107
107
  close(): Promise<void>;
108
108
  }
109
+ /**
110
+ * Reads a witness answer's `self_revoke_context` (issuer v1 witness route):
111
+ * the four anchoring members and `status_root`, which must name `root` —
112
+ * the root the witness was adopted on. `undefined` for `null`, for another
113
+ * root, or for anything not of that shape (the holder library would refuse
114
+ * it anyway; nothing is cached that cannot serve).
115
+ */
116
+ export declare function readSelfRevokeContext(value: unknown, root: string): SelfRevokeMaterial | undefined;
109
117
  /**
110
118
  * Reads the verification service's `POST` body as an issued challenge:
111
119
  * `{request, signature_hex}`, or the bare request. Returns `undefined` for
@@ -192,13 +192,10 @@ export class SellerHolder {
192
192
  });
193
193
  await this.persist();
194
194
  // The issuer serves the root's self-revocation material with the
195
- // witness: keep it, only when it names the adopted root.
196
- const material = body['self_revoke_context'];
197
- if (typeof material === 'object' &&
198
- material !== null &&
199
- material['status_root'] === String(root.root)) {
200
- this.material = material;
201
- }
195
+ // witness (`self_revoke_context`, null unless usable and anchored on the
196
+ // served root). Cached at every refresh, for the adopted root only: a
197
+ // context for another root, or none, clears what an older refresh left.
198
+ this.material = readSelfRevokeContext(body['self_revoke_context'], String(root.root));
202
199
  return {
203
200
  status: state,
204
201
  usable: true,
@@ -304,26 +301,95 @@ export class SellerHolder {
304
301
  * primitive under it.
305
302
  */
306
303
  async selfRevoke(context) {
307
- const anchoring = context.anchoring ?? this.material;
308
- if (!anchoring) {
304
+ const given = context.anchoring ?? this.material;
305
+ if (!given) {
309
306
  throw new HolderError('no self-revocation material: refresh the witness first, or pass the issuer anchoring');
310
307
  }
308
+ // `anchoring` is `self_revoke_context` without `status_root`; the root
309
+ // travels beside it (optional for the library, checked against both
310
+ // witnesses when given).
311
+ const { status_root: servedRoot, ...anchoring } = given;
311
312
  const full = { chain_id: context.chain_id, anchoring };
312
- const root = context.status_root ?? anchoring['status_root'];
313
+ const root = context.status_root ?? servedRoot;
313
314
  if (typeof root === 'string' && root !== '')
314
315
  full['status_root'] = root;
315
- const out = (await this.rpc.call('self_revoke', {
316
- holder: this.handle,
317
- context: full,
318
- }));
316
+ const out = await this.rpc.call('self_revoke', { holder: this.handle, context: full });
317
+ const revocation = readSelfRevocation(out);
319
318
  await this.persist();
320
- return out;
319
+ return revocation;
321
320
  }
322
321
  /** Forget the credential inside the process (its secret with it). The custody store is untouched. */
323
322
  async close() {
324
323
  await this.rpc.call('drop', { holder: this.handle });
325
324
  }
326
325
  }
326
+ const HEX = /^([0-9a-f]{2})*$/;
327
+ const isHex = (v, bytes) => typeof v === 'string' && HEX.test(v) && (bytes === undefined || v.length === bytes * 2);
328
+ function isLeafWitness(v) {
329
+ if (typeof v !== 'object' || v === null)
330
+ return false;
331
+ const w = v;
332
+ return (typeof w['status'] === 'string' &&
333
+ typeof w['status_epoch'] === 'number' &&
334
+ isHex(w['root']) &&
335
+ typeof w['index'] === 'number' &&
336
+ Array.isArray(w['path']) &&
337
+ w['path'].every((p) => isHex(p, 32)));
338
+ }
339
+ /**
340
+ * Reads a witness answer's `self_revoke_context` (issuer v1 witness route):
341
+ * the four anchoring members and `status_root`, which must name `root` —
342
+ * the root the witness was adopted on. `undefined` for `null`, for another
343
+ * root, or for anything not of that shape (the holder library would refuse
344
+ * it anyway; nothing is cached that cannot serve).
345
+ */
346
+ export function readSelfRevokeContext(value, root) {
347
+ if (typeof value !== 'object' || value === null)
348
+ return undefined;
349
+ const c = value;
350
+ const key = c['selfrevoke_encryption_key'];
351
+ if (c['status_root'] !== root ||
352
+ !isHex(c['issuer_definition']) ||
353
+ !isLeafWitness(c['definition_witness']) ||
354
+ !isLeafWitness(c['selfrevoke_encryption_key_witness']) ||
355
+ typeof key !== 'object' ||
356
+ key === null ||
357
+ typeof key['version'] !== 'number' ||
358
+ !isHex(key['public_key'], 48)) {
359
+ return undefined;
360
+ }
361
+ return value;
362
+ }
363
+ /**
364
+ * The library's `self_revoke` result as delivered: `{message (five
365
+ * fields), nullifier, ciphertext}`. `HolderError` on anything else — the
366
+ * holder has marked itself revoked by then, but nothing goes to a relay.
367
+ */
368
+ function readSelfRevocation(out) {
369
+ const m = out['message'];
370
+ if (typeof m !== 'object' ||
371
+ m === null ||
372
+ !isHex(m['issuer_id'], 32) ||
373
+ !isHex(m['nullifier'], 32) ||
374
+ typeof m['status_epoch'] !== 'number' ||
375
+ !isHex(m['zk_authorization_proof']) ||
376
+ typeof m['proof_system_version'] !== 'string' ||
377
+ !isHex(out['nullifier'], 32) ||
378
+ typeof out['ciphertext'] !== 'string') {
379
+ throw new HolderError('the holder library answered self_revoke with an unexpected shape');
380
+ }
381
+ return {
382
+ message: {
383
+ issuer_id: m['issuer_id'],
384
+ nullifier: m['nullifier'],
385
+ status_epoch: m['status_epoch'],
386
+ zk_authorization_proof: m['zk_authorization_proof'],
387
+ proof_system_version: m['proof_system_version'],
388
+ },
389
+ nullifier: out['nullifier'],
390
+ ciphertext: out['ciphertext'],
391
+ };
392
+ }
327
393
  /**
328
394
  * Reads the verification service's `POST` body as an issued challenge:
329
395
  * `{request, signature_hex}`, or the bare request. Returns `undefined` for
@@ -1,8 +1,8 @@
1
1
  export { CredentialRevokedError, HolderError, HolderLibraryError, HolderStepError, OfferError, } from './errors.js';
2
- export { HOLDER_BINARY_ENV, HOLDER_PROTOCOL, HolderProcess, type HolderParams, type HolderRpc, } from './process.js';
2
+ export { HOLDER_BINARY_ENV, HOLDER_PROTOCOL, HOLDER_RELEASE, HolderProcess, type HolderParams, type HolderRpc, } from './process.js';
3
3
  export { custodyKeyFromHex, fileCustodyStore, memoryCustodyStore, sealedCustodyStore, type CustodyKey, type CustodyStore, } from './custody.js';
4
- export { BUILD_INFO_PATH, CREDENTIAL_FORMAT, IDENTITY_CONFIGURATION_ID, METADATA_PATH, PRE_AUTHORIZED_GRANT, PRESENTATION_WINDOW_MS, PROOF_TYPE, SellerHolder, readCredentialOffer, readIssuedChallenge, type CredentialOffer, type Fetch, type HolderOptions, } from './holder.js';
5
- export type { ChallengeAnswer, ClaimPredicate, HolderInfo, IssuedChallenge, PresentationEnvelope, PresentationRequest, SelfRevocation, SelfRevokeContext, SelfRevokeMaterial, WitnessState, } from './types.js';
6
- export { CHAIN_ID_ENV, CHAIN_RPC_ENV, CONFIRM_ATTEMPTS, CONFIRM_INTERVAL_MS, DEFAULT_JITTER_MS, EFFECT_BOUND, RELAY_SUBMIT_PATH, RELAY_URL_ENV, RelayRefusedError, confirmSelfRevocation, revokeCredential, submitSelfRevocation, type Confirmation, type ConfirmOptions, type KillSwitchOptions, type KillSwitchReport, type RelayAnswer, type RelayOutcome, type SubmitOptions, } from './killswitch.js';
7
- export { RECORDED_NULLIFIERS_QUERY, decodeRecordedNullifiers, encodeIssuerQuery, readRecordedNullifiers, type ChainReadOptions, type RecordedState, } from './chain.js';
8
- export { CUSTODY_FILE_ENV, CUSTODY_KEY_ENV, DASHBOARD_REVOKED_STATE, DASHBOARD_SELLER_ENV, DASHBOARD_SESSION_ENV, DASHBOARD_URL_ENV, reportToDashboard, runKillSwitch, type CliReport, type KillSwitchDeps, type KillSwitchEnv, type KillSwitchIO, } from './cli.js';
4
+ export { BUILD_INFO_PATH, CREDENTIAL_FORMAT, IDENTITY_CONFIGURATION_ID, METADATA_PATH, PRE_AUTHORIZED_GRANT, PRESENTATION_WINDOW_MS, PROOF_TYPE, SellerHolder, readCredentialOffer, readIssuedChallenge, readSelfRevokeContext, type CredentialOffer, type Fetch, type HolderOptions, } from './holder.js';
5
+ export type { ChallengeAnswer, ClaimPredicate, HolderInfo, IssuedChallenge, PresentationEnvelope, PresentationRequest, AnchoredLeafWitness, SelfRevocation, SelfRevokeAnchoring, SelfRevokeContext, SelfRevokeMaterial, WitnessState, } from './types.js';
6
+ export { AGENT_CREDENTIAL_REFUSAL, CHAIN_ID_ENV, CHAIN_RPC_ENV, CONFIRM_ATTEMPTS, CONFIRM_INTERVAL_MS, DEFAULT_JITTER_MS, DEFAULT_RELAY_ATTEMPTS, EFFECT_BOUND, MAX_RETRY_AFTER_MS, RELAY_API_VERSION, RELAY_BODY_CAP_BYTES, RELAY_REASON_CODES, RELAY_RETRY_AFTER_SECONDS, RELAY_SUBMIT_PATH, RELAY_UNREACHABLE_RETRY_SECONDS, RELAY_URL_ENV, RelayRefusedError, confirmSelfRevocation, isRelayReasonCode, revokeCredential, submitSelfRevocation, type ClientReasonCode, type Confirmation, type ConfirmOptions, type KillSwitchOptions, type KillSwitchReport, type RelayAnswer, type RelayOutcome, type RelayReasonCode, type SubmitOptions, } from './killswitch.js';
7
+ export { RECORD_RETENTION_SECONDS, RECORDED_NULLIFIERS_QUERY, decodeRecordedNullifiers, encodeIssuerQuery, readRecordedNullifiers, type ChainReadOptions, type RecordedState, } from './chain.js';
8
+ export { CUSTODY_FILE_ENV, CUSTODY_KEY_ENV, DASHBOARD_REVOKED_STATE, DASHBOARD_SELLER_ENV, DASHBOARD_SESSION_ENV, DASHBOARD_URL_ENV, DEFAULT_WAIT_FOR_FOLD_MS, reportToDashboard, runKillSwitch, type CliReport, type KillSwitchDeps, type KillSwitchEnv, type KillSwitchIO, } from './cli.js';
@@ -3,12 +3,12 @@
3
3
  // process. Node-only (it spawns the holder process); the rest of this
4
4
  // repository stays platform-neutral.
5
5
  export { CredentialRevokedError, HolderError, HolderLibraryError, HolderStepError, OfferError, } from "./errors.js";
6
- export { HOLDER_BINARY_ENV, HOLDER_PROTOCOL, HolderProcess, } from "./process.js";
6
+ export { HOLDER_BINARY_ENV, HOLDER_PROTOCOL, HOLDER_RELEASE, HolderProcess, } from "./process.js";
7
7
  export { custodyKeyFromHex, fileCustodyStore, memoryCustodyStore, sealedCustodyStore, } from "./custody.js";
8
- export { BUILD_INFO_PATH, CREDENTIAL_FORMAT, IDENTITY_CONFIGURATION_ID, METADATA_PATH, PRE_AUTHORIZED_GRANT, PRESENTATION_WINDOW_MS, PROOF_TYPE, SellerHolder, readCredentialOffer, readIssuedChallenge, } from "./holder.js";
8
+ export { BUILD_INFO_PATH, CREDENTIAL_FORMAT, IDENTITY_CONFIGURATION_ID, METADATA_PATH, PRE_AUTHORIZED_GRANT, PRESENTATION_WINDOW_MS, PROOF_TYPE, SellerHolder, readCredentialOffer, readIssuedChallenge, readSelfRevokeContext, } from "./holder.js";
9
9
  // The kill switch: self-revocation through the privacy relay, confirmed on
10
10
  // the chain, reported value-free (killswitch.ts); the issuer-scoped chain
11
11
  // read under it (chain.ts).
12
- export { CHAIN_ID_ENV, CHAIN_RPC_ENV, CONFIRM_ATTEMPTS, CONFIRM_INTERVAL_MS, DEFAULT_JITTER_MS, EFFECT_BOUND, RELAY_SUBMIT_PATH, RELAY_URL_ENV, RelayRefusedError, confirmSelfRevocation, revokeCredential, submitSelfRevocation, } from "./killswitch.js";
13
- export { RECORDED_NULLIFIERS_QUERY, decodeRecordedNullifiers, encodeIssuerQuery, readRecordedNullifiers, } from "./chain.js";
14
- export { CUSTODY_FILE_ENV, CUSTODY_KEY_ENV, DASHBOARD_REVOKED_STATE, DASHBOARD_SELLER_ENV, DASHBOARD_SESSION_ENV, DASHBOARD_URL_ENV, reportToDashboard, runKillSwitch, } from "./cli.js";
12
+ export { AGENT_CREDENTIAL_REFUSAL, CHAIN_ID_ENV, CHAIN_RPC_ENV, CONFIRM_ATTEMPTS, CONFIRM_INTERVAL_MS, DEFAULT_JITTER_MS, DEFAULT_RELAY_ATTEMPTS, EFFECT_BOUND, MAX_RETRY_AFTER_MS, RELAY_API_VERSION, RELAY_BODY_CAP_BYTES, RELAY_REASON_CODES, RELAY_RETRY_AFTER_SECONDS, RELAY_SUBMIT_PATH, RELAY_UNREACHABLE_RETRY_SECONDS, RELAY_URL_ENV, RelayRefusedError, confirmSelfRevocation, isRelayReasonCode, revokeCredential, submitSelfRevocation, } from "./killswitch.js";
13
+ export { RECORD_RETENTION_SECONDS, RECORDED_NULLIFIERS_QUERY, decodeRecordedNullifiers, encodeIssuerQuery, readRecordedNullifiers, } from "./chain.js";
14
+ export { CUSTODY_FILE_ENV, CUSTODY_KEY_ENV, DASHBOARD_REVOKED_STATE, DASHBOARD_SELLER_ENV, DASHBOARD_SESSION_ENV, DASHBOARD_URL_ENV, DEFAULT_WAIT_FOR_FOLD_MS, reportToDashboard, runKillSwitch, } from "./cli.js";
@@ -5,27 +5,67 @@ import type { SelfRevocation } from './types.js';
5
5
  export type Fetch = typeof fetch;
6
6
  /** The relay's submission path (relay contract v0). */
7
7
  export declare const RELAY_SUBMIT_PATH = "/v1/self-revocations";
8
+ /** The relay contract this module implements (`X-Relay-Api-Version` on every answer). */
9
+ export declare const RELAY_API_VERSION = "0.1.0";
10
+ /** The relay's body cap: 16 KiB. A larger body is refused here, before it travels. */
11
+ export declare const RELAY_BODY_CAP_BYTES: number;
8
12
  /** Environment variables the middleware host is configured with. */
9
13
  export declare const RELAY_URL_ENV = "MINTID_RELAY_URL";
10
14
  export declare const CHAIN_RPC_ENV = "MINTID_CHAIN_RPC";
11
15
  export declare const CHAIN_ID_ENV = "MINTID_CHAIN_ID";
16
+ /**
17
+ * The relay's closed reason vocabulary (relay contract v0 0.1.0): its own
18
+ * codes, the shape and lookup codes, the proof-check codes and the chain's
19
+ * `selfrevoke` codes (any other chain code arrives as `chain_refused`). An
20
+ * answer naming anything else is read as `unexpected_answer`.
21
+ */
22
+ export declare const RELAY_REASON_CODES: readonly ["request_too_large", "malformed_json", "duplicate_submission", "busy", "chain_unavailable", "inclusion_timeout", "chain_refused", "proof_refused", "not_found", "method_not_allowed", "internal_error", "proof_system_version", "msg_shape", "nullifier", "nullifier_recorded", "unknown_root", "wire_version", "schema", "wire_shape", "path_depth", "anchor_definition", "anchor_encryption_key", "definition_shape", "encoding", "identity_point", "bbs_schnorr", "bbs_pairing", "acc_schnorr", "acc_pairing", "ve_c1", "ve_c2", "selfrevoke_disabled", "block_cap", "issuer_state", "issuer_cap", "pending_cap", "tx_shape", "params"];
23
+ export type RelayReasonCode = (typeof RELAY_REASON_CODES)[number];
24
+ /** True for a code of the relay's closed vocabulary. */
25
+ export declare function isRelayReasonCode(value: unknown): value is RelayReasonCode;
26
+ /**
27
+ * Codes decided on this side, never by the relay: the relay could not be
28
+ * reached, it answered outside its contract, or the message was refused
29
+ * before it left (`msg_shape`, `request_too_large` with stage `client`).
30
+ */
31
+ export type ClientReasonCode = 'relay_unreachable' | 'unexpected_answer';
32
+ /**
33
+ * The contract's `Retry-After` table, by code — used only when an answer
34
+ * that should carry the header does not (the header, when present, wins):
35
+ * `429 busy` 2 or 5 s, `503 chain_unavailable` 5 or 10 s, `503 block_cap` /
36
+ * `issuer_cap` 10 s, `503 busy` 10 s, `503 pending_cap` 60 s. The fallback
37
+ * takes the longer value of each row.
38
+ */
39
+ export declare const RELAY_RETRY_AFTER_SECONDS: Readonly<Record<string, number>>;
40
+ /** A relay that cannot be reached is tried again after this long. */
41
+ export declare const RELAY_UNREACHABLE_RETRY_SECONDS = 5;
42
+ /** The kill switch's submission attempts at one relay (the primitive's default is one). */
43
+ export declare const DEFAULT_RELAY_ATTEMPTS = 3;
44
+ /** No single wait is longer than the contract's longest `Retry-After` (pending_cap). */
45
+ export declare const MAX_RETRY_AFTER_MS = 60000;
12
46
  /** A random delay before the revocation is built and sent: up to a minute by default. */
13
47
  export declare const DEFAULT_JITTER_MS = 60000;
14
48
  /** The confirmation read: about one block, in a few short reads. */
15
49
  export declare const CONFIRM_ATTEMPTS = 4;
16
50
  export declare const CONFIRM_INTERVAL_MS = 750;
17
51
  /**
18
- * The published effect bound, restated from the substrate's operational
19
- * rules: a status root every 30 s, a root accepted for at most 180 s (a
20
- * presentation against an older root is refused, so a recorded revocation
21
- * is effective everywhere within that bound once the issuer folds it at
22
- * its next root); a revoked principal's agents follow over the next roots,
23
- * about four minutes.
52
+ * The published effect bound (the network's revocation rule, per path; the
53
+ * same parameters on production and the public testnet): a holder
54
+ * self-revocation — the kill switch — is effective everywhere within
55
+ * G + A + K = 245 s of the chain recording it (fold grace 60 s, a root
56
+ * accepted for at most 180 s, 5 s of future skew; a root every 30 s). A
57
+ * revoked principal's agents follow within 7·H + A + K = 395 s of the root
58
+ * that carries the trigger. The bound is the guarantee: a measured figure
59
+ * is only ever quoted next to it.
24
60
  */
25
61
  export declare const EFFECT_BOUND: {
26
- readonly statusRootSeconds: 30;
27
- readonly rootAcceptedSeconds: 180;
28
- readonly agentCascadeSeconds: 240;
62
+ readonly boundSeconds: 245;
63
+ readonly countedFrom: "recorded_on_chain";
64
+ readonly principalCascadeSeconds: 395;
65
+ readonly rootPeriodSeconds: 30;
66
+ readonly acceptanceWindowSeconds: 180;
67
+ readonly futureSkewSeconds: 5;
68
+ readonly foldGraceSeconds: 60;
29
69
  };
30
70
  /** What the relay answered, in the contract's own words. */
31
71
  export type RelayOutcome = 'recorded' | 'queued' | 'inclusion_timeout' | 'nullifier_recorded' | 'duplicate_submission';
@@ -33,29 +73,38 @@ export type RelayAnswer = {
33
73
  outcome: RelayOutcome;
34
74
  txHash?: string;
35
75
  height?: number;
76
+ /** What the relay says the chain holds (200 only) — never the receipt: the chain read decides. */
77
+ relayNullifierState?: RecordedState;
36
78
  };
37
- /** A relay refusal: the contract's bare reason code and stage, nothing of the message. */
79
+ /** A relay refusal: the bare reason code and stage, nothing of the message. */
38
80
  export declare class RelayRefusedError extends HolderError {
39
81
  readonly status: number;
40
- readonly code: string;
41
- readonly stage: string;
42
- /** Worth a retry, here or at another relay (`busy`, `chain_unavailable`, caps). */
82
+ readonly code: RelayReasonCode | ClientReasonCode;
83
+ /** `client`: decided on this side (unreachable, outside the contract, refused before sending). */
84
+ readonly stage: 'relay' | 'precheck' | 'chain' | 'client';
85
+ /** Worth a retry, here or at another relay (`busy`, `chain_unavailable`, caps, unreachable). */
43
86
  readonly retryable: boolean;
44
87
  readonly retryAfterMs: number | undefined;
45
- constructor(status: number, code: string, stage: string, retryAfterMs?: number);
88
+ constructor(status: number, code: RelayReasonCode | ClientReasonCode, stage: 'relay' | 'precheck' | 'chain' | 'client', retryAfterMs?: number);
46
89
  }
47
90
  export type SubmitOptions = {
48
91
  /** The relay's own transport — never the holder's issuer transport. */
49
92
  fetch?: Fetch;
50
93
  timeoutMs?: number;
94
+ /** Attempts at this relay; a retryable refusal waits its `Retry-After` first. Default 1. */
95
+ attempts?: number;
96
+ sleep?: (ms: number) => Promise<void>;
51
97
  };
52
98
  /**
53
99
  * Hands the self-revocation to the privacy relay: `{message}` with the five
54
100
  * message fields only, no header beyond the content type. The relay checks
55
- * the proof with the chain's rules, pays for the transaction and submits
56
- * it; this side signs nothing and holds no account. `RelayRefusedError` on
57
- * a refusal; every accepted answer (recorded, queued, already recorded,
58
- * already in flight, broadcast but not yet seen) still needs the chain read.
101
+ * the proof against the issuer's anchored root, pays for the transaction
102
+ * and submits it; this side signs nothing and holds no account.
103
+ * `RelayRefusedError` on a refusal (the code from the closed vocabulary);
104
+ * every accepted answer (recorded, queued, already recorded, already in
105
+ * flight, broadcast but not yet seen) still needs the chain read. With
106
+ * `attempts` above one, a retryable refusal (429, 503, unreachable) is
107
+ * tried again after the relay's `Retry-After` (or the contract's table).
59
108
  */
60
109
  export declare function submitSelfRevocation(relayUrl: string, revocation: SelfRevocation, options?: SubmitOptions): Promise<RelayAnswer>;
61
110
  export type ConfirmOptions = {
@@ -64,31 +113,45 @@ export type ConfirmOptions = {
64
113
  attempts?: number;
65
114
  intervalMs?: number;
66
115
  sleep?: (ms: number) => Promise<void>;
116
+ /**
117
+ * `recorded` (default): stop at the first read that shows the record.
118
+ * `folded`: keep reading while it is pending, until folded or the
119
+ * attempts run out (the report is then `pending`).
120
+ */
121
+ until?: 'recorded' | 'folded';
122
+ /**
123
+ * What an earlier read showed for this revocation. A value seen once and
124
+ * absent now has been folded and pruned (the chain keeps a record for
125
+ * `RECORD_RETENTION_SECONDS`), so it reads `folded`.
126
+ */
127
+ seen?: RecordedState;
67
128
  };
68
129
  export type Confirmation = {
69
130
  state: RecordedState;
131
+ pruned?: true;
70
132
  } | {
71
133
  state: 'unconfirmed';
72
134
  reason: 'no_chain_endpoint' | 'chain_unreachable' | 'not_recorded';
73
135
  };
74
136
  /**
75
- * Asks a node of the chain whether the revocation is there — for about one
76
- * block — reading the issuer's recorded set and finding the holder's own
77
- * value locally, so the node never learns which one. `pending` once
78
- * recorded, `folded` once the issuer's next root has folded it; otherwise
79
- * `unconfirmed` with the reason (no node named, node unreachable, not
80
- * recorded yet).
137
+ * Asks a node of the chain whether the revocation is there, reading the
138
+ * issuer's recorded set and finding the holder's own value locally, so the
139
+ * node never learns which one. `pending` once recorded, `folded` once the
140
+ * issuer has folded it (or, after an earlier sighting, once the chain has
141
+ * pruned it); otherwise `unconfirmed` with the reason (no node named, node
142
+ * unreachable, not recorded yet). The relay's answer never enters here.
81
143
  */
82
144
  export declare function confirmSelfRevocation(rpcUrl: string | undefined, revocation: SelfRevocation, options?: ConfirmOptions): Promise<Confirmation>;
83
145
  /** What the kill switch reports to the seller — value-free. */
84
146
  export type KillSwitchReport = {
85
147
  /**
86
- * `revoked`: the chain holds the record and the issuer has folded it —
87
- * the credential presents nowhere. `pending`: the chain holds the record;
88
- * it lands at the issuer's next root, within the bound. `unconfirmed`:
89
- * the relay accepted the submission but no node showed the record within
90
- * the read window (or none was named) — check again, or resubmit through
91
- * a relay; both are safe.
148
+ * `revoked`: the chain holds the record and the issuer has folded it.
149
+ * `pending`: the chain holds the record and the issuer folds it at a next
150
+ * root. Either way the credential is refused everywhere within the
151
+ * published bound (`effect`, 245 s from the record). `unconfirmed`: the
152
+ * relay accepted the submission but no node showed the record within the
153
+ * read window (or none was named) — check again, or resubmit through a
154
+ * relay; both are safe.
92
155
  */
93
156
  state: 'revoked' | 'pending' | 'unconfirmed';
94
157
  relay: RelayOutcome;
@@ -98,6 +161,7 @@ export type KillSwitchReport = {
98
161
  height?: number;
99
162
  /** The holder refuses to present from now on, whatever the chain shows. */
100
163
  holderRefusesToPresent: true;
164
+ /** The published bound — the guarantee; never a measured figure. */
101
165
  effect: typeof EFFECT_BOUND;
102
166
  };
103
167
  export type KillSwitchOptions = {
@@ -109,31 +173,50 @@ export type KillSwitchOptions = {
109
173
  /** The relay's transport; the chain read's transport. Neither is the issuer's. */
110
174
  relayFetch?: Fetch;
111
175
  chainFetch?: Fetch;
176
+ /** Submission attempts at the relay, `Retry-After` honoured between them (default 3). */
177
+ relayAttempts?: number;
112
178
  confirmAttempts?: number;
113
179
  confirmIntervalMs?: number;
180
+ /**
181
+ * Keep reading after the record shows, until the issuer has folded it,
182
+ * for up to this long (0, the default: report at the first sighting).
183
+ */
184
+ waitForFoldMs?: number;
114
185
  sleep?: (ms: number) => Promise<void>;
115
186
  /** Uniform in [0, 1); injected by tests. */
116
187
  random?: () => number;
117
188
  };
189
+ /**
190
+ * What the seller path answers for an agent credential: agent credentials
191
+ * are revoked through the verification service's agent kill route (the
192
+ * operator console's kill action), which keeps the operator view's state —
193
+ * never through this path.
194
+ */
195
+ export declare const AGENT_CREDENTIAL_REFUSAL: string;
118
196
  /**
119
197
  * The product kill switch, end to end, where the key is:
120
198
  *
121
199
  * 1. a random delay, so the moment the seller decided is not the moment
122
- * the chain records (the relay shuffles what it holds; this adds the
123
- * seller's own slack);
124
- * 2. a fresh witness from the issuer, so the self-revocation is built
125
- * against the root the chain currently holds (the material comes with
126
- * the witness answer; no other call);
127
- * 3. the self-revocation, built by the holder library — from that moment
128
- * the holder refuses to present, also after a reload from custody;
129
- * 4. the submission through the privacy relay; a refusal for a root the
130
- * chain no longer holds rebuilds against the current one, once;
131
- * 5. the confirmation read on the chain, issuer-scoped.
200
+ * the chain records (the relay adds its own 0–6 s and shuffles each
201
+ * block's batch; this adds the seller's slack);
202
+ * 2. a fresh witness from the issuer, whose answer carries the root's
203
+ * `self_revoke_context` (no other call);
204
+ * 3. the holder library's `self_revoke` — `context = {chain_id,
205
+ * status_root, anchoring}` with `anchoring` the served context without
206
+ * `status_root` — and from that moment the holder refuses to present,
207
+ * also after a reload from custody;
208
+ * 4. the submission through the privacy relay, `Retry-After` honoured; a
209
+ * refusal for a root the chain no longer holds (`unknown_root`)
210
+ * rebuilds the context once from a fresh witness;
211
+ * 5. the confirmation read on the chain, issuer-scoped — the relay's
212
+ * answer is never the receipt.
132
213
  *
133
- * Configuration: `relayUrl` (or `MINTID_RELAY_URL`), `chainRpc` (or
134
- * `MINTID_CHAIN_RPC`; without it the report is `unconfirmed`,
135
- * `no_chain_endpoint`), `chainId` (or `MINTID_CHAIN_ID`, else the issuer's
136
- * public build information). The client never bypasses the relay to the
137
- * chain: it holds no account and signs nothing.
214
+ * The seller's own credential only: an agent credential is refused
215
+ * (`AGENT_CREDENTIAL_REFUSAL`). Configuration: `relayUrl` (or
216
+ * `MINTID_RELAY_URL`), `chainRpc` (or `MINTID_CHAIN_RPC`; without it the
217
+ * report is `unconfirmed`, `no_chain_endpoint`), `chainId` (or
218
+ * `MINTID_CHAIN_ID`, else the issuer's public build information). The
219
+ * client never bypasses the relay to the chain: it holds no account and
220
+ * signs nothing.
138
221
  */
139
222
  export declare function revokeCredential(holder: SellerHolder, options?: KillSwitchOptions): Promise<KillSwitchReport>;