@alleyboss/micropay-solana-x402-paywall 1.0.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +624 -6
- 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 +604 -7
- 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 +79 -0
- package/dist/pricing/index.cjs.map +1 -0
- package/dist/pricing/index.d.cts +67 -0
- package/dist/pricing/index.d.ts +67 -0
- package/dist/pricing/index.js +72 -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,7 +2,15 @@ import { Connection, PublicKey, LAMPORTS_PER_SOL, clusterApiUrl } from '@solana/
|
|
|
2
2
|
import { SignJWT, jwtVerify } from 'jose';
|
|
3
3
|
import { v4 } from 'uuid';
|
|
4
4
|
|
|
5
|
-
// src/
|
|
5
|
+
// src/types/payment.ts
|
|
6
|
+
var TOKEN_MINTS = {
|
|
7
|
+
/** USDC on mainnet */
|
|
8
|
+
USDC_MAINNET: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
|
|
9
|
+
/** USDC on devnet */
|
|
10
|
+
USDC_DEVNET: "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
|
|
11
|
+
/** USDT on mainnet */
|
|
12
|
+
USDT_MAINNET: "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB"
|
|
13
|
+
};
|
|
6
14
|
var cachedConnection = null;
|
|
7
15
|
var cachedNetwork = null;
|
|
8
16
|
function buildRpcUrl(config) {
|
|
@@ -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) {
|
|
@@ -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,415 @@ 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, config) {
|
|
808
|
+
if (!matchesProtectedPath(path, config.protectedPaths)) {
|
|
809
|
+
return { allowed: true };
|
|
810
|
+
}
|
|
811
|
+
if (!sessionToken) {
|
|
812
|
+
return {
|
|
813
|
+
allowed: false,
|
|
814
|
+
reason: "No session token",
|
|
815
|
+
requiresPayment: true
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
const validation = await validateSession(sessionToken, config.sessionSecret);
|
|
819
|
+
if (!validation.valid || !validation.session) {
|
|
820
|
+
return {
|
|
821
|
+
allowed: false,
|
|
822
|
+
reason: validation.reason || "Invalid session",
|
|
823
|
+
requiresPayment: true
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
return {
|
|
827
|
+
allowed: true,
|
|
828
|
+
session: validation.session
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
function createPaywallMiddleware(config) {
|
|
832
|
+
const { cookieName = "x402_session" } = config;
|
|
833
|
+
return async function middleware(request) {
|
|
834
|
+
const url = new URL(request.url);
|
|
835
|
+
const path = url.pathname;
|
|
836
|
+
const cookieHeader = request.headers.get("cookie") || "";
|
|
837
|
+
const cookies = Object.fromEntries(
|
|
838
|
+
cookieHeader.split(";").map((c) => {
|
|
839
|
+
const [key, ...vals] = c.trim().split("=");
|
|
840
|
+
return [key, vals.join("=")];
|
|
841
|
+
})
|
|
842
|
+
);
|
|
843
|
+
const sessionToken = cookies[cookieName];
|
|
844
|
+
const result = await checkPaywallAccess(path, sessionToken, config);
|
|
845
|
+
if (!result.allowed && result.requiresPayment) {
|
|
846
|
+
const body = config.custom402Response ? config.custom402Response(path) : {
|
|
847
|
+
error: "Payment Required",
|
|
848
|
+
message: "This resource requires payment to access",
|
|
849
|
+
path
|
|
850
|
+
};
|
|
851
|
+
return new Response(JSON.stringify(body), {
|
|
852
|
+
status: 402,
|
|
853
|
+
headers: {
|
|
854
|
+
"Content-Type": "application/json"
|
|
855
|
+
}
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
return null;
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
function withPaywall(handler, options) {
|
|
862
|
+
const { sessionSecret, cookieName = "x402_session", articleId } = options;
|
|
863
|
+
return async function protectedHandler(request) {
|
|
864
|
+
const cookieHeader = request.headers.get("cookie") || "";
|
|
865
|
+
const cookies = Object.fromEntries(
|
|
866
|
+
cookieHeader.split(";").map((c) => {
|
|
867
|
+
const [key, ...vals] = c.trim().split("=");
|
|
868
|
+
return [key, vals.join("=")];
|
|
869
|
+
})
|
|
870
|
+
);
|
|
871
|
+
const sessionToken = cookies[cookieName];
|
|
872
|
+
if (!sessionToken) {
|
|
873
|
+
return new Response(
|
|
874
|
+
JSON.stringify({ error: "Payment Required", message: "No session token" }),
|
|
875
|
+
{ status: 402, headers: { "Content-Type": "application/json" } }
|
|
876
|
+
);
|
|
877
|
+
}
|
|
878
|
+
const validation = await validateSession(sessionToken, sessionSecret);
|
|
879
|
+
if (!validation.valid || !validation.session) {
|
|
880
|
+
return new Response(
|
|
881
|
+
JSON.stringify({ error: "Payment Required", message: validation.reason }),
|
|
882
|
+
{ status: 402, headers: { "Content-Type": "application/json" } }
|
|
883
|
+
);
|
|
884
|
+
}
|
|
885
|
+
if (articleId) {
|
|
886
|
+
const { session } = validation;
|
|
887
|
+
const hasAccess = session.siteWideUnlock || session.unlockedArticles.includes(articleId);
|
|
888
|
+
if (!hasAccess) {
|
|
889
|
+
return new Response(
|
|
890
|
+
JSON.stringify({ error: "Payment Required", message: "Article not unlocked" }),
|
|
891
|
+
{ status: 402, headers: { "Content-Type": "application/json" } }
|
|
892
|
+
);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
return handler(request, validation.session);
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// src/utils/retry.ts
|
|
900
|
+
function sleep(ms) {
|
|
901
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
902
|
+
}
|
|
903
|
+
function calculateDelay(attempt, options) {
|
|
904
|
+
const { baseDelay, maxDelay, jitter } = options;
|
|
905
|
+
let delay = baseDelay * Math.pow(2, attempt);
|
|
906
|
+
delay = Math.min(delay, maxDelay);
|
|
907
|
+
if (jitter) {
|
|
908
|
+
const jitterAmount = delay * 0.25;
|
|
909
|
+
delay += Math.random() * jitterAmount * 2 - jitterAmount;
|
|
910
|
+
}
|
|
911
|
+
return Math.floor(delay);
|
|
912
|
+
}
|
|
913
|
+
async function withRetry(fn, options = {}) {
|
|
914
|
+
const {
|
|
915
|
+
maxAttempts = 3,
|
|
916
|
+
baseDelay = 500,
|
|
917
|
+
maxDelay = 1e4,
|
|
918
|
+
jitter = true,
|
|
919
|
+
retryOn = () => true
|
|
920
|
+
} = options;
|
|
921
|
+
let lastError;
|
|
922
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
923
|
+
try {
|
|
924
|
+
return await fn();
|
|
925
|
+
} catch (error) {
|
|
926
|
+
lastError = error;
|
|
927
|
+
if (!retryOn(error)) {
|
|
928
|
+
throw error;
|
|
929
|
+
}
|
|
930
|
+
if (attempt < maxAttempts - 1) {
|
|
931
|
+
const delay = calculateDelay(attempt, {
|
|
932
|
+
baseDelay,
|
|
933
|
+
maxDelay,
|
|
934
|
+
jitter
|
|
935
|
+
});
|
|
936
|
+
await sleep(delay);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
throw lastError;
|
|
941
|
+
}
|
|
942
|
+
function isRetryableRPCError(error) {
|
|
943
|
+
if (error instanceof Error) {
|
|
944
|
+
const message = error.message.toLowerCase();
|
|
945
|
+
if (message.includes("429") || message.includes("rate limit")) {
|
|
946
|
+
return true;
|
|
947
|
+
}
|
|
948
|
+
if (message.includes("timeout") || message.includes("econnreset")) {
|
|
949
|
+
return true;
|
|
950
|
+
}
|
|
951
|
+
if (message.includes("503") || message.includes("502") || message.includes("500")) {
|
|
952
|
+
return true;
|
|
953
|
+
}
|
|
954
|
+
if (message.includes("blockhash not found") || message.includes("slot skipped")) {
|
|
955
|
+
return true;
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
return false;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
// src/client/payment.ts
|
|
962
|
+
function buildSolanaPayUrl(params) {
|
|
963
|
+
const { recipient, amount, splToken, reference, label, message } = params;
|
|
964
|
+
const url = new URL(`solana:${recipient}`);
|
|
965
|
+
if (amount !== void 0) {
|
|
966
|
+
url.searchParams.set("amount", amount.toString());
|
|
967
|
+
}
|
|
968
|
+
if (splToken) {
|
|
969
|
+
url.searchParams.set("spl-token", splToken);
|
|
970
|
+
}
|
|
971
|
+
if (reference) {
|
|
972
|
+
url.searchParams.set("reference", reference);
|
|
973
|
+
}
|
|
974
|
+
if (label) {
|
|
975
|
+
url.searchParams.set("label", label);
|
|
976
|
+
}
|
|
977
|
+
if (message) {
|
|
978
|
+
url.searchParams.set("message", message);
|
|
979
|
+
}
|
|
980
|
+
return url.toString();
|
|
981
|
+
}
|
|
982
|
+
function createPaymentFlow(config) {
|
|
983
|
+
const { network, recipientWallet, amount, asset = "native", memo } = config;
|
|
984
|
+
let decimals = 9;
|
|
985
|
+
let mintAddress;
|
|
986
|
+
if (asset === "usdc") {
|
|
987
|
+
decimals = 6;
|
|
988
|
+
mintAddress = network === "mainnet-beta" ? TOKEN_MINTS.USDC_MAINNET : TOKEN_MINTS.USDC_DEVNET;
|
|
989
|
+
} else if (asset === "usdt") {
|
|
990
|
+
decimals = 6;
|
|
991
|
+
mintAddress = TOKEN_MINTS.USDT_MAINNET;
|
|
992
|
+
} else if (typeof asset === "object" && "mint" in asset) {
|
|
993
|
+
decimals = asset.decimals ?? 6;
|
|
994
|
+
mintAddress = asset.mint;
|
|
995
|
+
}
|
|
996
|
+
const naturalAmount = Number(amount) / Math.pow(10, decimals);
|
|
997
|
+
return {
|
|
998
|
+
/** Get the payment configuration */
|
|
999
|
+
getConfig: () => ({ ...config }),
|
|
1000
|
+
/** Get amount in natural display units (e.g., 0.01 SOL) */
|
|
1001
|
+
getDisplayAmount: () => naturalAmount,
|
|
1002
|
+
/** Get amount formatted with symbol */
|
|
1003
|
+
getFormattedAmount: () => {
|
|
1004
|
+
const symbol = asset === "native" ? "SOL" : asset === "usdc" ? "USDC" : asset === "usdt" ? "USDT" : "tokens";
|
|
1005
|
+
return `${naturalAmount.toFixed(decimals > 6 ? 4 : 2)} ${symbol}`;
|
|
1006
|
+
},
|
|
1007
|
+
/** Generate Solana Pay URL for QR codes */
|
|
1008
|
+
getSolanaPayUrl: (options = {}) => {
|
|
1009
|
+
return buildSolanaPayUrl({
|
|
1010
|
+
recipient: recipientWallet,
|
|
1011
|
+
amount: naturalAmount,
|
|
1012
|
+
splToken: mintAddress,
|
|
1013
|
+
label: options.label,
|
|
1014
|
+
reference: options.reference,
|
|
1015
|
+
message: memo
|
|
1016
|
+
});
|
|
1017
|
+
},
|
|
1018
|
+
/** Get the token mint address (undefined for native SOL) */
|
|
1019
|
+
getMintAddress: () => mintAddress,
|
|
1020
|
+
/** Check if this is a native SOL payment */
|
|
1021
|
+
isNativePayment: () => asset === "native",
|
|
1022
|
+
/** Get network information */
|
|
1023
|
+
getNetworkInfo: () => ({
|
|
1024
|
+
network,
|
|
1025
|
+
isMainnet: network === "mainnet-beta",
|
|
1026
|
+
explorerUrl: network === "mainnet-beta" ? "https://explorer.solana.com" : "https://explorer.solana.com?cluster=devnet"
|
|
1027
|
+
}),
|
|
1028
|
+
/** Build explorer URL for a transaction */
|
|
1029
|
+
getExplorerUrl: (signature) => {
|
|
1030
|
+
const baseUrl = "https://explorer.solana.com/tx";
|
|
1031
|
+
const cluster = network === "mainnet-beta" ? "" : "?cluster=devnet";
|
|
1032
|
+
return `${baseUrl}/${signature}${cluster}`;
|
|
1033
|
+
}
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
function createPaymentReference() {
|
|
1037
|
+
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
1038
|
+
return crypto.randomUUID();
|
|
1039
|
+
}
|
|
1040
|
+
return `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
// src/pricing/index.ts
|
|
1044
|
+
var cachedPrice = null;
|
|
1045
|
+
var CACHE_TTL_MS = 6e4;
|
|
1046
|
+
async function getSolPrice() {
|
|
1047
|
+
if (cachedPrice && Date.now() - cachedPrice.fetchedAt.getTime() < CACHE_TTL_MS) {
|
|
1048
|
+
return cachedPrice;
|
|
1049
|
+
}
|
|
1050
|
+
try {
|
|
1051
|
+
const response = await fetch(
|
|
1052
|
+
"https://api.coingecko.com/api/v3/simple/price?ids=solana&vs_currencies=usd",
|
|
1053
|
+
{
|
|
1054
|
+
headers: { "Accept": "application/json" },
|
|
1055
|
+
signal: AbortSignal.timeout(5e3)
|
|
1056
|
+
}
|
|
1057
|
+
);
|
|
1058
|
+
if (!response.ok) {
|
|
1059
|
+
throw new Error(`Price fetch failed: ${response.status}`);
|
|
1060
|
+
}
|
|
1061
|
+
const data = await response.json();
|
|
1062
|
+
if (!data.solana?.usd) {
|
|
1063
|
+
throw new Error("Invalid price response");
|
|
1064
|
+
}
|
|
1065
|
+
cachedPrice = {
|
|
1066
|
+
solPrice: data.solana.usd,
|
|
1067
|
+
fetchedAt: /* @__PURE__ */ new Date(),
|
|
1068
|
+
source: "coingecko"
|
|
1069
|
+
};
|
|
1070
|
+
return cachedPrice;
|
|
1071
|
+
} catch (error) {
|
|
1072
|
+
if (cachedPrice) {
|
|
1073
|
+
return cachedPrice;
|
|
1074
|
+
}
|
|
1075
|
+
return {
|
|
1076
|
+
solPrice: 150,
|
|
1077
|
+
// Fallback price
|
|
1078
|
+
fetchedAt: /* @__PURE__ */ new Date(),
|
|
1079
|
+
source: "fallback"
|
|
1080
|
+
};
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
async function lamportsToUsd(lamports) {
|
|
1084
|
+
const { solPrice } = await getSolPrice();
|
|
1085
|
+
const sol = Number(lamports) / 1e9;
|
|
1086
|
+
return sol * solPrice;
|
|
1087
|
+
}
|
|
1088
|
+
async function usdToLamports(usd) {
|
|
1089
|
+
const { solPrice } = await getSolPrice();
|
|
1090
|
+
const sol = usd / solPrice;
|
|
1091
|
+
return BigInt(Math.floor(sol * 1e9));
|
|
1092
|
+
}
|
|
1093
|
+
async function formatPriceDisplay(lamports) {
|
|
1094
|
+
const { solPrice } = await getSolPrice();
|
|
1095
|
+
const sol = Number(lamports) / 1e9;
|
|
1096
|
+
const usd = sol * solPrice;
|
|
1097
|
+
return `${sol.toFixed(4)} SOL (~$${usd.toFixed(2)})`;
|
|
1098
|
+
}
|
|
1099
|
+
function formatPriceSync(lamports, solPrice) {
|
|
1100
|
+
const sol = Number(lamports) / 1e9;
|
|
1101
|
+
const usd = sol * solPrice;
|
|
1102
|
+
return {
|
|
1103
|
+
sol,
|
|
1104
|
+
usd,
|
|
1105
|
+
formatted: `${sol.toFixed(4)} SOL (~$${usd.toFixed(2)})`
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
function clearPriceCache() {
|
|
1109
|
+
cachedPrice = null;
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
export { TOKEN_MINTS, X402_HEADERS, addArticleToSession, buildPaymentRequirement, buildSolanaPayUrl, checkPaywallAccess, clearPriceCache, create402Headers, create402ResponseBody, createMemoryStore, createPaymentFlow, createPaymentReference, createPaywallMiddleware, createRedisStore, createSession, decodePaymentRequired, encodePaymentRequired, encodePaymentResponse, formatPriceDisplay, formatPriceSync, getConnection, getSolPrice, getTokenDecimals, getWalletTransactions, isArticleUnlocked, isMainnet, isNativeAsset, isRetryableRPCError, lamportsToSol, lamportsToUsd, parsePaymentHeader, resetConnection, resolveMintAddress, solToLamports, toX402Network, usdToLamports, validateSession, verifyPayment, verifySPLPayment, verifyX402Payment, waitForConfirmation, withPaywall, withRetry };
|
|
516
1113
|
//# sourceMappingURL=index.js.map
|
|
517
1114
|
//# sourceMappingURL=index.js.map
|