@relai-fi/x402 0.5.38 → 0.5.39

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.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- export { B as BridgePluginConfig, D as DynamicPrice, F as FreeTierPluginConfig, b as PaymentInfo, g as PluginContext, h as PluginResult, P as ProtectOptions, R as Relai, d as RelaiIntegritasFlow, e as RelaiIntegritasOptions, f as RelaiPlugin, a as RelaiServerConfig, S as SettleResult, c as StripePayTo, R as default, s as stripePayTo } from './server-BZNDcLCU.cjs';
1
+ export { B as BridgePluginConfig, D as DynamicPrice, F as FreeTierPluginConfig, b as PaymentInfo, g as PluginContext, h as PluginResult, P as ProtectOptions, R as Relai, d as RelaiIntegritasFlow, e as RelaiIntegritasOptions, f as RelaiPlugin, a as RelaiServerConfig, S as SettleResult, c as StripePayTo, R as default, s as stripePayTo } from './server-CyfEHW9D.cjs';
2
2
  export { RelayWebSocketFactory, RelayWebSocketLike, X402Client, X402ClientConfig, X402FetchInit, X402IntegritasConfig, X402IntegritasFlow, X402NetworkSelectionMode, X402RelayWsConfig, X402RelayWsError, X402RelayWsResponse, X402RequestOptions, default as createX402Client } from './client.cjs';
3
3
  export { A as AcceptsExtra, B as BASE_MAINNET_NETWORK, C as CAIP2_TO_NETWORK, b as CHAIN_IDS, E as EXPLORER_TX_URL, l as EvmWallet, N as NETWORK_CAIP2, e as NETWORK_LABELS, d as NETWORK_TOKENS, c as NetworkToken, P as PaymentAccept, o as PaymentRequired, R as RELAI_FACILITATOR_URL, h as RELAI_NETWORKS, a as RelaiNetwork, m as ResourceInfo, S as SOLANA_MAINNET_NETWORK, k as SolanaWallet, U as USDC_ADDRESSES, g as USDC_BASE, f as USDC_SOLANA, W as WalletSet, j as isEvm, i as isSolana, n as normalizeNetwork, r as resolveToken } from './types-Y9ni5XwY.cjs';
4
4
  export { BridgeBalances, BridgeQuoteResult, BridgeResult } from './management.cjs';
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { B as BridgePluginConfig, D as DynamicPrice, F as FreeTierPluginConfig, b as PaymentInfo, g as PluginContext, h as PluginResult, P as ProtectOptions, R as Relai, d as RelaiIntegritasFlow, e as RelaiIntegritasOptions, f as RelaiPlugin, a as RelaiServerConfig, S as SettleResult, c as StripePayTo, R as default, s as stripePayTo } from './server-Ba5C2Bv7.js';
1
+ export { B as BridgePluginConfig, D as DynamicPrice, F as FreeTierPluginConfig, b as PaymentInfo, g as PluginContext, h as PluginResult, P as ProtectOptions, R as Relai, d as RelaiIntegritasFlow, e as RelaiIntegritasOptions, f as RelaiPlugin, a as RelaiServerConfig, S as SettleResult, c as StripePayTo, R as default, s as stripePayTo } from './server-CaSmhDnd.js';
2
2
  export { RelayWebSocketFactory, RelayWebSocketLike, X402Client, X402ClientConfig, X402FetchInit, X402IntegritasConfig, X402IntegritasFlow, X402NetworkSelectionMode, X402RelayWsConfig, X402RelayWsError, X402RelayWsResponse, X402RequestOptions, default as createX402Client } from './client.js';
3
3
  export { A as AcceptsExtra, B as BASE_MAINNET_NETWORK, C as CAIP2_TO_NETWORK, b as CHAIN_IDS, E as EXPLORER_TX_URL, l as EvmWallet, N as NETWORK_CAIP2, e as NETWORK_LABELS, d as NETWORK_TOKENS, c as NetworkToken, P as PaymentAccept, o as PaymentRequired, R as RELAI_FACILITATOR_URL, h as RELAI_NETWORKS, a as RelaiNetwork, m as ResourceInfo, S as SOLANA_MAINNET_NETWORK, k as SolanaWallet, U as USDC_ADDRESSES, g as USDC_BASE, f as USDC_SOLANA, W as WalletSet, j as isEvm, i as isSolana, n as normalizeNetwork, r as resolveToken } from './types-Y9ni5XwY.js';
4
4
  export { BridgeBalances, BridgeQuoteResult, BridgeResult } from './management.js';
package/dist/plugins.cjs CHANGED
@@ -109,13 +109,11 @@ function bridge(config) {
109
109
  function freeTier(config) {
110
110
  const base = (config.baseUrl ?? RELAI_API_BASE).replace(/\/$/, "");
111
111
  const cacheTtl = config.cacheTtlMs ?? 5e3;
112
- const cache = /* @__PURE__ */ new Map();
113
- function resolveHeaders() {
114
- return {
115
- "X-Service-Key": config.serviceKey,
116
- "Content-Type": "application/json"
117
- };
118
- }
112
+ const isLocal = !config.serviceKey;
113
+ const resetPeriod = config.resetPeriod ?? "none";
114
+ const limit = config.perBuyerLimit;
115
+ const globalCap = config.globalCap ?? null;
116
+ const pluginPaths = config.paths ?? ["*"];
119
117
  function resolveBuyerId(req) {
120
118
  try {
121
119
  const auth = req.headers?.authorization || "";
@@ -131,11 +129,99 @@ function freeTier(config) {
131
129
  const ip = (req.headers?.["x-forwarded-for"] || "").split(",")[0].trim() || req.socket?.remoteAddress || req.ip || "unknown";
132
130
  return `ip:${ip}`;
133
131
  }
134
- function cacheKey(path, buyerId) {
132
+ function pathMatches(requestPath) {
133
+ return pluginPaths.includes("*") || pluginPaths.some((p) => {
134
+ const normalized = p.toLowerCase().replace(/\/+$/, "") || "/";
135
+ const reqNormalized = requestPath.toLowerCase().replace(/\/+$/, "") || "/";
136
+ return normalized === reqNormalized || normalized === "*";
137
+ });
138
+ }
139
+ const localUsage = /* @__PURE__ */ new Map();
140
+ let localGlobalCount = 0;
141
+ function currentPeriodStart() {
142
+ const now = /* @__PURE__ */ new Date();
143
+ if (resetPeriod === "daily") {
144
+ return new Date(now.getFullYear(), now.getMonth(), now.getDate()).toISOString();
145
+ }
146
+ if (resetPeriod === "monthly") {
147
+ return new Date(now.getFullYear(), now.getMonth(), 1).toISOString();
148
+ }
149
+ return "none";
150
+ }
151
+ function isCurrentPeriod(entry) {
152
+ if (resetPeriod === "none") return true;
153
+ return entry.periodStart === currentPeriodStart();
154
+ }
155
+ function localKey(path, buyerId) {
156
+ return `${path}:${buyerId}`;
157
+ }
158
+ function localCheck(path, buyerId) {
159
+ const key = localKey(path, buyerId);
160
+ const entry = localUsage.get(key);
161
+ const count = entry && isCurrentPeriod(entry) ? entry.count : 0;
162
+ const remaining = Math.max(0, limit - count);
163
+ if (globalCap != null && localGlobalCount >= globalCap) {
164
+ return { free: false, remaining: 0, total: limit, reason: "global_cap_reached", globalRemaining: 0 };
165
+ }
166
+ if (count >= limit) {
167
+ return { free: false, remaining: 0, total: limit, reason: "limit_reached" };
168
+ }
169
+ return {
170
+ free: true,
171
+ remaining: remaining - 1,
172
+ // after this call
173
+ total: limit,
174
+ ...globalCap != null ? { globalRemaining: globalCap - localGlobalCount - 1 } : {}
175
+ };
176
+ }
177
+ function localRecord(path, buyerId) {
178
+ const key = localKey(path, buyerId);
179
+ const period = currentPeriodStart();
180
+ const entry = localUsage.get(key);
181
+ const now = (/* @__PURE__ */ new Date()).toISOString();
182
+ if (entry && isCurrentPeriod(entry)) {
183
+ entry.count++;
184
+ entry.lastCall = now;
185
+ } else {
186
+ localUsage.set(key, { count: 1, periodStart: period, lastCall: now });
187
+ }
188
+ localGlobalCount++;
189
+ }
190
+ function exportLocalData() {
191
+ const usage = [];
192
+ for (const [key, entry] of localUsage.entries()) {
193
+ const firstSlash = key.indexOf("/");
194
+ const colonAfterPath = key.indexOf(":", firstSlash > -1 ? firstSlash : 0);
195
+ const path = colonAfterPath > -1 ? key.slice(0, colonAfterPath) : key;
196
+ const buyerId = colonAfterPath > -1 ? key.slice(colonAfterPath + 1) : "unknown";
197
+ usage.push({
198
+ buyerId,
199
+ path,
200
+ count: entry.count,
201
+ periodStart: entry.periodStart,
202
+ lastCall: entry.lastCall
203
+ });
204
+ }
205
+ return {
206
+ mode: "local",
207
+ config: { perBuyerLimit: limit, resetPeriod, globalCap, paths: pluginPaths },
208
+ usage,
209
+ globalCount: localGlobalCount,
210
+ exportedAt: (/* @__PURE__ */ new Date()).toISOString()
211
+ };
212
+ }
213
+ const cache = /* @__PURE__ */ new Map();
214
+ function resolveHeaders() {
215
+ return {
216
+ "X-Service-Key": config.serviceKey,
217
+ "Content-Type": "application/json"
218
+ };
219
+ }
220
+ function cloudCacheKey(path, buyerId) {
135
221
  return `${config.serviceKey}:${path}:${buyerId}`;
136
222
  }
137
223
  async function checkFreeTier(path, buyerId) {
138
- const key = cacheKey(path, buyerId);
224
+ const key = cloudCacheKey(path, buyerId);
139
225
  const now = Date.now();
140
226
  const cached = cache.get(key);
141
227
  if (cached && cached.expiresAt > now) {
@@ -164,7 +250,7 @@ function freeTier(config) {
164
250
  headers: resolveHeaders(),
165
251
  body: JSON.stringify({ path, buyerId })
166
252
  });
167
- cache.delete(cacheKey(path, buyerId));
253
+ cache.delete(cloudCacheKey(path, buyerId));
168
254
  } catch {
169
255
  }
170
256
  }
@@ -180,7 +266,6 @@ function freeTier(config) {
180
266
  return;
181
267
  }
182
268
  }
183
- const paths = config.paths ?? ["*"];
184
269
  await fetch(`${base}/v1/plugins/free-tier/config`, {
185
270
  method: "PUT",
186
271
  headers: resolveHeaders(),
@@ -188,7 +273,7 @@ function freeTier(config) {
188
273
  perBuyerLimit: config.perBuyerLimit,
189
274
  resetPeriod: config.resetPeriod ?? "none",
190
275
  globalCap: config.globalCap ?? null,
191
- paths
276
+ paths: pluginPaths
192
277
  })
193
278
  });
194
279
  } catch (err) {
@@ -197,21 +282,39 @@ function freeTier(config) {
197
282
  }
198
283
  return {
199
284
  name: "free-tier",
285
+ mode: isLocal ? "local" : "cloud",
286
+ getUsageData() {
287
+ if (!isLocal) return null;
288
+ return exportLocalData();
289
+ },
200
290
  async onInit() {
291
+ if (isLocal) {
292
+ console.log(`[relai:freeTier] Running in local mode (in-memory). perBuyerLimit=${limit}, resetPeriod=${resetPeriod}`);
293
+ return;
294
+ }
201
295
  await syncConfig();
202
296
  },
203
297
  async beforePaymentCheck(req, ctx) {
204
298
  const requestPath = ctx.path || "/";
205
- const paths = config.paths ?? ["*"];
206
- const pathMatches = paths.includes("*") || paths.some((p) => {
207
- const normalized = p.toLowerCase().replace(/\/+$/, "") || "/";
208
- const reqNormalized = requestPath.toLowerCase().replace(/\/+$/, "") || "/";
209
- return normalized === reqNormalized || normalized === "*";
210
- });
211
- if (!pathMatches) {
299
+ if (!pathMatches(requestPath)) return {};
300
+ const buyerId = resolveBuyerId(req);
301
+ if (isLocal) {
302
+ const result2 = localCheck(requestPath, buyerId);
303
+ if (result2.free) {
304
+ localRecord(requestPath, buyerId);
305
+ return {
306
+ skip: true,
307
+ headers: {
308
+ "X-Free-Calls-Remaining": String(result2.remaining ?? 0),
309
+ "X-Free-Calls-Total": String(result2.total ?? limit),
310
+ ...result2.globalRemaining != null ? { "X-Free-Calls-Global-Remaining": String(result2.globalRemaining) } : {},
311
+ "X-Free-Tier-Mode": "local"
312
+ },
313
+ meta: { freeTier: true, buyerId, remaining: result2.remaining, total: result2.total ?? limit, mode: "local" }
314
+ };
315
+ }
212
316
  return {};
213
317
  }
214
- const buyerId = resolveBuyerId(req);
215
318
  const result = await checkFreeTier(requestPath, buyerId);
216
319
  if (result.free) {
217
320
  recordCall(requestPath, buyerId).catch(() => {
@@ -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 crypto from 'crypto';\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?: 'none' | '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 /** Service key (sk_live_...) for tracking bridge usage per API owner */\n serviceKey?: string;\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 // Hash service key for tracking (never expose raw key to clients)\n // Must match backend: crypto.createHash(\"sha256\").update(key).digest(\"hex\").slice(0, 16)\n let serviceKeyHash: string | null = null;\n if (config?.serviceKey) {\n serviceKeyHash = crypto.createHash('sha256').update(config.serviceKey).digest('hex').slice(0, 16);\n }\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 ...(serviceKeyHash ? { serviceKeyHash } : {}),\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 ?? 'none',\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":["crypto"]}
1
+ {"version":3,"sources":["../src/plugins.ts"],"sourcesContent":["// src/plugins.ts\n// RelAI Plugin System - extensible middleware hooks for Relai.protect()\n\nimport crypto from 'crypto';\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 syncing with RelAI backend. If omitted, runs in local in-memory mode. */\n serviceKey?: string;\n /** Max free calls per buyer per period */\n perBuyerLimit: number;\n /** Reset period for per-buyer counters */\n resetPeriod?: 'none' | '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 /** Service key (sk_live_...) for tracking bridge usage per API owner */\n serviceKey?: string;\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 // Hash service key for tracking (never expose raw key to clients)\n // Must match backend: crypto.createHash(\"sha256\").update(key).digest(\"hex\").slice(0, 16)\n let serviceKeyHash: string | null = null;\n if (config?.serviceKey) {\n serviceKeyHash = crypto.createHash('sha256').update(config.serviceKey).digest('hex').slice(0, 16);\n }\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 ...(serviceKeyHash ? { serviceKeyHash } : {}),\n },\n };\n\n return response;\n },\n };\n}\n\n// ============================================================================\n// Free Tier Plugin\n// ============================================================================\n\n/**\n * Extended plugin interface with data export for free tier.\n */\nexport interface FreeTierPlugin extends RelaiPlugin {\n /** Export all in-memory usage data (local mode only). Returns null when using cloud mode. */\n getUsageData(): FreeTierUsageExport | null;\n /** Whether plugin is running in local (in-memory) or cloud (RelAI backend) mode. */\n readonly mode: 'local' | 'cloud';\n}\n\nexport interface FreeTierUsageExport {\n mode: 'local';\n config: {\n perBuyerLimit: number;\n resetPeriod: string;\n globalCap: number | null;\n paths: string[];\n };\n /** Per-buyer usage entries */\n usage: Array<{\n buyerId: string;\n path: string;\n count: number;\n periodStart: string;\n lastCall: string;\n }>;\n globalCount: number;\n exportedAt: string;\n}\n\nexport function freeTier(config: FreeTierPluginConfig): FreeTierPlugin {\n const base = (config.baseUrl ?? RELAI_API_BASE).replace(/\\/$/, '');\n const cacheTtl = config.cacheTtlMs ?? 5000;\n const isLocal = !config.serviceKey;\n const resetPeriod = config.resetPeriod ?? 'none';\n const limit = config.perBuyerLimit;\n const globalCap = config.globalCap ?? null;\n const pluginPaths = config.paths ?? ['*'];\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 pathMatches(requestPath: string): boolean {\n return pluginPaths.includes('*') || pluginPaths.some((p) => {\n const normalized = p.toLowerCase().replace(/\\/+$/, '') || '/';\n const reqNormalized = requestPath.toLowerCase().replace(/\\/+$/, '') || '/';\n return normalized === reqNormalized || normalized === '*';\n });\n }\n\n // ---------------------------------------------------------------------------\n // LOCAL IN-MEMORY MODE\n // ---------------------------------------------------------------------------\n\n interface LocalUsageEntry {\n count: number;\n periodStart: string;\n lastCall: string;\n }\n\n // Map key: \"path:buyerId\"\n const localUsage = new Map<string, LocalUsageEntry>();\n let localGlobalCount = 0;\n\n function currentPeriodStart(): string {\n const now = new Date();\n if (resetPeriod === 'daily') {\n return new Date(now.getFullYear(), now.getMonth(), now.getDate()).toISOString();\n }\n if (resetPeriod === 'monthly') {\n return new Date(now.getFullYear(), now.getMonth(), 1).toISOString();\n }\n return 'none'; // permanent\n }\n\n function isCurrentPeriod(entry: LocalUsageEntry): boolean {\n if (resetPeriod === 'none') return true;\n return entry.periodStart === currentPeriodStart();\n }\n\n function localKey(path: string, buyerId: string): string {\n return `${path}:${buyerId}`;\n }\n\n function localCheck(path: string, buyerId: string): FreeTierCheckResponse {\n const key = localKey(path, buyerId);\n const entry = localUsage.get(key);\n\n // If entry is from a previous period, it's stale — treat as 0\n const count = (entry && isCurrentPeriod(entry)) ? entry.count : 0;\n const remaining = Math.max(0, limit - count);\n\n // Global cap check\n if (globalCap != null && localGlobalCount >= globalCap) {\n return { free: false, remaining: 0, total: limit, reason: 'global_cap_reached', globalRemaining: 0 };\n }\n\n if (count >= limit) {\n return { free: false, remaining: 0, total: limit, reason: 'limit_reached' };\n }\n\n return {\n free: true,\n remaining: remaining - 1, // after this call\n total: limit,\n ...(globalCap != null ? { globalRemaining: globalCap - localGlobalCount - 1 } : {}),\n };\n }\n\n function localRecord(path: string, buyerId: string): void {\n const key = localKey(path, buyerId);\n const period = currentPeriodStart();\n const entry = localUsage.get(key);\n const now = new Date().toISOString();\n\n if (entry && isCurrentPeriod(entry)) {\n entry.count++;\n entry.lastCall = now;\n } else {\n localUsage.set(key, { count: 1, periodStart: period, lastCall: now });\n }\n localGlobalCount++;\n }\n\n function exportLocalData(): FreeTierUsageExport {\n const usage: FreeTierUsageExport['usage'] = [];\n for (const [key, entry] of localUsage.entries()) {\n // key format is \"path:buyerId\" but buyerId itself can contain ':'\n // We stored it as `${path}:${buyerId}` where path starts with '/'\n // So we find the first ':' after the path portion\n const firstSlash = key.indexOf('/');\n const colonAfterPath = key.indexOf(':', firstSlash > -1 ? firstSlash : 0);\n const path = colonAfterPath > -1 ? key.slice(0, colonAfterPath) : key;\n const buyerId = colonAfterPath > -1 ? key.slice(colonAfterPath + 1) : 'unknown';\n usage.push({\n buyerId,\n path,\n count: entry.count,\n periodStart: entry.periodStart,\n lastCall: entry.lastCall,\n });\n }\n return {\n mode: 'local',\n config: { perBuyerLimit: limit, resetPeriod, globalCap, paths: pluginPaths },\n usage,\n globalCount: localGlobalCount,\n exportedAt: new Date().toISOString(),\n };\n }\n\n // ---------------------------------------------------------------------------\n // CLOUD MODE (original — requires serviceKey)\n // ---------------------------------------------------------------------------\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 function cloudCacheKey(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 = cloudCacheKey(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(cloudCacheKey(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 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 ?? 'none',\n globalCap: config.globalCap ?? null,\n paths: pluginPaths,\n }),\n });\n } catch (err) {\n console.warn(`[relai:freeTier] Failed to sync config to RelAI: ${err}`);\n }\n }\n\n // ---------------------------------------------------------------------------\n // Plugin instance\n // ---------------------------------------------------------------------------\n\n return {\n name: 'free-tier',\n mode: isLocal ? 'local' : 'cloud',\n\n getUsageData(): FreeTierUsageExport | null {\n if (!isLocal) return null;\n return exportLocalData();\n },\n\n async onInit() {\n if (isLocal) {\n console.log(`[relai:freeTier] Running in local mode (in-memory). perBuyerLimit=${limit}, resetPeriod=${resetPeriod}`);\n return;\n }\n await syncConfig();\n },\n\n async beforePaymentCheck(req, ctx) {\n const requestPath = ctx.path || '/';\n if (!pathMatches(requestPath)) return {};\n\n const buyerId = resolveBuyerId(req);\n\n // ── Local mode ──\n if (isLocal) {\n const result = localCheck(requestPath, buyerId);\n if (result.free) {\n localRecord(requestPath, buyerId);\n return {\n skip: true,\n headers: {\n 'X-Free-Calls-Remaining': String(result.remaining ?? 0),\n 'X-Free-Calls-Total': String(result.total ?? limit),\n ...(result.globalRemaining != null\n ? { 'X-Free-Calls-Global-Remaining': String(result.globalRemaining) }\n : {}),\n 'X-Free-Tier-Mode': 'local',\n },\n meta: { freeTier: true, buyerId, remaining: result.remaining, total: result.total ?? limit, mode: 'local' },\n };\n }\n return {};\n }\n\n // ── Cloud mode ──\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":["crypto","result"]}
@@ -1,2 +1,2 @@
1
1
  import './types-Y9ni5XwY.cjs';
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-BZNDcLCU.cjs';
2
+ export { B as BridgePluginConfig, j as FreeTierPlugin, F as FreeTierPluginConfig, k as FreeTierUsageExport, g as PluginContext, h as PluginResult, f as RelaiPlugin, i as bridge, l as freeTier } from './server-CyfEHW9D.cjs';
package/dist/plugins.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  import './types-Y9ni5XwY.js';
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-Ba5C2Bv7.js';
2
+ export { B as BridgePluginConfig, j as FreeTierPlugin, F as FreeTierPluginConfig, k as FreeTierUsageExport, g as PluginContext, h as PluginResult, f as RelaiPlugin, i as bridge, l as freeTier } from './server-CaSmhDnd.js';
package/dist/plugins.js CHANGED
@@ -74,13 +74,11 @@ function bridge(config) {
74
74
  function freeTier(config) {
75
75
  const base = (config.baseUrl ?? RELAI_API_BASE).replace(/\/$/, "");
76
76
  const cacheTtl = config.cacheTtlMs ?? 5e3;
77
- const cache = /* @__PURE__ */ new Map();
78
- function resolveHeaders() {
79
- return {
80
- "X-Service-Key": config.serviceKey,
81
- "Content-Type": "application/json"
82
- };
83
- }
77
+ const isLocal = !config.serviceKey;
78
+ const resetPeriod = config.resetPeriod ?? "none";
79
+ const limit = config.perBuyerLimit;
80
+ const globalCap = config.globalCap ?? null;
81
+ const pluginPaths = config.paths ?? ["*"];
84
82
  function resolveBuyerId(req) {
85
83
  try {
86
84
  const auth = req.headers?.authorization || "";
@@ -96,11 +94,99 @@ function freeTier(config) {
96
94
  const ip = (req.headers?.["x-forwarded-for"] || "").split(",")[0].trim() || req.socket?.remoteAddress || req.ip || "unknown";
97
95
  return `ip:${ip}`;
98
96
  }
99
- function cacheKey(path, buyerId) {
97
+ function pathMatches(requestPath) {
98
+ return pluginPaths.includes("*") || pluginPaths.some((p) => {
99
+ const normalized = p.toLowerCase().replace(/\/+$/, "") || "/";
100
+ const reqNormalized = requestPath.toLowerCase().replace(/\/+$/, "") || "/";
101
+ return normalized === reqNormalized || normalized === "*";
102
+ });
103
+ }
104
+ const localUsage = /* @__PURE__ */ new Map();
105
+ let localGlobalCount = 0;
106
+ function currentPeriodStart() {
107
+ const now = /* @__PURE__ */ new Date();
108
+ if (resetPeriod === "daily") {
109
+ return new Date(now.getFullYear(), now.getMonth(), now.getDate()).toISOString();
110
+ }
111
+ if (resetPeriod === "monthly") {
112
+ return new Date(now.getFullYear(), now.getMonth(), 1).toISOString();
113
+ }
114
+ return "none";
115
+ }
116
+ function isCurrentPeriod(entry) {
117
+ if (resetPeriod === "none") return true;
118
+ return entry.periodStart === currentPeriodStart();
119
+ }
120
+ function localKey(path, buyerId) {
121
+ return `${path}:${buyerId}`;
122
+ }
123
+ function localCheck(path, buyerId) {
124
+ const key = localKey(path, buyerId);
125
+ const entry = localUsage.get(key);
126
+ const count = entry && isCurrentPeriod(entry) ? entry.count : 0;
127
+ const remaining = Math.max(0, limit - count);
128
+ if (globalCap != null && localGlobalCount >= globalCap) {
129
+ return { free: false, remaining: 0, total: limit, reason: "global_cap_reached", globalRemaining: 0 };
130
+ }
131
+ if (count >= limit) {
132
+ return { free: false, remaining: 0, total: limit, reason: "limit_reached" };
133
+ }
134
+ return {
135
+ free: true,
136
+ remaining: remaining - 1,
137
+ // after this call
138
+ total: limit,
139
+ ...globalCap != null ? { globalRemaining: globalCap - localGlobalCount - 1 } : {}
140
+ };
141
+ }
142
+ function localRecord(path, buyerId) {
143
+ const key = localKey(path, buyerId);
144
+ const period = currentPeriodStart();
145
+ const entry = localUsage.get(key);
146
+ const now = (/* @__PURE__ */ new Date()).toISOString();
147
+ if (entry && isCurrentPeriod(entry)) {
148
+ entry.count++;
149
+ entry.lastCall = now;
150
+ } else {
151
+ localUsage.set(key, { count: 1, periodStart: period, lastCall: now });
152
+ }
153
+ localGlobalCount++;
154
+ }
155
+ function exportLocalData() {
156
+ const usage = [];
157
+ for (const [key, entry] of localUsage.entries()) {
158
+ const firstSlash = key.indexOf("/");
159
+ const colonAfterPath = key.indexOf(":", firstSlash > -1 ? firstSlash : 0);
160
+ const path = colonAfterPath > -1 ? key.slice(0, colonAfterPath) : key;
161
+ const buyerId = colonAfterPath > -1 ? key.slice(colonAfterPath + 1) : "unknown";
162
+ usage.push({
163
+ buyerId,
164
+ path,
165
+ count: entry.count,
166
+ periodStart: entry.periodStart,
167
+ lastCall: entry.lastCall
168
+ });
169
+ }
170
+ return {
171
+ mode: "local",
172
+ config: { perBuyerLimit: limit, resetPeriod, globalCap, paths: pluginPaths },
173
+ usage,
174
+ globalCount: localGlobalCount,
175
+ exportedAt: (/* @__PURE__ */ new Date()).toISOString()
176
+ };
177
+ }
178
+ const cache = /* @__PURE__ */ new Map();
179
+ function resolveHeaders() {
180
+ return {
181
+ "X-Service-Key": config.serviceKey,
182
+ "Content-Type": "application/json"
183
+ };
184
+ }
185
+ function cloudCacheKey(path, buyerId) {
100
186
  return `${config.serviceKey}:${path}:${buyerId}`;
101
187
  }
102
188
  async function checkFreeTier(path, buyerId) {
103
- const key = cacheKey(path, buyerId);
189
+ const key = cloudCacheKey(path, buyerId);
104
190
  const now = Date.now();
105
191
  const cached = cache.get(key);
106
192
  if (cached && cached.expiresAt > now) {
@@ -129,7 +215,7 @@ function freeTier(config) {
129
215
  headers: resolveHeaders(),
130
216
  body: JSON.stringify({ path, buyerId })
131
217
  });
132
- cache.delete(cacheKey(path, buyerId));
218
+ cache.delete(cloudCacheKey(path, buyerId));
133
219
  } catch {
134
220
  }
135
221
  }
@@ -145,7 +231,6 @@ function freeTier(config) {
145
231
  return;
146
232
  }
147
233
  }
148
- const paths = config.paths ?? ["*"];
149
234
  await fetch(`${base}/v1/plugins/free-tier/config`, {
150
235
  method: "PUT",
151
236
  headers: resolveHeaders(),
@@ -153,7 +238,7 @@ function freeTier(config) {
153
238
  perBuyerLimit: config.perBuyerLimit,
154
239
  resetPeriod: config.resetPeriod ?? "none",
155
240
  globalCap: config.globalCap ?? null,
156
- paths
241
+ paths: pluginPaths
157
242
  })
158
243
  });
159
244
  } catch (err) {
@@ -162,21 +247,39 @@ function freeTier(config) {
162
247
  }
163
248
  return {
164
249
  name: "free-tier",
250
+ mode: isLocal ? "local" : "cloud",
251
+ getUsageData() {
252
+ if (!isLocal) return null;
253
+ return exportLocalData();
254
+ },
165
255
  async onInit() {
256
+ if (isLocal) {
257
+ console.log(`[relai:freeTier] Running in local mode (in-memory). perBuyerLimit=${limit}, resetPeriod=${resetPeriod}`);
258
+ return;
259
+ }
166
260
  await syncConfig();
167
261
  },
168
262
  async beforePaymentCheck(req, ctx) {
169
263
  const requestPath = ctx.path || "/";
170
- const paths = config.paths ?? ["*"];
171
- const pathMatches = paths.includes("*") || paths.some((p) => {
172
- const normalized = p.toLowerCase().replace(/\/+$/, "") || "/";
173
- const reqNormalized = requestPath.toLowerCase().replace(/\/+$/, "") || "/";
174
- return normalized === reqNormalized || normalized === "*";
175
- });
176
- if (!pathMatches) {
264
+ if (!pathMatches(requestPath)) return {};
265
+ const buyerId = resolveBuyerId(req);
266
+ if (isLocal) {
267
+ const result2 = localCheck(requestPath, buyerId);
268
+ if (result2.free) {
269
+ localRecord(requestPath, buyerId);
270
+ return {
271
+ skip: true,
272
+ headers: {
273
+ "X-Free-Calls-Remaining": String(result2.remaining ?? 0),
274
+ "X-Free-Calls-Total": String(result2.total ?? limit),
275
+ ...result2.globalRemaining != null ? { "X-Free-Calls-Global-Remaining": String(result2.globalRemaining) } : {},
276
+ "X-Free-Tier-Mode": "local"
277
+ },
278
+ meta: { freeTier: true, buyerId, remaining: result2.remaining, total: result2.total ?? limit, mode: "local" }
279
+ };
280
+ }
177
281
  return {};
178
282
  }
179
- const buyerId = resolveBuyerId(req);
180
283
  const result = await checkFreeTier(requestPath, buyerId);
181
284
  if (result.free) {
182
285
  recordCall(requestPath, buyerId).catch(() => {
@@ -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 crypto from 'crypto';\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?: 'none' | '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 /** Service key (sk_live_...) for tracking bridge usage per API owner */\n serviceKey?: string;\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 // Hash service key for tracking (never expose raw key to clients)\n // Must match backend: crypto.createHash(\"sha256\").update(key).digest(\"hex\").slice(0, 16)\n let serviceKeyHash: string | null = null;\n if (config?.serviceKey) {\n serviceKeyHash = crypto.createHash('sha256').update(config.serviceKey).digest('hex').slice(0, 16);\n }\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 ...(serviceKeyHash ? { serviceKeyHash } : {}),\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 ?? 'none',\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 crypto from 'crypto';\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 syncing with RelAI backend. If omitted, runs in local in-memory mode. */\n serviceKey?: string;\n /** Max free calls per buyer per period */\n perBuyerLimit: number;\n /** Reset period for per-buyer counters */\n resetPeriod?: 'none' | '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 /** Service key (sk_live_...) for tracking bridge usage per API owner */\n serviceKey?: string;\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 // Hash service key for tracking (never expose raw key to clients)\n // Must match backend: crypto.createHash(\"sha256\").update(key).digest(\"hex\").slice(0, 16)\n let serviceKeyHash: string | null = null;\n if (config?.serviceKey) {\n serviceKeyHash = crypto.createHash('sha256').update(config.serviceKey).digest('hex').slice(0, 16);\n }\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 ...(serviceKeyHash ? { serviceKeyHash } : {}),\n },\n };\n\n return response;\n },\n };\n}\n\n// ============================================================================\n// Free Tier Plugin\n// ============================================================================\n\n/**\n * Extended plugin interface with data export for free tier.\n */\nexport interface FreeTierPlugin extends RelaiPlugin {\n /** Export all in-memory usage data (local mode only). Returns null when using cloud mode. */\n getUsageData(): FreeTierUsageExport | null;\n /** Whether plugin is running in local (in-memory) or cloud (RelAI backend) mode. */\n readonly mode: 'local' | 'cloud';\n}\n\nexport interface FreeTierUsageExport {\n mode: 'local';\n config: {\n perBuyerLimit: number;\n resetPeriod: string;\n globalCap: number | null;\n paths: string[];\n };\n /** Per-buyer usage entries */\n usage: Array<{\n buyerId: string;\n path: string;\n count: number;\n periodStart: string;\n lastCall: string;\n }>;\n globalCount: number;\n exportedAt: string;\n}\n\nexport function freeTier(config: FreeTierPluginConfig): FreeTierPlugin {\n const base = (config.baseUrl ?? RELAI_API_BASE).replace(/\\/$/, '');\n const cacheTtl = config.cacheTtlMs ?? 5000;\n const isLocal = !config.serviceKey;\n const resetPeriod = config.resetPeriod ?? 'none';\n const limit = config.perBuyerLimit;\n const globalCap = config.globalCap ?? null;\n const pluginPaths = config.paths ?? ['*'];\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 pathMatches(requestPath: string): boolean {\n return pluginPaths.includes('*') || pluginPaths.some((p) => {\n const normalized = p.toLowerCase().replace(/\\/+$/, '') || '/';\n const reqNormalized = requestPath.toLowerCase().replace(/\\/+$/, '') || '/';\n return normalized === reqNormalized || normalized === '*';\n });\n }\n\n // ---------------------------------------------------------------------------\n // LOCAL IN-MEMORY MODE\n // ---------------------------------------------------------------------------\n\n interface LocalUsageEntry {\n count: number;\n periodStart: string;\n lastCall: string;\n }\n\n // Map key: \"path:buyerId\"\n const localUsage = new Map<string, LocalUsageEntry>();\n let localGlobalCount = 0;\n\n function currentPeriodStart(): string {\n const now = new Date();\n if (resetPeriod === 'daily') {\n return new Date(now.getFullYear(), now.getMonth(), now.getDate()).toISOString();\n }\n if (resetPeriod === 'monthly') {\n return new Date(now.getFullYear(), now.getMonth(), 1).toISOString();\n }\n return 'none'; // permanent\n }\n\n function isCurrentPeriod(entry: LocalUsageEntry): boolean {\n if (resetPeriod === 'none') return true;\n return entry.periodStart === currentPeriodStart();\n }\n\n function localKey(path: string, buyerId: string): string {\n return `${path}:${buyerId}`;\n }\n\n function localCheck(path: string, buyerId: string): FreeTierCheckResponse {\n const key = localKey(path, buyerId);\n const entry = localUsage.get(key);\n\n // If entry is from a previous period, it's stale — treat as 0\n const count = (entry && isCurrentPeriod(entry)) ? entry.count : 0;\n const remaining = Math.max(0, limit - count);\n\n // Global cap check\n if (globalCap != null && localGlobalCount >= globalCap) {\n return { free: false, remaining: 0, total: limit, reason: 'global_cap_reached', globalRemaining: 0 };\n }\n\n if (count >= limit) {\n return { free: false, remaining: 0, total: limit, reason: 'limit_reached' };\n }\n\n return {\n free: true,\n remaining: remaining - 1, // after this call\n total: limit,\n ...(globalCap != null ? { globalRemaining: globalCap - localGlobalCount - 1 } : {}),\n };\n }\n\n function localRecord(path: string, buyerId: string): void {\n const key = localKey(path, buyerId);\n const period = currentPeriodStart();\n const entry = localUsage.get(key);\n const now = new Date().toISOString();\n\n if (entry && isCurrentPeriod(entry)) {\n entry.count++;\n entry.lastCall = now;\n } else {\n localUsage.set(key, { count: 1, periodStart: period, lastCall: now });\n }\n localGlobalCount++;\n }\n\n function exportLocalData(): FreeTierUsageExport {\n const usage: FreeTierUsageExport['usage'] = [];\n for (const [key, entry] of localUsage.entries()) {\n // key format is \"path:buyerId\" but buyerId itself can contain ':'\n // We stored it as `${path}:${buyerId}` where path starts with '/'\n // So we find the first ':' after the path portion\n const firstSlash = key.indexOf('/');\n const colonAfterPath = key.indexOf(':', firstSlash > -1 ? firstSlash : 0);\n const path = colonAfterPath > -1 ? key.slice(0, colonAfterPath) : key;\n const buyerId = colonAfterPath > -1 ? key.slice(colonAfterPath + 1) : 'unknown';\n usage.push({\n buyerId,\n path,\n count: entry.count,\n periodStart: entry.periodStart,\n lastCall: entry.lastCall,\n });\n }\n return {\n mode: 'local',\n config: { perBuyerLimit: limit, resetPeriod, globalCap, paths: pluginPaths },\n usage,\n globalCount: localGlobalCount,\n exportedAt: new Date().toISOString(),\n };\n }\n\n // ---------------------------------------------------------------------------\n // CLOUD MODE (original — requires serviceKey)\n // ---------------------------------------------------------------------------\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 function cloudCacheKey(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 = cloudCacheKey(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(cloudCacheKey(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 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 ?? 'none',\n globalCap: config.globalCap ?? null,\n paths: pluginPaths,\n }),\n });\n } catch (err) {\n console.warn(`[relai:freeTier] Failed to sync config to RelAI: ${err}`);\n }\n }\n\n // ---------------------------------------------------------------------------\n // Plugin instance\n // ---------------------------------------------------------------------------\n\n return {\n name: 'free-tier',\n mode: isLocal ? 'local' : 'cloud',\n\n getUsageData(): FreeTierUsageExport | null {\n if (!isLocal) return null;\n return exportLocalData();\n },\n\n async onInit() {\n if (isLocal) {\n console.log(`[relai:freeTier] Running in local mode (in-memory). perBuyerLimit=${limit}, resetPeriod=${resetPeriod}`);\n return;\n }\n await syncConfig();\n },\n\n async beforePaymentCheck(req, ctx) {\n const requestPath = ctx.path || '/';\n if (!pathMatches(requestPath)) return {};\n\n const buyerId = resolveBuyerId(req);\n\n // ── Local mode ──\n if (isLocal) {\n const result = localCheck(requestPath, buyerId);\n if (result.free) {\n localRecord(requestPath, buyerId);\n return {\n skip: true,\n headers: {\n 'X-Free-Calls-Remaining': String(result.remaining ?? 0),\n 'X-Free-Calls-Total': String(result.total ?? limit),\n ...(result.globalRemaining != null\n ? { 'X-Free-Calls-Global-Remaining': String(result.globalRemaining) }\n : {}),\n 'X-Free-Tier-Mode': 'local',\n },\n meta: { freeTier: true, buyerId, remaining: result.remaining, total: result.total ?? limit, mode: 'local' },\n };\n }\n return {};\n }\n\n // ── Cloud mode ──\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":["result"]}
@@ -43,8 +43,8 @@ interface RelaiPlugin {
43
43
  enrich402Response?(response: any, ctx: PluginContext): any;
44
44
  }
45
45
  interface FreeTierPluginConfig {
46
- /** Service key (sk_live_...) for authenticating with RelAI API */
47
- serviceKey: string;
46
+ /** Service key (sk_live_...) for syncing with RelAI backend. If omitted, runs in local in-memory mode. */
47
+ serviceKey?: string;
48
48
  /** Max free calls per buyer per period */
49
49
  perBuyerLimit: number;
50
50
  /** Reset period for per-buyer counters */
@@ -139,7 +139,35 @@ interface BridgePluginConfig {
139
139
  * ```
140
140
  */
141
141
  declare function bridge(config?: BridgePluginConfig): RelaiPlugin;
142
- declare function freeTier(config: FreeTierPluginConfig): RelaiPlugin;
142
+ /**
143
+ * Extended plugin interface with data export for free tier.
144
+ */
145
+ interface FreeTierPlugin extends RelaiPlugin {
146
+ /** Export all in-memory usage data (local mode only). Returns null when using cloud mode. */
147
+ getUsageData(): FreeTierUsageExport | null;
148
+ /** Whether plugin is running in local (in-memory) or cloud (RelAI backend) mode. */
149
+ readonly mode: 'local' | 'cloud';
150
+ }
151
+ interface FreeTierUsageExport {
152
+ mode: 'local';
153
+ config: {
154
+ perBuyerLimit: number;
155
+ resetPeriod: string;
156
+ globalCap: number | null;
157
+ paths: string[];
158
+ };
159
+ /** Per-buyer usage entries */
160
+ usage: Array<{
161
+ buyerId: string;
162
+ path: string;
163
+ count: number;
164
+ periodStart: string;
165
+ lastCall: string;
166
+ }>;
167
+ globalCount: number;
168
+ exportedAt: string;
169
+ }
170
+ declare function freeTier(config: FreeTierPluginConfig): FreeTierPlugin;
143
171
 
144
172
  interface RelaiServerConfig {
145
173
  /** Network to accept payments on */
@@ -281,4 +309,4 @@ declare class Relai {
281
309
  protect(options: ProtectOptions): (req: any, res: any, next: any) => Promise<any>;
282
310
  }
283
311
 
284
- 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 };
312
+ 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, type FreeTierPlugin as j, type FreeTierUsageExport as k, freeTier as l, stripePayTo as s };
@@ -43,8 +43,8 @@ interface RelaiPlugin {
43
43
  enrich402Response?(response: any, ctx: PluginContext): any;
44
44
  }
45
45
  interface FreeTierPluginConfig {
46
- /** Service key (sk_live_...) for authenticating with RelAI API */
47
- serviceKey: string;
46
+ /** Service key (sk_live_...) for syncing with RelAI backend. If omitted, runs in local in-memory mode. */
47
+ serviceKey?: string;
48
48
  /** Max free calls per buyer per period */
49
49
  perBuyerLimit: number;
50
50
  /** Reset period for per-buyer counters */
@@ -139,7 +139,35 @@ interface BridgePluginConfig {
139
139
  * ```
140
140
  */
141
141
  declare function bridge(config?: BridgePluginConfig): RelaiPlugin;
142
- declare function freeTier(config: FreeTierPluginConfig): RelaiPlugin;
142
+ /**
143
+ * Extended plugin interface with data export for free tier.
144
+ */
145
+ interface FreeTierPlugin extends RelaiPlugin {
146
+ /** Export all in-memory usage data (local mode only). Returns null when using cloud mode. */
147
+ getUsageData(): FreeTierUsageExport | null;
148
+ /** Whether plugin is running in local (in-memory) or cloud (RelAI backend) mode. */
149
+ readonly mode: 'local' | 'cloud';
150
+ }
151
+ interface FreeTierUsageExport {
152
+ mode: 'local';
153
+ config: {
154
+ perBuyerLimit: number;
155
+ resetPeriod: string;
156
+ globalCap: number | null;
157
+ paths: string[];
158
+ };
159
+ /** Per-buyer usage entries */
160
+ usage: Array<{
161
+ buyerId: string;
162
+ path: string;
163
+ count: number;
164
+ periodStart: string;
165
+ lastCall: string;
166
+ }>;
167
+ globalCount: number;
168
+ exportedAt: string;
169
+ }
170
+ declare function freeTier(config: FreeTierPluginConfig): FreeTierPlugin;
143
171
 
144
172
  interface RelaiServerConfig {
145
173
  /** Network to accept payments on */
@@ -281,4 +309,4 @@ declare class Relai {
281
309
  protect(options: ProtectOptions): (req: any, res: any, next: any) => Promise<any>;
282
310
  }
283
311
 
284
- 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 };
312
+ 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, type FreeTierPlugin as j, type FreeTierUsageExport as k, freeTier as l, stripePayTo as s };
package/dist/server.d.cts CHANGED
@@ -1,2 +1,2 @@
1
1
  import './types-Y9ni5XwY.cjs';
2
- export { D as DynamicPrice, b as PaymentInfo, P as ProtectOptions, R as Relai, d as RelaiIntegritasFlow, e as RelaiIntegritasOptions, a as RelaiServerConfig, S as SettleResult, c as StripePayTo, R as default, s as stripePayTo } from './server-BZNDcLCU.cjs';
2
+ export { D as DynamicPrice, b as PaymentInfo, P as ProtectOptions, R as Relai, d as RelaiIntegritasFlow, e as RelaiIntegritasOptions, a as RelaiServerConfig, S as SettleResult, c as StripePayTo, R as default, s as stripePayTo } from './server-CyfEHW9D.cjs';
package/dist/server.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  import './types-Y9ni5XwY.js';
2
- export { D as DynamicPrice, b as PaymentInfo, P as ProtectOptions, R as Relai, d as RelaiIntegritasFlow, e as RelaiIntegritasOptions, a as RelaiServerConfig, S as SettleResult, c as StripePayTo, R as default, s as stripePayTo } from './server-Ba5C2Bv7.js';
2
+ export { D as DynamicPrice, b as PaymentInfo, P as ProtectOptions, R as Relai, d as RelaiIntegritasFlow, e as RelaiIntegritasOptions, a as RelaiServerConfig, S as SettleResult, c as StripePayTo, R as default, s as stripePayTo } from './server-CaSmhDnd.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@relai-fi/x402",
3
- "version": "0.5.38",
3
+ "version": "0.5.39",
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",