@alleyboss/micropay-solana-x402-paywall 2.1.1 → 2.2.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 +32 -17
- package/dist/client/index.cjs.map +1 -1
- package/dist/client/index.d.cts +1 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.js.map +1 -1
- package/dist/{client-Blkcv17a.d.cts → client-D-dteoJw.d.cts} +1 -1
- package/dist/{client-xhJSoeOH.d.ts → client-DfCIRrNG.d.ts} +1 -1
- package/dist/index.cjs +31 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +30 -4
- package/dist/index.js.map +1 -1
- package/dist/middleware/index.cjs +15 -3
- package/dist/middleware/index.cjs.map +1 -1
- package/dist/middleware/index.d.cts +2 -1
- package/dist/middleware/index.d.ts +2 -1
- package/dist/middleware/index.js +15 -3
- package/dist/middleware/index.js.map +1 -1
- package/dist/{nextjs-Bm272Jkj.d.cts → nextjs-BDyOqGAq.d.cts} +4 -1
- package/dist/{nextjs-BK0pVb9Y.d.ts → nextjs-CbX8_9yK.d.ts} +4 -1
- package/dist/{payment-CTxdtqmc.d.cts → payment-BGp7eMQl.d.cts} +2 -0
- package/dist/{payment-CTxdtqmc.d.ts → payment-BGp7eMQl.d.ts} +2 -0
- package/dist/solana/index.cjs.map +1 -1
- package/dist/solana/index.d.cts +3 -3
- package/dist/solana/index.d.ts +3 -3
- package/dist/solana/index.js.map +1 -1
- package/dist/x402/index.cjs +22 -0
- package/dist/x402/index.cjs.map +1 -1
- package/dist/x402/index.d.cts +25 -5
- package/dist/x402/index.d.ts +25 -5
- package/dist/x402/index.js +21 -1
- package/dist/x402/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
var jose = require('jose');
|
|
4
4
|
require('uuid');
|
|
5
|
+
require('@solana/web3.js');
|
|
5
6
|
|
|
6
7
|
// src/session/core.ts
|
|
7
8
|
var MIN_SECRET_LENGTH = 32;
|
|
@@ -50,6 +51,11 @@ async function validateSession(token, secret) {
|
|
|
50
51
|
}
|
|
51
52
|
}
|
|
52
53
|
|
|
54
|
+
// src/x402/verification.ts
|
|
55
|
+
function encodePaymentRequirement(requirement) {
|
|
56
|
+
return Buffer.from(JSON.stringify(requirement)).toString("base64");
|
|
57
|
+
}
|
|
58
|
+
|
|
53
59
|
// src/middleware/nextjs.ts
|
|
54
60
|
function matchesProtectedPath(path, patterns) {
|
|
55
61
|
for (const pattern of patterns) {
|
|
@@ -100,16 +106,22 @@ function createPaywallMiddleware(config) {
|
|
|
100
106
|
const sessionToken = cookies[cookieName];
|
|
101
107
|
const result = await checkPaywallAccess(path, sessionToken, config);
|
|
102
108
|
if (!result.allowed && result.requiresPayment) {
|
|
109
|
+
const headers = {
|
|
110
|
+
"Content-Type": "application/json"
|
|
111
|
+
};
|
|
112
|
+
if (config.paymentRequirement) {
|
|
113
|
+
const requirement = typeof config.paymentRequirement === "function" ? config.paymentRequirement(path) : config.paymentRequirement;
|
|
114
|
+
headers["X-Payment-Required"] = encodePaymentRequirement(requirement);
|
|
115
|
+
}
|
|
103
116
|
const body = config.custom402Response ? config.custom402Response(path) : {
|
|
104
117
|
error: "Payment Required",
|
|
105
118
|
message: "This resource requires payment to access",
|
|
119
|
+
x402Version: 1,
|
|
106
120
|
path
|
|
107
121
|
};
|
|
108
122
|
return new Response(JSON.stringify(body), {
|
|
109
123
|
status: 402,
|
|
110
|
-
headers
|
|
111
|
-
"Content-Type": "application/json"
|
|
112
|
-
}
|
|
124
|
+
headers
|
|
113
125
|
});
|
|
114
126
|
}
|
|
115
127
|
return null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/session/core.ts","../../src/middleware/nextjs.ts","../../src/middleware/express.ts"],"names":["jwtVerify"],"mappings":"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","file":"index.cjs","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","// Next.js Middleware for Paywall\n// Zero-boilerplate integration for protected routes\n\nimport type { SignatureStore } from '../store';\nimport type { SessionData } from '../types';\nimport { validateSession } from '../session';\n\n/**\n * Configuration for paywall middleware\n */\nexport interface PaywallMiddlewareConfig {\n /** Session secret for JWT validation */\n sessionSecret: string;\n /** Protected path patterns (glob-like) */\n protectedPaths: string[];\n /** Cookie name for session token */\n cookieName?: string;\n /** Optional signature store for anti-replay */\n signatureStore?: SignatureStore;\n /** Custom 402 response body */\n custom402Response?: (path: string) => object;\n}\n\n/**\n * Result of middleware check\n */\nexport interface MiddlewareResult {\n /** Whether access is allowed */\n allowed: boolean;\n /** Session data if valid */\n session?: SessionData;\n /** Reason for denial */\n reason?: string;\n /** Should respond with 402 */\n requiresPayment?: boolean;\n}\n\n/**\n * Check if path matches any protected pattern\n */\nfunction matchesProtectedPath(path: string, patterns: string[]): boolean {\n for (const pattern of patterns) {\n // Simple glob matching: * matches anything, ** matches path segments\n const regexPattern = pattern\n .replace(/\\*\\*/g, '{{DOUBLE_STAR}}')\n .replace(/\\*/g, '[^/]*')\n .replace(/{{DOUBLE_STAR}}/g, '.*');\n\n const regex = new RegExp(`^${regexPattern}$`);\n if (regex.test(path)) {\n return true;\n }\n }\n return false;\n}\n\n/**\n * Check paywall access for a request\n * Framework-agnostic core logic\n */\nexport async function checkPaywallAccess(\n path: string,\n sessionToken: string | undefined,\n config: PaywallMiddlewareConfig\n): Promise<MiddlewareResult> {\n // Check if path is protected\n if (!matchesProtectedPath(path, config.protectedPaths)) {\n return { allowed: true };\n }\n\n // No session token\n if (!sessionToken) {\n return {\n allowed: false,\n reason: 'No session token',\n requiresPayment: true,\n };\n }\n\n // Validate session\n const validation = await validateSession(sessionToken, config.sessionSecret);\n\n if (!validation.valid || !validation.session) {\n return {\n allowed: false,\n reason: validation.reason || 'Invalid session',\n requiresPayment: true,\n };\n }\n\n return {\n allowed: true,\n session: validation.session,\n };\n}\n\n/**\n * Create Next.js middleware handler\n * \n * @example\n * ```typescript\n * // middleware.ts\n * import { createPaywallMiddleware } from '@alleyboss/micropay-solana-x402-paywall/middleware';\n * \n * export const middleware = createPaywallMiddleware({\n * sessionSecret: process.env.SESSION_SECRET!,\n * protectedPaths: ['/api/premium/*', '/api/content/*'],\n * });\n * \n * export const config = { matcher: ['/api/premium/:path*', '/api/content/:path*'] };\n * ```\n */\nexport function createPaywallMiddleware(config: PaywallMiddlewareConfig) {\n const { cookieName = 'x402_session' } = config;\n\n return async function middleware(request: Request): Promise<Response | null> {\n const url = new URL(request.url);\n const path = url.pathname;\n\n // Get session from cookie header\n const cookieHeader = request.headers.get('cookie') || '';\n const cookies = Object.fromEntries(\n cookieHeader.split(';').map(c => {\n const [key, ...vals] = c.trim().split('=');\n return [key, vals.join('=')];\n })\n );\n const sessionToken = cookies[cookieName];\n\n const result = await checkPaywallAccess(path, sessionToken, config);\n\n if (!result.allowed && result.requiresPayment) {\n // Return 402 Payment Required\n const body = config.custom402Response\n ? config.custom402Response(path)\n : {\n error: 'Payment Required',\n message: 'This resource requires payment to access',\n path,\n };\n\n return new Response(JSON.stringify(body), {\n status: 402,\n headers: {\n 'Content-Type': 'application/json',\n },\n });\n }\n\n // Allow request to continue\n return null;\n };\n}\n\n/**\n * API route wrapper for Next.js App Router\n * \n * @example\n * ```typescript\n * // app/api/premium/route.ts\n * import { withPaywall } from '@alleyboss/micropay-solana-x402-paywall/middleware';\n * \n * async function handler(request: Request, session: SessionData) {\n * return Response.json({ content: 'Premium content', wallet: session.walletAddress });\n * }\n * \n * export const GET = withPaywall(handler, {\n * sessionSecret: process.env.SESSION_SECRET!,\n * articleId: 'article-123', // Optional: check specific article unlock\n * });\n * ```\n */\nexport function withPaywall<T>(\n handler: (request: Request, session: SessionData) => Promise<T>,\n options: {\n sessionSecret: string;\n cookieName?: string;\n articleId?: string;\n }\n): (request: Request) => Promise<Response | T> {\n const { sessionSecret, cookieName = 'x402_session', articleId } = options;\n\n return async function protectedHandler(request: Request): Promise<Response | T> {\n // Extract session token from cookie\n const cookieHeader = request.headers.get('cookie') || '';\n const cookies = Object.fromEntries(\n cookieHeader.split(';').map(c => {\n const [key, ...vals] = c.trim().split('=');\n return [key, vals.join('=')];\n })\n );\n const sessionToken = cookies[cookieName];\n\n if (!sessionToken) {\n return new Response(\n JSON.stringify({ error: 'Payment Required', message: 'No session token' }),\n { status: 402, headers: { 'Content-Type': 'application/json' } }\n );\n }\n\n // Validate session\n const validation = await validateSession(sessionToken, sessionSecret);\n\n if (!validation.valid || !validation.session) {\n return new Response(\n JSON.stringify({ error: 'Payment Required', message: validation.reason }),\n { status: 402, headers: { 'Content-Type': 'application/json' } }\n );\n }\n\n // Check article-specific unlock if required\n if (articleId) {\n const { session } = validation;\n const hasAccess = session.siteWideUnlock || session.unlockedArticles.includes(articleId);\n\n if (!hasAccess) {\n return new Response(\n JSON.stringify({ error: 'Payment Required', message: 'Article not unlocked' }),\n { status: 402, headers: { 'Content-Type': 'application/json' } }\n );\n }\n }\n\n // Call the actual handler with session\n return handler(request, validation.session);\n };\n}\n","// Express/Node.js Middleware\n// Universal middleware compatible with Express, Fastify, Koa adapters\n\nimport type { SignatureStore } from '../store';\nimport type { SessionData } from '../types';\nimport { validateSession } from '../session';\n\n/**\n * Generic HTTP request/response interfaces for framework compatibility\n * Works with Express, Fastify, raw Node.js http, etc.\n */\nexport interface GenericRequest {\n url?: string;\n path?: string;\n headers: Record<string, string | string[] | undefined> | { get(name: string): string | undefined };\n cookies?: Record<string, string>;\n}\n\nexport interface GenericResponse {\n status?(code: number): GenericResponse;\n statusCode?: number;\n json?(body: object): void;\n send?(body: string): void;\n setHeader?(name: string, value: string): void;\n end?(body?: string): void;\n}\n\nexport type NextFunction = (error?: Error) => void;\n\n/**\n * Express-style middleware configuration\n */\nexport interface ExpressPaywallConfig {\n /** Session secret for JWT validation */\n sessionSecret: string;\n /** Protected path patterns (glob-like) */\n protectedPaths: string[];\n /** Cookie name for session token (default: 'x402_session') */\n cookieName?: string;\n /** Optional signature store */\n signatureStore?: SignatureStore;\n /** Header name for session token (alternative to cookie) */\n headerName?: string;\n /** Custom 402 response */\n onPaymentRequired?: (req: GenericRequest, res: GenericResponse) => void;\n /** Called when access is granted */\n onAccessGranted?: (req: GenericRequest, session: SessionData) => void;\n}\n\n/**\n * Augmented request with session data\n */\nexport interface PaywallRequest extends GenericRequest {\n session?: SessionData;\n}\n\n/**\n * Simple glob pattern matching (supports * and **)\n */\nfunction matchPath(path: string, patterns: string[]): boolean {\n for (const pattern of patterns) {\n const regex = pattern\n .replace(/\\*\\*/g, '<<<GLOBSTAR>>>')\n .replace(/\\*/g, '[^/]*')\n .replace(/<<<GLOBSTAR>>>/g, '.*');\n\n if (new RegExp(`^${regex}$`).test(path)) {\n return true;\n }\n }\n return false;\n}\n\n/**\n * Check if headers object has a get method (Fetch/Next.js style)\n */\nfunction hasGetMethod(headers: GenericRequest['headers']): headers is { get(name: string): string | undefined } {\n return typeof (headers as { get?: unknown }).get === 'function';\n}\n\n/**\n * Get header value from either style of headers object\n */\nfunction getHeader(headers: GenericRequest['headers'], name: string): string | undefined {\n if (hasGetMethod(headers)) {\n return headers.get(name) ?? undefined;\n }\n const value = (headers as Record<string, string | string[] | undefined>)[name];\n return typeof value === 'string' ? value : undefined;\n}\n\n/**\n * Extract session token from request\n */\nfunction extractToken(req: GenericRequest, config: ExpressPaywallConfig): string | undefined {\n const { cookieName = 'x402_session', headerName } = config;\n\n // Try custom header first (for API clients)\n if (headerName) {\n const token = getHeader(req.headers, headerName.toLowerCase());\n if (token) return token;\n }\n\n // Try Authorization header (Bearer token)\n const authHeader = getHeader(req.headers, 'authorization');\n if (authHeader?.startsWith('Bearer ')) {\n return authHeader.slice(7);\n }\n\n // Try cookies\n if (req.cookies?.[cookieName]) {\n return req.cookies[cookieName];\n }\n\n // Parse cookie header manually if cookies not pre-parsed\n const cookieHeader = getHeader(req.headers, 'cookie');\n if (cookieHeader) {\n const match = cookieHeader.split(';')\n .map(c => c.trim())\n .find(c => c.startsWith(`${cookieName}=`));\n\n if (match) {\n return match.slice(cookieName.length + 1);\n }\n }\n\n return undefined;\n}\n\n/**\n * Create Express-compatible paywall middleware\n * Also works with Connect, Polka, and similar frameworks\n * \n * @example\n * ```typescript\n * import express from 'express';\n * import { createExpressMiddleware } from '@alleyboss/micropay-solana-x402-paywall/middleware';\n * \n * const app = express();\n * \n * app.use('/api/premium', createExpressMiddleware({\n * sessionSecret: process.env.SESSION_SECRET!,\n * protectedPaths: ['/**'],\n * }));\n * \n * app.get('/api/premium/content', (req, res) => {\n * res.json({ content: 'Premium!', wallet: req.session?.walletAddress });\n * });\n * ```\n */\nexport function createExpressMiddleware(config: ExpressPaywallConfig) {\n const { sessionSecret, protectedPaths, onPaymentRequired, onAccessGranted } = config;\n\n return async function paywallMiddleware(\n req: PaywallRequest,\n res: GenericResponse,\n next: NextFunction\n ): Promise<void> {\n const path = req.path || req.url?.split('?')[0] || '/';\n\n // Check if path needs protection\n if (!matchPath(path, protectedPaths)) {\n next();\n return;\n }\n\n // Extract token\n const token = extractToken(req, config);\n\n if (!token) {\n if (onPaymentRequired) {\n onPaymentRequired(req, res);\n } else {\n sendPaymentRequired(res, 'No session token');\n }\n return;\n }\n\n // Validate session\n const validation = await validateSession(token, sessionSecret);\n\n if (!validation.valid || !validation.session) {\n if (onPaymentRequired) {\n onPaymentRequired(req, res);\n } else {\n sendPaymentRequired(res, validation.reason || 'Invalid session');\n }\n return;\n }\n\n // Attach session to request\n req.session = validation.session;\n\n if (onAccessGranted) {\n onAccessGranted(req, validation.session);\n }\n\n next();\n };\n}\n\n/**\n * Send 402 Payment Required response\n */\nfunction sendPaymentRequired(res: GenericResponse, reason: string): void {\n const body = JSON.stringify({\n error: 'Payment Required',\n message: reason,\n });\n\n // Check for Express-style response (has both status and json methods)\n const statusFn = res.status;\n const jsonFn = res.json;\n\n if (statusFn && jsonFn) {\n // Express-style: res.status(402).json(...)\n const chainedRes = statusFn.call(res, 402);\n if (chainedRes?.json) {\n chainedRes.json({ error: 'Payment Required', message: reason });\n }\n } else if (res.statusCode !== undefined && res.setHeader && res.end) {\n // Raw Node.js http\n res.statusCode = 402;\n res.setHeader('Content-Type', 'application/json');\n res.end(body);\n }\n}\n\n/**\n * Fastify-compatible plugin factory\n * \n * @example\n * ```typescript\n * import Fastify from 'fastify';\n * import { createFastifyPlugin } from '@alleyboss/micropay-solana-x402-paywall/middleware';\n * \n * const fastify = Fastify();\n * fastify.register(createFastifyPlugin({\n * sessionSecret: process.env.SESSION_SECRET!,\n * protectedPaths: ['/api/premium/*'],\n * }));\n * ```\n */\nexport function createFastifyPlugin(config: ExpressPaywallConfig) {\n return async function paywallPlugin(fastify: any) {\n fastify.addHook('preHandler', createExpressMiddleware(config));\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/session/core.ts","../../src/x402/verification.ts","../../src/middleware/nextjs.ts","../../src/middleware/express.ts"],"names":["jwtVerify"],"mappings":"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","file":"index.cjs","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","// x402 payment verification service\n// SECURITY: Input validation, network verification, on-chain settlement check\nimport { verifyPayment, type SolanaClientConfig } from '../solana';\nimport type { PaymentPayload, PaymentRequirement, VerificationResponse } from '../types';\n\n// Signature validation regex (base58, 87-88 chars)\nconst SIGNATURE_REGEX = /^[1-9A-HJ-NP-Za-km-z]{87,88}$/;\n\n/**\n * Verify a payment payload against requirements\n * SECURITY: Full validation chain - format, network, on-chain\n */\nexport async function verifyX402Payment(\n payload: PaymentPayload,\n requirement: PaymentRequirement,\n clientConfig: SolanaClientConfig\n): Promise<VerificationResponse> {\n // SECURITY: Validate payload exists\n if (!payload || typeof payload !== 'object') {\n return { valid: false, invalidReason: 'Invalid payload' };\n }\n\n // SECURITY: Validate signature exists and format\n const signature = payload.payload?.signature;\n if (!signature || typeof signature !== 'string') {\n return { valid: false, invalidReason: 'Missing transaction signature' };\n }\n if (!SIGNATURE_REGEX.test(signature)) {\n return { valid: false, invalidReason: 'Invalid signature format' };\n }\n\n // SECURITY: Validate x402 version\n if (payload.x402Version !== 1) {\n return { valid: false, invalidReason: 'Unsupported x402 version' };\n }\n\n // SECURITY: Validate scheme\n if (payload.scheme !== 'exact') {\n return { valid: false, invalidReason: 'Unsupported payment scheme' };\n }\n\n // SECURITY: Validate network matches exactly\n if (payload.network !== requirement.network) {\n return {\n valid: false,\n invalidReason: `Network mismatch: expected ${requirement.network}`,\n };\n }\n\n // SECURITY: Validate requirement has valid payTo address\n const walletRegex = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;\n if (!walletRegex.test(requirement.payTo)) {\n return { valid: false, invalidReason: 'Invalid recipient configuration' };\n }\n\n // SECURITY: Validate amount is positive\n let expectedAmount: bigint;\n try {\n expectedAmount = BigInt(requirement.maxAmountRequired);\n if (expectedAmount <= 0n) {\n return { valid: false, invalidReason: 'Invalid payment amount' };\n }\n } catch {\n return { valid: false, invalidReason: 'Invalid payment amount format' };\n }\n\n // Verify on-chain\n const verification = await verifyPayment({\n signature,\n expectedRecipient: requirement.payTo,\n expectedAmount,\n maxAgeSeconds: requirement.maxTimeoutSeconds,\n clientConfig,\n });\n\n if (!verification.valid) {\n return {\n valid: false,\n invalidReason: verification.error || 'Transaction verification failed',\n };\n }\n\n return {\n valid: true,\n settled: verification.confirmed,\n from: verification.from,\n transaction: {\n signature: verification.signature,\n blockTime: verification.blockTime,\n slot: verification.slot,\n },\n };\n}\n\n/**\n * Parse payment payload from X-Payment header (base64 encoded JSON)\n * SECURITY: Safe JSON parsing with try-catch\n * \n * @example\n * const payload = parsePaymentHeader(request.headers.get('x-payment'));\n */\nexport function parsePaymentHeader(header: string): PaymentPayload | null {\n if (!header || typeof header !== 'string') {\n return null;\n }\n\n // SECURITY: Limit header size to prevent DoS\n if (header.length > 10000) {\n return null;\n }\n\n try {\n const decoded = Buffer.from(header, 'base64').toString('utf-8');\n const parsed = JSON.parse(decoded);\n\n // Basic structure validation\n if (!parsed || typeof parsed !== 'object') {\n return null;\n }\n\n return parsed as PaymentPayload;\n } catch {\n return null;\n }\n}\n\n/**\n * Encode PaymentRequirement for X-Payment-Required header\n * Per x402 spec: base64 encoded JSON\n * \n * @example\n * response.headers.set('X-Payment-Required', encodePaymentRequirement(requirement));\n */\nexport function encodePaymentRequirement(requirement: PaymentRequirement): string {\n return Buffer.from(JSON.stringify(requirement)).toString('base64');\n}\n\n/**\n * Encode VerificationResponse for X-Payment-Response header\n */\nexport function encodePaymentResponse(response: VerificationResponse): string {\n return Buffer.from(JSON.stringify(response)).toString('base64');\n}\n\n/**\n * Create a 402 Payment Required response with proper x402 headers\n * \n * @example\n * if (!hasValidPayment) {\n * return create402Response(requirement, { message: 'Payment required' });\n * }\n */\nexport function create402Response(\n requirement: PaymentRequirement,\n body?: object\n): Response {\n const headers = new Headers({\n 'Content-Type': 'application/json',\n 'X-Payment-Required': encodePaymentRequirement(requirement),\n });\n\n const responseBody = body || {\n error: 'Payment Required',\n message: 'This resource requires payment to access',\n x402Version: 1,\n accepts: [requirement],\n };\n\n return new Response(JSON.stringify(responseBody), {\n status: 402,\n headers,\n });\n}\n","// Next.js Middleware for Paywall\n// Zero-boilerplate integration for protected routes\n\nimport type { SignatureStore } from '../store';\nimport type { SessionData, PaymentRequirement } from '../types';\nimport { validateSession } from '../session';\nimport { encodePaymentRequirement } from '../x402';\n\n/**\n * Configuration for paywall middleware\n */\nexport interface PaywallMiddlewareConfig {\n /** Session secret for JWT validation */\n sessionSecret: string;\n /** Protected path patterns (glob-like) */\n protectedPaths: string[];\n /** Cookie name for session token */\n cookieName?: string;\n /** Optional signature store for anti-replay */\n signatureStore?: SignatureStore;\n /** Payment requirement for 402 responses (enables x402 headers) */\n paymentRequirement?: PaymentRequirement | ((path: string) => PaymentRequirement);\n /** Custom 402 response body (deprecated, use paymentRequirement) */\n custom402Response?: (path: string) => object;\n}\n\n/**\n * Result of middleware check\n */\nexport interface MiddlewareResult {\n /** Whether access is allowed */\n allowed: boolean;\n /** Session data if valid */\n session?: SessionData;\n /** Reason for denial */\n reason?: string;\n /** Should respond with 402 */\n requiresPayment?: boolean;\n}\n\n/**\n * Check if path matches any protected pattern\n */\nfunction matchesProtectedPath(path: string, patterns: string[]): boolean {\n for (const pattern of patterns) {\n // Simple glob matching: * matches anything, ** matches path segments\n const regexPattern = pattern\n .replace(/\\*\\*/g, '{{DOUBLE_STAR}}')\n .replace(/\\*/g, '[^/]*')\n .replace(/{{DOUBLE_STAR}}/g, '.*');\n\n const regex = new RegExp(`^${regexPattern}$`);\n if (regex.test(path)) {\n return true;\n }\n }\n return false;\n}\n\n/**\n * Check paywall access for a request\n * Framework-agnostic core logic\n */\nexport async function checkPaywallAccess(\n path: string,\n sessionToken: string | undefined,\n config: PaywallMiddlewareConfig\n): Promise<MiddlewareResult> {\n // Check if path is protected\n if (!matchesProtectedPath(path, config.protectedPaths)) {\n return { allowed: true };\n }\n\n // No session token\n if (!sessionToken) {\n return {\n allowed: false,\n reason: 'No session token',\n requiresPayment: true,\n };\n }\n\n // Validate session\n const validation = await validateSession(sessionToken, config.sessionSecret);\n\n if (!validation.valid || !validation.session) {\n return {\n allowed: false,\n reason: validation.reason || 'Invalid session',\n requiresPayment: true,\n };\n }\n\n return {\n allowed: true,\n session: validation.session,\n };\n}\n\n/**\n * Create Next.js middleware handler\n * \n * @example\n * ```typescript\n * // middleware.ts\n * import { createPaywallMiddleware } from '@alleyboss/micropay-solana-x402-paywall/middleware';\n * \n * export const middleware = createPaywallMiddleware({\n * sessionSecret: process.env.SESSION_SECRET!,\n * protectedPaths: ['/api/premium/*', '/api/content/*'],\n * });\n * \n * export const config = { matcher: ['/api/premium/:path*', '/api/content/:path*'] };\n * ```\n */\nexport function createPaywallMiddleware(config: PaywallMiddlewareConfig) {\n const { cookieName = 'x402_session' } = config;\n\n return async function middleware(request: Request): Promise<Response | null> {\n const url = new URL(request.url);\n const path = url.pathname;\n\n // Get session from cookie header\n const cookieHeader = request.headers.get('cookie') || '';\n const cookies = Object.fromEntries(\n cookieHeader.split(';').map(c => {\n const [key, ...vals] = c.trim().split('=');\n return [key, vals.join('=')];\n })\n );\n const sessionToken = cookies[cookieName];\n\n const result = await checkPaywallAccess(path, sessionToken, config);\n\n if (!result.allowed && result.requiresPayment) {\n // Build x402-compliant 402 response\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n };\n\n // Add X-Payment-Required header if paymentRequirement is configured\n if (config.paymentRequirement) {\n const requirement = typeof config.paymentRequirement === 'function'\n ? config.paymentRequirement(path)\n : config.paymentRequirement;\n headers['X-Payment-Required'] = encodePaymentRequirement(requirement);\n }\n\n const body = config.custom402Response\n ? config.custom402Response(path)\n : {\n error: 'Payment Required',\n message: 'This resource requires payment to access',\n x402Version: 1,\n path,\n };\n\n return new Response(JSON.stringify(body), {\n status: 402,\n headers,\n });\n }\n\n // Allow request to continue\n return null;\n };\n}\n\n/**\n * API route wrapper for Next.js App Router\n * \n * @example\n * ```typescript\n * // app/api/premium/route.ts\n * import { withPaywall } from '@alleyboss/micropay-solana-x402-paywall/middleware';\n * \n * async function handler(request: Request, session: SessionData) {\n * return Response.json({ content: 'Premium content', wallet: session.walletAddress });\n * }\n * \n * export const GET = withPaywall(handler, {\n * sessionSecret: process.env.SESSION_SECRET!,\n * articleId: 'article-123', // Optional: check specific article unlock\n * });\n * ```\n */\nexport function withPaywall<T>(\n handler: (request: Request, session: SessionData) => Promise<T>,\n options: {\n sessionSecret: string;\n cookieName?: string;\n articleId?: string;\n }\n): (request: Request) => Promise<Response | T> {\n const { sessionSecret, cookieName = 'x402_session', articleId } = options;\n\n return async function protectedHandler(request: Request): Promise<Response | T> {\n // Extract session token from cookie\n const cookieHeader = request.headers.get('cookie') || '';\n const cookies = Object.fromEntries(\n cookieHeader.split(';').map(c => {\n const [key, ...vals] = c.trim().split('=');\n return [key, vals.join('=')];\n })\n );\n const sessionToken = cookies[cookieName];\n\n if (!sessionToken) {\n return new Response(\n JSON.stringify({ error: 'Payment Required', message: 'No session token' }),\n { status: 402, headers: { 'Content-Type': 'application/json' } }\n );\n }\n\n // Validate session\n const validation = await validateSession(sessionToken, sessionSecret);\n\n if (!validation.valid || !validation.session) {\n return new Response(\n JSON.stringify({ error: 'Payment Required', message: validation.reason }),\n { status: 402, headers: { 'Content-Type': 'application/json' } }\n );\n }\n\n // Check article-specific unlock if required\n if (articleId) {\n const { session } = validation;\n const hasAccess = session.siteWideUnlock || session.unlockedArticles.includes(articleId);\n\n if (!hasAccess) {\n return new Response(\n JSON.stringify({ error: 'Payment Required', message: 'Article not unlocked' }),\n { status: 402, headers: { 'Content-Type': 'application/json' } }\n );\n }\n }\n\n // Call the actual handler with session\n return handler(request, validation.session);\n };\n}\n","// Express/Node.js Middleware\n// Universal middleware compatible with Express, Fastify, Koa adapters\n\nimport type { SignatureStore } from '../store';\nimport type { SessionData } from '../types';\nimport { validateSession } from '../session';\n\n/**\n * Generic HTTP request/response interfaces for framework compatibility\n * Works with Express, Fastify, raw Node.js http, etc.\n */\nexport interface GenericRequest {\n url?: string;\n path?: string;\n headers: Record<string, string | string[] | undefined> | { get(name: string): string | undefined };\n cookies?: Record<string, string>;\n}\n\nexport interface GenericResponse {\n status?(code: number): GenericResponse;\n statusCode?: number;\n json?(body: object): void;\n send?(body: string): void;\n setHeader?(name: string, value: string): void;\n end?(body?: string): void;\n}\n\nexport type NextFunction = (error?: Error) => void;\n\n/**\n * Express-style middleware configuration\n */\nexport interface ExpressPaywallConfig {\n /** Session secret for JWT validation */\n sessionSecret: string;\n /** Protected path patterns (glob-like) */\n protectedPaths: string[];\n /** Cookie name for session token (default: 'x402_session') */\n cookieName?: string;\n /** Optional signature store */\n signatureStore?: SignatureStore;\n /** Header name for session token (alternative to cookie) */\n headerName?: string;\n /** Custom 402 response */\n onPaymentRequired?: (req: GenericRequest, res: GenericResponse) => void;\n /** Called when access is granted */\n onAccessGranted?: (req: GenericRequest, session: SessionData) => void;\n}\n\n/**\n * Augmented request with session data\n */\nexport interface PaywallRequest extends GenericRequest {\n session?: SessionData;\n}\n\n/**\n * Simple glob pattern matching (supports * and **)\n */\nfunction matchPath(path: string, patterns: string[]): boolean {\n for (const pattern of patterns) {\n const regex = pattern\n .replace(/\\*\\*/g, '<<<GLOBSTAR>>>')\n .replace(/\\*/g, '[^/]*')\n .replace(/<<<GLOBSTAR>>>/g, '.*');\n\n if (new RegExp(`^${regex}$`).test(path)) {\n return true;\n }\n }\n return false;\n}\n\n/**\n * Check if headers object has a get method (Fetch/Next.js style)\n */\nfunction hasGetMethod(headers: GenericRequest['headers']): headers is { get(name: string): string | undefined } {\n return typeof (headers as { get?: unknown }).get === 'function';\n}\n\n/**\n * Get header value from either style of headers object\n */\nfunction getHeader(headers: GenericRequest['headers'], name: string): string | undefined {\n if (hasGetMethod(headers)) {\n return headers.get(name) ?? undefined;\n }\n const value = (headers as Record<string, string | string[] | undefined>)[name];\n return typeof value === 'string' ? value : undefined;\n}\n\n/**\n * Extract session token from request\n */\nfunction extractToken(req: GenericRequest, config: ExpressPaywallConfig): string | undefined {\n const { cookieName = 'x402_session', headerName } = config;\n\n // Try custom header first (for API clients)\n if (headerName) {\n const token = getHeader(req.headers, headerName.toLowerCase());\n if (token) return token;\n }\n\n // Try Authorization header (Bearer token)\n const authHeader = getHeader(req.headers, 'authorization');\n if (authHeader?.startsWith('Bearer ')) {\n return authHeader.slice(7);\n }\n\n // Try cookies\n if (req.cookies?.[cookieName]) {\n return req.cookies[cookieName];\n }\n\n // Parse cookie header manually if cookies not pre-parsed\n const cookieHeader = getHeader(req.headers, 'cookie');\n if (cookieHeader) {\n const match = cookieHeader.split(';')\n .map(c => c.trim())\n .find(c => c.startsWith(`${cookieName}=`));\n\n if (match) {\n return match.slice(cookieName.length + 1);\n }\n }\n\n return undefined;\n}\n\n/**\n * Create Express-compatible paywall middleware\n * Also works with Connect, Polka, and similar frameworks\n * \n * @example\n * ```typescript\n * import express from 'express';\n * import { createExpressMiddleware } from '@alleyboss/micropay-solana-x402-paywall/middleware';\n * \n * const app = express();\n * \n * app.use('/api/premium', createExpressMiddleware({\n * sessionSecret: process.env.SESSION_SECRET!,\n * protectedPaths: ['/**'],\n * }));\n * \n * app.get('/api/premium/content', (req, res) => {\n * res.json({ content: 'Premium!', wallet: req.session?.walletAddress });\n * });\n * ```\n */\nexport function createExpressMiddleware(config: ExpressPaywallConfig) {\n const { sessionSecret, protectedPaths, onPaymentRequired, onAccessGranted } = config;\n\n return async function paywallMiddleware(\n req: PaywallRequest,\n res: GenericResponse,\n next: NextFunction\n ): Promise<void> {\n const path = req.path || req.url?.split('?')[0] || '/';\n\n // Check if path needs protection\n if (!matchPath(path, protectedPaths)) {\n next();\n return;\n }\n\n // Extract token\n const token = extractToken(req, config);\n\n if (!token) {\n if (onPaymentRequired) {\n onPaymentRequired(req, res);\n } else {\n sendPaymentRequired(res, 'No session token');\n }\n return;\n }\n\n // Validate session\n const validation = await validateSession(token, sessionSecret);\n\n if (!validation.valid || !validation.session) {\n if (onPaymentRequired) {\n onPaymentRequired(req, res);\n } else {\n sendPaymentRequired(res, validation.reason || 'Invalid session');\n }\n return;\n }\n\n // Attach session to request\n req.session = validation.session;\n\n if (onAccessGranted) {\n onAccessGranted(req, validation.session);\n }\n\n next();\n };\n}\n\n/**\n * Send 402 Payment Required response\n */\nfunction sendPaymentRequired(res: GenericResponse, reason: string): void {\n const body = JSON.stringify({\n error: 'Payment Required',\n message: reason,\n });\n\n // Check for Express-style response (has both status and json methods)\n const statusFn = res.status;\n const jsonFn = res.json;\n\n if (statusFn && jsonFn) {\n // Express-style: res.status(402).json(...)\n const chainedRes = statusFn.call(res, 402);\n if (chainedRes?.json) {\n chainedRes.json({ error: 'Payment Required', message: reason });\n }\n } else if (res.statusCode !== undefined && res.setHeader && res.end) {\n // Raw Node.js http\n res.statusCode = 402;\n res.setHeader('Content-Type', 'application/json');\n res.end(body);\n }\n}\n\n/**\n * Fastify-compatible plugin factory\n * \n * @example\n * ```typescript\n * import Fastify from 'fastify';\n * import { createFastifyPlugin } from '@alleyboss/micropay-solana-x402-paywall/middleware';\n * \n * const fastify = Fastify();\n * fastify.register(createFastifyPlugin({\n * sessionSecret: process.env.SESSION_SECRET!,\n * protectedPaths: ['/api/premium/*'],\n * }));\n * ```\n */\nexport function createFastifyPlugin(config: ExpressPaywallConfig) {\n return async function paywallPlugin(fastify: any) {\n fastify.addHook('preHandler', createExpressMiddleware(config));\n };\n}\n"]}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
export { M as MiddlewareResult, P as PaywallMiddlewareConfig, a as checkPaywallAccess, c as createPaywallMiddleware, w as withPaywall } from '../nextjs-
|
|
1
|
+
export { M as MiddlewareResult, P as PaywallMiddlewareConfig, a as checkPaywallAccess, c as createPaywallMiddleware, w as withPaywall } from '../nextjs-BDyOqGAq.cjs';
|
|
2
2
|
import { S as SignatureStore } from '../memory-Daxkczti.cjs';
|
|
3
3
|
import { S as SessionData } from '../session-D2IoWAWV.cjs';
|
|
4
|
+
import '../payment-BGp7eMQl.cjs';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Generic HTTP request/response interfaces for framework compatibility
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
export { M as MiddlewareResult, P as PaywallMiddlewareConfig, a as checkPaywallAccess, c as createPaywallMiddleware, w as withPaywall } from '../nextjs-
|
|
1
|
+
export { M as MiddlewareResult, P as PaywallMiddlewareConfig, a as checkPaywallAccess, c as createPaywallMiddleware, w as withPaywall } from '../nextjs-CbX8_9yK.js';
|
|
2
2
|
import { S as SignatureStore } from '../memory-Daxkczti.js';
|
|
3
3
|
import { S as SessionData } from '../session-D2IoWAWV.js';
|
|
4
|
+
import '../payment-BGp7eMQl.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Generic HTTP request/response interfaces for framework compatibility
|
package/dist/middleware/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jwtVerify } from 'jose';
|
|
2
2
|
import 'uuid';
|
|
3
|
+
import '@solana/web3.js';
|
|
3
4
|
|
|
4
5
|
// src/session/core.ts
|
|
5
6
|
var MIN_SECRET_LENGTH = 32;
|
|
@@ -48,6 +49,11 @@ async function validateSession(token, secret) {
|
|
|
48
49
|
}
|
|
49
50
|
}
|
|
50
51
|
|
|
52
|
+
// src/x402/verification.ts
|
|
53
|
+
function encodePaymentRequirement(requirement) {
|
|
54
|
+
return Buffer.from(JSON.stringify(requirement)).toString("base64");
|
|
55
|
+
}
|
|
56
|
+
|
|
51
57
|
// src/middleware/nextjs.ts
|
|
52
58
|
function matchesProtectedPath(path, patterns) {
|
|
53
59
|
for (const pattern of patterns) {
|
|
@@ -98,16 +104,22 @@ function createPaywallMiddleware(config) {
|
|
|
98
104
|
const sessionToken = cookies[cookieName];
|
|
99
105
|
const result = await checkPaywallAccess(path, sessionToken, config);
|
|
100
106
|
if (!result.allowed && result.requiresPayment) {
|
|
107
|
+
const headers = {
|
|
108
|
+
"Content-Type": "application/json"
|
|
109
|
+
};
|
|
110
|
+
if (config.paymentRequirement) {
|
|
111
|
+
const requirement = typeof config.paymentRequirement === "function" ? config.paymentRequirement(path) : config.paymentRequirement;
|
|
112
|
+
headers["X-Payment-Required"] = encodePaymentRequirement(requirement);
|
|
113
|
+
}
|
|
101
114
|
const body = config.custom402Response ? config.custom402Response(path) : {
|
|
102
115
|
error: "Payment Required",
|
|
103
116
|
message: "This resource requires payment to access",
|
|
117
|
+
x402Version: 1,
|
|
104
118
|
path
|
|
105
119
|
};
|
|
106
120
|
return new Response(JSON.stringify(body), {
|
|
107
121
|
status: 402,
|
|
108
|
-
headers
|
|
109
|
-
"Content-Type": "application/json"
|
|
110
|
-
}
|
|
122
|
+
headers
|
|
111
123
|
});
|
|
112
124
|
}
|
|
113
125
|
return null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/session/core.ts","../../src/middleware/nextjs.ts","../../src/middleware/express.ts"],"names":[],"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","// Next.js Middleware for Paywall\n// Zero-boilerplate integration for protected routes\n\nimport type { SignatureStore } from '../store';\nimport type { SessionData } from '../types';\nimport { validateSession } from '../session';\n\n/**\n * Configuration for paywall middleware\n */\nexport interface PaywallMiddlewareConfig {\n /** Session secret for JWT validation */\n sessionSecret: string;\n /** Protected path patterns (glob-like) */\n protectedPaths: string[];\n /** Cookie name for session token */\n cookieName?: string;\n /** Optional signature store for anti-replay */\n signatureStore?: SignatureStore;\n /** Custom 402 response body */\n custom402Response?: (path: string) => object;\n}\n\n/**\n * Result of middleware check\n */\nexport interface MiddlewareResult {\n /** Whether access is allowed */\n allowed: boolean;\n /** Session data if valid */\n session?: SessionData;\n /** Reason for denial */\n reason?: string;\n /** Should respond with 402 */\n requiresPayment?: boolean;\n}\n\n/**\n * Check if path matches any protected pattern\n */\nfunction matchesProtectedPath(path: string, patterns: string[]): boolean {\n for (const pattern of patterns) {\n // Simple glob matching: * matches anything, ** matches path segments\n const regexPattern = pattern\n .replace(/\\*\\*/g, '{{DOUBLE_STAR}}')\n .replace(/\\*/g, '[^/]*')\n .replace(/{{DOUBLE_STAR}}/g, '.*');\n\n const regex = new RegExp(`^${regexPattern}$`);\n if (regex.test(path)) {\n return true;\n }\n }\n return false;\n}\n\n/**\n * Check paywall access for a request\n * Framework-agnostic core logic\n */\nexport async function checkPaywallAccess(\n path: string,\n sessionToken: string | undefined,\n config: PaywallMiddlewareConfig\n): Promise<MiddlewareResult> {\n // Check if path is protected\n if (!matchesProtectedPath(path, config.protectedPaths)) {\n return { allowed: true };\n }\n\n // No session token\n if (!sessionToken) {\n return {\n allowed: false,\n reason: 'No session token',\n requiresPayment: true,\n };\n }\n\n // Validate session\n const validation = await validateSession(sessionToken, config.sessionSecret);\n\n if (!validation.valid || !validation.session) {\n return {\n allowed: false,\n reason: validation.reason || 'Invalid session',\n requiresPayment: true,\n };\n }\n\n return {\n allowed: true,\n session: validation.session,\n };\n}\n\n/**\n * Create Next.js middleware handler\n * \n * @example\n * ```typescript\n * // middleware.ts\n * import { createPaywallMiddleware } from '@alleyboss/micropay-solana-x402-paywall/middleware';\n * \n * export const middleware = createPaywallMiddleware({\n * sessionSecret: process.env.SESSION_SECRET!,\n * protectedPaths: ['/api/premium/*', '/api/content/*'],\n * });\n * \n * export const config = { matcher: ['/api/premium/:path*', '/api/content/:path*'] };\n * ```\n */\nexport function createPaywallMiddleware(config: PaywallMiddlewareConfig) {\n const { cookieName = 'x402_session' } = config;\n\n return async function middleware(request: Request): Promise<Response | null> {\n const url = new URL(request.url);\n const path = url.pathname;\n\n // Get session from cookie header\n const cookieHeader = request.headers.get('cookie') || '';\n const cookies = Object.fromEntries(\n cookieHeader.split(';').map(c => {\n const [key, ...vals] = c.trim().split('=');\n return [key, vals.join('=')];\n })\n );\n const sessionToken = cookies[cookieName];\n\n const result = await checkPaywallAccess(path, sessionToken, config);\n\n if (!result.allowed && result.requiresPayment) {\n // Return 402 Payment Required\n const body = config.custom402Response\n ? config.custom402Response(path)\n : {\n error: 'Payment Required',\n message: 'This resource requires payment to access',\n path,\n };\n\n return new Response(JSON.stringify(body), {\n status: 402,\n headers: {\n 'Content-Type': 'application/json',\n },\n });\n }\n\n // Allow request to continue\n return null;\n };\n}\n\n/**\n * API route wrapper for Next.js App Router\n * \n * @example\n * ```typescript\n * // app/api/premium/route.ts\n * import { withPaywall } from '@alleyboss/micropay-solana-x402-paywall/middleware';\n * \n * async function handler(request: Request, session: SessionData) {\n * return Response.json({ content: 'Premium content', wallet: session.walletAddress });\n * }\n * \n * export const GET = withPaywall(handler, {\n * sessionSecret: process.env.SESSION_SECRET!,\n * articleId: 'article-123', // Optional: check specific article unlock\n * });\n * ```\n */\nexport function withPaywall<T>(\n handler: (request: Request, session: SessionData) => Promise<T>,\n options: {\n sessionSecret: string;\n cookieName?: string;\n articleId?: string;\n }\n): (request: Request) => Promise<Response | T> {\n const { sessionSecret, cookieName = 'x402_session', articleId } = options;\n\n return async function protectedHandler(request: Request): Promise<Response | T> {\n // Extract session token from cookie\n const cookieHeader = request.headers.get('cookie') || '';\n const cookies = Object.fromEntries(\n cookieHeader.split(';').map(c => {\n const [key, ...vals] = c.trim().split('=');\n return [key, vals.join('=')];\n })\n );\n const sessionToken = cookies[cookieName];\n\n if (!sessionToken) {\n return new Response(\n JSON.stringify({ error: 'Payment Required', message: 'No session token' }),\n { status: 402, headers: { 'Content-Type': 'application/json' } }\n );\n }\n\n // Validate session\n const validation = await validateSession(sessionToken, sessionSecret);\n\n if (!validation.valid || !validation.session) {\n return new Response(\n JSON.stringify({ error: 'Payment Required', message: validation.reason }),\n { status: 402, headers: { 'Content-Type': 'application/json' } }\n );\n }\n\n // Check article-specific unlock if required\n if (articleId) {\n const { session } = validation;\n const hasAccess = session.siteWideUnlock || session.unlockedArticles.includes(articleId);\n\n if (!hasAccess) {\n return new Response(\n JSON.stringify({ error: 'Payment Required', message: 'Article not unlocked' }),\n { status: 402, headers: { 'Content-Type': 'application/json' } }\n );\n }\n }\n\n // Call the actual handler with session\n return handler(request, validation.session);\n };\n}\n","// Express/Node.js Middleware\n// Universal middleware compatible with Express, Fastify, Koa adapters\n\nimport type { SignatureStore } from '../store';\nimport type { SessionData } from '../types';\nimport { validateSession } from '../session';\n\n/**\n * Generic HTTP request/response interfaces for framework compatibility\n * Works with Express, Fastify, raw Node.js http, etc.\n */\nexport interface GenericRequest {\n url?: string;\n path?: string;\n headers: Record<string, string | string[] | undefined> | { get(name: string): string | undefined };\n cookies?: Record<string, string>;\n}\n\nexport interface GenericResponse {\n status?(code: number): GenericResponse;\n statusCode?: number;\n json?(body: object): void;\n send?(body: string): void;\n setHeader?(name: string, value: string): void;\n end?(body?: string): void;\n}\n\nexport type NextFunction = (error?: Error) => void;\n\n/**\n * Express-style middleware configuration\n */\nexport interface ExpressPaywallConfig {\n /** Session secret for JWT validation */\n sessionSecret: string;\n /** Protected path patterns (glob-like) */\n protectedPaths: string[];\n /** Cookie name for session token (default: 'x402_session') */\n cookieName?: string;\n /** Optional signature store */\n signatureStore?: SignatureStore;\n /** Header name for session token (alternative to cookie) */\n headerName?: string;\n /** Custom 402 response */\n onPaymentRequired?: (req: GenericRequest, res: GenericResponse) => void;\n /** Called when access is granted */\n onAccessGranted?: (req: GenericRequest, session: SessionData) => void;\n}\n\n/**\n * Augmented request with session data\n */\nexport interface PaywallRequest extends GenericRequest {\n session?: SessionData;\n}\n\n/**\n * Simple glob pattern matching (supports * and **)\n */\nfunction matchPath(path: string, patterns: string[]): boolean {\n for (const pattern of patterns) {\n const regex = pattern\n .replace(/\\*\\*/g, '<<<GLOBSTAR>>>')\n .replace(/\\*/g, '[^/]*')\n .replace(/<<<GLOBSTAR>>>/g, '.*');\n\n if (new RegExp(`^${regex}$`).test(path)) {\n return true;\n }\n }\n return false;\n}\n\n/**\n * Check if headers object has a get method (Fetch/Next.js style)\n */\nfunction hasGetMethod(headers: GenericRequest['headers']): headers is { get(name: string): string | undefined } {\n return typeof (headers as { get?: unknown }).get === 'function';\n}\n\n/**\n * Get header value from either style of headers object\n */\nfunction getHeader(headers: GenericRequest['headers'], name: string): string | undefined {\n if (hasGetMethod(headers)) {\n return headers.get(name) ?? undefined;\n }\n const value = (headers as Record<string, string | string[] | undefined>)[name];\n return typeof value === 'string' ? value : undefined;\n}\n\n/**\n * Extract session token from request\n */\nfunction extractToken(req: GenericRequest, config: ExpressPaywallConfig): string | undefined {\n const { cookieName = 'x402_session', headerName } = config;\n\n // Try custom header first (for API clients)\n if (headerName) {\n const token = getHeader(req.headers, headerName.toLowerCase());\n if (token) return token;\n }\n\n // Try Authorization header (Bearer token)\n const authHeader = getHeader(req.headers, 'authorization');\n if (authHeader?.startsWith('Bearer ')) {\n return authHeader.slice(7);\n }\n\n // Try cookies\n if (req.cookies?.[cookieName]) {\n return req.cookies[cookieName];\n }\n\n // Parse cookie header manually if cookies not pre-parsed\n const cookieHeader = getHeader(req.headers, 'cookie');\n if (cookieHeader) {\n const match = cookieHeader.split(';')\n .map(c => c.trim())\n .find(c => c.startsWith(`${cookieName}=`));\n\n if (match) {\n return match.slice(cookieName.length + 1);\n }\n }\n\n return undefined;\n}\n\n/**\n * Create Express-compatible paywall middleware\n * Also works with Connect, Polka, and similar frameworks\n * \n * @example\n * ```typescript\n * import express from 'express';\n * import { createExpressMiddleware } from '@alleyboss/micropay-solana-x402-paywall/middleware';\n * \n * const app = express();\n * \n * app.use('/api/premium', createExpressMiddleware({\n * sessionSecret: process.env.SESSION_SECRET!,\n * protectedPaths: ['/**'],\n * }));\n * \n * app.get('/api/premium/content', (req, res) => {\n * res.json({ content: 'Premium!', wallet: req.session?.walletAddress });\n * });\n * ```\n */\nexport function createExpressMiddleware(config: ExpressPaywallConfig) {\n const { sessionSecret, protectedPaths, onPaymentRequired, onAccessGranted } = config;\n\n return async function paywallMiddleware(\n req: PaywallRequest,\n res: GenericResponse,\n next: NextFunction\n ): Promise<void> {\n const path = req.path || req.url?.split('?')[0] || '/';\n\n // Check if path needs protection\n if (!matchPath(path, protectedPaths)) {\n next();\n return;\n }\n\n // Extract token\n const token = extractToken(req, config);\n\n if (!token) {\n if (onPaymentRequired) {\n onPaymentRequired(req, res);\n } else {\n sendPaymentRequired(res, 'No session token');\n }\n return;\n }\n\n // Validate session\n const validation = await validateSession(token, sessionSecret);\n\n if (!validation.valid || !validation.session) {\n if (onPaymentRequired) {\n onPaymentRequired(req, res);\n } else {\n sendPaymentRequired(res, validation.reason || 'Invalid session');\n }\n return;\n }\n\n // Attach session to request\n req.session = validation.session;\n\n if (onAccessGranted) {\n onAccessGranted(req, validation.session);\n }\n\n next();\n };\n}\n\n/**\n * Send 402 Payment Required response\n */\nfunction sendPaymentRequired(res: GenericResponse, reason: string): void {\n const body = JSON.stringify({\n error: 'Payment Required',\n message: reason,\n });\n\n // Check for Express-style response (has both status and json methods)\n const statusFn = res.status;\n const jsonFn = res.json;\n\n if (statusFn && jsonFn) {\n // Express-style: res.status(402).json(...)\n const chainedRes = statusFn.call(res, 402);\n if (chainedRes?.json) {\n chainedRes.json({ error: 'Payment Required', message: reason });\n }\n } else if (res.statusCode !== undefined && res.setHeader && res.end) {\n // Raw Node.js http\n res.statusCode = 402;\n res.setHeader('Content-Type', 'application/json');\n res.end(body);\n }\n}\n\n/**\n * Fastify-compatible plugin factory\n * \n * @example\n * ```typescript\n * import Fastify from 'fastify';\n * import { createFastifyPlugin } from '@alleyboss/micropay-solana-x402-paywall/middleware';\n * \n * const fastify = Fastify();\n * fastify.register(createFastifyPlugin({\n * sessionSecret: process.env.SESSION_SECRET!,\n * protectedPaths: ['/api/premium/*'],\n * }));\n * ```\n */\nexport function createFastifyPlugin(config: ExpressPaywallConfig) {\n return async function paywallPlugin(fastify: any) {\n fastify.addHook('preHandler', createExpressMiddleware(config));\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/session/core.ts","../../src/x402/verification.ts","../../src/middleware/nextjs.ts","../../src/middleware/express.ts"],"names":[],"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","// x402 payment verification service\n// SECURITY: Input validation, network verification, on-chain settlement check\nimport { verifyPayment, type SolanaClientConfig } from '../solana';\nimport type { PaymentPayload, PaymentRequirement, VerificationResponse } from '../types';\n\n// Signature validation regex (base58, 87-88 chars)\nconst SIGNATURE_REGEX = /^[1-9A-HJ-NP-Za-km-z]{87,88}$/;\n\n/**\n * Verify a payment payload against requirements\n * SECURITY: Full validation chain - format, network, on-chain\n */\nexport async function verifyX402Payment(\n payload: PaymentPayload,\n requirement: PaymentRequirement,\n clientConfig: SolanaClientConfig\n): Promise<VerificationResponse> {\n // SECURITY: Validate payload exists\n if (!payload || typeof payload !== 'object') {\n return { valid: false, invalidReason: 'Invalid payload' };\n }\n\n // SECURITY: Validate signature exists and format\n const signature = payload.payload?.signature;\n if (!signature || typeof signature !== 'string') {\n return { valid: false, invalidReason: 'Missing transaction signature' };\n }\n if (!SIGNATURE_REGEX.test(signature)) {\n return { valid: false, invalidReason: 'Invalid signature format' };\n }\n\n // SECURITY: Validate x402 version\n if (payload.x402Version !== 1) {\n return { valid: false, invalidReason: 'Unsupported x402 version' };\n }\n\n // SECURITY: Validate scheme\n if (payload.scheme !== 'exact') {\n return { valid: false, invalidReason: 'Unsupported payment scheme' };\n }\n\n // SECURITY: Validate network matches exactly\n if (payload.network !== requirement.network) {\n return {\n valid: false,\n invalidReason: `Network mismatch: expected ${requirement.network}`,\n };\n }\n\n // SECURITY: Validate requirement has valid payTo address\n const walletRegex = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;\n if (!walletRegex.test(requirement.payTo)) {\n return { valid: false, invalidReason: 'Invalid recipient configuration' };\n }\n\n // SECURITY: Validate amount is positive\n let expectedAmount: bigint;\n try {\n expectedAmount = BigInt(requirement.maxAmountRequired);\n if (expectedAmount <= 0n) {\n return { valid: false, invalidReason: 'Invalid payment amount' };\n }\n } catch {\n return { valid: false, invalidReason: 'Invalid payment amount format' };\n }\n\n // Verify on-chain\n const verification = await verifyPayment({\n signature,\n expectedRecipient: requirement.payTo,\n expectedAmount,\n maxAgeSeconds: requirement.maxTimeoutSeconds,\n clientConfig,\n });\n\n if (!verification.valid) {\n return {\n valid: false,\n invalidReason: verification.error || 'Transaction verification failed',\n };\n }\n\n return {\n valid: true,\n settled: verification.confirmed,\n from: verification.from,\n transaction: {\n signature: verification.signature,\n blockTime: verification.blockTime,\n slot: verification.slot,\n },\n };\n}\n\n/**\n * Parse payment payload from X-Payment header (base64 encoded JSON)\n * SECURITY: Safe JSON parsing with try-catch\n * \n * @example\n * const payload = parsePaymentHeader(request.headers.get('x-payment'));\n */\nexport function parsePaymentHeader(header: string): PaymentPayload | null {\n if (!header || typeof header !== 'string') {\n return null;\n }\n\n // SECURITY: Limit header size to prevent DoS\n if (header.length > 10000) {\n return null;\n }\n\n try {\n const decoded = Buffer.from(header, 'base64').toString('utf-8');\n const parsed = JSON.parse(decoded);\n\n // Basic structure validation\n if (!parsed || typeof parsed !== 'object') {\n return null;\n }\n\n return parsed as PaymentPayload;\n } catch {\n return null;\n }\n}\n\n/**\n * Encode PaymentRequirement for X-Payment-Required header\n * Per x402 spec: base64 encoded JSON\n * \n * @example\n * response.headers.set('X-Payment-Required', encodePaymentRequirement(requirement));\n */\nexport function encodePaymentRequirement(requirement: PaymentRequirement): string {\n return Buffer.from(JSON.stringify(requirement)).toString('base64');\n}\n\n/**\n * Encode VerificationResponse for X-Payment-Response header\n */\nexport function encodePaymentResponse(response: VerificationResponse): string {\n return Buffer.from(JSON.stringify(response)).toString('base64');\n}\n\n/**\n * Create a 402 Payment Required response with proper x402 headers\n * \n * @example\n * if (!hasValidPayment) {\n * return create402Response(requirement, { message: 'Payment required' });\n * }\n */\nexport function create402Response(\n requirement: PaymentRequirement,\n body?: object\n): Response {\n const headers = new Headers({\n 'Content-Type': 'application/json',\n 'X-Payment-Required': encodePaymentRequirement(requirement),\n });\n\n const responseBody = body || {\n error: 'Payment Required',\n message: 'This resource requires payment to access',\n x402Version: 1,\n accepts: [requirement],\n };\n\n return new Response(JSON.stringify(responseBody), {\n status: 402,\n headers,\n });\n}\n","// Next.js Middleware for Paywall\n// Zero-boilerplate integration for protected routes\n\nimport type { SignatureStore } from '../store';\nimport type { SessionData, PaymentRequirement } from '../types';\nimport { validateSession } from '../session';\nimport { encodePaymentRequirement } from '../x402';\n\n/**\n * Configuration for paywall middleware\n */\nexport interface PaywallMiddlewareConfig {\n /** Session secret for JWT validation */\n sessionSecret: string;\n /** Protected path patterns (glob-like) */\n protectedPaths: string[];\n /** Cookie name for session token */\n cookieName?: string;\n /** Optional signature store for anti-replay */\n signatureStore?: SignatureStore;\n /** Payment requirement for 402 responses (enables x402 headers) */\n paymentRequirement?: PaymentRequirement | ((path: string) => PaymentRequirement);\n /** Custom 402 response body (deprecated, use paymentRequirement) */\n custom402Response?: (path: string) => object;\n}\n\n/**\n * Result of middleware check\n */\nexport interface MiddlewareResult {\n /** Whether access is allowed */\n allowed: boolean;\n /** Session data if valid */\n session?: SessionData;\n /** Reason for denial */\n reason?: string;\n /** Should respond with 402 */\n requiresPayment?: boolean;\n}\n\n/**\n * Check if path matches any protected pattern\n */\nfunction matchesProtectedPath(path: string, patterns: string[]): boolean {\n for (const pattern of patterns) {\n // Simple glob matching: * matches anything, ** matches path segments\n const regexPattern = pattern\n .replace(/\\*\\*/g, '{{DOUBLE_STAR}}')\n .replace(/\\*/g, '[^/]*')\n .replace(/{{DOUBLE_STAR}}/g, '.*');\n\n const regex = new RegExp(`^${regexPattern}$`);\n if (regex.test(path)) {\n return true;\n }\n }\n return false;\n}\n\n/**\n * Check paywall access for a request\n * Framework-agnostic core logic\n */\nexport async function checkPaywallAccess(\n path: string,\n sessionToken: string | undefined,\n config: PaywallMiddlewareConfig\n): Promise<MiddlewareResult> {\n // Check if path is protected\n if (!matchesProtectedPath(path, config.protectedPaths)) {\n return { allowed: true };\n }\n\n // No session token\n if (!sessionToken) {\n return {\n allowed: false,\n reason: 'No session token',\n requiresPayment: true,\n };\n }\n\n // Validate session\n const validation = await validateSession(sessionToken, config.sessionSecret);\n\n if (!validation.valid || !validation.session) {\n return {\n allowed: false,\n reason: validation.reason || 'Invalid session',\n requiresPayment: true,\n };\n }\n\n return {\n allowed: true,\n session: validation.session,\n };\n}\n\n/**\n * Create Next.js middleware handler\n * \n * @example\n * ```typescript\n * // middleware.ts\n * import { createPaywallMiddleware } from '@alleyboss/micropay-solana-x402-paywall/middleware';\n * \n * export const middleware = createPaywallMiddleware({\n * sessionSecret: process.env.SESSION_SECRET!,\n * protectedPaths: ['/api/premium/*', '/api/content/*'],\n * });\n * \n * export const config = { matcher: ['/api/premium/:path*', '/api/content/:path*'] };\n * ```\n */\nexport function createPaywallMiddleware(config: PaywallMiddlewareConfig) {\n const { cookieName = 'x402_session' } = config;\n\n return async function middleware(request: Request): Promise<Response | null> {\n const url = new URL(request.url);\n const path = url.pathname;\n\n // Get session from cookie header\n const cookieHeader = request.headers.get('cookie') || '';\n const cookies = Object.fromEntries(\n cookieHeader.split(';').map(c => {\n const [key, ...vals] = c.trim().split('=');\n return [key, vals.join('=')];\n })\n );\n const sessionToken = cookies[cookieName];\n\n const result = await checkPaywallAccess(path, sessionToken, config);\n\n if (!result.allowed && result.requiresPayment) {\n // Build x402-compliant 402 response\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n };\n\n // Add X-Payment-Required header if paymentRequirement is configured\n if (config.paymentRequirement) {\n const requirement = typeof config.paymentRequirement === 'function'\n ? config.paymentRequirement(path)\n : config.paymentRequirement;\n headers['X-Payment-Required'] = encodePaymentRequirement(requirement);\n }\n\n const body = config.custom402Response\n ? config.custom402Response(path)\n : {\n error: 'Payment Required',\n message: 'This resource requires payment to access',\n x402Version: 1,\n path,\n };\n\n return new Response(JSON.stringify(body), {\n status: 402,\n headers,\n });\n }\n\n // Allow request to continue\n return null;\n };\n}\n\n/**\n * API route wrapper for Next.js App Router\n * \n * @example\n * ```typescript\n * // app/api/premium/route.ts\n * import { withPaywall } from '@alleyboss/micropay-solana-x402-paywall/middleware';\n * \n * async function handler(request: Request, session: SessionData) {\n * return Response.json({ content: 'Premium content', wallet: session.walletAddress });\n * }\n * \n * export const GET = withPaywall(handler, {\n * sessionSecret: process.env.SESSION_SECRET!,\n * articleId: 'article-123', // Optional: check specific article unlock\n * });\n * ```\n */\nexport function withPaywall<T>(\n handler: (request: Request, session: SessionData) => Promise<T>,\n options: {\n sessionSecret: string;\n cookieName?: string;\n articleId?: string;\n }\n): (request: Request) => Promise<Response | T> {\n const { sessionSecret, cookieName = 'x402_session', articleId } = options;\n\n return async function protectedHandler(request: Request): Promise<Response | T> {\n // Extract session token from cookie\n const cookieHeader = request.headers.get('cookie') || '';\n const cookies = Object.fromEntries(\n cookieHeader.split(';').map(c => {\n const [key, ...vals] = c.trim().split('=');\n return [key, vals.join('=')];\n })\n );\n const sessionToken = cookies[cookieName];\n\n if (!sessionToken) {\n return new Response(\n JSON.stringify({ error: 'Payment Required', message: 'No session token' }),\n { status: 402, headers: { 'Content-Type': 'application/json' } }\n );\n }\n\n // Validate session\n const validation = await validateSession(sessionToken, sessionSecret);\n\n if (!validation.valid || !validation.session) {\n return new Response(\n JSON.stringify({ error: 'Payment Required', message: validation.reason }),\n { status: 402, headers: { 'Content-Type': 'application/json' } }\n );\n }\n\n // Check article-specific unlock if required\n if (articleId) {\n const { session } = validation;\n const hasAccess = session.siteWideUnlock || session.unlockedArticles.includes(articleId);\n\n if (!hasAccess) {\n return new Response(\n JSON.stringify({ error: 'Payment Required', message: 'Article not unlocked' }),\n { status: 402, headers: { 'Content-Type': 'application/json' } }\n );\n }\n }\n\n // Call the actual handler with session\n return handler(request, validation.session);\n };\n}\n","// Express/Node.js Middleware\n// Universal middleware compatible with Express, Fastify, Koa adapters\n\nimport type { SignatureStore } from '../store';\nimport type { SessionData } from '../types';\nimport { validateSession } from '../session';\n\n/**\n * Generic HTTP request/response interfaces for framework compatibility\n * Works with Express, Fastify, raw Node.js http, etc.\n */\nexport interface GenericRequest {\n url?: string;\n path?: string;\n headers: Record<string, string | string[] | undefined> | { get(name: string): string | undefined };\n cookies?: Record<string, string>;\n}\n\nexport interface GenericResponse {\n status?(code: number): GenericResponse;\n statusCode?: number;\n json?(body: object): void;\n send?(body: string): void;\n setHeader?(name: string, value: string): void;\n end?(body?: string): void;\n}\n\nexport type NextFunction = (error?: Error) => void;\n\n/**\n * Express-style middleware configuration\n */\nexport interface ExpressPaywallConfig {\n /** Session secret for JWT validation */\n sessionSecret: string;\n /** Protected path patterns (glob-like) */\n protectedPaths: string[];\n /** Cookie name for session token (default: 'x402_session') */\n cookieName?: string;\n /** Optional signature store */\n signatureStore?: SignatureStore;\n /** Header name for session token (alternative to cookie) */\n headerName?: string;\n /** Custom 402 response */\n onPaymentRequired?: (req: GenericRequest, res: GenericResponse) => void;\n /** Called when access is granted */\n onAccessGranted?: (req: GenericRequest, session: SessionData) => void;\n}\n\n/**\n * Augmented request with session data\n */\nexport interface PaywallRequest extends GenericRequest {\n session?: SessionData;\n}\n\n/**\n * Simple glob pattern matching (supports * and **)\n */\nfunction matchPath(path: string, patterns: string[]): boolean {\n for (const pattern of patterns) {\n const regex = pattern\n .replace(/\\*\\*/g, '<<<GLOBSTAR>>>')\n .replace(/\\*/g, '[^/]*')\n .replace(/<<<GLOBSTAR>>>/g, '.*');\n\n if (new RegExp(`^${regex}$`).test(path)) {\n return true;\n }\n }\n return false;\n}\n\n/**\n * Check if headers object has a get method (Fetch/Next.js style)\n */\nfunction hasGetMethod(headers: GenericRequest['headers']): headers is { get(name: string): string | undefined } {\n return typeof (headers as { get?: unknown }).get === 'function';\n}\n\n/**\n * Get header value from either style of headers object\n */\nfunction getHeader(headers: GenericRequest['headers'], name: string): string | undefined {\n if (hasGetMethod(headers)) {\n return headers.get(name) ?? undefined;\n }\n const value = (headers as Record<string, string | string[] | undefined>)[name];\n return typeof value === 'string' ? value : undefined;\n}\n\n/**\n * Extract session token from request\n */\nfunction extractToken(req: GenericRequest, config: ExpressPaywallConfig): string | undefined {\n const { cookieName = 'x402_session', headerName } = config;\n\n // Try custom header first (for API clients)\n if (headerName) {\n const token = getHeader(req.headers, headerName.toLowerCase());\n if (token) return token;\n }\n\n // Try Authorization header (Bearer token)\n const authHeader = getHeader(req.headers, 'authorization');\n if (authHeader?.startsWith('Bearer ')) {\n return authHeader.slice(7);\n }\n\n // Try cookies\n if (req.cookies?.[cookieName]) {\n return req.cookies[cookieName];\n }\n\n // Parse cookie header manually if cookies not pre-parsed\n const cookieHeader = getHeader(req.headers, 'cookie');\n if (cookieHeader) {\n const match = cookieHeader.split(';')\n .map(c => c.trim())\n .find(c => c.startsWith(`${cookieName}=`));\n\n if (match) {\n return match.slice(cookieName.length + 1);\n }\n }\n\n return undefined;\n}\n\n/**\n * Create Express-compatible paywall middleware\n * Also works with Connect, Polka, and similar frameworks\n * \n * @example\n * ```typescript\n * import express from 'express';\n * import { createExpressMiddleware } from '@alleyboss/micropay-solana-x402-paywall/middleware';\n * \n * const app = express();\n * \n * app.use('/api/premium', createExpressMiddleware({\n * sessionSecret: process.env.SESSION_SECRET!,\n * protectedPaths: ['/**'],\n * }));\n * \n * app.get('/api/premium/content', (req, res) => {\n * res.json({ content: 'Premium!', wallet: req.session?.walletAddress });\n * });\n * ```\n */\nexport function createExpressMiddleware(config: ExpressPaywallConfig) {\n const { sessionSecret, protectedPaths, onPaymentRequired, onAccessGranted } = config;\n\n return async function paywallMiddleware(\n req: PaywallRequest,\n res: GenericResponse,\n next: NextFunction\n ): Promise<void> {\n const path = req.path || req.url?.split('?')[0] || '/';\n\n // Check if path needs protection\n if (!matchPath(path, protectedPaths)) {\n next();\n return;\n }\n\n // Extract token\n const token = extractToken(req, config);\n\n if (!token) {\n if (onPaymentRequired) {\n onPaymentRequired(req, res);\n } else {\n sendPaymentRequired(res, 'No session token');\n }\n return;\n }\n\n // Validate session\n const validation = await validateSession(token, sessionSecret);\n\n if (!validation.valid || !validation.session) {\n if (onPaymentRequired) {\n onPaymentRequired(req, res);\n } else {\n sendPaymentRequired(res, validation.reason || 'Invalid session');\n }\n return;\n }\n\n // Attach session to request\n req.session = validation.session;\n\n if (onAccessGranted) {\n onAccessGranted(req, validation.session);\n }\n\n next();\n };\n}\n\n/**\n * Send 402 Payment Required response\n */\nfunction sendPaymentRequired(res: GenericResponse, reason: string): void {\n const body = JSON.stringify({\n error: 'Payment Required',\n message: reason,\n });\n\n // Check for Express-style response (has both status and json methods)\n const statusFn = res.status;\n const jsonFn = res.json;\n\n if (statusFn && jsonFn) {\n // Express-style: res.status(402).json(...)\n const chainedRes = statusFn.call(res, 402);\n if (chainedRes?.json) {\n chainedRes.json({ error: 'Payment Required', message: reason });\n }\n } else if (res.statusCode !== undefined && res.setHeader && res.end) {\n // Raw Node.js http\n res.statusCode = 402;\n res.setHeader('Content-Type', 'application/json');\n res.end(body);\n }\n}\n\n/**\n * Fastify-compatible plugin factory\n * \n * @example\n * ```typescript\n * import Fastify from 'fastify';\n * import { createFastifyPlugin } from '@alleyboss/micropay-solana-x402-paywall/middleware';\n * \n * const fastify = Fastify();\n * fastify.register(createFastifyPlugin({\n * sessionSecret: process.env.SESSION_SECRET!,\n * protectedPaths: ['/api/premium/*'],\n * }));\n * ```\n */\nexport function createFastifyPlugin(config: ExpressPaywallConfig) {\n return async function paywallPlugin(fastify: any) {\n fastify.addHook('preHandler', createExpressMiddleware(config));\n };\n}\n"]}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { S as SignatureStore } from './memory-Daxkczti.cjs';
|
|
2
|
+
import { P as PaymentRequirement } from './payment-BGp7eMQl.cjs';
|
|
2
3
|
import { S as SessionData } from './session-D2IoWAWV.cjs';
|
|
3
4
|
|
|
4
5
|
/**
|
|
@@ -13,7 +14,9 @@ interface PaywallMiddlewareConfig {
|
|
|
13
14
|
cookieName?: string;
|
|
14
15
|
/** Optional signature store for anti-replay */
|
|
15
16
|
signatureStore?: SignatureStore;
|
|
16
|
-
/**
|
|
17
|
+
/** Payment requirement for 402 responses (enables x402 headers) */
|
|
18
|
+
paymentRequirement?: PaymentRequirement | ((path: string) => PaymentRequirement);
|
|
19
|
+
/** Custom 402 response body (deprecated, use paymentRequirement) */
|
|
17
20
|
custom402Response?: (path: string) => object;
|
|
18
21
|
}
|
|
19
22
|
/**
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { S as SignatureStore } from './memory-Daxkczti.js';
|
|
2
|
+
import { P as PaymentRequirement } from './payment-BGp7eMQl.js';
|
|
2
3
|
import { S as SessionData } from './session-D2IoWAWV.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
@@ -13,7 +14,9 @@ interface PaywallMiddlewareConfig {
|
|
|
13
14
|
cookieName?: string;
|
|
14
15
|
/** Optional signature store for anti-replay */
|
|
15
16
|
signatureStore?: SignatureStore;
|
|
16
|
-
/**
|
|
17
|
+
/** Payment requirement for 402 responses (enables x402 headers) */
|
|
18
|
+
paymentRequirement?: PaymentRequirement | ((path: string) => PaymentRequirement);
|
|
19
|
+
/** Custom 402 response body (deprecated, use paymentRequirement) */
|
|
17
20
|
custom402Response?: (path: string) => object;
|
|
18
21
|
}
|
|
19
22
|
/**
|
|
@@ -76,6 +76,8 @@ interface VerificationResponse {
|
|
|
76
76
|
invalidReason?: string;
|
|
77
77
|
/** Whether the transaction is settled on-chain */
|
|
78
78
|
settled?: boolean;
|
|
79
|
+
/** Sender wallet address (payer) */
|
|
80
|
+
from?: string;
|
|
79
81
|
/** Transaction details */
|
|
80
82
|
transaction?: {
|
|
81
83
|
signature: string;
|
|
@@ -76,6 +76,8 @@ interface VerificationResponse {
|
|
|
76
76
|
invalidReason?: string;
|
|
77
77
|
/** Whether the transaction is settled on-chain */
|
|
78
78
|
settled?: boolean;
|
|
79
|
+
/** Sender wallet address (payer) */
|
|
80
|
+
from?: string;
|
|
79
81
|
/** Transaction details */
|
|
80
82
|
transaction?: {
|
|
81
83
|
signature: string;
|