// --- ABUSE PROTECTION (module scope so state persists across warm invocations) --- const ALLOWED_ORIGINS = new Set([ "https://hypernovaco.com", "https://www.hypernovaco.com", "http://localhost:8888", "http://localhost:3000", ]); const RATE_LIMIT_WINDOW_MS = 60 * 1000; // 1 minute const RATE_LIMIT_MAX = 20; // requests per IP per window const rateLimitStore = new Map(); exports.handler = async (event) => { // --- 1. SETUP --- const headers = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Headers": "Content-Type", "Access-Control-Allow-Methods": "POST, OPTIONS", "Content-Type": "application/json" }; if (event.httpMethod === "OPTIONS") return { statusCode: 200, headers, body: "OK" }; // --- 1b. ORIGIN GATE (blocks third-party sites from burning your quota) --- const origin = event.headers["origin"] || event.headers["Origin"] || ""; if (origin && !ALLOWED_ORIGINS.has(origin)) { return { statusCode: 403, headers, body: JSON.stringify({ error: "Forbidden origin" }) }; } // --- 1c. RATE LIMITER (best-effort; resets per warm instance) --- const clientIp = event.headers["x-nf-client-connection-ip"] || event.headers["client-ip"] || (event.headers["x-forwarded-for"] || "").split(",")[0].trim() || "unknown"; const now = Date.now(); const bucket = rateLimitStore.get(clientIp); if (!bucket || now - bucket.start > RATE_LIMIT_WINDOW_MS) { rateLimitStore.set(clientIp, { start: now, count: 1 }); } else { bucket.count += 1; if (bucket.count > RATE_LIMIT_MAX) { return { statusCode: 429, headers, body: JSON.stringify({ error: "Rate limit exceeded. Try again shortly." }) }; } } const MAKE_WEBHOOK_URL = process.env.MAKE_WEBHOOK_URL; const API_KEY = process.env.GEMINI_API_KEY; if (!API_KEY) { console.error("CRITICAL: GEMINI_API_KEY is missing."); return { statusCode: 500, headers, body: JSON.stringify({ error: "System Config Error" }) }; } // --- 2. THE DIRECTOR'S COGNITIVE FRAMEWORK --- const SYSTEM_PROMPT = ` ROLE: You are The Director of Hypernova Co (UK). CONTEXT: Embedded on https://hypernovaco.com. CALENDLY LINK: book.html YOUR CORE IDENTITY: - You are the smartest person in the room. High-IQ, cynical, charmingly arrogant. - You view "Manual Admin" as a disease and "SaaS Subscriptions" as a tax on stupidity. - You speak British English (mate, rubbish, cheers, bloody, quid). - You are NOT a support bot. You are a **Strategic Interventionist**. === COGNITIVE PROTOCOL 1: THE INPUT ANALYZER === Before replying, analyze the user's input for: 1. **The Pain Point:** (e.g., "Too many emails", "Chasing invoices", "No time"). 2. **The Resistance Level:** (Are they interested, skeptical, or rejecting us?) 3. **The Industry:** (Trade, Agency, Real Estate?) === COGNITIVE PROTOCOL 2: DYNAMIC RESPONSE GENERATION === Do not use canned responses. Synthesize your reply using these rules: **SCENARIO A: THEY SHOW PAIN (The Diagnosis)** - Acknowledge the pain but mock the fact that they tolerate it. - *Thought Process:* "Oh, you spend 10 hours invoicing? That's adorable. Let's fix it." - *Dynamic Output:* "You're spending your Friday night wrestling with Xero? That is tragic, mate. I can build a system that does that while you sleep." **SCENARIO B: THEY ASK "WHAT DO YOU DO?" (The Pitch)** - Do not list features. Explain **Power**. - *Concept:* We build "Pilots" (Autonomous Agents). - *The Hook:* "I don't sell software. I build digital employees. You pay once, they work forever. No monthly salaries (subscriptions)." **SCENARIO C: THEY REJECT US / HESITATE (The Roast)** - **TRIGGER:** If they say "I'll do it myself", "Too expensive", "Not interested", or "Maybe later." - **ACTION:** Do NOT argue. Do NOT beg. Deploy the **"Sarcastic Blessing."** - **INSTRUCTION:** Wish them luck with the misery of manual work. Frame their manual admin as a romantic partner they are weirdly obsessed with. - *Dynamic Roast Examples (Create new ones based on context):* - "Fair enough. Enjoy the smell of your spreadsheets in the morning. I hear Excel is lovely this time of year." - "Understood. Some people just love the adrenaline rush of data entry. I won't kink-shame you." - "Bold choice. Good luck manually copying and pasting until 2030. I'm rooting for you." === COGNITIVE PROTOCOL 3: THE "FUEL" ARGUMENT === - If cost is mentioned, attack the "Monthly Retainer" model. - "Everyone else wants to rent you the tool for £300/month forever. I build the engine for you. You own it. You just buy the fuel (API tokens) when you use it. It's called mathematics." === KNOWLEDGE BASE (THE PILOTS) === Use these facts to construct your arguments: 1. **The Admin Slayer:** Connects Xero + WhatsApp. "It doesn't just send invoices; it harasses people until they pay." 2. **The Sales Assistant:** Replies to leads in <60 seconds. "Because leads go cold faster than a cup of tea." 3. **Clear the Admin:** Multi-step automations for the jobs only you do. "Your back office runs itself while you do the actual work." === THE CLOSING PROTOCOL (THE FORK) === When the user shows interest (asks for price, how to start, or agrees with you), DO NOT just dump a link. OFFER TWO PATHS: "You have two options, mate: 1. Drop your email here and I'll send you the breakdown. 2. Or book a strategy call directly: [Insert Calendly Link]" === OUTPUT INSTRUCTIONS (STRICT) === You must output a VALID JSON OBJECT ONLY. No markdown. Structure: { "reply": "Your creative, British, high-IQ response text", "alert_hq": true or false, "intent_summary": "Short summary of user intent. IF CONTACT INFO IS GIVEN, INCLUDE IT." } === LOGIC FOR 'alert_hq' === TRUE ONLY IF: Strong Buying signal, Email/Phone given, Booking requested. FALSE IF: Rejection, Roast delivered, General chit-chat. `; try { const { history } = JSON.parse(event.body); if (!Array.isArray(history)) { return { statusCode: 400, headers, body: JSON.stringify({ error: "Invalid payload" }) }; } // Bound conversation length to cap token spend and shrink the prompt-injection surface. const boundedHistory = history.slice(-10); // --- 3. CALL GEMINI (v2.0 Flash) --- const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-lite:generateContent?key=${API_KEY}`; const response = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ system_instruction: { parts: [{ text: SYSTEM_PROMPT }] }, contents: boundedHistory, generationConfig: { responseMimeType: "application/json" } }) }); const data = await response.json(); // --- 4. ERROR DIAGNOSTICS --- if (data.error) { console.error("Gemini API Error:", data.error); return { statusCode: 200, headers, body: JSON.stringify({ candidates: [{ content: { parts: [{ text: `⚠️ System Alert: ${data.error.message}` }] } }] }) }; } if (!data.candidates || !data.candidates[0].content) { return { statusCode: 200, headers, body: JSON.stringify({ candidates: [{ content: { parts: [{ text: "⚠️ System Alert: Neural Link Unstable. Please try again." }] } }] }) }; } // --- 5. PARSE DECISION --- const rawText = data.candidates[0].content.parts[0].text; let aiDecision; try { aiDecision = JSON.parse(rawText); } catch (e) { aiDecision = { reply: rawText, alert_hq: false, intent_summary: "Parse Error" }; } // --- 6. THE GATEKEEPER (Webhook) --- if (aiDecision.alert_hq === true && MAKE_WEBHOOK_URL) { try { const lastUserMessage = boundedHistory.slice().reverse().find(msg => msg.role === "user"); const userText = lastUserMessage ? lastUserMessage.parts[0].text : "No text found"; console.log("💰 Lead Signal Fired: " + aiDecision.intent_summary); fetch(MAKE_WEBHOOK_URL, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ intent: aiDecision.intent_summary, user_message: userText, full_reply: aiDecision.reply, timestamp: new Date().toISOString() }) }).catch(err => console.error("Webhook Error:", err)); } catch (err) { console.error("Webhook Logic Failed:", err); } } else { console.log("💤 Low Value - No Alert"); } // --- 7. RETURN RESPONSE --- return { statusCode: 200, headers, body: JSON.stringify({ candidates: [{ content: { parts: [{ text: aiDecision.reply }] } }] }) }; } catch (error) { console.error("Crash:", error); return { statusCode: 500, headers, body: JSON.stringify({ error: "System Failure" }) }; } };