@x1id/resolve 0.19.2 → 0.22.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 +83 -1
- package/dist/adminConfig.d.ts +17 -3
- package/dist/adminConfig.js +24 -1
- package/dist/connect.d.ts +20 -8
- package/dist/connect.js +34 -10
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -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/siws.d.ts +310 -0
- package/dist/siws.js +607 -0
- package/dist/voucher.d.ts +27 -9
- package/dist/voucher.js +37 -16
- package/dist/x402.d.ts +70 -6
- package/dist/x402.js +158 -7
- package/package.json +1 -1
package/dist/voucher.js
CHANGED
|
@@ -22,21 +22,29 @@
|
|
|
22
22
|
* escrows the mint price (a snapshot of the live price) into
|
|
23
23
|
* `["voucher", name]` and reserves the voucher slot. `recipient` bound ⇒ the
|
|
24
24
|
* claim registers the handle to that wallet no matter who submits; `null` ⇒
|
|
25
|
-
*
|
|
26
|
-
*
|
|
25
|
+
* a bound recipient (audit S6 — open/recipient-less vouchers were removed as
|
|
26
|
+
* front-runnable; `claimSecretHash` is vestigial and rejected). An optional integrator referrer (the same trailing accounts as
|
|
27
27
|
* `register`, from `integrator.ts`) is snapshotted so the claim can split
|
|
28
|
-
* the escrow. `expiresAt` must be strictly in the future
|
|
28
|
+
* the escrow. `expiresAt` must be strictly in the future and at most
|
|
29
|
+
* `MAX_VOUCHER_DURATION_SECS` (366 days) away (audit R4-A-2).
|
|
29
30
|
* - `claim_voucher(name)` registers the handle, drawing the fee from the
|
|
30
31
|
* escrow (split integrator/treasury per the snapshot), and closes the
|
|
31
32
|
* voucher — rent back to the original payer. The `claimer` pays only the tx
|
|
32
|
-
* fee and the handle's rent, never the mint fee
|
|
33
|
-
*
|
|
33
|
+
* fee and the handle's rent, never the mint fee. This untokenized claim must
|
|
34
|
+
* be signed by the bound recipient (audit R4-A-3); a relayer uses
|
|
35
|
+
* `claim_voucher_tokenized`. Supply the integrator wallet iff the voucher
|
|
36
|
+
* carries one.
|
|
34
37
|
* - `refund_voucher(name)` returns the whole escrow (+ rent) to the payer,
|
|
35
|
-
* payer-signed,
|
|
38
|
+
* payer-signed, at/after `expiresAt`, or earlier once the name has been
|
|
39
|
+
* registered by someone else (pass `handle`, audit R4-A-2).
|
|
36
40
|
*/
|
|
37
41
|
import { encodeBase58, decodeBase58_32 } from "./base58.js";
|
|
38
42
|
/** Seed prefix of the voucher PDA: `["voucher", name]`. */
|
|
39
43
|
export const VOUCHER_SEED = "voucher";
|
|
44
|
+
/** Longest a voucher may stay open: `create_voucher` refuses an `expiresAt`
|
|
45
|
+
* more than this many seconds ahead (`VoucherExpiryTooFar`, 6112; audit
|
|
46
|
+
* R4-A-2). Mirrors lib.rs `MAX_VOUCHER_DURATION_SECS`. */
|
|
47
|
+
export const MAX_VOUCHER_DURATION_SECS = 366 * 24 * 60 * 60;
|
|
40
48
|
/** Anchor instruction discriminator `sha256("global:create_voucher")[0..8]`. */
|
|
41
49
|
export const CREATE_VOUCHER_DISCRIMINATOR = Uint8Array.from([22, 97, 32, 21, 104, 137, 188, 143]);
|
|
42
50
|
/** Anchor instruction discriminator `sha256("global:claim_voucher")[0..8]`. */
|
|
@@ -171,8 +179,19 @@ export function decodePrepaidVoucher(data) {
|
|
|
171
179
|
export function buildCreateVoucherIx(p) {
|
|
172
180
|
const name = encString(p.name);
|
|
173
181
|
const recipient = p.recipient ?? null;
|
|
182
|
+
// Audit S6: the program now REFUSES a recipient-less voucher (the claim tx
|
|
183
|
+
// reveals the secret in cleartext, so an open voucher is front-runnable —
|
|
184
|
+
// error 6106 VoucherMustBeBound). `claimSecretHash` is NO LONGER a second
|
|
185
|
+
// factor: with a recipient mandatory the program stores None and ignores any
|
|
186
|
+
// hash. Throw on both so no integrator believes either still works.
|
|
187
|
+
if (recipient === null) {
|
|
188
|
+
throw new Error("a voucher must bind its recipient (open secret-code vouchers are front-runnable and were disabled on-chain)");
|
|
189
|
+
}
|
|
190
|
+
if (p.claimSecretHash != null) {
|
|
191
|
+
throw new Error("claimSecretHash is removed — vouchers are recipient-bound and carry no claim secret (the program ignores it)");
|
|
192
|
+
}
|
|
174
193
|
const recBytes = recipient === null ? Uint8Array.from([0]) : Uint8Array.from([1, ...toBytes32(recipient, "recipient")]);
|
|
175
|
-
const hashBytes = encOption32(
|
|
194
|
+
const hashBytes = encOption32(null); // S6: always None (a supplied hash throws above)
|
|
176
195
|
const data = new Uint8Array(8 + name.length + 1 + recBytes.length + 8 + hashBytes.length);
|
|
177
196
|
let o = 0;
|
|
178
197
|
data.set(CREATE_VOUCHER_DISCRIMINATOR, o);
|
|
@@ -220,20 +239,22 @@ export function buildClaimVoucherIx(p) {
|
|
|
220
239
|
}
|
|
221
240
|
return { programId: toBase58(p.programId, "programId"), keys, data };
|
|
222
241
|
}
|
|
223
|
-
/** Build `refund_voucher` — payer-signed
|
|
242
|
+
/** Build `refund_voucher` — payer-signed. Valid at/after `expiresAt`, or
|
|
243
|
+
* earlier when `handle` is passed and that name is already registered
|
|
244
|
+
* (audit R4-A-2). */
|
|
224
245
|
export function buildRefundVoucherIx(p) {
|
|
225
246
|
const name = encString(p.name);
|
|
226
247
|
const data = new Uint8Array(8 + name.length);
|
|
227
248
|
data.set(REFUND_VOUCHER_DISCRIMINATOR, 0);
|
|
228
249
|
data.set(name, 8);
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
};
|
|
250
|
+
const keys = [
|
|
251
|
+
{ pubkey: toBase58(p.payer, "payer"), isSigner: true, isWritable: true },
|
|
252
|
+
{ pubkey: toBase58(p.voucher, "voucher"), isSigner: false, isWritable: true },
|
|
253
|
+
];
|
|
254
|
+
if (p.handle !== undefined) {
|
|
255
|
+
keys.push({ pubkey: toBase58(p.handle, "handle"), isSigner: false, isWritable: false });
|
|
256
|
+
}
|
|
257
|
+
return { programId: toBase58(p.programId, "programId"), keys, data };
|
|
237
258
|
}
|
|
238
259
|
// ===================== claim_voucher_tokenized (gift NFTs, #8335) =====================
|
|
239
260
|
/** `sha256("global:claim_voucher_tokenized")[0..8]`. */
|
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
|
+
}
|