@zadq/x402 0.0.0-stage → 0.2.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.
@@ -0,0 +1,257 @@
1
+ // The kill switch: the seller's holder revokes its own credential without
2
+ // naming it, through the network's privacy relay, and confirms the record
3
+ // on the chain itself. Restated from MintID's self-revocation relay contract
4
+ // (v0, provisional) and the holder library's confirmation rule, both cited
5
+ // by name. Runs where the key is — the seller's middleware host — never at
6
+ // the verification service, which holds no key and sees none of this.
7
+ //
8
+ // What leaves this host: the five fields of the self-revocation message,
9
+ // to the relay, over a connection of its own (never the one that talks to
10
+ // the issuer); and one question to a node of the chain that names the
11
+ // issuer only. What comes back from the relay is never the receipt: the
12
+ // credential is reported revoked or pending only once the chain shows the
13
+ // record, and unconfirmed otherwise, with the reason — resubmitting is
14
+ // safe (a record the chain already holds confirms the same way).
15
+ //
16
+ // Nothing here logs. The report is value-free: states, reason codes, the
17
+ // transaction hash and height the chain published anyway — never the
18
+ // revocation value, never the proof.
19
+ //
20
+ // CONTRACT POINT: the relay's path, answer shapes and reason
21
+ // codes handled in `submitSelfRevocation`, and the name of the material the
22
+ // issuer serves with a witness answer (`self_revoke_context`, holder.ts),
23
+ // are restated from MintID's relay contract v0 and holder-library rule and
24
+ // are to be confirmed against MintID's self-contained attachment on the
25
+ // card that tracks this integration, before the first live run.
26
+ import { HolderError, HolderStepError } from "./errors.js";
27
+ import { readRecordedNullifiers } from "./chain.js";
28
+ /** The relay's submission path (relay contract v0). */
29
+ export const RELAY_SUBMIT_PATH = '/v1/self-revocations';
30
+ /** Environment variables the middleware host is configured with. */
31
+ export const RELAY_URL_ENV = 'MINTID_RELAY_URL';
32
+ export const CHAIN_RPC_ENV = 'MINTID_CHAIN_RPC';
33
+ export const CHAIN_ID_ENV = 'MINTID_CHAIN_ID';
34
+ /** A random delay before the revocation is built and sent: up to a minute by default. */
35
+ export const DEFAULT_JITTER_MS = 60_000;
36
+ /** The confirmation read: about one block, in a few short reads. */
37
+ export const CONFIRM_ATTEMPTS = 4;
38
+ export const CONFIRM_INTERVAL_MS = 750;
39
+ /**
40
+ * The published effect bound, restated from the substrate's operational
41
+ * rules: a status root every 30 s, a root accepted for at most 180 s (a
42
+ * presentation against an older root is refused, so a recorded revocation
43
+ * is effective everywhere within that bound once the issuer folds it at
44
+ * its next root); a revoked principal's agents follow over the next roots,
45
+ * about four minutes.
46
+ */
47
+ export const EFFECT_BOUND = {
48
+ statusRootSeconds: 30,
49
+ rootAcceptedSeconds: 180,
50
+ agentCascadeSeconds: 240,
51
+ };
52
+ /** A relay refusal: the contract's bare reason code and stage, nothing of the message. */
53
+ export class RelayRefusedError extends HolderError {
54
+ status;
55
+ code;
56
+ stage;
57
+ /** Worth a retry, here or at another relay (`busy`, `chain_unavailable`, caps). */
58
+ retryable;
59
+ retryAfterMs;
60
+ constructor(status, code, stage, retryAfterMs) {
61
+ super(`relay refused (${stage}): ${code}`);
62
+ this.status = status;
63
+ this.code = code;
64
+ this.stage = stage;
65
+ this.retryable = status === 429 || status === 503;
66
+ this.retryAfterMs = retryAfterMs;
67
+ }
68
+ }
69
+ /**
70
+ * Hands the self-revocation to the privacy relay: `{message}` with the five
71
+ * message fields only, no header beyond the content type. The relay checks
72
+ * the proof with the chain's rules, pays for the transaction and submits
73
+ * it; this side signs nothing and holds no account. `RelayRefusedError` on
74
+ * a refusal; every accepted answer (recorded, queued, already recorded,
75
+ * already in flight, broadcast but not yet seen) still needs the chain read.
76
+ */
77
+ export async function submitSelfRevocation(relayUrl, revocation, options = {}) {
78
+ const fetchImpl = options.fetch ?? fetch;
79
+ const m = revocation.message;
80
+ const body = {
81
+ message: {
82
+ issuer_id: m.issuer_id,
83
+ nullifier: m.nullifier,
84
+ status_epoch: m.status_epoch,
85
+ zk_authorization_proof: m.zk_authorization_proof,
86
+ proof_system_version: m.proof_system_version,
87
+ },
88
+ };
89
+ let response;
90
+ try {
91
+ response = await fetchImpl(relayUrl.replace(/\/+$/, '') + RELAY_SUBMIT_PATH, {
92
+ method: 'POST',
93
+ headers: { 'content-type': 'application/json' },
94
+ body: JSON.stringify(body),
95
+ signal: AbortSignal.timeout(options.timeoutMs ?? 60_000),
96
+ });
97
+ }
98
+ catch {
99
+ throw new RelayRefusedError(0, 'relay_unreachable', 'relay');
100
+ }
101
+ let answer = {};
102
+ try {
103
+ const text = await response.text();
104
+ answer = text ? JSON.parse(text) : {};
105
+ }
106
+ catch {
107
+ answer = {};
108
+ }
109
+ const error = typeof answer['error'] === 'string' ? answer['error'] : '';
110
+ switch (response.status) {
111
+ case 200:
112
+ if (answer['status'] === 'recorded') {
113
+ const out = { outcome: 'recorded' };
114
+ if (typeof answer['tx_hash'] === 'string')
115
+ out.txHash = answer['tx_hash'];
116
+ if (typeof answer['height'] === 'number')
117
+ out.height = answer['height'];
118
+ return out;
119
+ }
120
+ break;
121
+ case 202:
122
+ return { outcome: 'queued' };
123
+ case 409:
124
+ if (error === 'nullifier_recorded' || error === 'duplicate_submission') {
125
+ return { outcome: error };
126
+ }
127
+ break;
128
+ case 504:
129
+ return { outcome: 'inclusion_timeout' };
130
+ }
131
+ const retryAfter = Number(response.headers.get('retry-after') ?? '');
132
+ throw new RelayRefusedError(response.status, error || 'unexpected_answer', typeof answer['stage'] === 'string' ? answer['stage'] : 'relay', Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter * 1000 : undefined);
133
+ }
134
+ /**
135
+ * Asks a node of the chain whether the revocation is there — for about one
136
+ * block — reading the issuer's recorded set and finding the holder's own
137
+ * value locally, so the node never learns which one. `pending` once
138
+ * recorded, `folded` once the issuer's next root has folded it; otherwise
139
+ * `unconfirmed` with the reason (no node named, node unreachable, not
140
+ * recorded yet).
141
+ */
142
+ export async function confirmSelfRevocation(rpcUrl, revocation, options = {}) {
143
+ if (!rpcUrl)
144
+ return { state: 'unconfirmed', reason: 'no_chain_endpoint' };
145
+ const attempts = Math.max(1, options.attempts ?? CONFIRM_ATTEMPTS);
146
+ const interval = options.intervalMs ?? CONFIRM_INTERVAL_MS;
147
+ const sleep = options.sleep ?? defaultSleep;
148
+ const value = revocation.nullifier || revocation.message.nullifier;
149
+ let reason = 'chain_unreachable';
150
+ for (let attempt = 0; attempt < attempts; attempt++) {
151
+ if (attempt > 0)
152
+ await sleep(interval);
153
+ let recorded;
154
+ try {
155
+ recorded = await readRecordedNullifiers(rpcUrl, revocation.message.issuer_id, options.fetch ? { fetch: options.fetch } : {});
156
+ }
157
+ catch (err) {
158
+ if (!(err instanceof HolderStepError))
159
+ throw err;
160
+ reason = 'chain_unreachable';
161
+ continue;
162
+ }
163
+ const state = recorded.get(value);
164
+ if (state)
165
+ return { state };
166
+ reason = 'not_recorded';
167
+ }
168
+ return { state: 'unconfirmed', reason };
169
+ }
170
+ /**
171
+ * The product kill switch, end to end, where the key is:
172
+ *
173
+ * 1. a random delay, so the moment the seller decided is not the moment
174
+ * the chain records (the relay shuffles what it holds; this adds the
175
+ * seller's own slack);
176
+ * 2. a fresh witness from the issuer, so the self-revocation is built
177
+ * against the root the chain currently holds (the material comes with
178
+ * the witness answer; no other call);
179
+ * 3. the self-revocation, built by the holder library — from that moment
180
+ * the holder refuses to present, also after a reload from custody;
181
+ * 4. the submission through the privacy relay; a refusal for a root the
182
+ * chain no longer holds rebuilds against the current one, once;
183
+ * 5. the confirmation read on the chain, issuer-scoped.
184
+ *
185
+ * Configuration: `relayUrl` (or `MINTID_RELAY_URL`), `chainRpc` (or
186
+ * `MINTID_CHAIN_RPC`; without it the report is `unconfirmed`,
187
+ * `no_chain_endpoint`), `chainId` (or `MINTID_CHAIN_ID`, else the issuer's
188
+ * public build information). The client never bypasses the relay to the
189
+ * chain: it holds no account and signs nothing.
190
+ */
191
+ export async function revokeCredential(holder, options = {}) {
192
+ const relayUrl = options.relayUrl ?? process.env[RELAY_URL_ENV];
193
+ if (!relayUrl)
194
+ throw new HolderError(`no privacy relay configured (${RELAY_URL_ENV})`);
195
+ const chainRpc = options.chainRpc ?? process.env[CHAIN_RPC_ENV];
196
+ const sleep = options.sleep ?? defaultSleep;
197
+ const random = options.random ?? secureRandom;
198
+ const jitter = options.jitterMs ?? DEFAULT_JITTER_MS;
199
+ if (jitter > 0)
200
+ await sleep(Math.floor(random() * jitter));
201
+ const chainId = options.chainId ?? process.env[CHAIN_ID_ENV] ?? (await holder.chainId());
202
+ const build = async () => {
203
+ await holder.refreshWitness();
204
+ return holder.selfRevoke({ chain_id: chainId });
205
+ };
206
+ const submitOptions = options.relayFetch ? { fetch: options.relayFetch } : {};
207
+ let revocation = await build();
208
+ let relay;
209
+ try {
210
+ relay = await submitSelfRevocation(relayUrl, revocation, submitOptions);
211
+ }
212
+ catch (err) {
213
+ if (!(err instanceof RelayRefusedError) || err.code !== 'unknown_root')
214
+ throw err;
215
+ // The root the message names has left the chain's ring: once more,
216
+ // against the root it holds now.
217
+ revocation = await build();
218
+ relay = await submitSelfRevocation(relayUrl, revocation, submitOptions);
219
+ }
220
+ const confirmOptions = { sleep };
221
+ if (options.chainFetch)
222
+ confirmOptions.fetch = options.chainFetch;
223
+ if (options.confirmAttempts !== undefined)
224
+ confirmOptions.attempts = options.confirmAttempts;
225
+ if (options.confirmIntervalMs !== undefined)
226
+ confirmOptions.intervalMs = options.confirmIntervalMs;
227
+ const confirmation = await confirmSelfRevocation(chainRpc, revocation, confirmOptions);
228
+ const report = {
229
+ state: confirmation.state === 'folded'
230
+ ? 'revoked'
231
+ : confirmation.state === 'pending'
232
+ ? 'pending'
233
+ : 'unconfirmed',
234
+ relay: relay.outcome,
235
+ chain: confirmation.state === 'unconfirmed'
236
+ ? confirmation.reason === 'not_recorded'
237
+ ? 'not_recorded'
238
+ : 'unread'
239
+ : confirmation.state,
240
+ holderRefusesToPresent: true,
241
+ effect: EFFECT_BOUND,
242
+ };
243
+ if (confirmation.state === 'unconfirmed')
244
+ report.reason = confirmation.reason;
245
+ if (relay.txHash !== undefined)
246
+ report.txHash = relay.txHash;
247
+ if (relay.height !== undefined)
248
+ report.height = relay.height;
249
+ return report;
250
+ }
251
+ function defaultSleep(ms) {
252
+ return new Promise((resolve) => setTimeout(resolve, ms));
253
+ }
254
+ function secureRandom() {
255
+ const [word] = globalThis.crypto.getRandomValues(new Uint32Array(1));
256
+ return (word ?? 0) / 0x1_0000_0000;
257
+ }
@@ -0,0 +1,31 @@
1
+ /** Environment variable locating the binary when no path is given. */
2
+ export declare const HOLDER_BINARY_ENV = "MINTID_HOLDER_BIN";
3
+ /** The holder process protocol this module was written against (`version` op). */
4
+ export declare const HOLDER_PROTOCOL: {
5
+ readonly holder: "mintid-holder";
6
+ readonly storageVersion: 1;
7
+ };
8
+ export type HolderParams = Record<string, unknown>;
9
+ export interface HolderRpc {
10
+ /** One op; resolves to `result`, rejects with `HolderLibraryError`. */
11
+ call(op: string, params?: HolderParams): Promise<Record<string, unknown>>;
12
+ close(): Promise<void>;
13
+ }
14
+ export declare class HolderProcess implements HolderRpc {
15
+ private readonly child;
16
+ private readonly lines;
17
+ private readonly pending;
18
+ private next;
19
+ private exited;
20
+ private constructor();
21
+ /**
22
+ * Launch the holder library process. No path: `$MINTID_HOLDER_BIN`, then
23
+ * `mintid-holder` on PATH. Its stderr is inherited (diagnostics only; the
24
+ * library writes no secret there).
25
+ */
26
+ static start(binary?: string, args?: string[]): HolderProcess;
27
+ call(op: string, params?: HolderParams): Promise<Record<string, unknown>>;
28
+ private onLine;
29
+ /** End the process; every credential it held is gone with its secret. */
30
+ close(): Promise<void>;
31
+ }
@@ -0,0 +1,84 @@
1
+ // The holder library as a child process: MintID's `mintid-holder` binary
2
+ // speaks one JSON request per line on stdin and answers one JSON line on
3
+ // stdout (its `version` op names the holder and proof-core pins; custody
4
+ // storage version 1). This module is transport only — the link secret lives
5
+ // in the child process' memory and reaches this side only inside the custody
6
+ // encoding, on request. Zero runtime dependencies (node:child_process).
7
+ //
8
+ // `HolderRpc` is the seam: `HolderProcess` spawns the real binary, tests
9
+ // inject a scripted one.
10
+ import { spawn } from 'node:child_process';
11
+ import { createInterface } from 'node:readline';
12
+ import { HolderLibraryError } from "./errors.js";
13
+ /** Environment variable locating the binary when no path is given. */
14
+ export const HOLDER_BINARY_ENV = 'MINTID_HOLDER_BIN';
15
+ /** The holder process protocol this module was written against (`version` op). */
16
+ export const HOLDER_PROTOCOL = { holder: 'mintid-holder', storageVersion: 1 };
17
+ export class HolderProcess {
18
+ child;
19
+ lines;
20
+ pending = new Map();
21
+ next = 1;
22
+ exited = null;
23
+ constructor(child) {
24
+ this.child = child;
25
+ this.lines = createInterface({ input: child.stdout });
26
+ this.lines.on('line', (line) => this.onLine(line));
27
+ const fail = (error) => {
28
+ this.exited = error;
29
+ for (const p of this.pending.values())
30
+ p.reject(error);
31
+ this.pending.clear();
32
+ };
33
+ child.on('error', (e) => fail(new HolderLibraryError('process', e.message)));
34
+ child.on('exit', () => fail(new HolderLibraryError('process', 'holder process exited')));
35
+ }
36
+ /**
37
+ * Launch the holder library process. No path: `$MINTID_HOLDER_BIN`, then
38
+ * `mintid-holder` on PATH. Its stderr is inherited (diagnostics only; the
39
+ * library writes no secret there).
40
+ */
41
+ static start(binary, args = []) {
42
+ const path = binary || process.env[HOLDER_BINARY_ENV] || 'mintid-holder';
43
+ const child = spawn(path, args, { stdio: ['pipe', 'pipe', 'inherit'] });
44
+ return new HolderProcess(child);
45
+ }
46
+ call(op, params = {}) {
47
+ if (this.exited)
48
+ return Promise.reject(this.exited);
49
+ const id = this.next++;
50
+ return new Promise((resolve, reject) => {
51
+ this.pending.set(id, { op, resolve, reject });
52
+ this.child.stdin.write(JSON.stringify({ ...params, id, op }) + '\n');
53
+ });
54
+ }
55
+ onLine(line) {
56
+ let msg;
57
+ try {
58
+ msg = JSON.parse(line);
59
+ }
60
+ catch {
61
+ return;
62
+ }
63
+ const id = typeof msg.id === 'number' ? msg.id : -1;
64
+ const pending = this.pending.get(id);
65
+ if (!pending)
66
+ return;
67
+ this.pending.delete(id);
68
+ if (msg.ok === true) {
69
+ pending.resolve((msg.result ?? {}));
70
+ }
71
+ else {
72
+ pending.reject(new HolderLibraryError(pending.op, String(msg.error ?? '')));
73
+ }
74
+ }
75
+ /** End the process; every credential it held is gone with its secret. */
76
+ async close() {
77
+ if (this.exited)
78
+ return;
79
+ const done = new Promise((resolve) => this.child.once('exit', () => resolve()));
80
+ this.child.stdin.end();
81
+ await done;
82
+ this.lines.close();
83
+ }
84
+ }
@@ -0,0 +1,108 @@
1
+ /** One entry of a challenge's claim policy (contract v1 `ClaimPredicate`). */
2
+ export type ClaimPredicate = {
3
+ claim: string;
4
+ value: string | number | boolean;
5
+ operator?: '>=' | '<=' | '==';
6
+ };
7
+ /** The signed presentation request a verifier issues (contract v1, schema-exact key names). */
8
+ export type PresentationRequest = {
9
+ session_id: string;
10
+ nonce: string;
11
+ verifier_id: string;
12
+ audience: string;
13
+ request_key_id: string;
14
+ /** Unix seconds; a presentation lives ten seconds. */
15
+ expires_at: number;
16
+ finalized_chain_height: number;
17
+ issuer_policy: {
18
+ accepted_issuer_ids?: string[];
19
+ issuer_state_minimum?: string;
20
+ };
21
+ claim_policy: ClaimPredicate[];
22
+ same_credential_required: boolean;
23
+ profile?: 'agent_v1';
24
+ };
25
+ /** The verifier's `createChallenge` answer, forwarded to the holder byte for byte. */
26
+ export type IssuedChallenge = {
27
+ request: PresentationRequest;
28
+ signature_hex: string;
29
+ };
30
+ /** The holder's answer: opaque proof plus public outputs. Never personal data. */
31
+ export type PresentationEnvelope = {
32
+ proof_hex: string;
33
+ issuer_id: string;
34
+ status_root: string;
35
+ claimed_predicates: string[];
36
+ };
37
+ /** The holder's own view of its credential: never the link secret, never a date of birth. */
38
+ export type HolderInfo = {
39
+ issuer_id: string;
40
+ grade: number;
41
+ issued_at_epoch: number;
42
+ expires_at_epoch: number;
43
+ credential_id: string;
44
+ accumulator_leaf: string;
45
+ root_height: number | null;
46
+ status_root: string | null;
47
+ witness_endpoint: string;
48
+ profile?: 'identity_v1' | 'agent_v1';
49
+ /** Set once the holder self-revoked: it refuses to present from then on. */
50
+ self_revoked?: boolean;
51
+ };
52
+ /** What a witness refresh learned from the issuer. */
53
+ export type WitnessState = {
54
+ status: string;
55
+ usable: boolean;
56
+ refreshed: boolean;
57
+ rootHeight: number | null;
58
+ statusRoot: string | null;
59
+ };
60
+ /**
61
+ * The issuer's self-revocation material for a finalized root the credential
62
+ * is a member of, exactly as the issuer serves it with every usable witness
63
+ * (`self_revoke_context`): its anchored definition with the digest-leaf
64
+ * witness, the profile's self-revocation encryption key with its witness,
65
+ * and the root they name. Public material, the same for every holder of
66
+ * the issuer — nothing per credential.
67
+ */
68
+ export type SelfRevokeMaterial = Record<string, unknown> & {
69
+ status_root?: string;
70
+ };
71
+ /**
72
+ * What a self-revocation needs: the chain id and the issuer's anchoring
73
+ * material for a finalized root the credential is a member of. Omitted, the
74
+ * material cached by the last witness refresh is used.
75
+ */
76
+ export type SelfRevokeContext = {
77
+ chain_id: string;
78
+ anchoring?: SelfRevokeMaterial;
79
+ status_root?: string;
80
+ };
81
+ /**
82
+ * The self-revocation message the holder library builds for a privacy
83
+ * relay: it names neither the holder nor the credential. Building it
84
+ * revokes nothing by itself — a relay submits it and the chain records it.
85
+ */
86
+ export type SelfRevocation = {
87
+ message: {
88
+ issuer_id: string;
89
+ nullifier: string;
90
+ status_epoch: number;
91
+ zk_authorization_proof: string;
92
+ proof_system_version: string;
93
+ };
94
+ nullifier: string;
95
+ ciphertext: string;
96
+ };
97
+ /** What the presentation endpoint answers the verification service with. */
98
+ export type ChallengeAnswer = {
99
+ status: 200;
100
+ body: PresentationEnvelope;
101
+ } | {
102
+ status: 400 | 409 | 503;
103
+ body: {
104
+ error: string;
105
+ reason: string;
106
+ detail?: string;
107
+ };
108
+ };
@@ -0,0 +1,5 @@
1
+ // Wire shapes the seller's holder exchanges, restated from MintID's frozen
2
+ // verifier-service contract v1 (bIP-0001) and the holder library's process
3
+ // protocol. Key names are the contract's; 32-byte fields are lowercase hex.
4
+ // Nothing here carries an attribute of the accountable party.
5
+ export {};
@@ -0,0 +1,9 @@
1
+ export { BRAND, VERDICT_EXTENSION_KEY } from './brand.js';
2
+ export { CHAINS, DEFAULT_TIMEOUT_MS, DEPOSIT_DECLARATION_STATES, DEPOSIT_LEVELS, canonicalPayTo, canonicalSubject, canonicalVerdictPayload, fetchVerdict, freshness, isChain, keyIdFor, readDepositDeclaration, verifyVerdictSignature, type Chain, type DepositDeclaration, type DepositDeclarationState, type DepositLevel, type Freshness, type SignatureCheck, type Verdict, type VerdictOutcome, type VerdictReason, type VerdictSubject, } from './verdict.js';
3
+ export { IDENTITY_POLICY_EXTENSION, MINTID_IDENTITY_EXTENSION, PERIMETER_EXTENSION, readIdentityPolicy, readMintidIdentity, readPerimeterInvitation, withIdentityPolicy, withMintidIdentity, withPerimeterInvitation, withSellerIdentity, type ClaimPolicyEntry, type CredentialState, type IdentityPolicy, type MintidIdentityPolicy, type PaymentRequiredBody, type PerimeterInvitation, type SellerIdentity, } from './extension.js';
4
+ export { X_PAYMENT_HEADER, X_PAYMENT_IDENTITY_HEADER, attachIdentityProof, readIdentityProof, type IdentityProof, } from './header.js';
5
+ export { ATTESTATION_RECEIPT_FORMAT, attestationReceiptProof, canonicalJson, isReceipt, readAttestationReceipt, receiptSigningInput, verifyAttestationReceipt, type AttestationReceipt, type ReceiptCheck, } from './receipt.js';
6
+ 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 { SIGNATURE_HEADER, SIGNATURE_KEY_HEADER, WEBHOOK_VERSION, WEBHOOK_VERSION_HEADER, parseVerdictEvent, verifyWebhookSignature, webhookKeyId, type VerdictEvent, } from './webhook.js';
9
+ export * from './holder/index.js';
package/dist/index.js ADDED
@@ -0,0 +1,20 @@
1
+ // x402 extension helpers and explorer integration code (Apache-2.0). No
2
+ // default exports; every module is strict TypeScript with zero runtime
3
+ // dependencies and carries the graceful-degradation pattern: an integration
4
+ // keeps working when no verification response is available.
5
+ //
6
+ // Transport follows the bIP-0002 x402 profile of the MintID identity network
7
+ // (cited by name): identity policy in a `PaymentRequired` extension, proof alongside
8
+ // the `X-PAYMENT` header.
9
+ export { BRAND, VERDICT_EXTENSION_KEY } from "./brand.js";
10
+ export { CHAINS, DEFAULT_TIMEOUT_MS, DEPOSIT_DECLARATION_STATES, DEPOSIT_LEVELS, canonicalPayTo, canonicalSubject, canonicalVerdictPayload, fetchVerdict, freshness, isChain, keyIdFor, readDepositDeclaration, verifyVerdictSignature, } from "./verdict.js";
11
+ export { IDENTITY_POLICY_EXTENSION, MINTID_IDENTITY_EXTENSION, PERIMETER_EXTENSION, readIdentityPolicy, readMintidIdentity, readPerimeterInvitation, withIdentityPolicy, withMintidIdentity, withPerimeterInvitation, withSellerIdentity, } from "./extension.js";
12
+ export { X_PAYMENT_HEADER, X_PAYMENT_IDENTITY_HEADER, attachIdentityProof, readIdentityProof, } from "./header.js";
13
+ export { ATTESTATION_RECEIPT_FORMAT, attestationReceiptProof, canonicalJson, isReceipt, readAttestationReceipt, receiptSigningInput, verifyAttestationReceipt, } from "./receipt.js";
14
+ export { UNKNOWN_SIGNAL, createVerificationClient, toSignal, } from "./explorer.js";
15
+ export { fetchVerificationKeys, findKey } from "./keys.js";
16
+ export { SIGNATURE_HEADER, SIGNATURE_KEY_HEADER, WEBHOOK_VERSION, WEBHOOK_VERSION_HEADER, parseVerdictEvent, verifyWebhookSignature, webhookKeyId, } from "./webhook.js";
17
+ // The seller-side holder module (Node-only: it spawns MintID's holder
18
+ // library process). Import it from './holder/index.ts' directly in a
19
+ // platform-neutral build; it is re-exported here for Node integrations.
20
+ export * from "./holder/index.js";
package/dist/keys.d.ts ADDED
@@ -0,0 +1,17 @@
1
+ /** One published verification key. */
2
+ export type VerificationKey = {
3
+ /** First 8 bytes of the public key's SHA-256, hex — matches `Verdict.keyId`. */
4
+ keyId: string;
5
+ algorithm: 'Ed25519';
6
+ /** Raw 32-byte public key, base64. */
7
+ publicKey: string;
8
+ };
9
+ /**
10
+ * Fetches the published verification keys. Resolves to `undefined` — never
11
+ * rejects — on timeout, network failure, a non-OK response or a malformed body.
12
+ */
13
+ export declare function fetchVerificationKeys(baseUrl: string, options?: {
14
+ timeoutMs?: number;
15
+ }): Promise<VerificationKey[] | undefined>;
16
+ /** Picks the key a verdict was signed with, by `keyId`. */
17
+ export declare function findKey(keys: readonly VerificationKey[] | undefined, keyId: string | undefined): VerificationKey | undefined;
package/dist/keys.js ADDED
@@ -0,0 +1,45 @@
1
+ // Verification keys — the public half of the verdict-signing key, served on
2
+ // the free path (GET /v0/verification-keys) so any party can check verdict
3
+ // signatures locally with verdict.ts#verifyVerdictSignature.
4
+ //
5
+ // Graceful degradation, by design: an unreachable service resolves to
6
+ // `undefined` — never an exception. Without a key, a signature is simply
7
+ // "unverifiable" and your integration keeps working as before.
8
+ import { DEFAULT_TIMEOUT_MS } from "./verdict.js";
9
+ /**
10
+ * Fetches the published verification keys. Resolves to `undefined` — never
11
+ * rejects — on timeout, network failure, a non-OK response or a malformed body.
12
+ */
13
+ export async function fetchVerificationKeys(baseUrl, options) {
14
+ try {
15
+ const res = await fetch(new URL('/v0/verification-keys', baseUrl), {
16
+ signal: AbortSignal.timeout(options?.timeoutMs ?? DEFAULT_TIMEOUT_MS),
17
+ });
18
+ if (!res.ok) {
19
+ return undefined;
20
+ }
21
+ const body = (await res.json());
22
+ if (!Array.isArray(body.keys)) {
23
+ return undefined;
24
+ }
25
+ return body.keys.filter(isVerificationKey);
26
+ }
27
+ catch {
28
+ return undefined;
29
+ }
30
+ }
31
+ /** Picks the key a verdict was signed with, by `keyId`. */
32
+ export function findKey(keys, keyId) {
33
+ if (keys === undefined || keyId === undefined) {
34
+ return undefined;
35
+ }
36
+ return keys.find((k) => k.keyId === keyId);
37
+ }
38
+ function isVerificationKey(value) {
39
+ if (typeof value !== 'object' || value === null)
40
+ return false;
41
+ const v = value;
42
+ return (typeof v['keyId'] === 'string' &&
43
+ v['algorithm'] === 'Ed25519' &&
44
+ typeof v['publicKey'] === 'string');
45
+ }
@@ -0,0 +1,50 @@
1
+ import { type IdentityProof } from './header.js';
2
+ /** Format tag of the v0 seller-side proof. */
3
+ export declare const ATTESTATION_RECEIPT_FORMAT = "attestation-ref/v0";
4
+ /** The issuer's receipt, byte-shaped as the canonical payload (snake_case). */
5
+ export type AttestationReceipt = {
6
+ /** 32-byte opaque reference, hex. */
7
+ attestation_ref: string;
8
+ /** 32-byte issuer identifier, hex. */
9
+ issuer_id: string;
10
+ /** Chain-qualified rail identifier as the issuer recorded it. */
11
+ chain: string;
12
+ pay_to: string;
13
+ resource_url: string;
14
+ /** Substrate grade 1..4 (the public form is the single letter G). */
15
+ assurance_grade: number;
16
+ issued_at_unix: number;
17
+ /** Ed25519 by the issuer's registered status signing key, hex (64 bytes). */
18
+ signature: string;
19
+ };
20
+ /** Sender side: the companion-header proof carrying this receipt. */
21
+ export declare function attestationReceiptProof(receipt: AttestationReceipt): IdentityProof;
22
+ /**
23
+ * Receiver side: decodes a receipt from a companion-header proof. Returns
24
+ * `undefined` for any other format or a malformed payload — a normal state;
25
+ * proceed as without the header.
26
+ */
27
+ export declare function readAttestationReceipt(proof: IdentityProof | undefined): AttestationReceipt | undefined;
28
+ /** Structural check against the canonical payload schema (closed object). */
29
+ export declare function isReceipt(value: unknown): value is AttestationReceipt;
30
+ /** Outcome of a local receipt check. */
31
+ export type ReceiptCheck = 'valid' | 'invalid' | 'unverifiable';
32
+ /**
33
+ * Verifies the issuer's signature locally: Ed25519 over
34
+ * `"mintid/seller-attestation/v0" || 0x00 || SHA-256(canonical JSON of the
35
+ * seven signed fields)` with the issuer's registered status signing key (raw
36
+ * 32 bytes, hex). The key is read from the substrate's issuer registry and
37
+ * pinned by the consumer — never hard-coded here, never taken from the
38
+ * receipt itself.
39
+ *
40
+ * `valid` proves issuance by that key. It says nothing about standing.
41
+ */
42
+ export declare function verifyAttestationReceipt(receipt: AttestationReceipt, issuerPublicKeyHex: string): Promise<ReceiptCheck>;
43
+ /** The exact bytes the issuer signed (domain-separated digest of the canonical payload). */
44
+ export declare function receiptSigningInput(receipt: AttestationReceipt): Promise<Uint8Array<ArrayBuffer>>;
45
+ /**
46
+ * Canonical JSON as the issuer produces it: keys sorted by code point, no
47
+ * whitespace, every non-ASCII code unit escaped as lowercase `\uXXXX`
48
+ * (the form Python's json.dumps emits with sort_keys and compact separators).
49
+ */
50
+ export declare function canonicalJson(value: Record<string, string | number>): string;