@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.js CHANGED
@@ -2,11 +2,19 @@ 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
- function buildRpcUrl(config) {
9
- const { network, rpcUrl, tatumApiKey } = config;
16
+ function buildRpcUrl(config2) {
17
+ const { network, rpcUrl, tatumApiKey } = config2;
10
18
  if (rpcUrl) {
11
19
  if (rpcUrl.includes("tatum.io") && tatumApiKey && !rpcUrl.includes(tatumApiKey)) {
12
20
  return rpcUrl.endsWith("/") ? `${rpcUrl}${tatumApiKey}` : `${rpcUrl}/${tatumApiKey}`;
@@ -19,12 +27,12 @@ function buildRpcUrl(config) {
19
27
  }
20
28
  return clusterApiUrl(network);
21
29
  }
22
- function getConnection(config) {
23
- const { network } = config;
30
+ function getConnection(config2) {
31
+ const { network } = config2;
24
32
  if (cachedConnection && cachedNetwork === network) {
25
33
  return cachedConnection;
26
34
  }
27
- const rpcUrl = buildRpcUrl(config);
35
+ const rpcUrl = buildRpcUrl(config2);
28
36
  cachedConnection = new Connection(rpcUrl, {
29
37
  commitment: "confirmed",
30
38
  confirmTransactionInitialTimeout: 6e4
@@ -209,6 +217,185 @@ function solToLamports(sol) {
209
217
  }
210
218
  return BigInt(Math.floor(sol * LAMPORTS_PER_SOL));
211
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
+ }
212
399
  var MAX_ARTICLES_PER_SESSION = 100;
213
400
  var MIN_SECRET_LENGTH = 32;
214
401
  function getSecretKey(secret) {
@@ -231,19 +418,19 @@ function validateArticleId(articleId) {
231
418
  const safeIdRegex = /^[a-zA-Z0-9_-]+$/;
232
419
  return safeIdRegex.test(articleId);
233
420
  }
234
- async function createSession(walletAddress, articleId, config, siteWide = false) {
421
+ async function createSession(walletAddress, articleId, config2, siteWide = false) {
235
422
  if (!validateWalletAddress(walletAddress)) {
236
423
  throw new Error("Invalid wallet address format");
237
424
  }
238
425
  if (!validateArticleId(articleId)) {
239
426
  throw new Error("Invalid article ID format");
240
427
  }
241
- if (!config.durationHours || config.durationHours <= 0 || config.durationHours > 720) {
428
+ if (!config2.durationHours || config2.durationHours <= 0 || config2.durationHours > 720) {
242
429
  throw new Error("Session duration must be between 1 and 720 hours");
243
430
  }
244
431
  const sessionId = v4();
245
432
  const now = Math.floor(Date.now() / 1e3);
246
- const expiresAt = now + config.durationHours * 3600;
433
+ const expiresAt = now + config2.durationHours * 3600;
247
434
  const session = {
248
435
  id: sessionId,
249
436
  walletAddress,
@@ -260,7 +447,7 @@ async function createSession(walletAddress, articleId, config, siteWide = false)
260
447
  iat: now,
261
448
  exp: expiresAt
262
449
  };
263
- const token = await new SignJWT(payload).setProtectedHeader({ alg: "HS256" }).setIssuedAt().setExpirationTime(`${config.durationHours}h`).sign(getSecretKey(config.secret));
450
+ const token = await new SignJWT(payload).setProtectedHeader({ alg: "HS256" }).setIssuedAt().setExpirationTime(`${config2.durationHours}h`).sign(getSecretKey(config2.secret));
264
451
  return { token, session };
265
452
  }
266
453
  async function validateSession(token, secret) {
@@ -338,7 +525,7 @@ async function isArticleUnlocked(token, articleId, secret) {
338
525
  }
339
526
 
340
527
  // src/x402/config.ts
341
- var WALLET_REGEX2 = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
528
+ var WALLET_REGEX3 = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
342
529
  function sanitizeDisplayString(str, maxLength = 200) {
343
530
  if (!str || typeof str !== "string") return "";
344
531
  return str.slice(0, maxLength).replace(/[<>"'&]/g, "");
@@ -352,7 +539,7 @@ function isValidUrl(url) {
352
539
  }
353
540
  }
354
541
  function buildPaymentRequirement(params) {
355
- if (!WALLET_REGEX2.test(params.creatorWallet)) {
542
+ if (!WALLET_REGEX3.test(params.creatorWallet)) {
356
543
  throw new Error("Invalid creator wallet address");
357
544
  }
358
545
  if (params.priceInLamports <= 0n) {
@@ -410,12 +597,13 @@ var X402_HEADERS = {
410
597
  PAYMENT_RESPONSE: "X-Payment-Response"
411
598
  };
412
599
  function create402ResponseBody(requirement) {
600
+ const assetStr = typeof requirement.asset === "string" ? requirement.asset : requirement.asset.mint;
413
601
  return {
414
602
  error: "Payment Required",
415
603
  message: requirement.description,
416
604
  price: {
417
605
  amount: requirement.maxAmountRequired,
418
- asset: requirement.asset,
606
+ asset: assetStr,
419
607
  network: requirement.network
420
608
  }
421
609
  };
@@ -430,7 +618,7 @@ function create402Headers(requirement) {
430
618
  }
431
619
 
432
620
  // src/x402/verification.ts
433
- var SIGNATURE_REGEX2 = /^[1-9A-HJ-NP-Za-km-z]{87,88}$/;
621
+ var SIGNATURE_REGEX3 = /^[1-9A-HJ-NP-Za-km-z]{87,88}$/;
434
622
  async function verifyX402Payment(payload, requirement, clientConfig) {
435
623
  if (!payload || typeof payload !== "object") {
436
624
  return { valid: false, invalidReason: "Invalid payload" };
@@ -439,7 +627,7 @@ async function verifyX402Payment(payload, requirement, clientConfig) {
439
627
  if (!signature || typeof signature !== "string") {
440
628
  return { valid: false, invalidReason: "Missing transaction signature" };
441
629
  }
442
- if (!SIGNATURE_REGEX2.test(signature)) {
630
+ if (!SIGNATURE_REGEX3.test(signature)) {
443
631
  return { valid: false, invalidReason: "Invalid signature format" };
444
632
  }
445
633
  if (payload.x402Version !== 1) {
@@ -512,6 +700,476 @@ function encodePaymentResponse(response) {
512
700
  return Buffer.from(JSON.stringify(response)).toString("base64");
513
701
  }
514
702
 
515
- 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, config2) {
808
+ if (!matchesProtectedPath(path, config2.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, config2.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(config2) {
832
+ const { cookieName = "x402_session" } = config2;
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, config2);
845
+ if (!result.allowed && result.requiresPayment) {
846
+ const body = config2.custom402Response ? config2.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(config2) {
983
+ const { network, recipientWallet, amount, asset = "native", memo } = config2;
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: () => ({ ...config2 }),
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 config = {};
1046
+ var lastProviderIndex = -1;
1047
+ function configurePricing(newConfig) {
1048
+ config = { ...config, ...newConfig };
1049
+ cachedPrice = null;
1050
+ }
1051
+ var PROVIDERS = [
1052
+ {
1053
+ name: "coincap",
1054
+ url: "https://api.coincap.io/v2/assets/solana",
1055
+ parse: (data) => parseFloat(data.data?.priceUsd || "0")
1056
+ },
1057
+ {
1058
+ name: "binance",
1059
+ url: "https://api.binance.com/api/v3/ticker/price?symbol=SOLUSDT",
1060
+ parse: (data) => parseFloat(data.price || "0")
1061
+ },
1062
+ {
1063
+ name: "coingecko",
1064
+ url: "https://api.coingecko.com/api/v3/simple/price?ids=solana&vs_currencies=usd",
1065
+ parse: (data) => data.solana?.usd || 0
1066
+ },
1067
+ {
1068
+ name: "kraken",
1069
+ url: "https://api.kraken.com/0/public/Ticker?pair=SOLUSD",
1070
+ parse: (data) => parseFloat(data.result?.SOLUSD?.c?.[0] || "0")
1071
+ }
1072
+ ];
1073
+ async function fetchFromProvider(provider, timeout) {
1074
+ const controller = new AbortController();
1075
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
1076
+ try {
1077
+ const response = await fetch(provider.url, {
1078
+ headers: { "Accept": "application/json" },
1079
+ signal: controller.signal
1080
+ });
1081
+ if (!response.ok) {
1082
+ throw new Error(`HTTP ${response.status}`);
1083
+ }
1084
+ const data = await response.json();
1085
+ const price = provider.parse(data);
1086
+ if (!price || price <= 0) {
1087
+ throw new Error("Invalid price");
1088
+ }
1089
+ return price;
1090
+ } finally {
1091
+ clearTimeout(timeoutId);
1092
+ }
1093
+ }
1094
+ async function getSolPrice() {
1095
+ const cacheTTL = config.cacheTTL ?? 6e4;
1096
+ const timeout = config.timeout ?? 5e3;
1097
+ if (cachedPrice && Date.now() - cachedPrice.fetchedAt.getTime() < cacheTTL) {
1098
+ return cachedPrice;
1099
+ }
1100
+ if (config.customProvider) {
1101
+ try {
1102
+ const price = await config.customProvider();
1103
+ if (price > 0) {
1104
+ cachedPrice = {
1105
+ solPrice: price,
1106
+ fetchedAt: /* @__PURE__ */ new Date(),
1107
+ source: "custom"
1108
+ };
1109
+ return cachedPrice;
1110
+ }
1111
+ } catch {
1112
+ }
1113
+ }
1114
+ for (let i = 0; i < PROVIDERS.length; i++) {
1115
+ const idx = (lastProviderIndex + 1 + i) % PROVIDERS.length;
1116
+ const provider = PROVIDERS[idx];
1117
+ try {
1118
+ const price = await fetchFromProvider(provider, timeout);
1119
+ lastProviderIndex = idx;
1120
+ cachedPrice = {
1121
+ solPrice: price,
1122
+ fetchedAt: /* @__PURE__ */ new Date(),
1123
+ source: provider.name
1124
+ };
1125
+ return cachedPrice;
1126
+ } catch {
1127
+ continue;
1128
+ }
1129
+ }
1130
+ if (cachedPrice) {
1131
+ return cachedPrice;
1132
+ }
1133
+ return {
1134
+ solPrice: 150,
1135
+ // Reasonable fallback
1136
+ fetchedAt: /* @__PURE__ */ new Date(),
1137
+ source: "fallback"
1138
+ };
1139
+ }
1140
+ async function lamportsToUsd(lamports) {
1141
+ const { solPrice } = await getSolPrice();
1142
+ const sol = Number(lamports) / 1e9;
1143
+ return sol * solPrice;
1144
+ }
1145
+ async function usdToLamports(usd) {
1146
+ const { solPrice } = await getSolPrice();
1147
+ const sol = usd / solPrice;
1148
+ return BigInt(Math.floor(sol * 1e9));
1149
+ }
1150
+ async function formatPriceDisplay(lamports) {
1151
+ const { solPrice } = await getSolPrice();
1152
+ const sol = Number(lamports) / 1e9;
1153
+ const usd = sol * solPrice;
1154
+ return `${sol.toFixed(4)} SOL (~$${usd.toFixed(2)})`;
1155
+ }
1156
+ function formatPriceSync(lamports, solPrice) {
1157
+ const sol = Number(lamports) / 1e9;
1158
+ const usd = sol * solPrice;
1159
+ return {
1160
+ sol,
1161
+ usd,
1162
+ formatted: `${sol.toFixed(4)} SOL (~$${usd.toFixed(2)})`
1163
+ };
1164
+ }
1165
+ function clearPriceCache() {
1166
+ cachedPrice = null;
1167
+ lastProviderIndex = -1;
1168
+ }
1169
+ function getProviders() {
1170
+ return PROVIDERS.map((p) => ({ name: p.name, url: p.url }));
1171
+ }
1172
+
1173
+ export { TOKEN_MINTS, X402_HEADERS, addArticleToSession, buildPaymentRequirement, buildSolanaPayUrl, checkPaywallAccess, clearPriceCache, configurePricing, create402Headers, create402ResponseBody, createMemoryStore, createPaymentFlow, createPaymentReference, createPaywallMiddleware, createRedisStore, createSession, decodePaymentRequired, encodePaymentRequired, encodePaymentResponse, formatPriceDisplay, formatPriceSync, getConnection, getProviders, getSolPrice, getTokenDecimals, getWalletTransactions, isArticleUnlocked, isMainnet, isNativeAsset, isRetryableRPCError, lamportsToSol, lamportsToUsd, parsePaymentHeader, resetConnection, resolveMintAddress, solToLamports, toX402Network, usdToLamports, validateSession, verifyPayment, verifySPLPayment, verifyX402Payment, waitForConfirmation, withPaywall, withRetry };
516
1174
  //# sourceMappingURL=index.js.map
517
1175
  //# sourceMappingURL=index.js.map