@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.
- package/README.md +28 -2
- package/dist/badge.d.ts +51 -0
- package/dist/badge.js +103 -0
- package/dist/brand.d.ts +1 -0
- package/dist/brand.js +3 -0
- package/dist/client.d.ts +26 -0
- package/dist/client.js +46 -0
- package/dist/explorer.d.ts +17 -4
- package/dist/explorer.js +22 -10
- package/dist/extension.d.ts +20 -6
- package/dist/extension.js +55 -10
- package/dist/header.d.ts +15 -5
- package/dist/header.js +29 -16
- package/dist/holder/chain.d.ts +6 -1
- package/dist/holder/chain.js +24 -8
- package/dist/holder/cli.d.ts +2 -0
- package/dist/holder/cli.js +25 -4
- package/dist/holder/holder.d.ts +8 -0
- package/dist/holder/holder.js +81 -15
- package/dist/holder/index.d.ts +6 -6
- package/dist/holder/index.js +5 -5
- package/dist/holder/killswitch.d.ts +128 -45
- package/dist/holder/killswitch.js +236 -56
- package/dist/holder/process.d.ts +9 -0
- package/dist/holder/process.js +9 -0
- package/dist/holder/types.d.ts +52 -15
- package/dist/index.d.ts +7 -5
- package/dist/index.js +7 -5
- package/dist/keys.d.ts +22 -1
- package/dist/keys.js +25 -7
- package/dist/receipt.d.ts +8 -3
- package/dist/receipt.js +11 -5
- package/dist/verdict.d.ts +85 -15
- package/dist/verdict.js +160 -17
- package/dist/webhook.d.ts +4 -2
- package/dist/webhook.js +6 -12
- package/package.json +18 -2
|
@@ -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
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
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
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
|
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
|
|
73
|
-
* it; this side signs nothing and holds no account.
|
|
74
|
-
* a refusal
|
|
75
|
-
*
|
|
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
|
-
|
|
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
|
|
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', '
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
* `unconfirmed` with the reason (no node named, node
|
|
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
|
|
175
|
-
* seller's
|
|
176
|
-
* 2. a fresh witness from the issuer,
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
* the holder refuses to present,
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
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
|
-
*
|
|
186
|
-
* `
|
|
187
|
-
* `
|
|
188
|
-
*
|
|
189
|
-
*
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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'
|
package/dist/holder/process.d.ts
CHANGED
|
@@ -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";
|
package/dist/holder/process.js
CHANGED
|
@@ -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 {
|
package/dist/holder/types.d.ts
CHANGED
|
@@ -58,30 +58,66 @@ export type WitnessState = {
|
|
|
58
58
|
statusRoot: string | null;
|
|
59
59
|
};
|
|
60
60
|
/**
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
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
|
|
69
|
-
|
|
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
|
|
73
|
-
*
|
|
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
|
|
83
|
-
*
|
|
84
|
-
*
|
|
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
|
-
export { BRAND } from './brand.js';
|
|
2
|
-
export {
|
|
3
|
-
export {
|
|
4
|
-
export {
|
|
1
|
+
export { BRAND, VERDICT_EXTENSION_KEY } from './brand.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
|
@@ -6,13 +6,15 @@
|
|
|
6
6
|
// Transport follows the bIP-0002 x402 profile of the MintID identity network
|
|
7
7
|
// (cited by name): identity policy in a `PaymentRequired` extension, proof alongside
|
|
8
8
|
// the `X-PAYMENT` header.
|
|
9
|
-
export { BRAND } from "./brand.js";
|
|
10
|
-
export {
|
|
11
|
-
export {
|
|
12
|
-
export {
|
|
9
|
+
export { BRAND, VERDICT_EXTENSION_KEY } from "./brand.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
|
-
/**
|
|
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;
|