@x1id/resolve 0.21.0 → 0.23.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 +3 -1
- package/dist/adminConfig.d.ts +17 -3
- package/dist/adminConfig.js +24 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/mintNft.d.ts +247 -0
- package/dist/mintNft.js +328 -0
- package/dist/namespacePricing.d.ts +207 -0
- package/dist/namespacePricing.js +378 -0
- package/dist/sha256.d.ts +10 -0
- package/dist/sha256.js +77 -0
- package/dist/voucher.d.ts +25 -7
- package/dist/voucher.js +23 -13
- package/dist/x402.d.ts +70 -6
- package/dist/x402.js +158 -7
- package/package.json +1 -1
package/dist/x402.d.ts
CHANGED
|
@@ -21,12 +21,21 @@
|
|
|
21
21
|
* its own SVM chain, but `@x402/core`'s reference client/facilitator only route
|
|
22
22
|
* the `solana:*` scheme family — so to let ANY standard, unmodified x402 client
|
|
23
23
|
* or wallet pay an x1id agent out of the box, X1 testnet uses the CAIP-30 form
|
|
24
|
-
* keyed on X1's genesis.
|
|
25
|
-
*
|
|
24
|
+
* keyed on X1's genesis. x1id's X1 rail is standalone and self-verified: the
|
|
25
|
+
* buyer self-sponsors and broadcasts, and the seller checks the finalized
|
|
26
|
+
* transaction on chain itself; no external facilitator is involved. An
|
|
27
|
+
* earlier release used a principled `"x1:<full-genesis>"`
|
|
26
28
|
* extension; it was OSS-incompatible, so this migrated to CAIP-30 —
|
|
27
29
|
* {@link X1_LEGACY_TESTNET_NETWORK} is kept only for reading older manifests.
|
|
28
30
|
* Verified against `getGenesisHash` (full hash
|
|
29
31
|
* `C7ucgdDEhxLTpXHhWSZxavSVmaNTUJWwT5iTdeaviDho`).
|
|
32
|
+
*
|
|
33
|
+
* # HTTP headers (x402 v2 transport)
|
|
34
|
+
*
|
|
35
|
+
* api.x1id.io sends the 402 body also as `PAYMENT-REQUIRED` (base64 JSON),
|
|
36
|
+
* accepts the payment in `PAYMENT-SIGNATURE` (base64 JSON; the v1 name
|
|
37
|
+
* `X-PAYMENT` still works), and returns `PAYMENT-RESPONSE` plus the v1
|
|
38
|
+
* `X-PAYMENT-RESPONSE` on success.
|
|
30
39
|
*/
|
|
31
40
|
/** X1 testnet's x402 network id — CAIP-30 `"solana:" + first 32 chars of the
|
|
32
41
|
* genesis hash — so unmodified OSS x402 clients route to it (#8325). */
|
|
@@ -72,9 +81,23 @@ export interface X402PaymentRequirements {
|
|
|
72
81
|
* no sponsor (see docs/x402.md): this SDK always sets it to the
|
|
73
82
|
* BUYER's own address (self-sponsored, no facilitator, no custody). */
|
|
74
83
|
readonly feePayer: string;
|
|
75
|
-
/**
|
|
76
|
-
*
|
|
77
|
-
|
|
84
|
+
/** Reference-key mode (api.x1id.io, R3-x402-1): a base58 pubkey derived
|
|
85
|
+
* from the request + challenge nonce + payer (see
|
|
86
|
+
* {@link deriveX402Reference}). The buyer appends it to the
|
|
87
|
+
* TransferChecked instruction as one extra READ-ONLY, NON-SIGNER account.
|
|
88
|
+
* Nobody holds a key for it. */
|
|
89
|
+
readonly reference?: string;
|
|
90
|
+
/** Hex SHA-256 of the canonical request the challenge was issued for
|
|
91
|
+
* ({@link x402RequestHash}). Informational: lets a client confirm it is
|
|
92
|
+
* about to pay for the request it will actually send. */
|
|
93
|
+
readonly requestHash?: string;
|
|
94
|
+
/** R4-D-1 payer-signed redemption (api.x1id.io): the exact UTF-8 message
|
|
95
|
+
* the bound payer signs (ed25519, `signMessage`) to redeem this challenge
|
|
96
|
+
* ({@link x402RedeemMessage}). The base58 signature goes in the payment
|
|
97
|
+
* payload as `payload.payerSig`. The on-chain payment is public once it
|
|
98
|
+
* lands; this signature is what proves the REDEEMER is the payer, so a
|
|
99
|
+
* front-runner who copies the transaction signature cannot redeem it. */
|
|
100
|
+
readonly redeemMessage?: string;
|
|
78
101
|
readonly recentBlockhash?: string;
|
|
79
102
|
readonly lastValidBlockHeight?: string;
|
|
80
103
|
};
|
|
@@ -97,6 +120,10 @@ export interface X402PaymentPayload {
|
|
|
97
120
|
};
|
|
98
121
|
}
|
|
99
122
|
export interface BuildX402RequirementsParams {
|
|
123
|
+
/** Reference-key mode: the derived reference pubkey + request hash
|
|
124
|
+
* ({@link deriveX402Reference}, {@link x402RequestHash}). */
|
|
125
|
+
readonly reference?: string;
|
|
126
|
+
readonly requestHash?: string;
|
|
100
127
|
/** The recipient's base58 address. */
|
|
101
128
|
readonly payTo: string;
|
|
102
129
|
/** The SPL/Token-2022 mint's base58 address. */
|
|
@@ -114,7 +141,6 @@ export interface BuildX402RequirementsParams {
|
|
|
114
141
|
* default. Pass {@link X1_TESTNET_NETWORK} for testnet. */
|
|
115
142
|
readonly network?: string;
|
|
116
143
|
readonly maxTimeoutSeconds?: number;
|
|
117
|
-
readonly memo?: string;
|
|
118
144
|
readonly recentBlockhash?: string;
|
|
119
145
|
readonly lastValidBlockHeight?: string;
|
|
120
146
|
}
|
|
@@ -128,3 +154,41 @@ export declare function buildX402Requirements(p: BuildX402RequirementsParams): X
|
|
|
128
154
|
/** Parse a 402 response body, returning null if it doesn't look like one
|
|
129
155
|
* (missing/wrong `x402Version`, `accepts` not an array). Never throws. */
|
|
130
156
|
export declare function parseX402Response(body: unknown): X402PaymentRequiredResponse | null;
|
|
157
|
+
/** Domain-separation tag for the reference preimage. */
|
|
158
|
+
export declare const X402_REFERENCE_DOMAIN = "x1id-x402-ref:v1|api.x1id.io/dev-api";
|
|
159
|
+
/** SHA-256("") in hex — the body hash of a GET/HEAD or empty body. */
|
|
160
|
+
export declare const X402_EMPTY_BODY_HASH = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
|
|
161
|
+
/** Canonicalise a URL path (see the section docs). */
|
|
162
|
+
export declare function canonicalizeX402Path(path: string): string;
|
|
163
|
+
/** Canonicalise a query string (with or without the leading "?"). */
|
|
164
|
+
export declare function canonicalizeX402Query(query: string): string;
|
|
165
|
+
export interface X402RequestInput {
|
|
166
|
+
/** HTTP method; uppercased. */
|
|
167
|
+
readonly method: string;
|
|
168
|
+
/** The request target: a path with optional `?query` and `#fragment`, or an
|
|
169
|
+
* absolute URL (scheme + host are dropped). */
|
|
170
|
+
readonly url: string;
|
|
171
|
+
/** The exact body bytes (a string is hashed as UTF-8). Ignored for GET/HEAD. */
|
|
172
|
+
readonly body?: Uint8Array | string | null;
|
|
173
|
+
}
|
|
174
|
+
/** The canonical request string (see the section docs). */
|
|
175
|
+
export declare function canonicalX402Request(req: X402RequestInput): string;
|
|
176
|
+
/** Hex SHA-256 of {@link canonicalX402Request} — `accepts[].extra.requestHash`. */
|
|
177
|
+
export declare function x402RequestHash(req: X402RequestInput): string;
|
|
178
|
+
/** Domain-separation tag for the payer-signed redemption message (R4-D-1). */
|
|
179
|
+
export declare const X402_REDEEM_DOMAIN = "x1id-x402-redeem:v1|api.x1id.io/dev-api";
|
|
180
|
+
/**
|
|
181
|
+
* R4-D-1: the message the bound payer signs to redeem a challenge — over its
|
|
182
|
+
* `accepts[].extra.reference`, which is single-use and already bound
|
|
183
|
+
* (server-side) to the endpoint, the exact request and the payer, so the
|
|
184
|
+
* signature cannot be replayed onto another challenge. Sent as base58 in
|
|
185
|
+
* `payload.payerSig`. The 402 also carries it verbatim as
|
|
186
|
+
* `accepts[].extra.redeemMessage`.
|
|
187
|
+
*/
|
|
188
|
+
export declare function x402RedeemMessage(reference: string): string;
|
|
189
|
+
/** Derive the base58 reference pubkey for a request + challenge nonce + payer. */
|
|
190
|
+
export declare function deriveX402Reference(p: {
|
|
191
|
+
readonly canonicalRequest: string;
|
|
192
|
+
readonly nonce: string;
|
|
193
|
+
readonly payer: string;
|
|
194
|
+
}): string;
|
package/dist/x402.js
CHANGED
|
@@ -21,13 +21,24 @@
|
|
|
21
21
|
* its own SVM chain, but `@x402/core`'s reference client/facilitator only route
|
|
22
22
|
* the `solana:*` scheme family — so to let ANY standard, unmodified x402 client
|
|
23
23
|
* or wallet pay an x1id agent out of the box, X1 testnet uses the CAIP-30 form
|
|
24
|
-
* keyed on X1's genesis.
|
|
25
|
-
*
|
|
24
|
+
* keyed on X1's genesis. x1id's X1 rail is standalone and self-verified: the
|
|
25
|
+
* buyer self-sponsors and broadcasts, and the seller checks the finalized
|
|
26
|
+
* transaction on chain itself; no external facilitator is involved. An
|
|
27
|
+
* earlier release used a principled `"x1:<full-genesis>"`
|
|
26
28
|
* extension; it was OSS-incompatible, so this migrated to CAIP-30 —
|
|
27
29
|
* {@link X1_LEGACY_TESTNET_NETWORK} is kept only for reading older manifests.
|
|
28
30
|
* Verified against `getGenesisHash` (full hash
|
|
29
31
|
* `C7ucgdDEhxLTpXHhWSZxavSVmaNTUJWwT5iTdeaviDho`).
|
|
32
|
+
*
|
|
33
|
+
* # HTTP headers (x402 v2 transport)
|
|
34
|
+
*
|
|
35
|
+
* api.x1id.io sends the 402 body also as `PAYMENT-REQUIRED` (base64 JSON),
|
|
36
|
+
* accepts the payment in `PAYMENT-SIGNATURE` (base64 JSON; the v1 name
|
|
37
|
+
* `X-PAYMENT` still works), and returns `PAYMENT-RESPONSE` plus the v1
|
|
38
|
+
* `X-PAYMENT-RESPONSE` on success.
|
|
30
39
|
*/
|
|
40
|
+
import { encodeBase58 } from "./base58.js";
|
|
41
|
+
import { sha256, toHex } from "./sha256.js";
|
|
31
42
|
/** X1 testnet's x402 network id — CAIP-30 `"solana:" + first 32 chars of the
|
|
32
43
|
* genesis hash — so unmodified OSS x402 clients route to it (#8325). */
|
|
33
44
|
export const X1_TESTNET_NETWORK = "solana:C7ucgdDEhxLTpXHhWSZxavSVmaNTUJWw";
|
|
@@ -71,12 +82,17 @@ export function buildX402Requirements(p) {
|
|
|
71
82
|
if (!/^\d+$/.test(p.amount)) {
|
|
72
83
|
throw new Error("amount must be a decimal string of smallest units (e.g. \"1000000\")");
|
|
73
84
|
}
|
|
74
|
-
if (p.memo !== undefined && new TextEncoder().encode(p.memo).length > 256) {
|
|
75
|
-
throw new Error("memo must be <=256 UTF-8 bytes");
|
|
76
|
-
}
|
|
77
85
|
const extra = { feePayer: p.buyer };
|
|
78
|
-
if (p.
|
|
79
|
-
|
|
86
|
+
if (p.reference !== undefined) {
|
|
87
|
+
if (!PUBKEY_RE.test(p.reference))
|
|
88
|
+
throw new Error("reference must be a base58 address");
|
|
89
|
+
extra.reference = p.reference;
|
|
90
|
+
}
|
|
91
|
+
if (p.requestHash !== undefined) {
|
|
92
|
+
if (!/^[0-9a-f]{64}$/.test(p.requestHash))
|
|
93
|
+
throw new Error("requestHash must be 64 lowercase hex chars");
|
|
94
|
+
extra.requestHash = p.requestHash;
|
|
95
|
+
}
|
|
80
96
|
if (p.recentBlockhash !== undefined) {
|
|
81
97
|
extra.recentBlockhash = p.recentBlockhash;
|
|
82
98
|
}
|
|
@@ -103,3 +119,138 @@ export function parseX402Response(body) {
|
|
|
103
119
|
return null;
|
|
104
120
|
return { x402Version: X402_VERSION, accepts: b.accepts };
|
|
105
121
|
}
|
|
122
|
+
// ── Request-bound reference keys (api.x1id.io R3-x402-1) ───────────────────────
|
|
123
|
+
//
|
|
124
|
+
// A 402 challenge from api.x1id.io binds the payment to the EXACT request it
|
|
125
|
+
// was issued for. The server derives a `reference` pubkey from the canonical
|
|
126
|
+
// request, the challenge nonce and the payer; the buyer appends it to the
|
|
127
|
+
// TransferChecked instruction as an extra read-only, non-signer account (SPL
|
|
128
|
+
// Token ignores extra accounts for a single-owner transfer), ~6.6k CU per
|
|
129
|
+
// payment. Nobody signs with the reference; it is not a keypair or a PDA, just
|
|
130
|
+
// 32 hash bytes read as an address.
|
|
131
|
+
//
|
|
132
|
+
// Canonical request (one UTF-8 string, "\n"-separated, no trailing newline):
|
|
133
|
+
//
|
|
134
|
+
// METHOD uppercased ("GET", "POST")
|
|
135
|
+
// path split on "/", each segment percent-decoded then re-encoded
|
|
136
|
+
// per RFC 3986 (unreserved A-Z a-z 0-9 - . _ ~ literal, every
|
|
137
|
+
// other byte %XX uppercase). No trailing-slash folding, case
|
|
138
|
+
// PRESERVED: /v1/resolve/@Alice and /v1/resolve/@alice are
|
|
139
|
+
// DIFFERENT requests (both canonicalise "@" to %40).
|
|
140
|
+
// query split on "&" (empty parts dropped), each "k=v" split on the
|
|
141
|
+
// first "=" ("k" alone == "k="), "+" read as a space, both
|
|
142
|
+
// sides percent-decoded then re-encoded as above, sorted by
|
|
143
|
+
// (key, value) on the encoded strings, duplicates KEPT, joined
|
|
144
|
+
// with "&". "" when there is no query. No fragment.
|
|
145
|
+
// bodyHash lowercase hex SHA-256 of the exact body bytes; GET/HEAD or
|
|
146
|
+
// no body = SHA-256("") = e3b0c442…b855.
|
|
147
|
+
//
|
|
148
|
+
// Scheme and host are excluded (the domain is fixed by the tag below).
|
|
149
|
+
//
|
|
150
|
+
// requestHash = hex(SHA-256(canonical))
|
|
151
|
+
// reference = base58(SHA-256(utf8(X402_REFERENCE_DOMAIN + "\n" + canonical
|
|
152
|
+
// + "\n" + nonce + "\n" + payer)))
|
|
153
|
+
/** Domain-separation tag for the reference preimage. */
|
|
154
|
+
export const X402_REFERENCE_DOMAIN = "x1id-x402-ref:v1|api.x1id.io/dev-api";
|
|
155
|
+
/** SHA-256("") in hex — the body hash of a GET/HEAD or empty body. */
|
|
156
|
+
export const X402_EMPTY_BODY_HASH = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
|
|
157
|
+
function isUnreserved(b) {
|
|
158
|
+
return ((b >= 0x41 && b <= 0x5a) || (b >= 0x61 && b <= 0x7a) || (b >= 0x30 && b <= 0x39) ||
|
|
159
|
+
b === 0x2d || b === 0x2e || b === 0x5f || b === 0x7e);
|
|
160
|
+
}
|
|
161
|
+
function hexVal(c) {
|
|
162
|
+
if (c >= 0x30 && c <= 0x39)
|
|
163
|
+
return c - 0x30;
|
|
164
|
+
if (c >= 0x41 && c <= 0x46)
|
|
165
|
+
return c - 0x37;
|
|
166
|
+
if (c >= 0x61 && c <= 0x66)
|
|
167
|
+
return c - 0x57;
|
|
168
|
+
return -1;
|
|
169
|
+
}
|
|
170
|
+
/** Percent-decode to raw bytes. A `%` not followed by two hex digits is kept
|
|
171
|
+
* literally (decoding never throws). */
|
|
172
|
+
function pctDecode(s, plusIsSpace) {
|
|
173
|
+
const raw = new TextEncoder().encode(s);
|
|
174
|
+
const out = [];
|
|
175
|
+
for (let i = 0; i < raw.length; i++) {
|
|
176
|
+
const c = raw[i];
|
|
177
|
+
if (c === 0x25 && i + 2 < raw.length) {
|
|
178
|
+
const hi = hexVal(raw[i + 1]);
|
|
179
|
+
const lo = hexVal(raw[i + 2]);
|
|
180
|
+
if (hi >= 0 && lo >= 0) {
|
|
181
|
+
out.push(hi * 16 + lo);
|
|
182
|
+
i += 2;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
out.push(plusIsSpace && c === 0x2b ? 0x20 : c);
|
|
187
|
+
}
|
|
188
|
+
return new Uint8Array(out);
|
|
189
|
+
}
|
|
190
|
+
function pctEncode(bytes) {
|
|
191
|
+
let s = "";
|
|
192
|
+
for (const b of bytes)
|
|
193
|
+
s += isUnreserved(b) ? String.fromCharCode(b) : "%" + b.toString(16).toUpperCase().padStart(2, "0");
|
|
194
|
+
return s;
|
|
195
|
+
}
|
|
196
|
+
/** Canonicalise a URL path (see the section docs). */
|
|
197
|
+
export function canonicalizeX402Path(path) {
|
|
198
|
+
const p = path === "" ? "/" : path;
|
|
199
|
+
return p
|
|
200
|
+
.split("/")
|
|
201
|
+
.map((seg) => pctEncode(pctDecode(seg, false)))
|
|
202
|
+
.join("/");
|
|
203
|
+
}
|
|
204
|
+
/** Canonicalise a query string (with or without the leading "?"). */
|
|
205
|
+
export function canonicalizeX402Query(query) {
|
|
206
|
+
const q = query.startsWith("?") ? query.slice(1) : query;
|
|
207
|
+
const pairs = [];
|
|
208
|
+
for (const part of q.split("&")) {
|
|
209
|
+
if (part === "")
|
|
210
|
+
continue;
|
|
211
|
+
const eq = part.indexOf("=");
|
|
212
|
+
const k = eq === -1 ? part : part.slice(0, eq);
|
|
213
|
+
const v = eq === -1 ? "" : part.slice(eq + 1);
|
|
214
|
+
pairs.push([pctEncode(pctDecode(k, true)), pctEncode(pctDecode(v, true))]);
|
|
215
|
+
}
|
|
216
|
+
pairs.sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : a[1] < b[1] ? -1 : a[1] > b[1] ? 1 : 0));
|
|
217
|
+
return pairs.map(([k, v]) => `${k}=${v}`).join("&");
|
|
218
|
+
}
|
|
219
|
+
/** The canonical request string (see the section docs). */
|
|
220
|
+
export function canonicalX402Request(req) {
|
|
221
|
+
const method = req.method.toUpperCase();
|
|
222
|
+
let target = req.url;
|
|
223
|
+
const abs = /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\/[^/?#]*/.exec(target);
|
|
224
|
+
if (abs)
|
|
225
|
+
target = target.slice(abs[0].length);
|
|
226
|
+
const hashAt = target.indexOf("#");
|
|
227
|
+
if (hashAt !== -1)
|
|
228
|
+
target = target.slice(0, hashAt);
|
|
229
|
+
const qAt = target.indexOf("?");
|
|
230
|
+
const path = qAt === -1 ? target : target.slice(0, qAt);
|
|
231
|
+
const query = qAt === -1 ? "" : target.slice(qAt + 1);
|
|
232
|
+
const noBody = method === "GET" || method === "HEAD" || req.body === undefined || req.body === null;
|
|
233
|
+
const bodyHash = noBody ? X402_EMPTY_BODY_HASH : toHex(sha256(req.body));
|
|
234
|
+
return `${method}\n${canonicalizeX402Path(path)}\n${canonicalizeX402Query(query)}\n${bodyHash}`;
|
|
235
|
+
}
|
|
236
|
+
/** Hex SHA-256 of {@link canonicalX402Request} — `accepts[].extra.requestHash`. */
|
|
237
|
+
export function x402RequestHash(req) {
|
|
238
|
+
return toHex(sha256(canonicalX402Request(req)));
|
|
239
|
+
}
|
|
240
|
+
/** Domain-separation tag for the payer-signed redemption message (R4-D-1). */
|
|
241
|
+
export const X402_REDEEM_DOMAIN = "x1id-x402-redeem:v1|api.x1id.io/dev-api";
|
|
242
|
+
/**
|
|
243
|
+
* R4-D-1: the message the bound payer signs to redeem a challenge — over its
|
|
244
|
+
* `accepts[].extra.reference`, which is single-use and already bound
|
|
245
|
+
* (server-side) to the endpoint, the exact request and the payer, so the
|
|
246
|
+
* signature cannot be replayed onto another challenge. Sent as base58 in
|
|
247
|
+
* `payload.payerSig`. The 402 also carries it verbatim as
|
|
248
|
+
* `accepts[].extra.redeemMessage`.
|
|
249
|
+
*/
|
|
250
|
+
export function x402RedeemMessage(reference) {
|
|
251
|
+
return `${X402_REDEEM_DOMAIN}\nreference:${reference}`;
|
|
252
|
+
}
|
|
253
|
+
/** Derive the base58 reference pubkey for a request + challenge nonce + payer. */
|
|
254
|
+
export function deriveX402Reference(p) {
|
|
255
|
+
return encodeBase58(sha256(`${X402_REFERENCE_DOMAIN}\n${p.canonicalRequest}\n${p.nonce}\n${p.payer}`));
|
|
256
|
+
}
|