/** * Workflow API Call Proxy Endpoint * * This endpoint acts as a proxy for API calls made during workflow execution. * It handles the new API node structure with proper authorization and avoids CORS issues. */ // Helper function to substitute variables in a string function substituteVariables(str, variables) { if (typeof str !== 'string') return str; // Replace {{variable}} first str = str.replace(/\{\{\s*([a-zA-Z0-9_]+)\s*\}\}/g, (match, varName) => { const value = variables[varName]; if (value === undefined || value === null) return ''; if (typeof value === 'object') { return JSON.stringify(value); } return String(value); }); // Then replace {variable} str = str.replace(/\{([a-zA-Z0-9_]+)\}/g, (match, varName) => { const value = variables[varName]; if (value === undefined || value === null) return ''; if (typeof value === 'object') { return JSON.stringify(value); } return String(value); }); return str; } // Build API request from node data function buildApiRequest(nodeData, variables) { // 1. URL (with param substitution) let url = substituteVariables(nodeData.apiUrl, variables); // 2. Params (for GET, DELETE, etc.) let params = Array.isArray(nodeData.params) ? nodeData.params : []; if (params.length) { const query = params .filter(p => p.key) .map(p => `${encodeURIComponent(substituteVariables(p.key, variables))}=${encodeURIComponent(substituteVariables(p.value, variables))}`) .join('&'); if (query) { url += (url.includes('?') ? '&' : '?') + query; } } // 3. Headers let headers = {}; if (Array.isArray(nodeData.headers)) { nodeData.headers.forEach(h => { if (h.key) headers[substituteVariables(h.key, variables)] = substituteVariables(h.value, variables); }); } else if (typeof nodeData.headers === 'object') { headers = { ...nodeData.headers }; } // 4. Authorization if (nodeData.authorization && nodeData.authorization.type && nodeData.authorization.type !== 'none') { const auth = nodeData.authorization; if (auth.type === 'bearer' && auth.token) { headers['Authorization'] = `Bearer ${substituteVariables(auth.token, variables)}`; } else if (auth.type === 'basic') { if (auth.token) { // Basic Auth with token (JWT or other token) headers['Authorization'] = `Basic ${substituteVariables(auth.token, variables)}`; } else if (auth.username && auth.password) { // Basic Auth with username/password const token = Buffer.from(`${substituteVariables(auth.username, variables)}:${substituteVariables(auth.password, variables)}`).toString('base64'); headers['Authorization'] = `Basic ${token}`; } } else if (auth.type === 'apiKey' && auth.key && auth.value) { if (auth.in === 'header') { headers[substituteVariables(auth.key, variables)] = substituteVariables(auth.value, variables); } else if (auth.in === 'query') { url += (url.includes('?') ? '&' : '?') + `${encodeURIComponent(substituteVariables(auth.key, variables))}=${encodeURIComponent(substituteVariables(auth.value, variables))}`; } } } // 5. Body let body; if (nodeData.body && nodeData.body.type && nodeData.body.type !== 'none') { if (['form-data', 'x-www-form-urlencoded'].includes(nodeData.body.type)) { const dataArr = Array.isArray(nodeData.body.data) ? nodeData.body.data : []; if (nodeData.body.type === 'form-data') { // For server-side, we'll use URLSearchParams for form-data const formData = new URLSearchParams(); dataArr.forEach(item => { if (item.key) formData.append(substituteVariables(item.key, variables), substituteVariables(item.value, variables)); }); body = formData.toString(); headers['Content-Type'] = 'application/x-www-form-urlencoded'; } else { // x-www-form-urlencoded body = dataArr .filter(item => item.key) .map(item => `${encodeURIComponent(substituteVariables(item.key, variables))}=${encodeURIComponent(substituteVariables(item.value, variables))}`) .join('&'); headers['Content-Type'] = 'application/x-www-form-urlencoded'; } } else if (nodeData.body.type === 'raw') { body = substituteVariables(nodeData.body.data, variables); // Try to detect JSON if (body && body.trim().startsWith('{')) { headers['Content-Type'] = 'application/json'; } } } return { url, headers, body }; } export default defineEventHandler(async (event) => { try { // Get request body const body = await readBody(event); // Extract node configuration and process variables const { nodeData, processVariables } = body; // Validate input if (!nodeData || !nodeData.apiUrl) { return { success: false, error: { message: 'Invalid API node configuration. Missing apiUrl.' } }; } // Build the API request const { url, headers, body: requestBody } = buildApiRequest(nodeData, processVariables); const apiMethod = nodeData.apiMethod || 'GET'; const outputVariable = nodeData.outputVariable || 'apiResponse'; const errorVariable = nodeData.errorVariable || 'apiError'; const continueOnError = nodeData.continueOnError || false; // Prepare fetch options const fetchOptions = { method: apiMethod, headers }; // Add body for non-GET requests if (!['GET', 'HEAD'].includes(apiMethod) && requestBody) { fetchOptions.body = requestBody; } // Make the API call const response = await fetch(url, fetchOptions); // Get response data let responseData; const contentType = response.headers.get('content-type'); if (contentType && contentType.includes('application/json')) { responseData = await response.json(); } else { responseData = await response.text(); } // Prepare result const result = { success: response.ok, data: responseData, status: response.status, statusText: response.statusText, headers: Object.fromEntries([...response.headers.entries()]) }; if (!response.ok) { result.error = { message: `API call failed with status ${response.status}`, status: response.status, statusText: response.statusText, data: responseData }; } return result; } catch (error) { return { success: false, error: { message: error.message || 'An error occurred while making the API call', stack: process.env.NODE_ENV === 'development' ? error.stack : undefined } }; } });