@relai-fi/x402 0.5.34 → 0.5.35

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/dist/plugins.cjs CHANGED
@@ -88,6 +88,16 @@ function freeTier(config) {
88
88
  }
89
89
  async function syncConfig() {
90
90
  try {
91
+ const existing = await fetch(`${base}/v1/plugins/free-tier/config`, {
92
+ method: "GET",
93
+ headers: resolveHeaders()
94
+ });
95
+ if (existing.ok) {
96
+ const data = await existing.json();
97
+ if (data.configs && data.configs.length > 0) {
98
+ return;
99
+ }
100
+ }
91
101
  const paths = config.paths ?? ["*"];
92
102
  await fetch(`${base}/v1/plugins/free-tier/config`, {
93
103
  method: "PUT",
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/plugins.ts"],"sourcesContent":["// src/plugins.ts\n// RelAI Plugin System - extensible middleware hooks for Relai.protect()\n\nimport type { RelaiNetwork } from './types';\nimport type { SettleResult } from './server';\n\nconst RELAI_API_BASE = 'https://api.relai.fi';\n\n// ============================================================================\n// Plugin Interface\n// ============================================================================\n\nexport interface PluginContext {\n /** Network for this endpoint */\n network: RelaiNetwork;\n /** Price in USD */\n price: number;\n /** Request path */\n path: string;\n /** HTTP method */\n method: string;\n}\n\nexport interface PluginResult {\n /** If true, skip payment and serve content for free */\n skip?: boolean;\n /** Extra response headers to set */\n headers?: Record<string, string>;\n /** Metadata attached to req.pluginMeta */\n meta?: Record<string, unknown>;\n}\n\nexport interface RelaiPlugin {\n /** Unique plugin name */\n name: string;\n\n /**\n * Called before the 402 payment check.\n * Return { skip: true } to bypass payment entirely.\n */\n beforePaymentCheck?(req: any, ctx: PluginContext): Promise<PluginResult>;\n\n /**\n * Called after a successful payment settlement.\n * Use for analytics, logging, webhooks, etc.\n */\n afterSettled?(req: any, result: SettleResult, ctx: PluginContext): Promise<void>;\n\n /**\n * Called once when the Relai instance initializes (server start).\n * Use to sync config to RelAI backend or validate credentials.\n */\n onInit?(): Promise<void>;\n}\n\n// ============================================================================\n// Free Tier Plugin\n// ============================================================================\n\nexport interface FreeTierPluginConfig {\n /** Service key (sk_live_...) for authenticating with RelAI API */\n serviceKey: string;\n /** Max free calls per buyer per period */\n perBuyerLimit: number;\n /** Reset period for per-buyer counters */\n resetPeriod?: 'never' | 'daily' | 'monthly';\n /** Optional global cap across all buyers */\n globalCap?: number;\n /** Specific paths to apply free tier to (default: '*' = all) */\n paths?: string[];\n /** Override RelAI API base URL (default: https://api.relai.fi) */\n baseUrl?: string;\n /** Cache TTL in ms for check results (default: 5000) */\n cacheTtlMs?: number;\n}\n\ninterface FreeTierCheckResponse {\n free: boolean;\n remaining?: number;\n total?: number;\n reason?: string;\n globalRemaining?: number;\n}\n\n/**\n * Free Tier plugin - gives buyers a number of free API calls\n * before requiring payment.\n *\n * State is stored in the RelAI backend, keyed by your service key.\n * Config can be set here (SDK-side) or overridden in the relai.fi dashboard.\n *\n * @example\n * ```typescript\n * import Relai from '@relai-fi/x402/server';\n * import { freeTier } from '@relai-fi/x402/plugins';\n *\n * const relai = new Relai({\n * network: 'base',\n * plugins: [\n * freeTier({\n * serviceKey: process.env.RELAI_SERVICE_KEY!,\n * perBuyerLimit: 10,\n * resetPeriod: 'daily',\n * }),\n * ],\n * });\n *\n * app.get('/api/data', relai.protect({\n * payTo: '0xYourWallet',\n * price: 0.01,\n * }), (req, res) => {\n * res.json({ data: 'paid content' });\n * });\n * ```\n */\nexport function freeTier(config: FreeTierPluginConfig): RelaiPlugin {\n const base = (config.baseUrl ?? RELAI_API_BASE).replace(/\\/$/, '');\n const cacheTtl = config.cacheTtlMs ?? 5000;\n\n // Simple in-memory cache: \"serviceKey:path:buyerId\" -> { result, expiresAt }\n const cache = new Map<string, { result: FreeTierCheckResponse; expiresAt: number }>();\n\n function resolveHeaders(): Record<string, string> {\n return {\n 'X-Service-Key': config.serviceKey,\n 'Content-Type': 'application/json',\n };\n }\n\n /**\n * Resolve buyer identity from the request.\n * Priority: JWT sub > x-wallet-address > IP fallback\n */\n function resolveBuyerId(req: any): string {\n // 1. JWT Bearer token\n try {\n const auth = req.headers?.authorization || '';\n if (auth.startsWith('Bearer ')) {\n const token = auth.slice(7).trim();\n const payload = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString());\n if (payload?.sub) return `user:${payload.sub}`;\n }\n } catch { /* ignore */ }\n\n // 2. Explicit wallet header\n const wallet = req.headers?.['x-wallet-address'] || req.headers?.['x-buyer-address'];\n if (wallet) return `wallet:${wallet}`;\n\n // 3. IP fallback\n const ip =\n (req.headers?.['x-forwarded-for'] || '').split(',')[0].trim() ||\n req.socket?.remoteAddress ||\n req.ip ||\n 'unknown';\n return `ip:${ip}`;\n }\n\n function cacheKey(path: string, buyerId: string): string {\n return `${config.serviceKey}:${path}:${buyerId}`;\n }\n\n async function checkFreeTier(path: string, buyerId: string): Promise<FreeTierCheckResponse> {\n const key = cacheKey(path, buyerId);\n const now = Date.now();\n const cached = cache.get(key);\n if (cached && cached.expiresAt > now) {\n return cached.result;\n }\n\n try {\n const res = await fetch(`${base}/v1/plugins/free-tier/check`, {\n method: 'POST',\n headers: resolveHeaders(),\n body: JSON.stringify({ path, buyerId }),\n });\n\n if (!res.ok) {\n // Non-blocking: if API unreachable, default to not-free\n return { free: false, reason: `api_error_${res.status}` };\n }\n\n const result = await res.json() as FreeTierCheckResponse;\n cache.set(key, { result, expiresAt: now + cacheTtl });\n return result;\n } catch (err) {\n // Network error - non-blocking, default to paid\n return { free: false, reason: 'network_error' };\n }\n }\n\n async function recordCall(path: string, buyerId: string): Promise<void> {\n try {\n await fetch(`${base}/v1/plugins/free-tier/record`, {\n method: 'POST',\n headers: resolveHeaders(),\n body: JSON.stringify({ path, buyerId }),\n });\n // Invalidate cache for this buyer+path after recording\n cache.delete(cacheKey(path, buyerId));\n } catch {\n // Fire-and-forget\n }\n }\n\n async function syncConfig(): Promise<void> {\n try {\n const paths = config.paths ?? ['*'];\n await fetch(`${base}/v1/plugins/free-tier/config`, {\n method: 'PUT',\n headers: resolveHeaders(),\n body: JSON.stringify({\n perBuyerLimit: config.perBuyerLimit,\n resetPeriod: config.resetPeriod ?? 'never',\n globalCap: config.globalCap ?? null,\n paths,\n }),\n });\n } catch (err) {\n console.warn(`[relai:freeTier] Failed to sync config to RelAI: ${err}`);\n }\n }\n\n return {\n name: 'free-tier',\n\n async onInit() {\n await syncConfig();\n },\n\n async beforePaymentCheck(req, ctx) {\n const requestPath = ctx.path || '/';\n\n // Check if this path is covered by the plugin\n const paths = config.paths ?? ['*'];\n const pathMatches = paths.includes('*') || paths.some((p) => {\n const normalized = p.toLowerCase().replace(/\\/+$/, '') || '/';\n const reqNormalized = requestPath.toLowerCase().replace(/\\/+$/, '') || '/';\n return normalized === reqNormalized || normalized === '*';\n });\n\n if (!pathMatches) {\n return {};\n }\n\n const buyerId = resolveBuyerId(req);\n const result = await checkFreeTier(requestPath, buyerId);\n\n if (result.free) {\n // Record the call (fire-and-forget)\n recordCall(requestPath, buyerId).catch(() => {});\n\n return {\n skip: true,\n headers: {\n 'X-Free-Calls-Remaining': String(result.remaining ?? 0),\n 'X-Free-Calls-Total': String(result.total ?? config.perBuyerLimit),\n ...(result.globalRemaining != null\n ? { 'X-Free-Calls-Global-Remaining': String(result.globalRemaining) }\n : {}),\n },\n meta: {\n freeTier: true,\n buyerId,\n remaining: result.remaining,\n total: result.total ?? config.perBuyerLimit,\n },\n };\n }\n\n return {};\n },\n };\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/plugins.ts"],"sourcesContent":["// src/plugins.ts\n// RelAI Plugin System - extensible middleware hooks for Relai.protect()\n\nimport type { RelaiNetwork } from './types';\nimport type { SettleResult } from './server';\n\nconst RELAI_API_BASE = 'https://api.relai.fi';\n\n// ============================================================================\n// Plugin Interface\n// ============================================================================\n\nexport interface PluginContext {\n /** Network for this endpoint */\n network: RelaiNetwork;\n /** Price in USD */\n price: number;\n /** Request path */\n path: string;\n /** HTTP method */\n method: string;\n}\n\nexport interface PluginResult {\n /** If true, skip payment and serve content for free */\n skip?: boolean;\n /** Extra response headers to set */\n headers?: Record<string, string>;\n /** Metadata attached to req.pluginMeta */\n meta?: Record<string, unknown>;\n}\n\nexport interface RelaiPlugin {\n /** Unique plugin name */\n name: string;\n\n /**\n * Called before the 402 payment check.\n * Return { skip: true } to bypass payment entirely.\n */\n beforePaymentCheck?(req: any, ctx: PluginContext): Promise<PluginResult>;\n\n /**\n * Called after a successful payment settlement.\n * Use for analytics, logging, webhooks, etc.\n */\n afterSettled?(req: any, result: SettleResult, ctx: PluginContext): Promise<void>;\n\n /**\n * Called once when the Relai instance initializes (server start).\n * Use to sync config to RelAI backend or validate credentials.\n */\n onInit?(): Promise<void>;\n}\n\n// ============================================================================\n// Free Tier Plugin\n// ============================================================================\n\nexport interface FreeTierPluginConfig {\n /** Service key (sk_live_...) for authenticating with RelAI API */\n serviceKey: string;\n /** Max free calls per buyer per period */\n perBuyerLimit: number;\n /** Reset period for per-buyer counters */\n resetPeriod?: 'never' | 'daily' | 'monthly';\n /** Optional global cap across all buyers */\n globalCap?: number;\n /** Specific paths to apply free tier to (default: '*' = all) */\n paths?: string[];\n /** Override RelAI API base URL (default: https://api.relai.fi) */\n baseUrl?: string;\n /** Cache TTL in ms for check results (default: 5000) */\n cacheTtlMs?: number;\n}\n\ninterface FreeTierCheckResponse {\n free: boolean;\n remaining?: number;\n total?: number;\n reason?: string;\n globalRemaining?: number;\n}\n\n/**\n * Free Tier plugin - gives buyers a number of free API calls\n * before requiring payment.\n *\n * State is stored in the RelAI backend, keyed by your service key.\n * Config can be set here (SDK-side) or overridden in the relai.fi dashboard.\n *\n * @example\n * ```typescript\n * import Relai from '@relai-fi/x402/server';\n * import { freeTier } from '@relai-fi/x402/plugins';\n *\n * const relai = new Relai({\n * network: 'base',\n * plugins: [\n * freeTier({\n * serviceKey: process.env.RELAI_SERVICE_KEY!,\n * perBuyerLimit: 10,\n * resetPeriod: 'daily',\n * }),\n * ],\n * });\n *\n * app.get('/api/data', relai.protect({\n * payTo: '0xYourWallet',\n * price: 0.01,\n * }), (req, res) => {\n * res.json({ data: 'paid content' });\n * });\n * ```\n */\nexport function freeTier(config: FreeTierPluginConfig): RelaiPlugin {\n const base = (config.baseUrl ?? RELAI_API_BASE).replace(/\\/$/, '');\n const cacheTtl = config.cacheTtlMs ?? 5000;\n\n // Simple in-memory cache: \"serviceKey:path:buyerId\" -> { result, expiresAt }\n const cache = new Map<string, { result: FreeTierCheckResponse; expiresAt: number }>();\n\n function resolveHeaders(): Record<string, string> {\n return {\n 'X-Service-Key': config.serviceKey,\n 'Content-Type': 'application/json',\n };\n }\n\n /**\n * Resolve buyer identity from the request.\n * Priority: JWT sub > x-wallet-address > IP fallback\n */\n function resolveBuyerId(req: any): string {\n // 1. JWT Bearer token\n try {\n const auth = req.headers?.authorization || '';\n if (auth.startsWith('Bearer ')) {\n const token = auth.slice(7).trim();\n const payload = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString());\n if (payload?.sub) return `user:${payload.sub}`;\n }\n } catch { /* ignore */ }\n\n // 2. Explicit wallet header\n const wallet = req.headers?.['x-wallet-address'] || req.headers?.['x-buyer-address'];\n if (wallet) return `wallet:${wallet}`;\n\n // 3. IP fallback\n const ip =\n (req.headers?.['x-forwarded-for'] || '').split(',')[0].trim() ||\n req.socket?.remoteAddress ||\n req.ip ||\n 'unknown';\n return `ip:${ip}`;\n }\n\n function cacheKey(path: string, buyerId: string): string {\n return `${config.serviceKey}:${path}:${buyerId}`;\n }\n\n async function checkFreeTier(path: string, buyerId: string): Promise<FreeTierCheckResponse> {\n const key = cacheKey(path, buyerId);\n const now = Date.now();\n const cached = cache.get(key);\n if (cached && cached.expiresAt > now) {\n return cached.result;\n }\n\n try {\n const res = await fetch(`${base}/v1/plugins/free-tier/check`, {\n method: 'POST',\n headers: resolveHeaders(),\n body: JSON.stringify({ path, buyerId }),\n });\n\n if (!res.ok) {\n // Non-blocking: if API unreachable, default to not-free\n return { free: false, reason: `api_error_${res.status}` };\n }\n\n const result = await res.json() as FreeTierCheckResponse;\n cache.set(key, { result, expiresAt: now + cacheTtl });\n return result;\n } catch (err) {\n // Network error - non-blocking, default to paid\n return { free: false, reason: 'network_error' };\n }\n }\n\n async function recordCall(path: string, buyerId: string): Promise<void> {\n try {\n await fetch(`${base}/v1/plugins/free-tier/record`, {\n method: 'POST',\n headers: resolveHeaders(),\n body: JSON.stringify({ path, buyerId }),\n });\n // Invalidate cache for this buyer+path after recording\n cache.delete(cacheKey(path, buyerId));\n } catch {\n // Fire-and-forget\n }\n }\n\n async function syncConfig(): Promise<void> {\n try {\n // Check if config already exists on backend — don't overwrite dashboard-managed configs\n const existing = await fetch(`${base}/v1/plugins/free-tier/config`, {\n method: 'GET',\n headers: resolveHeaders(),\n });\n if (existing.ok) {\n const data = await existing.json();\n if (data.configs && data.configs.length > 0) {\n return; // Config managed via dashboard or previous sync — skip\n }\n }\n\n const paths = config.paths ?? ['*'];\n await fetch(`${base}/v1/plugins/free-tier/config`, {\n method: 'PUT',\n headers: resolveHeaders(),\n body: JSON.stringify({\n perBuyerLimit: config.perBuyerLimit,\n resetPeriod: config.resetPeriod ?? 'never',\n globalCap: config.globalCap ?? null,\n paths,\n }),\n });\n } catch (err) {\n console.warn(`[relai:freeTier] Failed to sync config to RelAI: ${err}`);\n }\n }\n\n return {\n name: 'free-tier',\n\n async onInit() {\n await syncConfig();\n },\n\n async beforePaymentCheck(req, ctx) {\n const requestPath = ctx.path || '/';\n\n // Check if this path is covered by the plugin\n const paths = config.paths ?? ['*'];\n const pathMatches = paths.includes('*') || paths.some((p) => {\n const normalized = p.toLowerCase().replace(/\\/+$/, '') || '/';\n const reqNormalized = requestPath.toLowerCase().replace(/\\/+$/, '') || '/';\n return normalized === reqNormalized || normalized === '*';\n });\n\n if (!pathMatches) {\n return {};\n }\n\n const buyerId = resolveBuyerId(req);\n const result = await checkFreeTier(requestPath, buyerId);\n\n if (result.free) {\n // Record the call (fire-and-forget)\n recordCall(requestPath, buyerId).catch(() => {});\n\n return {\n skip: true,\n headers: {\n 'X-Free-Calls-Remaining': String(result.remaining ?? 0),\n 'X-Free-Calls-Total': String(result.total ?? config.perBuyerLimit),\n ...(result.globalRemaining != null\n ? { 'X-Free-Calls-Global-Remaining': String(result.globalRemaining) }\n : {}),\n },\n meta: {\n freeTier: true,\n buyerId,\n remaining: result.remaining,\n total: result.total ?? config.perBuyerLimit,\n },\n };\n }\n\n return {};\n },\n };\n}\n"],"mappings":"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","names":[]}
package/dist/plugins.js CHANGED
@@ -64,6 +64,16 @@ function freeTier(config) {
64
64
  }
65
65
  async function syncConfig() {
66
66
  try {
67
+ const existing = await fetch(`${base}/v1/plugins/free-tier/config`, {
68
+ method: "GET",
69
+ headers: resolveHeaders()
70
+ });
71
+ if (existing.ok) {
72
+ const data = await existing.json();
73
+ if (data.configs && data.configs.length > 0) {
74
+ return;
75
+ }
76
+ }
67
77
  const paths = config.paths ?? ["*"];
68
78
  await fetch(`${base}/v1/plugins/free-tier/config`, {
69
79
  method: "PUT",
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/plugins.ts"],"sourcesContent":["// src/plugins.ts\n// RelAI Plugin System - extensible middleware hooks for Relai.protect()\n\nimport type { RelaiNetwork } from './types';\nimport type { SettleResult } from './server';\n\nconst RELAI_API_BASE = 'https://api.relai.fi';\n\n// ============================================================================\n// Plugin Interface\n// ============================================================================\n\nexport interface PluginContext {\n /** Network for this endpoint */\n network: RelaiNetwork;\n /** Price in USD */\n price: number;\n /** Request path */\n path: string;\n /** HTTP method */\n method: string;\n}\n\nexport interface PluginResult {\n /** If true, skip payment and serve content for free */\n skip?: boolean;\n /** Extra response headers to set */\n headers?: Record<string, string>;\n /** Metadata attached to req.pluginMeta */\n meta?: Record<string, unknown>;\n}\n\nexport interface RelaiPlugin {\n /** Unique plugin name */\n name: string;\n\n /**\n * Called before the 402 payment check.\n * Return { skip: true } to bypass payment entirely.\n */\n beforePaymentCheck?(req: any, ctx: PluginContext): Promise<PluginResult>;\n\n /**\n * Called after a successful payment settlement.\n * Use for analytics, logging, webhooks, etc.\n */\n afterSettled?(req: any, result: SettleResult, ctx: PluginContext): Promise<void>;\n\n /**\n * Called once when the Relai instance initializes (server start).\n * Use to sync config to RelAI backend or validate credentials.\n */\n onInit?(): Promise<void>;\n}\n\n// ============================================================================\n// Free Tier Plugin\n// ============================================================================\n\nexport interface FreeTierPluginConfig {\n /** Service key (sk_live_...) for authenticating with RelAI API */\n serviceKey: string;\n /** Max free calls per buyer per period */\n perBuyerLimit: number;\n /** Reset period for per-buyer counters */\n resetPeriod?: 'never' | 'daily' | 'monthly';\n /** Optional global cap across all buyers */\n globalCap?: number;\n /** Specific paths to apply free tier to (default: '*' = all) */\n paths?: string[];\n /** Override RelAI API base URL (default: https://api.relai.fi) */\n baseUrl?: string;\n /** Cache TTL in ms for check results (default: 5000) */\n cacheTtlMs?: number;\n}\n\ninterface FreeTierCheckResponse {\n free: boolean;\n remaining?: number;\n total?: number;\n reason?: string;\n globalRemaining?: number;\n}\n\n/**\n * Free Tier plugin - gives buyers a number of free API calls\n * before requiring payment.\n *\n * State is stored in the RelAI backend, keyed by your service key.\n * Config can be set here (SDK-side) or overridden in the relai.fi dashboard.\n *\n * @example\n * ```typescript\n * import Relai from '@relai-fi/x402/server';\n * import { freeTier } from '@relai-fi/x402/plugins';\n *\n * const relai = new Relai({\n * network: 'base',\n * plugins: [\n * freeTier({\n * serviceKey: process.env.RELAI_SERVICE_KEY!,\n * perBuyerLimit: 10,\n * resetPeriod: 'daily',\n * }),\n * ],\n * });\n *\n * app.get('/api/data', relai.protect({\n * payTo: '0xYourWallet',\n * price: 0.01,\n * }), (req, res) => {\n * res.json({ data: 'paid content' });\n * });\n * ```\n */\nexport function freeTier(config: FreeTierPluginConfig): RelaiPlugin {\n const base = (config.baseUrl ?? RELAI_API_BASE).replace(/\\/$/, '');\n const cacheTtl = config.cacheTtlMs ?? 5000;\n\n // Simple in-memory cache: \"serviceKey:path:buyerId\" -> { result, expiresAt }\n const cache = new Map<string, { result: FreeTierCheckResponse; expiresAt: number }>();\n\n function resolveHeaders(): Record<string, string> {\n return {\n 'X-Service-Key': config.serviceKey,\n 'Content-Type': 'application/json',\n };\n }\n\n /**\n * Resolve buyer identity from the request.\n * Priority: JWT sub > x-wallet-address > IP fallback\n */\n function resolveBuyerId(req: any): string {\n // 1. JWT Bearer token\n try {\n const auth = req.headers?.authorization || '';\n if (auth.startsWith('Bearer ')) {\n const token = auth.slice(7).trim();\n const payload = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString());\n if (payload?.sub) return `user:${payload.sub}`;\n }\n } catch { /* ignore */ }\n\n // 2. Explicit wallet header\n const wallet = req.headers?.['x-wallet-address'] || req.headers?.['x-buyer-address'];\n if (wallet) return `wallet:${wallet}`;\n\n // 3. IP fallback\n const ip =\n (req.headers?.['x-forwarded-for'] || '').split(',')[0].trim() ||\n req.socket?.remoteAddress ||\n req.ip ||\n 'unknown';\n return `ip:${ip}`;\n }\n\n function cacheKey(path: string, buyerId: string): string {\n return `${config.serviceKey}:${path}:${buyerId}`;\n }\n\n async function checkFreeTier(path: string, buyerId: string): Promise<FreeTierCheckResponse> {\n const key = cacheKey(path, buyerId);\n const now = Date.now();\n const cached = cache.get(key);\n if (cached && cached.expiresAt > now) {\n return cached.result;\n }\n\n try {\n const res = await fetch(`${base}/v1/plugins/free-tier/check`, {\n method: 'POST',\n headers: resolveHeaders(),\n body: JSON.stringify({ path, buyerId }),\n });\n\n if (!res.ok) {\n // Non-blocking: if API unreachable, default to not-free\n return { free: false, reason: `api_error_${res.status}` };\n }\n\n const result = await res.json() as FreeTierCheckResponse;\n cache.set(key, { result, expiresAt: now + cacheTtl });\n return result;\n } catch (err) {\n // Network error - non-blocking, default to paid\n return { free: false, reason: 'network_error' };\n }\n }\n\n async function recordCall(path: string, buyerId: string): Promise<void> {\n try {\n await fetch(`${base}/v1/plugins/free-tier/record`, {\n method: 'POST',\n headers: resolveHeaders(),\n body: JSON.stringify({ path, buyerId }),\n });\n // Invalidate cache for this buyer+path after recording\n cache.delete(cacheKey(path, buyerId));\n } catch {\n // Fire-and-forget\n }\n }\n\n async function syncConfig(): Promise<void> {\n try {\n const paths = config.paths ?? ['*'];\n await fetch(`${base}/v1/plugins/free-tier/config`, {\n method: 'PUT',\n headers: resolveHeaders(),\n body: JSON.stringify({\n perBuyerLimit: config.perBuyerLimit,\n resetPeriod: config.resetPeriod ?? 'never',\n globalCap: config.globalCap ?? null,\n paths,\n }),\n });\n } catch (err) {\n console.warn(`[relai:freeTier] Failed to sync config to RelAI: ${err}`);\n }\n }\n\n return {\n name: 'free-tier',\n\n async onInit() {\n await syncConfig();\n },\n\n async beforePaymentCheck(req, ctx) {\n const requestPath = ctx.path || '/';\n\n // Check if this path is covered by the plugin\n const paths = config.paths ?? ['*'];\n const pathMatches = paths.includes('*') || paths.some((p) => {\n const normalized = p.toLowerCase().replace(/\\/+$/, '') || '/';\n const reqNormalized = requestPath.toLowerCase().replace(/\\/+$/, '') || '/';\n return normalized === reqNormalized || normalized === '*';\n });\n\n if (!pathMatches) {\n return {};\n }\n\n const buyerId = resolveBuyerId(req);\n const result = await checkFreeTier(requestPath, buyerId);\n\n if (result.free) {\n // Record the call (fire-and-forget)\n recordCall(requestPath, buyerId).catch(() => {});\n\n return {\n skip: true,\n headers: {\n 'X-Free-Calls-Remaining': String(result.remaining ?? 0),\n 'X-Free-Calls-Total': String(result.total ?? config.perBuyerLimit),\n ...(result.globalRemaining != null\n ? { 'X-Free-Calls-Global-Remaining': String(result.globalRemaining) }\n : {}),\n },\n meta: {\n freeTier: true,\n buyerId,\n remaining: result.remaining,\n total: result.total ?? config.perBuyerLimit,\n },\n };\n }\n\n return {};\n },\n };\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/plugins.ts"],"sourcesContent":["// src/plugins.ts\n// RelAI Plugin System - extensible middleware hooks for Relai.protect()\n\nimport type { RelaiNetwork } from './types';\nimport type { SettleResult } from './server';\n\nconst RELAI_API_BASE = 'https://api.relai.fi';\n\n// ============================================================================\n// Plugin Interface\n// ============================================================================\n\nexport interface PluginContext {\n /** Network for this endpoint */\n network: RelaiNetwork;\n /** Price in USD */\n price: number;\n /** Request path */\n path: string;\n /** HTTP method */\n method: string;\n}\n\nexport interface PluginResult {\n /** If true, skip payment and serve content for free */\n skip?: boolean;\n /** Extra response headers to set */\n headers?: Record<string, string>;\n /** Metadata attached to req.pluginMeta */\n meta?: Record<string, unknown>;\n}\n\nexport interface RelaiPlugin {\n /** Unique plugin name */\n name: string;\n\n /**\n * Called before the 402 payment check.\n * Return { skip: true } to bypass payment entirely.\n */\n beforePaymentCheck?(req: any, ctx: PluginContext): Promise<PluginResult>;\n\n /**\n * Called after a successful payment settlement.\n * Use for analytics, logging, webhooks, etc.\n */\n afterSettled?(req: any, result: SettleResult, ctx: PluginContext): Promise<void>;\n\n /**\n * Called once when the Relai instance initializes (server start).\n * Use to sync config to RelAI backend or validate credentials.\n */\n onInit?(): Promise<void>;\n}\n\n// ============================================================================\n// Free Tier Plugin\n// ============================================================================\n\nexport interface FreeTierPluginConfig {\n /** Service key (sk_live_...) for authenticating with RelAI API */\n serviceKey: string;\n /** Max free calls per buyer per period */\n perBuyerLimit: number;\n /** Reset period for per-buyer counters */\n resetPeriod?: 'never' | 'daily' | 'monthly';\n /** Optional global cap across all buyers */\n globalCap?: number;\n /** Specific paths to apply free tier to (default: '*' = all) */\n paths?: string[];\n /** Override RelAI API base URL (default: https://api.relai.fi) */\n baseUrl?: string;\n /** Cache TTL in ms for check results (default: 5000) */\n cacheTtlMs?: number;\n}\n\ninterface FreeTierCheckResponse {\n free: boolean;\n remaining?: number;\n total?: number;\n reason?: string;\n globalRemaining?: number;\n}\n\n/**\n * Free Tier plugin - gives buyers a number of free API calls\n * before requiring payment.\n *\n * State is stored in the RelAI backend, keyed by your service key.\n * Config can be set here (SDK-side) or overridden in the relai.fi dashboard.\n *\n * @example\n * ```typescript\n * import Relai from '@relai-fi/x402/server';\n * import { freeTier } from '@relai-fi/x402/plugins';\n *\n * const relai = new Relai({\n * network: 'base',\n * plugins: [\n * freeTier({\n * serviceKey: process.env.RELAI_SERVICE_KEY!,\n * perBuyerLimit: 10,\n * resetPeriod: 'daily',\n * }),\n * ],\n * });\n *\n * app.get('/api/data', relai.protect({\n * payTo: '0xYourWallet',\n * price: 0.01,\n * }), (req, res) => {\n * res.json({ data: 'paid content' });\n * });\n * ```\n */\nexport function freeTier(config: FreeTierPluginConfig): RelaiPlugin {\n const base = (config.baseUrl ?? RELAI_API_BASE).replace(/\\/$/, '');\n const cacheTtl = config.cacheTtlMs ?? 5000;\n\n // Simple in-memory cache: \"serviceKey:path:buyerId\" -> { result, expiresAt }\n const cache = new Map<string, { result: FreeTierCheckResponse; expiresAt: number }>();\n\n function resolveHeaders(): Record<string, string> {\n return {\n 'X-Service-Key': config.serviceKey,\n 'Content-Type': 'application/json',\n };\n }\n\n /**\n * Resolve buyer identity from the request.\n * Priority: JWT sub > x-wallet-address > IP fallback\n */\n function resolveBuyerId(req: any): string {\n // 1. JWT Bearer token\n try {\n const auth = req.headers?.authorization || '';\n if (auth.startsWith('Bearer ')) {\n const token = auth.slice(7).trim();\n const payload = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString());\n if (payload?.sub) return `user:${payload.sub}`;\n }\n } catch { /* ignore */ }\n\n // 2. Explicit wallet header\n const wallet = req.headers?.['x-wallet-address'] || req.headers?.['x-buyer-address'];\n if (wallet) return `wallet:${wallet}`;\n\n // 3. IP fallback\n const ip =\n (req.headers?.['x-forwarded-for'] || '').split(',')[0].trim() ||\n req.socket?.remoteAddress ||\n req.ip ||\n 'unknown';\n return `ip:${ip}`;\n }\n\n function cacheKey(path: string, buyerId: string): string {\n return `${config.serviceKey}:${path}:${buyerId}`;\n }\n\n async function checkFreeTier(path: string, buyerId: string): Promise<FreeTierCheckResponse> {\n const key = cacheKey(path, buyerId);\n const now = Date.now();\n const cached = cache.get(key);\n if (cached && cached.expiresAt > now) {\n return cached.result;\n }\n\n try {\n const res = await fetch(`${base}/v1/plugins/free-tier/check`, {\n method: 'POST',\n headers: resolveHeaders(),\n body: JSON.stringify({ path, buyerId }),\n });\n\n if (!res.ok) {\n // Non-blocking: if API unreachable, default to not-free\n return { free: false, reason: `api_error_${res.status}` };\n }\n\n const result = await res.json() as FreeTierCheckResponse;\n cache.set(key, { result, expiresAt: now + cacheTtl });\n return result;\n } catch (err) {\n // Network error - non-blocking, default to paid\n return { free: false, reason: 'network_error' };\n }\n }\n\n async function recordCall(path: string, buyerId: string): Promise<void> {\n try {\n await fetch(`${base}/v1/plugins/free-tier/record`, {\n method: 'POST',\n headers: resolveHeaders(),\n body: JSON.stringify({ path, buyerId }),\n });\n // Invalidate cache for this buyer+path after recording\n cache.delete(cacheKey(path, buyerId));\n } catch {\n // Fire-and-forget\n }\n }\n\n async function syncConfig(): Promise<void> {\n try {\n // Check if config already exists on backend — don't overwrite dashboard-managed configs\n const existing = await fetch(`${base}/v1/plugins/free-tier/config`, {\n method: 'GET',\n headers: resolveHeaders(),\n });\n if (existing.ok) {\n const data = await existing.json();\n if (data.configs && data.configs.length > 0) {\n return; // Config managed via dashboard or previous sync — skip\n }\n }\n\n const paths = config.paths ?? ['*'];\n await fetch(`${base}/v1/plugins/free-tier/config`, {\n method: 'PUT',\n headers: resolveHeaders(),\n body: JSON.stringify({\n perBuyerLimit: config.perBuyerLimit,\n resetPeriod: config.resetPeriod ?? 'never',\n globalCap: config.globalCap ?? null,\n paths,\n }),\n });\n } catch (err) {\n console.warn(`[relai:freeTier] Failed to sync config to RelAI: ${err}`);\n }\n }\n\n return {\n name: 'free-tier',\n\n async onInit() {\n await syncConfig();\n },\n\n async beforePaymentCheck(req, ctx) {\n const requestPath = ctx.path || '/';\n\n // Check if this path is covered by the plugin\n const paths = config.paths ?? ['*'];\n const pathMatches = paths.includes('*') || paths.some((p) => {\n const normalized = p.toLowerCase().replace(/\\/+$/, '') || '/';\n const reqNormalized = requestPath.toLowerCase().replace(/\\/+$/, '') || '/';\n return normalized === reqNormalized || normalized === '*';\n });\n\n if (!pathMatches) {\n return {};\n }\n\n const buyerId = resolveBuyerId(req);\n const result = await checkFreeTier(requestPath, buyerId);\n\n if (result.free) {\n // Record the call (fire-and-forget)\n recordCall(requestPath, buyerId).catch(() => {});\n\n return {\n skip: true,\n headers: {\n 'X-Free-Calls-Remaining': String(result.remaining ?? 0),\n 'X-Free-Calls-Total': String(result.total ?? config.perBuyerLimit),\n ...(result.globalRemaining != null\n ? { 'X-Free-Calls-Global-Remaining': String(result.globalRemaining) }\n : {}),\n },\n meta: {\n freeTier: true,\n buyerId,\n remaining: result.remaining,\n total: result.total ?? config.perBuyerLimit,\n },\n };\n }\n\n return {};\n },\n };\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@relai-fi/x402",
3
- "version": "0.5.34",
3
+ "version": "0.5.35",
4
4
  "description": "Unified x402 payment SDK for Solana, Base, Avalanche, SKALE Base, SKALE BITE, Polygon, and Ethereum. Automatic 402 handling with zero gas fees.",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.js",