@relai-fi/x402 0.5.35 → 0.5.37
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/index.cjs +53 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +53 -3
- package/dist/index.js.map +1 -1
- package/dist/plugins.cjs +67 -0
- package/dist/plugins.cjs.map +1 -1
- package/dist/plugins.d.cts +1 -1
- package/dist/plugins.d.ts +1 -1
- package/dist/plugins.js +66 -0
- package/dist/plugins.js.map +1 -1
- package/dist/{server-BRSLRU_Y.d.ts → server-DJGM7dFH.d.ts} +54 -1
- package/dist/{server-BAPqyEka.d.cts → server-DPYBh7fy.d.cts} +54 -1
- package/dist/server.cjs +53 -3
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/server.js +53 -3
- package/dist/server.js.map +1 -1
- package/package.json +1 -1
package/dist/plugins.cjs
CHANGED
|
@@ -20,10 +20,76 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/plugins.ts
|
|
21
21
|
var plugins_exports = {};
|
|
22
22
|
__export(plugins_exports, {
|
|
23
|
+
bridge: () => bridge,
|
|
23
24
|
freeTier: () => freeTier
|
|
24
25
|
});
|
|
25
26
|
module.exports = __toCommonJS(plugins_exports);
|
|
26
27
|
var RELAI_API_BASE = "https://api.relai.fi";
|
|
28
|
+
function bridge(config) {
|
|
29
|
+
const base = (config?.baseUrl ?? RELAI_API_BASE).replace(/\/$/, "");
|
|
30
|
+
let bridgeInfo = null;
|
|
31
|
+
async function fetchBridgeInfo() {
|
|
32
|
+
try {
|
|
33
|
+
const res = await fetch(`${base}/bridge/info`);
|
|
34
|
+
if (!res.ok) {
|
|
35
|
+
console.warn(`[relai:bridge] Failed to fetch /bridge/info: ${res.status}`);
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
const data = await res.json();
|
|
39
|
+
return {
|
|
40
|
+
settleEndpoint: config?.settleEndpoint || data.settleEndpoint,
|
|
41
|
+
supportedSourceChains: config?.supportedSourceChains || data.supportedSourceChains || [],
|
|
42
|
+
supportedSourceAssets: config?.supportedSourceAssets || data.supportedSourceAssets || [],
|
|
43
|
+
payTo: config?.payTo || data.payTo || {},
|
|
44
|
+
feePayerSvm: config?.feePayerSvm ?? data.feePayerSvm ?? null,
|
|
45
|
+
feeBps: config?.feeBps ?? data.feeBps ?? 100,
|
|
46
|
+
paymentFacilitator: config?.paymentFacilitator || data.paymentFacilitator || "https://facilitator.x402.fi"
|
|
47
|
+
};
|
|
48
|
+
} catch (err) {
|
|
49
|
+
console.warn(`[relai:bridge] Failed to fetch bridge info: ${err}`);
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
name: "bridge",
|
|
55
|
+
async onInit() {
|
|
56
|
+
bridgeInfo = await fetchBridgeInfo();
|
|
57
|
+
if (bridgeInfo) {
|
|
58
|
+
console.log(`[relai:bridge] Initialized \u2014 ${bridgeInfo.supportedSourceChains.length} source chains, settle: ${bridgeInfo.settleEndpoint}`);
|
|
59
|
+
} else {
|
|
60
|
+
console.warn("[relai:bridge] Bridge info not available \u2014 cross-chain payments disabled");
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
enrich402Response(response, ctx) {
|
|
64
|
+
if (!bridgeInfo || bridgeInfo.supportedSourceChains.length === 0) {
|
|
65
|
+
return response;
|
|
66
|
+
}
|
|
67
|
+
const merchantCaip2 = response?.accepts?.[0]?.network;
|
|
68
|
+
const otherSourceChains = bridgeInfo.supportedSourceChains.filter(
|
|
69
|
+
(c) => c !== merchantCaip2
|
|
70
|
+
);
|
|
71
|
+
if (otherSourceChains.length === 0) {
|
|
72
|
+
return response;
|
|
73
|
+
}
|
|
74
|
+
const solanaChain = otherSourceChains.find((c) => c.startsWith("solana:"));
|
|
75
|
+
const primaryPayTo = solanaChain ? bridgeInfo.payTo[solanaChain] : bridgeInfo.payTo[otherSourceChains[0]];
|
|
76
|
+
response.extensions = response.extensions || {};
|
|
77
|
+
response.extensions.bridge = {
|
|
78
|
+
info: {
|
|
79
|
+
settleEndpoint: bridgeInfo.settleEndpoint,
|
|
80
|
+
supportedSourceChains: otherSourceChains,
|
|
81
|
+
supportedSourceAssets: bridgeInfo.supportedSourceAssets,
|
|
82
|
+
payTo: primaryPayTo || null,
|
|
83
|
+
payToMap: bridgeInfo.payTo,
|
|
84
|
+
feePayerSvm: bridgeInfo.feePayerSvm,
|
|
85
|
+
feeBps: bridgeInfo.feeBps,
|
|
86
|
+
paymentFacilitator: bridgeInfo.paymentFacilitator
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
return response;
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
}
|
|
27
93
|
function freeTier(config) {
|
|
28
94
|
const base = (config.baseUrl ?? RELAI_API_BASE).replace(/\/$/, "");
|
|
29
95
|
const cacheTtl = config.cacheTtlMs ?? 5e3;
|
|
@@ -155,6 +221,7 @@ function freeTier(config) {
|
|
|
155
221
|
}
|
|
156
222
|
// Annotate the CommonJS export names for ESM import in node:
|
|
157
223
|
0 && (module.exports = {
|
|
224
|
+
bridge,
|
|
158
225
|
freeTier
|
|
159
226
|
});
|
|
160
227
|
//# sourceMappingURL=plugins.cjs.map
|
package/dist/plugins.cjs.map
CHANGED
|
@@ -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 // 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":[]}
|
|
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 * Called before sending the 402 response. Allows plugins to add\n * extensions or modify the response body (e.g. bridge info).\n */\n enrich402Response?(response: any, ctx: PluginContext): any;\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 */\n// ============================================================================\n// Bridge Plugin\n// ============================================================================\n\nexport interface BridgePluginConfig {\n /** RelAI API base URL (default: https://api.relai.fi) */\n baseUrl?: string;\n /** Override settle endpoint (auto-discovered from /bridge/info if not set) */\n settleEndpoint?: string;\n /** Override supported source chains (auto-discovered if not set) */\n supportedSourceChains?: string[];\n /** Override supported source assets (auto-discovered if not set) */\n supportedSourceAssets?: string[];\n /** Override bridge payTo map: { [caip2]: address } */\n payTo?: Record<string, string>;\n /** Override Solana fee payer address (auto-discovered if not set) */\n feePayerSvm?: string;\n /** Override payment facilitator URL */\n paymentFacilitator?: string;\n /** Bridge fee in basis points (default: auto-discovered) */\n feeBps?: number;\n}\n\ninterface BridgeInfo {\n settleEndpoint: string;\n supportedSourceChains: string[];\n supportedSourceAssets: string[];\n payTo: Record<string, string>;\n feePayerSvm: string | null;\n feeBps: number;\n paymentFacilitator: string;\n}\n\n/**\n * Bridge plugin - enables cross-chain payments via the RelAI bridge.\n *\n * When a buyer's wallet is on a different chain than the merchant accepts,\n * the client SDK can automatically route the payment through the bridge.\n * This plugin adds `extensions.bridge` to the 402 response with all the\n * info the client needs to execute a cross-chain payment.\n *\n * @example\n * ```typescript\n * import Relai from '@relai-fi/x402/server';\n * import { bridge } from '@relai-fi/x402/plugins';\n *\n * const relai = new Relai({\n * network: 'skale-base',\n * plugins: [\n * bridge(), // auto-discovers from https://api.relai.fi\n * ],\n * });\n *\n * // Buyer on Solana can now pay for a SKALE endpoint\n * app.get('/api/data', relai.protect({\n * payTo: '0xYourWallet',\n * price: 0.05,\n * }), (req, res) => {\n * res.json({ data: 'paid content' });\n * });\n * ```\n */\nexport function bridge(config?: BridgePluginConfig): RelaiPlugin {\n const base = (config?.baseUrl ?? RELAI_API_BASE).replace(/\\/$/, '');\n let bridgeInfo: BridgeInfo | null = null;\n\n async function fetchBridgeInfo(): Promise<BridgeInfo | null> {\n try {\n const res = await fetch(`${base}/bridge/info`);\n if (!res.ok) {\n console.warn(`[relai:bridge] Failed to fetch /bridge/info: ${res.status}`);\n return null;\n }\n const data = await res.json() as any;\n return {\n settleEndpoint: config?.settleEndpoint || data.settleEndpoint,\n supportedSourceChains: config?.supportedSourceChains || data.supportedSourceChains || [],\n supportedSourceAssets: config?.supportedSourceAssets || data.supportedSourceAssets || [],\n payTo: config?.payTo || data.payTo || {},\n feePayerSvm: config?.feePayerSvm ?? data.feePayerSvm ?? null,\n feeBps: config?.feeBps ?? data.feeBps ?? 100,\n paymentFacilitator: config?.paymentFacilitator || data.paymentFacilitator || 'https://facilitator.x402.fi',\n };\n } catch (err) {\n console.warn(`[relai:bridge] Failed to fetch bridge info: ${err}`);\n return null;\n }\n }\n\n return {\n name: 'bridge',\n\n async onInit() {\n bridgeInfo = await fetchBridgeInfo();\n if (bridgeInfo) {\n console.log(`[relai:bridge] Initialized — ${bridgeInfo.supportedSourceChains.length} source chains, settle: ${bridgeInfo.settleEndpoint}`);\n } else {\n console.warn('[relai:bridge] Bridge info not available — cross-chain payments disabled');\n }\n },\n\n enrich402Response(response: any, ctx: PluginContext) {\n if (!bridgeInfo || bridgeInfo.supportedSourceChains.length === 0) {\n return response;\n }\n\n // Don't add bridge extension if merchant's network is already a source chain\n // and there's only that one chain — no bridging needed\n const merchantCaip2 = response?.accepts?.[0]?.network;\n\n // Filter out the merchant's own chain from source chains\n // (buyer on same chain should pay directly, not bridge)\n const otherSourceChains = bridgeInfo.supportedSourceChains.filter(\n (c: string) => c !== merchantCaip2,\n );\n\n if (otherSourceChains.length === 0) {\n return response;\n }\n\n // Find the payTo for the first available source chain (Solana-first for UX)\n const solanaChain = otherSourceChains.find((c: string) => c.startsWith('solana:'));\n const primaryPayTo = solanaChain\n ? bridgeInfo.payTo[solanaChain]\n : bridgeInfo.payTo[otherSourceChains[0]];\n\n response.extensions = response.extensions || {};\n response.extensions.bridge = {\n info: {\n settleEndpoint: bridgeInfo.settleEndpoint,\n supportedSourceChains: otherSourceChains,\n supportedSourceAssets: bridgeInfo.supportedSourceAssets,\n payTo: primaryPayTo || null,\n payToMap: bridgeInfo.payTo,\n feePayerSvm: bridgeInfo.feePayerSvm,\n feeBps: bridgeInfo.feeBps,\n paymentFacilitator: bridgeInfo.paymentFacilitator,\n },\n };\n\n return response;\n },\n };\n}\n\n// ============================================================================\n// Free Tier Plugin\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.d.cts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import './types-Y9ni5XwY.cjs';
|
|
2
|
-
export { F as FreeTierPluginConfig, g as PluginContext, h as PluginResult, f as RelaiPlugin, i as freeTier } from './server-
|
|
2
|
+
export { B as BridgePluginConfig, F as FreeTierPluginConfig, g as PluginContext, h as PluginResult, f as RelaiPlugin, i as bridge, j as freeTier } from './server-DPYBh7fy.cjs';
|
package/dist/plugins.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import './types-Y9ni5XwY.js';
|
|
2
|
-
export { F as FreeTierPluginConfig, g as PluginContext, h as PluginResult, f as RelaiPlugin, i as freeTier } from './server-
|
|
2
|
+
export { B as BridgePluginConfig, F as FreeTierPluginConfig, g as PluginContext, h as PluginResult, f as RelaiPlugin, i as bridge, j as freeTier } from './server-DJGM7dFH.js';
|
package/dist/plugins.js
CHANGED
|
@@ -1,5 +1,70 @@
|
|
|
1
1
|
// src/plugins.ts
|
|
2
2
|
var RELAI_API_BASE = "https://api.relai.fi";
|
|
3
|
+
function bridge(config) {
|
|
4
|
+
const base = (config?.baseUrl ?? RELAI_API_BASE).replace(/\/$/, "");
|
|
5
|
+
let bridgeInfo = null;
|
|
6
|
+
async function fetchBridgeInfo() {
|
|
7
|
+
try {
|
|
8
|
+
const res = await fetch(`${base}/bridge/info`);
|
|
9
|
+
if (!res.ok) {
|
|
10
|
+
console.warn(`[relai:bridge] Failed to fetch /bridge/info: ${res.status}`);
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
const data = await res.json();
|
|
14
|
+
return {
|
|
15
|
+
settleEndpoint: config?.settleEndpoint || data.settleEndpoint,
|
|
16
|
+
supportedSourceChains: config?.supportedSourceChains || data.supportedSourceChains || [],
|
|
17
|
+
supportedSourceAssets: config?.supportedSourceAssets || data.supportedSourceAssets || [],
|
|
18
|
+
payTo: config?.payTo || data.payTo || {},
|
|
19
|
+
feePayerSvm: config?.feePayerSvm ?? data.feePayerSvm ?? null,
|
|
20
|
+
feeBps: config?.feeBps ?? data.feeBps ?? 100,
|
|
21
|
+
paymentFacilitator: config?.paymentFacilitator || data.paymentFacilitator || "https://facilitator.x402.fi"
|
|
22
|
+
};
|
|
23
|
+
} catch (err) {
|
|
24
|
+
console.warn(`[relai:bridge] Failed to fetch bridge info: ${err}`);
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
name: "bridge",
|
|
30
|
+
async onInit() {
|
|
31
|
+
bridgeInfo = await fetchBridgeInfo();
|
|
32
|
+
if (bridgeInfo) {
|
|
33
|
+
console.log(`[relai:bridge] Initialized \u2014 ${bridgeInfo.supportedSourceChains.length} source chains, settle: ${bridgeInfo.settleEndpoint}`);
|
|
34
|
+
} else {
|
|
35
|
+
console.warn("[relai:bridge] Bridge info not available \u2014 cross-chain payments disabled");
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
enrich402Response(response, ctx) {
|
|
39
|
+
if (!bridgeInfo || bridgeInfo.supportedSourceChains.length === 0) {
|
|
40
|
+
return response;
|
|
41
|
+
}
|
|
42
|
+
const merchantCaip2 = response?.accepts?.[0]?.network;
|
|
43
|
+
const otherSourceChains = bridgeInfo.supportedSourceChains.filter(
|
|
44
|
+
(c) => c !== merchantCaip2
|
|
45
|
+
);
|
|
46
|
+
if (otherSourceChains.length === 0) {
|
|
47
|
+
return response;
|
|
48
|
+
}
|
|
49
|
+
const solanaChain = otherSourceChains.find((c) => c.startsWith("solana:"));
|
|
50
|
+
const primaryPayTo = solanaChain ? bridgeInfo.payTo[solanaChain] : bridgeInfo.payTo[otherSourceChains[0]];
|
|
51
|
+
response.extensions = response.extensions || {};
|
|
52
|
+
response.extensions.bridge = {
|
|
53
|
+
info: {
|
|
54
|
+
settleEndpoint: bridgeInfo.settleEndpoint,
|
|
55
|
+
supportedSourceChains: otherSourceChains,
|
|
56
|
+
supportedSourceAssets: bridgeInfo.supportedSourceAssets,
|
|
57
|
+
payTo: primaryPayTo || null,
|
|
58
|
+
payToMap: bridgeInfo.payTo,
|
|
59
|
+
feePayerSvm: bridgeInfo.feePayerSvm,
|
|
60
|
+
feeBps: bridgeInfo.feeBps,
|
|
61
|
+
paymentFacilitator: bridgeInfo.paymentFacilitator
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
return response;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
}
|
|
3
68
|
function freeTier(config) {
|
|
4
69
|
const base = (config.baseUrl ?? RELAI_API_BASE).replace(/\/$/, "");
|
|
5
70
|
const cacheTtl = config.cacheTtlMs ?? 5e3;
|
|
@@ -130,6 +195,7 @@ function freeTier(config) {
|
|
|
130
195
|
};
|
|
131
196
|
}
|
|
132
197
|
export {
|
|
198
|
+
bridge,
|
|
133
199
|
freeTier
|
|
134
200
|
};
|
|
135
201
|
//# sourceMappingURL=plugins.js.map
|
package/dist/plugins.js.map
CHANGED
|
@@ -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 // 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":[]}
|
|
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 * Called before sending the 402 response. Allows plugins to add\n * extensions or modify the response body (e.g. bridge info).\n */\n enrich402Response?(response: any, ctx: PluginContext): any;\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 */\n// ============================================================================\n// Bridge Plugin\n// ============================================================================\n\nexport interface BridgePluginConfig {\n /** RelAI API base URL (default: https://api.relai.fi) */\n baseUrl?: string;\n /** Override settle endpoint (auto-discovered from /bridge/info if not set) */\n settleEndpoint?: string;\n /** Override supported source chains (auto-discovered if not set) */\n supportedSourceChains?: string[];\n /** Override supported source assets (auto-discovered if not set) */\n supportedSourceAssets?: string[];\n /** Override bridge payTo map: { [caip2]: address } */\n payTo?: Record<string, string>;\n /** Override Solana fee payer address (auto-discovered if not set) */\n feePayerSvm?: string;\n /** Override payment facilitator URL */\n paymentFacilitator?: string;\n /** Bridge fee in basis points (default: auto-discovered) */\n feeBps?: number;\n}\n\ninterface BridgeInfo {\n settleEndpoint: string;\n supportedSourceChains: string[];\n supportedSourceAssets: string[];\n payTo: Record<string, string>;\n feePayerSvm: string | null;\n feeBps: number;\n paymentFacilitator: string;\n}\n\n/**\n * Bridge plugin - enables cross-chain payments via the RelAI bridge.\n *\n * When a buyer's wallet is on a different chain than the merchant accepts,\n * the client SDK can automatically route the payment through the bridge.\n * This plugin adds `extensions.bridge` to the 402 response with all the\n * info the client needs to execute a cross-chain payment.\n *\n * @example\n * ```typescript\n * import Relai from '@relai-fi/x402/server';\n * import { bridge } from '@relai-fi/x402/plugins';\n *\n * const relai = new Relai({\n * network: 'skale-base',\n * plugins: [\n * bridge(), // auto-discovers from https://api.relai.fi\n * ],\n * });\n *\n * // Buyer on Solana can now pay for a SKALE endpoint\n * app.get('/api/data', relai.protect({\n * payTo: '0xYourWallet',\n * price: 0.05,\n * }), (req, res) => {\n * res.json({ data: 'paid content' });\n * });\n * ```\n */\nexport function bridge(config?: BridgePluginConfig): RelaiPlugin {\n const base = (config?.baseUrl ?? RELAI_API_BASE).replace(/\\/$/, '');\n let bridgeInfo: BridgeInfo | null = null;\n\n async function fetchBridgeInfo(): Promise<BridgeInfo | null> {\n try {\n const res = await fetch(`${base}/bridge/info`);\n if (!res.ok) {\n console.warn(`[relai:bridge] Failed to fetch /bridge/info: ${res.status}`);\n return null;\n }\n const data = await res.json() as any;\n return {\n settleEndpoint: config?.settleEndpoint || data.settleEndpoint,\n supportedSourceChains: config?.supportedSourceChains || data.supportedSourceChains || [],\n supportedSourceAssets: config?.supportedSourceAssets || data.supportedSourceAssets || [],\n payTo: config?.payTo || data.payTo || {},\n feePayerSvm: config?.feePayerSvm ?? data.feePayerSvm ?? null,\n feeBps: config?.feeBps ?? data.feeBps ?? 100,\n paymentFacilitator: config?.paymentFacilitator || data.paymentFacilitator || 'https://facilitator.x402.fi',\n };\n } catch (err) {\n console.warn(`[relai:bridge] Failed to fetch bridge info: ${err}`);\n return null;\n }\n }\n\n return {\n name: 'bridge',\n\n async onInit() {\n bridgeInfo = await fetchBridgeInfo();\n if (bridgeInfo) {\n console.log(`[relai:bridge] Initialized — ${bridgeInfo.supportedSourceChains.length} source chains, settle: ${bridgeInfo.settleEndpoint}`);\n } else {\n console.warn('[relai:bridge] Bridge info not available — cross-chain payments disabled');\n }\n },\n\n enrich402Response(response: any, ctx: PluginContext) {\n if (!bridgeInfo || bridgeInfo.supportedSourceChains.length === 0) {\n return response;\n }\n\n // Don't add bridge extension if merchant's network is already a source chain\n // and there's only that one chain — no bridging needed\n const merchantCaip2 = response?.accepts?.[0]?.network;\n\n // Filter out the merchant's own chain from source chains\n // (buyer on same chain should pay directly, not bridge)\n const otherSourceChains = bridgeInfo.supportedSourceChains.filter(\n (c: string) => c !== merchantCaip2,\n );\n\n if (otherSourceChains.length === 0) {\n return response;\n }\n\n // Find the payTo for the first available source chain (Solana-first for UX)\n const solanaChain = otherSourceChains.find((c: string) => c.startsWith('solana:'));\n const primaryPayTo = solanaChain\n ? bridgeInfo.payTo[solanaChain]\n : bridgeInfo.payTo[otherSourceChains[0]];\n\n response.extensions = response.extensions || {};\n response.extensions.bridge = {\n info: {\n settleEndpoint: bridgeInfo.settleEndpoint,\n supportedSourceChains: otherSourceChains,\n supportedSourceAssets: bridgeInfo.supportedSourceAssets,\n payTo: primaryPayTo || null,\n payToMap: bridgeInfo.payTo,\n feePayerSvm: bridgeInfo.feePayerSvm,\n feeBps: bridgeInfo.feeBps,\n paymentFacilitator: bridgeInfo.paymentFacilitator,\n },\n };\n\n return response;\n },\n };\n}\n\n// ============================================================================\n// Free Tier Plugin\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":[]}
|
|
@@ -36,6 +36,11 @@ interface RelaiPlugin {
|
|
|
36
36
|
* Use to sync config to RelAI backend or validate credentials.
|
|
37
37
|
*/
|
|
38
38
|
onInit?(): Promise<void>;
|
|
39
|
+
/**
|
|
40
|
+
* Called before sending the 402 response. Allows plugins to add
|
|
41
|
+
* extensions or modify the response body (e.g. bridge info).
|
|
42
|
+
*/
|
|
43
|
+
enrich402Response?(response: any, ctx: PluginContext): any;
|
|
39
44
|
}
|
|
40
45
|
interface FreeTierPluginConfig {
|
|
41
46
|
/** Service key (sk_live_...) for authenticating with RelAI API */
|
|
@@ -84,6 +89,54 @@ interface FreeTierPluginConfig {
|
|
|
84
89
|
* });
|
|
85
90
|
* ```
|
|
86
91
|
*/
|
|
92
|
+
interface BridgePluginConfig {
|
|
93
|
+
/** RelAI API base URL (default: https://api.relai.fi) */
|
|
94
|
+
baseUrl?: string;
|
|
95
|
+
/** Override settle endpoint (auto-discovered from /bridge/info if not set) */
|
|
96
|
+
settleEndpoint?: string;
|
|
97
|
+
/** Override supported source chains (auto-discovered if not set) */
|
|
98
|
+
supportedSourceChains?: string[];
|
|
99
|
+
/** Override supported source assets (auto-discovered if not set) */
|
|
100
|
+
supportedSourceAssets?: string[];
|
|
101
|
+
/** Override bridge payTo map: { [caip2]: address } */
|
|
102
|
+
payTo?: Record<string, string>;
|
|
103
|
+
/** Override Solana fee payer address (auto-discovered if not set) */
|
|
104
|
+
feePayerSvm?: string;
|
|
105
|
+
/** Override payment facilitator URL */
|
|
106
|
+
paymentFacilitator?: string;
|
|
107
|
+
/** Bridge fee in basis points (default: auto-discovered) */
|
|
108
|
+
feeBps?: number;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Bridge plugin - enables cross-chain payments via the RelAI bridge.
|
|
112
|
+
*
|
|
113
|
+
* When a buyer's wallet is on a different chain than the merchant accepts,
|
|
114
|
+
* the client SDK can automatically route the payment through the bridge.
|
|
115
|
+
* This plugin adds `extensions.bridge` to the 402 response with all the
|
|
116
|
+
* info the client needs to execute a cross-chain payment.
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* ```typescript
|
|
120
|
+
* import Relai from '@relai-fi/x402/server';
|
|
121
|
+
* import { bridge } from '@relai-fi/x402/plugins';
|
|
122
|
+
*
|
|
123
|
+
* const relai = new Relai({
|
|
124
|
+
* network: 'skale-base',
|
|
125
|
+
* plugins: [
|
|
126
|
+
* bridge(), // auto-discovers from https://api.relai.fi
|
|
127
|
+
* ],
|
|
128
|
+
* });
|
|
129
|
+
*
|
|
130
|
+
* // Buyer on Solana can now pay for a SKALE endpoint
|
|
131
|
+
* app.get('/api/data', relai.protect({
|
|
132
|
+
* payTo: '0xYourWallet',
|
|
133
|
+
* price: 0.05,
|
|
134
|
+
* }), (req, res) => {
|
|
135
|
+
* res.json({ data: 'paid content' });
|
|
136
|
+
* });
|
|
137
|
+
* ```
|
|
138
|
+
*/
|
|
139
|
+
declare function bridge(config?: BridgePluginConfig): RelaiPlugin;
|
|
87
140
|
declare function freeTier(config: FreeTierPluginConfig): RelaiPlugin;
|
|
88
141
|
|
|
89
142
|
interface RelaiServerConfig {
|
|
@@ -226,4 +279,4 @@ declare class Relai {
|
|
|
226
279
|
protect(options: ProtectOptions): (req: any, res: any, next: any) => Promise<any>;
|
|
227
280
|
}
|
|
228
281
|
|
|
229
|
-
export { type DynamicPrice as D, type FreeTierPluginConfig as F, type ProtectOptions as P, Relai as R, type SettleResult as S, type RelaiServerConfig as a, type PaymentInfo as b, type StripePayTo as c, type RelaiIntegritasFlow as d, type RelaiIntegritasOptions as e, type RelaiPlugin as f, type PluginContext as g, type PluginResult as h,
|
|
282
|
+
export { type BridgePluginConfig as B, type DynamicPrice as D, type FreeTierPluginConfig as F, type ProtectOptions as P, Relai as R, type SettleResult as S, type RelaiServerConfig as a, type PaymentInfo as b, type StripePayTo as c, type RelaiIntegritasFlow as d, type RelaiIntegritasOptions as e, type RelaiPlugin as f, type PluginContext as g, type PluginResult as h, bridge as i, freeTier as j, stripePayTo as s };
|
|
@@ -36,6 +36,11 @@ interface RelaiPlugin {
|
|
|
36
36
|
* Use to sync config to RelAI backend or validate credentials.
|
|
37
37
|
*/
|
|
38
38
|
onInit?(): Promise<void>;
|
|
39
|
+
/**
|
|
40
|
+
* Called before sending the 402 response. Allows plugins to add
|
|
41
|
+
* extensions or modify the response body (e.g. bridge info).
|
|
42
|
+
*/
|
|
43
|
+
enrich402Response?(response: any, ctx: PluginContext): any;
|
|
39
44
|
}
|
|
40
45
|
interface FreeTierPluginConfig {
|
|
41
46
|
/** Service key (sk_live_...) for authenticating with RelAI API */
|
|
@@ -84,6 +89,54 @@ interface FreeTierPluginConfig {
|
|
|
84
89
|
* });
|
|
85
90
|
* ```
|
|
86
91
|
*/
|
|
92
|
+
interface BridgePluginConfig {
|
|
93
|
+
/** RelAI API base URL (default: https://api.relai.fi) */
|
|
94
|
+
baseUrl?: string;
|
|
95
|
+
/** Override settle endpoint (auto-discovered from /bridge/info if not set) */
|
|
96
|
+
settleEndpoint?: string;
|
|
97
|
+
/** Override supported source chains (auto-discovered if not set) */
|
|
98
|
+
supportedSourceChains?: string[];
|
|
99
|
+
/** Override supported source assets (auto-discovered if not set) */
|
|
100
|
+
supportedSourceAssets?: string[];
|
|
101
|
+
/** Override bridge payTo map: { [caip2]: address } */
|
|
102
|
+
payTo?: Record<string, string>;
|
|
103
|
+
/** Override Solana fee payer address (auto-discovered if not set) */
|
|
104
|
+
feePayerSvm?: string;
|
|
105
|
+
/** Override payment facilitator URL */
|
|
106
|
+
paymentFacilitator?: string;
|
|
107
|
+
/** Bridge fee in basis points (default: auto-discovered) */
|
|
108
|
+
feeBps?: number;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Bridge plugin - enables cross-chain payments via the RelAI bridge.
|
|
112
|
+
*
|
|
113
|
+
* When a buyer's wallet is on a different chain than the merchant accepts,
|
|
114
|
+
* the client SDK can automatically route the payment through the bridge.
|
|
115
|
+
* This plugin adds `extensions.bridge` to the 402 response with all the
|
|
116
|
+
* info the client needs to execute a cross-chain payment.
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* ```typescript
|
|
120
|
+
* import Relai from '@relai-fi/x402/server';
|
|
121
|
+
* import { bridge } from '@relai-fi/x402/plugins';
|
|
122
|
+
*
|
|
123
|
+
* const relai = new Relai({
|
|
124
|
+
* network: 'skale-base',
|
|
125
|
+
* plugins: [
|
|
126
|
+
* bridge(), // auto-discovers from https://api.relai.fi
|
|
127
|
+
* ],
|
|
128
|
+
* });
|
|
129
|
+
*
|
|
130
|
+
* // Buyer on Solana can now pay for a SKALE endpoint
|
|
131
|
+
* app.get('/api/data', relai.protect({
|
|
132
|
+
* payTo: '0xYourWallet',
|
|
133
|
+
* price: 0.05,
|
|
134
|
+
* }), (req, res) => {
|
|
135
|
+
* res.json({ data: 'paid content' });
|
|
136
|
+
* });
|
|
137
|
+
* ```
|
|
138
|
+
*/
|
|
139
|
+
declare function bridge(config?: BridgePluginConfig): RelaiPlugin;
|
|
87
140
|
declare function freeTier(config: FreeTierPluginConfig): RelaiPlugin;
|
|
88
141
|
|
|
89
142
|
interface RelaiServerConfig {
|
|
@@ -226,4 +279,4 @@ declare class Relai {
|
|
|
226
279
|
protect(options: ProtectOptions): (req: any, res: any, next: any) => Promise<any>;
|
|
227
280
|
}
|
|
228
281
|
|
|
229
|
-
export { type DynamicPrice as D, type FreeTierPluginConfig as F, type ProtectOptions as P, Relai as R, type SettleResult as S, type RelaiServerConfig as a, type PaymentInfo as b, type StripePayTo as c, type RelaiIntegritasFlow as d, type RelaiIntegritasOptions as e, type RelaiPlugin as f, type PluginContext as g, type PluginResult as h,
|
|
282
|
+
export { type BridgePluginConfig as B, type DynamicPrice as D, type FreeTierPluginConfig as F, type ProtectOptions as P, Relai as R, type SettleResult as S, type RelaiServerConfig as a, type PaymentInfo as b, type StripePayTo as c, type RelaiIntegritasFlow as d, type RelaiIntegritasOptions as e, type RelaiPlugin as f, type PluginContext as g, type PluginResult as h, bridge as i, freeTier as j, stripePayTo as s };
|
package/dist/server.cjs
CHANGED
|
@@ -565,7 +565,7 @@ var Relai = class {
|
|
|
565
565
|
resolvedPayTo = options.payTo;
|
|
566
566
|
}
|
|
567
567
|
const feePayer = options.feePayer || await self.getFeePayer(caip2);
|
|
568
|
-
|
|
568
|
+
let paymentRequiredResponse = {
|
|
569
569
|
x402Version: 2,
|
|
570
570
|
error: "Payment required",
|
|
571
571
|
resource: {
|
|
@@ -603,7 +603,24 @@ var Relai = class {
|
|
|
603
603
|
}
|
|
604
604
|
}
|
|
605
605
|
} : {}
|
|
606
|
-
}
|
|
606
|
+
};
|
|
607
|
+
if (self.plugins.length > 0) {
|
|
608
|
+
const enrichCtx = {
|
|
609
|
+
network,
|
|
610
|
+
price: resolvedPrice,
|
|
611
|
+
path: req.path || req.originalUrl || "/",
|
|
612
|
+
method: (req.method || "GET").toUpperCase()
|
|
613
|
+
};
|
|
614
|
+
for (const plugin of self.plugins) {
|
|
615
|
+
if (!plugin.enrich402Response) continue;
|
|
616
|
+
try {
|
|
617
|
+
paymentRequiredResponse = plugin.enrich402Response(paymentRequiredResponse, enrichCtx) || paymentRequiredResponse;
|
|
618
|
+
} catch (pluginErr) {
|
|
619
|
+
console.warn(`[Relai] Plugin '${plugin.name}' enrich402Response error (non-blocking):`, pluginErr);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
return res.status(402).json(paymentRequiredResponse);
|
|
607
624
|
}
|
|
608
625
|
let paymentProof;
|
|
609
626
|
try {
|
|
@@ -619,6 +636,39 @@ var Relai = class {
|
|
|
619
636
|
});
|
|
620
637
|
}
|
|
621
638
|
}
|
|
639
|
+
if (paymentProof.bridged === true && paymentProof.targetTxId) {
|
|
640
|
+
console.log(`[Relai] Bridged payment accepted: source=${paymentProof.sourceTxId}, target=${paymentProof.targetTxId}`);
|
|
641
|
+
const paymentInfo2 = {
|
|
642
|
+
verified: true,
|
|
643
|
+
transactionId: paymentProof.targetTxId,
|
|
644
|
+
payer: paymentProof.sourceTxId || "bridge",
|
|
645
|
+
network,
|
|
646
|
+
amount: resolvedPrice
|
|
647
|
+
};
|
|
648
|
+
req.payment = paymentInfo2;
|
|
649
|
+
req.x402Payer = paymentProof.sourceTxId || "bridge";
|
|
650
|
+
req.x402Paid = true;
|
|
651
|
+
req.x402Transaction = paymentProof.targetTxId;
|
|
652
|
+
req.x402Network = network;
|
|
653
|
+
req.x402Bridged = true;
|
|
654
|
+
req.x402SourceChain = paymentProof.sourceChain;
|
|
655
|
+
const paymentResponse2 = {
|
|
656
|
+
x402Version: 2,
|
|
657
|
+
scheme: "exact",
|
|
658
|
+
network: caip2,
|
|
659
|
+
transaction: paymentProof.targetTxId,
|
|
660
|
+
payer: paymentProof.sourceTxId,
|
|
661
|
+
amount,
|
|
662
|
+
asset,
|
|
663
|
+
bridged: true
|
|
664
|
+
};
|
|
665
|
+
res.setHeader(
|
|
666
|
+
"PAYMENT-RESPONSE",
|
|
667
|
+
Buffer.from(JSON.stringify(paymentResponse2)).toString("base64")
|
|
668
|
+
);
|
|
669
|
+
options.onPaymentSettled?.(req, { success: true, transaction: paymentProof.targetTxId, payer: paymentProof.sourceTxId });
|
|
670
|
+
return next();
|
|
671
|
+
}
|
|
622
672
|
let settlePayTo;
|
|
623
673
|
if (stripeConfig) {
|
|
624
674
|
settlePayTo = paymentProof.payload?.authorization?.to || paymentProof.accepted?.payTo || "";
|
|
@@ -633,7 +683,7 @@ var Relai = class {
|
|
|
633
683
|
}
|
|
634
684
|
const paymentRequirements = {
|
|
635
685
|
scheme: "exact",
|
|
636
|
-
network,
|
|
686
|
+
network: caip2,
|
|
637
687
|
amount,
|
|
638
688
|
asset,
|
|
639
689
|
payTo: settlePayTo,
|