From 43cefd8067459bef3d2e402f32f601a159fbeaea Mon Sep 17 00:00:00 2001 From: neerajgrg Date: Tue, 1 Sep 2026 10:24:20 +0530 Subject: [PATCH 01/11] feat(sendgrid): add SendGrid integration plugin --- packages/corsair/core/constants.ts | 3 + packages/sendgrid/api.test.ts | 20 ++ packages/sendgrid/client.ts | 60 ++++ packages/sendgrid/endpoints.test.ts | 119 ++++++++ packages/sendgrid/endpoints/contacts.ts | 24 ++ packages/sendgrid/endpoints/index.ts | 5 + packages/sendgrid/endpoints/lists.ts | 42 +++ packages/sendgrid/endpoints/mail.ts | 18 ++ packages/sendgrid/endpoints/senders.ts | 23 ++ packages/sendgrid/endpoints/suppressions.ts | 32 +++ packages/sendgrid/endpoints/types.ts | 169 +++++++++++ packages/sendgrid/error-handlers.ts | 31 ++ packages/sendgrid/index.ts | 269 ++++++++++++++++++ packages/sendgrid/integration.test.ts | 26 ++ packages/sendgrid/jest.config.cjs | 55 ++++ packages/sendgrid/package.json | 44 +++ packages/sendgrid/schema.test.ts | 20 ++ packages/sendgrid/schema/database.ts | 9 + packages/sendgrid/schema/index.ts | 4 + packages/sendgrid/tsconfig.json | 20 ++ packages/sendgrid/tsup.config.ts | 15 + packages/sendgrid/webhooks/events.ts | 15 + packages/sendgrid/webhooks/index.ts | 5 + .../sendgrid/webhooks/oauth-tenant-link.ts | 31 ++ packages/sendgrid/webhooks/tenant-matcher.ts | 53 ++++ packages/sendgrid/webhooks/types.test.ts | 90 ++++++ packages/sendgrid/webhooks/types.ts | 114 ++++++++ 27 files changed, 1316 insertions(+) create mode 100644 packages/sendgrid/api.test.ts create mode 100644 packages/sendgrid/client.ts create mode 100644 packages/sendgrid/endpoints.test.ts create mode 100644 packages/sendgrid/endpoints/contacts.ts create mode 100644 packages/sendgrid/endpoints/index.ts create mode 100644 packages/sendgrid/endpoints/lists.ts create mode 100644 packages/sendgrid/endpoints/mail.ts create mode 100644 packages/sendgrid/endpoints/senders.ts create mode 100644 packages/sendgrid/endpoints/suppressions.ts create mode 100644 packages/sendgrid/endpoints/types.ts create mode 100644 packages/sendgrid/error-handlers.ts create mode 100644 packages/sendgrid/index.ts create mode 100644 packages/sendgrid/integration.test.ts create mode 100644 packages/sendgrid/jest.config.cjs create mode 100644 packages/sendgrid/package.json create mode 100644 packages/sendgrid/schema.test.ts create mode 100644 packages/sendgrid/schema/database.ts create mode 100644 packages/sendgrid/schema/index.ts create mode 100644 packages/sendgrid/tsconfig.json create mode 100644 packages/sendgrid/tsup.config.ts create mode 100644 packages/sendgrid/webhooks/events.ts create mode 100644 packages/sendgrid/webhooks/index.ts create mode 100644 packages/sendgrid/webhooks/oauth-tenant-link.ts create mode 100644 packages/sendgrid/webhooks/tenant-matcher.ts create mode 100644 packages/sendgrid/webhooks/types.test.ts create mode 100644 packages/sendgrid/webhooks/types.ts diff --git a/packages/corsair/core/constants.ts b/packages/corsair/core/constants.ts index 90dd40fd8..0d724453a 100644 --- a/packages/corsair/core/constants.ts +++ b/packages/corsair/core/constants.ts @@ -196,6 +196,7 @@ export const BaseProviders = [ 'retailed', 'salesforce', 'securitytrails', + 'sendgrid', 'sentry', 'serpapi', 'sharepoint', @@ -431,6 +432,7 @@ export const ProviderDisplayNames = { retailed: 'Retailed', salesforce: 'Salesforce', securitytrails: 'SecurityTrails', + sendgrid: 'SendGrid', sentry: 'Sentry', serpapi: 'Serpapi', sharepoint: 'SharePoint', @@ -672,6 +674,7 @@ export type AllProviders = | 'retailed' | 'salesforce' | 'securitytrails' + | 'sendgrid' | 'sentry' | 'serpapi' | 'sharepoint' diff --git a/packages/sendgrid/api.test.ts b/packages/sendgrid/api.test.ts new file mode 100644 index 000000000..0aa30cb70 --- /dev/null +++ b/packages/sendgrid/api.test.ts @@ -0,0 +1,20 @@ +import { makeSendGridRequest, SendGridAPIError } from './client'; + +describe('SendGrid API Client', () => { + it('formats request with Bearer authorization header', async () => { + const result = { success: true }; + const apiKey = 'SG.test_key_123'; + + expect(makeSendGridRequest).toBeDefined(); + expect(apiKey).toContain('SG.'); + expect(typeof result.success).toBe('boolean'); + }); + + it('handles API errors properly', () => { + const error = new SendGridAPIError('Unauthorized', '401'); + expect(error).toBeInstanceOf(Error); + expect(error.name).toBe('SendGridAPIError'); + expect(error.message).toBe('Unauthorized'); + expect(error.code).toBe('401'); + }); +}); diff --git a/packages/sendgrid/client.ts b/packages/sendgrid/client.ts new file mode 100644 index 000000000..ff77d2ff1 --- /dev/null +++ b/packages/sendgrid/client.ts @@ -0,0 +1,60 @@ +import type { ApiRequestOptions, OpenAPIConfig } from 'corsair/http'; +import { request } from 'corsair/http'; + +export class SendGridAPIError extends Error { + constructor( + message: string, + public readonly code?: string, + ) { + super(message); + this.name = 'SendGridAPIError'; + } +} + +const SENDGRID_API_BASE = 'https://api.sendgrid.com/v3'; + +export async function makeSendGridRequest( + endpoint: string, + apiKey: string, + options: { + method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; + body?: Record; + query?: Record; + } = {}, +): Promise { + const { method = 'GET', body, query } = options; + + const cleanEndpoint = endpoint.startsWith('/') ? endpoint : `/${endpoint}`; + + const config: OpenAPIConfig = { + BASE: SENDGRID_API_BASE, + VERSION: 'v3', + WITH_CREDENTIALS: false, + CREDENTIALS: 'omit', + TOKEN: apiKey, + HEADERS: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${apiKey}`, + }, + }; + + const requestOptions: ApiRequestOptions = { + method, + url: cleanEndpoint, + body: + method === 'POST' || method === 'PUT' || method === 'PATCH' + ? body + : undefined, + mediaType: 'application/json; charset=utf-8', + query: method === 'GET' ? query : undefined, + }; + + try { + return await request(config, requestOptions); + } catch (error) { + if (error instanceof Error) { + throw new SendGridAPIError(error.message); + } + throw new SendGridAPIError('Unknown error'); + } +} diff --git a/packages/sendgrid/endpoints.test.ts b/packages/sendgrid/endpoints.test.ts new file mode 100644 index 000000000..b6008c7fe --- /dev/null +++ b/packages/sendgrid/endpoints.test.ts @@ -0,0 +1,119 @@ +import { + SendGridEndpointInputSchemas, + SendGridEndpointOutputSchemas, +} from './endpoints/types'; + +describe('SendGrid Endpoint Schemas', () => { + it('validates mail.send input and output schemas', () => { + const validInput = { + personalizations: [ + { + to: [{ email: 'recipient@example.com', name: 'Recipient' }], + subject: 'Test Subject', + }, + ], + from: { email: 'sender@example.com', name: 'Sender' }, + subject: 'Global Subject', + content: [{ type: 'text/plain', value: 'Hello World' }], + }; + const parsedInput = SendGridEndpointInputSchemas.mailSend.parse(validInput); + expect(parsedInput.from.email).toBe('sender@example.com'); + expect(parsedInput.personalizations[0]!.to[0]!.email).toBe( + 'recipient@example.com', + ); + + const validOutput = { success: true }; + const parsedOutput = + SendGridEndpointOutputSchemas.mailSend.parse(validOutput); + expect(parsedOutput.success).toBe(true); + }); + + it('validates contacts.addOrUpdate input and output schemas', () => { + const validInput = { + contacts: [ + { email: 'john.doe@example.com', first_name: 'John', last_name: 'Doe' }, + ], + list_ids: ['list-123'], + }; + const parsedInput = + SendGridEndpointInputSchemas.contactsAddOrUpdate.parse(validInput); + expect(parsedInput.contacts[0]!.email).toBe('john.doe@example.com'); + + const validOutput = { job_id: 'job-456' }; + const parsedOutput = + SendGridEndpointOutputSchemas.contactsAddOrUpdate.parse(validOutput); + expect(parsedOutput.job_id).toBe('job-456'); + }); + + it('validates lists.getAll input and output schemas', () => { + const validInput = { pageSize: 10, pageToken: 'token-abc' }; + const parsedInput = + SendGridEndpointInputSchemas.listsGetAll.parse(validInput); + expect(parsedInput.pageSize).toBe(10); + + const validOutput = { + result: [{ id: 'list-1', name: 'Main List', contact_count: 50 }], + }; + const parsedOutput = + SendGridEndpointOutputSchemas.listsGetAll.parse(validOutput); + expect(parsedOutput.result[0]!.name).toBe('Main List'); + }); + + it('validates lists.create input and output schemas', () => { + const validInput = { name: 'New Subscribers' }; + const parsedInput = + SendGridEndpointInputSchemas.listsCreate.parse(validInput); + expect(parsedInput.name).toBe('New Subscribers'); + + const validOutput = { + id: 'list-999', + name: 'New Subscribers', + contact_count: 0, + }; + const parsedOutput = + SendGridEndpointOutputSchemas.listsCreate.parse(validOutput); + expect(parsedOutput.id).toBe('list-999'); + }); + + it('validates suppressions.getBounces input and output schemas', () => { + const validInput = { start_time: 1600000000, end_time: 1700000000 }; + const parsedInput = + SendGridEndpointInputSchemas.suppressionsGetBounces.parse(validInput); + expect(parsedInput.start_time).toBe(1600000000); + + const validOutput = { + bounces: [ + { + created: 1650000000, + email: 'bounced@example.com', + reason: '550 User unknown', + status: '5.1.1', + }, + ], + }; + const parsedOutput = + SendGridEndpointOutputSchemas.suppressionsGetBounces.parse(validOutput); + expect(parsedOutput.bounces[0]!.email).toBe('bounced@example.com'); + }); + + it('validates senders.getAll input and output schemas', () => { + const validInput = {}; + const parsedInput = + SendGridEndpointInputSchemas.sendersGetAll.parse(validInput); + expect(parsedInput).toBeDefined(); + + const validOutput = { + results: [ + { + id: 1, + nickname: 'Support', + from_email: 'support@example.com', + verified: true, + }, + ], + }; + const parsedOutput = + SendGridEndpointOutputSchemas.sendersGetAll.parse(validOutput); + expect(parsedOutput.results[0]!.verified).toBe(true); + }); +}); diff --git a/packages/sendgrid/endpoints/contacts.ts b/packages/sendgrid/endpoints/contacts.ts new file mode 100644 index 000000000..729a460de --- /dev/null +++ b/packages/sendgrid/endpoints/contacts.ts @@ -0,0 +1,24 @@ +import { logEventFromContext } from 'corsair/core'; +import type { SendGridEndpoints } from '..'; +import { makeSendGridRequest } from '../client'; +import type { SendGridEndpointOutputs } from './types'; + +export const addOrUpdate: SendGridEndpoints['contactsAddOrUpdate'] = async ( + ctx, + input, +) => { + const response = await makeSendGridRequest< + SendGridEndpointOutputs['contactsAddOrUpdate'] + >('marketing/contacts', ctx.key, { + method: 'PUT', + body: input as unknown as Record, + }); + + await logEventFromContext( + ctx, + 'sendgrid.contacts.addOrUpdate', + { ...input }, + 'completed', + ); + return response; +}; diff --git a/packages/sendgrid/endpoints/index.ts b/packages/sendgrid/endpoints/index.ts new file mode 100644 index 000000000..efc9e7d76 --- /dev/null +++ b/packages/sendgrid/endpoints/index.ts @@ -0,0 +1,5 @@ +export * as Contacts from './contacts'; +export * as Lists from './lists'; +export * as Mail from './mail'; +export * as Senders from './senders'; +export * as Suppressions from './suppressions'; diff --git a/packages/sendgrid/endpoints/lists.ts b/packages/sendgrid/endpoints/lists.ts new file mode 100644 index 000000000..6022e23fa --- /dev/null +++ b/packages/sendgrid/endpoints/lists.ts @@ -0,0 +1,42 @@ +import { logEventFromContext } from 'corsair/core'; +import type { SendGridEndpoints } from '..'; +import { makeSendGridRequest } from '../client'; +import type { SendGridEndpointOutputs } from './types'; + +export const getAll: SendGridEndpoints['listsGetAll'] = async (ctx, input) => { + const query: Record = {}; + if (input.pageSize) query.page_size = input.pageSize; + if (input.pageToken) query.page_token = input.pageToken; + + const response = await makeSendGridRequest< + SendGridEndpointOutputs['listsGetAll'] + >('marketing/lists', ctx.key, { + method: 'GET', + query, + }); + + await logEventFromContext( + ctx, + 'sendgrid.lists.getAll', + { ...input }, + 'completed', + ); + return response; +}; + +export const create: SendGridEndpoints['listsCreate'] = async (ctx, input) => { + const response = await makeSendGridRequest< + SendGridEndpointOutputs['listsCreate'] + >('marketing/lists', ctx.key, { + method: 'POST', + body: input as unknown as Record, + }); + + await logEventFromContext( + ctx, + 'sendgrid.lists.create', + { ...input }, + 'completed', + ); + return response; +}; diff --git a/packages/sendgrid/endpoints/mail.ts b/packages/sendgrid/endpoints/mail.ts new file mode 100644 index 000000000..177cb1107 --- /dev/null +++ b/packages/sendgrid/endpoints/mail.ts @@ -0,0 +1,18 @@ +import { logEventFromContext } from 'corsair/core'; +import type { SendGridEndpoints } from '..'; +import { makeSendGridRequest } from '../client'; + +export const send: SendGridEndpoints['mailSend'] = async (ctx, input) => { + await makeSendGridRequest('mail/send', ctx.key, { + method: 'POST', + body: input as unknown as Record, + }); + + await logEventFromContext( + ctx, + 'sendgrid.mail.send', + { ...input }, + 'completed', + ); + return { success: true }; +}; diff --git a/packages/sendgrid/endpoints/senders.ts b/packages/sendgrid/endpoints/senders.ts new file mode 100644 index 000000000..d5e2a33d6 --- /dev/null +++ b/packages/sendgrid/endpoints/senders.ts @@ -0,0 +1,23 @@ +import { logEventFromContext } from 'corsair/core'; +import type { SendGridEndpoints } from '..'; +import { makeSendGridRequest } from '../client'; +import type { SendGridEndpointOutputs } from './types'; + +export const getAll: SendGridEndpoints['sendersGetAll'] = async ( + ctx, + input, +) => { + const response = await makeSendGridRequest< + SendGridEndpointOutputs['sendersGetAll'] + >('verified_senders', ctx.key, { + method: 'GET', + }); + + await logEventFromContext( + ctx, + 'sendgrid.senders.getAll', + { ...input }, + 'completed', + ); + return response; +}; diff --git a/packages/sendgrid/endpoints/suppressions.ts b/packages/sendgrid/endpoints/suppressions.ts new file mode 100644 index 000000000..a3e56327a --- /dev/null +++ b/packages/sendgrid/endpoints/suppressions.ts @@ -0,0 +1,32 @@ +import { logEventFromContext } from 'corsair/core'; +import type { SendGridEndpoints } from '..'; +import { makeSendGridRequest } from '../client'; +import type { SuppressionsGetBouncesOutput } from './types'; + +export const getBounces: SendGridEndpoints['suppressionsGetBounces'] = async ( + ctx, + input, +) => { + const query: Record = {}; + if (input.start_time) query.start_time = input.start_time; + if (input.end_time) query.end_time = input.end_time; + + const response = await makeSendGridRequest( + 'suppression/bounces', + ctx.key, + { + method: 'GET', + query, + }, + ); + + const bounces = Array.isArray(response) ? response : []; + + await logEventFromContext( + ctx, + 'sendgrid.suppressions.getBounces', + { ...input }, + 'completed', + ); + return { bounces } as SuppressionsGetBouncesOutput; +}; diff --git a/packages/sendgrid/endpoints/types.ts b/packages/sendgrid/endpoints/types.ts new file mode 100644 index 000000000..fdb23059e --- /dev/null +++ b/packages/sendgrid/endpoints/types.ts @@ -0,0 +1,169 @@ +import { z } from 'zod'; + +const EmailRecipientSchema = z.object({ + email: z.string().email(), + name: z.string().optional(), +}); + +const PersonalizationSchema = z.object({ + to: z.array(EmailRecipientSchema).min(1), + cc: z.array(EmailRecipientSchema).optional(), + bcc: z.array(EmailRecipientSchema).optional(), + subject: z.string().optional(), + dynamic_template_data: z.record(z.string(), z.unknown()).optional(), +}); + +const ContentSchema = z.object({ + type: z.string(), + value: z.string(), +}); + +const MailSendInputSchema = z.object({ + personalizations: z.array(PersonalizationSchema).min(1), + from: EmailRecipientSchema, + subject: z.string().optional(), + content: z.array(ContentSchema).optional(), + reply_to: EmailRecipientSchema.optional(), + template_id: z.string().optional(), + categories: z.array(z.string()).optional(), +}); + +export type MailSendInput = z.infer; + +const MailSendOutputSchema = z.object({ + success: z.boolean(), + messageId: z.string().optional(), +}); + +export type MailSendOutput = z.infer; + +const ContactInputSchema = z.object({ + email: z.string().email(), + first_name: z.string().optional(), + last_name: z.string().optional(), + custom_fields: z.record(z.string(), z.unknown()).optional(), +}); + +const ContactsAddOrUpdateInputSchema = z.object({ + list_ids: z.array(z.string()).optional(), + contacts: z.array(ContactInputSchema).min(1), +}); + +export type ContactsAddOrUpdateInput = z.infer< + typeof ContactsAddOrUpdateInputSchema +>; + +const ContactsAddOrUpdateOutputSchema = z.object({ + job_id: z.string(), +}); + +export type ContactsAddOrUpdateOutput = z.infer< + typeof ContactsAddOrUpdateOutputSchema +>; + +const ListsGetAllInputSchema = z.object({ + pageSize: z.number().int().positive().optional(), + pageToken: z.string().optional(), +}); + +export type ListsGetAllInput = z.infer; + +const ContactListSchema = z.object({ + id: z.string(), + name: z.string(), + contact_count: z.number(), +}); + +const ListsGetAllOutputSchema = z.object({ + result: z.array(ContactListSchema), + _metadata: z.record(z.string(), z.unknown()).optional(), +}); + +export type ListsGetAllOutput = z.infer; + +const ListsCreateInputSchema = z.object({ + name: z.string().min(1), +}); + +export type ListsCreateInput = z.infer; + +const ListsCreateOutputSchema = ContactListSchema; + +export type ListsCreateOutput = z.infer; + +const SuppressionsGetBouncesInputSchema = z.object({ + start_time: z.number().int().optional(), + end_time: z.number().int().optional(), +}); + +export type SuppressionsGetBouncesInput = z.infer< + typeof SuppressionsGetBouncesInputSchema +>; + +const BounceItemSchema = z.object({ + created: z.number(), + email: z.string(), + reason: z.string(), + status: z.string(), +}); + +const SuppressionsGetBouncesOutputSchema = z.object({ + bounces: z.array(BounceItemSchema), +}); + +export type SuppressionsGetBouncesOutput = z.infer< + typeof SuppressionsGetBouncesOutputSchema +>; + +const SendersGetAllInputSchema = z.object({}); + +export type SendersGetAllInput = z.infer; + +const VerifiedSenderSchema = z.object({ + id: z.number(), + nickname: z.string(), + from_email: z.string(), + verified: z.boolean(), +}); + +const SendersGetAllOutputSchema = z.object({ + results: z.array(VerifiedSenderSchema), +}); + +export type SendersGetAllOutput = z.infer; + +export type SendGridEndpointInputs = { + mailSend: MailSendInput; + contactsAddOrUpdate: ContactsAddOrUpdateInput; + listsGetAll: ListsGetAllInput; + listsCreate: ListsCreateInput; + suppressionsGetBounces: SuppressionsGetBouncesInput; + sendersGetAll: SendersGetAllInput; +}; + +export type SendGridEndpointOutputs = { + mailSend: MailSendOutput; + contactsAddOrUpdate: ContactsAddOrUpdateOutput; + listsGetAll: ListsGetAllOutput; + listsCreate: ListsCreateOutput; + suppressionsGetBounces: SuppressionsGetBouncesOutput; + sendersGetAll: SendersGetAllOutput; +}; + +export const SendGridEndpointInputSchemas = { + mailSend: MailSendInputSchema, + contactsAddOrUpdate: ContactsAddOrUpdateInputSchema, + listsGetAll: ListsGetAllInputSchema, + listsCreate: ListsCreateInputSchema, + suppressionsGetBounces: SuppressionsGetBouncesInputSchema, + sendersGetAll: SendersGetAllInputSchema, +} as const; + +export const SendGridEndpointOutputSchemas = { + mailSend: MailSendOutputSchema, + contactsAddOrUpdate: ContactsAddOrUpdateOutputSchema, + listsGetAll: ListsGetAllOutputSchema, + listsCreate: ListsCreateOutputSchema, + suppressionsGetBounces: SuppressionsGetBouncesOutputSchema, + sendersGetAll: SendersGetAllOutputSchema, +} as const; diff --git a/packages/sendgrid/error-handlers.ts b/packages/sendgrid/error-handlers.ts new file mode 100644 index 000000000..5a4f4c19f --- /dev/null +++ b/packages/sendgrid/error-handlers.ts @@ -0,0 +1,31 @@ +import type { CorsairErrorHandler } from 'corsair/core'; +import { ApiError } from 'corsair/http'; + +export const errorHandlers = { + RATE_LIMIT_ERROR: { + match: (error: Error) => { + if (error instanceof ApiError && error.status === 429) return true; + const msg = error.message.toLowerCase(); + return msg.includes('rate_limited') || msg.includes('429'); + }, + handler: async (error: Error) => { + let retryAfterMs: number | undefined; + if (error instanceof ApiError && error.retryAfter !== undefined) { + retryAfterMs = error.retryAfter; + } + return { maxRetries: 5, headersRetryAfterMs: retryAfterMs }; + }, + }, + AUTH_ERROR: { + match: (error: Error) => { + if (error instanceof ApiError && error.status === 401) return true; + const msg = error.message.toLowerCase(); + return msg.includes('unauthorized') || msg.includes('invalid_auth'); + }, + handler: async () => ({ maxRetries: 0 }), + }, + DEFAULT: { + match: () => true, + handler: async () => ({ maxRetries: 0 }), + }, +} satisfies CorsairErrorHandler; diff --git a/packages/sendgrid/index.ts b/packages/sendgrid/index.ts new file mode 100644 index 000000000..e0fb150ec --- /dev/null +++ b/packages/sendgrid/index.ts @@ -0,0 +1,269 @@ +import type { + AuthTypes, + BindEndpoints, + BindWebhooks, + CorsairEndpoint, + CorsairErrorHandler, + CorsairPlugin, + CorsairPluginContext, + CorsairWebhook, + KeyBuilderContext, + PickAuth, + PluginAuthConfig, + PluginPermissionsConfig, + RequiredPluginEndpointMeta, + RequiredPluginEndpointSchemas, + RequiredPluginWebhookSchemas, +} from 'corsair/core'; +import { Contacts, Lists, Mail, Senders, Suppressions } from './endpoints'; +import type { + SendGridEndpointInputs, + SendGridEndpointOutputs, +} from './endpoints/types'; +import { + SendGridEndpointInputSchemas, + SendGridEndpointOutputSchemas, +} from './endpoints/types'; +import { errorHandlers } from './error-handlers'; +import { SendGridSchema } from './schema'; +import { EventWebhooks } from './webhooks'; +import { resolveSendGridOAuthWebhookTenantLink } from './webhooks/oauth-tenant-link'; +import { matchSendGridTenantWebhook } from './webhooks/tenant-matcher'; +import type { SendGridEvent, SendGridWebhookOutputs } from './webhooks/types'; +import { EmailEventWebhookSchema } from './webhooks/types'; + +export type SendGridPluginOptions = { + authType?: PickAuth<'api_key'>; + key?: string; + webhookSecret?: string; + hooks?: InternalSendGridPlugin['hooks']; + webhookHooks?: InternalSendGridPlugin['webhookHooks']; + errorHandlers?: CorsairErrorHandler; + permissions?: PluginPermissionsConfig; +}; + +export type SendGridContext = CorsairPluginContext< + typeof SendGridSchema, + SendGridPluginOptions +>; + +export type SendGridKeyBuilderContext = + KeyBuilderContext; + +export type SendGridBoundEndpoints = BindEndpoints< + typeof sendGridEndpointsNested +>; + +type SendGridEndpoint = + CorsairEndpoint< + SendGridContext, + SendGridEndpointInputs[K], + SendGridEndpointOutputs[K] + >; + +export type SendGridEndpoints = { + mailSend: SendGridEndpoint<'mailSend'>; + contactsAddOrUpdate: SendGridEndpoint<'contactsAddOrUpdate'>; + listsGetAll: SendGridEndpoint<'listsGetAll'>; + listsCreate: SendGridEndpoint<'listsCreate'>; + suppressionsGetBounces: SendGridEndpoint<'suppressionsGetBounces'>; + sendersGetAll: SendGridEndpoint<'sendersGetAll'>; +}; + +type SendGridWebhook< + K extends keyof SendGridWebhookOutputs, + TEvent, +> = CorsairWebhook; + +export type SendGridWebhooks = { + emailEvent: SendGridWebhook<'emailEvent', SendGridEvent>; +}; + +export type SendGridBoundWebhooks = BindWebhooks; + +const sendGridEndpointsNested = { + mail: { + send: Mail.send, + }, + contacts: { + addOrUpdate: Contacts.addOrUpdate, + }, + lists: { + getAll: Lists.getAll, + create: Lists.create, + }, + suppressions: { + getBounces: Suppressions.getBounces, + }, + senders: { + getAll: Senders.getAll, + }, +} as const; + +const sendGridWebhooksNested = { + events: { + emailEvent: EventWebhooks.emailEvent, + }, +} as const; + +export const sendGridEndpointSchemas = { + 'mail.send': { + input: SendGridEndpointInputSchemas.mailSend, + output: SendGridEndpointOutputSchemas.mailSend, + }, + 'contacts.addOrUpdate': { + input: SendGridEndpointInputSchemas.contactsAddOrUpdate, + output: SendGridEndpointOutputSchemas.contactsAddOrUpdate, + }, + 'lists.getAll': { + input: SendGridEndpointInputSchemas.listsGetAll, + output: SendGridEndpointOutputSchemas.listsGetAll, + }, + 'lists.create': { + input: SendGridEndpointInputSchemas.listsCreate, + output: SendGridEndpointOutputSchemas.listsCreate, + }, + 'suppressions.getBounces': { + input: SendGridEndpointInputSchemas.suppressionsGetBounces, + output: SendGridEndpointOutputSchemas.suppressionsGetBounces, + }, + 'senders.getAll': { + input: SendGridEndpointInputSchemas.sendersGetAll, + output: SendGridEndpointOutputSchemas.sendersGetAll, + }, +} as const satisfies RequiredPluginEndpointSchemas< + typeof sendGridEndpointsNested +>; + +const sendGridWebhookSchemas = { + 'events.emailEvent': { + description: 'SendGrid email event webhook notification', + payload: EmailEventWebhookSchema, + response: EmailEventWebhookSchema, + }, +} as const satisfies RequiredPluginWebhookSchemas< + typeof sendGridWebhooksNested +>; + +const defaultAuthType: AuthTypes = 'api_key' as const; + +const sendGridEndpointMeta = { + 'mail.send': { + riskLevel: 'write', + description: 'Send an email via SendGrid Mail Send API v3', + }, + 'contacts.addOrUpdate': { + riskLevel: 'write', + description: 'Add or update contacts in SendGrid Marketing', + }, + 'lists.getAll': { + riskLevel: 'read', + description: 'Retrieve all marketing contact lists', + }, + 'lists.create': { + riskLevel: 'write', + description: 'Create a new marketing contact list', + }, + 'suppressions.getBounces': { + riskLevel: 'read', + description: 'Retrieve email bounce suppressions', + }, + 'senders.getAll': { + riskLevel: 'read', + description: 'Retrieve verified senders', + }, +} as const satisfies RequiredPluginEndpointMeta; + +export const sendGridAuthConfig = { + api_key: { + account: ['tenant_external_id'] as const, + }, +} as const satisfies PluginAuthConfig; + +export type BaseSendGridPlugin = CorsairPlugin< + 'sendgrid', + typeof SendGridSchema, + typeof sendGridEndpointsNested, + typeof sendGridWebhooksNested, + T, + typeof defaultAuthType +>; + +export type InternalSendGridPlugin = BaseSendGridPlugin; + +export type ExternalSendGridPlugin = + BaseSendGridPlugin; + +export function sendgrid( + incomingOptions: SendGridPluginOptions & T = {} as SendGridPluginOptions & T, +): ExternalSendGridPlugin { + const options = { + ...incomingOptions, + authType: incomingOptions.authType ?? defaultAuthType, + }; + return { + id: 'sendgrid', + authConfig: sendGridAuthConfig, + schema: SendGridSchema, + options: options, + hooks: options.hooks, + webhookHooks: options.webhookHooks, + endpoints: sendGridEndpointsNested, + webhooks: sendGridWebhooksNested, + endpointMeta: sendGridEndpointMeta, + endpointSchemas: sendGridEndpointSchemas, + webhookSchemas: sendGridWebhookSchemas, + pluginWebhookMatcher: (request) => { + const headers = request.headers; + return 'x-twilio-email-event-webhook-signature' in headers; + }, + pluginTenantWebhookMatcher: matchSendGridTenantWebhook, + oauthWebhookTenantLinkResolver: resolveSendGridOAuthWebhookTenantLink, + errorHandlers: { + ...errorHandlers, + ...options.errorHandlers, + }, + keyBuilder: async (ctx: SendGridKeyBuilderContext, source) => { + if (source === 'webhook' && options.webhookSecret) { + return options.webhookSecret; + } + + if (source === 'webhook') { + const res = await ctx.keys.get_webhook_signature(); + return res ?? ''; + } + + if (source === 'endpoint' && options.key) { + return options.key; + } + + if (source === 'endpoint' && ctx.authType === 'api_key') { + const res = await ctx.keys.get_api_key(); + return res ?? ''; + } + + return ''; + }, + } satisfies InternalSendGridPlugin; +} + +export type { + ContactsAddOrUpdateInput, + ContactsAddOrUpdateOutput, + ListsCreateInput, + ListsCreateOutput, + ListsGetAllInput, + ListsGetAllOutput, + MailSendInput, + MailSendOutput, + SendersGetAllInput, + SendersGetAllOutput, + SendGridEndpointInputs, + SendGridEndpointOutputs, + SuppressionsGetBouncesInput, + SuppressionsGetBouncesOutput, +} from './endpoints/types'; +export type { + SendGridEvent, + SendGridWebhookOutputs, +} from './webhooks/types'; diff --git a/packages/sendgrid/integration.test.ts b/packages/sendgrid/integration.test.ts new file mode 100644 index 000000000..e77a9f5f8 --- /dev/null +++ b/packages/sendgrid/integration.test.ts @@ -0,0 +1,26 @@ +import { sendgrid } from './index'; + +describe('SendGrid Plugin Integration', () => { + it('instantiates plugin with default options', () => { + const plugin = sendgrid({ key: 'SG.test_key' }); + expect(plugin.id).toBe('sendgrid'); + expect(plugin.options?.key).toBe('SG.test_key'); + }); + + it('defines endpoints tree correctly', () => { + const plugin = sendgrid({ key: 'SG.test_key' }); + expect(plugin.endpoints?.mail?.send).toBeDefined(); + expect(plugin.endpoints?.contacts?.addOrUpdate).toBeDefined(); + expect(plugin.endpoints?.lists?.getAll).toBeDefined(); + expect(plugin.endpoints?.lists?.create).toBeDefined(); + expect(plugin.endpoints?.suppressions?.getBounces).toBeDefined(); + expect(plugin.endpoints?.senders?.getAll).toBeDefined(); + }); + + it('registers endpoint metadata correctly', () => { + const plugin = sendgrid({ key: 'SG.test_key' }); + expect(plugin.endpointMeta?.['mail.send']?.riskLevel).toBe('write'); + expect(plugin.endpointMeta?.['lists.getAll']?.riskLevel).toBe('read'); + expect(plugin.endpointMeta?.['senders.getAll']?.riskLevel).toBe('read'); + }); +}); diff --git a/packages/sendgrid/jest.config.cjs b/packages/sendgrid/jest.config.cjs new file mode 100644 index 000000000..8c6218f64 --- /dev/null +++ b/packages/sendgrid/jest.config.cjs @@ -0,0 +1,55 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + roots: [''], + testMatch: [ + '**/*.test.ts', + '**/tests/**/*.test.ts', + '**/plugins/**/*.test.ts', + '**/setup/**/*.test.ts', + ], + collectCoverageFrom: [ + '**/*.ts', + '!**/*.d.ts', + '!**/node_modules/**', + '!**/dist/**', + '!jest.config.ts', + '!tests/**', + ], + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'], + transform: { + '^.+\\.yaml$': '/../corsair/jest-yaml-transform.cjs', + '^.+\\.ts$': [ + 'ts-jest', + { + useESM: true, + tsconfig: { + esModuleInterop: true, + allowSyntheticDefaultImports: true, + verbatimModuleSyntax: false, + module: 'ESNext', + moduleResolution: 'Bundler', + }, + }, + ], + '.*\\.js$': [ + 'ts-jest', + { + useESM: true, + tsconfig: { + esModuleInterop: true, + allowSyntheticDefaultImports: true, + }, + }, + ], + }, + moduleNameMapper: { + '^corsair/core$': '/../corsair/core.ts', + '^corsair/http$': '/../corsair/http.ts', + '^(\\.\\.?/.*)\\.js$': '$1', + }, + transformIgnorePatterns: ['node_modules/(?!.*uuid.*)'], + extensionsToTreatAsEsm: ['.ts'], + testTimeout: 30000, + verbose: true, +}; diff --git a/packages/sendgrid/package.json b/packages/sendgrid/package.json new file mode 100644 index 000000000..c56b0e282 --- /dev/null +++ b/packages/sendgrid/package.json @@ -0,0 +1,44 @@ +{ + "name": "@corsair-dev/sendgrid", + "version": "0.1.0", + "description": "SendGrid plugin for Corsair", + "type": "module", + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "dev-source": "./index.ts", + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + }, + "scripts": { + "build": "rm -rf dist && tsc --build --force && tsup", + "typecheck": "tsc --noEmit", + "test": "jest" + }, + "peerDependencies": { + "corsair": ">=0.1.0", + "zod": "^4.1.13" + }, + "devDependencies": { + "@types/jest": "^29.5.14", + "corsair": "workspace:*", + "jest": "^29.7.0", + "ts-jest": "^29.4.9", + "tsup": "^8.0.1", + "typescript": "catalog:", + "zod": "^4.1.13" + }, + "keywords": [ + "corsair", + "sendgrid", + "plugin" + ], + "author": "", + "license": "Apache-2.0", + "files": [ + "dist" + ] +} diff --git a/packages/sendgrid/schema.test.ts b/packages/sendgrid/schema.test.ts new file mode 100644 index 000000000..d700b55b2 --- /dev/null +++ b/packages/sendgrid/schema.test.ts @@ -0,0 +1,20 @@ +import { SendGridSchema } from './schema'; + +describe('SendGrid schema', () => { + it('declares a semver version', () => { + expect(SendGridSchema.version).toBeDefined(); + expect(SendGridSchema.version).toMatch(/^\d+\.\d+\.\d+$/); + }); + + it('declares an entities map', () => { + expect(typeof SendGridSchema.entities).toBe('object'); + expect(SendGridSchema.entities).not.toBeNull(); + expect(Array.isArray(Object.keys(SendGridSchema.entities))).toBe(true); + for (const entity of Object.values(SendGridSchema.entities)) { + expect(entity).toBeDefined(); + } + }); +}); + +// Per .github/PLUGIN_PR_RULES.md (R2), every implemented endpoint +// needs a corresponding test. diff --git a/packages/sendgrid/schema/database.ts b/packages/sendgrid/schema/database.ts new file mode 100644 index 000000000..44371b5c7 --- /dev/null +++ b/packages/sendgrid/schema/database.ts @@ -0,0 +1,9 @@ +import { z } from 'zod'; + +// TODO: Define your database entities here +// export const SendGridExample = z.object({ +// id: z.string(), +// name: z.string(), +// created_at: z.coerce.date().nullable().optional(), +// }); +// export type SendGridExample = z.infer; diff --git a/packages/sendgrid/schema/index.ts b/packages/sendgrid/schema/index.ts new file mode 100644 index 000000000..118a2818c --- /dev/null +++ b/packages/sendgrid/schema/index.ts @@ -0,0 +1,4 @@ +export const SendGridSchema = { + version: '1.0.0', + entities: {}, +} as const; diff --git a/packages/sendgrid/tsconfig.json b/packages/sendgrid/tsconfig.json new file mode 100644 index 000000000..15e507a13 --- /dev/null +++ b/packages/sendgrid/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "lib": ["esnext"], + "types": ["node", "jest"], + "module": "ESNext", + "moduleResolution": "Bundler", + "outDir": "./dist", + "rootDir": "./", + "composite": true, + "incremental": true, + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true, + "skipLibCheck": true + }, + "include": ["./**/*"], + "exclude": ["dist", "node_modules"], + "references": [] +} diff --git a/packages/sendgrid/tsup.config.ts b/packages/sendgrid/tsup.config.ts new file mode 100644 index 000000000..3ec221e23 --- /dev/null +++ b/packages/sendgrid/tsup.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from 'tsup'; + +export default defineConfig({ + clean: false, + dts: false, + format: ['esm'], + target: 'esnext', + platform: 'node', + bundle: true, + splitting: true, + minify: true, + outDir: 'dist', + external: ['corsair', 'zod'], + entry: ['index.ts'], +}); diff --git a/packages/sendgrid/webhooks/events.ts b/packages/sendgrid/webhooks/events.ts new file mode 100644 index 000000000..d45a39ec4 --- /dev/null +++ b/packages/sendgrid/webhooks/events.ts @@ -0,0 +1,15 @@ +import type { SendGridWebhooks } from '..'; +import { createSendGridMatch } from './types'; + +export const emailEvent: SendGridWebhooks['emailEvent'] = { + match: createSendGridMatch(), + handler: async (_ctx, request) => { + const events = Array.isArray(request.payload) ? request.payload : []; + return { + success: true, + data: { + events, + }, + }; + }, +}; diff --git a/packages/sendgrid/webhooks/index.ts b/packages/sendgrid/webhooks/index.ts new file mode 100644 index 000000000..b801efa8c --- /dev/null +++ b/packages/sendgrid/webhooks/index.ts @@ -0,0 +1,5 @@ +import * as events from './events'; + +export const EventWebhooks = { + emailEvent: events.emailEvent, +}; diff --git a/packages/sendgrid/webhooks/oauth-tenant-link.ts b/packages/sendgrid/webhooks/oauth-tenant-link.ts new file mode 100644 index 000000000..d89d4d57a --- /dev/null +++ b/packages/sendgrid/webhooks/oauth-tenant-link.ts @@ -0,0 +1,31 @@ +import type { TokenResponse, WebhookTenantMatch } from 'corsair/core'; +import { asRecord, toExternalId } from 'corsair/core'; + +// TODO: Rename linkType 'tenant_external_id' to match pluginTenantWebhookMatcher. +// Called after OAuth to store the routing id on corsair_accounts.config. +export async function resolveSendGridOAuthWebhookTenantLink( + tokens: TokenResponse, +): Promise { + // TODO: Read from token response when the provider includes a stable id. + // const externalId = toExternalId(asRecord(tokens.team)?.id); + const externalId = toExternalId(tokens.tenant_external_id); + if (externalId) { + return { linkType: 'tenant_external_id', externalId }; + } + + const accessToken = tokens.access_token; + if (!accessToken) return null; + + // TODO: Fetch from provider API when the token response omits the id. + // const response = await fetch('https://api.example.com/me', { + // headers: { Authorization: `Bearer ${accessToken}` }, + // }); + // if (!response.ok) return null; + // const payload = (await response.json()) as { id?: string }; + // const fetchedId = toExternalId(payload.id); + // return fetchedId + // ? { linkType: 'tenant_external_id', externalId: fetchedId } + // : null; + + return null; +} diff --git a/packages/sendgrid/webhooks/tenant-matcher.ts b/packages/sendgrid/webhooks/tenant-matcher.ts new file mode 100644 index 000000000..24579efc1 --- /dev/null +++ b/packages/sendgrid/webhooks/tenant-matcher.ts @@ -0,0 +1,53 @@ +import type { RawWebhookRequest, WebhookTenantMatch } from 'corsair/core'; +import { asRecord, firstString, readBodyRecord } from 'corsair/core'; + +export function matchSendGridTenantWebhook( + request: RawWebhookRequest, +): WebhookTenantMatch | null { + let externalId: string | undefined; + + if (typeof request.body === 'string') { + try { + const parsed = JSON.parse(request.body); + if ( + Array.isArray(parsed) && + parsed.length > 0 && + typeof parsed[0] === 'object' && + parsed[0] !== null + ) { + externalId = firstString([ + (parsed[0] as Record).tenant_external_id, + ]); + } else if (typeof parsed === 'object' && parsed !== null) { + externalId = firstString([ + (parsed as Record).tenant_external_id, + asRecord((parsed as Record).data) + ?.tenant_external_id, + ]); + } + } catch { + // ignore JSON parse failure + } + } else if ( + Array.isArray(request.body) && + request.body.length > 0 && + typeof request.body[0] === 'object' && + request.body[0] !== null + ) { + externalId = firstString([ + (request.body[0] as Record).tenant_external_id, + ]); + } else { + const body = readBodyRecord(request); + if (body) { + externalId = firstString([ + body.tenant_external_id, + asRecord(body.data)?.tenant_external_id, + ]); + } + } + + if (!externalId) return null; + + return { linkType: 'tenant_external_id', externalId }; +} diff --git a/packages/sendgrid/webhooks/types.test.ts b/packages/sendgrid/webhooks/types.test.ts new file mode 100644 index 000000000..21b668b59 --- /dev/null +++ b/packages/sendgrid/webhooks/types.test.ts @@ -0,0 +1,90 @@ +import crypto from 'node:crypto'; +import type { RawWebhookRequest, WebhookRequest } from 'corsair/core'; +import { matchSendGridTenantWebhook } from './tenant-matcher'; +import { createSendGridMatch, verifySendGridWebhookSignature } from './types'; + +describe('SendGrid Webhooks', () => { + it('matches any valid SendGrid email event payload array', () => { + const matcher = createSendGridMatch(); + const request: RawWebhookRequest = { + body: JSON.stringify([ + { email: 'test@example.com', event: 'bounce', timestamp: 123456789 }, + ]), + headers: {}, + }; + expect(matcher(request)).toBe(true); + }); + + it('extracts tenant_external_id from SendGrid array body in tenant matcher', () => { + const request: RawWebhookRequest = { + body: JSON.stringify([ + { + email: 'test@example.com', + event: 'delivered', + tenant_external_id: 'tenant-123', + }, + ]), + headers: {}, + }; + const match = matchSendGridTenantWebhook(request); + expect(match).not.toBeNull(); + expect(match?.externalId).toBe('tenant-123'); + }); + + it('returns null when tenant_external_id is not present', () => { + const request: RawWebhookRequest = { + body: JSON.stringify([{ email: 'test@example.com', event: 'delivered' }]), + headers: {}, + }; + const match = matchSendGridTenantWebhook(request); + expect(match).toBeNull(); + }); + + it('verifies signature when secret is provided', () => { + const keyPair = crypto.generateKeyPairSync('ec', { + namedCurve: 'prime256v1', + publicKeyEncoding: { type: 'spki', format: 'pem' }, + privateKeyEncoding: { type: 'pkcs8', format: 'pem' }, + }); + + const timestamp = '1600000000'; + const payload = [ + { email: 'test@example.com', event: 'delivered', timestamp: 1600000000 }, + ]; + const rawBody = JSON.stringify(payload); + const payloadToSign = timestamp + rawBody; + + const signer = crypto.createSign('SHA256'); + signer.update(payloadToSign); + const signature = signer.sign(keyPair.privateKey, 'base64'); + + const webhookReq: WebhookRequest = { + headers: { + 'x-twilio-email-event-webhook-signature': signature, + 'x-twilio-email-event-webhook-timestamp': timestamp, + }, + payload, + rawBody, + }; + + const result = verifySendGridWebhookSignature( + webhookReq, + keyPair.publicKey, + ); + expect(result.valid).toBe(true); + }); + + it('rejects invalid signature when secret is provided', () => { + const webhookReq: WebhookRequest = { + headers: { + 'x-twilio-email-event-webhook-signature': 'invalid_sig', + 'x-twilio-email-event-webhook-timestamp': '1600000000', + }, + payload: [], + rawBody: '[]', + }; + + const result = verifySendGridWebhookSignature(webhookReq, 'some_key'); + expect(result.valid).toBe(false); + }); +}); diff --git a/packages/sendgrid/webhooks/types.ts b/packages/sendgrid/webhooks/types.ts new file mode 100644 index 000000000..337780bc7 --- /dev/null +++ b/packages/sendgrid/webhooks/types.ts @@ -0,0 +1,114 @@ +import crypto from 'node:crypto'; +import type { + CorsairWebhookMatcher, + RawWebhookRequest, + WebhookRequest, +} from 'corsair/core'; +import { z } from 'zod'; + +export const SendGridEventSchema = z.object({ + email: z.string(), + event: z.string(), + timestamp: z.number(), + sg_event_id: z.string().optional(), + sg_message_id: z.string().optional(), + reason: z.string().optional(), + status: z.string().optional(), + response: z.string().optional(), + tenant_external_id: z.string().optional(), +}); + +export type SendGridEvent = z.infer; + +export const EmailEventWebhookSchema = z.object({ + events: z.array(SendGridEventSchema), +}); + +export type EmailEventWebhookOutput = z.infer; + +export type SendGridWebhookOutputs = { + emailEvent: EmailEventWebhookOutput; +}; + +function parseBody(body: unknown): unknown { + if (typeof body === 'string') { + try { + return JSON.parse(body); + } catch { + return null; + } + } + return body; +} + +export function createSendGridMatch(eventType?: string): CorsairWebhookMatcher { + return (request: RawWebhookRequest) => { + const parsed = parseBody(request.body); + if (Array.isArray(parsed) && parsed.length > 0) { + if (!eventType) return true; + return parsed.some( + (item) => + typeof item === 'object' && + item !== null && + (item as Record).event === eventType, + ); + } + if (typeof parsed === 'object' && parsed !== null) { + if (!eventType) return true; + return (parsed as Record).event === eventType; + } + return false; + }; +} + +export function verifySendGridWebhookSignature( + request: WebhookRequest, + secret: string, +): { valid: boolean; error?: string } { + if (!secret) { + return { valid: true }; + } + + const headers = request.headers; + const signature = + (headers['x-twilio-email-event-webhook-signature'] as string | undefined) || + (headers['X-Twilio-Email-Event-Webhook-Signature'] as string | undefined); + const timestamp = + (headers['x-twilio-email-event-webhook-timestamp'] as string | undefined) || + (headers['X-Twilio-Email-Event-Webhook-Timestamp'] as string | undefined); + + if (!signature || !timestamp) { + return { + valid: false, + error: 'Missing SendGrid signature or timestamp header', + }; + } + + try { + const rawBody = + typeof request.rawBody === 'string' + ? request.rawBody + : JSON.stringify(request.payload); + const payloadToVerify = timestamp + rawBody; + + const verifier = crypto.createVerify('SHA256'); + verifier.update(payloadToVerify); + + const formattedKey = secret.startsWith('-----BEGIN') + ? secret + : `-----BEGIN PUBLIC KEY-----\n${secret}\n-----END PUBLIC KEY-----`; + + const isValid = verifier.verify(formattedKey, signature, 'base64'); + if (!isValid) { + return { valid: false, error: 'Invalid SendGrid webhook signature' }; + } + + return { valid: true }; + } catch (err) { + return { + valid: false, + error: + err instanceof Error ? err.message : 'Signature verification failed', + }; + } +} From 498b7fb2f96cfce73c7f1d11209d120e6be47221 Mon Sep 17 00:00:00 2001 From: neerajgrg Date: Tue, 1 Sep 2026 10:31:10 +0530 Subject: [PATCH 02/11] build: update pnpm-lock.yaml for sendgrid plugin --- pnpm-lock.yaml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dad7573d8..85c5c03de 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5043,6 +5043,30 @@ importers: specifier: 4.4.3 version: 4.4.3 + packages/sendgrid: + devDependencies: + '@types/jest': + specifier: ^29.5.14 + version: 29.5.14 + corsair: + specifier: workspace:* + version: link:../corsair + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) + ts-jest: + specifier: ^29.4.9 + version: 29.4.9(@babel/core@7.29.7)(@jest/transform@29.7.0)(@jest/types@30.4.1)(babel-jest@29.7.0(@babel/core@7.29.7))(esbuild@0.27.0)(jest-util@30.4.1)(jest@29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)))(typescript@5.9.3) + tsup: + specifier: ^8.0.1 + version: 8.5.1(jiti@2.7.0)(postcss@8.5.15)(tsx@4.22.4)(typescript@5.9.3)(yaml@2.9.0) + typescript: + specifier: 'catalog:' + version: 5.9.3 + zod: + specifier: 4.4.3 + version: 4.4.3 + packages/sentry: devDependencies: '@types/jest': From 5e0014ec2ebb1b035d526cd8405edd78e0a8fc1c Mon Sep 17 00:00:00 2001 From: neerajgrg Date: Tue, 1 Sep 2026 10:42:56 +0530 Subject: [PATCH 03/11] fix(sendgrid): remove unused imports --- packages/sendgrid/schema/database.ts | 10 +--------- .../sendgrid/webhooks/oauth-tenant-link.ts | 20 +------------------ 2 files changed, 2 insertions(+), 28 deletions(-) diff --git a/packages/sendgrid/schema/database.ts b/packages/sendgrid/schema/database.ts index 44371b5c7..4400b8920 100644 --- a/packages/sendgrid/schema/database.ts +++ b/packages/sendgrid/schema/database.ts @@ -1,9 +1 @@ -import { z } from 'zod'; - -// TODO: Define your database entities here -// export const SendGridExample = z.object({ -// id: z.string(), -// name: z.string(), -// created_at: z.coerce.date().nullable().optional(), -// }); -// export type SendGridExample = z.infer; +// Database entity schemas for SendGrid plugin diff --git a/packages/sendgrid/webhooks/oauth-tenant-link.ts b/packages/sendgrid/webhooks/oauth-tenant-link.ts index d89d4d57a..9d3999241 100644 --- a/packages/sendgrid/webhooks/oauth-tenant-link.ts +++ b/packages/sendgrid/webhooks/oauth-tenant-link.ts @@ -1,31 +1,13 @@ import type { TokenResponse, WebhookTenantMatch } from 'corsair/core'; -import { asRecord, toExternalId } from 'corsair/core'; +import { toExternalId } from 'corsair/core'; -// TODO: Rename linkType 'tenant_external_id' to match pluginTenantWebhookMatcher. -// Called after OAuth to store the routing id on corsair_accounts.config. export async function resolveSendGridOAuthWebhookTenantLink( tokens: TokenResponse, ): Promise { - // TODO: Read from token response when the provider includes a stable id. - // const externalId = toExternalId(asRecord(tokens.team)?.id); const externalId = toExternalId(tokens.tenant_external_id); if (externalId) { return { linkType: 'tenant_external_id', externalId }; } - const accessToken = tokens.access_token; - if (!accessToken) return null; - - // TODO: Fetch from provider API when the token response omits the id. - // const response = await fetch('https://api.example.com/me', { - // headers: { Authorization: `Bearer ${accessToken}` }, - // }); - // if (!response.ok) return null; - // const payload = (await response.json()) as { id?: string }; - // const fetchedId = toExternalId(payload.id); - // return fetchedId - // ? { linkType: 'tenant_external_id', externalId: fetchedId } - // : null; - return null; } From 4563e44c92bf0150e6421a711e46e96e0c790630 Mon Sep 17 00:00:00 2001 From: neerajgrg Date: Tue, 1 Sep 2026 10:55:14 +0530 Subject: [PATCH 04/11] fix(sendgrid): address review comments --- packages/sendgrid/api.test.ts | 152 +++++++++++++++++++++--- packages/sendgrid/client.ts | 7 +- packages/sendgrid/endpoints/contacts.ts | 6 +- packages/sendgrid/endpoints/mail.ts | 12 +- packages/sendgrid/webhooks/events.ts | 14 ++- 5 files changed, 171 insertions(+), 20 deletions(-) diff --git a/packages/sendgrid/api.test.ts b/packages/sendgrid/api.test.ts index 0aa30cb70..240c317d8 100644 --- a/packages/sendgrid/api.test.ts +++ b/packages/sendgrid/api.test.ts @@ -1,20 +1,144 @@ -import { makeSendGridRequest, SendGridAPIError } from './client'; +import { ApiError } from 'corsair/http'; +import { makeSendGridRequest } from './client'; +import { Contacts, Lists, Mail, Senders, Suppressions } from './endpoints'; -describe('SendGrid API Client', () => { - it('formats request with Bearer authorization header', async () => { - const result = { success: true }; - const apiKey = 'SG.test_key_123'; +describe('SendGrid Endpoints Execution & Error Policies', () => { + const mockCtx: any = { + key: 'SG.test_api_key_123', + authType: 'api_key', + $getAccountId: async () => 'acc-123', + database: {}, + }; - expect(makeSendGridRequest).toBeDefined(); - expect(apiKey).toContain('SG.'); - expect(typeof result.success).toBe('boolean'); + beforeEach(() => { + global.fetch = jest.fn(); }); - it('handles API errors properly', () => { - const error = new SendGridAPIError('Unauthorized', '401'); - expect(error).toBeInstanceOf(Error); - expect(error.name).toBe('SendGridAPIError'); - expect(error.message).toBe('Unauthorized'); - expect(error.code).toBe('401'); + afterEach(() => { + jest.resetAllMocks(); + }); + + function mockResponse(status: number, data: any) { + const bodyText = typeof data === 'string' ? data : JSON.stringify(data); + return { + ok: status >= 200 && status < 300, + status, + statusText: status === 401 ? 'Unauthorized' : 'OK', + headers: { + get: (name: string) => { + if (name.toLowerCase() === 'content-type') return 'application/json'; + return null; + }, + }, + json: async () => (typeof data === 'object' ? data : { message: data }), + text: async () => bodyText, + }; + } + + it('executes Mail.send endpoint with correct request formatting', async () => { + (global.fetch as jest.Mock).mockResolvedValueOnce(mockResponse(202, {})); + + const res = await Mail.send(mockCtx, { + personalizations: [{ to: [{ email: 'recipient@example.com' }] }], + from: { email: 'sender@example.com' }, + subject: 'Test Email', + content: [{ type: 'text/plain', value: 'Hello' }], + }); + + expect(res.success).toBe(true); + expect(global.fetch).toHaveBeenCalledWith( + 'https://api.sendgrid.com/v3/mail/send', + expect.objectContaining({ + method: 'POST', + }), + ); + }); + + it('executes Contacts.addOrUpdate endpoint', async () => { + (global.fetch as jest.Mock).mockResolvedValueOnce( + mockResponse(202, { job_id: 'job-123' }), + ); + + const res = await Contacts.addOrUpdate(mockCtx, { + contacts: [{ email: 'user@example.com', first_name: 'Jane' }], + }); + + expect(res.job_id).toBe('job-123'); + }); + + it('executes Lists.getAll with pagination query parameters', async () => { + (global.fetch as jest.Mock).mockResolvedValueOnce( + mockResponse(200, { + result: [{ id: 'l1', name: 'List 1', contact_count: 5 }], + }), + ); + + const res = await Lists.getAll(mockCtx, { + pageSize: 20, + pageToken: 'token123', + }); + + expect(res.result).toHaveLength(1); + expect(global.fetch).toHaveBeenCalledWith( + 'https://api.sendgrid.com/v3/marketing/lists?page_size=20&page_token=token123', + expect.anything(), + ); + }); + + it('executes Lists.create endpoint', async () => { + (global.fetch as jest.Mock).mockResolvedValueOnce( + mockResponse(201, { id: 'l2', name: 'List 2', contact_count: 0 }), + ); + + const res = await Lists.create(mockCtx, { name: 'List 2' }); + + expect(res.id).toBe('l2'); + }); + + it('executes Suppressions.getBounces endpoint', async () => { + (global.fetch as jest.Mock).mockResolvedValueOnce( + mockResponse(200, [ + { + created: 100, + email: 'b@example.com', + reason: 'Hard bounce', + status: '5.1.1', + }, + ]), + ); + + const res = await Suppressions.getBounces(mockCtx, {}); + + expect(res.bounces).toHaveLength(1); + expect(res.bounces[0]!.email).toBe('b@example.com'); + }); + + it('executes Senders.getAll endpoint', async () => { + (global.fetch as jest.Mock).mockResolvedValueOnce( + mockResponse(200, { + results: [ + { + id: 10, + nickname: 'Primary', + from_email: 's@example.com', + verified: true, + }, + ], + }), + ); + + const res = await Senders.getAll(mockCtx, {}); + + expect(res.results[0]!.verified).toBe(true); + }); + + it('preserves ApiError on HTTP error status (401)', async () => { + (global.fetch as jest.Mock).mockResolvedValueOnce( + mockResponse(401, { errors: [{ message: 'Unauthorized' }] }), + ); + + await expect( + makeSendGridRequest('mail/send', 'SG.key', { method: 'POST' }), + ).rejects.toThrow(ApiError); }); }); diff --git a/packages/sendgrid/client.ts b/packages/sendgrid/client.ts index ff77d2ff1..e98cb7112 100644 --- a/packages/sendgrid/client.ts +++ b/packages/sendgrid/client.ts @@ -1,10 +1,11 @@ import type { ApiRequestOptions, OpenAPIConfig } from 'corsair/http'; -import { request } from 'corsair/http'; +import { ApiError, request } from 'corsair/http'; export class SendGridAPIError extends Error { constructor( message: string, public readonly code?: string, + public readonly status?: number, ) { super(message); this.name = 'SendGridAPIError'; @@ -23,7 +24,6 @@ export async function makeSendGridRequest( } = {}, ): Promise { const { method = 'GET', body, query } = options; - const cleanEndpoint = endpoint.startsWith('/') ? endpoint : `/${endpoint}`; const config: OpenAPIConfig = { @@ -52,6 +52,9 @@ export async function makeSendGridRequest( try { return await request(config, requestOptions); } catch (error) { + if (error instanceof ApiError) { + throw error; + } if (error instanceof Error) { throw new SendGridAPIError(error.message); } diff --git a/packages/sendgrid/endpoints/contacts.ts b/packages/sendgrid/endpoints/contacts.ts index 729a460de..aaf2b018a 100644 --- a/packages/sendgrid/endpoints/contacts.ts +++ b/packages/sendgrid/endpoints/contacts.ts @@ -17,8 +17,12 @@ export const addOrUpdate: SendGridEndpoints['contactsAddOrUpdate'] = async ( await logEventFromContext( ctx, 'sendgrid.contacts.addOrUpdate', - { ...input }, + { + contact_count: input.contacts.length, + list_ids: input.list_ids, + }, 'completed', ); + return response; }; diff --git a/packages/sendgrid/endpoints/mail.ts b/packages/sendgrid/endpoints/mail.ts index 177cb1107..901b5618b 100644 --- a/packages/sendgrid/endpoints/mail.ts +++ b/packages/sendgrid/endpoints/mail.ts @@ -8,11 +8,21 @@ export const send: SendGridEndpoints['mailSend'] = async (ctx, input) => { body: input as unknown as Record, }); + const totalRecipients = input.personalizations.reduce( + (sum, p) => sum + p.to.length, + 0, + ); + await logEventFromContext( ctx, 'sendgrid.mail.send', - { ...input }, + { + from_email: input.from.email, + recipient_count: totalRecipients, + template_id: input.template_id, + }, 'completed', ); + return { success: true }; }; diff --git a/packages/sendgrid/webhooks/events.ts b/packages/sendgrid/webhooks/events.ts index d45a39ec4..ebd37b712 100644 --- a/packages/sendgrid/webhooks/events.ts +++ b/packages/sendgrid/webhooks/events.ts @@ -1,9 +1,19 @@ import type { SendGridWebhooks } from '..'; -import { createSendGridMatch } from './types'; +import { createSendGridMatch, verifySendGridWebhookSignature } from './types'; export const emailEvent: SendGridWebhooks['emailEvent'] = { match: createSendGridMatch(), - handler: async (_ctx, request) => { + handler: async (ctx, request) => { + const webhookSecret = ctx.key; + const verification = verifySendGridWebhookSignature(request, webhookSecret); + if (!verification.valid) { + return { + success: false, + statusCode: 401, + error: verification.error || 'Signature verification failed', + }; + } + const events = Array.isArray(request.payload) ? request.payload : []; return { success: true, From 0787a712652170239ba3af614d8d89fcaa09233d Mon Sep 17 00:00:00 2001 From: neerajgrg Date: Tue, 1 Sep 2026 14:08:51 +0530 Subject: [PATCH 05/11] docs(sendgrid): add demo video --- packages/sendgrid/sendgrid_demo.mp4 | Bin 0 -> 360919 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 packages/sendgrid/sendgrid_demo.mp4 diff --git a/packages/sendgrid/sendgrid_demo.mp4 b/packages/sendgrid/sendgrid_demo.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..22a681455bd05730a096e88d81e03a6c6d354a70 GIT binary patch literal 360919 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 oi8o1>0i&Q_##ubDVpzBGScx9p)RVndOKwOi; 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

m)-ok@%rQU3hT(G-|FB%hxDCDu-s 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

-R$6jy-{Lw}8M^Q>WiVJ&d-*9+i0<2Ex!W8BI zm{6Zf{pJfW;@zp~&{GnlfNA}D9XX&6))PI>gVMj~{i zw{8y2uInSna9RF~hx83wfX+G1SsoTl)g_4~slG-<_|w$+gYsN*lkBI8HEA@aI<&KY z@xFT2m0iE*hES|SI1fJ@RcWKf=Kvi`bbu(94Xc(KVpvE-|$?7883qgd!MNqu(`pQw`-xPv!f>oVNv{bP}vl-Ik_|_ zK~w#Ey;2ySt^nCJEBT*))Z|&L4&ieYHO%zv=+E3faz_o{u;nE>;c{ti5nPgCmTcJ? z1unNj82Ux%J)H9FPUqJ?Wm>=+c8C7~02ew1rf!sOk=WU2=WN?@FlKn=>@u(DezN_m z_1QIY9j&`AK{fdup#+y#x{Yn&4e{Pp`S?~ck@nn$`n0~C!c92niSO`eB>9wf+a%<` z^zrJF_DD=Jh^lZzNB1+zJwEM+(nrr$6THE2DNVHUg*S#i(^7RTt9EB3;R*UN5EJ1= zT2+no6jc*$=_aE%do$MGmQJV*!G^+XhvWsSFC}=6b8co2`#LyrJ`pjk!($Zgm88Z8 zeW=>L{$v+_hC2z2h(_Z;=A*=2P*Dp=7{V2>D=gCu7O!s3QtN~nxQq6*k$VZfIH(~g z$LxaQ2APdGo=G(>?@}9TbB;WNdma2chIg+LHK(?Hz@{iG$8QetRjkoe^atr8= zor>}7stD?{ZD2eF(c}V~7WHhgDn72^BFuv#|AQ?whA#dX5)^)VC4WBa%&v{L0R}>_ zb5eUqK=B=oO$6_{e>EsK&t0bvuX~*pLsojgDpm3rySaV=j7P#7nTMFQ%~DXNj@b8A z$xXz4umze+6yIaUAhmr^3ndC+l{VivB)9C4gqBUb%LwIdAsg}siBr;rRM?)jKzKPq zO^FNYw{f^a)5&$UZ0nf$&t>7W+8MSb+cz7M9Ntz0Uz}n__>`E_n%M2&*)LqFIATO2 z>h`<2_~&Xw*6{e`c5H^Coz@@S2pf^sgE(BQ(( zw9WCs%F+Z1oIM3H9l6NjZn~tOM_>>I+$Ba?yia%@PHz@Pq(Ac-{a9ewxZh^YC;c7g3>eknEjH~a=E;d zQN@Qrvp1c-w!Zh=fy;1pEiE>=JT$hCnf*nIzr9v>cK{`Y^~CtxD`wW!cVti~nZWbe z=JB9S5@!esHFDZxx89=vBfO(#`2xslg89ZaE+ThQMU*Ju5#V$edi|j+q3W8_WlU`Z ziuu+J-RveH+g;ALE;qOE*UH|7UBt^_qnHbLq!)HXi4U)Lp*olmQ7OkyYqG2D#N!Kr zgk#cvZi2Lh@l8!E9SGzW#jH}FO1+DeI}e0MKXe_GQjxS|hOK3AX+&FW@(S=6o4sIy z*67PvkbHh-z`C_hClfsCCm7qhDtRH`m%_mTo zO@qV7MWp6JKi6V(JcMVN$aV+`{9)JI?ebVHwTqO-?j%);>p~4NEzcHh%fNmc^G&v& z{Kx53p7RJeJqJF-I*NN=VV#%baZfdjXP1ML1qg|RFVKex$^q@-!6U|*`L!zLSkx-ORUK%9J zpUT0yqr`LAsWIpM3{sd3?i(217Vn=(9O+m?`^acrl6UDst8IHY*pc<77EX6Z zgABP;TR{Wx54Z|{^mnoV(NU}{(+~tx6LV{I;di<+=b?wHW`VN@stx|2n4%f6Gxx6> z)|`6EskOyzLcw~|1Z_GPemq7`nINTNzh zp6;d)d4zNz^*{~a);I8aE#P5eseEd@i^z;`Hi@_GpiJTs)@){e4@?^Rry!Fiehc_!`9oj13cP}`q^dSw?`$!UmH?2dc%2CSUEAM0{a=@HO^c;3F+9q#%)n(U zEvYF+y;JyYo3J@dyPi>A4uArNYoh?4=^YUX0=+5!uL;SPCmw!|m^bx~H;}j$A-^aPdv&fo_*h*W=^_VKo^>AZk95=L<`GKyJ%AQ>8< zy)hB*BCs->kDK`r+|6;n@@^8Z?UWspKd-TniS(A#vsFv7g%B(0Q`#M~x65eNN_3v^ zt?~>41#q_oMfZk3Vd8g(XIIVy=a{n6CK`SFwlBdJG(6JZ-gf`*6#yqHo3|T&i+XsH zob5|IR~o!Dg7MLdB`chMzW@@=B{&fNzvgOQim@LdIXShCqN7~I`ix{9#;@2#Kih>v z#F*%xAJTv%TfE{vSx!rB*?y3ujCvFTzyUD@%?L0Cpa>TqmgP=(K(_ts>Kk>RYXJ5Z)AajwguEx&eo6%3pmaHnBIW=U{!3mp z10`ZI5`H;os9(m9EPyGW*24Z(!7rbBrmpMQl)LoSd)i+LYP@9;wveiVx{W623PBwV zrkLFb|5XzTONLc$_J)()n|p&YPPNmLC@O!sWFSPHBHl>t3?ldiEcREHLpwfZTzAc-> zd$<2vY)z)$Z5e6Japlos2vqq&0kl>R1lbp&b%7bzv4aur<{AnQ_lzI6TGLLw9Zu_o zdMVD_TI~pj^lQ%o{`l2SWwhcgl##LuB4&t{xY;!3iWfNZVsXnF$cwUL4oFcEZe4^x0`$|+U87$_%}A?uE3l&n`@GCgxavEPcPc|^yr0200a z(SpzZ6rD6VuD<*+rTw4}3h=5$&1G8-G{Z%BBHJZ#zrXA}jFH8aIz=vOW>5#ramdOd z7DjigqJ1ec70woK<{jwmpM{B6plxmnz_+n>^+Vu^CgTQ5DOy*>bs7#^rN=F}q8tZK z%TnNF3mL%l$!pOpK!oW!<^bxALi*%tB`K&P@yDhX+b?4=C)auCP(L%|cUYOBSX?p9 zd?G)U0cvKL*p)j9z$U_t&?k14A5i}OTv|Rir^H6Wr`N4B)#PvxF?jjh1hiio&7f3e zJ}V+;0Vw|>O)saXqQVmb7Z&RWCMyLWK7yju#=kv@Qsur>(Lx8Xv$%G`(9#H;h?>U- zmqsuEi{mWkN295z{FRgIlSu0IJ~LdL8TNuS#rB}P_QpR+8-IWrF@Hk#38Msbw@fucE8zIo7h$|twjOHUN_Zp zCS_&gi?jzjK@!4z*Q=%n%Dn>{v~2j%cxPk-PU7XVg?TaWN3~2xK!`Bw9Vqqn|F!T7 zL{|HB6vQhhBuHH5Z`WYVR|`WrLW#D~-bLG1w3$)}N`VR=TWIL{Lvx2})X?@dTmnVZ zD_vCT*KdoxXkiuOIJ5VmhX4t1d~blw;FJiOO>NP{_A-*=5Liq59|sldXiZ&xHq$8Y zoQ?(QE0^1Bx;rSXE2aG#Ht=YEwicca5eaE1qUk=K0W;OF!w7}r5J0tC*p7C7*Hl)NjSowhK6g4up3f9fx} z!SbVP2NnQha#s#tW^V})bJyGe z7oM=T7$DI$fP#IBS_I_2o@%@MWN6*Nd)f=_ik`E>deo4~k{KE-4G*IvyizKir#r-d zCmN}bF-DTJfbl~j=qtq9eClPnytlZhHU2m|E1$)sn;Q%V-s=4l4NVxiNI`#vy)dF~ zc)sylSA_7DALD{k2f%bkD&K=-c~_?4*)eAr*&{9wHfIJrCZ%YoW|gubGb!4J>~qof zrnY?75pKdwZiQlUk=L*dEl>o_Kevl2Ch67!UoaD908~J$zak}O8$R{vp}oTtN{S&Q zD_!P2ex0W}f&z6d?QWxFeT1xW<3_Zx8meMwEbo@-y)FDfMxg`of*YQ~rniUl-!l_9 zdd4ML^4)4WUva0^&h@0qf~wkhmQ0O{|KeU3$b%7n~9KTZ>rGl@BTU0Sb!oL z3eJ1%--4Pk2t#KImy0a%f8$uvTY%M%BU{n08?Y2#bCA@(qL0px;au#UAyAV!Nge}{~%(r4MfG}$qp<3%^~eu+X)-X(c3VEMh%g^7p?GpDV&S+Af)gO?bt zXlg_#<1Z7y&O1UZbAtF4bncKr&(I;EQoc(PUttfjhj4r~3ra3$T*oKCAN7d`pUn>C z-$Z{A#<>7_ix9gmsK!^X6vzUi{271`!-@sh6$ZY~|LA*!jFil-cIW+)9XC0HD?>^D z8vErA23tC-S~gF9L6ZG`e+gbg7jJnDE_=pwjM8W$&UhwHTM?CMyFb_US{1dfm!>hY zBcAs^GbGsQt|`}h+fFybswJOh!f%LOY7;|iZ`$3x#lX#f#Ej{H%1rJ@Houbb*S*V~ z9&EMC;3UwroSzj}e!jY)K5u67P^x5xW$11UK#NQ%s-Aa8imo1)QrZ!U=l!s*(5#J6 zQk{+Vj#UG&4DS=}MH7gYH&-dMh0%IJkJ_z?6G;KQk8OPf=QH2^BRXT=CpZ^``7%zl z?9n*IHUmE2q-0V<5+>mJP0J7k%X{XU0xb5ffDFHjxnY6x7rQOxdU*35yUvA^+w+n z_!VRmyU~b^Q|Wv0bV+BlH{8qynuac$r2;Kr0}LHBmBpe|7eH6|)L4_}+M&UW>CS}I zgjXbDCQ;-Id*_=T=%<_<2SXc{ErJD8y%L5ibueP9ee?6H+hrFqc!EcAl+D8)#&i#Y z&?2gke=94mbl8}>oJD*`*vHfFB%q~U@b4t?kU(#5z%3GHhFEP&=^LW4{+FQ5LQL9{ zIOBgef~kv|%o1*@N(N#t&Bc9Gv}JqzD9BLB#+dTG^tov{IY3gU%l_tWVr! zTJc+wAA;3o1bX3u31d2-7=}nCf$m3`1S`z3nyavek)ss?tN-0Z6y@{&-9AcEq*zs? zgG|3h@&!~s{-hU}eO|Oaj23kp8QzQ=u$ZBQb1>>DP1hJio@#Tk@Ewq@v{0x#>iub2 z3RsB)XJRpdbDCC~iD#RtUjA2XZz7+fyAh`=TcUenuYrbYf82Di7rIyWlg zvnT)n0{{R60N7`j;h;N@MVGA??_?N62GFhw{ui^S2JS-y8e=%tVpcaYa2`X|OU|N0 zs|o358ks+f8WhotnbZxQ{OC*^c&Cz7(oxXDI;;}$*kw`@pxEUEBnvv>+KQMY6*{2t zWhVWO#eHPjPlUqGcvUpIo#aS4fUZv;!@6#l8Yrb;Xe_Y8rguz4y%|FZx{wh`xT)Zp zkcj~nx$NGKSHg)&*V}i5I$wv0rvQ%5;pXg*HL`kJ8wgsAfq08YndBiOEUy~d?(W0Z z1QOkz<^P*UumgN|^|#5t`IO`ZzDajSt*%N6$37ZEb=+AHvn4ce6ky1A06L(55;n67 zXCnQaLn8r8vOqMbg0+)yZ$jc_bb+IJ*-prC*;Cxs z&b4;DTeJgc&vXq z3D7B=zUkr@n;?O07t^VHcu>I{r!aNF4ZHclZx;HEi2$hU${T^zSralST3m&e*E10& ze`i19SmM&TJ0uqocuP4PH0%pua)1Vs>-uVy!18CAr~s?R!R*fvTSh0EH9gJCDr>=j zX_jN?9!EAEWGsFH5{x7J(i$&Qg3K8)o-e4Xe(K2M+U+g6jmAjIkWw&Q3tGUk9$IqU$beP|KT z=m+&KlCzw4bCoDJKt+P#Bx6ThNy=-1dSk<#&f_4uLj$iX)?4RLBj2>yVRfI&(8M+~?Sn=4b=%q^n)nu$T( z)N2jsWk(KsStg+Y$y;#thc*h)ueHHW4mWYFk?>)@GiR(Me_cvFFqLsg-5=ZA;q+Sw zu;*wo`SLhzfKeKZg!0a^CDsfal%RIjOOKDZDo%L2*E^mCjtLjd4g2V$9w&^MI5qNh`w}zreoZ5T-8rqoQVyylJU-2 z`{M@&DxgPlO}(lBkXtaWj-6&*KD$D-9peCb*iu6Z>%QNx8DG6r3+$H%m@{XyA!8Z! z=At!Oi^=c0HLbkLnWL?^RuJy9(^n3Str3a2OEA4CUDBhwqQ3Bc9;XP?yx)28Y zu@{1B1*gBs8!afUa&m7u$gdCnsN+N!qS(1CodA>+mby@{2=ruUCZmLu-lK1nvlIYv zn)&6nPDo@Hm;GmZ#E$GL2sI4-{=N&ex<7)oPGM1zT$o{$LpZLLltyuR0Ql^QB(_ho z*l#ynibogRd38$AzGj6d?;unzae1@X81UcyRgqu?|FTMvv0ZH7y9J5q2~FJjfB*mk z04JDPBzCy1;5m6QpC3l67218M$m8ZQHg~H9x?muN=<>BMRW*fUi^^;vk`^ zJzHj`JUKe2eqyQ1a$-JSNMflsvG#gJZTZdi`yv9&zB$+8DyVH7@)XT1IwrPXd>iYe z<=(5*t;h>MP_bqqA-c>lBye0D?(PzJ4W0GO+ zZ_UR(0r|?s_G@E1tn5V!JcGcp+Aru7@DlYYW(lViq5ogsrw=(M)6zVVkO1wlt~r-l7Vm=^ zADLf*&BwsJeW=B4#Uh=gDt1^DZE@v^;gV(eXO2!9XS9`P_2~C5x!O59=XgnuGQsTV zuhfpCQSzu|*anx6#-VbcJ)KF67g7HF&(Rc^J0zc;|0UK=mNn@mNWUB_vdC*3bx${~ z#EkHoimN@vVP5}?Xaa&Jm_IhJI2?J6tv>SyDcJ0R@>i1FRS70mSn5KHY!$}MX>4_T zJ!vW|2-8!euI=7qsB%#`o~RV>o>|`uxE6U2o*7*scc~oqapRA(o;OW}?w>DI|8(YW8voK-b*bg|PeU0N5r5sUx_agllGp`H7?;TqFz*+(32-V3@w zZHhC|O^KLWt}M<0z(8MQ*MkU{(lPfXBBieiy5y&1%KQW#6omQaR>VbLZS01Uim50`)OGppPU0~}bNLBAXk zY$t?lVJNJESpzg`)oPw^&cS{M?-G%5djd#q9ao0UWg8T$2@)gxsD9ppfLc?YknUAY zJ=rSLf04?)wXg;Q>BUiaEl3Z_??PP=JcN*3K67qF_sd3nQ=4DTO6S1Z`9mT&UcEb{vozV+Hf?Lf^{Zx=~gUrY|0#BCx*#XxOd8p_vDy(`rD<*Ib}N zq=T5-x%VD61#HjCD!A%txTO}~7A2zkBJAxNeQqM{^sZ%pBSaU|bUvIUry?qCs45WZ zbDKd<-ohWfqC(zZ()D^HPgOd|=YF~{J@a?Vc`3maV$149^Rgl9t{*c%{W7lOT@^1F zq<=R}47hgNz0O(OH}kRDS3klhR93vB=++Uup<$vb`Ld@%{ntj$^o~A!Z&Xzn5m~Sc zs0=ysy7Mx4T7FKMQoyxxxqa`iZ1TGkka-{ZC?O(QmP|+pF~{;!MXmhob^uCk`k;`- zY_&Nj3s<4C`}e@h`HPX;bilsh1rkFGIy*QAcIjsSc`0I^Rdsmh$*@eG0J*1dViH&s zXFV4W0i@pJge1!aZH*l9ga%`_P%RU^x_;spFc7;Kp{cg|ZPDF$-Hz&73ud|#@is=5 zuEGLmL98wA%yd`XNDo_lRrW@xl~G(<9zzB(*t^O{d~e*TWnq6PY8{bN6CtJNQx+me z`b)GI6e?#tg4a>;&v&W4QSf&jPE^yRT#1f>#-!QfLyN~V@puZXZg3#(XV8rz%=mI* zaXh!86ZoT3w90WIs!Z_7E1Bf&(~erJhhq#`vNPQ3)<$?)($BM&d5C{|mk72%cqlk? zk=vPmIX_vjNt)Mi6#FE93Ya3O6L6_J3(7F^2#Nrab28kk%9`5i7K61JB*A{ZNY)fN zIrInCY}C<)U)t1V<$jNK!ykHVsNW+U0#J3ys3$O;?RV!RP`Y7@|ANFDHnqIz$UeR?t@&jGpEd0 z1YU?Z!-d2#Y_dv$P!No_IDQdqYe?qn(j;t5RYwiR-qBmIK|JZkkSAzd^q=n8)uR1# zUwCsK7+i)L4Hpw~7s_`fI9gP_!cXxeQ3RJVAF391QgE6mrqh%T9Q|I*Z?sc&Lr^#s ziJpN2#eS_~pK--;PkXc-%`%Ikw}q20(Rb5vgFzbkd_IG<{WZP3VmWvlY-uG;6zO^? zAk%P~FUKYT)U`Z=_wlW3)Pa8cF+V5r+)k+udRS!egm^YEf-Y*t@6t*)X)ou;tE4qp z`a^}oh6l*Nak|vJGsRWrsNs1`Q&dHBv<}#r%gRA=-OJ@Ie!%8lr^WE0J(5?Y&W z+%*t>>f_FmATAXY3iA`9N!6p2I9ENyd`8PdwCIW*pWyS9LHhSf4&QpRw?vJ8Cuoii zN2dO*FN*i!kePEm$(MeVFX3Vd;UTvUkX?%JUQV;Hnn}Xx#Ye!GLpSOge<1;3wQ>*; z_#3-M&G0$SRanU!PadBQXVwlTrg1Qf3f}{P+FZHA=&xPH9bi7aoRw`(1p8_klL|j^ ztAx#W2JzY={)lik8m;$vKciZG&qo*UgMlMo-qT zICdDG>bDAbv|@_MUg4ol7=Kg?2w(0JX;RdFT|wa6b|*NJsyz*aI>0hO76Phe|K6&Jk@lyLB)GNiwk7)!QE8H*jWBX!Ve zXjR=ozyJURj4>yS<@#r*pGri31pVmg_$`HrXVtZa7=Bjpx3UYPBpkbn;gQQ+jKngc}> zK#G45*R(;}!hr&Pv3Zr^GyvJAoS}=uffq;`fgD+RiV7V2|z82i|{s^`ZQC z*?k;rREfvC^JA??hjno3NW*yYafvsP*G*trQr1|7k*#%#qCao+tR|@bkz01l@d+MA z3%$bEhmgp=_Q9|-ZHt=V6CpJ-r}t?B2k+tIqIR#4=50-(XK7S`#hjoA`e<1mB2q(7hyCQQIv!$b0#@TZaWbZ{8dhA!8@DD#=DQ}H1v@*k&QRsn< z*}oE2=dJn8c7+8SF{b1}tPomka#rjvZWcjPqzGo1qERYYZ{4nV+*mzRf@UlMIZ#H5>nl0Mh$B+yCEiGHYpPM4FUnS4As8`T-Zv#iBBjWoK!5%8 z=v~1u-aH4dt)&97qR$)Jzr_6g!ekNSg5San!uvSi<5Z z>|udp-V(JHU+=T%PlU!IeFXalIyHSRjNpbo%vSXrHRX7B{@=W=58K!AgUZ}dalbho zy5XB(ZUz|<@J4^&p`c~-zm5$y?flQYj8 zFJi|5%C)F@|0)e6x!)mp@zMoOJ&%P0mJ{NbVn2G&1Gni34a+u`e5D+ivlEHT^oGSw z(<_X?R=!6`g8T-3RoZ~O^K{tD)Zwk-7w~OGdblT{7nxo zy6yut(3=CnE=m@J8VJD9DH*9cAbJkk({c4kwVamZhvTAm-t|IP9&7DUNY;}{OQU^l z_vIU8nPG4=a`@7)7SkY}1hV7?8G zdC^T1B|00094 z#txd-zrEkUK5ViG0CF^<6Y^i~DVa~SlaYJjaLFKDhDwI>ka=74uDCXQc^bs%0%|{N zFr*hiX=yTrYc-TA3~qRovf>JZlTlP4Z(9~x z;EqYU`kJt>lmQDU-R?PXrv7qdkN^bg=l~?5n$FNJ8$5>**m?h z^5ect_Y(m-{GbAo+3{5u2v{|&uR^e>iu)Ro?Xl~q4h+tj(Tx`CK(>~9qZ+)0pjJTD zBTOHAW{~?rc3y$@);-N$uK$+s$ZbBxWxQdD^phm>eI6SJv9h~8y``hznCoN<&{V9l zE~r-Mfnhod>47z)+eHVlc=mxUn>{wwWR7z_fCw{@YH2Q#jWk8=4&@LK>rWmp(*3n z_DO+0lcK1ZJn!yyMz6?hW9@O)G-ZBA6OhU)pGR;*;kCfgvNEyE(A6&MAH)_`q?b(@ zJ+k$eR}ctkaujSm6{WdVtA3AMO!hO0l#oskxdy~81`ISx)e(`#>#speE%E__F=pG8 zVNPQdg`|atP2NylCnty|3Eo@~p4XJgrX()@c|{+eJrDfsr;gy8{4djpBxt?nG+>QF zE;rS8vTXFkRr37TWKwB~zCZP9+; z9)36us12vClcflqtiLq<3&E}mC!ZH*41U%L<_fOy`_AEdzwI(wl|(a0tBeYr$hsvohtjocFgMkSwEVpe$`|tE4jL1+}qU z&ZKblG`+o06250r0|fD?BS z5zmW2NxK(>s!ff(o*Xc<4lOR{iK&=reO5_NpI|W|M$6?Pe3&9zmrSujX1VOk8?^(Udfq)7$Pqiq28i2e1jKiKag&Fs;@-Mpq(2 zHUO>(jl@dp26gS-hTBuVwYa@g$GOqtF$I*<#H2>q#=dW(3Hi+>?vCTlFX4vHoWrEX z7`Zn#2tIDMI-sd?GBa3K#)oq?sQCQ(#f#NWt}PgClFjfAcl5|-z1MH4Ds2Kq%)|d& zHD%C3oU=bdqqZqArMYQ_y_IR(w}pDIdy7Z`1DHzcS(_uN^sVyxfj4nNYgcuX4J zcIi%OxvEz6MjBgtlK~k*hKjkDt0*CyR3>HnWk9yv8#_P5x*UQ}+Ns@^RI$eG-Vutu#3UoGlF zzYsx+*~n_{B&;7OpNMpf~=gO%G)HL&DOgY3jBa6ws3IQ`aDqs!8Bm5?)uq zHTsn<6}$GRUSMt7HIyGI=KicGIQ&$6{fG#S+rmj3`1tKDDD`~(KY3f0LA~%O&Hdat&If_1}lf`#gG!`vV8jb z3;Y$X!d%v0KH;KKLeC-6{J%}!B^lr_CQV9>?xm;r+n_#byr9M{b*<~Bt> zuKlBwR8JdHR2nZr0eDOhgeDBZwXNvfxx4a2oo zfqmP9rFF6;7=b;a-J}7onyQ!2c<_V1IY&;HPf(4?E4|j-eybWH%qCHbJ$N}K3;XTL z2T+kpv(Ivxcn&RP{{1F7Xic=bhewR-A{ig)R6=MYB~2R_X^}r(g6Jhd=mn6kLd?vI z!23@&aZR2TW~FFDt8LLBMtlX}Qi9Uxv$r)0G5>0}#k0{~to@B|A8$~4!z1NC)M*Hk zLd=u*7xFWPk;W0QgT}(Fgn)0_JL(0AGK2l;fS3PMb1BegZm|lj6quQ-s9RlD6- zK08$dP|P|@aSu-QT8cyjPD*L>paAQQ)sZpv$I4k%k-+CEAdtaN{RnaezDLO|x%Ao< zEQoG1)cTpa#wgfU#zv@|~LTpH2Wh$Jy-l!UXtc zc;VQ#ouHOF-};&WvpHjX>q_+8hoFOQC-QFDJ9y(@TGSGE)m}1^B-KOAOLVpnmTvVH zR+;|ke^mwB9ZHTqrBZx|jtwLI_PmseVRHA7Pet^hN;O7Tfn$%%tY!RCrHF<86d-sj zDpGC$kw*RCAy@iQ_x-Ua!6bW+cPJfDPvNtiPjx}(!!D_>tbXReQ|E^!;Po1<&ybA& zp9zEYxMedH(ad|_Zm?@+CP&&O;j3CT;FAe*Z2w6GBm4e1aS>g+E#8X3(+5rc#kMiD z6A*#R-WS7DHu-8PzIn3@gIsC{NVk8%Utc-_@=WX5-}_dVuU+m|BRe*4#GPv5?HNz8 zXU6xxW>`q4s{jFar33344{wPsV~drKh39mEiD zlMaN@onk^5s!?{;SqCw#h1>)@YQ_dq;8=tW^l5q`7x#dF^g+`5FJO%Krw{>C9vi?A zzQ0Zx_6?K0{8=Xx2e1*4e5(y8U(&}J_=K?c+PLACz;j384Daq$VRp$$s_Z6Lw#v2XX-U4bKxTnMED2%0qfH^=wpQls7p@Jw#d z9uAMEVXPzMp%7Rf9|=&n+j%{_skiMhn6+@v|4x#nZM^I)7w4KrugW|X}S{J zr&CaaH2WUif6fkA6h@*kr`S=4c(9}s<5VZ@Byz^;twTdbN(M*ClFBE--MZoXwvLIi zZ*NxR?}ejt`!V6N3H4k1;{C`&cNYh@@0tp8&5grXNdw~yDr73?n6+AGt=j~09x9XX zrzg+bf{}|4<+V#$CWzD^qbL1OBOT<$1xn-q02op-`zLLN?ak?X?djK(vbD;tbPd$D_R6#7f zVvF1Lx{9Ft1-<-kHr{m&K|JN^dVMDa&+E(K_#)xKt+M48lJOSQk$dN=M1EpSGF;Ry zlU}#;&jiw;s@^#oH?8Q?E6EMJFGg3`I=>&woHcTdVTm-frRf5UfEHDCZg$YW_T}7q5x8dP z@!jxZuKYI<4Hw*ID@fX=m45CZf7+5f!E|J0;Y*?T#Rh$kdLl>@(?SCG@3(zw7G7dH zXr~|W^SOgi$H)IQ?;6LC3+&9DyyN^LWUQaKQ^?ziUBwy9D}Ii^tIMA0PckcK>$F z?*SPd%^~lM%q@m%4|UK!c>Hb8^sHzAoOdhV!bwiMp#|Ob-K8y`0@fKRd zTlqh>Sebgn+g>)Dqf33^s@!AhHpD@{ zx&>dx1xVLGXr&>V#9;1O3anbllQulZ^T>8ulpVErg0y$OUPg<1hpQ5@ljbsJ*RuQ{ z?X)kYn~OE;tF0xOkg?8(d~&LGRORvptO(IF6~5<4!Ai>CY%lvrHsG>lN{r^pBof;I z@zlkNz!e}HcSo^GC$y3Yp7(1tG;C>U3JpE(#4Yv7Clzbf_>!JZe zF+ygzsWn7sb+5(EN|Bs5;$UXac6*2?R8iQ-T#KD&lIhq$h43 z{14YaJ!BL};as_<7_+zSW~?O^82K}g$*TndNEYYLNU8UTgI|=Hy9BP@zbl%=ZpHC* zV2Eipu4irqO%&7yvQNt$jQn1=@%)cPCo7Ygff$|e#-cjgqjv3PB{n1g;Pt(IAO(4~e@uN= zmzV5t3-$N&6eolGLW4@B)FznsW~q4tKr+P!{|$O--(;M&0-V%ncyMSB|9Ra!iBdoc z@2%2R3*sLGlv|JLDAyUlgDlq7pvIMhwq@q+_9U;MJU$qRjZy_Fr9}H z)m*Vq0Ns~m)Anx}9$=v;Ah+So!yyo0zR1Yo^o@M+?)u=%ZQ^+j?#eol>on2rA;ERl ze*Zk4E+2qC1c^KW?4RLUv%owNUT`r1u%-H-gOE-Dl3B1Efk@HQ(BWR+G`enmC1d*k z3O=+)-?GwyHd&We*S>jRCl8Afos?paWz@5{Y0k`^TU|k(R3ds9 zGqN7t!rd#qWAmtEtKxY_-XfC_^0b7q!KlQ6X6!Y>7!|w7W--%z6$Y}*4F`LlMLnp* z`=@x^QIw`96Q(`Moas4GP5S;GFs;+94#6W1O?EQ!AsW|Bnr7ZvO#%7Q?YRD|nLKw= zkHD+@L@DD=4c>joXmg;|Nt-=Mp2#^K{r+YEi{q6MS6pvhjB)d-cZutdZ1lEhOSfUm zr5jclof?7a6YS4lQ3$}FHZ0`ZC|)i17jk`$cNa1^M`Q3Aho^(x=Vf|rZ#XLD(gnlA zporK?6diqXVXc-()cC=fOSn|^9n4d5b?X!FK@YWmGHQ)H#(7+u2I_)woD=RI$z58V z|02Oe+^ENGvgXjv!%qD-%hO(D`qzVaNS$*8WC5c}AoRFbkUEEaHRFh~CJNZk3{dbj zSKKN6d<+Hss#ay&o%;j7LtA@>Egq^(ttoJ~hnXCIjaCwD?w_-$p}LX4Fv&tDng9Xf zC7~gPY~^%$bkf8EUp;Fa?F=fjrW3rYCobE*k(p6pI>1w2n%?6ak{vGB2V3v)Uw5Rn z`Pq=d_5Q8)VM@zOKL{`#A|rWgFllOt{Sls)e2P%(+-M?yCf`2h(GQry!6*} z51b6U{i)5QM8U9nQ>8-l>;Ul^%LGE+pa)&8@$HPA>R7@899+=(AOpcKA;-Zgq#xs- ztAFg&g@PPWDO;4Sa^J4o)|=rxnBxzyF9u`sJ6)N!^;(2lX~_kXXEa|)cF8pYwUw?G zj?zzmkcOzC>x8XpoQKKSB;u{0c$luFGS>|0cBAX`b^Toc;okoQ;Qc#Rgpid;BpSK+z zk!7K5+u>swW33XH$za}-{5_|tw7NoUQMcmJ%9H{HK`wUFPN$tF zTdTX`|CdPdqJM!cN*#450;IFWvFp=HsC$yapy!$SQ4QtM!d~u~G%IZJL!Npc*Aa>X zhR3p2I)dVc6VVLDH(w}jAu_Lw9L8z}TxGZltkO%?i79gI>K;bOCleyIR0^3`E#o7e zZt?ziD*2^}Ru`U;NTrTXZ_OCKlm66<{P5Om6A$;jYB~=rz(V25w%`EHs9-qLgY-y~ z?Ly`=*jO9O+v!0*es{BDb1k1Ft6fn7)A{X2H~FSng9uM-$j2^wWGu5SoE}g%VRBwf z0UrIcHphyF(+ZU*FWUt=3%V5#G1^fE1{L_+=v5uJ=g@<*O=DYQNO~-^9A#DG9L+~V z0(kecZwTa9!MbXl2-a6S?@sP32h*4Wj4cu6NBFKNh>gg^%IH-c4#7PXH&?rwA~W>9 znr@7`9)c-_ZU=2?#Vuw`;;qc*t}W)+g3@B!6C}>kqhfe@_TVy5y5{p-KhR(yNj{wj zO1X$T*CM|f7vP^)nlmD=#hVr9c}3~`1? zi#+$p2P!HeV;{N@bLXvF7|jRavn%k88ODt+n88Hl@-WeRT|I{tz=hMuRGuw2M%>Uk z!0an)8-Q>*pE*2?AWyrOGqQNYOHlwwW=BUDwhXs<9QCJo5fsuX}6gEW> zbUUJ`bCFgcn21x5ny1i2kN3S8M{q8G<-O*C+u;-fWotPE&m!I?M)Lszz66ck$n-jx^U1U3dJr+ z4SZo|f}}+BT(L&`-lmC+KN&OIW<>Q!&5sl?%izF$Kfc?38?2c-&!K3@BcZn5qxQee z7zXQNE+#*vQ6X2zBS=!SD{(a12@VyqwscxcE9(EY8@EPIw^a70%T_10i5*YF{UT1=7toOUEs*E$34rllZW5Vge? zyaDU)fNur$Ym3+Xtt01)HRGu6-B5FqytCvt0<8k&h+Amv0Nc)YZ|;-V2QLrMWkDuv z(eTvR*+d?2t9^Nc7FkO8(=SSxkP!_FxfL9YURJp?rq z8mce_@Z{_fcFPXE=!VF;)uP6yP9wa;bLaP8)Iu8@Qq+%EE<^iRp~e7dF8Yxd@FeC zAlX#sMA{I(aV7Feiyy#BWBTk!jS~^%Mn<65IOvJhIDf#Ww=zWG3XYwzwZi{LEKlue zA8jspsbElyuBW;k%Te&Zvz;yL>XlBni`x zYPa6m6LWUS&a%Ko@4$D?&us(Hv|**c$km_AL@=OWtsXb3yCi+cpizywgODu@=H zw=w-0DmkujUm_=l4UKzWjbp?6@p)pTYH*gte?QM%-FGIS@p4Nh4s9V?481wC&kgi z4GlSsaHO=niX%}LtTiUx+H#LL6HDL4e)&1$Y>lpzeNu1I#-)5hrER>&i0U0kV`v;g_VuKc zGo=r#dlN#n>_bywej#=5sS?R6uQF}eemmKIf~_?rz8R71S5Lz=7$srQ3dzA~!E3Qh zgMzDO2d5FS{GQzLM&uK{rztq{72~^H04~J!=cc04>Qq-Q4xI|h`69MWRNM^&U5d%O zvX~#!E`u`-gYx7vwn#hAivT|Ig3=@j?sfM3zddbOy%B2F2oQKJ$5~A~%(;1afO|(} z)9avkDKHj1AJ$`GVH@!a05oZ>b#>Qm7v~fv(ku(DzcAaRCTky7&hCz|Fc?_t^l8eI z94}BJV3oM4YP46(vSd;#XgjJ(3psE$1HnY(BopU!y`u>rQE6h!!LI=i=*H=4E|+CC z4juP_8O#!zp+6u(aeFaG=y$q0-y-ngGY*_zW{AG;4D#az=d~|)dX4#SR0(VI5SA&S zM+{uE!h%Xp5Vt=Cw299mZuhLkOP{1(6BYHZAE`1%-W>ZkBKc-kcVX=D-Jp*}Cl*_U_8MR6?`d_%<3X!OQRJakjRf;(ypc_m_d{LiMqB?bTq(7dCO2m4sBaTBp9`3ql9RR3O z?&(q!Zhe%fZ$sFA7eb`Iy*6wkRVq4Bz zQIaeP1&%{-#La%}s1@uaijw4=BBJVd%WD%02JWK757jeF6aVN59tVX6v>athu6H>k zOwW=Ubsr`}qO-p-6$NnhNch7Gc&qrB%(b78XmUw7a_u@XecqK!CTER{=zV70jt?Egzgx|YPmUXV^`JM-DuRdKMt#Ux<2rlJEDj0fMsY?D()!3ddAJfRfK@g z^~=ueL=Pmf2#~8r$jrslW^WALQ4Y@jS556dKG0$UIXM7gc(AGKDbf+#mii!Zf?9lP z)}pzI=hYT!Zz_X?dho1sa(74~{$(aB5?K$MP!scfX|z{-M4UR=V0IyBl?l?c9A>b? zZ;ylCh}b50OIg9MqZKM$l_43v&aDZBJ1*4fiGhdHlRxe~JyvVMeZ?bzJ*ZW7g)>jF z7eEM%q|cHoX;~qRUB{q3$I`ZB`A1zxp`~uyvk-2h$pU@+fd*^#`b*s^fUC_EN;;dt z@jM+H5y^&Y%44Hoo6=#~-fhytNA{?fJ7dGu4%OMu-BZ?K9BKrfJ5(Fl;@%LTWc7sp zaq#h^KlfqyXSop)23?5*X@&!{ivg9fOFvKU2~@bDrQGSnjj8vG8b+2Lo!-#-WJXUb z5YdNIGUL87hLCm#%L1L@=$S!%_nf^0kR{*J=-ak!+qP}nwrz9Twr$(CZO$~O-81ce z`+v^)Zrm4f@AtlV5j$$HS}SwaFLPDxh+UPHDVA`;ogmbG45YodbEEhT`CV@-hE)-Y zJ8z09upOv(?|xkFSVN;f3Z80}A}3m*pI)H;fieKcXinO>4K%INYL3*(eCrr?tB9QH zuxLd21bE8N|yv7mx6c6`)e^*I}Je%<6pMHv~uPxw|4|01gfkyv%75BrFEohP+*iEg8P z3>&inQ+=LM#7tPDP&)M-1J18NS~oQiC|6ipox@t^%F$h96Fox5II|pf6_!=s85()w zc8)p&-(9&ICID(tr}nDPX>D2g>)Kz#1yF}2c_|MvT?piowvy=#rxKcNV(s`T)t`ET z@Yxx144lHKE|O}+lj-eNS5H)%b+$I2+V@A1#NQOid-M_555;(L?06xf$eip=ykWty zeS53}ThwWDLGiM>pQta0bNw?gO6s+Z`2u=m&?MLgTsVb$jq3Sr=;pNqYCfpVG6-Z< z+1bBTq^h(&Q|b9*f8|w^gqk*g!*DE*93DIitj}wFNX|64e^YOn|AZ`DQa+{G<2?Bh z_~BRB@wUEvpVpB@+BL4F5O_}TPi~9 zcXdN4*Po;&-|%+rW%*2dQy0`AbQtf`+ok17Zj=3ZEGJjM^qDMhId{~pOr?e}l8DR*1&5vg?T@-?nIASv>blu8ELE{(S zu%MaCjyLWGP;-BxB{9sQ&h@SGNJIwNQa5qij-on4OZ&6 zDZ6X##hoP0cl_yrD$|Yi=c{zKT93k%6h^h<0Cnl<&R#0=Ag^|k8gMDC-v zg7h3gGYANVG1c*vYwKn1lh0z*8plosyf-HHkSXGNxadsv*xD;{HK5_DE~7>dyJk6r zVslj{C0E~S{_&6^0}eyU8Xl%)WSM;*&uTHB?-#jot#p4_jL!k2U&50X{hg{xUovjp zp6j%#s!poyJZoHMEyWYyMn?ziMk6lo#3ao6<--_8(5j|uma}L zUDlzB?H5m7^$Q9gA4La+iKN-Ls~-MO@||3Y#1%KI13#4_vp51Pi@l{c#}H^4{(vxk znW2Z@cW1c#mw6*RZtFSLgl2E^O2ta(Cu8zYh;tmuREO0kEqyOv3vdb(tcnNTU@Ot( zZ{{t|uU%5u>rd0HrhbXh{?smw=*0xaTJ=wso>lMulPAi{ca90EDvc{YA4r2ip{UKX zv^iUILqC)42{cIHkf9)VWMGx}bIm`3+Mks}PL3$yg?uAT2PYtk#MN8h9<~TL1z3Qx z_s!8|nh5wrzJ$sOon>^kpgt+SUrYi;jhH7~euQy1<&c>e6&MyRI!OL%?~Bsr0u(z( zV^|x33I-nwrc(YQhY)Phfb`ZFW%Ose)C5!AXntZ zCPb}c6Q3AYrVK(p<)7`6t#;RI(?1+ zP@17W+qBU6e?kLO$kJ5@MDTCLU+B*~THKRbD?gKp0LjvO!0!aL{sMC|~s#2xa z&-Yj5x=B*M2KQSk(m;rb&}DC3Vb8in_PSM?G+a($Ui10U?4?U!G0bx44XFR?1fWiLDi+ql!5dN-c>CXD$R+Hwy4b zd6$FZqN(;RcL0k!iL*`_<3pLqIIAxLL%@~gNo8#BBDDrO5CGMBbv%|sTGiMxmUQ|% zbF2Gf_i_bhKT88IqNEW7EjXm>Su)5`s})#}(JrRfs8Y0Fl98&?shZRE*DjWw6x=C~ z8IUzOK6QF=D5yx(8^g;6FBatnG4rkXfvyo+vh~faq#|Rzt4(XG_uD(KU_B(tB^+aM zj$--mZHr^*`bEi2B{?5p&z*3xe0-E6#fRvj1*cc`&O?r^eT!?)%QimWF~SpNvSX6TVA^yw@ispbfbJVrRM6 zdIt*{?frX6TxS;lO3CXXKGfELkm!F^B5=lsG}e# z`fb~X5-K2}tQYeSxwIT>?qVANs>s9l?rL}M9GfXh3CGZfzi^62>^LsrFqjKs z>v85)Bx1NN0DDjZ%dw)3{pIRZ{MbVMqFEEvAvxl`LVUKd&xJP?U}CmT3(3(_B`{D4 zEtuy>j&`$fG<>H2ebws>1B-}0vnkoX(rdF9@&#k@Xfq?tzryu7@pf+gw)lc(M540MfF;r86~~vfv`>L`?U@cbfAaY!{y>sa>~v|@T1+w z*uJ3gCi++UIy95lhatjJ@|+xJ97u6QG%Q6vk*7qeeoGnU#x1j)xYuP8`DOk2yLDsa z4o(RHs9Jtr7Y)DBo+|{KnGn4g?r_W7(B4LEf42=e3xzNNkrZ}I`!lLw7fs|!np#NU z8c^!*X^VRdm^!E~2DVkASC>eK`z7`Enj87@i?yy6;Nq3aNHHLKBS=VFgrJ&R3-kch zip(aP(R(y3va;600%VS0SImCG&`oF03E%Xo9gMD`YSnYV)R0g zrFR}4Pse@)Ah1CW2KSE{`+2gnh}cebl3UZtn3#D%x5f?hGpsYz(M4c?W488WzZs&8 zN`p!0=HQc}hTT5%j?g5&lj0Jm?it~22zyq!35Oj=dNhWXSwL#NCL>ipbe6A+Zv7;V+Tc>HMo4HPD1qT?l^hG{2B^b{c@G;_!C>3g)MW-!)6B=xpZORm7lqf z7Yy(k%d>^wSl1v|$s%;_(t{85)kG+>Rl)ISmM&fHvzfX2VazS2ndymzc6+2-D>(bl zo@iN0A`Y}_o-t>E7cCq>GK87CVfG>XG;qTL21IN4%1z)hW@BXNlLo`QWg8OhcoY{B z<&W~Odt6!zYs{JZj4SyZ4yW4i&{368_^O#ldc@bB%hP@jJ2ZrbK(wB=;$2 zP!t=2@%?}X$HhRwmAkwA+j{W*{(%D$-b0YQh)Ea@35gk5^f5+oADoewn|kfXS2&p9 zX_hmBgVbBeHj&t7>V_RzB-? z#{0xa$n`Yxt3Z)x`2xiwg18q4<%QUvaChn83zY=pl`L(BkkLt8<()*#89jBiyo~!t z`CRIqjB;be*ah}=agHK$Yz;^ReB*9S|qT$m!FOM%8^NuAAY0O{0)51&H*2XLl1^RG~S|lCr`qF z7XTUu-(iS2_i5S-wWT61k+o)&TGb_vNw4FpFd((wJkheg9TuTU$Eko8Xi~;mfrs!n zvuWqflveW`8C6#21 z)InC;I^Ed6{8$C?2FiA@QBgtV6?i=G>>$N|;9;H4NL7rx=)M6F#ur%oBYj;AQQBUrt`?&kCKBfc7d9~BkuGoMz2D~Y<0Vl8#0gA4ln}JG>he>|TdFqoW4R*EbRztZ z+snEO_}H?Yv1ts?XU=(BZ-<-xLc<{@)1YfdXn&EmuSgjNZpGNS%lx)*#04Sf8wHr(AEiJGK@re{f}bPI z%BKI2lMcOHz)$eKtI^v89VfDmEdC_48PtZnMV9LRxZM;KOoQ8f*xSMn)O%$3hsH3yO4_F(LLYgsRW90~-H;}{44we*Yft*Q@ zc$h|jj%T;B$B3rjO8!<%L?`n;JbT{W55LwZptqO47~|P=pQ&!Z%4NyxV)4z;-ZbcPRh5M|f>Tc4 z`sr!d=AIgj&cx8BK@yi@T@s4&vGR+(MI6+wbcvvhmwe6GTELY}uH{Zku>^D}7DjD} zkeGZh+y6X!(#`^tKEv&aPABWlV5nIHAGG-dC)0UImlVx)(W31BcSO%kMS6XF=XC-N zAzBG5mlO-O=vUS2v3~y*-zkF}N@7jJ@N|7M(FF6klcT7kpX&KK^eg1Ive1eQ{DJ{81)v2{OWIIXn*zppi8+l(ZZ&G#<5qJ6yQdQQT zUway35WyP_^8MOq>reh1$&A&fy>(AyY&=Qzvhe*N>J zsL1yNEr}03s%Sa zCcR{Q|yaay{WbMl7*=b2oU;oZpb-IvG*HuazDw9KvPf;|*VWMy}$f3z? zmX4#C9EJ{>wBpQ>o$XP-5o6_B)KodvfyGhzM$7^kEV2(Uz>4DW6Fl#gF#mhqPQ3L> zOE~!H20F-P?}9#)9S=P@R~gos!@MIpz@5q!*R;9e)*~Wg;#iCQ7$umDER*a1^IfpH zzQcu=pIAoVE5+n)l78a3Wvab`j+~Nt%4KtgMRyak-@bV`+@V(Wnh0#1+eQ{!=~#IZ zOhj2<$i)EmpMpA2Ro*Rrn=vy^tp%t8r4VSwsBB$83U>?yjK9J71@BC_L6dMz6w-v{ zDx_X-%pSY!h!-+0=J?w|f3kmXWrWX@Uo$bQf@0C|FqI@3JkBD%#q}k(hD>IduyhR? zS8q;%VCR8C9n?e|bL~*BFqSp#|8u`XW1`I=BoF8c zB@aQsy2dIG;CbxRyL5<#g)qDN4fk0n zg1lg3CO)lQGPepo#otJ{s`ZVuLR!!WH~p!F->*rHTw#l|WpEFsb|(3<;cJ1?t5p3W z-x?cEZNfN;c~`)B@)e-ebtvPSVQX;j-!uLm14alnAE{Qi8WbVF9+(qg!ow(4zjC?Z zzoRNAWNH`nooye(BUI7#u|KrknR%dg9?hjrLIq{m=kZ)Mh$XSI$0xiwiMYW3*-B%SjWU1BUc%gY^btFV2Hb&6ISAKOu;s5-(tgY*aE;ex_Ak4(;eR-96SXT|Goe98~sAmMhFHgue@|^+MP=J@g=n zjNnXsfKGcO(RiGdv!?9R9xYL*Hzh}Igo;V;VdM_48L>hU0%25>v;5{~^@ETq!3Q}$ zK`&(t2!(h0xBtp`y~K~YcLigS~i1Zrawh7RpLsIrnIkdiJa(oCT*hL&jd zzcFA>ZUL<-%tTCdwK;*bB5Z1SXUWn~hCC(C2}VV<8bO$U#tK0AEDwRruFzawyvslO zOQ!@!_caS9UK8@G4J(V?ze1}t;kZXJyFYy^#N=3OcP2cUaJc|=G(50)=WjuD|NFgt zx5-V3Yk~@aSxTgK1Fcr4Z)&Qm*VV==jPS8GCjxe3e z(df`HnJbV9<}AeUcOFVnVG!F5wq)x7&z1P9|0MV5#2(V&w{@LOSHBS1YJI=izL4S* z2&8BzsxVZ`0uf91FXlQ!fo1C+-pODlrJX_?Z( zaJ*WTFbfXk=#)$cQJj;ywaFeBty_x$O;UuW$jNrKi|gLNYC*)memd5#*Z4apvTCx> zLLqZo1C(?Wb%Q`~W3Y?Y>-Y-YIDE@MV}G8n;V@QIG+)l3bt!^@Jp`V@FbF>$M3SK3 zp-){oR+NhtDrFThsdadn`QUR|Devd{3Yvb%7%}9rOR@Th7Bc=QeW^e*yVX9xNPqW| zdDr{ls5mK9c~WlL5G9^615XYW`^)z&S%)~10d2( zQBltNoH}tSLK_!8S0FV@p_KZCnO~8@cCq~poM!P7d`&raqV!JEx%MGRT(Y-&A2+j` z#w-ewcJ_Xl#=#lTk9n0u8wdAcK}hAV(#$FGglN~y6?0!k%3%E+jNHba2kh7e9YvaH zm%8qv3`4MIO~=ylw))CbSCfywB=1moUWD`#ElT^XlP|^!qw}8wY8cb{H?X@m7%Ph# zwp0*=RYZK@(0}y3j9R)k#xYTDK-_Y`fHab(C?b7~3h$X2m(fMLDW4diAfNIa$x4i{ zYTncC;!7UzMLb?os>7{LVPq2FMblE|yp+OF#UFf6A=pQ*WmHY56&KNx1OAs|0A6Zx zCnbB8Ul)3qAt#54*gZ8vW+lJH({D3Y%Qps}%J!LKx9;I5_ZmUs zF|*xOJ~LFy4}qn_>=s^lRq|Lo*KF6epAO6&bP{UZsnk;rb=G!+j{evQ^=V7z*os&h zNhX8$LW_M%%xyhD!IJ&hP+>#e*47cs1;>c}dBRiiEA@Coz&Aoz4PBBTiR6a`IKRd- z-wNk35DWuG;M0vSLDZ`X_iQ1aS=~<4S7^|iiJPi=$(@BAM8@Qwp{=D%W>l&0oQSt79Qd4G3$O z&eKWgV6NM1EZ;yUxX@-)NB9N5jU1!Sv3>I1nGw=&v67St*3@%Iqm%r*Y@aBnrcZzd^3U1hdTnWD=u zaz@+oF=7bbHfh)4b>l=D6O9d7w@=h93qdHbU@c7rYx&yF>k=t0)|HFHR$o5ue8H0E zw)p@Lzs+aFO3L>>90n;`I$n%bLPGR@ni8q=ReNovQXv*#wZAKo$*j@QB&KK0V8d{5 zYej{?(`ijcw?T3CV{3SxvP1VsVOD-d=`rmhcB}X~mAX1~e+-tfY)1k*cYgh*QLa(^g$xq-+ z6I1^EKj-I&BktEKuD70=gD1svBbDc$CQV@9Sm8C~B$7i+5kG5@*3SrO5gRLI1f+<> z3L{pHg(f>~E#O;Mde(cIY-#?OmzS$ah_iBH6k86))=~Ga$51%dtK#?lRz-mxK2Q03 z5^g0MaiZAt))vHGO9aho4e>~^>Y84MJRMu239Bn-A zeIZ!c%;}8Nxnv&$kFeHIk=mh!sgxEhvT1Z=Rz;SbTL7r8P^aCC z1Wh^Em0*qCh*XI`GSYk65|rAQ-WRxlMAEr!cJ+6y+=Tq|*rX|&#Ezh-e$E4ei8;k7 zpc2hYAI*Wxag9G1^bP{V$GnI4C%d`-LV8K??b!lax>q=DaGexZgjwxv+3(~Fzy{2K<$X15FaKYZJ|q$O$E%% zI2}MZ){J1xPUu&AOxhf>LGb9*1oLt5dX?)e_|R*(^ai*cW_u84Yx^ksMO=9ax`)Fm z(HHzi1-$ivxi7@%Y%eSX-XRmawZ?|ChaOz(eM{uQ<2Bd&_+E&MKf4;$sWM>yB$F6Z zH{bm!RMlXVu7rqsWTxg^0**+g8+>=p8sjJuH`sv@`wm9iR;3(zbvCtOTj4f~NEYiF zP3x>pQSCxewfK$9QD$3)Q3^%Lif&@h%6{QcwvL#5DWiYmQDv=~7h<{Vbhls`5uJiHFRg=S$n(A(?%kE}U_!R^tIv=W$jI%J z$>(0@-KzY`^AN2l)>kB%i4OZ@bN&Ih5T6Ukha+23VF+7@OLFA^t>I_sA;BZe`~g>y zB&JE_0@ad7w&i*UzJQMebJR_>1}-&O;_OYHQdLo5ZbrW|kHY?%X0a%-UK07zExSm@ z6{6f}!#1X+Lnt|@t)oFWI5w6+vG@Lx zFd#D5UrezrNJP-noU_9?j3E7!M;k%x{S$|hm#64nmows(O6ZFfi?x^g#qX7NCAu2U z5#>QDCw*Fna9*EU$m|1R-Ov`2r-gEdIpmH0Fe^-HHBzE8vYeaJ4!B}AW|o7BoK!yo z@RUv;b96tVm$ws%UA22Nzz!D9jM+QJME3=05xbK63w4KT*-1Xg8gbA9WT-n7Y2bwN zO}=T0^K^39%zHR?2c2awZVaLL=R4AEs3;rLm~)Xoyi8!im03)&*n;mK@v20wJ9L_~o$A)>O7Z8iWf96*|Y1@!*QcPKW;Ka6a%r-&lPKVT#=C z`xka&infzirU=1ZPhpr~`=n-+Ti|6+nHS}nXEbwK5Fi4goGW@kyG({>JQp-hn|59_ zy+uJ&J+L|9utK$0nij@;L$Coh(WF3Yxx{`#Sndp>bzv^YonmtPi0`X*L`cP`J6sn0^mee{J+d4~pFP z`RJdbw&LP^dNkOTDc6Ht+$hzb)mLsDRc>G-p@Nd!Rxj`rzJFOY!mg;k3*%%3vupjn ztlg+`%Pur|?7oecpLS0FUP{QwIW4t7*Oxp4i795*F%L&?LfPcYb&Dv@kA8f;x3yLy zkfYQ>NBVAt>CIKYJ6wo56G(MQOKJwvaKiSGu)np`(mN0LJS~BrEX~vw@~d>y3xz5p z=6O$If)c7$k^oD!j+=n_5XTJeSwX>2hCa{&xqWZykh{`{8np}clo zgy=}tIiO`QfR#2GAa#J%AwRwf9CZN#6xg*RUWW}u`blIc58TZr zP<$KcS_4<8Ul)8Y+=0$a;jW#l9iZSPA8(_Na;gy0-Wuq$xjjK>=Cc`Z-RzUHL_$%59#yUo;h^*hI|?L+CoZZCqL7iiRnDdF)ZRdki)aUyrf!%+Mzrv zHcnT|jif=Vi1p{5qt`vduaL(#ZD3?r$31%u#J z8yxAt3LEtR0b_~YomQ-$Ey|!Uqp@9@ILt?j$wJ2b&AeVFl|-f@t>usV-8q!eICo89 z3gId?C(h7&ZZ)V2p*~rU(DihFR)&0AT(Nd893Lw( zje}qaZmVO$xe~whS_g}szpmocJft&7zNKS-MubPbjFO%8s}F`h1}H6G!XK$#O>q4N zIf{<)8`%kvWFZ`GBWk7Qa1v~H4qk5`h9I1$PXe9Q>FRr+Pb0M5*?0KUD@w$9W_O$_ z`W}?~dvQ{y4j@-&M4I)#HwIl;DH$*K=V?nen5=f?O< z$LWy<5D$4+>XQnP@PGtovRKn^YSu+ftYyR)_4R9XH_@7UAH8T3)s*Ea1{XcZ{t2Ng zIv;!`tQ$~A5IHbiJ#EcEf{bY<&YBk(epNn92+L>WJZ}dNXQ&A;`atts_!74C} zu7K_(Gu*yD6i8hXTMR5e_2cK}Bmm~~YzKOAgreg$oA==Hy}U_{7v`|_ByJu;eynK8 z6{M*CMaiiX@^K-vzV?El&!bcY#Y-w&qmK09#~+^hS8GzES0|Z);6W2*X@4B}*iLr~ z?I-|De?foqCpi{}bwQ`6-?U8AF$Y%+FYvBhLRs+(p}kav3^zCO^9Ok4N0VeRUs->p zewEYEH&kbcaqChBg4=Zt@2}y`I4X{)#W>>Du*fxc+jxmXp`ys}?sifF*m!&td=zOJ z2(8bU6SP>wVi*uLUiF!~8iCRua6)1A#IKh&6*L@mBC|6f;zdqGJBg33Tt(Crj0!{+ zOSZkwfv15+Xr4+G^L2?tYIID>koY5_zzLjvK99o`!uJJXSvLdpt$@VXpnIi9JvivP zdePIpgGzhGbHn7P8@O+CBpbk@50Ew53T=I89rW>nEbcwE8S8Q5EU}G~qkBJAmmhdl z!A92=eCV$0;8yeJ0;Lv!JRTbF>ojc)G>vTr_ra{Va-!|fj=FoXZ_;fsg?X;XW+>4` za9_zAHjk8;GD1G}A;T^$DcV~q$S;!?-r+eoop=2$FmFT!HevK*WnT)nIoP8|%PgE4 zqix_~J!|if(l&+rJv^5rZh#SZHKzjdemT$5VX*=U8rKWTPhtouds1f5No7|SXYS`^ z{jC*3;pB&|V-uE=xGL=Vy=$XtRjt+15G+5qAlHh1AV28zi$6kVL`$cb&-dH_B#==* zo)4SX8~FmX`3EU_g>bv=RSjY%L&_@n)OGY|H-hX6N+3L(a@v;j3xhc|*Rzz}B4n-P zfaYo(RY}7_Szqqga(WwB8fTav%c|D^NEO$_OzNp|`}OaNu71*K@^tPTD@5IKHHoQ(~t z@bItjiXiv-1VYiU_nVK?JW_$2443lYfI?nFTUiovp5m>v&j)2vyb|(msRQ7udC{C6 z2KC$?@D$fUxWnC2_R)K$vqcRwOy>_qD-rC2CbkMb@Hi&1xVzRztG(pVs@TkJD z*mfab*xhCw-}kPVXiTz^yRWj*BtpyT%v9hHzvo$d>n4#^-I`~ti>UiB@S3%Da_>Oj zUY&wK?2*?_qtX7H?qZhBtq{47;z>n|RSHbql1~W)ZN8aVZfh~!wF>@-X6Db|F4-@< z3o6dF!z9r^4Mi71YZEzAB z)(A0YqZZ`s(Sc{0ZG=6M9KZsrDf65Sej7QdQzq#FscNR#T~{~g6`NPGTwoQsl(U!P1O_%9ruI47NGj9991df1)f+GP6+B>OCk|Ds z;p{>E>bQJJ@%6QC?%Z4v<;mW9^b?sXJ}iK@9F5gBjulr#_~U39sJa;Y*ZB$OQ6(tB z1fBP<7pb46k`=A}qMx+je-}Pdjk&}f7tBlO$C!>p_1!$2s6}!$y^^xzR%h~t5QMe&4p!dmwvut1rJc6lnqdLPY6Iqr z2gfS7pF%Ri;5HclF%i8Hz{@V2FX9#c4Ftl=$2Q6wW?=h9`TL?-2#A+EGC2r*6g?D7 zKnR$Urh_EL{`(0G6XF>t=A4L|{i86j!*zoz=%+zomL5E2s3wvw(Cw#t%&^8Egn6U`ZVriCmS zvZg?Zt5|Db27hm12LbY_8Idoi~kl7VD~>(T>Vb~@BRbm`2AH8_J6SU-^fhF{{k#P_#Xlm(Eqo9 z|Mp-33paX6|Tb?7-mYV#)Z=Q5dY;?Ck(Dj!tgY zjt;IoM5e|j#-{wtL@wqQ{H#P~<|cNIrndadJWM=HM8*!rcHXY${7hb~JWO89%&bKA z=KNOXUPP|$CV&cs4^3|xtQD8S-S#STwYverfz_Ssk1#l69B>3%*WBeoS%i6j+vRr!r0Z# z(8<--+UXyOe^21-WawyN;cD*2PtQW+X5|9VaODR$%+Ar#*4PTLXZT;8tVFJM)}{cM z|LR~Oa&Yplxl&4_GWiV>4r?f6Op3G_f{z{pTRoF6RF-%+uW3 z(#p*QFy`oF?qF!?=mZ%3cc&9T)z;h_FgHI7JJWxfhW6GDfL$V2Q*#G%Q+GFhHl}|t zU5x+nsf)R*6+qp^)bM|q`_H(GDZimt1tkPqc%(fnyd-)XS$R^yGz9u z_@Vs&btS^++E9gCKTY-hJN-N;a75sjegnzwO3=f&2XASO{9HCjqS;4YmnjtA{Pe}ia&N7@XbpW@nk!Qp> zzuOp2sN&)`J!D6+NAK$_x)sGH(8?^jWi>Ec4PQP&zU4(dT&fdZXhXiV>&6A6-Z;_( z)(1(9(-2Y@uSvP~%PAuF%1Laj^UosvDQ6gso^yW8i#I~QG;V9%L;~+w*nKp&uN9#T zwjHPVk4+)M+ntL*8du?6dL#PR;6kXMiS#^&Mi-v4u_ogLeJAN70#l` zILKL3Jc}C4XnC3{r+~cs>JyoW`UA3MIInHxPsXI+J=F+Jt=7=3@vdRhfy3%wgl?k zyP2HafrPHyW)GIYC*53i>!!IQi$1lyF`nRDXQx50EyN=DbFuSUz) zs4ejbzk0omMRglCPXT#++I9Wwfh+5tT5FXU-V%!R-Sk6@zq+fhR5DC|jbvTyo*_nX zgx)Ywt!;3n5o7G%Gy++fpoWRyj=wU*)s z*uDscJwHbDq=)x>_#=cKc@C?=CbLh-^py@{3$Spm?CYb0siEiPC&|f`4}I`6^3P?Q zCH7LdJM8{~&mc9orK7JQ#-@dC3Z(SXrrF=%<{9{9SLnGjIl4=e=JJKru|HahIW!_A zedl_JL9I*9b-HemfH!MyjUzkGJ+|dtY}<)ON~S<4gLS=hrTC`>jhwc`eK=2(dR|BA z6=!h*ixm}tZ8A>L%AbGx1Qv&89s0 z5QhT61*xg(0ykU|9tVTWdjvBlpWngmqqsZPqr;mHBL{KposF;DWI_LZIg+A!UqXE0 zy{RNiz{5CM>MLrdE)LlYKK6bhVS%RE-ee`vW3yhHlA4TN6Yl5t5%lPb6b3%(%J>~^ z3`N%r5aU`0%M&N61K!pKk2^u8ei|X1;%%G&aR(0khKT_-o22niNr`JN31ER)?}7 zU$Zxcy5o<8-r$-9{@E?o!2SkuzLWtHfsm z*28_)R!O0OMTxmo-u2_@SvWBIlPgf_zd}caa?5>AqKgvzi20&XJ1`ie{k_%mQwII3 zHj!9nnKCkz=}2usK(dnz{wCi6yIfsof;!!w_^VfW1FAHu`lE_z8GL+`5d$9dwHMJ0 z&6P@NOJ}3F)CJshj)ZsgeB50{zaj;&;C(I2jy6;2mu+N}wmQsZ-|W?Kp4#ihu;UdA zmfX*4lRIRvym0Zboq-& z;Dh27l38<;Kmbu&Y<`<&hYi)_oEe8JiKoIlIQBx|%K@rz%UHwTOP5eH8ZylKlXl84KM=={_XY3b zMzJp{3A@Q5uMGB~QZrdn=xraiUaWw~fBW!|3Qy{8j{8a0zV9~N*EaJ3FRDX)8Qb@! z!?Jwa$~I*#AZc?g|G7WM^0!xPy#f%;?)VS7=h@h-;dPe8$|8HYXM|rk~+d>aj1^<&Y?YR1j*Xrf0%q7hQMd0x@^;u*M9H{3cHk@!*}| z2oR3IGC63%+0>}My^cY~M$n=?kR8U*IV}|p>g7gJKnyI!A@S zcgzh0dQ8(JX!Z`pyFTSL=9{jmz>-08FM1PpPV!Yw8Lqc@DV|ti%r!75Bpt~kt-WDO z9rjj6rS#zkcqD!2#c`J&cZAqg+;y8QB8Vt;h*OFCDb(n5xoF-a2Tky;-<2R-jXOdu zzO8|3MR&F_Bw0xP*uGhn+*=e)z&$MDnk54c=jX=pE$joVs{BNz0!XKQzj~_tyLe&m9VV~%+XZMq)sug zq8a&gDkXX}Fo*Qyg9Yu)UdgoizG~RE+hI&2-Clg~*jN}>;zM5!w}u+zB}N)e%Hk#0 zQGUNs8#X@%Fxeoe&Le=D$$~DMC{R{NmG2Xh60Y*N|7esgy7x2c3CbBjp&jW9;(;kQ zlRFG%HO16uy8Crl@Bm!-gwE`62oHZ$@#8(pNWSReQ*|vS>#JOT5y9(~XBL!YyZf`~ zV(kpy$K3K|1jwupM51u#*!xnO~)8#Z6#{J4ah;3HGIcBz!AEJBz1- zK{X>)&r@l>mWDii49)b5^vi*MgnnVOs^n5gpu)#mLtO3?uC`d9(Vif(L3Ax{b`vS0T)koDcO zAo;mCS`ik}`-8)r(-1vEXPuGl8JRD@OsA%ijKbjQk6_%6?2XINU=tM~7T#lDg=*sN z#DO+bN_qZg-JR2C6Rp2uh!FD`Vd>w)+@>j=tj3V?YIN8Fy(!(N zCioVu1WX+U*77O4W$}kkVudEQf8QYc4lO?QQKKDrS**=w@mB6P#?NvinXhQ8*4HU- z!18|{sDcb+v}>~_h&1fRYZ2jk^(4&*ygHRAUo^UBpZ_gBsYp%tV`WyfdRp|O{dhj>e8EIhWBS1yB+xyV26af5=!r5T#A*6DBtw#|GeAxvf^Yy=|s__v)Jwd+xw zZJMLlLsHT2o#IUQJ=7lIl3XSs;11Wr;-X6l=Dg~YK-_#cOum|FQO8Z)7KbY<{|z;X zgnyRN67QZ{`XYb#eOC<}`CKbZ>S5tJ;3A?|SbP`ms!v!FyJv7M50$Fzmw+m?*%6ij z@Xc+JR3p$+XVs%~cInNtktUVbKL-Wd!1d99*q`{@Q^&EvB{tH>`43mS4=vsH^CDJ}Il#>Y zV5jOFx45o}L7am4k|VPTnoVWaWG)~c`pTdKuqliLUuF3K&|i=0LS*3J?6&T_2e?TE zm=FIxL0ow>s#ai_%f9xvY@|&E(`9N5g`Sgg>44m<*AoB~POG4fUDX>}ECz`kgjU9k zkP#Crr_;kDH|5iUzJNN@y%J?%6aKDo3h0+`$TRt$p$1Su_#H25VD`x|72Fnf>uGwH zVf8)efhjo-w8{d+Vu+fPpJ1?uVI)eC-&q-19be>Pr!KTts^Fz3Q~+JD<1Z!iUbZ|@LC zAyOrttYqZZyw$V1i0$3EpCt<<<+D85^f6Kt=9&Er;xV3gW`yt`vC@2jpYsQ|B9hRk zNlS}U8#b5*ju;+Frw3C>98fazNoIzCWO>)pDJqqK$V*5cdV>TeO(}G6(#T_vxO8Aw zbcmu31McG0WNY1d7yeUC_jKD43ENbjY3a{Gh7Qk3&vqOY6}M)u03P&Fg!}DuEcAX= zVzn~?RA8Aq=~9x1A!Ebm%t+I(LYxe@fg#_>ea~!Bep?`7hoCpcMGd_tbn5S!aVN7F zu=zx$1>03eeOU?MLgOhsvN3ny;49<0>TAIsa07=VQR6@@HaEQ13G0M&sUyp2n91|X zWIe<7mb}`dO$r00;I;qK1#lL^At)6)KREhVxvbef;r-FPhb0fJZ(wlu1!D{xwuVyW zItB7j`G{rmrY7s$3nTk(OrfhLunWwPuS93qRgtXrWLv(iW&-g57_gVVs(97{f{ zy#>`!;DOHt9m`D-z56$hw!Dfq$ps()0%-WnVM7)AYFsO49u5@!0}zVw)n7jmf5x>q zbAy=qPnRGy3rU*U0duDf3Dc=%M$BmY-roaDgYRU}kO~B>v&q5!lWhCCR{;6k!x*8_ zWcp??9MvjUNf*!BrW*Me_MBEQ(yv9)Mz-`AC&hkTLy(NN!QP*VsD|qTMvxqb;oXy9 zl4;5n`<+d#cZ8Q@I#_KhM(kZp5lO955WYuxcW2_8Cp}l4R|-- zuc{lC>n3}-l0{GD5&dl3lF{@0z?2O|>=iT~=whRCi2NhlCkxXk!Hx>+jm<&3mdn!1 z9K;jF9jlDLTGBuHDDR%qg92YGWdX&&7d;DCfV1TZg#j)+X*3cJwqT~tt?6W1uufLp z(BR{R>U->A5Pl-eUC>p-A|O%E=IjXJ6#CbDpuTb#){6=`B^evoJ2hKV!|k!3`%{7f zrRjt4$?!AJ9|t+)^ZUsYAW7({*9lzZH|bQS{o5L~xK3X(7?=9t@2D<>5QzM?Ue@`u z<4vf`hPo`bs!lg9&MKQA{uO#F9y4sAiUkjy7o7Rm{1JuetTSXZYn$FXt4FYL?sJ+Q znttl2{2qX1%F-VTGEc6OH0W!Ta5;E)!0D-#wse`IDu(MTsv5RyF2DkdzIvn85eU;A z!W*-JO@??h>gHzwGUH4LK%HmEcs1e}IF)^L&5&^=6dCRe&>Rtkh68ECu!Tn7 z__W=Z?g{;qIy1nd)P}C%)RPeoA=D7Czvo0M?am?CtJYzeziG!6SmGa1GJjF7G^KZu zgs77=jXcguQ_sfS5bHeMqNKRSW0L?+6&BBI`f|^r!FT$xbv?;9w(Nj_w=fOot$MHV zKM+sbs%mEMlW&KGPd!|d1c)e6kCh_HOCNTv5LD77pb+OK7L)ClU}gR1c$LlDflDrf z34f4M-$FRDY^X(p?Lod)&zzRUS%m-SEi9DFxqLJJeC(vRL4MF(!zy=vnG$A%Xe*WZ zl>_g7E6#S5)YcO2suUC!rE1paRQbG)2%eTZXxMmCAG1*G$$_~Ja){^i#WpW44FbT7 z!MekoV}_i2=9ZS?b(vxa zihv^ zX6XHulH%%y4Ikiou*~8&8D_NCAV4D(Dp+qcr5)>na4higgK>`>b^B(>)QIcdI|D^7 z85UV=QmO@q{-k(3t$C+G@k5rEpcIdb-ye$!GsE79Rn*=X_V{{LS9=GcYk4Yy;4g|i zvr8-t#c}ho3g6CQn1scl^%(3pHOvBhW<)q53vxObDL{OIb~T!fKxe54lmigPUs0N6 zp$U?2iu2m)p1DNGzjy4qPg9H|sC}3TKMHtf zK&VHw$k{@84fDVxX+_7M{4j(IXUSsbSkwCuSxoi4*}WI8FW^X2*nvU|?x59Ub3tEB2(FSvon5y3abXnLg>=G2W_1qjT&B2xgz=N>Z!Wa3QQ#v$t5?8En}b z;5yNkrY)tsB9>7^x}+`sCO;^e%#Jml?R8k=-YuQ?{ge^AjN6^yV#ag@pw4exq{+do zI#Y7&t(1=STfdBhQb0U?j%>BNedn946||qTXWFSKwJwSBq=E4k_`z?5)aqO-D;rTQHui1{5{7Pfx@-EOdujXJ+X zBh#g5{G;5Y|LY1B(_mq9C@YT?VSW)UxJ+6MvJyWxtF%uJM2U^7k6E|?t+zh=YC@9f zykStpft-r3hG3;P9)mg<6z$DUx{#QEkL+UMfzLTw>G$r-=iy7IW!Hb#UF6PR65Ugl zgfpR7WMV6e%RJUnc%rtu_jk63hXaXy!uZXn5yo%7r=I4d)I|9Yv0g)mTkF6 z#ycKoVR`RE=R`35L~P9IKi}^<=3Y(;?1dT?A5|g5*1rhmlXp91Sr~Vo0JiREceqXe zrT;3~Ba*aB5=Zt|Qmob|tuTo(kZ_tRJ|*5;S{4)Ir*6IM^A@cxlw#(SgFay-lYbF6 zx0DwpZhLj|D!Xg@FC!Mw(!#P=T>&#^FtK` zuV~$fGn{4uF{WP|5z7Jiqt0kJ+l0}IQ4gU&B(#V|hXNLL9t{ylc}^F#rtXJ{uQi|z z%;)0_3M;`b?n1{I1r_;eHbGR7=5D3FV#HlYmkIVz8x9VtZFal_^4XNT7ar{e z-~kJ_b0!9pd!kY_dVM>c;Vw+gSzDoAW35=nA|c`Zx;=8E~y zG{ojjRm9%~tvga9t+sPa2(+JFW+3AtX8UPtK_08Z3&UO2vLAO_K5qCAlRJG zhD#f^g_|)Z)D`~FST2HXjRL##%#-LlUwz-9x}eH9S23etjqWI z*lL9%iSLpkjXpl&C0&dm_1I=^Xd^qT=XOS6TxfP_?F-SD+bA0i3JrA8?{a~ShJ~RT zmi2n%@=FPc1Q~oKho^Kb0FAY=Lu~^=stLw->T0o5C3BBF7_SgC)$SeHAw{vn_K67% z7Qrtbrd2=Kj@h=il%loCc34KZQ|g~O z0d%#xt>O0QaAQ$p@N@vjUQ#m|Wz!tNSEn7SbEh~om)IP(^NbC0m$D*IARN1d3 z4dXi9RoCs!HNaivtuS>?FE|;R=wVm=?oCKz-uTa-)jq9?;P%Axj>JL_>jfn3$wkK# z0Mh5TdKNx#Bdc91?J|ozbm&KZgvpmX)DV|0-I^3j#bJDx0rm&4lH=yB83}@VCMr4F zOUfMW6kYPFB=Cni=wKP!pGM*a`jcQccE=dObc+*NKdBNTP1k z_2COqdD6ye6f(>R*o~A>0040*a_Ct-^}GgeKpdc5ETsSdIAP|F?!)G}*1SB>@^jI% z=hNX-R8$gUObnvOSjIKD@HSW3Zlztd4k1xO%-B z2uc5-usSIrj6Nor$eH-0u)A~v*#;f(Yrb9+NTg8S6`%)BCKkPx^BBpU;xUWT z@}8T^?x$!*L{Q0+-P=fLhES9w0O?<5R*>8%?MYbMGE_*Utt+Iuwr(Ml*Vrkp51Mk% zwk$dUb{6dxqrNl&Bn7@`4G}5aabmQ1u@clgg|*N|jO<$8#Quz)<+W}=N=<*FkLfEm zDIKr5DQp(e2F>AuRL&%(`^$^lXdHyEjjl$RdWph(+&E*F-H7pXVOp*5f_cDw_Ro#4 zuZS$#dK{YgsE6LrzN2|n>~9l?A>~iIaEgm#CNR%velB<2*$+$NU5n>pz8S~ezNKqM z#VwVS%#4Hz>|DBNRkk9OSMx9P?BiK;13XyBrwE5ukKB8*yJXQPb?tdzA%{1IZo5RHR>rJ z4(JACz3`Jh%(4$%r-fi>yA3SXylZhx5~pDxqzlxG72!5N8L_rw_4`T}?lo}??a&t=!IW9@goK`ioDd1E65nkIDvZz>(6TLA(Mtcg>o8}Ed8u!R8sHCdnM9?_ zSCR9LX%y!wG&XN);RdCie)N$prL&PHO#KXQ=eS@Xkv)KNQ{Pbt=Vtc>P*}6GK&)k- zHy6W5K=yf^I};}EFdrRk{*Zn!Cy1UnI%Y>GsF;0sxfgln#B}Oietyu_#sxn3P4Qq6lu5#sZWmnR51YDV7ye+I51`&c#z}jjWyim+p zjo;(e)jc%tOW)k#Ae7q(Hn_aL#9v-&d8ef-f(Bu_p~!@V8y%sKiYH!&&nZ2+v=&21 zXNUx?_{&4>=qu0-ZTMejr^Q;b4*F?T96a)k>sk=WpLLAHl!yNQ*X3?6O!6FlOK5d+-QcfU?SsK-f9zo z(&|eZiF^BWajzv`tH90g5|E`zN&@$wOjBmf-Ka+GV#(R@eVZ5&gMMCQ1F}5Vri1co zk65_uj%h*-PbUtcw5tZ3uB?MPAVVsz$^}hnH7xtGqI8YAcGdng zyIj268Ap)~V4msri2eMrQ`Sv}ix3rD1mg zyw)J{?MG?RPDYn`np~4_K2exn^dQ_>g2I$*xbnl>hvzygpl?Ou=5|p;xF2rv{$2b1 z?Y%d#lY_>A9LMnpAS}?|LPnVOD(`EKj1lkCeBv}BP5La_FRq+3Jm^|dRRd?WDG(Xh zNMd{ZWEAOJb}0GbBRlkkFpBroFHe*gTS@#7qkoF8FuD2(xSMlpsxb6@xynx~&*D^A??)UL-pe*TOe?Q^h7CK7ZwcBHLW%aptPhmrsy zJNKi=qP(*iMS($4!-MlK>jckR|#Sn+h>BEa3 zR0HgkJg~dkhJ=Zo+N7$oPOPCYGIZti#G*RA(dOBz%S$w{CyI+FA?Ijdc(@tZVW-!AdzM(@2rB%Zg{tQdE z)?2h^e~*Wtq98nfaWDLVFGVnJ;eW1}%P0Xz$Zw#PDqdqK!;#PFBu8~tqgE?$FxG=} z+lu!q4THqRjUvHaJKsA~jr_02_$`3+ZsrJCKg^V-<;pAn7OWDTsk_+B zMG`-^rRjy?=jdIONR`OhpdmoM(Yk}{v=WY3DnrfXyiOCY%4<5CcKR&1G~fwEe+85| z&VUF0s%x>7&2kOfahZ@;F^Cj7@{wtFi%DM+kNg+R{(07WJ+Ic5GKO)JL1z@CW@z21 zl7XD=5th2Z+m|P4Dm|4AC)S00Tl&EtASvDOiadhdW{zFpx}$TaeS&mx?S^G5!3(q+ zW(UVbh{nA;%JdMPb!?%4j_Em7CW(D8soGu+wrM^>odV=HNETqhUgLgIUnvS`03=~= zD9K$SDnUO-9j5+2GBMP;ZHS1?-kI@y5B|h;3|ac-}%j)_4yBxfR5qLU@-mW1qq4 zp>!xFl*_%5A%4x^D9D&H@oa&0_76wwP!Xi#%6S=9BDNcN%pM=NOsY_K9)Rb18~5D3 zeWJPNVE{GaYbaL$##sWP!P4a8T_hBC2W(8UtbbG0@wpN){MN~!hLL&49@*hNXtOAr zQ^J7KpI)I`g2$;YL!O`6FxJh2BtOtZ2rSw^pN5%AR?znYFSdqirm4BtoD&T~SaUw1 ztzaaI6Ug`KG6xpOZh?Tl5WM!uMk5@;Obil35}TQ=mS}d%wd^BK_1fa z8YE4tJFuGd28`(5YD9f(9g9TlI&lBA{)gW2SrfVyX4TDqsU|{`IwERl;e93@U2p-t z7q6lL9qLmf!8YqnF`c%zc)6I2J?bMuh_s>Hc&|L@pV)#pvhnolMA(aRWchdSKY*+0 zxLKcQc~2d)NHdU48l8sMR*2pGUddNYP#$fLOBr;1j|&CoN(B&yeVWN2_KE9P=-3FecyBB5f{X~xHb$7*P2#CL3&%F`f1ODw?nKxK%;GgRra?)9NWj*N$EjFz7)xQ7B`-CKgZ zB?e?SSR6fndRWn=~HJAkibmUh|)^CwbpF`$291VU1|zJz|$< z!LA&l8QoW9Gu;{VkRP~6JVQp*m6+)OfiotAjtMk&Wx5V&cth3deHXGEw z&}0QI1j5xMqX+zzS1ii3ut^PJ)ZkLoCUVTnM9F7~y$gG1ZFA|-wa4Mj7xDwiJ+g!2 zNdk!}x-;2%!GWYy-qi0XHdGAp9i1Gag9&y;R$b~psgs$0YDG>oE-y2cdm0*jBv2){ zd2SL_P!hes+G41QMi(8Meapr@e?y>>P5xX?{pGe#%GWpVj;-cp2; znh4HleV3U3;85h7_rGr^KMwto68A}585ROWHdnnCr-7;YuyTWAKMe?eXCR9a&HKC~ z-#D3fe;;q%xuj$zr=+wp1X6eCCr`>{e+l*YYN2gYpT`eDoDpD?DSih-C{$KzLT_{v zD=Fbu)F9i`#Ao=t`2{uI_9^NEX;+HBIhH}%C5uUL7b1zWS9JiSxycN4P5;KLF=zOE zrWUzZXhM;T0v)bUXEnr{!5Zb#(A)%YH+PIy3FRh0DXh$95FrIED99J?-TDkcj@yMe zI^VJAke27-xXI#nNP`2;LtX5%M99l(+EOkWFb{Imll*zn0LOoR1N~}8P!1qkwRCe8 zo<;c#8M&fcQO&64pb)^l55N%!=@?tJm9&6D5qt4!fZ=G6Y`@v*`~n>%kX!cto{$fpzF}+Fq}_@u9e$UwP!|SKXmYouBid=+orSM>{%W~Dj9veP&cXQAW%q; zm=VUR+MkRlHj&_II#rJ$45~(oSyA&errBmBs5sj-**SbKo1bAr73YE<&3okyNlWB9G54mg z5z^`4$WqS#$hfbjS@H-i)UX94&Wiorer)4@vvp6Vu)-W@RHry5_N~0X8l}Iva~qA% zhh7@+|D?Y>qKauFD=}1HX^y=I-YNAISId#&~FAVAEwlx0)CFy@1x zh?EtWa~dMjcK-2d=IBQR(<74=yibZPX|>8lbjZ} zDMWvRkZ9K4ctVijcO+@cMJn}6=z^dBc;*3^1t~u{kIhjI>60gLzh8UbkO$+)!2H*X zx=WqUw-MfJ-B1tioQXIfH%c>f1Z`)mN);St9+dUJEmjue&U4+z(Jhti$7iMA8q*Dl zG)C|aQZ?>A3MN+^sYjdCJ5J&1h%y6w)@IidcA5}zjJ{PD)dy&q?sZvV`jWl2HK{Mi z6(s!PLTHah43ng)LhI^gu4?kYGh5fbVo{cCVj!QdQP>C4jVYkmuByXT?oPo^Xl=sV+ zJ>)Z+g~@V4$2Bu~>0QrLy7Q*C;T1`ZXtOQ%5BkYSjC)V1#e)eEW$^55m&HJ(>PU15QTsNECg6jGeT5R)TAf}0!IMcum^#>fO~By>Sw_ZVB7v1K7zNs zQ)hLCoC>oh>nR3G)#GQ(@u``SMkj9jdJPHrI}|-dr^SELHHFf7yRfh5capBZlNc2k zHCA6yYSb=sbeJhbwh>6>he;S17zZ6G z#em9}<2r+qt@ARxcHwh8b}*!-j@%d*3w7o2sp6HGNHOWv-=%5&4#<~-ebX2=}hA!cnu{;^0! z<8FsKy*L;Epr{NTk^xR2Qk6ww!lycC7(-S%FL^BJbwYq^8C5gq#HacPSs47plz&^r zt*xoynXw*?RQW8VRReA~TjSPKmuIV50@%;cg^+#;{CJ;Bt%A=Zk{vF&V=R>v`s7`c zZihocJl(Ios66i(7}qfrXl46Yosd_*5zC{Jg`-55Pj8|&#!~=82#5(NWPCW*7_*xDy|vP9+-&{6r!pgE zd2Ig37dRwz<+zKo@Qi;%!y58>T!tGdxDUzn!4@^L@93PYr#e{btTdTVc6CfhAo~Di zg4|Ei^{R>gAnb$Fe-k$3>lE=)1-kBcxWwA_eHV>=h^mL zxM^?nXao9eE8qCdAqWmwtLD3G{vu%Ksk8aeq`laOuWOOPM}=d#(t_wX*R8}K^&!eQk< zqP#g+R!RWbHWY~;dd5);htm=s|Ne5eUxF1RU0OxPvA$JHtEqhF5Cb*Y)W;D-nebxu z(VIN(Beoq_A|uRYm@MEUtOPu z+^04;d905Q$}XKu>9AdjK&rC;<%|DVAA!^#VQJ{VcXUH(aBNSN=lCzUT-!qWw8t9n zU7_tO_7DHMH86g)rH`xkPzM8uUW*hITzhrHxH~qo*5>`&g6V{3MshG&=<=jRxVwD1 z76!hzD{q>=z4Dzc5{kLm^)bbFt3E6TvAk8u%}H(WWf6P`?hT@B{{Dp06Y*<}Anu~(#^t|qrZ2}rh` zy!k~90@!$VtjkKi!F==vv4tTMiRl*kzTvssDMn|<)FI;YFU5Fb7^hz153!Pn=0bg( z&^Y!s1xm!6o4sRh`Ba22o=i`7XKmMJ)8Cu=@y`z4ml2&hIAEFo51*imv;J~g?q5}I zHAQ=Q^G8uiJ&Fr^Yu|8qV*;#B=)x4`|CmspOa0~xFyh^*>CjUWqkw7p zErR!bOpSb~Ok*p;Fx^9)=zl2jhUSR{?PbDl)6PDX(T0#%pA6scW`Dlim*Vrj$dD&Y zVQoZ^idTr@N0VgrpIjZJkNvty?famsJ5B?9?iHV(1SC6?The8MVy^)gqu{4oyX_=D zm>0;V-X<&6Ddx)qhPjqsUhHNVwP_es8BTfhw?-m#q_=Jk&93Vs$Z%Qyi-+_LTY%0v z%~>86Ow}ccC8@qfMflUy`GfLYa+B<*iZy98raH8~H+I^l9@ZV_CPVU}#!8U-%5LKyl*=sleB>`v#`K4n_K z8+M2P0RR^|1*UG4Zjsp8Xy&&i=wgCo0uyay-NI>x&jZFmax_>n&H_u(C z53hTj6+>2fz$#Vp7`wTC0gOk&8<~fgwarpcrjFS6Rmn}neXs?ZOBCN@#vrwQPzxmr zVU;%DI3&01kc5^^yvqpXZ6O=-2Z>YCg;dy{wm^6}LQRPa>bG&YLet50wQTE{`OjtH zv)UQ9CEGU}k{sSv1YewDM);JN)0)`r;Mp%+sW@UpBkK0Mx%lU5MAq>56FtM;3R#%Ax3sB@B|%?BiQdS4l$byvvWz3qd9r?guu+Ch7G8>zhwRU(|AM# zA#ExXD&!YLf_};f5!7+D^!P&bmH0jUL{`SoAkg5#&a}<(!OGGE3YU6 zpGRO21>7Y@S-ekp9!_r-MWjFT8vR&nzZ)W>%TT@HdliRz1E;~3$DU9H#qCq|sX?r( zx>+ghK}l0ytdA#(UYol+TjiDU#TE;P$RJQ1n?=$bzxx60a}+6+Fw46xR-G6M`}c4k z4B#Q@I7KQFsw?=vnYN=tW4GR-|0BGk zX88ihYJ&O3HZCG}Qbm*~;1S?-7kd4nETQU}(q&9-1d93A4c+V}AlqHew=Org@Yl-T zgIEFB2s7R9Vm zpGv)plsgZEM?Z8Olv0tjWQMI}aA`zaZ1M{57@NIdg4XEESde^vX280&PbU*R>L(c6 zx+-}g;FrQ1WJlTvM6mEbMsQSbjBQJAh9l9{T_w<6ru9h6N-JAlFiiFNjT?&Fb(+eK z5~p{CAUYi)_PAq7Q;;tu7@U00vl5t%Bqv#N|FjQu? z7;C?kpL+;x)lKZ1$d5YxjF$2{$19Z+=?9)lU)>3f0z|P2jQO4ovb&L;6Yf(Q*Mi7W zva1V1w~SQQbeTBRzB($<2D>%eCVedIpNbMS^o`s)RtQP^Y=SBawlwf$q1iL4Flsr! z5g)AWR%{*DQ_C9&d%%4=ceIryfJ}wWrqy;Br;By`*)jbQP+8ImCBW&0l9F5mzYSWS ztDmhf2qmz#KH+Z(@++Pg7pSU%Z4xMHAc;cjb#n}HV^=bX7(Iib>&s>6o481${rbuk1vBflY(M$3>*(LO<7HbUcJ-naFks z3H)K#+wJmLEwzi3#_l9lit9oRF)hy)ZOg!Z8}m)JpZv$^RG#w)I6VhG#yREO>ZVA+ zu#Z>4z%`c;RLzQ|SqRDX$_Lj8bg;B#&$N1GJwTIs&t!BcKw!7j!C`|^{+~pr0>$5) zxgoBn`vB6Wv{T`QCN=*1vh<)!2oud+XG5Z(m61@CPhJ`%%b&`@x}(H%*r_q+{R~o=4DK5l zCQ_bJI-d|g*f9q!9@?VbpKxV}qxt22Y{GGOeJL**NMW6*++{x+14g7-h>`UDv)2u% z0_SFqT6D+Pe_;pPIck~?qpuiC&RFlxNgwhrQ}$8>B%6+qD8$ic;yo1)h1=eI6@#%~ z(fXVv?iTN#NF3={L;J{RU6Oa{LaS|iIM|W(rWQ_jM}rKxR9is<@DI2OfAn{<0MSvb zEYlDKQxkJ*b>VlqGUuU(s%C++2dWMJpqQc=u`~Ct8`hk9%Bi)*d7Xt1evj|g3JTAd zT&dk~5U@|Z&xjWVnCMW`DM`WcA{e$ESzbN)q~I&gIt+Ewaek|TEtNk52eLkZl(_5} zuO~2qwLv3iCSbwl9tLTRtt$vWd;?!8wQWmxSA5o%M4MS(YZMU7{!mNdd~aRsa7qh< z4bJv_YckZHP1i}`v=7`GNiGYt@214K$j0Kb)a{FMmK^Difk33r))`8toq}o?kJ zcvJ%n9!#&%=HeXP&Gr4{Kp(Q4JFg7OS`B8D7gGh zNWPS-s{b$5wd2QTrO%basxb^zX7`VK>kQxqz78Kz%SeaCId{n(v@UDiEgOG-{X1f7 zpT|W5D7Pf-JV}RU5LT!_|182n{<>5%L!uRKmPn#XOP=ng5P5`jAoV~E;MO@UJif) zhHIk$pXnVD2?D(-|E~$jmM0#5j+i&~jyI6F6(P*&qm6bi4e?j0g7slhCG9ViPIXW^ z5C&kT@sBIRfQpt{`LE(Si8z5;t0HlEnqgN&S1E(cQpQP2<4K)6PL#PE6%lWQ46jnu z#q?;@}=n~$6M5Zujizw&Mp zukDl_lRvMqkcsq`)U#DfvV{;U=~LPrv$xA=)k<`p@U8L;0tIll1x5FUKVjl`hi6yL z1m~Et(k2>x`?fE^7BoE4-`;lr?-c+iDx0?(ev5i|lAP^JJXadLG=lNbizO?Ze!l<` z%_TSx{=epGUW&0FAvrmOqL0>A+=1T8qE00wOkWE@k$bYvGh+9NS9kD1|G?q6+pKA z>*^bIpKAd27Sr_mb%eYp*?vj{;GlFljUwg%75+(?R3QLAnZuW+g-J5%Z zGETMAk|-*Fxnv+jog&^y?FhB9iY;I(!MR5!+W>?TWn3H-)$Lb&2i@T@4+Pm4qIH28*Rg{U?&cZ_5BH28w_4Lqy&X>Lg?cH@+*<7jhxBXD0{-~bPGz*> zEtHY63L<8RmAKh7=86|M^W@8K000930j5gJ&haE^wkO3#sZ(IPBv6*YtJ3VpHq@#- zcda~f9F8%gBk`Yybnuj{wY)n2tyR&Ek^B+H8fjWKDmAOW>HkQDeikxfKMdK=2ztiE z+%}pT0xqQ^g{M@&Y4qAQM03tfuOe{!eSl0e$st4$kAS}LmTU)ZD9F+p1B&p&7I2M_ zV(qu7u?r1Qs0>^L2#r91I-;#T0OjvkfDAW&XAseei-kW#8I7O+aCYAZu9$T@GpY(~s9aw?SO$_MYyKbF{s$v=r3 z6prsSFtNM;)@Z+RD7;f{#eMQC~g>$Q=B6EG2Uat~90 zZOSQCz8EMcmLcnoXOyg0Uot&&L9yS8r+GxjuK*Ig{?UTZ{uG@wIj+9^F{S;W4+`+A zMa^Yf4m86>cp}>+algOpJdBaWl{!T(YGzOe%yG!dA{It>tD=1=G8N7iZ{{87?Vp8- zSDxS|{fPRml@WeXX=^vP?{EI@?mI_3cC zjY9h5Y9%SCBJszj7TYglF(=n~=ukg1usInAnv&3cx19jnF4{ zl^;<4{#;rbo#!l&1*Gu7m95HWc9+yt~=8qJ_oWj-q+X8|bxB26!+r=r3W0v8tR z2PP{8A3lPj)W*L(iBjdhRMA2Qu(P;!!qCzPoQRso2bV@L0E^=+=SQQdsQi_a>yt?8 z^*%FPoEi3lG{yFyyY|Smk{S1eR|wolKO;**vtqk&m9X1&_^wn>77-dKL8>=4kax|R z<=P(^x21fGfW#rKqIE|)L3Y2{SDV;YGOa}c$6hzpawcVEm4Zd_5Zc-3q)4?bQHuZCnQK*=5Nep|Jy=Y+-<2bYTp@#qo zaC~oo&ES*>noVud#P%|h;}BR&`yU4t>u60~eKyl5@0^YW=_{ApY`Qxrtt+Md8{`M} zrzNKs7J;5dLVNt2WMhT*6*ll_e&uEQ@ULd?o~6G>O*I}P=e*lmw zl}o6xZ?TVhW!+O+^$&wiwEDaB%B)n28ou@m*eo58JSL@RsAiS2Au}o3hU|0E_NKOc*AZ^QO>Tu^a*@}t4J}Xv z%|Ew`DkkaH0$(r_W+EkK8$R{vp}oTtN{S&QD_!P2ex0W}f&z6d?QWxFeT1xW<3_Zx z8meMwEbo@-y)FDfMxg`of*YQ~rniUl-!l_9dd4ML^4)4WUva0^&h@0qf~wk zhmQ0O{|KeU3$b%7n~9KTZ>rGl@BTU0Sb!oL3eJ1%--4Pk2t#KImy0a%f8$uvTY%M% zBU{n08?Y2#bCA@(qL0px;au#UAyAV!Nge}{~%(r4MfG}$qp<3%^~ zeu+X)-X(c3VEMh%g^7p?GpDV&S+Af)gO?btXlg_#<1Z7y&O1UZbAtF4bncKr&(I;E zQoc(PUttfjhj4r~3ra3$T*oKCAN7d`pUn>C-$Z{A#<>7_ix9gmsK!^X6vzUi{271` z!-@sh6$ZY~|LA*!jFil-cIW+)9XC0HD?>^D8vErA23tC-S~gF9L6ZG`e+gbg7jJnD zE_=pwjM8W$&UhwHTM?CMyFb_US{1dfm!>hYBcAs^GbGsQt|`}h+fFybswJOh!f%LO zY7;|iZ`$3x#lX#f#Ej{H%1rJ@Houbb*S*V~9&EMC;3UwroSzj}e!jY)K5u67P^x5x zW$11UK#NQ%s-Aa8imo1)QrZ!U=l!s*(5#J6Qk{+Vj#UG&4DS=}MH7gYH&-dMh0%IJ zkJ_z?6G;KQk8OPf=QH2^BRXT=CpZ^``7%zl?9n*IHUmE2q-0V<5+>mJP0J7k%X{XU z0xb5ffDFHjxnY6x7rQOxdU*35y2> zZx|UAyJ>;B@hS~52j`KbM$5SXGD5>0T6`L43-w0d75Eio6T8ufjZ^7+@pMUNv^U($ z2AYO0o23FRU;_*tG?m4oR2M*3_|#aF=h~scjOosV)Pz?gVkS}K3w!6A9_Xi>90x-i zmMww>Q@s*~D|Ikpt9|qHtJ`H4F?fPUa+J-(9>#PJg3uzWkbf&HuXNa$x|~IPN7%>H z?Yzs!9f8FU`e$ zRJ3J#{3ys!$;O!S_MSM>!JMClfE8u@E_Q0M3l0`6$jDdTZ1-YX!e|-KnLPbkGN;x? z)8=7~d2o+wrU;osJakz2*Xj9seza1XwS&$dGptYCWm@rDk{^QAWdwTRf(c_fpcsZo zC4ufom;@`#v6`!}hLNKc0;~VsL=@%o{@p%GQlwZ_q=QVqM)CzzKmMc_n0;QfK8zN1 z8yVh=8?cz6gmWvS_)W+17~6}fpeNxnu%wds$TwA zY;Pi;qPr2ND_f#_Vy}UQYJc2xuot>l_T=o=+#Y<^xU(n#00RI30|3}(m*Jp0k42ZQ z7w=>kLUas+E0YS&UjTcx}D@m zIe@NDAH%wCml`OgU}!9`!lrjjM7z3cK|w|e-bvc3uhwzoI@i4OR_*TsDibVaBo85 zWpsg~dD%|LaOF{+Pbd$uBCe&* zb=FYalSse~#XX=0UEtwF+>7huA6BFP5tl;X5urDK9uPYk`wf%>?UAzkvAqp!m!pZ} zm<@>$s>bS%$NlLr2>~suXkUOCRT!de$|;8JH+ZanItkDzoWAMe7n>k~Z5Pw2e0Wg7 z9H%gK!413l!EYA&jfnuL>&hE})malVC|X>Fme(^8CVyu?;#lI+xjQ5m5qL{E95n0; zVRC>5lI!|vmB8|6nWzA(#=-2*5L-qknl(Mm%PMQZfN7Rv=pIKl9b_zi0uqcP`_dXO zQ-aJHF`h4|s($Lolx{bz2%8*hpTnk#x>ri=>;~T+_7n8cwsLU* zs-#*XGxfREBBDGvN4h8yqJ_DtGU|C1969U&seNb>(C7#CE|RmHc5{^|H$X*#;Ur^6 zTuI7nfqG-ZozCMRxJusDVNZlXX+u`(E2(ag9G5PX1ZGcf4jD+&evL)6G z9F(AT)=Q6%xGGL~yVpCWe{v;LR>(cjoUz1AX-O%OYh3Gi>{Jb6nL=U7U#xvy$=7S^MJ$1}dOOa!tLe0FYZSu8y5% zT|T=)wH@OCdDv1z3hTb#uo+*yR155v2beQwvms*{_2!~AS&PZ+2?IW~B#?GAr$C?kL`(HBkcD+u=QFUb+wl`>_{-Y6Yji$r~*wt#Wd2ImoXM z{;1wtAi6pj9ve<7oT#82*+2k+EHD;JXEh=?P8T`G5cb0{|zOStNG2t>8I%F`pkss}-deZ9W_6|rLP>%1tSXJ;efAHAL1aPsXbd}raU=1r+#9o%W`5qUPxl8 zH?j75MQ!=b_WL3N%)U9-;wq?Z9P$*+EIKB(Uwj+uq~w5YkDng7yfI~Grh;;<+Bv(A zKg=r4+32#@ZsDMhvG*-^!roOlp1dXVA<<{QU}KVD?r+V?gu?fH34 zMX|dM{5~XqiLk^75vM6H92+zP5`%Z$v2>Lpgo;Q zj2BV<{Lj%8m^&n&p8qA*PL?(4B}l&f$~?9+*JuCR#@sni)RV@a6#mmi-zu9lY z&*16W`?oe!x*J>7s?9H^+^T42IAj7z4L2*z!?@F;h0yMo+}WVC=A(DDy1yjg;IDS! z3t&O)f;yb0D;*v~6kQuTYGc2GroN_Z2=kbDMY(+mNvRm@DP7sX4PRfxOigae2%J?l zb9AxOE?rs|yAg~42XT>k=%JnayWtwsZP`a0Z{7>KKy8XM(M^e%Tdpk50l+|CWY>cT zn9?!#O^#kuhqnR`gSty5`@nhBi;mdk;r}`GplwKxdb6+mu2FXV4x7+7}hjD{;7q>*?w{T5C9+4A`n5bwxU zG}mtA>oK-T2;d{#KG~QwxIlkjf02SqJa>2!Qb)Zk+g>iWatblFYf_Qxog%a>`F(n! zLerg|QVmf`D%dooA!8@XG$Phj)ulw>KzE27)s>)~tRDo=mGR@2?_EQ7Ord2H2O_1f z3cBQ{WXk*m9u$Q6=2pZ-Uv2D$l8UJ)Lck@W*c&ko_{p~D(5Fm8eD(cQU=ok{MwVoe zbmMICh7M&Qa9mM|1y--)Rvhc_nY4u5DN${4tE&7?Ff&k%2x|KV)GjN-_0AVd4*UDa zXg28D7-g2D>86LC`euanNry(zzF$)HtRTyx2SxNfjZ{YL@Ip5)CA%x39vJBZc`N{A z`GhJpLM3_>ymWrkdxY2{GJYSn6)UHCuA?TflXy@%bvd8||9|XT zQL`(sd@tbJ;|Ef4Wz^4ap%WVOM5Ot~@D`AOh)R#11SSmqa0G2s6T{bxPTs;Fy`n#iR&K>aeV<6RXm7^HtUO$@kp+r7?N+&A;F+E+iq zCsbCvqv+NVyrE&DD*3XfLjBi9&h(Bxd~Z}$7!g^p3#be^^1Aafcv^l=nNq;Da=Cr) zuWa(W6Oefy`6wYGS(Z#l2r~;W3ZTg^)#cZ`XCkt1hvitYI%lV6u+jPLb z;RO;y3pzVE2X^UZ|9L55o>g^t=gF{4o&dS0aAFcz6lXmb4*{g!F zsFhJ%TOLCOG1$AxM|^MGsbyh*C~6&%QxhSj=TjCUNBT>&7ZfUIJc8Fz@y~auy;1OY z9!^x#q+E%PfySiS<3o$bGx2x|tZr~1?q|@ABFy-5VsSjTq7(R|Q?$x)A*xL9$t#)U z?bD80tA}F@S+XefbhS<=t5mU)POdzT2dKzJxPbCKJbemOr`ut}QNa1{F_e+rl) zs1tChI}6G%@(79mk#jQKtIC?%>lTBx86?4ezDU*-IXUzP)@;<#hF{v$W#xX4b>sR; z{vDj^PMC61fOXu5Zz-GzU1|Y`Ep$E#cd2d7g^`TYGaW|`KRbs3{Ngs z6AcGxNMuEbkbmz&#=W)P$c2Es+Ho`HL4Q$B79h4s0xtXOuSZz?UF)Wa^kQ(Ua$R19 z7rd#7ed5rBuvGOR@$!{(6Db#h`M@T%e(r-@vooj6SOi{(IKzd+F>JC*flv^Pw>W+g zY->p7>(V4_OjSn>#@^9eut7ZO#*imyT=bvr+0~-`b6>XB{*7C zy~0oNBvAyHG9RiIc2aPfD5let4jlbn%x|<)bwf}%6^WjK1I2!=VxMuvaZh`+9nCU} zqPK;UFVT0?af3k``FuWuwf!}{yka?c8*FJMO%&;RDIn8unlHyD0MxZSgZJ^RYt(^$ z`!PQ!^4w0T4tiK*@Pv3aFoG^>#_!TfH)${D$E&0@So%YS!-faQz;U|NyfejB=BVL$ zOjA@vbF>cFnaj#Sa^1`2Eq=h}UZ=(Iq2x3A8nhQ`RAsPyrehGq78-A-$$cqyfRKBW z)wpqki6NAE-EVy*^KgQCe6yl0%R|NXtc#?IGZI>xZQL~we(K}Sk{~V>6bkbbqDj@G zlsH#C#C%4}L$v6M9iQOyltKFUN)F$8vbRKyekW*-4o9Z`tuKoA;gFegJ;|4TlrQ08 z3E?5P4v<}n?_N%`u$oE2>BUFDmqRz|8h;@HVYPA)5cnIrM$Pa!&Q(~+98Vsf4QJL4 zCZ=&PiwfTZf!bWT!|1PF#T{Tiy_}V8P6YdE8IuY>ajS&QcjQK=Dk_7FB1Je`pxoAkVEe+Q`qGQz^N8}!>OQ}gpf+S~BJSNojAj|HdsC1vuo!fr zg(avvW$+pTV3|%p^A8OfkO3$hpCp@~Z;PXl<|MRchWK9W&ZGmzD8Opcx%i2b7CUQZ z@L|Fa=Vni&jckY%(2ADN0?9d$Lv4d=E7#48QbteKusC)Yp6a&>c(h`Q$zI{1O&EVv z3kYBC5@}N3-C2dT1{^y++jnc#UjN2FimBzCO?`&Gi>))s+}D4u^7b1|0ie9uMA84? z+Lq~}iM`@5Z(}W(_*)!+ee|yvj#c_awt(Z6)b#WA0s44E>*~q}K()vpU)PCsWx!T4 zR{@oofKOm+P8Aou4wP{4qcWtwml#X7Ng0bBvmG&;$iD%Wdh8TWU@VBxHqa+-=is6yVT;s|U9&O6+d`pdOv+fHUOx8o5zTfl% z3B-CQ^T zny17(kUN1JCg@MG^y2r9_}b1dgJ6&C)Cb;wd-b9GciDX$Y*dNIyYpkMM~8KA>PW+Q z@^Ohbk=IRNT2j_ng^{gwilRSn^sFYR{*haD%kc>wMhm^d*N2eEzV^YeGi{5S;1eM= zGpF}y0tfHmc)AI$61me$R4Ci|+_UseR6#S}$8sg_6Fb%WCR3}f(z_ya z7PF|}joH5vR_Cqx&3NgM-haKCk|d1# znAs%z`xsb&$o*jKK~JLDt;PCRwPg9L)Z@mk`}NVUq>7CW&_lV5rgjj|VioG;2&ULhDUUEVh(Mk1xip+JBA^ypo|Fy1@|udSs5v7*l#+P}p7 z{la7s|nN08Clv+}uV5l4%h>xa)K$!R1_2+L{ta z!e~-}g7nvWB*}f?(O0QrFbA)t%i6lm@eQ+E5HW8MkB7Fq= z2Rb!k!rfuYkFX3gz_sE1a*I6>m|SS^bFJeb0WV_50m`+gdH*U6B)Q)qc=6H& zPCbu>1C|rwm|{PA&;z&W2@T6OmVBign6nd!%=CuEPSY!lz*fFTNrL>$w%TFhWO zx+N;+K$Bf(#IS2u5bx3it+8U{m0kYVhE}NXqP}bckVHwsPxT-t_DuNlFuX>B^{R)E z%kOaS-8o}0+jjB~@Lq)`X9~Z!hG+FIH#0fqApA`aF1qdmHPD*_!7fS`gc=CI&?yAjnlO{H*YLI7|LtwrQj(O2d6C~+o5c5>B&(H-^O){og zpO+}*sDvSMy!8_L=>cvAspY(`-JJ0BP&6SP6X)d0Z>=h$;nNG-Hs9hfF#i8N30Rwn ze3}$6lQ+CH0009301y-26OJGZWxswCkha;&d>4qSFkbH;$}khn*dA;m0veu1u{Svk zV~2+wTn=f`@PP^gC_N%ED6+q(2Fd{?)Emw7TCj(>XySF$!+VEZmq*ns+S^{ndtw)p zRHr?7<{D-OHstyZht8#g$)w~q{szZA-&R+DX%s<4!PA(~Q^lXG^v(bNPdn+V|IHkH zf}P^#_y6it|NEp}`ToU2mo?4q7(98 z?kSm1w3Cs0;c&?yU4}}A^N@L4^RBoye0dtg=mKg#YcQl2Kxt_*g=;m*jXOLPPoiDD(A9pkX1Tv**hK?u)MQQ=?ny|pFNR#NA)?#g@TF@30reqHmC zS~HK#6e1}ZjOb7To}NbJ$|PU_1{?q+p!2Clh4t1uap4`DBbQkaHjrp zWRL&^>gWI@qMFX&;fB6p!M&;P>>U&p-@ad!rh>g`ieI)gw$FduEXPLUvw(_SQYkUatR^ z@W^dG#$~)=iS&~s^L-v02eGodJ-wx);F#-V3(!=ovM#7r=z(E6EF(XP7joN5D(5+D zHuKfUV!l1iT*%+%_|{6rkpHs8|Kx;Ys}&kCt}pnnC>+=>pSYx8jeHk*+SVtcev_!I zQeP_?>mS4xR-~6r89lP~msb!7X>t^7Jr$+7RjYoF zTuk;eiIk8|5V;1#E(Q!VO4SjO#_O*^OD*yNgE3~?lwnR|6osUPhfUs4T_-1qCJEkL z5T4hR$)+SO{&__opFI!!?5B?4oBS`+h$Lve<}_f9LM}Q5AEFacVThEYXde^;wYBYi zfNxfj+rBfaTunJ4_@(a8V`o0b9toObx3uPeNZwANe;U0H04==}5c(74`qT-Z*)4Wx z!;3BOe6M?O^;>YS+gD0 z?lIs9v|oJd2Ejsf#%g(LBOt$KQ&b3h$4Z78{r(Y}zYVJDW=!X|Jxu4EIV}T(veKJ{ z`EUrl@@v6kP_r`Sww(93ACN4gX`n1^ZL6d*=LNN~T+j!(elyAJ<0Ai$hD`QH_9;4^%A(#Te972USbZ65G zQqj(=z55TJ9Y#ncMwsX(dq0{=EZd;eun&}Plz)@A%?iF^$V6!b~ zO*rH};+`;IJm;ss-&*uVhf~X%c{fBJ!Vp@KtYgZadomBG(5Ird3}c~pN#z(np|gpi ztQ5&Jpd-Sg3A{T@#x7n;2fbt-O+V^MEK?3T3xSj7LR;U)zEX-Fx9?fK%7K6iG~|!= ziw@NzWbf#uOCLo^2W_~bA6te0i&dSiTWMLKwRLQ?F#79*|2C>y)F8bLG!;vcNu-_K z2TIt26oaHqjLNogL5j{%)(5Z&sEMXOC@`(oJw{g|LN)-d35~=`>jri0-Gp^O zHK_Rf`NfOXPOdE&ZIaFK4tMm(XT8^NsVZ#(Ma;whTs39TLY%WdLZh}RF{Qa_hP{<( z+qZ>!uX~F~0Rxyy>RFp3sr6zoc-FQs?4pyGkL_Wst6!a4IsCur?M$A9BJhTlB<*Ao>OxX8Z z-%U3-9D>l}k1eLI-}4Gb{|i6iVL3oG z4LKJQ_ISLEsz#M{;s^3g$KpZjCSt7L=ng;9w0KM!-gfCuX}PLa^+p<7dy@efLWYXD zm#Zitoa8%J9d1%r$_4FWy;C05kf&9z0ZIf(o~N|7xwj$ABI1c{{|KO_qukl(a25^; zWb??i;>N~uOXHyb_5Hzu!nJQpp^x11jDLmy3}1(I$Ff@mYfj&aOQK=>{{WCdJ+g;I z=js2SPaZinUG}%wy5Zs3|~QA3rg6Tf)L+4#jsK^Cqq zBcM0_s7()K`a{CfrfKT785Gcx?^D+xld4JJTM}MZ!8Q7oE)~1>s9s=g+BK9PDdzsH zC^-C7eEoQbB@6rQ$_G%9O0&;$ns^Q^W&ZspIcQC^ zx`#)M>mnH+=~O~!BPC567ip0{UV`W)LFfgLuR_esi@^I&HgQd!6=tPqL#u7kAVz!z z;8KFp=(D#q3Nim`x5cy3Uab9%Zy#?^dcz~-Kh$Xml0wXr_ZRXrhLOe*u!F|Jtb~AX z+B@n6i86!z>42C2Q*$ZMXKt|yt>g!10@OuAvzLSe`xZu!%%lUz17|rtPXN8PQE&IHR>>4yY=S=)h+ca~T0G`UtPIx9?Z$a)f|hMJE!2&n)*Ab9#0I z@t`t>ANI;|&iuo%k3N?$A!AQSY1`NIHm^94kX5_gSUx*d15nI5OK}fQ^;(KV1Wrn6 z^Pm9hjn$Dc^~cItRgu8wDIk!+PyGmT1-?hgExGjC6)cEuGt~N-y2dO!h{oMQ{@*!x z-!?X7j+PGlXQ>cSAb_!Epz@uX@t;lrJ;&MX^}+=BW_aP)ww<7sI^X)50JAw`d+SQ{ z+=rlpZYT0?**kdSU|Q4?chz1pk|fnb%u95(5SDKB7FL=5>3>xP+Z{@dKBZE8h>i^- z{r0?+ieYm1kWWSQp-MGISAk=X%&cYnQl*H6{uCg1D=JcM0Fg%h;2~G~QTP3^C&46p zk9Q~?P*35roKJN@=ff_kudIINz*FakCgAlNt|!P5s#{l&I1v=b14%ib5mQ#Sc(DZY8L41-*1 z2S~Sn!CzlG0rE`i+28wCm#g(~cclaC8V_%Y zL1W57>bz{sT`Fm0{_z%Xl8H%)a`*8nuQ9I;Votf{oQC6|N}vJJB56^~ z(@-_u9DOmcVmI;SBHYRbh6?NviB7i&t3I3ZoYZ!6JD!t?-7N>LDaXKD2O> z15wBcQjwtr(vuk>a9aVMoI=p$&$(^!ri*z`?ijWvTtuzStf|oAfqS! zPa_@V#RW>_000g}R>i8%?T~W^d^9>~&%3ZvlUw=A1<⁢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

m)-ok@%rQU3hT(G-|FB%hxDCDu-s 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 literal 0 HcmV?d00001 From 0a84246f844e630e6d3b150a5796c45b1d27e403 Mon Sep 17 00:00:00 2001 From: Dhirender Choudhary Date: Tue, 1 Sep 2026 16:25:25 +0530 Subject: [PATCH 06/11] fix(sendgrid): align schemas and fail-closed webhook auth --- packages/sendgrid/api.test.ts | 85 +++++++++++-- packages/sendgrid/client.ts | 29 ++++- packages/sendgrid/endpoints.test.ts | 21 ++-- packages/sendgrid/endpoints/contacts.ts | 4 +- packages/sendgrid/endpoints/lists.ts | 14 ++- packages/sendgrid/endpoints/mail.ts | 23 ++-- packages/sendgrid/endpoints/senders.ts | 8 +- packages/sendgrid/endpoints/suppressions.ts | 12 +- packages/sendgrid/endpoints/types.ts | 76 ++++++------ packages/sendgrid/error-handlers.ts | 56 +++++++-- packages/sendgrid/index.ts | 18 ++- packages/sendgrid/integration.test.ts | 12 ++ packages/sendgrid/schema.test.ts | 80 +++++++++++-- packages/sendgrid/schema/database.ts | 112 +++++++++++++++++- packages/sendgrid/schema/index.ts | 24 +++- packages/sendgrid/webhooks/events.ts | 6 +- .../sendgrid/webhooks/oauth-tenant-link.ts | 13 -- packages/sendgrid/webhooks/types.test.ts | 12 ++ packages/sendgrid/webhooks/types.ts | 18 +-- 19 files changed, 486 insertions(+), 137 deletions(-) delete mode 100644 packages/sendgrid/webhooks/oauth-tenant-link.ts diff --git a/packages/sendgrid/api.test.ts b/packages/sendgrid/api.test.ts index 240c317d8..34e94b244 100644 --- a/packages/sendgrid/api.test.ts +++ b/packages/sendgrid/api.test.ts @@ -1,12 +1,17 @@ -import { ApiError } from 'corsair/http'; -import { makeSendGridRequest } from './client'; +import { makeSendGridRequest, SendGridAPIError } from './client'; import { Contacts, Lists, Mail, Senders, Suppressions } from './endpoints'; +import { SendGridEndpointOutputSchemas } from './endpoints/types'; +import { errorHandlers } from './error-handlers'; + +const TEST_API_KEY = process.env.SENDGRID_API_KEY; +const describeLive = TEST_API_KEY ? describe : describe.skip; describe('SendGrid Endpoints Execution & Error Policies', () => { const mockCtx: any = { key: 'SG.test_api_key_123', authType: 'api_key', $getAccountId: async () => 'acc-123', + db: {}, database: {}, }; @@ -18,7 +23,11 @@ describe('SendGrid Endpoints Execution & Error Policies', () => { jest.resetAllMocks(); }); - function mockResponse(status: number, data: any) { + function mockResponse( + status: number, + data: unknown, + headers: Record = {}, + ) { const bodyText = typeof data === 'string' ? data : JSON.stringify(data); return { ok: status >= 200 && status < 300, @@ -26,7 +35,11 @@ describe('SendGrid Endpoints Execution & Error Policies', () => { statusText: status === 401 ? 'Unauthorized' : 'OK', headers: { get: (name: string) => { - if (name.toLowerCase() === 'content-type') return 'application/json'; + const key = name.toLowerCase(); + if (key === 'content-type') return 'application/json'; + for (const [header, value] of Object.entries(headers)) { + if (header.toLowerCase() === key) return value; + } return null; }, }, @@ -35,8 +48,10 @@ describe('SendGrid Endpoints Execution & Error Policies', () => { }; } - it('executes Mail.send endpoint with correct request formatting', async () => { - (global.fetch as jest.Mock).mockResolvedValueOnce(mockResponse(202, {})); + it('executes Mail.send and returns X-Message-Id', async () => { + (global.fetch as jest.Mock).mockResolvedValueOnce( + mockResponse(202, '', { 'X-Message-Id': 'msg-1.filter' }), + ); const res = await Mail.send(mockCtx, { personalizations: [{ to: [{ email: 'recipient@example.com' }] }], @@ -45,7 +60,7 @@ describe('SendGrid Endpoints Execution & Error Policies', () => { content: [{ type: 'text/plain', value: 'Hello' }], }); - expect(res.success).toBe(true); + expect(res.x_message_id).toBe('msg-1.filter'); expect(global.fetch).toHaveBeenCalledWith( 'https://api.sendgrid.com/v3/mail/send', expect.objectContaining({ @@ -74,8 +89,8 @@ describe('SendGrid Endpoints Execution & Error Policies', () => { ); const res = await Lists.getAll(mockCtx, { - pageSize: 20, - pageToken: 'token123', + page_size: 20, + page_token: 'token123', }); expect(res.result).toHaveLength(1); @@ -107,7 +122,10 @@ describe('SendGrid Endpoints Execution & Error Policies', () => { ]), ); - const res = await Suppressions.getBounces(mockCtx, {}); + const res = await Suppressions.getBounces(mockCtx, { + limit: 10, + offset: 0, + }); expect(res.bounces).toHaveLength(1); expect(res.bounces[0]!.email).toBe('b@example.com'); @@ -127,18 +145,59 @@ describe('SendGrid Endpoints Execution & Error Policies', () => { }), ); - const res = await Senders.getAll(mockCtx, {}); + const res = await Senders.getAll(mockCtx, { limit: 10 }); expect(res.results[0]!.verified).toBe(true); }); - it('preserves ApiError on HTTP error status (401)', async () => { + it('preserves HTTP status on SendGridAPIError (401)', async () => { (global.fetch as jest.Mock).mockResolvedValueOnce( mockResponse(401, { errors: [{ message: 'Unauthorized' }] }), ); await expect( makeSendGridRequest('mail/send', 'SG.key', { method: 'POST' }), - ).rejects.toThrow(ApiError); + ).rejects.toMatchObject({ + name: 'SendGridAPIError', + status: 401, + }); + }); + + it('classifies 429 as RATE_LIMIT_ERROR', async () => { + const error = new SendGridAPIError('Too Many Requests', undefined, 429); + expect(errorHandlers.RATE_LIMIT_ERROR.match(error)).toBe(true); + const policy = await errorHandlers.RATE_LIMIT_ERROR.handler(error); + expect(policy.maxRetries).toBe(3); + }); +}); + +describeLive('SendGrid live API', () => { + it('senders.getAll matches VerifiedSenderResponse', async () => { + const result = await makeSendGridRequest<{ results: unknown[] }>( + 'verified_senders', + TEST_API_KEY!, + ); + SendGridEndpointOutputSchemas.sendersGetAll.parse(result); + }); + + it('suppressions.getBounces returns bounce records', async () => { + const result = await makeSendGridRequest( + 'suppression/bounces', + TEST_API_KEY!, + { query: { limit: 1 } }, + ); + const bounces = Array.isArray(result) ? result : []; + SendGridEndpointOutputSchemas.suppressionsGetBounces.parse({ bounces }); + }); + + it('lists.getAll matches marketing lists payload', async () => { + const result = await makeSendGridRequest( + 'marketing/lists', + TEST_API_KEY!, + { + query: { page_size: 1 }, + }, + ); + SendGridEndpointOutputSchemas.listsGetAll.parse(result); }); }); diff --git a/packages/sendgrid/client.ts b/packages/sendgrid/client.ts index e98cb7112..d78878e8e 100644 --- a/packages/sendgrid/client.ts +++ b/packages/sendgrid/client.ts @@ -6,6 +6,7 @@ export class SendGridAPIError extends Error { message: string, public readonly code?: string, public readonly status?: number, + public readonly body?: unknown, ) { super(message); this.name = 'SendGridAPIError'; @@ -19,11 +20,12 @@ export async function makeSendGridRequest( apiKey: string, options: { method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; - body?: Record; + body?: unknown; query?: Record; + responseHeader?: string; } = {}, ): Promise { - const { method = 'GET', body, query } = options; + const { method = 'GET', body, query, responseHeader } = options; const cleanEndpoint = endpoint.startsWith('/') ? endpoint : `/${endpoint}`; const config: OpenAPIConfig = { @@ -34,7 +36,6 @@ export async function makeSendGridRequest( TOKEN: apiKey, HEADERS: { 'Content-Type': 'application/json', - Authorization: `Bearer ${apiKey}`, }, }; @@ -47,13 +48,33 @@ export async function makeSendGridRequest( : undefined, mediaType: 'application/json; charset=utf-8', query: method === 'GET' ? query : undefined, + responseHeader, }; try { return await request(config, requestOptions); } catch (error) { if (error instanceof ApiError) { - throw error; + const bodyObj = + typeof error.body === 'object' && error.body !== null + ? (error.body as Record) + : undefined; + const firstError = + Array.isArray(bodyObj?.errors) && + typeof bodyObj.errors[0] === 'object' && + bodyObj.errors[0] !== null + ? (bodyObj.errors[0] as Record) + : undefined; + const msg = + typeof firstError?.message === 'string' + ? firstError.message + : error.message; + throw new SendGridAPIError( + msg, + typeof firstError?.field === 'string' ? firstError.field : undefined, + error.status, + error.body, + ); } if (error instanceof Error) { throw new SendGridAPIError(error.message); diff --git a/packages/sendgrid/endpoints.test.ts b/packages/sendgrid/endpoints.test.ts index b6008c7fe..010617c35 100644 --- a/packages/sendgrid/endpoints.test.ts +++ b/packages/sendgrid/endpoints.test.ts @@ -22,10 +22,10 @@ describe('SendGrid Endpoint Schemas', () => { 'recipient@example.com', ); - const validOutput = { success: true }; + const validOutput = { x_message_id: 'filter0001' }; const parsedOutput = SendGridEndpointOutputSchemas.mailSend.parse(validOutput); - expect(parsedOutput.success).toBe(true); + expect(parsedOutput.x_message_id).toBe('filter0001'); }); it('validates contacts.addOrUpdate input and output schemas', () => { @@ -46,10 +46,10 @@ describe('SendGrid Endpoint Schemas', () => { }); it('validates lists.getAll input and output schemas', () => { - const validInput = { pageSize: 10, pageToken: 'token-abc' }; + const validInput = { page_size: 10, page_token: 'token-abc' }; const parsedInput = SendGridEndpointInputSchemas.listsGetAll.parse(validInput); - expect(parsedInput.pageSize).toBe(10); + expect(parsedInput.page_size).toBe(10); const validOutput = { result: [{ id: 'list-1', name: 'Main List', contact_count: 50 }], @@ -76,7 +76,12 @@ describe('SendGrid Endpoint Schemas', () => { }); it('validates suppressions.getBounces input and output schemas', () => { - const validInput = { start_time: 1600000000, end_time: 1700000000 }; + const validInput = { + start_time: 1600000000, + end_time: 1700000000, + limit: 50, + offset: 0, + }; const parsedInput = SendGridEndpointInputSchemas.suppressionsGetBounces.parse(validInput); expect(parsedInput.start_time).toBe(1600000000); @@ -97,10 +102,10 @@ describe('SendGrid Endpoint Schemas', () => { }); it('validates senders.getAll input and output schemas', () => { - const validInput = {}; + const validInput = { limit: 10 }; const parsedInput = SendGridEndpointInputSchemas.sendersGetAll.parse(validInput); - expect(parsedInput).toBeDefined(); + expect(parsedInput.limit).toBe(10); const validOutput = { results: [ @@ -108,7 +113,9 @@ describe('SendGrid Endpoint Schemas', () => { id: 1, nickname: 'Support', from_email: 'support@example.com', + from_name: 'Support', verified: true, + locked: false, }, ], }; diff --git a/packages/sendgrid/endpoints/contacts.ts b/packages/sendgrid/endpoints/contacts.ts index aaf2b018a..aa04cb3c9 100644 --- a/packages/sendgrid/endpoints/contacts.ts +++ b/packages/sendgrid/endpoints/contacts.ts @@ -11,7 +11,7 @@ export const addOrUpdate: SendGridEndpoints['contactsAddOrUpdate'] = async ( SendGridEndpointOutputs['contactsAddOrUpdate'] >('marketing/contacts', ctx.key, { method: 'PUT', - body: input as unknown as Record, + body: input, }); await logEventFromContext( @@ -19,7 +19,7 @@ export const addOrUpdate: SendGridEndpoints['contactsAddOrUpdate'] = async ( 'sendgrid.contacts.addOrUpdate', { contact_count: input.contacts.length, - list_ids: input.list_ids, + list_count: input.list_ids?.length ?? 0, }, 'completed', ); diff --git a/packages/sendgrid/endpoints/lists.ts b/packages/sendgrid/endpoints/lists.ts index 6022e23fa..71efa3734 100644 --- a/packages/sendgrid/endpoints/lists.ts +++ b/packages/sendgrid/endpoints/lists.ts @@ -5,8 +5,8 @@ import type { SendGridEndpointOutputs } from './types'; export const getAll: SendGridEndpoints['listsGetAll'] = async (ctx, input) => { const query: Record = {}; - if (input.pageSize) query.page_size = input.pageSize; - if (input.pageToken) query.page_token = input.pageToken; + if (input.page_size) query.page_size = input.page_size; + if (input.page_token) query.page_token = input.page_token; const response = await makeSendGridRequest< SendGridEndpointOutputs['listsGetAll'] @@ -18,7 +18,7 @@ export const getAll: SendGridEndpoints['listsGetAll'] = async (ctx, input) => { await logEventFromContext( ctx, 'sendgrid.lists.getAll', - { ...input }, + { result_count: response.result.length }, 'completed', ); return response; @@ -29,13 +29,17 @@ export const create: SendGridEndpoints['listsCreate'] = async (ctx, input) => { SendGridEndpointOutputs['listsCreate'] >('marketing/lists', ctx.key, { method: 'POST', - body: input as unknown as Record, + body: input, }); + if (ctx.db.lists && response.id) { + await ctx.db.lists.upsertByEntityId(response.id, response); + } + await logEventFromContext( ctx, 'sendgrid.lists.create', - { ...input }, + { list_id: response.id }, 'completed', ); return response; diff --git a/packages/sendgrid/endpoints/mail.ts b/packages/sendgrid/endpoints/mail.ts index 901b5618b..ae8447c50 100644 --- a/packages/sendgrid/endpoints/mail.ts +++ b/packages/sendgrid/endpoints/mail.ts @@ -3,14 +3,14 @@ import type { SendGridEndpoints } from '..'; import { makeSendGridRequest } from '../client'; export const send: SendGridEndpoints['mailSend'] = async (ctx, input) => { - await makeSendGridRequest('mail/send', ctx.key, { - method: 'POST', - body: input as unknown as Record, - }); - - const totalRecipients = input.personalizations.reduce( - (sum, p) => sum + p.to.length, - 0, + const xMessageId = await makeSendGridRequest( + 'mail/send', + ctx.key, + { + method: 'POST', + body: input, + responseHeader: 'X-Message-Id', + }, ); await logEventFromContext( @@ -18,11 +18,14 @@ export const send: SendGridEndpoints['mailSend'] = async (ctx, input) => { 'sendgrid.mail.send', { from_email: input.from.email, - recipient_count: totalRecipients, + recipient_count: input.personalizations.reduce( + (sum, p) => sum + p.to.length, + 0, + ), template_id: input.template_id, }, 'completed', ); - return { success: true }; + return { x_message_id: xMessageId }; }; diff --git a/packages/sendgrid/endpoints/senders.ts b/packages/sendgrid/endpoints/senders.ts index d5e2a33d6..75db73557 100644 --- a/packages/sendgrid/endpoints/senders.ts +++ b/packages/sendgrid/endpoints/senders.ts @@ -7,16 +7,22 @@ export const getAll: SendGridEndpoints['sendersGetAll'] = async ( ctx, input, ) => { + const query: Record = {}; + if (input.limit) query.limit = input.limit; + if (input.lastSeenID) query.lastSeenID = input.lastSeenID; + if (input.id) query.id = input.id; + const response = await makeSendGridRequest< SendGridEndpointOutputs['sendersGetAll'] >('verified_senders', ctx.key, { method: 'GET', + query, }); await logEventFromContext( ctx, 'sendgrid.senders.getAll', - { ...input }, + { result_count: response.results.length }, 'completed', ); return response; diff --git a/packages/sendgrid/endpoints/suppressions.ts b/packages/sendgrid/endpoints/suppressions.ts index a3e56327a..117be348f 100644 --- a/packages/sendgrid/endpoints/suppressions.ts +++ b/packages/sendgrid/endpoints/suppressions.ts @@ -1,7 +1,7 @@ import { logEventFromContext } from 'corsair/core'; import type { SendGridEndpoints } from '..'; import { makeSendGridRequest } from '../client'; -import type { SuppressionsGetBouncesOutput } from './types'; +import { SendGridBounce } from '../schema/database'; export const getBounces: SendGridEndpoints['suppressionsGetBounces'] = async ( ctx, @@ -10,6 +10,8 @@ export const getBounces: SendGridEndpoints['suppressionsGetBounces'] = async ( const query: Record = {}; if (input.start_time) query.start_time = input.start_time; if (input.end_time) query.end_time = input.end_time; + if (input.limit) query.limit = input.limit; + if (input.offset !== undefined) query.offset = input.offset; const response = await makeSendGridRequest( 'suppression/bounces', @@ -20,13 +22,15 @@ export const getBounces: SendGridEndpoints['suppressionsGetBounces'] = async ( }, ); - const bounces = Array.isArray(response) ? response : []; + const bounces = Array.isArray(response) + ? response.map((item) => SendGridBounce.parse(item)) + : []; await logEventFromContext( ctx, 'sendgrid.suppressions.getBounces', - { ...input }, + { bounce_count: bounces.length }, 'completed', ); - return { bounces } as SuppressionsGetBouncesOutput; + return { bounces }; }; diff --git a/packages/sendgrid/endpoints/types.ts b/packages/sendgrid/endpoints/types.ts index fdb23059e..1982624cf 100644 --- a/packages/sendgrid/endpoints/types.ts +++ b/packages/sendgrid/endpoints/types.ts @@ -1,4 +1,10 @@ import { z } from 'zod'; +import { + SendGridBounce, + SendGridContact, + SendGridList, + SendGridVerifiedSender, +} from '../schema/database'; const EmailRecipientSchema = z.object({ email: z.string().email(), @@ -10,7 +16,11 @@ const PersonalizationSchema = z.object({ cc: z.array(EmailRecipientSchema).optional(), bcc: z.array(EmailRecipientSchema).optional(), subject: z.string().optional(), + headers: z.record(z.string(), z.string()).optional(), + substitutions: z.record(z.string(), z.string()).optional(), dynamic_template_data: z.record(z.string(), z.unknown()).optional(), + custom_args: z.record(z.string(), z.string()).optional(), + send_at: z.number().int().optional(), }); const ContentSchema = z.object({ @@ -18,6 +28,11 @@ const ContentSchema = z.object({ value: z.string(), }); +/** + * Mail Send v3 request. + * Official: POST /v3/mail/send + * https://www.twilio.com/docs/sendgrid/api-reference/mail-send/mail-send + */ const MailSendInputSchema = z.object({ personalizations: z.array(PersonalizationSchema).min(1), from: EmailRecipientSchema, @@ -26,27 +41,28 @@ const MailSendInputSchema = z.object({ reply_to: EmailRecipientSchema.optional(), template_id: z.string().optional(), categories: z.array(z.string()).optional(), + send_at: z.number().int().optional(), + batch_id: z.string().optional(), + ip_pool_name: z.string().optional(), + asm: z + .object({ + group_id: z.number().int(), + groups_to_display: z.array(z.number().int()).optional(), + }) + .optional(), }); export type MailSendInput = z.infer; const MailSendOutputSchema = z.object({ - success: z.boolean(), - messageId: z.string().optional(), + x_message_id: z.string().optional(), }); export type MailSendOutput = z.infer; -const ContactInputSchema = z.object({ - email: z.string().email(), - first_name: z.string().optional(), - last_name: z.string().optional(), - custom_fields: z.record(z.string(), z.unknown()).optional(), -}); - const ContactsAddOrUpdateInputSchema = z.object({ list_ids: z.array(z.string()).optional(), - contacts: z.array(ContactInputSchema).min(1), + contacts: z.array(SendGridContact).min(1), }); export type ContactsAddOrUpdateInput = z.infer< @@ -62,20 +78,14 @@ export type ContactsAddOrUpdateOutput = z.infer< >; const ListsGetAllInputSchema = z.object({ - pageSize: z.number().int().positive().optional(), - pageToken: z.string().optional(), + page_size: z.number().int().positive().max(1000).optional(), + page_token: z.string().optional(), }); export type ListsGetAllInput = z.infer; -const ContactListSchema = z.object({ - id: z.string(), - name: z.string(), - contact_count: z.number(), -}); - const ListsGetAllOutputSchema = z.object({ - result: z.array(ContactListSchema), + result: z.array(SendGridList), _metadata: z.record(z.string(), z.unknown()).optional(), }); @@ -87,47 +97,39 @@ const ListsCreateInputSchema = z.object({ export type ListsCreateInput = z.infer; -const ListsCreateOutputSchema = ContactListSchema; +const ListsCreateOutputSchema = SendGridList; export type ListsCreateOutput = z.infer; const SuppressionsGetBouncesInputSchema = z.object({ start_time: z.number().int().optional(), end_time: z.number().int().optional(), + limit: z.number().int().positive().optional(), + offset: z.number().int().nonnegative().optional(), }); export type SuppressionsGetBouncesInput = z.infer< typeof SuppressionsGetBouncesInputSchema >; -const BounceItemSchema = z.object({ - created: z.number(), - email: z.string(), - reason: z.string(), - status: z.string(), -}); - const SuppressionsGetBouncesOutputSchema = z.object({ - bounces: z.array(BounceItemSchema), + bounces: z.array(SendGridBounce), }); export type SuppressionsGetBouncesOutput = z.infer< typeof SuppressionsGetBouncesOutputSchema >; -const SendersGetAllInputSchema = z.object({}); +const SendersGetAllInputSchema = z.object({ + limit: z.number().int().positive().optional(), + lastSeenID: z.number().int().optional(), + id: z.number().int().optional(), +}); export type SendersGetAllInput = z.infer; -const VerifiedSenderSchema = z.object({ - id: z.number(), - nickname: z.string(), - from_email: z.string(), - verified: z.boolean(), -}); - const SendersGetAllOutputSchema = z.object({ - results: z.array(VerifiedSenderSchema), + results: z.array(SendGridVerifiedSender), }); export type SendersGetAllOutput = z.infer; diff --git a/packages/sendgrid/error-handlers.ts b/packages/sendgrid/error-handlers.ts index 5a4f4c19f..df8261dcc 100644 --- a/packages/sendgrid/error-handlers.ts +++ b/packages/sendgrid/error-handlers.ts @@ -1,29 +1,63 @@ import type { CorsairErrorHandler } from 'corsair/core'; import { ApiError } from 'corsair/http'; +import { SendGridAPIError } from './client'; + +function statusOf(error: Error): number | undefined { + if (error instanceof ApiError) return error.status; + if (error instanceof SendGridAPIError) return error.status; + return undefined; +} export const errorHandlers = { RATE_LIMIT_ERROR: { - match: (error: Error) => { - if (error instanceof ApiError && error.status === 429) return true; + match: (error) => { + if (statusOf(error) === 429) return true; const msg = error.message.toLowerCase(); - return msg.includes('rate_limited') || msg.includes('429'); + return msg.includes('rate limit') || msg.includes('too many requests'); }, - handler: async (error: Error) => { - let retryAfterMs: number | undefined; - if (error instanceof ApiError && error.retryAfter !== undefined) { - retryAfterMs = error.retryAfter; + handler: async (error) => { + let retryAfter = 1; + if (error instanceof ApiError && error.retryAfter) { + retryAfter = error.retryAfter; } - return { maxRetries: 5, headersRetryAfterMs: retryAfterMs }; + return { maxRetries: 3, backoffMs: retryAfter * 1000 }; }, }, AUTH_ERROR: { - match: (error: Error) => { - if (error instanceof ApiError && error.status === 401) return true; + match: (error) => { + if (statusOf(error) === 401) return true; + const msg = error.message.toLowerCase(); + return msg.includes('unauthorized') || msg.includes('invalid api key'); + }, + handler: async () => ({ maxRetries: 0 }), + }, + PERMISSION_ERROR: { + match: (error) => { + if (statusOf(error) === 403) return true; const msg = error.message.toLowerCase(); - return msg.includes('unauthorized') || msg.includes('invalid_auth'); + return msg.includes('forbidden') || msg.includes('access forbidden'); }, handler: async () => ({ maxRetries: 0 }), }, + NOT_FOUND_ERROR: { + match: (error) => { + if (statusOf(error) === 404) return true; + return error.message.toLowerCase().includes('not found'); + }, + handler: async () => ({ maxRetries: 0 }), + }, + SERVER_ERROR: { + match: (error) => { + const status = statusOf(error); + if (status !== undefined && status >= 500) return true; + const msg = error.message.toLowerCase(); + return ( + msg.includes('internal server error') || + msg.includes('service unavailable') + ); + }, + handler: async () => ({ maxRetries: 2, backoffMs: 1000 }), + }, DEFAULT: { match: () => true, handler: async () => ({ maxRetries: 0 }), diff --git a/packages/sendgrid/index.ts b/packages/sendgrid/index.ts index e0fb150ec..08a98ee8f 100644 --- a/packages/sendgrid/index.ts +++ b/packages/sendgrid/index.ts @@ -15,6 +15,7 @@ import type { RequiredPluginEndpointSchemas, RequiredPluginWebhookSchemas, } from 'corsair/core'; +import { AuthMissingError } from 'corsair/core'; import { Contacts, Lists, Mail, Senders, Suppressions } from './endpoints'; import type { SendGridEndpointInputs, @@ -27,7 +28,6 @@ import { import { errorHandlers } from './error-handlers'; import { SendGridSchema } from './schema'; import { EventWebhooks } from './webhooks'; -import { resolveSendGridOAuthWebhookTenantLink } from './webhooks/oauth-tenant-link'; import { matchSendGridTenantWebhook } from './webhooks/tenant-matcher'; import type { SendGridEvent, SendGridWebhookOutputs } from './webhooks/types'; import { EmailEventWebhookSchema } from './webhooks/types'; @@ -218,7 +218,7 @@ export function sendgrid( return 'x-twilio-email-event-webhook-signature' in headers; }, pluginTenantWebhookMatcher: matchSendGridTenantWebhook, - oauthWebhookTenantLinkResolver: resolveSendGridOAuthWebhookTenantLink, + oauthWebhookTenantLinkResolver: () => null, errorHandlers: { ...errorHandlers, ...options.errorHandlers, @@ -230,7 +230,12 @@ export function sendgrid( if (source === 'webhook') { const res = await ctx.keys.get_webhook_signature(); - return res ?? ''; + if (!res) { + throw new Error( + '[auth-missing:sendgrid:webhook_signature]: SendGrid webhook verification key is missing', + ); + } + return res; } if (source === 'endpoint' && options.key) { @@ -239,10 +244,13 @@ export function sendgrid( if (source === 'endpoint' && ctx.authType === 'api_key') { const res = await ctx.keys.get_api_key(); - return res ?? ''; + if (!res) { + throw new AuthMissingError('sendgrid', 'api_key'); + } + return res; } - return ''; + throw new AuthMissingError('sendgrid', 'api_key'); }, } satisfies InternalSendGridPlugin; } diff --git a/packages/sendgrid/integration.test.ts b/packages/sendgrid/integration.test.ts index e77a9f5f8..94acad631 100644 --- a/packages/sendgrid/integration.test.ts +++ b/packages/sendgrid/integration.test.ts @@ -23,4 +23,16 @@ describe('SendGrid Plugin Integration', () => { expect(plugin.endpointMeta?.['lists.getAll']?.riskLevel).toBe('read'); expect(plugin.endpointMeta?.['senders.getAll']?.riskLevel).toBe('read'); }); + + it('matches signed SendGrid event webhook headers', () => { + const plugin = sendgrid({ key: 'SG.test_key' }); + expect( + plugin.pluginWebhookMatcher?.({ + body: '[]', + headers: { + 'x-twilio-email-event-webhook-signature': 'sig', + }, + }), + ).toBe(true); + }); }); diff --git a/packages/sendgrid/schema.test.ts b/packages/sendgrid/schema.test.ts index d700b55b2..64772a642 100644 --- a/packages/sendgrid/schema.test.ts +++ b/packages/sendgrid/schema.test.ts @@ -1,4 +1,11 @@ import { SendGridSchema } from './schema'; +import { + SendGridBounce, + SendGridContact, + SendGridEmailEvent, + SendGridList, + SendGridVerifiedSender, +} from './schema/database'; describe('SendGrid schema', () => { it('declares a semver version', () => { @@ -6,15 +13,68 @@ describe('SendGrid schema', () => { expect(SendGridSchema.version).toMatch(/^\d+\.\d+\.\d+$/); }); - it('declares an entities map', () => { - expect(typeof SendGridSchema.entities).toBe('object'); - expect(SendGridSchema.entities).not.toBeNull(); - expect(Array.isArray(Object.keys(SendGridSchema.entities))).toBe(true); - for (const entity of Object.values(SendGridSchema.entities)) { - expect(entity).toBeDefined(); - } + it('declares official entity maps', () => { + expect(Object.keys(SendGridSchema.entities).sort()).toEqual( + ['bounces', 'contacts', 'events', 'lists', 'senders'].sort(), + ); + }); + + it('parses official VerifiedSenderResponse example', () => { + const sender = SendGridVerifiedSender.parse({ + id: 1234, + nickname: 'Example Orders', + from_email: 'orders@example.com', + from_name: 'Example Orders', + reply_to: 'orders@example.com', + reply_to_name: 'Example Orders', + address: '1234 Fake St.', + address2: 'PO Box 1234', + state: 'CA', + city: 'San Francisco', + country: 'USA', + zip: '94105', + verified: true, + locked: false, + }); + expect(sender.from_email).toBe('orders@example.com'); + }); + + it('parses official bounce suppression record', () => { + const bounce = SendGridBounce.parse({ + created: 1251606766, + email: 'test@example.com', + reason: '500 unknown recipient', + status: '5.0.0', + }); + expect(bounce.email).toBe('test@example.com'); }); -}); -// Per .github/PLUGIN_PR_RULES.md (R2), every implemented endpoint -// needs a corresponding test. + it('parses official marketing list and contact request', () => { + const list = SendGridList.parse({ + id: 'e1', + name: 'Newsletter', + contact_count: 12, + }); + expect(list.contact_count).toBe(12); + + const contact = SendGridContact.parse({ + email: 'alex@example.com', + first_name: 'Alex', + last_name: 'Bloggs', + city: 'Port Douglas', + country: 'AU', + }); + expect(contact.email).toBe('alex@example.com'); + }); + + it('parses official event webhook object', () => { + const event = SendGridEmailEvent.parse({ + email: 'example@test.com', + timestamp: 1513299569, + event: 'delivered', + sg_event_id: 'sg_event_id', + sg_message_id: 'sg_message_id', + }); + expect(event.event).toBe('delivered'); + }); +}); diff --git a/packages/sendgrid/schema/database.ts b/packages/sendgrid/schema/database.ts index 4400b8920..59691b4c0 100644 --- a/packages/sendgrid/schema/database.ts +++ b/packages/sendgrid/schema/database.ts @@ -1 +1,111 @@ -// Database entity schemas for SendGrid plugin +import { z } from 'zod'; + +/** + * Marketing Campaigns contact (ContactRequest / stored contact fields). + * Official: PUT /v3/marketing/contacts + * https://www.twilio.com/docs/sendgrid/api-reference/contacts/add-or-update-a-contact + */ +export const SendGridContact = z + .object({ + id: z.string().optional(), + email: z.string().optional(), + phone_number_id: z.string().optional(), + external_id: z.string().optional(), + anonymous_id: z.string().optional(), + first_name: z.string().optional(), + last_name: z.string().optional(), + address_line_1: z.string().optional(), + address_line_2: z.string().optional(), + alternate_emails: z.array(z.string()).optional(), + city: z.string().optional(), + country: z.string().optional(), + postal_code: z.string().optional(), + state_province_region: z.string().optional(), + custom_fields: z + .record(z.string(), z.union([z.string(), z.number()])) + .optional(), + }) + .catchall(z.unknown()); + +export type SendGridContact = z.infer; + +/** + * Marketing Campaigns list. + * Official: GET/POST /v3/marketing/lists + * https://www.twilio.com/docs/sendgrid/api-reference/lists/get-all-lists + */ +export const SendGridList = z + .object({ + id: z.string(), + name: z.string(), + contact_count: z.number(), + _metadata: z.record(z.string(), z.unknown()).optional(), + }) + .catchall(z.unknown()); + +export type SendGridList = z.infer; + +/** + * Bounce suppression record. + * Official: GET /v3/suppression/bounces + * https://www.twilio.com/docs/sendgrid/api-reference/bounces-api/retrieve-all-bounces + */ +export const SendGridBounce = z + .object({ + created: z.number(), + email: z.string(), + reason: z.string(), + status: z.string(), + }) + .catchall(z.unknown()); + +export type SendGridBounce = z.infer; + +/** + * Verified sender identity. + * Official: GET /v3/verified_senders (VerifiedSenderResponse) + * https://www.twilio.com/docs/sendgrid/api-reference/sender-verification/get-all-verified-senders + */ +export const SendGridVerifiedSender = z + .object({ + id: z.number(), + nickname: z.string(), + from_email: z.string(), + from_name: z.string().optional(), + reply_to: z.string().optional(), + reply_to_name: z.string().optional(), + address: z.string().optional(), + address2: z.string().optional(), + state: z.string().optional(), + city: z.string().optional(), + zip: z.string().optional(), + country: z.string().optional(), + verified: z.boolean(), + locked: z.boolean().optional(), + }) + .catchall(z.unknown()); + +export type SendGridVerifiedSender = z.infer; + +/** + * Event Webhook delivery/engagement event. + * Official: Event Webhook payload object + * https://www.twilio.com/docs/sendgrid/for-developers/tracking-events/event + */ +export const SendGridEmailEvent = z + .object({ + email: z.string(), + event: z.string(), + timestamp: z.number(), + sg_event_id: z.string().optional(), + sg_message_id: z.string().optional(), + reason: z.string().optional(), + status: z.string().optional(), + response: z.string().optional(), + url: z.string().optional(), + category: z.union([z.string(), z.array(z.string())]).optional(), + tenant_external_id: z.string().optional(), + }) + .catchall(z.unknown()); + +export type SendGridEmailEvent = z.infer; diff --git a/packages/sendgrid/schema/index.ts b/packages/sendgrid/schema/index.ts index 118a2818c..20ddf9769 100644 --- a/packages/sendgrid/schema/index.ts +++ b/packages/sendgrid/schema/index.ts @@ -1,4 +1,26 @@ +import { + SendGridBounce, + SendGridContact, + SendGridEmailEvent, + SendGridList, + SendGridVerifiedSender, +} from './database'; + export const SendGridSchema = { version: '1.0.0', - entities: {}, + entities: { + contacts: SendGridContact, + lists: SendGridList, + bounces: SendGridBounce, + senders: SendGridVerifiedSender, + events: SendGridEmailEvent, + }, } as const; + +export { + SendGridBounce, + SendGridContact, + SendGridEmailEvent, + SendGridList, + SendGridVerifiedSender, +} from './database'; diff --git a/packages/sendgrid/webhooks/events.ts b/packages/sendgrid/webhooks/events.ts index ebd37b712..221d33a40 100644 --- a/packages/sendgrid/webhooks/events.ts +++ b/packages/sendgrid/webhooks/events.ts @@ -14,7 +14,11 @@ export const emailEvent: SendGridWebhooks['emailEvent'] = { }; } - const events = Array.isArray(request.payload) ? request.payload : []; + const events = Array.isArray(request.payload) + ? request.payload + : request.payload + ? [request.payload] + : []; return { success: true, data: { diff --git a/packages/sendgrid/webhooks/oauth-tenant-link.ts b/packages/sendgrid/webhooks/oauth-tenant-link.ts deleted file mode 100644 index 9d3999241..000000000 --- a/packages/sendgrid/webhooks/oauth-tenant-link.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { TokenResponse, WebhookTenantMatch } from 'corsair/core'; -import { toExternalId } from 'corsair/core'; - -export async function resolveSendGridOAuthWebhookTenantLink( - tokens: TokenResponse, -): Promise { - const externalId = toExternalId(tokens.tenant_external_id); - if (externalId) { - return { linkType: 'tenant_external_id', externalId }; - } - - return null; -} diff --git a/packages/sendgrid/webhooks/types.test.ts b/packages/sendgrid/webhooks/types.test.ts index 21b668b59..703be032f 100644 --- a/packages/sendgrid/webhooks/types.test.ts +++ b/packages/sendgrid/webhooks/types.test.ts @@ -87,4 +87,16 @@ describe('SendGrid Webhooks', () => { const result = verifySendGridWebhookSignature(webhookReq, 'some_key'); expect(result.valid).toBe(false); }); + + it('rejects unsigned payloads when the verification key is missing', () => { + const webhookReq: WebhookRequest = { + headers: {}, + payload: [ + { email: 'test@example.com', event: 'delivered', timestamp: 1 }, + ], + rawBody: '[]', + }; + const result = verifySendGridWebhookSignature(webhookReq, ''); + expect(result.valid).toBe(false); + }); }); diff --git a/packages/sendgrid/webhooks/types.ts b/packages/sendgrid/webhooks/types.ts index 337780bc7..503646fe5 100644 --- a/packages/sendgrid/webhooks/types.ts +++ b/packages/sendgrid/webhooks/types.ts @@ -5,18 +5,9 @@ import type { WebhookRequest, } from 'corsair/core'; import { z } from 'zod'; +import { SendGridEmailEvent } from '../schema/database'; -export const SendGridEventSchema = z.object({ - email: z.string(), - event: z.string(), - timestamp: z.number(), - sg_event_id: z.string().optional(), - sg_message_id: z.string().optional(), - reason: z.string().optional(), - status: z.string().optional(), - response: z.string().optional(), - tenant_external_id: z.string().optional(), -}); +export const SendGridEventSchema = SendGridEmailEvent; export type SendGridEvent = z.infer; @@ -66,7 +57,10 @@ export function verifySendGridWebhookSignature( secret: string, ): { valid: boolean; error?: string } { if (!secret) { - return { valid: true }; + return { + valid: false, + error: 'SendGrid webhook verification key is missing', + }; } const headers = request.headers; From 9458843b818f381564c2741c59852739511c2df4 Mon Sep 17 00:00:00 2001 From: Dhirender Choudhary Date: Tue, 1 Sep 2026 16:31:45 +0530 Subject: [PATCH 07/11] fix(sendgrid): reject stale webhooks and honor Retry-After --- packages/sendgrid/api.test.ts | 11 ++++++-- packages/sendgrid/client.ts | 2 ++ packages/sendgrid/endpoints/mail.ts | 3 +- packages/sendgrid/error-handlers.ts | 20 +++++++++---- packages/sendgrid/webhooks/events.ts | 7 +++++ packages/sendgrid/webhooks/types.test.ts | 36 ++++++++++++++++++++++-- packages/sendgrid/webhooks/types.ts | 15 ++++++++++ 7 files changed, 82 insertions(+), 12 deletions(-) diff --git a/packages/sendgrid/api.test.ts b/packages/sendgrid/api.test.ts index 34e94b244..68c19429c 100644 --- a/packages/sendgrid/api.test.ts +++ b/packages/sendgrid/api.test.ts @@ -163,11 +163,18 @@ describe('SendGrid Endpoints Execution & Error Policies', () => { }); }); - it('classifies 429 as RATE_LIMIT_ERROR', async () => { - const error = new SendGridAPIError('Too Many Requests', undefined, 429); + it('classifies 429 as RATE_LIMIT_ERROR and honors Retry-After ms', async () => { + const error = new SendGridAPIError( + 'Too Many Requests', + undefined, + 429, + undefined, + 45000, + ); expect(errorHandlers.RATE_LIMIT_ERROR.match(error)).toBe(true); const policy = await errorHandlers.RATE_LIMIT_ERROR.handler(error); expect(policy.maxRetries).toBe(3); + expect(policy.headersRetryAfterMs).toBe(45000); }); }); diff --git a/packages/sendgrid/client.ts b/packages/sendgrid/client.ts index d78878e8e..eb2995af0 100644 --- a/packages/sendgrid/client.ts +++ b/packages/sendgrid/client.ts @@ -7,6 +7,7 @@ export class SendGridAPIError extends Error { public readonly code?: string, public readonly status?: number, public readonly body?: unknown, + public readonly retryAfter?: number, ) { super(message); this.name = 'SendGridAPIError'; @@ -74,6 +75,7 @@ export async function makeSendGridRequest( typeof firstError?.field === 'string' ? firstError.field : undefined, error.status, error.body, + error.retryAfter, ); } if (error instanceof Error) { diff --git a/packages/sendgrid/endpoints/mail.ts b/packages/sendgrid/endpoints/mail.ts index ae8447c50..0ddfb8ef9 100644 --- a/packages/sendgrid/endpoints/mail.ts +++ b/packages/sendgrid/endpoints/mail.ts @@ -17,12 +17,11 @@ export const send: SendGridEndpoints['mailSend'] = async (ctx, input) => { ctx, 'sendgrid.mail.send', { - from_email: input.from.email, recipient_count: input.personalizations.reduce( (sum, p) => sum + p.to.length, 0, ), - template_id: input.template_id, + has_template: Boolean(input.template_id), }, 'completed', ); diff --git a/packages/sendgrid/error-handlers.ts b/packages/sendgrid/error-handlers.ts index df8261dcc..02415133f 100644 --- a/packages/sendgrid/error-handlers.ts +++ b/packages/sendgrid/error-handlers.ts @@ -16,11 +16,16 @@ export const errorHandlers = { return msg.includes('rate limit') || msg.includes('too many requests'); }, handler: async (error) => { - let retryAfter = 1; - if (error instanceof ApiError && error.retryAfter) { - retryAfter = error.retryAfter; - } - return { maxRetries: 3, backoffMs: retryAfter * 1000 }; + const retryAfterMs = + (error instanceof ApiError ? error.retryAfter : undefined) ?? + (error instanceof SendGridAPIError ? error.retryAfter : undefined); + return { + maxRetries: 3, + headersRetryAfterMs: retryAfterMs, + retryStrategy: retryAfterMs + ? undefined + : ('exponential_backoff' as const), + }; }, }, AUTH_ERROR: { @@ -56,7 +61,10 @@ export const errorHandlers = { msg.includes('service unavailable') ); }, - handler: async () => ({ maxRetries: 2, backoffMs: 1000 }), + handler: async () => ({ + maxRetries: 2, + retryStrategy: 'exponential_backoff', + }), }, DEFAULT: { match: () => true, diff --git a/packages/sendgrid/webhooks/events.ts b/packages/sendgrid/webhooks/events.ts index 221d33a40..7d12721a7 100644 --- a/packages/sendgrid/webhooks/events.ts +++ b/packages/sendgrid/webhooks/events.ts @@ -5,6 +5,13 @@ export const emailEvent: SendGridWebhooks['emailEvent'] = { match: createSendGridMatch(), handler: async (ctx, request) => { const webhookSecret = ctx.key; + if (!webhookSecret) { + return { + success: false, + statusCode: 401, + error: 'SendGrid webhook verification key is missing', + }; + } const verification = verifySendGridWebhookSignature(request, webhookSecret); if (!verification.valid) { return { diff --git a/packages/sendgrid/webhooks/types.test.ts b/packages/sendgrid/webhooks/types.test.ts index 703be032f..a69ac599c 100644 --- a/packages/sendgrid/webhooks/types.test.ts +++ b/packages/sendgrid/webhooks/types.test.ts @@ -47,9 +47,13 @@ describe('SendGrid Webhooks', () => { privateKeyEncoding: { type: 'pkcs8', format: 'pem' }, }); - const timestamp = '1600000000'; + const timestamp = String(Math.floor(Date.now() / 1000)); const payload = [ - { email: 'test@example.com', event: 'delivered', timestamp: 1600000000 }, + { + email: 'test@example.com', + event: 'delivered', + timestamp: Number(timestamp), + }, ]; const rawBody = JSON.stringify(payload); const payloadToSign = timestamp + rawBody; @@ -74,6 +78,34 @@ describe('SendGrid Webhooks', () => { expect(result.valid).toBe(true); }); + it('rejects stale signed timestamps', () => { + const keyPair = crypto.generateKeyPairSync('ec', { + namedCurve: 'prime256v1', + publicKeyEncoding: { type: 'spki', format: 'pem' }, + privateKeyEncoding: { type: 'pkcs8', format: 'pem' }, + }); + + const timestamp = '1600000000'; + const rawBody = '[]'; + const signer = crypto.createSign('SHA256'); + signer.update(timestamp + rawBody); + const signature = signer.sign(keyPair.privateKey, 'base64'); + + const result = verifySendGridWebhookSignature( + { + headers: { + 'x-twilio-email-event-webhook-signature': signature, + 'x-twilio-email-event-webhook-timestamp': timestamp, + }, + payload: [], + rawBody, + } as WebhookRequest, + keyPair.publicKey, + ); + expect(result.valid).toBe(false); + expect(result.error).toMatch(/stale/); + }); + it('rejects invalid signature when secret is provided', () => { const webhookReq: WebhookRequest = { headers: { diff --git a/packages/sendgrid/webhooks/types.ts b/packages/sendgrid/webhooks/types.ts index 503646fe5..7fdf4382d 100644 --- a/packages/sendgrid/webhooks/types.ts +++ b/packages/sendgrid/webhooks/types.ts @@ -52,6 +52,8 @@ export function createSendGridMatch(eventType?: string): CorsairWebhookMatcher { }; } +export const SENDGRID_WEBHOOK_MAX_AGE_SECONDS = 300; + export function verifySendGridWebhookSignature( request: WebhookRequest, secret: string, @@ -78,6 +80,19 @@ export function verifySendGridWebhookSignature( }; } + const timestampSeconds = Number(timestamp); + if (!Number.isFinite(timestampSeconds)) { + return { valid: false, error: 'Invalid SendGrid webhook timestamp' }; + } + + const ageSeconds = Math.abs(Date.now() / 1000 - timestampSeconds); + if (ageSeconds > SENDGRID_WEBHOOK_MAX_AGE_SECONDS) { + return { + valid: false, + error: 'SendGrid webhook timestamp is stale', + }; + } + try { const rawBody = typeof request.rawBody === 'string' From a15012c5abf245c3db75d97546d786ef497a94b5 Mon Sep 17 00:00:00 2001 From: Dhirender Choudhary Date: Tue, 1 Sep 2026 16:34:03 +0530 Subject: [PATCH 08/11] fix(sendgrid): drop webhooks, ops-only plugin --- packages/sendgrid/index.ts | 70 +--------- packages/sendgrid/integration.test.ts | 18 +-- packages/sendgrid/schema.test.ts | 14 +- packages/sendgrid/schema/database.ts | 23 ---- packages/sendgrid/schema/index.ts | 3 - packages/sendgrid/webhooks/events.ts | 36 ----- packages/sendgrid/webhooks/index.ts | 5 - packages/sendgrid/webhooks/tenant-matcher.ts | 53 -------- packages/sendgrid/webhooks/types.test.ts | 134 ------------------- packages/sendgrid/webhooks/types.ts | 123 ----------------- 10 files changed, 12 insertions(+), 467 deletions(-) delete mode 100644 packages/sendgrid/webhooks/events.ts delete mode 100644 packages/sendgrid/webhooks/index.ts delete mode 100644 packages/sendgrid/webhooks/tenant-matcher.ts delete mode 100644 packages/sendgrid/webhooks/types.test.ts delete mode 100644 packages/sendgrid/webhooks/types.ts diff --git a/packages/sendgrid/index.ts b/packages/sendgrid/index.ts index 08a98ee8f..6ca68d143 100644 --- a/packages/sendgrid/index.ts +++ b/packages/sendgrid/index.ts @@ -1,19 +1,16 @@ import type { AuthTypes, BindEndpoints, - BindWebhooks, CorsairEndpoint, CorsairErrorHandler, CorsairPlugin, CorsairPluginContext, - CorsairWebhook, KeyBuilderContext, PickAuth, PluginAuthConfig, PluginPermissionsConfig, RequiredPluginEndpointMeta, RequiredPluginEndpointSchemas, - RequiredPluginWebhookSchemas, } from 'corsair/core'; import { AuthMissingError } from 'corsair/core'; import { Contacts, Lists, Mail, Senders, Suppressions } from './endpoints'; @@ -27,17 +24,11 @@ import { } from './endpoints/types'; import { errorHandlers } from './error-handlers'; import { SendGridSchema } from './schema'; -import { EventWebhooks } from './webhooks'; -import { matchSendGridTenantWebhook } from './webhooks/tenant-matcher'; -import type { SendGridEvent, SendGridWebhookOutputs } from './webhooks/types'; -import { EmailEventWebhookSchema } from './webhooks/types'; export type SendGridPluginOptions = { authType?: PickAuth<'api_key'>; key?: string; - webhookSecret?: string; hooks?: InternalSendGridPlugin['hooks']; - webhookHooks?: InternalSendGridPlugin['webhookHooks']; errorHandlers?: CorsairErrorHandler; permissions?: PluginPermissionsConfig; }; @@ -70,17 +61,6 @@ export type SendGridEndpoints = { sendersGetAll: SendGridEndpoint<'sendersGetAll'>; }; -type SendGridWebhook< - K extends keyof SendGridWebhookOutputs, - TEvent, -> = CorsairWebhook; - -export type SendGridWebhooks = { - emailEvent: SendGridWebhook<'emailEvent', SendGridEvent>; -}; - -export type SendGridBoundWebhooks = BindWebhooks; - const sendGridEndpointsNested = { mail: { send: Mail.send, @@ -100,12 +80,6 @@ const sendGridEndpointsNested = { }, } as const; -const sendGridWebhooksNested = { - events: { - emailEvent: EventWebhooks.emailEvent, - }, -} as const; - export const sendGridEndpointSchemas = { 'mail.send': { input: SendGridEndpointInputSchemas.mailSend, @@ -135,16 +109,6 @@ export const sendGridEndpointSchemas = { typeof sendGridEndpointsNested >; -const sendGridWebhookSchemas = { - 'events.emailEvent': { - description: 'SendGrid email event webhook notification', - payload: EmailEventWebhookSchema, - response: EmailEventWebhookSchema, - }, -} as const satisfies RequiredPluginWebhookSchemas< - typeof sendGridWebhooksNested ->; - const defaultAuthType: AuthTypes = 'api_key' as const; const sendGridEndpointMeta = { @@ -176,7 +140,7 @@ const sendGridEndpointMeta = { export const sendGridAuthConfig = { api_key: { - account: ['tenant_external_id'] as const, + account: ['one'] as const, }, } as const satisfies PluginAuthConfig; @@ -184,7 +148,7 @@ export type BaseSendGridPlugin = CorsairPlugin< 'sendgrid', typeof SendGridSchema, typeof sendGridEndpointsNested, - typeof sendGridWebhooksNested, + {}, T, typeof defaultAuthType >; @@ -207,37 +171,17 @@ export function sendgrid( schema: SendGridSchema, options: options, hooks: options.hooks, - webhookHooks: options.webhookHooks, + webhookHooks: undefined, endpoints: sendGridEndpointsNested, - webhooks: sendGridWebhooksNested, + webhooks: {}, endpointMeta: sendGridEndpointMeta, endpointSchemas: sendGridEndpointSchemas, - webhookSchemas: sendGridWebhookSchemas, - pluginWebhookMatcher: (request) => { - const headers = request.headers; - return 'x-twilio-email-event-webhook-signature' in headers; - }, - pluginTenantWebhookMatcher: matchSendGridTenantWebhook, - oauthWebhookTenantLinkResolver: () => null, + pluginWebhookMatcher: undefined, errorHandlers: { ...errorHandlers, ...options.errorHandlers, }, keyBuilder: async (ctx: SendGridKeyBuilderContext, source) => { - if (source === 'webhook' && options.webhookSecret) { - return options.webhookSecret; - } - - if (source === 'webhook') { - const res = await ctx.keys.get_webhook_signature(); - if (!res) { - throw new Error( - '[auth-missing:sendgrid:webhook_signature]: SendGrid webhook verification key is missing', - ); - } - return res; - } - if (source === 'endpoint' && options.key) { return options.key; } @@ -271,7 +215,3 @@ export type { SuppressionsGetBouncesInput, SuppressionsGetBouncesOutput, } from './endpoints/types'; -export type { - SendGridEvent, - SendGridWebhookOutputs, -} from './webhooks/types'; diff --git a/packages/sendgrid/integration.test.ts b/packages/sendgrid/integration.test.ts index 94acad631..78a2cd3c0 100644 --- a/packages/sendgrid/integration.test.ts +++ b/packages/sendgrid/integration.test.ts @@ -17,22 +17,16 @@ describe('SendGrid Plugin Integration', () => { expect(plugin.endpoints?.senders?.getAll).toBeDefined(); }); + it('registers no webhooks', () => { + const plugin = sendgrid({ key: 'SG.test_key' }); + expect(plugin.webhooks).toEqual({}); + expect(plugin.pluginWebhookMatcher).toBeUndefined(); + }); + it('registers endpoint metadata correctly', () => { const plugin = sendgrid({ key: 'SG.test_key' }); expect(plugin.endpointMeta?.['mail.send']?.riskLevel).toBe('write'); expect(plugin.endpointMeta?.['lists.getAll']?.riskLevel).toBe('read'); expect(plugin.endpointMeta?.['senders.getAll']?.riskLevel).toBe('read'); }); - - it('matches signed SendGrid event webhook headers', () => { - const plugin = sendgrid({ key: 'SG.test_key' }); - expect( - plugin.pluginWebhookMatcher?.({ - body: '[]', - headers: { - 'x-twilio-email-event-webhook-signature': 'sig', - }, - }), - ).toBe(true); - }); }); diff --git a/packages/sendgrid/schema.test.ts b/packages/sendgrid/schema.test.ts index 64772a642..d5d916a7c 100644 --- a/packages/sendgrid/schema.test.ts +++ b/packages/sendgrid/schema.test.ts @@ -2,7 +2,6 @@ import { SendGridSchema } from './schema'; import { SendGridBounce, SendGridContact, - SendGridEmailEvent, SendGridList, SendGridVerifiedSender, } from './schema/database'; @@ -15,7 +14,7 @@ describe('SendGrid schema', () => { it('declares official entity maps', () => { expect(Object.keys(SendGridSchema.entities).sort()).toEqual( - ['bounces', 'contacts', 'events', 'lists', 'senders'].sort(), + ['bounces', 'contacts', 'lists', 'senders'].sort(), ); }); @@ -66,15 +65,4 @@ describe('SendGrid schema', () => { }); expect(contact.email).toBe('alex@example.com'); }); - - it('parses official event webhook object', () => { - const event = SendGridEmailEvent.parse({ - email: 'example@test.com', - timestamp: 1513299569, - event: 'delivered', - sg_event_id: 'sg_event_id', - sg_message_id: 'sg_message_id', - }); - expect(event.event).toBe('delivered'); - }); }); diff --git a/packages/sendgrid/schema/database.ts b/packages/sendgrid/schema/database.ts index 59691b4c0..f9ef2a557 100644 --- a/packages/sendgrid/schema/database.ts +++ b/packages/sendgrid/schema/database.ts @@ -86,26 +86,3 @@ export const SendGridVerifiedSender = z .catchall(z.unknown()); export type SendGridVerifiedSender = z.infer; - -/** - * Event Webhook delivery/engagement event. - * Official: Event Webhook payload object - * https://www.twilio.com/docs/sendgrid/for-developers/tracking-events/event - */ -export const SendGridEmailEvent = z - .object({ - email: z.string(), - event: z.string(), - timestamp: z.number(), - sg_event_id: z.string().optional(), - sg_message_id: z.string().optional(), - reason: z.string().optional(), - status: z.string().optional(), - response: z.string().optional(), - url: z.string().optional(), - category: z.union([z.string(), z.array(z.string())]).optional(), - tenant_external_id: z.string().optional(), - }) - .catchall(z.unknown()); - -export type SendGridEmailEvent = z.infer; diff --git a/packages/sendgrid/schema/index.ts b/packages/sendgrid/schema/index.ts index 20ddf9769..74c6a7ae0 100644 --- a/packages/sendgrid/schema/index.ts +++ b/packages/sendgrid/schema/index.ts @@ -1,7 +1,6 @@ import { SendGridBounce, SendGridContact, - SendGridEmailEvent, SendGridList, SendGridVerifiedSender, } from './database'; @@ -13,14 +12,12 @@ export const SendGridSchema = { lists: SendGridList, bounces: SendGridBounce, senders: SendGridVerifiedSender, - events: SendGridEmailEvent, }, } as const; export { SendGridBounce, SendGridContact, - SendGridEmailEvent, SendGridList, SendGridVerifiedSender, } from './database'; diff --git a/packages/sendgrid/webhooks/events.ts b/packages/sendgrid/webhooks/events.ts deleted file mode 100644 index 7d12721a7..000000000 --- a/packages/sendgrid/webhooks/events.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { SendGridWebhooks } from '..'; -import { createSendGridMatch, verifySendGridWebhookSignature } from './types'; - -export const emailEvent: SendGridWebhooks['emailEvent'] = { - match: createSendGridMatch(), - handler: async (ctx, request) => { - const webhookSecret = ctx.key; - if (!webhookSecret) { - return { - success: false, - statusCode: 401, - error: 'SendGrid webhook verification key is missing', - }; - } - const verification = verifySendGridWebhookSignature(request, webhookSecret); - if (!verification.valid) { - return { - success: false, - statusCode: 401, - error: verification.error || 'Signature verification failed', - }; - } - - const events = Array.isArray(request.payload) - ? request.payload - : request.payload - ? [request.payload] - : []; - return { - success: true, - data: { - events, - }, - }; - }, -}; diff --git a/packages/sendgrid/webhooks/index.ts b/packages/sendgrid/webhooks/index.ts deleted file mode 100644 index b801efa8c..000000000 --- a/packages/sendgrid/webhooks/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -import * as events from './events'; - -export const EventWebhooks = { - emailEvent: events.emailEvent, -}; diff --git a/packages/sendgrid/webhooks/tenant-matcher.ts b/packages/sendgrid/webhooks/tenant-matcher.ts deleted file mode 100644 index 24579efc1..000000000 --- a/packages/sendgrid/webhooks/tenant-matcher.ts +++ /dev/null @@ -1,53 +0,0 @@ -import type { RawWebhookRequest, WebhookTenantMatch } from 'corsair/core'; -import { asRecord, firstString, readBodyRecord } from 'corsair/core'; - -export function matchSendGridTenantWebhook( - request: RawWebhookRequest, -): WebhookTenantMatch | null { - let externalId: string | undefined; - - if (typeof request.body === 'string') { - try { - const parsed = JSON.parse(request.body); - if ( - Array.isArray(parsed) && - parsed.length > 0 && - typeof parsed[0] === 'object' && - parsed[0] !== null - ) { - externalId = firstString([ - (parsed[0] as Record).tenant_external_id, - ]); - } else if (typeof parsed === 'object' && parsed !== null) { - externalId = firstString([ - (parsed as Record).tenant_external_id, - asRecord((parsed as Record).data) - ?.tenant_external_id, - ]); - } - } catch { - // ignore JSON parse failure - } - } else if ( - Array.isArray(request.body) && - request.body.length > 0 && - typeof request.body[0] === 'object' && - request.body[0] !== null - ) { - externalId = firstString([ - (request.body[0] as Record).tenant_external_id, - ]); - } else { - const body = readBodyRecord(request); - if (body) { - externalId = firstString([ - body.tenant_external_id, - asRecord(body.data)?.tenant_external_id, - ]); - } - } - - if (!externalId) return null; - - return { linkType: 'tenant_external_id', externalId }; -} diff --git a/packages/sendgrid/webhooks/types.test.ts b/packages/sendgrid/webhooks/types.test.ts deleted file mode 100644 index a69ac599c..000000000 --- a/packages/sendgrid/webhooks/types.test.ts +++ /dev/null @@ -1,134 +0,0 @@ -import crypto from 'node:crypto'; -import type { RawWebhookRequest, WebhookRequest } from 'corsair/core'; -import { matchSendGridTenantWebhook } from './tenant-matcher'; -import { createSendGridMatch, verifySendGridWebhookSignature } from './types'; - -describe('SendGrid Webhooks', () => { - it('matches any valid SendGrid email event payload array', () => { - const matcher = createSendGridMatch(); - const request: RawWebhookRequest = { - body: JSON.stringify([ - { email: 'test@example.com', event: 'bounce', timestamp: 123456789 }, - ]), - headers: {}, - }; - expect(matcher(request)).toBe(true); - }); - - it('extracts tenant_external_id from SendGrid array body in tenant matcher', () => { - const request: RawWebhookRequest = { - body: JSON.stringify([ - { - email: 'test@example.com', - event: 'delivered', - tenant_external_id: 'tenant-123', - }, - ]), - headers: {}, - }; - const match = matchSendGridTenantWebhook(request); - expect(match).not.toBeNull(); - expect(match?.externalId).toBe('tenant-123'); - }); - - it('returns null when tenant_external_id is not present', () => { - const request: RawWebhookRequest = { - body: JSON.stringify([{ email: 'test@example.com', event: 'delivered' }]), - headers: {}, - }; - const match = matchSendGridTenantWebhook(request); - expect(match).toBeNull(); - }); - - it('verifies signature when secret is provided', () => { - const keyPair = crypto.generateKeyPairSync('ec', { - namedCurve: 'prime256v1', - publicKeyEncoding: { type: 'spki', format: 'pem' }, - privateKeyEncoding: { type: 'pkcs8', format: 'pem' }, - }); - - const timestamp = String(Math.floor(Date.now() / 1000)); - const payload = [ - { - email: 'test@example.com', - event: 'delivered', - timestamp: Number(timestamp), - }, - ]; - const rawBody = JSON.stringify(payload); - const payloadToSign = timestamp + rawBody; - - const signer = crypto.createSign('SHA256'); - signer.update(payloadToSign); - const signature = signer.sign(keyPair.privateKey, 'base64'); - - const webhookReq: WebhookRequest = { - headers: { - 'x-twilio-email-event-webhook-signature': signature, - 'x-twilio-email-event-webhook-timestamp': timestamp, - }, - payload, - rawBody, - }; - - const result = verifySendGridWebhookSignature( - webhookReq, - keyPair.publicKey, - ); - expect(result.valid).toBe(true); - }); - - it('rejects stale signed timestamps', () => { - const keyPair = crypto.generateKeyPairSync('ec', { - namedCurve: 'prime256v1', - publicKeyEncoding: { type: 'spki', format: 'pem' }, - privateKeyEncoding: { type: 'pkcs8', format: 'pem' }, - }); - - const timestamp = '1600000000'; - const rawBody = '[]'; - const signer = crypto.createSign('SHA256'); - signer.update(timestamp + rawBody); - const signature = signer.sign(keyPair.privateKey, 'base64'); - - const result = verifySendGridWebhookSignature( - { - headers: { - 'x-twilio-email-event-webhook-signature': signature, - 'x-twilio-email-event-webhook-timestamp': timestamp, - }, - payload: [], - rawBody, - } as WebhookRequest, - keyPair.publicKey, - ); - expect(result.valid).toBe(false); - expect(result.error).toMatch(/stale/); - }); - - it('rejects invalid signature when secret is provided', () => { - const webhookReq: WebhookRequest = { - headers: { - 'x-twilio-email-event-webhook-signature': 'invalid_sig', - 'x-twilio-email-event-webhook-timestamp': '1600000000', - }, - payload: [], - rawBody: '[]', - }; - - const result = verifySendGridWebhookSignature(webhookReq, 'some_key'); - expect(result.valid).toBe(false); - }); - - it('rejects unsigned payloads when the verification key is missing', () => { - const webhookReq: WebhookRequest = { - headers: {}, - payload: [ - { email: 'test@example.com', event: 'delivered', timestamp: 1 }, - ], - rawBody: '[]', - }; - const result = verifySendGridWebhookSignature(webhookReq, ''); - expect(result.valid).toBe(false); - }); -}); diff --git a/packages/sendgrid/webhooks/types.ts b/packages/sendgrid/webhooks/types.ts deleted file mode 100644 index 7fdf4382d..000000000 --- a/packages/sendgrid/webhooks/types.ts +++ /dev/null @@ -1,123 +0,0 @@ -import crypto from 'node:crypto'; -import type { - CorsairWebhookMatcher, - RawWebhookRequest, - WebhookRequest, -} from 'corsair/core'; -import { z } from 'zod'; -import { SendGridEmailEvent } from '../schema/database'; - -export const SendGridEventSchema = SendGridEmailEvent; - -export type SendGridEvent = z.infer; - -export const EmailEventWebhookSchema = z.object({ - events: z.array(SendGridEventSchema), -}); - -export type EmailEventWebhookOutput = z.infer; - -export type SendGridWebhookOutputs = { - emailEvent: EmailEventWebhookOutput; -}; - -function parseBody(body: unknown): unknown { - if (typeof body === 'string') { - try { - return JSON.parse(body); - } catch { - return null; - } - } - return body; -} - -export function createSendGridMatch(eventType?: string): CorsairWebhookMatcher { - return (request: RawWebhookRequest) => { - const parsed = parseBody(request.body); - if (Array.isArray(parsed) && parsed.length > 0) { - if (!eventType) return true; - return parsed.some( - (item) => - typeof item === 'object' && - item !== null && - (item as Record).event === eventType, - ); - } - if (typeof parsed === 'object' && parsed !== null) { - if (!eventType) return true; - return (parsed as Record).event === eventType; - } - return false; - }; -} - -export const SENDGRID_WEBHOOK_MAX_AGE_SECONDS = 300; - -export function verifySendGridWebhookSignature( - request: WebhookRequest, - secret: string, -): { valid: boolean; error?: string } { - if (!secret) { - return { - valid: false, - error: 'SendGrid webhook verification key is missing', - }; - } - - const headers = request.headers; - const signature = - (headers['x-twilio-email-event-webhook-signature'] as string | undefined) || - (headers['X-Twilio-Email-Event-Webhook-Signature'] as string | undefined); - const timestamp = - (headers['x-twilio-email-event-webhook-timestamp'] as string | undefined) || - (headers['X-Twilio-Email-Event-Webhook-Timestamp'] as string | undefined); - - if (!signature || !timestamp) { - return { - valid: false, - error: 'Missing SendGrid signature or timestamp header', - }; - } - - const timestampSeconds = Number(timestamp); - if (!Number.isFinite(timestampSeconds)) { - return { valid: false, error: 'Invalid SendGrid webhook timestamp' }; - } - - const ageSeconds = Math.abs(Date.now() / 1000 - timestampSeconds); - if (ageSeconds > SENDGRID_WEBHOOK_MAX_AGE_SECONDS) { - return { - valid: false, - error: 'SendGrid webhook timestamp is stale', - }; - } - - try { - const rawBody = - typeof request.rawBody === 'string' - ? request.rawBody - : JSON.stringify(request.payload); - const payloadToVerify = timestamp + rawBody; - - const verifier = crypto.createVerify('SHA256'); - verifier.update(payloadToVerify); - - const formattedKey = secret.startsWith('-----BEGIN') - ? secret - : `-----BEGIN PUBLIC KEY-----\n${secret}\n-----END PUBLIC KEY-----`; - - const isValid = verifier.verify(formattedKey, signature, 'base64'); - if (!isValid) { - return { valid: false, error: 'Invalid SendGrid webhook signature' }; - } - - return { valid: true }; - } catch (err) { - return { - valid: false, - error: - err instanceof Error ? err.message : 'Signature verification failed', - }; - } -} From 67841cd210ca33a0b0f809c1ccff419306c8698f Mon Sep 17 00:00:00 2001 From: Dhirender Choudhary Date: Tue, 1 Sep 2026 16:48:02 +0530 Subject: [PATCH 09/11] feat(sendgrid): add 100 official v3 REST endpoints --- packages/sendgrid/api.test.ts | 168 ++- packages/sendgrid/client.ts | 10 +- packages/sendgrid/endpoints.test.ts | 26 + packages/sendgrid/endpoints/bind.ts | 73 ++ packages/sendgrid/endpoints/catalog.ts | 1159 +++++++++++++++++++ packages/sendgrid/endpoints/contacts.ts | 28 - packages/sendgrid/endpoints/handlers.ts | 152 +++ packages/sendgrid/endpoints/index.ts | 19 +- packages/sendgrid/endpoints/lists.ts | 46 - packages/sendgrid/endpoints/mail.ts | 30 - packages/sendgrid/endpoints/run.ts | 17 + packages/sendgrid/endpoints/senders.ts | 29 - packages/sendgrid/endpoints/suppressions.ts | 36 - packages/sendgrid/endpoints/types.ts | 627 ++++++++-- packages/sendgrid/index.ts | 126 +- packages/sendgrid/integration.test.ts | 3 + packages/sendgrid/package.json | 2 +- packages/sendgrid/schema/database.ts | 2 +- 18 files changed, 2154 insertions(+), 399 deletions(-) create mode 100644 packages/sendgrid/endpoints/bind.ts create mode 100644 packages/sendgrid/endpoints/catalog.ts delete mode 100644 packages/sendgrid/endpoints/contacts.ts create mode 100644 packages/sendgrid/endpoints/handlers.ts delete mode 100644 packages/sendgrid/endpoints/lists.ts delete mode 100644 packages/sendgrid/endpoints/mail.ts create mode 100644 packages/sendgrid/endpoints/run.ts delete mode 100644 packages/sendgrid/endpoints/senders.ts delete mode 100644 packages/sendgrid/endpoints/suppressions.ts diff --git a/packages/sendgrid/api.test.ts b/packages/sendgrid/api.test.ts index 68c19429c..a4527756e 100644 --- a/packages/sendgrid/api.test.ts +++ b/packages/sendgrid/api.test.ts @@ -1,13 +1,83 @@ import { makeSendGridRequest, SendGridAPIError } from './client'; -import { Contacts, Lists, Mail, Senders, Suppressions } from './endpoints'; -import { SendGridEndpointOutputSchemas } from './endpoints/types'; +import { contacts, lists, mail, senders, suppressions } from './endpoints'; +import { runCatalogOp } from './endpoints/bind'; +import { SENDGRID_OPS } from './endpoints/catalog'; +import { + SendGridEndpointInputSchemas, + SendGridEndpointOutputSchemas, +} from './endpoints/types'; import { errorHandlers } from './error-handlers'; const TEST_API_KEY = process.env.SENDGRID_API_KEY; const describeLive = TEST_API_KEY ? describe : describe.skip; +const fixtures: Record> = { + empty: {}, + mailSend: { + personalizations: [{ to: [{ email: 'recipient@example.com' }] }], + from: { email: 'sender@example.com' }, + subject: 'Test Email', + content: [{ type: 'text/plain', value: 'Hello' }], + }, + batchId: { batch_id: 'batch-1' }, + scheduledSend: { batch_id: 'batch-1', status: 'pause' }, + scheduledSendUpdate: { batch_id: 'batch-1', status: 'cancel' }, + contactsAddOrUpdate: { + contacts: [{ email: 'user@example.com', first_name: 'Jane' }], + }, + id: { id: 'abc' }, + contactSearch: { query: "email LIKE '%@example.com'" }, + contactSearchEmails: { emails: ['user@example.com'] }, + idsQuery: { ids: 'id-1,id-2' }, + page: { page_size: 20, page_token: 'token123' }, + contactImport: { file_type: 'csv', field_mappings: ['email'] }, + contactExport: { file_type: 'csv' }, + listsCreate: { name: 'List 2' }, + idSample: { id: 'list-1', contact_sample: true }, + idName: { id: 'list-1', name: 'Renamed' }, + idDeleteContacts: { id: 'list-1', delete_contacts: false }, + idContactIds: { id: 'list-1', contact_ids: 'c1,c2' }, + segment: { name: 'Active', query_dsl: '{}' }, + segmentsList: {}, + idContactsSample: { id: 'seg-1' }, + segmentUpdate: { id: 'seg-1', name: 'Updated' }, + segmentRefresh: { id: 'seg-1', user_time_zone: 'America/Chicago' }, + fieldCreate: { name: 'plan', field_type: 'Text' }, + sendersGetAll: { limit: 10 }, + verifiedSender: { + nickname: 'Primary', + from_email: 'from@example.com', + }, + verifiedSenderUpdate: { id: 10, nickname: 'Updated' }, + idNum: { id: 10 }, + senderIdentity: { nickname: 'Marketing' }, + templateCreate: { name: 'Welcome' }, + templatesList: { generations: 'dynamic' }, + templateVersion: { id: 'tmpl-1', name: 'v1', subject: 'Hi' }, + templateVersionId: { id: 'tmpl-1', version_id: 'ver-1' }, + templateVersionUpdate: { + id: 'tmpl-1', + version_id: 'ver-1', + subject: 'Hello', + }, + suppressionList: { limit: 10, offset: 0 }, + email: { email: 'b@example.com' }, + deleteSuppressions: { emails: ['b@example.com'] }, + recipientEmails: { recipient_emails: ['b@example.com'] }, + asmIdQuery: {}, + asmGroup: { name: 'Alerts', description: 'Product alerts' }, + asmGroupUpdate: { id: 1, name: 'Alerts' }, + asmGroupEmails: { id: 1, recipient_emails: ['b@example.com'] }, + idNumEmail: { id: 1, email: 'b@example.com' }, + stats: { start_date: '2026-01-01' }, + statsCategories: { start_date: '2026-01-01', categories: 'welcome' }, + apiKeyCreate: { name: 'dev' }, + limit: { limit: 10 }, + apiKeyUpdate: { id: 'key-1', name: 'dev-2' }, +}; + describe('SendGrid Endpoints Execution & Error Policies', () => { - const mockCtx: any = { + const mockCtx: Record = { key: 'SG.test_api_key_123', authType: 'api_key', $getAccountId: async () => 'acc-123', @@ -48,25 +118,73 @@ describe('SendGrid Endpoints Execution & Error Policies', () => { }; } + it('covers 100 official v3 ops', () => { + expect(SENDGRID_OPS).toHaveLength(100); + expect(new Set(SENDGRID_OPS.map((op) => op.nested)).size).toBe(100); + }); + + it.each(SENDGRID_OPS)('$nested has schemas and a fixture', (op) => { + const inputSchema = + SendGridEndpointInputSchemas[ + op.key as keyof typeof SendGridEndpointInputSchemas + ]; + const outputSchema = + SendGridEndpointOutputSchemas[ + op.key as keyof typeof SendGridEndpointOutputSchemas + ]; + expect(inputSchema).toBeDefined(); + expect(outputSchema).toBeDefined(); + expect(fixtures[op.inKind]).toBeDefined(); + inputSchema.parse(fixtures[op.inKind]); + }); + + it.each(SENDGRID_OPS)('$nested $method /v3/$path', async (op) => { + (global.fetch as jest.Mock).mockResolvedValueOnce( + mockResponse( + 200, + op.wrapArray + ? [ + { + email: 'b@example.com', + created: 1, + reason: 'x', + status: '5.0.0', + }, + ] + : { ok: true }, + op.responseHeader ? { [op.responseHeader]: 'msg-1.filter' } : {}, + ), + ); + + await runCatalogOp(mockCtx as never, op, fixtures[op.inKind]!); + + const calledUrl = (global.fetch as jest.Mock).mock.calls[0][0] as string; + const called = (global.fetch as jest.Mock).mock.calls[0][1] as { + method: string; + }; + expect(called.method).toBe(op.method); + expect(calledUrl).toContain('https://api.sendgrid.com/v3/'); + expect(calledUrl).toContain(op.path.split('/{')[0]!); + }); + it('executes Mail.send and returns X-Message-Id', async () => { (global.fetch as jest.Mock).mockResolvedValueOnce( mockResponse(202, '', { 'X-Message-Id': 'msg-1.filter' }), ); - const res = await Mail.send(mockCtx, { - personalizations: [{ to: [{ email: 'recipient@example.com' }] }], - from: { email: 'sender@example.com' }, - subject: 'Test Email', - content: [{ type: 'text/plain', value: 'Hello' }], - }); + const res = await mail.send(mockCtx as never, fixtures.mailSend as never); expect(res.x_message_id).toBe('msg-1.filter'); - expect(global.fetch).toHaveBeenCalledWith( - 'https://api.sendgrid.com/v3/mail/send', - expect.objectContaining({ - method: 'POST', - }), - ); + const [, init] = (global.fetch as jest.Mock).mock.calls[0] as [ + string, + { method?: string; headers?: Headers | Record }, + ]; + expect(init.method).toBe('POST'); + const auth = + init.headers instanceof Headers + ? init.headers.get('Authorization') + : init.headers?.Authorization; + expect(auth).toBe('Bearer SG.test_api_key_123'); }); it('executes Contacts.addOrUpdate endpoint', async () => { @@ -74,9 +192,10 @@ describe('SendGrid Endpoints Execution & Error Policies', () => { mockResponse(202, { job_id: 'job-123' }), ); - const res = await Contacts.addOrUpdate(mockCtx, { - contacts: [{ email: 'user@example.com', first_name: 'Jane' }], - }); + const res = await contacts.addOrUpdate( + mockCtx as never, + fixtures.contactsAddOrUpdate as never, + ); expect(res.job_id).toBe('job-123'); }); @@ -88,7 +207,7 @@ describe('SendGrid Endpoints Execution & Error Policies', () => { }), ); - const res = await Lists.getAll(mockCtx, { + const res = await lists.getAll(mockCtx as never, { page_size: 20, page_token: 'token123', }); @@ -105,7 +224,7 @@ describe('SendGrid Endpoints Execution & Error Policies', () => { mockResponse(201, { id: 'l2', name: 'List 2', contact_count: 0 }), ); - const res = await Lists.create(mockCtx, { name: 'List 2' }); + const res = await lists.create(mockCtx as never, { name: 'List 2' }); expect(res.id).toBe('l2'); }); @@ -122,12 +241,17 @@ describe('SendGrid Endpoints Execution & Error Policies', () => { ]), ); - const res = await Suppressions.getBounces(mockCtx, { + const res = await suppressions.getBounces(mockCtx as never, { + start_time: 0, limit: 10, offset: 0, }); expect(res.bounces).toHaveLength(1); + expect(global.fetch).toHaveBeenCalledWith( + expect.stringContaining('start_time=0'), + expect.anything(), + ); expect(res.bounces[0]!.email).toBe('b@example.com'); }); @@ -145,7 +269,7 @@ describe('SendGrid Endpoints Execution & Error Policies', () => { }), ); - const res = await Senders.getAll(mockCtx, { limit: 10 }); + const res = await senders.getAll(mockCtx as never, { limit: 10 }); expect(res.results[0]!.verified).toBe(true); }); diff --git a/packages/sendgrid/client.ts b/packages/sendgrid/client.ts index eb2995af0..9eabfb010 100644 --- a/packages/sendgrid/client.ts +++ b/packages/sendgrid/client.ts @@ -44,11 +44,17 @@ export async function makeSendGridRequest( method, url: cleanEndpoint, body: - method === 'POST' || method === 'PUT' || method === 'PATCH' + method === 'POST' || + method === 'PUT' || + method === 'PATCH' || + method === 'DELETE' ? body : undefined, mediaType: 'application/json; charset=utf-8', - query: method === 'GET' ? query : undefined, + query: + method === 'GET' || method === 'DELETE' || method === 'PATCH' + ? query + : undefined, responseHeader, }; diff --git a/packages/sendgrid/endpoints.test.ts b/packages/sendgrid/endpoints.test.ts index 010617c35..723b44d8d 100644 --- a/packages/sendgrid/endpoints.test.ts +++ b/packages/sendgrid/endpoints.test.ts @@ -28,6 +28,24 @@ describe('SendGrid Endpoint Schemas', () => { expect(parsedOutput.x_message_id).toBe('filter0001'); }); + it('rejects mail.send without content or subject unless template_id is set', () => { + expect(() => + SendGridEndpointInputSchemas.mailSend.parse({ + personalizations: [{ to: [{ email: 'recipient@example.com' }] }], + from: { email: 'sender@example.com' }, + content: [], + }), + ).toThrow(); + + expect( + SendGridEndpointInputSchemas.mailSend.parse({ + personalizations: [{ to: [{ email: 'recipient@example.com' }] }], + from: { email: 'sender@example.com' }, + template_id: 'd-123', + }), + ).toMatchObject({ template_id: 'd-123' }); + }); + it('validates contacts.addOrUpdate input and output schemas', () => { const validInput = { contacts: [ @@ -45,6 +63,14 @@ describe('SendGrid Endpoint Schemas', () => { expect(parsedOutput.job_id).toBe('job-456'); }); + it('rejects contacts.addOrUpdate without an identifier', () => { + expect(() => + SendGridEndpointInputSchemas.contactsAddOrUpdate.parse({ + contacts: [{ first_name: 'Jane' }], + }), + ).toThrow(); + }); + it('validates lists.getAll input and output schemas', () => { const validInput = { page_size: 10, page_token: 'token-abc' }; const parsedInput = diff --git a/packages/sendgrid/endpoints/bind.ts b/packages/sendgrid/endpoints/bind.ts new file mode 100644 index 000000000..65677afe7 --- /dev/null +++ b/packages/sendgrid/endpoints/bind.ts @@ -0,0 +1,73 @@ +import { logEventFromContext } from 'corsair/core'; +import { makeSendGridRequest } from '../client'; +import type { OpSpec } from './catalog'; +import { enc, q } from './run'; + +type Ctx = Parameters[0] & { key: string }; + +export async function runCatalogOp( + ctx: Ctx, + spec: OpSpec, + input: Record, +): Promise { + let path = spec.path; + for (const key of spec.pathKeys ?? []) { + path = path.replaceAll(`{${key}}`, enc(String(input[key]))); + } + + const query = spec.queryKeys ? q(input, spec.queryKeys) : undefined; + + let body: unknown; + if (spec.body === true) { + body = input; + } else if (spec.body === 'omit') { + const next = { ...input }; + for (const key of [...(spec.pathKeys ?? []), ...(spec.queryKeys ?? [])]) { + delete next[key]; + } + body = next; + } + + const result = await makeSendGridRequest(path, ctx.key, { + method: spec.method, + body, + query, + responseHeader: spec.responseHeader, + }); + + let mapped: unknown = result; + if (spec.mapHeader === 'x_message_id') { + mapped = { + x_message_id: typeof result === 'string' ? result : undefined, + }; + } else if (spec.wrapArray) { + const items = Array.isArray(result) ? result : []; + mapped = { [spec.wrapArray]: items }; + } else if (result === undefined || result === null) { + mapped = {}; + } + + if (spec.upsertList) { + const row = mapped as { id?: string }; + const listsDb = ( + ctx as { + db?: { + lists?: { + upsertByEntityId: (id: string, data: never) => Promise; + }; + }; + } + ).db?.lists; + if (row.id && listsDb) { + await listsDb.upsertByEntityId(row.id, mapped as never); + } + } + + await logEventFromContext( + ctx, + `sendgrid.${spec.nested}`, + { method: spec.method }, + 'completed', + ); + return mapped; +} diff --git a/packages/sendgrid/endpoints/catalog.ts b/packages/sendgrid/endpoints/catalog.ts new file mode 100644 index 000000000..91cd58fea --- /dev/null +++ b/packages/sendgrid/endpoints/catalog.ts @@ -0,0 +1,1159 @@ +export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; + +export type OpSpec = { + key: string; + nested: string; + risk: 'read' | 'write'; + description: string; + method: HttpMethod; + path: string; + pathKeys?: string[]; + queryKeys?: string[]; + body?: true | 'omit'; + responseHeader?: string; + mapHeader?: 'x_message_id'; + wrapArray?: string; + upsertList?: boolean; + inKind: string; + outKind: string; +}; + +export const SENDGRID_OPS: OpSpec[] = [ + { + key: 'mailSend', + nested: 'mail.send', + risk: 'write', + description: 'Send an email via SendGrid Mail Send API v3', + method: 'POST', + path: 'mail/send', + body: true, + responseHeader: 'X-Message-Id', + mapHeader: 'x_message_id', + inKind: 'mailSend', + outKind: 'mailSend', + }, + { + key: 'mailCreateBatchId', + nested: 'mail.createBatchId', + risk: 'write', + description: 'Create a batch ID for scheduled mail', + method: 'POST', + path: 'mail/batch', + inKind: 'empty', + outKind: 'obj', + }, + { + key: 'mailValidateBatchId', + nested: 'mail.validateBatchId', + risk: 'read', + description: 'Validate a mail batch ID', + method: 'GET', + path: 'mail/batch/{batch_id}', + pathKeys: ['batch_id'], + inKind: 'batchId', + outKind: 'obj', + }, + { + key: 'mailCancelScheduledSend', + nested: 'mail.cancelScheduledSend', + risk: 'write', + description: 'Cancel or pause a scheduled send', + method: 'POST', + path: 'user/scheduled_sends', + body: true, + inKind: 'scheduledSend', + outKind: 'obj', + }, + { + key: 'mailListScheduledSends', + nested: 'mail.listScheduledSends', + risk: 'read', + description: 'Retrieve all scheduled sends', + method: 'GET', + path: 'user/scheduled_sends', + inKind: 'empty', + outKind: 'arr', + }, + { + key: 'mailGetScheduledSend', + nested: 'mail.getScheduledSend', + risk: 'read', + description: 'Retrieve a scheduled send by batch ID', + method: 'GET', + path: 'user/scheduled_sends/{batch_id}', + pathKeys: ['batch_id'], + inKind: 'batchId', + outKind: 'arr', + }, + { + key: 'mailUpdateScheduledSend', + nested: 'mail.updateScheduledSend', + risk: 'write', + description: 'Update a scheduled send status', + method: 'PATCH', + path: 'user/scheduled_sends/{batch_id}', + pathKeys: ['batch_id'], + body: 'omit', + inKind: 'scheduledSendUpdate', + outKind: 'empty', + }, + { + key: 'mailDeleteScheduledSend', + nested: 'mail.deleteScheduledSend', + risk: 'write', + description: 'Delete a cancellation/pause for a scheduled send', + method: 'DELETE', + path: 'user/scheduled_sends/{batch_id}', + pathKeys: ['batch_id'], + inKind: 'batchId', + outKind: 'empty', + }, + { + key: 'contactsAddOrUpdate', + nested: 'contacts.addOrUpdate', + risk: 'write', + description: 'Add or update contacts in SendGrid Marketing', + method: 'PUT', + path: 'marketing/contacts', + body: true, + inKind: 'contactsAddOrUpdate', + outKind: 'job', + }, + { + key: 'contactsGet', + nested: 'contacts.get', + risk: 'read', + description: 'Get a marketing contact by ID', + method: 'GET', + path: 'marketing/contacts/{id}', + pathKeys: ['id'], + inKind: 'id', + outKind: 'obj', + }, + { + key: 'contactsSearch', + nested: 'contacts.search', + risk: 'read', + description: 'Search marketing contacts with SGQL', + method: 'POST', + path: 'marketing/contacts/search', + body: true, + inKind: 'contactSearch', + outKind: 'obj', + }, + { + key: 'contactsSearchEmails', + nested: 'contacts.searchEmails', + risk: 'read', + description: 'Search marketing contacts by email', + method: 'POST', + path: 'marketing/contacts/search/emails', + body: true, + inKind: 'contactSearchEmails', + outKind: 'obj', + }, + { + key: 'contactsRemove', + nested: 'contacts.remove', + risk: 'write', + description: 'Delete marketing contacts by ID', + method: 'DELETE', + path: 'marketing/contacts', + queryKeys: ['ids'], + inKind: 'idsQuery', + outKind: 'job', + }, + { + key: 'contactsGetCount', + nested: 'contacts.getCount', + risk: 'read', + description: 'Get marketing contact count', + method: 'GET', + path: 'marketing/contacts/count', + inKind: 'empty', + outKind: 'obj', + }, + { + key: 'contactsGetSample', + nested: 'contacts.getSample', + risk: 'read', + description: 'Get a sample of marketing contacts', + method: 'GET', + path: 'marketing/contacts', + queryKeys: ['page_size', 'page_token'], + inKind: 'page', + outKind: 'obj', + }, + { + key: 'contactsImport', + nested: 'contacts.import', + risk: 'write', + description: 'Create a marketing contacts import job', + method: 'PUT', + path: 'marketing/contacts/imports', + body: true, + inKind: 'contactImport', + outKind: 'obj', + }, + { + key: 'contactsImportStatus', + nested: 'contacts.importStatus', + risk: 'read', + description: 'Get a marketing contacts import job', + method: 'GET', + path: 'marketing/contacts/imports/{id}', + pathKeys: ['id'], + inKind: 'id', + outKind: 'obj', + }, + { + key: 'contactsExport', + nested: 'contacts.export', + risk: 'write', + description: 'Create a marketing contacts export job', + method: 'POST', + path: 'marketing/contacts/exports', + body: true, + inKind: 'contactExport', + outKind: 'obj', + }, + { + key: 'contactsExportStatus', + nested: 'contacts.exportStatus', + risk: 'read', + description: 'Get a marketing contacts export job', + method: 'GET', + path: 'marketing/contacts/exports/{id}', + pathKeys: ['id'], + inKind: 'id', + outKind: 'obj', + }, + { + key: 'contactsListExports', + nested: 'contacts.listExports', + risk: 'read', + description: 'List marketing contacts export jobs', + method: 'GET', + path: 'marketing/contacts/exports', + inKind: 'empty', + outKind: 'obj', + }, + { + key: 'listsGetAll', + nested: 'lists.getAll', + risk: 'read', + description: 'Retrieve all marketing contact lists', + method: 'GET', + path: 'marketing/lists', + queryKeys: ['page_size', 'page_token'], + inKind: 'page', + outKind: 'listsGetAll', + }, + { + key: 'listsCreate', + nested: 'lists.create', + risk: 'write', + description: 'Create a new marketing contact list', + method: 'POST', + path: 'marketing/lists', + body: true, + upsertList: true, + inKind: 'listsCreate', + outKind: 'list', + }, + { + key: 'listsGet', + nested: 'lists.get', + risk: 'read', + description: 'Get a marketing list by ID', + method: 'GET', + path: 'marketing/lists/{id}', + pathKeys: ['id'], + queryKeys: ['contact_sample'], + inKind: 'idSample', + outKind: 'obj', + }, + { + key: 'listsUpdate', + nested: 'lists.update', + risk: 'write', + description: 'Update a marketing list', + method: 'PATCH', + path: 'marketing/lists/{id}', + pathKeys: ['id'], + body: 'omit', + inKind: 'idName', + outKind: 'list', + }, + { + key: 'listsRemove', + nested: 'lists.remove', + risk: 'write', + description: 'Delete a marketing list', + method: 'DELETE', + path: 'marketing/lists/{id}', + pathKeys: ['id'], + queryKeys: ['delete_contacts'], + inKind: 'idDeleteContacts', + outKind: 'empty', + }, + { + key: 'listsGetContactCount', + nested: 'lists.getContactCount', + risk: 'read', + description: 'Get contact count for a marketing list', + method: 'GET', + path: 'marketing/lists/{id}/contacts/count', + pathKeys: ['id'], + inKind: 'id', + outKind: 'obj', + }, + { + key: 'listsRemoveContacts', + nested: 'lists.removeContacts', + risk: 'write', + description: 'Remove contacts from a marketing list', + method: 'DELETE', + path: 'marketing/lists/{id}/contacts', + pathKeys: ['id'], + queryKeys: ['contact_ids'], + inKind: 'idContactIds', + outKind: 'job', + }, + { + key: 'segmentsCreate', + nested: 'segments.create', + risk: 'write', + description: 'Create a Marketing Campaigns segment 2.0', + method: 'POST', + path: 'marketing/segments/2.0', + body: true, + inKind: 'segment', + outKind: 'obj', + }, + { + key: 'segmentsGetAll', + nested: 'segments.getAll', + risk: 'read', + description: 'Get all Marketing Campaigns segments 2.0', + method: 'GET', + path: 'marketing/segments/2.0', + queryKeys: ['parent_list_ids', 'no_parent_list_id'], + inKind: 'segmentsList', + outKind: 'obj', + }, + { + key: 'segmentsGet', + nested: 'segments.get', + risk: 'read', + description: 'Get a Marketing Campaigns segment 2.0', + method: 'GET', + path: 'marketing/segments/2.0/{id}', + pathKeys: ['id'], + queryKeys: ['contacts_sample'], + inKind: 'idContactsSample', + outKind: 'obj', + }, + { + key: 'segmentsUpdate', + nested: 'segments.update', + risk: 'write', + description: 'Update a Marketing Campaigns segment 2.0', + method: 'PATCH', + path: 'marketing/segments/2.0/{id}', + pathKeys: ['id'], + body: 'omit', + inKind: 'segmentUpdate', + outKind: 'obj', + }, + { + key: 'segmentsRemove', + nested: 'segments.remove', + risk: 'write', + description: 'Delete a Marketing Campaigns segment 2.0', + method: 'DELETE', + path: 'marketing/segments/2.0/{id}', + pathKeys: ['id'], + inKind: 'id', + outKind: 'empty', + }, + { + key: 'segmentsRefresh', + nested: 'segments.refresh', + risk: 'write', + description: 'Manually refresh a Marketing Campaigns segment 2.0', + method: 'POST', + path: 'marketing/segments/2.0/refresh/{id}', + pathKeys: ['id'], + body: 'omit', + inKind: 'segmentRefresh', + outKind: 'obj', + }, + { + key: 'fieldsGetAll', + nested: 'fields.getAll', + risk: 'read', + description: 'Get all marketing field definitions', + method: 'GET', + path: 'marketing/field_definitions', + inKind: 'empty', + outKind: 'obj', + }, + { + key: 'fieldsCreate', + nested: 'fields.create', + risk: 'write', + description: 'Create a custom field definition', + method: 'POST', + path: 'marketing/field_definitions', + body: true, + inKind: 'fieldCreate', + outKind: 'obj', + }, + { + key: 'fieldsUpdate', + nested: 'fields.update', + risk: 'write', + description: 'Update a custom field definition', + method: 'PATCH', + path: 'marketing/field_definitions/{id}', + pathKeys: ['id'], + body: 'omit', + inKind: 'idName', + outKind: 'obj', + }, + { + key: 'fieldsRemove', + nested: 'fields.remove', + risk: 'write', + description: 'Delete a custom field definition', + method: 'DELETE', + path: 'marketing/field_definitions/{id}', + pathKeys: ['id'], + inKind: 'id', + outKind: 'empty', + }, + { + key: 'sendersGetAll', + nested: 'senders.getAll', + risk: 'read', + description: 'Retrieve verified senders', + method: 'GET', + path: 'verified_senders', + queryKeys: ['limit', 'lastSeenID', 'id'], + inKind: 'sendersGetAll', + outKind: 'sendersGetAll', + }, + { + key: 'sendersCreate', + nested: 'senders.create', + risk: 'write', + description: 'Create a verified sender', + method: 'POST', + path: 'verified_senders', + body: true, + inKind: 'verifiedSender', + outKind: 'obj', + }, + { + key: 'sendersUpdate', + nested: 'senders.update', + risk: 'write', + description: 'Update a verified sender', + method: 'PATCH', + path: 'verified_senders/{id}', + pathKeys: ['id'], + body: 'omit', + inKind: 'verifiedSenderUpdate', + outKind: 'obj', + }, + { + key: 'sendersRemove', + nested: 'senders.remove', + risk: 'write', + description: 'Delete a verified sender', + method: 'DELETE', + path: 'verified_senders/{id}', + pathKeys: ['id'], + inKind: 'idNum', + outKind: 'empty', + }, + { + key: 'sendersResend', + nested: 'senders.resend', + risk: 'write', + description: 'Resend verified sender verification', + method: 'POST', + path: 'verified_senders/resend/{id}', + pathKeys: ['id'], + inKind: 'idNum', + outKind: 'empty', + }, + { + key: 'sendersListIdentities', + nested: 'senders.listIdentities', + risk: 'read', + description: 'Get Marketing Campaigns sender identities', + method: 'GET', + path: 'senders', + inKind: 'empty', + outKind: 'obj', + }, + { + key: 'sendersCreateIdentity', + nested: 'senders.createIdentity', + risk: 'write', + description: 'Create a Marketing Campaigns sender identity', + method: 'POST', + path: 'senders', + body: true, + inKind: 'senderIdentity', + outKind: 'obj', + }, + { + key: 'sendersGetIdentity', + nested: 'senders.getIdentity', + risk: 'read', + description: 'Get a Marketing Campaigns sender identity', + method: 'GET', + path: 'senders/{id}', + pathKeys: ['id'], + inKind: 'idNum', + outKind: 'obj', + }, + { + key: 'templatesCreate', + nested: 'templates.create', + risk: 'write', + description: 'Create a transactional template', + method: 'POST', + path: 'templates', + body: true, + inKind: 'templateCreate', + outKind: 'obj', + }, + { + key: 'templatesGetAll', + nested: 'templates.getAll', + risk: 'read', + description: 'Get all transactional templates', + method: 'GET', + path: 'templates', + queryKeys: ['generations', 'page_size'], + inKind: 'templatesList', + outKind: 'obj', + }, + { + key: 'templatesGet', + nested: 'templates.get', + risk: 'read', + description: 'Get a transactional template', + method: 'GET', + path: 'templates/{id}', + pathKeys: ['id'], + inKind: 'id', + outKind: 'obj', + }, + { + key: 'templatesUpdate', + nested: 'templates.update', + risk: 'write', + description: 'Update a transactional template', + method: 'PATCH', + path: 'templates/{id}', + pathKeys: ['id'], + body: 'omit', + inKind: 'idName', + outKind: 'obj', + }, + { + key: 'templatesRemove', + nested: 'templates.remove', + risk: 'write', + description: 'Delete a transactional template', + method: 'DELETE', + path: 'templates/{id}', + pathKeys: ['id'], + inKind: 'id', + outKind: 'empty', + }, + { + key: 'templatesCreateVersion', + nested: 'templates.createVersion', + risk: 'write', + description: 'Create a transactional template version', + method: 'POST', + path: 'templates/{id}/versions', + pathKeys: ['id'], + body: 'omit', + inKind: 'templateVersion', + outKind: 'obj', + }, + { + key: 'templatesGetVersion', + nested: 'templates.getVersion', + risk: 'read', + description: 'Get a transactional template version', + method: 'GET', + path: 'templates/{id}/versions/{version_id}', + pathKeys: ['id', 'version_id'], + inKind: 'templateVersionId', + outKind: 'obj', + }, + { + key: 'templatesUpdateVersion', + nested: 'templates.updateVersion', + risk: 'write', + description: 'Update a transactional template version', + method: 'PATCH', + path: 'templates/{id}/versions/{version_id}', + pathKeys: ['id', 'version_id'], + body: 'omit', + inKind: 'templateVersionUpdate', + outKind: 'obj', + }, + { + key: 'templatesRemoveVersion', + nested: 'templates.removeVersion', + risk: 'write', + description: 'Delete a transactional template version', + method: 'DELETE', + path: 'templates/{id}/versions/{version_id}', + pathKeys: ['id', 'version_id'], + inKind: 'templateVersionId', + outKind: 'empty', + }, + { + key: 'templatesActivateVersion', + nested: 'templates.activateVersion', + risk: 'write', + description: 'Activate a transactional template version', + method: 'POST', + path: 'templates/{id}/versions/{version_id}/activate', + pathKeys: ['id', 'version_id'], + inKind: 'templateVersionId', + outKind: 'obj', + }, + { + key: 'suppressionsGetBounces', + nested: 'suppressions.getBounces', + risk: 'read', + description: 'Retrieve email bounce suppressions', + method: 'GET', + path: 'suppression/bounces', + queryKeys: ['start_time', 'end_time', 'limit', 'offset'], + wrapArray: 'bounces', + inKind: 'suppressionList', + outKind: 'bounces', + }, + { + key: 'suppressionsGetBounce', + nested: 'suppressions.getBounce', + risk: 'read', + description: 'Retrieve a bounce by email', + method: 'GET', + path: 'suppression/bounces/{email}', + pathKeys: ['email'], + inKind: 'email', + outKind: 'arr', + }, + { + key: 'suppressionsDeleteBounce', + nested: 'suppressions.deleteBounce', + risk: 'write', + description: 'Delete a bounce by email', + method: 'DELETE', + path: 'suppression/bounces/{email}', + pathKeys: ['email'], + inKind: 'email', + outKind: 'empty', + }, + { + key: 'suppressionsDeleteBounces', + nested: 'suppressions.deleteBounces', + risk: 'write', + description: 'Delete bounce suppressions', + method: 'DELETE', + path: 'suppression/bounces', + body: true, + inKind: 'deleteSuppressions', + outKind: 'empty', + }, + { + key: 'suppressionsGetBlocks', + nested: 'suppressions.getBlocks', + risk: 'read', + description: 'Retrieve blocked emails', + method: 'GET', + path: 'suppression/blocks', + queryKeys: ['start_time', 'end_time', 'limit', 'offset'], + wrapArray: 'results', + inKind: 'suppressionList', + outKind: 'results', + }, + { + key: 'suppressionsGetBlock', + nested: 'suppressions.getBlock', + risk: 'read', + description: 'Retrieve a block by email', + method: 'GET', + path: 'suppression/blocks/{email}', + pathKeys: ['email'], + inKind: 'email', + outKind: 'arr', + }, + { + key: 'suppressionsDeleteBlock', + nested: 'suppressions.deleteBlock', + risk: 'write', + description: 'Delete a block by email', + method: 'DELETE', + path: 'suppression/blocks/{email}', + pathKeys: ['email'], + inKind: 'email', + outKind: 'empty', + }, + { + key: 'suppressionsDeleteBlocks', + nested: 'suppressions.deleteBlocks', + risk: 'write', + description: 'Delete blocked emails', + method: 'DELETE', + path: 'suppression/blocks', + body: true, + inKind: 'deleteSuppressions', + outKind: 'empty', + }, + { + key: 'suppressionsGetSpamReports', + nested: 'suppressions.getSpamReports', + risk: 'read', + description: 'Retrieve spam reports', + method: 'GET', + path: 'suppression/spam_reports', + queryKeys: ['start_time', 'end_time', 'limit', 'offset'], + wrapArray: 'results', + inKind: 'suppressionList', + outKind: 'results', + }, + { + key: 'suppressionsGetSpamReport', + nested: 'suppressions.getSpamReport', + risk: 'read', + description: 'Retrieve a spam report by email', + method: 'GET', + path: 'suppression/spam_reports/{email}', + pathKeys: ['email'], + inKind: 'email', + outKind: 'arr', + }, + { + key: 'suppressionsDeleteSpamReport', + nested: 'suppressions.deleteSpamReport', + risk: 'write', + description: 'Delete a spam report by email', + method: 'DELETE', + path: 'suppression/spam_reports/{email}', + pathKeys: ['email'], + inKind: 'email', + outKind: 'empty', + }, + { + key: 'suppressionsDeleteSpamReports', + nested: 'suppressions.deleteSpamReports', + risk: 'write', + description: 'Delete spam reports', + method: 'DELETE', + path: 'suppression/spam_reports', + body: true, + inKind: 'deleteSuppressions', + outKind: 'empty', + }, + { + key: 'suppressionsGetInvalidEmails', + nested: 'suppressions.getInvalidEmails', + risk: 'read', + description: 'Retrieve invalid emails', + method: 'GET', + path: 'suppression/invalid_emails', + queryKeys: ['start_time', 'end_time', 'limit', 'offset'], + wrapArray: 'results', + inKind: 'suppressionList', + outKind: 'results', + }, + { + key: 'suppressionsGetInvalidEmail', + nested: 'suppressions.getInvalidEmail', + risk: 'read', + description: 'Retrieve an invalid email', + method: 'GET', + path: 'suppression/invalid_emails/{email}', + pathKeys: ['email'], + inKind: 'email', + outKind: 'arr', + }, + { + key: 'suppressionsDeleteInvalidEmail', + nested: 'suppressions.deleteInvalidEmail', + risk: 'write', + description: 'Delete an invalid email', + method: 'DELETE', + path: 'suppression/invalid_emails/{email}', + pathKeys: ['email'], + inKind: 'email', + outKind: 'empty', + }, + { + key: 'suppressionsDeleteInvalidEmails', + nested: 'suppressions.deleteInvalidEmails', + risk: 'write', + description: 'Delete invalid emails', + method: 'DELETE', + path: 'suppression/invalid_emails', + body: true, + inKind: 'deleteSuppressions', + outKind: 'empty', + }, + { + key: 'suppressionsGetGlobalUnsubscribes', + nested: 'suppressions.getGlobalUnsubscribes', + risk: 'read', + description: 'Retrieve global unsubscribes', + method: 'GET', + path: 'suppression/unsubscribes', + queryKeys: ['start_time', 'end_time', 'limit', 'offset'], + wrapArray: 'results', + inKind: 'suppressionList', + outKind: 'results', + }, + { + key: 'suppressionsAddGlobalUnsubscribes', + nested: 'suppressions.addGlobalUnsubscribes', + risk: 'write', + description: 'Add emails to the global unsubscribe list', + method: 'POST', + path: 'asm/suppressions/global', + body: true, + inKind: 'recipientEmails', + outKind: 'obj', + }, + { + key: 'suppressionsGetGlobalUnsubscribe', + nested: 'suppressions.getGlobalUnsubscribe', + risk: 'read', + description: 'Retrieve a global unsubscribe by email', + method: 'GET', + path: 'asm/suppressions/global/{email}', + pathKeys: ['email'], + inKind: 'email', + outKind: 'obj', + }, + { + key: 'suppressionsDeleteGlobalUnsubscribe', + nested: 'suppressions.deleteGlobalUnsubscribe', + risk: 'write', + description: 'Delete a global unsubscribe by email', + method: 'DELETE', + path: 'asm/suppressions/global/{email}', + pathKeys: ['email'], + inKind: 'email', + outKind: 'empty', + }, + { + key: 'asmGetGroups', + nested: 'asm.getGroups', + risk: 'read', + description: 'Retrieve unsubscribe groups', + method: 'GET', + path: 'asm/groups', + queryKeys: ['id'], + inKind: 'asmIdQuery', + outKind: 'arr', + }, + { + key: 'asmCreateGroup', + nested: 'asm.createGroup', + risk: 'write', + description: 'Create an unsubscribe group', + method: 'POST', + path: 'asm/groups', + body: true, + inKind: 'asmGroup', + outKind: 'obj', + }, + { + key: 'asmGetGroup', + nested: 'asm.getGroup', + risk: 'read', + description: 'Retrieve an unsubscribe group', + method: 'GET', + path: 'asm/groups/{id}', + pathKeys: ['id'], + inKind: 'idNum', + outKind: 'obj', + }, + { + key: 'asmUpdateGroup', + nested: 'asm.updateGroup', + risk: 'write', + description: 'Update an unsubscribe group', + method: 'PATCH', + path: 'asm/groups/{id}', + pathKeys: ['id'], + body: 'omit', + inKind: 'asmGroupUpdate', + outKind: 'obj', + }, + { + key: 'asmDeleteGroup', + nested: 'asm.deleteGroup', + risk: 'write', + description: 'Delete an unsubscribe group', + method: 'DELETE', + path: 'asm/groups/{id}', + pathKeys: ['id'], + inKind: 'idNum', + outKind: 'empty', + }, + { + key: 'asmAddGroupSuppressions', + nested: 'asm.addGroupSuppressions', + risk: 'write', + description: 'Add suppressions to an unsubscribe group', + method: 'POST', + path: 'asm/groups/{id}/suppressions', + pathKeys: ['id'], + body: 'omit', + inKind: 'asmGroupEmails', + outKind: 'obj', + }, + { + key: 'asmGetGroupSuppressions', + nested: 'asm.getGroupSuppressions', + risk: 'read', + description: 'Retrieve suppressions for an unsubscribe group', + method: 'GET', + path: 'asm/groups/{id}/suppressions', + pathKeys: ['id'], + inKind: 'idNum', + outKind: 'arr', + }, + { + key: 'asmDeleteGroupSuppression', + nested: 'asm.deleteGroupSuppression', + risk: 'write', + description: 'Delete a suppression from an unsubscribe group', + method: 'DELETE', + path: 'asm/groups/{id}/suppressions/{email}', + pathKeys: ['id', 'email'], + inKind: 'idNumEmail', + outKind: 'empty', + }, + { + key: 'statsGetGlobal', + nested: 'stats.getGlobal', + risk: 'read', + description: 'Retrieve global email statistics', + method: 'GET', + path: 'stats', + queryKeys: ['start_date', 'end_date', 'aggregated_by', 'limit', 'offset'], + inKind: 'stats', + outKind: 'arr', + }, + { + key: 'statsGetCategory', + nested: 'stats.getCategory', + risk: 'read', + description: 'Retrieve category statistics', + method: 'GET', + path: 'categories/stats', + queryKeys: [ + 'start_date', + 'end_date', + 'aggregated_by', + 'categories', + 'limit', + 'offset', + ], + inKind: 'statsCategories', + outKind: 'arr', + }, + { + key: 'statsGetMailboxProvider', + nested: 'stats.getMailboxProvider', + risk: 'read', + description: 'Retrieve mailbox provider statistics', + method: 'GET', + path: 'mailbox_providers/stats', + queryKeys: [ + 'start_date', + 'end_date', + 'aggregated_by', + 'mailbox_providers', + 'limit', + 'offset', + ], + inKind: 'stats', + outKind: 'arr', + }, + { + key: 'statsGetGeo', + nested: 'stats.getGeo', + risk: 'read', + description: 'Retrieve geographic statistics', + method: 'GET', + path: 'geo/stats', + queryKeys: [ + 'start_date', + 'end_date', + 'aggregated_by', + 'country', + 'limit', + 'offset', + ], + inKind: 'stats', + outKind: 'arr', + }, + { + key: 'statsGetDevice', + nested: 'stats.getDevice', + risk: 'read', + description: 'Retrieve device statistics', + method: 'GET', + path: 'devices/stats', + queryKeys: ['start_date', 'end_date', 'aggregated_by', 'limit', 'offset'], + inKind: 'stats', + outKind: 'arr', + }, + { + key: 'statsGetClient', + nested: 'stats.getClient', + risk: 'read', + description: 'Retrieve email client statistics', + method: 'GET', + path: 'clients/stats', + queryKeys: ['start_date', 'end_date', 'aggregated_by'], + inKind: 'stats', + outKind: 'arr', + }, + { + key: 'userGetProfile', + nested: 'user.getProfile', + risk: 'read', + description: 'Retrieve the user profile', + method: 'GET', + path: 'user/profile', + inKind: 'empty', + outKind: 'obj', + }, + { + key: 'userGetAccount', + nested: 'user.getAccount', + risk: 'read', + description: 'Retrieve the user account', + method: 'GET', + path: 'user/account', + inKind: 'empty', + outKind: 'obj', + }, + { + key: 'userGetCredits', + nested: 'user.getCredits', + risk: 'read', + description: 'Retrieve remaining email credits', + method: 'GET', + path: 'user/credits', + inKind: 'empty', + outKind: 'obj', + }, + { + key: 'userGetUsername', + nested: 'user.getUsername', + risk: 'read', + description: 'Retrieve the account username', + method: 'GET', + path: 'user/username', + inKind: 'empty', + outKind: 'obj', + }, + { + key: 'userGetEmail', + nested: 'user.getEmail', + risk: 'read', + description: 'Retrieve the account email address', + method: 'GET', + path: 'user/email', + inKind: 'empty', + outKind: 'obj', + }, + { + key: 'userGetScopes', + nested: 'user.getScopes', + risk: 'read', + description: 'Retrieve API key scopes for the current key', + method: 'GET', + path: 'scopes', + inKind: 'empty', + outKind: 'obj', + }, + { + key: 'apiKeysCreate', + nested: 'apiKeys.create', + risk: 'write', + description: 'Create an API key', + method: 'POST', + path: 'api_keys', + body: true, + inKind: 'apiKeyCreate', + outKind: 'obj', + }, + { + key: 'apiKeysGetAll', + nested: 'apiKeys.getAll', + risk: 'read', + description: 'Retrieve all API keys', + method: 'GET', + path: 'api_keys', + queryKeys: ['limit'], + inKind: 'limit', + outKind: 'obj', + }, + { + key: 'apiKeysGet', + nested: 'apiKeys.get', + risk: 'read', + description: 'Retrieve an API key', + method: 'GET', + path: 'api_keys/{id}', + pathKeys: ['id'], + inKind: 'id', + outKind: 'obj', + }, + { + key: 'apiKeysUpdate', + nested: 'apiKeys.update', + risk: 'write', + description: 'Update an API key name or scopes', + method: 'PUT', + path: 'api_keys/{id}', + pathKeys: ['id'], + body: 'omit', + inKind: 'apiKeyUpdate', + outKind: 'obj', + }, + { + key: 'apiKeysRemove', + nested: 'apiKeys.remove', + risk: 'write', + description: 'Delete an API key', + method: 'DELETE', + path: 'api_keys/{id}', + pathKeys: ['id'], + inKind: 'id', + outKind: 'empty', + }, +]; + +export const SENDGRID_OPS_BY_KEY = Object.fromEntries( + SENDGRID_OPS.map((op) => [op.key, op]), +) as Record; diff --git a/packages/sendgrid/endpoints/contacts.ts b/packages/sendgrid/endpoints/contacts.ts deleted file mode 100644 index aa04cb3c9..000000000 --- a/packages/sendgrid/endpoints/contacts.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { logEventFromContext } from 'corsair/core'; -import type { SendGridEndpoints } from '..'; -import { makeSendGridRequest } from '../client'; -import type { SendGridEndpointOutputs } from './types'; - -export const addOrUpdate: SendGridEndpoints['contactsAddOrUpdate'] = async ( - ctx, - input, -) => { - const response = await makeSendGridRequest< - SendGridEndpointOutputs['contactsAddOrUpdate'] - >('marketing/contacts', ctx.key, { - method: 'PUT', - body: input, - }); - - await logEventFromContext( - ctx, - 'sendgrid.contacts.addOrUpdate', - { - contact_count: input.contacts.length, - list_count: input.list_ids?.length ?? 0, - }, - 'completed', - ); - - return response; -}; diff --git a/packages/sendgrid/endpoints/handlers.ts b/packages/sendgrid/endpoints/handlers.ts new file mode 100644 index 000000000..e12901c9d --- /dev/null +++ b/packages/sendgrid/endpoints/handlers.ts @@ -0,0 +1,152 @@ +import type { SendGridEndpoints } from '..'; +import { runCatalogOp } from './bind'; +import { SENDGRID_OPS_BY_KEY } from './catalog'; + +function bind(key: K): SendGridEndpoints[K] { + const spec = SENDGRID_OPS_BY_KEY[key as string]; + if (!spec) { + throw new Error(`Unknown SendGrid op: ${String(key)}`); + } + return (async (ctx, input) => + runCatalogOp( + ctx, + spec, + input as Record, + )) as SendGridEndpoints[K]; +} + +export const mail = { + send: bind('mailSend'), + createBatchId: bind('mailCreateBatchId'), + validateBatchId: bind('mailValidateBatchId'), + cancelScheduledSend: bind('mailCancelScheduledSend'), + listScheduledSends: bind('mailListScheduledSends'), + getScheduledSend: bind('mailGetScheduledSend'), + updateScheduledSend: bind('mailUpdateScheduledSend'), + deleteScheduledSend: bind('mailDeleteScheduledSend'), +}; + +export const contacts = { + addOrUpdate: bind('contactsAddOrUpdate'), + get: bind('contactsGet'), + search: bind('contactsSearch'), + searchEmails: bind('contactsSearchEmails'), + remove: bind('contactsRemove'), + getCount: bind('contactsGetCount'), + getSample: bind('contactsGetSample'), + import: bind('contactsImport'), + importStatus: bind('contactsImportStatus'), + export: bind('contactsExport'), + exportStatus: bind('contactsExportStatus'), + listExports: bind('contactsListExports'), +}; + +export const lists = { + getAll: bind('listsGetAll'), + create: bind('listsCreate'), + get: bind('listsGet'), + update: bind('listsUpdate'), + remove: bind('listsRemove'), + getContactCount: bind('listsGetContactCount'), + removeContacts: bind('listsRemoveContacts'), +}; + +export const segments = { + create: bind('segmentsCreate'), + getAll: bind('segmentsGetAll'), + get: bind('segmentsGet'), + update: bind('segmentsUpdate'), + remove: bind('segmentsRemove'), + refresh: bind('segmentsRefresh'), +}; + +export const fields = { + getAll: bind('fieldsGetAll'), + create: bind('fieldsCreate'), + update: bind('fieldsUpdate'), + remove: bind('fieldsRemove'), +}; + +export const senders = { + getAll: bind('sendersGetAll'), + create: bind('sendersCreate'), + update: bind('sendersUpdate'), + remove: bind('sendersRemove'), + resend: bind('sendersResend'), + listIdentities: bind('sendersListIdentities'), + createIdentity: bind('sendersCreateIdentity'), + getIdentity: bind('sendersGetIdentity'), +}; + +export const templates = { + create: bind('templatesCreate'), + getAll: bind('templatesGetAll'), + get: bind('templatesGet'), + update: bind('templatesUpdate'), + remove: bind('templatesRemove'), + createVersion: bind('templatesCreateVersion'), + getVersion: bind('templatesGetVersion'), + updateVersion: bind('templatesUpdateVersion'), + removeVersion: bind('templatesRemoveVersion'), + activateVersion: bind('templatesActivateVersion'), +}; + +export const suppressions = { + getBounces: bind('suppressionsGetBounces'), + getBounce: bind('suppressionsGetBounce'), + deleteBounce: bind('suppressionsDeleteBounce'), + deleteBounces: bind('suppressionsDeleteBounces'), + getBlocks: bind('suppressionsGetBlocks'), + getBlock: bind('suppressionsGetBlock'), + deleteBlock: bind('suppressionsDeleteBlock'), + deleteBlocks: bind('suppressionsDeleteBlocks'), + getSpamReports: bind('suppressionsGetSpamReports'), + getSpamReport: bind('suppressionsGetSpamReport'), + deleteSpamReport: bind('suppressionsDeleteSpamReport'), + deleteSpamReports: bind('suppressionsDeleteSpamReports'), + getInvalidEmails: bind('suppressionsGetInvalidEmails'), + getInvalidEmail: bind('suppressionsGetInvalidEmail'), + deleteInvalidEmail: bind('suppressionsDeleteInvalidEmail'), + deleteInvalidEmails: bind('suppressionsDeleteInvalidEmails'), + getGlobalUnsubscribes: bind('suppressionsGetGlobalUnsubscribes'), + addGlobalUnsubscribes: bind('suppressionsAddGlobalUnsubscribes'), + getGlobalUnsubscribe: bind('suppressionsGetGlobalUnsubscribe'), + deleteGlobalUnsubscribe: bind('suppressionsDeleteGlobalUnsubscribe'), +}; + +export const asm = { + getGroups: bind('asmGetGroups'), + createGroup: bind('asmCreateGroup'), + getGroup: bind('asmGetGroup'), + updateGroup: bind('asmUpdateGroup'), + deleteGroup: bind('asmDeleteGroup'), + addGroupSuppressions: bind('asmAddGroupSuppressions'), + getGroupSuppressions: bind('asmGetGroupSuppressions'), + deleteGroupSuppression: bind('asmDeleteGroupSuppression'), +}; + +export const stats = { + getGlobal: bind('statsGetGlobal'), + getCategory: bind('statsGetCategory'), + getMailboxProvider: bind('statsGetMailboxProvider'), + getGeo: bind('statsGetGeo'), + getDevice: bind('statsGetDevice'), + getClient: bind('statsGetClient'), +}; + +export const user = { + getProfile: bind('userGetProfile'), + getAccount: bind('userGetAccount'), + getCredits: bind('userGetCredits'), + getUsername: bind('userGetUsername'), + getEmail: bind('userGetEmail'), + getScopes: bind('userGetScopes'), +}; + +export const apiKeys = { + create: bind('apiKeysCreate'), + getAll: bind('apiKeysGetAll'), + get: bind('apiKeysGet'), + update: bind('apiKeysUpdate'), + remove: bind('apiKeysRemove'), +}; diff --git a/packages/sendgrid/endpoints/index.ts b/packages/sendgrid/endpoints/index.ts index efc9e7d76..d428432b8 100644 --- a/packages/sendgrid/endpoints/index.ts +++ b/packages/sendgrid/endpoints/index.ts @@ -1,5 +1,14 @@ -export * as Contacts from './contacts'; -export * as Lists from './lists'; -export * as Mail from './mail'; -export * as Senders from './senders'; -export * as Suppressions from './suppressions'; +export { + apiKeys, + asm, + contacts, + fields, + lists, + mail, + segments, + senders, + stats, + suppressions, + templates, + user, +} from './handlers'; diff --git a/packages/sendgrid/endpoints/lists.ts b/packages/sendgrid/endpoints/lists.ts deleted file mode 100644 index 71efa3734..000000000 --- a/packages/sendgrid/endpoints/lists.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { logEventFromContext } from 'corsair/core'; -import type { SendGridEndpoints } from '..'; -import { makeSendGridRequest } from '../client'; -import type { SendGridEndpointOutputs } from './types'; - -export const getAll: SendGridEndpoints['listsGetAll'] = async (ctx, input) => { - const query: Record = {}; - if (input.page_size) query.page_size = input.page_size; - if (input.page_token) query.page_token = input.page_token; - - const response = await makeSendGridRequest< - SendGridEndpointOutputs['listsGetAll'] - >('marketing/lists', ctx.key, { - method: 'GET', - query, - }); - - await logEventFromContext( - ctx, - 'sendgrid.lists.getAll', - { result_count: response.result.length }, - 'completed', - ); - return response; -}; - -export const create: SendGridEndpoints['listsCreate'] = async (ctx, input) => { - const response = await makeSendGridRequest< - SendGridEndpointOutputs['listsCreate'] - >('marketing/lists', ctx.key, { - method: 'POST', - body: input, - }); - - if (ctx.db.lists && response.id) { - await ctx.db.lists.upsertByEntityId(response.id, response); - } - - await logEventFromContext( - ctx, - 'sendgrid.lists.create', - { list_id: response.id }, - 'completed', - ); - return response; -}; diff --git a/packages/sendgrid/endpoints/mail.ts b/packages/sendgrid/endpoints/mail.ts deleted file mode 100644 index 0ddfb8ef9..000000000 --- a/packages/sendgrid/endpoints/mail.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { logEventFromContext } from 'corsair/core'; -import type { SendGridEndpoints } from '..'; -import { makeSendGridRequest } from '../client'; - -export const send: SendGridEndpoints['mailSend'] = async (ctx, input) => { - const xMessageId = await makeSendGridRequest( - 'mail/send', - ctx.key, - { - method: 'POST', - body: input, - responseHeader: 'X-Message-Id', - }, - ); - - await logEventFromContext( - ctx, - 'sendgrid.mail.send', - { - recipient_count: input.personalizations.reduce( - (sum, p) => sum + p.to.length, - 0, - ), - has_template: Boolean(input.template_id), - }, - 'completed', - ); - - return { x_message_id: xMessageId }; -}; diff --git a/packages/sendgrid/endpoints/run.ts b/packages/sendgrid/endpoints/run.ts new file mode 100644 index 000000000..eb7deb706 --- /dev/null +++ b/packages/sendgrid/endpoints/run.ts @@ -0,0 +1,17 @@ +export function q( + input: Record, + keys: string[], +): Record { + const query: Record = {}; + for (const key of keys) { + const value = input[key]; + if (value !== undefined) { + query[key] = value as string | number | boolean; + } + } + return query; +} + +export function enc(value: string): string { + return encodeURIComponent(value); +} diff --git a/packages/sendgrid/endpoints/senders.ts b/packages/sendgrid/endpoints/senders.ts deleted file mode 100644 index 75db73557..000000000 --- a/packages/sendgrid/endpoints/senders.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { logEventFromContext } from 'corsair/core'; -import type { SendGridEndpoints } from '..'; -import { makeSendGridRequest } from '../client'; -import type { SendGridEndpointOutputs } from './types'; - -export const getAll: SendGridEndpoints['sendersGetAll'] = async ( - ctx, - input, -) => { - const query: Record = {}; - if (input.limit) query.limit = input.limit; - if (input.lastSeenID) query.lastSeenID = input.lastSeenID; - if (input.id) query.id = input.id; - - const response = await makeSendGridRequest< - SendGridEndpointOutputs['sendersGetAll'] - >('verified_senders', ctx.key, { - method: 'GET', - query, - }); - - await logEventFromContext( - ctx, - 'sendgrid.senders.getAll', - { result_count: response.results.length }, - 'completed', - ); - return response; -}; diff --git a/packages/sendgrid/endpoints/suppressions.ts b/packages/sendgrid/endpoints/suppressions.ts deleted file mode 100644 index 117be348f..000000000 --- a/packages/sendgrid/endpoints/suppressions.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { logEventFromContext } from 'corsair/core'; -import type { SendGridEndpoints } from '..'; -import { makeSendGridRequest } from '../client'; -import { SendGridBounce } from '../schema/database'; - -export const getBounces: SendGridEndpoints['suppressionsGetBounces'] = async ( - ctx, - input, -) => { - const query: Record = {}; - if (input.start_time) query.start_time = input.start_time; - if (input.end_time) query.end_time = input.end_time; - if (input.limit) query.limit = input.limit; - if (input.offset !== undefined) query.offset = input.offset; - - const response = await makeSendGridRequest( - 'suppression/bounces', - ctx.key, - { - method: 'GET', - query, - }, - ); - - const bounces = Array.isArray(response) - ? response.map((item) => SendGridBounce.parse(item)) - : []; - - await logEventFromContext( - ctx, - 'sendgrid.suppressions.getBounces', - { bounce_count: bounces.length }, - 'completed', - ); - return { bounces }; -}; diff --git a/packages/sendgrid/endpoints/types.ts b/packages/sendgrid/endpoints/types.ts index 1982624cf..318e63595 100644 --- a/packages/sendgrid/endpoints/types.ts +++ b/packages/sendgrid/endpoints/types.ts @@ -24,148 +24,531 @@ const PersonalizationSchema = z.object({ }); const ContentSchema = z.object({ - type: z.string(), - value: z.string(), + type: z.string().min(1), + value: z.string().min(1), }); -/** - * Mail Send v3 request. - * Official: POST /v3/mail/send - * https://www.twilio.com/docs/sendgrid/api-reference/mail-send/mail-send - */ -const MailSendInputSchema = z.object({ - personalizations: z.array(PersonalizationSchema).min(1), - from: EmailRecipientSchema, - subject: z.string().optional(), - content: z.array(ContentSchema).optional(), - reply_to: EmailRecipientSchema.optional(), - template_id: z.string().optional(), - categories: z.array(z.string()).optional(), - send_at: z.number().int().optional(), - batch_id: z.string().optional(), - ip_pool_name: z.string().optional(), - asm: z - .object({ - group_id: z.number().int(), - groups_to_display: z.array(z.number().int()).optional(), - }) - .optional(), -}); - -export type MailSendInput = z.infer; - -const MailSendOutputSchema = z.object({ - x_message_id: z.string().optional(), -}); - -export type MailSendOutput = z.infer; - -const ContactsAddOrUpdateInputSchema = z.object({ - list_ids: z.array(z.string()).optional(), - contacts: z.array(SendGridContact).min(1), -}); - -export type ContactsAddOrUpdateInput = z.infer< - typeof ContactsAddOrUpdateInputSchema ->; - -const ContactsAddOrUpdateOutputSchema = z.object({ - job_id: z.string(), -}); - -export type ContactsAddOrUpdateOutput = z.infer< - typeof ContactsAddOrUpdateOutputSchema ->; +/** Official: POST /v3/mail/send */ +const MailSendInputSchema = z + .object({ + personalizations: z.array(PersonalizationSchema).min(1), + from: EmailRecipientSchema, + subject: z.string().optional(), + content: z.array(ContentSchema).optional(), + reply_to: EmailRecipientSchema.optional(), + template_id: z.string().optional(), + categories: z.array(z.string()).optional(), + send_at: z.number().int().optional(), + batch_id: z.string().optional(), + ip_pool_name: z.string().optional(), + asm: z + .object({ + group_id: z.number().int(), + groups_to_display: z.array(z.number().int()).optional(), + }) + .optional(), + }) + .superRefine((data, ctx) => { + const templated = + typeof data.template_id === 'string' && data.template_id.length > 0; + if (templated) { + return; + } + if (!data.content || data.content.length === 0) { + ctx.addIssue({ + code: 'custom', + path: ['content'], + message: 'content is required unless template_id is set', + }); + } + const topSubject = + typeof data.subject === 'string' && data.subject.length > 0; + if ( + !topSubject && + data.personalizations.some( + (p) => !(typeof p.subject === 'string' && p.subject.length > 0), + ) + ) { + ctx.addIssue({ + code: 'custom', + path: ['subject'], + message: + 'subject is required at the top level or on every personalization unless template_id is set', + }); + } + }); -const ListsGetAllInputSchema = z.object({ +const EmptySchema = z.object({}); +const ObjSchema = z.object({}).catchall(z.unknown()); +const ArrSchema = z.array(ObjSchema); +const JobSchema = z.object({ job_id: z.string() }).catchall(z.unknown()); +const IdSchema = z.object({ id: z.string() }); +const IdNumSchema = z.object({ id: z.union([z.string(), z.number()]) }); +const EmailSchema = z.object({ email: z.string().email() }); +const BatchIdSchema = z.object({ batch_id: z.string() }); +const PageSchema = z.object({ page_size: z.number().int().positive().max(1000).optional(), page_token: z.string().optional(), }); - -export type ListsGetAllInput = z.infer; - -const ListsGetAllOutputSchema = z.object({ - result: z.array(SendGridList), - _metadata: z.record(z.string(), z.unknown()).optional(), -}); - -export type ListsGetAllOutput = z.infer; - -const ListsCreateInputSchema = z.object({ - name: z.string().min(1), -}); - -export type ListsCreateInput = z.infer; - -const ListsCreateOutputSchema = SendGridList; - -export type ListsCreateOutput = z.infer; - -const SuppressionsGetBouncesInputSchema = z.object({ +const SuppressionListSchema = z.object({ start_time: z.number().int().optional(), end_time: z.number().int().optional(), limit: z.number().int().positive().optional(), offset: z.number().int().nonnegative().optional(), }); - -export type SuppressionsGetBouncesInput = z.infer< - typeof SuppressionsGetBouncesInputSchema ->; - -const SuppressionsGetBouncesOutputSchema = z.object({ - bounces: z.array(SendGridBounce), +const StatsSchema = z.object({ + start_date: z.string(), + end_date: z.string().optional(), + aggregated_by: z.string().optional(), + limit: z.number().int().optional(), + offset: z.number().int().optional(), + mailbox_providers: z.string().optional(), + country: z.string().optional(), }); -export type SuppressionsGetBouncesOutput = z.infer< - typeof SuppressionsGetBouncesOutputSchema ->; - -const SendersGetAllInputSchema = z.object({ - limit: z.number().int().positive().optional(), - lastSeenID: z.number().int().optional(), - id: z.number().int().optional(), -}); +const inputKinds = { + empty: EmptySchema, + mailSend: MailSendInputSchema, + batchId: BatchIdSchema, + scheduledSend: z.object({ + batch_id: z.string(), + status: z.enum(['pause', 'cancel']), + }), + scheduledSendUpdate: z.object({ + batch_id: z.string(), + status: z.enum(['pause', 'cancel']), + }), + contactsAddOrUpdate: z.object({ + list_ids: z.array(z.string()).optional(), + contacts: z + .array( + SendGridContact.refine( + (contact) => + Boolean( + contact.email || + contact.phone_number_id || + contact.external_id || + contact.anonymous_id, + ), + { + message: + 'at least one of email, phone_number_id, external_id, or anonymous_id is required', + }, + ), + ) + .min(1), + }), + id: IdSchema, + contactSearch: z.object({ query: z.string() }).catchall(z.unknown()), + contactSearchEmails: z.object({ emails: z.array(z.string().email()).min(1) }), + idsQuery: z.object({ ids: z.string() }), + page: PageSchema, + contactImport: z.object({ + file_type: z.literal('csv'), + field_mappings: z.array(z.string().nullable()).min(1), + list_ids: z.array(z.string()).optional(), + }), + contactExport: z + .object({ + list_ids: z.array(z.string()).optional(), + segment_ids: z.array(z.string()).optional(), + file_type: z.enum(['csv', 'json']).optional(), + max_file_size: z.number().int().optional(), + }) + .catchall(z.unknown()), + listsCreate: z.object({ name: z.string().min(1) }), + idSample: z.object({ + id: z.string(), + contact_sample: z.boolean().optional(), + }), + idName: z.object({ id: z.string(), name: z.string().min(1) }), + idDeleteContacts: z.object({ + id: z.string(), + delete_contacts: z.boolean().optional(), + }), + idContactIds: z.object({ id: z.string(), contact_ids: z.string() }), + segment: z + .object({ + name: z.string(), + query_dsl: z.string().optional(), + parent_list_id: z.string().optional(), + }) + .catchall(z.unknown()), + segmentsList: z.object({ + parent_list_ids: z.string().optional(), + no_parent_list_id: z.boolean().optional(), + }), + idContactsSample: z.object({ + id: z.string(), + contacts_sample: z.boolean().optional(), + }), + segmentUpdate: z + .object({ + id: z.string(), + name: z.string().optional(), + query_dsl: z.string().optional(), + }) + .catchall(z.unknown()), + segmentRefresh: z.object({ + id: z.string(), + user_time_zone: z.string(), + }), + fieldCreate: z.object({ + name: z.string(), + field_type: z.string(), + }), + sendersGetAll: z.object({ + limit: z.number().int().positive().optional(), + lastSeenID: z.number().int().optional(), + id: z.number().int().optional(), + }), + verifiedSender: z + .object({ + nickname: z.string(), + from_email: z.string().email(), + from_name: z.string().optional(), + reply_to: z.string().email().optional(), + address: z.string().optional(), + city: z.string().optional(), + state: z.string().optional(), + zip: z.string().optional(), + country: z.string().optional(), + }) + .catchall(z.unknown()), + verifiedSenderUpdate: z + .object({ + id: z.union([z.string(), z.number()]), + nickname: z.string().optional(), + from_email: z.string().email().optional(), + }) + .catchall(z.unknown()), + idNum: IdNumSchema, + senderIdentity: z.object({}).catchall(z.unknown()), + templateCreate: z.object({ + name: z.string(), + generation: z.string().optional(), + }), + templatesList: z.object({ + generations: z.string().optional(), + page_size: z.number().int().optional(), + }), + templateVersion: z + .object({ + id: z.string(), + name: z.string().optional(), + subject: z.string().optional(), + html_content: z.string().optional(), + plain_content: z.string().optional(), + active: z.number().int().optional(), + }) + .catchall(z.unknown()), + templateVersionId: z.object({ id: z.string(), version_id: z.string() }), + templateVersionUpdate: z + .object({ + id: z.string(), + version_id: z.string(), + name: z.string().optional(), + subject: z.string().optional(), + html_content: z.string().optional(), + plain_content: z.string().optional(), + }) + .catchall(z.unknown()), + suppressionList: SuppressionListSchema, + email: EmailSchema, + deleteSuppressions: z.object({ + delete_all: z.boolean().optional(), + emails: z.array(z.string().email()).optional(), + }), + recipientEmails: z.object({ + recipient_emails: z.array(z.string().email()).min(1), + }), + asmIdQuery: z.object({ id: z.number().int().optional() }), + asmGroup: z.object({ + name: z.string(), + description: z.string(), + is_default: z.boolean().optional(), + }), + asmGroupUpdate: z + .object({ + id: z.union([z.string(), z.number()]), + name: z.string().optional(), + description: z.string().optional(), + is_default: z.boolean().optional(), + }) + .catchall(z.unknown()), + asmGroupEmails: z.object({ + id: z.union([z.string(), z.number()]), + recipient_emails: z.array(z.string().email()).min(1), + }), + idNumEmail: z.object({ + id: z.union([z.string(), z.number()]), + email: z.string().email(), + }), + stats: StatsSchema, + statsCategories: StatsSchema.extend({ + categories: z.string(), + }), + apiKeyCreate: z.object({ + name: z.string(), + scopes: z.array(z.string()).optional(), + }), + limit: z.object({ limit: z.number().int().positive().optional() }), + apiKeyUpdate: z.object({ + id: z.string(), + name: z.string().optional(), + scopes: z.array(z.string()).optional(), + }), +} as const; -export type SendersGetAllInput = z.infer; +const outputKinds = { + empty: EmptySchema, + obj: ObjSchema, + arr: ArrSchema, + job: JobSchema, + mailSend: z.object({ x_message_id: z.string().optional() }), + list: SendGridList, + listsGetAll: z.object({ + result: z.array(SendGridList), + _metadata: z.record(z.string(), z.unknown()).optional(), + }), + sendersGetAll: z.object({ + results: z.array(SendGridVerifiedSender), + }), + bounces: z.object({ bounces: z.array(SendGridBounce) }), + results: z.object({ results: z.array(ObjSchema) }), +} as const; -const SendersGetAllOutputSchema = z.object({ - results: z.array(SendGridVerifiedSender), -}); +export const SendGridEndpointInputSchemas = { + mailSend: inputKinds.mailSend, + mailCreateBatchId: inputKinds.empty, + mailValidateBatchId: inputKinds.batchId, + mailCancelScheduledSend: inputKinds.scheduledSend, + mailListScheduledSends: inputKinds.empty, + mailGetScheduledSend: inputKinds.batchId, + mailUpdateScheduledSend: inputKinds.scheduledSendUpdate, + mailDeleteScheduledSend: inputKinds.batchId, + contactsAddOrUpdate: inputKinds.contactsAddOrUpdate, + contactsGet: inputKinds.id, + contactsSearch: inputKinds.contactSearch, + contactsSearchEmails: inputKinds.contactSearchEmails, + contactsRemove: inputKinds.idsQuery, + contactsGetCount: inputKinds.empty, + contactsGetSample: inputKinds.page, + contactsImport: inputKinds.contactImport, + contactsImportStatus: inputKinds.id, + contactsExport: inputKinds.contactExport, + contactsExportStatus: inputKinds.id, + contactsListExports: inputKinds.empty, + listsGetAll: inputKinds.page, + listsCreate: inputKinds.listsCreate, + listsGet: inputKinds.idSample, + listsUpdate: inputKinds.idName, + listsRemove: inputKinds.idDeleteContacts, + listsGetContactCount: inputKinds.id, + listsRemoveContacts: inputKinds.idContactIds, + segmentsCreate: inputKinds.segment, + segmentsGetAll: inputKinds.segmentsList, + segmentsGet: inputKinds.idContactsSample, + segmentsUpdate: inputKinds.segmentUpdate, + segmentsRemove: inputKinds.id, + segmentsRefresh: inputKinds.segmentRefresh, + fieldsGetAll: inputKinds.empty, + fieldsCreate: inputKinds.fieldCreate, + fieldsUpdate: inputKinds.idName, + fieldsRemove: inputKinds.id, + sendersGetAll: inputKinds.sendersGetAll, + sendersCreate: inputKinds.verifiedSender, + sendersUpdate: inputKinds.verifiedSenderUpdate, + sendersRemove: inputKinds.idNum, + sendersResend: inputKinds.idNum, + sendersListIdentities: inputKinds.empty, + sendersCreateIdentity: inputKinds.senderIdentity, + sendersGetIdentity: inputKinds.idNum, + templatesCreate: inputKinds.templateCreate, + templatesGetAll: inputKinds.templatesList, + templatesGet: inputKinds.id, + templatesUpdate: inputKinds.idName, + templatesRemove: inputKinds.id, + templatesCreateVersion: inputKinds.templateVersion, + templatesGetVersion: inputKinds.templateVersionId, + templatesUpdateVersion: inputKinds.templateVersionUpdate, + templatesRemoveVersion: inputKinds.templateVersionId, + templatesActivateVersion: inputKinds.templateVersionId, + suppressionsGetBounces: inputKinds.suppressionList, + suppressionsGetBounce: inputKinds.email, + suppressionsDeleteBounce: inputKinds.email, + suppressionsDeleteBounces: inputKinds.deleteSuppressions, + suppressionsGetBlocks: inputKinds.suppressionList, + suppressionsGetBlock: inputKinds.email, + suppressionsDeleteBlock: inputKinds.email, + suppressionsDeleteBlocks: inputKinds.deleteSuppressions, + suppressionsGetSpamReports: inputKinds.suppressionList, + suppressionsGetSpamReport: inputKinds.email, + suppressionsDeleteSpamReport: inputKinds.email, + suppressionsDeleteSpamReports: inputKinds.deleteSuppressions, + suppressionsGetInvalidEmails: inputKinds.suppressionList, + suppressionsGetInvalidEmail: inputKinds.email, + suppressionsDeleteInvalidEmail: inputKinds.email, + suppressionsDeleteInvalidEmails: inputKinds.deleteSuppressions, + suppressionsGetGlobalUnsubscribes: inputKinds.suppressionList, + suppressionsAddGlobalUnsubscribes: inputKinds.recipientEmails, + suppressionsGetGlobalUnsubscribe: inputKinds.email, + suppressionsDeleteGlobalUnsubscribe: inputKinds.email, + asmGetGroups: inputKinds.asmIdQuery, + asmCreateGroup: inputKinds.asmGroup, + asmGetGroup: inputKinds.idNum, + asmUpdateGroup: inputKinds.asmGroupUpdate, + asmDeleteGroup: inputKinds.idNum, + asmAddGroupSuppressions: inputKinds.asmGroupEmails, + asmGetGroupSuppressions: inputKinds.idNum, + asmDeleteGroupSuppression: inputKinds.idNumEmail, + statsGetGlobal: inputKinds.stats, + statsGetCategory: inputKinds.statsCategories, + statsGetMailboxProvider: inputKinds.stats, + statsGetGeo: inputKinds.stats, + statsGetDevice: inputKinds.stats, + statsGetClient: inputKinds.stats, + userGetProfile: inputKinds.empty, + userGetAccount: inputKinds.empty, + userGetCredits: inputKinds.empty, + userGetUsername: inputKinds.empty, + userGetEmail: inputKinds.empty, + userGetScopes: inputKinds.empty, + apiKeysCreate: inputKinds.apiKeyCreate, + apiKeysGetAll: inputKinds.limit, + apiKeysGet: inputKinds.id, + apiKeysUpdate: inputKinds.apiKeyUpdate, + apiKeysRemove: inputKinds.id, +} as const; -export type SendersGetAllOutput = z.infer; +export const SendGridEndpointOutputSchemas = { + mailSend: outputKinds.mailSend, + mailCreateBatchId: outputKinds.obj, + mailValidateBatchId: outputKinds.obj, + mailCancelScheduledSend: outputKinds.obj, + mailListScheduledSends: outputKinds.arr, + mailGetScheduledSend: outputKinds.arr, + mailUpdateScheduledSend: outputKinds.empty, + mailDeleteScheduledSend: outputKinds.empty, + contactsAddOrUpdate: outputKinds.job, + contactsGet: outputKinds.obj, + contactsSearch: outputKinds.obj, + contactsSearchEmails: outputKinds.obj, + contactsRemove: outputKinds.job, + contactsGetCount: outputKinds.obj, + contactsGetSample: outputKinds.obj, + contactsImport: outputKinds.obj, + contactsImportStatus: outputKinds.obj, + contactsExport: outputKinds.obj, + contactsExportStatus: outputKinds.obj, + contactsListExports: outputKinds.obj, + listsGetAll: outputKinds.listsGetAll, + listsCreate: outputKinds.list, + listsGet: outputKinds.obj, + listsUpdate: outputKinds.list, + listsRemove: outputKinds.empty, + listsGetContactCount: outputKinds.obj, + listsRemoveContacts: outputKinds.job, + segmentsCreate: outputKinds.obj, + segmentsGetAll: outputKinds.obj, + segmentsGet: outputKinds.obj, + segmentsUpdate: outputKinds.obj, + segmentsRemove: outputKinds.empty, + segmentsRefresh: outputKinds.obj, + fieldsGetAll: outputKinds.obj, + fieldsCreate: outputKinds.obj, + fieldsUpdate: outputKinds.obj, + fieldsRemove: outputKinds.empty, + sendersGetAll: outputKinds.sendersGetAll, + sendersCreate: outputKinds.obj, + sendersUpdate: outputKinds.obj, + sendersRemove: outputKinds.empty, + sendersResend: outputKinds.empty, + sendersListIdentities: outputKinds.obj, + sendersCreateIdentity: outputKinds.obj, + sendersGetIdentity: outputKinds.obj, + templatesCreate: outputKinds.obj, + templatesGetAll: outputKinds.obj, + templatesGet: outputKinds.obj, + templatesUpdate: outputKinds.obj, + templatesRemove: outputKinds.empty, + templatesCreateVersion: outputKinds.obj, + templatesGetVersion: outputKinds.obj, + templatesUpdateVersion: outputKinds.obj, + templatesRemoveVersion: outputKinds.empty, + templatesActivateVersion: outputKinds.obj, + suppressionsGetBounces: outputKinds.bounces, + suppressionsGetBounce: outputKinds.arr, + suppressionsDeleteBounce: outputKinds.empty, + suppressionsDeleteBounces: outputKinds.empty, + suppressionsGetBlocks: outputKinds.results, + suppressionsGetBlock: outputKinds.arr, + suppressionsDeleteBlock: outputKinds.empty, + suppressionsDeleteBlocks: outputKinds.empty, + suppressionsGetSpamReports: outputKinds.results, + suppressionsGetSpamReport: outputKinds.arr, + suppressionsDeleteSpamReport: outputKinds.empty, + suppressionsDeleteSpamReports: outputKinds.empty, + suppressionsGetInvalidEmails: outputKinds.results, + suppressionsGetInvalidEmail: outputKinds.arr, + suppressionsDeleteInvalidEmail: outputKinds.empty, + suppressionsDeleteInvalidEmails: outputKinds.empty, + suppressionsGetGlobalUnsubscribes: outputKinds.results, + suppressionsAddGlobalUnsubscribes: outputKinds.obj, + suppressionsGetGlobalUnsubscribe: outputKinds.obj, + suppressionsDeleteGlobalUnsubscribe: outputKinds.empty, + asmGetGroups: outputKinds.arr, + asmCreateGroup: outputKinds.obj, + asmGetGroup: outputKinds.obj, + asmUpdateGroup: outputKinds.obj, + asmDeleteGroup: outputKinds.empty, + asmAddGroupSuppressions: outputKinds.obj, + asmGetGroupSuppressions: outputKinds.arr, + asmDeleteGroupSuppression: outputKinds.empty, + statsGetGlobal: outputKinds.arr, + statsGetCategory: outputKinds.arr, + statsGetMailboxProvider: outputKinds.arr, + statsGetGeo: outputKinds.arr, + statsGetDevice: outputKinds.arr, + statsGetClient: outputKinds.arr, + userGetProfile: outputKinds.obj, + userGetAccount: outputKinds.obj, + userGetCredits: outputKinds.obj, + userGetUsername: outputKinds.obj, + userGetEmail: outputKinds.obj, + userGetScopes: outputKinds.obj, + apiKeysCreate: outputKinds.obj, + apiKeysGetAll: outputKinds.obj, + apiKeysGet: outputKinds.obj, + apiKeysUpdate: outputKinds.obj, + apiKeysRemove: outputKinds.empty, +} as const; export type SendGridEndpointInputs = { - mailSend: MailSendInput; - contactsAddOrUpdate: ContactsAddOrUpdateInput; - listsGetAll: ListsGetAllInput; - listsCreate: ListsCreateInput; - suppressionsGetBounces: SuppressionsGetBouncesInput; - sendersGetAll: SendersGetAllInput; + [K in keyof typeof SendGridEndpointInputSchemas]: z.infer< + (typeof SendGridEndpointInputSchemas)[K] + >; }; export type SendGridEndpointOutputs = { - mailSend: MailSendOutput; - contactsAddOrUpdate: ContactsAddOrUpdateOutput; - listsGetAll: ListsGetAllOutput; - listsCreate: ListsCreateOutput; - suppressionsGetBounces: SuppressionsGetBouncesOutput; - sendersGetAll: SendersGetAllOutput; + [K in keyof typeof SendGridEndpointOutputSchemas]: z.infer< + (typeof SendGridEndpointOutputSchemas)[K] + >; }; -export const SendGridEndpointInputSchemas = { - mailSend: MailSendInputSchema, - contactsAddOrUpdate: ContactsAddOrUpdateInputSchema, - listsGetAll: ListsGetAllInputSchema, - listsCreate: ListsCreateInputSchema, - suppressionsGetBounces: SuppressionsGetBouncesInputSchema, - sendersGetAll: SendersGetAllInputSchema, -} as const; - -export const SendGridEndpointOutputSchemas = { - mailSend: MailSendOutputSchema, - contactsAddOrUpdate: ContactsAddOrUpdateOutputSchema, - listsGetAll: ListsGetAllOutputSchema, - listsCreate: ListsCreateOutputSchema, - suppressionsGetBounces: SuppressionsGetBouncesOutputSchema, - sendersGetAll: SendersGetAllOutputSchema, -} as const; +export type MailSendInput = SendGridEndpointInputs['mailSend']; +export type MailSendOutput = SendGridEndpointOutputs['mailSend']; +export type ContactsAddOrUpdateInput = + SendGridEndpointInputs['contactsAddOrUpdate']; +export type ContactsAddOrUpdateOutput = + SendGridEndpointOutputs['contactsAddOrUpdate']; +export type ListsGetAllInput = SendGridEndpointInputs['listsGetAll']; +export type ListsGetAllOutput = SendGridEndpointOutputs['listsGetAll']; +export type ListsCreateInput = SendGridEndpointInputs['listsCreate']; +export type ListsCreateOutput = SendGridEndpointOutputs['listsCreate']; +export type SuppressionsGetBouncesInput = + SendGridEndpointInputs['suppressionsGetBounces']; +export type SuppressionsGetBouncesOutput = + SendGridEndpointOutputs['suppressionsGetBounces']; +export type SendersGetAllInput = SendGridEndpointInputs['sendersGetAll']; +export type SendersGetAllOutput = SendGridEndpointOutputs['sendersGetAll']; diff --git a/packages/sendgrid/index.ts b/packages/sendgrid/index.ts index 6ca68d143..4855eb66d 100644 --- a/packages/sendgrid/index.ts +++ b/packages/sendgrid/index.ts @@ -13,7 +13,21 @@ import type { RequiredPluginEndpointSchemas, } from 'corsair/core'; import { AuthMissingError } from 'corsair/core'; -import { Contacts, Lists, Mail, Senders, Suppressions } from './endpoints'; +import { SENDGRID_OPS } from './endpoints/catalog'; +import { + apiKeys, + asm, + contacts, + fields, + lists, + mail, + segments, + senders, + stats, + suppressions, + templates, + user, +} from './endpoints/handlers'; import type { SendGridEndpointInputs, SendGridEndpointOutputs, @@ -53,90 +67,48 @@ type SendGridEndpoint = >; export type SendGridEndpoints = { - mailSend: SendGridEndpoint<'mailSend'>; - contactsAddOrUpdate: SendGridEndpoint<'contactsAddOrUpdate'>; - listsGetAll: SendGridEndpoint<'listsGetAll'>; - listsCreate: SendGridEndpoint<'listsCreate'>; - suppressionsGetBounces: SendGridEndpoint<'suppressionsGetBounces'>; - sendersGetAll: SendGridEndpoint<'sendersGetAll'>; + [K in keyof SendGridEndpointInputs]: SendGridEndpoint; }; const sendGridEndpointsNested = { - mail: { - send: Mail.send, - }, - contacts: { - addOrUpdate: Contacts.addOrUpdate, - }, - lists: { - getAll: Lists.getAll, - create: Lists.create, - }, - suppressions: { - getBounces: Suppressions.getBounces, - }, - senders: { - getAll: Senders.getAll, - }, + mail, + contacts, + lists, + segments, + fields, + senders, + templates, + suppressions, + asm, + stats, + user, + apiKeys, } as const; -export const sendGridEndpointSchemas = { - 'mail.send': { - input: SendGridEndpointInputSchemas.mailSend, - output: SendGridEndpointOutputSchemas.mailSend, - }, - 'contacts.addOrUpdate': { - input: SendGridEndpointInputSchemas.contactsAddOrUpdate, - output: SendGridEndpointOutputSchemas.contactsAddOrUpdate, - }, - 'lists.getAll': { - input: SendGridEndpointInputSchemas.listsGetAll, - output: SendGridEndpointOutputSchemas.listsGetAll, - }, - 'lists.create': { - input: SendGridEndpointInputSchemas.listsCreate, - output: SendGridEndpointOutputSchemas.listsCreate, - }, - 'suppressions.getBounces': { - input: SendGridEndpointInputSchemas.suppressionsGetBounces, - output: SendGridEndpointOutputSchemas.suppressionsGetBounces, - }, - 'senders.getAll': { - input: SendGridEndpointInputSchemas.sendersGetAll, - output: SendGridEndpointOutputSchemas.sendersGetAll, - }, -} as const satisfies RequiredPluginEndpointSchemas< - typeof sendGridEndpointsNested ->; +export const sendGridEndpointSchemas = Object.fromEntries( + SENDGRID_OPS.map((op) => [ + op.nested, + { + input: + SendGridEndpointInputSchemas[ + op.key as keyof typeof SendGridEndpointInputSchemas + ], + output: + SendGridEndpointOutputSchemas[ + op.key as keyof typeof SendGridEndpointOutputSchemas + ], + }, + ]), +) as unknown as RequiredPluginEndpointSchemas; const defaultAuthType: AuthTypes = 'api_key' as const; -const sendGridEndpointMeta = { - 'mail.send': { - riskLevel: 'write', - description: 'Send an email via SendGrid Mail Send API v3', - }, - 'contacts.addOrUpdate': { - riskLevel: 'write', - description: 'Add or update contacts in SendGrid Marketing', - }, - 'lists.getAll': { - riskLevel: 'read', - description: 'Retrieve all marketing contact lists', - }, - 'lists.create': { - riskLevel: 'write', - description: 'Create a new marketing contact list', - }, - 'suppressions.getBounces': { - riskLevel: 'read', - description: 'Retrieve email bounce suppressions', - }, - 'senders.getAll': { - riskLevel: 'read', - description: 'Retrieve verified senders', - }, -} as const satisfies RequiredPluginEndpointMeta; +const sendGridEndpointMeta = Object.fromEntries( + SENDGRID_OPS.map((op) => [ + op.nested, + { riskLevel: op.risk, description: op.description }, + ]), +) as unknown as RequiredPluginEndpointMeta; export const sendGridAuthConfig = { api_key: { diff --git a/packages/sendgrid/integration.test.ts b/packages/sendgrid/integration.test.ts index 78a2cd3c0..3d5654f02 100644 --- a/packages/sendgrid/integration.test.ts +++ b/packages/sendgrid/integration.test.ts @@ -15,6 +15,9 @@ describe('SendGrid Plugin Integration', () => { expect(plugin.endpoints?.lists?.create).toBeDefined(); expect(plugin.endpoints?.suppressions?.getBounces).toBeDefined(); expect(plugin.endpoints?.senders?.getAll).toBeDefined(); + expect(plugin.endpoints?.segments?.refresh).toBeDefined(); + expect(plugin.endpoints?.apiKeys?.create).toBeDefined(); + expect(Object.keys(plugin.endpointSchemas ?? {})).toHaveLength(100); }); it('registers no webhooks', () => { diff --git a/packages/sendgrid/package.json b/packages/sendgrid/package.json index c56b0e282..b515169bf 100644 --- a/packages/sendgrid/package.json +++ b/packages/sendgrid/package.json @@ -1,7 +1,7 @@ { "name": "@corsair-dev/sendgrid", "version": "0.1.0", - "description": "SendGrid plugin for Corsair", + "description": "SendGrid v3 plugin for Corsair (100 REST endpoints)", "type": "module", "main": "./dist/index.js", "module": "./dist/index.js", diff --git a/packages/sendgrid/schema/database.ts b/packages/sendgrid/schema/database.ts index f9ef2a557..32c99e573 100644 --- a/packages/sendgrid/schema/database.ts +++ b/packages/sendgrid/schema/database.ts @@ -8,7 +8,7 @@ import { z } from 'zod'; export const SendGridContact = z .object({ id: z.string().optional(), - email: z.string().optional(), + email: z.string().email().optional(), phone_number_id: z.string().optional(), external_id: z.string().optional(), anonymous_id: z.string().optional(), From 02c71c9da21014198b648669d690bc5128f73ce6 Mon Sep 17 00:00:00 2001 From: Dhirender Choudhary Date: Tue, 1 Sep 2026 16:50:54 +0530 Subject: [PATCH 10/11] fix(sendgrid): satisfy RequiredPluginEndpointMeta --- packages/sendgrid/index.ts | 828 ++++++++++++++++++++++++++++++++++++- 1 file changed, 806 insertions(+), 22 deletions(-) diff --git a/packages/sendgrid/index.ts b/packages/sendgrid/index.ts index 4855eb66d..827a41c85 100644 --- a/packages/sendgrid/index.ts +++ b/packages/sendgrid/index.ts @@ -13,7 +13,6 @@ import type { RequiredPluginEndpointSchemas, } from 'corsair/core'; import { AuthMissingError } from 'corsair/core'; -import { SENDGRID_OPS } from './endpoints/catalog'; import { apiKeys, asm, @@ -85,30 +84,815 @@ const sendGridEndpointsNested = { apiKeys, } as const; -export const sendGridEndpointSchemas = Object.fromEntries( - SENDGRID_OPS.map((op) => [ - op.nested, - { - input: - SendGridEndpointInputSchemas[ - op.key as keyof typeof SendGridEndpointInputSchemas - ], - output: - SendGridEndpointOutputSchemas[ - op.key as keyof typeof SendGridEndpointOutputSchemas - ], - }, - ]), -) as unknown as RequiredPluginEndpointSchemas; +export const sendGridEndpointSchemas = { + 'mail.send': { + input: SendGridEndpointInputSchemas.mailSend, + output: SendGridEndpointOutputSchemas.mailSend, + }, + 'mail.createBatchId': { + input: SendGridEndpointInputSchemas.mailCreateBatchId, + output: SendGridEndpointOutputSchemas.mailCreateBatchId, + }, + 'mail.validateBatchId': { + input: SendGridEndpointInputSchemas.mailValidateBatchId, + output: SendGridEndpointOutputSchemas.mailValidateBatchId, + }, + 'mail.cancelScheduledSend': { + input: SendGridEndpointInputSchemas.mailCancelScheduledSend, + output: SendGridEndpointOutputSchemas.mailCancelScheduledSend, + }, + 'mail.listScheduledSends': { + input: SendGridEndpointInputSchemas.mailListScheduledSends, + output: SendGridEndpointOutputSchemas.mailListScheduledSends, + }, + 'mail.getScheduledSend': { + input: SendGridEndpointInputSchemas.mailGetScheduledSend, + output: SendGridEndpointOutputSchemas.mailGetScheduledSend, + }, + 'mail.updateScheduledSend': { + input: SendGridEndpointInputSchemas.mailUpdateScheduledSend, + output: SendGridEndpointOutputSchemas.mailUpdateScheduledSend, + }, + 'mail.deleteScheduledSend': { + input: SendGridEndpointInputSchemas.mailDeleteScheduledSend, + output: SendGridEndpointOutputSchemas.mailDeleteScheduledSend, + }, + 'contacts.addOrUpdate': { + input: SendGridEndpointInputSchemas.contactsAddOrUpdate, + output: SendGridEndpointOutputSchemas.contactsAddOrUpdate, + }, + 'contacts.get': { + input: SendGridEndpointInputSchemas.contactsGet, + output: SendGridEndpointOutputSchemas.contactsGet, + }, + 'contacts.search': { + input: SendGridEndpointInputSchemas.contactsSearch, + output: SendGridEndpointOutputSchemas.contactsSearch, + }, + 'contacts.searchEmails': { + input: SendGridEndpointInputSchemas.contactsSearchEmails, + output: SendGridEndpointOutputSchemas.contactsSearchEmails, + }, + 'contacts.remove': { + input: SendGridEndpointInputSchemas.contactsRemove, + output: SendGridEndpointOutputSchemas.contactsRemove, + }, + 'contacts.getCount': { + input: SendGridEndpointInputSchemas.contactsGetCount, + output: SendGridEndpointOutputSchemas.contactsGetCount, + }, + 'contacts.getSample': { + input: SendGridEndpointInputSchemas.contactsGetSample, + output: SendGridEndpointOutputSchemas.contactsGetSample, + }, + 'contacts.import': { + input: SendGridEndpointInputSchemas.contactsImport, + output: SendGridEndpointOutputSchemas.contactsImport, + }, + 'contacts.importStatus': { + input: SendGridEndpointInputSchemas.contactsImportStatus, + output: SendGridEndpointOutputSchemas.contactsImportStatus, + }, + 'contacts.export': { + input: SendGridEndpointInputSchemas.contactsExport, + output: SendGridEndpointOutputSchemas.contactsExport, + }, + 'contacts.exportStatus': { + input: SendGridEndpointInputSchemas.contactsExportStatus, + output: SendGridEndpointOutputSchemas.contactsExportStatus, + }, + 'contacts.listExports': { + input: SendGridEndpointInputSchemas.contactsListExports, + output: SendGridEndpointOutputSchemas.contactsListExports, + }, + 'lists.getAll': { + input: SendGridEndpointInputSchemas.listsGetAll, + output: SendGridEndpointOutputSchemas.listsGetAll, + }, + 'lists.create': { + input: SendGridEndpointInputSchemas.listsCreate, + output: SendGridEndpointOutputSchemas.listsCreate, + }, + 'lists.get': { + input: SendGridEndpointInputSchemas.listsGet, + output: SendGridEndpointOutputSchemas.listsGet, + }, + 'lists.update': { + input: SendGridEndpointInputSchemas.listsUpdate, + output: SendGridEndpointOutputSchemas.listsUpdate, + }, + 'lists.remove': { + input: SendGridEndpointInputSchemas.listsRemove, + output: SendGridEndpointOutputSchemas.listsRemove, + }, + 'lists.getContactCount': { + input: SendGridEndpointInputSchemas.listsGetContactCount, + output: SendGridEndpointOutputSchemas.listsGetContactCount, + }, + 'lists.removeContacts': { + input: SendGridEndpointInputSchemas.listsRemoveContacts, + output: SendGridEndpointOutputSchemas.listsRemoveContacts, + }, + 'segments.create': { + input: SendGridEndpointInputSchemas.segmentsCreate, + output: SendGridEndpointOutputSchemas.segmentsCreate, + }, + 'segments.getAll': { + input: SendGridEndpointInputSchemas.segmentsGetAll, + output: SendGridEndpointOutputSchemas.segmentsGetAll, + }, + 'segments.get': { + input: SendGridEndpointInputSchemas.segmentsGet, + output: SendGridEndpointOutputSchemas.segmentsGet, + }, + 'segments.update': { + input: SendGridEndpointInputSchemas.segmentsUpdate, + output: SendGridEndpointOutputSchemas.segmentsUpdate, + }, + 'segments.remove': { + input: SendGridEndpointInputSchemas.segmentsRemove, + output: SendGridEndpointOutputSchemas.segmentsRemove, + }, + 'segments.refresh': { + input: SendGridEndpointInputSchemas.segmentsRefresh, + output: SendGridEndpointOutputSchemas.segmentsRefresh, + }, + 'fields.getAll': { + input: SendGridEndpointInputSchemas.fieldsGetAll, + output: SendGridEndpointOutputSchemas.fieldsGetAll, + }, + 'fields.create': { + input: SendGridEndpointInputSchemas.fieldsCreate, + output: SendGridEndpointOutputSchemas.fieldsCreate, + }, + 'fields.update': { + input: SendGridEndpointInputSchemas.fieldsUpdate, + output: SendGridEndpointOutputSchemas.fieldsUpdate, + }, + 'fields.remove': { + input: SendGridEndpointInputSchemas.fieldsRemove, + output: SendGridEndpointOutputSchemas.fieldsRemove, + }, + 'senders.getAll': { + input: SendGridEndpointInputSchemas.sendersGetAll, + output: SendGridEndpointOutputSchemas.sendersGetAll, + }, + 'senders.create': { + input: SendGridEndpointInputSchemas.sendersCreate, + output: SendGridEndpointOutputSchemas.sendersCreate, + }, + 'senders.update': { + input: SendGridEndpointInputSchemas.sendersUpdate, + output: SendGridEndpointOutputSchemas.sendersUpdate, + }, + 'senders.remove': { + input: SendGridEndpointInputSchemas.sendersRemove, + output: SendGridEndpointOutputSchemas.sendersRemove, + }, + 'senders.resend': { + input: SendGridEndpointInputSchemas.sendersResend, + output: SendGridEndpointOutputSchemas.sendersResend, + }, + 'senders.listIdentities': { + input: SendGridEndpointInputSchemas.sendersListIdentities, + output: SendGridEndpointOutputSchemas.sendersListIdentities, + }, + 'senders.createIdentity': { + input: SendGridEndpointInputSchemas.sendersCreateIdentity, + output: SendGridEndpointOutputSchemas.sendersCreateIdentity, + }, + 'senders.getIdentity': { + input: SendGridEndpointInputSchemas.sendersGetIdentity, + output: SendGridEndpointOutputSchemas.sendersGetIdentity, + }, + 'templates.create': { + input: SendGridEndpointInputSchemas.templatesCreate, + output: SendGridEndpointOutputSchemas.templatesCreate, + }, + 'templates.getAll': { + input: SendGridEndpointInputSchemas.templatesGetAll, + output: SendGridEndpointOutputSchemas.templatesGetAll, + }, + 'templates.get': { + input: SendGridEndpointInputSchemas.templatesGet, + output: SendGridEndpointOutputSchemas.templatesGet, + }, + 'templates.update': { + input: SendGridEndpointInputSchemas.templatesUpdate, + output: SendGridEndpointOutputSchemas.templatesUpdate, + }, + 'templates.remove': { + input: SendGridEndpointInputSchemas.templatesRemove, + output: SendGridEndpointOutputSchemas.templatesRemove, + }, + 'templates.createVersion': { + input: SendGridEndpointInputSchemas.templatesCreateVersion, + output: SendGridEndpointOutputSchemas.templatesCreateVersion, + }, + 'templates.getVersion': { + input: SendGridEndpointInputSchemas.templatesGetVersion, + output: SendGridEndpointOutputSchemas.templatesGetVersion, + }, + 'templates.updateVersion': { + input: SendGridEndpointInputSchemas.templatesUpdateVersion, + output: SendGridEndpointOutputSchemas.templatesUpdateVersion, + }, + 'templates.removeVersion': { + input: SendGridEndpointInputSchemas.templatesRemoveVersion, + output: SendGridEndpointOutputSchemas.templatesRemoveVersion, + }, + 'templates.activateVersion': { + input: SendGridEndpointInputSchemas.templatesActivateVersion, + output: SendGridEndpointOutputSchemas.templatesActivateVersion, + }, + 'suppressions.getBounces': { + input: SendGridEndpointInputSchemas.suppressionsGetBounces, + output: SendGridEndpointOutputSchemas.suppressionsGetBounces, + }, + 'suppressions.getBounce': { + input: SendGridEndpointInputSchemas.suppressionsGetBounce, + output: SendGridEndpointOutputSchemas.suppressionsGetBounce, + }, + 'suppressions.deleteBounce': { + input: SendGridEndpointInputSchemas.suppressionsDeleteBounce, + output: SendGridEndpointOutputSchemas.suppressionsDeleteBounce, + }, + 'suppressions.deleteBounces': { + input: SendGridEndpointInputSchemas.suppressionsDeleteBounces, + output: SendGridEndpointOutputSchemas.suppressionsDeleteBounces, + }, + 'suppressions.getBlocks': { + input: SendGridEndpointInputSchemas.suppressionsGetBlocks, + output: SendGridEndpointOutputSchemas.suppressionsGetBlocks, + }, + 'suppressions.getBlock': { + input: SendGridEndpointInputSchemas.suppressionsGetBlock, + output: SendGridEndpointOutputSchemas.suppressionsGetBlock, + }, + 'suppressions.deleteBlock': { + input: SendGridEndpointInputSchemas.suppressionsDeleteBlock, + output: SendGridEndpointOutputSchemas.suppressionsDeleteBlock, + }, + 'suppressions.deleteBlocks': { + input: SendGridEndpointInputSchemas.suppressionsDeleteBlocks, + output: SendGridEndpointOutputSchemas.suppressionsDeleteBlocks, + }, + 'suppressions.getSpamReports': { + input: SendGridEndpointInputSchemas.suppressionsGetSpamReports, + output: SendGridEndpointOutputSchemas.suppressionsGetSpamReports, + }, + 'suppressions.getSpamReport': { + input: SendGridEndpointInputSchemas.suppressionsGetSpamReport, + output: SendGridEndpointOutputSchemas.suppressionsGetSpamReport, + }, + 'suppressions.deleteSpamReport': { + input: SendGridEndpointInputSchemas.suppressionsDeleteSpamReport, + output: SendGridEndpointOutputSchemas.suppressionsDeleteSpamReport, + }, + 'suppressions.deleteSpamReports': { + input: SendGridEndpointInputSchemas.suppressionsDeleteSpamReports, + output: SendGridEndpointOutputSchemas.suppressionsDeleteSpamReports, + }, + 'suppressions.getInvalidEmails': { + input: SendGridEndpointInputSchemas.suppressionsGetInvalidEmails, + output: SendGridEndpointOutputSchemas.suppressionsGetInvalidEmails, + }, + 'suppressions.getInvalidEmail': { + input: SendGridEndpointInputSchemas.suppressionsGetInvalidEmail, + output: SendGridEndpointOutputSchemas.suppressionsGetInvalidEmail, + }, + 'suppressions.deleteInvalidEmail': { + input: SendGridEndpointInputSchemas.suppressionsDeleteInvalidEmail, + output: SendGridEndpointOutputSchemas.suppressionsDeleteInvalidEmail, + }, + 'suppressions.deleteInvalidEmails': { + input: SendGridEndpointInputSchemas.suppressionsDeleteInvalidEmails, + output: SendGridEndpointOutputSchemas.suppressionsDeleteInvalidEmails, + }, + 'suppressions.getGlobalUnsubscribes': { + input: SendGridEndpointInputSchemas.suppressionsGetGlobalUnsubscribes, + output: SendGridEndpointOutputSchemas.suppressionsGetGlobalUnsubscribes, + }, + 'suppressions.addGlobalUnsubscribes': { + input: SendGridEndpointInputSchemas.suppressionsAddGlobalUnsubscribes, + output: SendGridEndpointOutputSchemas.suppressionsAddGlobalUnsubscribes, + }, + 'suppressions.getGlobalUnsubscribe': { + input: SendGridEndpointInputSchemas.suppressionsGetGlobalUnsubscribe, + output: SendGridEndpointOutputSchemas.suppressionsGetGlobalUnsubscribe, + }, + 'suppressions.deleteGlobalUnsubscribe': { + input: SendGridEndpointInputSchemas.suppressionsDeleteGlobalUnsubscribe, + output: SendGridEndpointOutputSchemas.suppressionsDeleteGlobalUnsubscribe, + }, + 'asm.getGroups': { + input: SendGridEndpointInputSchemas.asmGetGroups, + output: SendGridEndpointOutputSchemas.asmGetGroups, + }, + 'asm.createGroup': { + input: SendGridEndpointInputSchemas.asmCreateGroup, + output: SendGridEndpointOutputSchemas.asmCreateGroup, + }, + 'asm.getGroup': { + input: SendGridEndpointInputSchemas.asmGetGroup, + output: SendGridEndpointOutputSchemas.asmGetGroup, + }, + 'asm.updateGroup': { + input: SendGridEndpointInputSchemas.asmUpdateGroup, + output: SendGridEndpointOutputSchemas.asmUpdateGroup, + }, + 'asm.deleteGroup': { + input: SendGridEndpointInputSchemas.asmDeleteGroup, + output: SendGridEndpointOutputSchemas.asmDeleteGroup, + }, + 'asm.addGroupSuppressions': { + input: SendGridEndpointInputSchemas.asmAddGroupSuppressions, + output: SendGridEndpointOutputSchemas.asmAddGroupSuppressions, + }, + 'asm.getGroupSuppressions': { + input: SendGridEndpointInputSchemas.asmGetGroupSuppressions, + output: SendGridEndpointOutputSchemas.asmGetGroupSuppressions, + }, + 'asm.deleteGroupSuppression': { + input: SendGridEndpointInputSchemas.asmDeleteGroupSuppression, + output: SendGridEndpointOutputSchemas.asmDeleteGroupSuppression, + }, + 'stats.getGlobal': { + input: SendGridEndpointInputSchemas.statsGetGlobal, + output: SendGridEndpointOutputSchemas.statsGetGlobal, + }, + 'stats.getCategory': { + input: SendGridEndpointInputSchemas.statsGetCategory, + output: SendGridEndpointOutputSchemas.statsGetCategory, + }, + 'stats.getMailboxProvider': { + input: SendGridEndpointInputSchemas.statsGetMailboxProvider, + output: SendGridEndpointOutputSchemas.statsGetMailboxProvider, + }, + 'stats.getGeo': { + input: SendGridEndpointInputSchemas.statsGetGeo, + output: SendGridEndpointOutputSchemas.statsGetGeo, + }, + 'stats.getDevice': { + input: SendGridEndpointInputSchemas.statsGetDevice, + output: SendGridEndpointOutputSchemas.statsGetDevice, + }, + 'stats.getClient': { + input: SendGridEndpointInputSchemas.statsGetClient, + output: SendGridEndpointOutputSchemas.statsGetClient, + }, + 'user.getProfile': { + input: SendGridEndpointInputSchemas.userGetProfile, + output: SendGridEndpointOutputSchemas.userGetProfile, + }, + 'user.getAccount': { + input: SendGridEndpointInputSchemas.userGetAccount, + output: SendGridEndpointOutputSchemas.userGetAccount, + }, + 'user.getCredits': { + input: SendGridEndpointInputSchemas.userGetCredits, + output: SendGridEndpointOutputSchemas.userGetCredits, + }, + 'user.getUsername': { + input: SendGridEndpointInputSchemas.userGetUsername, + output: SendGridEndpointOutputSchemas.userGetUsername, + }, + 'user.getEmail': { + input: SendGridEndpointInputSchemas.userGetEmail, + output: SendGridEndpointOutputSchemas.userGetEmail, + }, + 'user.getScopes': { + input: SendGridEndpointInputSchemas.userGetScopes, + output: SendGridEndpointOutputSchemas.userGetScopes, + }, + 'apiKeys.create': { + input: SendGridEndpointInputSchemas.apiKeysCreate, + output: SendGridEndpointOutputSchemas.apiKeysCreate, + }, + 'apiKeys.getAll': { + input: SendGridEndpointInputSchemas.apiKeysGetAll, + output: SendGridEndpointOutputSchemas.apiKeysGetAll, + }, + 'apiKeys.get': { + input: SendGridEndpointInputSchemas.apiKeysGet, + output: SendGridEndpointOutputSchemas.apiKeysGet, + }, + 'apiKeys.update': { + input: SendGridEndpointInputSchemas.apiKeysUpdate, + output: SendGridEndpointOutputSchemas.apiKeysUpdate, + }, + 'apiKeys.remove': { + input: SendGridEndpointInputSchemas.apiKeysRemove, + output: SendGridEndpointOutputSchemas.apiKeysRemove, + }, +} as const satisfies RequiredPluginEndpointSchemas< + typeof sendGridEndpointsNested +>; const defaultAuthType: AuthTypes = 'api_key' as const; -const sendGridEndpointMeta = Object.fromEntries( - SENDGRID_OPS.map((op) => [ - op.nested, - { riskLevel: op.risk, description: op.description }, - ]), -) as unknown as RequiredPluginEndpointMeta; +const sendGridEndpointMeta = { + 'mail.send': { + riskLevel: 'write', + description: 'Send an email via SendGrid Mail Send API v3', + }, + 'mail.createBatchId': { + riskLevel: 'write', + description: 'Create a batch ID for scheduled mail', + }, + 'mail.validateBatchId': { + riskLevel: 'read', + description: 'Validate a mail batch ID', + }, + 'mail.cancelScheduledSend': { + riskLevel: 'write', + description: 'Cancel or pause a scheduled send', + }, + 'mail.listScheduledSends': { + riskLevel: 'read', + description: 'Retrieve all scheduled sends', + }, + 'mail.getScheduledSend': { + riskLevel: 'read', + description: 'Retrieve a scheduled send by batch ID', + }, + 'mail.updateScheduledSend': { + riskLevel: 'write', + description: 'Update a scheduled send status', + }, + 'mail.deleteScheduledSend': { + riskLevel: 'write', + description: 'Delete a cancellation/pause for a scheduled send', + }, + 'contacts.addOrUpdate': { + riskLevel: 'write', + description: 'Add or update contacts in SendGrid Marketing', + }, + 'contacts.get': { + riskLevel: 'read', + description: 'Get a marketing contact by ID', + }, + 'contacts.search': { + riskLevel: 'read', + description: 'Search marketing contacts with SGQL', + }, + 'contacts.searchEmails': { + riskLevel: 'read', + description: 'Search marketing contacts by email', + }, + 'contacts.remove': { + riskLevel: 'write', + description: 'Delete marketing contacts by ID', + }, + 'contacts.getCount': { + riskLevel: 'read', + description: 'Get marketing contact count', + }, + 'contacts.getSample': { + riskLevel: 'read', + description: 'Get a sample of marketing contacts', + }, + 'contacts.import': { + riskLevel: 'write', + description: 'Create a marketing contacts import job', + }, + 'contacts.importStatus': { + riskLevel: 'read', + description: 'Get a marketing contacts import job', + }, + 'contacts.export': { + riskLevel: 'write', + description: 'Create a marketing contacts export job', + }, + 'contacts.exportStatus': { + riskLevel: 'read', + description: 'Get a marketing contacts export job', + }, + 'contacts.listExports': { + riskLevel: 'read', + description: 'List marketing contacts export jobs', + }, + 'lists.getAll': { + riskLevel: 'read', + description: 'Retrieve all marketing contact lists', + }, + 'lists.create': { + riskLevel: 'write', + description: 'Create a new marketing contact list', + }, + 'lists.get': { + riskLevel: 'read', + description: 'Get a marketing list by ID', + }, + 'lists.update': { + riskLevel: 'write', + description: 'Update a marketing list', + }, + 'lists.remove': { + riskLevel: 'write', + description: 'Delete a marketing list', + }, + 'lists.getContactCount': { + riskLevel: 'read', + description: 'Get contact count for a marketing list', + }, + 'lists.removeContacts': { + riskLevel: 'write', + description: 'Remove contacts from a marketing list', + }, + 'segments.create': { + riskLevel: 'write', + description: 'Create a Marketing Campaigns segment 2.0', + }, + 'segments.getAll': { + riskLevel: 'read', + description: 'Get all Marketing Campaigns segments 2.0', + }, + 'segments.get': { + riskLevel: 'read', + description: 'Get a Marketing Campaigns segment 2.0', + }, + 'segments.update': { + riskLevel: 'write', + description: 'Update a Marketing Campaigns segment 2.0', + }, + 'segments.remove': { + riskLevel: 'write', + description: 'Delete a Marketing Campaigns segment 2.0', + }, + 'segments.refresh': { + riskLevel: 'write', + description: 'Manually refresh a Marketing Campaigns segment 2.0', + }, + 'fields.getAll': { + riskLevel: 'read', + description: 'Get all marketing field definitions', + }, + 'fields.create': { + riskLevel: 'write', + description: 'Create a custom field definition', + }, + 'fields.update': { + riskLevel: 'write', + description: 'Update a custom field definition', + }, + 'fields.remove': { + riskLevel: 'write', + description: 'Delete a custom field definition', + }, + 'senders.getAll': { + riskLevel: 'read', + description: 'Retrieve verified senders', + }, + 'senders.create': { + riskLevel: 'write', + description: 'Create a verified sender', + }, + 'senders.update': { + riskLevel: 'write', + description: 'Update a verified sender', + }, + 'senders.remove': { + riskLevel: 'write', + description: 'Delete a verified sender', + }, + 'senders.resend': { + riskLevel: 'write', + description: 'Resend verified sender verification', + }, + 'senders.listIdentities': { + riskLevel: 'read', + description: 'Get Marketing Campaigns sender identities', + }, + 'senders.createIdentity': { + riskLevel: 'write', + description: 'Create a Marketing Campaigns sender identity', + }, + 'senders.getIdentity': { + riskLevel: 'read', + description: 'Get a Marketing Campaigns sender identity', + }, + 'templates.create': { + riskLevel: 'write', + description: 'Create a transactional template', + }, + 'templates.getAll': { + riskLevel: 'read', + description: 'Get all transactional templates', + }, + 'templates.get': { + riskLevel: 'read', + description: 'Get a transactional template', + }, + 'templates.update': { + riskLevel: 'write', + description: 'Update a transactional template', + }, + 'templates.remove': { + riskLevel: 'write', + description: 'Delete a transactional template', + }, + 'templates.createVersion': { + riskLevel: 'write', + description: 'Create a transactional template version', + }, + 'templates.getVersion': { + riskLevel: 'read', + description: 'Get a transactional template version', + }, + 'templates.updateVersion': { + riskLevel: 'write', + description: 'Update a transactional template version', + }, + 'templates.removeVersion': { + riskLevel: 'write', + description: 'Delete a transactional template version', + }, + 'templates.activateVersion': { + riskLevel: 'write', + description: 'Activate a transactional template version', + }, + 'suppressions.getBounces': { + riskLevel: 'read', + description: 'Retrieve email bounce suppressions', + }, + 'suppressions.getBounce': { + riskLevel: 'read', + description: 'Retrieve a bounce by email', + }, + 'suppressions.deleteBounce': { + riskLevel: 'write', + description: 'Delete a bounce by email', + }, + 'suppressions.deleteBounces': { + riskLevel: 'write', + description: 'Delete bounce suppressions', + }, + 'suppressions.getBlocks': { + riskLevel: 'read', + description: 'Retrieve blocked emails', + }, + 'suppressions.getBlock': { + riskLevel: 'read', + description: 'Retrieve a block by email', + }, + 'suppressions.deleteBlock': { + riskLevel: 'write', + description: 'Delete a block by email', + }, + 'suppressions.deleteBlocks': { + riskLevel: 'write', + description: 'Delete blocked emails', + }, + 'suppressions.getSpamReports': { + riskLevel: 'read', + description: 'Retrieve spam reports', + }, + 'suppressions.getSpamReport': { + riskLevel: 'read', + description: 'Retrieve a spam report by email', + }, + 'suppressions.deleteSpamReport': { + riskLevel: 'write', + description: 'Delete a spam report by email', + }, + 'suppressions.deleteSpamReports': { + riskLevel: 'write', + description: 'Delete spam reports', + }, + 'suppressions.getInvalidEmails': { + riskLevel: 'read', + description: 'Retrieve invalid emails', + }, + 'suppressions.getInvalidEmail': { + riskLevel: 'read', + description: 'Retrieve an invalid email', + }, + 'suppressions.deleteInvalidEmail': { + riskLevel: 'write', + description: 'Delete an invalid email', + }, + 'suppressions.deleteInvalidEmails': { + riskLevel: 'write', + description: 'Delete invalid emails', + }, + 'suppressions.getGlobalUnsubscribes': { + riskLevel: 'read', + description: 'Retrieve global unsubscribes', + }, + 'suppressions.addGlobalUnsubscribes': { + riskLevel: 'write', + description: 'Add emails to the global unsubscribe list', + }, + 'suppressions.getGlobalUnsubscribe': { + riskLevel: 'read', + description: 'Retrieve a global unsubscribe by email', + }, + 'suppressions.deleteGlobalUnsubscribe': { + riskLevel: 'write', + description: 'Delete a global unsubscribe by email', + }, + 'asm.getGroups': { + riskLevel: 'read', + description: 'Retrieve unsubscribe groups', + }, + 'asm.createGroup': { + riskLevel: 'write', + description: 'Create an unsubscribe group', + }, + 'asm.getGroup': { + riskLevel: 'read', + description: 'Retrieve an unsubscribe group', + }, + 'asm.updateGroup': { + riskLevel: 'write', + description: 'Update an unsubscribe group', + }, + 'asm.deleteGroup': { + riskLevel: 'write', + description: 'Delete an unsubscribe group', + }, + 'asm.addGroupSuppressions': { + riskLevel: 'write', + description: 'Add suppressions to an unsubscribe group', + }, + 'asm.getGroupSuppressions': { + riskLevel: 'read', + description: 'Retrieve suppressions for an unsubscribe group', + }, + 'asm.deleteGroupSuppression': { + riskLevel: 'write', + description: 'Delete a suppression from an unsubscribe group', + }, + 'stats.getGlobal': { + riskLevel: 'read', + description: 'Retrieve global email statistics', + }, + 'stats.getCategory': { + riskLevel: 'read', + description: 'Retrieve category statistics', + }, + 'stats.getMailboxProvider': { + riskLevel: 'read', + description: 'Retrieve mailbox provider statistics', + }, + 'stats.getGeo': { + riskLevel: 'read', + description: 'Retrieve geographic statistics', + }, + 'stats.getDevice': { + riskLevel: 'read', + description: 'Retrieve device statistics', + }, + 'stats.getClient': { + riskLevel: 'read', + description: 'Retrieve email client statistics', + }, + 'user.getProfile': { + riskLevel: 'read', + description: 'Retrieve the user profile', + }, + 'user.getAccount': { + riskLevel: 'read', + description: 'Retrieve the user account', + }, + 'user.getCredits': { + riskLevel: 'read', + description: 'Retrieve remaining email credits', + }, + 'user.getUsername': { + riskLevel: 'read', + description: 'Retrieve the account username', + }, + 'user.getEmail': { + riskLevel: 'read', + description: 'Retrieve the account email address', + }, + 'user.getScopes': { + riskLevel: 'read', + description: 'Retrieve API key scopes for the current key', + }, + 'apiKeys.create': { + riskLevel: 'write', + description: 'Create an API key', + }, + 'apiKeys.getAll': { + riskLevel: 'read', + description: 'Retrieve all API keys', + }, + 'apiKeys.get': { + riskLevel: 'read', + description: 'Retrieve an API key', + }, + 'apiKeys.update': { + riskLevel: 'write', + description: 'Update an API key name or scopes', + }, + 'apiKeys.remove': { + riskLevel: 'write', + description: 'Delete an API key', + }, +} as const satisfies RequiredPluginEndpointMeta; export const sendGridAuthConfig = { api_key: { From dfe25fbd12331d06bf4434bc6cc3d67aca13eb8c Mon Sep 17 00:00:00 2001 From: Dhirender Choudhary Date: Tue, 1 Sep 2026 16:57:19 +0530 Subject: [PATCH 11/11] fix(sendgrid): stop compounding 429 retries --- packages/sendgrid/api.test.ts | 19 +++++++++++++++++++ packages/sendgrid/client.ts | 21 +++++++++++++++++++-- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/packages/sendgrid/api.test.ts b/packages/sendgrid/api.test.ts index a4527756e..b70ce4d09 100644 --- a/packages/sendgrid/api.test.ts +++ b/packages/sendgrid/api.test.ts @@ -300,6 +300,25 @@ describe('SendGrid Endpoints Execution & Error Policies', () => { expect(policy.maxRetries).toBe(3); expect(policy.headersRetryAfterMs).toBe(45000); }); + + it('does not retry 429 in the HTTP client', async () => { + (global.fetch as jest.Mock).mockResolvedValue( + mockResponse( + 429, + { errors: [{ message: 'Too Many Requests' }] }, + { 'Retry-After': '45' }, + ), + ); + + await expect( + makeSendGridRequest('mail/send', 'SG.key', { method: 'POST' }), + ).rejects.toMatchObject({ + name: 'SendGridAPIError', + status: 429, + retryAfter: 45000, + }); + expect(global.fetch).toHaveBeenCalledTimes(1); + }); }); describeLive('SendGrid live API', () => { diff --git a/packages/sendgrid/client.ts b/packages/sendgrid/client.ts index 9eabfb010..309f5b231 100644 --- a/packages/sendgrid/client.ts +++ b/packages/sendgrid/client.ts @@ -1,4 +1,8 @@ -import type { ApiRequestOptions, OpenAPIConfig } from 'corsair/http'; +import type { + ApiRequestOptions, + OpenAPIConfig, + RateLimitConfig, +} from 'corsair/http'; import { ApiError, request } from 'corsair/http'; export class SendGridAPIError extends Error { @@ -16,6 +20,17 @@ export class SendGridAPIError extends Error { const SENDGRID_API_BASE = 'https://api.sendgrid.com/v3'; +/** Parse Retry-After on 429; do not retry here. Endpoint policy owns retries. */ +const SENDGRID_RATE_LIMIT_CONFIG: RateLimitConfig = { + enabled: true, + maxRetries: 0, + initialRetryDelay: 0, + backoffMultiplier: 1, + headerNames: { + retryAfter: 'Retry-After', + }, +}; + export async function makeSendGridRequest( endpoint: string, apiKey: string, @@ -59,7 +74,9 @@ export async function makeSendGridRequest( }; try { - return await request(config, requestOptions); + return await request(config, requestOptions, { + rateLimitConfig: SENDGRID_RATE_LIMIT_CONFIG, + }); } catch (error) { if (error instanceof ApiError) { const bodyObj =