@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.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/solana/client.ts
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: `Transaction failed: ${JSON.stringify(transaction.meta.err)}`
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 > maxAgeSeconds) {
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: `Insufficient amount: expected ${expectedAmount}, got ${transferDetails.amount}`
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: error instanceof Error ? error.message : "Unknown verification 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 (error) {
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 signatures = await connection.getSignaturesForAddress(pubkey, { limit });
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.length < 32) {
187
- throw new Error("Session secret must be at least 32 characters");
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: ${params.articleTitle}`,
566
+ description: `Unlock: ${safeTitle}`,
282
567
  mimeType: "text/html",
283
568
  payTo: params.creatorWallet,
284
- maxTimeoutSeconds: params.maxTimeoutSeconds ?? 300,
569
+ maxTimeoutSeconds: timeout,
285
570
  asset: "native",
286
571
  extra: {
287
- name: params.articleTitle
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
- return JSON.parse(Buffer.from(encoded, "base64").toString("utf-8"));
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: requirement.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.payload?.signature) {
325
- return {
326
- valid: false,
327
- invalidReason: "Missing transaction signature"
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}, got ${payload.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: payload.payload.signature,
659
+ signature,
338
660
  expectedRecipient: requirement.payTo,
339
- expectedAmount: BigInt(requirement.maxAmountRequired),
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
- return JSON.parse(decoded);
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
- export { X402_HEADERS, addArticleToSession, buildPaymentRequirement, create402Headers, create402ResponseBody, createSession, decodePaymentRequired, encodePaymentRequired, encodePaymentResponse, getConnection, getWalletTransactions, isArticleUnlocked, isMainnet, lamportsToSol, parsePaymentHeader, resetConnection, solToLamports, toX402Network, validateSession, verifyPayment, verifyX402Payment, waitForConfirmation };
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