const express = require('express'); const router = express.Router(); const { v4: uuidv4 } = require('uuid'); const { scrape } = require('../services/firecrawl'); const { parseBrandContext, generateTemplates, processCurl } = require('../services/openai2'); const { sendViaWorkflow } = require('../services/workflowSender'); const { uploadJSON, fetchJSON, uploadImageFromUrl, listImages, listTemplateFiles, deleteBusinessFiles, } = require('../services/pixelbin'); const DEFAULT_EVENTS = require('../config/defaultEvents'); const axios = require('axios'); const MERCHANT_ID = () => process.env.MERCHANT_ID; // ─── Helpers ────────────────────────────────────────────────────────────────── function slugify(text) { return text.toLowerCase().trim().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, ''); } function businessRoot(merchantId, businessId) { return `${merchantId}/${businessId}`; } function indexPath(merchantId) { return merchantId; // index.json lives at {merchantId}/index.json } async function getIndex(merchantId) { const data = await fetchJSON(indexPath(merchantId), 'index'); return Array.isArray(data?.businesses) ? data.businesses : []; } async function saveIndex(merchantId, businesses) { await uploadJSON(indexPath(merchantId), 'index', { businesses }); } // ─── Business CRUD ──────────────────────────────────────────────────────────── // GET /api/businesses router.get('/', async (req, res) => { try { const businesses = await getIndex(MERCHANT_ID()); res.json({ businesses }); } catch (err) { res.status(500).json({ error: err.message }); } }); // POST /api/businesses — create new business from websiteUrl router.post('/', async (req, res) => { try { const { websiteUrl } = req.body; if (!websiteUrl) return res.status(400).json({ error: 'websiteUrl is required' }); const merchantId = MERCHANT_ID(); const businessId = uuidv4(); const bizRoot = businessRoot(merchantId, businessId); const imagesFolder = `${bizRoot}/images`; // 1. Scrape const scrapedData = await scrape(websiteUrl); // 2. Parse brand context const brandContext = await parseBrandContext(scrapedData); // 3. Upload relevant images const imagePaths = []; for (let i = 0; i < Math.min((brandContext.relevantImageUrls || []).length, 5); i++) { const url = await uploadImageFromUrl(brandContext.relevantImageUrls[i], imagesFolder, `image_${i + 1}`); if (url) imagePaths.push(url); } // 4. Build and upload context.json let domain = ''; try { domain = new URL(websiteUrl).hostname; } catch { } const contextJson = { businessId, merchantId, domain, brandName: brandContext.brandName || 'Unknown Brand', tone: brandContext.tone || 'professional', taglines: brandContext.taglines || [], colors: brandContext.colors || [], relevantImagePaths: imagePaths, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }; await uploadJSON(bizRoot, 'context', contextJson); // 5. Init events.json await uploadJSON(bizRoot, 'events', { events: DEFAULT_EVENTS }); // 6. Update index.json const businesses = await getIndex(merchantId); businesses.push({ businessId, brandName: contextJson.brandName, domain: contextJson.domain, createdAt: contextJson.createdAt, updatedAt: contextJson.updatedAt, }); await saveIndex(merchantId, businesses); res.json(contextJson); } catch (err) { console.error('Create business error:', err.message); res.status(500).json({ error: err.message }); } }); // GET /api/businesses/:businessId router.get('/:businessId', async (req, res) => { try { const { businessId } = req.params; const context = await fetchJSON(businessRoot(MERCHANT_ID(), businessId), 'context'); if (!context) return res.status(404).json({ error: 'Business not found' }); res.json(context); } catch (err) { res.status(500).json({ error: err.message }); } }); // DELETE /api/businesses/:businessId router.delete('/:businessId', async (req, res) => { try { const merchantId = MERCHANT_ID(); const { businessId } = req.params; await deleteBusinessFiles(merchantId, businessId); const businesses = await getIndex(merchantId); const updated = businesses.filter(b => b.businessId !== businessId); await saveIndex(merchantId, updated); res.json({ ok: true }); } catch (err) { console.error('Delete business error:', err.message); res.status(500).json({ error: err.message }); } }); // ─── Providers ──────────────────────────────────────────────────────────────── // GET /api/businesses/:businessId/providers router.get('/:businessId/providers', async (req, res) => { try { const data = await fetchJSON(businessRoot(MERCHANT_ID(), req.params.businessId), 'providers'); res.json(data || {}); } catch (err) { res.status(500).json({ error: err.message }); } }); // POST /api/businesses/:businessId/providers // Mandatory fields for publish/send: providerName, senderId, dltEntityId. // Per plan: these are NOT required to save — user can save partial config and is only // blocked when switching a template to Published. senderId format is still validated // if provided, so the stored value is always valid. router.post('/:businessId/providers', async (req, res) => { try { const { providerName, senderId, dltEntityId, authKey } = req.body; // If senderId is provided, it must still meet the format requirement if (senderId && (senderId.length !== 6 || !/^[A-Za-z]+$/.test(senderId))) { return res.status(400).json({ error: 'Sender ID must be exactly 6 alphabetic characters' }); } const config = { providerName: providerName || '', senderId: senderId ? senderId.toUpperCase() : '', dltEntityId: dltEntityId || '', authKey: authKey || '', updatedAt: new Date().toISOString(), }; await uploadJSON(businessRoot(MERCHANT_ID(), req.params.businessId), 'providers', config); res.json(config); } catch (err) { res.status(500).json({ error: err.message }); } }); // ─── Global SMS cURL (Compatibility layer — kept so existing sessions/frontend work) ──────────── // The new multi-profile system is below. These two routes delegate to the active profile. // GET /api/businesses/:businessId/global-sms // Returns the active cURL profile's rawCurl (or legacy global_sms.json as fallback). router.get('/:businessId/global-sms', async (req, res) => { try { const bizRoot = businessRoot(MERCHANT_ID(), req.params.businessId); const activeProfile = await getActiveProfile(bizRoot); if (activeProfile) { return res.json({ rawCurl: activeProfile.rawCurl, updatedAt: activeProfile.updatedAt }); } // Fallback: legacy global_sms.json (present on businesses created before profile system) const data = await fetchJSON(bizRoot, 'global_sms'); res.json(data || {}); } catch (err) { res.status(500).json({ error: err.message }); } }); // POST /api/businesses/:businessId/global-sms // Compat: creates/updates a default profile and sets it active. router.post('/:businessId/global-sms', async (req, res) => { try { const { rawCurl } = req.body; if (!rawCurl || !rawCurl.trim()) { return res.status(400).json({ error: 'rawCurl is required' }); } if (!rawCurl.trim().toLowerCase().startsWith('curl')) { return res.status(400).json({ error: 'rawCurl must be a valid cURL command' }); } const bizRoot = businessRoot(MERCHANT_ID(), req.params.businessId); const profileData = await fetchJSON(bizRoot, 'global_sms_profiles') || { profiles: [] }; const now = new Date().toISOString(); // Find or create the default profile let defaultProfile = profileData.profiles.find(p => p.name === 'Default'); if (defaultProfile) { defaultProfile.rawCurl = rawCurl.trim(); defaultProfile.updatedAt = now; } else { defaultProfile = { id: uuidv4(), name: 'Default', rawCurl: rawCurl.trim(), isDefault: true, createdAt: now, updatedAt: now }; profileData.profiles.push(defaultProfile); } await uploadJSON(bizRoot, 'global_sms_profiles', profileData); await uploadJSON(bizRoot, 'active_curl_profile', { profileId: defaultProfile.id, updatedAt: now }); res.json({ rawCurl: rawCurl.trim(), updatedAt: now }); } catch (err) { res.status(500).json({ error: err.message }); } }); // ─── cURL Profile Helpers ───────────────────────────────────────────────────── async function getActiveProfile(bizRoot) { try { const [profileData, activeRec] = await Promise.all([ fetchJSON(bizRoot, 'global_sms_profiles'), fetchJSON(bizRoot, 'active_curl_profile'), ]); if (!profileData?.profiles?.length) return null; if (activeRec?.profileId) { const found = profileData.profiles.find(p => p.id === activeRec.profileId); if (found) return found; } // Fall back to first profile return profileData.profiles[0]; } catch { return null; } } // ─── cURL Profiles CRUD ──────────────────────────────────────────────────────── // GET /api/businesses/:businessId/global-sms/profiles router.get('/:businessId/global-sms/profiles', async (req, res) => { try { const bizRoot = businessRoot(MERCHANT_ID(), req.params.businessId); const [profileData, activeRec] = await Promise.all([ fetchJSON(bizRoot, 'global_sms_profiles'), fetchJSON(bizRoot, 'active_curl_profile'), ]); const profiles = profileData?.profiles || []; const activeProfileId = activeRec?.profileId || (profiles[0]?.id ?? null); res.json({ profiles, activeProfileId }); } catch (err) { res.status(500).json({ error: err.message }); } }); // POST /api/businesses/:businessId/global-sms/profiles router.post('/:businessId/global-sms/profiles', async (req, res) => { try { const { name, rawCurl, setActive } = req.body; if (!name || !String(name).trim()) { return res.status(400).json({ error: 'name is required' }); } if (!rawCurl || !rawCurl.trim()) { return res.status(400).json({ error: 'rawCurl is required' }); } if (!rawCurl.trim().toLowerCase().startsWith('curl')) { return res.status(400).json({ error: 'rawCurl must be a valid cURL command' }); } const bizRoot = businessRoot(MERCHANT_ID(), req.params.businessId); const profileData = await fetchJSON(bizRoot, 'global_sms_profiles') || { profiles: [] }; const now = new Date().toISOString(); const newProfile = { id: uuidv4(), name: String(name).trim(), rawCurl: rawCurl.trim(), isDefault: false, createdAt: now, updatedAt: now }; profileData.profiles.push(newProfile); await uploadJSON(bizRoot, 'global_sms_profiles', profileData); // Activate this profile if requested or if it is the first one if (setActive || profileData.profiles.length === 1) { await uploadJSON(bizRoot, 'active_curl_profile', { profileId: newProfile.id, updatedAt: now }); } res.json(newProfile); } catch (err) { res.status(500).json({ error: err.message }); } }); // PATCH /api/businesses/:businessId/global-sms/profiles/:profileId router.patch('/:businessId/global-sms/profiles/:profileId', async (req, res) => { try { const { businessId, profileId } = req.params; const { name, rawCurl } = req.body; if (rawCurl !== undefined && !rawCurl.trim().toLowerCase().startsWith('curl')) { return res.status(400).json({ error: 'rawCurl must be a valid cURL command' }); } const bizRoot = businessRoot(MERCHANT_ID(), businessId); const profileData = await fetchJSON(bizRoot, 'global_sms_profiles') || { profiles: [] }; const profile = profileData.profiles.find(p => p.id === profileId); if (!profile) return res.status(404).json({ error: 'Profile not found' }); if (name !== undefined) profile.name = String(name).trim(); if (rawCurl !== undefined) profile.rawCurl = rawCurl.trim(); profile.updatedAt = new Date().toISOString(); await uploadJSON(bizRoot, 'global_sms_profiles', profileData); res.json(profile); } catch (err) { res.status(500).json({ error: err.message }); } }); // DELETE /api/businesses/:businessId/global-sms/profiles/:profileId router.delete('/:businessId/global-sms/profiles/:profileId', async (req, res) => { try { const { businessId, profileId } = req.params; const bizRoot = businessRoot(MERCHANT_ID(), businessId); const profileData = await fetchJSON(bizRoot, 'global_sms_profiles') || { profiles: [] }; const idx = profileData.profiles.findIndex(p => p.id === profileId); if (idx === -1) return res.status(404).json({ error: 'Profile not found' }); if (profileData.profiles.length === 1) { return res.status(400).json({ error: 'Cannot delete the last cURL profile' }); } profileData.profiles.splice(idx, 1); await uploadJSON(bizRoot, 'global_sms_profiles', profileData); // If deleted profile was active, switch to first remaining const activeRec = await fetchJSON(bizRoot, 'active_curl_profile'); if (activeRec?.profileId === profileId) { await uploadJSON(bizRoot, 'active_curl_profile', { profileId: profileData.profiles[0].id, updatedAt: new Date().toISOString() }); } res.json({ ok: true }); } catch (err) { res.status(500).json({ error: err.message }); } }); // POST /api/businesses/:businessId/global-sms/profiles/:profileId/activate router.post('/:businessId/global-sms/profiles/:profileId/activate', async (req, res) => { try { const { businessId, profileId } = req.params; const bizRoot = businessRoot(MERCHANT_ID(), businessId); const profileData = await fetchJSON(bizRoot, 'global_sms_profiles') || { profiles: [] }; const profile = profileData.profiles.find(p => p.id === profileId); if (!profile) return res.status(404).json({ error: 'Profile not found' }); await uploadJSON(bizRoot, 'active_curl_profile', { profileId, updatedAt: new Date().toISOString() }); res.json({ activeProfileId: profileId }); } catch (err) { res.status(500).json({ error: err.message }); } }); // GET /api/businesses/:businessId/global-sms/active router.get('/:businessId/global-sms/active', async (req, res) => { try { const bizRoot = businessRoot(MERCHANT_ID(), req.params.businessId); const [profileData, activeRec] = await Promise.all([ fetchJSON(bizRoot, 'global_sms_profiles'), fetchJSON(bizRoot, 'active_curl_profile'), ]); const profiles = profileData?.profiles || []; const activeProfileId = activeRec?.profileId || (profiles[0]?.id ?? null); const activeProfile = profiles.find(p => p.id === activeProfileId) || profiles[0] || null; res.json({ activeProfile, activeProfileId }); } catch (err) { res.status(500).json({ error: err.message }); } }); // ─── Events ─────────────────────────────────────────────────────────────────── // GET /api/businesses/:businessId/events router.get('/:businessId/events', async (req, res) => { try { const data = await fetchJSON(businessRoot(MERCHANT_ID(), req.params.businessId), 'events'); res.json(data || { events: DEFAULT_EVENTS }); } catch (err) { res.status(500).json({ error: err.message }); } }); // POST /api/businesses/:businessId/events — add custom event router.post('/:businessId/events', async (req, res) => { try { const { label } = req.body; if (!label) return res.status(400).json({ error: 'label is required' }); const bizRoot = businessRoot(MERCHANT_ID(), req.params.businessId); const data = await fetchJSON(bizRoot, 'events') || { events: [...DEFAULT_EVENTS] }; const slug = slugify(label); if (data.events.some(e => e.slug === slug)) { return res.status(409).json({ error: 'An event with this name already exists' }); } const newEvent = { slug, label, isDefault: false }; data.events.push(newEvent); await uploadJSON(bizRoot, 'events', data); res.json(newEvent); } catch (err) { res.status(500).json({ error: err.message }); } }); // DELETE /api/businesses/:businessId/events/:slug router.delete('/:businessId/events/:slug', async (req, res) => { try { const { businessId, slug } = req.params; const bizRoot = businessRoot(MERCHANT_ID(), businessId); const data = await fetchJSON(bizRoot, 'events') || { events: [...DEFAULT_EVENTS] }; const event = data.events.find(e => e.slug === slug); if (!event) return res.status(404).json({ error: 'Event not found' }); if (event.isDefault) return res.status(403).json({ error: 'Cannot delete a default event' }); data.events = data.events.filter(e => e.slug !== slug); await uploadJSON(bizRoot, 'events', data); res.json({ ok: true }); } catch (err) { res.status(500).json({ error: err.message }); } }); // POST /api/businesses/:businessId/events/:slug/generate router.post('/:businessId/events/:slug/generate', async (req, res) => { try { const { businessId, slug } = req.params; const bizRoot = businessRoot(MERCHANT_ID(), businessId); // Requirements check const [context, providers] = await Promise.all([ fetchJSON(bizRoot, 'context'), fetchJSON(bizRoot, 'providers'), ]); if (!context) return res.status(400).json({ error: 'Business context not found.' }); if (!providers?.senderId) return res.status(400).json({ error: 'Provider details must be configured before generating templates.' }); // Require an active cURL profile (new system), falling back to legacy global_sms.json const activeProfile = await getActiveProfile(bizRoot); const legacyGlobalSms = activeProfile ? null : await fetchJSON(bizRoot, 'global_sms'); const activeCurl = activeProfile?.rawCurl || legacyGlobalSms?.rawCurl || null; if (!activeCurl) { return res.status(400).json({ error: 'A cURL profile must be configured and active before generating templates.' }); } const eventsData = await fetchJSON(bizRoot, 'events') || { events: DEFAULT_EVENTS }; const event = eventsData.events.find(e => e.slug === slug); if (!event) return res.status(404).json({ error: 'Event not found' }); const variants = await generateTemplates(context, slug, event.label); const templateJson = { eventSlug: slug, eventLabel: event.label, generatedVariants: variants, selectedTemplate: null, status: 'generated', templateId: '', curlProfileId: activeProfile?.id || null, rawCurl: '', processedCurl: '', variableMap: {}, selectedImagePath: '', updatedAt: new Date().toISOString(), }; await uploadJSON(`${bizRoot}/templates`, slug, templateJson); res.json({ variants }); } catch (err) { console.error('Generate error:', err.message); res.status(500).json({ error: err.message }); } }); // ─── Templates ──────────────────────────────────────────────────────────────── // GET /api/businesses/:businessId/templates/images (must be before /:slug) router.get('/:businessId/templates/images', async (req, res) => { try { const images = await listImages(`${businessRoot(MERCHANT_ID(), req.params.businessId)}/images`); res.json({ images }); } catch (err) { res.status(500).json({ error: err.message }); } }); // GET /api/businesses/:businessId/templates router.get('/:businessId/templates', async (req, res) => { try { const bizRoot = businessRoot(MERCHANT_ID(), req.params.businessId); const folder = `${bizRoot}/templates`; const slugs = await listTemplateFiles(folder); const templates = []; for (const slug of slugs) { const tmpl = await fetchJSON(folder, slug); if (tmpl) templates.push(tmpl); } res.json({ templates }); } catch (err) { res.status(500).json({ error: err.message }); } }); // GET /api/businesses/:businessId/templates/:slug router.get('/:businessId/templates/:slug', async (req, res) => { try { const { businessId, slug } = req.params; const tmpl = await fetchJSON(`${businessRoot(MERCHANT_ID(), businessId)}/templates`, slug); if (!tmpl) return res.status(404).json({ error: 'Template not found' }); res.json(tmpl); } catch (err) { res.status(500).json({ error: err.message }); } }); // POST /api/businesses/:businessId/templates/:slug/select router.post('/:businessId/templates/:slug/select', async (req, res) => { try { const { businessId, slug } = req.params; const { selectedVariant } = req.body; if (!selectedVariant) return res.status(400).json({ error: 'selectedVariant is required' }); const bizRoot = businessRoot(MERCHANT_ID(), businessId); const folder = `${bizRoot}/templates`; const tmpl = await fetchJSON(folder, slug); if (!tmpl) return res.status(404).json({ error: 'Template not found' }); // Resolve active cURL (new profile system first, legacy fallback) const activeProfile = await getActiveProfile(bizRoot); const legacyGlobalSms = activeProfile ? null : await fetchJSON(bizRoot, 'global_sms'); const activeCurl = activeProfile?.rawCurl || legacyGlobalSms?.rawCurl || null; if (!activeCurl) { return res.status(400).json({ error: 'A cURL profile must be configured and active before selecting a template' }); } // Process the cURL against the selected template const { processedCurl, variableMap } = await processCurl(activeCurl, selectedVariant, slug); tmpl.selectedTemplate = selectedVariant; tmpl.generatedVariants = []; // discard non-selected variants tmpl.status = 'pending_whitelisting'; tmpl.curlProfileId = activeProfile?.id || null; // snapshot which profile was used tmpl.rawCurl = activeCurl; tmpl.processedCurl = processedCurl; tmpl.variableMap = variableMap; tmpl.updatedAt = new Date().toISOString(); await uploadJSON(folder, slug, tmpl); res.json(tmpl); } catch (err) { console.error('Select error:', err.message); res.status(500).json({ error: err.message }); } }); // POST /api/businesses/:businessId/templates/:slug/whitelist router.post('/:businessId/templates/:slug/whitelist', async (req, res) => { try { const { businessId, slug } = req.params; const { templateId } = req.body; if (!templateId || !String(templateId).trim()) { return res.status(400).json({ error: 'templateId is required' }); } const folder = `${businessRoot(MERCHANT_ID(), businessId)}/templates`; const tmpl = await fetchJSON(folder, slug); if (!tmpl) return res.status(404).json({ error: 'Template not found' }); if (tmpl.status !== 'pending_whitelisting') { return res.status(400).json({ error: 'Template must be in pending_whitelisting status to whitelist' }); } tmpl.templateId = String(templateId).trim(); tmpl.status = 'whitelisted'; tmpl.updatedAt = new Date().toISOString(); await uploadJSON(folder, slug, tmpl); res.json(tmpl); } catch (err) { res.status(500).json({ error: err.message }); } }); // POST /api/businesses/:businessId/templates/:slug/publish // Handles transition from pending_whitelisting -> whitelisted (Published). // Validates mandatory provider fields, collects toNumber, then calls workflow sender. router.post('/:businessId/templates/:slug/publish', async (req, res) => { try { const { businessId, slug } = req.params; const { templateId, toNumber, providerName, senderId, dltEntityId, authKey } = req.body; if (!templateId || !String(templateId).trim()) { return res.status(400).json({ error: 'templateId is required' }); } if (!toNumber || !String(toNumber).trim()) { return res.status(400).json({ error: 'toNumber is required' }); } const bizRoot = businessRoot(MERCHANT_ID(), businessId); const folder = `${bizRoot}/templates`; // Load template const tmpl = await fetchJSON(folder, slug); if (!tmpl) return res.status(404).json({ error: 'Template not found' }); if (tmpl.status !== 'pending_whitelisting') { return res.status(400).json({ error: 'Template must be in pending_whitelisting status to publish' }); } // Merge any submitted provider fields over stored values const storedProviders = await fetchJSON(bizRoot, 'providers') || {}; const mergedProviders = { providerName: providerName || storedProviders.providerName || '', senderId: senderId ? senderId.toUpperCase() : (storedProviders.senderId || ''), dltEntityId: dltEntityId || storedProviders.dltEntityId || '', authKey: authKey || storedProviders.authKey || '', }; // Validate mandatory fields const missing = []; if (!mergedProviders.providerName) missing.push('providerName'); if (!mergedProviders.senderId) missing.push('senderId'); if (!mergedProviders.dltEntityId) missing.push('dltEntityId'); if (missing.length > 0) { return res.status(422).json({ error: 'Missing mandatory provider fields', missingFields: missing, }); } // Validate senderId format if (mergedProviders.senderId.length !== 6 || !/^[A-Za-z]+$/.test(mergedProviders.senderId)) { return res.status(400).json({ error: 'Sender ID must be exactly 6 alphabetic characters' }); } // Persist any updated provider data const updatedProviders = { ...mergedProviders, updatedAt: new Date().toISOString() }; await uploadJSON(bizRoot, 'providers', updatedProviders); // Mark template as whitelisted tmpl.templateId = String(templateId).trim(); tmpl.status = 'whitelisted'; tmpl.updatedAt = new Date().toISOString(); await uploadJSON(folder, slug, tmpl); // Send via workflow (Published path) let sendResult; try { sendResult = await sendViaWorkflow({ senderId: mergedProviders.senderId, toNumber: String(toNumber).trim(), content: tmpl.selectedTemplate || '', }); } catch (sendErr) { // Template is already whitelisted at this point; report send error separately return res.status(502).json({ error: 'Template published but send failed', details: sendErr.message, template: tmpl, }); } res.json({ success: true, template: tmpl, sendResult, }); } catch (err) { console.error('Publish error:', err.message); res.status(500).json({ error: err.message }); } }); // POST /api/businesses/:businessId/templates/:slug/test // For Published (whitelisted) templates: routes to workflow sender (new path). // Legacy cURL execution code below (executeCurl) is kept intact and is NOT deleted. router.post('/:businessId/templates/:slug/test', async (req, res) => { try { const { businessId, slug } = req.params; const { toNumber } = req.body; if (!toNumber) return res.status(400).json({ error: 'toNumber is required' }); const bizRoot = businessRoot(MERCHANT_ID(), businessId); const folder = `${bizRoot}/templates`; const tmpl = await fetchJSON(folder, slug); if (!tmpl) return res.status(404).json({ error: 'Template not found' }); if (tmpl.status !== 'whitelisted') { return res.status(400).json({ error: 'Template must be whitelisted before testing' }); } if (!tmpl.templateId) { return res.status(400).json({ error: 'templateId must be set before testing' }); } // ── Published send path: route to workflow sender ────────────────────────── // Per plan: Published (whitelisted) templates use the workflow sender module, // not the legacy cURL execution path. The cURL code below remains but is not // reached for whitelisted templates in this branch. const providers = await fetchJSON(bizRoot, 'providers') || {}; if (!providers.senderId) { return res.status(422).json({ error: 'Provider senderId is required for sending' }); } let smsResult; try { smsResult = await sendViaWorkflow({ senderId: providers.senderId, toNumber: String(toNumber).trim(), content: tmpl.selectedTemplate || '', }); } catch (sendErr) { return res.status(502).json({ error: 'SMS send failed', details: sendErr.message }); } res.json({ success: true, statusCode: smsResult.statusCode, response: smsResult.response }); // ── Legacy cURL execution path (preserved, not reached for whitelisted) ──── // The code below is kept for reference and future restoration. // It is NOT executed in the current Published send flow. /* let curlToExecute = String(tmpl.processedCurl || tmpl.rawCurl || ''); curlToExecute = curlToExecute.replace(/\{#toNumber#\}/g, toNumber); curlToExecute = curlToExecute.replace(/\{#to#\}/g, toNumber); curlToExecute = curlToExecute.replace(/\{#mobile#\}/g, toNumber); curlToExecute = curlToExecute.replace(/\{#phone#\}/g, toNumber); let legacyResult; try { legacyResult = await executeCurl(curlToExecute); } catch (curlErr) { return res.status(502).json({ error: 'SMS send failed', details: curlErr.message }); } res.json({ success: true, statusCode: legacyResult.status, response: legacyResult.data }); */ } catch (err) { res.status(500).json({ error: err.message }); } }); /** * Minimal cURL parser: extracts method, URL, headers, and body from a cURL string, * then executes via axios. */ async function executeCurl(curlStr) { const method = /-X\s+([A-Z]+)/i.exec(curlStr)?.[1]?.toLowerCase() || 'post'; const urlMatch = /curl\s+(?:-[^\s]+\s+)*['"]?(https?:\/\/[^\s'"]+)['"]?/i.exec(curlStr); if (!urlMatch) throw new Error('Could not parse URL from cURL command'); const url = urlMatch[1]; // Extract headers (-H "Key: Value") const headers = {}; const headerRegex = /-H\s+['"]([^'"]+)['"]/gi; let hMatch; while ((hMatch = headerRegex.exec(curlStr)) !== null) { const parts = hMatch[1].split(/:\s*(.+)/); if (parts.length >= 2) headers[parts[0].trim()] = parts[1].trim(); } // Extract body (-d or --data or --data-raw) const bodyMatch = /(?:--data-raw|--data|-d)\s+['"]([^'"]+)['"]/i.exec(curlStr) || /(?:--data-raw|--data|-d)\s+(\S+)/i.exec(curlStr); const body = bodyMatch ? bodyMatch[1].replace(/\\n/g, '\n') : undefined; return axios({ method, url, headers, data: body, timeout: 15000, validateStatus: () => true, // don't throw on 4xx/5xx so we can relay the response }); } module.exports = router;