/** * Transak API utilities — shared across rFlows and rCart. * * Uses the Secure Widget URL API (mandatory since 2025): * 1. Get a Partner Access Token via refresh-token endpoint (cached 7 days) * 2. Call the gateway session endpoint with widget params * 3. Return the widgetUrl containing a single-use sessionId * * Environment: TRANSAK_ENV controls staging vs production. * API keys are split: TRANSAK_API_KEY_STAGING / TRANSAK_API_KEY_PRODUCTION * (falls back to legacy TRANSAK_API_KEY if per-env keys aren't set). */ export type TransakEnv = 'STAGING' | 'PRODUCTION'; // Both token refresh and session creation go through the gateway const GATEWAY_URLS = { STAGING: 'https://api-gateway-stg.transak.com', PRODUCTION: 'https://api-gateway.transak.com', } as const; // Fallback: legacy partner API (if gateway token refresh fails) const API_URLS = { STAGING: 'https://api-stg.transak.com', PRODUCTION: 'https://api.transak.com', } as const; // Cached access token (valid for 7 days) let _cachedToken: { token: string; expiresAt: number; env: TransakEnv } | null = null; /** Get the current Transak environment */ export function getTransakEnv(): TransakEnv { return (process.env.TRANSAK_ENV as TransakEnv) || 'STAGING'; } /** Get the API key for the current Transak environment */ export function getTransakApiKey(): string { const env = getTransakEnv(); return (env === 'PRODUCTION' ? process.env.TRANSAK_API_KEY_PRODUCTION : process.env.TRANSAK_API_KEY_STAGING ) || process.env.TRANSAK_API_KEY || ''; } /** Get the API secret for the current Transak environment */ export function getTransakApiSecret(): string { const env = getTransakEnv(); return (env === 'PRODUCTION' ? process.env.TRANSAK_WEBHOOK_SECRET_PRODUCTION : process.env.TRANSAK_WEBHOOK_SECRET_STAGING ) || process.env.TRANSAK_WEBHOOK_SECRET || ''; } /** Get the webhook secret for the current Transak environment */ export function getTransakWebhookSecret(): string { return getTransakApiSecret(); } /** Extract root domain from a hostname (e.g. "demo.rspace.online" → "rspace.online") */ export function extractRootDomain(host: string): string { const parts = host.replace(/:\d+$/, '').split('.'); return parts.length > 2 ? parts.slice(-2).join('.') : parts.join('.'); } /** * Get a Partner Access Token (cached for 7 days). * Calls the refresh-token endpoint only when the cached token has expired. */ async function getAccessToken(): Promise { const env = getTransakEnv(); const now = Math.floor(Date.now() / 1000); if (_cachedToken && _cachedToken.env === env && _cachedToken.expiresAt > now + 60) { return _cachedToken.token; } const apiKey = getTransakApiKey(); const apiSecret = getTransakApiSecret(); if (!apiKey || !apiSecret) throw new Error('Transak API key or secret not configured'); const body = JSON.stringify({ apiKey }); const headers = { 'Content-Type': 'application/json', 'api-secret': apiSecret }; // Try gateway first (required for production session tokens), then legacy API const urls = [ `${GATEWAY_URLS[env]}/partners/api/v2/refresh-token`, `${API_URLS[env]}/partners/api/v2/refresh-token`, ]; let lastError = ''; for (const url of urls) { try { const res = await fetch(url, { method: 'POST', headers, body }); if (!res.ok) { lastError = `${url}: ${res.status} ${await res.text()}`; console.warn(`[transak] Token refresh failed at ${url}: ${res.status}`); continue; } const data = await res.json() as { data: { accessToken: string; expiresAt: number } }; _cachedToken = { token: data.data.accessToken, expiresAt: data.data.expiresAt, env, }; console.log(`[transak] Access token refreshed from ${url} (env=${env}, expires=${new Date(data.data.expiresAt * 1000).toISOString()})`); return _cachedToken.token; } catch (err) { lastError = `${url}: ${err}`; console.warn(`[transak] Token refresh error at ${url}:`, err); } } throw new Error(`Transak refresh-token failed on all endpoints: ${lastError}`); } /** * Create a Secure Widget URL via the Transak gateway session API. * Returns a URL with a single-use sessionId (valid for 5 minutes). */ export async function createSecureWidgetUrl( widgetParams: Record, ): Promise { const env = getTransakEnv(); const accessToken = await getAccessToken(); const gatewayUrl = GATEWAY_URLS[env]; const res = await fetch(`${gatewayUrl}/api/v2/auth/session`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'access-token': accessToken, }, body: JSON.stringify({ widgetParams }), }); if (!res.ok) { const err = await res.text(); console.error(`[transak] Gateway session failed (${res.status}):`, err); // Fall back to direct URL construction if gateway fails (e.g. IP not whitelisted) console.warn('[transak] Falling back to direct widget URL'); return createTransakWidgetUrl(widgetParams as Record); } const data = await res.json() as { data: { widgetUrl: string } }; return data.data.widgetUrl; } /** Legacy: build widget URL from query parameters (fallback only) */ export function createTransakWidgetUrl(params: Record): string { const env = getTransakEnv(); const base = env === 'PRODUCTION' ? 'https://global.transak.com' : 'https://global-stg.transak.com'; const url = new URL(base); for (const [key, value] of Object.entries(params)) { if (value != null && value !== '') { url.searchParams.set(key, String(value)); } } return url.toString(); }