@easybits.cloud/mailmask 0.4.12 → 0.6.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.
@@ -0,0 +1,371 @@
1
+ import { MailMaskError } from "./client.js";
2
+ import type { TemplateVars, PartnerSummary, CreatePartnerRequest, ReputationPolicy, ListCredentialsResponse, CreateCredentialRequest, CreateCredentialResponse, RotateCredentialRequest, RotateCredentialResponse, RevokeCredentialResponse, PartnerErrorBody, PartnerErrorCode, Tenant, PutTenantRequest, ListTenantsQuery, ListTenantsResponse, DeleteTenantResponse, PartnerDomain, SendRequest, SendResponse, BatchRequest, BatchStatus, UsageResponse, WebhookConfig, SetWebhookRequest, SetWebhookResponse, TestWebhookResponse, PartnerWebhookPayload, RotateWebhookSecretRequest, RotateWebhookSecretResponse, MessageStream, SuppressionReason, FallbackZone } from "./partner-contract.js";
3
+ export * from "./partner-contract.js";
4
+ /**
5
+ * Discriminante estable de los errores de partners, para un `switch` sin cadenas de `instanceof`.
6
+ * `network` y `timeout` llevan `status` 0 (no hubo respuesta); `unknown` es un status que el
7
+ * contrato no define (p. ej. un 400 de un proxy).
8
+ */
9
+ export type PartnerErrorKind = "auth" | "forbidden" | "reputation_hold" | "not_found" | "conflict" | "validation" | "suppressed" | "paused" | "quota" | "rate_limit" | "server" | "network" | "timeout" | "unknown";
10
+ /** Metadatos de la respuesta que no vienen en el cuerpo. */
11
+ export interface PartnerErrorMeta {
12
+ requestId?: string;
13
+ retryAfterMs?: number | null;
14
+ cause?: unknown;
15
+ }
16
+ /**
17
+ * Error de la API de partners. Extiende el `MailMaskError` del SDK (mismo `instanceof`) y suma
18
+ * el `code` del contrato, el cuerpo completo, `kind`, `requestId` y `retryable`. Nunca contiene
19
+ * la credencial: el mensaje sale del servidor (o del runtime, en errores de red).
20
+ */
21
+ export declare class PartnerApiError extends MailMaskError {
22
+ code: PartnerErrorCode | undefined;
23
+ body: Partial<PartnerErrorBody>;
24
+ kind: PartnerErrorKind;
25
+ /** `Retry-After` de la respuesta en ms; null si no vino. */
26
+ retryAfterMs: number | null;
27
+ constructor(status: number, message: string, body?: Partial<PartnerErrorBody>, meta?: PartnerErrorMeta);
28
+ }
29
+ /** 401: firma ausente, mala o vencida; credencial revocada o inexistente. */
30
+ export declare class AuthError extends PartnerApiError {
31
+ name: string;
32
+ kind: PartnerErrorKind;
33
+ /**
34
+ * Diferencia entre el reloj de MailMask (header `Date` de la respuesta) y el tuyo, en ms
35
+ * (positivo = tu reloj va atrasado). null si la respuesta no trajo `Date`. Con más de
36
+ * `PARTNER_SIGNATURE_TOLERANCE_S` segundos la firma nunca va a pasar.
37
+ */
38
+ clockSkewMs: number | null;
39
+ }
40
+ /** 403: partner desactivado o tenant suspendido por MailMask. */
41
+ export declare class ForbiddenError extends PartnerApiError {
42
+ name: string;
43
+ kind: PartnerErrorKind;
44
+ }
45
+ /**
46
+ * 403 `reputation_hold`: `tenants.resume` sobre una pausa que puso MailMask o SES por reputación
47
+ * (`pausedBy` `mailmask` o `ses`). No se arregla reintentando: la levanta MailMask tras revisar el caso.
48
+ */
49
+ export declare class ReputationHoldError extends ForbiddenError {
50
+ name: string;
51
+ kind: PartnerErrorKind;
52
+ }
53
+ /** 404: el recurso no existe para este partner. */
54
+ export declare class NotFoundError extends PartnerApiError {
55
+ name: string;
56
+ kind: PartnerErrorKind;
57
+ }
58
+ /** 409: dominio de otra cuenta/tenant, máximo de credenciales, estado incompatible. */
59
+ export declare class ConflictError extends PartnerApiError {
60
+ name: string;
61
+ kind: PartnerErrorKind;
62
+ }
63
+ /** 422: validación; `field` dice qué campo falló. */
64
+ export declare class ValidationError extends PartnerApiError {
65
+ name: string;
66
+ kind: PartnerErrorKind;
67
+ get field(): string | undefined;
68
+ }
69
+ /**
70
+ * 422 `recipient_suppressed`: el destinatario está suprimido para ese stream (rebote, queja, baja o
71
+ * manual). No es un error del cuerpo: no reintentes; marca a la persona en tu sistema.
72
+ */
73
+ export declare class RecipientSuppressedError extends PartnerApiError {
74
+ name: string;
75
+ kind: PartnerErrorKind;
76
+ get reason(): SuppressionReason | undefined;
77
+ get stream(): MessageStream | undefined;
78
+ get recipient(): string | undefined;
79
+ }
80
+ /** 423: el tenant está en pausa; no sale nada hasta `tenants.resume`. */
81
+ export declare class PausedError extends PartnerApiError {
82
+ name: string;
83
+ kind: PartnerErrorKind;
84
+ }
85
+ /**
86
+ * 429 `quota_exceeded`: cuota mensual agotada; trae `limit` y `resetsAt` y NO se reintenta. El 429
87
+ * de ritmo es la subclase `RateLimitError`.
88
+ */
89
+ export declare class QuotaError extends PartnerApiError {
90
+ name: string;
91
+ kind: PartnerErrorKind;
92
+ get limit(): number | undefined;
93
+ get resetsAt(): string | undefined;
94
+ }
95
+ /**
96
+ * 429 `rate_limited`: demasiadas peticiones. El cliente ya reintentó respetando `Retry-After`;
97
+ * si llega hasta ti, se acabaron los reintentos. Extiende `QuotaError` por compatibilidad (0.5).
98
+ */
99
+ export declare class RateLimitError extends QuotaError {
100
+ name: string;
101
+ kind: PartnerErrorKind;
102
+ }
103
+ /** No hubo respuesta: DNS, conexión rechazada o cortada. `status` 0. */
104
+ export declare class NetworkError extends PartnerApiError {
105
+ name: string;
106
+ kind: PartnerErrorKind;
107
+ constructor(message: string, cause?: unknown);
108
+ }
109
+ /** Se agotó `timeoutMs` esperando la respuesta. `status` 0. */
110
+ export declare class TimeoutError extends NetworkError {
111
+ name: string;
112
+ kind: PartnerErrorKind;
113
+ }
114
+ /** Firma una petición de partner; expuesta para quien no use el cliente (o para pruebas). */
115
+ export declare function signPartnerRequest(opts: {
116
+ keyId: string;
117
+ secret: string;
118
+ method: string;
119
+ pathAndQuery: string;
120
+ rawBody?: string;
121
+ ts?: number;
122
+ }): Promise<Record<string, string>>;
123
+ /** Lo que recibe `onRetry` antes de cada espera. */
124
+ export interface PartnerRetryInfo {
125
+ /** 1 = primer reintento. */
126
+ attempt: number;
127
+ delayMs: number;
128
+ method: string;
129
+ /** Ruta sin host (la credencial nunca va aquí). */
130
+ path: string;
131
+ error: PartnerApiError;
132
+ }
133
+ /** Ajustes de transporte compartidos por `MailMaskPartner` y `MailMaskPartnerAdmin`. */
134
+ export interface PartnerTransportConfig {
135
+ /** Default `https://www.mailmask.studio`. */
136
+ baseUrl?: string;
137
+ /**
138
+ * Reintentos ante 429 de ritmo, 5xx, red y tiempo agotado (default 3, máximo 10). Sólo se
139
+ * reintentan peticiones idempotentes (GET, PUT, DELETE, pausa, reanudar, alta de dominio) y los
140
+ * envíos, que siempre llevan `idempotencyKey`. `webhook.rotateSecret` y `webhook.test` no.
141
+ */
142
+ maxRetries?: number;
143
+ /** Base del backoff exponencial en ms (default 500), con jitter: 250–500, 500–1000, 1000–2000… */
144
+ retryBaseMs?: number;
145
+ /**
146
+ * Tope de espera entre reintentos en ms (default 30 000). Si `Retry-After` pide más, no se
147
+ * reintenta: el error sale con `retryAfterMs` para que decidas tú.
148
+ */
149
+ retryMaxDelayMs?: number;
150
+ /** Tiempo máximo por intento, en ms (default 30 000). Al agotarse lanza `TimeoutError`. */
151
+ timeoutMs?: number;
152
+ /** Se llama antes de cada reintento (para tus logs; no trae secretos). */
153
+ onRetry?: (info: PartnerRetryInfo) => void;
154
+ /** fetch alterno (pruebas, runtimes raros). */
155
+ fetch?: typeof fetch;
156
+ }
157
+ export interface MailMaskPartnerConfig extends PartnerTransportConfig {
158
+ /** Id público de la credencial, `mmk_…`. */
159
+ keyId: string;
160
+ /** Secreto `mms_…`. Sólo en el servidor del partner, nunca en el navegador. */
161
+ secret: string;
162
+ /**
163
+ * Modo de prueba: manda `X-MailMask-Test: 1` en cada petición. `send` y `batch` validan todo y
164
+ * responden con `test: true` sin enviar, sin cobrar y sin eventos. Úsalo en desarrollo y CI.
165
+ * También se puede pedir por envío con `{ test: true }` en las opciones de `send`/`batch`.
166
+ */
167
+ test?: boolean;
168
+ }
169
+ export interface PartnerResponseMeta {
170
+ status: number;
171
+ /** true si el servidor devolvió la respuesta guardada de una `idempotencyKey` repetida. */
172
+ idempotentReplayed: boolean;
173
+ }
174
+ /** Opciones por envío. */
175
+ export interface PartnerSendOptions {
176
+ /** Modo de prueba sólo para esta llamada (igual que `test` del constructor). */
177
+ test?: boolean;
178
+ }
179
+ export declare class MailMaskPartner {
180
+ readonly keyId: string;
181
+ private transport;
182
+ private test;
183
+ tenants: PartnerTenantsResource;
184
+ domains: PartnerDomainsResource;
185
+ webhook: PartnerWebhookResource;
186
+ constructor(config: MailMaskPartnerConfig);
187
+ /** Base URL efectiva (sin barra final). */
188
+ get baseUrl(): string;
189
+ /**
190
+ * Petición firmada. Cada intento se firma de nuevo (ts fresco) con el MISMO cuerpo, así que la
191
+ * `idempotencyKey` del cuerpo viaja idéntica en los reintentos.
192
+ * @internal
193
+ */
194
+ request<T>(method: string, path: string, body?: unknown, extraHeaders?: Record<string, string>, opts?: {
195
+ idempotent?: boolean;
196
+ test?: boolean;
197
+ }): Promise<{
198
+ data: T;
199
+ meta: PartnerResponseMeta;
200
+ }>;
201
+ /**
202
+ * Manda un correo a nombre de un tenant. Pone `idempotencyKey` si no viene (la misma en cada
203
+ * reintento). `stream` default `transactional` (sin baja de un clic); usa `broadcast` para
204
+ * boletines. `vars` sustituye los `{{nombre}}`. Un destinatario suprimido lanza `RecipientSuppressedError`.
205
+ */
206
+ send(externalId: string, input: SendRequest, opts?: PartnerSendOptions): Promise<SendResponse & {
207
+ idempotencyKey: string;
208
+ replayed: boolean;
209
+ }>;
210
+ /**
211
+ * Encola un lote (hasta 10,000 destinatarios: correos pelados o `{ to, vars }`). Pone
212
+ * `idempotencyKey` si no viene. `stream` default `broadcast` (cada correo lleva baja de un clic);
213
+ * los suprimidos se saltan.
214
+ */
215
+ batch(externalId: string, input: BatchRequest, opts?: PartnerSendOptions): Promise<BatchStatus & {
216
+ idempotencyKey: string;
217
+ replayed: boolean;
218
+ }>;
219
+ /** Estado de un lote (`batchId` del resultado de `batch`): envíos y, después, entregas, rebotes, quejas y bajas. */
220
+ batchStatus(externalId: string, batchId: string): Promise<BatchStatus>;
221
+ /** Uso del mes (`YYYY-MM`, default el mes en curso UTC): tenants activos y envíos. */
222
+ usage(opts?: {
223
+ month?: string;
224
+ }): Promise<UsageResponse>;
225
+ }
226
+ declare class PartnerTenantsResource {
227
+ private c;
228
+ constructor(c: MailMaskPartner);
229
+ /** Crea o actualiza el tenant (idempotente por `externalId`). */
230
+ put(externalId: string, input?: PutTenantRequest): Promise<Tenant>;
231
+ get(externalId: string): Promise<Tenant>;
232
+ /** Una página de tenants; sigue `nextCursor` para la siguiente. */
233
+ list(query?: ListTenantsQuery): Promise<ListTenantsResponse>;
234
+ /**
235
+ * Borra el tenant y suelta sus dominios (los que nadie más usa se borran con sus recursos en SES).
236
+ * Sin vuelta atrás. `ConflictError` si está en pausa por reputación.
237
+ */
238
+ delete(externalId: string): Promise<DeleteTenantResponse>;
239
+ pause(externalId: string, reason?: string): Promise<Tenant>;
240
+ /**
241
+ * Levanta una pausa que puso el partner (`pausedBy: "partner"`). Sobre una pausa por reputación
242
+ * (`mailmask` o `ses`) lanza `ReputationHoldError`.
243
+ */
244
+ resume(externalId: string): Promise<Tenant>;
245
+ }
246
+ declare class PartnerDomainsResource {
247
+ private c;
248
+ constructor(c: MailMaskPartner);
249
+ /** Da de alta el dominio en el tenant y devuelve los registros DNS a pegar. */
250
+ add(externalId: string, domain: string): Promise<PartnerDomain>;
251
+ /** Estado en vivo (SES y DNS público) de cada registro. */
252
+ status(externalId: string, domain: string): Promise<PartnerDomain>;
253
+ }
254
+ declare class PartnerWebhookResource {
255
+ private c;
256
+ constructor(c: MailMaskPartner);
257
+ get(): Promise<WebhookConfig>;
258
+ /**
259
+ * Configura la URL. `secret` (`whsec_…`) sólo viene la primera vez; después es null porque cambiar
260
+ * la URL no rota el secreto (para eso, `rotateSecret`).
261
+ */
262
+ set(input: SetWebhookRequest): Promise<SetWebhookResponse>;
263
+ /**
264
+ * Secreto nuevo (se ve UNA vez). Durante la gracia (default 24 h, 0–10080 min) cada entrega lleva
265
+ * las dos firmas: despliega el nuevo y pasa los dos a `verifyWebhook` mientras tanto. No se
266
+ * reintenta solo: un segundo intento rotaría otra vez y el secreto de la respuesta perdida no
267
+ * lo vería nadie.
268
+ */
269
+ rotateSecret(input?: RotateWebhookSecretRequest): Promise<RotateWebhookSecretResponse>;
270
+ /** Encola un `ping` al webhook. No se reintenta solo (un reintento encolaría otro ping). */
271
+ test(): Promise<TestWebhookResponse>;
272
+ }
273
+ /** Headers como `Headers` de fetch, o como objeto plano de Node/Express (llaves en minúsculas o no). */
274
+ export type WebhookHeadersInput = Headers | Record<string, string | string[] | undefined>;
275
+ /**
276
+ * Verifica una entrega del webhook del partner con el cuerpo CRUDO (sin parsear). Devuelve el
277
+ * evento ya parseado si la firma es válida y el timestamp está a ≤ 5 min (borde incluido); `null`
278
+ * si no. Compara en tiempo constante.
279
+ *
280
+ * Rotación: `secret` acepta uno o dos secretos (`[nuevo, viejo]`), y el header puede traer dos
281
+ * firmas separadas por coma (`sha256=<nueva>,sha256=<vieja>`). Basta con que una coincida.
282
+ */
283
+ export declare function verifyWebhook(opts: {
284
+ rawBody: string;
285
+ headers: WebhookHeadersInput;
286
+ secret: string | readonly string[];
287
+ /** Default 5 min, como el contrato. */
288
+ toleranceMs?: number;
289
+ /** Reloj alterno (pruebas). */
290
+ now?: number;
291
+ }): Promise<PartnerWebhookPayload | null>;
292
+ /**
293
+ * Handler listo para runtimes con `Request`/`Response` (Workers, Deno, Bun, Next.js, Hono): verifica
294
+ * la firma con el cuerpo crudo y llama a `onEvent`. Responde 401 si la firma no cuadra, 200 si
295
+ * `onEvent` terminó bien y 500 si lanzó (MailMask reintenta la entrega). Deduplica tú por `event.id`.
296
+ */
297
+ export declare function createWebhookHandler(opts: {
298
+ secret: string | readonly string[];
299
+ onEvent: (event: PartnerWebhookPayload) => unknown | Promise<unknown>;
300
+ toleranceMs?: number;
301
+ }): (request: Request) => Promise<Response>;
302
+ /** Nombres de los `{{marcadores}}` de una plantilla, sin repetir. */
303
+ export declare function templateVarNames(template: string): string[];
304
+ /**
305
+ * Valida unas `vars` con las mismas reglas que el servidor (máx. 50, nombres `[a-zA-Z_]\w*` de hasta
306
+ * 64, valores de texto de hasta 2000, `unsubscribe_url` reservada). Devuelve el problema o null.
307
+ */
308
+ export declare function templateVarsProblem(vars: unknown): string | null;
309
+ /** Resultado de `previewMessage`. */
310
+ export interface MessagePreview {
311
+ subject: string;
312
+ html?: string;
313
+ text?: string;
314
+ stream: MessageStream;
315
+ /** true si MailMask agregará el pie de baja (broadcast cuyo html/text no usa `{{unsubscribe_url}}`). */
316
+ unsubscribeFooter: boolean;
317
+ /** Marcadores de la plantilla sin variable: saldrán vacíos. */
318
+ missingVars: string[];
319
+ /** Variables que mandaste y ninguna plantilla usa. */
320
+ unusedVars: string[];
321
+ }
322
+ /**
323
+ * Lo que va a salir para UN destinatario, con la MISMA función que usa el servidor
324
+ * (`personalizePartnerMessage` del contrato): variables, escape en html y, en `broadcast`, la liga
325
+ * de baja (`unsubscribeUrl`, default una liga de ejemplo) o el pie que MailMask agrega. Sin red.
326
+ */
327
+ export declare function previewMessage(input: {
328
+ subject: string;
329
+ html?: string;
330
+ text?: string;
331
+ stream?: MessageStream;
332
+ vars?: TemplateVars;
333
+ }, opts?: {
334
+ unsubscribeUrl?: string;
335
+ }): MessagePreview;
336
+ export interface MailMaskPartnerAdminConfig extends PartnerTransportConfig {
337
+ /** Llave `mk_…` del DUEÑO del partner (nunca la credencial `mmk_`/`mms_`). */
338
+ apiKey: string;
339
+ }
340
+ /**
341
+ * Rutas de administración (`/api/partners/…`): credenciales del partner y, sólo para cuentas de
342
+ * MailMask, alta de partners, política de reputación y levantar pausas por reputación. Mismos
343
+ * errores tipados, timeout y reintentos (sólo en lecturas) que `MailMaskPartner`.
344
+ */
345
+ export declare class MailMaskPartnerAdmin {
346
+ private transport;
347
+ constructor(config: MailMaskPartnerAdminConfig);
348
+ private req;
349
+ /** Los partners de la cuenta. */
350
+ listPartners(): Promise<PartnerSummary[]>;
351
+ /** Sólo MailMask (admin). */
352
+ createPartner(input: CreatePartnerRequest): Promise<PartnerSummary>;
353
+ /** Sólo MailMask (admin); null = automática (Strict el primer mes, Standard después). */
354
+ setReputationPolicy(partnerId: string, policy: ReputationPolicy | null): Promise<PartnerSummary>;
355
+ /** Sólo MailMask (admin): levanta cualquier pausa, también la de reputación. */
356
+ resumeTenant(partnerId: string, externalId: string): Promise<Tenant>;
357
+ /**
358
+ * Sólo MailMask (admin): crea o adopta la hosted zone de la zona de respaldo (`marketing.denik.me`)
359
+ * y devuelve los NS que el partner tiene que delegar. Idempotente con la misma zona.
360
+ */
361
+ setFallbackZone(partnerId: string, zone: string): Promise<FallbackZone>;
362
+ /** La zona de respaldo y si el DNS público ya ve la delegación. */
363
+ getFallbackZone(partnerId: string): Promise<FallbackZone>;
364
+ listCredentials(partnerId: string): Promise<ListCredentialsResponse>;
365
+ /** El secreto `mms_…` viene UNA vez en la respuesta. */
366
+ createCredential(partnerId: string, input?: CreateCredentialRequest): Promise<CreateCredentialResponse>;
367
+ /** Secreto nuevo (una vez); el anterior sigue firmando `graceMinutes` (default 60). */
368
+ rotateCredential(partnerId: string, keyId: string, input?: RotateCredentialRequest): Promise<RotateCredentialResponse>;
369
+ /** Inmediato y sin vuelta. */
370
+ revokeCredential(partnerId: string, keyId: string): Promise<RevokeCredentialResponse>;
371
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@easybits.cloud/mailmask",
3
- "version": "0.4.12",
3
+ "version": "0.6.1",
4
4
  "description": "Official SDK for the MailMask email alias/forwarding API",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",