@easybits.cloud/mailmask 0.7.0 → 0.9.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
@@ -20,11 +20,13 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ AUTO_TOPUP_SUGGESTIONS: () => AUTO_TOPUP_SUGGESTIONS,
23
24
  AuthError: () => AuthError,
24
25
  BALANCE_LOW_FRACTION: () => BALANCE_LOW_FRACTION,
25
26
  BALANCE_MOVEMENT_KINDS: () => BALANCE_MOVEMENT_KINDS,
26
27
  ConflictError: () => ConflictError,
27
28
  DOMAIN_CLAIM_TTL_DAYS: () => DOMAIN_CLAIM_TTL_DAYS,
29
+ DailyLimitError: () => DailyLimitError,
28
30
  EXTERNAL_ID_RE: () => EXTERNAL_ID_RE,
29
31
  FALLBACK_LABEL_RE: () => FALLBACK_LABEL_RE,
30
32
  ForbiddenError: () => ForbiddenError,
@@ -43,8 +45,10 @@ __export(index_exports, {
43
45
  NotFoundError: () => NotFoundError,
44
46
  PARTNER_ADMIN_ROUTES: () => PARTNER_ADMIN_ROUTES,
45
47
  PARTNER_API_PREFIX: () => PARTNER_API_PREFIX,
48
+ PARTNER_CURRENCIES: () => PARTNER_CURRENCIES,
46
49
  PARTNER_ERROR_STATUS: () => PARTNER_ERROR_STATUS,
47
50
  PARTNER_HEADERS: () => PARTNER_HEADERS,
51
+ PARTNER_PRICES: () => PARTNER_PRICES,
48
52
  PARTNER_PRICE_PER_EMAIL_CENTS: () => PARTNER_PRICE_PER_EMAIL_CENTS,
49
53
  PARTNER_ROUTES: () => PARTNER_ROUTES,
50
54
  PARTNER_SIGNATURE_TOLERANCE_S: () => PARTNER_SIGNATURE_TOLERANCE_S,
@@ -52,6 +56,7 @@ __export(index_exports, {
52
56
  PARTNER_TOPUP_MAX_CENTS: () => PARTNER_TOPUP_MAX_CENTS,
53
57
  PARTNER_TOPUP_MIN_CENTS: () => PARTNER_TOPUP_MIN_CENTS,
54
58
  PARTNER_WEBHOOK_EVENTS: () => PARTNER_WEBHOOK_EVENTS,
59
+ PARTNER_WELCOME_CENTS: () => PARTNER_WELCOME_CENTS,
55
60
  PartnerApiError: () => PartnerApiError,
56
61
  PausedError: () => PausedError,
57
62
  QuotaError: () => QuotaError,
@@ -62,12 +67,17 @@ __export(index_exports, {
62
67
  STRICT_FIRST_DAYS: () => STRICT_FIRST_DAYS,
63
68
  TAG_RE: () => TAG_RE,
64
69
  TEMPLATE_VAR_NAME_RE: () => TEMPLATE_VAR_NAME_RE,
70
+ TENANT_DAILY_LIMIT_MAX: () => TENANT_DAILY_LIMIT_MAX,
71
+ TENANT_DAILY_LIMIT_START: () => TENANT_DAILY_LIMIT_START,
72
+ TENANT_WARMUP_STEP_DAYS: () => TENANT_WARMUP_STEP_DAYS,
65
73
  TimeoutError: () => TimeoutError,
66
74
  UNSUBSCRIBE_URL_VAR: () => UNSUBSCRIBE_URL_VAR,
67
75
  ValidationError: () => ValidationError,
68
76
  WEBHOOK_HEADERS: () => WEBHOOK_HEADERS,
69
77
  WEBHOOK_TOLERANCE_MS: () => WEBHOOK_TOLERANCE_MS,
70
78
  createWebhookHandler: () => createWebhookHandler,
79
+ estimateSendDays: () => estimateSendDays,
80
+ formatMoney: () => formatMoney,
71
81
  isValidExternalId: () => isValidExternalId,
72
82
  isValidFallbackLabel: () => isValidFallbackLabel,
73
83
  partnerSignatureBase: () => partnerSignatureBase,
@@ -80,6 +90,7 @@ __export(index_exports, {
80
90
  templateVarsProblem: () => templateVarsProblem,
81
91
  verifyWebhook: () => verifyWebhook,
82
92
  verifyWebhookSignature: () => verifyWebhookSignature,
93
+ warmupLimit: () => warmupLimit,
83
94
  webhookSignatureBase: () => webhookSignatureBase
84
95
  });
85
96
  module.exports = __toCommonJS(index_exports);
@@ -724,11 +735,16 @@ var PARTNER_ERROR_STATUS = {
724
735
  /** El tenant está en pausa (por MailMask o por el partner). No se manda nada hasta `resume`. */
725
736
  tenant_paused: 423,
726
737
  /**
727
- * Saldo prepagado insuficiente (con `balanceCents` y `requiredCents`). Sólo cuando MailMask
728
- * activó el cobro por adelantado del partner. No se reintenta: recarga con `POST /topup`.
738
+ * Saldo prepagado insuficiente (con `balanceCents` y `requiredCents`). No se reintenta: recarga
739
+ * con `POST /topup`.
729
740
  */
730
741
  insufficient_balance: 402,
731
- /** Cuota mensual del tenant agotada. */
742
+ /**
743
+ * El tenant llegó a su límite del día (con `limit`, `sentToday` y `resetsAt`, la medianoche UTC).
744
+ * Sólo en envíos sueltos: un lote no falla, espera al día siguiente (`batch.throttled`).
745
+ */
746
+ daily_limit: 429,
747
+ /** Ya no sale desde la 0.8 (no hay tope mensual); se deja para clientes viejos. */
732
748
  quota_exceeded: 429,
733
749
  /** Demasiadas peticiones; reintenta tras `Retry-After`. */
734
750
  rate_limited: 429,
@@ -742,6 +758,30 @@ function isValidExternalId(id) {
742
758
  var MAX_BATCH_RECIPIENTS = 1e4;
743
759
  var MAX_TAGS = 10;
744
760
  var TAG_RE = /^[A-Za-z0-9_-]{1,64}$/;
761
+ var TENANT_DAILY_LIMIT_START = 2e3;
762
+ var TENANT_DAILY_LIMIT_MAX = 2e4;
763
+ var TENANT_WARMUP_STEP_DAYS = 7;
764
+ function warmupLimit(step) {
765
+ return Math.min(TENANT_DAILY_LIMIT_MAX, TENANT_DAILY_LIMIT_START * 2 ** Math.max(0, step));
766
+ }
767
+ function estimateSendDays(s, recipients) {
768
+ let left = recipients - Math.max(0, s.dailyLimit - s.sentToday);
769
+ if (left <= 0) return 1;
770
+ let days = 1;
771
+ let limit = s.dailyLimit;
772
+ let nextAt = s.nextStepAt ? Date.parse(s.nextStepAt) : null;
773
+ let day = Date.parse(s.resetsAt);
774
+ while (left > 0 && days < 3650) {
775
+ while (nextAt !== null && nextAt <= day && limit < TENANT_DAILY_LIMIT_MAX && !s.fixed) {
776
+ limit = Math.min(TENANT_DAILY_LIMIT_MAX, limit * 2);
777
+ nextAt += TENANT_WARMUP_STEP_DAYS * 864e5;
778
+ }
779
+ left -= limit;
780
+ days++;
781
+ day += 864e5;
782
+ }
783
+ return days;
784
+ }
745
785
  var DOMAIN_CLAIM_TTL_DAYS = 7;
746
786
  var FALLBACK_LABEL_RE = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
747
787
  var RESERVED_FALLBACK_LABELS = [
@@ -807,12 +847,25 @@ function personalizePartnerMessage(input) {
807
847
  ...text !== void 0 ? { text: renderTemplate(text, vars) } : {}
808
848
  };
809
849
  }
810
- var PARTNER_PRICE_PER_EMAIL_CENTS = 6;
811
- var PARTNER_TENANT_MONTH_CENTS = 4900;
812
- var PARTNER_TOPUP_MIN_CENTS = 5e4;
813
- var PARTNER_TOPUP_MAX_CENTS = 1e7;
850
+ var PARTNER_CURRENCIES = ["MXN", "USD"];
851
+ var PARTNER_PRICES = {
852
+ MXN: { perEmailCents: 6, tenantMonthCents: 4900, welcomeCents: 5e4, topupMinCents: 5e4, topupMaxCents: 1e7 },
853
+ USD: { perEmailCents: 0.35, tenantMonthCents: 290, welcomeCents: 2900, topupMinCents: 2900, topupMaxCents: 55e4 }
854
+ };
855
+ var PARTNER_PRICE_PER_EMAIL_CENTS = PARTNER_PRICES.MXN.perEmailCents;
856
+ var PARTNER_TENANT_MONTH_CENTS = PARTNER_PRICES.MXN.tenantMonthCents;
857
+ var PARTNER_WELCOME_CENTS = PARTNER_PRICES.MXN.welcomeCents;
858
+ var PARTNER_TOPUP_MIN_CENTS = PARTNER_PRICES.MXN.topupMinCents;
859
+ var PARTNER_TOPUP_MAX_CENTS = PARTNER_PRICES.MXN.topupMaxCents;
860
+ function formatMoney(cents, currency) {
861
+ const v = Math.abs(cents) / 100;
862
+ const decimals = v !== 0 && v < 0.01 ? 4 : 2;
863
+ const n = v.toLocaleString("en-US", { minimumFractionDigits: decimals, maximumFractionDigits: decimals });
864
+ return `${cents < 0 ? "\u2212" : ""}$${n} ${currency}`;
865
+ }
814
866
  var BALANCE_LOW_FRACTION = 0.2;
815
867
  var BALANCE_MOVEMENT_KINDS = ["topup", "send", "tenant_fee", "refund", "adjust"];
868
+ var AUTO_TOPUP_SUGGESTIONS = { MXN: [500, 1e3, 2500], USD: [29, 59, 149] };
816
869
  var PARTNER_WEBHOOK_EVENTS = [
817
870
  "email.sent",
818
871
  "email.delivered",
@@ -823,7 +876,8 @@ var PARTNER_WEBHOOK_EVENTS = [
823
876
  "tenant.paused",
824
877
  "tenant.resumed",
825
878
  "domain.verified",
826
- "balance.low"
879
+ "balance.low",
880
+ "batch.throttled"
827
881
  ];
828
882
  var WEBHOOK_HEADERS = {
829
883
  event: "X-MailMask-Event",
@@ -858,6 +912,8 @@ var PARTNER_ROUTES = {
858
912
  balance: { method: "GET", path: "/balance" },
859
913
  balanceMovements: { method: "GET", path: "/balance/movements" },
860
914
  topup: { method: "POST", path: "/topup" },
915
+ getAutoTopup: { method: "GET", path: "/auto-topup" },
916
+ setAutoTopup: { method: "PUT", path: "/auto-topup" },
861
917
  getWebhook: { method: "GET", path: "/webhook" },
862
918
  setWebhook: { method: "PUT", path: "/webhook" },
863
919
  testWebhook: { method: "POST", path: "/webhook/test" },
@@ -877,10 +933,14 @@ var PARTNER_ADMIN_ROUTES = {
877
933
  getBalance: { method: "GET", path: "/api/partners/:partnerId/balance" },
878
934
  /** Sólo MailMask (admin): abono o cargo manual (`adjust`). */
879
935
  adjustBalance: { method: "POST", path: "/api/partners/:partnerId/balance/adjust" },
880
- /** Sólo MailMask (admin): prende o apaga el cobro por adelantado. */
881
- setPrepaidEnforced: { method: "PUT", path: "/api/partners/:partnerId/prepaid" },
882
936
  /** Recarga con MercadoPago desde la sesión del dueño (misma respuesta que `POST /topup`). */
883
937
  topup: { method: "POST", path: "/api/partners/:partnerId/topup" },
938
+ getAutoTopup: { method: "GET", path: "/api/partners/:partnerId/auto-topup" },
939
+ setAutoTopup: { method: "PUT", path: "/api/partners/:partnerId/auto-topup" },
940
+ /** Sólo MailMask: moneda del saldo, mientras no haya más movimientos que la bienvenida. */
941
+ setCurrency: { method: "PUT", path: "/api/partners/:partnerId/currency" },
942
+ /** Sólo MailMask: fija (o quita, con `null`) el límite diario de un tenant. Devuelve el `Tenant`. */
943
+ setTenantDailyLimit: { method: "PUT", path: "/api/partners/:partnerId/tenants/:externalId/daily-limit" },
884
944
  listCredentials: { method: "GET", path: "/api/partners/:partnerId/credentials" },
885
945
  createCredential: { method: "POST", path: "/api/partners/:partnerId/credentials" },
886
946
  rotateCredential: { method: "POST", path: "/api/partners/:partnerId/credentials/:keyId/rotate" },
@@ -894,8 +954,8 @@ var PartnerApiError = class extends MailMaskError {
894
954
  code: body.code,
895
955
  requestId: meta.requestId,
896
956
  cause: meta.cause,
897
- // `quota_exceeded` es un 429 que no se arregla reintentando: hasta el mes siguiente.
898
- retryable: status === 0 || status >= 500 || status === 429 && body.code !== "quota_exceeded"
957
+ // `daily_limit` (y el viejo `quota_exceeded`) son 429 que no se arreglan reintentando: hasta mañana.
958
+ retryable: status === 0 || status >= 500 || status === 429 && body.code !== "quota_exceeded" && body.code !== "daily_limit"
899
959
  });
900
960
  this.kind = "unknown";
901
961
  this.name = "PartnerApiError";
@@ -998,6 +1058,16 @@ var RateLimitError = class extends QuotaError {
998
1058
  this.kind = "rate_limit";
999
1059
  }
1000
1060
  };
1061
+ var DailyLimitError = class extends QuotaError {
1062
+ constructor() {
1063
+ super(...arguments);
1064
+ this.name = "DailyLimitError";
1065
+ this.kind = "daily_limit";
1066
+ }
1067
+ get sentToday() {
1068
+ return this.body.sentToday;
1069
+ }
1070
+ };
1001
1071
  var InsufficientBalanceError = class extends PartnerApiError {
1002
1072
  constructor() {
1003
1073
  super(...arguments);
@@ -1010,6 +1080,10 @@ var InsufficientBalanceError = class extends PartnerApiError {
1010
1080
  get requiredCents() {
1011
1081
  return this.body.requiredCents;
1012
1082
  }
1083
+ /** Moneda de los dos montos de arriba. */
1084
+ get currency() {
1085
+ return this.body.currency;
1086
+ }
1013
1087
  };
1014
1088
  var NetworkError = class extends PartnerApiError {
1015
1089
  constructor(message, cause) {
@@ -1029,6 +1103,7 @@ function errorFor(status, body, fallback, meta = {}) {
1029
1103
  const msg = body.error || fallback;
1030
1104
  if (body.code === "reputation_hold") return new ReputationHoldError(status, msg, body, meta);
1031
1105
  if (body.code === "recipient_suppressed") return new RecipientSuppressedError(status, msg, body, meta);
1106
+ if (status === 429 && body.code === "daily_limit") return new DailyLimitError(status, msg, body, meta);
1032
1107
  if (status === 429 && body.code !== "quota_exceeded") return new RateLimitError(status, msg, body, meta);
1033
1108
  switch (status) {
1034
1109
  case 401:
@@ -1249,12 +1324,24 @@ var MailMaskPartner = class {
1249
1324
  return (await this.request("GET", `${PARTNER_ROUTES.balanceMovements.path}${qs ? `?${qs}` : ""}`)).data;
1250
1325
  }
1251
1326
  /**
1252
- * Liga de MercadoPago para recargar saldo (pago único, $500 a $100,000 MXN). El saldo sube cuando
1253
- * MercadoPago confirma el pago, no al crear la liga. Abre `initPoint` en el navegador de quien paga.
1327
+ * Liga de MercadoPago para recargar saldo (pago único; `amount` en unidades enteras de tu moneda:
1328
+ * MXN 500 a 100,000, USD 29 a 5,500). El saldo sube cuando MercadoPago confirma el pago, no al
1329
+ * crear la liga. Abre `initPoint` en el navegador de quien paga.
1254
1330
  */
1255
1331
  async topup(input) {
1256
1332
  return (await this.request("POST", PARTNER_ROUTES.topup.path, input)).data;
1257
1333
  }
1334
+ /** La recarga automática mensual: `off`, `pending` (falta autorizarla) o `active`. */
1335
+ async autoTopup() {
1336
+ return (await this.request("GET", PARTNER_ROUTES.getAutoTopup.path)).data;
1337
+ }
1338
+ /**
1339
+ * Prende o cambia (`amount` entero de tu moneda) la recarga automática mensual, o la apaga
1340
+ * (`amount: null`). Al prenderla o cambiarla devuelve `initPoint`: ábrelo para autorizar el cobro.
1341
+ */
1342
+ async setAutoTopup(input) {
1343
+ return (await this.request("PUT", PARTNER_ROUTES.setAutoTopup.path, input)).data;
1344
+ }
1258
1345
  /** Uso del mes (`YYYY-MM`, default el mes en curso UTC): tenants activos y envíos. */
1259
1346
  async usage(opts = {}) {
1260
1347
  const q = opts.month ? `?month=${encodeURIComponent(opts.month)}` : "";
@@ -1455,10 +1542,18 @@ var MailMaskPartnerAdmin = class {
1455
1542
  setReputationPolicy(partnerId, policy) {
1456
1543
  return this.req("setReputationPolicy", { partnerId }, { policy });
1457
1544
  }
1545
+ /** Sólo MailMask (admin): moneda del saldo (MXN o USD); sólo antes de cualquier movimiento aparte de la bienvenida. */
1546
+ setCurrency(partnerId, currency) {
1547
+ return this.req("setCurrency", { partnerId }, { currency });
1548
+ }
1458
1549
  /** Sólo MailMask (admin): levanta cualquier pausa, también la de reputación. */
1459
1550
  resumeTenant(partnerId, externalId) {
1460
1551
  return this.req("resumeTenant", { partnerId, externalId });
1461
1552
  }
1553
+ /** Sólo MailMask: tope diario fijo del tenant, o `null` para volver al calentamiento. */
1554
+ setTenantDailyLimit(partnerId, externalId, limit) {
1555
+ return this.req("setTenantDailyLimit", { partnerId, externalId }, { limit });
1556
+ }
1462
1557
  /**
1463
1558
  * Sólo MailMask (admin): crea o adopta la hosted zone de la zona de respaldo (`marketing.denik.me`)
1464
1559
  * y devuelve los NS que el partner tiene que delegar. Idempotente con la misma zona.
@@ -1478,14 +1573,18 @@ var MailMaskPartnerAdmin = class {
1478
1573
  adjustBalance(partnerId, input) {
1479
1574
  return this.req("adjustBalance", { partnerId }, input);
1480
1575
  }
1481
- /** Sólo MailMask (admin): prende o apaga el cobro por adelantado del partner. */
1482
- setPrepaidEnforced(partnerId, enforced) {
1483
- return this.req("setPrepaidEnforced", { partnerId }, { enforced });
1484
- }
1485
1576
  /** Liga de MercadoPago para recargar, con la llave del dueño. */
1486
1577
  topup(partnerId, input) {
1487
1578
  return this.req("topup", { partnerId }, input);
1488
1579
  }
1580
+ /** La recarga automática mensual del partner (dueño o MailMask). */
1581
+ getAutoTopup(partnerId) {
1582
+ return this.req("getAutoTopup", { partnerId });
1583
+ }
1584
+ /** Prende, cambia (`amount`) o apaga (`null`) la recarga automática; trae `initPoint` al prenderla. */
1585
+ setAutoTopup(partnerId, input) {
1586
+ return this.req("setAutoTopup", { partnerId }, input);
1587
+ }
1489
1588
  listCredentials(partnerId) {
1490
1589
  return this.req("listCredentials", { partnerId });
1491
1590
  }
@@ -1504,11 +1603,13 @@ var MailMaskPartnerAdmin = class {
1504
1603
  };
1505
1604
  // Annotate the CommonJS export names for ESM import in node:
1506
1605
  0 && (module.exports = {
1606
+ AUTO_TOPUP_SUGGESTIONS,
1507
1607
  AuthError,
1508
1608
  BALANCE_LOW_FRACTION,
1509
1609
  BALANCE_MOVEMENT_KINDS,
1510
1610
  ConflictError,
1511
1611
  DOMAIN_CLAIM_TTL_DAYS,
1612
+ DailyLimitError,
1512
1613
  EXTERNAL_ID_RE,
1513
1614
  FALLBACK_LABEL_RE,
1514
1615
  ForbiddenError,
@@ -1527,8 +1628,10 @@ var MailMaskPartnerAdmin = class {
1527
1628
  NotFoundError,
1528
1629
  PARTNER_ADMIN_ROUTES,
1529
1630
  PARTNER_API_PREFIX,
1631
+ PARTNER_CURRENCIES,
1530
1632
  PARTNER_ERROR_STATUS,
1531
1633
  PARTNER_HEADERS,
1634
+ PARTNER_PRICES,
1532
1635
  PARTNER_PRICE_PER_EMAIL_CENTS,
1533
1636
  PARTNER_ROUTES,
1534
1637
  PARTNER_SIGNATURE_TOLERANCE_S,
@@ -1536,6 +1639,7 @@ var MailMaskPartnerAdmin = class {
1536
1639
  PARTNER_TOPUP_MAX_CENTS,
1537
1640
  PARTNER_TOPUP_MIN_CENTS,
1538
1641
  PARTNER_WEBHOOK_EVENTS,
1642
+ PARTNER_WELCOME_CENTS,
1539
1643
  PartnerApiError,
1540
1644
  PausedError,
1541
1645
  QuotaError,
@@ -1546,12 +1650,17 @@ var MailMaskPartnerAdmin = class {
1546
1650
  STRICT_FIRST_DAYS,
1547
1651
  TAG_RE,
1548
1652
  TEMPLATE_VAR_NAME_RE,
1653
+ TENANT_DAILY_LIMIT_MAX,
1654
+ TENANT_DAILY_LIMIT_START,
1655
+ TENANT_WARMUP_STEP_DAYS,
1549
1656
  TimeoutError,
1550
1657
  UNSUBSCRIBE_URL_VAR,
1551
1658
  ValidationError,
1552
1659
  WEBHOOK_HEADERS,
1553
1660
  WEBHOOK_TOLERANCE_MS,
1554
1661
  createWebhookHandler,
1662
+ estimateSendDays,
1663
+ formatMoney,
1555
1664
  isValidExternalId,
1556
1665
  isValidFallbackLabel,
1557
1666
  partnerSignatureBase,
@@ -1564,5 +1673,6 @@ var MailMaskPartnerAdmin = class {
1564
1673
  templateVarsProblem,
1565
1674
  verifyWebhook,
1566
1675
  verifyWebhookSignature,
1676
+ warmupLimit,
1567
1677
  webhookSignatureBase
1568
1678
  });
package/dist/index.js CHANGED
@@ -638,11 +638,16 @@ var PARTNER_ERROR_STATUS = {
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
640
  /**
641
- * Saldo prepagado insuficiente (con `balanceCents` y `requiredCents`). Sólo cuando MailMask
642
- * activó el cobro por adelantado del partner. No se reintenta: recarga con `POST /topup`.
641
+ * Saldo prepagado insuficiente (con `balanceCents` y `requiredCents`). No se reintenta: recarga
642
+ * con `POST /topup`.
643
643
  */
644
644
  insufficient_balance: 402,
645
- /** Cuota mensual del tenant agotada. */
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. */
646
651
  quota_exceeded: 429,
647
652
  /** Demasiadas peticiones; reintenta tras `Retry-After`. */
648
653
  rate_limited: 429,
@@ -656,6 +661,30 @@ function isValidExternalId(id) {
656
661
  var MAX_BATCH_RECIPIENTS = 1e4;
657
662
  var MAX_TAGS = 10;
658
663
  var TAG_RE = /^[A-Za-z0-9_-]{1,64}$/;
664
+ var TENANT_DAILY_LIMIT_START = 2e3;
665
+ var TENANT_DAILY_LIMIT_MAX = 2e4;
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
+ }
659
688
  var DOMAIN_CLAIM_TTL_DAYS = 7;
660
689
  var FALLBACK_LABEL_RE = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
661
690
  var RESERVED_FALLBACK_LABELS = [
@@ -721,12 +750,25 @@ function personalizePartnerMessage(input) {
721
750
  ...text !== void 0 ? { text: renderTemplate(text, vars) } : {}
722
751
  };
723
752
  }
724
- var PARTNER_PRICE_PER_EMAIL_CENTS = 6;
725
- var PARTNER_TENANT_MONTH_CENTS = 4900;
726
- var PARTNER_TOPUP_MIN_CENTS = 5e4;
727
- var PARTNER_TOPUP_MAX_CENTS = 1e7;
753
+ var PARTNER_CURRENCIES = ["MXN", "USD"];
754
+ var PARTNER_PRICES = {
755
+ MXN: { perEmailCents: 6, tenantMonthCents: 4900, welcomeCents: 5e4, topupMinCents: 5e4, topupMaxCents: 1e7 },
756
+ USD: { perEmailCents: 0.35, tenantMonthCents: 290, welcomeCents: 2900, topupMinCents: 2900, topupMaxCents: 55e4 }
757
+ };
758
+ var PARTNER_PRICE_PER_EMAIL_CENTS = PARTNER_PRICES.MXN.perEmailCents;
759
+ var PARTNER_TENANT_MONTH_CENTS = PARTNER_PRICES.MXN.tenantMonthCents;
760
+ var PARTNER_WELCOME_CENTS = PARTNER_PRICES.MXN.welcomeCents;
761
+ var PARTNER_TOPUP_MIN_CENTS = PARTNER_PRICES.MXN.topupMinCents;
762
+ var PARTNER_TOPUP_MAX_CENTS = PARTNER_PRICES.MXN.topupMaxCents;
763
+ function formatMoney(cents, currency) {
764
+ const v = Math.abs(cents) / 100;
765
+ const decimals = v !== 0 && v < 0.01 ? 4 : 2;
766
+ const n = v.toLocaleString("en-US", { minimumFractionDigits: decimals, maximumFractionDigits: decimals });
767
+ return `${cents < 0 ? "\u2212" : ""}$${n} ${currency}`;
768
+ }
728
769
  var BALANCE_LOW_FRACTION = 0.2;
729
770
  var BALANCE_MOVEMENT_KINDS = ["topup", "send", "tenant_fee", "refund", "adjust"];
771
+ var AUTO_TOPUP_SUGGESTIONS = { MXN: [500, 1e3, 2500], USD: [29, 59, 149] };
730
772
  var PARTNER_WEBHOOK_EVENTS = [
731
773
  "email.sent",
732
774
  "email.delivered",
@@ -737,7 +779,8 @@ var PARTNER_WEBHOOK_EVENTS = [
737
779
  "tenant.paused",
738
780
  "tenant.resumed",
739
781
  "domain.verified",
740
- "balance.low"
782
+ "balance.low",
783
+ "batch.throttled"
741
784
  ];
742
785
  var WEBHOOK_HEADERS = {
743
786
  event: "X-MailMask-Event",
@@ -772,6 +815,8 @@ var PARTNER_ROUTES = {
772
815
  balance: { method: "GET", path: "/balance" },
773
816
  balanceMovements: { method: "GET", path: "/balance/movements" },
774
817
  topup: { method: "POST", path: "/topup" },
818
+ getAutoTopup: { method: "GET", path: "/auto-topup" },
819
+ setAutoTopup: { method: "PUT", path: "/auto-topup" },
775
820
  getWebhook: { method: "GET", path: "/webhook" },
776
821
  setWebhook: { method: "PUT", path: "/webhook" },
777
822
  testWebhook: { method: "POST", path: "/webhook/test" },
@@ -791,10 +836,14 @@ var PARTNER_ADMIN_ROUTES = {
791
836
  getBalance: { method: "GET", path: "/api/partners/:partnerId/balance" },
792
837
  /** Sólo MailMask (admin): abono o cargo manual (`adjust`). */
793
838
  adjustBalance: { method: "POST", path: "/api/partners/:partnerId/balance/adjust" },
794
- /** Sólo MailMask (admin): prende o apaga el cobro por adelantado. */
795
- setPrepaidEnforced: { method: "PUT", path: "/api/partners/:partnerId/prepaid" },
796
839
  /** Recarga con MercadoPago desde la sesión del dueño (misma respuesta que `POST /topup`). */
797
840
  topup: { method: "POST", path: "/api/partners/:partnerId/topup" },
841
+ getAutoTopup: { method: "GET", path: "/api/partners/:partnerId/auto-topup" },
842
+ setAutoTopup: { method: "PUT", path: "/api/partners/:partnerId/auto-topup" },
843
+ /** Sólo MailMask: moneda del saldo, mientras no haya más movimientos que la bienvenida. */
844
+ setCurrency: { method: "PUT", path: "/api/partners/:partnerId/currency" },
845
+ /** Sólo MailMask: fija (o quita, con `null`) el límite diario de un tenant. Devuelve el `Tenant`. */
846
+ setTenantDailyLimit: { method: "PUT", path: "/api/partners/:partnerId/tenants/:externalId/daily-limit" },
798
847
  listCredentials: { method: "GET", path: "/api/partners/:partnerId/credentials" },
799
848
  createCredential: { method: "POST", path: "/api/partners/:partnerId/credentials" },
800
849
  rotateCredential: { method: "POST", path: "/api/partners/:partnerId/credentials/:keyId/rotate" },
@@ -808,8 +857,8 @@ var PartnerApiError = class extends MailMaskError {
808
857
  code: body.code,
809
858
  requestId: meta.requestId,
810
859
  cause: meta.cause,
811
- // `quota_exceeded` es un 429 que no se arregla reintentando: hasta el mes siguiente.
812
- retryable: status === 0 || status >= 500 || status === 429 && body.code !== "quota_exceeded"
860
+ // `daily_limit` (y el viejo `quota_exceeded`) son 429 que no se arreglan reintentando: hasta mañana.
861
+ retryable: status === 0 || status >= 500 || status === 429 && body.code !== "quota_exceeded" && body.code !== "daily_limit"
813
862
  });
814
863
  this.kind = "unknown";
815
864
  this.name = "PartnerApiError";
@@ -912,6 +961,16 @@ var RateLimitError = class extends QuotaError {
912
961
  this.kind = "rate_limit";
913
962
  }
914
963
  };
964
+ var DailyLimitError = class extends QuotaError {
965
+ constructor() {
966
+ super(...arguments);
967
+ this.name = "DailyLimitError";
968
+ this.kind = "daily_limit";
969
+ }
970
+ get sentToday() {
971
+ return this.body.sentToday;
972
+ }
973
+ };
915
974
  var InsufficientBalanceError = class extends PartnerApiError {
916
975
  constructor() {
917
976
  super(...arguments);
@@ -924,6 +983,10 @@ var InsufficientBalanceError = class extends PartnerApiError {
924
983
  get requiredCents() {
925
984
  return this.body.requiredCents;
926
985
  }
986
+ /** Moneda de los dos montos de arriba. */
987
+ get currency() {
988
+ return this.body.currency;
989
+ }
927
990
  };
928
991
  var NetworkError = class extends PartnerApiError {
929
992
  constructor(message, cause) {
@@ -943,6 +1006,7 @@ function errorFor(status, body, fallback, meta = {}) {
943
1006
  const msg = body.error || fallback;
944
1007
  if (body.code === "reputation_hold") return new ReputationHoldError(status, msg, body, meta);
945
1008
  if (body.code === "recipient_suppressed") return new RecipientSuppressedError(status, msg, body, meta);
1009
+ if (status === 429 && body.code === "daily_limit") return new DailyLimitError(status, msg, body, meta);
946
1010
  if (status === 429 && body.code !== "quota_exceeded") return new RateLimitError(status, msg, body, meta);
947
1011
  switch (status) {
948
1012
  case 401:
@@ -1163,12 +1227,24 @@ var MailMaskPartner = class {
1163
1227
  return (await this.request("GET", `${PARTNER_ROUTES.balanceMovements.path}${qs ? `?${qs}` : ""}`)).data;
1164
1228
  }
1165
1229
  /**
1166
- * Liga de MercadoPago para recargar saldo (pago único, $500 a $100,000 MXN). El saldo sube cuando
1167
- * MercadoPago confirma el pago, no al crear la liga. Abre `initPoint` en el navegador de quien paga.
1230
+ * Liga de MercadoPago para recargar saldo (pago único; `amount` en unidades enteras de tu moneda:
1231
+ * MXN 500 a 100,000, USD 29 a 5,500). El saldo sube cuando MercadoPago confirma el pago, no al
1232
+ * crear la liga. Abre `initPoint` en el navegador de quien paga.
1168
1233
  */
1169
1234
  async topup(input) {
1170
1235
  return (await this.request("POST", PARTNER_ROUTES.topup.path, input)).data;
1171
1236
  }
1237
+ /** La recarga automática mensual: `off`, `pending` (falta autorizarla) o `active`. */
1238
+ async autoTopup() {
1239
+ return (await this.request("GET", PARTNER_ROUTES.getAutoTopup.path)).data;
1240
+ }
1241
+ /**
1242
+ * Prende o cambia (`amount` entero de tu moneda) la recarga automática mensual, o la apaga
1243
+ * (`amount: null`). Al prenderla o cambiarla devuelve `initPoint`: ábrelo para autorizar el cobro.
1244
+ */
1245
+ async setAutoTopup(input) {
1246
+ return (await this.request("PUT", PARTNER_ROUTES.setAutoTopup.path, input)).data;
1247
+ }
1172
1248
  /** Uso del mes (`YYYY-MM`, default el mes en curso UTC): tenants activos y envíos. */
1173
1249
  async usage(opts = {}) {
1174
1250
  const q = opts.month ? `?month=${encodeURIComponent(opts.month)}` : "";
@@ -1369,10 +1445,18 @@ var MailMaskPartnerAdmin = class {
1369
1445
  setReputationPolicy(partnerId, policy) {
1370
1446
  return this.req("setReputationPolicy", { partnerId }, { policy });
1371
1447
  }
1448
+ /** Sólo MailMask (admin): moneda del saldo (MXN o USD); sólo antes de cualquier movimiento aparte de la bienvenida. */
1449
+ setCurrency(partnerId, currency) {
1450
+ return this.req("setCurrency", { partnerId }, { currency });
1451
+ }
1372
1452
  /** Sólo MailMask (admin): levanta cualquier pausa, también la de reputación. */
1373
1453
  resumeTenant(partnerId, externalId) {
1374
1454
  return this.req("resumeTenant", { partnerId, externalId });
1375
1455
  }
1456
+ /** Sólo MailMask: tope diario fijo del tenant, o `null` para volver al calentamiento. */
1457
+ setTenantDailyLimit(partnerId, externalId, limit) {
1458
+ return this.req("setTenantDailyLimit", { partnerId, externalId }, { limit });
1459
+ }
1376
1460
  /**
1377
1461
  * Sólo MailMask (admin): crea o adopta la hosted zone de la zona de respaldo (`marketing.denik.me`)
1378
1462
  * y devuelve los NS que el partner tiene que delegar. Idempotente con la misma zona.
@@ -1392,14 +1476,18 @@ var MailMaskPartnerAdmin = class {
1392
1476
  adjustBalance(partnerId, input) {
1393
1477
  return this.req("adjustBalance", { partnerId }, input);
1394
1478
  }
1395
- /** Sólo MailMask (admin): prende o apaga el cobro por adelantado del partner. */
1396
- setPrepaidEnforced(partnerId, enforced) {
1397
- return this.req("setPrepaidEnforced", { partnerId }, { enforced });
1398
- }
1399
1479
  /** Liga de MercadoPago para recargar, con la llave del dueño. */
1400
1480
  topup(partnerId, input) {
1401
1481
  return this.req("topup", { partnerId }, input);
1402
1482
  }
1483
+ /** La recarga automática mensual del partner (dueño o MailMask). */
1484
+ getAutoTopup(partnerId) {
1485
+ return this.req("getAutoTopup", { partnerId });
1486
+ }
1487
+ /** Prende, cambia (`amount`) o apaga (`null`) la recarga automática; trae `initPoint` al prenderla. */
1488
+ setAutoTopup(partnerId, input) {
1489
+ return this.req("setAutoTopup", { partnerId }, input);
1490
+ }
1403
1491
  listCredentials(partnerId) {
1404
1492
  return this.req("listCredentials", { partnerId });
1405
1493
  }
@@ -1417,11 +1505,13 @@ var MailMaskPartnerAdmin = class {
1417
1505
  }
1418
1506
  };
1419
1507
  export {
1508
+ AUTO_TOPUP_SUGGESTIONS,
1420
1509
  AuthError,
1421
1510
  BALANCE_LOW_FRACTION,
1422
1511
  BALANCE_MOVEMENT_KINDS,
1423
1512
  ConflictError,
1424
1513
  DOMAIN_CLAIM_TTL_DAYS,
1514
+ DailyLimitError,
1425
1515
  EXTERNAL_ID_RE,
1426
1516
  FALLBACK_LABEL_RE,
1427
1517
  ForbiddenError,
@@ -1440,8 +1530,10 @@ export {
1440
1530
  NotFoundError,
1441
1531
  PARTNER_ADMIN_ROUTES,
1442
1532
  PARTNER_API_PREFIX,
1533
+ PARTNER_CURRENCIES,
1443
1534
  PARTNER_ERROR_STATUS,
1444
1535
  PARTNER_HEADERS,
1536
+ PARTNER_PRICES,
1445
1537
  PARTNER_PRICE_PER_EMAIL_CENTS,
1446
1538
  PARTNER_ROUTES,
1447
1539
  PARTNER_SIGNATURE_TOLERANCE_S,
@@ -1449,6 +1541,7 @@ export {
1449
1541
  PARTNER_TOPUP_MAX_CENTS,
1450
1542
  PARTNER_TOPUP_MIN_CENTS,
1451
1543
  PARTNER_WEBHOOK_EVENTS,
1544
+ PARTNER_WELCOME_CENTS,
1452
1545
  PartnerApiError,
1453
1546
  PausedError,
1454
1547
  QuotaError,
@@ -1459,12 +1552,17 @@ export {
1459
1552
  STRICT_FIRST_DAYS,
1460
1553
  TAG_RE,
1461
1554
  TEMPLATE_VAR_NAME_RE,
1555
+ TENANT_DAILY_LIMIT_MAX,
1556
+ TENANT_DAILY_LIMIT_START,
1557
+ TENANT_WARMUP_STEP_DAYS,
1462
1558
  TimeoutError,
1463
1559
  UNSUBSCRIBE_URL_VAR,
1464
1560
  ValidationError,
1465
1561
  WEBHOOK_HEADERS,
1466
1562
  WEBHOOK_TOLERANCE_MS,
1467
1563
  createWebhookHandler,
1564
+ estimateSendDays,
1565
+ formatMoney,
1468
1566
  isValidExternalId,
1469
1567
  isValidFallbackLabel,
1470
1568
  partnerSignatureBase,
@@ -1477,5 +1575,6 @@ export {
1477
1575
  templateVarsProblem,
1478
1576
  verifyWebhook,
1479
1577
  verifyWebhookSignature,
1578
+ warmupLimit,
1480
1579
  webhookSignatureBase
1481
1580
  };
@@ -60,11 +60,16 @@ export declare const PARTNER_ERROR_STATUS: {
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
62
  /**
63
- * Saldo prepagado insuficiente (con `balanceCents` y `requiredCents`). Sólo cuando MailMask
64
- * activó el cobro por adelantado del partner. No se reintenta: recarga con `POST /topup`.
63
+ * Saldo prepagado insuficiente (con `balanceCents` y `requiredCents`). No se reintenta: recarga
64
+ * con `POST /topup`.
65
65
  */
66
66
  readonly insufficient_balance: 402;
67
- /** Cuota mensual del tenant agotada. */
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. */
68
73
  readonly quota_exceeded: 429;
69
74
  /** Demasiadas peticiones; reintenta tras `Retry-After`. */
70
75
  readonly rate_limited: 429;
@@ -77,12 +82,15 @@ export interface PartnerErrorBody {
77
82
  code: PartnerErrorCode;
78
83
  /** Sólo en `invalid_request`: el campo que falló, si aplica. */
79
84
  field?: string;
80
- /** 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). */
81
86
  limit?: number;
87
+ sentToday?: number;
82
88
  resetsAt?: string;
83
- /** Sólo en `insufficient_balance`: saldo actual y lo que pedía el envío (centavos MXN). */
89
+ /** Sólo en `insufficient_balance`: saldo actual y lo que pedía el envío (centavos de `currency`). */
84
90
  balanceCents?: number;
85
91
  requiredCents?: number;
92
+ /** Moneda de `balanceCents` y `requiredCents`. */
93
+ currency?: PartnerCurrency;
86
94
  /** Sólo en `recipient_suppressed`. */
87
95
  reason?: SuppressionReason;
88
96
  stream?: MessageStream;
@@ -112,7 +120,38 @@ export interface Tenant {
112
120
  lastSendAt: string | null;
113
121
  createdAt: string;
114
122
  updatedAt: string;
123
+ /** Límite diario con calentamiento. Ver `TenantSending`. */
124
+ sending: TenantSending;
115
125
  }
126
+ /**
127
+ * Límite diario por tenant que crece solo, como el calentamiento de SES: empieza en
128
+ * `TENANT_DAILY_LIMIT_START` al primer envío y se duplica cada `TENANT_WARMUP_STEP_DAYS` días sanos
129
+ * (rebotes < 2 %, quejas < 0.1 %) hasta `TENANT_DAILY_LIMIT_MAX`. Una pausa por reputación lo
130
+ * regresa al inicio al reanudar. No hay tope mensual: el saldo es el otro freno.
131
+ */
132
+ export declare const TENANT_DAILY_LIMIT_START = 2000;
133
+ export declare const TENANT_DAILY_LIMIT_MAX = 20000;
134
+ export declare const TENANT_WARMUP_STEP_DAYS = 7;
135
+ /** El límite del escalón `step` (0 = inicio). */
136
+ export declare function warmupLimit(step: number): number;
137
+ export interface TenantSending {
138
+ /** Correos que puede mandar hoy (UTC) en total. */
139
+ dailyLimit: number;
140
+ sentToday: number;
141
+ /** Medianoche UTC siguiente. */
142
+ resetsAt: string;
143
+ /** 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. */
144
+ nextStepAt: string | null;
145
+ /** El límite que tendrá en `nextStepAt`; null si no sube. */
146
+ nextStepLimit: number | null;
147
+ /** true = MailMask fijó el límite a mano (no se calienta). */
148
+ fixed: boolean;
149
+ }
150
+ /**
151
+ * Días (contando hoy) que tarda en salir un lote de `recipients` correos si los escalones llegan a
152
+ * tiempo. 1 = sale hoy. Sirve para avisar «se manda en N días» antes de encolar.
153
+ */
154
+ export declare function estimateSendDays(s: TenantSending, recipients: number): number;
116
155
  /** `PUT /tenants/:externalId` — crea o actualiza. 201 si lo creó, 200 si ya existía. */
117
156
  export interface PutTenantRequest {
118
157
  name?: string;
@@ -351,6 +390,11 @@ export interface BatchStatus {
351
390
  lastError: string | null;
352
391
  createdAt: string;
353
392
  completedAt: string | null;
393
+ /**
394
+ * El lote llegó al límite diario del tenant y sigue solo a esta hora (medianoche UTC); null si no
395
+ * está esperando por el límite. Mientras espera, el lote no caduca.
396
+ */
397
+ waitingUntil: string | null;
354
398
  /**
355
399
  * Sólo en modo de prueba: el lote se valida y responde `completed` al instante (los suprimidos en
356
400
  * `skippedSuppressed`, el resto en `sent`), con `batchId` `test_…` que no se guarda (su GET da 404).
@@ -368,6 +412,8 @@ export interface TenantUsage {
368
412
  bounced: number;
369
413
  complained: number;
370
414
  unsubscribed: number;
415
+ /** Límite diario de hoy; falta si el tenant ya se borró. */
416
+ sending?: TenantSending;
371
417
  }
372
418
  /** `GET /usage?month=YYYY-MM` (default: el mes en curso, UTC). Activo = al menos un envío en el mes. */
373
419
  export interface UsageResponse {
@@ -378,18 +424,37 @@ export interface UsageResponse {
378
424
  }
379
425
  /**
380
426
  * El partner paga por adelantado: cada correo y la cuota mensual de cada tenant activo se
381
- * descuentan de un saldo que se recarga con MercadoPago (`POST /topup`). Todo en centavos de MXN.
382
- */
383
- /** Precio por correo enviado (por destinatario). */
384
- export declare const PARTNER_PRICE_PER_EMAIL_CENTS = 6;
385
- /**
386
- * Cuota mensual por tenant activo. Activo = envía ese mes: se descuenta con su primer envío del
387
- * mes (UTC), una sola vez, así que un tenant dormido no cuesta nada.
427
+ * descuentan de un saldo que se recarga con MercadoPago (`POST /topup`). El saldo es en la moneda
428
+ * del partner (MXN o USD) y es la única unidad: no hay «créditos». Todos los montos `*Cents` son
429
+ * centavos de esa moneda y pueden traer decimales (en USD un correo cuesta 0.35 centavos; por
430
+ * dentro se lleva en milésimas de centavo). Se muestran con 2 decimales.
388
431
  */
389
- export declare const PARTNER_TENANT_MONTH_CENTS = 4900;
390
- /** Recarga mínima y máxima por pago. */
391
- export declare const PARTNER_TOPUP_MIN_CENTS = 50000;
392
- export declare const PARTNER_TOPUP_MAX_CENTS = 10000000;
432
+ export type PartnerCurrency = "MXN" | "USD";
433
+ export declare const PARTNER_CURRENCIES: readonly PartnerCurrency[];
434
+ export interface PartnerPrices {
435
+ /** Por correo enviado (por destinatario). */
436
+ perEmailCents: number;
437
+ /**
438
+ * Cuota mensual por tenant activo. Activo = envía ese mes: se descuenta con su primer envío del
439
+ * mes (UTC), una sola vez, así que un tenant dormido no cuesta nada.
440
+ */
441
+ tenantMonthCents: number;
442
+ /** Saldo de bienvenida con el que nace el partner (`adjust`, idemKey `welcome:<partnerId>`). */
443
+ welcomeCents: number;
444
+ /** Recarga mínima y máxima por pago (enteros de la moneda × 100). */
445
+ topupMinCents: number;
446
+ topupMaxCents: number;
447
+ }
448
+ /** Precios por moneda. USD = MXN redondeado; se cobra en MXN por MercadoPago con el tipo de cambio. */
449
+ export declare const PARTNER_PRICES: Record<PartnerCurrency, PartnerPrices>;
450
+ /** Los de MXN, con su nombre de antes (0.7). */
451
+ export declare const PARTNER_PRICE_PER_EMAIL_CENTS: number;
452
+ export declare const PARTNER_TENANT_MONTH_CENTS: number;
453
+ export declare const PARTNER_WELCOME_CENTS: number;
454
+ export declare const PARTNER_TOPUP_MIN_CENTS: number;
455
+ export declare const PARTNER_TOPUP_MAX_CENTS: number;
456
+ /** `$1,234.50 USD`: 2 decimales (4 si el monto es menor a un centavo, como el precio por correo en USD). */
457
+ export declare function formatMoney(cents: number, currency: PartnerCurrency): string;
393
458
  /** `balance.low` sale cuando el saldo baja del 20 % de la última recarga (una vez por recarga). */
394
459
  export declare const BALANCE_LOW_FRACTION = 0.2;
395
460
  export type BalanceMovementKind = "topup" | "send" | "tenant_fee" | "refund" | "adjust";
@@ -409,18 +474,11 @@ export interface BalanceMovement {
409
474
  }
410
475
  /** `GET /balance` */
411
476
  export interface BalanceResponse {
412
- currency: "MXN";
477
+ /** La moneda del saldo y de todos los montos de esta respuesta. */
478
+ currency: PartnerCurrency;
479
+ /** Sin saldo suficiente el envío se rechaza (402 `insufficient_balance`) y el lote espera. Siempre. */
413
480
  balanceCents: number;
414
- /**
415
- * true = sin saldo suficiente los envíos se rechazan (402 `insufficient_balance`) y los lotes
416
- * esperan. false = el saldo se lleva igual (puede quedar negativo) pero no frena nada.
417
- */
418
- enforced: boolean;
419
- prices: {
420
- perEmailCents: number;
421
- tenantMonthCents: number;
422
- topupMinCents: number;
423
- };
481
+ prices: PartnerPrices;
424
482
  lastTopup: {
425
483
  amountCents: number;
426
484
  at: string;
@@ -429,7 +487,7 @@ export interface BalanceResponse {
429
487
  lowBalanceThresholdCents: number | null;
430
488
  /** Mes en curso (UTC). */
431
489
  month: string;
432
- /** Tenants a los que ya se les cobró la cuota del mes. */
490
+ /** Tenants que enviaron este mes (la misma cuenta que `/usage`): cada uno paga la cuota del mes. */
433
491
  activeTenants: number;
434
492
  /** Correos cobrados en el mes (sin los devueltos). */
435
493
  monthEmails: number;
@@ -444,6 +502,8 @@ export interface BalanceMovementsQuery {
444
502
  kind?: BalanceMovementKind[];
445
503
  }
446
504
  export interface BalanceMovementsResponse {
505
+ /** La moneda de los montos. */
506
+ currency: PartnerCurrency;
447
507
  movements: BalanceMovement[];
448
508
  nextCursor: string | null;
449
509
  }
@@ -452,37 +512,89 @@ export interface BalanceMovementsResponse {
452
512
  * el pago (webhook), no al crear la liga. `returnUrl` (https) es a dónde regresa el navegador.
453
513
  */
454
514
  export interface TopupRequest {
455
- /** Pesos MXN enteros, 500 a 100,000. */
456
- amountMxn: number;
515
+ /**
516
+ * Unidades enteras de la moneda del partner (MXN: 500 a 100,000; USD: 29 a 5,500). En USD
517
+ * MercadoPago cobra el equivalente en MXN con el tipo de cambio de MailMask y se abona lo pedido
518
+ * en USD.
519
+ */
520
+ amount?: number;
521
+ /** Lo mismo que `amount`, sólo para partners en MXN (0.7). */
522
+ amountMxn?: number;
457
523
  returnUrl?: string;
458
524
  }
459
525
  export interface TopupResponse {
460
526
  topupId: string;
527
+ /** Lo que se abona al saldo, en la moneda del partner. */
461
528
  amountCents: number;
529
+ currency: PartnerCurrency;
530
+ /** Lo que cobra MercadoPago, en centavos MXN (igual a `amountCents` si el partner es MXN). */
531
+ chargeMxnCents: number;
532
+ /** MXN por USD con que se cotizó; null en MXN. */
533
+ fxRate: number | null;
462
534
  /** Abre esto en el navegador de quien paga. */
463
535
  initPoint: string;
464
536
  }
537
+ /**
538
+ * Recarga automática mensual: un cobro de MercadoPago cada mes (preapproval con la tarjeta del
539
+ * dueño) que abona `amountCents` al saldo con nota «Recarga automática». En USD MercadoPago cobra
540
+ * `chargeMxnCents` (fijado al activarla) y se abona lo elegido en USD.
541
+ */
542
+ export interface AutoTopup {
543
+ /** off = apagada; pending = falta autorizarla en MercadoPago (`initPoint`); active = cobra cada mes. */
544
+ status: "off" | "pending" | "active";
545
+ currency: PartnerCurrency;
546
+ amountCents: number | null;
547
+ chargeMxnCents: number | null;
548
+ nextChargeAt: string | null;
549
+ lastChargeAt: string | null;
550
+ /** Cobros rechazados seguidos (se reinicia con uno aprobado). */
551
+ failedCharges: number;
552
+ lastError: string | null;
553
+ /** Sólo en la respuesta de prenderla o cambiarla: ábrelo para autorizar el cobro mensual. */
554
+ initPoint?: string;
555
+ }
556
+ /**
557
+ * `PUT /auto-topup` — `amount` en unidades enteras de la moneda (mismo rango que `/topup`) la prende
558
+ * o cambia (cancela la anterior y crea otra); `null` la apaga.
559
+ */
560
+ export interface SetAutoTopupRequest {
561
+ amount: number | null;
562
+ returnUrl?: string;
563
+ }
564
+ /** Montos sugeridos para la recarga automática (unidades enteras). */
565
+ export declare const AUTO_TOPUP_SUGGESTIONS: Record<PartnerCurrency, readonly number[]>;
465
566
  /**
466
567
  * `GET /api/partners/:partnerId/balance` (dueño o MailMask): el saldo, sus últimos 20 movimientos,
467
568
  * el partner (con `tenants` y `ownerEmail`) y si quien mira es de MailMask (ve abonar y el cobro).
468
569
  */
570
+ /** Un tenant en la ficha: los 50 que enviaron más recientemente, con su límite de hoy. */
571
+ export interface PartnerDetailTenant {
572
+ externalId: string;
573
+ name: string | null;
574
+ status: TenantStatus;
575
+ lastSendAt: string | null;
576
+ sending: TenantSending;
577
+ }
469
578
  export type PartnerBalanceDetail = BalanceResponse & {
470
579
  movements: BalanceMovement[];
471
580
  partner: PartnerSummary;
581
+ tenants: PartnerDetailTenant[];
582
+ /** La recarga automática mensual. */
583
+ autoTopup: AutoTopup;
472
584
  staff: boolean;
473
585
  };
586
+ /** `PUT /api/partners/:partnerId/tenants/:externalId/daily-limit` — sólo MailMask: tope fijo o `null` (vuelve al calentamiento). */
587
+ export interface SetTenantDailyLimitRequest {
588
+ limit: number | null;
589
+ }
474
590
  /** `POST /api/partners/:partnerId/balance/adjust` — sólo MailMask (admin): abono o cargo manual. */
475
591
  export interface AdjustBalanceRequest {
476
- /** Centavos MXN, distinto de 0 (negativo = cargo). */
592
+ /** Centavos enteros de la moneda del partner, distinto de 0 (negativo = cargo). */
477
593
  amountCents: number;
478
594
  note: string;
479
595
  /** Repetir con la misma llave no vuelve a abonar. */
480
596
  idempotencyKey?: string;
481
597
  }
482
- /** `PUT /api/partners/:partnerId/prepaid` — sólo MailMask (admin): prende o apaga el cobro por adelantado. */
483
- export interface SetPrepaidEnforcedRequest {
484
- enforced: boolean;
485
- }
486
598
  /** `GET /webhook` */
487
599
  export interface WebhookConfig {
488
600
  url: string | null;
@@ -516,7 +628,7 @@ export interface RotateWebhookSecretResponse {
516
628
  export interface TestWebhookResponse {
517
629
  deliveryId: string;
518
630
  }
519
- 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"];
631
+ 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"];
520
632
  export type PartnerWebhookEvent = typeof PARTNER_WEBHOOK_EVENTS[number];
521
633
  /** Headers de cada entrega al webhook del partner (los mismos que los webhooks de dominio). */
522
634
  export declare const WEBHOOK_HEADERS: {
@@ -599,6 +711,15 @@ export interface PartnerEventData {
599
711
  balanceCents: number;
600
712
  thresholdCents: number;
601
713
  lastTopupCents: number;
714
+ currency: PartnerCurrency;
715
+ };
716
+ /** Un lote llegó al límite diario del tenant: sigue solo en `resumesAt`. Una vez por lote. */
717
+ "batch.throttled": {
718
+ batchId: string;
719
+ sent: number;
720
+ total: number;
721
+ dailyLimit: number;
722
+ resumesAt: string;
602
723
  };
603
724
  ping: Record<string, never>;
604
725
  }
@@ -623,6 +744,7 @@ export declare const STRICT_FIRST_DAYS = 30;
623
744
  export interface PartnerSummary {
624
745
  id: string;
625
746
  name: string;
747
+ /** `sendsPerTenantMonth` ya no frena (0.8): el límite es diario por tenant, ver `TenantSending`. */
626
748
  plan: {
627
749
  includedTenants: number;
628
750
  sendsPerTenantMonth: number;
@@ -636,9 +758,9 @@ export interface PartnerSummary {
636
758
  effectiveReputationPolicy: ReputationPolicy;
637
759
  /** Zona de respaldo (`marketing.denik.me`); null = sin zona. Ver `FallbackZone`. */
638
760
  fallbackZone: string | null;
639
- /** Saldo prepagado (centavos MXN) y si ya frena envíos. Ver `BalanceResponse`. */
761
+ /** Saldo prepagado, en centavos de `currency`. Ver `BalanceResponse`. */
640
762
  balanceCents: number;
641
- prepaidEnforced: boolean;
763
+ currency: PartnerCurrency;
642
764
  /** Sólo en `GET /api/partners?all=1` (MailMask, admin): tenants totales y activos del mes. */
643
765
  tenants?: {
644
766
  total: number;
@@ -654,6 +776,15 @@ export interface CreatePartnerRequest {
654
776
  ownerEmail: string;
655
777
  plan?: Partial<PartnerSummary["plan"]>;
656
778
  reputationPolicy?: ReputationPolicy | null;
779
+ /** Moneda del saldo; default MXN. Nace con la bienvenida de esa moneda. */
780
+ currency?: PartnerCurrency;
781
+ }
782
+ /**
783
+ * `PUT /api/partners/:partnerId/currency` — sólo MailMask, y sólo mientras el partner no tenga más
784
+ * movimientos que la bienvenida (que se cambia por la de la nueva moneda).
785
+ */
786
+ export interface SetPartnerCurrencyRequest {
787
+ currency: PartnerCurrency;
657
788
  }
658
789
  /** `PUT /api/partners/:partnerId/reputation-policy` — sólo MailMask (admin); null = automática. */
659
790
  export interface SetReputationPolicyRequest {
@@ -773,6 +904,14 @@ export declare const PARTNER_ROUTES: {
773
904
  readonly method: "POST";
774
905
  readonly path: "/topup";
775
906
  };
907
+ readonly getAutoTopup: {
908
+ readonly method: "GET";
909
+ readonly path: "/auto-topup";
910
+ };
911
+ readonly setAutoTopup: {
912
+ readonly method: "PUT";
913
+ readonly path: "/auto-topup";
914
+ };
776
915
  readonly getWebhook: {
777
916
  readonly method: "GET";
778
917
  readonly path: "/webhook";
@@ -829,16 +968,29 @@ export declare const PARTNER_ADMIN_ROUTES: {
829
968
  readonly method: "POST";
830
969
  readonly path: "/api/partners/:partnerId/balance/adjust";
831
970
  };
832
- /** Sólo MailMask (admin): prende o apaga el cobro por adelantado. */
833
- readonly setPrepaidEnforced: {
834
- readonly method: "PUT";
835
- readonly path: "/api/partners/:partnerId/prepaid";
836
- };
837
971
  /** Recarga con MercadoPago desde la sesión del dueño (misma respuesta que `POST /topup`). */
838
972
  readonly topup: {
839
973
  readonly method: "POST";
840
974
  readonly path: "/api/partners/:partnerId/topup";
841
975
  };
976
+ readonly getAutoTopup: {
977
+ readonly method: "GET";
978
+ readonly path: "/api/partners/:partnerId/auto-topup";
979
+ };
980
+ readonly setAutoTopup: {
981
+ readonly method: "PUT";
982
+ readonly path: "/api/partners/:partnerId/auto-topup";
983
+ };
984
+ /** Sólo MailMask: moneda del saldo, mientras no haya más movimientos que la bienvenida. */
985
+ readonly setCurrency: {
986
+ readonly method: "PUT";
987
+ readonly path: "/api/partners/:partnerId/currency";
988
+ };
989
+ /** Sólo MailMask: fija (o quita, con `null`) el límite diario de un tenant. Devuelve el `Tenant`. */
990
+ readonly setTenantDailyLimit: {
991
+ readonly method: "PUT";
992
+ readonly path: "/api/partners/:partnerId/tenants/:externalId/daily-limit";
993
+ };
842
994
  readonly listCredentials: {
843
995
  readonly method: "GET";
844
996
  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, BalanceResponse, BalanceMovementsQuery, BalanceMovementsResponse, TopupRequest, TopupResponse, PartnerBalanceDetail, AdjustBalanceRequest } 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, PartnerCurrency, AutoTopup, SetAutoTopupRequest } 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" | "insufficient_balance" | "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;
@@ -101,14 +101,25 @@ export declare class RateLimitError extends QuotaError {
101
101
  kind: PartnerErrorKind;
102
102
  }
103
103
  /**
104
- * 402 `insufficient_balance`: el saldo prepagado no alcanza (sólo con el cobro por adelantado
105
- * prendido). No se reintenta: recarga con `partner.topup()`. Trae `balanceCents` y `requiredCents`.
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`, `requiredCents` y `currency`.
106
115
  */
107
116
  export declare class InsufficientBalanceError extends PartnerApiError {
108
117
  name: string;
109
118
  kind: PartnerErrorKind;
110
119
  get balanceCents(): number | undefined;
111
120
  get requiredCents(): number | undefined;
121
+ /** Moneda de los dos montos de arriba. */
122
+ get currency(): PartnerCurrency | undefined;
112
123
  }
113
124
  /** No hubo respuesta: DNS, conexión rechazada o cortada. `status` 0. */
114
125
  export declare class NetworkError extends PartnerApiError {
@@ -233,10 +244,18 @@ export declare class MailMaskPartner {
233
244
  /** Movimientos del saldo, del más nuevo al más viejo; pagina con `nextCursor`. */
234
245
  balanceMovements(query?: BalanceMovementsQuery): Promise<BalanceMovementsResponse>;
235
246
  /**
236
- * Liga de MercadoPago para recargar saldo (pago único, $500 a $100,000 MXN). El saldo sube cuando
237
- * MercadoPago confirma el pago, no al crear la liga. Abre `initPoint` en el navegador de quien paga.
247
+ * Liga de MercadoPago para recargar saldo (pago único; `amount` en unidades enteras de tu moneda:
248
+ * MXN 500 a 100,000, USD 29 a 5,500). El saldo sube cuando MercadoPago confirma el pago, no al
249
+ * crear la liga. Abre `initPoint` en el navegador de quien paga.
238
250
  */
239
251
  topup(input: TopupRequest): Promise<TopupResponse>;
252
+ /** La recarga automática mensual: `off`, `pending` (falta autorizarla) o `active`. */
253
+ autoTopup(): Promise<AutoTopup>;
254
+ /**
255
+ * Prende o cambia (`amount` entero de tu moneda) la recarga automática mensual, o la apaga
256
+ * (`amount: null`). Al prenderla o cambiarla devuelve `initPoint`: ábrelo para autorizar el cobro.
257
+ */
258
+ setAutoTopup(input: SetAutoTopupRequest): Promise<AutoTopup>;
240
259
  /** Uso del mes (`YYYY-MM`, default el mes en curso UTC): tenants activos y envíos. */
241
260
  usage(opts?: {
242
261
  month?: string;
@@ -371,8 +390,12 @@ export declare class MailMaskPartnerAdmin {
371
390
  createPartner(input: CreatePartnerRequest): Promise<PartnerSummary>;
372
391
  /** Sólo MailMask (admin); null = automática (Strict el primer mes, Standard después). */
373
392
  setReputationPolicy(partnerId: string, policy: ReputationPolicy | null): Promise<PartnerSummary>;
393
+ /** Sólo MailMask (admin): moneda del saldo (MXN o USD); sólo antes de cualquier movimiento aparte de la bienvenida. */
394
+ setCurrency(partnerId: string, currency: PartnerCurrency): Promise<PartnerSummary>;
374
395
  /** Sólo MailMask (admin): levanta cualquier pausa, también la de reputación. */
375
396
  resumeTenant(partnerId: string, externalId: string): Promise<Tenant>;
397
+ /** Sólo MailMask: tope diario fijo del tenant, o `null` para volver al calentamiento. */
398
+ setTenantDailyLimit(partnerId: string, externalId: string, limit: number | null): Promise<Tenant>;
376
399
  /**
377
400
  * Sólo MailMask (admin): crea o adopta la hosted zone de la zona de respaldo (`marketing.denik.me`)
378
401
  * y devuelve los NS que el partner tiene que delegar. Idempotente con la misma zona.
@@ -384,10 +407,12 @@ export declare class MailMaskPartnerAdmin {
384
407
  getBalance(partnerId: string): Promise<PartnerBalanceDetail>;
385
408
  /** Sólo MailMask (admin): abono (o cargo, negativo) manual. Con la misma `idempotencyKey` no se repite. */
386
409
  adjustBalance(partnerId: string, input: AdjustBalanceRequest): Promise<PartnerBalanceDetail>;
387
- /** Sólo MailMask (admin): prende o apaga el cobro por adelantado del partner. */
388
- setPrepaidEnforced(partnerId: string, enforced: boolean): Promise<PartnerSummary>;
389
410
  /** Liga de MercadoPago para recargar, con la llave del dueño. */
390
411
  topup(partnerId: string, input: TopupRequest): Promise<TopupResponse>;
412
+ /** La recarga automática mensual del partner (dueño o MailMask). */
413
+ getAutoTopup(partnerId: string): Promise<AutoTopup>;
414
+ /** Prende, cambia (`amount`) o apaga (`null`) la recarga automática; trae `initPoint` al prenderla. */
415
+ setAutoTopup(partnerId: string, input: SetAutoTopupRequest): Promise<AutoTopup>;
391
416
  listCredentials(partnerId: string): Promise<ListCredentialsResponse>;
392
417
  /** El secreto `mms_…` viene UNA vez en la respuesta. */
393
418
  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.7.0",
3
+ "version": "0.9.0",
4
4
  "description": "Official SDK for the MailMask email alias/forwarding API",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",