@easybits.cloud/mailmask 0.6.1 → 0.8.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/dist/index.cjs CHANGED
@@ -21,11 +21,15 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
23
  AuthError: () => AuthError,
24
+ BALANCE_LOW_FRACTION: () => BALANCE_LOW_FRACTION,
25
+ BALANCE_MOVEMENT_KINDS: () => BALANCE_MOVEMENT_KINDS,
24
26
  ConflictError: () => ConflictError,
25
27
  DOMAIN_CLAIM_TTL_DAYS: () => DOMAIN_CLAIM_TTL_DAYS,
28
+ DailyLimitError: () => DailyLimitError,
26
29
  EXTERNAL_ID_RE: () => EXTERNAL_ID_RE,
27
30
  FALLBACK_LABEL_RE: () => FALLBACK_LABEL_RE,
28
31
  ForbiddenError: () => ForbiddenError,
32
+ InsufficientBalanceError: () => InsufficientBalanceError,
29
33
  MAX_BATCH_RECIPIENTS: () => MAX_BATCH_RECIPIENTS,
30
34
  MAX_TAGS: () => MAX_TAGS,
31
35
  MAX_TEMPLATE_VARS: () => MAX_TEMPLATE_VARS,
@@ -42,9 +46,14 @@ __export(index_exports, {
42
46
  PARTNER_API_PREFIX: () => PARTNER_API_PREFIX,
43
47
  PARTNER_ERROR_STATUS: () => PARTNER_ERROR_STATUS,
44
48
  PARTNER_HEADERS: () => PARTNER_HEADERS,
49
+ PARTNER_PRICE_PER_EMAIL_CENTS: () => PARTNER_PRICE_PER_EMAIL_CENTS,
45
50
  PARTNER_ROUTES: () => PARTNER_ROUTES,
46
51
  PARTNER_SIGNATURE_TOLERANCE_S: () => PARTNER_SIGNATURE_TOLERANCE_S,
52
+ PARTNER_TENANT_MONTH_CENTS: () => PARTNER_TENANT_MONTH_CENTS,
53
+ PARTNER_TOPUP_MAX_CENTS: () => PARTNER_TOPUP_MAX_CENTS,
54
+ PARTNER_TOPUP_MIN_CENTS: () => PARTNER_TOPUP_MIN_CENTS,
47
55
  PARTNER_WEBHOOK_EVENTS: () => PARTNER_WEBHOOK_EVENTS,
56
+ PARTNER_WELCOME_CENTS: () => PARTNER_WELCOME_CENTS,
48
57
  PartnerApiError: () => PartnerApiError,
49
58
  PausedError: () => PausedError,
50
59
  QuotaError: () => QuotaError,
@@ -55,12 +64,16 @@ __export(index_exports, {
55
64
  STRICT_FIRST_DAYS: () => STRICT_FIRST_DAYS,
56
65
  TAG_RE: () => TAG_RE,
57
66
  TEMPLATE_VAR_NAME_RE: () => TEMPLATE_VAR_NAME_RE,
67
+ TENANT_DAILY_LIMIT_MAX: () => TENANT_DAILY_LIMIT_MAX,
68
+ TENANT_DAILY_LIMIT_START: () => TENANT_DAILY_LIMIT_START,
69
+ TENANT_WARMUP_STEP_DAYS: () => TENANT_WARMUP_STEP_DAYS,
58
70
  TimeoutError: () => TimeoutError,
59
71
  UNSUBSCRIBE_URL_VAR: () => UNSUBSCRIBE_URL_VAR,
60
72
  ValidationError: () => ValidationError,
61
73
  WEBHOOK_HEADERS: () => WEBHOOK_HEADERS,
62
74
  WEBHOOK_TOLERANCE_MS: () => WEBHOOK_TOLERANCE_MS,
63
75
  createWebhookHandler: () => createWebhookHandler,
76
+ estimateSendDays: () => estimateSendDays,
64
77
  isValidExternalId: () => isValidExternalId,
65
78
  isValidFallbackLabel: () => isValidFallbackLabel,
66
79
  partnerSignatureBase: () => partnerSignatureBase,
@@ -73,6 +86,7 @@ __export(index_exports, {
73
86
  templateVarsProblem: () => templateVarsProblem,
74
87
  verifyWebhook: () => verifyWebhook,
75
88
  verifyWebhookSignature: () => verifyWebhookSignature,
89
+ warmupLimit: () => warmupLimit,
76
90
  webhookSignatureBase: () => webhookSignatureBase
77
91
  });
78
92
  module.exports = __toCommonJS(index_exports);
@@ -716,7 +730,17 @@ var PARTNER_ERROR_STATUS = {
716
730
  recipient_suppressed: 422,
717
731
  /** El tenant está en pausa (por MailMask o por el partner). No se manda nada hasta `resume`. */
718
732
  tenant_paused: 423,
719
- /** Cuota mensual del tenant agotada. */
733
+ /**
734
+ * Saldo prepagado insuficiente (con `balanceCents` y `requiredCents`). No se reintenta: recarga
735
+ * con `POST /topup`.
736
+ */
737
+ insufficient_balance: 402,
738
+ /**
739
+ * El tenant llegó a su límite del día (con `limit`, `sentToday` y `resetsAt`, la medianoche UTC).
740
+ * Sólo en envíos sueltos: un lote no falla, espera al día siguiente (`batch.throttled`).
741
+ */
742
+ daily_limit: 429,
743
+ /** Ya no sale desde la 0.8 (no hay tope mensual); se deja para clientes viejos. */
720
744
  quota_exceeded: 429,
721
745
  /** Demasiadas peticiones; reintenta tras `Retry-After`. */
722
746
  rate_limited: 429,
@@ -730,6 +754,30 @@ function isValidExternalId(id) {
730
754
  var MAX_BATCH_RECIPIENTS = 1e4;
731
755
  var MAX_TAGS = 10;
732
756
  var TAG_RE = /^[A-Za-z0-9_-]{1,64}$/;
757
+ var TENANT_DAILY_LIMIT_START = 2e3;
758
+ var TENANT_DAILY_LIMIT_MAX = 5e4;
759
+ var TENANT_WARMUP_STEP_DAYS = 7;
760
+ function warmupLimit(step) {
761
+ return Math.min(TENANT_DAILY_LIMIT_MAX, TENANT_DAILY_LIMIT_START * 2 ** Math.max(0, step));
762
+ }
763
+ function estimateSendDays(s, recipients) {
764
+ let left = recipients - Math.max(0, s.dailyLimit - s.sentToday);
765
+ if (left <= 0) return 1;
766
+ let days = 1;
767
+ let limit = s.dailyLimit;
768
+ let nextAt = s.nextStepAt ? Date.parse(s.nextStepAt) : null;
769
+ let day = Date.parse(s.resetsAt);
770
+ while (left > 0 && days < 3650) {
771
+ while (nextAt !== null && nextAt <= day && limit < TENANT_DAILY_LIMIT_MAX && !s.fixed) {
772
+ limit = Math.min(TENANT_DAILY_LIMIT_MAX, limit * 2);
773
+ nextAt += TENANT_WARMUP_STEP_DAYS * 864e5;
774
+ }
775
+ left -= limit;
776
+ days++;
777
+ day += 864e5;
778
+ }
779
+ return days;
780
+ }
733
781
  var DOMAIN_CLAIM_TTL_DAYS = 7;
734
782
  var FALLBACK_LABEL_RE = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
735
783
  var RESERVED_FALLBACK_LABELS = [
@@ -795,6 +843,13 @@ function personalizePartnerMessage(input) {
795
843
  ...text !== void 0 ? { text: renderTemplate(text, vars) } : {}
796
844
  };
797
845
  }
846
+ var PARTNER_PRICE_PER_EMAIL_CENTS = 6;
847
+ var PARTNER_TENANT_MONTH_CENTS = 4900;
848
+ var PARTNER_WELCOME_CENTS = 5e4;
849
+ var PARTNER_TOPUP_MIN_CENTS = 5e4;
850
+ var PARTNER_TOPUP_MAX_CENTS = 1e7;
851
+ var BALANCE_LOW_FRACTION = 0.2;
852
+ var BALANCE_MOVEMENT_KINDS = ["topup", "send", "tenant_fee", "refund", "adjust"];
798
853
  var PARTNER_WEBHOOK_EVENTS = [
799
854
  "email.sent",
800
855
  "email.delivered",
@@ -804,7 +859,9 @@ var PARTNER_WEBHOOK_EVENTS = [
804
859
  "contact.unsubscribed",
805
860
  "tenant.paused",
806
861
  "tenant.resumed",
807
- "domain.verified"
862
+ "domain.verified",
863
+ "balance.low",
864
+ "batch.throttled"
808
865
  ];
809
866
  var WEBHOOK_HEADERS = {
810
867
  event: "X-MailMask-Event",
@@ -836,6 +893,9 @@ var PARTNER_ROUTES = {
836
893
  batch: { method: "POST", path: "/tenants/:externalId/batch" },
837
894
  getBatch: { method: "GET", path: "/tenants/:externalId/batch/:batchId" },
838
895
  usage: { method: "GET", path: "/usage" },
896
+ balance: { method: "GET", path: "/balance" },
897
+ balanceMovements: { method: "GET", path: "/balance/movements" },
898
+ topup: { method: "POST", path: "/topup" },
839
899
  getWebhook: { method: "GET", path: "/webhook" },
840
900
  setWebhook: { method: "PUT", path: "/webhook" },
841
901
  testWebhook: { method: "POST", path: "/webhook/test" },
@@ -851,6 +911,14 @@ var PARTNER_ADMIN_ROUTES = {
851
911
  setFallbackZone: { method: "PUT", path: "/api/partners/:partnerId/fallback-zone" },
852
912
  /** La zona de respaldo y si la delegación ya se ve en el DNS público. */
853
913
  getFallbackZone: { method: "GET", path: "/api/partners/:partnerId/fallback-zone" },
914
+ /** El saldo del partner (dueño o MailMask). */
915
+ getBalance: { method: "GET", path: "/api/partners/:partnerId/balance" },
916
+ /** Sólo MailMask (admin): abono o cargo manual (`adjust`). */
917
+ adjustBalance: { method: "POST", path: "/api/partners/:partnerId/balance/adjust" },
918
+ /** Recarga con MercadoPago desde la sesión del dueño (misma respuesta que `POST /topup`). */
919
+ topup: { method: "POST", path: "/api/partners/:partnerId/topup" },
920
+ /** Sólo MailMask: fija (o quita, con `null`) el límite diario de un tenant. Devuelve el `Tenant`. */
921
+ setTenantDailyLimit: { method: "PUT", path: "/api/partners/:partnerId/tenants/:externalId/daily-limit" },
854
922
  listCredentials: { method: "GET", path: "/api/partners/:partnerId/credentials" },
855
923
  createCredential: { method: "POST", path: "/api/partners/:partnerId/credentials" },
856
924
  rotateCredential: { method: "POST", path: "/api/partners/:partnerId/credentials/:keyId/rotate" },
@@ -864,8 +932,8 @@ var PartnerApiError = class extends MailMaskError {
864
932
  code: body.code,
865
933
  requestId: meta.requestId,
866
934
  cause: meta.cause,
867
- // `quota_exceeded` es un 429 que no se arregla reintentando: hasta el mes siguiente.
868
- retryable: status === 0 || status >= 500 || status === 429 && body.code !== "quota_exceeded"
935
+ // `daily_limit` (y el viejo `quota_exceeded`) son 429 que no se arreglan reintentando: hasta mañana.
936
+ retryable: status === 0 || status >= 500 || status === 429 && body.code !== "quota_exceeded" && body.code !== "daily_limit"
869
937
  });
870
938
  this.kind = "unknown";
871
939
  this.name = "PartnerApiError";
@@ -968,6 +1036,29 @@ var RateLimitError = class extends QuotaError {
968
1036
  this.kind = "rate_limit";
969
1037
  }
970
1038
  };
1039
+ var DailyLimitError = class extends QuotaError {
1040
+ constructor() {
1041
+ super(...arguments);
1042
+ this.name = "DailyLimitError";
1043
+ this.kind = "daily_limit";
1044
+ }
1045
+ get sentToday() {
1046
+ return this.body.sentToday;
1047
+ }
1048
+ };
1049
+ var InsufficientBalanceError = class extends PartnerApiError {
1050
+ constructor() {
1051
+ super(...arguments);
1052
+ this.name = "InsufficientBalanceError";
1053
+ this.kind = "insufficient_balance";
1054
+ }
1055
+ get balanceCents() {
1056
+ return this.body.balanceCents;
1057
+ }
1058
+ get requiredCents() {
1059
+ return this.body.requiredCents;
1060
+ }
1061
+ };
971
1062
  var NetworkError = class extends PartnerApiError {
972
1063
  constructor(message, cause) {
973
1064
  super(0, message, {}, { cause });
@@ -986,10 +1077,13 @@ function errorFor(status, body, fallback, meta = {}) {
986
1077
  const msg = body.error || fallback;
987
1078
  if (body.code === "reputation_hold") return new ReputationHoldError(status, msg, body, meta);
988
1079
  if (body.code === "recipient_suppressed") return new RecipientSuppressedError(status, msg, body, meta);
1080
+ if (status === 429 && body.code === "daily_limit") return new DailyLimitError(status, msg, body, meta);
989
1081
  if (status === 429 && body.code !== "quota_exceeded") return new RateLimitError(status, msg, body, meta);
990
1082
  switch (status) {
991
1083
  case 401:
992
1084
  return new AuthError(status, msg, body, meta);
1085
+ case 402:
1086
+ return new InsufficientBalanceError(status, msg, body, meta);
993
1087
  case 403:
994
1088
  return new ForbiddenError(status, msg, body, meta);
995
1089
  case 404:
@@ -1190,6 +1284,26 @@ var MailMaskPartner = class {
1190
1284
  async batchStatus(externalId, batchId) {
1191
1285
  return (await this.request("GET", routePath(PARTNER_ROUTES.getBatch.path, { externalId, batchId }))).data;
1192
1286
  }
1287
+ /** Saldo prepagado: saldo, precios, última recarga y consumo del mes (centavos MXN). */
1288
+ async balance() {
1289
+ return (await this.request("GET", PARTNER_ROUTES.balance.path)).data;
1290
+ }
1291
+ /** Movimientos del saldo, del más nuevo al más viejo; pagina con `nextCursor`. */
1292
+ async balanceMovements(query = {}) {
1293
+ const q = new URLSearchParams();
1294
+ if (query.cursor) q.set("cursor", query.cursor);
1295
+ if (query.limit !== void 0) q.set("limit", String(query.limit));
1296
+ if (query.kind?.length) q.set("kind", query.kind.join(","));
1297
+ const qs = q.toString();
1298
+ return (await this.request("GET", `${PARTNER_ROUTES.balanceMovements.path}${qs ? `?${qs}` : ""}`)).data;
1299
+ }
1300
+ /**
1301
+ * Liga de MercadoPago para recargar saldo (pago único, $500 a $100,000 MXN). El saldo sube cuando
1302
+ * MercadoPago confirma el pago, no al crear la liga. Abre `initPoint` en el navegador de quien paga.
1303
+ */
1304
+ async topup(input) {
1305
+ return (await this.request("POST", PARTNER_ROUTES.topup.path, input)).data;
1306
+ }
1193
1307
  /** Uso del mes (`YYYY-MM`, default el mes en curso UTC): tenants activos y envíos. */
1194
1308
  async usage(opts = {}) {
1195
1309
  const q = opts.month ? `?month=${encodeURIComponent(opts.month)}` : "";
@@ -1394,6 +1508,10 @@ var MailMaskPartnerAdmin = class {
1394
1508
  resumeTenant(partnerId, externalId) {
1395
1509
  return this.req("resumeTenant", { partnerId, externalId });
1396
1510
  }
1511
+ /** Sólo MailMask: tope diario fijo del tenant, o `null` para volver al calentamiento. */
1512
+ setTenantDailyLimit(partnerId, externalId, limit) {
1513
+ return this.req("setTenantDailyLimit", { partnerId, externalId }, { limit });
1514
+ }
1397
1515
  /**
1398
1516
  * Sólo MailMask (admin): crea o adopta la hosted zone de la zona de respaldo (`marketing.denik.me`)
1399
1517
  * y devuelve los NS que el partner tiene que delegar. Idempotente con la misma zona.
@@ -1405,6 +1523,18 @@ var MailMaskPartnerAdmin = class {
1405
1523
  getFallbackZone(partnerId) {
1406
1524
  return this.req("getFallbackZone", { partnerId });
1407
1525
  }
1526
+ /** Saldo del partner con sus últimos 20 movimientos (dueño o MailMask). */
1527
+ getBalance(partnerId) {
1528
+ return this.req("getBalance", { partnerId });
1529
+ }
1530
+ /** Sólo MailMask (admin): abono (o cargo, negativo) manual. Con la misma `idempotencyKey` no se repite. */
1531
+ adjustBalance(partnerId, input) {
1532
+ return this.req("adjustBalance", { partnerId }, input);
1533
+ }
1534
+ /** Liga de MercadoPago para recargar, con la llave del dueño. */
1535
+ topup(partnerId, input) {
1536
+ return this.req("topup", { partnerId }, input);
1537
+ }
1408
1538
  listCredentials(partnerId) {
1409
1539
  return this.req("listCredentials", { partnerId });
1410
1540
  }
@@ -1424,11 +1554,15 @@ var MailMaskPartnerAdmin = class {
1424
1554
  // Annotate the CommonJS export names for ESM import in node:
1425
1555
  0 && (module.exports = {
1426
1556
  AuthError,
1557
+ BALANCE_LOW_FRACTION,
1558
+ BALANCE_MOVEMENT_KINDS,
1427
1559
  ConflictError,
1428
1560
  DOMAIN_CLAIM_TTL_DAYS,
1561
+ DailyLimitError,
1429
1562
  EXTERNAL_ID_RE,
1430
1563
  FALLBACK_LABEL_RE,
1431
1564
  ForbiddenError,
1565
+ InsufficientBalanceError,
1432
1566
  MAX_BATCH_RECIPIENTS,
1433
1567
  MAX_TAGS,
1434
1568
  MAX_TEMPLATE_VARS,
@@ -1445,9 +1579,14 @@ var MailMaskPartnerAdmin = class {
1445
1579
  PARTNER_API_PREFIX,
1446
1580
  PARTNER_ERROR_STATUS,
1447
1581
  PARTNER_HEADERS,
1582
+ PARTNER_PRICE_PER_EMAIL_CENTS,
1448
1583
  PARTNER_ROUTES,
1449
1584
  PARTNER_SIGNATURE_TOLERANCE_S,
1585
+ PARTNER_TENANT_MONTH_CENTS,
1586
+ PARTNER_TOPUP_MAX_CENTS,
1587
+ PARTNER_TOPUP_MIN_CENTS,
1450
1588
  PARTNER_WEBHOOK_EVENTS,
1589
+ PARTNER_WELCOME_CENTS,
1451
1590
  PartnerApiError,
1452
1591
  PausedError,
1453
1592
  QuotaError,
@@ -1458,12 +1597,16 @@ var MailMaskPartnerAdmin = class {
1458
1597
  STRICT_FIRST_DAYS,
1459
1598
  TAG_RE,
1460
1599
  TEMPLATE_VAR_NAME_RE,
1600
+ TENANT_DAILY_LIMIT_MAX,
1601
+ TENANT_DAILY_LIMIT_START,
1602
+ TENANT_WARMUP_STEP_DAYS,
1461
1603
  TimeoutError,
1462
1604
  UNSUBSCRIBE_URL_VAR,
1463
1605
  ValidationError,
1464
1606
  WEBHOOK_HEADERS,
1465
1607
  WEBHOOK_TOLERANCE_MS,
1466
1608
  createWebhookHandler,
1609
+ estimateSendDays,
1467
1610
  isValidExternalId,
1468
1611
  isValidFallbackLabel,
1469
1612
  partnerSignatureBase,
@@ -1476,5 +1619,6 @@ var MailMaskPartnerAdmin = class {
1476
1619
  templateVarsProblem,
1477
1620
  verifyWebhook,
1478
1621
  verifyWebhookSignature,
1622
+ warmupLimit,
1479
1623
  webhookSignatureBase
1480
1624
  });
package/dist/index.js CHANGED
@@ -637,7 +637,17 @@ var PARTNER_ERROR_STATUS = {
637
637
  recipient_suppressed: 422,
638
638
  /** El tenant está en pausa (por MailMask o por el partner). No se manda nada hasta `resume`. */
639
639
  tenant_paused: 423,
640
- /** Cuota mensual del tenant agotada. */
640
+ /**
641
+ * Saldo prepagado insuficiente (con `balanceCents` y `requiredCents`). No se reintenta: recarga
642
+ * con `POST /topup`.
643
+ */
644
+ insufficient_balance: 402,
645
+ /**
646
+ * El tenant llegó a su límite del día (con `limit`, `sentToday` y `resetsAt`, la medianoche UTC).
647
+ * Sólo en envíos sueltos: un lote no falla, espera al día siguiente (`batch.throttled`).
648
+ */
649
+ daily_limit: 429,
650
+ /** Ya no sale desde la 0.8 (no hay tope mensual); se deja para clientes viejos. */
641
651
  quota_exceeded: 429,
642
652
  /** Demasiadas peticiones; reintenta tras `Retry-After`. */
643
653
  rate_limited: 429,
@@ -651,6 +661,30 @@ function isValidExternalId(id) {
651
661
  var MAX_BATCH_RECIPIENTS = 1e4;
652
662
  var MAX_TAGS = 10;
653
663
  var TAG_RE = /^[A-Za-z0-9_-]{1,64}$/;
664
+ var TENANT_DAILY_LIMIT_START = 2e3;
665
+ var TENANT_DAILY_LIMIT_MAX = 5e4;
666
+ var TENANT_WARMUP_STEP_DAYS = 7;
667
+ function warmupLimit(step) {
668
+ return Math.min(TENANT_DAILY_LIMIT_MAX, TENANT_DAILY_LIMIT_START * 2 ** Math.max(0, step));
669
+ }
670
+ function estimateSendDays(s, recipients) {
671
+ let left = recipients - Math.max(0, s.dailyLimit - s.sentToday);
672
+ if (left <= 0) return 1;
673
+ let days = 1;
674
+ let limit = s.dailyLimit;
675
+ let nextAt = s.nextStepAt ? Date.parse(s.nextStepAt) : null;
676
+ let day = Date.parse(s.resetsAt);
677
+ while (left > 0 && days < 3650) {
678
+ while (nextAt !== null && nextAt <= day && limit < TENANT_DAILY_LIMIT_MAX && !s.fixed) {
679
+ limit = Math.min(TENANT_DAILY_LIMIT_MAX, limit * 2);
680
+ nextAt += TENANT_WARMUP_STEP_DAYS * 864e5;
681
+ }
682
+ left -= limit;
683
+ days++;
684
+ day += 864e5;
685
+ }
686
+ return days;
687
+ }
654
688
  var DOMAIN_CLAIM_TTL_DAYS = 7;
655
689
  var FALLBACK_LABEL_RE = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
656
690
  var RESERVED_FALLBACK_LABELS = [
@@ -716,6 +750,13 @@ function personalizePartnerMessage(input) {
716
750
  ...text !== void 0 ? { text: renderTemplate(text, vars) } : {}
717
751
  };
718
752
  }
753
+ var PARTNER_PRICE_PER_EMAIL_CENTS = 6;
754
+ var PARTNER_TENANT_MONTH_CENTS = 4900;
755
+ var PARTNER_WELCOME_CENTS = 5e4;
756
+ var PARTNER_TOPUP_MIN_CENTS = 5e4;
757
+ var PARTNER_TOPUP_MAX_CENTS = 1e7;
758
+ var BALANCE_LOW_FRACTION = 0.2;
759
+ var BALANCE_MOVEMENT_KINDS = ["topup", "send", "tenant_fee", "refund", "adjust"];
719
760
  var PARTNER_WEBHOOK_EVENTS = [
720
761
  "email.sent",
721
762
  "email.delivered",
@@ -725,7 +766,9 @@ var PARTNER_WEBHOOK_EVENTS = [
725
766
  "contact.unsubscribed",
726
767
  "tenant.paused",
727
768
  "tenant.resumed",
728
- "domain.verified"
769
+ "domain.verified",
770
+ "balance.low",
771
+ "batch.throttled"
729
772
  ];
730
773
  var WEBHOOK_HEADERS = {
731
774
  event: "X-MailMask-Event",
@@ -757,6 +800,9 @@ var PARTNER_ROUTES = {
757
800
  batch: { method: "POST", path: "/tenants/:externalId/batch" },
758
801
  getBatch: { method: "GET", path: "/tenants/:externalId/batch/:batchId" },
759
802
  usage: { method: "GET", path: "/usage" },
803
+ balance: { method: "GET", path: "/balance" },
804
+ balanceMovements: { method: "GET", path: "/balance/movements" },
805
+ topup: { method: "POST", path: "/topup" },
760
806
  getWebhook: { method: "GET", path: "/webhook" },
761
807
  setWebhook: { method: "PUT", path: "/webhook" },
762
808
  testWebhook: { method: "POST", path: "/webhook/test" },
@@ -772,6 +818,14 @@ var PARTNER_ADMIN_ROUTES = {
772
818
  setFallbackZone: { method: "PUT", path: "/api/partners/:partnerId/fallback-zone" },
773
819
  /** La zona de respaldo y si la delegación ya se ve en el DNS público. */
774
820
  getFallbackZone: { method: "GET", path: "/api/partners/:partnerId/fallback-zone" },
821
+ /** El saldo del partner (dueño o MailMask). */
822
+ getBalance: { method: "GET", path: "/api/partners/:partnerId/balance" },
823
+ /** Sólo MailMask (admin): abono o cargo manual (`adjust`). */
824
+ adjustBalance: { method: "POST", path: "/api/partners/:partnerId/balance/adjust" },
825
+ /** Recarga con MercadoPago desde la sesión del dueño (misma respuesta que `POST /topup`). */
826
+ topup: { method: "POST", path: "/api/partners/:partnerId/topup" },
827
+ /** Sólo MailMask: fija (o quita, con `null`) el límite diario de un tenant. Devuelve el `Tenant`. */
828
+ setTenantDailyLimit: { method: "PUT", path: "/api/partners/:partnerId/tenants/:externalId/daily-limit" },
775
829
  listCredentials: { method: "GET", path: "/api/partners/:partnerId/credentials" },
776
830
  createCredential: { method: "POST", path: "/api/partners/:partnerId/credentials" },
777
831
  rotateCredential: { method: "POST", path: "/api/partners/:partnerId/credentials/:keyId/rotate" },
@@ -785,8 +839,8 @@ var PartnerApiError = class extends MailMaskError {
785
839
  code: body.code,
786
840
  requestId: meta.requestId,
787
841
  cause: meta.cause,
788
- // `quota_exceeded` es un 429 que no se arregla reintentando: hasta el mes siguiente.
789
- retryable: status === 0 || status >= 500 || status === 429 && body.code !== "quota_exceeded"
842
+ // `daily_limit` (y el viejo `quota_exceeded`) son 429 que no se arreglan reintentando: hasta mañana.
843
+ retryable: status === 0 || status >= 500 || status === 429 && body.code !== "quota_exceeded" && body.code !== "daily_limit"
790
844
  });
791
845
  this.kind = "unknown";
792
846
  this.name = "PartnerApiError";
@@ -889,6 +943,29 @@ var RateLimitError = class extends QuotaError {
889
943
  this.kind = "rate_limit";
890
944
  }
891
945
  };
946
+ var DailyLimitError = class extends QuotaError {
947
+ constructor() {
948
+ super(...arguments);
949
+ this.name = "DailyLimitError";
950
+ this.kind = "daily_limit";
951
+ }
952
+ get sentToday() {
953
+ return this.body.sentToday;
954
+ }
955
+ };
956
+ var InsufficientBalanceError = class extends PartnerApiError {
957
+ constructor() {
958
+ super(...arguments);
959
+ this.name = "InsufficientBalanceError";
960
+ this.kind = "insufficient_balance";
961
+ }
962
+ get balanceCents() {
963
+ return this.body.balanceCents;
964
+ }
965
+ get requiredCents() {
966
+ return this.body.requiredCents;
967
+ }
968
+ };
892
969
  var NetworkError = class extends PartnerApiError {
893
970
  constructor(message, cause) {
894
971
  super(0, message, {}, { cause });
@@ -907,10 +984,13 @@ function errorFor(status, body, fallback, meta = {}) {
907
984
  const msg = body.error || fallback;
908
985
  if (body.code === "reputation_hold") return new ReputationHoldError(status, msg, body, meta);
909
986
  if (body.code === "recipient_suppressed") return new RecipientSuppressedError(status, msg, body, meta);
987
+ if (status === 429 && body.code === "daily_limit") return new DailyLimitError(status, msg, body, meta);
910
988
  if (status === 429 && body.code !== "quota_exceeded") return new RateLimitError(status, msg, body, meta);
911
989
  switch (status) {
912
990
  case 401:
913
991
  return new AuthError(status, msg, body, meta);
992
+ case 402:
993
+ return new InsufficientBalanceError(status, msg, body, meta);
914
994
  case 403:
915
995
  return new ForbiddenError(status, msg, body, meta);
916
996
  case 404:
@@ -1111,6 +1191,26 @@ var MailMaskPartner = class {
1111
1191
  async batchStatus(externalId, batchId) {
1112
1192
  return (await this.request("GET", routePath(PARTNER_ROUTES.getBatch.path, { externalId, batchId }))).data;
1113
1193
  }
1194
+ /** Saldo prepagado: saldo, precios, última recarga y consumo del mes (centavos MXN). */
1195
+ async balance() {
1196
+ return (await this.request("GET", PARTNER_ROUTES.balance.path)).data;
1197
+ }
1198
+ /** Movimientos del saldo, del más nuevo al más viejo; pagina con `nextCursor`. */
1199
+ async balanceMovements(query = {}) {
1200
+ const q = new URLSearchParams();
1201
+ if (query.cursor) q.set("cursor", query.cursor);
1202
+ if (query.limit !== void 0) q.set("limit", String(query.limit));
1203
+ if (query.kind?.length) q.set("kind", query.kind.join(","));
1204
+ const qs = q.toString();
1205
+ return (await this.request("GET", `${PARTNER_ROUTES.balanceMovements.path}${qs ? `?${qs}` : ""}`)).data;
1206
+ }
1207
+ /**
1208
+ * Liga de MercadoPago para recargar saldo (pago único, $500 a $100,000 MXN). El saldo sube cuando
1209
+ * MercadoPago confirma el pago, no al crear la liga. Abre `initPoint` en el navegador de quien paga.
1210
+ */
1211
+ async topup(input) {
1212
+ return (await this.request("POST", PARTNER_ROUTES.topup.path, input)).data;
1213
+ }
1114
1214
  /** Uso del mes (`YYYY-MM`, default el mes en curso UTC): tenants activos y envíos. */
1115
1215
  async usage(opts = {}) {
1116
1216
  const q = opts.month ? `?month=${encodeURIComponent(opts.month)}` : "";
@@ -1315,6 +1415,10 @@ var MailMaskPartnerAdmin = class {
1315
1415
  resumeTenant(partnerId, externalId) {
1316
1416
  return this.req("resumeTenant", { partnerId, externalId });
1317
1417
  }
1418
+ /** Sólo MailMask: tope diario fijo del tenant, o `null` para volver al calentamiento. */
1419
+ setTenantDailyLimit(partnerId, externalId, limit) {
1420
+ return this.req("setTenantDailyLimit", { partnerId, externalId }, { limit });
1421
+ }
1318
1422
  /**
1319
1423
  * Sólo MailMask (admin): crea o adopta la hosted zone de la zona de respaldo (`marketing.denik.me`)
1320
1424
  * y devuelve los NS que el partner tiene que delegar. Idempotente con la misma zona.
@@ -1326,6 +1430,18 @@ var MailMaskPartnerAdmin = class {
1326
1430
  getFallbackZone(partnerId) {
1327
1431
  return this.req("getFallbackZone", { partnerId });
1328
1432
  }
1433
+ /** Saldo del partner con sus últimos 20 movimientos (dueño o MailMask). */
1434
+ getBalance(partnerId) {
1435
+ return this.req("getBalance", { partnerId });
1436
+ }
1437
+ /** Sólo MailMask (admin): abono (o cargo, negativo) manual. Con la misma `idempotencyKey` no se repite. */
1438
+ adjustBalance(partnerId, input) {
1439
+ return this.req("adjustBalance", { partnerId }, input);
1440
+ }
1441
+ /** Liga de MercadoPago para recargar, con la llave del dueño. */
1442
+ topup(partnerId, input) {
1443
+ return this.req("topup", { partnerId }, input);
1444
+ }
1329
1445
  listCredentials(partnerId) {
1330
1446
  return this.req("listCredentials", { partnerId });
1331
1447
  }
@@ -1344,11 +1460,15 @@ var MailMaskPartnerAdmin = class {
1344
1460
  };
1345
1461
  export {
1346
1462
  AuthError,
1463
+ BALANCE_LOW_FRACTION,
1464
+ BALANCE_MOVEMENT_KINDS,
1347
1465
  ConflictError,
1348
1466
  DOMAIN_CLAIM_TTL_DAYS,
1467
+ DailyLimitError,
1349
1468
  EXTERNAL_ID_RE,
1350
1469
  FALLBACK_LABEL_RE,
1351
1470
  ForbiddenError,
1471
+ InsufficientBalanceError,
1352
1472
  MAX_BATCH_RECIPIENTS,
1353
1473
  MAX_TAGS,
1354
1474
  MAX_TEMPLATE_VARS,
@@ -1365,9 +1485,14 @@ export {
1365
1485
  PARTNER_API_PREFIX,
1366
1486
  PARTNER_ERROR_STATUS,
1367
1487
  PARTNER_HEADERS,
1488
+ PARTNER_PRICE_PER_EMAIL_CENTS,
1368
1489
  PARTNER_ROUTES,
1369
1490
  PARTNER_SIGNATURE_TOLERANCE_S,
1491
+ PARTNER_TENANT_MONTH_CENTS,
1492
+ PARTNER_TOPUP_MAX_CENTS,
1493
+ PARTNER_TOPUP_MIN_CENTS,
1370
1494
  PARTNER_WEBHOOK_EVENTS,
1495
+ PARTNER_WELCOME_CENTS,
1371
1496
  PartnerApiError,
1372
1497
  PausedError,
1373
1498
  QuotaError,
@@ -1378,12 +1503,16 @@ export {
1378
1503
  STRICT_FIRST_DAYS,
1379
1504
  TAG_RE,
1380
1505
  TEMPLATE_VAR_NAME_RE,
1506
+ TENANT_DAILY_LIMIT_MAX,
1507
+ TENANT_DAILY_LIMIT_START,
1508
+ TENANT_WARMUP_STEP_DAYS,
1381
1509
  TimeoutError,
1382
1510
  UNSUBSCRIBE_URL_VAR,
1383
1511
  ValidationError,
1384
1512
  WEBHOOK_HEADERS,
1385
1513
  WEBHOOK_TOLERANCE_MS,
1386
1514
  createWebhookHandler,
1515
+ estimateSendDays,
1387
1516
  isValidExternalId,
1388
1517
  isValidFallbackLabel,
1389
1518
  partnerSignatureBase,
@@ -1396,5 +1525,6 @@ export {
1396
1525
  templateVarsProblem,
1397
1526
  verifyWebhook,
1398
1527
  verifyWebhookSignature,
1528
+ warmupLimit,
1399
1529
  webhookSignatureBase
1400
1530
  };
@@ -59,7 +59,17 @@ export declare const PARTNER_ERROR_STATUS: {
59
59
  readonly recipient_suppressed: 422;
60
60
  /** El tenant está en pausa (por MailMask o por el partner). No se manda nada hasta `resume`. */
61
61
  readonly tenant_paused: 423;
62
- /** Cuota mensual del tenant agotada. */
62
+ /**
63
+ * Saldo prepagado insuficiente (con `balanceCents` y `requiredCents`). No se reintenta: recarga
64
+ * con `POST /topup`.
65
+ */
66
+ readonly insufficient_balance: 402;
67
+ /**
68
+ * El tenant llegó a su límite del día (con `limit`, `sentToday` y `resetsAt`, la medianoche UTC).
69
+ * Sólo en envíos sueltos: un lote no falla, espera al día siguiente (`batch.throttled`).
70
+ */
71
+ readonly daily_limit: 429;
72
+ /** Ya no sale desde la 0.8 (no hay tope mensual); se deja para clientes viejos. */
63
73
  readonly quota_exceeded: 429;
64
74
  /** Demasiadas peticiones; reintenta tras `Retry-After`. */
65
75
  readonly rate_limited: 429;
@@ -72,9 +82,13 @@ export interface PartnerErrorBody {
72
82
  code: PartnerErrorCode;
73
83
  /** Sólo en `invalid_request`: el campo que falló, si aplica. */
74
84
  field?: string;
75
- /** Sólo en `quota_exceeded`: el tope del mes y cuándo se reinicia (ISO, día 1 a las 00:00 UTC). */
85
+ /** Sólo en `daily_limit`: el límite de hoy, lo enviado y cuándo se reinicia (medianoche UTC). */
76
86
  limit?: number;
87
+ sentToday?: number;
77
88
  resetsAt?: string;
89
+ /** Sólo en `insufficient_balance`: saldo actual y lo que pedía el envío (centavos MXN). */
90
+ balanceCents?: number;
91
+ requiredCents?: number;
78
92
  /** Sólo en `recipient_suppressed`. */
79
93
  reason?: SuppressionReason;
80
94
  stream?: MessageStream;
@@ -104,7 +118,38 @@ export interface Tenant {
104
118
  lastSendAt: string | null;
105
119
  createdAt: string;
106
120
  updatedAt: string;
121
+ /** Límite diario con calentamiento. Ver `TenantSending`. */
122
+ sending: TenantSending;
107
123
  }
124
+ /**
125
+ * Límite diario por tenant que crece solo, como el calentamiento de SES: empieza en
126
+ * `TENANT_DAILY_LIMIT_START` al primer envío y se duplica cada `TENANT_WARMUP_STEP_DAYS` días sanos
127
+ * (rebotes < 2 %, quejas < 0.1 %) hasta `TENANT_DAILY_LIMIT_MAX`. Una pausa por reputación lo
128
+ * regresa al inicio al reanudar. No hay tope mensual: el saldo es el otro freno.
129
+ */
130
+ export declare const TENANT_DAILY_LIMIT_START = 2000;
131
+ export declare const TENANT_DAILY_LIMIT_MAX = 50000;
132
+ export declare const TENANT_WARMUP_STEP_DAYS = 7;
133
+ /** El límite del escalón `step` (0 = inicio). */
134
+ export declare function warmupLimit(step: number): number;
135
+ export interface TenantSending {
136
+ /** Correos que puede mandar hoy (UTC) en total. */
137
+ dailyLimit: number;
138
+ sentToday: number;
139
+ /** Medianoche UTC siguiente. */
140
+ resetsAt: string;
141
+ /** Cuándo sube al siguiente escalón si sigue sano; null = ya en el máximo, con tope fijo o sin envíos aún. */
142
+ nextStepAt: string | null;
143
+ /** El límite que tendrá en `nextStepAt`; null si no sube. */
144
+ nextStepLimit: number | null;
145
+ /** true = MailMask fijó el límite a mano (no se calienta). */
146
+ fixed: boolean;
147
+ }
148
+ /**
149
+ * Días (contando hoy) que tarda en salir un lote de `recipients` correos si los escalones llegan a
150
+ * tiempo. 1 = sale hoy. Sirve para avisar «se manda en N días» antes de encolar.
151
+ */
152
+ export declare function estimateSendDays(s: TenantSending, recipients: number): number;
108
153
  /** `PUT /tenants/:externalId` — crea o actualiza. 201 si lo creó, 200 si ya existía. */
109
154
  export interface PutTenantRequest {
110
155
  name?: string;
@@ -343,6 +388,11 @@ export interface BatchStatus {
343
388
  lastError: string | null;
344
389
  createdAt: string;
345
390
  completedAt: string | null;
391
+ /**
392
+ * El lote llegó al límite diario del tenant y sigue solo a esta hora (medianoche UTC); null si no
393
+ * está esperando por el límite. Mientras espera, el lote no caduca.
394
+ */
395
+ waitingUntil: string | null;
346
396
  /**
347
397
  * Sólo en modo de prueba: el lote se valida y responde `completed` al instante (los suprimidos en
348
398
  * `skippedSuppressed`, el resto en `sent`), con `batchId` `test_…` que no se guarda (su GET da 404).
@@ -360,6 +410,8 @@ export interface TenantUsage {
360
410
  bounced: number;
361
411
  complained: number;
362
412
  unsubscribed: number;
413
+ /** Límite diario de hoy; falta si el tenant ya se borró. */
414
+ sending?: TenantSending;
363
415
  }
364
416
  /** `GET /usage?month=YYYY-MM` (default: el mes en curso, UTC). Activo = al menos un envío en el mes. */
365
417
  export interface UsageResponse {
@@ -368,6 +420,120 @@ export interface UsageResponse {
368
420
  sent: number;
369
421
  tenants: TenantUsage[];
370
422
  }
423
+ /**
424
+ * El partner paga por adelantado: cada correo y la cuota mensual de cada tenant activo se
425
+ * descuentan de un saldo que se recarga con MercadoPago (`POST /topup`). Todo en centavos de MXN.
426
+ */
427
+ /** Precio por correo enviado (por destinatario). */
428
+ export declare const PARTNER_PRICE_PER_EMAIL_CENTS = 6;
429
+ /**
430
+ * Cuota mensual por tenant activo. Activo = envía ese mes: se descuenta con su primer envío del
431
+ * mes (UTC), una sola vez, así que un tenant dormido no cuesta nada.
432
+ */
433
+ export declare const PARTNER_TENANT_MONTH_CENTS = 4900;
434
+ /** Saldo de bienvenida con el que nace todo partner (`adjust`, idemKey `welcome:<partnerId>`). */
435
+ export declare const PARTNER_WELCOME_CENTS = 50000;
436
+ /** Recarga mínima y máxima por pago. */
437
+ export declare const PARTNER_TOPUP_MIN_CENTS = 50000;
438
+ export declare const PARTNER_TOPUP_MAX_CENTS = 10000000;
439
+ /** `balance.low` sale cuando el saldo baja del 20 % de la última recarga (una vez por recarga). */
440
+ export declare const BALANCE_LOW_FRACTION = 0.2;
441
+ export type BalanceMovementKind = "topup" | "send" | "tenant_fee" | "refund" | "adjust";
442
+ export declare const BALANCE_MOVEMENT_KINDS: readonly BalanceMovementKind[];
443
+ export interface BalanceMovement {
444
+ id: number;
445
+ kind: BalanceMovementKind;
446
+ /** Positivo = abono (recarga, devolución, ajuste a favor); negativo = cargo. */
447
+ amountCents: number;
448
+ balanceAfterCents: number;
449
+ /** Tenant del cargo (`send`, `tenant_fee`, `refund`); null en recargas y ajustes. */
450
+ externalId: string | null;
451
+ /** Referencia: `batch:<id>`, mes de la cuota (`YYYY-MM`), pago de MercadoPago… */
452
+ ref: string | null;
453
+ note: string | null;
454
+ createdAt: string;
455
+ }
456
+ /** `GET /balance` */
457
+ export interface BalanceResponse {
458
+ currency: "MXN";
459
+ /** Sin saldo suficiente el envío se rechaza (402 `insufficient_balance`) y el lote espera. Siempre. */
460
+ balanceCents: number;
461
+ prices: {
462
+ perEmailCents: number;
463
+ tenantMonthCents: number;
464
+ topupMinCents: number;
465
+ };
466
+ lastTopup: {
467
+ amountCents: number;
468
+ at: string;
469
+ } | null;
470
+ /** Debajo de esto sale `balance.low`; null sin recargas. */
471
+ lowBalanceThresholdCents: number | null;
472
+ /** Mes en curso (UTC). */
473
+ month: string;
474
+ /** Tenants que enviaron este mes (la misma cuenta que `/usage`): cada uno paga la cuota del mes. */
475
+ activeTenants: number;
476
+ /** Correos cobrados en el mes (sin los devueltos). */
477
+ monthEmails: number;
478
+ /** Lo gastado en el mes: correos + cuotas − devoluciones. */
479
+ monthSpentCents: number;
480
+ }
481
+ /** `GET /balance/movements?cursor=&limit=&kind=topup,adjust` — del más nuevo al más viejo. */
482
+ export interface BalanceMovementsQuery {
483
+ cursor?: string;
484
+ /** 1–200, default 50. */
485
+ limit?: number;
486
+ kind?: BalanceMovementKind[];
487
+ }
488
+ export interface BalanceMovementsResponse {
489
+ movements: BalanceMovement[];
490
+ nextCursor: string | null;
491
+ }
492
+ /**
493
+ * `POST /topup` — liga de pago único de MercadoPago. El saldo sube cuando MercadoPago confirma
494
+ * el pago (webhook), no al crear la liga. `returnUrl` (https) es a dónde regresa el navegador.
495
+ */
496
+ export interface TopupRequest {
497
+ /** Pesos MXN enteros, 500 a 100,000. */
498
+ amountMxn: number;
499
+ returnUrl?: string;
500
+ }
501
+ export interface TopupResponse {
502
+ topupId: string;
503
+ amountCents: number;
504
+ /** Abre esto en el navegador de quien paga. */
505
+ initPoint: string;
506
+ }
507
+ /**
508
+ * `GET /api/partners/:partnerId/balance` (dueño o MailMask): el saldo, sus últimos 20 movimientos,
509
+ * el partner (con `tenants` y `ownerEmail`) y si quien mira es de MailMask (ve abonar y el cobro).
510
+ */
511
+ /** Un tenant en la ficha: los 50 que enviaron más recientemente, con su límite de hoy. */
512
+ export interface PartnerDetailTenant {
513
+ externalId: string;
514
+ name: string | null;
515
+ status: TenantStatus;
516
+ lastSendAt: string | null;
517
+ sending: TenantSending;
518
+ }
519
+ export type PartnerBalanceDetail = BalanceResponse & {
520
+ movements: BalanceMovement[];
521
+ partner: PartnerSummary;
522
+ tenants: PartnerDetailTenant[];
523
+ staff: boolean;
524
+ };
525
+ /** `PUT /api/partners/:partnerId/tenants/:externalId/daily-limit` — sólo MailMask: tope fijo o `null` (vuelve al calentamiento). */
526
+ export interface SetTenantDailyLimitRequest {
527
+ limit: number | null;
528
+ }
529
+ /** `POST /api/partners/:partnerId/balance/adjust` — sólo MailMask (admin): abono o cargo manual. */
530
+ export interface AdjustBalanceRequest {
531
+ /** Centavos MXN, distinto de 0 (negativo = cargo). */
532
+ amountCents: number;
533
+ note: string;
534
+ /** Repetir con la misma llave no vuelve a abonar. */
535
+ idempotencyKey?: string;
536
+ }
371
537
  /** `GET /webhook` */
372
538
  export interface WebhookConfig {
373
539
  url: string | null;
@@ -401,7 +567,7 @@ export interface RotateWebhookSecretResponse {
401
567
  export interface TestWebhookResponse {
402
568
  deliveryId: string;
403
569
  }
404
- export declare const PARTNER_WEBHOOK_EVENTS: readonly ["email.sent", "email.delivered", "email.bounced", "email.complained", "email.received", "contact.unsubscribed", "tenant.paused", "tenant.resumed", "domain.verified"];
570
+ export declare const PARTNER_WEBHOOK_EVENTS: readonly ["email.sent", "email.delivered", "email.bounced", "email.complained", "email.received", "contact.unsubscribed", "tenant.paused", "tenant.resumed", "domain.verified", "balance.low", "batch.throttled"];
405
571
  export type PartnerWebhookEvent = typeof PARTNER_WEBHOOK_EVENTS[number];
406
572
  /** Headers de cada entrega al webhook del partner (los mismos que los webhooks de dominio). */
407
573
  export declare const WEBHOOK_HEADERS: {
@@ -479,9 +645,23 @@ export interface PartnerEventData {
479
645
  domain: string;
480
646
  mailFromVerified: boolean;
481
647
  };
648
+ /** El saldo prepagado bajó del 20 % de la última recarga. `externalId` va null. */
649
+ "balance.low": {
650
+ balanceCents: number;
651
+ thresholdCents: number;
652
+ lastTopupCents: number;
653
+ };
654
+ /** Un lote llegó al límite diario del tenant: sigue solo en `resumesAt`. Una vez por lote. */
655
+ "batch.throttled": {
656
+ batchId: string;
657
+ sent: number;
658
+ total: number;
659
+ dailyLimit: number;
660
+ resumesAt: string;
661
+ };
482
662
  ping: Record<string, never>;
483
663
  }
484
- /** Sobre de cada evento. `externalId` es null sólo en `ping`. */
664
+ /** Sobre de cada evento. `externalId` es null en `ping` y `balance.low`. */
485
665
  export interface PartnerWebhookPayload<E extends keyof PartnerEventData = keyof PartnerEventData> {
486
666
  /** Id de la entrega (igual que `X-MailMask-Delivery`); úsalo para descartar duplicados. */
487
667
  id: string;
@@ -498,10 +678,11 @@ export interface PartnerWebhookPayload<E extends keyof PartnerEventData = keyof
498
678
  export type ReputationPolicy = "standard" | "strict";
499
679
  /** Primer mes del partner: sus tenants van en Strict salvo que MailMask fije otra política. */
500
680
  export declare const STRICT_FIRST_DAYS = 30;
501
- /** `GET /api/partners` — los partners de la cuenta. */
681
+ /** `GET /api/partners` — los partners de la cuenta. MailMask (admin) con `?all=1`: todos, con `tenants`. */
502
682
  export interface PartnerSummary {
503
683
  id: string;
504
684
  name: string;
685
+ /** `sendsPerTenantMonth` ya no frena (0.8): el límite es diario por tenant, ver `TenantSending`. */
505
686
  plan: {
506
687
  includedTenants: number;
507
688
  sendsPerTenantMonth: number;
@@ -515,6 +696,15 @@ export interface PartnerSummary {
515
696
  effectiveReputationPolicy: ReputationPolicy;
516
697
  /** Zona de respaldo (`marketing.denik.me`); null = sin zona. Ver `FallbackZone`. */
517
698
  fallbackZone: string | null;
699
+ /** Saldo prepagado (centavos MXN). Ver `BalanceResponse`. */
700
+ balanceCents: number;
701
+ /** Sólo en `GET /api/partners?all=1` (MailMask, admin): tenants totales y activos del mes. */
702
+ tenants?: {
703
+ total: number;
704
+ activeThisMonth: number;
705
+ };
706
+ /** Sólo en `?all=1` (admin): cuenta dueña. */
707
+ ownerEmail?: string;
518
708
  }
519
709
  /** `POST /api/partners` — alta (sólo MailMask, admin). 201 con `PartnerSummary`. */
520
710
  export interface CreatePartnerRequest {
@@ -630,6 +820,18 @@ export declare const PARTNER_ROUTES: {
630
820
  readonly method: "GET";
631
821
  readonly path: "/usage";
632
822
  };
823
+ readonly balance: {
824
+ readonly method: "GET";
825
+ readonly path: "/balance";
826
+ };
827
+ readonly balanceMovements: {
828
+ readonly method: "GET";
829
+ readonly path: "/balance/movements";
830
+ };
831
+ readonly topup: {
832
+ readonly method: "POST";
833
+ readonly path: "/topup";
834
+ };
633
835
  readonly getWebhook: {
634
836
  readonly method: "GET";
635
837
  readonly path: "/webhook";
@@ -676,6 +878,26 @@ export declare const PARTNER_ADMIN_ROUTES: {
676
878
  readonly method: "GET";
677
879
  readonly path: "/api/partners/:partnerId/fallback-zone";
678
880
  };
881
+ /** El saldo del partner (dueño o MailMask). */
882
+ readonly getBalance: {
883
+ readonly method: "GET";
884
+ readonly path: "/api/partners/:partnerId/balance";
885
+ };
886
+ /** Sólo MailMask (admin): abono o cargo manual (`adjust`). */
887
+ readonly adjustBalance: {
888
+ readonly method: "POST";
889
+ readonly path: "/api/partners/:partnerId/balance/adjust";
890
+ };
891
+ /** Recarga con MercadoPago desde la sesión del dueño (misma respuesta que `POST /topup`). */
892
+ readonly topup: {
893
+ readonly method: "POST";
894
+ readonly path: "/api/partners/:partnerId/topup";
895
+ };
896
+ /** Sólo MailMask: fija (o quita, con `null`) el límite diario de un tenant. Devuelve el `Tenant`. */
897
+ readonly setTenantDailyLimit: {
898
+ readonly method: "PUT";
899
+ readonly path: "/api/partners/:partnerId/tenants/:externalId/daily-limit";
900
+ };
679
901
  readonly listCredentials: {
680
902
  readonly method: "GET";
681
903
  readonly path: "/api/partners/:partnerId/credentials";
package/dist/partner.d.ts CHANGED
@@ -1,12 +1,12 @@
1
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";
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, BalanceResponse, BalanceMovementsQuery, BalanceMovementsResponse, TopupRequest, TopupResponse, PartnerBalanceDetail, AdjustBalanceRequest } from "./partner-contract.js";
3
3
  export * from "./partner-contract.js";
4
4
  /**
5
5
  * Discriminante estable de los errores de partners, para un `switch` sin cadenas de `instanceof`.
6
6
  * `network` y `timeout` llevan `status` 0 (no hubo respuesta); `unknown` es un status que el
7
7
  * contrato no define (p. ej. un 400 de un proxy).
8
8
  */
9
- export type PartnerErrorKind = "auth" | "forbidden" | "reputation_hold" | "not_found" | "conflict" | "validation" | "suppressed" | "paused" | "quota" | "rate_limit" | "server" | "network" | "timeout" | "unknown";
9
+ export type PartnerErrorKind = "auth" | "forbidden" | "reputation_hold" | "not_found" | "conflict" | "validation" | "suppressed" | "paused" | "quota" | "daily_limit" | "insufficient_balance" | "rate_limit" | "server" | "network" | "timeout" | "unknown";
10
10
  /** Metadatos de la respuesta que no vienen en el cuerpo. */
11
11
  export interface PartnerErrorMeta {
12
12
  requestId?: string;
@@ -100,6 +100,25 @@ export declare class RateLimitError extends QuotaError {
100
100
  name: string;
101
101
  kind: PartnerErrorKind;
102
102
  }
103
+ /**
104
+ * 429 `daily_limit`: el tenant llegó a su límite del día (crece solo con buena reputación). NO se
105
+ * reintenta: vuelve a intentar después de `resetsAt` (medianoche UTC) o manda un `batch`, que espera
106
+ * solo. Trae `limit` y `sentToday`.
107
+ */
108
+ export declare class DailyLimitError extends QuotaError {
109
+ name: string;
110
+ kind: PartnerErrorKind;
111
+ get sentToday(): number | undefined;
112
+ }
113
+ /**
114
+ * 402 `insufficient_balance`: el saldo prepagado no alcanza. No se reintenta: recarga con `partner.topup()`. Trae `balanceCents` y `requiredCents`.
115
+ */
116
+ export declare class InsufficientBalanceError extends PartnerApiError {
117
+ name: string;
118
+ kind: PartnerErrorKind;
119
+ get balanceCents(): number | undefined;
120
+ get requiredCents(): number | undefined;
121
+ }
103
122
  /** No hubo respuesta: DNS, conexión rechazada o cortada. `status` 0. */
104
123
  export declare class NetworkError extends PartnerApiError {
105
124
  name: string;
@@ -218,6 +237,15 @@ export declare class MailMaskPartner {
218
237
  }>;
219
238
  /** Estado de un lote (`batchId` del resultado de `batch`): envíos y, después, entregas, rebotes, quejas y bajas. */
220
239
  batchStatus(externalId: string, batchId: string): Promise<BatchStatus>;
240
+ /** Saldo prepagado: saldo, precios, última recarga y consumo del mes (centavos MXN). */
241
+ balance(): Promise<BalanceResponse>;
242
+ /** Movimientos del saldo, del más nuevo al más viejo; pagina con `nextCursor`. */
243
+ balanceMovements(query?: BalanceMovementsQuery): Promise<BalanceMovementsResponse>;
244
+ /**
245
+ * Liga de MercadoPago para recargar saldo (pago único, $500 a $100,000 MXN). El saldo sube cuando
246
+ * MercadoPago confirma el pago, no al crear la liga. Abre `initPoint` en el navegador de quien paga.
247
+ */
248
+ topup(input: TopupRequest): Promise<TopupResponse>;
221
249
  /** Uso del mes (`YYYY-MM`, default el mes en curso UTC): tenants activos y envíos. */
222
250
  usage(opts?: {
223
251
  month?: string;
@@ -354,6 +382,8 @@ export declare class MailMaskPartnerAdmin {
354
382
  setReputationPolicy(partnerId: string, policy: ReputationPolicy | null): Promise<PartnerSummary>;
355
383
  /** Sólo MailMask (admin): levanta cualquier pausa, también la de reputación. */
356
384
  resumeTenant(partnerId: string, externalId: string): Promise<Tenant>;
385
+ /** Sólo MailMask: tope diario fijo del tenant, o `null` para volver al calentamiento. */
386
+ setTenantDailyLimit(partnerId: string, externalId: string, limit: number | null): Promise<Tenant>;
357
387
  /**
358
388
  * Sólo MailMask (admin): crea o adopta la hosted zone de la zona de respaldo (`marketing.denik.me`)
359
389
  * y devuelve los NS que el partner tiene que delegar. Idempotente con la misma zona.
@@ -361,6 +391,12 @@ export declare class MailMaskPartnerAdmin {
361
391
  setFallbackZone(partnerId: string, zone: string): Promise<FallbackZone>;
362
392
  /** La zona de respaldo y si el DNS público ya ve la delegación. */
363
393
  getFallbackZone(partnerId: string): Promise<FallbackZone>;
394
+ /** Saldo del partner con sus últimos 20 movimientos (dueño o MailMask). */
395
+ getBalance(partnerId: string): Promise<PartnerBalanceDetail>;
396
+ /** Sólo MailMask (admin): abono (o cargo, negativo) manual. Con la misma `idempotencyKey` no se repite. */
397
+ adjustBalance(partnerId: string, input: AdjustBalanceRequest): Promise<PartnerBalanceDetail>;
398
+ /** Liga de MercadoPago para recargar, con la llave del dueño. */
399
+ topup(partnerId: string, input: TopupRequest): Promise<TopupResponse>;
364
400
  listCredentials(partnerId: string): Promise<ListCredentialsResponse>;
365
401
  /** El secreto `mms_…` viene UNA vez en la respuesta. */
366
402
  createCredential(partnerId: string, input?: CreateCredentialRequest): Promise<CreateCredentialResponse>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@easybits.cloud/mailmask",
3
- "version": "0.6.1",
3
+ "version": "0.8.0",
4
4
  "description": "Official SDK for the MailMask email alias/forwarding API",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",