import { Novu } from "npm:@novu/api@1.6.0"; import { corsHeaders } from '../_shared/cors.ts'; import { edgeLogger } from '../_shared/logger.ts'; import { formatEdgeError } from '../_shared/errorFormatter.ts'; import { createEdgeFunction } from '../_shared/edgeFunctionWrapper.ts'; import { validateString } from '../_shared/typeValidation.ts'; export default createEdgeFunction( { name: 'create-novu-subscriber', requireAuth: false, corsHeaders: corsHeaders }, async (req, context) => { const novuApiKey = Deno.env.get('NOVU_API_KEY'); if (!novuApiKey) { throw new Error('NOVU_API_KEY is not configured'); } const novu = new Novu({ secretKey: novuApiKey }); const { subscriberId, email, firstName, lastName, phone, avatar, data } = await req.json(); // Validate required fields validateString(subscriberId, 'subscriberId', { requestId: context.requestId }); validateString(email, 'email', { requestId: context.requestId }); // Validate email format const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(email)) { throw new Error('Invalid email format. Please provide a valid email address'); } // Validate optional fields if provided if (firstName !== undefined && firstName !== null && (typeof firstName !== 'string' || firstName.length > 100)) { throw new Error('firstName must be a string with maximum 100 characters'); } if (lastName !== undefined && lastName !== null && (typeof lastName !== 'string' || lastName.length > 100)) { throw new Error('lastName must be a string with maximum 100 characters'); } if (phone !== undefined && phone !== null) { if (typeof phone !== 'string') { throw new Error('phone must be a string'); } const phoneRegex = /^\+?[1-9]\d{1,14}$/; if (!phoneRegex.test(phone.replace(/[\s\-\(\)]/g, ''))) { throw new Error('Invalid phone format. Please provide a valid international phone number'); } } if (avatar !== undefined && avatar !== null && (typeof avatar !== 'string' || !avatar.startsWith('http'))) { throw new Error('avatar must be a valid URL'); } if (data !== undefined && data !== null) { if (typeof data !== 'object' || Array.isArray(data)) { throw new Error('data must be a valid object'); } const dataSize = JSON.stringify(data).length; if (dataSize > 10240) { throw new Error('data field is too large (maximum 10KB)'); } } context.span.setAttribute('action', 'create_novu_subscriber'); edgeLogger.info('Creating Novu subscriber', { subscriberId, email: '***', firstName, requestId: context.requestId }); const subscriber = await novu.subscribers.identify(subscriberId, { email, firstName, lastName, phone, avatar, data, }); edgeLogger.info('Subscriber created successfully', { subscriberId: subscriber.data._id, requestId: context.requestId }); // Add subscriber to "users" topic for global announcements try { edgeLogger.info('Adding subscriber to users topic', { subscriberId, requestId: context.requestId }); await novu.topics.addSubscribers('users', { subscribers: [subscriberId], }); edgeLogger.info('Successfully added subscriber to users topic', { subscriberId, requestId: context.requestId }); } catch (topicError: unknown) { // Non-blocking - log error but don't fail the request edgeLogger.error('Failed to add subscriber to users topic', { error: formatEdgeError(topicError), subscriberId, requestId: context.requestId }); } return new Response( JSON.stringify({ success: true, subscriberId: subscriber.data._id, }), { headers: { 'Content-Type': 'application/json' }, status: 200, } ); } );