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