@easybits.cloud/mailmask 0.2.2 → 0.3.1

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
@@ -38,6 +38,29 @@ await mm.send.send(domainId, {
38
38
  const aliases = await mm.aliases.list(domainId);
39
39
  ```
40
40
 
41
+ ## More
42
+
43
+ ```ts
44
+ // Attachments, copies, threading, idempotency
45
+ const pdf = await mm.attachments.upload(domainId, { filename: "invoice.pdf", contentType: "application/pdf", data: bytes });
46
+ await mm.send.send(domainId, {
47
+ from: "billing", to: "client@example.com", cc: ["cfo@example.com"],
48
+ subject: "Re: Invoice 42", markdown: "Attached.", attachments: [pdf],
49
+ inReplyTo: "<abc@mail.example.com>", references: "<abc@mail.example.com>",
50
+ }, { idempotencyKey: `invoice-42` });
51
+
52
+ // Suppression list (hard bounces / complaints)
53
+ await mm.suppressions.list(domainId);
54
+ await mm.suppressions.remove(domainId, "fixed@example.com");
55
+
56
+ // Webhooks (Developer plan) — secret is shown once
57
+ const wh = await mm.webhooks.create(domainId, { url: "https://app.example.com/hooks/mailmask", events: ["email.received", "email.delivered", "email.bounced"] });
58
+
59
+ // On your server: verify the signature with the raw body
60
+ import { verifyWebhookSignature } from "@easybits.cloud/mailmask";
61
+ const ok = await verifyWebhookSignature(secret, { signature: req.headers["x-mailmask-signature"], timestamp: req.headers["x-mailmask-timestamp"] }, rawBody);
62
+ ```
63
+
41
64
  ## Docs
42
65
 
43
66
  Full API reference and examples: https://mailmask.studio/docs
package/dist/index.cjs CHANGED
@@ -21,7 +21,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
23
  MailMask: () => MailMask,
24
- MailMaskError: () => MailMaskError
24
+ MailMaskError: () => MailMaskError,
25
+ verifyWebhookSignature: () => verifyWebhookSignature
25
26
  });
26
27
  module.exports = __toCommonJS(index_exports);
27
28
 
@@ -34,14 +35,9 @@ var MailMaskError = class extends Error {
34
35
  }
35
36
  };
36
37
  async function request(baseUrl, apiKey, path, opts, doFetch = fetch) {
37
- const res = await doFetch(`${baseUrl}${path}`, {
38
- ...opts,
39
- headers: {
40
- "Authorization": `Bearer ${apiKey}`,
41
- "Content-Type": "application/json",
42
- ...opts?.headers
43
- }
44
- });
38
+ const headers = { "Authorization": `Bearer ${apiKey}`, ...opts?.headers };
39
+ if (!(opts?.body instanceof FormData) && !("Content-Type" in headers)) headers["Content-Type"] = "application/json";
40
+ const res = await doFetch(`${baseUrl}${path}`, { ...opts, headers });
45
41
  if (!res.ok) {
46
42
  const body = await res.json().catch(() => ({ error: res.statusText }));
47
43
  throw new MailMaskError(res.status, body.error || res.statusText);
@@ -59,6 +55,9 @@ var MailMask = class {
59
55
  this.rules = new RulesResource(req);
60
56
  this.logs = new LogsResource(req);
61
57
  this.send = new SendResource(req);
58
+ this.attachments = new AttachmentsResource(req);
59
+ this.suppressions = new SuppressionsResource(req);
60
+ this.webhooks = new WebhooksResource(req);
62
61
  this.smtp = new SmtpResource(req);
63
62
  this.apiKeys = new ApiKeysResource(req);
64
63
  }
@@ -134,8 +133,10 @@ var SendResource = class {
134
133
  constructor(req) {
135
134
  this.req = req;
136
135
  }
137
- send(domainId, input) {
138
- return this.req(`/api/domains/${domainId}/send`, { method: "POST", body: JSON.stringify(input) });
136
+ send(domainId, input, opts) {
137
+ const headers = {};
138
+ if (opts?.idempotencyKey) headers["Idempotency-Key"] = opts.idempotencyKey;
139
+ return this.req(`/api/domains/${domainId}/send`, { method: "POST", body: JSON.stringify(input), headers });
139
140
  }
140
141
  bulkSend(domainId, input) {
141
142
  return this.req(`/api/domains/${domainId}/send-bulk`, { method: "POST", body: JSON.stringify(input) });
@@ -144,6 +145,56 @@ var SendResource = class {
144
145
  return this.req(`/api/domains/${domainId}/bulk/${jobId}`);
145
146
  }
146
147
  };
148
+ var AttachmentsResource = class {
149
+ constructor(req) {
150
+ this.req = req;
151
+ }
152
+ /** Máx. 5 MB. Ejecutables bloqueados (415). La llave vale hasta que se envía. */
153
+ upload(domainId, input) {
154
+ const form = new FormData();
155
+ const blob = input.data instanceof Blob ? input.data : new Blob([input.data], { type: input.contentType });
156
+ form.append("file", blob, input.filename);
157
+ return this.req(`/api/domains/${domainId}/attachments`, { method: "POST", body: form });
158
+ }
159
+ };
160
+ var SuppressionsResource = class {
161
+ constructor(req) {
162
+ this.req = req;
163
+ }
164
+ list(domainId) {
165
+ return this.req(`/api/domains/${domainId}/suppressions`);
166
+ }
167
+ add(domainId, email) {
168
+ return this.req(`/api/domains/${domainId}/suppressions`, { method: "POST", body: JSON.stringify({ email }) });
169
+ }
170
+ remove(domainId, email) {
171
+ return this.req(`/api/domains/${domainId}/suppressions/${encodeURIComponent(email)}`, { method: "DELETE" });
172
+ }
173
+ };
174
+ var WebhooksResource = class {
175
+ constructor(req) {
176
+ this.req = req;
177
+ }
178
+ list(domainId) {
179
+ return this.req(`/api/domains/${domainId}/webhooks`);
180
+ }
181
+ create(domainId, input) {
182
+ return this.req(`/api/domains/${domainId}/webhooks`, { method: "POST", body: JSON.stringify(input) });
183
+ }
184
+ update(domainId, webhookId, input) {
185
+ return this.req(`/api/domains/${domainId}/webhooks/${webhookId}`, { method: "PUT", body: JSON.stringify(input) });
186
+ }
187
+ delete(domainId, webhookId) {
188
+ return this.req(`/api/domains/${domainId}/webhooks/${webhookId}`, { method: "DELETE" });
189
+ }
190
+ /** Encola un `ping`; llega en el siguiente minuto. */
191
+ test(domainId, webhookId) {
192
+ return this.req(`/api/domains/${domainId}/webhooks/${webhookId}/test`, { method: "POST" });
193
+ }
194
+ deliveries(domainId, webhookId) {
195
+ return this.req(`/api/domains/${domainId}/webhooks/${webhookId}/deliveries`);
196
+ }
197
+ };
147
198
  var SmtpResource = class {
148
199
  constructor(req) {
149
200
  this.req = req;
@@ -172,8 +223,28 @@ var ApiKeysResource = class {
172
223
  return this.req(`/api/api-keys/${id}`, { method: "DELETE" });
173
224
  }
174
225
  };
226
+
227
+ // src/verify.ts
228
+ function hex(buf) {
229
+ return Array.from(new Uint8Array(buf)).map((b) => b.toString(16).padStart(2, "0")).join("");
230
+ }
231
+ function constantTimeEqual(a, b) {
232
+ if (a.length !== b.length) return false;
233
+ let diff = 0;
234
+ for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
235
+ return diff === 0;
236
+ }
237
+ async function verifyWebhookSignature(secret, headers, rawBody, toleranceMs = 5 * 6e4) {
238
+ const ts = Number(headers.timestamp);
239
+ if (!Number.isFinite(ts) || Math.abs(Date.now() - ts) > toleranceMs) return false;
240
+ const enc = new TextEncoder();
241
+ const key = await crypto.subtle.importKey("raw", enc.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
242
+ const mac = await crypto.subtle.sign("HMAC", key, enc.encode(`${headers.timestamp}.${rawBody}`));
243
+ return constantTimeEqual(`sha256=${hex(mac)}`, headers.signature);
244
+ }
175
245
  // Annotate the CommonJS export names for ESM import in node:
176
246
  0 && (module.exports = {
177
247
  MailMask,
178
- MailMaskError
248
+ MailMaskError,
249
+ verifyWebhookSignature
179
250
  });
package/dist/index.d.cts CHANGED
@@ -111,6 +111,87 @@ interface SendEmailInput {
111
111
  from?: string;
112
112
  /** Nombre visible del remitente: `Libretas <hola@tudominio.com>`. */
113
113
  fromName?: string;
114
+ /** Copias visibles. Máx. 20; también pasan por la lista de supresión. */
115
+ cc?: string[];
116
+ /** Copias ocultas. Máx. 20. */
117
+ bcc?: string[];
118
+ /** `Message-ID` del correo al que se responde, para que el cliente lo enhebre. */
119
+ inReplyTo?: string;
120
+ /** Cadena `References` del hilo. */
121
+ references?: string;
122
+ /** Archivos subidos antes con `attachments.upload`. Se borran de S3 al enviarse. */
123
+ attachments?: AttachmentRef[];
124
+ }
125
+ /** Lo que devuelve `attachments.upload`; se pasa tal cual en `SendEmailInput.attachments`. */
126
+ interface AttachmentRef {
127
+ key: string;
128
+ filename: string;
129
+ contentType?: string;
130
+ }
131
+ interface UploadAttachmentInput {
132
+ filename: string;
133
+ contentType: string;
134
+ data: Uint8Array | Blob;
135
+ }
136
+ interface UploadedAttachment {
137
+ ok: boolean;
138
+ key: string;
139
+ filename: string;
140
+ size: number;
141
+ }
142
+ interface SendOptions {
143
+ /**
144
+ * Reintentar con la misma clave (máx. 128 caracteres) devuelve la respuesta
145
+ * original sin volver a enviar ni consumir cuota, durante 24 h.
146
+ */
147
+ idempotencyKey?: string;
148
+ }
149
+ interface Suppression {
150
+ email: string;
151
+ /** `bounce:Permanent`, `complaint` o `manual`. */
152
+ reason: string;
153
+ createdAt: string;
154
+ }
155
+ type WebhookEvent = "email.received" | "email.sent" | "email.delivered" | "email.bounced" | "email.complained";
156
+ interface Webhook {
157
+ id: string;
158
+ domainId: string;
159
+ url: string;
160
+ events: WebhookEvent[];
161
+ enabled: boolean;
162
+ createdAt: string;
163
+ }
164
+ /** Lo que devuelve `webhooks.create`. El `secret` se muestra una sola vez. */
165
+ interface WebhookCreated extends Webhook {
166
+ secret: string;
167
+ }
168
+ interface CreateWebhookInput {
169
+ /** Debe ser https y pública. */
170
+ url: string;
171
+ events: WebhookEvent[];
172
+ }
173
+ interface UpdateWebhookInput {
174
+ url?: string;
175
+ events?: WebhookEvent[];
176
+ enabled?: boolean;
177
+ }
178
+ interface WebhookDelivery {
179
+ id: string;
180
+ webhookId: string;
181
+ event: WebhookEvent | "ping";
182
+ attempts: number;
183
+ status: "pending" | "delivered" | "failed";
184
+ nextAt: string;
185
+ lastError: string | null;
186
+ lastStatusCode: number | null;
187
+ createdAt: string;
188
+ }
189
+ /** Cuerpo que recibe tu endpoint. `data` depende del evento. */
190
+ interface WebhookPayload<T = Record<string, unknown>> {
191
+ event: WebhookEvent | "ping";
192
+ domainId: string;
193
+ timestamp: string;
194
+ data: T;
114
195
  }
115
196
  interface BulkSendInput {
116
197
  recipients: string[];
@@ -184,6 +265,9 @@ declare class MailMask {
184
265
  rules: RulesResource;
185
266
  logs: LogsResource;
186
267
  send: SendResource;
268
+ attachments: AttachmentsResource;
269
+ suppressions: SuppressionsResource;
270
+ webhooks: WebhooksResource;
187
271
  smtp: SmtpResource;
188
272
  apiKeys: ApiKeysResource;
189
273
  constructor(config: MailMaskConfig);
@@ -234,13 +318,44 @@ declare class LogsResource {
234
318
  declare class SendResource {
235
319
  private req;
236
320
  constructor(req: Req);
237
- send(domainId: string, input: SendEmailInput): Promise<{
321
+ send(domainId: string, input: SendEmailInput, opts?: SendOptions): Promise<{
238
322
  ok: boolean;
239
323
  messageId: string;
240
324
  }>;
241
325
  bulkSend(domainId: string, input: BulkSendInput): Promise<BulkJobCreated>;
242
326
  bulkStatus(domainId: string, jobId: string): Promise<BulkJob>;
243
327
  }
328
+ declare class AttachmentsResource {
329
+ private req;
330
+ constructor(req: Req);
331
+ /** Máx. 5 MB. Ejecutables bloqueados (415). La llave vale hasta que se envía. */
332
+ upload(domainId: string, input: UploadAttachmentInput): Promise<UploadedAttachment>;
333
+ }
334
+ declare class SuppressionsResource {
335
+ private req;
336
+ constructor(req: Req);
337
+ list(domainId: string): Promise<Suppression[]>;
338
+ add(domainId: string, email: string): Promise<Suppression>;
339
+ remove(domainId: string, email: string): Promise<{
340
+ ok: boolean;
341
+ }>;
342
+ }
343
+ declare class WebhooksResource {
344
+ private req;
345
+ constructor(req: Req);
346
+ list(domainId: string): Promise<Webhook[]>;
347
+ create(domainId: string, input: CreateWebhookInput): Promise<WebhookCreated>;
348
+ update(domainId: string, webhookId: string, input: UpdateWebhookInput): Promise<Webhook>;
349
+ delete(domainId: string, webhookId: string): Promise<{
350
+ ok: boolean;
351
+ }>;
352
+ /** Encola un `ping`; llega en el siguiente minuto. */
353
+ test(domainId: string, webhookId: string): Promise<{
354
+ ok: boolean;
355
+ deliveryId: string;
356
+ }>;
357
+ deliveries(domainId: string, webhookId: string): Promise<WebhookDelivery[]>;
358
+ }
244
359
  declare class SmtpResource {
245
360
  private req;
246
361
  constructor(req: Req);
@@ -262,4 +377,17 @@ declare class ApiKeysResource {
262
377
  }>;
263
378
  }
264
379
 
265
- export { type Alias, type ApiKey, type BulkJob, type BulkJobCreated, type BulkSendInput, type CreateAliasInput, type CreateRuleInput, type Domain, type DomainVerification, type EmailLog, MailMask, type MailMaskConfig, MailMaskError, type Rule, type RuleAction, type RuleField, type RuleMatch, type SendEmailInput, type SmtpCredential, type SmtpCredentialCreated, type UpdateAliasInput, type UpdateRuleInput };
380
+ interface WebhookSignatureHeaders {
381
+ /** Header `X-MailMask-Signature`, con forma `sha256=<hex>`. */
382
+ signature: string;
383
+ /** Header `X-MailMask-Timestamp`, milisegundos Unix. */
384
+ timestamp: string;
385
+ }
386
+ /**
387
+ * Comprueba que `rawBody` (el cuerpo tal cual llegó, sin parsear) fue firmado
388
+ * con `secret`. Rechaza timestamps con más de `toleranceMs` de diferencia para
389
+ * frenar repeticiones.
390
+ */
391
+ declare function verifyWebhookSignature(secret: string, headers: WebhookSignatureHeaders, rawBody: string, toleranceMs?: number): Promise<boolean>;
392
+
393
+ export { type Alias, type ApiKey, type AttachmentRef, type BulkJob, type BulkJobCreated, type BulkSendInput, type CreateAliasInput, type CreateRuleInput, type CreateWebhookInput, type Domain, type DomainVerification, type EmailLog, MailMask, type MailMaskConfig, MailMaskError, type Rule, type RuleAction, type RuleField, type RuleMatch, type SendEmailInput, type SendOptions, type SmtpCredential, type SmtpCredentialCreated, type Suppression, type UpdateAliasInput, type UpdateRuleInput, type UpdateWebhookInput, type UploadAttachmentInput, type UploadedAttachment, type Webhook, type WebhookCreated, type WebhookDelivery, type WebhookEvent, type WebhookPayload, type WebhookSignatureHeaders, verifyWebhookSignature };
package/dist/index.d.ts CHANGED
@@ -111,6 +111,87 @@ interface SendEmailInput {
111
111
  from?: string;
112
112
  /** Nombre visible del remitente: `Libretas <hola@tudominio.com>`. */
113
113
  fromName?: string;
114
+ /** Copias visibles. Máx. 20; también pasan por la lista de supresión. */
115
+ cc?: string[];
116
+ /** Copias ocultas. Máx. 20. */
117
+ bcc?: string[];
118
+ /** `Message-ID` del correo al que se responde, para que el cliente lo enhebre. */
119
+ inReplyTo?: string;
120
+ /** Cadena `References` del hilo. */
121
+ references?: string;
122
+ /** Archivos subidos antes con `attachments.upload`. Se borran de S3 al enviarse. */
123
+ attachments?: AttachmentRef[];
124
+ }
125
+ /** Lo que devuelve `attachments.upload`; se pasa tal cual en `SendEmailInput.attachments`. */
126
+ interface AttachmentRef {
127
+ key: string;
128
+ filename: string;
129
+ contentType?: string;
130
+ }
131
+ interface UploadAttachmentInput {
132
+ filename: string;
133
+ contentType: string;
134
+ data: Uint8Array | Blob;
135
+ }
136
+ interface UploadedAttachment {
137
+ ok: boolean;
138
+ key: string;
139
+ filename: string;
140
+ size: number;
141
+ }
142
+ interface SendOptions {
143
+ /**
144
+ * Reintentar con la misma clave (máx. 128 caracteres) devuelve la respuesta
145
+ * original sin volver a enviar ni consumir cuota, durante 24 h.
146
+ */
147
+ idempotencyKey?: string;
148
+ }
149
+ interface Suppression {
150
+ email: string;
151
+ /** `bounce:Permanent`, `complaint` o `manual`. */
152
+ reason: string;
153
+ createdAt: string;
154
+ }
155
+ type WebhookEvent = "email.received" | "email.sent" | "email.delivered" | "email.bounced" | "email.complained";
156
+ interface Webhook {
157
+ id: string;
158
+ domainId: string;
159
+ url: string;
160
+ events: WebhookEvent[];
161
+ enabled: boolean;
162
+ createdAt: string;
163
+ }
164
+ /** Lo que devuelve `webhooks.create`. El `secret` se muestra una sola vez. */
165
+ interface WebhookCreated extends Webhook {
166
+ secret: string;
167
+ }
168
+ interface CreateWebhookInput {
169
+ /** Debe ser https y pública. */
170
+ url: string;
171
+ events: WebhookEvent[];
172
+ }
173
+ interface UpdateWebhookInput {
174
+ url?: string;
175
+ events?: WebhookEvent[];
176
+ enabled?: boolean;
177
+ }
178
+ interface WebhookDelivery {
179
+ id: string;
180
+ webhookId: string;
181
+ event: WebhookEvent | "ping";
182
+ attempts: number;
183
+ status: "pending" | "delivered" | "failed";
184
+ nextAt: string;
185
+ lastError: string | null;
186
+ lastStatusCode: number | null;
187
+ createdAt: string;
188
+ }
189
+ /** Cuerpo que recibe tu endpoint. `data` depende del evento. */
190
+ interface WebhookPayload<T = Record<string, unknown>> {
191
+ event: WebhookEvent | "ping";
192
+ domainId: string;
193
+ timestamp: string;
194
+ data: T;
114
195
  }
115
196
  interface BulkSendInput {
116
197
  recipients: string[];
@@ -184,6 +265,9 @@ declare class MailMask {
184
265
  rules: RulesResource;
185
266
  logs: LogsResource;
186
267
  send: SendResource;
268
+ attachments: AttachmentsResource;
269
+ suppressions: SuppressionsResource;
270
+ webhooks: WebhooksResource;
187
271
  smtp: SmtpResource;
188
272
  apiKeys: ApiKeysResource;
189
273
  constructor(config: MailMaskConfig);
@@ -234,13 +318,44 @@ declare class LogsResource {
234
318
  declare class SendResource {
235
319
  private req;
236
320
  constructor(req: Req);
237
- send(domainId: string, input: SendEmailInput): Promise<{
321
+ send(domainId: string, input: SendEmailInput, opts?: SendOptions): Promise<{
238
322
  ok: boolean;
239
323
  messageId: string;
240
324
  }>;
241
325
  bulkSend(domainId: string, input: BulkSendInput): Promise<BulkJobCreated>;
242
326
  bulkStatus(domainId: string, jobId: string): Promise<BulkJob>;
243
327
  }
328
+ declare class AttachmentsResource {
329
+ private req;
330
+ constructor(req: Req);
331
+ /** Máx. 5 MB. Ejecutables bloqueados (415). La llave vale hasta que se envía. */
332
+ upload(domainId: string, input: UploadAttachmentInput): Promise<UploadedAttachment>;
333
+ }
334
+ declare class SuppressionsResource {
335
+ private req;
336
+ constructor(req: Req);
337
+ list(domainId: string): Promise<Suppression[]>;
338
+ add(domainId: string, email: string): Promise<Suppression>;
339
+ remove(domainId: string, email: string): Promise<{
340
+ ok: boolean;
341
+ }>;
342
+ }
343
+ declare class WebhooksResource {
344
+ private req;
345
+ constructor(req: Req);
346
+ list(domainId: string): Promise<Webhook[]>;
347
+ create(domainId: string, input: CreateWebhookInput): Promise<WebhookCreated>;
348
+ update(domainId: string, webhookId: string, input: UpdateWebhookInput): Promise<Webhook>;
349
+ delete(domainId: string, webhookId: string): Promise<{
350
+ ok: boolean;
351
+ }>;
352
+ /** Encola un `ping`; llega en el siguiente minuto. */
353
+ test(domainId: string, webhookId: string): Promise<{
354
+ ok: boolean;
355
+ deliveryId: string;
356
+ }>;
357
+ deliveries(domainId: string, webhookId: string): Promise<WebhookDelivery[]>;
358
+ }
244
359
  declare class SmtpResource {
245
360
  private req;
246
361
  constructor(req: Req);
@@ -262,4 +377,17 @@ declare class ApiKeysResource {
262
377
  }>;
263
378
  }
264
379
 
265
- export { type Alias, type ApiKey, type BulkJob, type BulkJobCreated, type BulkSendInput, type CreateAliasInput, type CreateRuleInput, type Domain, type DomainVerification, type EmailLog, MailMask, type MailMaskConfig, MailMaskError, type Rule, type RuleAction, type RuleField, type RuleMatch, type SendEmailInput, type SmtpCredential, type SmtpCredentialCreated, type UpdateAliasInput, type UpdateRuleInput };
380
+ interface WebhookSignatureHeaders {
381
+ /** Header `X-MailMask-Signature`, con forma `sha256=<hex>`. */
382
+ signature: string;
383
+ /** Header `X-MailMask-Timestamp`, milisegundos Unix. */
384
+ timestamp: string;
385
+ }
386
+ /**
387
+ * Comprueba que `rawBody` (el cuerpo tal cual llegó, sin parsear) fue firmado
388
+ * con `secret`. Rechaza timestamps con más de `toleranceMs` de diferencia para
389
+ * frenar repeticiones.
390
+ */
391
+ declare function verifyWebhookSignature(secret: string, headers: WebhookSignatureHeaders, rawBody: string, toleranceMs?: number): Promise<boolean>;
392
+
393
+ export { type Alias, type ApiKey, type AttachmentRef, type BulkJob, type BulkJobCreated, type BulkSendInput, type CreateAliasInput, type CreateRuleInput, type CreateWebhookInput, type Domain, type DomainVerification, type EmailLog, MailMask, type MailMaskConfig, MailMaskError, type Rule, type RuleAction, type RuleField, type RuleMatch, type SendEmailInput, type SendOptions, type SmtpCredential, type SmtpCredentialCreated, type Suppression, type UpdateAliasInput, type UpdateRuleInput, type UpdateWebhookInput, type UploadAttachmentInput, type UploadedAttachment, type Webhook, type WebhookCreated, type WebhookDelivery, type WebhookEvent, type WebhookPayload, type WebhookSignatureHeaders, verifyWebhookSignature };
package/dist/index.js CHANGED
@@ -7,14 +7,9 @@ var MailMaskError = class extends Error {
7
7
  }
8
8
  };
9
9
  async function request(baseUrl, apiKey, path, opts, doFetch = fetch) {
10
- const res = await doFetch(`${baseUrl}${path}`, {
11
- ...opts,
12
- headers: {
13
- "Authorization": `Bearer ${apiKey}`,
14
- "Content-Type": "application/json",
15
- ...opts?.headers
16
- }
17
- });
10
+ const headers = { "Authorization": `Bearer ${apiKey}`, ...opts?.headers };
11
+ if (!(opts?.body instanceof FormData) && !("Content-Type" in headers)) headers["Content-Type"] = "application/json";
12
+ const res = await doFetch(`${baseUrl}${path}`, { ...opts, headers });
18
13
  if (!res.ok) {
19
14
  const body = await res.json().catch(() => ({ error: res.statusText }));
20
15
  throw new MailMaskError(res.status, body.error || res.statusText);
@@ -32,6 +27,9 @@ var MailMask = class {
32
27
  this.rules = new RulesResource(req);
33
28
  this.logs = new LogsResource(req);
34
29
  this.send = new SendResource(req);
30
+ this.attachments = new AttachmentsResource(req);
31
+ this.suppressions = new SuppressionsResource(req);
32
+ this.webhooks = new WebhooksResource(req);
35
33
  this.smtp = new SmtpResource(req);
36
34
  this.apiKeys = new ApiKeysResource(req);
37
35
  }
@@ -107,8 +105,10 @@ var SendResource = class {
107
105
  constructor(req) {
108
106
  this.req = req;
109
107
  }
110
- send(domainId, input) {
111
- return this.req(`/api/domains/${domainId}/send`, { method: "POST", body: JSON.stringify(input) });
108
+ send(domainId, input, opts) {
109
+ const headers = {};
110
+ if (opts?.idempotencyKey) headers["Idempotency-Key"] = opts.idempotencyKey;
111
+ return this.req(`/api/domains/${domainId}/send`, { method: "POST", body: JSON.stringify(input), headers });
112
112
  }
113
113
  bulkSend(domainId, input) {
114
114
  return this.req(`/api/domains/${domainId}/send-bulk`, { method: "POST", body: JSON.stringify(input) });
@@ -117,6 +117,56 @@ var SendResource = class {
117
117
  return this.req(`/api/domains/${domainId}/bulk/${jobId}`);
118
118
  }
119
119
  };
120
+ var AttachmentsResource = class {
121
+ constructor(req) {
122
+ this.req = req;
123
+ }
124
+ /** Máx. 5 MB. Ejecutables bloqueados (415). La llave vale hasta que se envía. */
125
+ upload(domainId, input) {
126
+ const form = new FormData();
127
+ const blob = input.data instanceof Blob ? input.data : new Blob([input.data], { type: input.contentType });
128
+ form.append("file", blob, input.filename);
129
+ return this.req(`/api/domains/${domainId}/attachments`, { method: "POST", body: form });
130
+ }
131
+ };
132
+ var SuppressionsResource = class {
133
+ constructor(req) {
134
+ this.req = req;
135
+ }
136
+ list(domainId) {
137
+ return this.req(`/api/domains/${domainId}/suppressions`);
138
+ }
139
+ add(domainId, email) {
140
+ return this.req(`/api/domains/${domainId}/suppressions`, { method: "POST", body: JSON.stringify({ email }) });
141
+ }
142
+ remove(domainId, email) {
143
+ return this.req(`/api/domains/${domainId}/suppressions/${encodeURIComponent(email)}`, { method: "DELETE" });
144
+ }
145
+ };
146
+ var WebhooksResource = class {
147
+ constructor(req) {
148
+ this.req = req;
149
+ }
150
+ list(domainId) {
151
+ return this.req(`/api/domains/${domainId}/webhooks`);
152
+ }
153
+ create(domainId, input) {
154
+ return this.req(`/api/domains/${domainId}/webhooks`, { method: "POST", body: JSON.stringify(input) });
155
+ }
156
+ update(domainId, webhookId, input) {
157
+ return this.req(`/api/domains/${domainId}/webhooks/${webhookId}`, { method: "PUT", body: JSON.stringify(input) });
158
+ }
159
+ delete(domainId, webhookId) {
160
+ return this.req(`/api/domains/${domainId}/webhooks/${webhookId}`, { method: "DELETE" });
161
+ }
162
+ /** Encola un `ping`; llega en el siguiente minuto. */
163
+ test(domainId, webhookId) {
164
+ return this.req(`/api/domains/${domainId}/webhooks/${webhookId}/test`, { method: "POST" });
165
+ }
166
+ deliveries(domainId, webhookId) {
167
+ return this.req(`/api/domains/${domainId}/webhooks/${webhookId}/deliveries`);
168
+ }
169
+ };
120
170
  var SmtpResource = class {
121
171
  constructor(req) {
122
172
  this.req = req;
@@ -145,7 +195,27 @@ var ApiKeysResource = class {
145
195
  return this.req(`/api/api-keys/${id}`, { method: "DELETE" });
146
196
  }
147
197
  };
198
+
199
+ // src/verify.ts
200
+ function hex(buf) {
201
+ return Array.from(new Uint8Array(buf)).map((b) => b.toString(16).padStart(2, "0")).join("");
202
+ }
203
+ function constantTimeEqual(a, b) {
204
+ if (a.length !== b.length) return false;
205
+ let diff = 0;
206
+ for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
207
+ return diff === 0;
208
+ }
209
+ async function verifyWebhookSignature(secret, headers, rawBody, toleranceMs = 5 * 6e4) {
210
+ const ts = Number(headers.timestamp);
211
+ if (!Number.isFinite(ts) || Math.abs(Date.now() - ts) > toleranceMs) return false;
212
+ const enc = new TextEncoder();
213
+ const key = await crypto.subtle.importKey("raw", enc.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
214
+ const mac = await crypto.subtle.sign("HMAC", key, enc.encode(`${headers.timestamp}.${rawBody}`));
215
+ return constantTimeEqual(`sha256=${hex(mac)}`, headers.signature);
216
+ }
148
217
  export {
149
218
  MailMask,
150
- MailMaskError
219
+ MailMaskError,
220
+ verifyWebhookSignature
151
221
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@easybits.cloud/mailmask",
3
- "version": "0.2.2",
3
+ "version": "0.3.1",
4
4
  "description": "Official SDK for the MailMask email alias/forwarding API",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",