@x1id/resolve 0.19.0 → 0.19.2

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.
@@ -44,9 +44,20 @@ export declare const NAMESPACE_MAX_LEN = 16;
44
44
  export type NamespaceStatus = "Active" | "Paused" | "Retired";
45
45
  export interface CreateNamespaceParams {
46
46
  readonly programId: AddressLike;
47
- /** admin — signs AND pays rent; writable. */
48
- readonly admin: AddressLike;
47
+ /** The creator — signs AND pays rent + the creation fee; writable.
48
+ * Permissionless: any wallet, not an admin. (Was mis-named `admin` and
49
+ * built with too few accounts, so the ix failed AccountNotEnoughKeys 3005 —
50
+ * W169.) */
51
+ readonly payer: AddressLike;
49
52
  readonly config: AddressLike;
53
+ /** `Config.treasury` — receives the creation fee; writable. */
54
+ readonly treasury: AddressLike;
55
+ /** The creator's chosen payout wallet, recorded on the namespace for future
56
+ * 75% primary/secondary cuts. An arbitrary pubkey; never signs. */
57
+ readonly creatorPayout: AddressLike;
58
+ /** `["reserved_namespace", label]` PDA — the denylist shadow the handler
59
+ * refuses creation against if it exists; read-only, never written. */
60
+ readonly reserved: AddressLike;
50
61
  /** `["namespace", label]` PDA — init, writable. */
51
62
  readonly namespace: AddressLike;
52
63
  /** Canonical ASCII label (a-z 0-9 -), 1..=16 bytes — an instruction ARG. */
@@ -111,9 +111,14 @@ export function buildCreateNamespaceIx(p) {
111
111
  throw new Error(`label must be 1..=${NAMESPACE_MAX_LEN} chars`);
112
112
  }
113
113
  const data = concat([CREATE_NAMESPACE_DISCRIMINATOR, borshString(p.label)]);
114
+ // Account order matches the program's `CreateNamespace` context exactly:
115
+ // payer, config, treasury, creator_payout, reserved, namespace, system.
114
116
  const keys = [
115
- { pubkey: toBase58(p.admin, "admin"), isSigner: true, isWritable: true },
117
+ { pubkey: toBase58(p.payer, "payer"), isSigner: true, isWritable: true },
116
118
  { pubkey: toBase58(p.config, "config"), isSigner: false, isWritable: false },
119
+ { pubkey: toBase58(p.treasury, "treasury"), isSigner: false, isWritable: true },
120
+ { pubkey: toBase58(p.creatorPayout, "creatorPayout"), isSigner: false, isWritable: false },
121
+ { pubkey: toBase58(p.reserved, "reserved"), isSigner: false, isWritable: false },
117
122
  { pubkey: toBase58(p.namespace, "namespace"), isSigner: false, isWritable: true },
118
123
  { pubkey: toBase58(p.systemProgram ?? SYSTEM_PROGRAM_ID, "systemProgram"), isSigner: false, isWritable: false },
119
124
  ];
package/dist/base58.js CHANGED
@@ -65,5 +65,13 @@ export function decodeBase58_32(s) {
65
65
  out[i] = Number(n & 255n);
66
66
  n >>= 8n;
67
67
  }
68
+ // W167: reject anything that isn't the canonical base58 of EXACTLY these 32
69
+ // bytes. Without this, a short valid-base58 string ("notakey") decodes to a
70
+ // small number and is silently LEFT-PADDED into a bogus 32-byte "address",
71
+ // so builders / sign-in / resolve / MCP prepare_register accept garbage as a
72
+ // pubkey. The round-trip also correctly accepts a real key with leading zero
73
+ // bytes (encodeBase58 re-emits its leading '1's).
74
+ if (encodeBase58(out) !== s)
75
+ return null;
68
76
  return out;
69
77
  }
package/dist/gate.js CHANGED
@@ -15,6 +15,7 @@
15
15
  * attestor-signed `Attestation` account.
16
16
  */
17
17
  import { fetchAttestations, isHandleVerified, ATTESTATION_KIND_DNS, ATTESTATION_KIND_SOCIAL, ATTESTATION_KIND_X, ATTESTATION_KIND_DISCORD, ATTESTATION_KIND_GITHUB, ATTESTATION_KIND_TELEGRAM, } from "./attestation.js";
18
+ import { ResolveError } from "./types.js";
18
19
  /** Named platform kinds, re-exported so callers gate without magic numbers. */
19
20
  export const GateKind = {
20
21
  dns: ATTESTATION_KIND_DNS,
@@ -29,7 +30,16 @@ export const GateKind = {
29
30
  export function attestationsPassGate(attestations, gate) {
30
31
  if (gate === "any-verified")
31
32
  return isHandleVerified(attestations);
32
- const kinds = "kind" in gate ? [gate.kind] : gate.anyOf;
33
+ // W171: a malformed gate (null, a string other than "any-verified", or an
34
+ // object with neither `kind` nor `anyOf`) otherwise threw a raw
35
+ // `TypeError: Cannot use 'in' operator…`. Surface it as a typed ResolveError.
36
+ if (gate === null || typeof gate !== "object") {
37
+ throw new ResolveError("invalid-input", 'gate must be "any-verified", { kind } or { anyOf: [...] }', String(gate));
38
+ }
39
+ const kinds = "kind" in gate ? [gate.kind] : "anyOf" in gate ? gate.anyOf : null;
40
+ if (kinds === null) {
41
+ throw new ResolveError("invalid-input", "gate object needs a `kind` or `anyOf`", JSON.stringify(gate));
42
+ }
33
43
  return attestations.some((a) => !a.stale && !a.holderMismatch && kinds.includes(a.kind));
34
44
  }
35
45
  /** Fetch a handle's attestations and test the gate in one call. `handleAccount`,
package/dist/index.d.ts CHANGED
@@ -42,6 +42,7 @@ export * from "./integrator.js";
42
42
  export * from "./voucher.js";
43
43
  export * from "./agent.js";
44
44
  export * from "./register.js";
45
+ export * from "./transfer.js";
45
46
  export * from "./adminConfig.js";
46
47
  export * from "./lease.js";
47
48
  export * from "./accounts.js";
package/dist/index.js CHANGED
@@ -42,6 +42,7 @@ export * from "./integrator.js";
42
42
  export * from "./voucher.js";
43
43
  export * from "./agent.js";
44
44
  export * from "./register.js";
45
+ export * from "./transfer.js";
45
46
  export * from "./adminConfig.js";
46
47
  export * from "./lease.js";
47
48
  // Was previously imported here for internal use only, never re-exported —
package/dist/register.js CHANGED
@@ -16,6 +16,7 @@
16
16
  * field this builder needs (the treasury address every registration pays).
17
17
  */
18
18
  import { encodeBase58, decodeBase58_32 } from "./base58.js";
19
+ import { normalizeHandle } from "./parse.js";
19
20
  /** Anchor instruction discriminator: `sha256("global:register")[0..8]`. */
20
21
  export const REGISTER_DISCRIMINATOR = Uint8Array.from([
21
22
  211, 124, 67, 15, 211, 194, 178, 240,
@@ -73,6 +74,16 @@ export function buildRegisterIx(p) {
73
74
  if (!Number.isInteger(p.handleType) || p.handleType < 0 || p.handleType > 3) {
74
75
  throw new Error("handleType must be 0 (Human), 1 (Merchant), 2 (Org) or 3 (Agent)");
75
76
  }
77
+ // W171: the Handle PDA is derived from the CANONICAL name, so a non-canonical
78
+ // name (uppercase, surrounding space, …) builds an instruction the chain
79
+ // rejects with an opaque ConstraintSeeds (2006). normalizeHandle validates the
80
+ // grammar (ASCII, length, no leading/trailing/double hyphen, not all-digit)
81
+ // AND lowercases; require the caller to pass that canonical form so `handle`
82
+ // and the encoded name agree.
83
+ const canonical = normalizeHandle(p.name);
84
+ if (canonical !== p.name) {
85
+ throw new Error(`name must be the canonical handle "${canonical}" (got "${p.name}") — normalizeHandle() it and derive \`handle\` from the same value`);
86
+ }
76
87
  const nameBytes = new TextEncoder().encode(p.name);
77
88
  if (nameBytes.length < 1 || nameBytes.length > 32) {
78
89
  throw new Error("name must be 1..=32 bytes (UTF-8)");
@@ -123,6 +134,16 @@ export function buildRegisterReservedIx(p) {
123
134
  if (!Number.isInteger(p.handleType) || p.handleType < 0 || p.handleType > 3) {
124
135
  throw new Error("handleType must be 0 (Human), 1 (Merchant), 2 (Org) or 3 (Agent)");
125
136
  }
137
+ // W171: the Handle PDA is derived from the CANONICAL name, so a non-canonical
138
+ // name (uppercase, surrounding space, …) builds an instruction the chain
139
+ // rejects with an opaque ConstraintSeeds (2006). normalizeHandle validates the
140
+ // grammar (ASCII, length, no leading/trailing/double hyphen, not all-digit)
141
+ // AND lowercases; require the caller to pass that canonical form so `handle`
142
+ // and the encoded name agree.
143
+ const canonical = normalizeHandle(p.name);
144
+ if (canonical !== p.name) {
145
+ throw new Error(`name must be the canonical handle "${canonical}" (got "${p.name}") — normalizeHandle() it and derive \`handle\` from the same value`);
146
+ }
126
147
  const nameBytes = new TextEncoder().encode(p.name);
127
148
  if (nameBytes.length < 1 || nameBytes.length > 32) {
128
149
  throw new Error("name must be 1..=32 bytes (UTF-8)");
@@ -194,6 +215,16 @@ export function buildRegisterWithFortiIx(p) {
194
215
  if (!Number.isInteger(p.handleType) || p.handleType < 0 || p.handleType > 3) {
195
216
  throw new Error("handleType must be 0 (Human), 1 (Merchant), 2 (Org) or 3 (Agent)");
196
217
  }
218
+ // W171: the Handle PDA is derived from the CANONICAL name, so a non-canonical
219
+ // name (uppercase, surrounding space, …) builds an instruction the chain
220
+ // rejects with an opaque ConstraintSeeds (2006). normalizeHandle validates the
221
+ // grammar (ASCII, length, no leading/trailing/double hyphen, not all-digit)
222
+ // AND lowercases; require the caller to pass that canonical form so `handle`
223
+ // and the encoded name agree.
224
+ const canonical = normalizeHandle(p.name);
225
+ if (canonical !== p.name) {
226
+ throw new Error(`name must be the canonical handle "${canonical}" (got "${p.name}") — normalizeHandle() it and derive \`handle\` from the same value`);
227
+ }
197
228
  const nameBytes = new TextEncoder().encode(p.name);
198
229
  if (nameBytes.length < 1 || nameBytes.length > 32) {
199
230
  throw new Error("name must be 1..=32 bytes (UTF-8)");
package/dist/subname.js CHANGED
@@ -173,6 +173,12 @@ export function buildCreateSubnameIx(p) {
173
173
  if (labelBytes.length === 0 || labelBytes.length > SUBNAME_LABEL_MAX_LEN) {
174
174
  throw new Error(`label must be 1..=${SUBNAME_LABEL_MAX_LEN} bytes`);
175
175
  }
176
+ // W171: a subname label follows handle-label grammar — a label like
177
+ // "Bad Label" (space, uppercase) otherwise builds and is only rejected on
178
+ // chain. Lowercase a-z / 0-9 with interior single hyphens only.
179
+ if (!/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(p.label) || p.label.includes("--")) {
180
+ throw new Error("label must be lowercase a-z, 0-9 and interior single hyphens only (no spaces, uppercase, or leading/trailing/double hyphen)");
181
+ }
176
182
  const encLabel = encVec(labelBytes);
177
183
  const data = new Uint8Array(8 + encLabel.length);
178
184
  data.set(CREATE_SUBNAME_DISCRIMINATOR, 0);
@@ -297,6 +297,13 @@ function checkKey(key) {
297
297
  if (bytes.length === 0 || bytes.length > TEXT_KEY_MAX_LEN) {
298
298
  throw new Error(`key must be 1..=${TEXT_KEY_MAX_LEN} bytes of UTF-8`);
299
299
  }
300
+ // W171: reject keys with spaces / control chars / non-ASCII — a key like
301
+ // "BAD KEY!" otherwise builds and only fails loosely downstream. Record keys
302
+ // are dotted identifiers (e.g. "agent.manifest", "com.twitter"): visible
303
+ // ASCII, no whitespace.
304
+ if (!/^[\x21-\x7e]+$/.test(key)) {
305
+ throw new Error('key must be visible ASCII with no spaces (e.g. "agent.manifest")');
306
+ }
300
307
  return bytes;
301
308
  }
302
309
  function checkValue(value) {
@@ -0,0 +1,45 @@
1
+ /**
2
+ * `transfer` — hand an UNTOKENIZED handle to a new owner (a direct gift /
3
+ * reassignment of `handle.owner`). This is the plain-handle counterpart to a
4
+ * tokenized handle's pNFT transfer: a tokenized handle's ownership IS its NFT,
5
+ * so the program REFUSES `transfer` on one (`HandleIsTokenized`) — move the
6
+ * pNFT instead (see `pnftTransfer.ts`).
7
+ *
8
+ * Signed by the handle's CURRENT owner (`has_one = owner` on-chain). The
9
+ * program also refuses the move if the handle is locked (`require_unlocked`)
10
+ * or a live auction on it still has bids. It is NOT gated by the registry
11
+ * pause, so a treasury gift can be made even while registration is paused.
12
+ *
13
+ * Hand-rolled like the rest of this package — no Anchor client, no web3.js.
14
+ * PDAs are NOT derived here (see delegate.ts's module docs); derive
15
+ * `["handle", name]` and `["auction", handle]` with your runtime's canonical
16
+ * `findProgramAddress` and pass them in.
17
+ */
18
+ import type { AddressLike, BuiltInstruction } from "./delegate.js";
19
+ /** Anchor instruction discriminator: `sha256("global:transfer")[0..8]`. */
20
+ export declare const TRANSFER_DISCRIMINATOR: Uint8Array;
21
+ export interface TransferParams {
22
+ /** The registry program id. */
23
+ readonly programId: AddressLike;
24
+ /** The handle's CURRENT owner — must equal `handle.owner` on-chain. Signer. */
25
+ readonly owner: AddressLike;
26
+ /** The `["handle", name]` (or scoped `["handle", label, name]`) PDA. Must be
27
+ * UNTOKENIZED; a tokenized handle is moved by transferring its pNFT. */
28
+ readonly handle: AddressLike;
29
+ /** The `["auction", handle]` PDA — read-only; the program reads it only to
30
+ * refuse a transfer while a live auction still has bids. Pass the derived
31
+ * address even if no auction account exists there. */
32
+ readonly auction: AddressLike;
33
+ /** The address to hand the handle to. */
34
+ readonly newOwner: AddressLike;
35
+ /** Scoped (under-TLD) handles only: the account(s) `verify_handle_pda` needs
36
+ * to re-derive `["handle", label, name]`. Omit for a bare `@handle`. */
37
+ readonly remainingAccounts?: readonly AddressLike[];
38
+ }
39
+ /**
40
+ * Build `transfer(new_owner)`. The data is the 8-byte discriminator followed by
41
+ * the 32-byte new owner; the accounts are `owner` (signer), `handle` (writable)
42
+ * and the read-only `["auction", handle]` PDA, plus any scoped-TLD
43
+ * remaining-accounts. Returns an UNSIGNED instruction — sign with `owner`.
44
+ */
45
+ export declare function buildTransferIx(p: TransferParams): BuiltInstruction;
@@ -0,0 +1,56 @@
1
+ /**
2
+ * `transfer` — hand an UNTOKENIZED handle to a new owner (a direct gift /
3
+ * reassignment of `handle.owner`). This is the plain-handle counterpart to a
4
+ * tokenized handle's pNFT transfer: a tokenized handle's ownership IS its NFT,
5
+ * so the program REFUSES `transfer` on one (`HandleIsTokenized`) — move the
6
+ * pNFT instead (see `pnftTransfer.ts`).
7
+ *
8
+ * Signed by the handle's CURRENT owner (`has_one = owner` on-chain). The
9
+ * program also refuses the move if the handle is locked (`require_unlocked`)
10
+ * or a live auction on it still has bids. It is NOT gated by the registry
11
+ * pause, so a treasury gift can be made even while registration is paused.
12
+ *
13
+ * Hand-rolled like the rest of this package — no Anchor client, no web3.js.
14
+ * PDAs are NOT derived here (see delegate.ts's module docs); derive
15
+ * `["handle", name]` and `["auction", handle]` with your runtime's canonical
16
+ * `findProgramAddress` and pass them in.
17
+ */
18
+ import { encodeBase58, decodeBase58_32 } from "./base58.js";
19
+ /** Anchor instruction discriminator: `sha256("global:transfer")[0..8]`. */
20
+ export const TRANSFER_DISCRIMINATOR = Uint8Array.from([
21
+ 0xa3, 0x34, 0xc8, 0xe7, 0x8c, 0x03, 0x45, 0xba,
22
+ ]);
23
+ function toBytes32(v, what) {
24
+ if (typeof v === "string") {
25
+ const b = decodeBase58_32(v);
26
+ if (!b)
27
+ throw new Error(`${what} is not a valid base58 address`);
28
+ return b;
29
+ }
30
+ if (v.length !== 32)
31
+ throw new Error(`${what} must be exactly 32 bytes`);
32
+ return v;
33
+ }
34
+ function toBase58(v, what) {
35
+ return encodeBase58(toBytes32(v, what));
36
+ }
37
+ /**
38
+ * Build `transfer(new_owner)`. The data is the 8-byte discriminator followed by
39
+ * the 32-byte new owner; the accounts are `owner` (signer), `handle` (writable)
40
+ * and the read-only `["auction", handle]` PDA, plus any scoped-TLD
41
+ * remaining-accounts. Returns an UNSIGNED instruction — sign with `owner`.
42
+ */
43
+ export function buildTransferIx(p) {
44
+ const data = new Uint8Array(8 + 32);
45
+ data.set(TRANSFER_DISCRIMINATOR, 0);
46
+ data.set(toBytes32(p.newOwner, "newOwner"), 8);
47
+ const keys = [
48
+ { pubkey: toBase58(p.owner, "owner"), isSigner: true, isWritable: false },
49
+ { pubkey: toBase58(p.handle, "handle"), isSigner: false, isWritable: true },
50
+ { pubkey: toBase58(p.auction, "auction"), isSigner: false, isWritable: false },
51
+ ];
52
+ for (const a of p.remainingAccounts ?? []) {
53
+ keys.push({ pubkey: toBase58(a, "remainingAccount"), isSigner: false, isWritable: false });
54
+ }
55
+ return { programId: toBase58(p.programId, "programId"), keys, data };
56
+ }
package/dist/types.d.ts CHANGED
@@ -94,6 +94,10 @@ export interface Resolved {
94
94
  export type Chain = "X1" | "SOL" | "ETH" | "BTC";
95
95
  export declare const CHAIN_COIN_TYPE: Readonly<Record<Chain, number>>;
96
96
  export type ResolveErrorCode = "unrecognized" | "ambiguous" | "invalid-handle" | "invalid-domain" | "not-found" | "no-record-for-chain" | "rpc-error"
97
+ /** A caller passed a malformed value to an SDK primitive (e.g. a gate that
98
+ * isn't "any-verified"/{kind}/{anyOf}) — a programming error, surfaced as a
99
+ * typed ResolveError rather than a raw TypeError. */
100
+ | "invalid-input"
97
101
  /**
98
102
  * A reachable namespace exists for this name, but the adapter that would
99
103
  * resolve it is not configured on this deployment (e.g. no `ETH_RPC_URL` for
package/dist/x402.js CHANGED
@@ -60,6 +60,14 @@ export const X402_VERSION = 2;
60
60
  * and let the buyer fetch their own — the spec allows either).
61
61
  */
62
62
  export function buildX402Requirements(p) {
63
+ // W171: payTo / asset / buyer are REQUIRED base58 pubkeys — a missing one
64
+ // otherwise sails through (payTo: undefined) straight into the requirements.
65
+ const PUBKEY_RE = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
66
+ for (const [field, value] of [["payTo", p.payTo], ["asset", p.asset], ["buyer", p.buyer]]) {
67
+ if (!PUBKEY_RE.test((value ?? "").trim())) {
68
+ throw new Error(`${field} must be a base58 address (32-44 chars); got ${value === undefined ? "undefined" : `"${value}"`}`);
69
+ }
70
+ }
63
71
  if (!/^\d+$/.test(p.amount)) {
64
72
  throw new Error("amount must be a decimal string of smallest units (e.g. \"1000000\")");
65
73
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@x1id/resolve",
3
- "version": "0.19.0",
3
+ "version": "0.19.2",
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",