@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/session/index.js
CHANGED
|
@@ -2,13 +2,38 @@ import { SignJWT, jwtVerify } from 'jose';
|
|
|
2
2
|
import { v4 } from 'uuid';
|
|
3
3
|
|
|
4
4
|
// src/session/core.ts
|
|
5
|
+
var MAX_ARTICLES_PER_SESSION = 100;
|
|
6
|
+
var MIN_SECRET_LENGTH = 32;
|
|
5
7
|
function getSecretKey(secret) {
|
|
6
|
-
if (secret
|
|
7
|
-
throw new Error("Session secret
|
|
8
|
+
if (!secret || typeof secret !== "string") {
|
|
9
|
+
throw new Error("Session secret is required");
|
|
10
|
+
}
|
|
11
|
+
if (secret.length < MIN_SECRET_LENGTH) {
|
|
12
|
+
throw new Error(`Session secret must be at least ${MIN_SECRET_LENGTH} characters`);
|
|
8
13
|
}
|
|
9
14
|
return new TextEncoder().encode(secret);
|
|
10
15
|
}
|
|
16
|
+
function validateWalletAddress(address) {
|
|
17
|
+
if (!address || typeof address !== "string") return false;
|
|
18
|
+
const base58Regex = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
|
|
19
|
+
return base58Regex.test(address);
|
|
20
|
+
}
|
|
21
|
+
function validateArticleId(articleId) {
|
|
22
|
+
if (!articleId || typeof articleId !== "string") return false;
|
|
23
|
+
if (articleId.length > 128) return false;
|
|
24
|
+
const safeIdRegex = /^[a-zA-Z0-9_-]+$/;
|
|
25
|
+
return safeIdRegex.test(articleId);
|
|
26
|
+
}
|
|
11
27
|
async function createSession(walletAddress, articleId, config, siteWide = false) {
|
|
28
|
+
if (!validateWalletAddress(walletAddress)) {
|
|
29
|
+
throw new Error("Invalid wallet address format");
|
|
30
|
+
}
|
|
31
|
+
if (!validateArticleId(articleId)) {
|
|
32
|
+
throw new Error("Invalid article ID format");
|
|
33
|
+
}
|
|
34
|
+
if (!config.durationHours || config.durationHours <= 0 || config.durationHours > 720) {
|
|
35
|
+
throw new Error("Session duration must be between 1 and 720 hours");
|
|
36
|
+
}
|
|
12
37
|
const sessionId = v4();
|
|
13
38
|
const now = Math.floor(Date.now() / 1e3);
|
|
14
39
|
const expiresAt = now + config.durationHours * 3600;
|
|
@@ -16,7 +41,7 @@ async function createSession(walletAddress, articleId, config, siteWide = false)
|
|
|
16
41
|
id: sessionId,
|
|
17
42
|
walletAddress,
|
|
18
43
|
unlockedArticles: [articleId],
|
|
19
|
-
siteWideUnlock: siteWide,
|
|
44
|
+
siteWideUnlock: Boolean(siteWide),
|
|
20
45
|
createdAt: now,
|
|
21
46
|
expiresAt
|
|
22
47
|
};
|
|
@@ -24,7 +49,7 @@ async function createSession(walletAddress, articleId, config, siteWide = false)
|
|
|
24
49
|
sub: walletAddress,
|
|
25
50
|
sid: sessionId,
|
|
26
51
|
articles: session.unlockedArticles,
|
|
27
|
-
siteWide,
|
|
52
|
+
siteWide: session.siteWideUnlock,
|
|
28
53
|
iat: now,
|
|
29
54
|
exp: expiresAt
|
|
30
55
|
};
|
|
@@ -32,30 +57,39 @@ async function createSession(walletAddress, articleId, config, siteWide = false)
|
|
|
32
57
|
return { token, session };
|
|
33
58
|
}
|
|
34
59
|
async function validateSession(token, secret) {
|
|
60
|
+
if (!token || typeof token !== "string") {
|
|
61
|
+
return { valid: false, reason: "Invalid token format" };
|
|
62
|
+
}
|
|
35
63
|
try {
|
|
36
64
|
const { payload } = await jwtVerify(token, getSecretKey(secret));
|
|
37
65
|
const sessionPayload = payload;
|
|
66
|
+
if (!sessionPayload.sub || !sessionPayload.sid || !sessionPayload.exp) {
|
|
67
|
+
return { valid: false, reason: "Malformed session payload" };
|
|
68
|
+
}
|
|
38
69
|
const now = Math.floor(Date.now() / 1e3);
|
|
39
70
|
if (sessionPayload.exp < now) {
|
|
40
71
|
return { valid: false, reason: "Session expired" };
|
|
41
72
|
}
|
|
73
|
+
if (!validateWalletAddress(sessionPayload.sub)) {
|
|
74
|
+
return { valid: false, reason: "Invalid session data" };
|
|
75
|
+
}
|
|
42
76
|
const session = {
|
|
43
77
|
id: sessionPayload.sid,
|
|
44
78
|
walletAddress: sessionPayload.sub,
|
|
45
|
-
unlockedArticles: sessionPayload.articles,
|
|
46
|
-
siteWideUnlock: sessionPayload.siteWide,
|
|
47
|
-
createdAt: sessionPayload.iat,
|
|
79
|
+
unlockedArticles: Array.isArray(sessionPayload.articles) ? sessionPayload.articles : [],
|
|
80
|
+
siteWideUnlock: Boolean(sessionPayload.siteWide),
|
|
81
|
+
createdAt: sessionPayload.iat ?? 0,
|
|
48
82
|
expiresAt: sessionPayload.exp
|
|
49
83
|
};
|
|
50
84
|
return { valid: true, session };
|
|
51
85
|
} catch (error) {
|
|
52
|
-
return {
|
|
53
|
-
valid: false,
|
|
54
|
-
reason: error instanceof Error ? error.message : "Invalid session"
|
|
55
|
-
};
|
|
86
|
+
return { valid: false, reason: "Invalid session" };
|
|
56
87
|
}
|
|
57
88
|
}
|
|
58
89
|
async function addArticleToSession(token, articleId, secret) {
|
|
90
|
+
if (!validateArticleId(articleId)) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
59
93
|
const validation = await validateSession(token, secret);
|
|
60
94
|
if (!validation.valid || !validation.session) {
|
|
61
95
|
return null;
|
|
@@ -64,6 +98,9 @@ async function addArticleToSession(token, articleId, secret) {
|
|
|
64
98
|
if (session.unlockedArticles.includes(articleId)) {
|
|
65
99
|
return { token, session };
|
|
66
100
|
}
|
|
101
|
+
if (session.unlockedArticles.length >= MAX_ARTICLES_PER_SESSION) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
67
104
|
const updatedArticles = [...session.unlockedArticles, articleId];
|
|
68
105
|
const payload = {
|
|
69
106
|
sub: session.walletAddress,
|
|
@@ -80,6 +117,9 @@ async function addArticleToSession(token, articleId, secret) {
|
|
|
80
117
|
};
|
|
81
118
|
}
|
|
82
119
|
async function isArticleUnlocked(token, articleId, secret) {
|
|
120
|
+
if (!validateArticleId(articleId)) {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
83
123
|
const validation = await validateSession(token, secret);
|
|
84
124
|
if (!validation.valid || !validation.session) {
|
|
85
125
|
return false;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/session/core.ts"],"names":["uuidv4"],"mappings":"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","file":"index.js","sourcesContent":["// Session management with JWT (framework-agnostic core)\nimport { SignJWT, jwtVerify } from 'jose';\nimport { v4 as uuidv4 } from 'uuid';\nimport type { SessionData, SessionConfig, SessionValidation, SessionJWTPayload } from '../types';\n\n/**\n * Get the secret key for JWT signing\n */\nfunction getSecretKey(secret: string): Uint8Array {\n if (secret.length < 32) {\n throw new Error('Session secret must be at least 32 characters');\n }\n return new TextEncoder().encode(secret);\n}\n\n/**\n * Create a new session after successful payment\n */\nexport async function createSession(\n walletAddress: string,\n articleId: string,\n config: SessionConfig,\n siteWide: boolean = false\n): Promise<{ token: string; session: SessionData }> {\n const sessionId = uuidv4();\n const now = Math.floor(Date.now() / 1000);\n const expiresAt = now + (config.durationHours * 3600);\n\n const session: SessionData = {\n id: sessionId,\n walletAddress,\n unlockedArticles: [articleId],\n siteWideUnlock: siteWide,\n createdAt: now,\n expiresAt,\n };\n\n const payload: SessionJWTPayload = {\n sub: walletAddress,\n sid: sessionId,\n articles: session.unlockedArticles,\n siteWide,\n iat: now,\n exp: expiresAt,\n };\n\n const token = await new SignJWT(payload as unknown as Record<string, unknown>)\n .setProtectedHeader({ alg: 'HS256' })\n .setIssuedAt()\n .setExpirationTime(`${config.durationHours}h`)\n .sign(getSecretKey(config.secret));\n\n return { token, session };\n}\n\n/**\n * Validate an existing session token\n */\nexport async function validateSession(\n token: string,\n secret: string\n): Promise<SessionValidation> {\n try {\n const { payload } = await jwtVerify(token, getSecretKey(secret));\n const sessionPayload = payload as unknown as SessionJWTPayload;\n\n const now = Math.floor(Date.now() / 1000);\n if (sessionPayload.exp < now) {\n return { valid: false, reason: 'Session expired' };\n }\n\n const session: SessionData = {\n id: sessionPayload.sid,\n walletAddress: sessionPayload.sub,\n unlockedArticles: sessionPayload.articles,\n siteWideUnlock: sessionPayload.siteWide,\n createdAt: sessionPayload.iat,\n expiresAt: sessionPayload.exp,\n };\n\n return { valid: true, session };\n } catch (error) {\n return {\n valid: false,\n reason: error instanceof Error ? error.message : 'Invalid session',\n };\n }\n}\n\n/**\n * Add an article to an existing session\n */\nexport async function addArticleToSession(\n token: string,\n articleId: string,\n secret: string\n): Promise<{ token: string; session: SessionData } | null> {\n const validation = await validateSession(token, secret);\n if (!validation.valid || !validation.session) {\n return null;\n }\n\n const session = validation.session;\n\n // Already unlocked\n if (session.unlockedArticles.includes(articleId)) {\n return { token, session };\n }\n\n const updatedArticles = [...session.unlockedArticles, articleId];\n\n const payload: SessionJWTPayload = {\n sub: session.walletAddress,\n sid: session.id,\n articles: updatedArticles,\n siteWide: session.siteWideUnlock,\n iat: session.createdAt,\n exp: session.expiresAt,\n };\n\n const newToken = await new SignJWT(payload as unknown as Record<string, unknown>)\n .setProtectedHeader({ alg: 'HS256' })\n .sign(getSecretKey(secret));\n\n return {\n token: newToken,\n session: { ...session, unlockedArticles: updatedArticles },\n };\n}\n\n/**\n * Check if an article is unlocked for a session\n */\nexport async function isArticleUnlocked(\n token: string,\n articleId: string,\n secret: string\n): Promise<boolean> {\n const validation = await validateSession(token, secret);\n if (!validation.valid || !validation.session) {\n return false;\n }\n\n if (validation.session.siteWideUnlock) {\n return true;\n }\n\n return validation.session.unlockedArticles.includes(articleId);\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/session/core.ts"],"names":["uuidv4"],"mappings":"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","file":"index.js","sourcesContent":["// Session management with JWT (framework-agnostic core)\n// SECURITY: Uses jose library with HS256, constant-time validation, input sanitization\nimport { SignJWT, jwtVerify } from 'jose';\nimport { v4 as uuidv4 } from 'uuid';\nimport type { SessionData, SessionConfig, SessionValidation, SessionJWTPayload } from '../types';\n\n// Maximum articles per session to prevent unbounded growth\nconst MAX_ARTICLES_PER_SESSION = 100;\n\n// Minimum secret length for security\nconst MIN_SECRET_LENGTH = 32;\n\n/**\n * Get the secret key for JWT signing\n * SECURITY: Enforces minimum secret length\n */\nfunction getSecretKey(secret: string): Uint8Array {\n if (!secret || typeof secret !== 'string') {\n throw new Error('Session secret is required');\n }\n if (secret.length < MIN_SECRET_LENGTH) {\n throw new Error(`Session secret must be at least ${MIN_SECRET_LENGTH} characters`);\n }\n return new TextEncoder().encode(secret);\n}\n\n/**\n * Validate wallet address format (base58, 32-44 chars)\n * SECURITY: Prevents injection via wallet address field\n */\nfunction validateWalletAddress(address: string): boolean {\n if (!address || typeof address !== 'string') return false;\n // Solana addresses are base58, typically 32-44 characters\n const base58Regex = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;\n return base58Regex.test(address);\n}\n\n/**\n * Validate article ID format\n * SECURITY: Prevents injection via articleId field\n */\nfunction validateArticleId(articleId: string): boolean {\n if (!articleId || typeof articleId !== 'string') return false;\n // Allow alphanumeric, hyphens, underscores, max 128 chars\n if (articleId.length > 128) return false;\n const safeIdRegex = /^[a-zA-Z0-9_-]+$/;\n return safeIdRegex.test(articleId);\n}\n\n/**\n * Create a new session after successful payment\n * SECURITY: Validates inputs, enforces limits\n */\nexport async function createSession(\n walletAddress: string,\n articleId: string,\n config: SessionConfig,\n siteWide: boolean = false\n): Promise<{ token: string; session: SessionData }> {\n // Input validation\n if (!validateWalletAddress(walletAddress)) {\n throw new Error('Invalid wallet address format');\n }\n if (!validateArticleId(articleId)) {\n throw new Error('Invalid article ID format');\n }\n if (!config.durationHours || config.durationHours <= 0 || config.durationHours > 720) {\n throw new Error('Session duration must be between 1 and 720 hours');\n }\n\n const sessionId = uuidv4();\n const now = Math.floor(Date.now() / 1000);\n const expiresAt = now + (config.durationHours * 3600);\n\n const session: SessionData = {\n id: sessionId,\n walletAddress,\n unlockedArticles: [articleId],\n siteWideUnlock: Boolean(siteWide),\n createdAt: now,\n expiresAt,\n };\n\n const payload: SessionJWTPayload = {\n sub: walletAddress,\n sid: sessionId,\n articles: session.unlockedArticles,\n siteWide: session.siteWideUnlock,\n iat: now,\n exp: expiresAt,\n };\n\n const token = await new SignJWT(payload as unknown as Record<string, unknown>)\n .setProtectedHeader({ alg: 'HS256' })\n .setIssuedAt()\n .setExpirationTime(`${config.durationHours}h`)\n .sign(getSecretKey(config.secret));\n\n return { token, session };\n}\n\n/**\n * Validate an existing session token\n * SECURITY: jose library handles timing-safe comparison internally\n */\nexport async function validateSession(\n token: string,\n secret: string\n): Promise<SessionValidation> {\n // Input validation\n if (!token || typeof token !== 'string') {\n return { valid: false, reason: 'Invalid token format' };\n }\n\n try {\n const { payload } = await jwtVerify(token, getSecretKey(secret));\n const sessionPayload = payload as unknown as SessionJWTPayload;\n\n // Validate required fields exist\n if (!sessionPayload.sub || !sessionPayload.sid || !sessionPayload.exp) {\n return { valid: false, reason: 'Malformed session payload' };\n }\n\n // Check expiration (jose already checks, but double-check)\n const now = Math.floor(Date.now() / 1000);\n if (sessionPayload.exp < now) {\n return { valid: false, reason: 'Session expired' };\n }\n\n // Validate wallet address format from token\n if (!validateWalletAddress(sessionPayload.sub)) {\n return { valid: false, reason: 'Invalid session data' };\n }\n\n const session: SessionData = {\n id: sessionPayload.sid,\n walletAddress: sessionPayload.sub,\n unlockedArticles: Array.isArray(sessionPayload.articles) ? sessionPayload.articles : [],\n siteWideUnlock: Boolean(sessionPayload.siteWide),\n createdAt: sessionPayload.iat ?? 0,\n expiresAt: sessionPayload.exp,\n };\n\n return { valid: true, session };\n } catch (error) {\n // SECURITY: Don't expose internal error details\n return { valid: false, reason: 'Invalid session' };\n }\n}\n\n/**\n * Add an article to an existing session\n * SECURITY: Enforces article limit to prevent token bloat\n */\nexport async function addArticleToSession(\n token: string,\n articleId: string,\n secret: string\n): Promise<{ token: string; session: SessionData } | null> {\n // Validate article ID\n if (!validateArticleId(articleId)) {\n return null;\n }\n\n const validation = await validateSession(token, secret);\n if (!validation.valid || !validation.session) {\n return null;\n }\n\n const session = validation.session;\n\n // Already unlocked\n if (session.unlockedArticles.includes(articleId)) {\n return { token, session };\n }\n\n // SECURITY: Enforce maximum articles per session\n if (session.unlockedArticles.length >= MAX_ARTICLES_PER_SESSION) {\n return null;\n }\n\n const updatedArticles = [...session.unlockedArticles, articleId];\n\n const payload: SessionJWTPayload = {\n sub: session.walletAddress,\n sid: session.id,\n articles: updatedArticles,\n siteWide: session.siteWideUnlock,\n iat: session.createdAt,\n exp: session.expiresAt,\n };\n\n const newToken = await new SignJWT(payload as unknown as Record<string, unknown>)\n .setProtectedHeader({ alg: 'HS256' })\n .sign(getSecretKey(secret));\n\n return {\n token: newToken,\n session: { ...session, unlockedArticles: updatedArticles },\n };\n}\n\n/**\n * Check if an article is unlocked for a session\n */\nexport async function isArticleUnlocked(\n token: string,\n articleId: string,\n secret: string\n): Promise<boolean> {\n if (!validateArticleId(articleId)) {\n return false;\n }\n\n const validation = await validateSession(token, secret);\n if (!validation.valid || !validation.session) {\n return false;\n }\n\n if (validation.session.siteWideUnlock) {\n return true;\n }\n\n return validation.session.unlockedArticles.includes(articleId);\n}\n"]}
|
|
@@ -45,27 +45,4 @@ interface SessionJWTPayload {
|
|
|
45
45
|
exp: number;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
* Create a new session after successful payment
|
|
50
|
-
*/
|
|
51
|
-
declare function createSession(walletAddress: string, articleId: string, config: SessionConfig, siteWide?: boolean): Promise<{
|
|
52
|
-
token: string;
|
|
53
|
-
session: SessionData;
|
|
54
|
-
}>;
|
|
55
|
-
/**
|
|
56
|
-
* Validate an existing session token
|
|
57
|
-
*/
|
|
58
|
-
declare function validateSession(token: string, secret: string): Promise<SessionValidation>;
|
|
59
|
-
/**
|
|
60
|
-
* Add an article to an existing session
|
|
61
|
-
*/
|
|
62
|
-
declare function addArticleToSession(token: string, articleId: string, secret: string): Promise<{
|
|
63
|
-
token: string;
|
|
64
|
-
session: SessionData;
|
|
65
|
-
} | null>;
|
|
66
|
-
/**
|
|
67
|
-
* Check if an article is unlocked for a session
|
|
68
|
-
*/
|
|
69
|
-
declare function isArticleUnlocked(token: string, articleId: string, secret: string): Promise<boolean>;
|
|
70
|
-
|
|
71
|
-
export { type SessionData as S, type SessionConfig as a, type SessionValidation as b, type SessionJWTPayload as c, createSession as d, addArticleToSession as e, isArticleUnlocked as i, validateSession as v };
|
|
48
|
+
export type { SessionData as S, SessionConfig as a, SessionValidation as b, SessionJWTPayload as c };
|
|
@@ -45,27 +45,4 @@ interface SessionJWTPayload {
|
|
|
45
45
|
exp: number;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
* Create a new session after successful payment
|
|
50
|
-
*/
|
|
51
|
-
declare function createSession(walletAddress: string, articleId: string, config: SessionConfig, siteWide?: boolean): Promise<{
|
|
52
|
-
token: string;
|
|
53
|
-
session: SessionData;
|
|
54
|
-
}>;
|
|
55
|
-
/**
|
|
56
|
-
* Validate an existing session token
|
|
57
|
-
*/
|
|
58
|
-
declare function validateSession(token: string, secret: string): Promise<SessionValidation>;
|
|
59
|
-
/**
|
|
60
|
-
* Add an article to an existing session
|
|
61
|
-
*/
|
|
62
|
-
declare function addArticleToSession(token: string, articleId: string, secret: string): Promise<{
|
|
63
|
-
token: string;
|
|
64
|
-
session: SessionData;
|
|
65
|
-
} | null>;
|
|
66
|
-
/**
|
|
67
|
-
* Check if an article is unlocked for a session
|
|
68
|
-
*/
|
|
69
|
-
declare function isArticleUnlocked(token: string, articleId: string, secret: string): Promise<boolean>;
|
|
70
|
-
|
|
71
|
-
export { type SessionData as S, type SessionConfig as a, type SessionValidation as b, type SessionJWTPayload as c, createSession as d, addArticleToSession as e, isArticleUnlocked as i, validateSession as v };
|
|
48
|
+
export type { SessionData as S, SessionConfig as a, SessionValidation as b, SessionJWTPayload as c };
|
package/dist/solana/index.cjs
CHANGED
|
@@ -42,6 +42,16 @@ function isMainnet(network) {
|
|
|
42
42
|
function toX402Network(network) {
|
|
43
43
|
return network === "mainnet-beta" ? "solana-mainnet" : "solana-devnet";
|
|
44
44
|
}
|
|
45
|
+
var SIGNATURE_REGEX = /^[1-9A-HJ-NP-Za-km-z]{87,88}$/;
|
|
46
|
+
var WALLET_REGEX = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
|
|
47
|
+
function isValidSignature(signature) {
|
|
48
|
+
if (!signature || typeof signature !== "string") return false;
|
|
49
|
+
return SIGNATURE_REGEX.test(signature);
|
|
50
|
+
}
|
|
51
|
+
function isValidWalletAddress(address) {
|
|
52
|
+
if (!address || typeof address !== "string") return false;
|
|
53
|
+
return WALLET_REGEX.test(address);
|
|
54
|
+
}
|
|
45
55
|
function parseSOLTransfer(transaction, expectedRecipient) {
|
|
46
56
|
const instructions = transaction.transaction.message.instructions;
|
|
47
57
|
for (const ix of instructions) {
|
|
@@ -82,6 +92,16 @@ async function verifyPayment(params) {
|
|
|
82
92
|
maxAgeSeconds = 300,
|
|
83
93
|
clientConfig
|
|
84
94
|
} = params;
|
|
95
|
+
if (!isValidSignature(signature)) {
|
|
96
|
+
return { valid: false, confirmed: false, signature, error: "Invalid signature format" };
|
|
97
|
+
}
|
|
98
|
+
if (!isValidWalletAddress(expectedRecipient)) {
|
|
99
|
+
return { valid: false, confirmed: false, signature, error: "Invalid recipient address" };
|
|
100
|
+
}
|
|
101
|
+
if (expectedAmount <= 0n) {
|
|
102
|
+
return { valid: false, confirmed: false, signature, error: "Invalid expected amount" };
|
|
103
|
+
}
|
|
104
|
+
const effectiveMaxAge = Math.min(Math.max(maxAgeSeconds, 60), 3600);
|
|
85
105
|
const connection = getConnection(clientConfig);
|
|
86
106
|
try {
|
|
87
107
|
const transaction = await connection.getParsedTransaction(signature, {
|
|
@@ -96,14 +116,17 @@ async function verifyPayment(params) {
|
|
|
96
116
|
valid: false,
|
|
97
117
|
confirmed: true,
|
|
98
118
|
signature,
|
|
99
|
-
error:
|
|
119
|
+
error: "Transaction failed on-chain"
|
|
100
120
|
};
|
|
101
121
|
}
|
|
102
122
|
if (transaction.blockTime) {
|
|
103
123
|
const now = Math.floor(Date.now() / 1e3);
|
|
104
|
-
if (now - transaction.blockTime >
|
|
124
|
+
if (now - transaction.blockTime > effectiveMaxAge) {
|
|
105
125
|
return { valid: false, confirmed: true, signature, error: "Transaction too old" };
|
|
106
126
|
}
|
|
127
|
+
if (transaction.blockTime > now + 60) {
|
|
128
|
+
return { valid: false, confirmed: true, signature, error: "Invalid transaction time" };
|
|
129
|
+
}
|
|
107
130
|
}
|
|
108
131
|
const transferDetails = parseSOLTransfer(transaction, expectedRecipient);
|
|
109
132
|
if (!transferDetails) {
|
|
@@ -122,7 +145,7 @@ async function verifyPayment(params) {
|
|
|
122
145
|
from: transferDetails.from,
|
|
123
146
|
to: transferDetails.to,
|
|
124
147
|
amount: transferDetails.amount,
|
|
125
|
-
error:
|
|
148
|
+
error: "Insufficient payment amount"
|
|
126
149
|
};
|
|
127
150
|
}
|
|
128
151
|
return {
|
|
@@ -140,33 +163,34 @@ async function verifyPayment(params) {
|
|
|
140
163
|
valid: false,
|
|
141
164
|
confirmed: false,
|
|
142
165
|
signature,
|
|
143
|
-
error:
|
|
166
|
+
error: "Verification failed"
|
|
144
167
|
};
|
|
145
168
|
}
|
|
146
169
|
}
|
|
147
170
|
async function waitForConfirmation(signature, clientConfig) {
|
|
171
|
+
if (!isValidSignature(signature)) {
|
|
172
|
+
return { confirmed: false, error: "Invalid signature format" };
|
|
173
|
+
}
|
|
148
174
|
const connection = getConnection(clientConfig);
|
|
149
175
|
try {
|
|
150
176
|
const confirmation = await connection.confirmTransaction(signature, "confirmed");
|
|
151
177
|
if (confirmation.value.err) {
|
|
152
|
-
return {
|
|
153
|
-
confirmed: false,
|
|
154
|
-
error: `Transaction failed: ${JSON.stringify(confirmation.value.err)}`
|
|
155
|
-
};
|
|
178
|
+
return { confirmed: false, error: "Transaction failed" };
|
|
156
179
|
}
|
|
157
180
|
return { confirmed: true, slot: confirmation.context?.slot };
|
|
158
|
-
} catch
|
|
159
|
-
return {
|
|
160
|
-
confirmed: false,
|
|
161
|
-
error: error instanceof Error ? error.message : "Confirmation timeout"
|
|
162
|
-
};
|
|
181
|
+
} catch {
|
|
182
|
+
return { confirmed: false, error: "Confirmation timeout" };
|
|
163
183
|
}
|
|
164
184
|
}
|
|
165
185
|
async function getWalletTransactions(walletAddress, clientConfig, limit = 20) {
|
|
186
|
+
if (!isValidWalletAddress(walletAddress)) {
|
|
187
|
+
return [];
|
|
188
|
+
}
|
|
189
|
+
const safeLimit = Math.min(Math.max(limit, 1), 100);
|
|
166
190
|
const connection = getConnection(clientConfig);
|
|
167
|
-
const pubkey = new web3_js.PublicKey(walletAddress);
|
|
168
191
|
try {
|
|
169
|
-
const
|
|
192
|
+
const pubkey = new web3_js.PublicKey(walletAddress);
|
|
193
|
+
const signatures = await connection.getSignaturesForAddress(pubkey, { limit: safeLimit });
|
|
170
194
|
return signatures.map((sig) => ({
|
|
171
195
|
signature: sig.signature,
|
|
172
196
|
blockTime: sig.blockTime ?? void 0,
|
|
@@ -180,17 +204,213 @@ function lamportsToSol(lamports) {
|
|
|
180
204
|
return Number(lamports) / web3_js.LAMPORTS_PER_SOL;
|
|
181
205
|
}
|
|
182
206
|
function solToLamports(sol) {
|
|
207
|
+
if (!Number.isFinite(sol) || sol < 0) {
|
|
208
|
+
throw new Error("Invalid SOL amount");
|
|
209
|
+
}
|
|
183
210
|
return BigInt(Math.floor(sol * web3_js.LAMPORTS_PER_SOL));
|
|
184
211
|
}
|
|
185
212
|
|
|
213
|
+
// src/types/payment.ts
|
|
214
|
+
var TOKEN_MINTS = {
|
|
215
|
+
/** USDC on mainnet */
|
|
216
|
+
USDC_MAINNET: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
|
|
217
|
+
/** USDC on devnet */
|
|
218
|
+
USDC_DEVNET: "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
|
|
219
|
+
/** USDT on mainnet */
|
|
220
|
+
USDT_MAINNET: "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB"
|
|
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
|
+
|
|
186
402
|
exports.getConnection = getConnection;
|
|
403
|
+
exports.getTokenDecimals = getTokenDecimals;
|
|
187
404
|
exports.getWalletTransactions = getWalletTransactions;
|
|
188
405
|
exports.isMainnet = isMainnet;
|
|
406
|
+
exports.isNativeAsset = isNativeAsset;
|
|
189
407
|
exports.lamportsToSol = lamportsToSol;
|
|
190
408
|
exports.resetConnection = resetConnection;
|
|
409
|
+
exports.resolveMintAddress = resolveMintAddress;
|
|
191
410
|
exports.solToLamports = solToLamports;
|
|
192
411
|
exports.toX402Network = toX402Network;
|
|
193
412
|
exports.verifyPayment = verifyPayment;
|
|
413
|
+
exports.verifySPLPayment = verifySPLPayment;
|
|
194
414
|
exports.waitForConfirmation = waitForConfirmation;
|
|
195
415
|
//# sourceMappingURL=index.cjs.map
|
|
196
416
|
//# sourceMappingURL=index.cjs.map
|