typedmailer 1.1.0 → 1.3.0

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.
package/dist/testing.d.ts CHANGED
@@ -2,7 +2,7 @@ import type { Mailer, SendMailInput } from './types.js';
2
2
  export interface CapturedMail extends SendMailInput {
3
3
  readonly from: NonNullable<SendMailInput['from']>;
4
4
  }
5
- export interface TestMailer extends Mailer {
5
+ export interface TestMailer extends Mailer<'test'> {
6
6
  readonly sent: CapturedMail[];
7
7
  clear(): void;
8
8
  }
package/dist/types.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export type MailAddress = string | {
2
2
  readonly email: string;
3
- readonly name?: string;
3
+ readonly name?: string | undefined;
4
4
  };
5
5
  export interface MailAttachment {
6
6
  readonly filename: string;
@@ -24,18 +24,29 @@ export interface SendMailInput {
24
24
  readonly metadata?: Readonly<Record<string, string>>;
25
25
  }
26
26
  export type ProviderName = 'resend' | 'brevo' | 'smtp' | 'postmark' | 'sendgrid' | 'mailgun' | 'ses';
27
- export interface SendMailResult {
28
- readonly provider: ProviderName | 'test';
27
+ export interface SendMailResult<TProvider extends string = ProviderName | 'test'> {
28
+ readonly provider: TProvider;
29
29
  readonly messageId: string;
30
30
  /** Time when the provider accepted the request. This does not confirm inbox delivery. */
31
31
  readonly acceptedAt: Date;
32
32
  }
33
- export interface Mailer {
34
- send(input: SendMailInput): Promise<SendMailResult>;
33
+ export interface Mailer<TProvider extends string = ProviderName | 'test'> {
34
+ send(input: SendMailInput): Promise<SendMailResult<TProvider>>;
35
35
  verifyConnection(): Promise<void>;
36
36
  /** Rejects new operations, waits for active operations, and closes the provider at most once. */
37
37
  close(): Promise<void>;
38
38
  }
39
+ export interface ProviderAdapter<TProvider extends string = string> {
40
+ readonly name: TProvider;
41
+ send(input: NormalizedMailInput): Promise<ProviderSendResult>;
42
+ verifyConnection?(): Promise<void>;
43
+ close?(): Promise<void> | void;
44
+ }
45
+ export interface CustomMailerOptions<TProvider extends string = string> {
46
+ readonly provider: ProviderAdapter<TProvider>;
47
+ readonly from: MailAddress;
48
+ readonly maxAttachmentBytes?: number;
49
+ }
39
50
  export interface NormalizedMailInput extends Omit<SendMailInput, 'from' | 'to' | 'cc' | 'bcc' | 'replyTo'> {
40
51
  readonly from: MailAddress;
41
52
  readonly to: readonly MailAddress[];
@@ -0,0 +1,6 @@
1
+ import type { EmailWebhookEvent } from './types.js';
2
+ export declare function normalizeResend(payload: unknown): readonly EmailWebhookEvent[];
3
+ export declare function normalizeMailgun(payload: unknown): readonly EmailWebhookEvent[];
4
+ export declare function normalizeSendGrid(payload: unknown): readonly EmailWebhookEvent[];
5
+ export declare function normalizeSes(payload: unknown): readonly EmailWebhookEvent[];
6
+ export declare function normalizeProviderEvents(provider: 'brevo' | 'postmark', payload: unknown): readonly EmailWebhookEvent[];
@@ -0,0 +1,154 @@
1
+ import { WebhookVerificationError } from './types.js';
2
+ import { asId, asOptionalRecord, asRecord, asString, asStringArray, firstString, isString, parseDate, } from './shared.js';
3
+ export function normalizeResend(payload) {
4
+ const root = asRecord(payload);
5
+ const data = asRecord(root.data);
6
+ return [
7
+ makeEvent('resend', root, {
8
+ id: asString(root.id),
9
+ eventType: asString(root.type),
10
+ type: mapEventType(asString(root.type)),
11
+ messageId: asString(data.email_id),
12
+ recipient: firstString(data.to),
13
+ occurredAt: parseDate(root.created_at),
14
+ }),
15
+ ];
16
+ }
17
+ export function normalizeMailgun(payload) {
18
+ const root = asRecord(payload);
19
+ const event = asRecord(root['event-data']);
20
+ const message = asRecord(event.message);
21
+ const headers = asRecord(message.headers);
22
+ const eventName = asString(event.event);
23
+ return [
24
+ makeEvent('mailgun', event, {
25
+ id: asId(event.id),
26
+ eventType: eventName,
27
+ type: mapEventType(eventName),
28
+ messageId: asString(headers['message-id']),
29
+ recipient: asString(event.recipient),
30
+ occurredAt: parseDate(event.timestamp),
31
+ }),
32
+ ];
33
+ }
34
+ export function normalizeSendGrid(payload) {
35
+ if (!Array.isArray(payload))
36
+ throw new WebhookVerificationError('invalid_payload');
37
+ return payload.map((item) => {
38
+ const event = asRecord(item);
39
+ const eventName = asString(event.event);
40
+ return makeEvent('sendgrid', event, {
41
+ id: asString(event.sg_event_id),
42
+ eventType: eventName,
43
+ type: mapEventType(eventName),
44
+ messageId: asString(event.sg_message_id),
45
+ recipient: asString(event.email),
46
+ occurredAt: parseDate(event.timestamp),
47
+ });
48
+ });
49
+ }
50
+ export function normalizeSes(payload) {
51
+ const message = asRecord(payload);
52
+ const eventName = asString(message.eventType) ?? asString(message.notificationType);
53
+ const mail = asRecord(message.mail);
54
+ const bounce = asOptionalRecord(message.bounce);
55
+ const complaint = asOptionalRecord(message.complaint);
56
+ const delivery = asOptionalRecord(message.delivery);
57
+ const recipients = Array.isArray(bounce?.bouncedRecipients)
58
+ ? bounce.bouncedRecipients
59
+ : Array.isArray(complaint?.complainedRecipients)
60
+ ? complaint.complainedRecipients
61
+ : [];
62
+ const to = recipients.map((recipient) => asString(asRecord(recipient).emailAddress)).filter(isString);
63
+ const emails = to.length > 0 ? to : asStringArray(mail.destination);
64
+ return emails.length > 0
65
+ ? emails.map((recipient) => makeEvent('ses', message, {
66
+ id: asString(message.messageId),
67
+ eventType: eventName,
68
+ type: mapEventType(eventName),
69
+ messageId: asString(mail.messageId),
70
+ recipient,
71
+ occurredAt: parseDate(delivery?.timestamp) ?? parseDate(bounce?.timestamp),
72
+ }))
73
+ : [
74
+ makeEvent('ses', message, {
75
+ id: asString(message.messageId),
76
+ eventType: eventName,
77
+ type: mapEventType(eventName),
78
+ messageId: asString(mail.messageId),
79
+ occurredAt: parseDate(asRecord(message.delivery).timestamp),
80
+ }),
81
+ ];
82
+ }
83
+ export function normalizeProviderEvents(provider, payload) {
84
+ const items = Array.isArray(payload) ? payload : [payload];
85
+ return items.map((item) => {
86
+ const event = asRecord(item);
87
+ const eventName = provider === 'brevo' ? asString(event.event) : asString(event.RecordType);
88
+ return makeEvent(provider, event, {
89
+ id: provider === 'brevo'
90
+ ? (asId(event.id) ?? asString(event['message-id']))
91
+ : (asId(event.ID) ?? asString(event.MessageID)),
92
+ eventType: eventName,
93
+ type: mapEventType(eventName),
94
+ messageId: provider === 'brevo' ? asString(event['message-id']) : asString(event.MessageID),
95
+ recipient: provider === 'brevo' ? asString(event.email) : asString(event.Recipient),
96
+ occurredAt: parseDate(provider === 'brevo' ? (event.ts_epoch ?? event.date) : (event.DeliveredAt ?? event.BouncedAt)),
97
+ });
98
+ });
99
+ }
100
+ function makeEvent(provider, raw, fields) {
101
+ if (!fields.eventType)
102
+ throw new WebhookVerificationError('invalid_payload');
103
+ return {
104
+ provider,
105
+ raw,
106
+ eventType: fields.eventType,
107
+ type: fields.type,
108
+ ...(fields.id ? { id: fields.id } : {}),
109
+ ...(fields.messageId ? { messageId: fields.messageId } : {}),
110
+ ...(fields.recipient ? { recipient: fields.recipient } : {}),
111
+ ...(fields.occurredAt ? { occurredAt: fields.occurredAt } : {}),
112
+ };
113
+ }
114
+ function mapEventType(eventType) {
115
+ const event = eventType?.toLowerCase().replaceAll(/[._ -]/g, '') ?? '';
116
+ const mappings = {
117
+ accepted: 'accepted',
118
+ sent: 'accepted',
119
+ request: 'accepted',
120
+ emailsent: 'accepted',
121
+ delivery: 'delivered',
122
+ delivered: 'delivered',
123
+ emaildelivered: 'delivered',
124
+ bounce: 'bounced',
125
+ bounced: 'bounced',
126
+ emailbounced: 'bounced',
127
+ hardbounce: 'bounced',
128
+ softbounce: 'bounced',
129
+ spamcomplaint: 'complained',
130
+ complaint: 'complained',
131
+ complained: 'complained',
132
+ emailcomplained: 'complained',
133
+ spamreport: 'complained',
134
+ deferred: 'delayed',
135
+ deliverydelayed: 'delayed',
136
+ deliverydelay: 'delayed',
137
+ emaildeliverydelayed: 'delayed',
138
+ open: 'opened',
139
+ opened: 'opened',
140
+ emailopened: 'opened',
141
+ click: 'clicked',
142
+ clicked: 'clicked',
143
+ emailclicked: 'clicked',
144
+ unsubscribed: 'unsubscribed',
145
+ groupunsubscribe: 'unsubscribed',
146
+ reject: 'rejected',
147
+ rejected: 'rejected',
148
+ dropped: 'failed',
149
+ failed: 'failed',
150
+ emailfailed: 'failed',
151
+ renderingfailure: 'failed',
152
+ };
153
+ return mappings[event] ?? 'other';
154
+ }
@@ -0,0 +1,14 @@
1
+ import type { WebhookHeaders } from './types.js';
2
+ export declare function assertRecentTimestamp(value: string | undefined, now: Date | undefined, toleranceSeconds: number | undefined): void;
3
+ export declare function getHeader(headers: WebhookHeaders, name: string): string | undefined;
4
+ export declare function safeEqual(left: Uint8Array, right: Uint8Array): boolean;
5
+ export declare function decodeBase64(value: string): Buffer;
6
+ export declare function decodeHex(value: string): Buffer;
7
+ export declare function parseDate(value: unknown): Date | undefined;
8
+ export declare function asRecord(value: unknown): Record<string, unknown>;
9
+ export declare function asOptionalRecord(value: unknown): Record<string, unknown> | undefined;
10
+ export declare function asString(value: unknown): string | undefined;
11
+ export declare function asId(value: unknown): string | undefined;
12
+ export declare function isString(value: unknown): value is string;
13
+ export declare function asStringArray(value: unknown): string[];
14
+ export declare function firstString(value: unknown): string | undefined;
@@ -0,0 +1,69 @@
1
+ import { timingSafeEqual } from 'node:crypto';
2
+ import { WebhookVerificationError, defaultTimestampToleranceSeconds } from './types.js';
3
+ export function assertRecentTimestamp(value, now, toleranceSeconds) {
4
+ if (!value || !/^\d+$/.test(value))
5
+ throw new WebhookVerificationError('invalid_signature');
6
+ const timestamp = Number(value);
7
+ const tolerance = toleranceSeconds ?? defaultTimestampToleranceSeconds;
8
+ if (!Number.isSafeInteger(timestamp) || !Number.isSafeInteger(tolerance) || tolerance < 1) {
9
+ throw new WebhookVerificationError('invalid_signature');
10
+ }
11
+ const seconds = Math.floor((now ?? new Date()).getTime() / 1000);
12
+ if (Math.abs(seconds - timestamp) > tolerance)
13
+ throw new WebhookVerificationError('invalid_signature');
14
+ }
15
+ export function getHeader(headers, name) {
16
+ const entry = Object.entries(headers).find(([key]) => key.toLowerCase() === name.toLowerCase())?.[1];
17
+ return typeof entry === 'string' ? entry : Array.isArray(entry) ? entry.join(' ') : undefined;
18
+ }
19
+ export function safeEqual(left, right) {
20
+ return left.byteLength === right.byteLength && timingSafeEqual(left, right);
21
+ }
22
+ export function decodeBase64(value) {
23
+ if (!/^[A-Za-z0-9+/]+={0,2}$/.test(value))
24
+ return Buffer.alloc(0);
25
+ return Buffer.from(value, 'base64');
26
+ }
27
+ export function decodeHex(value) {
28
+ if (!/^(?:[a-fA-F0-9]{2})+$/.test(value))
29
+ return Buffer.alloc(0);
30
+ return Buffer.from(value, 'hex');
31
+ }
32
+ export function parseDate(value) {
33
+ if (typeof value === 'number' && Number.isFinite(value)) {
34
+ const milliseconds = value < 10_000_000_000 ? value * 1000 : value;
35
+ const result = new Date(milliseconds);
36
+ return Number.isNaN(result.getTime()) ? undefined : result;
37
+ }
38
+ if (typeof value === 'string' && value.length > 0) {
39
+ const result = new Date(value);
40
+ return Number.isNaN(result.getTime()) ? undefined : result;
41
+ }
42
+ return undefined;
43
+ }
44
+ export function asRecord(value) {
45
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
46
+ throw new WebhookVerificationError('invalid_payload');
47
+ }
48
+ return value;
49
+ }
50
+ export function asOptionalRecord(value) {
51
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
52
+ ? value
53
+ : undefined;
54
+ }
55
+ export function asString(value) {
56
+ return typeof value === 'string' && value.length > 0 ? value : undefined;
57
+ }
58
+ export function asId(value) {
59
+ return typeof value === 'string' || typeof value === 'number' ? String(value) : undefined;
60
+ }
61
+ export function isString(value) {
62
+ return typeof value === 'string';
63
+ }
64
+ export function asStringArray(value) {
65
+ return Array.isArray(value) ? value.filter(isString) : [];
66
+ }
67
+ export function firstString(value) {
68
+ return asString(value) ?? asStringArray(value)[0];
69
+ }
@@ -0,0 +1,16 @@
1
+ import type { VerifyWebhookInput } from './types.js';
2
+ export declare function verifyResend(input: Extract<VerifyWebhookInput, {
3
+ provider: 'resend';
4
+ }>, rawBody: Uint8Array): void;
5
+ export declare function verifyMailgun(input: Extract<VerifyWebhookInput, {
6
+ provider: 'mailgun';
7
+ }>, payload: unknown): void;
8
+ export declare function verifySendGrid(input: Extract<VerifyWebhookInput, {
9
+ provider: 'sendgrid';
10
+ }>, rawBody: Uint8Array): void;
11
+ export declare function verifyAuthorization(input: Extract<VerifyWebhookInput, {
12
+ provider: 'brevo' | 'postmark';
13
+ }>, headerName: string): void;
14
+ export declare function verifySnsNotification(input: Extract<VerifyWebhookInput, {
15
+ provider: 'ses';
16
+ }>, payload: unknown): Promise<unknown>;
@@ -0,0 +1,159 @@
1
+ import { createHmac, createVerify, X509Certificate } from 'node:crypto';
2
+ import { WebhookVerificationError } from './types.js';
3
+ import { asRecord, asString, assertRecentTimestamp, decodeBase64, decodeHex, getHeader, safeEqual } from './shared.js';
4
+ export function verifyResend(input, rawBody) {
5
+ const messageId = getHeader(input.headers, 'svix-id');
6
+ const timestamp = getHeader(input.headers, 'svix-timestamp');
7
+ const signatures = getHeader(input.headers, 'svix-signature');
8
+ assertRecentTimestamp(timestamp, input.now, input.toleranceSeconds);
9
+ if (!messageId || !signatures || !input.webhookSecret.startsWith('whsec_')) {
10
+ throw new WebhookVerificationError('invalid_signature');
11
+ }
12
+ let key;
13
+ try {
14
+ key = Buffer.from(input.webhookSecret.slice('whsec_'.length), 'base64');
15
+ }
16
+ catch {
17
+ throw new WebhookVerificationError('invalid_signature');
18
+ }
19
+ if (key.length === 0)
20
+ throw new WebhookVerificationError('invalid_signature');
21
+ const expected = createHmac('sha256', key).update(`${messageId}.${timestamp}.`).update(rawBody).digest();
22
+ const matches = signatures
23
+ .split(' ')
24
+ .some((item) => item.startsWith('v1,') && safeEqual(expected, decodeBase64(item.slice(3))));
25
+ if (!matches)
26
+ throw new WebhookVerificationError('invalid_signature');
27
+ }
28
+ export function verifyMailgun(input, payload) {
29
+ const root = asRecord(payload);
30
+ const signature = asRecord(root.signature);
31
+ const timestamp = asString(signature.timestamp);
32
+ const token = asString(signature.token);
33
+ const received = asString(signature.signature);
34
+ assertRecentTimestamp(timestamp, input.now, input.toleranceSeconds);
35
+ if (!timestamp || !token || !received || !input.signingKey) {
36
+ throw new WebhookVerificationError('invalid_signature');
37
+ }
38
+ const expected = createHmac('sha256', input.signingKey).update(`${timestamp}${token}`).digest('hex');
39
+ if (!safeEqual(Buffer.from(expected, 'hex'), decodeHex(received))) {
40
+ throw new WebhookVerificationError('invalid_signature');
41
+ }
42
+ }
43
+ export function verifySendGrid(input, rawBody) {
44
+ const timestamp = getHeader(input.headers, 'x-twilio-email-event-webhook-timestamp');
45
+ const signature = getHeader(input.headers, 'x-twilio-email-event-webhook-signature');
46
+ assertRecentTimestamp(timestamp, input.now, input.toleranceSeconds);
47
+ if (!timestamp || !signature || !input.publicKey)
48
+ throw new WebhookVerificationError('invalid_signature');
49
+ try {
50
+ const verifier = createVerify('sha256');
51
+ verifier.update(timestamp);
52
+ verifier.update(rawBody);
53
+ verifier.end();
54
+ if (!verifier.verify(input.publicKey, signature, 'base64')) {
55
+ throw new WebhookVerificationError('invalid_signature');
56
+ }
57
+ }
58
+ catch (error) {
59
+ if (error instanceof WebhookVerificationError)
60
+ throw error;
61
+ throw new WebhookVerificationError('invalid_signature');
62
+ }
63
+ }
64
+ export function verifyAuthorization(input, headerName) {
65
+ const received = getHeader(input.headers, headerName);
66
+ if (!received || !input.authorization || !safeEqual(Buffer.from(received), Buffer.from(input.authorization))) {
67
+ throw new WebhookVerificationError('invalid_signature');
68
+ }
69
+ }
70
+ export async function verifySnsNotification(input, payload) {
71
+ const envelope = asRecord(payload);
72
+ const topicArn = asString(envelope.TopicArn);
73
+ const signingCertUrl = asString(envelope.SigningCertURL);
74
+ const signature = asString(envelope.Signature);
75
+ const signatureVersion = asString(envelope.SignatureVersion);
76
+ if (envelope.Type !== 'Notification' ||
77
+ topicArn !== input.topicArn ||
78
+ !signingCertUrl ||
79
+ !signature ||
80
+ (signatureVersion !== '1' && signatureVersion !== '2')) {
81
+ throw new WebhookVerificationError('invalid_signature');
82
+ }
83
+ let certificateUrl;
84
+ try {
85
+ certificateUrl = new URL(signingCertUrl);
86
+ }
87
+ catch {
88
+ throw new WebhookVerificationError('invalid_signature');
89
+ }
90
+ const topicMatch = /^arn:(aws|aws-us-gov|aws-cn):sns:([a-z0-9-]+):\d{12}:[^:]+$/.exec(input.topicArn);
91
+ const expectedHost = topicMatch
92
+ ? topicMatch[1] === 'aws-cn'
93
+ ? `sns.${topicMatch[2]}.amazonaws.com.cn`
94
+ : `sns.${topicMatch[2]}.amazonaws.com`
95
+ : undefined;
96
+ if (certificateUrl.protocol !== 'https:' ||
97
+ certificateUrl.username ||
98
+ certificateUrl.password ||
99
+ certificateUrl.port ||
100
+ certificateUrl.search ||
101
+ certificateUrl.hash ||
102
+ certificateUrl.hostname !== expectedHost ||
103
+ !/^\/SimpleNotificationService-[A-Za-z0-9_-]+\.pem$/.test(certificateUrl.pathname)) {
104
+ throw new WebhookVerificationError('invalid_signature');
105
+ }
106
+ let response;
107
+ try {
108
+ response = await fetch(certificateUrl, { redirect: 'error', signal: AbortSignal.timeout(5_000) });
109
+ }
110
+ catch {
111
+ throw new WebhookVerificationError('invalid_signature');
112
+ }
113
+ if (!response.ok)
114
+ throw new WebhookVerificationError('invalid_signature');
115
+ let certificateBody;
116
+ try {
117
+ certificateBody = await response.text();
118
+ }
119
+ catch {
120
+ throw new WebhookVerificationError('invalid_signature');
121
+ }
122
+ if (certificateBody.length > 32_768)
123
+ throw new WebhookVerificationError('invalid_signature');
124
+ try {
125
+ const certificate = new X509Certificate(certificateBody);
126
+ const now = input.now ?? new Date();
127
+ if (!certificate.subject.split('\n').some((line) => line === 'CN=Amazon SNS') ||
128
+ now < new Date(certificate.validFrom) ||
129
+ now > new Date(certificate.validTo)) {
130
+ throw new WebhookVerificationError('invalid_signature');
131
+ }
132
+ const fields = snsFields(envelope);
133
+ const verifier = createVerify(signatureVersion === '2' ? 'sha256' : 'sha1');
134
+ verifier.update(fields);
135
+ verifier.end();
136
+ if (!verifier.verify(certificate.publicKey, signature, 'base64')) {
137
+ throw new WebhookVerificationError('invalid_signature');
138
+ }
139
+ }
140
+ catch (error) {
141
+ if (error instanceof WebhookVerificationError)
142
+ throw error;
143
+ throw new WebhookVerificationError('invalid_signature');
144
+ }
145
+ try {
146
+ const message = JSON.parse(asString(envelope.Message) ?? '');
147
+ return message;
148
+ }
149
+ catch {
150
+ throw new WebhookVerificationError('invalid_payload');
151
+ }
152
+ }
153
+ function snsFields(envelope) {
154
+ const fields = ['Message', 'MessageId'];
155
+ if (envelope.Subject !== undefined)
156
+ fields.push('Subject');
157
+ fields.push('Timestamp', 'TopicArn', 'Type');
158
+ return fields.map((field) => `${field}\n${asString(envelope[field]) ?? ''}\n`).join('');
159
+ }
@@ -0,0 +1,44 @@
1
+ export type ProviderName = import('../types.js').ProviderName;
2
+ export type EmailWebhookEventType = 'accepted' | 'delivered' | 'bounced' | 'complained' | 'delayed' | 'opened' | 'clicked' | 'unsubscribed' | 'rejected' | 'failed' | 'other';
3
+ export interface EmailWebhookEvent {
4
+ readonly provider: Exclude<ProviderName, 'smtp'>;
5
+ readonly id?: string;
6
+ readonly type: EmailWebhookEventType;
7
+ readonly eventType: string;
8
+ readonly messageId?: string;
9
+ readonly recipient?: string;
10
+ readonly occurredAt?: Date;
11
+ /** Original provider data. It can contain message metadata and personal information. */
12
+ readonly raw: unknown;
13
+ }
14
+ export type WebhookHeaders = Readonly<Record<string, string | readonly string[] | undefined>>;
15
+ interface RawWebhookInput {
16
+ readonly rawBody: string | Uint8Array;
17
+ readonly headers: WebhookHeaders;
18
+ readonly maxBodyBytes?: number;
19
+ readonly now?: Date;
20
+ readonly toleranceSeconds?: number;
21
+ }
22
+ export type VerifyWebhookInput = (RawWebhookInput & {
23
+ readonly provider: 'resend';
24
+ readonly webhookSecret: string;
25
+ }) | (RawWebhookInput & {
26
+ readonly provider: 'mailgun';
27
+ readonly signingKey: string;
28
+ }) | (RawWebhookInput & {
29
+ readonly provider: 'sendgrid';
30
+ readonly publicKey: string;
31
+ }) | (RawWebhookInput & {
32
+ readonly provider: 'brevo' | 'postmark';
33
+ readonly authorization: string;
34
+ readonly authorizationHeader?: string;
35
+ }) | (RawWebhookInput & {
36
+ readonly provider: 'ses';
37
+ readonly topicArn: string;
38
+ });
39
+ export declare class WebhookVerificationError extends Error {
40
+ readonly code: 'invalid_signature' | 'invalid_payload' | 'unsupported_event';
41
+ constructor(code: 'invalid_signature' | 'invalid_payload' | 'unsupported_event');
42
+ }
43
+ export declare const defaultTimestampToleranceSeconds = 300;
44
+ export {};
@@ -0,0 +1,13 @@
1
+ export class WebhookVerificationError extends Error {
2
+ code;
3
+ constructor(code) {
4
+ super(code === 'invalid_signature'
5
+ ? 'The webhook request could not be authenticated.'
6
+ : code === 'unsupported_event'
7
+ ? 'The provider webhook event is not supported.'
8
+ : 'The provider webhook payload is invalid.');
9
+ this.code = code;
10
+ this.name = 'WebhookVerificationError';
11
+ }
12
+ }
13
+ export const defaultTimestampToleranceSeconds = 300;
@@ -1,44 +1,5 @@
1
- import type { ProviderName } from './types.js';
2
- export type EmailWebhookEventType = 'accepted' | 'delivered' | 'bounced' | 'complained' | 'delayed' | 'opened' | 'clicked' | 'unsubscribed' | 'rejected' | 'failed' | 'other';
3
- export interface EmailWebhookEvent {
4
- readonly provider: Exclude<ProviderName, 'smtp'>;
5
- readonly id?: string;
6
- readonly type: EmailWebhookEventType;
7
- readonly eventType: string;
8
- readonly messageId?: string;
9
- readonly recipient?: string;
10
- readonly occurredAt?: Date;
11
- /** Original provider data. It can contain message metadata and personal information. */
12
- readonly raw: unknown;
13
- }
14
- export type WebhookHeaders = Readonly<Record<string, string | readonly string[] | undefined>>;
15
- interface RawWebhookInput {
16
- readonly rawBody: string | Uint8Array;
17
- readonly headers: WebhookHeaders;
18
- readonly now?: Date;
19
- readonly toleranceSeconds?: number;
20
- }
21
- export type VerifyWebhookInput = (RawWebhookInput & {
22
- readonly provider: 'resend';
23
- readonly webhookSecret: string;
24
- }) | (RawWebhookInput & {
25
- readonly provider: 'mailgun';
26
- readonly signingKey: string;
27
- }) | (RawWebhookInput & {
28
- readonly provider: 'sendgrid';
29
- readonly publicKey: string;
30
- }) | (RawWebhookInput & {
31
- readonly provider: 'brevo' | 'postmark';
32
- readonly authorization: string;
33
- readonly authorizationHeader?: string;
34
- }) | (RawWebhookInput & {
35
- readonly provider: 'ses';
36
- readonly topicArn: string;
37
- });
38
- export declare class WebhookVerificationError extends Error {
39
- readonly code: 'invalid_signature' | 'invalid_payload' | 'unsupported_event';
40
- constructor(code: 'invalid_signature' | 'invalid_payload' | 'unsupported_event');
41
- }
1
+ import type { EmailWebhookEvent, VerifyWebhookInput } from './webhooks/types.js';
2
+ export type { EmailWebhookEvent, EmailWebhookEventType, VerifyWebhookInput, WebhookHeaders } from './webhooks/types.js';
3
+ export { WebhookVerificationError } from './webhooks/types.js';
42
4
  /** Authenticates a provider webhook before returning normalized, typed email events. */
43
5
  export declare function verifyWebhook(input: VerifyWebhookInput): Promise<readonly EmailWebhookEvent[]>;
44
- export {};