@x1id/resolve 0.19.1 → 0.21.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.
- package/README.md +80 -0
- package/dist/connect.d.ts +20 -8
- package/dist/connect.js +34 -10
- package/dist/gate.js +11 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/register.js +31 -0
- package/dist/siws.d.ts +310 -0
- package/dist/siws.js +607 -0
- package/dist/subname.js +6 -0
- package/dist/textRecords.js +7 -0
- package/dist/transfer.d.ts +45 -0
- package/dist/transfer.js +56 -0
- package/dist/types.d.ts +4 -0
- package/dist/voucher.d.ts +2 -2
- package/dist/voucher.js +14 -3
- package/dist/x402.js +8 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -620,6 +620,86 @@ of this zero-dependency package). Use the MCP server's `prepare_x402_payment`
|
|
|
620
620
|
> `confirmed` gets a `402 "payment not yet confirmed; finalize it and retry"`.
|
|
621
621
|
> Poll the signature to `finalized` before the retry.
|
|
622
622
|
|
|
623
|
+
## Sign in with Solana
|
|
624
|
+
|
|
625
|
+
Sign a user in with any [Wallet Standard](https://github.com/wallet-standard/wallet-standard)
|
|
626
|
+
wallet that implements `solana:signIn` (SIWS), against the hosted X1ID Connect
|
|
627
|
+
API. You get back a Bearer session proving **wallet control**; the wallet's
|
|
628
|
+
primary `@handle`, if it has one, comes along as metadata.
|
|
629
|
+
|
|
630
|
+
```ts
|
|
631
|
+
import { signInWithSolana, SiwsError } from "@x1id/resolve";
|
|
632
|
+
|
|
633
|
+
try {
|
|
634
|
+
const session = await signInWithSolana({
|
|
635
|
+
wallet, // a Wallet Standard wallet object (e.g. from getWallets().get())
|
|
636
|
+
apiKey: "x1id_…", // your integrator key from developers.x1id.io
|
|
637
|
+
// domain defaults to location.host. It must be on your deployment's SIWS allowlist.
|
|
638
|
+
statement: "Sign in to Example.",
|
|
639
|
+
});
|
|
640
|
+
// session.token (send as `Authorization: Bearer …`), session.address, session.handle
|
|
641
|
+
} catch (e) {
|
|
642
|
+
if (e instanceof SiwsError) {
|
|
643
|
+
// e.code: "wallet-unsupported" | "user-rejected" | "domain-not-allowed"
|
|
644
|
+
// | "nonce-expired" | "invalid-params" | "wallet-timeout" | "wallet-error" | "network" | "api-error"
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
```
|
|
648
|
+
|
|
649
|
+
### With fallbacks: `signInWithWallet`
|
|
650
|
+
|
|
651
|
+
Some wallets lack `solana:signIn` or implement it badly, and a Ledger refuses
|
|
652
|
+
message signing. `signInWithWallet` takes the same arguments and tries, in the
|
|
653
|
+
developers.x1id.io order:
|
|
654
|
+
|
|
655
|
+
1. SIWS (`solana:signIn`);
|
|
656
|
+
2. a signed, **never-sent** memo transaction (`solana:signTransaction`) whose
|
|
657
|
+
only instruction is an SPL-Memo carrying the server's
|
|
658
|
+
`x1id-verify:<audience>:<nonce>`;
|
|
659
|
+
3. `solana:signMessage` over the server's
|
|
660
|
+
`x1id-connect:v2:wallet-control:<audience>:<address>:<nonce>`.
|
|
661
|
+
|
|
662
|
+
```ts
|
|
663
|
+
import { signInWithWallet, X1ID_MAINNET_SIGNIN_FEE_PAYER } from "@x1id/resolve";
|
|
664
|
+
|
|
665
|
+
const { method, ...session } = await signInWithWallet({
|
|
666
|
+
wallet,
|
|
667
|
+
apiKey: "x1id_…",
|
|
668
|
+
rpcUrl: "https://rpc.mainnet.x1.xyz", // optional: recent blockhash for the memo tx
|
|
669
|
+
signinFeePayer: X1ID_MAINNET_SIGNIN_FEE_PAYER, // optional: no-fee layout (api.x1id.io mainnet)
|
|
670
|
+
});
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
A user rejection or a wallet timeout stops the chain, so the user is never
|
|
674
|
+
prompted twice after saying no. The audience (your key id) is part of every
|
|
675
|
+
signed string, and the SDK signs the server's strings verbatim. It refuses to
|
|
676
|
+
sign a memo or challenge that is not bound to an audience. Without
|
|
677
|
+
`signinFeePayer` the user is the memo transaction's fee payer, which every
|
|
678
|
+
deployment accepts. The transaction is never sent either way. Outside a
|
|
679
|
+
browser, pass `domain` or set `methods: ["memo-tx", "sign-message"]`.
|
|
680
|
+
|
|
681
|
+
The steps are also exported on their own, for wallets you drive yourself:
|
|
682
|
+
|
|
683
|
+
| function | does |
|
|
684
|
+
|---|---|
|
|
685
|
+
| `requestSignInChallenge({ apiKey, wallet, apiBase? })` | `POST /v1/auth/challenge {wallet, mode:"signin"}` → `{ nonce, expires_at, … }` |
|
|
686
|
+
| `buildSignInInput({ domain?, address, nonce, statement?, uri?, issuedAt? })` | the `solana:signIn` input: domain, address, statement, uri, version `"1"`, nonce, issuedAt |
|
|
687
|
+
| `verifySolanaSignIn({ apiKey, nonce, signedMessage, signature, apiBase? })` | `POST /v1/auth/verify-signin` (signed bytes as base64, signature as base58) → session |
|
|
688
|
+
| `buildSignInMessageText(input)` | the SIWS message text a wallet renders from that input |
|
|
689
|
+
| `requestConnectChallenge({ apiKey, wallet, mode? })` | challenge for `"signin"`, `"tx"` (adds `memo`), or signMessage (no mode) |
|
|
690
|
+
| `buildSignInMemoTransaction({ user, memo, blockhash?, feePayer? })` | the unsigned memo-only legacy transaction (no web3.js) |
|
|
691
|
+
| `verifySignInTransaction({ apiKey, nonce, signedTransaction })` | `POST /v1/auth/verify-tx` |
|
|
692
|
+
| `verifySignInMessage({ apiKey, nonce, signature, signer })` | `POST /v1/auth/verify` |
|
|
693
|
+
|
|
694
|
+
The server checks that line 1 is `<domain> wants you to sign in with your Solana account:`
|
|
695
|
+
with an allowed domain, that line 2 is the challenged wallet, that a `Nonce:` line
|
|
696
|
+
carries the single-use challenge nonce, and the ed25519 signature (raw bytes or
|
|
697
|
+
the SIMD-0048 off-chain envelope). A nonce is single-use and expires, so on
|
|
698
|
+
`nonce-expired` call `signInWithSolana` again.
|
|
699
|
+
|
|
700
|
+
This is the hosted-session flow. For a server-less proof your own backend
|
|
701
|
+
verifies, see `signInWithX1ID` in [SIGNIN.md](./SIGNIN.md).
|
|
702
|
+
|
|
623
703
|
## Errors
|
|
624
704
|
|
|
625
705
|
`ResolveError` carries a `code` a UI can branch on:
|
package/dist/connect.d.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*
|
|
14
14
|
* So this flow proves wallet control a different, robust way: the wallet signs a
|
|
15
15
|
* throwaway MEMO TRANSACTION — one SPL-Memo instruction carrying
|
|
16
|
-
* `x1id-verify:<nonce>`, fee-paid by the wallet, never broadcast. The hosted
|
|
16
|
+
* `x1id-verify:<audience>:<nonce>`, fee-paid by the wallet, never broadcast. The hosted
|
|
17
17
|
* `POST /v1/auth/verify-tx` endpoint deserializes the signed artifact, asserts
|
|
18
18
|
* it is EXACTLY that memo (no disguised transfer/approval), that the fee payer
|
|
19
19
|
* is the wallet, that the memo binds our single-use nonce, and verifies the
|
|
@@ -33,13 +33,19 @@
|
|
|
33
33
|
* free of a hard web3 dependency and tests can supply a fake.
|
|
34
34
|
*/
|
|
35
35
|
/** Memo prefix the verify-tx proof binds to. The signed memo instruction's
|
|
36
|
-
* UTF-8 data MUST be EXACTLY `x1id-verify:<nonce>` — the server
|
|
37
|
-
* other data, which is what kills replay of an unrelated signed
|
|
36
|
+
* UTF-8 data MUST be EXACTLY `x1id-verify:<audience>:<nonce>` — the server
|
|
37
|
+
* rejects any other data, which is what kills replay of an unrelated signed
|
|
38
|
+
* transaction. The AUDIENCE (your API key id, or developers.x1id.io for the
|
|
39
|
+
* Developer Portal) is returned by the challenge as `audience` and binds the
|
|
40
|
+
* signature to the application it was made for (R3-L2P-1); the hosted API no
|
|
41
|
+
* longer accepts the unbound `x1id-verify:<nonce>`. */
|
|
38
42
|
export declare const VERIFY_TX_MEMO_PREFIX: "x1id-verify:";
|
|
39
43
|
/** Canonical SPL Memo program id — the program the challenge memo rides on. */
|
|
40
44
|
export declare const MEMO_PROGRAM_ID: "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr";
|
|
41
|
-
/** The exact memo string for a
|
|
42
|
-
|
|
45
|
+
/** The exact memo string for a challenge nonce and its audience. Without an
|
|
46
|
+
* audience this is the legacy unbound form, which the current hosted API
|
|
47
|
+
* REJECTS — always pass the challenge's `audience` (or use its `memo`). */
|
|
48
|
+
export declare function verifyTxMemo(nonce: string, audience?: string): string;
|
|
43
49
|
/** The slice of a web3.js `PublicKey` used here. */
|
|
44
50
|
export interface PublicKeyInstance {
|
|
45
51
|
toBytes(): Uint8Array;
|
|
@@ -87,10 +93,14 @@ export declare const PLACEHOLDER_BLOCKHASH: "11111111111111111111111111111111";
|
|
|
87
93
|
export interface BuildVerifyTxChallengeParams {
|
|
88
94
|
/** The single-use nonce from the tx-mode challenge. */
|
|
89
95
|
readonly nonce: string;
|
|
90
|
-
/** The memo the server returned (`x1id-verify:<nonce>`)
|
|
91
|
-
*
|
|
96
|
+
/** The memo the server returned (`x1id-verify:<audience>:<nonce>`) —
|
|
97
|
+
* RECOMMENDED: pass it verbatim. When omitted it is derived from `nonce` +
|
|
98
|
+
* `audience`. When supplied it MUST be an `x1id-verify:` memo for THIS
|
|
99
|
+
* nonce (and, if `audience` is given, for that audience), or the build
|
|
92
100
|
* throws (catches a client that paired the wrong nonce/memo). */
|
|
93
101
|
readonly memo?: string;
|
|
102
|
+
/** The challenge's `audience` (R3-L2P-1). */
|
|
103
|
+
readonly audience?: string;
|
|
94
104
|
/** The signing wallet, base58 — becomes the fee payer AND the memo signer. */
|
|
95
105
|
readonly feePayer: string;
|
|
96
106
|
/** A recent blockhash. Defaults to {@link PLACEHOLDER_BLOCKHASH} (the tx is
|
|
@@ -101,7 +111,7 @@ export interface BuildVerifyTxChallengeParams {
|
|
|
101
111
|
* Build the memo-only challenge transaction the wallet signs for verify-tx.
|
|
102
112
|
*
|
|
103
113
|
* The result is a legacy `Transaction` with EXACTLY one instruction: an SPL-Memo
|
|
104
|
-
* instruction whose data is the UTF-8 bytes of `x1id-verify:<nonce>`, with the
|
|
114
|
+
* instruction whose data is the UTF-8 bytes of `x1id-verify:<audience>:<nonce>`, with the
|
|
105
115
|
* fee payer listed as a required signer on that instruction. Nothing else — no
|
|
106
116
|
* transfer, no compute-budget, no durable nonce — so the signed artifact can
|
|
107
117
|
* only ever be a sign-in proof, never a disguised value-moving transaction. The
|
|
@@ -136,6 +146,8 @@ export interface ConnectSession {
|
|
|
136
146
|
export interface TxChallenge {
|
|
137
147
|
readonly nonce: string;
|
|
138
148
|
readonly memo: string;
|
|
149
|
+
/** The audience the memo names (R3-L2P-1); absent from older deployments. */
|
|
150
|
+
readonly audience?: string;
|
|
139
151
|
readonly handle: string | null;
|
|
140
152
|
readonly expires_at: number;
|
|
141
153
|
}
|
package/dist/connect.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*
|
|
14
14
|
* So this flow proves wallet control a different, robust way: the wallet signs a
|
|
15
15
|
* throwaway MEMO TRANSACTION — one SPL-Memo instruction carrying
|
|
16
|
-
* `x1id-verify:<nonce>`, fee-paid by the wallet, never broadcast. The hosted
|
|
16
|
+
* `x1id-verify:<audience>:<nonce>`, fee-paid by the wallet, never broadcast. The hosted
|
|
17
17
|
* `POST /v1/auth/verify-tx` endpoint deserializes the signed artifact, asserts
|
|
18
18
|
* it is EXACTLY that memo (no disguised transfer/approval), that the fee payer
|
|
19
19
|
* is the wallet, that the memo binds our single-use nonce, and verifies the
|
|
@@ -33,14 +33,26 @@
|
|
|
33
33
|
* free of a hard web3 dependency and tests can supply a fake.
|
|
34
34
|
*/
|
|
35
35
|
/** Memo prefix the verify-tx proof binds to. The signed memo instruction's
|
|
36
|
-
* UTF-8 data MUST be EXACTLY `x1id-verify:<nonce>` — the server
|
|
37
|
-
* other data, which is what kills replay of an unrelated signed
|
|
36
|
+
* UTF-8 data MUST be EXACTLY `x1id-verify:<audience>:<nonce>` — the server
|
|
37
|
+
* rejects any other data, which is what kills replay of an unrelated signed
|
|
38
|
+
* transaction. The AUDIENCE (your API key id, or developers.x1id.io for the
|
|
39
|
+
* Developer Portal) is returned by the challenge as `audience` and binds the
|
|
40
|
+
* signature to the application it was made for (R3-L2P-1); the hosted API no
|
|
41
|
+
* longer accepts the unbound `x1id-verify:<nonce>`. */
|
|
38
42
|
export const VERIFY_TX_MEMO_PREFIX = "x1id-verify:";
|
|
39
43
|
/** Canonical SPL Memo program id — the program the challenge memo rides on. */
|
|
40
44
|
export const MEMO_PROGRAM_ID = "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr";
|
|
41
|
-
/**
|
|
42
|
-
|
|
43
|
-
|
|
45
|
+
/** An audience: a key id (16 base64url chars) or a bare DNS name. */
|
|
46
|
+
const AUDIENCE_RE = /^[A-Za-z0-9._-]{1,253}$/;
|
|
47
|
+
/** The exact memo string for a challenge nonce and its audience. Without an
|
|
48
|
+
* audience this is the legacy unbound form, which the current hosted API
|
|
49
|
+
* REJECTS — always pass the challenge's `audience` (or use its `memo`). */
|
|
50
|
+
export function verifyTxMemo(nonce, audience) {
|
|
51
|
+
if (audience === undefined)
|
|
52
|
+
return VERIFY_TX_MEMO_PREFIX + nonce;
|
|
53
|
+
if (!AUDIENCE_RE.test(audience))
|
|
54
|
+
throw new Error(`invalid audience "${audience}"`);
|
|
55
|
+
return `${VERIFY_TX_MEMO_PREFIX}${audience}:${nonce}`;
|
|
44
56
|
}
|
|
45
57
|
/** 32 zero-bytes in base58 — a structurally valid blockhash. The transaction is
|
|
46
58
|
* NEVER broadcast, so a placeholder blockhash is fine; a wallet that insists on
|
|
@@ -50,7 +62,7 @@ export const PLACEHOLDER_BLOCKHASH = "11111111111111111111111111111111";
|
|
|
50
62
|
* Build the memo-only challenge transaction the wallet signs for verify-tx.
|
|
51
63
|
*
|
|
52
64
|
* The result is a legacy `Transaction` with EXACTLY one instruction: an SPL-Memo
|
|
53
|
-
* instruction whose data is the UTF-8 bytes of `x1id-verify:<nonce>`, with the
|
|
65
|
+
* instruction whose data is the UTF-8 bytes of `x1id-verify:<audience>:<nonce>`, with the
|
|
54
66
|
* fee payer listed as a required signer on that instruction. Nothing else — no
|
|
55
67
|
* transfer, no compute-budget, no durable nonce — so the signed artifact can
|
|
56
68
|
* only ever be a sign-in proof, never a disguised value-moving transaction. The
|
|
@@ -62,9 +74,20 @@ export function buildVerifyTxChallenge(web3, params) {
|
|
|
62
74
|
throw new Error("nonce is required");
|
|
63
75
|
if (typeof feePayer !== "string" || feePayer.length === 0)
|
|
64
76
|
throw new Error("feePayer is required");
|
|
65
|
-
const memo = params.memo ?? verifyTxMemo(nonce);
|
|
66
|
-
if (
|
|
67
|
-
|
|
77
|
+
const memo = params.memo ?? verifyTxMemo(nonce, params.audience);
|
|
78
|
+
if (params.audience !== undefined) {
|
|
79
|
+
const want = verifyTxMemo(nonce, params.audience);
|
|
80
|
+
if (memo !== want)
|
|
81
|
+
throw new Error(`memo must be exactly "${want}" for this nonce`);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
// No audience given: accept the server's audience-bound memo for this
|
|
85
|
+
// nonce, or the legacy unbound form (older deployments).
|
|
86
|
+
const rest = memo.startsWith(VERIFY_TX_MEMO_PREFIX) ? memo.slice(VERIFY_TX_MEMO_PREFIX.length) : null;
|
|
87
|
+
const aud = rest !== null && rest.endsWith(`:${nonce}`) ? rest.slice(0, -(nonce.length + 1)) : null;
|
|
88
|
+
if (rest !== nonce && !(aud !== null && AUDIENCE_RE.test(aud))) {
|
|
89
|
+
throw new Error(`memo must be "${VERIFY_TX_MEMO_PREFIX}<audience>:${nonce}" for this nonce`);
|
|
90
|
+
}
|
|
68
91
|
}
|
|
69
92
|
const payer = new web3.PublicKey(feePayer);
|
|
70
93
|
const memoProgram = new web3.PublicKey(MEMO_PROGRAM_ID);
|
|
@@ -169,6 +192,7 @@ export async function signinWithTx(cfg, params) {
|
|
|
169
192
|
const tx = buildVerifyTxChallenge(params.web3, {
|
|
170
193
|
nonce: challenge.nonce,
|
|
171
194
|
memo: challenge.memo,
|
|
195
|
+
...(typeof challenge.audience === "string" ? { audience: challenge.audience } : {}),
|
|
172
196
|
feePayer: params.wallet,
|
|
173
197
|
...(params.blockhash !== undefined ? { blockhash: params.blockhash } : {}),
|
|
174
198
|
});
|
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
|
-
|
|
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";
|
|
@@ -54,6 +55,7 @@ export * from "./pnftTransfer.js";
|
|
|
54
55
|
export * from "./payment.js";
|
|
55
56
|
export * from "./signin.js";
|
|
56
57
|
export * from "./connect.js";
|
|
58
|
+
export * from "./siws.js";
|
|
57
59
|
export * from "./domainProof.js";
|
|
58
60
|
export { scopedTldCandidate, namespaceIsActive, makeScopedHandleDeriver, type ScopedCandidate, type ScopedHandleDeriver, } from "./scoped.js";
|
|
59
61
|
import { type Chain, type Resolved } from "./types.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 —
|
|
@@ -61,6 +62,7 @@ export * from "./pnftTransfer.js";
|
|
|
61
62
|
export * from "./payment.js";
|
|
62
63
|
export * from "./signin.js";
|
|
63
64
|
export * from "./connect.js";
|
|
65
|
+
export * from "./siws.js";
|
|
64
66
|
export * from "./domainProof.js";
|
|
65
67
|
// Named (not `export *`): `NAMESPACE_MAX_LEN` is already exported by
|
|
66
68
|
// adminMarket.js, and two `export *` sharing a name would make it ambiguous
|
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)");
|