@easybits.cloud/mailmask 0.7.0 → 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
@@ -25,6 +25,7 @@ __export(index_exports, {
25
25
  BALANCE_MOVEMENT_KINDS: () => BALANCE_MOVEMENT_KINDS,
26
26
  ConflictError: () => ConflictError,
27
27
  DOMAIN_CLAIM_TTL_DAYS: () => DOMAIN_CLAIM_TTL_DAYS,
28
+ DailyLimitError: () => DailyLimitError,
28
29
  EXTERNAL_ID_RE: () => EXTERNAL_ID_RE,
29
30
  FALLBACK_LABEL_RE: () => FALLBACK_LABEL_RE,
30
31
  ForbiddenError: () => ForbiddenError,
@@ -52,6 +53,7 @@ __export(index_exports, {
52
53
  PARTNER_TOPUP_MAX_CENTS: () => PARTNER_TOPUP_MAX_CENTS,
53
54
  PARTNER_TOPUP_MIN_CENTS: () => PARTNER_TOPUP_MIN_CENTS,
54
55
  PARTNER_WEBHOOK_EVENTS: () => PARTNER_WEBHOOK_EVENTS,
56
+ PARTNER_WELCOME_CENTS: () => PARTNER_WELCOME_CENTS,
55
57
  PartnerApiError: () => PartnerApiError,
56
58
  PausedError: () => PausedError,
57
59
  QuotaError: () => QuotaError,
@@ -62,12 +64,16 @@ __export(index_exports, {
62
64
  STRICT_FIRST_DAYS: () => STRICT_FIRST_DAYS,
63
65
  TAG_RE: () => TAG_RE,
64
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,
65
70
  TimeoutError: () => TimeoutError,
66
71
  UNSUBSCRIBE_URL_VAR: () => UNSUBSCRIBE_URL_VAR,
67
72
  ValidationError: () => ValidationError,
68
73
  WEBHOOK_HEADERS: () => WEBHOOK_HEADERS,
69
74
  WEBHOOK_TOLERANCE_MS: () => WEBHOOK_TOLERANCE_MS,
70
75
  createWebhookHandler: () => createWebhookHandler,
76
+ estimateSendDays: () => estimateSendDays,
71
77
  isValidExternalId: () => isValidExternalId,
72
78
  isValidFallbackLabel: () => isValidFallbackLabel,
73
79
  partnerSignatureBase: () => partnerSignatureBase,
@@ -80,6 +86,7 @@ __export(index_exports, {
80
86
  templateVarsProblem: () => templateVarsProblem,
81
87
  verifyWebhook: () => verifyWebhook,
82
88
  verifyWebhookSignature: () => verifyWebhookSignature,
89
+ warmupLimit: () => warmupLimit,
83
90
  webhookSignatureBase: () => webhookSignatureBase
84
91
  });
85
92
  module.exports = __toCommonJS(index_exports);
@@ -724,11 +731,16 @@ var PARTNER_ERROR_STATUS = {
724
731
  /** El tenant está en pausa (por MailMask o por el partner). No se manda nada hasta `resume`. */
725
732
  tenant_paused: 423,
726
733
  /**
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`.
734
+ * Saldo prepagado insuficiente (con `balanceCents` y `requiredCents`). No se reintenta: recarga
735
+ * con `POST /topup`.
729
736
  */
730
737
  insufficient_balance: 402,
731
- /** Cuota mensual del tenant agotada. */
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. */
732
744
  quota_exceeded: 429,
733
745
  /** Demasiadas peticiones; reintenta tras `Retry-After`. */
734
746
  rate_limited: 429,
@@ -742,6 +754,30 @@ function isValidExternalId(id) {
742
754
  var MAX_BATCH_RECIPIENTS = 1e4;
743
755
  var MAX_TAGS = 10;
744
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
+ }
745
781
  var DOMAIN_CLAIM_TTL_DAYS = 7;
746
782
  var FALLBACK_LABEL_RE = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
747
783
  var RESERVED_FALLBACK_LABELS = [
@@ -809,6 +845,7 @@ function personalizePartnerMessage(input) {
809
845
  }
810
846
  var PARTNER_PRICE_PER_EMAIL_CENTS = 6;
811
847
  var PARTNER_TENANT_MONTH_CENTS = 4900;
848
+ var PARTNER_WELCOME_CENTS = 5e4;
812
849
  var PARTNER_TOPUP_MIN_CENTS = 5e4;
813
850
  var PARTNER_TOPUP_MAX_CENTS = 1e7;
814
851
  var BALANCE_LOW_FRACTION = 0.2;
@@ -823,7 +860,8 @@ var PARTNER_WEBHOOK_EVENTS = [
823
860
  "tenant.paused",
824
861
  "tenant.resumed",
825
862
  "domain.verified",
826
- "balance.low"
863
+ "balance.low",
864
+ "batch.throttled"
827
865
  ];
828
866
  var WEBHOOK_HEADERS = {
829
867
  event: "X-MailMask-Event",
@@ -877,10 +915,10 @@ var PARTNER_ADMIN_ROUTES = {
877
915
  getBalance: { method: "GET", path: "/api/partners/:partnerId/balance" },
878
916
  /** Sólo MailMask (admin): abono o cargo manual (`adjust`). */
879
917
  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
918
  /** Recarga con MercadoPago desde la sesión del dueño (misma respuesta que `POST /topup`). */
883
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" },
884
922
  listCredentials: { method: "GET", path: "/api/partners/:partnerId/credentials" },
885
923
  createCredential: { method: "POST", path: "/api/partners/:partnerId/credentials" },
886
924
  rotateCredential: { method: "POST", path: "/api/partners/:partnerId/credentials/:keyId/rotate" },
@@ -894,8 +932,8 @@ var PartnerApiError = class extends MailMaskError {
894
932
  code: body.code,
895
933
  requestId: meta.requestId,
896
934
  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"
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"
899
937
  });
900
938
  this.kind = "unknown";
901
939
  this.name = "PartnerApiError";
@@ -998,6 +1036,16 @@ var RateLimitError = class extends QuotaError {
998
1036
  this.kind = "rate_limit";
999
1037
  }
1000
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
+ };
1001
1049
  var InsufficientBalanceError = class extends PartnerApiError {
1002
1050
  constructor() {
1003
1051
  super(...arguments);
@@ -1029,6 +1077,7 @@ function errorFor(status, body, fallback, meta = {}) {
1029
1077
  const msg = body.error || fallback;
1030
1078
  if (body.code === "reputation_hold") return new ReputationHoldError(status, msg, body, meta);
1031
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);
1032
1081
  if (status === 429 && body.code !== "quota_exceeded") return new RateLimitError(status, msg, body, meta);
1033
1082
  switch (status) {
1034
1083
  case 401:
@@ -1459,6 +1508,10 @@ var MailMaskPartnerAdmin = class {
1459
1508
  resumeTenant(partnerId, externalId) {
1460
1509
  return this.req("resumeTenant", { partnerId, externalId });
1461
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
+ }
1462
1515
  /**
1463
1516
  * Sólo MailMask (admin): crea o adopta la hosted zone de la zona de respaldo (`marketing.denik.me`)
1464
1517
  * y devuelve los NS que el partner tiene que delegar. Idempotente con la misma zona.
@@ -1478,10 +1531,6 @@ var MailMaskPartnerAdmin = class {
1478
1531
  adjustBalance(partnerId, input) {
1479
1532
  return this.req("adjustBalance", { partnerId }, input);
1480
1533
  }
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
1534
  /** Liga de MercadoPago para recargar, con la llave del dueño. */
1486
1535
  topup(partnerId, input) {
1487
1536
  return this.req("topup", { partnerId }, input);
@@ -1509,6 +1558,7 @@ var MailMaskPartnerAdmin = class {
1509
1558
  BALANCE_MOVEMENT_KINDS,
1510
1559
  ConflictError,
1511
1560
  DOMAIN_CLAIM_TTL_DAYS,
1561
+ DailyLimitError,
1512
1562
  EXTERNAL_ID_RE,
1513
1563
  FALLBACK_LABEL_RE,
1514
1564
  ForbiddenError,
@@ -1536,6 +1586,7 @@ var MailMaskPartnerAdmin = class {
1536
1586
  PARTNER_TOPUP_MAX_CENTS,
1537
1587
  PARTNER_TOPUP_MIN_CENTS,
1538
1588
  PARTNER_WEBHOOK_EVENTS,
1589
+ PARTNER_WELCOME_CENTS,
1539
1590
  PartnerApiError,
1540
1591
  PausedError,
1541
1592
  QuotaError,
@@ -1546,12 +1597,16 @@ var MailMaskPartnerAdmin = class {
1546
1597
  STRICT_FIRST_DAYS,
1547
1598
  TAG_RE,
1548
1599
  TEMPLATE_VAR_NAME_RE,
1600
+ TENANT_DAILY_LIMIT_MAX,
1601
+ TENANT_DAILY_LIMIT_START,
1602
+ TENANT_WARMUP_STEP_DAYS,
1549
1603
  TimeoutError,
1550
1604
  UNSUBSCRIBE_URL_VAR,
1551
1605
  ValidationError,
1552
1606
  WEBHOOK_HEADERS,
1553
1607
  WEBHOOK_TOLERANCE_MS,
1554
1608
  createWebhookHandler,
1609
+ estimateSendDays,
1555
1610
  isValidExternalId,
1556
1611
  isValidFallbackLabel,
1557
1612
  partnerSignatureBase,
@@ -1564,5 +1619,6 @@ var MailMaskPartnerAdmin = class {
1564
1619
  templateVarsProblem,
1565
1620
  verifyWebhook,
1566
1621
  verifyWebhookSignature,
1622
+ warmupLimit,
1567
1623
  webhookSignatureBase
1568
1624
  });
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 = 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
+ }
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 = [
@@ -723,6 +752,7 @@ function personalizePartnerMessage(input) {
723
752
  }
724
753
  var PARTNER_PRICE_PER_EMAIL_CENTS = 6;
725
754
  var PARTNER_TENANT_MONTH_CENTS = 4900;
755
+ var PARTNER_WELCOME_CENTS = 5e4;
726
756
  var PARTNER_TOPUP_MIN_CENTS = 5e4;
727
757
  var PARTNER_TOPUP_MAX_CENTS = 1e7;
728
758
  var BALANCE_LOW_FRACTION = 0.2;
@@ -737,7 +767,8 @@ var PARTNER_WEBHOOK_EVENTS = [
737
767
  "tenant.paused",
738
768
  "tenant.resumed",
739
769
  "domain.verified",
740
- "balance.low"
770
+ "balance.low",
771
+ "batch.throttled"
741
772
  ];
742
773
  var WEBHOOK_HEADERS = {
743
774
  event: "X-MailMask-Event",
@@ -791,10 +822,10 @@ var PARTNER_ADMIN_ROUTES = {
791
822
  getBalance: { method: "GET", path: "/api/partners/:partnerId/balance" },
792
823
  /** Sólo MailMask (admin): abono o cargo manual (`adjust`). */
793
824
  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
825
  /** Recarga con MercadoPago desde la sesión del dueño (misma respuesta que `POST /topup`). */
797
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" },
798
829
  listCredentials: { method: "GET", path: "/api/partners/:partnerId/credentials" },
799
830
  createCredential: { method: "POST", path: "/api/partners/:partnerId/credentials" },
800
831
  rotateCredential: { method: "POST", path: "/api/partners/:partnerId/credentials/:keyId/rotate" },
@@ -808,8 +839,8 @@ var PartnerApiError = class extends MailMaskError {
808
839
  code: body.code,
809
840
  requestId: meta.requestId,
810
841
  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"
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"
813
844
  });
814
845
  this.kind = "unknown";
815
846
  this.name = "PartnerApiError";
@@ -912,6 +943,16 @@ var RateLimitError = class extends QuotaError {
912
943
  this.kind = "rate_limit";
913
944
  }
914
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
+ };
915
956
  var InsufficientBalanceError = class extends PartnerApiError {
916
957
  constructor() {
917
958
  super(...arguments);
@@ -943,6 +984,7 @@ function errorFor(status, body, fallback, meta = {}) {
943
984
  const msg = body.error || fallback;
944
985
  if (body.code === "reputation_hold") return new ReputationHoldError(status, msg, body, meta);
945
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);
946
988
  if (status === 429 && body.code !== "quota_exceeded") return new RateLimitError(status, msg, body, meta);
947
989
  switch (status) {
948
990
  case 401:
@@ -1373,6 +1415,10 @@ var MailMaskPartnerAdmin = class {
1373
1415
  resumeTenant(partnerId, externalId) {
1374
1416
  return this.req("resumeTenant", { partnerId, externalId });
1375
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
+ }
1376
1422
  /**
1377
1423
  * Sólo MailMask (admin): crea o adopta la hosted zone de la zona de respaldo (`marketing.denik.me`)
1378
1424
  * y devuelve los NS que el partner tiene que delegar. Idempotente con la misma zona.
@@ -1392,10 +1438,6 @@ var MailMaskPartnerAdmin = class {
1392
1438
  adjustBalance(partnerId, input) {
1393
1439
  return this.req("adjustBalance", { partnerId }, input);
1394
1440
  }
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
1441
  /** Liga de MercadoPago para recargar, con la llave del dueño. */
1400
1442
  topup(partnerId, input) {
1401
1443
  return this.req("topup", { partnerId }, input);
@@ -1422,6 +1464,7 @@ export {
1422
1464
  BALANCE_MOVEMENT_KINDS,
1423
1465
  ConflictError,
1424
1466
  DOMAIN_CLAIM_TTL_DAYS,
1467
+ DailyLimitError,
1425
1468
  EXTERNAL_ID_RE,
1426
1469
  FALLBACK_LABEL_RE,
1427
1470
  ForbiddenError,
@@ -1449,6 +1492,7 @@ export {
1449
1492
  PARTNER_TOPUP_MAX_CENTS,
1450
1493
  PARTNER_TOPUP_MIN_CENTS,
1451
1494
  PARTNER_WEBHOOK_EVENTS,
1495
+ PARTNER_WELCOME_CENTS,
1452
1496
  PartnerApiError,
1453
1497
  PausedError,
1454
1498
  QuotaError,
@@ -1459,12 +1503,16 @@ export {
1459
1503
  STRICT_FIRST_DAYS,
1460
1504
  TAG_RE,
1461
1505
  TEMPLATE_VAR_NAME_RE,
1506
+ TENANT_DAILY_LIMIT_MAX,
1507
+ TENANT_DAILY_LIMIT_START,
1508
+ TENANT_WARMUP_STEP_DAYS,
1462
1509
  TimeoutError,
1463
1510
  UNSUBSCRIBE_URL_VAR,
1464
1511
  ValidationError,
1465
1512
  WEBHOOK_HEADERS,
1466
1513
  WEBHOOK_TOLERANCE_MS,
1467
1514
  createWebhookHandler,
1515
+ estimateSendDays,
1468
1516
  isValidExternalId,
1469
1517
  isValidFallbackLabel,
1470
1518
  partnerSignatureBase,
@@ -1477,5 +1525,6 @@ export {
1477
1525
  templateVarsProblem,
1478
1526
  verifyWebhook,
1479
1527
  verifyWebhookSignature,
1528
+ warmupLimit,
1480
1529
  webhookSignatureBase
1481
1530
  };
@@ -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,8 +82,9 @@ 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
89
  /** Sólo en `insufficient_balance`: saldo actual y lo que pedía el envío (centavos MXN). */
84
90
  balanceCents?: number;
@@ -112,7 +118,38 @@ export interface Tenant {
112
118
  lastSendAt: string | null;
113
119
  createdAt: string;
114
120
  updatedAt: string;
121
+ /** Límite diario con calentamiento. Ver `TenantSending`. */
122
+ sending: TenantSending;
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;
115
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;
116
153
  /** `PUT /tenants/:externalId` — crea o actualiza. 201 si lo creó, 200 si ya existía. */
117
154
  export interface PutTenantRequest {
118
155
  name?: string;
@@ -351,6 +388,11 @@ export interface BatchStatus {
351
388
  lastError: string | null;
352
389
  createdAt: string;
353
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;
354
396
  /**
355
397
  * Sólo en modo de prueba: el lote se valida y responde `completed` al instante (los suprimidos en
356
398
  * `skippedSuppressed`, el resto en `sent`), con `batchId` `test_…` que no se guarda (su GET da 404).
@@ -368,6 +410,8 @@ export interface TenantUsage {
368
410
  bounced: number;
369
411
  complained: number;
370
412
  unsubscribed: number;
413
+ /** Límite diario de hoy; falta si el tenant ya se borró. */
414
+ sending?: TenantSending;
371
415
  }
372
416
  /** `GET /usage?month=YYYY-MM` (default: el mes en curso, UTC). Activo = al menos un envío en el mes. */
373
417
  export interface UsageResponse {
@@ -387,6 +431,8 @@ export declare const PARTNER_PRICE_PER_EMAIL_CENTS = 6;
387
431
  * mes (UTC), una sola vez, así que un tenant dormido no cuesta nada.
388
432
  */
389
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;
390
436
  /** Recarga mínima y máxima por pago. */
391
437
  export declare const PARTNER_TOPUP_MIN_CENTS = 50000;
392
438
  export declare const PARTNER_TOPUP_MAX_CENTS = 10000000;
@@ -410,12 +456,8 @@ export interface BalanceMovement {
410
456
  /** `GET /balance` */
411
457
  export interface BalanceResponse {
412
458
  currency: "MXN";
459
+ /** Sin saldo suficiente el envío se rechaza (402 `insufficient_balance`) y el lote espera. Siempre. */
413
460
  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
461
  prices: {
420
462
  perEmailCents: number;
421
463
  tenantMonthCents: number;
@@ -429,7 +471,7 @@ export interface BalanceResponse {
429
471
  lowBalanceThresholdCents: number | null;
430
472
  /** Mes en curso (UTC). */
431
473
  month: string;
432
- /** Tenants a los que ya se les cobró la cuota del mes. */
474
+ /** Tenants que enviaron este mes (la misma cuenta que `/usage`): cada uno paga la cuota del mes. */
433
475
  activeTenants: number;
434
476
  /** Correos cobrados en el mes (sin los devueltos). */
435
477
  monthEmails: number;
@@ -466,11 +508,24 @@ export interface TopupResponse {
466
508
  * `GET /api/partners/:partnerId/balance` (dueño o MailMask): el saldo, sus últimos 20 movimientos,
467
509
  * el partner (con `tenants` y `ownerEmail`) y si quien mira es de MailMask (ve abonar y el cobro).
468
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
+ }
469
519
  export type PartnerBalanceDetail = BalanceResponse & {
470
520
  movements: BalanceMovement[];
471
521
  partner: PartnerSummary;
522
+ tenants: PartnerDetailTenant[];
472
523
  staff: boolean;
473
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
+ }
474
529
  /** `POST /api/partners/:partnerId/balance/adjust` — sólo MailMask (admin): abono o cargo manual. */
475
530
  export interface AdjustBalanceRequest {
476
531
  /** Centavos MXN, distinto de 0 (negativo = cargo). */
@@ -479,10 +534,6 @@ export interface AdjustBalanceRequest {
479
534
  /** Repetir con la misma llave no vuelve a abonar. */
480
535
  idempotencyKey?: string;
481
536
  }
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
537
  /** `GET /webhook` */
487
538
  export interface WebhookConfig {
488
539
  url: string | null;
@@ -516,7 +567,7 @@ export interface RotateWebhookSecretResponse {
516
567
  export interface TestWebhookResponse {
517
568
  deliveryId: string;
518
569
  }
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"];
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"];
520
571
  export type PartnerWebhookEvent = typeof PARTNER_WEBHOOK_EVENTS[number];
521
572
  /** Headers de cada entrega al webhook del partner (los mismos que los webhooks de dominio). */
522
573
  export declare const WEBHOOK_HEADERS: {
@@ -600,6 +651,14 @@ export interface PartnerEventData {
600
651
  thresholdCents: number;
601
652
  lastTopupCents: number;
602
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
+ };
603
662
  ping: Record<string, never>;
604
663
  }
605
664
  /** Sobre de cada evento. `externalId` es null en `ping` y `balance.low`. */
@@ -623,6 +682,7 @@ export declare const STRICT_FIRST_DAYS = 30;
623
682
  export interface PartnerSummary {
624
683
  id: string;
625
684
  name: string;
685
+ /** `sendsPerTenantMonth` ya no frena (0.8): el límite es diario por tenant, ver `TenantSending`. */
626
686
  plan: {
627
687
  includedTenants: number;
628
688
  sendsPerTenantMonth: number;
@@ -636,9 +696,8 @@ export interface PartnerSummary {
636
696
  effectiveReputationPolicy: ReputationPolicy;
637
697
  /** Zona de respaldo (`marketing.denik.me`); null = sin zona. Ver `FallbackZone`. */
638
698
  fallbackZone: string | null;
639
- /** Saldo prepagado (centavos MXN) y si ya frena envíos. Ver `BalanceResponse`. */
699
+ /** Saldo prepagado (centavos MXN). Ver `BalanceResponse`. */
640
700
  balanceCents: number;
641
- prepaidEnforced: boolean;
642
701
  /** Sólo en `GET /api/partners?all=1` (MailMask, admin): tenants totales y activos del mes. */
643
702
  tenants?: {
644
703
  total: number;
@@ -829,16 +888,16 @@ export declare const PARTNER_ADMIN_ROUTES: {
829
888
  readonly method: "POST";
830
889
  readonly path: "/api/partners/:partnerId/balance/adjust";
831
890
  };
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
891
  /** Recarga con MercadoPago desde la sesión del dueño (misma respuesta que `POST /topup`). */
838
892
  readonly topup: {
839
893
  readonly method: "POST";
840
894
  readonly path: "/api/partners/:partnerId/topup";
841
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
+ };
842
901
  readonly listCredentials: {
843
902
  readonly method: "GET";
844
903
  readonly path: "/api/partners/:partnerId/credentials";
package/dist/partner.d.ts CHANGED
@@ -6,7 +6,7 @@ export * from "./partner-contract.js";
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,8 +101,17 @@ 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` y `requiredCents`.
106
115
  */
107
116
  export declare class InsufficientBalanceError extends PartnerApiError {
108
117
  name: string;
@@ -373,6 +382,8 @@ export declare class MailMaskPartnerAdmin {
373
382
  setReputationPolicy(partnerId: string, policy: ReputationPolicy | null): Promise<PartnerSummary>;
374
383
  /** Sólo MailMask (admin): levanta cualquier pausa, también la de reputación. */
375
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>;
376
387
  /**
377
388
  * Sólo MailMask (admin): crea o adopta la hosted zone de la zona de respaldo (`marketing.denik.me`)
378
389
  * y devuelve los NS que el partner tiene que delegar. Idempotente con la misma zona.
@@ -384,8 +395,6 @@ export declare class MailMaskPartnerAdmin {
384
395
  getBalance(partnerId: string): Promise<PartnerBalanceDetail>;
385
396
  /** Sólo MailMask (admin): abono (o cargo, negativo) manual. Con la misma `idempotencyKey` no se repite. */
386
397
  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
398
  /** Liga de MercadoPago para recargar, con la llave del dueño. */
390
399
  topup(partnerId: string, input: TopupRequest): Promise<TopupResponse>;
391
400
  listCredentials(partnerId: string): Promise<ListCredentialsResponse>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@easybits.cloud/mailmask",
3
- "version": "0.7.0",
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",