diff --git a/docker-compose.yml b/docker-compose.yml index eca3228..78b8c7f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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} diff --git a/modules/rcart/mod.ts b/modules/rcart/mod.ts index b13448b..560dd6b 100644 --- a/modules/rcart/mod.ts +++ b/modules/rcart/mod.ts @@ -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 = { 8453: 'base', 84532: 'base', 1: 'ethereum' }; diff --git a/modules/rflows/lib/transak-onramp.ts b/modules/rflows/lib/transak-onramp.ts index 6eeee37..6cfeb68 100644 --- a/modules/rflows/lib/transak-onramp.ts +++ b/modules/rflows/lib/transak-onramp.ts @@ -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 { - const apiKey = process.env.TRANSAK_API_KEY; + const apiKey = getTransakApiKey(); if (!apiKey) throw new Error('Transak not configured'); const widgetParams: Record = { diff --git a/modules/rflows/mod.ts b/modules/rflows/mod.ts index e1eabb4..335c468 100644 --- a/modules/rflows/mod.ts +++ b/modules/rflows/mod.ts @@ -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"); diff --git a/scripts/seed-infisical.sh b/scripts/seed-infisical.sh index a349e39..11f87ea 100755 --- a/scripts/seed-infisical.sh +++ b/scripts/seed-infisical.sh @@ -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) " diff --git a/shared/transak.ts b/shared/transak.ts index 96d9336..ecb162f 100644 --- a/shared/transak.ts +++ b/shared/transak.ts @@ -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 { - 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);