@alleyboss/micropay-solana-x402-paywall 1.0.0 → 2.0.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 +100 -167
- package/dist/client/index.cjs +99 -0
- package/dist/client/index.cjs.map +1 -0
- package/dist/client/index.d.cts +112 -0
- package/dist/client/index.d.ts +112 -0
- package/dist/client/index.js +95 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client-CSZHI8o8.d.ts +32 -0
- package/dist/client-vRr48m2x.d.cts +32 -0
- package/dist/index.cjs +803 -41
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -3
- package/dist/index.d.ts +11 -3
- package/dist/index.js +783 -42
- package/dist/index.js.map +1 -1
- package/dist/memory-Daxkczti.d.cts +29 -0
- package/dist/memory-Daxkczti.d.ts +29 -0
- package/dist/middleware/index.cjs +261 -0
- package/dist/middleware/index.cjs.map +1 -0
- package/dist/middleware/index.d.cts +90 -0
- package/dist/middleware/index.d.ts +90 -0
- package/dist/middleware/index.js +255 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/nextjs-BK0pVb9Y.d.ts +78 -0
- package/dist/nextjs-Bm272Jkj.d.cts +78 -0
- package/dist/{client-kfCr7G-P.d.cts → payment-CTxdtqmc.d.cts} +23 -34
- package/dist/{client-kfCr7G-P.d.ts → payment-CTxdtqmc.d.ts} +23 -34
- package/dist/pricing/index.cjs +79 -0
- package/dist/pricing/index.cjs.map +1 -0
- package/dist/pricing/index.d.cts +67 -0
- package/dist/pricing/index.d.ts +67 -0
- package/dist/pricing/index.js +72 -0
- package/dist/pricing/index.js.map +1 -0
- package/dist/session/index.cjs +51 -11
- package/dist/session/index.cjs.map +1 -1
- package/dist/session/index.d.cts +29 -1
- package/dist/session/index.d.ts +29 -1
- package/dist/session/index.js +51 -11
- package/dist/session/index.js.map +1 -1
- package/dist/{index-DptevtnU.d.cts → session-D2IoWAWV.d.cts} +1 -24
- package/dist/{index-DptevtnU.d.ts → session-D2IoWAWV.d.ts} +1 -24
- package/dist/solana/index.cjs +235 -15
- package/dist/solana/index.cjs.map +1 -1
- package/dist/solana/index.d.cts +61 -3
- package/dist/solana/index.d.ts +61 -3
- package/dist/solana/index.js +232 -16
- package/dist/solana/index.js.map +1 -1
- package/dist/store/index.cjs +99 -0
- package/dist/store/index.cjs.map +1 -0
- package/dist/store/index.d.cts +38 -0
- package/dist/store/index.d.ts +38 -0
- package/dist/store/index.js +96 -0
- package/dist/store/index.js.map +1 -0
- package/dist/utils/index.cjs +68 -0
- package/dist/utils/index.cjs.map +1 -0
- package/dist/utils/index.d.cts +30 -0
- package/dist/utils/index.d.ts +30 -0
- package/dist/utils/index.js +65 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/x402/index.cjs +119 -18
- package/dist/x402/index.cjs.map +1 -1
- package/dist/x402/index.d.cts +6 -1
- package/dist/x402/index.d.ts +6 -1
- package/dist/x402/index.js +119 -18
- package/dist/x402/index.js.map +1 -1
- package/package.json +56 -3
package/dist/index.js
CHANGED
|
@@ -2,7 +2,15 @@ import { Connection, PublicKey, LAMPORTS_PER_SOL, clusterApiUrl } from '@solana/
|
|
|
2
2
|
import { SignJWT, jwtVerify } from 'jose';
|
|
3
3
|
import { v4 } from 'uuid';
|
|
4
4
|
|
|
5
|
-
// src/
|
|
5
|
+
// src/types/payment.ts
|
|
6
|
+
var TOKEN_MINTS = {
|
|
7
|
+
/** USDC on mainnet */
|
|
8
|
+
USDC_MAINNET: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
|
|
9
|
+
/** USDC on devnet */
|
|
10
|
+
USDC_DEVNET: "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
|
|
11
|
+
/** USDT on mainnet */
|
|
12
|
+
USDT_MAINNET: "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB"
|
|
13
|
+
};
|
|
6
14
|
var cachedConnection = null;
|
|
7
15
|
var cachedNetwork = null;
|
|
8
16
|
function buildRpcUrl(config) {
|
|
@@ -42,6 +50,16 @@ function isMainnet(network) {
|
|
|
42
50
|
function toX402Network(network) {
|
|
43
51
|
return network === "mainnet-beta" ? "solana-mainnet" : "solana-devnet";
|
|
44
52
|
}
|
|
53
|
+
var SIGNATURE_REGEX = /^[1-9A-HJ-NP-Za-km-z]{87,88}$/;
|
|
54
|
+
var WALLET_REGEX = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
|
|
55
|
+
function isValidSignature(signature) {
|
|
56
|
+
if (!signature || typeof signature !== "string") return false;
|
|
57
|
+
return SIGNATURE_REGEX.test(signature);
|
|
58
|
+
}
|
|
59
|
+
function isValidWalletAddress(address) {
|
|
60
|
+
if (!address || typeof address !== "string") return false;
|
|
61
|
+
return WALLET_REGEX.test(address);
|
|
62
|
+
}
|
|
45
63
|
function parseSOLTransfer(transaction, expectedRecipient) {
|
|
46
64
|
const instructions = transaction.transaction.message.instructions;
|
|
47
65
|
for (const ix of instructions) {
|
|
@@ -82,6 +100,16 @@ async function verifyPayment(params) {
|
|
|
82
100
|
maxAgeSeconds = 300,
|
|
83
101
|
clientConfig
|
|
84
102
|
} = params;
|
|
103
|
+
if (!isValidSignature(signature)) {
|
|
104
|
+
return { valid: false, confirmed: false, signature, error: "Invalid signature format" };
|
|
105
|
+
}
|
|
106
|
+
if (!isValidWalletAddress(expectedRecipient)) {
|
|
107
|
+
return { valid: false, confirmed: false, signature, error: "Invalid recipient address" };
|
|
108
|
+
}
|
|
109
|
+
if (expectedAmount <= 0n) {
|
|
110
|
+
return { valid: false, confirmed: false, signature, error: "Invalid expected amount" };
|
|
111
|
+
}
|
|
112
|
+
const effectiveMaxAge = Math.min(Math.max(maxAgeSeconds, 60), 3600);
|
|
85
113
|
const connection = getConnection(clientConfig);
|
|
86
114
|
try {
|
|
87
115
|
const transaction = await connection.getParsedTransaction(signature, {
|
|
@@ -96,14 +124,17 @@ async function verifyPayment(params) {
|
|
|
96
124
|
valid: false,
|
|
97
125
|
confirmed: true,
|
|
98
126
|
signature,
|
|
99
|
-
error:
|
|
127
|
+
error: "Transaction failed on-chain"
|
|
100
128
|
};
|
|
101
129
|
}
|
|
102
130
|
if (transaction.blockTime) {
|
|
103
131
|
const now = Math.floor(Date.now() / 1e3);
|
|
104
|
-
if (now - transaction.blockTime >
|
|
132
|
+
if (now - transaction.blockTime > effectiveMaxAge) {
|
|
105
133
|
return { valid: false, confirmed: true, signature, error: "Transaction too old" };
|
|
106
134
|
}
|
|
135
|
+
if (transaction.blockTime > now + 60) {
|
|
136
|
+
return { valid: false, confirmed: true, signature, error: "Invalid transaction time" };
|
|
137
|
+
}
|
|
107
138
|
}
|
|
108
139
|
const transferDetails = parseSOLTransfer(transaction, expectedRecipient);
|
|
109
140
|
if (!transferDetails) {
|
|
@@ -122,7 +153,7 @@ async function verifyPayment(params) {
|
|
|
122
153
|
from: transferDetails.from,
|
|
123
154
|
to: transferDetails.to,
|
|
124
155
|
amount: transferDetails.amount,
|
|
125
|
-
error:
|
|
156
|
+
error: "Insufficient payment amount"
|
|
126
157
|
};
|
|
127
158
|
}
|
|
128
159
|
return {
|
|
@@ -140,33 +171,34 @@ async function verifyPayment(params) {
|
|
|
140
171
|
valid: false,
|
|
141
172
|
confirmed: false,
|
|
142
173
|
signature,
|
|
143
|
-
error:
|
|
174
|
+
error: "Verification failed"
|
|
144
175
|
};
|
|
145
176
|
}
|
|
146
177
|
}
|
|
147
178
|
async function waitForConfirmation(signature, clientConfig) {
|
|
179
|
+
if (!isValidSignature(signature)) {
|
|
180
|
+
return { confirmed: false, error: "Invalid signature format" };
|
|
181
|
+
}
|
|
148
182
|
const connection = getConnection(clientConfig);
|
|
149
183
|
try {
|
|
150
184
|
const confirmation = await connection.confirmTransaction(signature, "confirmed");
|
|
151
185
|
if (confirmation.value.err) {
|
|
152
|
-
return {
|
|
153
|
-
confirmed: false,
|
|
154
|
-
error: `Transaction failed: ${JSON.stringify(confirmation.value.err)}`
|
|
155
|
-
};
|
|
186
|
+
return { confirmed: false, error: "Transaction failed" };
|
|
156
187
|
}
|
|
157
188
|
return { confirmed: true, slot: confirmation.context?.slot };
|
|
158
|
-
} catch
|
|
159
|
-
return {
|
|
160
|
-
confirmed: false,
|
|
161
|
-
error: error instanceof Error ? error.message : "Confirmation timeout"
|
|
162
|
-
};
|
|
189
|
+
} catch {
|
|
190
|
+
return { confirmed: false, error: "Confirmation timeout" };
|
|
163
191
|
}
|
|
164
192
|
}
|
|
165
193
|
async function getWalletTransactions(walletAddress, clientConfig, limit = 20) {
|
|
194
|
+
if (!isValidWalletAddress(walletAddress)) {
|
|
195
|
+
return [];
|
|
196
|
+
}
|
|
197
|
+
const safeLimit = Math.min(Math.max(limit, 1), 100);
|
|
166
198
|
const connection = getConnection(clientConfig);
|
|
167
|
-
const pubkey = new PublicKey(walletAddress);
|
|
168
199
|
try {
|
|
169
|
-
const
|
|
200
|
+
const pubkey = new PublicKey(walletAddress);
|
|
201
|
+
const signatures = await connection.getSignaturesForAddress(pubkey, { limit: safeLimit });
|
|
170
202
|
return signatures.map((sig) => ({
|
|
171
203
|
signature: sig.signature,
|
|
172
204
|
blockTime: sig.blockTime ?? void 0,
|
|
@@ -180,15 +212,222 @@ function lamportsToSol(lamports) {
|
|
|
180
212
|
return Number(lamports) / LAMPORTS_PER_SOL;
|
|
181
213
|
}
|
|
182
214
|
function solToLamports(sol) {
|
|
215
|
+
if (!Number.isFinite(sol) || sol < 0) {
|
|
216
|
+
throw new Error("Invalid SOL amount");
|
|
217
|
+
}
|
|
183
218
|
return BigInt(Math.floor(sol * LAMPORTS_PER_SOL));
|
|
184
219
|
}
|
|
220
|
+
|
|
221
|
+
// src/solana/spl.ts
|
|
222
|
+
var SIGNATURE_REGEX2 = /^[1-9A-HJ-NP-Za-km-z]{87,88}$/;
|
|
223
|
+
var WALLET_REGEX2 = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
|
|
224
|
+
function resolveMintAddress(asset, network) {
|
|
225
|
+
if (asset === "native") return null;
|
|
226
|
+
if (asset === "usdc") {
|
|
227
|
+
return network === "mainnet-beta" ? TOKEN_MINTS.USDC_MAINNET : TOKEN_MINTS.USDC_DEVNET;
|
|
228
|
+
}
|
|
229
|
+
if (asset === "usdt") {
|
|
230
|
+
return TOKEN_MINTS.USDT_MAINNET;
|
|
231
|
+
}
|
|
232
|
+
if (typeof asset === "object" && "mint" in asset) {
|
|
233
|
+
return asset.mint;
|
|
234
|
+
}
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
function getTokenDecimals(asset) {
|
|
238
|
+
if (asset === "native") return 9;
|
|
239
|
+
if (asset === "usdc" || asset === "usdt") return 6;
|
|
240
|
+
if (typeof asset === "object" && "decimals" in asset) {
|
|
241
|
+
return asset.decimals ?? 6;
|
|
242
|
+
}
|
|
243
|
+
return 6;
|
|
244
|
+
}
|
|
245
|
+
function parseSPLTransfer(transaction, expectedRecipient, expectedMint) {
|
|
246
|
+
const instructions = transaction.transaction.message.instructions;
|
|
247
|
+
for (const ix of instructions) {
|
|
248
|
+
if ("parsed" in ix && (ix.program === "spl-token" || ix.program === "spl-token-2022")) {
|
|
249
|
+
const parsed = ix.parsed;
|
|
250
|
+
if (parsed.type === "transfer" || parsed.type === "transferChecked") {
|
|
251
|
+
const amount = parsed.info.amount || parsed.info.tokenAmount?.amount;
|
|
252
|
+
if (amount && parsed.info.destination) {
|
|
253
|
+
return {
|
|
254
|
+
from: parsed.info.authority || parsed.info.source || "",
|
|
255
|
+
to: parsed.info.destination,
|
|
256
|
+
amount: BigInt(amount),
|
|
257
|
+
mint: parsed.info.mint || expectedMint
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (transaction.meta?.innerInstructions) {
|
|
264
|
+
for (const inner of transaction.meta.innerInstructions) {
|
|
265
|
+
for (const ix of inner.instructions) {
|
|
266
|
+
if ("parsed" in ix && (ix.program === "spl-token" || ix.program === "spl-token-2022")) {
|
|
267
|
+
const parsed = ix.parsed;
|
|
268
|
+
if (parsed.type === "transfer" || parsed.type === "transferChecked") {
|
|
269
|
+
const amount = parsed.info.amount || parsed.info.tokenAmount?.amount;
|
|
270
|
+
if (amount) {
|
|
271
|
+
return {
|
|
272
|
+
from: parsed.info.authority || parsed.info.source || "",
|
|
273
|
+
to: parsed.info.destination || "",
|
|
274
|
+
amount: BigInt(amount),
|
|
275
|
+
mint: parsed.info.mint || expectedMint
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if (transaction.meta?.postTokenBalances && transaction.meta?.preTokenBalances) {
|
|
284
|
+
const preBalances = transaction.meta.preTokenBalances;
|
|
285
|
+
const postBalances = transaction.meta.postTokenBalances;
|
|
286
|
+
for (const post of postBalances) {
|
|
287
|
+
if (post.mint === expectedMint && post.owner === expectedRecipient) {
|
|
288
|
+
const pre = preBalances.find(
|
|
289
|
+
(p) => p.accountIndex === post.accountIndex
|
|
290
|
+
);
|
|
291
|
+
const preAmount = BigInt(pre?.uiTokenAmount?.amount || "0");
|
|
292
|
+
const postAmount = BigInt(post.uiTokenAmount?.amount || "0");
|
|
293
|
+
const transferred = postAmount - preAmount;
|
|
294
|
+
if (transferred > 0n) {
|
|
295
|
+
return {
|
|
296
|
+
from: "",
|
|
297
|
+
// Can't determine from balance changes
|
|
298
|
+
to: expectedRecipient,
|
|
299
|
+
amount: transferred,
|
|
300
|
+
mint: expectedMint
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
async function verifySPLPayment(params) {
|
|
309
|
+
const {
|
|
310
|
+
signature,
|
|
311
|
+
expectedRecipient,
|
|
312
|
+
expectedAmount,
|
|
313
|
+
asset,
|
|
314
|
+
clientConfig,
|
|
315
|
+
maxAgeSeconds = 300
|
|
316
|
+
} = params;
|
|
317
|
+
if (!SIGNATURE_REGEX2.test(signature)) {
|
|
318
|
+
return { valid: false, confirmed: false, signature, error: "Invalid signature format" };
|
|
319
|
+
}
|
|
320
|
+
if (!WALLET_REGEX2.test(expectedRecipient)) {
|
|
321
|
+
return { valid: false, confirmed: false, signature, error: "Invalid recipient address" };
|
|
322
|
+
}
|
|
323
|
+
const mintAddress = resolveMintAddress(asset, clientConfig.network);
|
|
324
|
+
if (!mintAddress) {
|
|
325
|
+
return { valid: false, confirmed: false, signature, error: "Invalid asset configuration" };
|
|
326
|
+
}
|
|
327
|
+
if (expectedAmount <= 0n) {
|
|
328
|
+
return { valid: false, confirmed: false, signature, error: "Invalid expected amount" };
|
|
329
|
+
}
|
|
330
|
+
const effectiveMaxAge = Math.min(Math.max(maxAgeSeconds, 60), 3600);
|
|
331
|
+
const connection = getConnection(clientConfig);
|
|
332
|
+
try {
|
|
333
|
+
const transaction = await connection.getParsedTransaction(signature, {
|
|
334
|
+
commitment: "confirmed",
|
|
335
|
+
maxSupportedTransactionVersion: 0
|
|
336
|
+
});
|
|
337
|
+
if (!transaction) {
|
|
338
|
+
return { valid: false, confirmed: false, signature, error: "Transaction not found" };
|
|
339
|
+
}
|
|
340
|
+
if (transaction.meta?.err) {
|
|
341
|
+
return { valid: false, confirmed: true, signature, error: "Transaction failed on-chain" };
|
|
342
|
+
}
|
|
343
|
+
if (transaction.blockTime) {
|
|
344
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
345
|
+
if (now - transaction.blockTime > effectiveMaxAge) {
|
|
346
|
+
return { valid: false, confirmed: true, signature, error: "Transaction too old" };
|
|
347
|
+
}
|
|
348
|
+
if (transaction.blockTime > now + 60) {
|
|
349
|
+
return { valid: false, confirmed: true, signature, error: "Invalid transaction time" };
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
const transfer = parseSPLTransfer(transaction, expectedRecipient, mintAddress);
|
|
353
|
+
if (!transfer) {
|
|
354
|
+
return {
|
|
355
|
+
valid: false,
|
|
356
|
+
confirmed: true,
|
|
357
|
+
signature,
|
|
358
|
+
error: "No valid token transfer to recipient found"
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
if (transfer.mint !== mintAddress) {
|
|
362
|
+
return {
|
|
363
|
+
valid: false,
|
|
364
|
+
confirmed: true,
|
|
365
|
+
signature,
|
|
366
|
+
error: "Token mint mismatch"
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
if (transfer.amount < expectedAmount) {
|
|
370
|
+
return {
|
|
371
|
+
valid: false,
|
|
372
|
+
confirmed: true,
|
|
373
|
+
signature,
|
|
374
|
+
from: transfer.from,
|
|
375
|
+
to: transfer.to,
|
|
376
|
+
mint: transfer.mint,
|
|
377
|
+
amount: transfer.amount,
|
|
378
|
+
error: "Insufficient payment amount"
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
return {
|
|
382
|
+
valid: true,
|
|
383
|
+
confirmed: true,
|
|
384
|
+
signature,
|
|
385
|
+
from: transfer.from,
|
|
386
|
+
to: transfer.to,
|
|
387
|
+
mint: transfer.mint,
|
|
388
|
+
amount: transfer.amount,
|
|
389
|
+
blockTime: transaction.blockTime ?? void 0,
|
|
390
|
+
slot: transaction.slot
|
|
391
|
+
};
|
|
392
|
+
} catch {
|
|
393
|
+
return { valid: false, confirmed: false, signature, error: "Verification failed" };
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
function isNativeAsset(asset) {
|
|
397
|
+
return asset === "native";
|
|
398
|
+
}
|
|
399
|
+
var MAX_ARTICLES_PER_SESSION = 100;
|
|
400
|
+
var MIN_SECRET_LENGTH = 32;
|
|
185
401
|
function getSecretKey(secret) {
|
|
186
|
-
if (secret
|
|
187
|
-
throw new Error("Session secret
|
|
402
|
+
if (!secret || typeof secret !== "string") {
|
|
403
|
+
throw new Error("Session secret is required");
|
|
404
|
+
}
|
|
405
|
+
if (secret.length < MIN_SECRET_LENGTH) {
|
|
406
|
+
throw new Error(`Session secret must be at least ${MIN_SECRET_LENGTH} characters`);
|
|
188
407
|
}
|
|
189
408
|
return new TextEncoder().encode(secret);
|
|
190
409
|
}
|
|
410
|
+
function validateWalletAddress(address) {
|
|
411
|
+
if (!address || typeof address !== "string") return false;
|
|
412
|
+
const base58Regex = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
|
|
413
|
+
return base58Regex.test(address);
|
|
414
|
+
}
|
|
415
|
+
function validateArticleId(articleId) {
|
|
416
|
+
if (!articleId || typeof articleId !== "string") return false;
|
|
417
|
+
if (articleId.length > 128) return false;
|
|
418
|
+
const safeIdRegex = /^[a-zA-Z0-9_-]+$/;
|
|
419
|
+
return safeIdRegex.test(articleId);
|
|
420
|
+
}
|
|
191
421
|
async function createSession(walletAddress, articleId, config, siteWide = false) {
|
|
422
|
+
if (!validateWalletAddress(walletAddress)) {
|
|
423
|
+
throw new Error("Invalid wallet address format");
|
|
424
|
+
}
|
|
425
|
+
if (!validateArticleId(articleId)) {
|
|
426
|
+
throw new Error("Invalid article ID format");
|
|
427
|
+
}
|
|
428
|
+
if (!config.durationHours || config.durationHours <= 0 || config.durationHours > 720) {
|
|
429
|
+
throw new Error("Session duration must be between 1 and 720 hours");
|
|
430
|
+
}
|
|
192
431
|
const sessionId = v4();
|
|
193
432
|
const now = Math.floor(Date.now() / 1e3);
|
|
194
433
|
const expiresAt = now + config.durationHours * 3600;
|
|
@@ -196,7 +435,7 @@ async function createSession(walletAddress, articleId, config, siteWide = false)
|
|
|
196
435
|
id: sessionId,
|
|
197
436
|
walletAddress,
|
|
198
437
|
unlockedArticles: [articleId],
|
|
199
|
-
siteWideUnlock: siteWide,
|
|
438
|
+
siteWideUnlock: Boolean(siteWide),
|
|
200
439
|
createdAt: now,
|
|
201
440
|
expiresAt
|
|
202
441
|
};
|
|
@@ -204,7 +443,7 @@ async function createSession(walletAddress, articleId, config, siteWide = false)
|
|
|
204
443
|
sub: walletAddress,
|
|
205
444
|
sid: sessionId,
|
|
206
445
|
articles: session.unlockedArticles,
|
|
207
|
-
siteWide,
|
|
446
|
+
siteWide: session.siteWideUnlock,
|
|
208
447
|
iat: now,
|
|
209
448
|
exp: expiresAt
|
|
210
449
|
};
|
|
@@ -212,30 +451,39 @@ async function createSession(walletAddress, articleId, config, siteWide = false)
|
|
|
212
451
|
return { token, session };
|
|
213
452
|
}
|
|
214
453
|
async function validateSession(token, secret) {
|
|
454
|
+
if (!token || typeof token !== "string") {
|
|
455
|
+
return { valid: false, reason: "Invalid token format" };
|
|
456
|
+
}
|
|
215
457
|
try {
|
|
216
458
|
const { payload } = await jwtVerify(token, getSecretKey(secret));
|
|
217
459
|
const sessionPayload = payload;
|
|
460
|
+
if (!sessionPayload.sub || !sessionPayload.sid || !sessionPayload.exp) {
|
|
461
|
+
return { valid: false, reason: "Malformed session payload" };
|
|
462
|
+
}
|
|
218
463
|
const now = Math.floor(Date.now() / 1e3);
|
|
219
464
|
if (sessionPayload.exp < now) {
|
|
220
465
|
return { valid: false, reason: "Session expired" };
|
|
221
466
|
}
|
|
467
|
+
if (!validateWalletAddress(sessionPayload.sub)) {
|
|
468
|
+
return { valid: false, reason: "Invalid session data" };
|
|
469
|
+
}
|
|
222
470
|
const session = {
|
|
223
471
|
id: sessionPayload.sid,
|
|
224
472
|
walletAddress: sessionPayload.sub,
|
|
225
|
-
unlockedArticles: sessionPayload.articles,
|
|
226
|
-
siteWideUnlock: sessionPayload.siteWide,
|
|
227
|
-
createdAt: sessionPayload.iat,
|
|
473
|
+
unlockedArticles: Array.isArray(sessionPayload.articles) ? sessionPayload.articles : [],
|
|
474
|
+
siteWideUnlock: Boolean(sessionPayload.siteWide),
|
|
475
|
+
createdAt: sessionPayload.iat ?? 0,
|
|
228
476
|
expiresAt: sessionPayload.exp
|
|
229
477
|
};
|
|
230
478
|
return { valid: true, session };
|
|
231
479
|
} catch (error) {
|
|
232
|
-
return {
|
|
233
|
-
valid: false,
|
|
234
|
-
reason: error instanceof Error ? error.message : "Invalid session"
|
|
235
|
-
};
|
|
480
|
+
return { valid: false, reason: "Invalid session" };
|
|
236
481
|
}
|
|
237
482
|
}
|
|
238
483
|
async function addArticleToSession(token, articleId, secret) {
|
|
484
|
+
if (!validateArticleId(articleId)) {
|
|
485
|
+
return null;
|
|
486
|
+
}
|
|
239
487
|
const validation = await validateSession(token, secret);
|
|
240
488
|
if (!validation.valid || !validation.session) {
|
|
241
489
|
return null;
|
|
@@ -244,6 +492,9 @@ async function addArticleToSession(token, articleId, secret) {
|
|
|
244
492
|
if (session.unlockedArticles.includes(articleId)) {
|
|
245
493
|
return { token, session };
|
|
246
494
|
}
|
|
495
|
+
if (session.unlockedArticles.length >= MAX_ARTICLES_PER_SESSION) {
|
|
496
|
+
return null;
|
|
497
|
+
}
|
|
247
498
|
const updatedArticles = [...session.unlockedArticles, articleId];
|
|
248
499
|
const payload = {
|
|
249
500
|
sub: session.walletAddress,
|
|
@@ -260,6 +511,9 @@ async function addArticleToSession(token, articleId, secret) {
|
|
|
260
511
|
};
|
|
261
512
|
}
|
|
262
513
|
async function isArticleUnlocked(token, articleId, secret) {
|
|
514
|
+
if (!validateArticleId(articleId)) {
|
|
515
|
+
return false;
|
|
516
|
+
}
|
|
263
517
|
const validation = await validateSession(token, secret);
|
|
264
518
|
if (!validation.valid || !validation.session) {
|
|
265
519
|
return false;
|
|
@@ -271,20 +525,52 @@ async function isArticleUnlocked(token, articleId, secret) {
|
|
|
271
525
|
}
|
|
272
526
|
|
|
273
527
|
// src/x402/config.ts
|
|
528
|
+
var WALLET_REGEX3 = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
|
|
529
|
+
function sanitizeDisplayString(str, maxLength = 200) {
|
|
530
|
+
if (!str || typeof str !== "string") return "";
|
|
531
|
+
return str.slice(0, maxLength).replace(/[<>"'&]/g, "");
|
|
532
|
+
}
|
|
533
|
+
function isValidUrl(url) {
|
|
534
|
+
try {
|
|
535
|
+
const parsed = new URL(url);
|
|
536
|
+
return parsed.protocol === "http:" || parsed.protocol === "https:";
|
|
537
|
+
} catch {
|
|
538
|
+
return false;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
274
541
|
function buildPaymentRequirement(params) {
|
|
542
|
+
if (!WALLET_REGEX3.test(params.creatorWallet)) {
|
|
543
|
+
throw new Error("Invalid creator wallet address");
|
|
544
|
+
}
|
|
545
|
+
if (params.priceInLamports <= 0n) {
|
|
546
|
+
throw new Error("Price must be positive");
|
|
547
|
+
}
|
|
548
|
+
if (!isValidUrl(params.resourceUrl)) {
|
|
549
|
+
throw new Error("Invalid resource URL");
|
|
550
|
+
}
|
|
551
|
+
if (params.network !== "devnet" && params.network !== "mainnet-beta") {
|
|
552
|
+
throw new Error("Invalid network");
|
|
553
|
+
}
|
|
554
|
+
const timeout = params.maxTimeoutSeconds ?? 300;
|
|
555
|
+
if (timeout < 60 || timeout > 3600) {
|
|
556
|
+
throw new Error("Timeout must be between 60 and 3600 seconds");
|
|
557
|
+
}
|
|
275
558
|
const x402Network = toX402Network(params.network);
|
|
559
|
+
const safeTitle = sanitizeDisplayString(params.articleTitle, 200);
|
|
560
|
+
const safeArticleId = sanitizeDisplayString(params.articleId, 128);
|
|
276
561
|
return {
|
|
277
562
|
scheme: "exact",
|
|
278
563
|
network: x402Network,
|
|
279
564
|
maxAmountRequired: params.priceInLamports.toString(),
|
|
280
565
|
resource: params.resourceUrl,
|
|
281
|
-
description: `Unlock: ${
|
|
566
|
+
description: `Unlock: ${safeTitle}`,
|
|
282
567
|
mimeType: "text/html",
|
|
283
568
|
payTo: params.creatorWallet,
|
|
284
|
-
maxTimeoutSeconds:
|
|
569
|
+
maxTimeoutSeconds: timeout,
|
|
285
570
|
asset: "native",
|
|
286
571
|
extra: {
|
|
287
|
-
name:
|
|
572
|
+
name: safeTitle,
|
|
573
|
+
articleId: safeArticleId
|
|
288
574
|
}
|
|
289
575
|
};
|
|
290
576
|
}
|
|
@@ -292,7 +578,18 @@ function encodePaymentRequired(requirement) {
|
|
|
292
578
|
return Buffer.from(JSON.stringify(requirement)).toString("base64");
|
|
293
579
|
}
|
|
294
580
|
function decodePaymentRequired(encoded) {
|
|
295
|
-
|
|
581
|
+
if (!encoded || typeof encoded !== "string") {
|
|
582
|
+
throw new Error("Invalid encoded requirement");
|
|
583
|
+
}
|
|
584
|
+
if (encoded.length > 1e4) {
|
|
585
|
+
throw new Error("Encoded requirement too large");
|
|
586
|
+
}
|
|
587
|
+
try {
|
|
588
|
+
const decoded = Buffer.from(encoded, "base64").toString("utf-8");
|
|
589
|
+
return JSON.parse(decoded);
|
|
590
|
+
} catch {
|
|
591
|
+
throw new Error("Failed to decode payment requirement");
|
|
592
|
+
}
|
|
296
593
|
}
|
|
297
594
|
var X402_HEADERS = {
|
|
298
595
|
PAYMENT_REQUIRED: "X-Payment-Required",
|
|
@@ -300,12 +597,13 @@ var X402_HEADERS = {
|
|
|
300
597
|
PAYMENT_RESPONSE: "X-Payment-Response"
|
|
301
598
|
};
|
|
302
599
|
function create402ResponseBody(requirement) {
|
|
600
|
+
const assetStr = typeof requirement.asset === "string" ? requirement.asset : requirement.asset.mint;
|
|
303
601
|
return {
|
|
304
602
|
error: "Payment Required",
|
|
305
603
|
message: requirement.description,
|
|
306
604
|
price: {
|
|
307
605
|
amount: requirement.maxAmountRequired,
|
|
308
|
-
asset:
|
|
606
|
+
asset: assetStr,
|
|
309
607
|
network: requirement.network
|
|
310
608
|
}
|
|
311
609
|
};
|
|
@@ -320,23 +618,47 @@ function create402Headers(requirement) {
|
|
|
320
618
|
}
|
|
321
619
|
|
|
322
620
|
// src/x402/verification.ts
|
|
621
|
+
var SIGNATURE_REGEX3 = /^[1-9A-HJ-NP-Za-km-z]{87,88}$/;
|
|
323
622
|
async function verifyX402Payment(payload, requirement, clientConfig) {
|
|
324
|
-
if (!payload
|
|
325
|
-
return {
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
623
|
+
if (!payload || typeof payload !== "object") {
|
|
624
|
+
return { valid: false, invalidReason: "Invalid payload" };
|
|
625
|
+
}
|
|
626
|
+
const signature = payload.payload?.signature;
|
|
627
|
+
if (!signature || typeof signature !== "string") {
|
|
628
|
+
return { valid: false, invalidReason: "Missing transaction signature" };
|
|
629
|
+
}
|
|
630
|
+
if (!SIGNATURE_REGEX3.test(signature)) {
|
|
631
|
+
return { valid: false, invalidReason: "Invalid signature format" };
|
|
632
|
+
}
|
|
633
|
+
if (payload.x402Version !== 1) {
|
|
634
|
+
return { valid: false, invalidReason: "Unsupported x402 version" };
|
|
635
|
+
}
|
|
636
|
+
if (payload.scheme !== "exact") {
|
|
637
|
+
return { valid: false, invalidReason: "Unsupported payment scheme" };
|
|
329
638
|
}
|
|
330
639
|
if (payload.network !== requirement.network) {
|
|
331
640
|
return {
|
|
332
641
|
valid: false,
|
|
333
|
-
invalidReason: `Network mismatch: expected ${requirement.network}
|
|
642
|
+
invalidReason: `Network mismatch: expected ${requirement.network}`
|
|
334
643
|
};
|
|
335
644
|
}
|
|
645
|
+
const walletRegex = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
|
|
646
|
+
if (!walletRegex.test(requirement.payTo)) {
|
|
647
|
+
return { valid: false, invalidReason: "Invalid recipient configuration" };
|
|
648
|
+
}
|
|
649
|
+
let expectedAmount;
|
|
650
|
+
try {
|
|
651
|
+
expectedAmount = BigInt(requirement.maxAmountRequired);
|
|
652
|
+
if (expectedAmount <= 0n) {
|
|
653
|
+
return { valid: false, invalidReason: "Invalid payment amount" };
|
|
654
|
+
}
|
|
655
|
+
} catch {
|
|
656
|
+
return { valid: false, invalidReason: "Invalid payment amount format" };
|
|
657
|
+
}
|
|
336
658
|
const verification = await verifyPayment({
|
|
337
|
-
signature
|
|
659
|
+
signature,
|
|
338
660
|
expectedRecipient: requirement.payTo,
|
|
339
|
-
expectedAmount
|
|
661
|
+
expectedAmount,
|
|
340
662
|
maxAgeSeconds: requirement.maxTimeoutSeconds,
|
|
341
663
|
clientConfig
|
|
342
664
|
});
|
|
@@ -357,9 +679,19 @@ async function verifyX402Payment(payload, requirement, clientConfig) {
|
|
|
357
679
|
};
|
|
358
680
|
}
|
|
359
681
|
function parsePaymentHeader(header) {
|
|
682
|
+
if (!header || typeof header !== "string") {
|
|
683
|
+
return null;
|
|
684
|
+
}
|
|
685
|
+
if (header.length > 1e4) {
|
|
686
|
+
return null;
|
|
687
|
+
}
|
|
360
688
|
try {
|
|
361
689
|
const decoded = Buffer.from(header, "base64").toString("utf-8");
|
|
362
|
-
|
|
690
|
+
const parsed = JSON.parse(decoded);
|
|
691
|
+
if (!parsed || typeof parsed !== "object") {
|
|
692
|
+
return null;
|
|
693
|
+
}
|
|
694
|
+
return parsed;
|
|
363
695
|
} catch {
|
|
364
696
|
return null;
|
|
365
697
|
}
|
|
@@ -368,6 +700,415 @@ function encodePaymentResponse(response) {
|
|
|
368
700
|
return Buffer.from(JSON.stringify(response)).toString("base64");
|
|
369
701
|
}
|
|
370
702
|
|
|
371
|
-
|
|
703
|
+
// src/store/memory.ts
|
|
704
|
+
function createMemoryStore(options = {}) {
|
|
705
|
+
const { cleanupInterval = 6e4 } = options;
|
|
706
|
+
const store = /* @__PURE__ */ new Map();
|
|
707
|
+
const cleanupTimer = setInterval(() => {
|
|
708
|
+
const now = Date.now();
|
|
709
|
+
for (const [key, record] of store.entries()) {
|
|
710
|
+
if (record.expiresAt < now) {
|
|
711
|
+
store.delete(key);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}, cleanupInterval);
|
|
715
|
+
return {
|
|
716
|
+
async hasBeenUsed(signature) {
|
|
717
|
+
const record = store.get(signature);
|
|
718
|
+
if (!record) return false;
|
|
719
|
+
if (record.expiresAt < Date.now()) {
|
|
720
|
+
store.delete(signature);
|
|
721
|
+
return false;
|
|
722
|
+
}
|
|
723
|
+
return true;
|
|
724
|
+
},
|
|
725
|
+
async markAsUsed(signature, resourceId, expiresAt) {
|
|
726
|
+
store.set(signature, {
|
|
727
|
+
resourceId,
|
|
728
|
+
usedAt: Date.now(),
|
|
729
|
+
expiresAt: expiresAt.getTime()
|
|
730
|
+
});
|
|
731
|
+
},
|
|
732
|
+
async getUsage(signature) {
|
|
733
|
+
const record = store.get(signature);
|
|
734
|
+
if (!record) return null;
|
|
735
|
+
if (record.expiresAt < Date.now()) {
|
|
736
|
+
store.delete(signature);
|
|
737
|
+
return null;
|
|
738
|
+
}
|
|
739
|
+
return {
|
|
740
|
+
signature,
|
|
741
|
+
resourceId: record.resourceId,
|
|
742
|
+
usedAt: new Date(record.usedAt),
|
|
743
|
+
expiresAt: new Date(record.expiresAt),
|
|
744
|
+
walletAddress: record.walletAddress
|
|
745
|
+
};
|
|
746
|
+
},
|
|
747
|
+
/** Stop cleanup timer (for graceful shutdown) */
|
|
748
|
+
close() {
|
|
749
|
+
clearInterval(cleanupTimer);
|
|
750
|
+
store.clear();
|
|
751
|
+
}
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// src/store/redis.ts
|
|
756
|
+
function createRedisStore(options) {
|
|
757
|
+
const { client, keyPrefix = "micropay:sig:" } = options;
|
|
758
|
+
const buildKey = (signature) => `${keyPrefix}${signature}`;
|
|
759
|
+
return {
|
|
760
|
+
async hasBeenUsed(signature) {
|
|
761
|
+
const exists = await client.exists(buildKey(signature));
|
|
762
|
+
return exists > 0;
|
|
763
|
+
},
|
|
764
|
+
async markAsUsed(signature, resourceId, expiresAt) {
|
|
765
|
+
const key = buildKey(signature);
|
|
766
|
+
const ttl = Math.max(1, Math.floor((expiresAt.getTime() - Date.now()) / 1e3));
|
|
767
|
+
const record = {
|
|
768
|
+
signature,
|
|
769
|
+
resourceId,
|
|
770
|
+
usedAt: /* @__PURE__ */ new Date(),
|
|
771
|
+
expiresAt
|
|
772
|
+
};
|
|
773
|
+
if (client.setex) {
|
|
774
|
+
await client.setex(key, ttl, JSON.stringify(record));
|
|
775
|
+
} else {
|
|
776
|
+
await client.set(key, JSON.stringify(record), { EX: ttl });
|
|
777
|
+
}
|
|
778
|
+
},
|
|
779
|
+
async getUsage(signature) {
|
|
780
|
+
const data = await client.get(buildKey(signature));
|
|
781
|
+
if (!data) return null;
|
|
782
|
+
try {
|
|
783
|
+
const record = JSON.parse(data);
|
|
784
|
+
return {
|
|
785
|
+
...record,
|
|
786
|
+
usedAt: new Date(record.usedAt),
|
|
787
|
+
expiresAt: new Date(record.expiresAt)
|
|
788
|
+
};
|
|
789
|
+
} catch {
|
|
790
|
+
return null;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// src/middleware/nextjs.ts
|
|
797
|
+
function matchesProtectedPath(path, patterns) {
|
|
798
|
+
for (const pattern of patterns) {
|
|
799
|
+
const regexPattern = pattern.replace(/\*\*/g, "{{DOUBLE_STAR}}").replace(/\*/g, "[^/]*").replace(/{{DOUBLE_STAR}}/g, ".*");
|
|
800
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
801
|
+
if (regex.test(path)) {
|
|
802
|
+
return true;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
return false;
|
|
806
|
+
}
|
|
807
|
+
async function checkPaywallAccess(path, sessionToken, config) {
|
|
808
|
+
if (!matchesProtectedPath(path, config.protectedPaths)) {
|
|
809
|
+
return { allowed: true };
|
|
810
|
+
}
|
|
811
|
+
if (!sessionToken) {
|
|
812
|
+
return {
|
|
813
|
+
allowed: false,
|
|
814
|
+
reason: "No session token",
|
|
815
|
+
requiresPayment: true
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
const validation = await validateSession(sessionToken, config.sessionSecret);
|
|
819
|
+
if (!validation.valid || !validation.session) {
|
|
820
|
+
return {
|
|
821
|
+
allowed: false,
|
|
822
|
+
reason: validation.reason || "Invalid session",
|
|
823
|
+
requiresPayment: true
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
return {
|
|
827
|
+
allowed: true,
|
|
828
|
+
session: validation.session
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
function createPaywallMiddleware(config) {
|
|
832
|
+
const { cookieName = "x402_session" } = config;
|
|
833
|
+
return async function middleware(request) {
|
|
834
|
+
const url = new URL(request.url);
|
|
835
|
+
const path = url.pathname;
|
|
836
|
+
const cookieHeader = request.headers.get("cookie") || "";
|
|
837
|
+
const cookies = Object.fromEntries(
|
|
838
|
+
cookieHeader.split(";").map((c) => {
|
|
839
|
+
const [key, ...vals] = c.trim().split("=");
|
|
840
|
+
return [key, vals.join("=")];
|
|
841
|
+
})
|
|
842
|
+
);
|
|
843
|
+
const sessionToken = cookies[cookieName];
|
|
844
|
+
const result = await checkPaywallAccess(path, sessionToken, config);
|
|
845
|
+
if (!result.allowed && result.requiresPayment) {
|
|
846
|
+
const body = config.custom402Response ? config.custom402Response(path) : {
|
|
847
|
+
error: "Payment Required",
|
|
848
|
+
message: "This resource requires payment to access",
|
|
849
|
+
path
|
|
850
|
+
};
|
|
851
|
+
return new Response(JSON.stringify(body), {
|
|
852
|
+
status: 402,
|
|
853
|
+
headers: {
|
|
854
|
+
"Content-Type": "application/json"
|
|
855
|
+
}
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
return null;
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
function withPaywall(handler, options) {
|
|
862
|
+
const { sessionSecret, cookieName = "x402_session", articleId } = options;
|
|
863
|
+
return async function protectedHandler(request) {
|
|
864
|
+
const cookieHeader = request.headers.get("cookie") || "";
|
|
865
|
+
const cookies = Object.fromEntries(
|
|
866
|
+
cookieHeader.split(";").map((c) => {
|
|
867
|
+
const [key, ...vals] = c.trim().split("=");
|
|
868
|
+
return [key, vals.join("=")];
|
|
869
|
+
})
|
|
870
|
+
);
|
|
871
|
+
const sessionToken = cookies[cookieName];
|
|
872
|
+
if (!sessionToken) {
|
|
873
|
+
return new Response(
|
|
874
|
+
JSON.stringify({ error: "Payment Required", message: "No session token" }),
|
|
875
|
+
{ status: 402, headers: { "Content-Type": "application/json" } }
|
|
876
|
+
);
|
|
877
|
+
}
|
|
878
|
+
const validation = await validateSession(sessionToken, sessionSecret);
|
|
879
|
+
if (!validation.valid || !validation.session) {
|
|
880
|
+
return new Response(
|
|
881
|
+
JSON.stringify({ error: "Payment Required", message: validation.reason }),
|
|
882
|
+
{ status: 402, headers: { "Content-Type": "application/json" } }
|
|
883
|
+
);
|
|
884
|
+
}
|
|
885
|
+
if (articleId) {
|
|
886
|
+
const { session } = validation;
|
|
887
|
+
const hasAccess = session.siteWideUnlock || session.unlockedArticles.includes(articleId);
|
|
888
|
+
if (!hasAccess) {
|
|
889
|
+
return new Response(
|
|
890
|
+
JSON.stringify({ error: "Payment Required", message: "Article not unlocked" }),
|
|
891
|
+
{ status: 402, headers: { "Content-Type": "application/json" } }
|
|
892
|
+
);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
return handler(request, validation.session);
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// src/utils/retry.ts
|
|
900
|
+
function sleep(ms) {
|
|
901
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
902
|
+
}
|
|
903
|
+
function calculateDelay(attempt, options) {
|
|
904
|
+
const { baseDelay, maxDelay, jitter } = options;
|
|
905
|
+
let delay = baseDelay * Math.pow(2, attempt);
|
|
906
|
+
delay = Math.min(delay, maxDelay);
|
|
907
|
+
if (jitter) {
|
|
908
|
+
const jitterAmount = delay * 0.25;
|
|
909
|
+
delay += Math.random() * jitterAmount * 2 - jitterAmount;
|
|
910
|
+
}
|
|
911
|
+
return Math.floor(delay);
|
|
912
|
+
}
|
|
913
|
+
async function withRetry(fn, options = {}) {
|
|
914
|
+
const {
|
|
915
|
+
maxAttempts = 3,
|
|
916
|
+
baseDelay = 500,
|
|
917
|
+
maxDelay = 1e4,
|
|
918
|
+
jitter = true,
|
|
919
|
+
retryOn = () => true
|
|
920
|
+
} = options;
|
|
921
|
+
let lastError;
|
|
922
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
923
|
+
try {
|
|
924
|
+
return await fn();
|
|
925
|
+
} catch (error) {
|
|
926
|
+
lastError = error;
|
|
927
|
+
if (!retryOn(error)) {
|
|
928
|
+
throw error;
|
|
929
|
+
}
|
|
930
|
+
if (attempt < maxAttempts - 1) {
|
|
931
|
+
const delay = calculateDelay(attempt, {
|
|
932
|
+
baseDelay,
|
|
933
|
+
maxDelay,
|
|
934
|
+
jitter
|
|
935
|
+
});
|
|
936
|
+
await sleep(delay);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
throw lastError;
|
|
941
|
+
}
|
|
942
|
+
function isRetryableRPCError(error) {
|
|
943
|
+
if (error instanceof Error) {
|
|
944
|
+
const message = error.message.toLowerCase();
|
|
945
|
+
if (message.includes("429") || message.includes("rate limit")) {
|
|
946
|
+
return true;
|
|
947
|
+
}
|
|
948
|
+
if (message.includes("timeout") || message.includes("econnreset")) {
|
|
949
|
+
return true;
|
|
950
|
+
}
|
|
951
|
+
if (message.includes("503") || message.includes("502") || message.includes("500")) {
|
|
952
|
+
return true;
|
|
953
|
+
}
|
|
954
|
+
if (message.includes("blockhash not found") || message.includes("slot skipped")) {
|
|
955
|
+
return true;
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
return false;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
// src/client/payment.ts
|
|
962
|
+
function buildSolanaPayUrl(params) {
|
|
963
|
+
const { recipient, amount, splToken, reference, label, message } = params;
|
|
964
|
+
const url = new URL(`solana:${recipient}`);
|
|
965
|
+
if (amount !== void 0) {
|
|
966
|
+
url.searchParams.set("amount", amount.toString());
|
|
967
|
+
}
|
|
968
|
+
if (splToken) {
|
|
969
|
+
url.searchParams.set("spl-token", splToken);
|
|
970
|
+
}
|
|
971
|
+
if (reference) {
|
|
972
|
+
url.searchParams.set("reference", reference);
|
|
973
|
+
}
|
|
974
|
+
if (label) {
|
|
975
|
+
url.searchParams.set("label", label);
|
|
976
|
+
}
|
|
977
|
+
if (message) {
|
|
978
|
+
url.searchParams.set("message", message);
|
|
979
|
+
}
|
|
980
|
+
return url.toString();
|
|
981
|
+
}
|
|
982
|
+
function createPaymentFlow(config) {
|
|
983
|
+
const { network, recipientWallet, amount, asset = "native", memo } = config;
|
|
984
|
+
let decimals = 9;
|
|
985
|
+
let mintAddress;
|
|
986
|
+
if (asset === "usdc") {
|
|
987
|
+
decimals = 6;
|
|
988
|
+
mintAddress = network === "mainnet-beta" ? TOKEN_MINTS.USDC_MAINNET : TOKEN_MINTS.USDC_DEVNET;
|
|
989
|
+
} else if (asset === "usdt") {
|
|
990
|
+
decimals = 6;
|
|
991
|
+
mintAddress = TOKEN_MINTS.USDT_MAINNET;
|
|
992
|
+
} else if (typeof asset === "object" && "mint" in asset) {
|
|
993
|
+
decimals = asset.decimals ?? 6;
|
|
994
|
+
mintAddress = asset.mint;
|
|
995
|
+
}
|
|
996
|
+
const naturalAmount = Number(amount) / Math.pow(10, decimals);
|
|
997
|
+
return {
|
|
998
|
+
/** Get the payment configuration */
|
|
999
|
+
getConfig: () => ({ ...config }),
|
|
1000
|
+
/** Get amount in natural display units (e.g., 0.01 SOL) */
|
|
1001
|
+
getDisplayAmount: () => naturalAmount,
|
|
1002
|
+
/** Get amount formatted with symbol */
|
|
1003
|
+
getFormattedAmount: () => {
|
|
1004
|
+
const symbol = asset === "native" ? "SOL" : asset === "usdc" ? "USDC" : asset === "usdt" ? "USDT" : "tokens";
|
|
1005
|
+
return `${naturalAmount.toFixed(decimals > 6 ? 4 : 2)} ${symbol}`;
|
|
1006
|
+
},
|
|
1007
|
+
/** Generate Solana Pay URL for QR codes */
|
|
1008
|
+
getSolanaPayUrl: (options = {}) => {
|
|
1009
|
+
return buildSolanaPayUrl({
|
|
1010
|
+
recipient: recipientWallet,
|
|
1011
|
+
amount: naturalAmount,
|
|
1012
|
+
splToken: mintAddress,
|
|
1013
|
+
label: options.label,
|
|
1014
|
+
reference: options.reference,
|
|
1015
|
+
message: memo
|
|
1016
|
+
});
|
|
1017
|
+
},
|
|
1018
|
+
/** Get the token mint address (undefined for native SOL) */
|
|
1019
|
+
getMintAddress: () => mintAddress,
|
|
1020
|
+
/** Check if this is a native SOL payment */
|
|
1021
|
+
isNativePayment: () => asset === "native",
|
|
1022
|
+
/** Get network information */
|
|
1023
|
+
getNetworkInfo: () => ({
|
|
1024
|
+
network,
|
|
1025
|
+
isMainnet: network === "mainnet-beta",
|
|
1026
|
+
explorerUrl: network === "mainnet-beta" ? "https://explorer.solana.com" : "https://explorer.solana.com?cluster=devnet"
|
|
1027
|
+
}),
|
|
1028
|
+
/** Build explorer URL for a transaction */
|
|
1029
|
+
getExplorerUrl: (signature) => {
|
|
1030
|
+
const baseUrl = "https://explorer.solana.com/tx";
|
|
1031
|
+
const cluster = network === "mainnet-beta" ? "" : "?cluster=devnet";
|
|
1032
|
+
return `${baseUrl}/${signature}${cluster}`;
|
|
1033
|
+
}
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
function createPaymentReference() {
|
|
1037
|
+
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
1038
|
+
return crypto.randomUUID();
|
|
1039
|
+
}
|
|
1040
|
+
return `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
// src/pricing/index.ts
|
|
1044
|
+
var cachedPrice = null;
|
|
1045
|
+
var CACHE_TTL_MS = 6e4;
|
|
1046
|
+
async function getSolPrice() {
|
|
1047
|
+
if (cachedPrice && Date.now() - cachedPrice.fetchedAt.getTime() < CACHE_TTL_MS) {
|
|
1048
|
+
return cachedPrice;
|
|
1049
|
+
}
|
|
1050
|
+
try {
|
|
1051
|
+
const response = await fetch(
|
|
1052
|
+
"https://api.coingecko.com/api/v3/simple/price?ids=solana&vs_currencies=usd",
|
|
1053
|
+
{
|
|
1054
|
+
headers: { "Accept": "application/json" },
|
|
1055
|
+
signal: AbortSignal.timeout(5e3)
|
|
1056
|
+
}
|
|
1057
|
+
);
|
|
1058
|
+
if (!response.ok) {
|
|
1059
|
+
throw new Error(`Price fetch failed: ${response.status}`);
|
|
1060
|
+
}
|
|
1061
|
+
const data = await response.json();
|
|
1062
|
+
if (!data.solana?.usd) {
|
|
1063
|
+
throw new Error("Invalid price response");
|
|
1064
|
+
}
|
|
1065
|
+
cachedPrice = {
|
|
1066
|
+
solPrice: data.solana.usd,
|
|
1067
|
+
fetchedAt: /* @__PURE__ */ new Date(),
|
|
1068
|
+
source: "coingecko"
|
|
1069
|
+
};
|
|
1070
|
+
return cachedPrice;
|
|
1071
|
+
} catch (error) {
|
|
1072
|
+
if (cachedPrice) {
|
|
1073
|
+
return cachedPrice;
|
|
1074
|
+
}
|
|
1075
|
+
return {
|
|
1076
|
+
solPrice: 150,
|
|
1077
|
+
// Fallback price
|
|
1078
|
+
fetchedAt: /* @__PURE__ */ new Date(),
|
|
1079
|
+
source: "fallback"
|
|
1080
|
+
};
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
async function lamportsToUsd(lamports) {
|
|
1084
|
+
const { solPrice } = await getSolPrice();
|
|
1085
|
+
const sol = Number(lamports) / 1e9;
|
|
1086
|
+
return sol * solPrice;
|
|
1087
|
+
}
|
|
1088
|
+
async function usdToLamports(usd) {
|
|
1089
|
+
const { solPrice } = await getSolPrice();
|
|
1090
|
+
const sol = usd / solPrice;
|
|
1091
|
+
return BigInt(Math.floor(sol * 1e9));
|
|
1092
|
+
}
|
|
1093
|
+
async function formatPriceDisplay(lamports) {
|
|
1094
|
+
const { solPrice } = await getSolPrice();
|
|
1095
|
+
const sol = Number(lamports) / 1e9;
|
|
1096
|
+
const usd = sol * solPrice;
|
|
1097
|
+
return `${sol.toFixed(4)} SOL (~$${usd.toFixed(2)})`;
|
|
1098
|
+
}
|
|
1099
|
+
function formatPriceSync(lamports, solPrice) {
|
|
1100
|
+
const sol = Number(lamports) / 1e9;
|
|
1101
|
+
const usd = sol * solPrice;
|
|
1102
|
+
return {
|
|
1103
|
+
sol,
|
|
1104
|
+
usd,
|
|
1105
|
+
formatted: `${sol.toFixed(4)} SOL (~$${usd.toFixed(2)})`
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
function clearPriceCache() {
|
|
1109
|
+
cachedPrice = null;
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
export { TOKEN_MINTS, X402_HEADERS, addArticleToSession, buildPaymentRequirement, buildSolanaPayUrl, checkPaywallAccess, clearPriceCache, create402Headers, create402ResponseBody, createMemoryStore, createPaymentFlow, createPaymentReference, createPaywallMiddleware, createRedisStore, createSession, decodePaymentRequired, encodePaymentRequired, encodePaymentResponse, formatPriceDisplay, formatPriceSync, getConnection, getSolPrice, getTokenDecimals, getWalletTransactions, isArticleUnlocked, isMainnet, isNativeAsset, isRetryableRPCError, lamportsToSol, lamportsToUsd, parsePaymentHeader, resetConnection, resolveMintAddress, solToLamports, toX402Network, usdToLamports, validateSession, verifyPayment, verifySPLPayment, verifyX402Payment, waitForConfirmation, withPaywall, withRetry };
|
|
372
1113
|
//# sourceMappingURL=index.js.map
|
|
373
1114
|
//# sourceMappingURL=index.js.map
|