@easybits.cloud/mailmask 0.2.1 → 0.3.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
@@ -31,13 +31,36 @@ await mm.send.send(domainId, {
31
31
  fromName: "Acme", // optional display name: Acme <hello@yourdomain.com>
32
32
  to: "client@example.com",
33
33
  subject: "Welcome!",
34
- html: "<p>Thanks for signing up.</p>",
34
+ html: "<p>Thanks for signing up.</p>", // or `body` (plain text) / `markdown`
35
35
  });
36
36
 
37
37
  // List aliases for a domain
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.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
  }
@@ -124,16 +123,20 @@ var LogsResource = class {
124
123
  constructor(req) {
125
124
  this.req = req;
126
125
  }
127
- list(domainId) {
128
- return this.req(`/api/domains/${domainId}/logs`);
126
+ /** `limit` por omisión 50, máximo 100. */
127
+ list(domainId, opts) {
128
+ const qs = opts?.limit ? `?limit=${encodeURIComponent(opts.limit)}` : "";
129
+ return this.req(`/api/domains/${domainId}/logs${qs}`);
129
130
  }
130
131
  };
131
132
  var SendResource = class {
132
133
  constructor(req) {
133
134
  this.req = req;
134
135
  }
135
- send(domainId, input) {
136
- 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 });
137
140
  }
138
141
  bulkSend(domainId, input) {
139
142
  return this.req(`/api/domains/${domainId}/send-bulk`, { method: "POST", body: JSON.stringify(input) });
@@ -142,6 +145,56 @@ var SendResource = class {
142
145
  return this.req(`/api/domains/${domainId}/bulk/${jobId}`);
143
146
  }
144
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
+ };
145
198
  var SmtpResource = class {
146
199
  constructor(req) {
147
200
  this.req = req;
@@ -170,8 +223,28 @@ var ApiKeysResource = class {
170
223
  return this.req(`/api/api-keys/${id}`, { method: "DELETE" });
171
224
  }
172
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
+ }
173
245
  // Annotate the CommonJS export names for ESM import in node:
174
246
  0 && (module.exports = {
175
247
  MailMask,
176
- MailMaskError
248
+ MailMaskError,
249
+ verifyWebhookSignature
177
250
  });
package/dist/index.d.cts CHANGED
@@ -13,7 +13,12 @@ interface Domain {
13
13
  domain: string;
14
14
  verified: boolean;
15
15
  mxConfigured: boolean;
16
+ registeredViaMailmask: boolean;
16
17
  createdAt: string;
18
+ /** Sólo en `domains.list`: reenvíos del mes en curso. */
19
+ monthlyForwards?: number;
20
+ /** Sólo en `domains.list`: tope de reenvíos por hora del plan. */
21
+ forwardPerHour?: number;
17
22
  }
18
23
  /** Respuesta de `domains.verify`. */
19
24
  interface DomainVerification {
@@ -30,6 +35,8 @@ interface Alias {
30
35
  destinations: string[];
31
36
  enabled: boolean;
32
37
  forwardCount: number;
38
+ lastFrom?: string | null;
39
+ lastAt?: string | null;
33
40
  createdAt: string;
34
41
  }
35
42
  interface CreateAliasInput {
@@ -41,31 +48,38 @@ interface UpdateAliasInput {
41
48
  enabled?: boolean;
42
49
  destinations?: string[];
43
50
  }
51
+ type RuleField = "to" | "from" | "subject";
52
+ type RuleMatch = "contains" | "equals" | "regex";
53
+ /** `forward` y `webhook` requieren `target`; `discard` no. */
54
+ type RuleAction = "forward" | "webhook" | "discard";
44
55
  interface Rule {
45
56
  id: string;
46
57
  domainId: string;
47
- field: "to" | "from" | "subject";
48
- match: string;
58
+ field: RuleField;
59
+ match: RuleMatch;
49
60
  value: string;
50
- action: string;
61
+ action: RuleAction;
51
62
  target: string;
52
63
  priority: number;
53
64
  enabled: boolean;
54
65
  createdAt: string;
55
66
  }
56
67
  interface CreateRuleInput {
57
- field: "to" | "from" | "subject";
58
- match: string;
68
+ field: RuleField;
69
+ /** Los patrones `regex` se validan al guardar: uno peligroso responde 400. */
70
+ match: RuleMatch;
59
71
  value: string;
60
- action: string;
61
- target: string;
72
+ action: RuleAction;
73
+ /** Destino del `forward` o URL del `webhook`. Se ignora con `discard`. */
74
+ target?: string;
62
75
  priority?: number;
76
+ enabled?: boolean;
63
77
  }
64
78
  interface UpdateRuleInput {
65
- field?: "to" | "from" | "subject";
66
- match?: string;
79
+ field?: RuleField;
80
+ match?: RuleMatch;
67
81
  value?: string;
68
- action?: string;
82
+ action?: RuleAction;
69
83
  target?: string;
70
84
  priority?: number;
71
85
  enabled?: boolean;
@@ -79,14 +93,16 @@ interface EmailLog {
79
93
  subject: string;
80
94
  status: string;
81
95
  forwardedTo: string;
96
+ sizeBytes: number;
82
97
  error?: string;
83
98
  }
84
99
  interface SendEmailInput {
85
100
  to: string;
86
101
  subject: string;
87
- /** Al menos uno de `html` o `body`. Con los dos se manda multipart. */
102
+ /** Al menos uno de `html`, `body` o `markdown`. Con html y body se manda multipart. HTML máx. 100 KB. */
88
103
  html?: string;
89
104
  body?: string;
105
+ markdown?: string;
90
106
  replyTo?: string;
91
107
  /**
92
108
  * Parte local de un alias **activo** del dominio. Sin esto el correo sale
@@ -95,6 +111,87 @@ interface SendEmailInput {
95
111
  from?: string;
96
112
  /** Nombre visible del remitente: `Libretas <hola@tudominio.com>`. */
97
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.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;
98
195
  }
99
196
  interface BulkSendInput {
100
197
  recipients: string[];
@@ -150,6 +247,8 @@ interface SmtpCredentialCreated {
150
247
  interface ApiKey {
151
248
  id: string;
152
249
  name: string;
250
+ /** Primeros caracteres de la llave, para identificarla en listados. */
251
+ keyPrefix: string;
153
252
  lastUsedAt?: string;
154
253
  createdAt: string;
155
254
  }
@@ -166,6 +265,9 @@ declare class MailMask {
166
265
  rules: RulesResource;
167
266
  logs: LogsResource;
168
267
  send: SendResource;
268
+ attachments: AttachmentsResource;
269
+ suppressions: SuppressionsResource;
270
+ webhooks: WebhooksResource;
169
271
  smtp: SmtpResource;
170
272
  apiKeys: ApiKeysResource;
171
273
  constructor(config: MailMaskConfig);
@@ -208,17 +310,52 @@ declare class RulesResource {
208
310
  declare class LogsResource {
209
311
  private req;
210
312
  constructor(req: Req);
211
- list(domainId: string): Promise<EmailLog[]>;
313
+ /** `limit` por omisión 50, máximo 100. */
314
+ list(domainId: string, opts?: {
315
+ limit?: number;
316
+ }): Promise<EmailLog[]>;
212
317
  }
213
318
  declare class SendResource {
214
319
  private req;
215
320
  constructor(req: Req);
216
- send(domainId: string, input: SendEmailInput): Promise<{
321
+ send(domainId: string, input: SendEmailInput, opts?: SendOptions): Promise<{
322
+ ok: boolean;
217
323
  messageId: string;
218
324
  }>;
219
325
  bulkSend(domainId: string, input: BulkSendInput): Promise<BulkJobCreated>;
220
326
  bulkStatus(domainId: string, jobId: string): Promise<BulkJob>;
221
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
+ }
222
359
  declare class SmtpResource {
223
360
  private req;
224
361
  constructor(req: Req);
@@ -240,4 +377,17 @@ declare class ApiKeysResource {
240
377
  }>;
241
378
  }
242
379
 
243
- 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 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
@@ -13,7 +13,12 @@ interface Domain {
13
13
  domain: string;
14
14
  verified: boolean;
15
15
  mxConfigured: boolean;
16
+ registeredViaMailmask: boolean;
16
17
  createdAt: string;
18
+ /** Sólo en `domains.list`: reenvíos del mes en curso. */
19
+ monthlyForwards?: number;
20
+ /** Sólo en `domains.list`: tope de reenvíos por hora del plan. */
21
+ forwardPerHour?: number;
17
22
  }
18
23
  /** Respuesta de `domains.verify`. */
19
24
  interface DomainVerification {
@@ -30,6 +35,8 @@ interface Alias {
30
35
  destinations: string[];
31
36
  enabled: boolean;
32
37
  forwardCount: number;
38
+ lastFrom?: string | null;
39
+ lastAt?: string | null;
33
40
  createdAt: string;
34
41
  }
35
42
  interface CreateAliasInput {
@@ -41,31 +48,38 @@ interface UpdateAliasInput {
41
48
  enabled?: boolean;
42
49
  destinations?: string[];
43
50
  }
51
+ type RuleField = "to" | "from" | "subject";
52
+ type RuleMatch = "contains" | "equals" | "regex";
53
+ /** `forward` y `webhook` requieren `target`; `discard` no. */
54
+ type RuleAction = "forward" | "webhook" | "discard";
44
55
  interface Rule {
45
56
  id: string;
46
57
  domainId: string;
47
- field: "to" | "from" | "subject";
48
- match: string;
58
+ field: RuleField;
59
+ match: RuleMatch;
49
60
  value: string;
50
- action: string;
61
+ action: RuleAction;
51
62
  target: string;
52
63
  priority: number;
53
64
  enabled: boolean;
54
65
  createdAt: string;
55
66
  }
56
67
  interface CreateRuleInput {
57
- field: "to" | "from" | "subject";
58
- match: string;
68
+ field: RuleField;
69
+ /** Los patrones `regex` se validan al guardar: uno peligroso responde 400. */
70
+ match: RuleMatch;
59
71
  value: string;
60
- action: string;
61
- target: string;
72
+ action: RuleAction;
73
+ /** Destino del `forward` o URL del `webhook`. Se ignora con `discard`. */
74
+ target?: string;
62
75
  priority?: number;
76
+ enabled?: boolean;
63
77
  }
64
78
  interface UpdateRuleInput {
65
- field?: "to" | "from" | "subject";
66
- match?: string;
79
+ field?: RuleField;
80
+ match?: RuleMatch;
67
81
  value?: string;
68
- action?: string;
82
+ action?: RuleAction;
69
83
  target?: string;
70
84
  priority?: number;
71
85
  enabled?: boolean;
@@ -79,14 +93,16 @@ interface EmailLog {
79
93
  subject: string;
80
94
  status: string;
81
95
  forwardedTo: string;
96
+ sizeBytes: number;
82
97
  error?: string;
83
98
  }
84
99
  interface SendEmailInput {
85
100
  to: string;
86
101
  subject: string;
87
- /** Al menos uno de `html` o `body`. Con los dos se manda multipart. */
102
+ /** Al menos uno de `html`, `body` o `markdown`. Con html y body se manda multipart. HTML máx. 100 KB. */
88
103
  html?: string;
89
104
  body?: string;
105
+ markdown?: string;
90
106
  replyTo?: string;
91
107
  /**
92
108
  * Parte local de un alias **activo** del dominio. Sin esto el correo sale
@@ -95,6 +111,87 @@ interface SendEmailInput {
95
111
  from?: string;
96
112
  /** Nombre visible del remitente: `Libretas <hola@tudominio.com>`. */
97
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.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;
98
195
  }
99
196
  interface BulkSendInput {
100
197
  recipients: string[];
@@ -150,6 +247,8 @@ interface SmtpCredentialCreated {
150
247
  interface ApiKey {
151
248
  id: string;
152
249
  name: string;
250
+ /** Primeros caracteres de la llave, para identificarla en listados. */
251
+ keyPrefix: string;
153
252
  lastUsedAt?: string;
154
253
  createdAt: string;
155
254
  }
@@ -166,6 +265,9 @@ declare class MailMask {
166
265
  rules: RulesResource;
167
266
  logs: LogsResource;
168
267
  send: SendResource;
268
+ attachments: AttachmentsResource;
269
+ suppressions: SuppressionsResource;
270
+ webhooks: WebhooksResource;
169
271
  smtp: SmtpResource;
170
272
  apiKeys: ApiKeysResource;
171
273
  constructor(config: MailMaskConfig);
@@ -208,17 +310,52 @@ declare class RulesResource {
208
310
  declare class LogsResource {
209
311
  private req;
210
312
  constructor(req: Req);
211
- list(domainId: string): Promise<EmailLog[]>;
313
+ /** `limit` por omisión 50, máximo 100. */
314
+ list(domainId: string, opts?: {
315
+ limit?: number;
316
+ }): Promise<EmailLog[]>;
212
317
  }
213
318
  declare class SendResource {
214
319
  private req;
215
320
  constructor(req: Req);
216
- send(domainId: string, input: SendEmailInput): Promise<{
321
+ send(domainId: string, input: SendEmailInput, opts?: SendOptions): Promise<{
322
+ ok: boolean;
217
323
  messageId: string;
218
324
  }>;
219
325
  bulkSend(domainId: string, input: BulkSendInput): Promise<BulkJobCreated>;
220
326
  bulkStatus(domainId: string, jobId: string): Promise<BulkJob>;
221
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
+ }
222
359
  declare class SmtpResource {
223
360
  private req;
224
361
  constructor(req: Req);
@@ -240,4 +377,17 @@ declare class ApiKeysResource {
240
377
  }>;
241
378
  }
242
379
 
243
- 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 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
  }
@@ -97,16 +95,20 @@ var LogsResource = class {
97
95
  constructor(req) {
98
96
  this.req = req;
99
97
  }
100
- list(domainId) {
101
- return this.req(`/api/domains/${domainId}/logs`);
98
+ /** `limit` por omisión 50, máximo 100. */
99
+ list(domainId, opts) {
100
+ const qs = opts?.limit ? `?limit=${encodeURIComponent(opts.limit)}` : "";
101
+ return this.req(`/api/domains/${domainId}/logs${qs}`);
102
102
  }
103
103
  };
104
104
  var SendResource = class {
105
105
  constructor(req) {
106
106
  this.req = req;
107
107
  }
108
- send(domainId, input) {
109
- 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 });
110
112
  }
111
113
  bulkSend(domainId, input) {
112
114
  return this.req(`/api/domains/${domainId}/send-bulk`, { method: "POST", body: JSON.stringify(input) });
@@ -115,6 +117,56 @@ var SendResource = class {
115
117
  return this.req(`/api/domains/${domainId}/bulk/${jobId}`);
116
118
  }
117
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
+ };
118
170
  var SmtpResource = class {
119
171
  constructor(req) {
120
172
  this.req = req;
@@ -143,7 +195,27 @@ var ApiKeysResource = class {
143
195
  return this.req(`/api/api-keys/${id}`, { method: "DELETE" });
144
196
  }
145
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
+ }
146
217
  export {
147
218
  MailMask,
148
- MailMaskError
219
+ MailMaskError,
220
+ verifyWebhookSignature
149
221
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@easybits.cloud/mailmask",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "Official SDK for the MailMask email alias/forwarding API",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",