@automate.ax/integration-contracts 0.114.4 → 0.115.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,449 +1,320 @@
1
1
  import * as z from "zod"
2
2
 
3
- export const BREVO_TRANSACTIONAL_EVENT_TYPES = [
3
+ export const BREVO_EVENT_FAMILIES = [
4
+ "transactionalEmail",
5
+ "transactionalSms",
6
+ "marketingEmail",
7
+ "marketingSms",
8
+ "contact",
9
+ ] as const
10
+
11
+ export const BREVO_TRANSACTIONAL_EMAIL_EVENT_TYPES = [
12
+ "sent",
13
+ "delivered",
14
+ "hardBounced",
15
+ "softBounced",
4
16
  "blocked",
5
- "click",
17
+ "markedAsSpam",
18
+ "invalid",
6
19
  "deferred",
7
- "delivered",
8
- "error",
9
- "hard_bounce",
10
- "invalid_email",
20
+ "clicked",
11
21
  "opened",
12
- "request",
13
- "soft_bounce",
14
- "spam",
15
- "unique_opened",
22
+ "firstOpened",
16
23
  "unsubscribed",
17
24
  ] as const
18
25
 
19
- const BREVO_PROVIDER_EVENT_BASE_SCHEMA = z
20
- .looseObject({
21
- /** Custom header supplied with the original email. */
22
- "X-Mailin-custom": z.string().optional(),
23
-
24
- /** Provider-local formatted date for the event. */
25
- date: z.string().optional(),
26
-
27
- /** Recipient email address. */
28
- email: z.string().optional(),
29
-
30
- /** Brevo webhook ID that delivered the event. */
31
- id: z.number().int().nonnegative(),
32
-
33
- /** Provider message identity. */
34
- "message-id": z.string().optional(),
35
-
36
- /** Email subject. */
37
- subject: z.string().optional(),
38
-
39
- /** Tags supplied with the original email. */
40
- tags: z.string().array().optional(),
41
-
42
- /** Internal template ID used to send the email. */
43
- template_id: z.number().int().nonnegative().optional(),
44
-
45
- /** Unix timestamp in seconds for the underlying email event. */
46
- ts: z.number().int().nonnegative().optional(),
47
-
48
- /** Unix timestamp in seconds when Brevo emitted the webhook event. */
49
- ts_event: z.number().int().nonnegative(),
50
- })
51
- .catchall(z.json())
52
-
53
- const BREVO_PROVIDER_DELIVERY_METADATA_SCHEMA = z.object({
54
- /** Sending IP used for the message. */
55
- sending_ip: z.string().optional(),
56
-
57
- /** Provider timestamp in milliseconds. */
58
- ts_epoch: z.number().int().nonnegative().optional(),
59
- })
60
-
61
- const BREVO_PROVIDER_ENGAGEMENT_METADATA_SCHEMA = z.object({
62
- /** Brevo contact ID, or zero when the recipient is not a contact. */
63
- contact_id: z.number().int().nonnegative().optional(),
64
-
65
- /** Device category reported for the interaction. */
66
- device_used: z.string().optional(),
67
-
68
- /** Brevo log preview URL. */
69
- mirror_link: z.string().optional(),
70
-
71
- /** Browser or email-client user agent. */
72
- user_agent: z.string().optional(),
73
- })
74
-
75
- const BREVO_PROVIDER_SENT_EVENT_SCHEMA =
76
- BREVO_PROVIDER_EVENT_BASE_SCHEMA.extend({
77
- ...BREVO_PROVIDER_DELIVERY_METADATA_SCHEMA.shape,
78
- ...BREVO_PROVIDER_ENGAGEMENT_METADATA_SCHEMA.pick({
79
- contact_id: true,
80
- mirror_link: true,
81
- }).shape,
82
- event: z.literal("request"),
83
- })
84
-
85
- const BREVO_PROVIDER_DELIVERED_EVENT_SCHEMA =
86
- BREVO_PROVIDER_EVENT_BASE_SCHEMA.extend({
87
- ...BREVO_PROVIDER_DELIVERY_METADATA_SCHEMA.shape,
88
- event: z.literal("delivered"),
89
- })
90
-
91
- const BREVO_PROVIDER_HARD_BOUNCED_EVENT_SCHEMA =
92
- BREVO_PROVIDER_EVENT_BASE_SCHEMA.extend({
93
- ...BREVO_PROVIDER_DELIVERY_METADATA_SCHEMA.shape,
94
- event: z.literal("hard_bounce"),
95
- reason: z.string().optional(),
96
- })
97
-
98
- const BREVO_PROVIDER_SOFT_BOUNCED_EVENT_SCHEMA =
99
- BREVO_PROVIDER_EVENT_BASE_SCHEMA.extend({
100
- ...BREVO_PROVIDER_DELIVERY_METADATA_SCHEMA.shape,
101
- event: z.literal("soft_bounce"),
102
- reason: z.string().optional(),
103
- })
104
-
105
- const BREVO_PROVIDER_BLOCKED_EVENT_SCHEMA =
106
- BREVO_PROVIDER_EVENT_BASE_SCHEMA.extend({
107
- event: z.literal("blocked"),
108
- ts_epoch: z.number().int().nonnegative().optional(),
109
- })
110
-
111
- const BREVO_PROVIDER_DEFERRED_EVENT_SCHEMA =
112
- BREVO_PROVIDER_EVENT_BASE_SCHEMA.extend({
113
- ...BREVO_PROVIDER_DELIVERY_METADATA_SCHEMA.shape,
114
- event: z.literal("deferred"),
115
- reason: z.string().optional(),
116
- })
117
-
118
- const BREVO_PROVIDER_OPENED_EVENT_SCHEMA =
119
- BREVO_PROVIDER_EVENT_BASE_SCHEMA.extend({
120
- ...BREVO_PROVIDER_DELIVERY_METADATA_SCHEMA.shape,
121
- ...BREVO_PROVIDER_ENGAGEMENT_METADATA_SCHEMA.shape,
122
- event: z.literal("opened"),
123
- })
124
-
125
- const BREVO_PROVIDER_UNIQUE_OPENED_EVENT_SCHEMA =
126
- BREVO_PROVIDER_EVENT_BASE_SCHEMA.extend({
127
- ...BREVO_PROVIDER_DELIVERY_METADATA_SCHEMA.shape,
128
- ...BREVO_PROVIDER_ENGAGEMENT_METADATA_SCHEMA.shape,
129
- event: z.literal("unique_opened"),
130
- })
131
-
132
- const BREVO_PROVIDER_CLICKED_EVENT_SCHEMA =
133
- BREVO_PROVIDER_EVENT_BASE_SCHEMA.extend({
134
- ...BREVO_PROVIDER_DELIVERY_METADATA_SCHEMA.shape,
135
- ...BREVO_PROVIDER_ENGAGEMENT_METADATA_SCHEMA.shape,
136
- event: z.literal("click"),
137
- link: z.string().optional(),
138
- })
139
-
140
- const BREVO_PROVIDER_SPAM_EVENT_SCHEMA =
141
- BREVO_PROVIDER_EVENT_BASE_SCHEMA.extend({ event: z.literal("spam") })
142
-
143
- const BREVO_PROVIDER_UNSUBSCRIBED_EVENT_SCHEMA =
144
- BREVO_PROVIDER_EVENT_BASE_SCHEMA.extend({
145
- ...BREVO_PROVIDER_DELIVERY_METADATA_SCHEMA.shape,
146
- ...BREVO_PROVIDER_ENGAGEMENT_METADATA_SCHEMA.shape,
147
- event: z.literal("unsubscribed"),
148
- tag: z.string().optional(),
149
- })
26
+ export const BREVO_TRANSACTIONAL_SMS_EVENT_TYPES = [
27
+ "sent",
28
+ "delivered",
29
+ "hardBounced",
30
+ "softBounced",
31
+ "replied",
32
+ "unsubscribed",
33
+ ] as const
150
34
 
151
- const BREVO_PROVIDER_INVALID_EMAIL_EVENT_SCHEMA =
152
- BREVO_PROVIDER_EVENT_BASE_SCHEMA.extend({
153
- event: z.literal("invalid_email"),
154
- ts_epoch: z.number().int().nonnegative().optional(),
155
- })
35
+ export const BREVO_MARKETING_EMAIL_EVENT_TYPES = [
36
+ "delivered",
37
+ "hardBounced",
38
+ "softBounced",
39
+ "markedAsSpam",
40
+ "opened",
41
+ "clicked",
42
+ "unsubscribed",
43
+ ] as const
156
44
 
157
- const BREVO_PROVIDER_ERROR_EVENT_SCHEMA =
158
- BREVO_PROVIDER_EVENT_BASE_SCHEMA.extend({
159
- event: z.literal("error"),
160
- ts_epoch: z.number().int().nonnegative().optional(),
161
- })
45
+ export const BREVO_CONTACT_EVENT_TYPES = [
46
+ "addedToList",
47
+ "updated",
48
+ "deleted",
49
+ ] as const
162
50
 
163
- const BREVO_PROVIDER_TRANSACTIONAL_EVENT_SCHEMA = z.discriminatedUnion(
164
- "event",
165
- [
166
- BREVO_PROVIDER_BLOCKED_EVENT_SCHEMA,
167
- BREVO_PROVIDER_CLICKED_EVENT_SCHEMA,
168
- BREVO_PROVIDER_DEFERRED_EVENT_SCHEMA,
169
- BREVO_PROVIDER_DELIVERED_EVENT_SCHEMA,
170
- BREVO_PROVIDER_ERROR_EVENT_SCHEMA,
171
- BREVO_PROVIDER_HARD_BOUNCED_EVENT_SCHEMA,
172
- BREVO_PROVIDER_INVALID_EMAIL_EVENT_SCHEMA,
173
- BREVO_PROVIDER_OPENED_EVENT_SCHEMA,
174
- BREVO_PROVIDER_SENT_EVENT_SCHEMA,
175
- BREVO_PROVIDER_SOFT_BOUNCED_EVENT_SCHEMA,
176
- BREVO_PROVIDER_SPAM_EVENT_SCHEMA,
177
- BREVO_PROVIDER_UNIQUE_OPENED_EVENT_SCHEMA,
178
- BREVO_PROVIDER_UNSUBSCRIBED_EVENT_SCHEMA,
179
- ],
180
- )
51
+ export const BREVO_MARKETING_SMS_EVENT_TYPES = [
52
+ "sent",
53
+ "delivered",
54
+ "hardBounced",
55
+ "softBounced",
56
+ "replied",
57
+ "unsubscribed",
58
+ ] as const
181
59
 
182
- const BREVO_EVENT_BASE_SCHEMA = z.object({
183
- /** Custom header supplied with the original email. */
60
+ const BREVO_NORMALIZED_EVENT_FIELDS = {
61
+ bounceType: z.string().optional(),
62
+ campaignId: z.number().int().nonnegative().optional(),
63
+ campaignName: z.string().optional(),
64
+ contactId: z.number().int().nonnegative().optional(),
65
+ content: z.json().optional(),
66
+ creditsUsed: z.number().nonnegative().optional(),
184
67
  customHeader: z.string().optional(),
185
-
186
- /** Provider-local formatted date for the event. */
187
68
  date: z.string().optional(),
188
-
189
- /** Recipient email address. */
69
+ dateEvent: z.string().optional(),
70
+ dateSent: z.string().optional(),
71
+ description: z.string().optional(),
72
+ deviceUsed: z.string().optional(),
190
73
  email: z.string().optional(),
191
-
192
- /** Provider message identity. */
193
- messageId: z.string().optional(),
194
-
195
- /** Unix timestamp in seconds when Brevo emitted the webhook event. */
196
- occurredAt: z.number().int().nonnegative(),
197
-
198
- /** Email subject. */
74
+ errorCode: z.number().int().optional(),
75
+ event: z.record(z.string(), z.json()),
76
+ key: z.string().optional(),
77
+ link: z.string().optional(),
78
+ listIds: z.number().int().nonnegative().array().optional(),
79
+ messageId: z.union([z.string(), z.number().int()]).optional(),
80
+ mirrorLink: z.string().optional(),
81
+ occurredAt: z.number().int().nonnegative().optional(),
82
+ phone: z.string().optional(),
83
+ reason: z.string().optional(),
84
+ reference: z.union([z.string(), z.record(z.string(), z.string())]).optional(),
85
+ remainingCredit: z.number().nonnegative().optional(),
86
+ reply: z.string().optional(),
87
+ segmentIds: z.number().int().nonnegative().array().optional(),
88
+ senderEmail: z.string().optional(),
89
+ sendingIp: z.string().optional(),
90
+ smsCount: z.number().int().nonnegative().optional(),
91
+ status: z.string().optional(),
199
92
  subject: z.string().optional(),
200
-
201
- /** Tags supplied with the original email. */
93
+ tag: z.union([z.string(), z.string().array()]).optional(),
202
94
  tags: z.string().array().optional(),
203
-
204
- /** Internal template ID used to send the email. */
205
95
  templateId: z.number().int().nonnegative().optional(),
206
-
207
- /** Unix timestamp in seconds for the underlying email event. */
208
96
  timestamp: z.number().int().nonnegative().optional(),
209
-
210
- /** Brevo webhook ID that delivered the event. */
211
- webhookId: z.number().int().nonnegative(),
212
- })
213
-
214
- const BREVO_DELIVERY_METADATA_SCHEMA = z.object({
215
- /** Sending IP used for the message. */
216
- sendingIp: z.string().optional(),
217
-
218
- /** Provider timestamp in milliseconds. */
219
97
  timestampMilliseconds: z.number().int().nonnegative().optional(),
220
- })
221
-
222
- const BREVO_ENGAGEMENT_METADATA_SCHEMA = z.object({
223
- /** Brevo contact ID, or zero when the recipient is not a contact. */
224
- contactId: z.number().int().nonnegative().optional(),
225
-
226
- /** Device category reported for the interaction. */
227
- deviceUsed: z.string().optional(),
228
-
229
- /** Brevo log preview URL. */
230
- mirrorLink: z.string().optional(),
231
-
232
- /** Browser or email-client user agent. */
98
+ timestampSent: z.number().int().nonnegative().optional(),
233
99
  userAgent: z.string().optional(),
234
- })
235
-
236
- export const BREVO_EMAIL_SENT_EVENT_SCHEMA = BREVO_EVENT_BASE_SCHEMA.extend({
237
- ...BREVO_DELIVERY_METADATA_SCHEMA.shape,
238
- ...BREVO_ENGAGEMENT_METADATA_SCHEMA.pick({
239
- contactId: true,
240
- mirrorLink: true,
241
- }).shape,
242
-
243
- /** Original provider webhook payload. */
244
- event: BREVO_PROVIDER_SENT_EVENT_SCHEMA,
245
-
246
- /** Brevo transactional event category. */
247
- type: z.literal("request"),
248
- })
249
-
250
- export const BREVO_EMAIL_DELIVERED_EVENT_SCHEMA =
251
- BREVO_EVENT_BASE_SCHEMA.extend({
252
- ...BREVO_DELIVERY_METADATA_SCHEMA.shape,
253
-
254
- /** Original provider webhook payload. */
255
- event: BREVO_PROVIDER_DELIVERED_EVENT_SCHEMA,
256
-
257
- /** Brevo transactional event category. */
258
- type: z.literal("delivered"),
259
- })
260
-
261
- export const BREVO_EMAIL_HARD_BOUNCED_EVENT_SCHEMA =
262
- BREVO_EVENT_BASE_SCHEMA.extend({
263
- ...BREVO_DELIVERY_METADATA_SCHEMA.shape,
264
-
265
- /** Original provider webhook payload. */
266
- event: BREVO_PROVIDER_HARD_BOUNCED_EVENT_SCHEMA,
267
-
268
- /** Provider explanation for the permanent delivery failure. */
269
- reason: z.string().optional(),
270
-
271
- /** Brevo transactional event category. */
272
- type: z.literal("hard_bounce"),
273
- })
274
-
275
- export const BREVO_EMAIL_SOFT_BOUNCED_EVENT_SCHEMA =
276
- BREVO_EVENT_BASE_SCHEMA.extend({
277
- ...BREVO_DELIVERY_METADATA_SCHEMA.shape,
278
-
279
- /** Original provider webhook payload. */
280
- event: BREVO_PROVIDER_SOFT_BOUNCED_EVENT_SCHEMA,
281
-
282
- /** Provider explanation for the temporary delivery failure. */
283
- reason: z.string().optional(),
100
+ webhookId: z.number().int().nonnegative(),
101
+ }
284
102
 
285
- /** Brevo transactional event category. */
286
- type: z.literal("soft_bounce"),
103
+ const createBrevoFamilyEventSchema = <
104
+ const TFamily extends (typeof BREVO_EVENT_FAMILIES)[number],
105
+ const TTypes extends readonly [string, ...string[]],
106
+ >(
107
+ family: TFamily,
108
+ types: TTypes,
109
+ ) =>
110
+ z.object({
111
+ ...BREVO_NORMALIZED_EVENT_FIELDS,
112
+ family: z.literal(family),
113
+ type: z.enum(types),
287
114
  })
288
115
 
289
- export const BREVO_EMAIL_BLOCKED_EVENT_SCHEMA = BREVO_EVENT_BASE_SCHEMA.extend({
290
- /** Original provider webhook payload. */
291
- event: BREVO_PROVIDER_BLOCKED_EVENT_SCHEMA,
292
-
293
- /** Provider timestamp in milliseconds. */
294
- timestampMilliseconds: z.number().int().nonnegative().optional(),
295
-
296
- /** Brevo transactional event category. */
297
- type: z.literal("blocked"),
298
- })
299
-
300
- export const BREVO_EMAIL_DEFERRED_EVENT_SCHEMA = BREVO_EVENT_BASE_SCHEMA.extend(
301
- {
302
- ...BREVO_DELIVERY_METADATA_SCHEMA.shape,
303
-
304
- /** Original provider webhook payload. */
305
- event: BREVO_PROVIDER_DEFERRED_EVENT_SCHEMA,
306
-
307
- /** Provider explanation for the delayed delivery attempt. */
308
- reason: z.string().optional(),
309
-
310
- /** Brevo transactional event category. */
311
- type: z.literal("deferred"),
312
- },
116
+ export const BREVO_TRANSACTIONAL_EMAIL_EVENT_SCHEMA =
117
+ createBrevoFamilyEventSchema(
118
+ "transactionalEmail",
119
+ BREVO_TRANSACTIONAL_EMAIL_EVENT_TYPES,
120
+ )
121
+ export const BREVO_TRANSACTIONAL_SMS_EVENT_SCHEMA =
122
+ createBrevoFamilyEventSchema(
123
+ "transactionalSms",
124
+ BREVO_TRANSACTIONAL_SMS_EVENT_TYPES,
125
+ )
126
+ export const BREVO_MARKETING_EMAIL_EVENT_SCHEMA = createBrevoFamilyEventSchema(
127
+ "marketingEmail",
128
+ BREVO_MARKETING_EMAIL_EVENT_TYPES,
129
+ )
130
+ export const BREVO_MARKETING_SMS_EVENT_SCHEMA = createBrevoFamilyEventSchema(
131
+ "marketingSms",
132
+ BREVO_MARKETING_SMS_EVENT_TYPES,
133
+ )
134
+ export const BREVO_CONTACT_EVENT_SCHEMA = createBrevoFamilyEventSchema(
135
+ "contact",
136
+ BREVO_CONTACT_EVENT_TYPES,
313
137
  )
314
138
 
315
- const BREVO_EMAIL_OPENED_EVENT_MEMBER_SCHEMA = BREVO_EVENT_BASE_SCHEMA.extend({
316
- ...BREVO_DELIVERY_METADATA_SCHEMA.shape,
317
- ...BREVO_ENGAGEMENT_METADATA_SCHEMA.shape,
318
-
319
- /** Original provider webhook payload. */
320
- event: BREVO_PROVIDER_OPENED_EVENT_SCHEMA,
321
-
322
- /** Brevo transactional event category. */
323
- type: z.literal("opened"),
324
- })
325
-
326
- const BREVO_EMAIL_UNIQUE_OPENED_EVENT_MEMBER_SCHEMA =
327
- BREVO_EVENT_BASE_SCHEMA.extend({
328
- ...BREVO_DELIVERY_METADATA_SCHEMA.shape,
329
- ...BREVO_ENGAGEMENT_METADATA_SCHEMA.shape,
330
-
331
- /** Original provider webhook payload. */
332
- event: BREVO_PROVIDER_UNIQUE_OPENED_EVENT_SCHEMA,
333
-
334
- /** Brevo transactional event category. */
335
- type: z.literal("unique_opened"),
336
- })
337
-
338
- export const BREVO_EMAIL_OPENED_EVENT_SCHEMA = z.discriminatedUnion("type", [
339
- BREVO_EMAIL_OPENED_EVENT_MEMBER_SCHEMA,
340
- BREVO_EMAIL_UNIQUE_OPENED_EVENT_MEMBER_SCHEMA,
139
+ export const BREVO_EVENT_SCHEMA = z.discriminatedUnion("family", [
140
+ BREVO_TRANSACTIONAL_EMAIL_EVENT_SCHEMA,
141
+ BREVO_TRANSACTIONAL_SMS_EVENT_SCHEMA,
142
+ BREVO_MARKETING_EMAIL_EVENT_SCHEMA,
143
+ BREVO_MARKETING_SMS_EVENT_SCHEMA,
144
+ BREVO_CONTACT_EVENT_SCHEMA,
341
145
  ])
342
146
 
343
- export const BREVO_EMAIL_CLICKED_EVENT_SCHEMA = BREVO_EVENT_BASE_SCHEMA.extend({
344
- ...BREVO_DELIVERY_METADATA_SCHEMA.shape,
345
- ...BREVO_ENGAGEMENT_METADATA_SCHEMA.shape,
346
-
347
- /** Original provider webhook payload. */
348
- event: BREVO_PROVIDER_CLICKED_EVENT_SCHEMA,
349
-
350
- /** URL followed by the recipient. */
351
- link: z.string().optional(),
352
-
353
- /** Brevo transactional event category. */
354
- type: z.literal("click"),
355
- })
356
-
357
- export const BREVO_EMAIL_MARKED_AS_SPAM_EVENT_SCHEMA =
358
- BREVO_EVENT_BASE_SCHEMA.extend({
359
- /** Original provider webhook payload. */
360
- event: BREVO_PROVIDER_SPAM_EVENT_SCHEMA,
361
-
362
- /** Brevo transactional event category. */
363
- type: z.literal("spam"),
147
+ export const createBrevoSemanticEventSchema = <
148
+ const TFamily extends (typeof BREVO_EVENT_FAMILIES)[number],
149
+ const TType extends string,
150
+ >(
151
+ family: TFamily,
152
+ type: TType,
153
+ ) =>
154
+ z.object({
155
+ ...BREVO_NORMALIZED_EVENT_FIELDS,
156
+ family: z.literal(family),
157
+ type: z.literal(type),
364
158
  })
365
159
 
366
- export const BREVO_EMAIL_UNSUBSCRIBED_EVENT_SCHEMA =
367
- BREVO_EVENT_BASE_SCHEMA.extend({
368
- ...BREVO_DELIVERY_METADATA_SCHEMA.shape,
369
- ...BREVO_ENGAGEMENT_METADATA_SCHEMA.shape,
370
-
371
- /** Original provider webhook payload. */
372
- event: BREVO_PROVIDER_UNSUBSCRIBED_EVENT_SCHEMA,
373
-
374
- /** Provider-native serialized tag value for the unsubscribe. */
375
- tag: z.string().optional(),
376
-
377
- /** Brevo transactional event category. */
378
- type: z.literal("unsubscribed"),
160
+ const TRANSACTIONAL_EMAIL_PROVIDER_TYPES = {
161
+ blocked: "blocked",
162
+ click: "clicked",
163
+ deferred: "deferred",
164
+ delivered: "delivered",
165
+ hard_bounce: "hardBounced",
166
+ invalid_email: "invalid",
167
+ opened: "opened",
168
+ proxy_open: "opened",
169
+ request: "sent",
170
+ soft_bounce: "softBounced",
171
+ spam: "markedAsSpam",
172
+ unique_opened: "firstOpened",
173
+ unique_proxy_open: "firstOpened",
174
+ unsubscribed: "unsubscribed",
175
+ } as const
176
+ const SMS_PROVIDER_TYPES = {
177
+ delivered: "delivered",
178
+ hard_bounce: "hardBounced",
179
+ replied: "replied",
180
+ sent: "sent",
181
+ soft_bounce: "softBounced",
182
+ unsubscribed: "unsubscribed",
183
+ } as const
184
+ const MARKETING_EMAIL_PROVIDER_TYPES = {
185
+ click: "clicked",
186
+ delivered: "delivered",
187
+ hard_bounce: "hardBounced",
188
+ opened: "opened",
189
+ proxy_open: "opened",
190
+ soft_bounce: "softBounced",
191
+ spam: "markedAsSpam",
192
+ unsubscribe: "unsubscribed",
193
+ unsubscribed: "unsubscribed",
194
+ } as const
195
+ const CONTACT_PROVIDER_TYPES = {
196
+ contact_deleted: "deleted",
197
+ contact_updated: "updated",
198
+ list_addition: "addedToList",
199
+ } as const
200
+
201
+ const BREVO_PROVIDER_EVENT_SCHEMA = z
202
+ .looseObject({
203
+ camp_id: z.number().int().nonnegative().optional(),
204
+ campaign_id: z.number().int().nonnegative().optional(),
205
+ event: z.string().optional(),
206
+ id: z.number().int().nonnegative(),
207
+ msg_status: z.string().optional(),
208
+ type: z.string().optional(),
379
209
  })
380
-
381
- const BREVO_INVALID_EMAIL_EVENT_SCHEMA = BREVO_EVENT_BASE_SCHEMA.extend({
382
- /** Original provider webhook payload. */
383
- event: BREVO_PROVIDER_INVALID_EMAIL_EVENT_SCHEMA,
384
-
385
- /** Provider timestamp in milliseconds. */
386
- timestampMilliseconds: z.number().int().nonnegative().optional(),
387
-
388
- /** Brevo transactional event category. */
389
- type: z.literal("invalid_email"),
390
- })
391
-
392
- const BREVO_ERROR_EVENT_SCHEMA = BREVO_EVENT_BASE_SCHEMA.extend({
393
- /** Original provider webhook payload. */
394
- event: BREVO_PROVIDER_ERROR_EVENT_SCHEMA,
395
-
396
- /** Provider timestamp in milliseconds. */
397
- timestampMilliseconds: z.number().int().nonnegative().optional(),
398
-
399
- /** Brevo transactional event category. */
400
- type: z.literal("error"),
401
- })
402
-
403
- export const BREVO_TRANSACTIONAL_EVENT_SCHEMA = z.discriminatedUnion("type", [
404
- BREVO_EMAIL_BLOCKED_EVENT_SCHEMA,
405
- BREVO_EMAIL_CLICKED_EVENT_SCHEMA,
406
- BREVO_EMAIL_DEFERRED_EVENT_SCHEMA,
407
- BREVO_EMAIL_DELIVERED_EVENT_SCHEMA,
408
- BREVO_ERROR_EVENT_SCHEMA,
409
- BREVO_EMAIL_HARD_BOUNCED_EVENT_SCHEMA,
410
- BREVO_INVALID_EMAIL_EVENT_SCHEMA,
411
- BREVO_EMAIL_OPENED_EVENT_MEMBER_SCHEMA,
412
- BREVO_EMAIL_SENT_EVENT_SCHEMA,
413
- BREVO_EMAIL_SOFT_BOUNCED_EVENT_SCHEMA,
414
- BREVO_EMAIL_MARKED_AS_SPAM_EVENT_SCHEMA,
415
- BREVO_EMAIL_UNIQUE_OPENED_EVENT_MEMBER_SCHEMA,
416
- BREVO_EMAIL_UNSUBSCRIBED_EVENT_SCHEMA,
417
- ])
210
+ .catchall(z.json())
418
211
 
419
212
  /**
420
- * Normalizes one provider-native Brevo transactional webhook.
213
+ * Normalizes every supported Brevo managed-webhook delivery.
421
214
  *
422
- * @param value - Untrusted provider webhook payload.
215
+ * @param value - Untrusted provider callback payload.
216
+ * @param webhookId - Authenticated provider webhook registration identity.
217
+ * @throws When the payload or event discriminator is unsupported.
423
218
  */
424
- export function normalizeBrevoTransactionalEvent(value: unknown) {
425
- const event = BREVO_PROVIDER_TRANSACTIONAL_EVENT_SCHEMA.parse(value)
426
- return BREVO_TRANSACTIONAL_EVENT_SCHEMA.parse({
219
+ export function normalizeBrevoEvent(value: unknown, webhookId?: number) {
220
+ const event = BREVO_PROVIDER_EVENT_SCHEMA.parse(value)
221
+ const family = classifyBrevoEventFamily(event)
222
+ const providerType = event.msg_status ?? event.event
223
+ if (!providerType) throw new Error("Brevo webhook event type is missing.")
224
+ // Resolve the semantic discriminator before assembling the normalized payload.
225
+ const type = normalizeBrevoProviderType(family, providerType)
226
+ // Keep provider-to-public field mapping readable and validate it as one unit.
227
+ const payload = {
228
+ bounceType: event.bounce_type,
229
+ campaignId: event.camp_id ?? event.campaign_id,
230
+ campaignName: event["campaign name"] ?? event.campaign_name,
427
231
  contactId: event.contact_id,
232
+ content: event.content,
233
+ creditsUsed: event.credits_used,
428
234
  customHeader: event["X-Mailin-custom"],
429
235
  date: event.date,
236
+ dateEvent: event.date_event,
237
+ dateSent: event.date_sent,
238
+ description: event.description,
430
239
  deviceUsed: event.device_used,
431
240
  email: event.email,
241
+ errorCode: event.error_code,
432
242
  event,
243
+ family,
244
+ key: event.key,
433
245
  link: event.link,
434
- messageId: event["message-id"],
246
+ listIds: event.list_id,
247
+ messageId: event["message-id"] ?? event.messageId,
435
248
  mirrorLink: event.mirror_link,
436
249
  occurredAt: event.ts_event,
250
+ phone: event.to,
437
251
  reason: event.reason,
252
+ reference: event.reference,
253
+ remainingCredit: event.remaining_credit,
254
+ reply: event.reply,
255
+ segmentIds: event.segment_ids,
256
+ senderEmail: event.sender_email,
438
257
  sendingIp: event.sending_ip,
258
+ smsCount: event.sms_count,
259
+ status: event.status,
439
260
  subject: event.subject,
440
261
  tag: event.tag,
441
262
  tags: event.tags,
442
263
  templateId: event.template_id,
443
264
  timestamp: event.ts,
444
265
  timestampMilliseconds: event.ts_epoch,
445
- type: event.event,
266
+ timestampSent: event.ts_sent,
446
267
  userAgent: event.user_agent,
447
- webhookId: event.id,
448
- })
268
+ type,
269
+ webhookId: webhookId ?? event.id,
270
+ }
271
+ return BREVO_EVENT_SCHEMA.parse(payload)
272
+ }
273
+
274
+ /**
275
+ * Classifies a provider callback into its managed webhook family.
276
+ *
277
+ * @param event - Parsed provider callback.
278
+ */
279
+ function classifyBrevoEventFamily(
280
+ event: z.infer<typeof BREVO_PROVIDER_EVENT_SCHEMA>,
281
+ ) {
282
+ if (event.msg_status) {
283
+ return event.type?.toLowerCase() === "marketing"
284
+ ? ("marketingSms" as const)
285
+ : ("transactionalSms" as const)
286
+ }
287
+ if (event.event && Object.hasOwn(CONTACT_PROVIDER_TYPES, event.event)) {
288
+ return "contact" as const
289
+ }
290
+ return event.camp_id !== undefined
291
+ ? ("marketingEmail" as const)
292
+ : ("transactionalEmail" as const)
293
+ }
294
+
295
+ /**
296
+ * Maps a provider event discriminator to the public semantic type.
297
+ *
298
+ * @param family - Managed webhook family.
299
+ * @param providerType - Provider event discriminator.
300
+ * @throws When the provider event is unsupported for the family.
301
+ */
302
+ function normalizeBrevoProviderType(
303
+ family: (typeof BREVO_EVENT_FAMILIES)[number],
304
+ providerType: string,
305
+ ) {
306
+ // Select one family mapping before performing the shared lookup and failure path.
307
+ const mapping =
308
+ family === "transactionalEmail"
309
+ ? TRANSACTIONAL_EMAIL_PROVIDER_TYPES
310
+ : family === "marketingEmail"
311
+ ? MARKETING_EMAIL_PROVIDER_TYPES
312
+ : family === "contact"
313
+ ? CONTACT_PROVIDER_TYPES
314
+ : SMS_PROVIDER_TYPES
315
+ const type = (mapping as Record<string, string>)[providerType]
316
+ if (!type) {
317
+ throw new Error(`Unsupported Brevo ${family} event: ${providerType}`)
318
+ }
319
+ return type
449
320
  }