groundmail 0.26.1 → 0.28.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/authjs.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { G as Groundmail } from './client-DZNmwhee.js';
1
+ import { G as Groundmail } from './client-DdtHUMEu.js';
2
2
 
3
3
  /** Subset of Auth.js theme used for the default magic-link email. */
4
4
  type AuthjsTheme = {
package/dist/authjs.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  DEFAULT_API_BASE_URL,
3
3
  Groundmail
4
- } from "./chunk-E6G7G2PM.js";
4
+ } from "./chunk-POS35D4J.js";
5
5
 
6
6
  // src/authjs.ts
7
7
  function escapeHtml(value) {
@@ -1,4 +1,4 @@
1
- import { S as SendEmailResult, G as Groundmail } from './client-DZNmwhee.js';
1
+ import { S as SendEmailResult, G as Groundmail } from './client-DdtHUMEu.js';
2
2
 
3
3
  type BetterAuthEmailUser = {
4
4
  email: string;
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  DEFAULT_API_BASE_URL,
3
3
  Groundmail
4
- } from "./chunk-E6G7G2PM.js";
4
+ } from "./chunk-POS35D4J.js";
5
5
 
6
6
  // src/better-auth.ts
7
7
  function linkHtml(label, url) {
@@ -71,9 +71,9 @@ function isSpfRecord(value) {
71
71
  }
72
72
 
73
73
  // src/dns/cloudflare.ts
74
- function relativeName(fqdn, zone) {
74
+ function relativeName(fqdn2, zone) {
75
75
  const domain = zone.replace(/\.$/, "").toLowerCase();
76
- const host = fqdn.replace(/\.$/, "").toLowerCase();
76
+ const host = fqdn2.replace(/\.$/, "").toLowerCase();
77
77
  if (host === domain) return "@";
78
78
  if (host.endsWith(`.${domain}`)) return host.slice(0, -(domain.length + 1));
79
79
  return host;
@@ -301,10 +301,185 @@ function createCloudflareProvider(options) {
301
301
  };
302
302
  }
303
303
 
304
+ // src/dns/record-store.ts
305
+ function zoneNameCandidates2(domain) {
306
+ const labels = domain.replace(/\.$/, "").toLowerCase().split(".").filter(Boolean);
307
+ const candidates = [];
308
+ for (let i = 0; i <= labels.length - 2; i++) {
309
+ candidates.push(labels.slice(i).join("."));
310
+ }
311
+ return candidates;
312
+ }
313
+ function relativeName2(fqdn2, zone) {
314
+ const domain = zone.replace(/\.$/, "").toLowerCase();
315
+ const host = fqdn2.replace(/\.$/, "").toLowerCase();
316
+ if (host === domain) return "";
317
+ if (host.endsWith(`.${domain}`)) return host.slice(0, -(domain.length + 1));
318
+ return host;
319
+ }
320
+ function fqdn(name, zone) {
321
+ return name ? `${name}.${zone}` : zone;
322
+ }
323
+ function normalizeValue(type, value) {
324
+ const trimmed = value.trim();
325
+ if (type === "TXT") return trimmed.replace(/^"([\s\S]*)"$/, "$1").replace(/"\s+"/g, "");
326
+ return trimmed.replace(/\.$/, "").toLowerCase();
327
+ }
328
+ function ownedTxt(purpose, value) {
329
+ const lower = value.toLowerCase();
330
+ if (purpose === "Domain ownership") return lower.includes("groundmail-verify=");
331
+ if (purpose === "DKIM") return lower.includes("v=dkim1");
332
+ if (purpose === "DMARC") return lower.includes("v=dmarc1");
333
+ return false;
334
+ }
335
+ async function applyToRecordStore(zone, records, store) {
336
+ const results = [];
337
+ let cached = await store.list();
338
+ const at = (type, name) => cached.filter((row) => row.type.toUpperCase() === type && row.name === name);
339
+ for (const record of records) {
340
+ const type = record.type.toUpperCase();
341
+ const name = relativeName2(record.host, zone);
342
+ const value = normalizeValue(type, record.value);
343
+ const done = (action, detail) => results.push({ purpose: record.purpose, host: record.host, action, ...detail ? { detail } : {} });
344
+ if (type === "CNAME") {
345
+ const existing = at("CNAME", name)[0];
346
+ if (existing && normalizeValue(type, existing.value) === value) {
347
+ done("unchanged");
348
+ continue;
349
+ }
350
+ if (existing) {
351
+ await store.update(existing, { name, type, value });
352
+ done("updated");
353
+ } else {
354
+ await store.create({ name, type, value });
355
+ done("created");
356
+ }
357
+ } else if (type === "MX") {
358
+ const priority = record.priority ?? 10;
359
+ const sameName = at("MX", name);
360
+ if (sameName.some((row) => normalizeValue(type, row.value) === value && (row.priority ?? 10) === priority)) {
361
+ done("unchanged");
362
+ continue;
363
+ }
364
+ await store.create({ name, type, value, priority });
365
+ done(sameName.length > 0 ? "merged" : "created");
366
+ } else if (type !== "TXT") {
367
+ done("unchanged", `Skipped unsupported type ${record.type}`);
368
+ continue;
369
+ } else if (record.purpose === "SPF") {
370
+ const existing = at("TXT", name).find((row) => isSpfRecord(normalizeValue(type, row.value)));
371
+ const includeHost = record.value.match(/include:([^\s"]+)/i)?.[1] || "spf.groundmail.dev";
372
+ const current = existing ? normalizeValue(type, existing.value) : void 0;
373
+ const merged = mergeSpfInclude(current, includeHost);
374
+ if (existing && current?.trim() === merged.trim()) {
375
+ done("unchanged", "SPF already includes Groundmail");
376
+ continue;
377
+ }
378
+ if (existing) {
379
+ await store.update(existing, { name, type, value: merged });
380
+ done("merged", merged);
381
+ } else {
382
+ await store.create({ name, type, value: merged });
383
+ done("created", merged);
384
+ }
385
+ } else {
386
+ const sameName = at("TXT", name);
387
+ if (sameName.some((row) => normalizeValue(type, row.value) === value)) {
388
+ done("unchanged");
389
+ continue;
390
+ }
391
+ const replaceable = sameName.find((row) => ownedTxt(record.purpose, normalizeValue(type, row.value)));
392
+ if (replaceable) {
393
+ await store.update(replaceable, { name, type, value });
394
+ done("updated");
395
+ } else {
396
+ await store.create({ name, type, value });
397
+ done("created");
398
+ }
399
+ }
400
+ cached = await store.list();
401
+ }
402
+ return results;
403
+ }
404
+ async function resolveZone(domain, hostLabel, lookup) {
405
+ for (const candidate of zoneNameCandidates2(domain)) {
406
+ const info = await lookup(candidate);
407
+ if (info !== null) return { zone: candidate, info };
408
+ }
409
+ throw new Error(`No ${hostLabel} DNS zone found for ${domain}. The domain must use ${hostLabel} nameservers.`);
410
+ }
411
+
412
+ // src/dns/godaddy-token.ts
413
+ function createGoDaddyTokenProvider(options) {
414
+ const fetchImpl = options.fetchImpl || fetch;
415
+ async function godaddy(path, init) {
416
+ const response = await fetchImpl(`https://api.godaddy.com/v3/domains${path}`, {
417
+ ...init,
418
+ headers: {
419
+ Authorization: `Bearer ${options.apiToken}`,
420
+ "Content-Type": "application/json",
421
+ Accept: "application/json",
422
+ ...init?.headers || {}
423
+ }
424
+ });
425
+ const body = response.status === 204 ? {} : await response.json().catch(() => ({}));
426
+ if (!response.ok && response.status !== 404) {
427
+ if (response.status === 401 || response.status === 403) {
428
+ throw new Error(
429
+ "GoDaddy refused the token. Create a Personal Access Token with the domains.domain:read and domains.dns:update scopes."
430
+ );
431
+ }
432
+ throw new Error(body.message || body.code || `GoDaddy HTTP ${response.status}`);
433
+ }
434
+ return { status: response.status, body };
435
+ }
436
+ return {
437
+ name: "godaddy",
438
+ async apply(domain, records) {
439
+ const { zone } = await resolveZone(domain, "GoDaddy", async (candidate) => {
440
+ const { status } = await godaddy(`/zones/${encodeURIComponent(candidate)}/dns-records?pageSize=1`);
441
+ return status === 404 ? null : true;
442
+ });
443
+ const path = `/zones/${encodeURIComponent(zone)}/dns-records`;
444
+ const body = (record) => ({
445
+ name: record.name || "@",
446
+ type: record.type,
447
+ data: record.value,
448
+ ttl: 600,
449
+ ...record.priority != null ? { priority: record.priority } : {}
450
+ });
451
+ const write = async (method, suffix, record) => {
452
+ const { status } = await godaddy(`${path}${suffix}`, { method, body: JSON.stringify(body(record)) });
453
+ if (status === 404) throw new Error(`GoDaddy lost the ${zone} zone mid-update`);
454
+ };
455
+ return applyToRecordStore(zone, records, {
456
+ async list() {
457
+ const rows = [];
458
+ for (let page = 1; page <= 20; page++) {
459
+ const { body: list } = await godaddy(`${path}?page=${page}&pageSize=100`);
460
+ const items = list.items || [];
461
+ rows.push(...items);
462
+ if (items.length < 100 || !list.links?.some((link) => link.rel === "next")) break;
463
+ }
464
+ return rows.map((row) => ({
465
+ id: row.recordId,
466
+ name: row.name === "@" ? "" : row.name.toLowerCase(),
467
+ type: row.type,
468
+ value: row.data,
469
+ priority: row.priority ?? void 0
470
+ }));
471
+ },
472
+ create: (record) => write("POST", "", record),
473
+ update: (existing, record) => write("PUT", `/${encodeURIComponent(existing.id)}`, record)
474
+ });
475
+ }
476
+ };
477
+ }
478
+
304
479
  // src/dns/godaddy.ts
305
- function relativeName2(fqdn, zone) {
480
+ function relativeName3(fqdn2, zone) {
306
481
  const domain = zone.replace(/\.$/, "").toLowerCase();
307
- const host = fqdn.replace(/\.$/, "").toLowerCase();
482
+ const host = fqdn2.replace(/\.$/, "").toLowerCase();
308
483
  if (host === domain) return "@";
309
484
  if (host.endsWith(`.${domain}`)) return host.slice(0, -(domain.length + 1));
310
485
  return host;
@@ -318,7 +493,7 @@ function serializeGoDaddyMxRecords(name, rows) {
318
493
  ttl: Math.max(row.ttl || 600, 600)
319
494
  }));
320
495
  }
321
- function createGoDaddyProvider(options) {
496
+ function createGoDaddyKeyProvider(options) {
322
497
  const fetchImpl = options.fetchImpl || fetch;
323
498
  const headers = {
324
499
  Authorization: `sso-key ${options.apiKey}:${options.apiSecret}`,
@@ -345,7 +520,7 @@ function createGoDaddyProvider(options) {
345
520
  const results = [];
346
521
  for (const record of records) {
347
522
  const recordType = record.type.toUpperCase();
348
- const name = relativeName2(record.host, zone);
523
+ const name = relativeName3(record.host, zone);
349
524
  if (recordType === "CNAME") {
350
525
  const target = record.value.replace(/\.$/, "");
351
526
  const existing2 = await gd(
@@ -470,9 +645,192 @@ function createGoDaddyProvider(options) {
470
645
  }
471
646
  };
472
647
  }
648
+ function createGoDaddyProvider(options) {
649
+ if ("apiToken" in options) return createGoDaddyTokenProvider({ apiToken: options.apiToken, fetchImpl: options.fetchImpl });
650
+ return createGoDaddyKeyProvider(options);
651
+ }
652
+
653
+ // src/dns/digitalocean.ts
654
+ function createDigitalOceanProvider(options) {
655
+ const fetchImpl = options.fetchImpl || fetch;
656
+ async function digitalocean(path, init) {
657
+ const response = await fetchImpl(`https://api.digitalocean.com/v2${path}`, {
658
+ ...init,
659
+ headers: { Authorization: `Bearer ${options.apiToken}`, "Content-Type": "application/json", ...init?.headers || {} }
660
+ });
661
+ const body = await response.json().catch(() => ({}));
662
+ if (!response.ok && response.status !== 404) {
663
+ if (response.status === 403) {
664
+ throw new Error("DigitalOcean refused the DNS change. Reconnect DigitalOcean under Integrations to allow DNS.");
665
+ }
666
+ throw new Error(body.message || `DigitalOcean HTTP ${response.status}`);
667
+ }
668
+ return { status: response.status, body };
669
+ }
670
+ return {
671
+ name: "digitalocean",
672
+ async apply(domain, records) {
673
+ const { zone } = await resolveZone(domain, "DigitalOcean", async (candidate) => {
674
+ const { status } = await digitalocean(`/domains/${encodeURIComponent(candidate)}`);
675
+ return status === 404 ? null : true;
676
+ });
677
+ const path = `/domains/${encodeURIComponent(zone)}/records`;
678
+ const body = (record) => ({
679
+ type: record.type,
680
+ name: record.name || "@",
681
+ data: record.type === "TXT" ? record.value : `${record.value}.`,
682
+ ttl: 1800,
683
+ ...record.priority != null ? { priority: record.priority } : {}
684
+ });
685
+ const write = async (method, suffix, record) => {
686
+ const { status } = await digitalocean(`${path}${suffix}`, { method, body: JSON.stringify(body(record)) });
687
+ if (status === 404) throw new Error(`DigitalOcean lost the ${zone} zone mid-update`);
688
+ };
689
+ return applyToRecordStore(zone, records, {
690
+ async list() {
691
+ const rows = [];
692
+ for (let page = 1; page <= 20; page++) {
693
+ const { body: list } = await digitalocean(`${path}?per_page=200&page=${page}`);
694
+ rows.push(...list.domain_records || []);
695
+ if (!list.links?.pages?.next) break;
696
+ }
697
+ return rows.map((row) => ({
698
+ id: String(row.id),
699
+ name: row.name === "@" ? "" : row.name.toLowerCase(),
700
+ type: row.type,
701
+ value: row.data,
702
+ priority: row.priority ?? void 0
703
+ }));
704
+ },
705
+ create: (record) => write("POST", "", record),
706
+ update: (existing, record) => write("PUT", `/${existing.id}`, record)
707
+ });
708
+ }
709
+ };
710
+ }
711
+
712
+ // src/dns/dnsimple.ts
713
+ function createDnsimpleProvider(options) {
714
+ const fetchImpl = options.fetchImpl || fetch;
715
+ const base = `https://api.dnsimple.com/v2/${encodeURIComponent(options.accountId)}`;
716
+ async function dnsimple(path, init) {
717
+ const response = await fetchImpl(`${base}${path}`, {
718
+ ...init,
719
+ headers: {
720
+ Authorization: `Bearer ${options.apiToken}`,
721
+ "Content-Type": "application/json",
722
+ Accept: "application/json",
723
+ ...init?.headers || {}
724
+ }
725
+ });
726
+ const body = await response.json().catch(() => ({}));
727
+ if (!response.ok && response.status !== 404) throw new Error(body.message || `DNSimple HTTP ${response.status}`);
728
+ return { status: response.status, body };
729
+ }
730
+ return {
731
+ name: "dnsimple",
732
+ async apply(domain, records) {
733
+ const { zone } = await resolveZone(domain, "DNSimple", async (candidate) => {
734
+ const { status } = await dnsimple(`/zones/${encodeURIComponent(candidate)}`);
735
+ return status === 404 ? null : true;
736
+ });
737
+ const path = `/zones/${encodeURIComponent(zone)}/records`;
738
+ const body = (record) => ({
739
+ name: record.name,
740
+ type: record.type,
741
+ content: record.value,
742
+ ttl: 3600,
743
+ ...record.priority != null ? { priority: record.priority } : {}
744
+ });
745
+ const write = async (method, suffix, record) => {
746
+ const { status } = await dnsimple(`${path}${suffix}`, { method, body: JSON.stringify(body(record)) });
747
+ if (status === 404) throw new Error(`DNSimple lost the ${zone} zone mid-update`);
748
+ };
749
+ return applyToRecordStore(zone, records, {
750
+ async list() {
751
+ const rows = [];
752
+ for (let page = 1; page <= 20; page++) {
753
+ const { body: list } = await dnsimple(`${path}?per_page=100&page=${page}`);
754
+ rows.push(...list.data || []);
755
+ if ((list.pagination?.total_pages ?? 1) <= page) break;
756
+ }
757
+ return rows.filter((row) => !row.system_record).map((row) => ({
758
+ id: String(row.id),
759
+ name: row.name.toLowerCase(),
760
+ type: row.type,
761
+ value: row.content,
762
+ priority: row.priority ?? void 0
763
+ }));
764
+ },
765
+ create: (record) => write("POST", "", record),
766
+ update: (existing, record) => write("PATCH", `/${existing.id}`, record)
767
+ });
768
+ }
769
+ };
770
+ }
771
+
772
+ // src/dns/netlify.ts
773
+ function createNetlifyProvider(options) {
774
+ const fetchImpl = options.fetchImpl || fetch;
775
+ async function netlify(path, init) {
776
+ const response = await fetchImpl(`https://api.netlify.com/api/v1${path}`, {
777
+ ...init,
778
+ headers: { Authorization: `Bearer ${options.apiToken}`, "Content-Type": "application/json", ...init?.headers || {} }
779
+ });
780
+ const body = await response.json().catch(() => ({}));
781
+ if (!response.ok) throw new Error(body.message || `Netlify HTTP ${response.status}`);
782
+ return body;
783
+ }
784
+ return {
785
+ name: "netlify",
786
+ async apply(domain, records) {
787
+ const zones = await netlify("/dns_zones");
788
+ const { zone, info } = await resolveZone(
789
+ domain,
790
+ "Netlify",
791
+ async (candidate) => zones.find((z) => z.name.toLowerCase() === candidate) ?? null
792
+ );
793
+ const body = (record) => ({
794
+ type: record.type,
795
+ hostname: fqdn(record.name, zone),
796
+ value: record.value,
797
+ ttl: 3600,
798
+ ...record.priority != null ? { priority: record.priority } : {}
799
+ });
800
+ const create = async (record) => {
801
+ await netlify(`/dns_zones/${info.id}/dns_records`, { method: "POST", body: JSON.stringify(body(record)) });
802
+ };
803
+ return applyToRecordStore(zone, records, {
804
+ async list() {
805
+ const rows = await netlify(`/dns_zones/${info.id}/dns_records`);
806
+ return rows.filter((row) => !row.managed).map((row) => ({
807
+ id: row.id,
808
+ name: relativeName2(row.hostname, zone),
809
+ type: row.type,
810
+ value: row.value,
811
+ priority: row.priority ?? void 0
812
+ }));
813
+ },
814
+ create,
815
+ // TXT: add the new value before removing the old, so SPF never disappears. A
816
+ // name can only hold one CNAME, so that one goes the other way round.
817
+ async update(existing, record) {
818
+ const remove = () => netlify(`/dns_zones/${info.id}/dns_records/${existing.id}`, { method: "DELETE" });
819
+ if (record.type === "CNAME") {
820
+ await remove();
821
+ await create(record);
822
+ } else {
823
+ await create(record);
824
+ await remove();
825
+ }
826
+ }
827
+ });
828
+ }
829
+ };
830
+ }
473
831
 
474
832
  // src/dns/vercel.ts
475
- function zoneNameCandidates2(domain) {
833
+ function zoneNameCandidates3(domain) {
476
834
  const labels = domain.replace(/\.$/, "").toLowerCase().split(".").filter(Boolean);
477
835
  const candidates = [];
478
836
  for (let i = 0; i <= labels.length - 2; i++) {
@@ -480,9 +838,9 @@ function zoneNameCandidates2(domain) {
480
838
  }
481
839
  return candidates;
482
840
  }
483
- function relativeName3(fqdn, zone) {
841
+ function relativeName4(fqdn2, zone) {
484
842
  const domain = zone.replace(/\.$/, "").toLowerCase();
485
- const host = fqdn.replace(/\.$/, "").toLowerCase();
843
+ const host = fqdn2.replace(/\.$/, "").toLowerCase();
486
844
  if (host === domain) return "";
487
845
  if (host.endsWith(`.${domain}`)) return host.slice(0, -(domain.length + 1));
488
846
  return host;
@@ -530,7 +888,7 @@ function createVercelProvider(options) {
530
888
  }
531
889
  async function resolveDomain(domain) {
532
890
  const errors = [];
533
- for (const candidate of zoneNameCandidates2(domain)) {
891
+ for (const candidate of zoneNameCandidates3(domain)) {
534
892
  try {
535
893
  await listRecords(candidate);
536
894
  return candidate;
@@ -554,7 +912,7 @@ function createVercelProvider(options) {
554
912
  };
555
913
  for (const record of records) {
556
914
  const recordType = record.type.toUpperCase();
557
- const name = relativeName3(record.host, zone);
915
+ const name = relativeName4(record.host, zone);
558
916
  if (recordType === "CNAME") {
559
917
  const target = record.value.replace(/\.$/, "").toLowerCase();
560
918
  const existing = cached.find(
@@ -706,11 +1064,13 @@ function createDnsProvider(provider, credentials, fetchImpl) {
706
1064
  });
707
1065
  }
708
1066
  if (provider === "godaddy") {
1067
+ const apiToken = credentials.godaddyApiToken;
1068
+ if (apiToken) return createGoDaddyProvider({ apiToken, fetchImpl });
709
1069
  const apiKey = credentials.godaddyApiKey;
710
1070
  const apiSecret = credentials.godaddyApiSecret;
711
1071
  if (!apiKey || !apiSecret) {
712
1072
  throw new Error(
713
- "Missing GoDaddy credentials. Set GODADDY_API_KEY and GODADDY_API_SECRET."
1073
+ "Missing GoDaddy credentials. Set GODADDY_PAT (a Personal Access Token), or GODADDY_API_KEY and GODADDY_API_SECRET."
714
1074
  );
715
1075
  }
716
1076
  return createGoDaddyProvider({ apiKey, apiSecret, fetchImpl });
@@ -726,6 +1086,24 @@ function createDnsProvider(provider, credentials, fetchImpl) {
726
1086
  fetchImpl
727
1087
  });
728
1088
  }
1089
+ if (provider === "netlify") {
1090
+ const apiToken = credentials.netlifyApiToken;
1091
+ if (!apiToken) throw new Error("Missing Netlify token. Set NETLIFY_AUTH_TOKEN or pass netlifyApiToken.");
1092
+ return createNetlifyProvider({ apiToken, fetchImpl });
1093
+ }
1094
+ if (provider === "digitalocean") {
1095
+ const apiToken = credentials.digitaloceanApiToken;
1096
+ if (!apiToken) throw new Error("Missing DigitalOcean token. Set DIGITALOCEAN_TOKEN or pass digitaloceanApiToken.");
1097
+ return createDigitalOceanProvider({ apiToken, fetchImpl });
1098
+ }
1099
+ if (provider === "dnsimple") {
1100
+ const apiToken = credentials.dnsimpleApiToken;
1101
+ const accountId = credentials.dnsimpleAccountId;
1102
+ if (!apiToken || !accountId) {
1103
+ throw new Error("Missing DNSimple credentials. Set DNSIMPLE_TOKEN and DNSIMPLE_ACCOUNT_ID.");
1104
+ }
1105
+ return createDnsimpleProvider({ apiToken, accountId, fetchImpl });
1106
+ }
729
1107
  throw new Error(`Unsupported DNS provider: ${provider}`);
730
1108
  }
731
1109
  async function applyDnsRecords(input) {
@@ -1059,8 +1437,9 @@ var DomainsResource = class {
1059
1437
  });
1060
1438
  }
1061
1439
  /**
1062
- * Apply DNS using the public API. Cloudflare, Vercel, and GoDaddy can use stored dashboard
1063
- * credentials; GoDaddy also accepts one-time credentials on the request.
1440
+ * Apply DNS using the public API with the DNS host connected in the dashboard (Cloudflare,
1441
+ * Vercel, GoDaddy, Netlify, DigitalOcean or DNSimple). GoDaddy also accepts one-time
1442
+ * credentials on the request.
1064
1443
  */
1065
1444
  async apply(idOrName, options = {}) {
1066
1445
  return this.client.request(`/domains/${encodeURIComponent(idOrName)}`, {
@@ -1073,8 +1452,8 @@ var DomainsResource = class {
1073
1452
  });
1074
1453
  }
1075
1454
  /**
1076
- * Fetch Groundmail DNS records, then write them at Cloudflare, Vercel, or GoDaddy
1077
- * using the caller's provider credentials (never sent to Groundmail).
1455
+ * Fetch Groundmail DNS records, then write them at Cloudflare, Vercel, GoDaddy, Netlify,
1456
+ * DigitalOcean or DNSimple using the caller's provider credentials (never sent to Groundmail).
1078
1457
  */
1079
1458
  async applyDns(params) {
1080
1459
  const detail = await this.get(params.idOrName);
@@ -1089,10 +1468,15 @@ var DomainsResource = class {
1089
1468
  credentials: {
1090
1469
  cloudflareApiToken: params.cloudflareApiToken,
1091
1470
  cloudflareZoneId: params.cloudflareZoneId,
1471
+ godaddyApiToken: params.godaddyApiToken,
1092
1472
  godaddyApiKey: params.godaddyApiKey,
1093
1473
  godaddyApiSecret: params.godaddyApiSecret,
1094
1474
  vercelApiToken: params.vercelApiToken,
1095
- vercelTeamId: params.vercelTeamId
1475
+ vercelTeamId: params.vercelTeamId,
1476
+ netlifyApiToken: params.netlifyApiToken,
1477
+ digitaloceanApiToken: params.digitaloceanApiToken,
1478
+ dnsimpleApiToken: params.dnsimpleApiToken,
1479
+ dnsimpleAccountId: params.dnsimpleAccountId
1096
1480
  },
1097
1481
  fetchImpl: this.client.fetchImpl
1098
1482
  });
package/dist/clerk.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { G as Groundmail, S as SendEmailResult } from './client-DZNmwhee.js';
1
+ import { G as Groundmail, S as SendEmailResult } from './client-DdtHUMEu.js';
2
2
 
3
3
  /**
4
4
  * Subset of Clerk `email.created` webhook `data` used for delivery.
package/dist/clerk.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  DEFAULT_API_BASE_URL,
3
3
  Groundmail
4
- } from "./chunk-E6G7G2PM.js";
4
+ } from "./chunk-POS35D4J.js";
5
5
 
6
6
  // src/clerk.ts
7
7
  function resolveApiKey(options) {
@@ -1013,20 +1013,28 @@ type UpdateDomainParams = {
1013
1013
  /** What to do with received mail at or above the threshold. */
1014
1014
  inbound_spam_action?: 'flag' | 'drop';
1015
1015
  };
1016
- type DnsProviderName = 'cloudflare' | 'godaddy' | 'vercel';
1016
+ type DnsProviderName = 'cloudflare' | 'godaddy' | 'vercel' | 'netlify' | 'digitalocean' | 'dnsimple';
1017
1017
  type RemoteApplyDnsCredentials = {
1018
1018
  /** Inline Cloudflare token instead of the stored dashboard integration. */
1019
1019
  apiToken?: string;
1020
1020
  zoneId?: string;
1021
1021
  cloudflareApiToken?: string;
1022
1022
  cloudflareZoneId?: string;
1023
- /** GoDaddy one-time credentials. */
1023
+ /** GoDaddy one-time credentials: a Personal Access Token in apiToken, or the classic key and secret. */
1024
+ godaddyApiToken?: string;
1024
1025
  apiKey?: string;
1025
1026
  apiSecret?: string;
1026
1027
  godaddyApiKey?: string;
1027
1028
  godaddyApiSecret?: string;
1028
1029
  vercelApiToken?: string;
1029
1030
  vercelTeamId?: string;
1031
+ /** Inline token for netlify, digitalocean or dnsimple instead of the stored connection. */
1032
+ netlifyApiToken?: string;
1033
+ digitaloceanApiToken?: string;
1034
+ dnsimpleApiToken?: string;
1035
+ /** DNSimple account the inline token belongs to. */
1036
+ dnsimpleAccountId?: string;
1037
+ accountId?: string;
1030
1038
  };
1031
1039
  type ApplyDnsParams = {
1032
1040
  provider: DnsProviderName;
@@ -1034,10 +1042,16 @@ type ApplyDnsParams = {
1034
1042
  idOrName: string;
1035
1043
  cloudflareApiToken?: string;
1036
1044
  cloudflareZoneId?: string;
1045
+ /** GoDaddy Personal Access Token (preferred over the key and secret). */
1046
+ godaddyApiToken?: string;
1037
1047
  godaddyApiKey?: string;
1038
1048
  godaddyApiSecret?: string;
1039
1049
  vercelApiToken?: string;
1040
1050
  vercelTeamId?: string;
1051
+ netlifyApiToken?: string;
1052
+ digitaloceanApiToken?: string;
1053
+ dnsimpleApiToken?: string;
1054
+ dnsimpleAccountId?: string;
1041
1055
  };
1042
1056
  type ApplyDnsRecordResult = {
1043
1057
  purpose: string;
@@ -1514,7 +1528,7 @@ type DmarcMonitoringDisabled = {
1514
1528
  type DmarcStatus = DmarcMonitoring | DmarcMonitoringDisabled;
1515
1529
  type EnableDmarcParams = {
1516
1530
  /** Also write the record through a DNS provider connected under Settings → Integrations. */
1517
- apply?: 'cloudflare' | 'vercel' | 'godaddy';
1531
+ apply?: DnsProviderName;
1518
1532
  digest_enabled?: boolean;
1519
1533
  };
1520
1534
  type EnableDmarcResult = {
@@ -1561,16 +1575,17 @@ declare class DomainsResource {
1561
1575
  ok: boolean;
1562
1576
  }>;
1563
1577
  /**
1564
- * Apply DNS using the public API. Cloudflare, Vercel, and GoDaddy can use stored dashboard
1565
- * credentials; GoDaddy also accepts one-time credentials on the request.
1578
+ * Apply DNS using the public API with the DNS host connected in the dashboard (Cloudflare,
1579
+ * Vercel, GoDaddy, Netlify, DigitalOcean or DNSimple). GoDaddy also accepts one-time
1580
+ * credentials on the request.
1566
1581
  */
1567
1582
  apply(idOrName: string, options?: {
1568
- provider?: 'cloudflare' | 'godaddy' | 'vercel';
1583
+ provider?: DnsProviderName;
1569
1584
  credentials?: RemoteApplyDnsCredentials;
1570
1585
  }): Promise<ApplyDnsResult>;
1571
1586
  /**
1572
- * Fetch Groundmail DNS records, then write them at Cloudflare, Vercel, or GoDaddy
1573
- * using the caller's provider credentials (never sent to Groundmail).
1587
+ * Fetch Groundmail DNS records, then write them at Cloudflare, Vercel, GoDaddy, Netlify,
1588
+ * DigitalOcean or DNSimple using the caller's provider credentials (never sent to Groundmail).
1574
1589
  */
1575
1590
  applyDns(params: ApplyDnsParams): Promise<ApplyDnsResult>;
1576
1591
  }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { A as ApiErrorBody, D as DnsRecord } from './client-DZNmwhee.js';
2
- export { a as ApiErrorCode, b as ApiKey, c as ApiKeyScope, d as ApplyDnsParams, e as ApplyDnsRecordResult, f as ApplyDnsResult, g as Asset, h as Automation, i as AutomationResult, j as AutomationRun, k as AutomationRunResponse, l as AutomationRunStatus, m as AutomationRunStep, n as AutomationRunStepStatus, o as AutomationStatus, p as AutomationStep, q as AutomationTrigger, B as BatchSendEmailResult, r as BounceType, C as CategoryOptOut, s as CategoryOptOutCategory, t as CategoryUnsubscribedErrorDetails, u as ConnectTwilioParams, v as ContactWebhookEventPayload, w as CreateApiKeyParams, x as CreateApiKeyResult, y as CreateAutomationParams, z as CreateDomainParams, E as CreateDomainResult, F as CreateMigrationParams, H as CreateSuppressionParams, I as CreateTemplateParams, J as CreateTenantParams, K as CreateWebhookParams, L as DEFAULT_API_BASE_URL, M as DailyRampLimitErrorDetails, N as DeleteTenantResult, O as DeliverabilityBucket, P as DeliverabilityDomainInsight, Q as DeliverabilityInsightsResult, R as DeliverabilityMailServerRelay, T as DeliverabilityWindow, U as DmarcDailyPoint, V as DmarcMonitoring, W as DmarcMonitoringDisabled, X as DmarcReadiness, Y as DmarcReadinessVerdict, Z as DmarcSource, _ as DmarcSourceStatus, $ as DmarcStatus, a0 as DnsProviderName, a1 as Domain, a2 as DomainBimiAnalysis, a3 as DomainDetailResult, a4 as DomainDmarcAnalysis, a5 as DomainDnsAnalysis, a6 as DomainStatus, a7 as DomainTlsMode, a8 as DomainTrackingDns, a9 as DomainTrackingStatus, aa as EmailListItem, ab as EmailWebhookEventPayload, ac as EmailWebhookEventType, ad as EnableDmarcParams, ae as EnableDmarcResult, af as EnrollmentSkip, ag as EventRecord, G as Groundmail, ah as GroundmailAccessToken, ai as GroundmailOptions, aj as GroundmailTokenOptions, ak as GroundmailTokenProvider, al as HeldBackErrorDetails, am as HeldBackReason, an as HeldBackRecipient, ao as ListApiKeysParams, ap as ListApiKeysResult, aq as ListAutomationRunsParams, ar as ListAutomationRunsResult, as as ListAutomationsResult, at as ListCategoryOptOutsParams, au as ListCategoryOptOutsResult, av as ListDomainsParams, aw as ListDomainsResult, ax as ListEmailsParams, ay as ListEmailsResult, az as ListReceivedEmailsParams, aA as ListReceivedEmailsResult, aB as ListSuppressionsParams, aC as ListSuppressionsResult, aD as ListTemplatesParams, aE as ListTemplatesResult, aF as ListTenantDomainsResult, aG as ListTenantsParams, aH as ListTenantsResult, aI as ListTestInboxEmailsParams, aJ as ListTestInboxEmailsResult, aK as MailServerSigningStatus, aL as MailStream, aM as Migration, aN as MigrationDnsChange, aO as MigrationDnsPlan, aP as MigrationDomainItem, aQ as MigrationItemStatus, aR as MigrationProvider, aS as MigrationResource, aT as MigrationSummary, aU as MigrationTemplateItem, aV as MigrationTotals, aW as MigrationWebhookItem, aX as ReceivedEmail, aY as ReceivedEmailAttachment, aZ as ReceivedEmailAttachmentMeta, a_ as RemoteApplyDnsCredentials, a$ as RemoveCategoryOptOutOptions, b0 as RunMigrationOptions, b1 as SendEmailParams, S as SendEmailResult, b2 as StreamDeliverability, b3 as SuppressedErrorDetails, b4 as Suppression, b5 as TEST_INBOX_WAIT_DEFAULT_MS, b6 as TEST_INBOX_WAIT_MAX_SECONDS, b7 as Template, b8 as TemplateFormat, b9 as TemplateLintWarning, ba as TemplateStarter, bb as TemplateStatus, bc as TemplateVersion, bd as Tenant, be as TenantPauseSource, bf as TenantRamp, bg as TenantStats, bh as TenantStatsResult, bi as TenantStatus, bj as TenantStreamPolicy, bk as TestEmailWebhookParams, bl as TestEmailWebhookResult, bm as TestInbox, bn as TestInboxAddressOptions, bo as TestInboxEmail, bp as TestInboxEmailDetail, bq as TestInboxHeader, br as TestInboxResource, bs as TestInboxUsage, bt as TriggerEventParams, bu as TriggerEventResult, bv as TwilioIntegrationResponse, bw as TwilioIntegrationStatus, bx as UpdateAutomationParams, by as UpdateDomainParams, bz as UpdateEmailParams, bA as UpdateTemplateParams, bB as UpdateTenantParams, bC as UpdateWebhookParams, bD as ValidateEmailResult, bE as ValidationReason, bF as ValidationVerdict, bG as VerifyDomainResult, bH as WaitForTestEmailParams, bI as WebhookEmailTag, bJ as WebhookEndpoint, bK as WebhookEndpointWithSecret, bL as WebhookEventType, bM as mergeMigrations, bN as normalizeAccessToken } from './client-DZNmwhee.js';
1
+ import { A as ApiErrorBody, D as DnsRecord } from './client-DdtHUMEu.js';
2
+ export { a as ApiErrorCode, b as ApiKey, c as ApiKeyScope, d as ApplyDnsParams, e as ApplyDnsRecordResult, f as ApplyDnsResult, g as Asset, h as Automation, i as AutomationResult, j as AutomationRun, k as AutomationRunResponse, l as AutomationRunStatus, m as AutomationRunStep, n as AutomationRunStepStatus, o as AutomationStatus, p as AutomationStep, q as AutomationTrigger, B as BatchSendEmailResult, r as BounceType, C as CategoryOptOut, s as CategoryOptOutCategory, t as CategoryUnsubscribedErrorDetails, u as ConnectTwilioParams, v as ContactWebhookEventPayload, w as CreateApiKeyParams, x as CreateApiKeyResult, y as CreateAutomationParams, z as CreateDomainParams, E as CreateDomainResult, F as CreateMigrationParams, H as CreateSuppressionParams, I as CreateTemplateParams, J as CreateTenantParams, K as CreateWebhookParams, L as DEFAULT_API_BASE_URL, M as DailyRampLimitErrorDetails, N as DeleteTenantResult, O as DeliverabilityBucket, P as DeliverabilityDomainInsight, Q as DeliverabilityInsightsResult, R as DeliverabilityMailServerRelay, T as DeliverabilityWindow, U as DmarcDailyPoint, V as DmarcMonitoring, W as DmarcMonitoringDisabled, X as DmarcReadiness, Y as DmarcReadinessVerdict, Z as DmarcSource, _ as DmarcSourceStatus, $ as DmarcStatus, a0 as DnsProviderName, a1 as Domain, a2 as DomainBimiAnalysis, a3 as DomainDetailResult, a4 as DomainDmarcAnalysis, a5 as DomainDnsAnalysis, a6 as DomainStatus, a7 as DomainTlsMode, a8 as DomainTrackingDns, a9 as DomainTrackingStatus, aa as EmailListItem, ab as EmailWebhookEventPayload, ac as EmailWebhookEventType, ad as EnableDmarcParams, ae as EnableDmarcResult, af as EnrollmentSkip, ag as EventRecord, G as Groundmail, ah as GroundmailAccessToken, ai as GroundmailOptions, aj as GroundmailTokenOptions, ak as GroundmailTokenProvider, al as HeldBackErrorDetails, am as HeldBackReason, an as HeldBackRecipient, ao as ListApiKeysParams, ap as ListApiKeysResult, aq as ListAutomationRunsParams, ar as ListAutomationRunsResult, as as ListAutomationsResult, at as ListCategoryOptOutsParams, au as ListCategoryOptOutsResult, av as ListDomainsParams, aw as ListDomainsResult, ax as ListEmailsParams, ay as ListEmailsResult, az as ListReceivedEmailsParams, aA as ListReceivedEmailsResult, aB as ListSuppressionsParams, aC as ListSuppressionsResult, aD as ListTemplatesParams, aE as ListTemplatesResult, aF as ListTenantDomainsResult, aG as ListTenantsParams, aH as ListTenantsResult, aI as ListTestInboxEmailsParams, aJ as ListTestInboxEmailsResult, aK as MailServerSigningStatus, aL as MailStream, aM as Migration, aN as MigrationDnsChange, aO as MigrationDnsPlan, aP as MigrationDomainItem, aQ as MigrationItemStatus, aR as MigrationProvider, aS as MigrationResource, aT as MigrationSummary, aU as MigrationTemplateItem, aV as MigrationTotals, aW as MigrationWebhookItem, aX as ReceivedEmail, aY as ReceivedEmailAttachment, aZ as ReceivedEmailAttachmentMeta, a_ as RemoteApplyDnsCredentials, a$ as RemoveCategoryOptOutOptions, b0 as RunMigrationOptions, b1 as SendEmailParams, S as SendEmailResult, b2 as StreamDeliverability, b3 as SuppressedErrorDetails, b4 as Suppression, b5 as TEST_INBOX_WAIT_DEFAULT_MS, b6 as TEST_INBOX_WAIT_MAX_SECONDS, b7 as Template, b8 as TemplateFormat, b9 as TemplateLintWarning, ba as TemplateStarter, bb as TemplateStatus, bc as TemplateVersion, bd as Tenant, be as TenantPauseSource, bf as TenantRamp, bg as TenantStats, bh as TenantStatsResult, bi as TenantStatus, bj as TenantStreamPolicy, bk as TestEmailWebhookParams, bl as TestEmailWebhookResult, bm as TestInbox, bn as TestInboxAddressOptions, bo as TestInboxEmail, bp as TestInboxEmailDetail, bq as TestInboxHeader, br as TestInboxResource, bs as TestInboxUsage, bt as TriggerEventParams, bu as TriggerEventResult, bv as TwilioIntegrationResponse, bw as TwilioIntegrationStatus, bx as UpdateAutomationParams, by as UpdateDomainParams, bz as UpdateEmailParams, bA as UpdateTemplateParams, bB as UpdateTenantParams, bC as UpdateWebhookParams, bD as ValidateEmailResult, bE as ValidationReason, bF as ValidationVerdict, bG as VerifyDomainResult, bH as WaitForTestEmailParams, bI as WebhookEmailTag, bJ as WebhookEndpoint, bK as WebhookEndpointWithSecret, bL as WebhookEventType, bM as mergeMigrations, bN as normalizeAccessToken } from './client-DdtHUMEu.js';
3
3
 
4
4
  type GroundmailErrorOptions = {
5
5
  code?: string;
@@ -80,7 +80,7 @@ declare function isTestApiKey(key: string | null | undefined): boolean;
80
80
  /** Merge `include:host` into an existing SPF record (or create a minimal one). */
81
81
  declare function mergeSpfInclude(existing: string | null | undefined, includeHost: string): string;
82
82
 
83
- type DnsProviderName = 'cloudflare' | 'godaddy' | 'vercel';
83
+ type DnsProviderName = 'cloudflare' | 'godaddy' | 'vercel' | 'netlify' | 'digitalocean' | 'dnsimple';
84
84
  type ApplyDnsRecordResult = {
85
85
  purpose: string;
86
86
  host: string;
@@ -95,10 +95,16 @@ type ApplyDnsResult = {
95
95
  type ApplyDnsCredentials = {
96
96
  cloudflareApiToken?: string;
97
97
  cloudflareZoneId?: string;
98
+ /** GoDaddy Personal Access Token (preferred over the key and secret). */
99
+ godaddyApiToken?: string;
98
100
  godaddyApiKey?: string;
99
101
  godaddyApiSecret?: string;
100
102
  vercelApiToken?: string;
101
103
  vercelTeamId?: string;
104
+ netlifyApiToken?: string;
105
+ digitaloceanApiToken?: string;
106
+ dnsimpleApiToken?: string;
107
+ dnsimpleAccountId?: string;
102
108
  };
103
109
 
104
110
  declare function applyDnsRecords(input: {
package/dist/index.js CHANGED
@@ -12,7 +12,7 @@ import {
12
12
  normalizeAccessToken,
13
13
  signWebhookPayload,
14
14
  verifyWebhookSignature
15
- } from "./chunk-E6G7G2PM.js";
15
+ } from "./chunk-POS35D4J.js";
16
16
 
17
17
  // src/simulator.ts
18
18
  var SIMULATOR_DOMAIN = "simulator.groundmail.dev";
package/dist/payload.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { G as Groundmail } from './client-DZNmwhee.js';
1
+ import { G as Groundmail } from './client-DdtHUMEu.js';
2
2
 
3
3
  /**
4
4
  * Minimal Payload `SendEmailOptions` / address shapes used by this adapter.
package/dist/payload.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  DEFAULT_API_BASE_URL,
3
3
  Groundmail
4
- } from "./chunk-E6G7G2PM.js";
4
+ } from "./chunk-POS35D4J.js";
5
5
 
6
6
  // src/payload.ts
7
7
  function resolveApiKey(args) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "groundmail",
3
- "version": "0.26.1",
3
+ "version": "0.28.0",
4
4
  "description": "Groundmail transactional email API client",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -89,7 +89,7 @@
89
89
  "prepublishOnly": "npm run build && npm run build:nodemailer-bridge"
90
90
  },
91
91
  "dependencies": {
92
- "groundmail-nodemailer": "0.15.14"
92
+ "groundmail-nodemailer": "0.15.16"
93
93
  },
94
94
  "devDependencies": {
95
95
  "@types/nodemailer": "^8.0.1",