@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 +24 -1
- package/dist/index.cjs +87 -14
- package/dist/index.d.cts +164 -14
- package/dist/index.d.ts +164 -14
- package/dist/index.js +85 -13
- package/package.json +1 -1
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
|
|
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
|
}
|
|
@@ -124,16 +123,20 @@ var LogsResource = class {
|
|
|
124
123
|
constructor(req) {
|
|
125
124
|
this.req = req;
|
|
126
125
|
}
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
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:
|
|
48
|
-
match:
|
|
58
|
+
field: RuleField;
|
|
59
|
+
match: RuleMatch;
|
|
49
60
|
value: string;
|
|
50
|
-
action:
|
|
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:
|
|
58
|
-
|
|
68
|
+
field: RuleField;
|
|
69
|
+
/** Los patrones `regex` se validan al guardar: uno peligroso responde 400. */
|
|
70
|
+
match: RuleMatch;
|
|
59
71
|
value: string;
|
|
60
|
-
action:
|
|
61
|
-
|
|
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?:
|
|
66
|
-
match?:
|
|
79
|
+
field?: RuleField;
|
|
80
|
+
match?: RuleMatch;
|
|
67
81
|
value?: string;
|
|
68
|
-
action?:
|
|
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 `
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
48
|
-
match:
|
|
58
|
+
field: RuleField;
|
|
59
|
+
match: RuleMatch;
|
|
49
60
|
value: string;
|
|
50
|
-
action:
|
|
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:
|
|
58
|
-
|
|
68
|
+
field: RuleField;
|
|
69
|
+
/** Los patrones `regex` se validan al guardar: uno peligroso responde 400. */
|
|
70
|
+
match: RuleMatch;
|
|
59
71
|
value: string;
|
|
60
|
-
action:
|
|
61
|
-
|
|
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?:
|
|
66
|
-
match?:
|
|
79
|
+
field?: RuleField;
|
|
80
|
+
match?: RuleMatch;
|
|
67
81
|
value?: string;
|
|
68
|
-
action?:
|
|
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 `
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
|
@@ -97,16 +95,20 @@ var LogsResource = class {
|
|
|
97
95
|
constructor(req) {
|
|
98
96
|
this.req = req;
|
|
99
97
|
}
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
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
|
};
|