mailchannels-sdk 1.0.0 → 1.2.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.
@@ -2,7 +2,7 @@ import { $fetch } from "ofetch";
2
2
  import { subtle } from "node:crypto";
3
3
  import { Buffer } from "node:buffer";
4
4
  import mime from "mime";
5
- var version = "1.0.0";
5
+ var version = "1.2.0";
6
6
  var MailChannelsClient = class MailChannelsClient {
7
7
  static DEFAULT_BASE_URL = "https://api.mailchannels.net";
8
8
  static DEFAULT_TIMEOUT = 12e4;
@@ -79,21 +79,26 @@ const STATUS_ERROR_TYPE_MAP = {
79
79
  [429]: "rate_limit_error",
80
80
  [500]: "internal_server_error"
81
81
  };
82
- const createError = (message, statusCode = null, type) => {
82
+ const createError = (message, statusCode = null, type, response = null) => {
83
83
  return {
84
84
  message,
85
85
  statusCode,
86
- type
86
+ type,
87
+ response
87
88
  };
88
89
  };
89
90
  const getStatusError = (response, errors = {}) => {
90
91
  const statusText = errors[response.status] || "Unknown error.";
91
92
  const payload = response._data ?? response.data;
92
93
  let details;
94
+ let errorResponse = null;
93
95
  if (typeof payload === "string") details = payload;
94
- else if (payload?.message) details = payload.message;
95
- else if (Array.isArray(payload?.errors) && payload.errors?.length) details = payload.errors.join(", ");
96
- return createError(details ? `${statusText} ${details}` : statusText, response.status ?? null, STATUS_ERROR_TYPE_MAP[response.status] || "api_error");
96
+ else if (typeof payload === "object" && payload !== null) {
97
+ if (typeof payload.message === "string") details = payload.message;
98
+ else if (Array.isArray(payload.errors) && payload.errors.length) details = payload.errors.join(", ");
99
+ errorResponse = payload;
100
+ }
101
+ return createError(details ? `${statusText} ${details}` : statusText, response.status ?? null, STATUS_ERROR_TYPE_MAP[response.status] || "api_error", errorResponse);
97
102
  };
98
103
  const getResultError = (e, fallback) => {
99
104
  return createError(e instanceof Error ? e.message : fallback, null, "application_error");
@@ -107,6 +112,12 @@ const validatePagination = (pagination = {}) => {
107
112
  if (typeof offset === "number" && offset < 0) return createValidationError("Offset must be greater than or equal to 0.");
108
113
  return null;
109
114
  };
115
+ const CUSTOM_TRACKING_NAME_PATTERN = /^[a-z0-9-]+$/;
116
+ const validateCustomTrackingName = (name) => {
117
+ if (!name || name.length < 1 || name.length > 64) return createValidationError("The custom tracking domain name must be between 1 and 64 characters.");
118
+ if (!CUSTOM_TRACKING_NAME_PATTERN.test(name)) return createValidationError("The custom tracking domain name must match ^[a-z0-9-]+$");
119
+ return null;
120
+ };
110
121
  const clean = (data) => {
111
122
  if (Array.isArray(data)) {
112
123
  const result = [];
@@ -160,7 +171,7 @@ const parseArrayRecipients = (recipients) => {
160
171
  return filtered.length > 0 ? filtered : void 0;
161
172
  };
162
173
  const stripPemHeaders = (pem) => pem.replace(/-----[^-]+-----|\s|#.*$/gm, "");
163
- const RESERVED_HEADER_NAMES = new Set([
174
+ const RESERVED_HEADER_NAMES = /* @__PURE__ */ new Set([
164
175
  "authentication-results",
165
176
  "bcc",
166
177
  "cc",
@@ -305,6 +316,18 @@ const buildSendPayload = async (options) => {
305
316
  return getRecipientCount(personalization.to) + getRecipientCount(personalization.cc) + getRecipientCount(personalization.bcc) !== 1;
306
317
  })) return "Non-transactional messages must have exactly one recipient per personalization.";
307
318
  }
319
+ if (options.tracking?.click?.customDomainName !== void 0) {
320
+ const clickCustomTrackingNameError = validateCustomTrackingName(options.tracking.click.customDomainName);
321
+ if (clickCustomTrackingNameError) return `Invalid click tracking: ${clickCustomTrackingNameError.message}`;
322
+ }
323
+ if (options.tracking?.open?.customDomainName !== void 0) {
324
+ const openCustomTrackingNameError = validateCustomTrackingName(options.tracking.open.customDomainName);
325
+ if (openCustomTrackingNameError) return `Invalid open tracking: ${openCustomTrackingNameError.message}`;
326
+ }
327
+ if (options.unsubscribe?.customDomainName !== void 0) {
328
+ const unsubscribeCustomTrackingNameError = validateCustomTrackingName(options.unsubscribe.customDomainName);
329
+ if (unsubscribeCustomTrackingNameError) return `Invalid unsubscribe settings: ${unsubscribeCustomTrackingNameError.message}`;
330
+ }
308
331
  const content = [];
309
332
  const template_type = options.template?.type;
310
333
  if (text) content.push({
@@ -335,11 +358,18 @@ const buildSendPayload = async (options) => {
335
358
  from: parsedFrom,
336
359
  subject: options.subject,
337
360
  content,
338
- tracking_settings: options.tracking ? {
339
- click_tracking: options.tracking.click ? { enable: options.tracking.click.enable } : void 0,
340
- open_tracking: options.tracking.open ? { enable: options.tracking.open.enable } : void 0
341
- } : void 0,
342
- transactional: options.transactional
361
+ tracking_settings: options.tracking && {
362
+ click_tracking: options.tracking.click && {
363
+ custom_domain_name: options.tracking.click.customDomainName,
364
+ enable: options.tracking.click.enable
365
+ },
366
+ open_tracking: options.tracking.open && {
367
+ custom_domain_name: options.tracking.open.customDomainName,
368
+ enable: options.tracking.open.enable
369
+ }
370
+ },
371
+ transactional: options.transactional,
372
+ unsubscribe_settings: options.unsubscribe && { custom_domain_name: options.unsubscribe.customDomainName }
343
373
  };
344
374
  };
345
375
  var Emails = class {
@@ -424,80 +454,6 @@ const mapDkimKey = (key) => ({
424
454
  status: key.status,
425
455
  statusModifiedAt: key.status_modified_at
426
456
  });
427
- var Domains = class {
428
- mailchannels;
429
- dkim;
430
- constructor(mailchannels) {
431
- this.mailchannels = mailchannels;
432
- this.dkim = new DomainsDkim(mailchannels);
433
- }
434
- async check(domain, options) {
435
- let error = null;
436
- if (!domain) {
437
- error = createValidationError("No domain provided.");
438
- return {
439
- data: null,
440
- error
441
- };
442
- }
443
- const dkimOptions = options?.dkim ? Array.isArray(options.dkim) ? options.dkim : [options.dkim] : void 0;
444
- if (dkimOptions && dkimOptions.length > 10) {
445
- error = createValidationError("A maximum of 10 DKIM settings can be provided.");
446
- return {
447
- data: null,
448
- error
449
- };
450
- }
451
- if (dkimOptions?.find(({ privateKey, selector }) => privateKey && !selector)) {
452
- error = createValidationError("DKIM settings with a privateKey must also include a selector.");
453
- return {
454
- data: null,
455
- error
456
- };
457
- }
458
- const payload = {
459
- dkim_settings: dkimOptions?.map((dkim) => ({
460
- dkim_domain: dkim.domain,
461
- dkim_private_key: dkim.privateKey ? stripPemHeaders(dkim.privateKey) : void 0,
462
- dkim_selector: dkim.selector
463
- })),
464
- domain,
465
- sender_id: options?.senderId
466
- };
467
- const response = await this.mailchannels.post("/tx/v1/check-domain", {
468
- body: payload,
469
- onResponseError: async ({ response }) => {
470
- error = getStatusError(response, {
471
- [400]: "Bad Request.",
472
- [403]: "User does not have access to this feature."
473
- });
474
- }
475
- }).catch((e) => {
476
- error ||= getResultError(e, "Failed to check domain.");
477
- return null;
478
- });
479
- if (!response) return {
480
- data: null,
481
- error
482
- };
483
- return {
484
- data: clean({
485
- dkim: response.check_results.dkim.map((dkimResults) => ({
486
- domain: dkimResults.dkim_domain,
487
- keyStatus: dkimResults.dkim_key_status,
488
- selector: dkimResults.dkim_selector,
489
- reason: dkimResults.reason,
490
- verdict: dkimResults.verdict
491
- })),
492
- domainLockdown: response.check_results.domain_lockdown,
493
- senderDomain: response.check_results.sender_domain,
494
- spf: response.check_results.spf,
495
- references: response.references
496
- }),
497
- error: null
498
- };
499
- }
500
- };
501
457
  var DomainsDkim = class {
502
458
  mailchannels;
503
459
  constructor(mailchannels) {
@@ -524,7 +480,7 @@ var DomainsDkim = class {
524
480
  key_length: options.length,
525
481
  selector: options.selector
526
482
  };
527
- const response = await this.mailchannels.post(`/tx/v1/domains/${domain}/dkim-keys`, {
483
+ const response = await this.mailchannels.post(`/tx/v1/domains/${encodeURIComponent(domain)}/dkim-keys`, {
528
484
  body: payload,
529
485
  onResponseError: async ({ response }) => {
530
486
  error = getStatusError(response, {
@@ -576,7 +532,7 @@ var DomainsDkim = class {
576
532
  limit: options?.limit,
577
533
  include_dns_record: options?.includeDnsRecord
578
534
  };
579
- const response = await this.mailchannels.get(`/tx/v1/domains/${domain}/dkim-keys`, {
535
+ const response = await this.mailchannels.get(`/tx/v1/domains/${encodeURIComponent(domain)}/dkim-keys`, {
580
536
  query: payload,
581
537
  onResponseError: async ({ response }) => {
582
538
  error = getStatusError(response, { [400]: "Bad Request." });
@@ -611,7 +567,7 @@ var DomainsDkim = class {
611
567
  };
612
568
  }
613
569
  const payload = { status: options.status };
614
- await this.mailchannels.patch(`/tx/v1/domains/${domain}/dkim-keys/${options.selector}`, {
570
+ await this.mailchannels.patch(`/tx/v1/domains/${encodeURIComponent(domain)}/dkim-keys/${encodeURIComponent(options.selector)}`, {
615
571
  body: payload,
616
572
  onResponseError: async ({ response }) => {
617
573
  error = getStatusError(response, {
@@ -651,7 +607,7 @@ var DomainsDkim = class {
651
607
  };
652
608
  }
653
609
  const payload = { new_key: { selector: options.newKey.selector } };
654
- const response = await this.mailchannels.post(`/tx/v1/domains/${domain}/dkim-keys/${selector}/rotate`, {
610
+ const response = await this.mailchannels.post(`/tx/v1/domains/${encodeURIComponent(domain)}/dkim-keys/${encodeURIComponent(selector)}/rotate`, {
655
611
  body: payload,
656
612
  onResponseError: async ({ response }) => {
657
613
  error = getStatusError(response, {
@@ -677,118 +633,33 @@ var DomainsDkim = class {
677
633
  };
678
634
  }
679
635
  };
680
- const HMAC_SHA256 = {
681
- name: "HMAC",
682
- hash: "SHA-256"
683
- };
684
- const ED25519 = {
685
- name: "Ed25519",
686
- namedCurve: "Ed25519"
687
- };
688
- const encoder = new TextEncoder();
689
- const HEADER_CONTENT_DIGEST = "content-digest";
690
- const HEADER_SIGNATURE = "signature";
691
- const HEADER_SIGNATURE_INPUT = "signature-input";
692
- const signingKeyCache = /* @__PURE__ */ new Map();
693
- const validateContentDigest = async (header, body) => {
694
- const match = header.match(/^(.*?)=:(.*?):$/);
695
- if (!match) return false;
696
- const [, algorithm, hash] = match;
697
- if (!algorithm || !hash) return false;
698
- const normalizedAlgorithm = algorithm.replace("-", "").toLowerCase();
699
- if (!["sha256"].includes(normalizedAlgorithm)) return false;
700
- const signatureBuffer = await subtle.digest(HMAC_SHA256.hash, encoder.encode(body));
701
- return Buffer.from(signatureBuffer).toString("base64") === hash;
702
- };
703
- const extractSignature = (signatureHeader) => {
704
- const signatureMatch = signatureHeader.match(/sig_\d+=:([^:]+):/);
705
- return signatureMatch && signatureMatch[1] ? signatureMatch[1] : null;
706
- };
707
- const extractInputValues = (header) => {
708
- const match = header.match(/^(\w+)=\(([^)]+)\);created=(\d+);alg="([^"]+)";keyid="([^"]+)"$/);
709
- if (!match) return null;
710
- return {
711
- name: match[1],
712
- timestamp: Number.parseInt(match[3], 10),
713
- algorithm: match[4],
714
- keyId: match[5]
715
- };
716
- };
717
- async function isValidWebhook(options) {
718
- const { payload, headers, cache = true } = options;
719
- const contentDigest = headers[HEADER_CONTENT_DIGEST];
720
- const messageSignature = headers[HEADER_SIGNATURE];
721
- const signatureInput = headers[HEADER_SIGNATURE_INPUT];
722
- if (!payload || !contentDigest || !messageSignature || !signatureInput || !await validateContentDigest(contentDigest, payload)) return false;
723
- const signature = extractSignature(messageSignature);
724
- if (!signature) return false;
725
- const values = extractInputValues(signatureInput);
726
- if (!values) return false;
727
- if (Math.floor(Date.now() / 1e3) - values.timestamp > 300) return false;
728
- const signingString = `"content-digest": ${contentDigest}
729
- "@signature-params": ("content-digest");created=${values.timestamp};alg="${values.algorithm}";keyid="${values.keyId}"`;
730
- let publicKey = options.publicKey;
731
- if (!publicKey && values.keyId) {
732
- if (cache) publicKey = signingKeyCache.get(values.keyId);
733
- if (!publicKey) {
734
- const publicKeyResponse = await $fetch("/tx/v1/webhook/public-key", {
735
- baseURL: "https://api.mailchannels.net",
736
- query: { id: values.keyId }
737
- }).catch(() => null);
738
- if (!publicKeyResponse) return false;
739
- publicKey = publicKeyResponse.key;
740
- if (cache && publicKey) signingKeyCache.set(values.keyId, publicKey);
741
- }
742
- }
743
- if (!publicKey) return false;
744
- publicKey = stripPemHeaders(publicKey);
745
- const encoding = "base64";
746
- const format = "spki";
747
- const publicKeyBuffer = Buffer.from(publicKey, encoding);
748
- const webhookSignatureBuffer = Buffer.from(signature, encoding);
749
- const key = await subtle.importKey(format, publicKeyBuffer, ED25519, false, ["verify"]);
750
- return subtle.verify(ED25519.name, key, webhookSignatureBuffer, encoder.encode(signingString));
751
- }
752
- var Webhooks = class Webhooks {
636
+ var DomainsCustomTracking = class DomainsCustomTracking {
753
637
  mailchannels;
638
+ static SCOPE_VALUES = /* @__PURE__ */ new Set([
639
+ "click",
640
+ "open",
641
+ "unsubscribe"
642
+ ]);
754
643
  constructor(mailchannels) {
755
644
  this.mailchannels = mailchannels;
756
645
  }
757
- async create(endpoint) {
646
+ async list(options) {
758
647
  let error = null;
759
- if (!endpoint) {
760
- error = createValidationError("No endpoint provided.");
761
- return {
762
- success: false,
763
- error
764
- };
765
- }
766
- if (endpoint.length > 8e3) {
767
- error = createValidationError("The endpoint exceeds the maximum length of 8000 characters.");
768
- return {
769
- success: false,
770
- error
771
- };
772
- }
773
- await this.mailchannels.post("/tx/v1/webhook", {
774
- query: { endpoint },
775
- onResponseError: async ({ response }) => {
776
- error = getStatusError(response, { [409]: `Endpoint '${endpoint}' is already enrolled to receive notifications.` });
777
- }
778
- }).catch((e) => {
779
- error ||= getResultError(e, "Failed to create webhook.");
648
+ error = validatePagination({
649
+ ...options,
650
+ max: 1e3
780
651
  });
781
- return {
782
- success: !error,
652
+ if (error) return {
653
+ data: null,
783
654
  error
784
655
  };
785
- }
786
- async list() {
787
- let error = null;
788
- const response = await this.mailchannels.get("/tx/v1/webhook", { onResponseError: async ({ response }) => {
789
- error = getStatusError(response);
790
- } }).catch((e) => {
791
- error ||= getResultError(e, "Failed to fetch webhooks.");
656
+ const response = await this.mailchannels.get("/tx/v1/custom-tracking-domains", {
657
+ query: options,
658
+ onResponseError: async ({ response }) => {
659
+ error = getStatusError(response, { [400]: "Bad Request." });
660
+ }
661
+ }).catch((e) => {
662
+ error ||= getResultError(e, "Failed to fetch custom tracking domains.");
792
663
  return null;
793
664
  });
794
665
  if (!response) return {
@@ -796,68 +667,489 @@ var Webhooks = class Webhooks {
796
667
  error
797
668
  };
798
669
  return {
799
- data: clean(response.map((webhooks) => ({ webhook: webhooks.webhook }))),
670
+ data: clean({
671
+ customTrackingDomains: response.custom_tracking_domains.map((d) => ({
672
+ name: d.name,
673
+ hostname: d.hostname,
674
+ scope: d.scope,
675
+ status: d.status,
676
+ createdAt: d.created_at
677
+ })),
678
+ total: response.total
679
+ }),
800
680
  error: null
801
681
  };
802
682
  }
803
- async deleteAll() {
683
+ async create(name, hostname, scope) {
804
684
  let error = null;
805
- await this.mailchannels.delete("/tx/v1/webhook", { onResponseError: async ({ response }) => {
806
- error = getStatusError(response);
807
- } }).catch((e) => {
808
- error ||= getResultError(e, "Failed to delete webhooks.");
809
- });
810
- return {
811
- success: !error,
685
+ error = validateCustomTrackingName(name);
686
+ if (error) return {
687
+ data: null,
812
688
  error
813
689
  };
814
- }
815
- async getSigningKey(id) {
816
- let error = null;
817
- const response = await this.mailchannels.get("/tx/v1/webhook/public-key", {
818
- query: { id },
690
+ if (!hostname) {
691
+ error = createValidationError("Hostname is required.");
692
+ return {
693
+ data: null,
694
+ error
695
+ };
696
+ }
697
+ if (!scope || !DomainsCustomTracking.SCOPE_VALUES.has(scope)) {
698
+ error = createValidationError("Scope must be one of 'click', 'open', or 'unsubscribe'.");
699
+ return {
700
+ data: null,
701
+ error
702
+ };
703
+ }
704
+ const payload = {
705
+ name,
706
+ hostname,
707
+ scope
708
+ };
709
+ let statusCode = null;
710
+ const response = await this.mailchannels.post("/tx/v1/custom-tracking-domains", {
711
+ body: payload,
712
+ onResponse: async ({ response }) => {
713
+ statusCode = response.status;
714
+ },
819
715
  onResponseError: async ({ response }) => {
820
716
  error = getStatusError(response, {
821
- [400]: "Bad Request.",
822
- [404]: `The key '${id}' is not found.`
717
+ [400]: "Invalid request body.",
718
+ [403]: "No permission to register this domain.",
719
+ [409]: "A domain with the same name already exists, or the hostname and scope combination is already registered.",
720
+ [422]: "DNS verification incomplete. Either the TXT ownership record has not propagated yet or the hostname CNAME does not point to the required target."
823
721
  });
824
722
  }
825
723
  }).catch((e) => {
826
- error ||= getResultError(e, "Failed to get signing key.");
724
+ error ||= getResultError(e, "Failed to create custom tracking domain.");
827
725
  return null;
828
726
  });
829
727
  if (!response) return {
830
728
  data: null,
831
729
  error
832
730
  };
731
+ if (statusCode === 202) {
732
+ const dnsSetupRequiredResponse = response;
733
+ return {
734
+ data: clean({
735
+ dnsSetupRequired: true,
736
+ token: dnsSetupRequiredResponse.token,
737
+ txtRecordName: dnsSetupRequiredResponse.txt_record_name,
738
+ txtRecordValue: dnsSetupRequiredResponse.txt_record_value,
739
+ instructions: dnsSetupRequiredResponse.instructions
740
+ }),
741
+ error: null
742
+ };
743
+ }
744
+ const createResponse = response;
833
745
  return {
834
746
  data: clean({
835
- id: response.id,
836
- key: response.key
747
+ dnsSetupRequired: false,
748
+ name: createResponse.name,
749
+ hostname: createResponse.hostname,
750
+ scope: createResponse.scope,
751
+ status: createResponse.status,
752
+ createdAt: createResponse.created_at
837
753
  }),
838
754
  error: null
839
755
  };
840
756
  }
841
- async validate(requestId) {
757
+ async update(hostname, scope, options) {
842
758
  let error = null;
843
- if (requestId && requestId.length > 28) {
844
- error = createValidationError("The request id should not exceed 28 characters.");
759
+ if (!hostname) {
760
+ error = createValidationError("Hostname is required.");
845
761
  return {
846
762
  data: null,
847
763
  error
848
764
  };
849
765
  }
850
- const response = await this.mailchannels.post("/tx/v1/webhook/validate", {
851
- body: { request_id: requestId },
852
- onResponseError: async ({ response }) => {
853
- error = getStatusError(response, {
854
- [400]: "Bad Request.",
855
- [404]: "No webhooks found for the account."
856
- });
857
- }
858
- }).catch((e) => {
859
- error ||= getResultError(e, "Failed to validate webhooks.");
860
- return null;
766
+ if (!scope || !DomainsCustomTracking.SCOPE_VALUES.has(scope)) {
767
+ error = createValidationError("Scope must be one of 'click', 'open', or 'unsubscribe'.");
768
+ return {
769
+ data: null,
770
+ error
771
+ };
772
+ }
773
+ if (options.name !== void 0) {
774
+ error = validateCustomTrackingName(options.name);
775
+ if (error) return {
776
+ data: null,
777
+ error
778
+ };
779
+ }
780
+ if (options.name === void 0 && options.status === void 0) {
781
+ error = createValidationError("At least one of 'name' or 'status' must be provided.");
782
+ return {
783
+ data: null,
784
+ error
785
+ };
786
+ }
787
+ const payload = {
788
+ name: options.name,
789
+ status: options.status
790
+ };
791
+ let statusCode = null;
792
+ const response = await this.mailchannels.patch(`/tx/v1/custom-tracking-domains/${encodeURIComponent(hostname)}/${encodeURIComponent(scope)}`, {
793
+ body: payload,
794
+ onResponse: async ({ response }) => {
795
+ statusCode = response.status;
796
+ },
797
+ onResponseError: async ({ response }) => {
798
+ error = getStatusError(response, {
799
+ [400]: "Bad Request.",
800
+ [403]: "No permission to update this domain.",
801
+ [404]: `Custom tracking domain for hostname '${hostname}' and scope '${scope}' not found.`,
802
+ [409]: "Name already used by another domain.",
803
+ [422]: "DNS verification incomplete. Either the TXT ownership record has not propagated yet or the hostname CNAME does not point to the required target."
804
+ });
805
+ }
806
+ }).catch((e) => {
807
+ error ||= getResultError(e, "Failed to update custom tracking domain.");
808
+ return null;
809
+ });
810
+ if (!response) return {
811
+ data: null,
812
+ error
813
+ };
814
+ if (statusCode === 202) {
815
+ const dnsSetupRequiredResponse = response;
816
+ return {
817
+ data: clean({
818
+ dnsSetupRequired: true,
819
+ token: dnsSetupRequiredResponse.token,
820
+ txtRecordName: dnsSetupRequiredResponse.txt_record_name,
821
+ txtRecordValue: dnsSetupRequiredResponse.txt_record_value,
822
+ instructions: dnsSetupRequiredResponse.instructions
823
+ }),
824
+ error: null
825
+ };
826
+ }
827
+ const updateResponse = response;
828
+ return {
829
+ data: clean({
830
+ dnsSetupRequired: false,
831
+ name: updateResponse.name,
832
+ hostname: updateResponse.hostname,
833
+ scope: updateResponse.scope,
834
+ status: updateResponse.status,
835
+ createdAt: updateResponse.created_at
836
+ }),
837
+ error: null
838
+ };
839
+ }
840
+ async delete(hostname, scope) {
841
+ let error = null;
842
+ if (!hostname) {
843
+ error = createValidationError("Hostname is required.");
844
+ return {
845
+ success: false,
846
+ error
847
+ };
848
+ }
849
+ if (!scope || !DomainsCustomTracking.SCOPE_VALUES.has(scope)) {
850
+ error = createValidationError("Scope must be one of 'click', 'open', or 'unsubscribe'.");
851
+ return {
852
+ success: false,
853
+ error
854
+ };
855
+ }
856
+ await this.mailchannels.delete(`/tx/v1/custom-tracking-domains/${encodeURIComponent(hostname)}/${encodeURIComponent(scope)}`, { onResponseError: async ({ response }) => {
857
+ error = getStatusError(response, { [400]: "Invalid hostname or scope value" });
858
+ } }).catch((e) => {
859
+ error ||= getResultError(e, "Failed to delete custom tracking domain.");
860
+ });
861
+ return {
862
+ success: !error,
863
+ error
864
+ };
865
+ }
866
+ };
867
+ var Domains = class {
868
+ mailchannels;
869
+ dkim;
870
+ customTracking;
871
+ constructor(mailchannels) {
872
+ this.mailchannels = mailchannels;
873
+ this.dkim = new DomainsDkim(mailchannels);
874
+ this.customTracking = new DomainsCustomTracking(mailchannels);
875
+ }
876
+ async check(domain, options) {
877
+ let error = null;
878
+ if (!domain) {
879
+ error = createValidationError("No domain provided.");
880
+ return {
881
+ data: null,
882
+ error
883
+ };
884
+ }
885
+ const dkimOptions = options?.dkim ? Array.isArray(options.dkim) ? options.dkim : [options.dkim] : void 0;
886
+ if (dkimOptions && dkimOptions.length > 10) {
887
+ error = createValidationError("A maximum of 10 DKIM settings can be provided.");
888
+ return {
889
+ data: null,
890
+ error
891
+ };
892
+ }
893
+ if (dkimOptions?.find(({ privateKey, selector }) => privateKey && !selector)) {
894
+ error = createValidationError("DKIM settings with a privateKey must also include a selector.");
895
+ return {
896
+ data: null,
897
+ error
898
+ };
899
+ }
900
+ const payload = {
901
+ dkim_settings: dkimOptions?.map((dkim) => ({
902
+ dkim_domain: dkim.domain,
903
+ dkim_private_key: dkim.privateKey ? stripPemHeaders(dkim.privateKey) : void 0,
904
+ dkim_selector: dkim.selector
905
+ })),
906
+ domain,
907
+ sender_id: options?.senderId
908
+ };
909
+ const response = await this.mailchannels.post("/tx/v1/check-domain", {
910
+ body: payload,
911
+ onResponseError: async ({ response }) => {
912
+ error = getStatusError(response, {
913
+ [400]: "Bad Request.",
914
+ [403]: "User does not have access to this feature."
915
+ });
916
+ }
917
+ }).catch((e) => {
918
+ error ||= getResultError(e, "Failed to check domain.");
919
+ return null;
920
+ });
921
+ if (!response) return {
922
+ data: null,
923
+ error
924
+ };
925
+ return {
926
+ data: clean({
927
+ dkim: response.check_results.dkim.map((dkimResults) => ({
928
+ domain: dkimResults.dkim_domain,
929
+ keyStatus: dkimResults.dkim_key_status,
930
+ selector: dkimResults.dkim_selector,
931
+ reason: dkimResults.reason,
932
+ verdict: dkimResults.verdict
933
+ })),
934
+ domainLockdown: response.check_results.domain_lockdown,
935
+ senderDomain: response.check_results.sender_domain,
936
+ spf: response.check_results.spf,
937
+ references: response.references
938
+ }),
939
+ error: null
940
+ };
941
+ }
942
+ };
943
+ const HMAC_SHA256 = {
944
+ name: "HMAC",
945
+ hash: "SHA-256"
946
+ };
947
+ const ED25519 = {
948
+ name: "Ed25519",
949
+ namedCurve: "Ed25519"
950
+ };
951
+ const encoder = new TextEncoder();
952
+ const HEADER_CONTENT_DIGEST = "content-digest";
953
+ const HEADER_SIGNATURE = "signature";
954
+ const HEADER_SIGNATURE_INPUT = "signature-input";
955
+ const signingKeyCache = /* @__PURE__ */ new Map();
956
+ const validateContentDigest = async (header, body) => {
957
+ const match = header.match(/^(.*?)=:(.*?):$/);
958
+ if (!match) return false;
959
+ const [, algorithm, hash] = match;
960
+ if (!algorithm || !hash) return false;
961
+ const normalizedAlgorithm = algorithm.replace("-", "").toLowerCase();
962
+ if (!["sha256"].includes(normalizedAlgorithm)) return false;
963
+ const signatureBuffer = await subtle.digest(HMAC_SHA256.hash, encoder.encode(body));
964
+ return Buffer.from(signatureBuffer).toString("base64") === hash;
965
+ };
966
+ const extractSignature = (signatureHeader) => {
967
+ const signatureMatch = signatureHeader.match(/sig_\d+=:([^:]+):/);
968
+ return signatureMatch && signatureMatch[1] ? signatureMatch[1] : null;
969
+ };
970
+ const extractInputValues = (header) => {
971
+ const match = header.match(/^(\w+)=\(([^)]+)\);created=(\d+);alg="([^"]+)";keyid="([^"]+)"$/);
972
+ if (!match) return null;
973
+ return {
974
+ name: match[1],
975
+ timestamp: Number.parseInt(match[3], 10),
976
+ algorithm: match[4],
977
+ keyId: match[5]
978
+ };
979
+ };
980
+ async function isValidWebhook(options) {
981
+ const { payload, headers, cache = true } = options;
982
+ const contentDigest = headers[HEADER_CONTENT_DIGEST];
983
+ const messageSignature = headers[HEADER_SIGNATURE];
984
+ const signatureInput = headers[HEADER_SIGNATURE_INPUT];
985
+ if (!payload || !contentDigest || !messageSignature || !signatureInput || !await validateContentDigest(contentDigest, payload)) return false;
986
+ const signature = extractSignature(messageSignature);
987
+ if (!signature) return false;
988
+ const values = extractInputValues(signatureInput);
989
+ if (!values) return false;
990
+ if (Math.floor(Date.now() / 1e3) - values.timestamp > 300) return false;
991
+ const signingString = `"content-digest": ${contentDigest}
992
+ "@signature-params": ("content-digest");created=${values.timestamp};alg="${values.algorithm}";keyid="${values.keyId}"`;
993
+ let publicKey = options.publicKey;
994
+ if (!publicKey && values.keyId) {
995
+ if (cache) publicKey = signingKeyCache.get(values.keyId);
996
+ if (!publicKey) {
997
+ const publicKeyResponse = await $fetch("/tx/v1/webhook/public-key", {
998
+ baseURL: "https://api.mailchannels.net",
999
+ query: { id: values.keyId }
1000
+ }).catch(() => null);
1001
+ if (!publicKeyResponse) return false;
1002
+ publicKey = publicKeyResponse.key;
1003
+ if (cache && publicKey) signingKeyCache.set(values.keyId, publicKey);
1004
+ }
1005
+ }
1006
+ if (!publicKey) return false;
1007
+ publicKey = stripPemHeaders(publicKey);
1008
+ const encoding = "base64";
1009
+ const format = "spki";
1010
+ const publicKeyBuffer = Buffer.from(publicKey, encoding);
1011
+ const webhookSignatureBuffer = Buffer.from(signature, encoding);
1012
+ const key = await subtle.importKey(format, publicKeyBuffer, ED25519, false, ["verify"]);
1013
+ return subtle.verify(ED25519.name, key, webhookSignatureBuffer, encoder.encode(signingString));
1014
+ }
1015
+ const ISO_DATE_REGEX = /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})$/;
1016
+ const ISO_DATE_ONLY_REGEX = /^\d{4}-\d{2}-\d{2}$/;
1017
+ const formatDateInput = (value) => {
1018
+ if (value instanceof Date) {
1019
+ if (Number.isNaN(value.getTime())) return null;
1020
+ return value.toISOString();
1021
+ }
1022
+ if (typeof value === "string") {
1023
+ if (!ISO_DATE_REGEX.test(value) && !ISO_DATE_ONLY_REGEX.test(value) || Number.isNaN(Date.parse(value))) return null;
1024
+ if (ISO_DATE_REGEX.test(value)) return new Date(value).toISOString();
1025
+ }
1026
+ return value;
1027
+ };
1028
+ const parseDateInputs = (options) => {
1029
+ const dates = {};
1030
+ const keys = Object.keys(options);
1031
+ for (const key of keys) {
1032
+ const formatted = formatDateInput(options?.[key]);
1033
+ if (formatted === null) return {
1034
+ dates: null,
1035
+ error: createValidationError(`The '${key}' value is not a valid date.`)
1036
+ };
1037
+ dates[key] = formatted;
1038
+ }
1039
+ return {
1040
+ dates,
1041
+ error: null
1042
+ };
1043
+ };
1044
+ var Webhooks = class Webhooks {
1045
+ mailchannels;
1046
+ constructor(mailchannels) {
1047
+ this.mailchannels = mailchannels;
1048
+ }
1049
+ async create(endpoint) {
1050
+ let error = null;
1051
+ if (!endpoint) {
1052
+ error = createValidationError("No endpoint provided.");
1053
+ return {
1054
+ success: false,
1055
+ error
1056
+ };
1057
+ }
1058
+ if (endpoint.length > 8e3) {
1059
+ error = createValidationError("The endpoint exceeds the maximum length of 8000 characters.");
1060
+ return {
1061
+ success: false,
1062
+ error
1063
+ };
1064
+ }
1065
+ await this.mailchannels.post("/tx/v1/webhook", {
1066
+ query: { endpoint },
1067
+ onResponseError: async ({ response }) => {
1068
+ error = getStatusError(response, { [409]: `Endpoint '${endpoint}' is already enrolled to receive notifications.` });
1069
+ }
1070
+ }).catch((e) => {
1071
+ error ||= getResultError(e, "Failed to create webhook.");
1072
+ });
1073
+ return {
1074
+ success: !error,
1075
+ error
1076
+ };
1077
+ }
1078
+ async list() {
1079
+ let error = null;
1080
+ const response = await this.mailchannels.get("/tx/v1/webhook", { onResponseError: async ({ response }) => {
1081
+ error = getStatusError(response);
1082
+ } }).catch((e) => {
1083
+ error ||= getResultError(e, "Failed to fetch webhooks.");
1084
+ return null;
1085
+ });
1086
+ if (!response) return {
1087
+ data: null,
1088
+ error
1089
+ };
1090
+ return {
1091
+ data: clean(response.map((webhooks) => ({ webhook: webhooks.webhook }))),
1092
+ error: null
1093
+ };
1094
+ }
1095
+ async deleteAll() {
1096
+ let error = null;
1097
+ await this.mailchannels.delete("/tx/v1/webhook", { onResponseError: async ({ response }) => {
1098
+ error = getStatusError(response);
1099
+ } }).catch((e) => {
1100
+ error ||= getResultError(e, "Failed to delete webhooks.");
1101
+ });
1102
+ return {
1103
+ success: !error,
1104
+ error
1105
+ };
1106
+ }
1107
+ async getSigningKey(id) {
1108
+ let error = null;
1109
+ const response = await this.mailchannels.get("/tx/v1/webhook/public-key", {
1110
+ query: { id },
1111
+ onResponseError: async ({ response }) => {
1112
+ error = getStatusError(response, {
1113
+ [400]: "Bad Request.",
1114
+ [404]: `The key '${id}' is not found.`
1115
+ });
1116
+ }
1117
+ }).catch((e) => {
1118
+ error ||= getResultError(e, "Failed to get signing key.");
1119
+ return null;
1120
+ });
1121
+ if (!response) return {
1122
+ data: null,
1123
+ error
1124
+ };
1125
+ return {
1126
+ data: clean({
1127
+ id: response.id,
1128
+ key: response.key
1129
+ }),
1130
+ error: null
1131
+ };
1132
+ }
1133
+ async validate(requestId) {
1134
+ let error = null;
1135
+ if (requestId && requestId.length > 28) {
1136
+ error = createValidationError("The request id should not exceed 28 characters.");
1137
+ return {
1138
+ data: null,
1139
+ error
1140
+ };
1141
+ }
1142
+ const response = await this.mailchannels.post("/tx/v1/webhook/validate", {
1143
+ body: { request_id: requestId },
1144
+ onResponseError: async ({ response }) => {
1145
+ error = getStatusError(response, {
1146
+ [400]: "Bad Request.",
1147
+ [404]: "No webhooks found for the account."
1148
+ });
1149
+ }
1150
+ }).catch((e) => {
1151
+ error ||= getResultError(e, "Failed to validate webhooks.");
1152
+ return null;
861
1153
  });
862
1154
  if (!response) return {
863
1155
  data: null,
@@ -936,17 +1228,17 @@ var Webhooks = class Webhooks {
936
1228
  data: null,
937
1229
  error: createValidationError("Status filters must be unique.")
938
1230
  };
939
- if (options?.createdAfter && Number.isNaN(Date.parse(options.createdAfter))) return {
940
- data: null,
941
- error: createValidationError("createdAfter must be a valid date string.")
942
- };
943
- if (options?.createdBefore && Number.isNaN(Date.parse(options.createdBefore))) return {
1231
+ const { dates, error: dateError } = parseDateInputs({
1232
+ createdAfter: options?.createdAfter,
1233
+ createdBefore: options?.createdBefore
1234
+ });
1235
+ if (!dates || dateError) return {
944
1236
  data: null,
945
- error: createValidationError("createdBefore must be a valid date string.")
1237
+ error: dateError
946
1238
  };
947
- if (options?.createdAfter && options?.createdBefore) {
948
- const createdAfter = Date.parse(options.createdAfter);
949
- const createdBefore = Date.parse(options.createdBefore);
1239
+ if (dates.createdAfter && dates.createdBefore) {
1240
+ const createdAfter = Date.parse(dates.createdAfter);
1241
+ const createdBefore = Date.parse(dates.createdBefore);
950
1242
  const maxRangeMs = 744 * 60 * 60 * 1e3;
951
1243
  if (createdBefore <= createdAfter) return {
952
1244
  data: null,
@@ -959,8 +1251,8 @@ var Webhooks = class Webhooks {
959
1251
  }
960
1252
  const response = await this.mailchannels.get("/tx/v1/webhook-batch", {
961
1253
  query: {
962
- created_after: options?.createdAfter,
963
- created_before: options?.createdBefore,
1254
+ created_after: dates.createdAfter,
1255
+ created_before: dates.createdBefore,
964
1256
  statuses: options?.statuses,
965
1257
  webhook: options?.webhook,
966
1258
  limit: options?.limit,
@@ -972,200 +1264,60 @@ var Webhooks = class Webhooks {
972
1264
  }).catch((e) => {
973
1265
  error ||= getResultError(e, "Failed to fetch webhook batches.");
974
1266
  return null;
975
- });
976
- if (!response) return {
977
- data: null,
978
- error
979
- };
980
- return {
981
- data: clean(response.webhook_batches.map((batch) => ({
982
- batchId: batch.batch_id,
983
- createdAt: batch.created_at,
984
- customerHandle: batch.customer_handle,
985
- duration: batch.duration,
986
- eventCount: batch.event_count,
987
- status: batch.status,
988
- statusCode: batch.status_code,
989
- webhook: batch.webhook
990
- }))),
991
- error: null
992
- };
993
- }
994
- async resendBatch(batchId) {
995
- let error = null;
996
- const response = await this.mailchannels.post(`/tx/v1/webhook-batch/${batchId}/resend`, { onResponseError: async ({ response }) => {
997
- error = getStatusError(response, {
998
- [400]: "Bad Request. The batch ID is invalid.",
999
- [404]: `The batch '${batchId}' is not found for the customer.`
1000
- });
1001
- } }).catch((e) => {
1002
- error ||= getResultError(e, "Failed to resend webhook batch.");
1003
- return null;
1004
- });
1005
- if (!response) return {
1006
- data: null,
1007
- error
1008
- };
1009
- return {
1010
- data: clean({
1011
- batchId: response.batch_id,
1012
- customerHandle: response.customer_handle,
1013
- webhook: response.webhook,
1014
- createdAt: response.created_at,
1015
- eventCount: response.event_count,
1016
- duration: response.duration_in_ms,
1017
- statusCode: response.status_code
1018
- }),
1019
- error: null
1020
- };
1021
- }
1022
- };
1023
- var SubAccounts = class SubAccounts {
1024
- mailchannels;
1025
- static COMPANY_PATTERN = /^.{3,128}$/;
1026
- static HANDLE_PATTERN = /^[a-z0-9]{3,128}$/;
1027
- constructor(mailchannels) {
1028
- this.mailchannels = mailchannels;
1029
- }
1030
- async create(companyName, handle) {
1031
- let error = null;
1032
- if (!SubAccounts.COMPANY_PATTERN.test(companyName)) {
1033
- error = createValidationError("Invalid company name. Company name must be between 3 and 128 characters.");
1034
- return {
1035
- data: null,
1036
- error
1037
- };
1038
- }
1039
- if (handle) {
1040
- if (!SubAccounts.HANDLE_PATTERN.test(handle)) {
1041
- error = createValidationError("Invalid handle. Sub-account handle must be between 3 and 128 characters and contain only lowercase letters and numbers.");
1042
- return {
1043
- data: null,
1044
- error
1045
- };
1046
- }
1047
- }
1048
- const response = await this.mailchannels.post("/tx/v1/sub-account", {
1049
- body: {
1050
- company_name: companyName,
1051
- handle
1052
- },
1053
- onResponseError: async ({ response }) => {
1054
- error = getStatusError(response, {
1055
- [403]: "The parent account does not have permission to create sub-accounts.",
1056
- [409]: `Sub-account with handle '${handle}' already exists.`
1057
- });
1058
- }
1059
- }).catch((e) => {
1060
- error ||= getResultError(e, "Failed to create sub-account.");
1061
- return null;
1062
- });
1063
- if (!response) return {
1064
- data: null,
1065
- error
1066
- };
1067
- return {
1068
- data: clean({
1069
- companyName: response.company_name,
1070
- enabled: response.enabled,
1071
- handle: response.handle
1072
- }),
1073
- error: null
1074
- };
1075
- }
1076
- async list(options) {
1077
- let error = null;
1078
- error = validatePagination({
1079
- ...options,
1080
- max: 1e3
1081
- });
1082
- if (error) return {
1083
- data: null,
1084
- error
1085
- };
1086
- const response = await this.mailchannels.get("/tx/v1/sub-account", {
1087
- query: options,
1088
- onResponseError: async ({ response }) => {
1089
- error = getStatusError(response);
1090
- }
1091
- }).catch((e) => {
1092
- error ||= getResultError(e, "Failed to fetch sub-accounts.");
1093
- return null;
1094
- });
1095
- if (!response) return {
1096
- data: null,
1097
- error
1098
- };
1099
- return {
1100
- data: clean(response.map((account) => ({
1101
- companyName: account.company_name,
1102
- enabled: account.enabled,
1103
- handle: account.handle
1104
- }))),
1105
- error: null
1106
- };
1107
- }
1108
- async delete(handle) {
1109
- let error = null;
1110
- if (!handle) {
1111
- error = createValidationError("No handle provided.");
1112
- return {
1113
- success: false,
1114
- error
1115
- };
1116
- }
1117
- await this.mailchannels.delete(`/tx/v1/sub-account/${handle}`, { onResponseError: async ({ response }) => {
1118
- error = getStatusError(response);
1119
- } }).catch((e) => {
1120
- error ||= getResultError(e, "Failed to delete sub-account.");
1121
- });
1122
- return {
1123
- success: !error,
1267
+ });
1268
+ if (!response) return {
1269
+ data: null,
1124
1270
  error
1125
1271
  };
1126
- }
1127
- async suspend(handle) {
1128
- let error = null;
1129
- if (!handle) {
1130
- error = createValidationError("No handle provided.");
1131
- return {
1132
- success: false,
1133
- error
1134
- };
1135
- }
1136
- await this.mailchannels.post(`/tx/v1/sub-account/${handle}/suspend`, { onResponseError: async ({ response }) => {
1137
- error = getStatusError(response, { [404]: `The specified sub-account '${handle}' does not exist.` });
1138
- } }).catch((e) => {
1139
- error ||= getResultError(e, "Failed to suspend sub-account.");
1140
- });
1141
1272
  return {
1142
- success: !error,
1143
- error
1273
+ data: clean(response.webhook_batches.map((batch) => ({
1274
+ batchId: batch.batch_id,
1275
+ createdAt: batch.created_at,
1276
+ customerHandle: batch.customer_handle,
1277
+ duration: batch.duration,
1278
+ eventCount: batch.event_count,
1279
+ status: batch.status,
1280
+ statusCode: batch.status_code,
1281
+ webhook: batch.webhook
1282
+ }))),
1283
+ error: null
1144
1284
  };
1145
1285
  }
1146
- async activate(handle) {
1286
+ async resendBatch(batchId) {
1147
1287
  let error = null;
1148
- if (!handle) {
1149
- error = createValidationError("No handle provided.");
1150
- return {
1151
- success: false,
1152
- error
1153
- };
1154
- }
1155
- await this.mailchannels.post(`/tx/v1/sub-account/${handle}/activate`, { onResponseError: async ({ response }) => {
1288
+ const response = await this.mailchannels.post(`/tx/v1/webhook-batch/${encodeURIComponent(batchId)}/resend`, { onResponseError: async ({ response }) => {
1156
1289
  error = getStatusError(response, {
1157
- [403]: "The parent account does not have permission to activate the sub-account.",
1158
- [404]: `The specified sub-account '${handle}' does not exist.`
1290
+ [400]: "Bad Request. The batch ID is invalid.",
1291
+ [404]: `The batch '${batchId}' is not found for the customer.`
1159
1292
  });
1160
1293
  } }).catch((e) => {
1161
- error ||= getResultError(e, "Failed to activate sub-account.");
1294
+ error ||= getResultError(e, "Failed to resend webhook batch.");
1295
+ return null;
1162
1296
  });
1163
- return {
1164
- success: !error,
1297
+ if (!response) return {
1298
+ data: null,
1165
1299
  error
1166
1300
  };
1301
+ return {
1302
+ data: clean({
1303
+ batchId: response.batch_id,
1304
+ customerHandle: response.customer_handle,
1305
+ webhook: response.webhook,
1306
+ createdAt: response.created_at,
1307
+ eventCount: response.event_count,
1308
+ duration: response.duration_in_ms,
1309
+ statusCode: response.status_code
1310
+ }),
1311
+ error: null
1312
+ };
1313
+ }
1314
+ };
1315
+ var SubAccountsApiKeys = class {
1316
+ mailchannels;
1317
+ constructor(mailchannels) {
1318
+ this.mailchannels = mailchannels;
1167
1319
  }
1168
- async createApiKey(handle) {
1320
+ async create(handle) {
1169
1321
  let error = null;
1170
1322
  if (!handle) {
1171
1323
  error = createValidationError("No handle provided.");
@@ -1174,7 +1326,7 @@ var SubAccounts = class SubAccounts {
1174
1326
  error
1175
1327
  };
1176
1328
  }
1177
- const response = await this.mailchannels.post(`/tx/v1/sub-account/${handle}/api-key`, { onResponseError: async ({ response }) => {
1329
+ const response = await this.mailchannels.post(`/tx/v1/sub-account/${encodeURIComponent(handle)}/api-key`, { onResponseError: async ({ response }) => {
1178
1330
  error = getStatusError(response, {
1179
1331
  [403]: "You can't create API keys for this sub-account.",
1180
1332
  [404]: `Sub-account with handle '${handle}' not found.`,
@@ -1193,7 +1345,7 @@ var SubAccounts = class SubAccounts {
1193
1345
  error: null
1194
1346
  };
1195
1347
  }
1196
- async listApiKeys(handle, options) {
1348
+ async list(handle, options) {
1197
1349
  let error = null;
1198
1350
  if (!handle) {
1199
1351
  error = createValidationError("No handle provided.");
@@ -1210,9 +1362,12 @@ var SubAccounts = class SubAccounts {
1210
1362
  data: null,
1211
1363
  error
1212
1364
  };
1213
- const response = await this.mailchannels.get(`/tx/v1/sub-account/${handle}/api-key`, { onResponseError: async ({ response }) => {
1214
- error = getStatusError(response, { [404]: `Sub-account with handle '${handle}' not found.` });
1215
- } }).catch((e) => {
1365
+ const response = await this.mailchannels.get(`/tx/v1/sub-account/${encodeURIComponent(handle)}/api-key`, {
1366
+ query: options,
1367
+ onResponseError: async ({ response }) => {
1368
+ error = getStatusError(response, { [404]: `Sub-account with handle '${handle}' not found.` });
1369
+ }
1370
+ }).catch((e) => {
1216
1371
  error ||= getResultError(e, "Failed to fetch sub-account API keys.");
1217
1372
  return null;
1218
1373
  });
@@ -1228,7 +1383,7 @@ var SubAccounts = class SubAccounts {
1228
1383
  error: null
1229
1384
  };
1230
1385
  }
1231
- async deleteApiKey(handle, id) {
1386
+ async delete(handle, id) {
1232
1387
  let error = null;
1233
1388
  if (!handle) {
1234
1389
  error = createValidationError("No handle provided.");
@@ -1237,7 +1392,7 @@ var SubAccounts = class SubAccounts {
1237
1392
  error
1238
1393
  };
1239
1394
  }
1240
- await this.mailchannels.delete(`/tx/v1/sub-account/${handle}/api-key/${id}`, { onResponseError: async ({ response }) => {
1395
+ await this.mailchannels.delete(`/tx/v1/sub-account/${encodeURIComponent(handle)}/api-key/${encodeURIComponent(id)}`, { onResponseError: async ({ response }) => {
1241
1396
  error = getStatusError(response, { [400]: "Missing or invalid API key ID." });
1242
1397
  } }).catch((e) => {
1243
1398
  error ||= getResultError(e, "Failed to delete sub-account API key.");
@@ -1247,7 +1402,13 @@ var SubAccounts = class SubAccounts {
1247
1402
  error
1248
1403
  };
1249
1404
  }
1250
- async createSmtpPassword(handle) {
1405
+ };
1406
+ var SubAccountsSmtpPasswords = class {
1407
+ mailchannels;
1408
+ constructor(mailchannels) {
1409
+ this.mailchannels = mailchannels;
1410
+ }
1411
+ async create(handle) {
1251
1412
  let error = null;
1252
1413
  if (!handle) {
1253
1414
  error = createValidationError("No handle provided.");
@@ -1256,7 +1417,7 @@ var SubAccounts = class SubAccounts {
1256
1417
  error
1257
1418
  };
1258
1419
  }
1259
- const response = await this.mailchannels.post(`/tx/v1/sub-account/${handle}/smtp-password`, { onResponseError: async ({ response }) => {
1420
+ const response = await this.mailchannels.post(`/tx/v1/sub-account/${encodeURIComponent(handle)}/smtp-password`, { onResponseError: async ({ response }) => {
1260
1421
  error = getStatusError(response, {
1261
1422
  [403]: "You can't create SMTP passwords for this sub-account.",
1262
1423
  [404]: `Sub-account with handle '${handle}' not found.`,
@@ -1279,7 +1440,7 @@ var SubAccounts = class SubAccounts {
1279
1440
  error: null
1280
1441
  };
1281
1442
  }
1282
- async listSmtpPasswords(handle) {
1443
+ async list(handle) {
1283
1444
  let error = null;
1284
1445
  if (!handle) {
1285
1446
  error = createValidationError("No handle provided.");
@@ -1288,7 +1449,7 @@ var SubAccounts = class SubAccounts {
1288
1449
  error
1289
1450
  };
1290
1451
  }
1291
- const response = await this.mailchannels.get(`/tx/v1/sub-account/${handle}/smtp-password`, { onResponseError: async ({ response }) => {
1452
+ const response = await this.mailchannels.get(`/tx/v1/sub-account/${encodeURIComponent(handle)}/smtp-password`, { onResponseError: async ({ response }) => {
1292
1453
  error = getStatusError(response, { [404]: `Sub-account with handle '${handle}' not found.` });
1293
1454
  } }).catch((e) => {
1294
1455
  error ||= getResultError(e, "Failed to fetch sub-account SMTP passwords.");
@@ -1307,7 +1468,7 @@ var SubAccounts = class SubAccounts {
1307
1468
  error: null
1308
1469
  };
1309
1470
  }
1310
- async deleteSmtpPassword(handle, id) {
1471
+ async delete(handle, id) {
1311
1472
  let error = null;
1312
1473
  if (!handle) {
1313
1474
  error = createValidationError("No handle provided.");
@@ -1316,7 +1477,7 @@ var SubAccounts = class SubAccounts {
1316
1477
  error
1317
1478
  };
1318
1479
  }
1319
- await this.mailchannels.delete(`/tx/v1/sub-account/${handle}/smtp-password/${id}`, { onResponseError: async ({ response }) => {
1480
+ await this.mailchannels.delete(`/tx/v1/sub-account/${encodeURIComponent(handle)}/smtp-password/${encodeURIComponent(id)}`, { onResponseError: async ({ response }) => {
1320
1481
  error = getStatusError(response, { [400]: "Missing or invalid SMTP password ID." });
1321
1482
  } }).catch((e) => {
1322
1483
  error ||= getResultError(e, "Failed to delete sub-account SMTP password.");
@@ -1326,7 +1487,13 @@ var SubAccounts = class SubAccounts {
1326
1487
  error
1327
1488
  };
1328
1489
  }
1329
- async getLimit(handle) {
1490
+ };
1491
+ var SubAccountsLimits = class {
1492
+ mailchannels;
1493
+ constructor(mailchannels) {
1494
+ this.mailchannels = mailchannels;
1495
+ }
1496
+ async get(handle) {
1330
1497
  let error = null;
1331
1498
  if (!handle) {
1332
1499
  error = createValidationError("No handle provided.");
@@ -1335,7 +1502,7 @@ var SubAccounts = class SubAccounts {
1335
1502
  error
1336
1503
  };
1337
1504
  }
1338
- const response = await this.mailchannels.get(`/tx/v1/sub-account/${handle}/limit`, { onResponseError: async ({ response }) => {
1505
+ const response = await this.mailchannels.get(`/tx/v1/sub-account/${encodeURIComponent(handle)}/limit`, { onResponseError: async ({ response }) => {
1339
1506
  error = getStatusError(response, { [404]: `Sub-account with handle '${handle}' not found.` });
1340
1507
  } }).catch((e) => {
1341
1508
  error ||= getResultError(e, "Failed to fetch sub-account limit.");
@@ -1350,7 +1517,7 @@ var SubAccounts = class SubAccounts {
1350
1517
  error: null
1351
1518
  };
1352
1519
  }
1353
- async setLimit(handle, limit) {
1520
+ async set(handle, options) {
1354
1521
  let error = null;
1355
1522
  if (!handle) {
1356
1523
  error = createValidationError("No handle provided.");
@@ -1359,15 +1526,15 @@ var SubAccounts = class SubAccounts {
1359
1526
  error
1360
1527
  };
1361
1528
  }
1362
- if (limit.sends < 0) {
1529
+ if (options.sends < 0) {
1363
1530
  error = createValidationError("The sends value must be at least 0.");
1364
1531
  return {
1365
1532
  success: false,
1366
1533
  error
1367
1534
  };
1368
1535
  }
1369
- await this.mailchannels.put(`/tx/v1/sub-account/${handle}/limit`, {
1370
- body: limit,
1536
+ await this.mailchannels.put(`/tx/v1/sub-account/${encodeURIComponent(handle)}/limit`, {
1537
+ body: options,
1371
1538
  onResponseError: async ({ response }) => {
1372
1539
  error = getStatusError(response, {
1373
1540
  [400]: "Bad Request.",
@@ -1382,7 +1549,7 @@ var SubAccounts = class SubAccounts {
1382
1549
  error
1383
1550
  };
1384
1551
  }
1385
- async deleteLimit(handle) {
1552
+ async delete(handle) {
1386
1553
  let error = null;
1387
1554
  if (!handle) {
1388
1555
  error = createValidationError("No handle provided.");
@@ -1391,7 +1558,7 @@ var SubAccounts = class SubAccounts {
1391
1558
  error
1392
1559
  };
1393
1560
  }
1394
- await this.mailchannels.delete(`/tx/v1/sub-account/${handle}/limit`, { onResponseError: async ({ response }) => {
1561
+ await this.mailchannels.delete(`/tx/v1/sub-account/${encodeURIComponent(handle)}/limit`, { onResponseError: async ({ response }) => {
1395
1562
  error = getStatusError(response, { [404]: `Sub-account with handle '${handle}' not found.` });
1396
1563
  } }).catch((e) => {
1397
1564
  error ||= getResultError(e, "Failed to delete sub-account limit.");
@@ -1401,6 +1568,158 @@ var SubAccounts = class SubAccounts {
1401
1568
  error
1402
1569
  };
1403
1570
  }
1571
+ };
1572
+ var SubAccounts = class SubAccounts {
1573
+ mailchannels;
1574
+ static COMPANY_PATTERN = /^.{3,128}$/;
1575
+ static HANDLE_PATTERN = /^[a-z0-9]{3,128}$/;
1576
+ apiKeys;
1577
+ smtpPasswords;
1578
+ limits;
1579
+ constructor(mailchannels) {
1580
+ this.mailchannels = mailchannels;
1581
+ this.apiKeys = new SubAccountsApiKeys(mailchannels);
1582
+ this.smtpPasswords = new SubAccountsSmtpPasswords(mailchannels);
1583
+ this.limits = new SubAccountsLimits(mailchannels);
1584
+ }
1585
+ async create(companyName, handle) {
1586
+ let error = null;
1587
+ if (!SubAccounts.COMPANY_PATTERN.test(companyName)) {
1588
+ error = createValidationError("Invalid company name. Company name must be between 3 and 128 characters.");
1589
+ return {
1590
+ data: null,
1591
+ error
1592
+ };
1593
+ }
1594
+ if (handle) {
1595
+ if (!SubAccounts.HANDLE_PATTERN.test(handle)) {
1596
+ error = createValidationError("Invalid handle. Sub-account handle must be between 3 and 128 characters and contain only lowercase letters and numbers.");
1597
+ return {
1598
+ data: null,
1599
+ error
1600
+ };
1601
+ }
1602
+ }
1603
+ const response = await this.mailchannels.post("/tx/v1/sub-account", {
1604
+ body: {
1605
+ company_name: companyName,
1606
+ handle
1607
+ },
1608
+ onResponseError: async ({ response }) => {
1609
+ error = getStatusError(response, {
1610
+ [403]: "The parent account does not have permission to create sub-accounts.",
1611
+ [409]: `Sub-account with handle '${handle}' already exists.`
1612
+ });
1613
+ }
1614
+ }).catch((e) => {
1615
+ error ||= getResultError(e, "Failed to create sub-account.");
1616
+ return null;
1617
+ });
1618
+ if (!response) return {
1619
+ data: null,
1620
+ error
1621
+ };
1622
+ return {
1623
+ data: clean({
1624
+ companyName: response.company_name,
1625
+ enabled: response.enabled,
1626
+ handle: response.handle
1627
+ }),
1628
+ error: null
1629
+ };
1630
+ }
1631
+ async list(options) {
1632
+ let error = null;
1633
+ error = validatePagination({
1634
+ ...options,
1635
+ max: 1e3
1636
+ });
1637
+ if (error) return {
1638
+ data: null,
1639
+ error
1640
+ };
1641
+ const response = await this.mailchannels.get("/tx/v1/sub-account", {
1642
+ query: options,
1643
+ onResponseError: async ({ response }) => {
1644
+ error = getStatusError(response);
1645
+ }
1646
+ }).catch((e) => {
1647
+ error ||= getResultError(e, "Failed to fetch sub-accounts.");
1648
+ return null;
1649
+ });
1650
+ if (!response) return {
1651
+ data: null,
1652
+ error
1653
+ };
1654
+ return {
1655
+ data: clean(response.map((account) => ({
1656
+ companyName: account.company_name,
1657
+ enabled: account.enabled,
1658
+ handle: account.handle
1659
+ }))),
1660
+ error: null
1661
+ };
1662
+ }
1663
+ async delete(handle) {
1664
+ let error = null;
1665
+ if (!handle) {
1666
+ error = createValidationError("No handle provided.");
1667
+ return {
1668
+ success: false,
1669
+ error
1670
+ };
1671
+ }
1672
+ await this.mailchannels.delete(`/tx/v1/sub-account/${encodeURIComponent(handle)}`, { onResponseError: async ({ response }) => {
1673
+ error = getStatusError(response);
1674
+ } }).catch((e) => {
1675
+ error ||= getResultError(e, "Failed to delete sub-account.");
1676
+ });
1677
+ return {
1678
+ success: !error,
1679
+ error
1680
+ };
1681
+ }
1682
+ async suspend(handle) {
1683
+ let error = null;
1684
+ if (!handle) {
1685
+ error = createValidationError("No handle provided.");
1686
+ return {
1687
+ success: false,
1688
+ error
1689
+ };
1690
+ }
1691
+ await this.mailchannels.post(`/tx/v1/sub-account/${encodeURIComponent(handle)}/suspend`, { onResponseError: async ({ response }) => {
1692
+ error = getStatusError(response, { [404]: `The specified sub-account '${handle}' does not exist.` });
1693
+ } }).catch((e) => {
1694
+ error ||= getResultError(e, "Failed to suspend sub-account.");
1695
+ });
1696
+ return {
1697
+ success: !error,
1698
+ error
1699
+ };
1700
+ }
1701
+ async activate(handle) {
1702
+ let error = null;
1703
+ if (!handle) {
1704
+ error = createValidationError("No handle provided.");
1705
+ return {
1706
+ success: false,
1707
+ error
1708
+ };
1709
+ }
1710
+ await this.mailchannels.post(`/tx/v1/sub-account/${encodeURIComponent(handle)}/activate`, { onResponseError: async ({ response }) => {
1711
+ error = getStatusError(response, {
1712
+ [403]: "The parent account does not have permission to activate the sub-account.",
1713
+ [404]: `The specified sub-account '${handle}' does not exist.`
1714
+ });
1715
+ } }).catch((e) => {
1716
+ error ||= getResultError(e, "Failed to activate sub-account.");
1717
+ });
1718
+ return {
1719
+ success: !error,
1720
+ error
1721
+ };
1722
+ }
1404
1723
  async getUsage(handle) {
1405
1724
  let error = null;
1406
1725
  if (!handle) {
@@ -1410,7 +1729,7 @@ var SubAccounts = class SubAccounts {
1410
1729
  error
1411
1730
  };
1412
1731
  }
1413
- const response = await this.mailchannels.get(`/tx/v1/sub-account/${handle}/usage`, { onResponseError: async ({ response }) => {
1732
+ const response = await this.mailchannels.get(`/tx/v1/sub-account/${encodeURIComponent(handle)}/usage`, { onResponseError: async ({ response }) => {
1414
1733
  error = getStatusError(response, { [404]: `Sub-account with handle '${handle}' not found.` });
1415
1734
  } }).catch((e) => {
1416
1735
  error ||= getResultError(e, "Failed to fetch sub-account usage.");
@@ -1429,6 +1748,33 @@ var SubAccounts = class SubAccounts {
1429
1748
  error: null
1430
1749
  };
1431
1750
  }
1751
+ createApiKey(...args) {
1752
+ return this.apiKeys.create(...args);
1753
+ }
1754
+ listApiKeys(...args) {
1755
+ return this.apiKeys.list(...args);
1756
+ }
1757
+ deleteApiKey(...args) {
1758
+ return this.apiKeys.delete(...args);
1759
+ }
1760
+ createSmtpPassword(...args) {
1761
+ return this.smtpPasswords.create(...args);
1762
+ }
1763
+ listSmtpPasswords(...args) {
1764
+ return this.smtpPasswords.list(...args);
1765
+ }
1766
+ deleteSmtpPassword(...args) {
1767
+ return this.smtpPasswords.delete(...args);
1768
+ }
1769
+ setLimit(...args) {
1770
+ return this.limits.set(...args);
1771
+ }
1772
+ getLimit(...args) {
1773
+ return this.limits.get(...args);
1774
+ }
1775
+ deleteLimit(...args) {
1776
+ return this.limits.delete(...args);
1777
+ }
1432
1778
  };
1433
1779
  const mapBucket = (bucket) => {
1434
1780
  return {
@@ -1443,10 +1789,18 @@ var Metrics = class {
1443
1789
  }
1444
1790
  async engagement(options) {
1445
1791
  let error = null;
1792
+ const { dates, error: dateError } = parseDateInputs({
1793
+ startTime: options?.startTime,
1794
+ endTime: options?.endTime
1795
+ });
1796
+ if (!dates || dateError) return {
1797
+ data: null,
1798
+ error: dateError
1799
+ };
1446
1800
  const response = await this.mailchannels.get("/tx/v1/metrics/engagement", {
1447
1801
  query: {
1448
- start_time: options?.startTime,
1449
- end_time: options?.endTime,
1802
+ start_time: dates.startTime,
1803
+ end_time: dates.endTime,
1450
1804
  campaign_id: options?.campaignId,
1451
1805
  interval: options?.interval
1452
1806
  },
@@ -1467,24 +1821,40 @@ var Metrics = class {
1467
1821
  click: response.buckets.click.map(mapBucket),
1468
1822
  clickTrackingDelivered: response.buckets.click_tracking_delivered.map(mapBucket),
1469
1823
  open: response.buckets.open.map(mapBucket),
1470
- openTrackingDelivered: response.buckets.open_tracking_delivered.map(mapBucket)
1824
+ openTrackingDelivered: response.buckets.open_tracking_delivered.map(mapBucket),
1825
+ uniqueClick: response.buckets.unique_click?.map(mapBucket),
1826
+ uniqueClickTrackingDelivered: response.buckets.unique_click_tracking_delivered?.map(mapBucket),
1827
+ uniqueOpen: response.buckets.unique_open?.map(mapBucket),
1828
+ uniqueOpenTrackingDelivered: response.buckets.unique_open_tracking_delivered?.map(mapBucket)
1471
1829
  },
1472
1830
  click: response.click,
1473
1831
  clickTrackingDelivered: response.click_tracking_delivered,
1474
1832
  endTime: response.end_time,
1475
1833
  open: response.open,
1476
1834
  openTrackingDelivered: response.open_tracking_delivered,
1477
- startTime: response.start_time
1835
+ startTime: response.start_time,
1836
+ uniqueClick: response.unique_click,
1837
+ uniqueClickTrackingDelivered: response.unique_click_tracking_delivered,
1838
+ uniqueOpen: response.unique_open,
1839
+ uniqueOpenTrackingDelivered: response.unique_open_tracking_delivered
1478
1840
  }),
1479
1841
  error: null
1480
1842
  };
1481
1843
  }
1482
1844
  async performance(options) {
1483
1845
  let error = null;
1846
+ const { dates, error: dateError } = parseDateInputs({
1847
+ startTime: options?.startTime,
1848
+ endTime: options?.endTime
1849
+ });
1850
+ if (!dates || dateError) return {
1851
+ data: null,
1852
+ error: dateError
1853
+ };
1484
1854
  const response = await this.mailchannels.get("/tx/v1/metrics/performance", {
1485
1855
  query: {
1486
- start_time: options?.startTime,
1487
- end_time: options?.endTime,
1856
+ start_time: dates.startTime,
1857
+ end_time: dates.endTime,
1488
1858
  campaign_id: options?.campaignId,
1489
1859
  interval: options?.interval
1490
1860
  },
@@ -1502,8 +1872,10 @@ var Metrics = class {
1502
1872
  return {
1503
1873
  data: clean({
1504
1874
  bounced: response.bounced,
1875
+ complained: response.complained,
1505
1876
  buckets: {
1506
1877
  bounced: response.buckets.bounced.map(mapBucket),
1878
+ complained: response.buckets.complained.map(mapBucket),
1507
1879
  delivered: response.buckets.delivered.map(mapBucket),
1508
1880
  processed: response.buckets.processed.map(mapBucket)
1509
1881
  },
@@ -1517,10 +1889,18 @@ var Metrics = class {
1517
1889
  }
1518
1890
  async recipientBehaviour(options) {
1519
1891
  let error = null;
1892
+ const { dates, error: dateError } = parseDateInputs({
1893
+ startTime: options?.startTime,
1894
+ endTime: options?.endTime
1895
+ });
1896
+ if (!dates || dateError) return {
1897
+ data: null,
1898
+ error: dateError
1899
+ };
1520
1900
  const response = await this.mailchannels.get("/tx/v1/metrics/recipient-behaviour", {
1521
1901
  query: {
1522
- start_time: options?.startTime,
1523
- end_time: options?.endTime,
1902
+ start_time: dates.startTime,
1903
+ end_time: dates.endTime,
1524
1904
  campaign_id: options?.campaignId,
1525
1905
  interval: options?.interval
1526
1906
  },
@@ -1551,10 +1931,18 @@ var Metrics = class {
1551
1931
  }
1552
1932
  async volume(options) {
1553
1933
  let error = null;
1934
+ const { dates, error: dateError } = parseDateInputs({
1935
+ startTime: options?.startTime,
1936
+ endTime: options?.endTime
1937
+ });
1938
+ if (!dates || dateError) return {
1939
+ data: null,
1940
+ error: dateError
1941
+ };
1554
1942
  const response = await this.mailchannels.get("/tx/v1/metrics/volume", {
1555
1943
  query: {
1556
- start_time: options?.startTime,
1557
- end_time: options?.endTime,
1944
+ start_time: dates.startTime,
1945
+ end_time: dates.endTime,
1558
1946
  campaign_id: options?.campaignId,
1559
1947
  interval: options?.interval
1560
1948
  },
@@ -1608,6 +1996,14 @@ var Metrics = class {
1608
1996
  }
1609
1997
  async senders(type, options) {
1610
1998
  let error = null;
1999
+ const { dates, error: dateError } = parseDateInputs({
2000
+ startTime: options?.startTime,
2001
+ endTime: options?.endTime
2002
+ });
2003
+ if (!dates || dateError) return {
2004
+ data: null,
2005
+ error: dateError
2006
+ };
1611
2007
  error = validatePagination({
1612
2008
  ...options,
1613
2009
  max: 1e3
@@ -1616,10 +2012,10 @@ var Metrics = class {
1616
2012
  data: null,
1617
2013
  error
1618
2014
  };
1619
- const response = await this.mailchannels.get(`/tx/v1/metrics/senders/${type}`, {
2015
+ const response = await this.mailchannels.get(`/tx/v1/metrics/senders/${encodeURIComponent(type)}`, {
1620
2016
  query: {
1621
- start_time: options?.startTime,
1622
- end_time: options?.endTime,
2017
+ start_time: dates.startTime,
2018
+ end_time: dates.endTime,
1623
2019
  limit: options?.limit,
1624
2020
  offset: options?.offset,
1625
2021
  sort_order: options?.sortOrder
@@ -1690,7 +2086,7 @@ var Suppressions = class {
1690
2086
  }
1691
2087
  async delete(recipient, source) {
1692
2088
  let error = null;
1693
- await this.mailchannels.delete(`/tx/v1/suppression-list/recipients/${recipient}`, {
2089
+ await this.mailchannels.delete(`/tx/v1/suppression-list/recipients/${encodeURIComponent(recipient)}`, {
1694
2090
  query: { source },
1695
2091
  onResponseError: async ({ response }) => {
1696
2092
  error = getStatusError(response, { [400]: "Bad Request." });
@@ -1712,6 +2108,14 @@ var Suppressions = class {
1712
2108
  error
1713
2109
  };
1714
2110
  }
2111
+ const { dates, error: dateError } = parseDateInputs({
2112
+ createdBefore: options?.createdBefore,
2113
+ createdAfter: options?.createdAfter
2114
+ });
2115
+ if (!dates || dateError) return {
2116
+ data: null,
2117
+ error: dateError
2118
+ };
1715
2119
  error = validatePagination({
1716
2120
  ...options,
1717
2121
  max: 1e3
@@ -1723,8 +2127,8 @@ var Suppressions = class {
1723
2127
  const payload = {
1724
2128
  recipient: options?.recipient,
1725
2129
  source: options?.source,
1726
- created_before: options?.createdBefore,
1727
- created_after: options?.createdAfter,
2130
+ created_before: dates.createdBefore,
2131
+ created_after: dates.createdAfter,
1728
2132
  limit: options?.limit,
1729
2133
  offset: options?.offset
1730
2134
  };