typedmailer 1.3.0 → 1.4.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/README.md CHANGED
@@ -41,6 +41,8 @@ npm install typedmailer @aws-sdk/client-sesv2
41
41
 
42
42
  Requires Node.js 22 or newer. Provider SDKs are optional peers and are loaded only when their adapter is selected.
43
43
 
44
+ For SMTP, use `secure: true` with implicit TLS (commonly port 465), or use STARTTLS with `secure: false` and `requireTLS: true`. Port 587 requires STARTTLS by default; other ports use opportunistic TLS unless `requireTLS` is set.
45
+
44
46
  Supported runtime and release guarantees are documented in [`SUPPORT.md`](SUPPORT.md). Provider behavior guarantees and their test coverage are described in [`docs/provider-contracts.md`](docs/provider-contracts.md). The scheduled and manual provider smoke workflow is documented in [`docs/integration-testing.md`](docs/integration-testing.md).
45
47
 
46
48
  TypedMailer is for trusted server-side Node.js runtimes. It is not intended for browser or mobile client bundles.
@@ -44,7 +44,7 @@ export function createBrevoProvider(options) {
44
44
  return { messageId: result.messageId };
45
45
  }
46
46
  catch (error) {
47
- throw normalizeProviderError(error, 'brevo');
47
+ throw normalizeProviderError(error, 'brevo', 'send');
48
48
  }
49
49
  },
50
50
  async verifyConnection() {
@@ -62,7 +62,7 @@ export function createMailgunProvider(options) {
62
62
  return { messageId: response.id };
63
63
  }
64
64
  catch (error) {
65
- throw normalizeProviderError(error, 'mailgun');
65
+ throw normalizeProviderError(error, 'mailgun', 'send');
66
66
  }
67
67
  },
68
68
  async verifyConnection() {
@@ -42,7 +42,7 @@ export function createPostmarkProvider(options) {
42
42
  return { messageId: response.MessageID };
43
43
  }
44
44
  catch (error) {
45
- throw normalizeProviderError(error, 'postmark');
45
+ throw normalizeProviderError(error, 'postmark', 'send');
46
46
  }
47
47
  },
48
48
  async verifyConnection() {
@@ -77,6 +77,7 @@ export declare const providerOptions: {
77
77
  host: z.ZodString;
78
78
  port: z.ZodNumber;
79
79
  secure: z.ZodBoolean;
80
+ requireTLS: z.ZodOptional<z.ZodBoolean>;
80
81
  user: z.ZodOptional<z.ZodString>;
81
82
  password: z.ZodOptional<z.ZodString>;
82
83
  connectionTimeout: z.ZodDefault<z.ZodNumber>;
@@ -147,6 +148,7 @@ export declare const mailerOptionsSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
147
148
  host: z.ZodString;
148
149
  port: z.ZodNumber;
149
150
  secure: z.ZodBoolean;
151
+ requireTLS: z.ZodOptional<z.ZodBoolean>;
150
152
  user: z.ZodOptional<z.ZodString>;
151
153
  password: z.ZodOptional<z.ZodString>;
152
154
  connectionTimeout: z.ZodDefault<z.ZodNumber>;
@@ -27,6 +27,7 @@ export const providerOptions = {
27
27
  host: z.string().min(1),
28
28
  port: z.number().int().min(1).max(65535),
29
29
  secure: z.boolean(),
30
+ requireTLS: z.boolean().optional(),
30
31
  user: z.string().min(1).optional(),
31
32
  password: z.string().min(1).optional(),
32
33
  connectionTimeout: z.number().int().positive().default(8_000),
@@ -80,6 +81,7 @@ export async function loadProvider(options) {
80
81
  host: options.host,
81
82
  port: options.port,
82
83
  secure: options.secure,
84
+ ...(options.requireTLS !== undefined ? { requireTLS: options.requireTLS } : {}),
83
85
  ...(options.user ? { user: options.user } : {}),
84
86
  ...(options.password ? { password: options.password } : {}),
85
87
  connectionTimeout: options.connectionTimeout,
@@ -50,7 +50,7 @@ export function createSendGridProvider(options) {
50
50
  return { messageId };
51
51
  }
52
52
  catch (error) {
53
- throw normalizeProviderError(error, 'sendgrid');
53
+ throw normalizeProviderError(error, 'sendgrid', 'send');
54
54
  }
55
55
  },
56
56
  async verifyConnection() {
@@ -55,7 +55,7 @@ export function createSesProvider(options) {
55
55
  return { messageId: response.MessageId };
56
56
  }
57
57
  catch (error) {
58
- throw normalizeProviderError(error, 'ses');
58
+ throw normalizeProviderError(error, 'ses', 'send');
59
59
  }
60
60
  },
61
61
  async verifyConnection() {
@@ -3,6 +3,7 @@ interface SmtpOptions {
3
3
  host: string;
4
4
  port: number;
5
5
  secure: boolean;
6
+ requireTLS?: boolean;
6
7
  user?: string;
7
8
  password?: string;
8
9
  connectionTimeout: number;
@@ -11,7 +11,7 @@ export function createSmtpProvider(options) {
11
11
  host: options.host,
12
12
  port: options.port,
13
13
  secure: options.secure,
14
- ...(options.port === 587 && !options.secure ? { requireTLS: true } : {}),
14
+ requireTLS: options.requireTLS ?? (options.port === 587 && !options.secure),
15
15
  connectionTimeout: options.connectionTimeout,
16
16
  greetingTimeout: options.greetingTimeout,
17
17
  socketTimeout: options.socketTimeout,
@@ -53,7 +53,7 @@ export function createSmtpProvider(options) {
53
53
  return { messageId: result.messageId };
54
54
  }
55
55
  catch (error) {
56
- throw normalizeProviderError(error, 'smtp');
56
+ throw normalizeProviderError(error, 'smtp', 'send');
57
57
  }
58
58
  },
59
59
  async verifyConnection() {
@@ -1,6 +1,6 @@
1
1
  import type { EmailWebhookEvent } from './types.js';
2
2
  export declare function normalizeResend(payload: unknown): readonly EmailWebhookEvent[];
3
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[];
4
+ export declare function normalizeSendGrid(payload: unknown, maxEvents: number): readonly EmailWebhookEvent[];
5
+ export declare function normalizeSes(payload: unknown, maxEvents: number): readonly EmailWebhookEvent[];
6
+ export declare function normalizeProviderEvents(provider: 'brevo' | 'postmark', payload: unknown, maxEvents: number): readonly EmailWebhookEvent[];
@@ -31,9 +31,10 @@ export function normalizeMailgun(payload) {
31
31
  }),
32
32
  ];
33
33
  }
34
- export function normalizeSendGrid(payload) {
35
- if (!Array.isArray(payload))
34
+ export function normalizeSendGrid(payload, maxEvents) {
35
+ if (!Array.isArray(payload) || payload.length > maxEvents) {
36
36
  throw new WebhookVerificationError('invalid_payload');
37
+ }
37
38
  return payload.map((item) => {
38
39
  const event = asRecord(item);
39
40
  const eventName = asString(event.event);
@@ -47,7 +48,7 @@ export function normalizeSendGrid(payload) {
47
48
  });
48
49
  });
49
50
  }
50
- export function normalizeSes(payload) {
51
+ export function normalizeSes(payload, maxEvents) {
51
52
  const message = asRecord(payload);
52
53
  const eventName = asString(message.eventType) ?? asString(message.notificationType);
53
54
  const mail = asRecord(message.mail);
@@ -59,6 +60,10 @@ export function normalizeSes(payload) {
59
60
  : Array.isArray(complaint?.complainedRecipients)
60
61
  ? complaint.complainedRecipients
61
62
  : [];
63
+ if (recipients.length > maxEvents ||
64
+ (!recipients.length && Array.isArray(mail.destination) && mail.destination.length > maxEvents)) {
65
+ throw new WebhookVerificationError('invalid_payload');
66
+ }
62
67
  const to = recipients.map((recipient) => asString(asRecord(recipient).emailAddress)).filter(isString);
63
68
  const emails = to.length > 0 ? to : asStringArray(mail.destination);
64
69
  return emails.length > 0
@@ -80,8 +85,10 @@ export function normalizeSes(payload) {
80
85
  }),
81
86
  ];
82
87
  }
83
- export function normalizeProviderEvents(provider, payload) {
88
+ export function normalizeProviderEvents(provider, payload, maxEvents) {
84
89
  const items = Array.isArray(payload) ? payload : [payload];
90
+ if (items.length > maxEvents)
91
+ throw new WebhookVerificationError('invalid_payload');
85
92
  return items.map((item) => {
86
93
  const event = asRecord(item);
87
94
  const eventName = provider === 'brevo' ? asString(event.event) : asString(event.RecordType);
@@ -16,6 +16,7 @@ interface RawWebhookInput {
16
16
  readonly rawBody: string | Uint8Array;
17
17
  readonly headers: WebhookHeaders;
18
18
  readonly maxBodyBytes?: number;
19
+ readonly maxEvents?: number;
19
20
  readonly now?: Date;
20
21
  readonly toleranceSeconds?: number;
21
22
  }
package/dist/webhooks.js CHANGED
@@ -1,12 +1,18 @@
1
1
  import { WebhookVerificationError } from './webhooks/types.js';
2
2
  import { verifyAuthorization, verifyMailgun, verifyResend, verifySendGrid, verifySnsNotification, } from './webhooks/signatures.js';
3
3
  import { normalizeMailgun, normalizeProviderEvents, normalizeResend, normalizeSendGrid, normalizeSes, } from './webhooks/normalize.js';
4
+ const DEFAULT_MAX_BODY_BYTES = 1_048_576;
5
+ const DEFAULT_MAX_EVENTS = 1_000;
4
6
  export { WebhookVerificationError } from './webhooks/types.js';
5
7
  /** Authenticates a provider webhook before returning normalized, typed email events. */
6
8
  export async function verifyWebhook(input) {
7
9
  const rawBodyByteLength = typeof input.rawBody === 'string' ? Buffer.byteLength(input.rawBody) : input.rawBody.byteLength;
8
- if (input.maxBodyBytes !== undefined &&
9
- (!Number.isSafeInteger(input.maxBodyBytes) || input.maxBodyBytes < 1 || rawBodyByteLength > input.maxBodyBytes)) {
10
+ const maxBodyBytes = input.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;
11
+ if (!Number.isSafeInteger(maxBodyBytes) || maxBodyBytes < 1 || rawBodyByteLength > maxBodyBytes) {
12
+ throw new WebhookVerificationError('invalid_payload');
13
+ }
14
+ const maxEvents = input.maxEvents ?? DEFAULT_MAX_EVENTS;
15
+ if (!Number.isSafeInteger(maxEvents) || maxEvents < 1) {
10
16
  throw new WebhookVerificationError('invalid_payload');
11
17
  }
12
18
  const rawBody = Buffer.from(input.rawBody);
@@ -26,12 +32,12 @@ export async function verifyWebhook(input) {
26
32
  return normalizeMailgun(payload);
27
33
  case 'sendgrid':
28
34
  verifySendGrid(input, rawBody);
29
- return normalizeSendGrid(payload);
35
+ return normalizeSendGrid(payload, maxEvents);
30
36
  case 'brevo':
31
37
  case 'postmark':
32
38
  verifyAuthorization(input, input.authorizationHeader ?? 'authorization');
33
- return normalizeProviderEvents(input.provider, payload);
39
+ return normalizeProviderEvents(input.provider, payload, maxEvents);
34
40
  case 'ses':
35
- return normalizeSes(await verifySnsNotification(input, payload));
41
+ return normalizeSes(await verifySnsNotification(input, payload), maxEvents);
36
42
  }
37
43
  }
package/docs/webhooks.md CHANGED
@@ -4,7 +4,7 @@ TypedMailer exposes `verifyWebhook()` from `typedmailer/webhooks`. It authentica
4
4
 
5
5
  ## Raw request bodies
6
6
 
7
- Pass the exact bytes received by your HTTP framework. Parsing JSON and serializing it again changes the signed input and causes verification to fail. `rawBody` accepts a string or `Uint8Array`; `headers` is a case-insensitive record of request header names and values. If the framework parses request bodies automatically, configure a raw-body capture before its JSON parser. Apply an HTTP request-body size limit before buffering the body. You can also set `maxBodyBytes` on `verifyWebhook()` to reject an oversized payload before JSON parsing; it has no default because provider event batches vary in size.
7
+ Pass the exact bytes received by your HTTP framework. Parsing JSON and serializing it again changes the signed input and causes verification to fail. `rawBody` accepts a string or `Uint8Array`; `headers` is a case-insensitive record of request header names and values. If the framework parses request bodies automatically, configure a raw-body capture before its JSON parser. Apply an HTTP request-body size limit before buffering the body. `verifyWebhook()` also rejects bodies larger than 1 MiB by default; set `maxBodyBytes` to a suitable positive value for your provider and expected batch size. Normalized event batches are limited to 1,000 events by default; set `maxEvents` to adjust that limit. Keep both limits finite and consistent with the upstream request limit.
8
8
 
9
9
  ## Authentication configuration
10
10
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "typedmailer",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "A type-safe Node.js email library with one API for Resend, Brevo, Postmark, SendGrid, Mailgun, Amazon SES, or SMTP.",
5
5
  "author": "Erol Senol",
6
6
  "license": "MIT",