import axios from 'axios'; // Shared server-to-server access to the one Nachklang Salesforce org. Both the // newsletter-signup sync (feedback module) and the transactional-email relay // (common.mail) authenticate the same way - OAuth 2.0 client credentials // against the nk_Nachklang_API_Integration external client app - so the token // cache and the retry-once-on-401 live here rather than being duplicated. // // Salesforce's client-credentials token response does not reliably include // expires_in, so the cache lifetime is a conservative guess rather than a // value read from the response - a 401 on the next call just triggers a fresh // fetch (see salesforceApexRestPost). const TOKEN_CACHE_MS = 15 * 60 * 1000; let cachedToken: {accessToken: string; fetchedAt: number} | null = null; export const salesforceEnabled = (): boolean => process.env.SALESFORCE_ENABLED === 'true'; const readConfig = (): {instanceUrl: string; clientId: string; clientSecret: string} => { const instanceUrl = process.env.SALESFORCE_API_URL; const clientId = process.env.SALESFORCE_CLIENT_ID; const clientSecret = process.env.SALESFORCE_CLIENT_SECRET; if (!instanceUrl || !clientId || !clientSecret) { throw new Error('SALESFORCE_ENABLED is true but SALESFORCE_API_URL/SALESFORCE_CLIENT_ID/SALESFORCE_CLIENT_SECRET are not fully configured.'); } return {instanceUrl, clientId, clientSecret}; }; const getAccessToken = async (forceRefresh: boolean): Promise => { if (!forceRefresh && cachedToken && Date.now() - cachedToken.fetchedAt < TOKEN_CACHE_MS) { return cachedToken.accessToken; } const {instanceUrl, clientId, clientSecret} = readConfig(); const res = await axios.post( `${instanceUrl}/services/oauth2/token`, new URLSearchParams({grant_type: 'client_credentials', client_id: clientId, client_secret: clientSecret}).toString(), {headers: {'Content-Type': 'application/x-www-form-urlencoded'}, timeout: 10000} ); cachedToken = {accessToken: res.data.access_token, fetchedAt: Date.now()}; return cachedToken.accessToken; }; /** * POSTs a JSON body to an Apex REST path (e.g. '/services/apexrest/newsletter/signup') * and returns the parsed response body. Retries once with a forced token * refresh on a 401 - the server-side token may have expired even though our * conservative local TTL has not. All other errors propagate to the caller. */ export const salesforceApexRestPost = async (path: string, body: unknown): Promise => { const {instanceUrl} = readConfig(); const url = `${instanceUrl}${path}`; try { const token = await getAccessToken(false); const res = await axios.post(url, body, {headers: {Authorization: `Bearer ${token}`}, timeout: 10000}); return res.data; } catch (err: any) { if (err?.response?.status === 401) { const token = await getAccessToken(true); const res = await axios.post(url, body, {headers: {Authorization: `Bearer ${token}`}, timeout: 10000}); return res.data; } throw err; } };