@zadq/x402 0.0.0-stage → 0.1.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/LICENSE +202 -0
- package/NOTICE +8 -0
- package/README.md +35 -2
- package/dist/brand.d.ts +1 -0
- package/dist/brand.js +3 -0
- package/dist/explorer.d.ts +47 -0
- package/dist/explorer.js +76 -0
- package/dist/extension.d.ts +169 -0
- package/dist/extension.js +182 -0
- package/dist/header.d.ts +30 -0
- package/dist/header.js +84 -0
- package/dist/holder/chain.d.ts +28 -0
- package/dist/holder/chain.js +167 -0
- package/dist/holder/cli.d.ts +42 -0
- package/dist/holder/cli.js +173 -0
- package/dist/holder/custody.d.ts +20 -0
- package/dist/holder/custody.js +112 -0
- package/dist/holder/errors.d.ts +22 -0
- package/dist/holder/errors.js +38 -0
- package/dist/holder/holder.d.ts +115 -0
- package/dist/holder/holder.js +381 -0
- package/dist/holder/index.d.ts +8 -0
- package/dist/holder/index.js +14 -0
- package/dist/holder/killswitch.d.ts +139 -0
- package/dist/holder/killswitch.js +257 -0
- package/dist/holder/process.d.ts +31 -0
- package/dist/holder/process.js +84 -0
- package/dist/holder/types.d.ts +108 -0
- package/dist/holder/types.js +5 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +20 -0
- package/dist/keys.d.ts +17 -0
- package/dist/keys.js +45 -0
- package/dist/receipt.d.ts +50 -0
- package/dist/receipt.js +193 -0
- package/dist/verdict.d.ts +101 -0
- package/dist/verdict.js +179 -0
- package/dist/webhook.d.ts +29 -0
- package/dist/webhook.js +96 -0
- package/package.json +85 -4
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
// The seller's holder: the accountable party's own identity credential,
|
|
2
|
+
// held in the seller's infrastructure over MintID's holder library process
|
|
3
|
+
// (the identity class: issuance from the operator's offer, witness refresh,
|
|
4
|
+
// bound presentations, self-check, self-revocation). Restated from MintID's
|
|
5
|
+
// holder-library contract, cited by name; this module carries no
|
|
6
|
+
// cryptography and no chain or relay client of its own.
|
|
7
|
+
//
|
|
8
|
+
// Where things live: the credential and its link secret inside the holder
|
|
9
|
+
// process; the custody encoding under the seller's own key (custody.ts);
|
|
10
|
+
// state only — offered / held / revoked — at the verification service,
|
|
11
|
+
// which never sees a key, a code, a proof or the encoding.
|
|
12
|
+
//
|
|
13
|
+
// Graceful degradation: a middleware without a credential emits the v0
|
|
14
|
+
// object only and every payment proceeds as before; a holder that cannot
|
|
15
|
+
// present answers the verification service's challenge with a refusal, and
|
|
16
|
+
// the verdict on the endpoint falls back to the v0 standing. Nothing blocks.
|
|
17
|
+
import { CredentialRevokedError, HolderError, HolderLibraryError, HolderStepError, OfferError, } from "./errors.js";
|
|
18
|
+
/** The identity credential's configuration id at the issuer's metadata. */
|
|
19
|
+
export const IDENTITY_CONFIGURATION_ID = 'mintid_identity_v1';
|
|
20
|
+
export const CREDENTIAL_FORMAT = 'mintid/anoncreds-v2';
|
|
21
|
+
export const PROOF_TYPE = 'mintid_blind_request';
|
|
22
|
+
export const PRE_AUTHORIZED_GRANT = 'urn:ietf:params:oauth:grant-type:pre-authorized_code';
|
|
23
|
+
export const METADATA_PATH = '/.well-known/openid-credential-issuer';
|
|
24
|
+
/** The issuer's public build information: names the chain it anchors on. */
|
|
25
|
+
export const BUILD_INFO_PATH = '/v0/build-info';
|
|
26
|
+
/** A presentation lives ten seconds from the challenge; the holder answers well inside it. */
|
|
27
|
+
export const PRESENTATION_WINDOW_MS = 10_000;
|
|
28
|
+
/**
|
|
29
|
+
* Reads the operator's hand-out in any of its forms — the dashboard's
|
|
30
|
+
* `{offer, expiresAt, credential}`, the issuer's `{credential_offer,
|
|
31
|
+
* expires_at}`, or the bare offer — and refuses anything that is not a
|
|
32
|
+
* pre-authorized MintID identity offer, or one already past its expiry.
|
|
33
|
+
*/
|
|
34
|
+
export function readCredentialOffer(input, now = new Date()) {
|
|
35
|
+
let value = input;
|
|
36
|
+
if (typeof value === 'string') {
|
|
37
|
+
try {
|
|
38
|
+
value = JSON.parse(value);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
throw new OfferError('the hand-out is not JSON');
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (typeof value !== 'object' || value === null)
|
|
45
|
+
throw new OfferError('the hand-out is not an object');
|
|
46
|
+
const outer = value;
|
|
47
|
+
let expiresAt;
|
|
48
|
+
let offer;
|
|
49
|
+
if (typeof outer['offer'] === 'object' && outer['offer'] !== null) {
|
|
50
|
+
offer = outer['offer'];
|
|
51
|
+
if (typeof outer['expiresAt'] === 'string')
|
|
52
|
+
expiresAt = new Date(outer['expiresAt']);
|
|
53
|
+
}
|
|
54
|
+
else if (typeof outer['credential_offer'] === 'object' && outer['credential_offer'] !== null) {
|
|
55
|
+
offer = outer['credential_offer'];
|
|
56
|
+
if (typeof outer['expires_at'] === 'number')
|
|
57
|
+
expiresAt = new Date(outer['expires_at'] * 1000);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
offer = outer;
|
|
61
|
+
}
|
|
62
|
+
const issuer = String(offer['credential_issuer'] ?? '').replace(/\/+$/, '');
|
|
63
|
+
const grants = offer['grants'];
|
|
64
|
+
const code = grants?.[PRE_AUTHORIZED_GRANT]?.['pre-authorized_code'];
|
|
65
|
+
if (!issuer.startsWith('http') || typeof code !== 'string' || code === '') {
|
|
66
|
+
throw new OfferError('not a pre-authorized MintID credential offer');
|
|
67
|
+
}
|
|
68
|
+
const ids = offer['credential_configuration_ids'];
|
|
69
|
+
if (!Array.isArray(ids) || !ids.includes(IDENTITY_CONFIGURATION_ID)) {
|
|
70
|
+
throw new OfferError('the offer does not carry the MintID identity credential');
|
|
71
|
+
}
|
|
72
|
+
if (expiresAt !== undefined && (Number.isNaN(expiresAt.getTime()) || expiresAt <= now)) {
|
|
73
|
+
throw new OfferError('the offer has expired with the verification behind it — re-verify');
|
|
74
|
+
}
|
|
75
|
+
return expiresAt === undefined ? { offer } : { offer, expiresAt };
|
|
76
|
+
}
|
|
77
|
+
export class SellerHolder {
|
|
78
|
+
rpc;
|
|
79
|
+
handle;
|
|
80
|
+
fetchImpl;
|
|
81
|
+
custody;
|
|
82
|
+
// The issuer's self-revocation material for the root of the last adopted
|
|
83
|
+
// witness — served with the witness answer, cached in memory only (the
|
|
84
|
+
// custody encoding is the library's). Public, the same for every holder.
|
|
85
|
+
material;
|
|
86
|
+
constructor(rpc, handle, options) {
|
|
87
|
+
this.rpc = rpc;
|
|
88
|
+
this.handle = handle;
|
|
89
|
+
this.fetchImpl = options.fetch ?? fetch;
|
|
90
|
+
this.custody = options.custody;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Redeem the operator's offer, once: issuer metadata → the one-shot
|
|
94
|
+
* grant (with its nonce) → the library's blinded request → the
|
|
95
|
+
* credential → the library completes and holds it. The custody encoding
|
|
96
|
+
* is saved when a store is given. `OfferError` on a hand-out of another
|
|
97
|
+
* class, `HolderStepError` on a terminal issuer refusal (an offer past
|
|
98
|
+
* its verification's expiry answers 409 `session_reuse_expired`; the way
|
|
99
|
+
* back is a new verification, never a retry).
|
|
100
|
+
*/
|
|
101
|
+
static async issue(handout, rpc, options = {}) {
|
|
102
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
103
|
+
const { offer } = readCredentialOffer(handout);
|
|
104
|
+
const issuerUrl = String(offer['credential_issuer']).replace(/\/+$/, '');
|
|
105
|
+
const grants = offer['grants'];
|
|
106
|
+
const code = String(grants[PRE_AUTHORIZED_GRANT]?.['pre-authorized_code']);
|
|
107
|
+
const meta = await getJson(fetchImpl, 'metadata', issuerUrl + METADATA_PATH);
|
|
108
|
+
if (meta.status !== 200)
|
|
109
|
+
throw new HolderStepError('metadata', meta.status, meta.body);
|
|
110
|
+
const configs = meta.body['credential_configurations_supported'];
|
|
111
|
+
const definition = configs?.[IDENTITY_CONFIGURATION_ID]?.['issuer_definition'];
|
|
112
|
+
const grantEndpoint = meta.body['token_endpoint'];
|
|
113
|
+
const credentialEndpoint = meta.body['credential_endpoint'];
|
|
114
|
+
if (typeof definition !== 'string' ||
|
|
115
|
+
typeof grantEndpoint !== 'string' ||
|
|
116
|
+
typeof credentialEndpoint !== 'string') {
|
|
117
|
+
throw new HolderStepError('metadata', meta.status, { error: 'incomplete metadata' });
|
|
118
|
+
}
|
|
119
|
+
const grant = await getJson(fetchImpl, 'grant', grantEndpoint, postJson({ grant_type: PRE_AUTHORIZED_GRANT, 'pre-authorized_code': code }));
|
|
120
|
+
if (grant.status !== 200)
|
|
121
|
+
throw new HolderStepError('grant', grant.status, grant.body);
|
|
122
|
+
const flow = await rpc.call('issue_begin', {
|
|
123
|
+
issuer_definition: definition,
|
|
124
|
+
c_nonce: grant.body['c_nonce'],
|
|
125
|
+
});
|
|
126
|
+
const cred = await getJson(fetchImpl, 'credential', credentialEndpoint, postJson({
|
|
127
|
+
format: CREDENTIAL_FORMAT,
|
|
128
|
+
credential_configuration_id: IDENTITY_CONFIGURATION_ID,
|
|
129
|
+
proof: { proof_type: PROOF_TYPE, request: flow['request'] },
|
|
130
|
+
}, { authorization: `Bearer ${String(grant.body['access_token'])}` }));
|
|
131
|
+
if (cred.status !== 200)
|
|
132
|
+
throw new HolderStepError('credential', cred.status, cred.body);
|
|
133
|
+
const done = await rpc.call('issue_complete', {
|
|
134
|
+
flow: flow['flow'],
|
|
135
|
+
credential: cred.body['credential'],
|
|
136
|
+
credential_id: cred.body['credential_id'],
|
|
137
|
+
witness_endpoint: String(cred.body['witness_endpoint'] ?? ''),
|
|
138
|
+
});
|
|
139
|
+
const holder = new SellerHolder(rpc, Number(done['holder']), options);
|
|
140
|
+
await holder.persist();
|
|
141
|
+
return holder;
|
|
142
|
+
}
|
|
143
|
+
/** Resume from the custody store (or a custody encoding given directly). */
|
|
144
|
+
static async load(rpc, options = {}) {
|
|
145
|
+
const encoding = options.encoding ?? (await options.custody?.load());
|
|
146
|
+
if (!encoding)
|
|
147
|
+
throw new OfferError('no custody encoding to load — issue from an offer first');
|
|
148
|
+
const out = await rpc.call('load', { storage: encoding });
|
|
149
|
+
return new SellerHolder(rpc, Number(out['holder']), options);
|
|
150
|
+
}
|
|
151
|
+
/** Custody encoding — it contains the link secret; keep it sealed, never log it. */
|
|
152
|
+
async storage() {
|
|
153
|
+
return String((await this.rpc.call('storage', { holder: this.handle }))['storage']);
|
|
154
|
+
}
|
|
155
|
+
/** Writes the current custody encoding to the store, when one is configured. */
|
|
156
|
+
async persist() {
|
|
157
|
+
if (this.custody)
|
|
158
|
+
await this.custody.save(await this.storage());
|
|
159
|
+
}
|
|
160
|
+
async info() {
|
|
161
|
+
return (await this.rpc.call('info', { holder: this.handle }));
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Fresh witness material from the issuer, adopted once a finalized root
|
|
165
|
+
* anchors it. Run it on your cadence — sized to how often roots are
|
|
166
|
+
* published — and right before presenting: a stale witness fails closed.
|
|
167
|
+
* `CredentialRevokedError` once the issuer serves none (revoked, or
|
|
168
|
+
* expired with its verification).
|
|
169
|
+
*/
|
|
170
|
+
async refreshWitness() {
|
|
171
|
+
const endpoint = (await this.info()).witness_endpoint;
|
|
172
|
+
if (!endpoint)
|
|
173
|
+
throw new HolderStepError('witness', 0, { error: 'no witness endpoint' });
|
|
174
|
+
const { status, body } = await getJson(this.fetchImpl, 'witness', endpoint);
|
|
175
|
+
if (status !== 200)
|
|
176
|
+
throw new HolderStepError('witness', status, body);
|
|
177
|
+
const state = String(body['status'] ?? '');
|
|
178
|
+
const witness = body['witness'];
|
|
179
|
+
const root = body['root'];
|
|
180
|
+
if (state !== 'active' || typeof witness !== 'string') {
|
|
181
|
+
throw new CredentialRevokedError(`issuer reports status ${JSON.stringify(state)}`);
|
|
182
|
+
}
|
|
183
|
+
if (!body['usable'] || !root) {
|
|
184
|
+
return { status: state, usable: false, refreshed: false, rootHeight: null, statusRoot: null };
|
|
185
|
+
}
|
|
186
|
+
await this.rpc.call('refresh_witness', {
|
|
187
|
+
holder: this.handle,
|
|
188
|
+
issuer_definition: body['issuer_definition'],
|
|
189
|
+
witness,
|
|
190
|
+
root_height: Number(root.height),
|
|
191
|
+
root: String(root.root),
|
|
192
|
+
});
|
|
193
|
+
await this.persist();
|
|
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
|
+
}
|
|
202
|
+
return {
|
|
203
|
+
status: state,
|
|
204
|
+
usable: true,
|
|
205
|
+
refreshed: true,
|
|
206
|
+
rootHeight: Number(root.height),
|
|
207
|
+
statusRoot: String(root.root),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* One bound, ten-second envelope for a verifier's challenge — the
|
|
212
|
+
* `createChallenge` answer as received (`{request, signature_hex}`) or
|
|
213
|
+
* its `request` object. The library refuses (naming an output key at
|
|
214
|
+
* most) when the credential cannot satisfy the policy: nothing is sent.
|
|
215
|
+
*/
|
|
216
|
+
async present(challenge) {
|
|
217
|
+
const c = challenge;
|
|
218
|
+
const request = typeof c['request'] === 'object' && c['request'] !== null ? c['request'] : c;
|
|
219
|
+
return (await this.rpc.call('present', {
|
|
220
|
+
holder: this.handle,
|
|
221
|
+
request,
|
|
222
|
+
}));
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* The presentation endpoint's whole answer to the verification service's
|
|
226
|
+
* challenge (`POST` of the issued challenge, byte for byte): the witness
|
|
227
|
+
* is refreshed first, then one envelope is built. A witness endpoint that
|
|
228
|
+
* cannot be reached is not a refusal — the holder presents against the
|
|
229
|
+
* root it holds and the verifier decides. Refusals are value-free reason
|
|
230
|
+
* codes: `challenge_malformed` (400), `challenge_expired`,
|
|
231
|
+
* `credential_revoked`, `holder_refused` (409). Nothing of the exchange
|
|
232
|
+
* is logged here, and nothing is retried: the next cadence brings the
|
|
233
|
+
* next challenge.
|
|
234
|
+
*/
|
|
235
|
+
async answerChallenge(body, now = new Date()) {
|
|
236
|
+
const challenge = readIssuedChallenge(body);
|
|
237
|
+
if (challenge === undefined) {
|
|
238
|
+
return { status: 400, body: { error: 'refused', reason: 'challenge_malformed' } };
|
|
239
|
+
}
|
|
240
|
+
if (challenge.request.expires_at * 1000 <= now.getTime()) {
|
|
241
|
+
return { status: 409, body: { error: 'refused', reason: 'challenge_expired' } };
|
|
242
|
+
}
|
|
243
|
+
try {
|
|
244
|
+
await this.refreshWitness();
|
|
245
|
+
}
|
|
246
|
+
catch (err) {
|
|
247
|
+
if (err instanceof CredentialRevokedError) {
|
|
248
|
+
return { status: 409, body: { error: 'refused', reason: 'credential_revoked' } };
|
|
249
|
+
}
|
|
250
|
+
if (err instanceof HolderLibraryError) {
|
|
251
|
+
return {
|
|
252
|
+
status: 409,
|
|
253
|
+
body: { error: 'refused', reason: 'holder_refused', detail: err.text },
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
if (!(err instanceof HolderStepError))
|
|
257
|
+
throw err;
|
|
258
|
+
// The issuer's witness endpoint did not answer: present with the held witness.
|
|
259
|
+
}
|
|
260
|
+
try {
|
|
261
|
+
return { status: 200, body: await this.present(challenge) };
|
|
262
|
+
}
|
|
263
|
+
catch (err) {
|
|
264
|
+
if (err instanceof HolderLibraryError) {
|
|
265
|
+
return {
|
|
266
|
+
status: 409,
|
|
267
|
+
body: { error: 'refused', reason: 'holder_refused', detail: err.text },
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
throw err;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
/** The library's own consistency check under a caller nonce (hex); rejects otherwise. */
|
|
274
|
+
async selfCheck(nonce = randomNonceHex()) {
|
|
275
|
+
await this.rpc.call('self_check', { holder: this.handle, nonce });
|
|
276
|
+
}
|
|
277
|
+
/** The self-revocation material cached by the last witness refresh, if any. */
|
|
278
|
+
selfRevokeContext() {
|
|
279
|
+
return this.material;
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* The chain the issuer anchors on, from its public build information
|
|
283
|
+
* (the issuer is where the witness endpoint lives). `HolderStepError`
|
|
284
|
+
* when it cannot be read.
|
|
285
|
+
*/
|
|
286
|
+
async chainId() {
|
|
287
|
+
const endpoint = (await this.info()).witness_endpoint;
|
|
288
|
+
if (!endpoint)
|
|
289
|
+
throw new HolderStepError('build-info', 0, { error: 'no witness endpoint' });
|
|
290
|
+
const { status, body } = await getJson(this.fetchImpl, 'build-info', new URL(endpoint).origin + BUILD_INFO_PATH);
|
|
291
|
+
if (status !== 200 || typeof body['chain_id'] !== 'string' || body['chain_id'] === '') {
|
|
292
|
+
throw new HolderStepError('build-info', status, { error: 'no chain id' });
|
|
293
|
+
}
|
|
294
|
+
return body['chain_id'];
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Builds the credential's self-revocation against the issuer's published
|
|
298
|
+
* anchoring of a finalized root — the material cached by the last witness
|
|
299
|
+
* refresh unless the caller passes its own. From this call on the holder
|
|
300
|
+
* refuses to present, also after a reload from custody (the store is
|
|
301
|
+
* updated). The message revokes nothing until a privacy relay submits it
|
|
302
|
+
* and the chain records it: `revokeCredential` (killswitch.ts) is the
|
|
303
|
+
* product kill switch that does both and reports; this method is the
|
|
304
|
+
* primitive under it.
|
|
305
|
+
*/
|
|
306
|
+
async selfRevoke(context) {
|
|
307
|
+
const anchoring = context.anchoring ?? this.material;
|
|
308
|
+
if (!anchoring) {
|
|
309
|
+
throw new HolderError('no self-revocation material: refresh the witness first, or pass the issuer anchoring');
|
|
310
|
+
}
|
|
311
|
+
const full = { chain_id: context.chain_id, anchoring };
|
|
312
|
+
const root = context.status_root ?? anchoring['status_root'];
|
|
313
|
+
if (typeof root === 'string' && root !== '')
|
|
314
|
+
full['status_root'] = root;
|
|
315
|
+
const out = (await this.rpc.call('self_revoke', {
|
|
316
|
+
holder: this.handle,
|
|
317
|
+
context: full,
|
|
318
|
+
}));
|
|
319
|
+
await this.persist();
|
|
320
|
+
return out;
|
|
321
|
+
}
|
|
322
|
+
/** Forget the credential inside the process (its secret with it). The custody store is untouched. */
|
|
323
|
+
async close() {
|
|
324
|
+
await this.rpc.call('drop', { holder: this.handle });
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Reads the verification service's `POST` body as an issued challenge:
|
|
329
|
+
* `{request, signature_hex}`, or the bare request. Returns `undefined` for
|
|
330
|
+
* anything that is not a presentation request (no nonce, no audience, no
|
|
331
|
+
* expiry, no policy) — the endpoint answers 400 and moves on.
|
|
332
|
+
*/
|
|
333
|
+
export function readIssuedChallenge(body) {
|
|
334
|
+
if (typeof body !== 'object' || body === null)
|
|
335
|
+
return undefined;
|
|
336
|
+
const outer = body;
|
|
337
|
+
const request = (typeof outer['request'] === 'object' && outer['request'] !== null ? outer['request'] : outer);
|
|
338
|
+
const hex = (v) => typeof v === 'string' && /^[0-9a-f]{64}$/.test(v);
|
|
339
|
+
if (typeof request['session_id'] !== 'string' ||
|
|
340
|
+
!hex(request['nonce']) ||
|
|
341
|
+
!hex(request['verifier_id']) ||
|
|
342
|
+
typeof request['audience'] !== 'string' ||
|
|
343
|
+
typeof request['expires_at'] !== 'number' ||
|
|
344
|
+
!Array.isArray(request['claim_policy']) ||
|
|
345
|
+
request['claim_policy'].length === 0) {
|
|
346
|
+
return undefined;
|
|
347
|
+
}
|
|
348
|
+
return {
|
|
349
|
+
request: request,
|
|
350
|
+
signature_hex: typeof outer['signature_hex'] === 'string' ? outer['signature_hex'] : '',
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
function randomNonceHex() {
|
|
354
|
+
const bytes = globalThis.crypto.getRandomValues(new Uint8Array(32));
|
|
355
|
+
return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
|
|
356
|
+
}
|
|
357
|
+
async function getJson(fetchImpl, step, url, init = {}) {
|
|
358
|
+
let response;
|
|
359
|
+
try {
|
|
360
|
+
response = await fetchImpl(url, init);
|
|
361
|
+
}
|
|
362
|
+
catch (cause) {
|
|
363
|
+
throw new HolderStepError(step, 0, { error: String(cause) });
|
|
364
|
+
}
|
|
365
|
+
let body = {};
|
|
366
|
+
try {
|
|
367
|
+
const text = await response.text();
|
|
368
|
+
body = text ? JSON.parse(text) : {};
|
|
369
|
+
}
|
|
370
|
+
catch {
|
|
371
|
+
body = { error: 'non_json_response' };
|
|
372
|
+
}
|
|
373
|
+
return { status: response.status, body };
|
|
374
|
+
}
|
|
375
|
+
function postJson(body, headers = {}) {
|
|
376
|
+
return {
|
|
377
|
+
method: 'POST',
|
|
378
|
+
headers: { 'content-type': 'application/json', ...headers },
|
|
379
|
+
body: JSON.stringify(body),
|
|
380
|
+
};
|
|
381
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
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';
|
|
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';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// The seller-side holder module: the accountable party's identity credential
|
|
2
|
+
// held in the seller's own infrastructure over MintID's holder library
|
|
3
|
+
// process. Node-only (it spawns the holder process); the rest of this
|
|
4
|
+
// repository stays platform-neutral.
|
|
5
|
+
export { CredentialRevokedError, HolderError, HolderLibraryError, HolderStepError, OfferError, } from "./errors.js";
|
|
6
|
+
export { HOLDER_BINARY_ENV, HOLDER_PROTOCOL, HolderProcess, } from "./process.js";
|
|
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";
|
|
9
|
+
// The kill switch: self-revocation through the privacy relay, confirmed on
|
|
10
|
+
// the chain, reported value-free (killswitch.ts); the issuer-scoped chain
|
|
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";
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { HolderError } from './errors.js';
|
|
2
|
+
import type { SellerHolder } from './holder.js';
|
|
3
|
+
import { type RecordedState } from './chain.js';
|
|
4
|
+
import type { SelfRevocation } from './types.js';
|
|
5
|
+
export type Fetch = typeof fetch;
|
|
6
|
+
/** The relay's submission path (relay contract v0). */
|
|
7
|
+
export declare const RELAY_SUBMIT_PATH = "/v1/self-revocations";
|
|
8
|
+
/** Environment variables the middleware host is configured with. */
|
|
9
|
+
export declare const RELAY_URL_ENV = "MINTID_RELAY_URL";
|
|
10
|
+
export declare const CHAIN_RPC_ENV = "MINTID_CHAIN_RPC";
|
|
11
|
+
export declare const CHAIN_ID_ENV = "MINTID_CHAIN_ID";
|
|
12
|
+
/** A random delay before the revocation is built and sent: up to a minute by default. */
|
|
13
|
+
export declare const DEFAULT_JITTER_MS = 60000;
|
|
14
|
+
/** The confirmation read: about one block, in a few short reads. */
|
|
15
|
+
export declare const CONFIRM_ATTEMPTS = 4;
|
|
16
|
+
export declare const CONFIRM_INTERVAL_MS = 750;
|
|
17
|
+
/**
|
|
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.
|
|
24
|
+
*/
|
|
25
|
+
export declare const EFFECT_BOUND: {
|
|
26
|
+
readonly statusRootSeconds: 30;
|
|
27
|
+
readonly rootAcceptedSeconds: 180;
|
|
28
|
+
readonly agentCascadeSeconds: 240;
|
|
29
|
+
};
|
|
30
|
+
/** What the relay answered, in the contract's own words. */
|
|
31
|
+
export type RelayOutcome = 'recorded' | 'queued' | 'inclusion_timeout' | 'nullifier_recorded' | 'duplicate_submission';
|
|
32
|
+
export type RelayAnswer = {
|
|
33
|
+
outcome: RelayOutcome;
|
|
34
|
+
txHash?: string;
|
|
35
|
+
height?: number;
|
|
36
|
+
};
|
|
37
|
+
/** A relay refusal: the contract's bare reason code and stage, nothing of the message. */
|
|
38
|
+
export declare class RelayRefusedError extends HolderError {
|
|
39
|
+
readonly status: number;
|
|
40
|
+
readonly code: string;
|
|
41
|
+
readonly stage: string;
|
|
42
|
+
/** Worth a retry, here or at another relay (`busy`, `chain_unavailable`, caps). */
|
|
43
|
+
readonly retryable: boolean;
|
|
44
|
+
readonly retryAfterMs: number | undefined;
|
|
45
|
+
constructor(status: number, code: string, stage: string, retryAfterMs?: number);
|
|
46
|
+
}
|
|
47
|
+
export type SubmitOptions = {
|
|
48
|
+
/** The relay's own transport — never the holder's issuer transport. */
|
|
49
|
+
fetch?: Fetch;
|
|
50
|
+
timeoutMs?: number;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Hands the self-revocation to the privacy relay: `{message}` with the five
|
|
54
|
+
* 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.
|
|
59
|
+
*/
|
|
60
|
+
export declare function submitSelfRevocation(relayUrl: string, revocation: SelfRevocation, options?: SubmitOptions): Promise<RelayAnswer>;
|
|
61
|
+
export type ConfirmOptions = {
|
|
62
|
+
/** The chain read's transport — a path of its own if the caller's address matters. */
|
|
63
|
+
fetch?: Fetch;
|
|
64
|
+
attempts?: number;
|
|
65
|
+
intervalMs?: number;
|
|
66
|
+
sleep?: (ms: number) => Promise<void>;
|
|
67
|
+
};
|
|
68
|
+
export type Confirmation = {
|
|
69
|
+
state: RecordedState;
|
|
70
|
+
} | {
|
|
71
|
+
state: 'unconfirmed';
|
|
72
|
+
reason: 'no_chain_endpoint' | 'chain_unreachable' | 'not_recorded';
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
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).
|
|
81
|
+
*/
|
|
82
|
+
export declare function confirmSelfRevocation(rpcUrl: string | undefined, revocation: SelfRevocation, options?: ConfirmOptions): Promise<Confirmation>;
|
|
83
|
+
/** What the kill switch reports to the seller — value-free. */
|
|
84
|
+
export type KillSwitchReport = {
|
|
85
|
+
/**
|
|
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.
|
|
92
|
+
*/
|
|
93
|
+
state: 'revoked' | 'pending' | 'unconfirmed';
|
|
94
|
+
relay: RelayOutcome;
|
|
95
|
+
chain: RecordedState | 'not_recorded' | 'unread';
|
|
96
|
+
reason?: 'no_chain_endpoint' | 'chain_unreachable' | 'not_recorded';
|
|
97
|
+
txHash?: string;
|
|
98
|
+
height?: number;
|
|
99
|
+
/** The holder refuses to present from now on, whatever the chain shows. */
|
|
100
|
+
holderRefusesToPresent: true;
|
|
101
|
+
effect: typeof EFFECT_BOUND;
|
|
102
|
+
};
|
|
103
|
+
export type KillSwitchOptions = {
|
|
104
|
+
relayUrl?: string;
|
|
105
|
+
chainRpc?: string;
|
|
106
|
+
chainId?: string;
|
|
107
|
+
/** Upper bound of the random delay before the revocation is built and sent; 0 disables it. */
|
|
108
|
+
jitterMs?: number;
|
|
109
|
+
/** The relay's transport; the chain read's transport. Neither is the issuer's. */
|
|
110
|
+
relayFetch?: Fetch;
|
|
111
|
+
chainFetch?: Fetch;
|
|
112
|
+
confirmAttempts?: number;
|
|
113
|
+
confirmIntervalMs?: number;
|
|
114
|
+
sleep?: (ms: number) => Promise<void>;
|
|
115
|
+
/** Uniform in [0, 1); injected by tests. */
|
|
116
|
+
random?: () => number;
|
|
117
|
+
};
|
|
118
|
+
/**
|
|
119
|
+
* The product kill switch, end to end, where the key is:
|
|
120
|
+
*
|
|
121
|
+
* 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.
|
|
132
|
+
*
|
|
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.
|
|
138
|
+
*/
|
|
139
|
+
export declare function revokeCredential(holder: SellerHolder, options?: KillSwitchOptions): Promise<KillSwitchReport>;
|