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 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.0", ...(opts.idempotencyKey ? { "Idempotency-Key": opts.idempotencyKey } : {}) },
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mailtruss",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Official TypeScript and JavaScript client for the Mailtruss email API",
5
5
  "license": "MIT",
6
6
  "author": "Mailtruss <hello@mailtruss.com>",