import { serve } from "https://deno.land/std@0.168.0/http/server.ts"; import { Novu } from "npm:@novu/node@2.0.2"; // TODO: In production, restrict CORS to specific domains // For now, allowing all origins for development flexibility // Example production config: 'Access-Control-Allow-Origin': 'https://yourdomain.com' const corsHeaders = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type', }; serve(async (req) => { if (req.method === 'OPTIONS') { return new Response(null, { headers: corsHeaders }); } try { const novuApiKey = Deno.env.get('NOVU_API_KEY'); if (!novuApiKey) { throw new Error('NOVU_API_KEY is not configured'); } const novu = new Novu(novuApiKey, { backendUrl: Deno.env.get('VITE_NOVU_API_URL') || 'https://api.novu.co', }); // Parse and validate request body let requestBody; try { requestBody = await req.json(); } catch (parseError) { return new Response( JSON.stringify({ success: false, error: 'Invalid JSON in request body', }), { headers: { ...corsHeaders, 'Content-Type': 'application/json' }, status: 400, } ); } const { subscriberId, email, firstName, lastName, phone, avatar, data } = requestBody; // Validate required fields if (!subscriberId || typeof subscriberId !== 'string' || subscriberId.trim() === '') { return new Response( JSON.stringify({ success: false, error: 'subscriberId is required and must be a non-empty string', }), { headers: { ...corsHeaders, 'Content-Type': 'application/json' }, status: 400, } ); } if (!email || typeof email !== 'string' || email.trim() === '') { return new Response( JSON.stringify({ success: false, error: 'email is required and must be a non-empty string', }), { headers: { ...corsHeaders, 'Content-Type': 'application/json' }, status: 400, } ); } // Validate email format using regex const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(email)) { return new Response( JSON.stringify({ success: false, error: 'Invalid email format. Please provide a valid email address', }), { headers: { ...corsHeaders, 'Content-Type': 'application/json' }, status: 400, } ); } // Validate optional fields if provided if (firstName !== undefined && firstName !== null && (typeof firstName !== 'string' || firstName.length > 100)) { return new Response( JSON.stringify({ success: false, error: 'firstName must be a string with maximum 100 characters', }), { headers: { ...corsHeaders, 'Content-Type': 'application/json' }, status: 400, } ); } if (lastName !== undefined && lastName !== null && (typeof lastName !== 'string' || lastName.length > 100)) { return new Response( JSON.stringify({ success: false, error: 'lastName must be a string with maximum 100 characters', }), { headers: { ...corsHeaders, 'Content-Type': 'application/json' }, status: 400, } ); } if (phone !== undefined && phone !== null) { if (typeof phone !== 'string') { return new Response( JSON.stringify({ success: false, error: 'phone must be a string', }), { headers: { ...corsHeaders, 'Content-Type': 'application/json' }, status: 400, } ); } // Validate phone format (basic validation for international numbers) const phoneRegex = /^\+?[1-9]\d{1,14}$/; if (!phoneRegex.test(phone.replace(/[\s\-\(\)]/g, ''))) { return new Response( JSON.stringify({ success: false, error: 'Invalid phone format. Please provide a valid international phone number', }), { headers: { ...corsHeaders, 'Content-Type': 'application/json' }, status: 400, } ); } } if (avatar !== undefined && avatar !== null && (typeof avatar !== 'string' || !avatar.startsWith('http'))) { return new Response( JSON.stringify({ success: false, error: 'avatar must be a valid URL', }), { headers: { ...corsHeaders, 'Content-Type': 'application/json' }, status: 400, } ); } // Validate data field if provided if (data !== undefined && data !== null) { if (typeof data !== 'object' || Array.isArray(data)) { return new Response( JSON.stringify({ success: false, error: 'data must be a valid object', }), { headers: { ...corsHeaders, 'Content-Type': 'application/json' }, status: 400, } ); } // Check data size (limit to 10KB serialized) const dataSize = JSON.stringify(data).length; if (dataSize > 10240) { return new Response( JSON.stringify({ success: false, error: 'data field is too large (maximum 10KB)', }), { headers: { ...corsHeaders, 'Content-Type': 'application/json' }, status: 400, } ); } } console.log('Creating Novu subscriber:', { subscriberId, email, firstName }); const subscriber = await novu.subscribers.identify(subscriberId, { email, firstName, lastName, phone, avatar, data, }); console.log('Subscriber created successfully:', subscriber.data); return new Response( JSON.stringify({ success: true, subscriberId: subscriber.data._id, }), { headers: { ...corsHeaders, 'Content-Type': 'application/json' }, status: 200, } ); } catch (error: any) { console.error('Error creating Novu subscriber:', error); return new Response( JSON.stringify({ success: false, error: error.message, }), { headers: { ...corsHeaders, 'Content-Type': 'application/json' }, status: 500, } ); } });