@x1id/resolve 0.11.0 → 0.12.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,223 @@
1
+ /**
2
+ * SNS adapter — `.sol` domains, read directly from Solana mainnet.
3
+ *
4
+ * `.sol` is built on the SPL Name Service program, the same primitive X1NS uses,
5
+ * so the registry header is identical (parentName(32) | owner(32) | class(32),
6
+ * then data at offset 96). The differences from the native X1NS read are the
7
+ * derivation constants and that the name-account PDA needs `find_program_address`
8
+ * — which this SDK deliberately does not hand-roll (it needs an ed25519 on-curve
9
+ * check; see wasm.ts). Derivation is therefore an INJECTED dependency: pass your
10
+ * own {@link SnsDeriver}, or build the default via {@link makeSnsDeriver} (which
11
+ * uses `@solana/web3.js`).
12
+ *
13
+ * Every constant and the header layout were verified BOTH against live Solana
14
+ * mainnet accounts (bonfida.sol, toly.sol — 2026-10-02) AND against the SNS-SDK
15
+ * source (github.com/SolanaNameService/sns-sdk `js/src/constants.ts`,
16
+ * `state.ts`, `nft/const.ts`, `nft/getDomainMint.ts`). Not copied from an
17
+ * adjacent API surface.
18
+ *
19
+ * bonfida.sol → Crf8hzfthWGbGbLTVCiqRqV5MVnbpHB1L9KQMd6gsinb
20
+ * owning program = namesLPneVptA9Z5rqUDD9tMTWEJwofgaYwp8cawRkX
21
+ * parent = 58PwtjSDuFHuUkYjH9BYnnQKHfwo9reZhC2zMJv9JPkx (the .sol TLD)
22
+ * owner = Fw1ETanDZafof7xEULsnq9UY6o71Tpds89tNwPkWLb1v
23
+ *
24
+ * # Scope of this increment (honest limits)
25
+ *
26
+ * We resolve the domain's CURRENT OWNER: the registry `owner` field, OR — when
27
+ * the domain is wrapped as an NFT — the NFT holder (the registry `owner` of a
28
+ * wrapped domain is a tokenizer ESCROW PDA, never the human, so returning it
29
+ * would be a wrong address). We do NOT yet honor SOL payment records (SNS-IP-1
30
+ * V1 / SNS-IP-3 V2), which can redirect a domain's receive address and carry
31
+ * their own staleness/signature rules — a documented follow-up. We therefore
32
+ * return ownership, marked `unverified`.
33
+ *
34
+ * # SRS migration watch
35
+ *
36
+ * The SNS-SDK `master` ships a new "SRS" (Solana Record Service) resolver under
37
+ * program `srsWjm76StJucL7atFyPSdXFaVLNPFqEt1uFEDPrZsn`, currently gated OFF
38
+ * (`SOL_SRS_RESOLUTION_ENABLED = false`, cutoff ~slot 452_825_395, est.
39
+ * 2026-10-15). Until that flips, the classic NameRegistry path below is the live
40
+ * resolution path. Revisit after the cutover.
41
+ */
42
+ import { ResolveError } from "./types.js";
43
+ import { encodeBase58, decodeBase58 } from "./base58.js";
44
+ import { makeAccountReader, readU64, bytesEqual, SPL_TOKEN_PROGRAM, TOKEN_ACCOUNT_MIN_LEN, } from "./accounts.js";
45
+ import { externalLabel } from "./universalDetect.js";
46
+ /** SPL Name Service program on Solana mainnet (owns every `.sol` registry account). */
47
+ export const SPL_NAME_PROGRAM_ID = "namesLPneVptA9Z5rqUDD9tMTWEJwofgaYwp8cawRkX";
48
+ /** The `.sol` TLD authority — the `nameParent` every root `.sol` domain derives under. */
49
+ export const SOL_TLD_AUTHORITY = "58PwtjSDuFHuUkYjH9BYnnQKHfwo9reZhC2zMJv9JPkx";
50
+ /** Prefix hashed with the label to form the SPL Name Service seed. */
51
+ export const SNS_HASH_PREFIX = "SPL Name Service";
52
+ /** SNS Name Tokenizer program — wraps a domain into a Metaplex NFT. */
53
+ export const NAME_TOKENIZER_ID = "nftD3vbNkNqfj2Sd3HZwbpw4BxxKWr4AjGb9X38JeZk";
54
+ /** Tokenizer mint-PDA seed prefix (`["tokenized_name", nameAccount]`). */
55
+ export const SNS_MINT_PREFIX = "tokenized_name";
56
+ /** SPL Name Service header: parentName(32) | owner(32) | class(32) = 96 bytes, then data. */
57
+ const SPL_NAME_HEADER_LEN = 96;
58
+ /**
59
+ * `hashed_name = sha256(HASH_PREFIX + label)` — the SPL Name Service name hash.
60
+ * Pure (Web Crypto SHA-256, available in Node 18+ and browsers); no curve math,
61
+ * so it lives in the SDK and is reused by a deriver as the first PDA seed.
62
+ */
63
+ export async function snsHashedName(label) {
64
+ const subtle = globalThis.crypto?.subtle;
65
+ if (!subtle) {
66
+ throw new ResolveError("not-configured", "Web Crypto (crypto.subtle) is unavailable for SNS name hashing");
67
+ }
68
+ const digest = await subtle.digest("SHA-256", new TextEncoder().encode(SNS_HASH_PREFIX + label));
69
+ return new Uint8Array(digest);
70
+ }
71
+ /**
72
+ * Build the default SNS deriver using `@solana/web3.js`.
73
+ *
74
+ * Seeds (verified live + against the SNS-SDK source):
75
+ * domainKey: [ sha256("SPL Name Service"+label), 32 zero bytes (no class),
76
+ * SOL_TLD(32) ] under the SPL Name Service program.
77
+ * mintKey: [ "tokenized_name", nameAccount(32) ] under the Name Tokenizer.
78
+ */
79
+ export async function makeSnsDeriver(web3Module) {
80
+ // Prefer an INJECTED `@solana/web3.js` module — the robust path when this
81
+ // package is linked (`file:`/`npm link`), where Node resolves a bare specifier
82
+ // from THIS package's realpath, not the consumer's node_modules, so the
83
+ // consumer that owns web3.js (e.g. the dev-api) passes it in. Fall back to a
84
+ // dynamic import (variable specifier, so tsc never tries to resolve it) for a
85
+ // normal `npm i @x1id/resolve @solana/web3.js` install.
86
+ let web3;
87
+ if (web3Module) {
88
+ web3 = web3Module;
89
+ }
90
+ else {
91
+ const spec = "@solana/web3.js";
92
+ try {
93
+ web3 = (await import(spec));
94
+ }
95
+ catch {
96
+ throw new ResolveError("not-configured", "SNS resolution needs a deriver: install @solana/web3.js, pass the web3 module to makeSnsDeriver, or inject `deriver` in the SNS adapter config");
97
+ }
98
+ }
99
+ const program = new web3.PublicKey(SPL_NAME_PROGRAM_ID);
100
+ const tld = new web3.PublicKey(SOL_TLD_AUTHORITY).toBytes();
101
+ const tokenizer = new web3.PublicKey(NAME_TOKENIZER_ID);
102
+ const prefix = new TextEncoder().encode(SNS_MINT_PREFIX);
103
+ const zeroClass = new Uint8Array(32);
104
+ return {
105
+ async domainKey(label) {
106
+ const hashed = await snsHashedName(label);
107
+ const [key] = web3.PublicKey.findProgramAddressSync([hashed, zeroClass, tld], program);
108
+ return key.toBase58();
109
+ },
110
+ mintKey(nameAccount) {
111
+ const [key] = web3.PublicKey.findProgramAddressSync([prefix, new web3.PublicKey(nameAccount).toBytes()], tokenizer);
112
+ return key.toBase58();
113
+ },
114
+ };
115
+ }
116
+ /** Build the `.sol` (SNS) adapter. */
117
+ export function createSnsAdapter(config = {}) {
118
+ const reader = config.rpcUrl
119
+ ? makeAccountReader(config.rpcUrl, config.fetchImpl)
120
+ : null;
121
+ const deriver = config.deriver ?? null;
122
+ function status() {
123
+ if (!reader) {
124
+ return { namespace: "sol", source: "sns", configured: false, reason: "no Solana mainnet RPC configured (SOLANA_RPC_URL)" };
125
+ }
126
+ if (!deriver) {
127
+ return { namespace: "sol", source: "sns", configured: false, reason: "no SNS deriver configured (install @solana/web3.js or inject `deriver`)" };
128
+ }
129
+ return { namespace: "sol", source: "sns", configured: true };
130
+ }
131
+ /**
132
+ * The NFT holder of a wrapped domain, or null when the domain is not
133
+ * tokenized. Mirrors the SNS-SDK `retrieveNftOwnerV2`: derive the tokenizer
134
+ * mint for the name account, take its unique (`amount === 1`) token account,
135
+ * return that account's SPL-Token owner.
136
+ */
137
+ async function nftHolder(rdr, drv, nameAccount) {
138
+ const mint = await drv.mintKey(nameAccount);
139
+ // A `.sol` domain is wrapped only if its tokenizer mint account EXISTS. The
140
+ // common case is an un-wrapped domain, whose derived mint PDA has never been
141
+ // created — and `getTokenLargestAccounts` on a non-existent account is an RPC
142
+ // *error* ("could not find account"), NOT an empty result. Letting that error
143
+ // propagate surfaced as a 502 for ordinary `.sol` names. Probe the mint first:
144
+ // absent (or not an SPL-Token mint) → the domain is not tokenized, so fall
145
+ // back to the registry owner; present → it is wrapped, read the NFT holder and
146
+ // let a genuine RPC failure there remain an rpc-error.
147
+ const mintInfo = await rdr.accountInfo(mint);
148
+ if (!mintInfo || mintInfo.owner !== SPL_TOKEN_PROGRAM)
149
+ return null;
150
+ const largest = (await rdr.rpc("getTokenLargestAccounts", [mint, { commitment: "confirmed" }]));
151
+ const holders = (largest?.value ?? []).filter((a) => a.amount === "1");
152
+ if (holders.length !== 1)
153
+ return null; // not tokenized / no unique holder
154
+ const t = await rdr.accountInfo(holders[0].address);
155
+ if (!t ||
156
+ t.owner !== SPL_TOKEN_PROGRAM ||
157
+ t.data.length < TOKEN_ACCOUNT_MIN_LEN ||
158
+ readU64(t.data, 64) !== 1n) {
159
+ return null;
160
+ }
161
+ const mintBytes = decodeBase58(mint);
162
+ if (!mintBytes || !bytesEqual(t.data.slice(0, 32), mintBytes))
163
+ return null;
164
+ return encodeBase58(t.data.slice(32, 64));
165
+ }
166
+ return {
167
+ namespace: "sol",
168
+ source: "sns",
169
+ detect(input) {
170
+ const t = input.trim();
171
+ const dot = t.lastIndexOf(".");
172
+ return dot > 0 && t.slice(dot + 1).toLowerCase() === "sol" && !t.startsWith("@");
173
+ },
174
+ status,
175
+ async resolve(input) {
176
+ const label = externalLabel(input, "sol"); // throws invalid-domain on bad shape
177
+ if (!reader || !deriver) {
178
+ throw new ResolveError("not-configured", `.sol (SNS) resolution is not enabled on this deployment: ${status().reason}`, input);
179
+ }
180
+ let accountKey;
181
+ try {
182
+ accountKey = await deriver.domainKey(label);
183
+ }
184
+ catch (e) {
185
+ if (e instanceof ResolveError)
186
+ throw e;
187
+ throw new ResolveError("rpc-error", `SNS derivation failed: ${String(e)}`, input);
188
+ }
189
+ const acct = await reader.accountInfo(accountKey);
190
+ if (!acct) {
191
+ throw new ResolveError("not-found", `${label}.sol is not registered`, input);
192
+ }
193
+ // Must be owned by the SPL Name Service program — anyone can fund an
194
+ // address into existence; its bytes are not a registry record.
195
+ if (acct.owner !== SPL_NAME_PROGRAM_ID) {
196
+ throw new ResolveError("not-found", `${label}.sol is not an SNS registry account`, input);
197
+ }
198
+ if (acct.data.length < SPL_NAME_HEADER_LEN) {
199
+ throw new ResolveError("rpc-error", `${label}.sol returned a malformed account`, input);
200
+ }
201
+ // Verify the record sits under the .sol TLD — without this an arbitrary SPL
202
+ // name account could be presented and its bytes read as an owner.
203
+ if (encodeBase58(acct.data.slice(0, 32)) !== SOL_TLD_AUTHORITY) {
204
+ throw new ResolveError("not-found", `${label}.sol is not a .sol domain`, input);
205
+ }
206
+ // Wrapped domains: the registry `owner` is a tokenizer escrow — resolve the
207
+ // NFT holder instead, and NEVER return the escrow.
208
+ const holder = await nftHolder(reader, deriver, accountKey);
209
+ const address = holder ?? encodeBase58(acct.data.slice(32, 64));
210
+ return {
211
+ input,
212
+ name: `${label}.sol`,
213
+ namespace: "sol",
214
+ address,
215
+ chain: "SOL",
216
+ source: "sns",
217
+ // Ownership (registry owner or NFT holder), not a per-chain payment
218
+ // proof and not yet SOL-record-aware — surfaced conservatively.
219
+ verification: "unverified",
220
+ };
221
+ },
222
+ };
223
+ }
@@ -0,0 +1,117 @@
1
+ /**
2
+ * The X1ID Universal Resolver — shared types.
3
+ *
4
+ * X1ID becomes the aggregator: one call resolves a NATIVE `@handle` / X1NS name
5
+ * AND an external namespace (`.sol` via SNS on Solana, `.eth` via ENS on
6
+ * Ethereum) by reading each name's OWN chain. Every external namespace is read
7
+ * directly from its chain over RPC — never through a third party's resolver API,
8
+ * exactly like the native path's "reads chain, not an API" rule.
9
+ *
10
+ * # The one rule still holds
11
+ *
12
+ * `@jack`, `jack.x1`, `jack.sol` and `jack.eth` are FOUR different names with
13
+ * four potentially different owners on three different chains. The universal
14
+ * resolver classifies by SHAPE and dispatches to exactly one adapter — it never
15
+ * tries one namespace and falls back to another, and every result carries both
16
+ * its `namespace` and the `source` system that answered.
17
+ */
18
+ import type { Chain, Verification } from "./types.js";
19
+ /**
20
+ * The FIXED namespaces the universal resolver classifies by a known suffix. The
21
+ * first four are native (resolved by `@x1id/resolve`'s on-chain resolver);
22
+ * `sol` and `eth` are external adapters. Scoped customer-TLD labels are NOT
23
+ * here — see {@link UniversalNamespace}.
24
+ */
25
+ export type FixedNamespace = "handle" | "x1" | "xnt" | "xen" | "sol" | "eth";
26
+ /**
27
+ * Every namespace a universal result can carry. The fixed six above, PLUS — via
28
+ * `(string & {})` — an arbitrary SCOPED customer-TLD label (`"testtld"`,
29
+ * #8484/#8485), which resolves through the native (`source: "x1id"`) path with
30
+ * `namespace` set to the `.tld`. Literal autocomplete for the fixed six is
31
+ * preserved; a value that is none of them is a scoped TLD label.
32
+ */
33
+ export type UniversalNamespace = FixedNamespace | (string & {});
34
+ /** Native X1 namespaces — resolved by the bundled `createResolver`. */
35
+ export declare const NATIVE_NAMESPACES: readonly UniversalNamespace[];
36
+ /**
37
+ * Which naming SYSTEM produced a result. Orthogonal to `namespace`: `x1`, `xnt`
38
+ * and `xen` all come from `source: "x1id"`. Carried so an integrator can show
39
+ * "resolved via SNS" / "resolved via ENS" and reason about trust per system.
40
+ */
41
+ export type NamespaceSource = "x1id" | "sns" | "ens";
42
+ /**
43
+ * A normalized, cross-namespace resolution result. Deliberately the same shape
44
+ * regardless of which chain answered, so a recipient field treats every
45
+ * namespace uniformly — plus `source` and `verification` so it can label and
46
+ * warn.
47
+ */
48
+ export interface UniversalResolved {
49
+ /** Exactly what the user typed, for display. */
50
+ readonly input: string;
51
+ /** Canonical form of the name (e.g. `"jack"`, `"jack.x1"`, `"jack.sol"`). */
52
+ readonly name: string;
53
+ /** Which naming system matched. Render this before allowing a send. */
54
+ readonly namespace: UniversalNamespace;
55
+ /** The address to send to — base58 for X1/SOL, 0x-hex for ETH. */
56
+ readonly address: string;
57
+ /** Chain `address` belongs to. */
58
+ readonly chain: Chain;
59
+ /** The system that answered (`x1id` | `sns` | `ens`). */
60
+ readonly source: NamespaceSource;
61
+ /**
62
+ * Whether ownership of `address` was PROVED. External namespaces that only
63
+ * expose a registry-owner (SNS's registry `owner`, ENS's `addr` record) come
64
+ * back `unverified` — the address controls/claims the name, which is not the
65
+ * same as a per-chain ownership proof.
66
+ */
67
+ readonly verification: Verification;
68
+ /**
69
+ * Optional per-chain / text records, when an adapter supplies them. Omitted in
70
+ * the first increment for external namespaces (SNS Records V2 / ENS text
71
+ * records are a follow-up); the field exists so adding them later is additive.
72
+ */
73
+ readonly records?: readonly UniversalRecord[];
74
+ }
75
+ /** A single auxiliary record an adapter chose to surface alongside the address. */
76
+ export interface UniversalRecord {
77
+ /** Record key — a chain symbol (`"ETH"`), coin type, or text key. */
78
+ readonly key: string;
79
+ /** Record value as stored (address or free text). */
80
+ readonly value: string;
81
+ }
82
+ /** Whether an adapter can actually reach its chain on this deployment. */
83
+ export interface AdapterStatus {
84
+ readonly namespace: UniversalNamespace | readonly UniversalNamespace[];
85
+ readonly source: NamespaceSource;
86
+ readonly configured: boolean;
87
+ /** Human-readable reason when `configured` is false (what env/dep is missing). */
88
+ readonly reason?: string;
89
+ }
90
+ /** Optional per-call options. `chain` applies only to the native namespaces —
91
+ * SNS always answers on SOL and ENS on ETH. */
92
+ export interface UniversalResolveOptions {
93
+ readonly chain?: Chain;
94
+ }
95
+ /**
96
+ * A pluggable resolver for one namespace family. Adapters are modular and
97
+ * independently testable: each owns its own detection, chain read and
98
+ * normalization, and reports whether it is configured.
99
+ */
100
+ export interface NamespaceAdapter {
101
+ /** The namespace(s) this adapter answers for. */
102
+ readonly namespace: UniversalNamespace | readonly UniversalNamespace[];
103
+ /** The system this adapter represents. */
104
+ readonly source: NamespaceSource;
105
+ /** True iff `input` is shaped for THIS adapter (suffix/prefix classification,
106
+ * never a speculative resolve). */
107
+ detect(input: string): boolean;
108
+ /** Whether this adapter can reach its chain (RPC + any crypto dep present). */
109
+ status(): AdapterStatus;
110
+ /**
111
+ * Resolve `input` to a normalized result.
112
+ * @throws {ResolveError} `not-configured` when `status().configured` is false;
113
+ * `not-found` / `rpc-error` / shape codes exactly like the native resolver.
114
+ * NEVER returns a fabricated address.
115
+ */
116
+ resolve(input: string, opts?: UniversalResolveOptions): Promise<UniversalResolved>;
117
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * The X1ID Universal Resolver — shared types.
3
+ *
4
+ * X1ID becomes the aggregator: one call resolves a NATIVE `@handle` / X1NS name
5
+ * AND an external namespace (`.sol` via SNS on Solana, `.eth` via ENS on
6
+ * Ethereum) by reading each name's OWN chain. Every external namespace is read
7
+ * directly from its chain over RPC — never through a third party's resolver API,
8
+ * exactly like the native path's "reads chain, not an API" rule.
9
+ *
10
+ * # The one rule still holds
11
+ *
12
+ * `@jack`, `jack.x1`, `jack.sol` and `jack.eth` are FOUR different names with
13
+ * four potentially different owners on three different chains. The universal
14
+ * resolver classifies by SHAPE and dispatches to exactly one adapter — it never
15
+ * tries one namespace and falls back to another, and every result carries both
16
+ * its `namespace` and the `source` system that answered.
17
+ */
18
+ /** Native X1 namespaces — resolved by the bundled `createResolver`. */
19
+ export const NATIVE_NAMESPACES = Object.freeze([
20
+ "handle",
21
+ "x1",
22
+ "xnt",
23
+ "xen",
24
+ ]);
package/dist/wasm.d.ts CHANGED
@@ -78,5 +78,22 @@ export declare class WasmResolver {
78
78
  * Needed on BOTH sides of a `ProgrammableNonFungible` transfer.
79
79
  */
80
80
  deriveTokenRecordAccount(mint: Uint8Array, token: Uint8Array, programId: Uint8Array): Uint8Array | null;
81
+ /**
82
+ * Derive the per-handle `NamespaceOverride` PDA under the registry program id
83
+ * — seeds `["namespace_override", handle, label]`, matching the on-chain
84
+ * `CreateNamespaceOverride` account. `handle` is the 32-byte handle PDA
85
+ * (from {@link deriveHandleAccount}); `label` is the namespace label (e.g.
86
+ * "xnt"), 1..=16 bytes. Returns the 32-byte override account, or null.
87
+ *
88
+ * NOTE: overrides are built but not yet honored by `resolve()` — this derive
89
+ * is for the write builders (namespaceOverride.ts) and the gated read path.
90
+ */
91
+ /**
92
+ * Derive a `Namespace` directory PDA under the registry program id — seeds
93
+ * `["namespace", label]`. Needed to pass the namespace account into a
94
+ * create-override instruction. `label` is 1..=16 bytes. Returns 32 bytes or null.
95
+ */
96
+ deriveNamespaceAccount(label: string, programId: Uint8Array): Uint8Array | null;
97
+ deriveNamespaceOverrideAccount(handle: Uint8Array, label: string, programId: Uint8Array): Uint8Array | null;
81
98
  }
82
99
  export {};
package/dist/wasm.js CHANGED
@@ -146,4 +146,42 @@ export class WasmResolver {
146
146
  const n = this.#write(joined);
147
147
  return this.#x.derive_token_record_account(n) === 1 ? this.#read() : null;
148
148
  }
149
+ /**
150
+ * Derive the per-handle `NamespaceOverride` PDA under the registry program id
151
+ * — seeds `["namespace_override", handle, label]`, matching the on-chain
152
+ * `CreateNamespaceOverride` account. `handle` is the 32-byte handle PDA
153
+ * (from {@link deriveHandleAccount}); `label` is the namespace label (e.g.
154
+ * "xnt"), 1..=16 bytes. Returns the 32-byte override account, or null.
155
+ *
156
+ * NOTE: overrides are built but not yet honored by `resolve()` — this derive
157
+ * is for the write builders (namespaceOverride.ts) and the gated read path.
158
+ */
159
+ /**
160
+ * Derive a `Namespace` directory PDA under the registry program id — seeds
161
+ * `["namespace", label]`. Needed to pass the namespace account into a
162
+ * create-override instruction. `label` is 1..=16 bytes. Returns 32 bytes or null.
163
+ */
164
+ deriveNamespaceAccount(label, programId) {
165
+ if (programId.length !== 32)
166
+ return null;
167
+ const labelBytes = this.#enc.encode(label);
168
+ if (labelBytes.length < 1 || labelBytes.length > 16)
169
+ return null;
170
+ new Uint8Array(this.#x.memory.buffer, this.#x.program_ptr(), 32).set(programId);
171
+ const n = this.#write(labelBytes);
172
+ return this.#x.derive_namespace_account(n) === 1 ? this.#read() : null;
173
+ }
174
+ deriveNamespaceOverrideAccount(handle, label, programId) {
175
+ if (handle.length !== 32 || programId.length !== 32)
176
+ return null;
177
+ const labelBytes = this.#enc.encode(label);
178
+ if (labelBytes.length < 1 || labelBytes.length > 16)
179
+ return null;
180
+ new Uint8Array(this.#x.memory.buffer, this.#x.program_ptr(), 32).set(programId);
181
+ const joined = new Uint8Array(32 + labelBytes.length);
182
+ joined.set(handle, 0);
183
+ joined.set(labelBytes, 32);
184
+ const n = this.#write(joined);
185
+ return this.#x.derive_namespace_override_account(n) === 1 ? this.#read() : null;
186
+ }
149
187
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@x1id/resolve",
3
- "version": "0.11.0",
3
+ "version": "0.12.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",
Binary file