import * as z from "zod" import { defineAction } from "../../automation/actions" import { getBrevoApi, parseBrevoResponse, } from "@automate.ax/integration-contracts/brevo" const BREVO_ACCOUNT = "brevo" const SENDER_IP_SCHEMA = z.object({ /** Sending domain associated with the dedicated IP. */ domain: z.string().trim().min(1), /** Dedicated IPv4 address. */ ip: z.ipv4(), /** Percentage of traffic assigned to this IP. */ weight: z.number().int().min(0).max(100).optional(), }) const SENDER_IPS_SCHEMA = SENDER_IP_SCHEMA.array().superRefine( (ips, context) => { const weights = ips.map(({ weight }) => weight) if (weights.every((weight) => weight === undefined)) return if (weights.some((weight) => weight === undefined)) { context.addIssue({ code: "custom", message: "Provide weights for every dedicated IP or none of them.", }) return } if ( weights.reduce((total, weight) => total + (weight ?? 0), 0) !== 100 ) { context.addIssue({ code: "custom", message: "Dedicated IP weights must total 100.", }) } }, ) const NONEMPTY_SENDER_IPS_SCHEMA = SENDER_IPS_SCHEMA.refine( (ips) => ips.length > 0, { message: "Provide at least one dedicated IP." }, ) const SENDER_SCHEMA = z.object({ /** Whether Brevo has activated the sender. */ active: z.boolean(), /** Sender email address. */ email: z.email(), /** Numeric Brevo sender ID. */ id: z.number().int().positive(), /** Dedicated IP assignments. */ ips: SENDER_IP_SCHEMA.required({ weight: true }).array(), /** Sender display name. */ name: z.string(), }) const ACCOUNT_IP_SCHEMA = z.object({ /** Whether the IP is active. */ active: z.boolean(), /** Associated sending domain. */ domain: z.string(), /** Numeric Brevo IP ID. */ id: z.number().int().positive(), /** Dedicated IPv4 address. */ ip: z.ipv4(), }) const ASSIGNED_IP_SCHEMA = SENDER_IP_SCHEMA.extend({ /** Numeric Brevo IP ID. */ id: z.number().int().positive(), /** Percentage of sender traffic assigned to this IP. */ weight: z.number().int().min(0).max(100), }) /** Lists configured Brevo senders. */ export const listBrevoSenders = defineAction("List Brevo senders") .describe("Lists Brevo senders with optional domain and IP filters.") .account(BREVO_ACCOUNT) .input( z.object({ domain: z.string().trim().min(1).optional(), ip: z.ipv4().optional(), }), ) .output(z.object({ senders: SENDER_SCHEMA.array() })) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { const { senders } = await parseBrevoResponse( await getBrevoApi(account.secret).request("senders", { query: input }), z.object({ senders: SENDER_SCHEMA.array().optional() }), ) return { senders: senders ?? [] } }) /** Creates and starts verification for a Brevo sender. */ export const createBrevoSender = defineAction("Create Brevo sender") .describe("Creates a sender and starts Brevo email verification.") .account(BREVO_ACCOUNT) .input( z.object({ email: z.email(), ips: NONEMPTY_SENDER_IPS_SCHEMA.optional(), name: z.string().trim().min(1), }), ) .output( z.object({ dkimError: z.boolean().optional(), id: z.number().int().positive(), spfError: z.boolean().optional(), }), ) .retry({ replaySafety: "unsafe" }) .handler( async ({ account, input }) => await parseBrevoResponse( await getBrevoApi(account.secret).request("senders", { body: JSON.stringify(input), method: "POST", }), z.object({ dkimError: z.boolean().optional(), id: z.number().int().positive(), spfError: z.boolean().optional(), }), ), ) /** Updates a Brevo sender and its dedicated-IP assignments. */ export const updateBrevoSender = defineAction("Update Brevo sender") .describe("Updates a Brevo sender and dedicated-IP assignments.") .account(BREVO_ACCOUNT) .input( z .object({ email: z.email().optional(), ips: SENDER_IPS_SCHEMA.optional(), name: z.string().trim().min(1).optional(), senderId: z.number().int().positive(), }) .refine(({ email, ips, name }) => email || ips || name, { message: "Provide at least one sender field to update.", }), ) .output(z.void()) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { const { senderId, ...body } = input await getBrevoApi(account.secret).request(`senders/${senderId}`, { body: JSON.stringify(body), method: "PUT", }) }) /** Deletes a Brevo sender. */ export const deleteBrevoSender = defineAction("Delete Brevo sender") .describe("Permanently deletes a Brevo sender.") .account(BREVO_ACCOUNT) .input(z.object({ senderId: z.number().int().positive() })) .output(z.void()) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { await getBrevoApi(account.secret).request(`senders/${input.senderId}`, { method: "DELETE", }) }) /** Validates a Brevo sender with the emailed six-digit code. */ export const validateBrevoSender = defineAction("Validate Brevo sender") .describe("Validates a Brevo sender with its emailed OTP.") .account(BREVO_ACCOUNT) .input( z.object({ otp: z.number().int().min(100_000).max(999_999), senderId: z.number().int().positive(), }), ) .output(z.void()) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { await getBrevoApi(account.secret).request( `senders/${input.senderId}/validate`, { body: JSON.stringify({ otp: input.otp }), method: "PUT" }, ) }) /** Lists dedicated IPs available in the Brevo account. */ export const listBrevoIps = defineAction("List Brevo IPs") .describe("Lists dedicated sending IPs available in the Brevo account.") .account(BREVO_ACCOUNT) .output(z.object({ ips: ACCOUNT_IP_SCHEMA.array() })) .retry({ replaySafety: "safe" }) .handler( async ({ account }) => await parseBrevoResponse( await getBrevoApi(account.secret).request("senders/ips"), z.object({ ips: ACCOUNT_IP_SCHEMA.array() }), ), ) /** Lists dedicated IPs assigned to one Brevo sender. */ export const listBrevoSenderIps = defineAction("List Brevo sender IPs") .describe("Lists dedicated sending IPs assigned to one sender.") .account(BREVO_ACCOUNT) .input(z.object({ senderId: z.number().int().positive() })) .output(z.object({ ips: ASSIGNED_IP_SCHEMA.array() })) .retry({ replaySafety: "safe" }) .handler( async ({ account, input }) => await parseBrevoResponse( await getBrevoApi(account.secret).request( `senders/${input.senderId}/ips`, ), z.object({ ips: ASSIGNED_IP_SCHEMA.array() }), ), )