@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 +23 -0
- package/dist/index.cjs +83 -12
- package/dist/index.d.cts +130 -2
- package/dist/index.d.ts +130 -2
- package/dist/index.js +81 -11
- package/package.json +1 -1
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
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
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
|
};
|