@alleyboss/micropay-solana-x402-paywall 1.0.1 → 2.0.1

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 (62) 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 +696 -15
  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 +674 -16
  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 +142 -0
  29. package/dist/pricing/index.cjs.map +1 -0
  30. package/dist/pricing/index.d.cts +111 -0
  31. package/dist/pricing/index.d.ts +111 -0
  32. package/dist/pricing/index.js +133 -0
  33. package/dist/pricing/index.js.map +1 -0
  34. package/dist/session/index.d.cts +29 -1
  35. package/dist/session/index.d.ts +29 -1
  36. package/dist/{index-uxMb72hH.d.cts → session-D2IoWAWV.d.cts} +1 -27
  37. package/dist/{index-uxMb72hH.d.ts → session-D2IoWAWV.d.ts} +1 -27
  38. package/dist/solana/index.cjs +193 -0
  39. package/dist/solana/index.cjs.map +1 -1
  40. package/dist/solana/index.d.cts +60 -3
  41. package/dist/solana/index.d.ts +60 -3
  42. package/dist/solana/index.js +190 -1
  43. package/dist/solana/index.js.map +1 -1
  44. package/dist/store/index.cjs +99 -0
  45. package/dist/store/index.cjs.map +1 -0
  46. package/dist/store/index.d.cts +38 -0
  47. package/dist/store/index.d.ts +38 -0
  48. package/dist/store/index.js +96 -0
  49. package/dist/store/index.js.map +1 -0
  50. package/dist/utils/index.cjs +68 -0
  51. package/dist/utils/index.cjs.map +1 -0
  52. package/dist/utils/index.d.cts +30 -0
  53. package/dist/utils/index.d.ts +30 -0
  54. package/dist/utils/index.js +65 -0
  55. package/dist/utils/index.js.map +1 -0
  56. package/dist/x402/index.cjs +2 -1
  57. package/dist/x402/index.cjs.map +1 -1
  58. package/dist/x402/index.d.cts +2 -1
  59. package/dist/x402/index.d.ts +2 -1
  60. package/dist/x402/index.js +2 -1
  61. package/dist/x402/index.js.map +1 -1
  62. package/package.json +56 -3
package/dist/index.cjs CHANGED
@@ -4,11 +4,19 @@ 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
- function buildRpcUrl(config) {
11
- const { network, rpcUrl, tatumApiKey } = config;
18
+ function buildRpcUrl(config2) {
19
+ const { network, rpcUrl, tatumApiKey } = config2;
12
20
  if (rpcUrl) {
13
21
  if (rpcUrl.includes("tatum.io") && tatumApiKey && !rpcUrl.includes(tatumApiKey)) {
14
22
  return rpcUrl.endsWith("/") ? `${rpcUrl}${tatumApiKey}` : `${rpcUrl}/${tatumApiKey}`;
@@ -21,12 +29,12 @@ function buildRpcUrl(config) {
21
29
  }
22
30
  return web3_js.clusterApiUrl(network);
23
31
  }
24
- function getConnection(config) {
25
- const { network } = config;
32
+ function getConnection(config2) {
33
+ const { network } = config2;
26
34
  if (cachedConnection && cachedNetwork === network) {
27
35
  return cachedConnection;
28
36
  }
29
- const rpcUrl = buildRpcUrl(config);
37
+ const rpcUrl = buildRpcUrl(config2);
30
38
  cachedConnection = new web3_js.Connection(rpcUrl, {
31
39
  commitment: "confirmed",
32
40
  confirmTransactionInitialTimeout: 6e4
@@ -211,6 +219,185 @@ function solToLamports(sol) {
211
219
  }
212
220
  return BigInt(Math.floor(sol * web3_js.LAMPORTS_PER_SOL));
213
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
+ }
214
401
  var MAX_ARTICLES_PER_SESSION = 100;
215
402
  var MIN_SECRET_LENGTH = 32;
216
403
  function getSecretKey(secret) {
@@ -233,19 +420,19 @@ function validateArticleId(articleId) {
233
420
  const safeIdRegex = /^[a-zA-Z0-9_-]+$/;
234
421
  return safeIdRegex.test(articleId);
235
422
  }
236
- async function createSession(walletAddress, articleId, config, siteWide = false) {
423
+ async function createSession(walletAddress, articleId, config2, siteWide = false) {
237
424
  if (!validateWalletAddress(walletAddress)) {
238
425
  throw new Error("Invalid wallet address format");
239
426
  }
240
427
  if (!validateArticleId(articleId)) {
241
428
  throw new Error("Invalid article ID format");
242
429
  }
243
- if (!config.durationHours || config.durationHours <= 0 || config.durationHours > 720) {
430
+ if (!config2.durationHours || config2.durationHours <= 0 || config2.durationHours > 720) {
244
431
  throw new Error("Session duration must be between 1 and 720 hours");
245
432
  }
246
433
  const sessionId = uuid.v4();
247
434
  const now = Math.floor(Date.now() / 1e3);
248
- const expiresAt = now + config.durationHours * 3600;
435
+ const expiresAt = now + config2.durationHours * 3600;
249
436
  const session = {
250
437
  id: sessionId,
251
438
  walletAddress,
@@ -262,7 +449,7 @@ async function createSession(walletAddress, articleId, config, siteWide = false)
262
449
  iat: now,
263
450
  exp: expiresAt
264
451
  };
265
- const token = await new jose.SignJWT(payload).setProtectedHeader({ alg: "HS256" }).setIssuedAt().setExpirationTime(`${config.durationHours}h`).sign(getSecretKey(config.secret));
452
+ const token = await new jose.SignJWT(payload).setProtectedHeader({ alg: "HS256" }).setIssuedAt().setExpirationTime(`${config2.durationHours}h`).sign(getSecretKey(config2.secret));
266
453
  return { token, session };
267
454
  }
268
455
  async function validateSession(token, secret) {
@@ -340,7 +527,7 @@ async function isArticleUnlocked(token, articleId, secret) {
340
527
  }
341
528
 
342
529
  // src/x402/config.ts
343
- var WALLET_REGEX2 = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
530
+ var WALLET_REGEX3 = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
344
531
  function sanitizeDisplayString(str, maxLength = 200) {
345
532
  if (!str || typeof str !== "string") return "";
346
533
  return str.slice(0, maxLength).replace(/[<>"'&]/g, "");
@@ -354,7 +541,7 @@ function isValidUrl(url) {
354
541
  }
355
542
  }
356
543
  function buildPaymentRequirement(params) {
357
- if (!WALLET_REGEX2.test(params.creatorWallet)) {
544
+ if (!WALLET_REGEX3.test(params.creatorWallet)) {
358
545
  throw new Error("Invalid creator wallet address");
359
546
  }
360
547
  if (params.priceInLamports <= 0n) {
@@ -412,12 +599,13 @@ var X402_HEADERS = {
412
599
  PAYMENT_RESPONSE: "X-Payment-Response"
413
600
  };
414
601
  function create402ResponseBody(requirement) {
602
+ const assetStr = typeof requirement.asset === "string" ? requirement.asset : requirement.asset.mint;
415
603
  return {
416
604
  error: "Payment Required",
417
605
  message: requirement.description,
418
606
  price: {
419
607
  amount: requirement.maxAmountRequired,
420
- asset: requirement.asset,
608
+ asset: assetStr,
421
609
  network: requirement.network
422
610
  }
423
611
  };
@@ -432,7 +620,7 @@ function create402Headers(requirement) {
432
620
  }
433
621
 
434
622
  // src/x402/verification.ts
435
- var SIGNATURE_REGEX2 = /^[1-9A-HJ-NP-Za-km-z]{87,88}$/;
623
+ var SIGNATURE_REGEX3 = /^[1-9A-HJ-NP-Za-km-z]{87,88}$/;
436
624
  async function verifyX402Payment(payload, requirement, clientConfig) {
437
625
  if (!payload || typeof payload !== "object") {
438
626
  return { valid: false, invalidReason: "Invalid payload" };
@@ -441,7 +629,7 @@ async function verifyX402Payment(payload, requirement, clientConfig) {
441
629
  if (!signature || typeof signature !== "string") {
442
630
  return { valid: false, invalidReason: "Missing transaction signature" };
443
631
  }
444
- if (!SIGNATURE_REGEX2.test(signature)) {
632
+ if (!SIGNATURE_REGEX3.test(signature)) {
445
633
  return { valid: false, invalidReason: "Invalid signature format" };
446
634
  }
447
635
  if (payload.x402Version !== 1) {
@@ -514,27 +702,520 @@ function encodePaymentResponse(response) {
514
702
  return Buffer.from(JSON.stringify(response)).toString("base64");
515
703
  }
516
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, config2) {
810
+ if (!matchesProtectedPath(path, config2.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, config2.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(config2) {
834
+ const { cookieName = "x402_session" } = config2;
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, config2);
847
+ if (!result.allowed && result.requiresPayment) {
848
+ const body = config2.custom402Response ? config2.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(config2) {
985
+ const { network, recipientWallet, amount, asset = "native", memo } = config2;
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: () => ({ ...config2 }),
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 config = {};
1048
+ var lastProviderIndex = -1;
1049
+ function configurePricing(newConfig) {
1050
+ config = { ...config, ...newConfig };
1051
+ cachedPrice = null;
1052
+ }
1053
+ var PROVIDERS = [
1054
+ {
1055
+ name: "coincap",
1056
+ url: "https://api.coincap.io/v2/assets/solana",
1057
+ parse: (data) => parseFloat(data.data?.priceUsd || "0")
1058
+ },
1059
+ {
1060
+ name: "binance",
1061
+ url: "https://api.binance.com/api/v3/ticker/price?symbol=SOLUSDT",
1062
+ parse: (data) => parseFloat(data.price || "0")
1063
+ },
1064
+ {
1065
+ name: "coingecko",
1066
+ url: "https://api.coingecko.com/api/v3/simple/price?ids=solana&vs_currencies=usd",
1067
+ parse: (data) => data.solana?.usd || 0
1068
+ },
1069
+ {
1070
+ name: "kraken",
1071
+ url: "https://api.kraken.com/0/public/Ticker?pair=SOLUSD",
1072
+ parse: (data) => parseFloat(data.result?.SOLUSD?.c?.[0] || "0")
1073
+ }
1074
+ ];
1075
+ async function fetchFromProvider(provider, timeout) {
1076
+ const controller = new AbortController();
1077
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
1078
+ try {
1079
+ const response = await fetch(provider.url, {
1080
+ headers: { "Accept": "application/json" },
1081
+ signal: controller.signal
1082
+ });
1083
+ if (!response.ok) {
1084
+ throw new Error(`HTTP ${response.status}`);
1085
+ }
1086
+ const data = await response.json();
1087
+ const price = provider.parse(data);
1088
+ if (!price || price <= 0) {
1089
+ throw new Error("Invalid price");
1090
+ }
1091
+ return price;
1092
+ } finally {
1093
+ clearTimeout(timeoutId);
1094
+ }
1095
+ }
1096
+ async function getSolPrice() {
1097
+ const cacheTTL = config.cacheTTL ?? 6e4;
1098
+ const timeout = config.timeout ?? 5e3;
1099
+ if (cachedPrice && Date.now() - cachedPrice.fetchedAt.getTime() < cacheTTL) {
1100
+ return cachedPrice;
1101
+ }
1102
+ if (config.customProvider) {
1103
+ try {
1104
+ const price = await config.customProvider();
1105
+ if (price > 0) {
1106
+ cachedPrice = {
1107
+ solPrice: price,
1108
+ fetchedAt: /* @__PURE__ */ new Date(),
1109
+ source: "custom"
1110
+ };
1111
+ return cachedPrice;
1112
+ }
1113
+ } catch {
1114
+ }
1115
+ }
1116
+ for (let i = 0; i < PROVIDERS.length; i++) {
1117
+ const idx = (lastProviderIndex + 1 + i) % PROVIDERS.length;
1118
+ const provider = PROVIDERS[idx];
1119
+ try {
1120
+ const price = await fetchFromProvider(provider, timeout);
1121
+ lastProviderIndex = idx;
1122
+ cachedPrice = {
1123
+ solPrice: price,
1124
+ fetchedAt: /* @__PURE__ */ new Date(),
1125
+ source: provider.name
1126
+ };
1127
+ return cachedPrice;
1128
+ } catch {
1129
+ continue;
1130
+ }
1131
+ }
1132
+ if (cachedPrice) {
1133
+ return cachedPrice;
1134
+ }
1135
+ return {
1136
+ solPrice: 150,
1137
+ // Reasonable fallback
1138
+ fetchedAt: /* @__PURE__ */ new Date(),
1139
+ source: "fallback"
1140
+ };
1141
+ }
1142
+ async function lamportsToUsd(lamports) {
1143
+ const { solPrice } = await getSolPrice();
1144
+ const sol = Number(lamports) / 1e9;
1145
+ return sol * solPrice;
1146
+ }
1147
+ async function usdToLamports(usd) {
1148
+ const { solPrice } = await getSolPrice();
1149
+ const sol = usd / solPrice;
1150
+ return BigInt(Math.floor(sol * 1e9));
1151
+ }
1152
+ async function formatPriceDisplay(lamports) {
1153
+ const { solPrice } = await getSolPrice();
1154
+ const sol = Number(lamports) / 1e9;
1155
+ const usd = sol * solPrice;
1156
+ return `${sol.toFixed(4)} SOL (~$${usd.toFixed(2)})`;
1157
+ }
1158
+ function formatPriceSync(lamports, solPrice) {
1159
+ const sol = Number(lamports) / 1e9;
1160
+ const usd = sol * solPrice;
1161
+ return {
1162
+ sol,
1163
+ usd,
1164
+ formatted: `${sol.toFixed(4)} SOL (~$${usd.toFixed(2)})`
1165
+ };
1166
+ }
1167
+ function clearPriceCache() {
1168
+ cachedPrice = null;
1169
+ lastProviderIndex = -1;
1170
+ }
1171
+ function getProviders() {
1172
+ return PROVIDERS.map((p) => ({ name: p.name, url: p.url }));
1173
+ }
1174
+
1175
+ exports.TOKEN_MINTS = TOKEN_MINTS;
517
1176
  exports.X402_HEADERS = X402_HEADERS;
518
1177
  exports.addArticleToSession = addArticleToSession;
519
1178
  exports.buildPaymentRequirement = buildPaymentRequirement;
1179
+ exports.buildSolanaPayUrl = buildSolanaPayUrl;
1180
+ exports.checkPaywallAccess = checkPaywallAccess;
1181
+ exports.clearPriceCache = clearPriceCache;
1182
+ exports.configurePricing = configurePricing;
520
1183
  exports.create402Headers = create402Headers;
521
1184
  exports.create402ResponseBody = create402ResponseBody;
1185
+ exports.createMemoryStore = createMemoryStore;
1186
+ exports.createPaymentFlow = createPaymentFlow;
1187
+ exports.createPaymentReference = createPaymentReference;
1188
+ exports.createPaywallMiddleware = createPaywallMiddleware;
1189
+ exports.createRedisStore = createRedisStore;
522
1190
  exports.createSession = createSession;
523
1191
  exports.decodePaymentRequired = decodePaymentRequired;
524
1192
  exports.encodePaymentRequired = encodePaymentRequired;
525
1193
  exports.encodePaymentResponse = encodePaymentResponse;
1194
+ exports.formatPriceDisplay = formatPriceDisplay;
1195
+ exports.formatPriceSync = formatPriceSync;
526
1196
  exports.getConnection = getConnection;
1197
+ exports.getProviders = getProviders;
1198
+ exports.getSolPrice = getSolPrice;
1199
+ exports.getTokenDecimals = getTokenDecimals;
527
1200
  exports.getWalletTransactions = getWalletTransactions;
528
1201
  exports.isArticleUnlocked = isArticleUnlocked;
529
1202
  exports.isMainnet = isMainnet;
1203
+ exports.isNativeAsset = isNativeAsset;
1204
+ exports.isRetryableRPCError = isRetryableRPCError;
530
1205
  exports.lamportsToSol = lamportsToSol;
1206
+ exports.lamportsToUsd = lamportsToUsd;
531
1207
  exports.parsePaymentHeader = parsePaymentHeader;
532
1208
  exports.resetConnection = resetConnection;
1209
+ exports.resolveMintAddress = resolveMintAddress;
533
1210
  exports.solToLamports = solToLamports;
534
1211
  exports.toX402Network = toX402Network;
1212
+ exports.usdToLamports = usdToLamports;
535
1213
  exports.validateSession = validateSession;
536
1214
  exports.verifyPayment = verifyPayment;
1215
+ exports.verifySPLPayment = verifySPLPayment;
537
1216
  exports.verifyX402Payment = verifyX402Payment;
538
1217
  exports.waitForConfirmation = waitForConfirmation;
1218
+ exports.withPaywall = withPaywall;
1219
+ exports.withRetry = withRetry;
539
1220
  //# sourceMappingURL=index.cjs.map
540
1221
  //# sourceMappingURL=index.cjs.map