@x1id/resolve 0.2.1 → 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.
@@ -0,0 +1,278 @@
1
+ /**
2
+ * Domain/social verification attestations (`Attestation` accounts, #7379) —
3
+ * read WITH the universal staleness rule from docs/record-trust.md
4
+ * structurally enforced, plus instruction builders for `set_attestor` /
5
+ * `create_attestation` / `close_attestation`.
6
+ *
7
+ * Hand-rolled like the rest of this package — no Anchor client, no
8
+ * `@solana/web3.js` import (it stays an optional peer). Builders return the
9
+ * transport-neutral {@link BuiltInstruction} `delegate.ts` defines; PDAs are
10
+ * NOT derived here (see delegate.ts's module docs for why — derive
11
+ * `["attestation", handlePda, kindByte]` / `["attestor_config"]` with your
12
+ * runtime's canonical `findProgramAddress`).
13
+ *
14
+ * # What an attestation proves — and, honestly, what it does not
15
+ *
16
+ * An `Attestation` proves exactly one statement: **"the key configured in
17
+ * `AttestorConfig` — the x1id review process — attested this evidence at
18
+ * time `attestedAt`."** It is NOT a trustless proof of domain or social
19
+ * control: the verification (DNS lookup, social-post check, human review)
20
+ * happens OFF-chain, and the chain records only that the attestor key signed
21
+ * off on it. That key is admin-rotatable, so the trust anchor is "whoever
22
+ * the registry admin currently designates" — rotatable-key trust, not
23
+ * trustlessness. Consumers needing stronger guarantees must not render an
24
+ * attestation as more than it is.
25
+ *
26
+ * # Verified = ONE live attestation of EITHER kind
27
+ *
28
+ * Per the #7145 decision (a single strong signal is enough — requiring two
29
+ * would reject Nike proving control of nike.com), a handle is "verified"
30
+ * when at least one NON-STALE attestation of either kind exists; `kind`
31
+ * records which signal proved it. {@link isHandleVerified} implements
32
+ * exactly this.
33
+ *
34
+ * # Why every function here demands `registeredAt` — epoch-bound, no TTL
35
+ *
36
+ * Attestations do not expire on a timer (owner decision 2026-09-03); they
37
+ * are invalidated by OWNERSHIP EPOCH. A `Handle`'s address is
38
+ * `["handle", name]` — a pure function of the name — so release +
39
+ * re-register lands the new registration at the SAME pubkey, and the
40
+ * previous owner's attestation is physically attached to the new owner's
41
+ * name with no action by anyone. The mandatory read-side rule
42
+ * (docs/record-trust.md, the universal rule):
43
+ *
44
+ * attestation.attested_at >= handle.registered_at
45
+ *
46
+ * An attestation that fails it belongs to a previous, unrelated owner and
47
+ * must never be rendered as verifying the current one. Like `records.ts`,
48
+ * there is deliberately no way to decode or fetch an attestation through
49
+ * this module without the handle's `registered_at` in hand. (The one
50
+ * documented blind spot is shared with `Handle.owner`/`Primary`/
51
+ * `RecordDelegate`: a bearer-NFT marketplace trade bumps no epoch, so the
52
+ * attestation keeps reading live until the attestor re-reviews or revokes.)
53
+ */
54
+ import { encodeBase58 } from "./base58.js";
55
+ import { decodeBase58_32 } from "./base58.js";
56
+ /** Seed prefix of an attestation PDA: `["attestation", handlePda, kindByte]`. */
57
+ export const ATTESTATION_SEED = "attestation";
58
+ /** Seed of the attestor-config singleton PDA: `["attestor_config"]`. */
59
+ export const ATTESTOR_CONFIG_SEED = "attestor_config";
60
+ /** `Attestation.kind` — domain control proven (DNS TXT challenge). */
61
+ export const ATTESTATION_KIND_DNS = 0;
62
+ /** `Attestation.kind` — social-account control proven. */
63
+ export const ATTESTATION_KIND_SOCIAL = 1;
64
+ /** Anchor account discriminator: `sha256("account:Attestation")[0..8]`.
65
+ * Pinned (this SDK is zero-dependency and cannot assume WebCrypto SHA-256
66
+ * everywhere it runs); asserted against a re-derivation in the test suite
67
+ * so a typo can never silently pass. */
68
+ export const ATTESTATION_DISCRIMINATOR = Uint8Array.from([
69
+ 152, 125, 183, 86, 36, 146, 121, 73,
70
+ ]);
71
+ /** Anchor account discriminator: `sha256("account:AttestorConfig")[0..8]`. */
72
+ export const ATTESTOR_CONFIG_DISCRIMINATOR = Uint8Array.from([
73
+ 72, 128, 1, 99, 238, 231, 80, 72,
74
+ ]);
75
+ /** Anchor instruction discriminator: `sha256("global:set_attestor")[0..8]`. */
76
+ export const SET_ATTESTOR_DISCRIMINATOR = Uint8Array.from([
77
+ 95, 11, 236, 157, 234, 146, 163, 237,
78
+ ]);
79
+ /** Anchor instruction discriminator: `sha256("global:create_attestation")[0..8]`. */
80
+ export const CREATE_ATTESTATION_DISCRIMINATOR = Uint8Array.from([
81
+ 49, 24, 67, 80, 12, 249, 96, 239,
82
+ ]);
83
+ /** Anchor instruction discriminator: `sha256("global:close_attestation")[0..8]`. */
84
+ export const CLOSE_ATTESTATION_DISCRIMINATOR = Uint8Array.from([
85
+ 249, 84, 133, 23, 48, 175, 252, 221,
86
+ ]);
87
+ /** `Attestation` account size — every field is fixed-width, so unlike a
88
+ * `Handle` the account is exactly this long:
89
+ * disc(8) + handle(32) + kind(1) + evidence_hash(32) + attested_at(8)
90
+ * + attestor(32) + bump(1). */
91
+ export const ATTESTATION_LEN = 114;
92
+ /** `AttestorConfig` account size: disc(8) + attestor(32) + bump(1). */
93
+ export const ATTESTOR_CONFIG_LEN = 41;
94
+ /** Byte offset of `handle` within an Attestation account. */
95
+ const ATTESTATION_HANDLE_OFFSET = 8;
96
+ const SYSTEM_PROGRAM = "11111111111111111111111111111111";
97
+ function toBytes32(v, what) {
98
+ if (typeof v === "string") {
99
+ const b = decodeBase58_32(v);
100
+ if (!b)
101
+ throw new Error(`${what} is not a valid base58 address`);
102
+ return b;
103
+ }
104
+ if (v.length !== 32)
105
+ throw new Error(`${what} must be exactly 32 bytes`);
106
+ return v;
107
+ }
108
+ function toBase58(v, what) {
109
+ // Round-trip through bytes so a non-canonical base58 spelling and a byte
110
+ // input both come out identically.
111
+ return encodeBase58(toBytes32(v, what));
112
+ }
113
+ /** Which verification signal an attestation `kind` byte names, or null for a
114
+ * kind this SDK does not know (future program versions may add kinds). */
115
+ export function attestationKindName(kind) {
116
+ if (kind === ATTESTATION_KIND_DNS)
117
+ return "dns";
118
+ if (kind === ATTESTATION_KIND_SOCIAL)
119
+ return "social";
120
+ return null;
121
+ }
122
+ /**
123
+ * Decode one Attestation account.
124
+ *
125
+ * `registeredAt` is the owning Handle's `registered_at`, decoded by the
126
+ * caller from the Handle account — it decides `stale`. There is
127
+ * intentionally no overload without it (see the module docs).
128
+ *
129
+ * Returns null for anything that is not an Attestation: wrong length or
130
+ * wrong discriminator.
131
+ */
132
+ export function decodeAttestation(raw, account, registeredAt) {
133
+ if (raw.length !== ATTESTATION_LEN)
134
+ return null;
135
+ for (let i = 0; i < 8; i++) {
136
+ if (raw[i] !== ATTESTATION_DISCRIMINATOR[i])
137
+ return null;
138
+ }
139
+ const dv = new DataView(raw.buffer, raw.byteOffset, raw.byteLength);
140
+ let o = 8;
141
+ const handle = encodeBase58(raw.slice(o, o + 32));
142
+ o += 32;
143
+ const kind = raw[o];
144
+ o += 1;
145
+ const evidenceHash = raw.slice(o, o + 32);
146
+ o += 32;
147
+ const attestedAt = dv.getBigInt64(o, true);
148
+ o += 8;
149
+ const attestor = encodeBase58(raw.slice(o, o + 32));
150
+ return {
151
+ account,
152
+ handle,
153
+ kind,
154
+ kindName: attestationKindName(kind),
155
+ evidenceHash,
156
+ attestedAt,
157
+ attestor,
158
+ stale: attestedAt < registeredAt,
159
+ };
160
+ }
161
+ /** The attestations that vouch for the CURRENT owner — `stale` ones
162
+ * excluded. This is the list to judge verification from. */
163
+ export function liveAttestations(attestations) {
164
+ return attestations.filter((a) => !a.stale);
165
+ }
166
+ /**
167
+ * The #7145 verified rule: a handle is verified when at least ONE non-stale
168
+ * attestation of EITHER kind exists (a single strong signal is enough; the
169
+ * surviving `kindName`s say which signals proved it).
170
+ */
171
+ export function isHandleVerified(attestations) {
172
+ return attestations.some((a) => !a.stale);
173
+ }
174
+ /**
175
+ * Fetch every Attestation account of a handle — one `getProgramAccounts`
176
+ * call, filtered by the RPC on size (114), the Attestation discriminator at
177
+ * offset 0 and the handle pubkey at offset 8, then every byte re-checked
178
+ * locally (the node's filters are an optimisation, never the guarantee) —
179
+ * the exact shape of `fetchRecords`. Scoping the scan to the registry
180
+ * program id also IS the ownership check.
181
+ *
182
+ * `registeredAt` is `Handle.registered_at` as decoded from the Handle
183
+ * account the caller already has — the staleness rule needs it, and there
184
+ * is no variant of this function without it. Every attestation is returned,
185
+ * stale ones flagged, so an owner surface can show what a previous
186
+ * registration left behind; anything that renders a verified badge takes
187
+ * {@link isHandleVerified} / {@link liveAttestations}.
188
+ */
189
+ export async function fetchAttestations(rpc, programId, handleAccount, registeredAt) {
190
+ const res = (await rpc("getProgramAccounts", [
191
+ programId,
192
+ {
193
+ encoding: "base64",
194
+ commitment: "confirmed",
195
+ filters: [
196
+ { dataSize: ATTESTATION_LEN },
197
+ { memcmp: { offset: 0, bytes: encodeBase58(ATTESTATION_DISCRIMINATOR) } },
198
+ { memcmp: { offset: ATTESTATION_HANDLE_OFFSET, bytes: handleAccount } },
199
+ ],
200
+ },
201
+ ]));
202
+ const out = [];
203
+ for (const a of res ?? []) {
204
+ const bin = atob(a.account.data[0]);
205
+ const raw = new Uint8Array(bin.length);
206
+ for (let i = 0; i < bin.length; i++)
207
+ raw[i] = bin.charCodeAt(i);
208
+ const decoded = decodeAttestation(raw, a.pubkey, registeredAt);
209
+ // Defence in depth: the memcmp filter should guarantee the handle
210
+ // match, but a wrong offset would silently attribute someone else's
211
+ // attestation to this handle. A malformed account is skipped, not fatal.
212
+ if (decoded && decoded.handle === handleAccount)
213
+ out.push(decoded);
214
+ }
215
+ out.sort((x, y) => x.kind - y.kind);
216
+ return out;
217
+ }
218
+ /**
219
+ * Build `set_attestor` — admin-only: initialize or rotate the attestor key.
220
+ * Rotation does not void existing attestations (they record their signer as
221
+ * a historical fact); revoking a bad key's output is `close_attestation`.
222
+ */
223
+ export function buildSetAttestorIx(p) {
224
+ const data = new Uint8Array(8 + 32);
225
+ data.set(SET_ATTESTOR_DISCRIMINATOR, 0);
226
+ data.set(toBytes32(p.attestor, "attestor"), 8);
227
+ const keys = [
228
+ { pubkey: toBase58(p.admin, "admin"), isSigner: true, isWritable: true },
229
+ { pubkey: toBase58(p.config, "config"), isSigner: false, isWritable: false },
230
+ { pubkey: toBase58(p.attestorConfig, "attestorConfig"), isSigner: false, isWritable: true },
231
+ { pubkey: SYSTEM_PROGRAM, isSigner: false, isWritable: false },
232
+ ];
233
+ return { programId: toBase58(p.programId, "programId"), keys, data };
234
+ }
235
+ /**
236
+ * Build `create_attestation` — attestor-only: stamp (or re-stamp,
237
+ * re-deriving `attested_at` from the Clock and replacing the evidence hash)
238
+ * the (handle, kind) attestation.
239
+ */
240
+ export function buildCreateAttestationIx(p) {
241
+ if (!Number.isInteger(p.kind) || p.kind < 0 || p.kind > 1) {
242
+ throw new Error("kind must be 0 (dns) or 1 (social)");
243
+ }
244
+ if (p.evidenceHash.length !== 32) {
245
+ throw new Error("evidenceHash must be exactly 32 bytes (a sha256 digest)");
246
+ }
247
+ const data = new Uint8Array(8 + 1 + 32);
248
+ data.set(CREATE_ATTESTATION_DISCRIMINATOR, 0);
249
+ data[8] = p.kind;
250
+ data.set(p.evidenceHash, 9);
251
+ const keys = [
252
+ { pubkey: toBase58(p.attestor, "attestor"), isSigner: true, isWritable: true },
253
+ { pubkey: toBase58(p.attestorConfig, "attestorConfig"), isSigner: false, isWritable: false },
254
+ { pubkey: toBase58(p.handle, "handle"), isSigner: false, isWritable: false },
255
+ { pubkey: toBase58(p.attestation, "attestation"), isSigner: false, isWritable: true },
256
+ { pubkey: SYSTEM_PROGRAM, isSigner: false, isWritable: false },
257
+ ];
258
+ return { programId: toBase58(p.programId, "programId"), keys, data };
259
+ }
260
+ /**
261
+ * Build `close_attestation` — revoke: close the account, rent to
262
+ * `recipient`. Attestor- or admin-signed (the admin path is the cleanup for
263
+ * a rotated-away key's output).
264
+ */
265
+ export function buildCloseAttestationIx(p) {
266
+ const keys = [
267
+ { pubkey: toBase58(p.signer, "signer"), isSigner: true, isWritable: false },
268
+ { pubkey: toBase58(p.config, "config"), isSigner: false, isWritable: false },
269
+ { pubkey: toBase58(p.attestorConfig, "attestorConfig"), isSigner: false, isWritable: false },
270
+ { pubkey: toBase58(p.attestation, "attestation"), isSigner: false, isWritable: true },
271
+ { pubkey: toBase58(p.recipient, "recipient"), isSigner: false, isWritable: true },
272
+ ];
273
+ return {
274
+ programId: toBase58(p.programId, "programId"),
275
+ keys,
276
+ data: CLOSE_ATTESTATION_DISCRIMINATOR.slice(),
277
+ };
278
+ }
@@ -0,0 +1,201 @@
1
+ /**
2
+ * Control-verification primitive — prove to a third party that a wallet
3
+ * currently controls a `@handle`. Companion to docs/control-proof.md, which
4
+ * is normative; this file implements it.
5
+ *
6
+ * The flow has exactly two SDK entry points, one per side:
7
+ *
8
+ * - **Verifier** calls {@link createControlChallenge}: it generates a fresh
9
+ * nonce, reads the handle's CURRENT ownership epoch
10
+ * (`Handle.registered_at`) from chain, and returns the challenge the
11
+ * prover must sign. The verifier stores the returned object (nonce is
12
+ * single-use, proofs expire — see the spec).
13
+ * - The prover signs the raw UTF-8 bytes of `challenge` with the wallet key
14
+ * that controls the handle (no wallet prefix — see the spec's envelope
15
+ * section) and returns `(signer, signature)`.
16
+ * - **Verifier** calls {@link verifyControlProof} with the ISSUED challenge
17
+ * object (never a challenge string received from the prover) and the
18
+ * proof. It re-reads the handle, requires the epoch to still match,
19
+ * resolves who currently holds authority (`require_current_authority`
20
+ * semantics: untokenized → `Handle.owner`; tokenized → the NFT holder,
21
+ * NFT in that holder's associated token account), and verifies the
22
+ * signature RFC-8032-strictly.
23
+ *
24
+ * Challenge format (built by `handle_normalize::control_challenge` in Rust
25
+ * and {@link buildControlChallenge} here — byte-identical by test):
26
+ *
27
+ * x1-handles:ctl:v1:<handle>:<registered_at>:<nonce>
28
+ *
29
+ * The `ctl` purpose tag makes this space disjoint from the record
30
+ * verification challenges (`x1-handles:v1:...`): a signature obtained for a
31
+ * record verification can never validate as a control proof, and vice versa.
32
+ */
33
+ import type { WasmResolver } from "./wasm.js";
34
+ /** `<protocol>:<purpose>:<version>:` — everything a control challenge starts
35
+ * with, and nothing a record-verification challenge ever starts with (their
36
+ * second segment is the literal `v1`, and neither a canonical handle nor a
37
+ * nonce may contain `:`). */
38
+ export declare const CONTROL_CHALLENGE_PREFIX = "x1-handles:ctl:v1:";
39
+ export interface ControlConfig {
40
+ /** X1 RPC endpoint. */
41
+ readonly rpcUrl: string;
42
+ /** WASM module for account derivation — same instance the resolver uses. */
43
+ readonly wasm: WasmResolver;
44
+ /** Optional fetch override for testing or custom transport. */
45
+ readonly fetchImpl?: typeof fetch;
46
+ /** @handle registry program id. Defaults to the canonical X1 deployment. */
47
+ readonly handleProgramId?: string;
48
+ /**
49
+ * Override for the ed25519 verifier — for runtimes whose WebCrypto lacks
50
+ * Ed25519. Any replacement MUST be RFC-8032 strict (reject `s >= L`);
51
+ * docs/control-proof.md names acceptable libraries. Defaults to
52
+ * {@link verifyEd25519Strict}.
53
+ */
54
+ readonly verifyEd25519?: (publicKey: Uint8Array, message: Uint8Array, signature: Uint8Array) => Promise<boolean>;
55
+ }
56
+ /**
57
+ * A challenge the VERIFIER issued and must keep. `challenge` is what the
58
+ * prover signs; the other fields are the verifier's evidence of what it
59
+ * asked for. `verifyControlProof` takes this object — not a string handed
60
+ * back by the prover — so a prover can never substitute a challenge of its
61
+ * own choosing.
62
+ */
63
+ export interface ControlChallenge {
64
+ /** The exact string whose raw UTF-8 bytes the prover must sign. */
65
+ readonly challenge: string;
66
+ /** Canonical handle (no `@`). */
67
+ readonly handle: string;
68
+ /** `Handle.registered_at` at issuance — the ownership epoch this
69
+ * challenge is bound to. */
70
+ readonly registeredAt: bigint;
71
+ /** The verifier-issued single-use nonce embedded in `challenge`. */
72
+ readonly nonce: string;
73
+ /** `Date.now()` at issuance, ms — enforce the max proof age against it. */
74
+ readonly issuedAt: number;
75
+ }
76
+ /** What the prover hands back. */
77
+ export interface ControlProof {
78
+ /** The wallet claiming control, base58. */
79
+ readonly signer: string;
80
+ /** 64-byte ed25519 signature over the raw UTF-8 bytes of the challenge. */
81
+ readonly signature: Uint8Array;
82
+ }
83
+ export type ControlFailureReason =
84
+ /** The handle account no longer exists or is not owned by the registry. */
85
+ "handle-not-registered"
86
+ /** `Handle.registered_at` changed since the challenge was issued — the
87
+ * name was sold, released + re-registered, or otherwise changed epoch.
88
+ * The proof is a previous owner's; issue a fresh challenge. */
89
+ | "epoch-changed"
90
+ /** Tokenized handle whose NFT was burned — nobody controls the name. */
91
+ | "nft-burned"
92
+ /** The signer is not the current authority (untokenized: not
93
+ * `Handle.owner`; tokenized: not the NFT holder). */
94
+ | "signer-not-authority"
95
+ /** Tokenized handle whose NFT sits outside the holder's associated token
96
+ * account — the registry does not recognise that wallet as able to act
97
+ * for the name (`require_current_authority` parity), so neither does a
98
+ * control proof. */
99
+ | "nft-not-in-authority-ata"
100
+ /** The signature does not verify (strictly) over the challenge bytes. */
101
+ | "bad-signature";
102
+ export type ControlVerification = {
103
+ readonly valid: true;
104
+ /** Canonical handle the proof establishes control of. */
105
+ readonly handle: string;
106
+ /** The verified controller, base58. */
107
+ readonly signer: string;
108
+ /** The ownership epoch the proof is valid for. */
109
+ readonly registeredAt: bigint;
110
+ /** Whether authority came from holding the NFT (true) or from
111
+ * `Handle.owner` (false). */
112
+ readonly tokenized: boolean;
113
+ } | {
114
+ readonly valid: false;
115
+ readonly reason: ControlFailureReason;
116
+ readonly message: string;
117
+ };
118
+ /**
119
+ * Build the exact challenge string — byte-identical to the Rust
120
+ * `handle_normalize::control_challenge`. Throws {@link ResolveError}
121
+ * (`invalid-handle`) for a non-canonical handle and `RangeError` for a
122
+ * nonce outside the shared charset, rather than silently producing a string
123
+ * the Rust side would refuse.
124
+ *
125
+ * Verifiers normally never call this directly — {@link
126
+ * createControlChallenge} does, after reading `registeredAt` from chain, so
127
+ * a challenge cannot be built against a guessed or stale epoch.
128
+ */
129
+ export declare function buildControlChallenge(handle: string, registeredAt: bigint, nonce: string): string;
130
+ /**
131
+ * Parse a control challenge string, or null for anything that is not one —
132
+ * including every record-verification challenge (`x1-handles:v1:...`), whose
133
+ * signatures must never be accepted as control proofs.
134
+ */
135
+ export declare function parseControlChallenge(challenge: string): {
136
+ readonly handle: string;
137
+ readonly registeredAt: bigint;
138
+ readonly nonce: string;
139
+ } | null;
140
+ /**
141
+ * Generate a verifier-issued nonce: `bytes` bytes (>= 16, the spec's entropy
142
+ * floor) from the platform CSPRNG, hex-encoded to stay inside the shared
143
+ * nonce charset. The nonce is SINGLE-USE — store it with the issued
144
+ * challenge and delete it the moment a proof against it is checked, pass or
145
+ * fail. A prover-chosen nonce is not a nonce; see docs/control-proof.md.
146
+ */
147
+ export declare function generateControlNonce(bytes?: number): string;
148
+ /**
149
+ * Verifier side, step 1: issue a challenge for a handle.
150
+ *
151
+ * Reads the handle's CURRENT `registered_at` from chain and binds the
152
+ * challenge to it — there is no way to obtain a challenge through this API
153
+ * without the live epoch in hand, which is what makes a past owner's proof
154
+ * worthless the moment the name changes hands.
155
+ *
156
+ * Store the returned object server-side (it is the only thing
157
+ * {@link verifyControlProof} accepts), enforce single use of `nonce`, and
158
+ * enforce a max proof age against `issuedAt` (the spec recommends 5 minutes).
159
+ *
160
+ * @throws {ResolveError} `invalid-handle` for bad input, `not-found` for an
161
+ * unregistered handle, `rpc-error` for transport problems.
162
+ */
163
+ export declare function createControlChallenge(cfg: ControlConfig, handleInput: string): Promise<ControlChallenge>;
164
+ /**
165
+ * Verifier side, step 2: check a proof against the challenge YOU issued.
166
+ *
167
+ * Takes the stored {@link ControlChallenge} — never a challenge string
168
+ * received from the prover — and re-derives the signed bytes from its
169
+ * fields, so neither side can substitute a different string. Then:
170
+ *
171
+ * 1. re-reads the handle and requires `registered_at` to still equal the
172
+ * epoch the challenge was issued under (`epoch-changed` otherwise —
173
+ * the name changed hands after issuance);
174
+ * 2. resolves the CURRENT authority with `require_current_authority`
175
+ * semantics — untokenized → `Handle.owner`; tokenized → the NFT holder,
176
+ * and only with the NFT in that holder's associated token account
177
+ * (`Handle.owner` is stale once tokenized and is never consulted);
178
+ * 3. requires `proof.signer` to be that authority;
179
+ * 4. verifies the 64-byte ed25519 signature over the raw UTF-8 challenge
180
+ * bytes, RFC-8032-strictly.
181
+ *
182
+ * Verification outcomes come back as `{ valid: false, reason }` — only
183
+ * transport failures throw (`ResolveError` `rpc-error`). Nonce single-use
184
+ * and max proof age are the CALLER's responsibility: delete the stored
185
+ * challenge the moment this returns, whatever the outcome, and refuse
186
+ * challenges older than your proof-age limit before calling.
187
+ */
188
+ export declare function verifyControlProof(cfg: ControlConfig, issued: ControlChallenge, proof: ControlProof): Promise<ControlVerification>;
189
+ /**
190
+ * RFC-8032-strict ed25519 verification via WebCrypto (`Ed25519`), available
191
+ * in Node 20+ and current browsers. Both are backed by implementations that
192
+ * reject a non-canonical `s >= L` (the malleability the spec's strictness
193
+ * requirement exists for) — the test suite pins this with known-answer
194
+ * vectors, including a rejected `s + L` forgery of a valid signature.
195
+ *
196
+ * Returns false for anything malformed (wrong lengths, off-curve key,
197
+ * invalid signature); throws only when the runtime has no Ed25519 WebCrypto
198
+ * at all — pass `verifyEd25519` in {@link ControlConfig} there, using a
199
+ * strict library named in docs/control-proof.md.
200
+ */
201
+ export declare function verifyEd25519Strict(publicKey: Uint8Array, message: Uint8Array, signature: Uint8Array): Promise<boolean>;