@x1id/resolve 0.9.0 → 0.11.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,166 @@
1
+ /**
2
+ * Independent, re-verifiable domain->wallet proof via DNS-over-HTTPS (#8318,
3
+ * Approach C — the "auditable attestor" honesty upgrade).
4
+ *
5
+ * # Why this exists
6
+ *
7
+ * A `dns`-kind {@link import("./attestation.js").HandleAttestation} records only
8
+ * that x1id's admin-rotatable attestor key *signed off* on an off-chain DNS
9
+ * lookup ("attestation.ts" says this plainly): the chain stores the attestor's
10
+ * say-so, not a proof. The fully-trustless answers (an on-chain DNSSEC verifier,
11
+ * or a zkTLS proof — Approaches A/B in `docs/trustless-domain-proof-plan.md`)
12
+ * are BLOCKED on this X1 fork because the runtime does not have the crypto
13
+ * primitives they need active (`big_mod_exp` / `secp256r1` / `alt_bn128` are all
14
+ * feature-gated off). Until one of those lands, this module is the shippable
15
+ * middle ground: it lets ANYONE independently re-resolve the domain's DNS record
16
+ * and confirm the binding, so the attestor can no longer lie *undetected*. Trust
17
+ * moves from "the x1id attestor" to "DNS + the domain owner" — a real, cheap
18
+ * upgrade that needs no on-chain change.
19
+ *
20
+ * It is dependency-free and runs in both the browser and Node: a DoH query is a
21
+ * plain HTTPS GET, and the JSON shape ("application/dns-json", the Cloudflare /
22
+ * Google convention) is parsed by hand, like the rest of this package.
23
+ *
24
+ * # The TXT record convention (the interoperability contract)
25
+ *
26
+ * A domain owner proves control of `<domain>` by publishing a TXT record at the
27
+ * name **`_x1id.<domain>`** whose value is:
28
+ *
29
+ * x1id-wallet=<base58 wallet pubkey>
30
+ *
31
+ * - The prefix is the exported {@link TXT_BINDING_PREFIX} (`x1id-wallet=`).
32
+ * - `<base58 wallet pubkey>` is the 32-byte X1/SVM wallet address, base58, that
33
+ * the domain is bound to (the handle's authority wallet). Multiple TXT records
34
+ * under the same name are allowed — a domain may bind more than one wallet
35
+ * (e.g. during a key rotation); {@link resolveDomainBinding} returns them all.
36
+ * - A single TXT record split into several character-strings (RFC 1035 §3.3.14 /
37
+ * RFC 7208 §3.3) is concatenated with no separator before the prefix is
38
+ * matched, exactly as a mail SPF/DKIM verifier would.
39
+ *
40
+ * This is the contract a domain owner follows, so it is deliberately simple and
41
+ * stable. It mirrors the `_dnslink`/`_atproto`/`_domainkey` underscore-prefixed
42
+ * convention rather than putting x1id data on the apex TXT (which would collide
43
+ * with SPF/verification tokens from every other service).
44
+ *
45
+ * # NOTE — the paired attestor-side follow-up (NOT done here)
46
+ *
47
+ * This SDK-only change gives verifiers the re-check primitive. The *other* half
48
+ * of Approach C — having the attestor publish the exact `_x1id.<domain>` TXT it
49
+ * checked inside the attestation's `evidence_hash` preimage bundle, so a
50
+ * back-dated attestation whose TXT was later removed is still disprovable — is a
51
+ * change to the admin-api attest tooling (`tools/`), tracked as a FOLLOW-UP. See
52
+ * `docs/trustless-domain-proof-plan.md` §4. Until that ships,
53
+ * {@link verifyDomainBinding} can only confirm a binding that is *still live* in
54
+ * DNS; it cannot audit a removed one.
55
+ */
56
+ import type { RpcFn } from "./accounts.js";
57
+ /** The canonical prefix of the `_x1id.<domain>` TXT binding value. A TXT string
58
+ * that (after character-string concatenation) starts with this, followed by a
59
+ * valid base58 32-byte address, is an x1id binding record. */
60
+ export declare const TXT_BINDING_PREFIX = "x1id-wallet=";
61
+ /** The DNS label prefixed to a domain to locate its x1id binding TXT records —
62
+ * the query name is `${TXT_NAME_PREFIX}.<domain>`. */
63
+ export declare const TXT_NAME_PREFIX = "_x1id";
64
+ /** Built-in DoH endpoints (both speak the `application/dns-json` shape). */
65
+ declare const DOH_PROVIDERS: Readonly<{
66
+ readonly cloudflare: "https://cloudflare-dns.com/dns-query";
67
+ readonly google: "https://dns.google/resolve";
68
+ }>;
69
+ export interface DomainProofOptions {
70
+ /** Which built-in DoH provider to use when {@link dohUrl} is not given.
71
+ * Default `"cloudflare"`. */
72
+ readonly provider?: keyof typeof DOH_PROVIDERS;
73
+ /** A full DoH endpoint base URL (overrides {@link provider}), e.g. a
74
+ * self-hosted resolver. The query string (`?name=...&type=TXT`) is appended;
75
+ * a URL that already has query params is respected (`&` is used). Must speak
76
+ * the `application/dns-json` response shape. */
77
+ readonly dohUrl?: string;
78
+ /** `fetch` override — inject one in tests (return canned DoH JSON) or to use a
79
+ * custom transport. Defaults to `globalThis.fetch`. */
80
+ readonly fetchImpl?: typeof fetch;
81
+ /** Abort the DoH request after this many ms. Default 6000. */
82
+ readonly timeoutMs?: number;
83
+ }
84
+ /** The outcome of resolving a domain's `_x1id` TXT bindings. `ok` is about the
85
+ * LOOKUP succeeding, not about any wallet being found: a domain that simply has
86
+ * no binding is `ok: true` with an empty `wallets`. `ok: false` means the DNS
87
+ * query itself failed (network, timeout, DoH error) and the answer is unknown —
88
+ * never a throw. */
89
+ export interface DomainBindingResult {
90
+ /** The DNS lookup completed and its answer was parsed. */
91
+ readonly ok: boolean;
92
+ /** The domain queried (lowercased, canonical). */
93
+ readonly domain: string;
94
+ /** Every distinct wallet the `_x1id.<domain>` TXT records bind, base58
95
+ * (canonical spelling), first-seen order, de-duplicated. Empty when the
96
+ * domain publishes no valid binding (or when `ok` is false). */
97
+ readonly wallets: string[];
98
+ /** Why the lookup failed, or a note when it succeeded with nothing usable.
99
+ * Absent on a clean success that found bindings. */
100
+ readonly reason?: string;
101
+ }
102
+ /**
103
+ * Resolve the wallet(s) a domain binds via its `_x1id.<domain>` TXT records,
104
+ * independently of x1id's attestor — a DoH GET, parsed and validated defensively.
105
+ *
106
+ * Never throws: a network failure, timeout, or malformed answer comes back as
107
+ * `{ ok: false, reason }`; a domain with no binding as `{ ok: true, wallets: [] }`.
108
+ */
109
+ export declare function resolveDomainBinding(domain: string, opts?: DomainProofOptions): Promise<DomainBindingResult>;
110
+ /** The outcome of checking a domain against an expected wallet. */
111
+ export interface VerifyDomainBindingResult {
112
+ /** True iff the domain's `_x1id` TXT binds `expectedWallet` and the lookup
113
+ * succeeded. */
114
+ readonly ok: boolean;
115
+ /** Every wallet the domain binds (base58, canonical) — for context/UX even on
116
+ * a mismatch. Empty on a lookup failure. */
117
+ readonly found: string[];
118
+ /** Why `ok` is false: `"invalid-domain"` / `"invalid-wallet"` for bad inputs,
119
+ * a DoH failure reason (`"timeout"`, `"network-error"`, ...), `"not-found"`
120
+ * when the domain publishes no binding, or `"mismatch"` when it binds other
121
+ * wallets but not the expected one. Absent when `ok` is true. */
122
+ readonly reason?: string;
123
+ }
124
+ /**
125
+ * The core "anyone can check it" primitive: does `<domain>` independently bind
126
+ * `expectedWallet` in DNS? Returns `{ ok, found, reason? }` and never throws.
127
+ *
128
+ * A verifier calling this trusts DNS and the domain owner — NOT x1id's attestor.
129
+ */
130
+ export declare function verifyDomainBinding(domain: string, expectedWallet: string, opts?: DomainProofOptions): Promise<VerifyDomainBindingResult>;
131
+ /** The combined on-chain-attestation + live-DNS check. */
132
+ export interface VerifyDomainAttestationResult {
133
+ /** Both halves hold: x1id attested the domain (a non-stale `dns` attestation)
134
+ * AND DNS still independently confirms the binding. This is the strongest
135
+ * signal this SDK can give for a domain today. */
136
+ readonly ok: boolean;
137
+ /** The handle holds a NON-STALE `dns`-kind attestation for the current owner
138
+ * ("x1id attested it"). */
139
+ readonly attested: boolean;
140
+ /** DNS independently confirms the binding right now ("still current, attestor
141
+ * couldn't have lied"). */
142
+ readonly dnsConfirmed: boolean;
143
+ /** The wallet(s) the domain currently binds in DNS. */
144
+ readonly found: string[];
145
+ /** A note when `ok` is false — e.g. `"attestation-error"` if the chain read
146
+ * threw, or the {@link verifyDomainBinding} reason for the DNS half. */
147
+ readonly reason?: string;
148
+ }
149
+ /**
150
+ * Convenience: confirm BOTH that x1id attested a domain AND that DNS still
151
+ * independently backs it up — so a caller learns not just "x1id says so" but
152
+ * "and it's verifiably still true", closing the attestor's ability to have lied.
153
+ *
154
+ * `registeredAt` / `recordsClearedAt` are the owning Handle's fields the caller
155
+ * already decoded (see {@link import("./accounts.js").parseHandleAccount}); they
156
+ * are MANDATORY here for the same reason every attestation read demands them —
157
+ * the staleness rule (`attested_at >= registered_at`, and `>= records_cleared_at`)
158
+ * decides whether an attestation vouches for the *current* owner at all. The
159
+ * two halves are evaluated independently, so a caller can see which one holds:
160
+ * a chain-read failure does not suppress the DNS answer, and vice-versa.
161
+ *
162
+ * Never throws: a failed chain read degrades to `attested: false` with
163
+ * `reason: "attestation-error"`, mirroring `signin.ts`'s best-effort summary.
164
+ */
165
+ export declare function verifyDomainAttestation(rpc: RpcFn, programId: string, handleAccount: string, registeredAt: bigint, recordsClearedAt: bigint, domain: string, expectedWallet: string, opts?: DomainProofOptions): Promise<VerifyDomainAttestationResult>;
166
+ export {};
@@ -0,0 +1,278 @@
1
+ /**
2
+ * Independent, re-verifiable domain->wallet proof via DNS-over-HTTPS (#8318,
3
+ * Approach C — the "auditable attestor" honesty upgrade).
4
+ *
5
+ * # Why this exists
6
+ *
7
+ * A `dns`-kind {@link import("./attestation.js").HandleAttestation} records only
8
+ * that x1id's admin-rotatable attestor key *signed off* on an off-chain DNS
9
+ * lookup ("attestation.ts" says this plainly): the chain stores the attestor's
10
+ * say-so, not a proof. The fully-trustless answers (an on-chain DNSSEC verifier,
11
+ * or a zkTLS proof — Approaches A/B in `docs/trustless-domain-proof-plan.md`)
12
+ * are BLOCKED on this X1 fork because the runtime does not have the crypto
13
+ * primitives they need active (`big_mod_exp` / `secp256r1` / `alt_bn128` are all
14
+ * feature-gated off). Until one of those lands, this module is the shippable
15
+ * middle ground: it lets ANYONE independently re-resolve the domain's DNS record
16
+ * and confirm the binding, so the attestor can no longer lie *undetected*. Trust
17
+ * moves from "the x1id attestor" to "DNS + the domain owner" — a real, cheap
18
+ * upgrade that needs no on-chain change.
19
+ *
20
+ * It is dependency-free and runs in both the browser and Node: a DoH query is a
21
+ * plain HTTPS GET, and the JSON shape ("application/dns-json", the Cloudflare /
22
+ * Google convention) is parsed by hand, like the rest of this package.
23
+ *
24
+ * # The TXT record convention (the interoperability contract)
25
+ *
26
+ * A domain owner proves control of `<domain>` by publishing a TXT record at the
27
+ * name **`_x1id.<domain>`** whose value is:
28
+ *
29
+ * x1id-wallet=<base58 wallet pubkey>
30
+ *
31
+ * - The prefix is the exported {@link TXT_BINDING_PREFIX} (`x1id-wallet=`).
32
+ * - `<base58 wallet pubkey>` is the 32-byte X1/SVM wallet address, base58, that
33
+ * the domain is bound to (the handle's authority wallet). Multiple TXT records
34
+ * under the same name are allowed — a domain may bind more than one wallet
35
+ * (e.g. during a key rotation); {@link resolveDomainBinding} returns them all.
36
+ * - A single TXT record split into several character-strings (RFC 1035 §3.3.14 /
37
+ * RFC 7208 §3.3) is concatenated with no separator before the prefix is
38
+ * matched, exactly as a mail SPF/DKIM verifier would.
39
+ *
40
+ * This is the contract a domain owner follows, so it is deliberately simple and
41
+ * stable. It mirrors the `_dnslink`/`_atproto`/`_domainkey` underscore-prefixed
42
+ * convention rather than putting x1id data on the apex TXT (which would collide
43
+ * with SPF/verification tokens from every other service).
44
+ *
45
+ * # NOTE — the paired attestor-side follow-up (NOT done here)
46
+ *
47
+ * This SDK-only change gives verifiers the re-check primitive. The *other* half
48
+ * of Approach C — having the attestor publish the exact `_x1id.<domain>` TXT it
49
+ * checked inside the attestation's `evidence_hash` preimage bundle, so a
50
+ * back-dated attestation whose TXT was later removed is still disprovable — is a
51
+ * change to the admin-api attest tooling (`tools/`), tracked as a FOLLOW-UP. See
52
+ * `docs/trustless-domain-proof-plan.md` §4. Until that ships,
53
+ * {@link verifyDomainBinding} can only confirm a binding that is *still live* in
54
+ * DNS; it cannot audit a removed one.
55
+ */
56
+ import { decodeBase58_32, encodeBase58 } from "./base58.js";
57
+ import { fetchAttestations } from "./attestation.js";
58
+ import { attestationsPassGate, GateKind } from "./gate.js";
59
+ /** The canonical prefix of the `_x1id.<domain>` TXT binding value. A TXT string
60
+ * that (after character-string concatenation) starts with this, followed by a
61
+ * valid base58 32-byte address, is an x1id binding record. */
62
+ export const TXT_BINDING_PREFIX = "x1id-wallet=";
63
+ /** The DNS label prefixed to a domain to locate its x1id binding TXT records —
64
+ * the query name is `${TXT_NAME_PREFIX}.<domain>`. */
65
+ export const TXT_NAME_PREFIX = "_x1id";
66
+ /** DoH JSON `type` code for a TXT record (RFC 1035). */
67
+ const DNS_TYPE_TXT = 16;
68
+ /** Built-in DoH endpoints (both speak the `application/dns-json` shape). */
69
+ const DOH_PROVIDERS = Object.freeze({
70
+ cloudflare: "https://cloudflare-dns.com/dns-query",
71
+ google: "https://dns.google/resolve",
72
+ });
73
+ const DEFAULT_TIMEOUT_MS = 6000;
74
+ /** A domain is a case-insensitive dotted hostname (1..253 chars, labels 1..63 of
75
+ * `[a-z0-9-]`, no leading/trailing hyphen). Strict enough that the name can be
76
+ * safely interpolated into a query and can never smuggle a second query param. */
77
+ const DOMAIN_RE = /^(?=.{1,253}$)([a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)(\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+$/;
78
+ function normalizeDomain(domain) {
79
+ if (typeof domain !== "string")
80
+ return null;
81
+ // Tolerate a trailing dot (fully-qualified form) and case; reject everything
82
+ // else so the name is safe to interpolate into the query.
83
+ const d = domain.trim().replace(/\.$/, "").toLowerCase();
84
+ if (!DOMAIN_RE.test(d))
85
+ return null;
86
+ return d;
87
+ }
88
+ function buildDohUrl(base, name) {
89
+ const q = `name=${encodeURIComponent(name)}&type=TXT`;
90
+ return base.includes("?") ? `${base}&${q}` : `${base}?${q}`;
91
+ }
92
+ /**
93
+ * Concatenate a DoH TXT `data` field into its logical string.
94
+ *
95
+ * DoH returns a TXT record's RDATA as its character-strings, each wrapped in
96
+ * double quotes; a record split into several character-strings comes back as
97
+ * several quoted runs (e.g. `"\"x1id-ver\" \"ification=...\""`). Per RFC 1035
98
+ * §3.3.14 they concatenate with NO separator. Escaped `\"` and `\\` inside a
99
+ * run are unescaped. A `data` with no quotes at all (some resolvers) is used
100
+ * verbatim.
101
+ */
102
+ function unquoteTxtData(data) {
103
+ if (!data.includes('"'))
104
+ return data;
105
+ let out = "";
106
+ let inStr = false;
107
+ for (let i = 0; i < data.length; i++) {
108
+ const c = data[i];
109
+ if (!inStr) {
110
+ if (c === '"')
111
+ inStr = true;
112
+ // characters between quoted runs (whitespace) are dropped
113
+ continue;
114
+ }
115
+ if (c === "\\") {
116
+ // escaped char: take the next byte literally
117
+ const next = data[i + 1];
118
+ if (next !== undefined) {
119
+ out += next;
120
+ i++;
121
+ }
122
+ continue;
123
+ }
124
+ if (c === '"') {
125
+ inStr = false;
126
+ continue;
127
+ }
128
+ out += c;
129
+ }
130
+ return out;
131
+ }
132
+ /** Extract a canonical base58 wallet from one concatenated TXT string, or null
133
+ * if it is not a well-formed x1id binding. */
134
+ function walletFromTxt(txt) {
135
+ if (!txt.startsWith(TXT_BINDING_PREFIX))
136
+ return null;
137
+ const raw = txt.slice(TXT_BINDING_PREFIX.length).trim();
138
+ const bytes = decodeBase58_32(raw);
139
+ if (!bytes)
140
+ return null;
141
+ // Re-encode so a non-canonical base58 spelling and the caller's wallet compare
142
+ // byte-for-byte, exactly like the rest of the SDK does.
143
+ return encodeBase58(bytes);
144
+ }
145
+ async function fetchDohJson(url, fetchImpl, timeoutMs) {
146
+ const controller = new AbortController();
147
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
148
+ try {
149
+ const res = await fetchImpl(url, {
150
+ signal: controller.signal,
151
+ // Cloudflare's /dns-query REQUIRES this to return JSON; harmless for Google.
152
+ headers: { accept: "application/dns-json" },
153
+ });
154
+ if (!res.ok)
155
+ return { ok: false, reason: `doh-http-${res.status}` };
156
+ const json = (await res.json());
157
+ return { ok: true, json };
158
+ }
159
+ catch (e) {
160
+ const reason = e?.name === "AbortError" ? "timeout" : "network-error";
161
+ return { ok: false, reason };
162
+ }
163
+ finally {
164
+ clearTimeout(timer);
165
+ }
166
+ }
167
+ /**
168
+ * Resolve the wallet(s) a domain binds via its `_x1id.<domain>` TXT records,
169
+ * independently of x1id's attestor — a DoH GET, parsed and validated defensively.
170
+ *
171
+ * Never throws: a network failure, timeout, or malformed answer comes back as
172
+ * `{ ok: false, reason }`; a domain with no binding as `{ ok: true, wallets: [] }`.
173
+ */
174
+ export async function resolveDomainBinding(domain, opts) {
175
+ const canonical = normalizeDomain(domain);
176
+ if (canonical === null) {
177
+ return { ok: false, domain: String(domain), wallets: [], reason: "invalid-domain" };
178
+ }
179
+ const fetchImpl = opts?.fetchImpl ?? globalThis.fetch;
180
+ if (typeof fetchImpl !== "function") {
181
+ return { ok: false, domain: canonical, wallets: [], reason: "no-fetch" };
182
+ }
183
+ const base = opts?.dohUrl ?? DOH_PROVIDERS[opts?.provider ?? "cloudflare"];
184
+ const url = buildDohUrl(base, `${TXT_NAME_PREFIX}.${canonical}`);
185
+ const timeoutMs = opts?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
186
+ const res = await fetchDohJson(url, fetchImpl, timeoutMs);
187
+ if (!res.ok)
188
+ return { ok: false, domain: canonical, wallets: [], reason: res.reason };
189
+ const body = res.json;
190
+ if (typeof body !== "object" || body === null) {
191
+ return { ok: false, domain: canonical, wallets: [], reason: "malformed-response" };
192
+ }
193
+ const b = body;
194
+ const status = typeof b.Status === "number" ? b.Status : -1;
195
+ // 0 = NOERROR (parse answers), 3 = NXDOMAIN (the name simply doesn't exist =
196
+ // no binding, a clean empty result). Anything else (SERVFAIL, etc.) is an
197
+ // unknown answer, not a proven absence.
198
+ if (status === 3)
199
+ return { ok: true, domain: canonical, wallets: [] };
200
+ if (status !== 0)
201
+ return { ok: false, domain: canonical, wallets: [], reason: `doh-status-${status}` };
202
+ const answers = Array.isArray(b.Answer) ? b.Answer : [];
203
+ const wallets = [];
204
+ for (const ans of answers) {
205
+ if (typeof ans !== "object" || ans === null)
206
+ continue;
207
+ const a = ans;
208
+ if (a.type !== DNS_TYPE_TXT)
209
+ continue; // skip CNAME/other chained answers
210
+ if (typeof a.data !== "string")
211
+ continue;
212
+ const wallet = walletFromTxt(unquoteTxtData(a.data));
213
+ if (wallet && !wallets.includes(wallet))
214
+ wallets.push(wallet);
215
+ }
216
+ return { ok: true, domain: canonical, wallets };
217
+ }
218
+ /**
219
+ * The core "anyone can check it" primitive: does `<domain>` independently bind
220
+ * `expectedWallet` in DNS? Returns `{ ok, found, reason? }` and never throws.
221
+ *
222
+ * A verifier calling this trusts DNS and the domain owner — NOT x1id's attestor.
223
+ */
224
+ export async function verifyDomainBinding(domain, expectedWallet, opts) {
225
+ const wanted = typeof expectedWallet === "string" ? decodeBase58_32(expectedWallet) : null;
226
+ if (!wanted)
227
+ return { ok: false, found: [], reason: "invalid-wallet" };
228
+ const wantedBase58 = encodeBase58(wanted);
229
+ const res = await resolveDomainBinding(domain, opts);
230
+ if (!res.ok)
231
+ return { ok: false, found: [], reason: res.reason ?? "lookup-failed" };
232
+ if (res.wallets.length === 0)
233
+ return { ok: false, found: [], reason: "not-found" };
234
+ if (!res.wallets.includes(wantedBase58)) {
235
+ return { ok: false, found: res.wallets, reason: "mismatch" };
236
+ }
237
+ return { ok: true, found: res.wallets };
238
+ }
239
+ /**
240
+ * Convenience: confirm BOTH that x1id attested a domain AND that DNS still
241
+ * independently backs it up — so a caller learns not just "x1id says so" but
242
+ * "and it's verifiably still true", closing the attestor's ability to have lied.
243
+ *
244
+ * `registeredAt` / `recordsClearedAt` are the owning Handle's fields the caller
245
+ * already decoded (see {@link import("./accounts.js").parseHandleAccount}); they
246
+ * are MANDATORY here for the same reason every attestation read demands them —
247
+ * the staleness rule (`attested_at >= registered_at`, and `>= records_cleared_at`)
248
+ * decides whether an attestation vouches for the *current* owner at all. The
249
+ * two halves are evaluated independently, so a caller can see which one holds:
250
+ * a chain-read failure does not suppress the DNS answer, and vice-versa.
251
+ *
252
+ * Never throws: a failed chain read degrades to `attested: false` with
253
+ * `reason: "attestation-error"`, mirroring `signin.ts`'s best-effort summary.
254
+ */
255
+ export async function verifyDomainAttestation(rpc, programId, handleAccount, registeredAt, recordsClearedAt, domain, expectedWallet, opts) {
256
+ // Half 1 (on-chain): a non-stale dns attestation for the current owner. A
257
+ // read failure must not sink the DNS half — degrade, never throw.
258
+ let attested = false;
259
+ let attestationReason;
260
+ try {
261
+ const attestations = await fetchAttestations(rpc, programId, handleAccount, registeredAt, recordsClearedAt);
262
+ attested = attestationsPassGate(attestations, { kind: GateKind.dns });
263
+ }
264
+ catch {
265
+ attestationReason = "attestation-error";
266
+ }
267
+ // Half 2 (off-chain, independent): DNS still binds the expected wallet.
268
+ const dns = await verifyDomainBinding(domain, expectedWallet, opts);
269
+ const ok = attested && dns.ok;
270
+ const reason = ok ? undefined : (attestationReason ?? (attested ? dns.reason : "not-attested"));
271
+ return {
272
+ ok,
273
+ attested,
274
+ dnsConfirmed: dns.ok,
275
+ found: dns.found,
276
+ ...(reason !== undefined ? { reason } : {}),
277
+ };
278
+ }
package/dist/index.d.ts CHANGED
@@ -41,6 +41,7 @@ export * from "./integrator.js";
41
41
  export * from "./voucher.js";
42
42
  export * from "./agent.js";
43
43
  export * from "./register.js";
44
+ export * from "./lease.js";
44
45
  export * from "./accounts.js";
45
46
  export * from "./x402.js";
46
47
  export * from "./recordWrite.js";
@@ -48,6 +49,7 @@ export * from "./clearRecords.js";
48
49
  export * from "./commitReveal.js";
49
50
  export * from "./pnftTransfer.js";
50
51
  export * from "./signin.js";
52
+ export * from "./domainProof.js";
51
53
  import { type Chain, type Resolved } from "./types.js";
52
54
  import { WasmResolver } from "./wasm.js";
53
55
  import { type HandleRecord } from "./records.js";
package/dist/index.js CHANGED
@@ -41,6 +41,7 @@ export * from "./integrator.js";
41
41
  export * from "./voucher.js";
42
42
  export * from "./agent.js";
43
43
  export * from "./register.js";
44
+ export * from "./lease.js";
44
45
  // Was previously imported here for internal use only, never re-exported —
45
46
  // promoted to public API 2026-09-22 because a second real package (mcp/)
46
47
  // now needs `parseHandleAccount`/`RpcFn` directly rather than duplicating
@@ -55,6 +56,7 @@ export * from "./clearRecords.js";
55
56
  export * from "./commitReveal.js";
56
57
  export * from "./pnftTransfer.js";
57
58
  export * from "./signin.js";
59
+ export * from "./domainProof.js";
58
60
  import { ResolveError, CHAIN_COIN_TYPE } from "./types.js";
59
61
  import { parseName } from "./parse.js";
60
62
  import { encodeBase58, decodeBase58_32 } from "./base58.js";
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Lease-to-own instructions (#8150, #8320) — the SIBLING of `register` for term
3
+ * leases, not a replacement: the permanent one-time `register` path
4
+ * (register.ts) is untouched and stays the default.
5
+ *
6
+ * - `register_leased` — claim an unregistered `["handle", name]` under a lease,
7
+ * paying the first installment now.
8
+ * - `renew_lease` — pay more toward a leased handle. PERMISSIONLESS: the payer
9
+ * need not be the owner (anyone may keep or pay off someone else's lease,
10
+ * ENS-registrar-style).
11
+ * - `reclaim_expired_lease` — take over a leased handle whose grace period has
12
+ * ENDED, under a FRESH lease; the reclaimer becomes `owner`. Also
13
+ * permissionless. The program enforces `now >= expires_at + grace`.
14
+ *
15
+ * Hand-rolled like the rest of this package — no Anchor client, no
16
+ * `@solana/web3.js`. PDAs are NOT derived here (see delegate.ts) — derive
17
+ * `["config"]` / `["handle", name]` with your runtime's `findProgramAddress`,
18
+ * and read `Config.treasury` with {@link fetchConfigTreasury} (register.ts).
19
+ *
20
+ * `payment` is in lamports — the amount accepted toward the lease now. The
21
+ * on-chain `lease_seconds_purchased` / `lease_accept_payment` logic converts it
22
+ * to lease-time, capped at the permanent price (offering the full permanent
23
+ * price up front settles the handle permanently in one instruction, writing
24
+ * `expires_at = 0`). This builder does not compute that split; simulate or
25
+ * mirror it client-side if you need to preview before signing.
26
+ */
27
+ import type { AddressLike, BuiltInstruction } from "./delegate.js";
28
+ import type { HandleTypeValue } from "./voucher.js";
29
+ /** `sha256("global:register_leased")[0..8]`. */
30
+ export declare const REGISTER_LEASED_DISCRIMINATOR: Uint8Array;
31
+ /** `sha256("global:renew_lease")[0..8]`. */
32
+ export declare const RENEW_LEASE_DISCRIMINATOR: Uint8Array;
33
+ /** `sha256("global:reclaim_expired_lease")[0..8]`. */
34
+ export declare const RECLAIM_EXPIRED_LEASE_DISCRIMINATOR: Uint8Array;
35
+ export interface RegisterLeasedParams {
36
+ /** The registry program id. */
37
+ readonly programId: AddressLike;
38
+ /** Pays the first lease installment + the `Handle` account's rent. Signer. */
39
+ readonly payer: AddressLike;
40
+ /** The handle's owner. Need not sign — may be registered on another's behalf. */
41
+ readonly owner: AddressLike;
42
+ /** The `["config"]` PDA. */
43
+ readonly config: AddressLike;
44
+ /** `Config.treasury` — read it fresh via {@link fetchConfigTreasury}. */
45
+ readonly treasury: AddressLike;
46
+ /** The `["handle", name]` PDA being claimed — must not already exist. */
47
+ readonly handle: AddressLike;
48
+ /** Canonical form (`parseName(...).canonical`), 1..=32 bytes. */
49
+ readonly name: string;
50
+ readonly handleType: HandleTypeValue;
51
+ /** First lease payment, in lamports. `>= permanentPrice` settles it outright. */
52
+ readonly payment: bigint;
53
+ }
54
+ /** Build `register_leased`. Data: `disc ‖ borsh(name) ‖ handleType(u8) ‖ payment(u64 LE)`. */
55
+ export declare function buildRegisterLeasedIx(p: RegisterLeasedParams): BuiltInstruction;
56
+ export interface RenewLeaseParams {
57
+ /** The registry program id. */
58
+ readonly programId: AddressLike;
59
+ /** Pays toward the lease. Signer. PERMISSIONLESS — need NOT be the owner. */
60
+ readonly payer: AddressLike;
61
+ /** The `["config"]` PDA (read-only on this path). */
62
+ readonly config: AddressLike;
63
+ /** `Config.treasury` — the program address-constrains it to this, so it can't
64
+ * be substituted; read it fresh via {@link fetchConfigTreasury}. */
65
+ readonly treasury: AddressLike;
66
+ /** The leased `["handle", name]` PDA being paid toward. */
67
+ readonly handle: AddressLike;
68
+ /** Lamports to accept toward the lease (capped at the permanent price on-chain). */
69
+ readonly payment: bigint;
70
+ }
71
+ /** Build `renew_lease`. Data: `disc ‖ payment(u64 LE)`. */
72
+ export declare function buildRenewLeaseIx(p: RenewLeaseParams): BuiltInstruction;
73
+ export interface ReclaimExpiredLeaseParams {
74
+ /** The registry program id. */
75
+ readonly programId: AddressLike;
76
+ /** Pays the fresh lease's first installment; becomes the new owner. Signer.
77
+ * PERMISSIONLESS — anyone may reclaim a grace-ended leased handle. */
78
+ readonly payer: AddressLike;
79
+ /** The new owner (typically the same as `payer`). Need not sign. */
80
+ readonly owner: AddressLike;
81
+ /** The `["config"]` PDA. */
82
+ readonly config: AddressLike;
83
+ /** `Config.treasury` — address-constrained on-chain; {@link fetchConfigTreasury}. */
84
+ readonly treasury: AddressLike;
85
+ /** The leased `["handle", name]` PDA being reclaimed — its grace must have ended. */
86
+ readonly handle: AddressLike;
87
+ readonly handleType: HandleTypeValue;
88
+ /** The fresh lease's first payment, in lamports. */
89
+ readonly payment: bigint;
90
+ }
91
+ /** Build `reclaim_expired_lease`. Data: `disc ‖ handleType(u8) ‖ payment(u64 LE)`. */
92
+ export declare function buildReclaimExpiredLeaseIx(p: ReclaimExpiredLeaseParams): BuiltInstruction;
package/dist/lease.js ADDED
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Lease-to-own instructions (#8150, #8320) — the SIBLING of `register` for term
3
+ * leases, not a replacement: the permanent one-time `register` path
4
+ * (register.ts) is untouched and stays the default.
5
+ *
6
+ * - `register_leased` — claim an unregistered `["handle", name]` under a lease,
7
+ * paying the first installment now.
8
+ * - `renew_lease` — pay more toward a leased handle. PERMISSIONLESS: the payer
9
+ * need not be the owner (anyone may keep or pay off someone else's lease,
10
+ * ENS-registrar-style).
11
+ * - `reclaim_expired_lease` — take over a leased handle whose grace period has
12
+ * ENDED, under a FRESH lease; the reclaimer becomes `owner`. Also
13
+ * permissionless. The program enforces `now >= expires_at + grace`.
14
+ *
15
+ * Hand-rolled like the rest of this package — no Anchor client, no
16
+ * `@solana/web3.js`. PDAs are NOT derived here (see delegate.ts) — derive
17
+ * `["config"]` / `["handle", name]` with your runtime's `findProgramAddress`,
18
+ * and read `Config.treasury` with {@link fetchConfigTreasury} (register.ts).
19
+ *
20
+ * `payment` is in lamports — the amount accepted toward the lease now. The
21
+ * on-chain `lease_seconds_purchased` / `lease_accept_payment` logic converts it
22
+ * to lease-time, capped at the permanent price (offering the full permanent
23
+ * price up front settles the handle permanently in one instruction, writing
24
+ * `expires_at = 0`). This builder does not compute that split; simulate or
25
+ * mirror it client-side if you need to preview before signing.
26
+ */
27
+ import { encodeBase58, decodeBase58_32 } from "./base58.js";
28
+ /** `sha256("global:register_leased")[0..8]`. */
29
+ export const REGISTER_LEASED_DISCRIMINATOR = Uint8Array.from([83, 192, 142, 193, 191, 53, 8, 15]);
30
+ /** `sha256("global:renew_lease")[0..8]`. */
31
+ export const RENEW_LEASE_DISCRIMINATOR = Uint8Array.from([11, 121, 141, 53, 216, 217, 123, 139]);
32
+ /** `sha256("global:reclaim_expired_lease")[0..8]`. */
33
+ export const RECLAIM_EXPIRED_LEASE_DISCRIMINATOR = Uint8Array.from([66, 80, 245, 8, 1, 21, 161, 110]);
34
+ const SYSTEM_PROGRAM = "11111111111111111111111111111111";
35
+ function toBytes32(v, what) {
36
+ if (typeof v === "string") {
37
+ const b = decodeBase58_32(v);
38
+ if (!b)
39
+ throw new Error(`${what} is not a valid base58 address`);
40
+ return b;
41
+ }
42
+ if (v.length !== 32)
43
+ throw new Error(`${what} must be exactly 32 bytes`);
44
+ return v;
45
+ }
46
+ function toBase58(v, what) {
47
+ return encodeBase58(toBytes32(v, what));
48
+ }
49
+ function assertHandleType(t) {
50
+ if (!Number.isInteger(t) || t < 0 || t > 3) {
51
+ throw new Error("handleType must be 0 (Human), 1 (Merchant), 2 (Org) or 3 (Agent)");
52
+ }
53
+ }
54
+ function encodeName(name) {
55
+ const b = new TextEncoder().encode(name);
56
+ if (b.length < 1 || b.length > 32)
57
+ throw new Error("name must be 1..=32 bytes (UTF-8)");
58
+ return b;
59
+ }
60
+ function u64le(n) {
61
+ if (n < 0n)
62
+ throw new Error("payment must be a non-negative u64 (lamports)");
63
+ const b = new Uint8Array(8);
64
+ new DataView(b.buffer).setBigUint64(0, n, true);
65
+ return b;
66
+ }
67
+ /** Build `register_leased`. Data: `disc ‖ borsh(name) ‖ handleType(u8) ‖ payment(u64 LE)`. */
68
+ export function buildRegisterLeasedIx(p) {
69
+ assertHandleType(p.handleType);
70
+ const nb = encodeName(p.name);
71
+ const data = new Uint8Array(8 + 4 + nb.length + 1 + 8);
72
+ data.set(REGISTER_LEASED_DISCRIMINATOR, 0);
73
+ new DataView(data.buffer).setUint32(8, nb.length, true);
74
+ data.set(nb, 12);
75
+ data[12 + nb.length] = p.handleType;
76
+ data.set(u64le(p.payment), 12 + nb.length + 1);
77
+ const keys = [
78
+ { pubkey: toBase58(p.payer, "payer"), isSigner: true, isWritable: true },
79
+ { pubkey: toBase58(p.owner, "owner"), isSigner: false, isWritable: false },
80
+ { pubkey: toBase58(p.config, "config"), isSigner: false, isWritable: true },
81
+ { pubkey: toBase58(p.treasury, "treasury"), isSigner: false, isWritable: true },
82
+ { pubkey: toBase58(p.handle, "handle"), isSigner: false, isWritable: true },
83
+ { pubkey: SYSTEM_PROGRAM, isSigner: false, isWritable: false },
84
+ ];
85
+ return { programId: toBase58(p.programId, "programId"), keys, data };
86
+ }
87
+ /** Build `renew_lease`. Data: `disc ‖ payment(u64 LE)`. */
88
+ export function buildRenewLeaseIx(p) {
89
+ const data = new Uint8Array(8 + 8);
90
+ data.set(RENEW_LEASE_DISCRIMINATOR, 0);
91
+ data.set(u64le(p.payment), 8);
92
+ const keys = [
93
+ { pubkey: toBase58(p.payer, "payer"), isSigner: true, isWritable: true },
94
+ { pubkey: toBase58(p.config, "config"), isSigner: false, isWritable: false },
95
+ { pubkey: toBase58(p.treasury, "treasury"), isSigner: false, isWritable: true },
96
+ { pubkey: toBase58(p.handle, "handle"), isSigner: false, isWritable: true },
97
+ { pubkey: SYSTEM_PROGRAM, isSigner: false, isWritable: false },
98
+ ];
99
+ return { programId: toBase58(p.programId, "programId"), keys, data };
100
+ }
101
+ /** Build `reclaim_expired_lease`. Data: `disc ‖ handleType(u8) ‖ payment(u64 LE)`. */
102
+ export function buildReclaimExpiredLeaseIx(p) {
103
+ assertHandleType(p.handleType);
104
+ const data = new Uint8Array(8 + 1 + 8);
105
+ data.set(RECLAIM_EXPIRED_LEASE_DISCRIMINATOR, 0);
106
+ data[8] = p.handleType;
107
+ data.set(u64le(p.payment), 9);
108
+ const keys = [
109
+ { pubkey: toBase58(p.payer, "payer"), isSigner: true, isWritable: true },
110
+ { pubkey: toBase58(p.owner, "owner"), isSigner: false, isWritable: false },
111
+ { pubkey: toBase58(p.config, "config"), isSigner: false, isWritable: true },
112
+ { pubkey: toBase58(p.treasury, "treasury"), isSigner: false, isWritable: true },
113
+ { pubkey: toBase58(p.handle, "handle"), isSigner: false, isWritable: true },
114
+ { pubkey: SYSTEM_PROGRAM, isSigner: false, isWritable: false },
115
+ ];
116
+ return { programId: toBase58(p.programId, "programId"), keys, data };
117
+ }
package/dist/voucher.d.ts CHANGED
@@ -128,3 +128,56 @@ export interface RefundVoucherParams {
128
128
  }
129
129
  /** Build `refund_voucher` — payer-signed, only valid at/after `expiresAt`. */
130
130
  export declare function buildRefundVoucherIx(p: RefundVoucherParams): BuiltInstruction;
131
+ /** `sha256("global:claim_voucher_tokenized")[0..8]`. */
132
+ export declare const CLAIM_VOUCHER_TOKENIZED_DISCRIMINATOR: Uint8Array;
133
+ export interface ClaimVoucherTokenizedParams {
134
+ readonly programId: AddressLike;
135
+ /** Submits + signs; fronts the tx fee (reimbursed) + the handle rent. */
136
+ readonly claimer: AddressLike;
137
+ readonly config: AddressLike;
138
+ readonly treasury: AddressLike;
139
+ readonly voucher: AddressLike;
140
+ /** The original voucher payer — receives rent back when the voucher closes. */
141
+ readonly voucherPayer: AddressLike;
142
+ /** Who the pNFT + ownership go to: the bound recipient, or the claimer for an
143
+ * open voucher. The mint's ATA + token record derive against THIS address. */
144
+ readonly owner: AddressLike;
145
+ /** The `["handle", name]` PDA. */
146
+ readonly handle: AddressLike;
147
+ readonly name: string;
148
+ /** `["nft_mint", handle]` PDA. */
149
+ readonly mint: AddressLike;
150
+ /** `owner`'s associated token account for `mint` (receives the frozen unit). */
151
+ readonly ownerTokenAccount: AddressLike;
152
+ /** Metaplex metadata PDA for `mint`. */
153
+ readonly metadata: AddressLike;
154
+ /** Metaplex master-edition PDA for `mint`. */
155
+ readonly masterEdition: AddressLike;
156
+ /** pNFT TokenRecord PDA for (`mint`, `ownerTokenAccount`). */
157
+ readonly tokenRecord: AddressLike;
158
+ /** The collection parent mint (`["collection_mint"]`). */
159
+ readonly collectionMint: AddressLike;
160
+ /** The collection parent metadata (writable — sized-collection bump). */
161
+ readonly collectionMetadata: AddressLike;
162
+ /** The collection parent master edition. */
163
+ readonly collectionMasterEdition: AddressLike;
164
+ /** The collection authority PDA. */
165
+ readonly collectionAuthority: AddressLike;
166
+ /** The integrator wallet — REQUIRED iff the voucher carries one; must equal
167
+ * the voucher's `integrator`. Omit otherwise. */
168
+ readonly integrator?: AddressLike;
169
+ }
170
+ /**
171
+ * Build `claim_voucher_tokenized` (#8335) — the tokenized sibling of
172
+ * {@link buildClaimVoucherIx} and the DEFAULT path the live gift-claim flow
173
+ * uses: it registers the handle AND mints its tradeable capability pNFT in one
174
+ * instruction. The pNFT + ownership go to `owner` (the bound recipient, or the
175
+ * claimer for an open voucher), so `ownerTokenAccount` / `tokenRecord` derive
176
+ * against `owner`, not the claimer. Data: `disc ‖ borsh(name)` — the handle type
177
+ * comes from the voucher, not an argument.
178
+ *
179
+ * The register + 3 Metaplex CPIs exceed the 200k default compute budget —
180
+ * prepend a ComputeBudget `setComputeUnitLimit` instruction in the SAME
181
+ * transaction (the app uses ~400k) or the claim fails on-chain.
182
+ */
183
+ export declare function buildClaimVoucherTokenizedIx(p: ClaimVoucherTokenizedParams): BuiltInstruction;
package/dist/voucher.js CHANGED
@@ -183,3 +183,59 @@ export function buildRefundVoucherIx(p) {
183
183
  data,
184
184
  };
185
185
  }
186
+ // ===================== claim_voucher_tokenized (gift NFTs, #8335) =====================
187
+ /** `sha256("global:claim_voucher_tokenized")[0..8]`. */
188
+ export const CLAIM_VOUCHER_TOKENIZED_DISCRIMINATOR = Uint8Array.from([131, 111, 174, 105, 154, 248, 191, 235]);
189
+ // Fixed Metaplex / SPL program ids + sysvar in the pNFT-mint tail — hardcoded,
190
+ // never derived (matches the app's mintNft.ts).
191
+ const TOKEN_PROGRAM_ID = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
192
+ const ASSOCIATED_TOKEN_PROGRAM_ID = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL";
193
+ const TOKEN_METADATA_PROGRAM = "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s";
194
+ const SYSVAR_INSTRUCTIONS = "Sysvar1nstructions1111111111111111111111111";
195
+ /**
196
+ * Build `claim_voucher_tokenized` (#8335) — the tokenized sibling of
197
+ * {@link buildClaimVoucherIx} and the DEFAULT path the live gift-claim flow
198
+ * uses: it registers the handle AND mints its tradeable capability pNFT in one
199
+ * instruction. The pNFT + ownership go to `owner` (the bound recipient, or the
200
+ * claimer for an open voucher), so `ownerTokenAccount` / `tokenRecord` derive
201
+ * against `owner`, not the claimer. Data: `disc ‖ borsh(name)` — the handle type
202
+ * comes from the voucher, not an argument.
203
+ *
204
+ * The register + 3 Metaplex CPIs exceed the 200k default compute budget —
205
+ * prepend a ComputeBudget `setComputeUnitLimit` instruction in the SAME
206
+ * transaction (the app uses ~400k) or the claim fails on-chain.
207
+ */
208
+ export function buildClaimVoucherTokenizedIx(p) {
209
+ const name = encString(p.name);
210
+ const data = new Uint8Array(8 + name.length);
211
+ data.set(CLAIM_VOUCHER_TOKENIZED_DISCRIMINATOR, 0);
212
+ data.set(name, 8);
213
+ const keys = [
214
+ { pubkey: toBase58(p.claimer, "claimer"), isSigner: true, isWritable: true },
215
+ { pubkey: toBase58(p.config, "config"), isSigner: false, isWritable: true },
216
+ { pubkey: toBase58(p.treasury, "treasury"), isSigner: false, isWritable: true },
217
+ { pubkey: toBase58(p.voucher, "voucher"), isSigner: false, isWritable: true },
218
+ { pubkey: toBase58(p.voucherPayer, "voucherPayer"), isSigner: false, isWritable: true },
219
+ { pubkey: toBase58(p.owner, "owner"), isSigner: false, isWritable: false },
220
+ { pubkey: toBase58(p.handle, "handle"), isSigner: false, isWritable: true },
221
+ // Metaplex pNFT tail — order + flags verbatim from mintNft.ts::tokenizedTailKeys.
222
+ { pubkey: toBase58(p.mint, "mint"), isSigner: false, isWritable: true },
223
+ { pubkey: toBase58(p.ownerTokenAccount, "ownerTokenAccount"), isSigner: false, isWritable: true },
224
+ { pubkey: toBase58(p.metadata, "metadata"), isSigner: false, isWritable: true },
225
+ { pubkey: toBase58(p.masterEdition, "masterEdition"), isSigner: false, isWritable: true },
226
+ { pubkey: toBase58(p.tokenRecord, "tokenRecord"), isSigner: false, isWritable: true },
227
+ { pubkey: toBase58(p.collectionMint, "collectionMint"), isSigner: false, isWritable: false },
228
+ { pubkey: toBase58(p.collectionMetadata, "collectionMetadata"), isSigner: false, isWritable: true },
229
+ { pubkey: toBase58(p.collectionMasterEdition, "collectionMasterEdition"), isSigner: false, isWritable: false },
230
+ { pubkey: toBase58(p.collectionAuthority, "collectionAuthority"), isSigner: false, isWritable: false },
231
+ { pubkey: TOKEN_METADATA_PROGRAM, isSigner: false, isWritable: false },
232
+ { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
233
+ { pubkey: ASSOCIATED_TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
234
+ { pubkey: SYSTEM_PROGRAM, isSigner: false, isWritable: false },
235
+ { pubkey: SYSVAR_INSTRUCTIONS, isSigner: false, isWritable: false },
236
+ ];
237
+ if (p.integrator !== undefined) {
238
+ keys.push({ pubkey: toBase58(p.integrator, "integrator"), isSigner: false, isWritable: true });
239
+ }
240
+ return { programId: toBase58(p.programId, "programId"), keys, data };
241
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@x1id/resolve",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "description": "Resolve @handles and X1NS names on X1. Never silently picks between namespaces.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",