rspace-online/shared/transak.ts

153 lines
4.8 KiB
TypeScript

/**
* 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';
const API_URLS = {
STAGING: 'https://api-stg.transak.com',
PRODUCTION: 'https://api.transak.com',
} as const;
const GATEWAY_URLS = {
STAGING: 'https://api-gateway-stg.transak.com',
PRODUCTION: 'https://api-gateway.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<string> {
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 apiUrl = API_URLS[env];
const res = await fetch(`${apiUrl}/partners/api/v2/refresh-token`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'api-secret': apiSecret,
},
body: JSON.stringify({ apiKey }),
});
if (!res.ok) {
const err = await res.text();
throw new Error(`Transak refresh-token failed: ${res.status} ${err}`);
}
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 (env=${env}, expires=${new Date(data.data.expiresAt * 1000).toISOString()})`);
return _cachedToken.token;
}
/**
* 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<string, unknown>,
): Promise<string> {
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<string, string>);
}
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, string>): 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();
}