refactor(transak): split API keys by environment (staging/production)

Add getTransakApiKey() and getTransakWebhookSecret() helpers that
resolve TRANSAK_API_KEY_STAGING or TRANSAK_API_KEY_PRODUCTION based
on TRANSAK_ENV, with fallback to legacy TRANSAK_API_KEY. All consumers
(rcart, rflows, transak-onramp) now use the shared helpers instead of
reading env vars directly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-03-12 12:06:33 -07:00
parent a8b99d3462
commit 357e0bb4c0
6 changed files with 49 additions and 11 deletions

View File

@ -45,7 +45,11 @@ services:
- TWENTY_API_URL=http://twenty-ch-server:3000
- TWENTY_API_TOKEN=${TWENTY_API_TOKEN:-}
- TRANSAK_API_KEY=${TRANSAK_API_KEY:-}
- TRANSAK_API_KEY_STAGING=${TRANSAK_API_KEY_STAGING:-}
- TRANSAK_API_KEY_PRODUCTION=${TRANSAK_API_KEY_PRODUCTION:-}
- TRANSAK_SECRET=${TRANSAK_SECRET:-}
- TRANSAK_WEBHOOK_SECRET_STAGING=${TRANSAK_WEBHOOK_SECRET_STAGING:-}
- TRANSAK_WEBHOOK_SECRET_PRODUCTION=${TRANSAK_WEBHOOK_SECRET_PRODUCTION:-}
- TRANSAK_ENV=${TRANSAK_ENV:-STAGING}
- OLLAMA_URL=http://ollama:11434
- INFISICAL_AI_CLIENT_ID=${INFISICAL_AI_CLIENT_ID}

View File

@ -30,7 +30,7 @@ import {
type CartItem, type CartStatus,
} from './schemas';
import { extractProductFromUrl } from './extract';
import { createTransakWidgetUrl, extractRootDomain } from '../../shared/transak';
import { createTransakWidgetUrl, extractRootDomain, getTransakApiKey } from '../../shared/transak';
import QRCode from 'qrcode';
let _syncServer: SyncServer | null = null;
@ -1363,7 +1363,7 @@ routes.post("/api/payments/:id/transak-session", async (c) => {
const { email } = await c.req.json();
if (!email) return c.json({ error: "Required: email" }, 400);
const transakApiKey = process.env.TRANSAK_API_KEY;
const transakApiKey = getTransakApiKey();
if (!transakApiKey) return c.json({ error: "Transak not configured" }, 503);
const networkMap: Record<number, string> = { 8453: 'base', 84532: 'base', 1: 'ethereum' };

View File

@ -4,18 +4,18 @@
*/
import type { OnrampProvider, OnrampSessionRequest, OnrampSessionResult } from './onramp-provider';
import { createTransakWidgetUrl, extractRootDomain } from '../../../shared/transak';
import { createTransakWidgetUrl, extractRootDomain, getTransakApiKey } from '../../../shared/transak';
export class TransakOnrampAdapter implements OnrampProvider {
id = 'transak' as const;
name = 'Transak';
isAvailable(): boolean {
return !!process.env.TRANSAK_API_KEY;
return !!getTransakApiKey();
}
async createSession(req: OnrampSessionRequest): Promise<OnrampSessionResult> {
const apiKey = process.env.TRANSAK_API_KEY;
const apiKey = getTransakApiKey();
if (!apiKey) throw new Error('Transak not configured');
const widgetParams: Record<string, string> = {

View File

@ -11,6 +11,7 @@ import type { RSpaceModule } from "../../shared/module";
import { getModuleInfoList } from "../../shared/module";
import { verifyEncryptIDToken, extractToken } from "@encryptid/sdk/server";
import { renderLanding } from "./landing";
import { getTransakEnv, getTransakWebhookSecret } from "../../shared/transak";
import type { SyncServer } from '../../server/local-first/sync-server';
import { flowsSchema, flowsDocId, type FlowsDoc, type SpaceFlow, type CanvasFlow } from './schemas';
import { demoNodes } from './lib/presets';
@ -297,7 +298,7 @@ routes.get("/api/onramp/config", (c) => {
routes.get("/api/transak/config", (c) => {
return c.json({
provider: "transak",
environment: process.env.TRANSAK_ENV || "STAGING",
environment: getTransakEnv(),
});
});
@ -305,8 +306,8 @@ routes.post("/api/transak/webhook", async (c) => {
let body: any;
try { body = await c.req.json(); } catch { return c.json({ error: "Invalid JSON" }, 400); }
// HMAC verification — if TRANSAK_WEBHOOK_SECRET is set, validate signature
const webhookSecret = process.env.TRANSAK_WEBHOOK_SECRET;
// HMAC verification — if webhook secret is set, validate signature
const webhookSecret = getTransakWebhookSecret();
if (webhookSecret) {
const signature = c.req.header("x-transak-signature") || "";
const { createHmac } = await import("crypto");

View File

@ -69,8 +69,12 @@ RUNPOD_API_KEY|AI/MI|RunPod API key
X402_PAY_TO|payments|Payment recipient address
MAILCOW_API_KEY|EncryptID|Mailcow admin API key
ENCRYPTID_DEMO_SPACES|EncryptID|Comma-separated demo space slugs
TRANSAK_API_KEY|rFunds|Transak widget API key (public, scoped to app)
TRANSAK_WEBHOOK_SECRET|rFunds|Transak webhook HMAC secret for signature verification
TRANSAK_API_KEY|rFunds|Transak widget API key (legacy fallback)
TRANSAK_API_KEY_STAGING|rFunds|Transak staging API key
TRANSAK_API_KEY_PRODUCTION|rFunds|Transak production API key
TRANSAK_WEBHOOK_SECRET|rFunds|Transak webhook HMAC secret (legacy fallback)
TRANSAK_WEBHOOK_SECRET_STAGING|rFunds|Transak staging webhook HMAC secret
TRANSAK_WEBHOOK_SECRET_PRODUCTION|rFunds|Transak production webhook HMAC secret
TRANSAK_ENV|rFunds|Transak environment: STAGING or PRODUCTION (default: STAGING)
"

View File

@ -4,11 +4,40 @@
* Builds Transak widget URLs using direct query parameters.
* The gateway session API has auth issues, so we use the direct
* URL approach which Transak still supports.
*
* 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).
*/
const TRANSAK_WIDGET_BASE = 'https://global.transak.com';
const TRANSAK_WIDGET_BASE_STG = 'https://global-stg.transak.com';
export type TransakEnv = 'STAGING' | 'PRODUCTION';
/** 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 webhook secret for the current Transak environment */
export function getTransakWebhookSecret(): string {
const env = getTransakEnv();
return (env === 'PRODUCTION'
? process.env.TRANSAK_WEBHOOK_SECRET_PRODUCTION
: process.env.TRANSAK_WEBHOOK_SECRET_STAGING
) || process.env.TRANSAK_WEBHOOK_SECRET || '';
}
/** 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('.');
@ -16,7 +45,7 @@ export function extractRootDomain(host: string): string {
}
export function createTransakWidgetUrl(params: Record<string, string>): string {
const env = process.env.TRANSAK_ENV || 'STAGING';
const env = getTransakEnv();
const base = env === 'PRODUCTION' ? TRANSAK_WIDGET_BASE : TRANSAK_WIDGET_BASE_STG;
const url = new URL(base);