@alleyboss/micropay-solana-x402-paywall 1.0.0 → 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 +803 -41
- 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 +783 -42
- 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.cjs +51 -11
- package/dist/session/index.cjs.map +1 -1
- package/dist/session/index.d.cts +29 -1
- package/dist/session/index.d.ts +29 -1
- package/dist/session/index.js +51 -11
- package/dist/session/index.js.map +1 -1
- package/dist/{index-DptevtnU.d.cts → session-D2IoWAWV.d.cts} +1 -24
- package/dist/{index-DptevtnU.d.ts → session-D2IoWAWV.d.ts} +1 -24
- package/dist/solana/index.cjs +235 -15
- package/dist/solana/index.cjs.map +1 -1
- package/dist/solana/index.d.cts +61 -3
- package/dist/solana/index.d.ts +61 -3
- package/dist/solana/index.js +232 -16
- 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 +119 -18
- package/dist/x402/index.cjs.map +1 -1
- package/dist/x402/index.d.cts +6 -1
- package/dist/x402/index.d.ts +6 -1
- package/dist/x402/index.js +119 -18
- package/dist/x402/index.js.map +1 -1
- package/package.json +56 -3
package/dist/index.cjs
CHANGED
|
@@ -4,7 +4,15 @@ var web3_js = require('@solana/web3.js');
|
|
|
4
4
|
var jose = require('jose');
|
|
5
5
|
var uuid = require('uuid');
|
|
6
6
|
|
|
7
|
-
// src/
|
|
7
|
+
// src/types/payment.ts
|
|
8
|
+
var TOKEN_MINTS = {
|
|
9
|
+
/** USDC on mainnet */
|
|
10
|
+
USDC_MAINNET: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
|
|
11
|
+
/** USDC on devnet */
|
|
12
|
+
USDC_DEVNET: "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
|
|
13
|
+
/** USDT on mainnet */
|
|
14
|
+
USDT_MAINNET: "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB"
|
|
15
|
+
};
|
|
8
16
|
var cachedConnection = null;
|
|
9
17
|
var cachedNetwork = null;
|
|
10
18
|
function buildRpcUrl(config) {
|
|
@@ -44,6 +52,16 @@ function isMainnet(network) {
|
|
|
44
52
|
function toX402Network(network) {
|
|
45
53
|
return network === "mainnet-beta" ? "solana-mainnet" : "solana-devnet";
|
|
46
54
|
}
|
|
55
|
+
var SIGNATURE_REGEX = /^[1-9A-HJ-NP-Za-km-z]{87,88}$/;
|
|
56
|
+
var WALLET_REGEX = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
|
|
57
|
+
function isValidSignature(signature) {
|
|
58
|
+
if (!signature || typeof signature !== "string") return false;
|
|
59
|
+
return SIGNATURE_REGEX.test(signature);
|
|
60
|
+
}
|
|
61
|
+
function isValidWalletAddress(address) {
|
|
62
|
+
if (!address || typeof address !== "string") return false;
|
|
63
|
+
return WALLET_REGEX.test(address);
|
|
64
|
+
}
|
|
47
65
|
function parseSOLTransfer(transaction, expectedRecipient) {
|
|
48
66
|
const instructions = transaction.transaction.message.instructions;
|
|
49
67
|
for (const ix of instructions) {
|
|
@@ -84,6 +102,16 @@ async function verifyPayment(params) {
|
|
|
84
102
|
maxAgeSeconds = 300,
|
|
85
103
|
clientConfig
|
|
86
104
|
} = params;
|
|
105
|
+
if (!isValidSignature(signature)) {
|
|
106
|
+
return { valid: false, confirmed: false, signature, error: "Invalid signature format" };
|
|
107
|
+
}
|
|
108
|
+
if (!isValidWalletAddress(expectedRecipient)) {
|
|
109
|
+
return { valid: false, confirmed: false, signature, error: "Invalid recipient address" };
|
|
110
|
+
}
|
|
111
|
+
if (expectedAmount <= 0n) {
|
|
112
|
+
return { valid: false, confirmed: false, signature, error: "Invalid expected amount" };
|
|
113
|
+
}
|
|
114
|
+
const effectiveMaxAge = Math.min(Math.max(maxAgeSeconds, 60), 3600);
|
|
87
115
|
const connection = getConnection(clientConfig);
|
|
88
116
|
try {
|
|
89
117
|
const transaction = await connection.getParsedTransaction(signature, {
|
|
@@ -98,14 +126,17 @@ async function verifyPayment(params) {
|
|
|
98
126
|
valid: false,
|
|
99
127
|
confirmed: true,
|
|
100
128
|
signature,
|
|
101
|
-
error:
|
|
129
|
+
error: "Transaction failed on-chain"
|
|
102
130
|
};
|
|
103
131
|
}
|
|
104
132
|
if (transaction.blockTime) {
|
|
105
133
|
const now = Math.floor(Date.now() / 1e3);
|
|
106
|
-
if (now - transaction.blockTime >
|
|
134
|
+
if (now - transaction.blockTime > effectiveMaxAge) {
|
|
107
135
|
return { valid: false, confirmed: true, signature, error: "Transaction too old" };
|
|
108
136
|
}
|
|
137
|
+
if (transaction.blockTime > now + 60) {
|
|
138
|
+
return { valid: false, confirmed: true, signature, error: "Invalid transaction time" };
|
|
139
|
+
}
|
|
109
140
|
}
|
|
110
141
|
const transferDetails = parseSOLTransfer(transaction, expectedRecipient);
|
|
111
142
|
if (!transferDetails) {
|
|
@@ -124,7 +155,7 @@ async function verifyPayment(params) {
|
|
|
124
155
|
from: transferDetails.from,
|
|
125
156
|
to: transferDetails.to,
|
|
126
157
|
amount: transferDetails.amount,
|
|
127
|
-
error:
|
|
158
|
+
error: "Insufficient payment amount"
|
|
128
159
|
};
|
|
129
160
|
}
|
|
130
161
|
return {
|
|
@@ -142,33 +173,34 @@ async function verifyPayment(params) {
|
|
|
142
173
|
valid: false,
|
|
143
174
|
confirmed: false,
|
|
144
175
|
signature,
|
|
145
|
-
error:
|
|
176
|
+
error: "Verification failed"
|
|
146
177
|
};
|
|
147
178
|
}
|
|
148
179
|
}
|
|
149
180
|
async function waitForConfirmation(signature, clientConfig) {
|
|
181
|
+
if (!isValidSignature(signature)) {
|
|
182
|
+
return { confirmed: false, error: "Invalid signature format" };
|
|
183
|
+
}
|
|
150
184
|
const connection = getConnection(clientConfig);
|
|
151
185
|
try {
|
|
152
186
|
const confirmation = await connection.confirmTransaction(signature, "confirmed");
|
|
153
187
|
if (confirmation.value.err) {
|
|
154
|
-
return {
|
|
155
|
-
confirmed: false,
|
|
156
|
-
error: `Transaction failed: ${JSON.stringify(confirmation.value.err)}`
|
|
157
|
-
};
|
|
188
|
+
return { confirmed: false, error: "Transaction failed" };
|
|
158
189
|
}
|
|
159
190
|
return { confirmed: true, slot: confirmation.context?.slot };
|
|
160
|
-
} catch
|
|
161
|
-
return {
|
|
162
|
-
confirmed: false,
|
|
163
|
-
error: error instanceof Error ? error.message : "Confirmation timeout"
|
|
164
|
-
};
|
|
191
|
+
} catch {
|
|
192
|
+
return { confirmed: false, error: "Confirmation timeout" };
|
|
165
193
|
}
|
|
166
194
|
}
|
|
167
195
|
async function getWalletTransactions(walletAddress, clientConfig, limit = 20) {
|
|
196
|
+
if (!isValidWalletAddress(walletAddress)) {
|
|
197
|
+
return [];
|
|
198
|
+
}
|
|
199
|
+
const safeLimit = Math.min(Math.max(limit, 1), 100);
|
|
168
200
|
const connection = getConnection(clientConfig);
|
|
169
|
-
const pubkey = new web3_js.PublicKey(walletAddress);
|
|
170
201
|
try {
|
|
171
|
-
const
|
|
202
|
+
const pubkey = new web3_js.PublicKey(walletAddress);
|
|
203
|
+
const signatures = await connection.getSignaturesForAddress(pubkey, { limit: safeLimit });
|
|
172
204
|
return signatures.map((sig) => ({
|
|
173
205
|
signature: sig.signature,
|
|
174
206
|
blockTime: sig.blockTime ?? void 0,
|
|
@@ -182,15 +214,222 @@ function lamportsToSol(lamports) {
|
|
|
182
214
|
return Number(lamports) / web3_js.LAMPORTS_PER_SOL;
|
|
183
215
|
}
|
|
184
216
|
function solToLamports(sol) {
|
|
217
|
+
if (!Number.isFinite(sol) || sol < 0) {
|
|
218
|
+
throw new Error("Invalid SOL amount");
|
|
219
|
+
}
|
|
185
220
|
return BigInt(Math.floor(sol * web3_js.LAMPORTS_PER_SOL));
|
|
186
221
|
}
|
|
222
|
+
|
|
223
|
+
// src/solana/spl.ts
|
|
224
|
+
var SIGNATURE_REGEX2 = /^[1-9A-HJ-NP-Za-km-z]{87,88}$/;
|
|
225
|
+
var WALLET_REGEX2 = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
|
|
226
|
+
function resolveMintAddress(asset, network) {
|
|
227
|
+
if (asset === "native") return null;
|
|
228
|
+
if (asset === "usdc") {
|
|
229
|
+
return network === "mainnet-beta" ? TOKEN_MINTS.USDC_MAINNET : TOKEN_MINTS.USDC_DEVNET;
|
|
230
|
+
}
|
|
231
|
+
if (asset === "usdt") {
|
|
232
|
+
return TOKEN_MINTS.USDT_MAINNET;
|
|
233
|
+
}
|
|
234
|
+
if (typeof asset === "object" && "mint" in asset) {
|
|
235
|
+
return asset.mint;
|
|
236
|
+
}
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
function getTokenDecimals(asset) {
|
|
240
|
+
if (asset === "native") return 9;
|
|
241
|
+
if (asset === "usdc" || asset === "usdt") return 6;
|
|
242
|
+
if (typeof asset === "object" && "decimals" in asset) {
|
|
243
|
+
return asset.decimals ?? 6;
|
|
244
|
+
}
|
|
245
|
+
return 6;
|
|
246
|
+
}
|
|
247
|
+
function parseSPLTransfer(transaction, expectedRecipient, expectedMint) {
|
|
248
|
+
const instructions = transaction.transaction.message.instructions;
|
|
249
|
+
for (const ix of instructions) {
|
|
250
|
+
if ("parsed" in ix && (ix.program === "spl-token" || ix.program === "spl-token-2022")) {
|
|
251
|
+
const parsed = ix.parsed;
|
|
252
|
+
if (parsed.type === "transfer" || parsed.type === "transferChecked") {
|
|
253
|
+
const amount = parsed.info.amount || parsed.info.tokenAmount?.amount;
|
|
254
|
+
if (amount && parsed.info.destination) {
|
|
255
|
+
return {
|
|
256
|
+
from: parsed.info.authority || parsed.info.source || "",
|
|
257
|
+
to: parsed.info.destination,
|
|
258
|
+
amount: BigInt(amount),
|
|
259
|
+
mint: parsed.info.mint || expectedMint
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (transaction.meta?.innerInstructions) {
|
|
266
|
+
for (const inner of transaction.meta.innerInstructions) {
|
|
267
|
+
for (const ix of inner.instructions) {
|
|
268
|
+
if ("parsed" in ix && (ix.program === "spl-token" || ix.program === "spl-token-2022")) {
|
|
269
|
+
const parsed = ix.parsed;
|
|
270
|
+
if (parsed.type === "transfer" || parsed.type === "transferChecked") {
|
|
271
|
+
const amount = parsed.info.amount || parsed.info.tokenAmount?.amount;
|
|
272
|
+
if (amount) {
|
|
273
|
+
return {
|
|
274
|
+
from: parsed.info.authority || parsed.info.source || "",
|
|
275
|
+
to: parsed.info.destination || "",
|
|
276
|
+
amount: BigInt(amount),
|
|
277
|
+
mint: parsed.info.mint || expectedMint
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
if (transaction.meta?.postTokenBalances && transaction.meta?.preTokenBalances) {
|
|
286
|
+
const preBalances = transaction.meta.preTokenBalances;
|
|
287
|
+
const postBalances = transaction.meta.postTokenBalances;
|
|
288
|
+
for (const post of postBalances) {
|
|
289
|
+
if (post.mint === expectedMint && post.owner === expectedRecipient) {
|
|
290
|
+
const pre = preBalances.find(
|
|
291
|
+
(p) => p.accountIndex === post.accountIndex
|
|
292
|
+
);
|
|
293
|
+
const preAmount = BigInt(pre?.uiTokenAmount?.amount || "0");
|
|
294
|
+
const postAmount = BigInt(post.uiTokenAmount?.amount || "0");
|
|
295
|
+
const transferred = postAmount - preAmount;
|
|
296
|
+
if (transferred > 0n) {
|
|
297
|
+
return {
|
|
298
|
+
from: "",
|
|
299
|
+
// Can't determine from balance changes
|
|
300
|
+
to: expectedRecipient,
|
|
301
|
+
amount: transferred,
|
|
302
|
+
mint: expectedMint
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
async function verifySPLPayment(params) {
|
|
311
|
+
const {
|
|
312
|
+
signature,
|
|
313
|
+
expectedRecipient,
|
|
314
|
+
expectedAmount,
|
|
315
|
+
asset,
|
|
316
|
+
clientConfig,
|
|
317
|
+
maxAgeSeconds = 300
|
|
318
|
+
} = params;
|
|
319
|
+
if (!SIGNATURE_REGEX2.test(signature)) {
|
|
320
|
+
return { valid: false, confirmed: false, signature, error: "Invalid signature format" };
|
|
321
|
+
}
|
|
322
|
+
if (!WALLET_REGEX2.test(expectedRecipient)) {
|
|
323
|
+
return { valid: false, confirmed: false, signature, error: "Invalid recipient address" };
|
|
324
|
+
}
|
|
325
|
+
const mintAddress = resolveMintAddress(asset, clientConfig.network);
|
|
326
|
+
if (!mintAddress) {
|
|
327
|
+
return { valid: false, confirmed: false, signature, error: "Invalid asset configuration" };
|
|
328
|
+
}
|
|
329
|
+
if (expectedAmount <= 0n) {
|
|
330
|
+
return { valid: false, confirmed: false, signature, error: "Invalid expected amount" };
|
|
331
|
+
}
|
|
332
|
+
const effectiveMaxAge = Math.min(Math.max(maxAgeSeconds, 60), 3600);
|
|
333
|
+
const connection = getConnection(clientConfig);
|
|
334
|
+
try {
|
|
335
|
+
const transaction = await connection.getParsedTransaction(signature, {
|
|
336
|
+
commitment: "confirmed",
|
|
337
|
+
maxSupportedTransactionVersion: 0
|
|
338
|
+
});
|
|
339
|
+
if (!transaction) {
|
|
340
|
+
return { valid: false, confirmed: false, signature, error: "Transaction not found" };
|
|
341
|
+
}
|
|
342
|
+
if (transaction.meta?.err) {
|
|
343
|
+
return { valid: false, confirmed: true, signature, error: "Transaction failed on-chain" };
|
|
344
|
+
}
|
|
345
|
+
if (transaction.blockTime) {
|
|
346
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
347
|
+
if (now - transaction.blockTime > effectiveMaxAge) {
|
|
348
|
+
return { valid: false, confirmed: true, signature, error: "Transaction too old" };
|
|
349
|
+
}
|
|
350
|
+
if (transaction.blockTime > now + 60) {
|
|
351
|
+
return { valid: false, confirmed: true, signature, error: "Invalid transaction time" };
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
const transfer = parseSPLTransfer(transaction, expectedRecipient, mintAddress);
|
|
355
|
+
if (!transfer) {
|
|
356
|
+
return {
|
|
357
|
+
valid: false,
|
|
358
|
+
confirmed: true,
|
|
359
|
+
signature,
|
|
360
|
+
error: "No valid token transfer to recipient found"
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
if (transfer.mint !== mintAddress) {
|
|
364
|
+
return {
|
|
365
|
+
valid: false,
|
|
366
|
+
confirmed: true,
|
|
367
|
+
signature,
|
|
368
|
+
error: "Token mint mismatch"
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
if (transfer.amount < expectedAmount) {
|
|
372
|
+
return {
|
|
373
|
+
valid: false,
|
|
374
|
+
confirmed: true,
|
|
375
|
+
signature,
|
|
376
|
+
from: transfer.from,
|
|
377
|
+
to: transfer.to,
|
|
378
|
+
mint: transfer.mint,
|
|
379
|
+
amount: transfer.amount,
|
|
380
|
+
error: "Insufficient payment amount"
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
return {
|
|
384
|
+
valid: true,
|
|
385
|
+
confirmed: true,
|
|
386
|
+
signature,
|
|
387
|
+
from: transfer.from,
|
|
388
|
+
to: transfer.to,
|
|
389
|
+
mint: transfer.mint,
|
|
390
|
+
amount: transfer.amount,
|
|
391
|
+
blockTime: transaction.blockTime ?? void 0,
|
|
392
|
+
slot: transaction.slot
|
|
393
|
+
};
|
|
394
|
+
} catch {
|
|
395
|
+
return { valid: false, confirmed: false, signature, error: "Verification failed" };
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
function isNativeAsset(asset) {
|
|
399
|
+
return asset === "native";
|
|
400
|
+
}
|
|
401
|
+
var MAX_ARTICLES_PER_SESSION = 100;
|
|
402
|
+
var MIN_SECRET_LENGTH = 32;
|
|
187
403
|
function getSecretKey(secret) {
|
|
188
|
-
if (secret
|
|
189
|
-
throw new Error("Session secret
|
|
404
|
+
if (!secret || typeof secret !== "string") {
|
|
405
|
+
throw new Error("Session secret is required");
|
|
406
|
+
}
|
|
407
|
+
if (secret.length < MIN_SECRET_LENGTH) {
|
|
408
|
+
throw new Error(`Session secret must be at least ${MIN_SECRET_LENGTH} characters`);
|
|
190
409
|
}
|
|
191
410
|
return new TextEncoder().encode(secret);
|
|
192
411
|
}
|
|
412
|
+
function validateWalletAddress(address) {
|
|
413
|
+
if (!address || typeof address !== "string") return false;
|
|
414
|
+
const base58Regex = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
|
|
415
|
+
return base58Regex.test(address);
|
|
416
|
+
}
|
|
417
|
+
function validateArticleId(articleId) {
|
|
418
|
+
if (!articleId || typeof articleId !== "string") return false;
|
|
419
|
+
if (articleId.length > 128) return false;
|
|
420
|
+
const safeIdRegex = /^[a-zA-Z0-9_-]+$/;
|
|
421
|
+
return safeIdRegex.test(articleId);
|
|
422
|
+
}
|
|
193
423
|
async function createSession(walletAddress, articleId, config, siteWide = false) {
|
|
424
|
+
if (!validateWalletAddress(walletAddress)) {
|
|
425
|
+
throw new Error("Invalid wallet address format");
|
|
426
|
+
}
|
|
427
|
+
if (!validateArticleId(articleId)) {
|
|
428
|
+
throw new Error("Invalid article ID format");
|
|
429
|
+
}
|
|
430
|
+
if (!config.durationHours || config.durationHours <= 0 || config.durationHours > 720) {
|
|
431
|
+
throw new Error("Session duration must be between 1 and 720 hours");
|
|
432
|
+
}
|
|
194
433
|
const sessionId = uuid.v4();
|
|
195
434
|
const now = Math.floor(Date.now() / 1e3);
|
|
196
435
|
const expiresAt = now + config.durationHours * 3600;
|
|
@@ -198,7 +437,7 @@ async function createSession(walletAddress, articleId, config, siteWide = false)
|
|
|
198
437
|
id: sessionId,
|
|
199
438
|
walletAddress,
|
|
200
439
|
unlockedArticles: [articleId],
|
|
201
|
-
siteWideUnlock: siteWide,
|
|
440
|
+
siteWideUnlock: Boolean(siteWide),
|
|
202
441
|
createdAt: now,
|
|
203
442
|
expiresAt
|
|
204
443
|
};
|
|
@@ -206,7 +445,7 @@ async function createSession(walletAddress, articleId, config, siteWide = false)
|
|
|
206
445
|
sub: walletAddress,
|
|
207
446
|
sid: sessionId,
|
|
208
447
|
articles: session.unlockedArticles,
|
|
209
|
-
siteWide,
|
|
448
|
+
siteWide: session.siteWideUnlock,
|
|
210
449
|
iat: now,
|
|
211
450
|
exp: expiresAt
|
|
212
451
|
};
|
|
@@ -214,30 +453,39 @@ async function createSession(walletAddress, articleId, config, siteWide = false)
|
|
|
214
453
|
return { token, session };
|
|
215
454
|
}
|
|
216
455
|
async function validateSession(token, secret) {
|
|
456
|
+
if (!token || typeof token !== "string") {
|
|
457
|
+
return { valid: false, reason: "Invalid token format" };
|
|
458
|
+
}
|
|
217
459
|
try {
|
|
218
460
|
const { payload } = await jose.jwtVerify(token, getSecretKey(secret));
|
|
219
461
|
const sessionPayload = payload;
|
|
462
|
+
if (!sessionPayload.sub || !sessionPayload.sid || !sessionPayload.exp) {
|
|
463
|
+
return { valid: false, reason: "Malformed session payload" };
|
|
464
|
+
}
|
|
220
465
|
const now = Math.floor(Date.now() / 1e3);
|
|
221
466
|
if (sessionPayload.exp < now) {
|
|
222
467
|
return { valid: false, reason: "Session expired" };
|
|
223
468
|
}
|
|
469
|
+
if (!validateWalletAddress(sessionPayload.sub)) {
|
|
470
|
+
return { valid: false, reason: "Invalid session data" };
|
|
471
|
+
}
|
|
224
472
|
const session = {
|
|
225
473
|
id: sessionPayload.sid,
|
|
226
474
|
walletAddress: sessionPayload.sub,
|
|
227
|
-
unlockedArticles: sessionPayload.articles,
|
|
228
|
-
siteWideUnlock: sessionPayload.siteWide,
|
|
229
|
-
createdAt: sessionPayload.iat,
|
|
475
|
+
unlockedArticles: Array.isArray(sessionPayload.articles) ? sessionPayload.articles : [],
|
|
476
|
+
siteWideUnlock: Boolean(sessionPayload.siteWide),
|
|
477
|
+
createdAt: sessionPayload.iat ?? 0,
|
|
230
478
|
expiresAt: sessionPayload.exp
|
|
231
479
|
};
|
|
232
480
|
return { valid: true, session };
|
|
233
481
|
} catch (error) {
|
|
234
|
-
return {
|
|
235
|
-
valid: false,
|
|
236
|
-
reason: error instanceof Error ? error.message : "Invalid session"
|
|
237
|
-
};
|
|
482
|
+
return { valid: false, reason: "Invalid session" };
|
|
238
483
|
}
|
|
239
484
|
}
|
|
240
485
|
async function addArticleToSession(token, articleId, secret) {
|
|
486
|
+
if (!validateArticleId(articleId)) {
|
|
487
|
+
return null;
|
|
488
|
+
}
|
|
241
489
|
const validation = await validateSession(token, secret);
|
|
242
490
|
if (!validation.valid || !validation.session) {
|
|
243
491
|
return null;
|
|
@@ -246,6 +494,9 @@ async function addArticleToSession(token, articleId, secret) {
|
|
|
246
494
|
if (session.unlockedArticles.includes(articleId)) {
|
|
247
495
|
return { token, session };
|
|
248
496
|
}
|
|
497
|
+
if (session.unlockedArticles.length >= MAX_ARTICLES_PER_SESSION) {
|
|
498
|
+
return null;
|
|
499
|
+
}
|
|
249
500
|
const updatedArticles = [...session.unlockedArticles, articleId];
|
|
250
501
|
const payload = {
|
|
251
502
|
sub: session.walletAddress,
|
|
@@ -262,6 +513,9 @@ async function addArticleToSession(token, articleId, secret) {
|
|
|
262
513
|
};
|
|
263
514
|
}
|
|
264
515
|
async function isArticleUnlocked(token, articleId, secret) {
|
|
516
|
+
if (!validateArticleId(articleId)) {
|
|
517
|
+
return false;
|
|
518
|
+
}
|
|
265
519
|
const validation = await validateSession(token, secret);
|
|
266
520
|
if (!validation.valid || !validation.session) {
|
|
267
521
|
return false;
|
|
@@ -273,20 +527,52 @@ async function isArticleUnlocked(token, articleId, secret) {
|
|
|
273
527
|
}
|
|
274
528
|
|
|
275
529
|
// src/x402/config.ts
|
|
530
|
+
var WALLET_REGEX3 = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
|
|
531
|
+
function sanitizeDisplayString(str, maxLength = 200) {
|
|
532
|
+
if (!str || typeof str !== "string") return "";
|
|
533
|
+
return str.slice(0, maxLength).replace(/[<>"'&]/g, "");
|
|
534
|
+
}
|
|
535
|
+
function isValidUrl(url) {
|
|
536
|
+
try {
|
|
537
|
+
const parsed = new URL(url);
|
|
538
|
+
return parsed.protocol === "http:" || parsed.protocol === "https:";
|
|
539
|
+
} catch {
|
|
540
|
+
return false;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
276
543
|
function buildPaymentRequirement(params) {
|
|
544
|
+
if (!WALLET_REGEX3.test(params.creatorWallet)) {
|
|
545
|
+
throw new Error("Invalid creator wallet address");
|
|
546
|
+
}
|
|
547
|
+
if (params.priceInLamports <= 0n) {
|
|
548
|
+
throw new Error("Price must be positive");
|
|
549
|
+
}
|
|
550
|
+
if (!isValidUrl(params.resourceUrl)) {
|
|
551
|
+
throw new Error("Invalid resource URL");
|
|
552
|
+
}
|
|
553
|
+
if (params.network !== "devnet" && params.network !== "mainnet-beta") {
|
|
554
|
+
throw new Error("Invalid network");
|
|
555
|
+
}
|
|
556
|
+
const timeout = params.maxTimeoutSeconds ?? 300;
|
|
557
|
+
if (timeout < 60 || timeout > 3600) {
|
|
558
|
+
throw new Error("Timeout must be between 60 and 3600 seconds");
|
|
559
|
+
}
|
|
277
560
|
const x402Network = toX402Network(params.network);
|
|
561
|
+
const safeTitle = sanitizeDisplayString(params.articleTitle, 200);
|
|
562
|
+
const safeArticleId = sanitizeDisplayString(params.articleId, 128);
|
|
278
563
|
return {
|
|
279
564
|
scheme: "exact",
|
|
280
565
|
network: x402Network,
|
|
281
566
|
maxAmountRequired: params.priceInLamports.toString(),
|
|
282
567
|
resource: params.resourceUrl,
|
|
283
|
-
description: `Unlock: ${
|
|
568
|
+
description: `Unlock: ${safeTitle}`,
|
|
284
569
|
mimeType: "text/html",
|
|
285
570
|
payTo: params.creatorWallet,
|
|
286
|
-
maxTimeoutSeconds:
|
|
571
|
+
maxTimeoutSeconds: timeout,
|
|
287
572
|
asset: "native",
|
|
288
573
|
extra: {
|
|
289
|
-
name:
|
|
574
|
+
name: safeTitle,
|
|
575
|
+
articleId: safeArticleId
|
|
290
576
|
}
|
|
291
577
|
};
|
|
292
578
|
}
|
|
@@ -294,7 +580,18 @@ function encodePaymentRequired(requirement) {
|
|
|
294
580
|
return Buffer.from(JSON.stringify(requirement)).toString("base64");
|
|
295
581
|
}
|
|
296
582
|
function decodePaymentRequired(encoded) {
|
|
297
|
-
|
|
583
|
+
if (!encoded || typeof encoded !== "string") {
|
|
584
|
+
throw new Error("Invalid encoded requirement");
|
|
585
|
+
}
|
|
586
|
+
if (encoded.length > 1e4) {
|
|
587
|
+
throw new Error("Encoded requirement too large");
|
|
588
|
+
}
|
|
589
|
+
try {
|
|
590
|
+
const decoded = Buffer.from(encoded, "base64").toString("utf-8");
|
|
591
|
+
return JSON.parse(decoded);
|
|
592
|
+
} catch {
|
|
593
|
+
throw new Error("Failed to decode payment requirement");
|
|
594
|
+
}
|
|
298
595
|
}
|
|
299
596
|
var X402_HEADERS = {
|
|
300
597
|
PAYMENT_REQUIRED: "X-Payment-Required",
|
|
@@ -302,12 +599,13 @@ var X402_HEADERS = {
|
|
|
302
599
|
PAYMENT_RESPONSE: "X-Payment-Response"
|
|
303
600
|
};
|
|
304
601
|
function create402ResponseBody(requirement) {
|
|
602
|
+
const assetStr = typeof requirement.asset === "string" ? requirement.asset : requirement.asset.mint;
|
|
305
603
|
return {
|
|
306
604
|
error: "Payment Required",
|
|
307
605
|
message: requirement.description,
|
|
308
606
|
price: {
|
|
309
607
|
amount: requirement.maxAmountRequired,
|
|
310
|
-
asset:
|
|
608
|
+
asset: assetStr,
|
|
311
609
|
network: requirement.network
|
|
312
610
|
}
|
|
313
611
|
};
|
|
@@ -322,23 +620,47 @@ function create402Headers(requirement) {
|
|
|
322
620
|
}
|
|
323
621
|
|
|
324
622
|
// src/x402/verification.ts
|
|
623
|
+
var SIGNATURE_REGEX3 = /^[1-9A-HJ-NP-Za-km-z]{87,88}$/;
|
|
325
624
|
async function verifyX402Payment(payload, requirement, clientConfig) {
|
|
326
|
-
if (!payload
|
|
327
|
-
return {
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
625
|
+
if (!payload || typeof payload !== "object") {
|
|
626
|
+
return { valid: false, invalidReason: "Invalid payload" };
|
|
627
|
+
}
|
|
628
|
+
const signature = payload.payload?.signature;
|
|
629
|
+
if (!signature || typeof signature !== "string") {
|
|
630
|
+
return { valid: false, invalidReason: "Missing transaction signature" };
|
|
631
|
+
}
|
|
632
|
+
if (!SIGNATURE_REGEX3.test(signature)) {
|
|
633
|
+
return { valid: false, invalidReason: "Invalid signature format" };
|
|
634
|
+
}
|
|
635
|
+
if (payload.x402Version !== 1) {
|
|
636
|
+
return { valid: false, invalidReason: "Unsupported x402 version" };
|
|
637
|
+
}
|
|
638
|
+
if (payload.scheme !== "exact") {
|
|
639
|
+
return { valid: false, invalidReason: "Unsupported payment scheme" };
|
|
331
640
|
}
|
|
332
641
|
if (payload.network !== requirement.network) {
|
|
333
642
|
return {
|
|
334
643
|
valid: false,
|
|
335
|
-
invalidReason: `Network mismatch: expected ${requirement.network}
|
|
644
|
+
invalidReason: `Network mismatch: expected ${requirement.network}`
|
|
336
645
|
};
|
|
337
646
|
}
|
|
647
|
+
const walletRegex = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
|
|
648
|
+
if (!walletRegex.test(requirement.payTo)) {
|
|
649
|
+
return { valid: false, invalidReason: "Invalid recipient configuration" };
|
|
650
|
+
}
|
|
651
|
+
let expectedAmount;
|
|
652
|
+
try {
|
|
653
|
+
expectedAmount = BigInt(requirement.maxAmountRequired);
|
|
654
|
+
if (expectedAmount <= 0n) {
|
|
655
|
+
return { valid: false, invalidReason: "Invalid payment amount" };
|
|
656
|
+
}
|
|
657
|
+
} catch {
|
|
658
|
+
return { valid: false, invalidReason: "Invalid payment amount format" };
|
|
659
|
+
}
|
|
338
660
|
const verification = await verifyPayment({
|
|
339
|
-
signature
|
|
661
|
+
signature,
|
|
340
662
|
expectedRecipient: requirement.payTo,
|
|
341
|
-
expectedAmount
|
|
663
|
+
expectedAmount,
|
|
342
664
|
maxAgeSeconds: requirement.maxTimeoutSeconds,
|
|
343
665
|
clientConfig
|
|
344
666
|
});
|
|
@@ -359,9 +681,19 @@ async function verifyX402Payment(payload, requirement, clientConfig) {
|
|
|
359
681
|
};
|
|
360
682
|
}
|
|
361
683
|
function parsePaymentHeader(header) {
|
|
684
|
+
if (!header || typeof header !== "string") {
|
|
685
|
+
return null;
|
|
686
|
+
}
|
|
687
|
+
if (header.length > 1e4) {
|
|
688
|
+
return null;
|
|
689
|
+
}
|
|
362
690
|
try {
|
|
363
691
|
const decoded = Buffer.from(header, "base64").toString("utf-8");
|
|
364
|
-
|
|
692
|
+
const parsed = JSON.parse(decoded);
|
|
693
|
+
if (!parsed || typeof parsed !== "object") {
|
|
694
|
+
return null;
|
|
695
|
+
}
|
|
696
|
+
return parsed;
|
|
365
697
|
} catch {
|
|
366
698
|
return null;
|
|
367
699
|
}
|
|
@@ -370,27 +702,457 @@ function encodePaymentResponse(response) {
|
|
|
370
702
|
return Buffer.from(JSON.stringify(response)).toString("base64");
|
|
371
703
|
}
|
|
372
704
|
|
|
705
|
+
// src/store/memory.ts
|
|
706
|
+
function createMemoryStore(options = {}) {
|
|
707
|
+
const { cleanupInterval = 6e4 } = options;
|
|
708
|
+
const store = /* @__PURE__ */ new Map();
|
|
709
|
+
const cleanupTimer = setInterval(() => {
|
|
710
|
+
const now = Date.now();
|
|
711
|
+
for (const [key, record] of store.entries()) {
|
|
712
|
+
if (record.expiresAt < now) {
|
|
713
|
+
store.delete(key);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}, cleanupInterval);
|
|
717
|
+
return {
|
|
718
|
+
async hasBeenUsed(signature) {
|
|
719
|
+
const record = store.get(signature);
|
|
720
|
+
if (!record) return false;
|
|
721
|
+
if (record.expiresAt < Date.now()) {
|
|
722
|
+
store.delete(signature);
|
|
723
|
+
return false;
|
|
724
|
+
}
|
|
725
|
+
return true;
|
|
726
|
+
},
|
|
727
|
+
async markAsUsed(signature, resourceId, expiresAt) {
|
|
728
|
+
store.set(signature, {
|
|
729
|
+
resourceId,
|
|
730
|
+
usedAt: Date.now(),
|
|
731
|
+
expiresAt: expiresAt.getTime()
|
|
732
|
+
});
|
|
733
|
+
},
|
|
734
|
+
async getUsage(signature) {
|
|
735
|
+
const record = store.get(signature);
|
|
736
|
+
if (!record) return null;
|
|
737
|
+
if (record.expiresAt < Date.now()) {
|
|
738
|
+
store.delete(signature);
|
|
739
|
+
return null;
|
|
740
|
+
}
|
|
741
|
+
return {
|
|
742
|
+
signature,
|
|
743
|
+
resourceId: record.resourceId,
|
|
744
|
+
usedAt: new Date(record.usedAt),
|
|
745
|
+
expiresAt: new Date(record.expiresAt),
|
|
746
|
+
walletAddress: record.walletAddress
|
|
747
|
+
};
|
|
748
|
+
},
|
|
749
|
+
/** Stop cleanup timer (for graceful shutdown) */
|
|
750
|
+
close() {
|
|
751
|
+
clearInterval(cleanupTimer);
|
|
752
|
+
store.clear();
|
|
753
|
+
}
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// src/store/redis.ts
|
|
758
|
+
function createRedisStore(options) {
|
|
759
|
+
const { client, keyPrefix = "micropay:sig:" } = options;
|
|
760
|
+
const buildKey = (signature) => `${keyPrefix}${signature}`;
|
|
761
|
+
return {
|
|
762
|
+
async hasBeenUsed(signature) {
|
|
763
|
+
const exists = await client.exists(buildKey(signature));
|
|
764
|
+
return exists > 0;
|
|
765
|
+
},
|
|
766
|
+
async markAsUsed(signature, resourceId, expiresAt) {
|
|
767
|
+
const key = buildKey(signature);
|
|
768
|
+
const ttl = Math.max(1, Math.floor((expiresAt.getTime() - Date.now()) / 1e3));
|
|
769
|
+
const record = {
|
|
770
|
+
signature,
|
|
771
|
+
resourceId,
|
|
772
|
+
usedAt: /* @__PURE__ */ new Date(),
|
|
773
|
+
expiresAt
|
|
774
|
+
};
|
|
775
|
+
if (client.setex) {
|
|
776
|
+
await client.setex(key, ttl, JSON.stringify(record));
|
|
777
|
+
} else {
|
|
778
|
+
await client.set(key, JSON.stringify(record), { EX: ttl });
|
|
779
|
+
}
|
|
780
|
+
},
|
|
781
|
+
async getUsage(signature) {
|
|
782
|
+
const data = await client.get(buildKey(signature));
|
|
783
|
+
if (!data) return null;
|
|
784
|
+
try {
|
|
785
|
+
const record = JSON.parse(data);
|
|
786
|
+
return {
|
|
787
|
+
...record,
|
|
788
|
+
usedAt: new Date(record.usedAt),
|
|
789
|
+
expiresAt: new Date(record.expiresAt)
|
|
790
|
+
};
|
|
791
|
+
} catch {
|
|
792
|
+
return null;
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// src/middleware/nextjs.ts
|
|
799
|
+
function matchesProtectedPath(path, patterns) {
|
|
800
|
+
for (const pattern of patterns) {
|
|
801
|
+
const regexPattern = pattern.replace(/\*\*/g, "{{DOUBLE_STAR}}").replace(/\*/g, "[^/]*").replace(/{{DOUBLE_STAR}}/g, ".*");
|
|
802
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
803
|
+
if (regex.test(path)) {
|
|
804
|
+
return true;
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
return false;
|
|
808
|
+
}
|
|
809
|
+
async function checkPaywallAccess(path, sessionToken, config) {
|
|
810
|
+
if (!matchesProtectedPath(path, config.protectedPaths)) {
|
|
811
|
+
return { allowed: true };
|
|
812
|
+
}
|
|
813
|
+
if (!sessionToken) {
|
|
814
|
+
return {
|
|
815
|
+
allowed: false,
|
|
816
|
+
reason: "No session token",
|
|
817
|
+
requiresPayment: true
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
const validation = await validateSession(sessionToken, config.sessionSecret);
|
|
821
|
+
if (!validation.valid || !validation.session) {
|
|
822
|
+
return {
|
|
823
|
+
allowed: false,
|
|
824
|
+
reason: validation.reason || "Invalid session",
|
|
825
|
+
requiresPayment: true
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
return {
|
|
829
|
+
allowed: true,
|
|
830
|
+
session: validation.session
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
function createPaywallMiddleware(config) {
|
|
834
|
+
const { cookieName = "x402_session" } = config;
|
|
835
|
+
return async function middleware(request) {
|
|
836
|
+
const url = new URL(request.url);
|
|
837
|
+
const path = url.pathname;
|
|
838
|
+
const cookieHeader = request.headers.get("cookie") || "";
|
|
839
|
+
const cookies = Object.fromEntries(
|
|
840
|
+
cookieHeader.split(";").map((c) => {
|
|
841
|
+
const [key, ...vals] = c.trim().split("=");
|
|
842
|
+
return [key, vals.join("=")];
|
|
843
|
+
})
|
|
844
|
+
);
|
|
845
|
+
const sessionToken = cookies[cookieName];
|
|
846
|
+
const result = await checkPaywallAccess(path, sessionToken, config);
|
|
847
|
+
if (!result.allowed && result.requiresPayment) {
|
|
848
|
+
const body = config.custom402Response ? config.custom402Response(path) : {
|
|
849
|
+
error: "Payment Required",
|
|
850
|
+
message: "This resource requires payment to access",
|
|
851
|
+
path
|
|
852
|
+
};
|
|
853
|
+
return new Response(JSON.stringify(body), {
|
|
854
|
+
status: 402,
|
|
855
|
+
headers: {
|
|
856
|
+
"Content-Type": "application/json"
|
|
857
|
+
}
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
return null;
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
function withPaywall(handler, options) {
|
|
864
|
+
const { sessionSecret, cookieName = "x402_session", articleId } = options;
|
|
865
|
+
return async function protectedHandler(request) {
|
|
866
|
+
const cookieHeader = request.headers.get("cookie") || "";
|
|
867
|
+
const cookies = Object.fromEntries(
|
|
868
|
+
cookieHeader.split(";").map((c) => {
|
|
869
|
+
const [key, ...vals] = c.trim().split("=");
|
|
870
|
+
return [key, vals.join("=")];
|
|
871
|
+
})
|
|
872
|
+
);
|
|
873
|
+
const sessionToken = cookies[cookieName];
|
|
874
|
+
if (!sessionToken) {
|
|
875
|
+
return new Response(
|
|
876
|
+
JSON.stringify({ error: "Payment Required", message: "No session token" }),
|
|
877
|
+
{ status: 402, headers: { "Content-Type": "application/json" } }
|
|
878
|
+
);
|
|
879
|
+
}
|
|
880
|
+
const validation = await validateSession(sessionToken, sessionSecret);
|
|
881
|
+
if (!validation.valid || !validation.session) {
|
|
882
|
+
return new Response(
|
|
883
|
+
JSON.stringify({ error: "Payment Required", message: validation.reason }),
|
|
884
|
+
{ status: 402, headers: { "Content-Type": "application/json" } }
|
|
885
|
+
);
|
|
886
|
+
}
|
|
887
|
+
if (articleId) {
|
|
888
|
+
const { session } = validation;
|
|
889
|
+
const hasAccess = session.siteWideUnlock || session.unlockedArticles.includes(articleId);
|
|
890
|
+
if (!hasAccess) {
|
|
891
|
+
return new Response(
|
|
892
|
+
JSON.stringify({ error: "Payment Required", message: "Article not unlocked" }),
|
|
893
|
+
{ status: 402, headers: { "Content-Type": "application/json" } }
|
|
894
|
+
);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
return handler(request, validation.session);
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// src/utils/retry.ts
|
|
902
|
+
function sleep(ms) {
|
|
903
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
904
|
+
}
|
|
905
|
+
function calculateDelay(attempt, options) {
|
|
906
|
+
const { baseDelay, maxDelay, jitter } = options;
|
|
907
|
+
let delay = baseDelay * Math.pow(2, attempt);
|
|
908
|
+
delay = Math.min(delay, maxDelay);
|
|
909
|
+
if (jitter) {
|
|
910
|
+
const jitterAmount = delay * 0.25;
|
|
911
|
+
delay += Math.random() * jitterAmount * 2 - jitterAmount;
|
|
912
|
+
}
|
|
913
|
+
return Math.floor(delay);
|
|
914
|
+
}
|
|
915
|
+
async function withRetry(fn, options = {}) {
|
|
916
|
+
const {
|
|
917
|
+
maxAttempts = 3,
|
|
918
|
+
baseDelay = 500,
|
|
919
|
+
maxDelay = 1e4,
|
|
920
|
+
jitter = true,
|
|
921
|
+
retryOn = () => true
|
|
922
|
+
} = options;
|
|
923
|
+
let lastError;
|
|
924
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
925
|
+
try {
|
|
926
|
+
return await fn();
|
|
927
|
+
} catch (error) {
|
|
928
|
+
lastError = error;
|
|
929
|
+
if (!retryOn(error)) {
|
|
930
|
+
throw error;
|
|
931
|
+
}
|
|
932
|
+
if (attempt < maxAttempts - 1) {
|
|
933
|
+
const delay = calculateDelay(attempt, {
|
|
934
|
+
baseDelay,
|
|
935
|
+
maxDelay,
|
|
936
|
+
jitter
|
|
937
|
+
});
|
|
938
|
+
await sleep(delay);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
throw lastError;
|
|
943
|
+
}
|
|
944
|
+
function isRetryableRPCError(error) {
|
|
945
|
+
if (error instanceof Error) {
|
|
946
|
+
const message = error.message.toLowerCase();
|
|
947
|
+
if (message.includes("429") || message.includes("rate limit")) {
|
|
948
|
+
return true;
|
|
949
|
+
}
|
|
950
|
+
if (message.includes("timeout") || message.includes("econnreset")) {
|
|
951
|
+
return true;
|
|
952
|
+
}
|
|
953
|
+
if (message.includes("503") || message.includes("502") || message.includes("500")) {
|
|
954
|
+
return true;
|
|
955
|
+
}
|
|
956
|
+
if (message.includes("blockhash not found") || message.includes("slot skipped")) {
|
|
957
|
+
return true;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
return false;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
// src/client/payment.ts
|
|
964
|
+
function buildSolanaPayUrl(params) {
|
|
965
|
+
const { recipient, amount, splToken, reference, label, message } = params;
|
|
966
|
+
const url = new URL(`solana:${recipient}`);
|
|
967
|
+
if (amount !== void 0) {
|
|
968
|
+
url.searchParams.set("amount", amount.toString());
|
|
969
|
+
}
|
|
970
|
+
if (splToken) {
|
|
971
|
+
url.searchParams.set("spl-token", splToken);
|
|
972
|
+
}
|
|
973
|
+
if (reference) {
|
|
974
|
+
url.searchParams.set("reference", reference);
|
|
975
|
+
}
|
|
976
|
+
if (label) {
|
|
977
|
+
url.searchParams.set("label", label);
|
|
978
|
+
}
|
|
979
|
+
if (message) {
|
|
980
|
+
url.searchParams.set("message", message);
|
|
981
|
+
}
|
|
982
|
+
return url.toString();
|
|
983
|
+
}
|
|
984
|
+
function createPaymentFlow(config) {
|
|
985
|
+
const { network, recipientWallet, amount, asset = "native", memo } = config;
|
|
986
|
+
let decimals = 9;
|
|
987
|
+
let mintAddress;
|
|
988
|
+
if (asset === "usdc") {
|
|
989
|
+
decimals = 6;
|
|
990
|
+
mintAddress = network === "mainnet-beta" ? TOKEN_MINTS.USDC_MAINNET : TOKEN_MINTS.USDC_DEVNET;
|
|
991
|
+
} else if (asset === "usdt") {
|
|
992
|
+
decimals = 6;
|
|
993
|
+
mintAddress = TOKEN_MINTS.USDT_MAINNET;
|
|
994
|
+
} else if (typeof asset === "object" && "mint" in asset) {
|
|
995
|
+
decimals = asset.decimals ?? 6;
|
|
996
|
+
mintAddress = asset.mint;
|
|
997
|
+
}
|
|
998
|
+
const naturalAmount = Number(amount) / Math.pow(10, decimals);
|
|
999
|
+
return {
|
|
1000
|
+
/** Get the payment configuration */
|
|
1001
|
+
getConfig: () => ({ ...config }),
|
|
1002
|
+
/** Get amount in natural display units (e.g., 0.01 SOL) */
|
|
1003
|
+
getDisplayAmount: () => naturalAmount,
|
|
1004
|
+
/** Get amount formatted with symbol */
|
|
1005
|
+
getFormattedAmount: () => {
|
|
1006
|
+
const symbol = asset === "native" ? "SOL" : asset === "usdc" ? "USDC" : asset === "usdt" ? "USDT" : "tokens";
|
|
1007
|
+
return `${naturalAmount.toFixed(decimals > 6 ? 4 : 2)} ${symbol}`;
|
|
1008
|
+
},
|
|
1009
|
+
/** Generate Solana Pay URL for QR codes */
|
|
1010
|
+
getSolanaPayUrl: (options = {}) => {
|
|
1011
|
+
return buildSolanaPayUrl({
|
|
1012
|
+
recipient: recipientWallet,
|
|
1013
|
+
amount: naturalAmount,
|
|
1014
|
+
splToken: mintAddress,
|
|
1015
|
+
label: options.label,
|
|
1016
|
+
reference: options.reference,
|
|
1017
|
+
message: memo
|
|
1018
|
+
});
|
|
1019
|
+
},
|
|
1020
|
+
/** Get the token mint address (undefined for native SOL) */
|
|
1021
|
+
getMintAddress: () => mintAddress,
|
|
1022
|
+
/** Check if this is a native SOL payment */
|
|
1023
|
+
isNativePayment: () => asset === "native",
|
|
1024
|
+
/** Get network information */
|
|
1025
|
+
getNetworkInfo: () => ({
|
|
1026
|
+
network,
|
|
1027
|
+
isMainnet: network === "mainnet-beta",
|
|
1028
|
+
explorerUrl: network === "mainnet-beta" ? "https://explorer.solana.com" : "https://explorer.solana.com?cluster=devnet"
|
|
1029
|
+
}),
|
|
1030
|
+
/** Build explorer URL for a transaction */
|
|
1031
|
+
getExplorerUrl: (signature) => {
|
|
1032
|
+
const baseUrl = "https://explorer.solana.com/tx";
|
|
1033
|
+
const cluster = network === "mainnet-beta" ? "" : "?cluster=devnet";
|
|
1034
|
+
return `${baseUrl}/${signature}${cluster}`;
|
|
1035
|
+
}
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
function createPaymentReference() {
|
|
1039
|
+
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
1040
|
+
return crypto.randomUUID();
|
|
1041
|
+
}
|
|
1042
|
+
return `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
// src/pricing/index.ts
|
|
1046
|
+
var cachedPrice = null;
|
|
1047
|
+
var CACHE_TTL_MS = 6e4;
|
|
1048
|
+
async function getSolPrice() {
|
|
1049
|
+
if (cachedPrice && Date.now() - cachedPrice.fetchedAt.getTime() < CACHE_TTL_MS) {
|
|
1050
|
+
return cachedPrice;
|
|
1051
|
+
}
|
|
1052
|
+
try {
|
|
1053
|
+
const response = await fetch(
|
|
1054
|
+
"https://api.coingecko.com/api/v3/simple/price?ids=solana&vs_currencies=usd",
|
|
1055
|
+
{
|
|
1056
|
+
headers: { "Accept": "application/json" },
|
|
1057
|
+
signal: AbortSignal.timeout(5e3)
|
|
1058
|
+
}
|
|
1059
|
+
);
|
|
1060
|
+
if (!response.ok) {
|
|
1061
|
+
throw new Error(`Price fetch failed: ${response.status}`);
|
|
1062
|
+
}
|
|
1063
|
+
const data = await response.json();
|
|
1064
|
+
if (!data.solana?.usd) {
|
|
1065
|
+
throw new Error("Invalid price response");
|
|
1066
|
+
}
|
|
1067
|
+
cachedPrice = {
|
|
1068
|
+
solPrice: data.solana.usd,
|
|
1069
|
+
fetchedAt: /* @__PURE__ */ new Date(),
|
|
1070
|
+
source: "coingecko"
|
|
1071
|
+
};
|
|
1072
|
+
return cachedPrice;
|
|
1073
|
+
} catch (error) {
|
|
1074
|
+
if (cachedPrice) {
|
|
1075
|
+
return cachedPrice;
|
|
1076
|
+
}
|
|
1077
|
+
return {
|
|
1078
|
+
solPrice: 150,
|
|
1079
|
+
// Fallback price
|
|
1080
|
+
fetchedAt: /* @__PURE__ */ new Date(),
|
|
1081
|
+
source: "fallback"
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
async function lamportsToUsd(lamports) {
|
|
1086
|
+
const { solPrice } = await getSolPrice();
|
|
1087
|
+
const sol = Number(lamports) / 1e9;
|
|
1088
|
+
return sol * solPrice;
|
|
1089
|
+
}
|
|
1090
|
+
async function usdToLamports(usd) {
|
|
1091
|
+
const { solPrice } = await getSolPrice();
|
|
1092
|
+
const sol = usd / solPrice;
|
|
1093
|
+
return BigInt(Math.floor(sol * 1e9));
|
|
1094
|
+
}
|
|
1095
|
+
async function formatPriceDisplay(lamports) {
|
|
1096
|
+
const { solPrice } = await getSolPrice();
|
|
1097
|
+
const sol = Number(lamports) / 1e9;
|
|
1098
|
+
const usd = sol * solPrice;
|
|
1099
|
+
return `${sol.toFixed(4)} SOL (~$${usd.toFixed(2)})`;
|
|
1100
|
+
}
|
|
1101
|
+
function formatPriceSync(lamports, solPrice) {
|
|
1102
|
+
const sol = Number(lamports) / 1e9;
|
|
1103
|
+
const usd = sol * solPrice;
|
|
1104
|
+
return {
|
|
1105
|
+
sol,
|
|
1106
|
+
usd,
|
|
1107
|
+
formatted: `${sol.toFixed(4)} SOL (~$${usd.toFixed(2)})`
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
function clearPriceCache() {
|
|
1111
|
+
cachedPrice = null;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
exports.TOKEN_MINTS = TOKEN_MINTS;
|
|
373
1115
|
exports.X402_HEADERS = X402_HEADERS;
|
|
374
1116
|
exports.addArticleToSession = addArticleToSession;
|
|
375
1117
|
exports.buildPaymentRequirement = buildPaymentRequirement;
|
|
1118
|
+
exports.buildSolanaPayUrl = buildSolanaPayUrl;
|
|
1119
|
+
exports.checkPaywallAccess = checkPaywallAccess;
|
|
1120
|
+
exports.clearPriceCache = clearPriceCache;
|
|
376
1121
|
exports.create402Headers = create402Headers;
|
|
377
1122
|
exports.create402ResponseBody = create402ResponseBody;
|
|
1123
|
+
exports.createMemoryStore = createMemoryStore;
|
|
1124
|
+
exports.createPaymentFlow = createPaymentFlow;
|
|
1125
|
+
exports.createPaymentReference = createPaymentReference;
|
|
1126
|
+
exports.createPaywallMiddleware = createPaywallMiddleware;
|
|
1127
|
+
exports.createRedisStore = createRedisStore;
|
|
378
1128
|
exports.createSession = createSession;
|
|
379
1129
|
exports.decodePaymentRequired = decodePaymentRequired;
|
|
380
1130
|
exports.encodePaymentRequired = encodePaymentRequired;
|
|
381
1131
|
exports.encodePaymentResponse = encodePaymentResponse;
|
|
1132
|
+
exports.formatPriceDisplay = formatPriceDisplay;
|
|
1133
|
+
exports.formatPriceSync = formatPriceSync;
|
|
382
1134
|
exports.getConnection = getConnection;
|
|
1135
|
+
exports.getSolPrice = getSolPrice;
|
|
1136
|
+
exports.getTokenDecimals = getTokenDecimals;
|
|
383
1137
|
exports.getWalletTransactions = getWalletTransactions;
|
|
384
1138
|
exports.isArticleUnlocked = isArticleUnlocked;
|
|
385
1139
|
exports.isMainnet = isMainnet;
|
|
1140
|
+
exports.isNativeAsset = isNativeAsset;
|
|
1141
|
+
exports.isRetryableRPCError = isRetryableRPCError;
|
|
386
1142
|
exports.lamportsToSol = lamportsToSol;
|
|
1143
|
+
exports.lamportsToUsd = lamportsToUsd;
|
|
387
1144
|
exports.parsePaymentHeader = parsePaymentHeader;
|
|
388
1145
|
exports.resetConnection = resetConnection;
|
|
1146
|
+
exports.resolveMintAddress = resolveMintAddress;
|
|
389
1147
|
exports.solToLamports = solToLamports;
|
|
390
1148
|
exports.toX402Network = toX402Network;
|
|
1149
|
+
exports.usdToLamports = usdToLamports;
|
|
391
1150
|
exports.validateSession = validateSession;
|
|
392
1151
|
exports.verifyPayment = verifyPayment;
|
|
1152
|
+
exports.verifySPLPayment = verifySPLPayment;
|
|
393
1153
|
exports.verifyX402Payment = verifyX402Payment;
|
|
394
1154
|
exports.waitForConfirmation = waitForConfirmation;
|
|
1155
|
+
exports.withPaywall = withPaywall;
|
|
1156
|
+
exports.withRetry = withRetry;
|
|
395
1157
|
//# sourceMappingURL=index.cjs.map
|
|
396
1158
|
//# sourceMappingURL=index.cjs.map
|