mailtruss 0.1.0 → 0.2.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/index.d.ts +41 -0
- package/dist/index.js +40 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -28,6 +28,11 @@ export type SendEmailParams = {
|
|
|
28
28
|
id: string;
|
|
29
29
|
variables?: Record<string, unknown>;
|
|
30
30
|
};
|
|
31
|
+
/** `false` (or `{ clicks: false, opens: false }`) switches click and open tracking off for this message. It cannot switch on what the domain has off. */
|
|
32
|
+
tracking?: boolean | {
|
|
33
|
+
clicks?: boolean;
|
|
34
|
+
opens?: boolean;
|
|
35
|
+
};
|
|
31
36
|
};
|
|
32
37
|
export type Email = {
|
|
33
38
|
object: "email";
|
|
@@ -77,6 +82,30 @@ export type ClientOptions = {
|
|
|
77
82
|
maxRetries?: number;
|
|
78
83
|
fetch?: typeof fetch;
|
|
79
84
|
};
|
|
85
|
+
export type EmailEvents = {
|
|
86
|
+
object: "email_events";
|
|
87
|
+
email_id: string;
|
|
88
|
+
status: string;
|
|
89
|
+
diagnosis: {
|
|
90
|
+
title: string;
|
|
91
|
+
explanation: string;
|
|
92
|
+
retry_safe: boolean;
|
|
93
|
+
next_step: string;
|
|
94
|
+
} | null;
|
|
95
|
+
recipients: {
|
|
96
|
+
address: string;
|
|
97
|
+
kind: "to" | "cc" | "bcc";
|
|
98
|
+
status: string;
|
|
99
|
+
}[];
|
|
100
|
+
data: {
|
|
101
|
+
type: string;
|
|
102
|
+
occurred_at: string;
|
|
103
|
+
source: string;
|
|
104
|
+
recipient: string | null;
|
|
105
|
+
meaning: string | null;
|
|
106
|
+
detail: Record<string, unknown>;
|
|
107
|
+
}[];
|
|
108
|
+
};
|
|
80
109
|
export type RequestOptions = {
|
|
81
110
|
idempotencyKey?: string;
|
|
82
111
|
signal?: AbortSignal;
|
|
@@ -117,6 +146,8 @@ export declare class MailClient {
|
|
|
117
146
|
cancel: (id: string, o?: RequestOptions) => Promise<{
|
|
118
147
|
id: string;
|
|
119
148
|
}>;
|
|
149
|
+
/** The delivery timeline with what each event proves, and a plain-language diagnosis when something went wrong. */
|
|
150
|
+
events: (id: string, o?: RequestOptions) => Promise<EmailEvents>;
|
|
120
151
|
};
|
|
121
152
|
readonly domains: {
|
|
122
153
|
create: (p: {
|
|
@@ -160,10 +191,12 @@ export declare class MailClient {
|
|
|
160
191
|
setTracking: (id: string, p: {
|
|
161
192
|
click_tracking?: boolean;
|
|
162
193
|
open_tracking?: boolean;
|
|
194
|
+
tracking_scope?: "all" | "broadcasts";
|
|
163
195
|
}, o?: RequestOptions) => Promise<{
|
|
164
196
|
id: string;
|
|
165
197
|
click_tracking: boolean;
|
|
166
198
|
open_tracking: boolean;
|
|
199
|
+
tracking_scope: "all" | "broadcasts";
|
|
167
200
|
}>;
|
|
168
201
|
remove: (id: string, o?: RequestOptions) => Promise<{
|
|
169
202
|
id: string;
|
|
@@ -425,3 +458,11 @@ export declare class MailClient {
|
|
|
425
458
|
}>>;
|
|
426
459
|
};
|
|
427
460
|
}
|
|
461
|
+
/**
|
|
462
|
+
* Verifies a Mailtruss webhook (Standard Webhooks format). Pass the raw request body exactly as received, the request headers,
|
|
463
|
+
* and the endpoint's signing secret. Returns the parsed event, or throws if the signature or timestamp is wrong.
|
|
464
|
+
* Works in Node 18+, edge runtimes and Workers (Web Crypto). Standard libraries (`standardwebhooks`, `svix`) also work.
|
|
465
|
+
*
|
|
466
|
+
* const event = await verifyWebhook(await req.text(), req.headers, process.env.MAILTRUSS_WEBHOOK_SECRET!);
|
|
467
|
+
*/
|
|
468
|
+
export declare function verifyWebhook<T = unknown>(body: string, headers: Headers | Record<string, string | string[] | undefined>, secret: string, toleranceSeconds?: number): Promise<T>;
|
package/dist/index.js
CHANGED
|
@@ -35,7 +35,7 @@ export class MailClient {
|
|
|
35
35
|
try {
|
|
36
36
|
const res = await f(`${base}${path}`, {
|
|
37
37
|
method, signal: ctl.signal,
|
|
38
|
-
headers: { Authorization: `Bearer ${this.o.apiKey}`, "Content-Type": "application/json", "User-Agent": "mailtruss-node/0.1.
|
|
38
|
+
headers: { Authorization: `Bearer ${this.o.apiKey}`, "Content-Type": "application/json", "User-Agent": "mailtruss-node/0.1.1", ...(opts.idempotencyKey ? { "Idempotency-Key": opts.idempotencyKey } : {}) },
|
|
39
39
|
body: body === undefined ? undefined : JSON.stringify(body),
|
|
40
40
|
});
|
|
41
41
|
const json = (await res.json().catch(() => null));
|
|
@@ -74,6 +74,8 @@ export class MailClient {
|
|
|
74
74
|
list: (q = {}, o) => this.request("GET", `/api/v1/emails${this.q(q)}`, undefined, o),
|
|
75
75
|
reschedule: (id, scheduled_at, o) => this.request("PATCH", `/api/v1/emails/${encodeURIComponent(id)}`, { scheduled_at }, o),
|
|
76
76
|
cancel: (id, o) => this.request("POST", `/api/v1/emails/${encodeURIComponent(id)}/cancel`, undefined, o),
|
|
77
|
+
/** The delivery timeline with what each event proves, and a plain-language diagnosis when something went wrong. */
|
|
78
|
+
events: (id, o) => this.request("GET", `/api/v1/emails/${encodeURIComponent(id)}/events`, undefined, o),
|
|
77
79
|
};
|
|
78
80
|
domains = {
|
|
79
81
|
create: (p, o) => this.request("POST", "/api/v1/domains", p, o),
|
|
@@ -149,3 +151,40 @@ export class MailClient {
|
|
|
149
151
|
list: (q = {}, o) => this.request("GET", `/api/v1/logs${this.q(q)}`, undefined, o),
|
|
150
152
|
};
|
|
151
153
|
}
|
|
154
|
+
/**
|
|
155
|
+
* Verifies a Mailtruss webhook (Standard Webhooks format). Pass the raw request body exactly as received, the request headers,
|
|
156
|
+
* and the endpoint's signing secret. Returns the parsed event, or throws if the signature or timestamp is wrong.
|
|
157
|
+
* Works in Node 18+, edge runtimes and Workers (Web Crypto). Standard libraries (`standardwebhooks`, `svix`) also work.
|
|
158
|
+
*
|
|
159
|
+
* const event = await verifyWebhook(await req.text(), req.headers, process.env.MAILTRUSS_WEBHOOK_SECRET!);
|
|
160
|
+
*/
|
|
161
|
+
export async function verifyWebhook(body, headers, secret, toleranceSeconds = 300) {
|
|
162
|
+
const h = (k) => {
|
|
163
|
+
const v = headers instanceof Headers ? headers.get(k) : headers[k] ?? headers[k.toLowerCase()];
|
|
164
|
+
return Array.isArray(v) ? v[0] : v ?? undefined;
|
|
165
|
+
};
|
|
166
|
+
const id = h("webhook-id") ?? h("svix-id"), ts = h("webhook-timestamp") ?? h("svix-timestamp"), sigs = h("webhook-signature") ?? h("svix-signature");
|
|
167
|
+
if (!id || !ts || !sigs)
|
|
168
|
+
throw new Error("Missing webhook-id, webhook-timestamp or webhook-signature header");
|
|
169
|
+
if (Math.abs(Date.now() / 1000 - Number(ts)) > toleranceSeconds)
|
|
170
|
+
throw new Error("Webhook timestamp is outside the allowed window");
|
|
171
|
+
// Secrets issued before 2026-11-14 (whsec_ + 32 url-safe characters) were used as text; newer ones are base64 key bytes.
|
|
172
|
+
const raw = secret.replace(/^whsec_/, "");
|
|
173
|
+
const legacy = /^whsec_[A-Za-z0-9_-]{32}$/.test(secret);
|
|
174
|
+
const keyBytes = legacy ? new TextEncoder().encode(secret) : Uint8Array.from(atob(raw), (c) => c.charCodeAt(0));
|
|
175
|
+
const key = await crypto.subtle.importKey("raw", keyBytes, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
176
|
+
const mac = new Uint8Array(await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(`${id}.${ts}.${body}`)));
|
|
177
|
+
const expected = btoa(String.fromCharCode(...mac));
|
|
178
|
+
const ok = sigs.split(" ").some((part) => {
|
|
179
|
+
const [v, sig] = part.split(",");
|
|
180
|
+
if (v !== "v1" || !sig || sig.length !== expected.length)
|
|
181
|
+
return false;
|
|
182
|
+
let diff = 0;
|
|
183
|
+
for (let i = 0; i < sig.length; i++)
|
|
184
|
+
diff |= sig.charCodeAt(i) ^ expected.charCodeAt(i);
|
|
185
|
+
return diff === 0;
|
|
186
|
+
});
|
|
187
|
+
if (!ok)
|
|
188
|
+
throw new Error("Webhook signature does not match");
|
|
189
|
+
return JSON.parse(body);
|
|
190
|
+
}
|