@x1id/resolve 0.10.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,84 @@
1
+ /**
2
+ * Scoped customer-TLD names (`alices.testtld`) — the SDK last-mile of the
3
+ * namespace-registrar feature (#8484/#8485).
4
+ *
5
+ * A launched X1ID namespace `.tld` is a program-owned `Namespace` account at
6
+ * `["namespace", tld]` with `status == Active`. A name registered under it is a
7
+ * (always-tokenized) `Handle` at the TWO-SEED-plus-prefix PDA
8
+ * `["handle", tld, name]` — a DIFFERENT account from the bare `@name`'s
9
+ * `["handle", name]`, so `alices.testtld` and `@alices` can have different
10
+ * owners. This module is the shape classifier, the `Namespace` status reader and
11
+ * the injected PDA deriver the resolver uses to answer a scoped name; the
12
+ * owner/tokenization/verification read reuses the bare-handle authority path
13
+ * verbatim (see `createResolver` in index.ts).
14
+ *
15
+ * The resolution CONTRACT is `tools/api`'s `resolve_scoped` (Rust, the
16
+ * authority): canonicalize both halves exactly as the program does, confirm
17
+ * `.tld` is a launched Active namespace, then derive `["handle", tld, name]` and
18
+ * resolve its current authority — with `namespace` set to the TLD label.
19
+ *
20
+ * # Why the deriver is injected (not hand-rolled, not WASM)
21
+ *
22
+ * The WASM module exposes `["namespace", label]` (`deriveNamespaceAccount`) but
23
+ * NO two-seed scoped-handle derivation, and this package's one rule is to never
24
+ * hand-roll `find_program_address`'s ed25519 on-curve check in TypeScript (see
25
+ * wasm.ts). So `["handle", tld, name]` is derived through an INJECTED
26
+ * {@link ScopedHandleDeriver} — exactly the pattern the SNS adapter uses for its
27
+ * `.sol` name-account PDA. Build the default with {@link makeScopedHandleDeriver}
28
+ * (which uses `@solana/web3.js`), or inject your own (a test fake, or a runtime
29
+ * that already owns a `findProgramAddress`).
30
+ */
31
+ /** Max byte length of a TLD label (`NAMESPACE_MAX_LEN`, the program's state.rs). */
32
+ export declare const NAMESPACE_MAX_LEN = 16;
33
+ /** A `name.tld` split, when `input` is SHAPED like a scoped customer-TLD name. */
34
+ export interface ScopedCandidate {
35
+ /** The sub-name half (`"alices"`), lowercased; validated for real at resolve. */
36
+ readonly sub: string;
37
+ /** The TLD-label half (`"testtld"`), lowercased and label-charset-checked. */
38
+ readonly tld: string;
39
+ }
40
+ /**
41
+ * Classify `input` as a scoped customer-TLD candidate by SHAPE ONLY — no chain
42
+ * read. Returns the split, or null when the input is not that shape.
43
+ *
44
+ * The shape is a single interior dot with non-empty halves, no leading `@`, and
45
+ * a `tld` that is a plausible namespace label (the program's charset + the
46
+ * 1..=`NAMESPACE_MAX_LEN` length rule) and is NOT one of the reserved suffixes.
47
+ * Whether `.tld` is actually a launched namespace is a chain fact the resolver
48
+ * establishes separately — this only decides whether it is worth asking.
49
+ * Mirrors `app/src/lib/x1/namespaceRegister.ts`'s `namespaceSublabelShape` and
50
+ * the shape gate in `tools/api`'s `resolve`.
51
+ */
52
+ export declare function scopedTldCandidate(input: string): ScopedCandidate | null;
53
+ /**
54
+ * Whether a raw `Namespace` account's bytes say it is `Active`, byte-for-byte
55
+ * the authority's `namespace_active_from_bytes` (tools/api). Layout: disc(8) |
56
+ * label(Borsh `String`: u32 LE length + UTF-8 bytes) | status(u8), and
57
+ * `NamespaceStatus::Active` is the first enum variant (Borsh tag 0). Only the
58
+ * length prefix and the one status byte are parsed — nothing past it. `false`
59
+ * for any buffer too short to hold disc + length prefix + the status byte (a
60
+ * malformed/wrong account is never read as Active). The CALLER must also check
61
+ * the account is owned by the registry program — this reads bytes only.
62
+ */
63
+ export declare function namespaceIsActive(data: Uint8Array): boolean;
64
+ /**
65
+ * The one `find_program_address` derivation scoped resolution needs: the
66
+ * two-seed-plus-prefix scoped handle PDA `["handle", tld, name]`. Injected so
67
+ * the SDK core stays free of curve math (and of a hard `@solana/web3.js`
68
+ * dependency), and so tests can supply a deterministic fake.
69
+ */
70
+ export interface ScopedHandleDeriver {
71
+ /** The `["handle", label, name]` PDA under `programId`, base58. */
72
+ scopedHandleKey(label: string, name: string, programId: string): Promise<string> | string;
73
+ }
74
+ /**
75
+ * Build the default scoped-handle deriver using `@solana/web3.js`.
76
+ *
77
+ * Seeds are `["handle", label, name]` under the registry program — exactly the
78
+ * order `tools/api`'s `scoped_handle_pda` and the program's
79
+ * `register_under_namespace` use, so the derived PDA is the account the name
80
+ * actually lives at. Prefers an INJECTED web3 module (robust under `file:` /
81
+ * `npm link`, where a bare specifier resolves from THIS package's realpath, not
82
+ * the consumer's); falls back to a dynamic import for a normal install.
83
+ */
84
+ export declare function makeScopedHandleDeriver(web3Module?: unknown): Promise<ScopedHandleDeriver>;
package/dist/scoped.js ADDED
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Scoped customer-TLD names (`alices.testtld`) — the SDK last-mile of the
3
+ * namespace-registrar feature (#8484/#8485).
4
+ *
5
+ * A launched X1ID namespace `.tld` is a program-owned `Namespace` account at
6
+ * `["namespace", tld]` with `status == Active`. A name registered under it is a
7
+ * (always-tokenized) `Handle` at the TWO-SEED-plus-prefix PDA
8
+ * `["handle", tld, name]` — a DIFFERENT account from the bare `@name`'s
9
+ * `["handle", name]`, so `alices.testtld` and `@alices` can have different
10
+ * owners. This module is the shape classifier, the `Namespace` status reader and
11
+ * the injected PDA deriver the resolver uses to answer a scoped name; the
12
+ * owner/tokenization/verification read reuses the bare-handle authority path
13
+ * verbatim (see `createResolver` in index.ts).
14
+ *
15
+ * The resolution CONTRACT is `tools/api`'s `resolve_scoped` (Rust, the
16
+ * authority): canonicalize both halves exactly as the program does, confirm
17
+ * `.tld` is a launched Active namespace, then derive `["handle", tld, name]` and
18
+ * resolve its current authority — with `namespace` set to the TLD label.
19
+ *
20
+ * # Why the deriver is injected (not hand-rolled, not WASM)
21
+ *
22
+ * The WASM module exposes `["namespace", label]` (`deriveNamespaceAccount`) but
23
+ * NO two-seed scoped-handle derivation, and this package's one rule is to never
24
+ * hand-roll `find_program_address`'s ed25519 on-curve check in TypeScript (see
25
+ * wasm.ts). So `["handle", tld, name]` is derived through an INJECTED
26
+ * {@link ScopedHandleDeriver} — exactly the pattern the SNS adapter uses for its
27
+ * `.sol` name-account PDA. Build the default with {@link makeScopedHandleDeriver}
28
+ * (which uses `@solana/web3.js`), or inject your own (a test fake, or a runtime
29
+ * that already owns a `findProgramAddress`).
30
+ */
31
+ import { ResolveError } from "./types.js";
32
+ /** Max byte length of a TLD label (`NAMESPACE_MAX_LEN`, the program's state.rs). */
33
+ export const NAMESPACE_MAX_LEN = 16;
34
+ /** The suffixes that are NEVER a scoped X1ID TLD: the native X1NS namespaces
35
+ * and the external ones the universal resolver owns. A scoped candidate whose
36
+ * suffix is one of these is refused here so `.x1`/`.xnt`/`.xen` stay on the
37
+ * X1NS path and `.sol`/`.eth` stay with their own adapters — the whole
38
+ * never-conflate-a-namespace rule. */
39
+ const RESERVED_SUFFIXES = new Set(["x1", "xnt", "xen", "sol", "eth"]);
40
+ /**
41
+ * Classify `input` as a scoped customer-TLD candidate by SHAPE ONLY — no chain
42
+ * read. Returns the split, or null when the input is not that shape.
43
+ *
44
+ * The shape is a single interior dot with non-empty halves, no leading `@`, and
45
+ * a `tld` that is a plausible namespace label (the program's charset + the
46
+ * 1..=`NAMESPACE_MAX_LEN` length rule) and is NOT one of the reserved suffixes.
47
+ * Whether `.tld` is actually a launched namespace is a chain fact the resolver
48
+ * establishes separately — this only decides whether it is worth asking.
49
+ * Mirrors `app/src/lib/x1/namespaceRegister.ts`'s `namespaceSublabelShape` and
50
+ * the shape gate in `tools/api`'s `resolve`.
51
+ */
52
+ export function scopedTldCandidate(input) {
53
+ const t = input.trim().toLowerCase();
54
+ if (!t || t.startsWith("@"))
55
+ return null;
56
+ const dot = t.lastIndexOf(".");
57
+ if (dot <= 0 || dot === t.length - 1)
58
+ return null;
59
+ const sub = t.slice(0, dot);
60
+ const tld = t.slice(dot + 1);
61
+ if (sub.includes("."))
62
+ return null; // a deeper dotted name is a true sub-subname
63
+ if (RESERVED_SUFFIXES.has(tld))
64
+ return null; // never conflate with X1NS / external
65
+ if (tld.length > NAMESPACE_MAX_LEN)
66
+ return null;
67
+ if (!/^[a-z0-9-]+$/.test(tld))
68
+ return null;
69
+ if (tld.startsWith("-") || tld.endsWith("-") || tld.includes("--") || /^[0-9]+$/.test(tld)) {
70
+ return null;
71
+ }
72
+ return { sub, tld };
73
+ }
74
+ /**
75
+ * Whether a raw `Namespace` account's bytes say it is `Active`, byte-for-byte
76
+ * the authority's `namespace_active_from_bytes` (tools/api). Layout: disc(8) |
77
+ * label(Borsh `String`: u32 LE length + UTF-8 bytes) | status(u8), and
78
+ * `NamespaceStatus::Active` is the first enum variant (Borsh tag 0). Only the
79
+ * length prefix and the one status byte are parsed — nothing past it. `false`
80
+ * for any buffer too short to hold disc + length prefix + the status byte (a
81
+ * malformed/wrong account is never read as Active). The CALLER must also check
82
+ * the account is owned by the registry program — this reads bytes only.
83
+ */
84
+ export function namespaceIsActive(data) {
85
+ if (data.length < 12)
86
+ return false;
87
+ const labelLen = new DataView(data.buffer, data.byteOffset, data.byteLength).getUint32(8, true);
88
+ const statusOff = 12 + labelLen;
89
+ if (data.length <= statusOff)
90
+ return false;
91
+ return data[statusOff] === 0;
92
+ }
93
+ /**
94
+ * Build the default scoped-handle deriver using `@solana/web3.js`.
95
+ *
96
+ * Seeds are `["handle", label, name]` under the registry program — exactly the
97
+ * order `tools/api`'s `scoped_handle_pda` and the program's
98
+ * `register_under_namespace` use, so the derived PDA is the account the name
99
+ * actually lives at. Prefers an INJECTED web3 module (robust under `file:` /
100
+ * `npm link`, where a bare specifier resolves from THIS package's realpath, not
101
+ * the consumer's); falls back to a dynamic import for a normal install.
102
+ */
103
+ export async function makeScopedHandleDeriver(web3Module) {
104
+ let web3;
105
+ if (web3Module) {
106
+ web3 = web3Module;
107
+ }
108
+ else {
109
+ const spec = "@solana/web3.js";
110
+ try {
111
+ web3 = (await import(spec));
112
+ }
113
+ catch {
114
+ throw new ResolveError("not-configured", "scoped TLD resolution needs a deriver: install @solana/web3.js, pass the web3 module to makeScopedHandleDeriver, or inject `scopedHandleDeriver` in the resolver config");
115
+ }
116
+ }
117
+ const enc = new TextEncoder();
118
+ const handleSeed = enc.encode("handle");
119
+ return {
120
+ scopedHandleKey(label, name, programId) {
121
+ const program = new web3.PublicKey(programId);
122
+ const [key] = web3.PublicKey.findProgramAddressSync([handleSeed, enc.encode(label), enc.encode(name)], program);
123
+ return key.toBase58();
124
+ },
125
+ };
126
+ }
package/dist/types.d.ts CHANGED
@@ -1,8 +1,18 @@
1
- /** Which naming system produced a result. */
2
- export type Namespace = "handle" | "x1" | "xnt" | "xen";
1
+ /**
2
+ * Which naming system produced a result.
3
+ *
4
+ * The first four are the fixed native namespaces. A SCOPED customer-TLD name
5
+ * (`alices.testtld`, #8484/#8485) carries its TLD LABEL here — an arbitrary,
6
+ * launch-time string — so the union is widened with `(string & {})`: literal
7
+ * autocomplete for the fixed four is preserved, while a scoped result can set
8
+ * `namespace` to its `.tld`. Compare against the literals as before; a value
9
+ * that is none of them is a scoped TLD label.
10
+ */
11
+ export type Namespace = "handle" | "x1" | "xnt" | "xen" | (string & {});
3
12
  /**
4
13
  * Display label for a namespace. **Must be shown** next to any resolved
5
- * address — see the module docs on why this is not optional.
14
+ * address — see the module docs on why this is not optional. A scoped TLD label
15
+ * renders as `.<label>` (e.g. `.testtld`), the same as an X1NS suffix.
6
16
  */
7
17
  export declare function namespaceLabel(ns: Namespace): string;
8
18
  /** Confidence in the address a name resolves to. */
@@ -83,7 +93,16 @@ export interface Resolved {
83
93
  /** Chains a handle can carry a record for. SLIP-44 based. */
84
94
  export type Chain = "X1" | "SOL" | "ETH" | "BTC";
85
95
  export declare const CHAIN_COIN_TYPE: Readonly<Record<Chain, number>>;
86
- export type ResolveErrorCode = "unrecognized" | "ambiguous" | "invalid-handle" | "invalid-domain" | "not-found" | "no-record-for-chain" | "rpc-error";
96
+ export type ResolveErrorCode = "unrecognized" | "ambiguous" | "invalid-handle" | "invalid-domain" | "not-found" | "no-record-for-chain" | "rpc-error"
97
+ /**
98
+ * A reachable namespace exists for this name, but the adapter that would
99
+ * resolve it is not configured on this deployment (e.g. no `ETH_RPC_URL` for
100
+ * ENS, or no Solana mainnet RPC for SNS). Distinct from `unrecognized`: the
101
+ * name IS a known namespace — we just cannot reach its chain from here. A UI
102
+ * should surface "this namespace isn't enabled", not "unknown name", and the
103
+ * universal resolver NEVER fabricates an address to paper over it.
104
+ */
105
+ | "not-configured";
87
106
  /**
88
107
  * Finer-grained cause for a `ResolveError`, when the code alone is not
89
108
  * enough for a UI to explain what happened.
package/dist/types.js CHANGED
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Display label for a namespace. **Must be shown** next to any resolved
3
- * address — see the module docs on why this is not optional.
3
+ * address — see the module docs on why this is not optional. A scoped TLD label
4
+ * renders as `.<label>` (e.g. `.testtld`), the same as an X1NS suffix.
4
5
  */
5
6
  export function namespaceLabel(ns) {
6
7
  return ns === "handle" ? "@handle" : `.${ns}`;
@@ -0,0 +1,58 @@
1
+ /**
2
+ * The X1ID Universal Resolver — one entry that resolves native `@handle` / X1NS
3
+ * names AND external namespaces (`.sol` via SNS, `.eth` via ENS) by reading each
4
+ * name's own chain, returning a single normalized {@link UniversalResolved}.
5
+ *
6
+ * ```ts
7
+ * import { createResolver, WasmResolver } from "@x1id/resolve";
8
+ * import { createUniversalResolver, makeSnsDeriver } from "@x1id/resolve";
9
+ *
10
+ * const wasm = await WasmResolver.fromBytes(wasmBytes);
11
+ * const native = createResolver({ rpcUrl: X1_RPC, wasm });
12
+ * const universal = createUniversalResolver({
13
+ * native,
14
+ * sns: { rpcUrl: SOLANA_RPC, deriver: await makeSnsDeriver() }, // live
15
+ * ens: {}, // gated off (no ETH_RPC_URL)
16
+ * });
17
+ *
18
+ * await universal.resolve("@jack"); // → source: "x1id"
19
+ * await universal.resolve("bob.sol"); // → source: "sns"
20
+ * await universal.resolve("bob.eth"); // → ResolveError "not-configured" (gated)
21
+ * ```
22
+ *
23
+ * Dispatch is by SHAPE (see universalDetect.ts) to exactly one adapter — never a
24
+ * try-one-then-fall-back chain. An unconfigured external namespace throws a typed
25
+ * `not-configured`; it does not poison resolution of the namespaces that ARE live.
26
+ */
27
+ import type { Resolver } from "./index.js";
28
+ import { type SnsAdapterConfig } from "./universalSns.js";
29
+ import { type EnsAdapterConfig } from "./universalEns.js";
30
+ import type { AdapterStatus, UniversalNamespace, UniversalResolveOptions, UniversalResolved } from "./universalTypes.js";
31
+ export * from "./universalTypes.js";
32
+ export { detectNamespace, isUniversalName, sourceForNamespace, externalLabel, type DetectedNamespace, } from "./universalDetect.js";
33
+ export { createNativeAdapter } from "./universalNative.js";
34
+ export { createSnsAdapter, makeSnsDeriver, snsHashedName, SPL_NAME_PROGRAM_ID, SOL_TLD_AUTHORITY, SNS_HASH_PREFIX, NAME_TOKENIZER_ID, SNS_MINT_PREFIX, type SnsAdapterConfig, type SnsDeriver, } from "./universalSns.js";
35
+ export { createEnsAdapter, ensNamehash, ENS_REGISTRY_ADDRESS, type EnsAdapterConfig, type Keccak256, } from "./universalEns.js";
36
+ export interface UniversalResolverConfig {
37
+ /** The native `@handle` / X1NS resolver (from `createResolver`). Required. */
38
+ readonly native: Resolver;
39
+ /** SNS (`.sol`) adapter config. Omit (or leave RPC/deriver unset) to gate it off. */
40
+ readonly sns?: SnsAdapterConfig;
41
+ /** ENS (`.eth`) adapter config. Omit (or leave ETH_RPC_URL unset) to gate it off. */
42
+ readonly ens?: EnsAdapterConfig;
43
+ }
44
+ export interface NamespaceResolver {
45
+ /**
46
+ * Resolve any supported name to a normalized result.
47
+ * @throws {ResolveError} with a `code` a UI can branch on — `unrecognized`,
48
+ * `ambiguous`, `invalid-domain`, `not-found`, `rpc-error`, or
49
+ * `not-configured` for a known-but-disabled namespace.
50
+ */
51
+ resolve(input: string, opts?: UniversalResolveOptions): Promise<UniversalResolved>;
52
+ /** Classify `input` into a namespace by shape, or null if unrecognized. */
53
+ detect(input: string): UniversalNamespace | null;
54
+ /** Per-source adapter status (which namespaces are live vs config-gated). */
55
+ status(): readonly AdapterStatus[];
56
+ }
57
+ /** Build the universal resolver over a native resolver plus optional external adapters. */
58
+ export declare function createUniversalResolver(config: UniversalResolverConfig): NamespaceResolver;
@@ -0,0 +1,67 @@
1
+ /**
2
+ * The X1ID Universal Resolver — one entry that resolves native `@handle` / X1NS
3
+ * names AND external namespaces (`.sol` via SNS, `.eth` via ENS) by reading each
4
+ * name's own chain, returning a single normalized {@link UniversalResolved}.
5
+ *
6
+ * ```ts
7
+ * import { createResolver, WasmResolver } from "@x1id/resolve";
8
+ * import { createUniversalResolver, makeSnsDeriver } from "@x1id/resolve";
9
+ *
10
+ * const wasm = await WasmResolver.fromBytes(wasmBytes);
11
+ * const native = createResolver({ rpcUrl: X1_RPC, wasm });
12
+ * const universal = createUniversalResolver({
13
+ * native,
14
+ * sns: { rpcUrl: SOLANA_RPC, deriver: await makeSnsDeriver() }, // live
15
+ * ens: {}, // gated off (no ETH_RPC_URL)
16
+ * });
17
+ *
18
+ * await universal.resolve("@jack"); // → source: "x1id"
19
+ * await universal.resolve("bob.sol"); // → source: "sns"
20
+ * await universal.resolve("bob.eth"); // → ResolveError "not-configured" (gated)
21
+ * ```
22
+ *
23
+ * Dispatch is by SHAPE (see universalDetect.ts) to exactly one adapter — never a
24
+ * try-one-then-fall-back chain. An unconfigured external namespace throws a typed
25
+ * `not-configured`; it does not poison resolution of the namespaces that ARE live.
26
+ */
27
+ import { ResolveError } from "./types.js";
28
+ import { detectNamespace, isUniversalName } from "./universalDetect.js";
29
+ import { createNativeAdapter } from "./universalNative.js";
30
+ import { createSnsAdapter } from "./universalSns.js";
31
+ import { createEnsAdapter } from "./universalEns.js";
32
+ export * from "./universalTypes.js";
33
+ export { detectNamespace, isUniversalName, sourceForNamespace, externalLabel, } from "./universalDetect.js";
34
+ export { createNativeAdapter } from "./universalNative.js";
35
+ export { createSnsAdapter, makeSnsDeriver, snsHashedName, SPL_NAME_PROGRAM_ID, SOL_TLD_AUTHORITY, SNS_HASH_PREFIX, NAME_TOKENIZER_ID, SNS_MINT_PREFIX, } from "./universalSns.js";
36
+ export { createEnsAdapter, ensNamehash, ENS_REGISTRY_ADDRESS, } from "./universalEns.js";
37
+ /** Build the universal resolver over a native resolver plus optional external adapters. */
38
+ export function createUniversalResolver(config) {
39
+ const nativeAdapter = createNativeAdapter(config.native);
40
+ const snsAdapter = createSnsAdapter(config.sns ?? {});
41
+ const ensAdapter = createEnsAdapter(config.ens ?? {});
42
+ const bySource = Object.freeze({
43
+ x1id: nativeAdapter,
44
+ sns: snsAdapter,
45
+ ens: ensAdapter,
46
+ });
47
+ return {
48
+ async resolve(input, opts) {
49
+ const { source } = detectNamespace(input); // throws unrecognized / ambiguous by shape
50
+ const adapter = bySource[source];
51
+ // Defensive: detection and dispatch agree by construction, but never let a
52
+ // mismatch silently resolve through the wrong chain.
53
+ if (!adapter) {
54
+ throw new ResolveError("unrecognized", `no adapter for "${input}"`, input);
55
+ }
56
+ return adapter.resolve(input, opts);
57
+ },
58
+ detect(input) {
59
+ if (!isUniversalName(input))
60
+ return null;
61
+ return detectNamespace(input).namespace;
62
+ },
63
+ status() {
64
+ return [nativeAdapter.status(), snsAdapter.status(), ensAdapter.status()];
65
+ },
66
+ };
67
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Shape-based namespace detection for the universal resolver.
3
+ *
4
+ * Classification is by SHAPE ONLY — the same discipline as `parse.ts`'s
5
+ * `parseName`: we never try one namespace and fall back to another, because that
6
+ * is exactly how `@jack`, `jack.x1` and `jack.sol` get conflated and a transfer
7
+ * succeeds to the wrong person on the wrong chain.
8
+ */
9
+ import type { NamespaceSource, UniversalNamespace } from "./universalTypes.js";
10
+ export interface DetectedNamespace {
11
+ readonly namespace: UniversalNamespace;
12
+ readonly source: NamespaceSource;
13
+ }
14
+ /** The system that owns a given namespace. A scoped customer-TLD label (not one
15
+ * of the fixed namespaces) is always the native X1ID system. */
16
+ export declare function sourceForNamespace(ns: UniversalNamespace): NamespaceSource;
17
+ /**
18
+ * Classify `input` into exactly one namespace by shape, or throw.
19
+ *
20
+ * - `@name` → `handle` (never a dotted TLD; `@x.sol` is ambiguous → throws).
21
+ * - `name.<tld>` for a known tld → that namespace.
22
+ * - `name.<tld>` for an unknown tld shaped like a customer-TLD label → the
23
+ * label itself, `source: "x1id"` (a SCOPED customer-TLD candidate, #8484/
24
+ * #8485). Whether `.tld` is a LAUNCHED namespace is a chain fact the native
25
+ * resolver establishes — this only classifies the shape.
26
+ * - anything else → `unrecognized`.
27
+ *
28
+ * @throws {ResolveError} `ambiguous` for `@…​.tld`; `unrecognized` otherwise.
29
+ */
30
+ export declare function detectNamespace(input: string): DetectedNamespace;
31
+ /** Non-throwing probe: is `input` shaped like any namespace the resolver knows? */
32
+ export declare function isUniversalName(input: string): boolean;
33
+ /**
34
+ * The single label of an external domain (`"bob"` from `"bob.sol"`), lowercased
35
+ * and validated. Shared by the SNS and ENS adapters so their label rules stay
36
+ * identical and homograph-safe.
37
+ *
38
+ * ASCII only — a payment identifier that renders a Cyrillic homograph as a
39
+ * successful result is a vector we refuse, the same stance as `parse.ts`.
40
+ * Interior dots (subdomains like `pay.bob.sol` / `pay.bob.eth`) use a different
41
+ * derivation per namespace and are NOT supported in this increment: rejected
42
+ * explicitly rather than mis-derived to a wrong account.
43
+ */
44
+ export declare function externalLabel(input: string, suffix: UniversalNamespace): string;
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Shape-based namespace detection for the universal resolver.
3
+ *
4
+ * Classification is by SHAPE ONLY — the same discipline as `parse.ts`'s
5
+ * `parseName`: we never try one namespace and fall back to another, because that
6
+ * is exactly how `@jack`, `jack.x1` and `jack.sol` get conflated and a transfer
7
+ * succeeds to the wrong person on the wrong chain.
8
+ */
9
+ import { ResolveError } from "./types.js";
10
+ import { scopedTldCandidate } from "./scoped.js";
11
+ /** Suffix → namespace for the dotted (domain-shaped) FIXED namespaces. */
12
+ const SUFFIX_NAMESPACE = Object.freeze({
13
+ x1: "x1",
14
+ xnt: "xnt",
15
+ xen: "xen",
16
+ sol: "sol",
17
+ eth: "eth",
18
+ });
19
+ const SOURCE_FOR = Object.freeze({
20
+ handle: "x1id",
21
+ x1: "x1id",
22
+ xnt: "x1id",
23
+ xen: "x1id",
24
+ sol: "sns",
25
+ eth: "ens",
26
+ });
27
+ /** The system that owns a given namespace. A scoped customer-TLD label (not one
28
+ * of the fixed namespaces) is always the native X1ID system. */
29
+ export function sourceForNamespace(ns) {
30
+ return SOURCE_FOR[ns] ?? "x1id";
31
+ }
32
+ /**
33
+ * Classify `input` into exactly one namespace by shape, or throw.
34
+ *
35
+ * - `@name` → `handle` (never a dotted TLD; `@x.sol` is ambiguous → throws).
36
+ * - `name.<tld>` for a known tld → that namespace.
37
+ * - `name.<tld>` for an unknown tld shaped like a customer-TLD label → the
38
+ * label itself, `source: "x1id"` (a SCOPED customer-TLD candidate, #8484/
39
+ * #8485). Whether `.tld` is a LAUNCHED namespace is a chain fact the native
40
+ * resolver establishes — this only classifies the shape.
41
+ * - anything else → `unrecognized`.
42
+ *
43
+ * @throws {ResolveError} `ambiguous` for `@…​.tld`; `unrecognized` otherwise.
44
+ */
45
+ export function detectNamespace(input) {
46
+ const t = input.trim();
47
+ const looksHandle = t.startsWith("@");
48
+ const dot = t.lastIndexOf(".");
49
+ const suffix = dot === -1 ? "" : t.slice(dot + 1).toLowerCase();
50
+ const dottedNs = SUFFIX_NAMESPACE[suffix];
51
+ if (looksHandle && dottedNs) {
52
+ throw new ResolveError("ambiguous", `"${t}" is both a handle and a domain shape`, t);
53
+ }
54
+ if (looksHandle) {
55
+ return { namespace: "handle", source: "x1id" };
56
+ }
57
+ if (dottedNs) {
58
+ return { namespace: dottedNs, source: SOURCE_FOR[dottedNs] };
59
+ }
60
+ // A dotted, non-`@`, non-fixed-TLD name shaped like `name.<label>` is a scoped
61
+ // customer-TLD candidate — route it to the native (x1id) path, which reads
62
+ // the chain to confirm the TLD is launched (and otherwise re-throws
63
+ // `unrecognized`, so `.sol`/`.eth`/unknown TLDs are unaffected here).
64
+ const scoped = scopedTldCandidate(t);
65
+ if (scoped) {
66
+ return { namespace: scoped.tld, source: "x1id" };
67
+ }
68
+ throw new ResolveError("unrecognized", `"${t}" is not a handle or a known namespace (.x1 .xnt .xen .sol .eth)`, t);
69
+ }
70
+ /** Non-throwing probe: is `input` shaped like any namespace the resolver knows? */
71
+ export function isUniversalName(input) {
72
+ try {
73
+ detectNamespace(input);
74
+ return true;
75
+ }
76
+ catch {
77
+ return false;
78
+ }
79
+ }
80
+ /**
81
+ * The single label of an external domain (`"bob"` from `"bob.sol"`), lowercased
82
+ * and validated. Shared by the SNS and ENS adapters so their label rules stay
83
+ * identical and homograph-safe.
84
+ *
85
+ * ASCII only — a payment identifier that renders a Cyrillic homograph as a
86
+ * successful result is a vector we refuse, the same stance as `parse.ts`.
87
+ * Interior dots (subdomains like `pay.bob.sol` / `pay.bob.eth`) use a different
88
+ * derivation per namespace and are NOT supported in this increment: rejected
89
+ * explicitly rather than mis-derived to a wrong account.
90
+ */
91
+ export function externalLabel(input, suffix) {
92
+ const t = input.trim();
93
+ const dot = t.lastIndexOf(".");
94
+ if (dot <= 0 || t.slice(dot + 1).toLowerCase() !== suffix) {
95
+ throw new ResolveError("invalid-domain", `"${t}" is not a .${suffix} domain`, input);
96
+ }
97
+ const label = t.slice(0, dot).toLowerCase();
98
+ if (label.includes(".")) {
99
+ throw new ResolveError("invalid-domain", `subdomains are not supported yet (.${suffix}): "${t}"`, input);
100
+ }
101
+ if (!/^[a-z0-9-]+$/.test(label)) {
102
+ throw new ResolveError("invalid-domain", `.${suffix} labels must be ASCII letters, digits or hyphen`, input);
103
+ }
104
+ return label;
105
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * ENS adapter — `.eth` names on Ethereum. CONFIG-GATED.
3
+ *
4
+ * Full ENS resolution reads Ethereum mainnet: namehash(name) → the ENS registry's
5
+ * `resolver(node)` → that resolver's `addr(node)`. We do NOT have an Ethereum RPC
6
+ * endpoint configured on this deployment, so this adapter is GATED:
7
+ *
8
+ * - Without `ethRpcUrl` (and a `keccak256` for namehash) it is NOT configured,
9
+ * and `resolve` throws a typed `not-configured` ResolveError. It NEVER
10
+ * fabricates an address — a gated namespace returns "not enabled", never a
11
+ * guess.
12
+ * - With both injected, it performs the real read over JSON-RPC `eth_call`.
13
+ *
14
+ * `keccak256` is injected for the same reason the native path injects curve math
15
+ * via WASM: this SDK does not hand-roll cryptographic primitives. A consumer with
16
+ * an Ethereum stack (ethers/viem/js-sha3) passes their vetted keccak256.
17
+ *
18
+ * The constants below are the well-known ENS MAINNET deployment values. This path
19
+ * has NOT been exercised against a live endpoint here (we have none) — enabling
20
+ * ENS is an owner decision (provision `ETH_RPC_URL`), at which point it must be
21
+ * verified end-to-end before being trusted.
22
+ */
23
+ import type { NamespaceAdapter } from "./universalTypes.js";
24
+ /** ENS registry (mainnet). `resolver(bytes32)` lives here. */
25
+ export declare const ENS_REGISTRY_ADDRESS = "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e";
26
+ /** A keccak-256 implementation over bytes → 32 bytes. Injected (not hand-rolled). */
27
+ export type Keccak256 = (data: Uint8Array) => Uint8Array;
28
+ export interface EnsAdapterConfig {
29
+ /** Ethereum mainnet JSON-RPC endpoint. Absent → adapter not configured. */
30
+ readonly ethRpcUrl?: string;
31
+ /** keccak-256 for namehash. Absent → adapter not configured. */
32
+ readonly keccak256?: Keccak256;
33
+ /** Optional fetch override (testing / custom transport). */
34
+ readonly fetchImpl?: typeof fetch;
35
+ /** ENS registry override (defaults to the mainnet registry). */
36
+ readonly registryAddress?: string;
37
+ }
38
+ /**
39
+ * EIP-137 namehash. `node("")` = 32 zero bytes; for each label (right-to-left)
40
+ * `node = keccak256(node ++ keccak256(label))`. Returns 32 bytes.
41
+ */
42
+ export declare function ensNamehash(name: string, keccak256: Keccak256): Uint8Array;
43
+ /** Build the `.eth` (ENS) adapter — gated on `ethRpcUrl` + `keccak256`. */
44
+ export declare function createEnsAdapter(config?: EnsAdapterConfig): NamespaceAdapter;