@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.
- package/README.md +100 -167
- package/dist/client/index.cjs +99 -0
- package/dist/client/index.cjs.map +1 -0
- package/dist/client/index.d.cts +112 -0
- package/dist/client/index.d.ts +112 -0
- package/dist/client/index.js +95 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client-CSZHI8o8.d.ts +32 -0
- package/dist/client-vRr48m2x.d.cts +32 -0
- package/dist/index.cjs +696 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -3
- package/dist/index.d.ts +11 -3
- package/dist/index.js +674 -16
- package/dist/index.js.map +1 -1
- package/dist/memory-Daxkczti.d.cts +29 -0
- package/dist/memory-Daxkczti.d.ts +29 -0
- package/dist/middleware/index.cjs +261 -0
- package/dist/middleware/index.cjs.map +1 -0
- package/dist/middleware/index.d.cts +90 -0
- package/dist/middleware/index.d.ts +90 -0
- package/dist/middleware/index.js +255 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/nextjs-BK0pVb9Y.d.ts +78 -0
- package/dist/nextjs-Bm272Jkj.d.cts +78 -0
- package/dist/{client-kfCr7G-P.d.cts → payment-CTxdtqmc.d.cts} +23 -34
- package/dist/{client-kfCr7G-P.d.ts → payment-CTxdtqmc.d.ts} +23 -34
- package/dist/pricing/index.cjs +142 -0
- package/dist/pricing/index.cjs.map +1 -0
- package/dist/pricing/index.d.cts +111 -0
- package/dist/pricing/index.d.ts +111 -0
- package/dist/pricing/index.js +133 -0
- package/dist/pricing/index.js.map +1 -0
- package/dist/session/index.d.cts +29 -1
- package/dist/session/index.d.ts +29 -1
- package/dist/{index-uxMb72hH.d.cts → session-D2IoWAWV.d.cts} +1 -27
- package/dist/{index-uxMb72hH.d.ts → session-D2IoWAWV.d.ts} +1 -27
- package/dist/solana/index.cjs +193 -0
- package/dist/solana/index.cjs.map +1 -1
- package/dist/solana/index.d.cts +60 -3
- package/dist/solana/index.d.ts +60 -3
- package/dist/solana/index.js +190 -1
- package/dist/solana/index.js.map +1 -1
- package/dist/store/index.cjs +99 -0
- package/dist/store/index.cjs.map +1 -0
- package/dist/store/index.d.cts +38 -0
- package/dist/store/index.d.ts +38 -0
- package/dist/store/index.js +96 -0
- package/dist/store/index.js.map +1 -0
- package/dist/utils/index.cjs +68 -0
- package/dist/utils/index.cjs.map +1 -0
- package/dist/utils/index.d.cts +30 -0
- package/dist/utils/index.d.ts +30 -0
- package/dist/utils/index.js +65 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/x402/index.cjs +2 -1
- package/dist/x402/index.cjs.map +1 -1
- package/dist/x402/index.d.cts +2 -1
- package/dist/x402/index.d.ts +2 -1
- package/dist/x402/index.js +2 -1
- package/dist/x402/index.js.map +1 -1
- 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/
|
|
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(
|
|
9
|
-
const { network, rpcUrl, tatumApiKey } =
|
|
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(
|
|
23
|
-
const { network } =
|
|
30
|
+
function getConnection(config2) {
|
|
31
|
+
const { network } = config2;
|
|
24
32
|
if (cachedConnection && cachedNetwork === network) {
|
|
25
33
|
return cachedConnection;
|
|
26
34
|
}
|
|
27
|
-
const rpcUrl = buildRpcUrl(
|
|
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,
|
|
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 (!
|
|
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 +
|
|
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(`${
|
|
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
|
|
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 (!
|
|
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:
|
|
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
|
|
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 (!
|
|
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
|
-
|
|
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
|