@alleyboss/micropay-solana-x402-paywall 2.2.0 → 2.3.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.
package/dist/index.cjs CHANGED
@@ -148,7 +148,8 @@ async function verifyPayment(params) {
148
148
  expectedRecipient,
149
149
  expectedAmount,
150
150
  maxAgeSeconds = 300,
151
- clientConfig
151
+ clientConfig,
152
+ signatureStore
152
153
  } = params;
153
154
  if (!isValidSignature(signature)) {
154
155
  return { valid: false, confirmed: false, signature, error: "Invalid signature format" };
@@ -159,6 +160,12 @@ async function verifyPayment(params) {
159
160
  if (expectedAmount <= 0n) {
160
161
  return { valid: false, confirmed: false, signature, error: "Invalid expected amount" };
161
162
  }
163
+ if (signatureStore) {
164
+ const isUsed = await signatureStore.hasBeenUsed(signature);
165
+ if (isUsed) {
166
+ return { valid: false, confirmed: true, signature, error: "Signature already used" };
167
+ }
168
+ }
162
169
  const effectiveMaxAge = Math.min(Math.max(maxAgeSeconds, 60), 3600);
163
170
  const connection = getConnection(clientConfig);
164
171
  try {
@@ -267,8 +274,6 @@ function solToLamports(sol) {
267
274
  }
268
275
  return BigInt(Math.floor(sol * web3_js.LAMPORTS_PER_SOL));
269
276
  }
270
-
271
- // src/solana/spl.ts
272
277
  var SIGNATURE_REGEX2 = /^[1-9A-HJ-NP-Za-km-z]{87,88}$/;
273
278
  var WALLET_REGEX2 = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
274
279
  function resolveMintAddress(asset, network) {
@@ -362,8 +367,15 @@ async function verifySPLPayment(params) {
362
367
  expectedAmount,
363
368
  asset,
364
369
  clientConfig,
365
- maxAgeSeconds = 300
370
+ maxAgeSeconds = 300,
371
+ signatureStore
366
372
  } = params;
373
+ if (signatureStore) {
374
+ const isUsed = await signatureStore.hasBeenUsed(signature);
375
+ if (isUsed) {
376
+ return { valid: false, confirmed: true, signature, error: "Signature already used" };
377
+ }
378
+ }
367
379
  if (!SIGNATURE_REGEX2.test(signature)) {
368
380
  return { valid: false, confirmed: false, signature, error: "Invalid signature format" };
369
381
  }
@@ -408,6 +420,27 @@ async function verifySPLPayment(params) {
408
420
  error: "No valid token transfer to recipient found"
409
421
  };
410
422
  }
423
+ if (transfer.to) {
424
+ try {
425
+ const destinationInfo = await connection.getParsedAccountInfo(new web3_js.PublicKey(transfer.to));
426
+ const owner = destinationInfo.value?.data?.parsed?.info?.owner;
427
+ if (owner && owner !== expectedRecipient) {
428
+ return {
429
+ valid: false,
430
+ confirmed: true,
431
+ signature,
432
+ error: "Recipient mismatch: Token account not owned by merchant"
433
+ };
434
+ }
435
+ } catch (e) {
436
+ return {
437
+ valid: false,
438
+ confirmed: true,
439
+ signature,
440
+ error: "Could not verify token account owner"
441
+ };
442
+ }
443
+ }
411
444
  if (transfer.mint !== mintAddress) {
412
445
  return {
413
446
  valid: false,
@@ -1286,14 +1319,14 @@ async function getSolPrice() {
1286
1319
  }
1287
1320
  }
1288
1321
  if (cachedPrice) {
1289
- return cachedPrice;
1322
+ return {
1323
+ ...cachedPrice,
1324
+ source: `${cachedPrice.source} (stale)`
1325
+ };
1290
1326
  }
1291
- return {
1292
- solPrice: 150,
1293
- // Reasonable fallback
1294
- fetchedAt: /* @__PURE__ */ new Date(),
1295
- source: "fallback"
1296
- };
1327
+ throw new Error(
1328
+ "Failed to fetch SOL price from all providers. Configure a custom provider or ensure network connectivity."
1329
+ );
1297
1330
  }
1298
1331
  async function lamportsToUsd(lamports) {
1299
1332
  const { solPrice } = await getSolPrice();
@@ -1327,10 +1360,300 @@ function clearPriceCache() {
1327
1360
  function getProviders() {
1328
1361
  return PROVIDERS.map((p) => ({ name: p.name, url: p.url }));
1329
1362
  }
1363
+ var WALLET_REGEX4 = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
1364
+ function isValidWalletAddress2(address) {
1365
+ if (!address || typeof address !== "string") return false;
1366
+ return WALLET_REGEX4.test(address);
1367
+ }
1368
+ async function executeAgentPayment(params) {
1369
+ const {
1370
+ connection,
1371
+ agentKeypair,
1372
+ recipientAddress,
1373
+ amountLamports,
1374
+ priorityFee,
1375
+ confirmationTimeout = 6e4
1376
+ } = params;
1377
+ if (!isValidWalletAddress2(recipientAddress)) {
1378
+ return {
1379
+ success: false,
1380
+ error: "Invalid recipient address format"
1381
+ };
1382
+ }
1383
+ if (amountLamports <= 0n) {
1384
+ return {
1385
+ success: false,
1386
+ error: "Amount must be greater than 0"
1387
+ };
1388
+ }
1389
+ try {
1390
+ const recipientPubkey = new web3_js.PublicKey(recipientAddress);
1391
+ const transferInstruction = web3_js.SystemProgram.transfer({
1392
+ fromPubkey: agentKeypair.publicKey,
1393
+ toPubkey: recipientPubkey,
1394
+ lamports: amountLamports
1395
+ });
1396
+ const { transaction, lastValidBlockHeight } = await buildVersionedTransaction({
1397
+ connection,
1398
+ payer: agentKeypair.publicKey,
1399
+ instructions: [transferInstruction],
1400
+ priorityFee
1401
+ });
1402
+ transaction.sign([agentKeypair]);
1403
+ const signature = await connection.sendTransaction(transaction, {
1404
+ maxRetries: 3,
1405
+ skipPreflight: false
1406
+ });
1407
+ const confirmationPromise = connection.confirmTransaction(
1408
+ {
1409
+ signature,
1410
+ lastValidBlockHeight,
1411
+ blockhash: transaction.message.recentBlockhash
1412
+ },
1413
+ "confirmed"
1414
+ );
1415
+ const timeoutPromise = new Promise((_, reject) => {
1416
+ setTimeout(() => reject(new Error("Confirmation timeout")), confirmationTimeout);
1417
+ });
1418
+ const confirmation = await Promise.race([confirmationPromise, timeoutPromise]);
1419
+ if (confirmation.value.err) {
1420
+ return {
1421
+ success: false,
1422
+ signature,
1423
+ error: "Transaction failed on-chain"
1424
+ };
1425
+ }
1426
+ const txDetails = await connection.getTransaction(signature, {
1427
+ commitment: "confirmed",
1428
+ maxSupportedTransactionVersion: 0
1429
+ });
1430
+ return {
1431
+ success: true,
1432
+ signature,
1433
+ confirmedAt: txDetails?.blockTime ?? Math.floor(Date.now() / 1e3),
1434
+ slot: txDetails?.slot ?? confirmation.context.slot,
1435
+ amountLamports,
1436
+ amountSol: Number(amountLamports) / web3_js.LAMPORTS_PER_SOL
1437
+ };
1438
+ } catch (error) {
1439
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
1440
+ return {
1441
+ success: false,
1442
+ error: errorMessage
1443
+ };
1444
+ }
1445
+ }
1446
+ async function getAgentBalance(connection, agentKeypair) {
1447
+ const balance = await connection.getBalance(agentKeypair.publicKey);
1448
+ return {
1449
+ balance: BigInt(balance),
1450
+ balanceSol: balance / web3_js.LAMPORTS_PER_SOL
1451
+ };
1452
+ }
1453
+ async function hasAgentSufficientBalance(connection, agentKeypair, requiredLamports) {
1454
+ const { balance } = await getAgentBalance(connection, agentKeypair);
1455
+ const totalRequired = requiredLamports + 10000n;
1456
+ return {
1457
+ sufficient: balance >= totalRequired,
1458
+ balance,
1459
+ required: totalRequired
1460
+ };
1461
+ }
1462
+ function keypairFromBase58(base58Secret) {
1463
+ const bytes = Buffer.from(base58Secret, "base64");
1464
+ if (bytes.length !== 64) {
1465
+ const parts = base58Secret.split(",").map((n) => parseInt(n.trim(), 10));
1466
+ if (parts.length === 64) {
1467
+ return web3_js.Keypair.fromSecretKey(Uint8Array.from(parts));
1468
+ }
1469
+ throw new Error("Invalid secret key format. Expected base58 string or comma-separated bytes.");
1470
+ }
1471
+ return web3_js.Keypair.fromSecretKey(bytes);
1472
+ }
1473
+ function generateAgentKeypair() {
1474
+ const keypair = web3_js.Keypair.generate();
1475
+ const secretBytes = Array.from(keypair.secretKey);
1476
+ return {
1477
+ keypair,
1478
+ secretBase58: secretBytes.join(","),
1479
+ // Comma-separated for easy storage
1480
+ publicKey: keypair.publicKey.toBase58()
1481
+ };
1482
+ }
1483
+ var MAX_CREDITS = 1e3;
1484
+ var MIN_SECRET_LENGTH2 = 32;
1485
+ function getSecretKey2(secret) {
1486
+ if (!secret || typeof secret !== "string") {
1487
+ throw new Error("Session secret is required");
1488
+ }
1489
+ if (secret.length < MIN_SECRET_LENGTH2) {
1490
+ throw new Error(`Session secret must be at least ${MIN_SECRET_LENGTH2} characters`);
1491
+ }
1492
+ return new TextEncoder().encode(secret);
1493
+ }
1494
+ function validateWalletAddress2(address) {
1495
+ if (!address || typeof address !== "string") return false;
1496
+ const base58Regex = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
1497
+ return base58Regex.test(address);
1498
+ }
1499
+ async function createCreditSession(walletAddress, purchaseId, config2) {
1500
+ if (!validateWalletAddress2(walletAddress)) {
1501
+ throw new Error("Invalid wallet address format");
1502
+ }
1503
+ if (config2.initialCredits <= 0 || config2.initialCredits > MAX_CREDITS) {
1504
+ throw new Error(`Credits must be between 1 and ${MAX_CREDITS}`);
1505
+ }
1506
+ if (!config2.durationHours || config2.durationHours <= 0 || config2.durationHours > 8760) {
1507
+ throw new Error("Session duration must be between 1 and 8760 hours (1 year)");
1508
+ }
1509
+ const sessionId = uuid.v4();
1510
+ const now = Math.floor(Date.now() / 1e3);
1511
+ const expiresAt = now + config2.durationHours * 3600;
1512
+ const bundleExpiry = config2.bundleExpiryHours ? now + config2.bundleExpiryHours * 3600 : expiresAt;
1513
+ const session = {
1514
+ id: sessionId,
1515
+ walletAddress,
1516
+ unlockedArticles: [purchaseId],
1517
+ siteWideUnlock: false,
1518
+ createdAt: now,
1519
+ expiresAt,
1520
+ credits: config2.initialCredits,
1521
+ bundleExpiry,
1522
+ bundleType: config2.bundleType
1523
+ };
1524
+ const payload = {
1525
+ sub: walletAddress,
1526
+ sid: sessionId,
1527
+ articles: session.unlockedArticles,
1528
+ siteWide: false,
1529
+ credits: config2.initialCredits,
1530
+ bundleExpiry,
1531
+ bundleType: config2.bundleType,
1532
+ iat: now,
1533
+ exp: expiresAt
1534
+ };
1535
+ const token = await new jose.SignJWT(payload).setProtectedHeader({ alg: "HS256" }).setIssuedAt().setExpirationTime(`${config2.durationHours}h`).sign(getSecretKey2(config2.secret));
1536
+ return { token, session };
1537
+ }
1538
+ async function validateCreditSession(token, secret) {
1539
+ if (!token || typeof token !== "string") {
1540
+ return { valid: false, reason: "Invalid token format" };
1541
+ }
1542
+ try {
1543
+ const { payload } = await jose.jwtVerify(token, getSecretKey2(secret));
1544
+ const creditPayload = payload;
1545
+ if (!creditPayload.sub || !creditPayload.sid || !creditPayload.exp) {
1546
+ return { valid: false, reason: "Malformed session payload" };
1547
+ }
1548
+ const now = Math.floor(Date.now() / 1e3);
1549
+ if (creditPayload.exp < now) {
1550
+ return { valid: false, reason: "Session expired" };
1551
+ }
1552
+ if (creditPayload.bundleExpiry && creditPayload.bundleExpiry < now) {
1553
+ return { valid: false, reason: "Bundle expired" };
1554
+ }
1555
+ if (!validateWalletAddress2(creditPayload.sub)) {
1556
+ return { valid: false, reason: "Invalid session data" };
1557
+ }
1558
+ const session = {
1559
+ id: creditPayload.sid,
1560
+ walletAddress: creditPayload.sub,
1561
+ unlockedArticles: Array.isArray(creditPayload.articles) ? creditPayload.articles : [],
1562
+ siteWideUnlock: Boolean(creditPayload.siteWide),
1563
+ createdAt: creditPayload.iat ?? 0,
1564
+ expiresAt: creditPayload.exp,
1565
+ credits: creditPayload.credits ?? 0,
1566
+ bundleExpiry: creditPayload.bundleExpiry,
1567
+ bundleType: creditPayload.bundleType
1568
+ };
1569
+ return { valid: true, session };
1570
+ } catch {
1571
+ return { valid: false, reason: "Invalid session" };
1572
+ }
1573
+ }
1574
+ async function useCredit(token, secret, creditsToUse = 1) {
1575
+ if (creditsToUse <= 0) {
1576
+ return { success: false, remainingCredits: 0, error: "Invalid credit amount" };
1577
+ }
1578
+ const validation = await validateCreditSession(token, secret);
1579
+ if (!validation.valid || !validation.session) {
1580
+ return {
1581
+ success: false,
1582
+ remainingCredits: 0,
1583
+ error: validation.reason || "Invalid session"
1584
+ };
1585
+ }
1586
+ const session = validation.session;
1587
+ if (session.credits < creditsToUse) {
1588
+ return {
1589
+ success: false,
1590
+ remainingCredits: session.credits,
1591
+ error: "Insufficient credits"
1592
+ };
1593
+ }
1594
+ const newCredits = session.credits - creditsToUse;
1595
+ const payload = {
1596
+ sub: session.walletAddress,
1597
+ sid: session.id,
1598
+ articles: session.unlockedArticles,
1599
+ siteWide: session.siteWideUnlock,
1600
+ credits: newCredits,
1601
+ bundleExpiry: session.bundleExpiry,
1602
+ bundleType: session.bundleType,
1603
+ iat: session.createdAt,
1604
+ exp: session.expiresAt
1605
+ };
1606
+ const newToken = await new jose.SignJWT(payload).setProtectedHeader({ alg: "HS256" }).sign(getSecretKey2(secret));
1607
+ return {
1608
+ success: true,
1609
+ remainingCredits: newCredits,
1610
+ newToken
1611
+ };
1612
+ }
1613
+ async function addCredits(token, secret, creditsToAdd) {
1614
+ if (creditsToAdd <= 0 || creditsToAdd > MAX_CREDITS) {
1615
+ return { success: false, error: "Invalid credit amount" };
1616
+ }
1617
+ const validation = await validateCreditSession(token, secret);
1618
+ if (!validation.valid || !validation.session) {
1619
+ return { success: false, error: validation.reason || "Invalid session" };
1620
+ }
1621
+ const session = validation.session;
1622
+ const newCredits = Math.min(session.credits + creditsToAdd, MAX_CREDITS);
1623
+ const payload = {
1624
+ sub: session.walletAddress,
1625
+ sid: session.id,
1626
+ articles: session.unlockedArticles,
1627
+ siteWide: session.siteWideUnlock,
1628
+ credits: newCredits,
1629
+ bundleExpiry: session.bundleExpiry,
1630
+ bundleType: session.bundleType,
1631
+ iat: session.createdAt,
1632
+ exp: session.expiresAt
1633
+ };
1634
+ const newToken = await new jose.SignJWT(payload).setProtectedHeader({ alg: "HS256" }).sign(getSecretKey2(secret));
1635
+ return {
1636
+ success: true,
1637
+ newToken,
1638
+ totalCredits: newCredits
1639
+ };
1640
+ }
1641
+ async function getRemainingCredits(token, secret) {
1642
+ const validation = await validateCreditSession(token, secret);
1643
+ if (!validation.valid || !validation.session) {
1644
+ return { credits: 0, valid: false };
1645
+ }
1646
+ return {
1647
+ credits: validation.session.credits,
1648
+ valid: true,
1649
+ bundleExpiry: validation.session.bundleExpiry
1650
+ };
1651
+ }
1330
1652
 
1331
1653
  exports.TOKEN_MINTS = TOKEN_MINTS;
1332
1654
  exports.X402_HEADERS = X402_HEADERS;
1333
1655
  exports.addArticleToSession = addArticleToSession;
1656
+ exports.addCredits = addCredits;
1334
1657
  exports.buildPaymentRequirement = buildPaymentRequirement;
1335
1658
  exports.buildSolanaPayUrl = buildSolanaPayUrl;
1336
1659
  exports.buildVersionedTransaction = buildVersionedTransaction;
@@ -1341,6 +1664,7 @@ exports.configurePricing = configurePricing;
1341
1664
  exports.create402Headers = create402Headers;
1342
1665
  exports.create402Response = create402Response;
1343
1666
  exports.create402ResponseBody = create402ResponseBody;
1667
+ exports.createCreditSession = createCreditSession;
1344
1668
  exports.createMemoryStore = createMemoryStore;
1345
1669
  exports.createPaymentFlow = createPaymentFlow;
1346
1670
  exports.createPaymentReference = createPaymentReference;
@@ -1353,20 +1677,26 @@ exports.encodePaymentRequired = encodePaymentRequired;
1353
1677
  exports.encodePaymentRequirement = encodePaymentRequirement;
1354
1678
  exports.encodePaymentResponse = encodePaymentResponse;
1355
1679
  exports.estimatePriorityFee = estimatePriorityFee;
1680
+ exports.executeAgentPayment = executeAgentPayment;
1356
1681
  exports.fetchLookupTables = fetchLookupTables;
1357
1682
  exports.formatPriceDisplay = formatPriceDisplay;
1358
1683
  exports.formatPriceSync = formatPriceSync;
1684
+ exports.generateAgentKeypair = generateAgentKeypair;
1685
+ exports.getAgentBalance = getAgentBalance;
1359
1686
  exports.getConnection = getConnection;
1360
1687
  exports.getConnectionWithFallback = getConnectionWithFallback;
1361
1688
  exports.getProviders = getProviders;
1689
+ exports.getRemainingCredits = getRemainingCredits;
1362
1690
  exports.getSolPrice = getSolPrice;
1363
1691
  exports.getTokenDecimals = getTokenDecimals;
1364
1692
  exports.getWalletTransactions = getWalletTransactions;
1693
+ exports.hasAgentSufficientBalance = hasAgentSufficientBalance;
1365
1694
  exports.isArticleUnlocked = isArticleUnlocked;
1366
1695
  exports.isMainnet = isMainnet;
1367
1696
  exports.isNativeAsset = isNativeAsset;
1368
1697
  exports.isRetryableRPCError = isRetryableRPCError;
1369
1698
  exports.isVersionedTransaction = isVersionedTransaction;
1699
+ exports.keypairFromBase58 = keypairFromBase58;
1370
1700
  exports.lamportsToSol = lamportsToSol;
1371
1701
  exports.lamportsToUsd = lamportsToUsd;
1372
1702
  exports.parsePaymentHeader = parsePaymentHeader;
@@ -1375,6 +1705,8 @@ exports.resolveMintAddress = resolveMintAddress;
1375
1705
  exports.solToLamports = solToLamports;
1376
1706
  exports.toX402Network = toX402Network;
1377
1707
  exports.usdToLamports = usdToLamports;
1708
+ exports.useCredit = useCredit;
1709
+ exports.validateCreditSession = validateCreditSession;
1378
1710
  exports.validateSession = validateSession;
1379
1711
  exports.verifyPayment = verifyPayment;
1380
1712
  exports.verifySPLPayment = verifySPLPayment;