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.
@@ -55,13 +55,14 @@ const createMetricsBuckets = (count) => [{
55
55
  const createAccountState = (apiKey) => ({
56
56
  apiKey,
57
57
  customerHandle: createId("customer"),
58
+ customTrackingDomains: [],
58
59
  dkimKeysByDomain: /* @__PURE__ */ new Map(),
59
60
  messages: [],
60
61
  subAccounts: /* @__PURE__ */ new Map(),
61
62
  suppressionEntries: [],
62
63
  webhookBatches: [],
63
64
  webhooks: /* @__PURE__ */ new Set(),
64
- webhookSigningKeys: new Map([[SIMULATOR_SIGNING_KEY_ID, "SIMULATOR_PUBLIC_SIGNING_KEY"]]),
65
+ webhookSigningKeys: /* @__PURE__ */ new Map([[SIMULATOR_SIGNING_KEY_ID, "SIMULATOR_PUBLIC_SIGNING_KEY"]]),
65
66
  nextIds: {
66
67
  apiKey: 1,
67
68
  batch: 1,
@@ -101,6 +102,16 @@ const createDkimKey = (domain, selector, overrides = {}) => {
101
102
  };
102
103
  };
103
104
  const listDkimKeys = (account, domain) => account.dkimKeysByDomain.get(domain) || [];
105
+ const createCustomTrackingDomain = ({ hostname, name, scope, status = "active" }) => ({
106
+ created_at: currentTimestamp(),
107
+ hostname,
108
+ name,
109
+ scope,
110
+ status
111
+ });
112
+ const findCustomTrackingDomain = (account, hostname, scope) => {
113
+ return account.customTrackingDomains.find((domain) => domain.hostname === hostname && domain.scope === scope);
114
+ };
104
115
  const recordWebhookBatch = (account, webhook, eventCount, status = "2xx_response", statusCode = 200) => {
105
116
  account.webhookBatches.unshift({
106
117
  batch_id: account.nextIds.batch++,
@@ -125,6 +136,7 @@ const collectMessages = (account, filters = {}) => {
125
136
  };
126
137
  const summarizeMessages = (messages) => ({
127
138
  bounced: messages.reduce((total, message) => total + message.bounced, 0),
139
+ complained: messages.reduce((total, message) => total + message.complained, 0),
128
140
  click: messages.reduce((total, message) => total + message.click, 0),
129
141
  clickTrackingDelivered: messages.reduce((total, message) => total + message.clickTrackingDelivered, 0),
130
142
  delivered: messages.reduce((total, message) => total + message.delivered, 0),
@@ -198,6 +210,7 @@ const createEmailApiHandler = ({ logRequests = true } = {}) => {
198
210
  account.messages.push({
199
211
  bounced: 0,
200
212
  campaignId: body?.campaign_id || "uncategorized",
213
+ complained: 0,
201
214
  click: body?.tracking_settings?.click_tracking?.enable ? 1 : 0,
202
215
  clickTrackingDelivered: body?.tracking_settings?.click_tracking?.enable ? 1 : 0,
203
216
  delivered: 1,
@@ -240,11 +253,12 @@ const createEmailApiHandler = ({ logRequests = true } = {}) => {
240
253
  }
241
254
  if (method === "POST" && url.pathname === "/tx/v1/check-domain") {
242
255
  const domain = body?.domain || "example.com";
256
+ const dkimSettings = body?.dkim_settings?.length ? body.dkim_settings : listDkimKeys(account, domain).map((key) => ({
257
+ dkim_domain: key.domain,
258
+ dkim_selector: key.selector
259
+ }));
243
260
  sendJson(response, 200, { check_results: {
244
- dkim: (body?.dkim_settings?.length ? body.dkim_settings : listDkimKeys(account, domain).map((key) => ({
245
- dkim_domain: key.domain,
246
- dkim_selector: key.selector
247
- }))).map((setting) => ({
261
+ dkim: dkimSettings.map((setting) => ({
248
262
  dkim_domain: setting.dkim_domain || domain,
249
263
  dkim_key_status: setting.dkim_private_key ? "provided" : listDkimKeys(account, setting.dkim_domain || domain).find((key) => key.selector === setting.dkim_selector)?.status || "active",
250
264
  dkim_selector: setting.dkim_selector || "default",
@@ -321,6 +335,71 @@ const createEmailApiHandler = ({ logRequests = true } = {}) => {
321
335
  sendNoContent(response);
322
336
  return;
323
337
  }
338
+ if (url.pathname === "/tx/v1/custom-tracking-domains") {
339
+ if (method === "POST") {
340
+ const { hostname, name, scope } = body || {};
341
+ if (!hostname || !name || !scope) {
342
+ sendJson(response, 400, { error: "Invalid request body." });
343
+ return;
344
+ }
345
+ const hasDuplicateName = account.customTrackingDomains.some((domain) => domain.name === name);
346
+ const hasDuplicateHostnameAndScope = Boolean(findCustomTrackingDomain(account, hostname, scope));
347
+ if (hasDuplicateName || hasDuplicateHostnameAndScope) {
348
+ sendJson(response, 409, { error: "Custom tracking domain already exists." });
349
+ return;
350
+ }
351
+ const domain = createCustomTrackingDomain({
352
+ hostname,
353
+ name,
354
+ scope
355
+ });
356
+ account.customTrackingDomains.push(domain);
357
+ sendJson(response, 201, clone(domain));
358
+ return;
359
+ }
360
+ if (method === "GET") {
361
+ const name = url.searchParams.get("name");
362
+ const status = url.searchParams.get("status");
363
+ const scope = url.searchParams.get("scope");
364
+ const offset = Number(url.searchParams.get("offset") || "0");
365
+ const limit = Number(url.searchParams.get("limit") || "100");
366
+ let domains = account.customTrackingDomains;
367
+ if (name) domains = domains.filter((domain) => domain.name === name);
368
+ if (status) domains = domains.filter((domain) => domain.status === status);
369
+ if (scope) domains = domains.filter((domain) => domain.scope === scope);
370
+ sendJson(response, 200, {
371
+ custom_tracking_domains: clone(domains.slice(offset, offset + limit)),
372
+ total: domains.length
373
+ });
374
+ return;
375
+ }
376
+ }
377
+ const customTrackingDomainMatch = url.pathname.match(/^\/tx\/v1\/custom-tracking-domains\/([^/]+)\/([^/]+)$/);
378
+ if (customTrackingDomainMatch) {
379
+ const [, hostname, scope] = customTrackingDomainMatch;
380
+ const targetDomain = findCustomTrackingDomain(account, hostname, scope);
381
+ if (!targetDomain) {
382
+ sendJson(response, 404, { error: `Custom tracking domain for hostname '${hostname}' and scope '${scope}' not found.` });
383
+ return;
384
+ }
385
+ if (method === "PATCH") {
386
+ if (body?.name) {
387
+ if (account.customTrackingDomains.some((domain) => domain !== targetDomain && domain.name === body.name)) {
388
+ sendJson(response, 409, { error: "Name already used by another domain." });
389
+ return;
390
+ }
391
+ targetDomain.name = body.name;
392
+ }
393
+ if (body?.status) targetDomain.status = body.status;
394
+ sendJson(response, 200, clone(targetDomain));
395
+ return;
396
+ }
397
+ if (method === "DELETE") {
398
+ account.customTrackingDomains = account.customTrackingDomains.filter((domain) => domain !== targetDomain);
399
+ sendNoContent(response);
400
+ return;
401
+ }
402
+ }
324
403
  if (url.pathname === "/tx/v1/webhook" && method === "POST") {
325
404
  const endpoint = url.searchParams.get("endpoint");
326
405
  if (!endpoint) {
@@ -360,19 +439,20 @@ const createEmailApiHandler = ({ logRequests = true } = {}) => {
360
439
  sendJson(response, 404, { error: "No webhooks found for the account." });
361
440
  return;
362
441
  }
442
+ const results = Array.from(account.webhooks).map((webhook) => {
443
+ recordWebhookBatch(account, webhook, 1);
444
+ return {
445
+ result: "passed",
446
+ webhook,
447
+ response: {
448
+ body: "simulated validation ok",
449
+ status: 200
450
+ }
451
+ };
452
+ });
363
453
  sendJson(response, 200, {
364
454
  all_passed: true,
365
- results: Array.from(account.webhooks).map((webhook) => {
366
- recordWebhookBatch(account, webhook, 1);
367
- return {
368
- result: "passed",
369
- webhook,
370
- response: {
371
- body: "simulated validation ok",
372
- status: 200
373
- }
374
- };
375
- })
455
+ results
376
456
  });
377
457
  return;
378
458
  }
@@ -383,7 +463,7 @@ const createEmailApiHandler = ({ logRequests = true } = {}) => {
383
463
  const webhook = url.searchParams.get("webhook");
384
464
  const limit = Number(url.searchParams.get("limit") || "500");
385
465
  const offset = Number(url.searchParams.get("offset") || "0");
386
- sendJson(response, 200, { webhook_batches: account.webhookBatches.filter((batch) => {
466
+ const batches = account.webhookBatches.filter((batch) => {
387
467
  if (createdAfter && batch.created_at < createdAfter) return false;
388
468
  if (createdBefore && batch.created_at >= createdBefore) return false;
389
469
  if (webhook && batch.webhook !== webhook) return false;
@@ -391,7 +471,8 @@ const createEmailApiHandler = ({ logRequests = true } = {}) => {
391
471
  if (!statuses.map((status) => `${status}_response`.replace("no_response_response", "no_response")).includes(batch.status)) return false;
392
472
  }
393
473
  return true;
394
- }).slice(offset, offset + limit) });
474
+ });
475
+ sendJson(response, 200, { webhook_batches: batches.slice(offset, offset + limit) });
395
476
  return;
396
477
  }
397
478
  const webhookBatchResendMatch = url.pathname.match(/^\/tx\/v1\/webhook-batch\/(\d+)\/resend$/);
@@ -528,7 +609,8 @@ const createEmailApiHandler = ({ logRequests = true } = {}) => {
528
609
  }
529
610
  }
530
611
  if (url.pathname === "/tx/v1/metrics/engagement" && method === "GET") {
531
- const summary = summarizeMessages(collectMessages(account, { campaignId: url.searchParams.get("campaign_id") || void 0 }));
612
+ const messages = collectMessages(account, { campaignId: url.searchParams.get("campaign_id") || void 0 });
613
+ const summary = summarizeMessages(messages);
532
614
  sendJson(response, 200, {
533
615
  buckets: {
534
616
  click: createMetricsBuckets(summary.click),
@@ -546,11 +628,14 @@ const createEmailApiHandler = ({ logRequests = true } = {}) => {
546
628
  return;
547
629
  }
548
630
  if (url.pathname === "/tx/v1/metrics/performance" && method === "GET") {
549
- const summary = summarizeMessages(collectMessages(account, { campaignId: url.searchParams.get("campaign_id") || void 0 }));
631
+ const messages = collectMessages(account, { campaignId: url.searchParams.get("campaign_id") || void 0 });
632
+ const summary = summarizeMessages(messages);
550
633
  sendJson(response, 200, {
551
634
  bounced: summary.bounced,
635
+ complained: summary.complained,
552
636
  buckets: {
553
637
  bounced: createMetricsBuckets(summary.bounced),
638
+ complained: createMetricsBuckets(summary.complained),
554
639
  delivered: createMetricsBuckets(summary.delivered),
555
640
  processed: createMetricsBuckets(summary.processed)
556
641
  },
@@ -562,7 +647,8 @@ const createEmailApiHandler = ({ logRequests = true } = {}) => {
562
647
  return;
563
648
  }
564
649
  if (url.pathname === "/tx/v1/metrics/recipient-behaviour" && method === "GET") {
565
- const summary = summarizeMessages(collectMessages(account, { campaignId: url.searchParams.get("campaign_id") || void 0 }));
650
+ const messages = collectMessages(account, { campaignId: url.searchParams.get("campaign_id") || void 0 });
651
+ const summary = summarizeMessages(messages);
566
652
  sendJson(response, 200, {
567
653
  buckets: {
568
654
  unsubscribe_delivered: createMetricsBuckets(summary.unsubscribeDelivered),
@@ -576,7 +662,8 @@ const createEmailApiHandler = ({ logRequests = true } = {}) => {
576
662
  return;
577
663
  }
578
664
  if (url.pathname === "/tx/v1/metrics/volume" && method === "GET") {
579
- const summary = summarizeMessages(collectMessages(account, { campaignId: url.searchParams.get("campaign_id") || void 0 }));
665
+ const messages = collectMessages(account, { campaignId: url.searchParams.get("campaign_id") || void 0 });
666
+ const summary = summarizeMessages(messages);
580
667
  sendJson(response, 200, {
581
668
  buckets: {
582
669
  delivered: createMetricsBuckets(summary.delivered),
@@ -678,7 +765,8 @@ const createEmailApiHandler = ({ logRequests = true } = {}) => {
678
765
  }
679
766
  notFound(response);
680
767
  } catch (error) {
681
- sendText(response, 500, error instanceof Error ? error.message : "Simulator error");
768
+ const message = error instanceof Error ? error.message : "Simulator error";
769
+ sendText(response, 500, message);
682
770
  }
683
771
  };
684
772
  return {