@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.
@@ -0,0 +1,173 @@
1
+ #!/usr/bin/env node
2
+ // The kill switch as a command the seller runs on its middleware host —
3
+ // where the custody file and the payment key live, never at the
4
+ // verification service. It loads the credential from custody, revokes it
5
+ // through the privacy relay, confirms on the chain, prints one value-free
6
+ // JSON report, and optionally tells the seller's dashboard record that the
7
+ // credential was revoked (state only; the dashboard learns nothing else).
8
+ //
9
+ // MINTID_HOLDER_BIN=/usr/local/bin/mintid-holder \
10
+ // CREDENTIAL_CUSTODY_FILE=/var/lib/seller/credential.custody \
11
+ // CREDENTIAL_CUSTODY_KEY=<64 hex> \
12
+ // MINTID_RELAY_URL=https://relay.example MINTID_CHAIN_RPC=https://rpc.example \
13
+ // node --experimental-strip-types src/holder/cli.ts --yes
14
+ //
15
+ // Flags: --yes (required: the revocation is irreversible), --no-jitter or
16
+ // --jitter-ms <n> (the random delay before the submission, up to a minute
17
+ // by default), --report (tell the dashboard, with VERIFICATION_BASE_URL,
18
+ // VERIFICATION_SELLER_ID and VERIFICATION_SESSION — the dashboard's short-lived session
19
+ // credential — set). Nothing here logs; stdout carries the report only.
20
+ import { pathToFileURL } from 'node:url';
21
+ import { custodyKeyFromHex, fileCustodyStore, sealedCustodyStore, } from "./custody.js";
22
+ import { HolderError } from "./errors.js";
23
+ import { SellerHolder } from "./holder.js";
24
+ import { CHAIN_ID_ENV, CHAIN_RPC_ENV, RELAY_URL_ENV, RelayRefusedError, revokeCredential, } from "./killswitch.js";
25
+ import { HOLDER_BINARY_ENV, HolderProcess } from "./process.js";
26
+ export const CUSTODY_FILE_ENV = 'CREDENTIAL_CUSTODY_FILE';
27
+ export const CUSTODY_KEY_ENV = 'CREDENTIAL_CUSTODY_KEY';
28
+ export const DASHBOARD_URL_ENV = 'VERIFICATION_BASE_URL';
29
+ export const DASHBOARD_SELLER_ENV = 'VERIFICATION_SELLER_ID';
30
+ export const DASHBOARD_SESSION_ENV = 'VERIFICATION_SESSION';
31
+ /** The credential state the seller's record takes at the dashboard service. */
32
+ export const DASHBOARD_REVOKED_STATE = 'self_revoked';
33
+ const NEXT_STEP = 'Open the dashboard and press "Revoke my credential" → "My middleware revoked it" ' +
34
+ 'so your record shows it; then re-verify when you want a fresh credential.';
35
+ function flag(argv, name) {
36
+ return argv.includes(name);
37
+ }
38
+ function value(argv, name) {
39
+ const at = argv.indexOf(name);
40
+ return at === -1 ? undefined : argv[at + 1];
41
+ }
42
+ /**
43
+ * Runs the kill switch; resolves to the process exit code. `0`: the
44
+ * credential is reported `revoked` or `pending`; `2`: `unconfirmed` (the
45
+ * holder refuses to present already; check again or resubmit — both are
46
+ * safe); `1`: refused before anything was submitted, or misuse.
47
+ */
48
+ export async function runKillSwitch(argv, env, io, deps = {}) {
49
+ if (!flag(argv, '--yes')) {
50
+ io.err('This revokes your credential for good: from the next status root on it presents nowhere, ' +
51
+ 'and only a new verification gives you a fresh one. Run again with --yes.');
52
+ return 1;
53
+ }
54
+ const relayUrl = env[RELAY_URL_ENV];
55
+ if (!relayUrl) {
56
+ io.err(`${RELAY_URL_ENV} is not set: the revocation travels through the privacy relay only.`);
57
+ return 1;
58
+ }
59
+ const custodyFile = env[CUSTODY_FILE_ENV];
60
+ if (!custodyFile && !deps.custody) {
61
+ io.err(`${CUSTODY_FILE_ENV} is not set: where the custody encoding lives.`);
62
+ return 1;
63
+ }
64
+ let custody = deps.custody ?? fileCustodyStore(custodyFile ?? '');
65
+ const keyHex = env[CUSTODY_KEY_ENV];
66
+ if (keyHex) {
67
+ try {
68
+ custody = sealedCustodyStore(custody, custodyKeyFromHex(keyHex));
69
+ }
70
+ catch {
71
+ io.err(`${CUSTODY_KEY_ENV} must be 32 bytes as hex.`);
72
+ return 1;
73
+ }
74
+ }
75
+ const options = { relayUrl };
76
+ const chainRpc = env[CHAIN_RPC_ENV];
77
+ if (chainRpc)
78
+ options.chainRpc = chainRpc;
79
+ const chainId = env[CHAIN_ID_ENV];
80
+ if (chainId)
81
+ options.chainId = chainId;
82
+ if (flag(argv, '--no-jitter'))
83
+ options.jitterMs = 0;
84
+ const jitter = value(argv, '--jitter-ms');
85
+ if (jitter !== undefined) {
86
+ const ms = Number(jitter);
87
+ if (!Number.isFinite(ms) || ms < 0) {
88
+ io.err('--jitter-ms takes a non-negative number of milliseconds.');
89
+ return 1;
90
+ }
91
+ options.jitterMs = ms;
92
+ }
93
+ const rpc = deps.rpc ?? HolderProcess.start(env[HOLDER_BINARY_ENV]);
94
+ const fetchImpl = deps.fetch ?? fetch;
95
+ let report;
96
+ try {
97
+ const holder = await SellerHolder.load(rpc, { custody, fetch: fetchImpl });
98
+ try {
99
+ report = await (deps.revoke ?? revokeCredential)(holder, options);
100
+ }
101
+ finally {
102
+ await holder.close().catch(() => undefined);
103
+ }
104
+ }
105
+ catch (err) {
106
+ if (err instanceof RelayRefusedError) {
107
+ io.err(`relay refused at stage ${err.stage}: ${err.code}` +
108
+ (err.retryable ? ' — worth a retry, here or at another relay' : '') +
109
+ '. The holder refuses to present from now on regardless.');
110
+ return 2;
111
+ }
112
+ if (err instanceof HolderError) {
113
+ io.err(`${err.name}: ${err.message}`);
114
+ return 1;
115
+ }
116
+ throw err;
117
+ }
118
+ finally {
119
+ if (!deps.rpc)
120
+ await rpc.close();
121
+ }
122
+ const out = { ...report, dashboard: 'not_reported' };
123
+ if (flag(argv, '--report')) {
124
+ const base = env[DASHBOARD_URL_ENV];
125
+ const sellerId = env[DASHBOARD_SELLER_ENV];
126
+ const session = env[DASHBOARD_SESSION_ENV];
127
+ if (!base || !sellerId || !session) {
128
+ io.err(`--report needs ${DASHBOARD_URL_ENV}, ${DASHBOARD_SELLER_ENV} and ${DASHBOARD_SESSION_ENV}.`);
129
+ out.next = NEXT_STEP;
130
+ }
131
+ else {
132
+ out.dashboard = (await reportToDashboard(fetchImpl, base, sellerId, session))
133
+ ? 'reported'
134
+ : 'report_failed';
135
+ if (out.dashboard === 'report_failed')
136
+ out.next = NEXT_STEP;
137
+ }
138
+ }
139
+ else {
140
+ out.next = NEXT_STEP;
141
+ }
142
+ io.out(JSON.stringify(out));
143
+ return out.state === 'unconfirmed' ? 2 : 0;
144
+ }
145
+ /**
146
+ * Tells the seller's own record at the dashboard service that the
147
+ * credential was revoked — the state and nothing else: no value, no
148
+ * transaction, no time of the seller's choosing. The service keeps the
149
+ * state change and writes nothing next to it.
150
+ */
151
+ export async function reportToDashboard(fetchImpl, baseUrl, sellerId, session) {
152
+ try {
153
+ const response = await fetchImpl(`${baseUrl.replace(/\/+$/, '')}/v0/sellers/${encodeURIComponent(sellerId)}/credential`, {
154
+ method: 'POST',
155
+ headers: { 'content-type': 'application/json', authorization: `Bearer ${session}` },
156
+ body: JSON.stringify({ state: DASHBOARD_REVOKED_STATE }),
157
+ signal: AbortSignal.timeout(15_000),
158
+ });
159
+ return response.ok;
160
+ }
161
+ catch {
162
+ return false;
163
+ }
164
+ }
165
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
166
+ runKillSwitch(process.argv.slice(2), process.env, {
167
+ out: (line) => process.stdout.write(line + '\n'),
168
+ err: (line) => process.stderr.write(line + '\n'),
169
+ }).then((code) => process.exit(code), (err) => {
170
+ process.stderr.write(`${err instanceof Error ? err.name : 'error'}\n`);
171
+ process.exit(1);
172
+ });
173
+ }
@@ -0,0 +1,20 @@
1
+ /** Where the custody encoding lives. `load` resolves `undefined` when nothing is stored. */
2
+ export interface CustodyStore {
3
+ load(): Promise<string | undefined>;
4
+ save(encoding: string): Promise<void>;
5
+ clear(): Promise<void>;
6
+ }
7
+ /** In-process only: for tests and for a seller that keeps the encoding elsewhere itself. */
8
+ export declare function memoryCustodyStore(initial?: string): CustodyStore;
9
+ /** One file, owner-readable only (0600), created with its directory. */
10
+ export declare function fileCustodyStore(path: string): CustodyStore;
11
+ /** The seller's key: 32 raw bytes, given directly or fetched when needed (KMS/HSM). */
12
+ export type CustodyKey = Uint8Array | (() => Promise<Uint8Array>);
13
+ /**
14
+ * Wraps another store so that what it holds is AES-256-GCM ciphertext under
15
+ * the seller's key (`v1.<nonce>.<ciphertext>`, base64url). A wrong key or a
16
+ * tampered record opens to nothing: `load` rejects, it never returns garbage.
17
+ */
18
+ export declare function sealedCustodyStore(inner: CustodyStore, key: CustodyKey): CustodyStore;
19
+ /** A 32-byte key from its hex form (64 hex characters), e.g. read from an environment secret. */
20
+ export declare function custodyKeyFromHex(hex: string): Uint8Array;
@@ -0,0 +1,112 @@
1
+ // Custody of the credential's storage encoding — the one serialization the
2
+ // holder library produces. It holds the link secret, so it is the seller's
3
+ // to keep: in the seller's own infrastructure, encrypted under a key the
4
+ // seller controls, next to the payment key it already protects. Nothing here
5
+ // ever leaves the seller's side; the verification service holds no copy.
6
+ //
7
+ // This module invents no cryptography: the sealed store wraps the platform's
8
+ // WebCrypto AES-256-GCM under a key the seller supplies (from an environment
9
+ // secret, a cloud KMS data key or an HSM-wrapped key — the key never travels
10
+ // through the holder module beyond the seal/open calls).
11
+ import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
12
+ import { dirname } from 'node:path';
13
+ /** In-process only: for tests and for a seller that keeps the encoding elsewhere itself. */
14
+ export function memoryCustodyStore(initial) {
15
+ let value = initial;
16
+ return {
17
+ load: async () => value,
18
+ save: async (encoding) => {
19
+ value = encoding;
20
+ },
21
+ clear: async () => {
22
+ value = undefined;
23
+ },
24
+ };
25
+ }
26
+ /** One file, owner-readable only (0600), created with its directory. */
27
+ export function fileCustodyStore(path) {
28
+ return {
29
+ load: async () => {
30
+ try {
31
+ return (await readFile(path, 'utf8')).trim() || undefined;
32
+ }
33
+ catch (err) {
34
+ if (err.code === 'ENOENT')
35
+ return undefined;
36
+ throw err;
37
+ }
38
+ },
39
+ save: async (encoding) => {
40
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 });
41
+ await writeFile(path, encoding + '\n', { mode: 0o600 });
42
+ },
43
+ clear: async () => {
44
+ await rm(path, { force: true });
45
+ },
46
+ };
47
+ }
48
+ // WebCrypto takes views over a plain ArrayBuffer; copy so a shared or
49
+ // pooled buffer never reaches it.
50
+ function plain(bytes) {
51
+ const out = new Uint8Array(new ArrayBuffer(bytes.byteLength));
52
+ out.set(bytes);
53
+ return out;
54
+ }
55
+ const SEAL_VERSION = 'v1';
56
+ /**
57
+ * Wraps another store so that what it holds is AES-256-GCM ciphertext under
58
+ * the seller's key (`v1.<nonce>.<ciphertext>`, base64url). A wrong key or a
59
+ * tampered record opens to nothing: `load` rejects, it never returns garbage.
60
+ */
61
+ export function sealedCustodyStore(inner, key) {
62
+ const subtle = globalThis.crypto.subtle;
63
+ const cryptoKey = async () => {
64
+ const raw = typeof key === 'function' ? await key() : key;
65
+ if (raw.byteLength !== 32)
66
+ throw new Error('custody key must be 32 bytes');
67
+ return subtle.importKey('raw', plain(raw), { name: 'AES-GCM' }, false, ['encrypt', 'decrypt']);
68
+ };
69
+ return {
70
+ load: async () => {
71
+ const sealed = await inner.load();
72
+ if (sealed === undefined)
73
+ return undefined;
74
+ const [version, nonce, ciphertext] = sealed.split('.');
75
+ if (version !== SEAL_VERSION || !nonce || !ciphertext) {
76
+ throw new Error('custody record is not a sealed encoding');
77
+ }
78
+ const opened = await subtle.decrypt({ name: 'AES-GCM', iv: fromBase64Url(nonce) }, await cryptoKey(), fromBase64Url(ciphertext));
79
+ return new TextDecoder().decode(opened);
80
+ },
81
+ save: async (encoding) => {
82
+ const nonce = globalThis.crypto.getRandomValues(new Uint8Array(12));
83
+ const ciphertext = await subtle.encrypt({ name: 'AES-GCM', iv: nonce }, await cryptoKey(), new TextEncoder().encode(encoding));
84
+ await inner.save(`${SEAL_VERSION}.${toBase64Url(nonce)}.${toBase64Url(new Uint8Array(ciphertext))}`);
85
+ },
86
+ clear: () => inner.clear(),
87
+ };
88
+ }
89
+ /** A 32-byte key from its hex form (64 hex characters), e.g. read from an environment secret. */
90
+ export function custodyKeyFromHex(hex) {
91
+ const clean = hex.trim().toLowerCase();
92
+ if (!/^[0-9a-f]{64}$/.test(clean))
93
+ throw new Error('custody key hex must be 64 hex characters');
94
+ const out = new Uint8Array(32);
95
+ for (let i = 0; i < 32; i++)
96
+ out[i] = parseInt(clean.slice(i * 2, i * 2 + 2), 16);
97
+ return out;
98
+ }
99
+ function toBase64Url(bytes) {
100
+ let binary = '';
101
+ for (const b of bytes)
102
+ binary += String.fromCharCode(b);
103
+ return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
104
+ }
105
+ function fromBase64Url(value) {
106
+ const base64 = value.replace(/-/g, '+').replace(/_/g, '/');
107
+ const binary = atob(base64);
108
+ const out = new Uint8Array(new ArrayBuffer(binary.length));
109
+ for (let i = 0; i < binary.length; i++)
110
+ out[i] = binary.charCodeAt(i);
111
+ return out;
112
+ }
@@ -0,0 +1,22 @@
1
+ export declare class HolderError extends Error {
2
+ constructor(message: string);
3
+ }
4
+ /** The hand-off is not a redeemable MintID identity offer (wrong class, shape or past its expiry). */
5
+ export declare class OfferError extends HolderError {
6
+ }
7
+ /** A refusal of the holder library process; `text` names no claim value. */
8
+ export declare class HolderLibraryError extends HolderError {
9
+ readonly op: string;
10
+ readonly text: string;
11
+ constructor(op: string, text: string);
12
+ }
13
+ /** A refused HTTP step against the issuer (metadata, grant, credential, witness). */
14
+ export declare class HolderStepError extends HolderError {
15
+ readonly step: string;
16
+ readonly status: number;
17
+ readonly body: unknown;
18
+ constructor(step: string, status: number, body: unknown);
19
+ }
20
+ /** The issuer no longer serves a witness for this credential: revoked, or expired with its verification. */
21
+ export declare class CredentialRevokedError extends HolderError {
22
+ }
@@ -0,0 +1,38 @@
1
+ // What the seller-side holder module throws. Every refusal is value-free: a
2
+ // step name, an HTTP status, a reason code or the holder library's own
3
+ // refusal text (which names an output key at most) — never a secret, never
4
+ // a custody encoding, never an attribute of the accountable party.
5
+ export class HolderError extends Error {
6
+ constructor(message) {
7
+ super(message);
8
+ this.name = new.target.name;
9
+ }
10
+ }
11
+ /** The hand-off is not a redeemable MintID identity offer (wrong class, shape or past its expiry). */
12
+ export class OfferError extends HolderError {
13
+ }
14
+ /** A refusal of the holder library process; `text` names no claim value. */
15
+ export class HolderLibraryError extends HolderError {
16
+ op;
17
+ text;
18
+ constructor(op, text) {
19
+ super(`${op}: ${text}`);
20
+ this.op = op;
21
+ this.text = text;
22
+ }
23
+ }
24
+ /** A refused HTTP step against the issuer (metadata, grant, credential, witness). */
25
+ export class HolderStepError extends HolderError {
26
+ step;
27
+ status;
28
+ body;
29
+ constructor(step, status, body) {
30
+ super(`${step}: HTTP ${status}`);
31
+ this.step = step;
32
+ this.status = status;
33
+ this.body = body;
34
+ }
35
+ }
36
+ /** The issuer no longer serves a witness for this credential: revoked, or expired with its verification. */
37
+ export class CredentialRevokedError extends HolderError {
38
+ }
@@ -0,0 +1,115 @@
1
+ import type { HolderRpc } from './process.js';
2
+ import type { CustodyStore } from './custody.js';
3
+ import type { ChallengeAnswer, HolderInfo, IssuedChallenge, PresentationEnvelope, PresentationRequest, SelfRevocation, SelfRevokeContext, SelfRevokeMaterial, WitnessState } from './types.js';
4
+ /** The identity credential's configuration id at the issuer's metadata. */
5
+ export declare const IDENTITY_CONFIGURATION_ID = "mintid_identity_v1";
6
+ export declare const CREDENTIAL_FORMAT = "mintid/anoncreds-v2";
7
+ export declare const PROOF_TYPE = "mintid_blind_request";
8
+ export declare const PRE_AUTHORIZED_GRANT = "urn:ietf:params:oauth:grant-type:pre-authorized_code";
9
+ export declare const METADATA_PATH = "/.well-known/openid-credential-issuer";
10
+ /** The issuer's public build information: names the chain it anchors on. */
11
+ export declare const BUILD_INFO_PATH = "/v0/build-info";
12
+ /** A presentation lives ten seconds from the challenge; the holder answers well inside it. */
13
+ export declare const PRESENTATION_WINDOW_MS = 10000;
14
+ export type Fetch = typeof fetch;
15
+ export type HolderOptions = {
16
+ fetch?: Fetch;
17
+ /** Where the custody encoding is kept; `persist()` writes to it, `issue`/`selfRevoke` do so too. */
18
+ custody?: CustodyStore;
19
+ };
20
+ /** The parsed operator hand-out: the offer object and, when the hand-off says, its expiry. */
21
+ export type CredentialOffer = {
22
+ offer: Record<string, unknown>;
23
+ expiresAt?: Date;
24
+ };
25
+ /**
26
+ * Reads the operator's hand-out in any of its forms — the dashboard's
27
+ * `{offer, expiresAt, credential}`, the issuer's `{credential_offer,
28
+ * expires_at}`, or the bare offer — and refuses anything that is not a
29
+ * pre-authorized MintID identity offer, or one already past its expiry.
30
+ */
31
+ export declare function readCredentialOffer(input: unknown, now?: Date): CredentialOffer;
32
+ export declare class SellerHolder {
33
+ readonly rpc: HolderRpc;
34
+ readonly handle: number;
35
+ private readonly fetchImpl;
36
+ private readonly custody;
37
+ private material;
38
+ private constructor();
39
+ /**
40
+ * Redeem the operator's offer, once: issuer metadata → the one-shot
41
+ * grant (with its nonce) → the library's blinded request → the
42
+ * credential → the library completes and holds it. The custody encoding
43
+ * is saved when a store is given. `OfferError` on a hand-out of another
44
+ * class, `HolderStepError` on a terminal issuer refusal (an offer past
45
+ * its verification's expiry answers 409 `session_reuse_expired`; the way
46
+ * back is a new verification, never a retry).
47
+ */
48
+ static issue(handout: unknown, rpc: HolderRpc, options?: HolderOptions): Promise<SellerHolder>;
49
+ /** Resume from the custody store (or a custody encoding given directly). */
50
+ static load(rpc: HolderRpc, options?: HolderOptions & {
51
+ encoding?: string;
52
+ }): Promise<SellerHolder>;
53
+ /** Custody encoding — it contains the link secret; keep it sealed, never log it. */
54
+ storage(): Promise<string>;
55
+ /** Writes the current custody encoding to the store, when one is configured. */
56
+ persist(): Promise<void>;
57
+ info(): Promise<HolderInfo>;
58
+ /**
59
+ * Fresh witness material from the issuer, adopted once a finalized root
60
+ * anchors it. Run it on your cadence — sized to how often roots are
61
+ * published — and right before presenting: a stale witness fails closed.
62
+ * `CredentialRevokedError` once the issuer serves none (revoked, or
63
+ * expired with its verification).
64
+ */
65
+ refreshWitness(): Promise<WitnessState>;
66
+ /**
67
+ * One bound, ten-second envelope for a verifier's challenge — the
68
+ * `createChallenge` answer as received (`{request, signature_hex}`) or
69
+ * its `request` object. The library refuses (naming an output key at
70
+ * most) when the credential cannot satisfy the policy: nothing is sent.
71
+ */
72
+ present(challenge: IssuedChallenge | PresentationRequest): Promise<PresentationEnvelope>;
73
+ /**
74
+ * The presentation endpoint's whole answer to the verification service's
75
+ * challenge (`POST` of the issued challenge, byte for byte): the witness
76
+ * is refreshed first, then one envelope is built. A witness endpoint that
77
+ * cannot be reached is not a refusal — the holder presents against the
78
+ * root it holds and the verifier decides. Refusals are value-free reason
79
+ * codes: `challenge_malformed` (400), `challenge_expired`,
80
+ * `credential_revoked`, `holder_refused` (409). Nothing of the exchange
81
+ * is logged here, and nothing is retried: the next cadence brings the
82
+ * next challenge.
83
+ */
84
+ answerChallenge(body: unknown, now?: Date): Promise<ChallengeAnswer>;
85
+ /** The library's own consistency check under a caller nonce (hex); rejects otherwise. */
86
+ selfCheck(nonce?: string): Promise<void>;
87
+ /** The self-revocation material cached by the last witness refresh, if any. */
88
+ selfRevokeContext(): SelfRevokeMaterial | undefined;
89
+ /**
90
+ * The chain the issuer anchors on, from its public build information
91
+ * (the issuer is where the witness endpoint lives). `HolderStepError`
92
+ * when it cannot be read.
93
+ */
94
+ chainId(): Promise<string>;
95
+ /**
96
+ * Builds the credential's self-revocation against the issuer's published
97
+ * anchoring of a finalized root — the material cached by the last witness
98
+ * refresh unless the caller passes its own. From this call on the holder
99
+ * refuses to present, also after a reload from custody (the store is
100
+ * updated). The message revokes nothing until a privacy relay submits it
101
+ * and the chain records it: `revokeCredential` (killswitch.ts) is the
102
+ * product kill switch that does both and reports; this method is the
103
+ * primitive under it.
104
+ */
105
+ selfRevoke(context: SelfRevokeContext): Promise<SelfRevocation>;
106
+ /** Forget the credential inside the process (its secret with it). The custody store is untouched. */
107
+ close(): Promise<void>;
108
+ }
109
+ /**
110
+ * Reads the verification service's `POST` body as an issued challenge:
111
+ * `{request, signature_hex}`, or the bare request. Returns `undefined` for
112
+ * anything that is not a presentation request (no nonce, no audience, no
113
+ * expiry, no policy) — the endpoint answers 400 and moves on.
114
+ */
115
+ export declare function readIssuedChallenge(body: unknown): IssuedChallenge | undefined;