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 +2 -0
- package/dist/providers/brevo.js +1 -1
- package/dist/providers/mailgun.js +1 -1
- package/dist/providers/postmark.js +1 -1
- package/dist/providers/registry.d.ts +2 -0
- package/dist/providers/registry.js +2 -0
- package/dist/providers/sendgrid.js +1 -1
- package/dist/providers/ses.js +1 -1
- package/dist/providers/smtp.d.ts +1 -0
- package/dist/providers/smtp.js +2 -2
- package/dist/webhooks/normalize.d.ts +3 -3
- package/dist/webhooks/normalize.js +11 -4
- package/dist/webhooks/types.d.ts +1 -0
- package/dist/webhooks.js +11 -5
- package/docs/webhooks.md +1 -1
- package/package.json +1 -1
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.
|
package/dist/providers/brevo.js
CHANGED
|
@@ -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,
|
package/dist/providers/ses.js
CHANGED
package/dist/providers/smtp.d.ts
CHANGED
package/dist/providers/smtp.js
CHANGED
|
@@ -11,7 +11,7 @@ export function createSmtpProvider(options) {
|
|
|
11
11
|
host: options.host,
|
|
12
12
|
port: options.port,
|
|
13
13
|
secure: options.secure,
|
|
14
|
-
|
|
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);
|
package/dist/webhooks/types.d.ts
CHANGED
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
|
-
|
|
9
|
-
|
|
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.
|
|
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