@wraps.dev/cli 3.11.0 → 3.11.1

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/cli.js CHANGED
@@ -24974,668 +24974,147 @@ var init_presets = __esm({
24974
24974
  }
24975
24975
  });
24976
24976
 
24977
- // src/utils/route53.ts
24978
- var route53_exports = {};
24979
- __export(route53_exports, {
24980
- createDNSRecords: () => createDNSRecords,
24981
- createSelectedDNSRecords: () => createSelectedDNSRecords,
24982
- deleteDNSRecords: () => deleteDNSRecords,
24983
- findHostedZone: () => findHostedZone,
24984
- mergeSPFRecord: () => mergeSPFRecord,
24985
- previewDNSChanges: () => previewDNSChanges
24977
+ // src/utils/dns/cloudflare.ts
24978
+ var cloudflare_exports = {};
24979
+ __export(cloudflare_exports, {
24980
+ CloudflareDNSClient: () => CloudflareDNSClient
24986
24981
  });
24987
- import {
24988
- ChangeResourceRecordSetsCommand,
24989
- ListHostedZonesByNameCommand,
24990
- ListResourceRecordSetsCommand,
24991
- Route53Client
24992
- } from "@aws-sdk/client-route-53";
24993
- async function getExistingTXTRecords(client2, hostedZoneId, domain) {
24994
- try {
24995
- const response = await client2.send(
24996
- new ListResourceRecordSetsCommand({
24997
- HostedZoneId: hostedZoneId,
24998
- StartRecordName: domain,
24999
- StartRecordType: "TXT",
25000
- MaxItems: 100
25001
- })
25002
- );
25003
- const txtRecordSet = response.ResourceRecordSets?.find(
25004
- (rs) => rs.Type === "TXT" && (rs.Name === domain || rs.Name === `${domain}.`)
25005
- );
25006
- if (!txtRecordSet?.ResourceRecords) {
25007
- return { allValues: [], spfValue: null, ttl: 1800 };
25008
- }
25009
- const allValues = [];
25010
- let spfValue = null;
25011
- for (const record of txtRecordSet.ResourceRecords) {
25012
- const value = record.Value || "";
25013
- allValues.push(value);
25014
- const unquoted = value.replace(/^"|"$/g, "");
25015
- if (unquoted.startsWith("v=spf1")) {
25016
- spfValue = unquoted;
25017
- }
25018
- }
25019
- return {
25020
- allValues,
25021
- spfValue,
25022
- ttl: txtRecordSet.TTL || 1800
25023
- };
25024
- } catch (_error) {
25025
- return { allValues: [], spfValue: null, ttl: 1800 };
25026
- }
25027
- }
25028
- function mergeSPFRecord(existingSPF) {
25029
- const sesInclude = "include:amazonses.com";
25030
- if (existingSPF.includes(sesInclude)) {
25031
- return existingSPF;
25032
- }
25033
- const allMatch = existingSPF.match(/\s([~+?-]?all)$/);
25034
- if (allMatch) {
25035
- const beforeAll = existingSPF.slice(0, allMatch.index);
25036
- const allPart = allMatch[1];
25037
- return `${beforeAll} ${sesInclude} ${allPart}`;
25038
- }
25039
- return `${existingSPF} ${sesInclude}`;
25040
- }
25041
- async function getExistingRecords(client2, hostedZoneId) {
25042
- try {
25043
- const response = await client2.send(
25044
- new ListResourceRecordSetsCommand({
25045
- HostedZoneId: hostedZoneId,
25046
- MaxItems: 500
25047
- })
25048
- );
25049
- return response.ResourceRecordSets || [];
25050
- } catch (_error) {
25051
- return [];
25052
- }
25053
- }
25054
- function findExistingRecord(records, name, type) {
25055
- const normalizedName = name.endsWith(".") ? name : `${name}.`;
25056
- return records.find((r6) => r6.Name === normalizedName && r6.Type === type) || null;
25057
- }
25058
- function getRecordValue(record) {
25059
- if (!record?.ResourceRecords?.length) {
25060
- return null;
25061
- }
25062
- return record.ResourceRecords.map((r6) => r6.Value || "").join(", ");
24982
+ function normalizeRecordValue(value) {
24983
+ return value.replace(/^"/, "").replace(/"$/, "").trim();
25063
24984
  }
25064
- async function previewDNSChanges(hostedZoneId, domain, dkimTokens, region, customTrackingDomain, mailFromDomain) {
25065
- const client2 = new Route53Client({ region });
25066
- const existingRecords = await getExistingRecords(client2, hostedZoneId);
25067
- const existingTXT = await getExistingTXTRecords(client2, hostedZoneId, domain);
25068
- const records = [];
25069
- for (const token of dkimTokens) {
25070
- const name = `${token}._domainkey.${domain}`;
25071
- const proposedValue = `${token}.dkim.amazonses.com`;
25072
- const existing = findExistingRecord(existingRecords, name, "CNAME");
25073
- const existingValue = getRecordValue(existing);
25074
- let status2 = "new";
25075
- if (existingValue === proposedValue) {
25076
- status2 = "no_change";
25077
- } else if (existingValue) {
25078
- status2 = "update";
25079
- }
25080
- records.push({
25081
- name,
25082
- type: "CNAME",
25083
- proposedValue,
25084
- existingValue,
25085
- status: status2,
25086
- category: "dkim"
25087
- });
25088
- }
25089
- const proposedSPF = existingTXT.spfValue ? mergeSPFRecord(existingTXT.spfValue) : "v=spf1 include:amazonses.com ~all";
25090
- let spfStatus = "new";
25091
- if (existingTXT.spfValue) {
25092
- if (existingTXT.spfValue === proposedSPF) {
25093
- spfStatus = "no_change";
25094
- } else if (existingTXT.spfValue.includes("include:amazonses.com")) {
25095
- spfStatus = "no_change";
25096
- } else {
25097
- spfStatus = "update";
25098
- }
25099
- }
25100
- records.push({
25101
- name: domain,
25102
- type: "TXT",
25103
- proposedValue: `"${proposedSPF}"`,
25104
- existingValue: existingTXT.spfValue ? `"${existingTXT.spfValue}"` : null,
25105
- status: spfStatus,
25106
- category: "spf",
25107
- conflictReason: void 0
25108
- });
25109
- const dmarcName = `_dmarc.${domain}`;
25110
- const dmarcRuaDomain = mailFromDomain || domain;
25111
- const proposedDMARC = `"v=DMARC1; p=quarantine; sp=quarantine; np=reject; rua=mailto:postmaster@${dmarcRuaDomain}"`;
25112
- const existingDMARC = findExistingRecord(existingRecords, dmarcName, "TXT");
25113
- const existingDMARCValue = getRecordValue(existingDMARC);
25114
- let dmarcStatus = "new";
25115
- let dmarcConflictReason;
25116
- if (existingDMARCValue) {
25117
- if (existingDMARCValue === proposedDMARC) {
25118
- dmarcStatus = "no_change";
25119
- } else {
25120
- dmarcStatus = "conflict";
25121
- dmarcConflictReason = "Existing DMARC policy will be replaced";
25122
- }
25123
- }
25124
- records.push({
25125
- name: dmarcName,
25126
- type: "TXT",
25127
- proposedValue: proposedDMARC,
25128
- existingValue: existingDMARCValue,
25129
- status: dmarcStatus,
25130
- category: "dmarc",
25131
- conflictReason: dmarcConflictReason
25132
- });
25133
- if (customTrackingDomain) {
25134
- const proposedTracking = `r.${region}.awstrack.me`;
25135
- const existingTracking = findExistingRecord(
25136
- existingRecords,
25137
- customTrackingDomain,
25138
- "CNAME"
25139
- );
25140
- const existingTrackingValue = getRecordValue(existingTracking);
25141
- let trackingStatus = "new";
25142
- let trackingConflictReason;
25143
- if (existingTrackingValue === proposedTracking) {
25144
- trackingStatus = "no_change";
25145
- } else if (existingTrackingValue) {
25146
- trackingStatus = "conflict";
25147
- trackingConflictReason = `Points to ${existingTrackingValue}, will be replaced`;
25148
- }
25149
- records.push({
25150
- name: customTrackingDomain,
25151
- type: "CNAME",
25152
- proposedValue: proposedTracking,
25153
- existingValue: existingTrackingValue,
25154
- status: trackingStatus,
25155
- category: "tracking",
25156
- conflictReason: trackingConflictReason
25157
- });
25158
- }
25159
- if (mailFromDomain) {
25160
- const proposedMX = `10 feedback-smtp.${region}.amazonses.com`;
25161
- const existingMX = findExistingRecord(
25162
- existingRecords,
25163
- mailFromDomain,
25164
- "MX"
25165
- );
25166
- const existingMXValue = getRecordValue(existingMX);
25167
- let mxStatus = "new";
25168
- if (existingMXValue === proposedMX) {
25169
- mxStatus = "no_change";
25170
- } else if (existingMXValue) {
25171
- mxStatus = "update";
25172
- }
25173
- records.push({
25174
- name: mailFromDomain,
25175
- type: "MX",
25176
- proposedValue: proposedMX,
25177
- existingValue: existingMXValue,
25178
- status: mxStatus,
25179
- category: "mailfrom_mx"
25180
- });
25181
- const mailFromSPF = '"v=spf1 include:amazonses.com ~all"';
25182
- const existingMailFromTXT = await getExistingTXTRecords(
25183
- client2,
25184
- hostedZoneId,
25185
- mailFromDomain
25186
- );
25187
- let mailFromSpfStatus = "new";
25188
- if (existingMailFromTXT.spfValue?.includes("include:amazonses.com")) {
25189
- mailFromSpfStatus = "no_change";
25190
- } else if (existingMailFromTXT.spfValue) {
25191
- mailFromSpfStatus = "update";
25192
- }
25193
- records.push({
25194
- name: mailFromDomain,
25195
- type: "TXT",
25196
- proposedValue: mailFromSPF,
25197
- existingValue: existingMailFromTXT.spfValue ? `"${existingMailFromTXT.spfValue}"` : null,
25198
- status: mailFromSpfStatus,
25199
- category: "mailfrom_spf"
25200
- });
24985
+ function valuesMatch(type, actual, expected) {
24986
+ const normalizedActual = normalizeRecordValue(actual);
24987
+ const normalizedExpected = normalizeRecordValue(expected);
24988
+ if (type === "MX") {
24989
+ return normalizedActual.toLowerCase() === normalizedExpected.toLowerCase();
25201
24990
  }
25202
- const conflictCount = records.filter((r6) => r6.status === "conflict").length;
25203
- const newCount = records.filter((r6) => r6.status === "new").length;
25204
- const updateCount = records.filter((r6) => r6.status === "update").length;
25205
- const noChangeCount = records.filter((r6) => r6.status === "no_change").length;
25206
- return {
25207
- records,
25208
- hasConflicts: conflictCount > 0,
25209
- conflictCount,
25210
- newCount,
25211
- updateCount,
25212
- noChangeCount
25213
- };
24991
+ return normalizedActual === normalizedExpected;
25214
24992
  }
25215
- async function createSelectedDNSRecords(hostedZoneId, domain, dkimTokens, region, selectedCategories, customTrackingDomain, mailFromDomain, trackingCnameTarget) {
25216
- const client2 = new Route53Client({ region });
25217
- const changes = [];
25218
- if (selectedCategories.has("dkim")) {
25219
- for (const token of dkimTokens) {
25220
- changes.push({
25221
- Action: "UPSERT",
25222
- ResourceRecordSet: {
25223
- Name: `${token}._domainkey.${domain}`,
25224
- Type: "CNAME",
25225
- TTL: 1800,
25226
- ResourceRecords: [{ Value: `${token}.dkim.amazonses.com` }]
25227
- }
25228
- });
25229
- }
25230
- }
25231
- if (selectedCategories.has("spf")) {
25232
- const existingTXT = await getExistingTXTRecords(
25233
- client2,
25234
- hostedZoneId,
25235
- domain
25236
- );
25237
- const newTXTValues = [];
25238
- if (existingTXT.spfValue) {
25239
- const mergedSPF = mergeSPFRecord(existingTXT.spfValue);
25240
- newTXTValues.push(`"${mergedSPF}"`);
25241
- for (const value of existingTXT.allValues) {
25242
- const unquoted = value.replace(/^"|"$/g, "");
25243
- if (!unquoted.startsWith("v=spf1")) {
25244
- newTXTValues.push(value);
25245
- }
25246
- }
25247
- } else if (existingTXT.allValues.length > 0) {
25248
- newTXTValues.push('"v=spf1 include:amazonses.com ~all"');
25249
- newTXTValues.push(...existingTXT.allValues);
25250
- } else {
25251
- newTXTValues.push('"v=spf1 include:amazonses.com ~all"');
25252
- }
25253
- changes.push({
25254
- Action: "UPSERT",
25255
- ResourceRecordSet: {
25256
- Name: domain,
25257
- Type: "TXT",
25258
- TTL: existingTXT.ttl,
25259
- ResourceRecords: newTXTValues.map((v) => ({ Value: v }))
24993
+ var CLOUDFLARE_API_BASE, CloudflareDNSClient;
24994
+ var init_cloudflare = __esm({
24995
+ "src/utils/dns/cloudflare.ts"() {
24996
+ "use strict";
24997
+ init_esm_shims();
24998
+ CLOUDFLARE_API_BASE = "https://api.cloudflare.com/client/v4";
24999
+ CloudflareDNSClient = class {
25000
+ zoneId;
25001
+ apiToken;
25002
+ constructor(zoneId, apiToken) {
25003
+ this.zoneId = zoneId;
25004
+ this.apiToken = apiToken;
25260
25005
  }
25261
- });
25262
- }
25263
- if (selectedCategories.has("dmarc")) {
25264
- const dmarcRuaDomain = mailFromDomain || domain;
25265
- changes.push({
25266
- Action: "UPSERT",
25267
- ResourceRecordSet: {
25268
- Name: `_dmarc.${domain}`,
25269
- Type: "TXT",
25270
- TTL: 1800,
25271
- ResourceRecords: [
25006
+ async request(endpoint, method = "GET", body) {
25007
+ const response = await fetch(
25008
+ `${CLOUDFLARE_API_BASE}/zones/${this.zoneId}${endpoint}`,
25272
25009
  {
25273
- Value: `"v=DMARC1; p=quarantine; sp=quarantine; np=reject; rua=mailto:postmaster@${dmarcRuaDomain}"`
25010
+ method,
25011
+ headers: {
25012
+ Authorization: `Bearer ${this.apiToken}`,
25013
+ "Content-Type": "application/json"
25014
+ },
25015
+ body: body ? JSON.stringify(body) : void 0
25274
25016
  }
25275
- ]
25017
+ );
25018
+ return response.json();
25276
25019
  }
25277
- });
25278
- }
25279
- if (selectedCategories.has("tracking") && customTrackingDomain) {
25280
- changes.push({
25281
- Action: "UPSERT",
25282
- ResourceRecordSet: {
25283
- Name: customTrackingDomain,
25284
- Type: "CNAME",
25285
- TTL: 1800,
25286
- ResourceRecords: [
25287
- { Value: trackingCnameTarget ?? `r.${region}.awstrack.me` }
25288
- ]
25020
+ async createRecord(name, type, content, priority) {
25021
+ const recordContent = type === "TXT" ? `"${content}"` : content;
25022
+ const body = {
25023
+ name,
25024
+ type,
25025
+ content: recordContent,
25026
+ ttl: 1800,
25027
+ proxied: false
25028
+ // Must not be proxied for email records
25029
+ };
25030
+ if (priority !== void 0) {
25031
+ body.priority = priority;
25032
+ }
25033
+ const result = await this.request(
25034
+ "/dns_records",
25035
+ "POST",
25036
+ body
25037
+ );
25038
+ return result.success;
25289
25039
  }
25290
- });
25291
- }
25292
- if (selectedCategories.has("mailfrom_mx") && mailFromDomain) {
25293
- changes.push({
25294
- Action: "UPSERT",
25295
- ResourceRecordSet: {
25296
- Name: mailFromDomain,
25297
- Type: "MX",
25298
- TTL: 1800,
25299
- ResourceRecords: [
25300
- { Value: `10 feedback-smtp.${region}.amazonses.com` }
25301
- ]
25040
+ async findRecord(name, type) {
25041
+ const result = await this.request(
25042
+ `/dns_records?name=${encodeURIComponent(name)}&type=${type}`
25043
+ );
25044
+ if (result.success && result.result.length > 0) {
25045
+ return result.result[0];
25046
+ }
25047
+ return null;
25302
25048
  }
25303
- });
25304
- }
25305
- if (selectedCategories.has("mailfrom_spf") && mailFromDomain) {
25306
- changes.push({
25307
- Action: "UPSERT",
25308
- ResourceRecordSet: {
25309
- Name: mailFromDomain,
25310
- Type: "TXT",
25311
- TTL: 1800,
25312
- ResourceRecords: [{ Value: '"v=spf1 include:amazonses.com ~all"' }]
25049
+ /**
25050
+ * All records matching name+type. Unlike `findRecord`, this returns every
25051
+ * match — callers that need to know whether a name ALREADY has mail records
25052
+ * cannot use a single-result lookup.
25053
+ */
25054
+ async listRecords(name, type) {
25055
+ const result = await this.request(
25056
+ `/dns_records?name=${encodeURIComponent(name)}&type=${type}`
25057
+ );
25058
+ if (!(result.success && result.result)) {
25059
+ return [];
25060
+ }
25061
+ return result.result.map((r6) => ({
25062
+ content: r6.content,
25063
+ priority: r6.priority
25064
+ }));
25313
25065
  }
25314
- });
25315
- }
25316
- if (changes.length === 0) {
25317
- return;
25318
- }
25319
- await client2.send(
25320
- new ChangeResourceRecordSetsCommand({
25321
- HostedZoneId: hostedZoneId,
25322
- ChangeBatch: {
25323
- Changes: changes
25066
+ async deleteRecord(recordId) {
25067
+ const result = await this.request(
25068
+ `/dns_records/${recordId}`,
25069
+ "DELETE"
25070
+ );
25071
+ return result.success;
25324
25072
  }
25325
- })
25326
- );
25327
- }
25328
- async function findHostedZone(domain, region) {
25329
- const client2 = new Route53Client({ region });
25330
- try {
25331
- const response = await client2.send(
25332
- new ListHostedZonesByNameCommand({
25333
- DNSName: domain,
25334
- MaxItems: 1
25335
- })
25336
- );
25337
- const zone = response.HostedZones?.[0];
25338
- if (zone && zone.Name === `${domain}.` && zone.Id) {
25339
- return {
25340
- id: zone.Id.replace("/hostedzone/", ""),
25341
- name: zone.Name
25342
- };
25343
- }
25344
- return null;
25345
- } catch (_error) {
25346
- return null;
25347
- }
25348
- }
25349
- async function createDNSRecords(hostedZoneId, domain, dkimTokens, region, customTrackingDomain, mailFromDomain, cloudFrontDomain) {
25350
- const client2 = new Route53Client({ region });
25351
- const changes = [];
25352
- for (const token of dkimTokens) {
25353
- changes.push({
25354
- Action: "UPSERT",
25355
- ResourceRecordSet: {
25356
- Name: `${token}._domainkey.${domain}`,
25357
- Type: "CNAME",
25358
- TTL: 1800,
25359
- ResourceRecords: [{ Value: `${token}.dkim.amazonses.com` }]
25073
+ /**
25074
+ * Delete an existing record by name+type, if one exists. `createRecord`
25075
+ * always POSTs a new record rather than upserting, so callers that need
25076
+ * to change an existing record's value (e.g. swapping the tracking CNAME
25077
+ * target) must delete the old one first to avoid ending up with two.
25078
+ */
25079
+ async deleteRecordIfExists(name, type) {
25080
+ const existing = await this.findRecord(name, type);
25081
+ if (existing) {
25082
+ await this.deleteRecord(existing.id);
25083
+ }
25360
25084
  }
25361
- });
25362
- }
25363
- const existingTXT = await getExistingTXTRecords(client2, hostedZoneId, domain);
25364
- const newTXTValues = [];
25365
- if (existingTXT.spfValue) {
25366
- const mergedSPF = mergeSPFRecord(existingTXT.spfValue);
25367
- newTXTValues.push(`"${mergedSPF}"`);
25368
- for (const value of existingTXT.allValues) {
25369
- const unquoted = value.replace(/^"|"$/g, "");
25370
- if (!unquoted.startsWith("v=spf1")) {
25371
- newTXTValues.push(value);
25372
- }
25373
- }
25374
- } else if (existingTXT.allValues.length > 0) {
25375
- newTXTValues.push('"v=spf1 include:amazonses.com ~all"');
25376
- newTXTValues.push(...existingTXT.allValues);
25377
- } else {
25378
- newTXTValues.push('"v=spf1 include:amazonses.com ~all"');
25379
- }
25380
- changes.push({
25381
- Action: "UPSERT",
25382
- ResourceRecordSet: {
25383
- Name: domain,
25384
- Type: "TXT",
25385
- TTL: existingTXT.ttl,
25386
- ResourceRecords: newTXTValues.map((v) => ({ Value: v }))
25387
- }
25388
- });
25389
- const dmarcRuaDomain = mailFromDomain || domain;
25390
- changes.push({
25391
- Action: "UPSERT",
25392
- ResourceRecordSet: {
25393
- Name: `_dmarc.${domain}`,
25394
- Type: "TXT",
25395
- TTL: 1800,
25396
- ResourceRecords: [
25397
- {
25398
- Value: `"v=DMARC1; p=quarantine; sp=quarantine; np=reject; rua=mailto:postmaster@${dmarcRuaDomain}"`
25399
- }
25400
- ]
25401
- }
25402
- });
25403
- if (customTrackingDomain) {
25404
- const targetDomain = cloudFrontDomain || `r.${region}.awstrack.me`;
25405
- changes.push({
25406
- Action: "UPSERT",
25407
- ResourceRecordSet: {
25408
- Name: customTrackingDomain,
25409
- Type: "CNAME",
25410
- TTL: 1800,
25411
- ResourceRecords: [{ Value: targetDomain }]
25412
- }
25413
- });
25414
- }
25415
- if (mailFromDomain) {
25416
- changes.push({
25417
- Action: "UPSERT",
25418
- ResourceRecordSet: {
25419
- Name: mailFromDomain,
25420
- Type: "MX",
25421
- TTL: 1800,
25422
- ResourceRecords: [
25423
- { Value: `10 feedback-smtp.${region}.amazonses.com` }
25424
- ]
25425
- }
25426
- });
25427
- changes.push({
25428
- Action: "UPSERT",
25429
- ResourceRecordSet: {
25430
- Name: mailFromDomain,
25431
- Type: "TXT",
25432
- TTL: 1800,
25433
- ResourceRecords: [{ Value: '"v=spf1 include:amazonses.com ~all"' }]
25434
- }
25435
- });
25436
- }
25437
- await client2.send(
25438
- new ChangeResourceRecordSetsCommand({
25439
- HostedZoneId: hostedZoneId,
25440
- ChangeBatch: {
25441
- Changes: changes
25442
- }
25443
- })
25444
- );
25445
- }
25446
- async function deleteDNSRecords(hostedZoneId, domain, dkimTokens, region, customTrackingDomain, mailFromDomain) {
25447
- const client2 = new Route53Client({ region });
25448
- const response = await client2.send(
25449
- new ListResourceRecordSetsCommand({
25450
- HostedZoneId: hostedZoneId,
25451
- MaxItems: 500
25452
- })
25453
- );
25454
- const recordSets = response.ResourceRecordSets || [];
25455
- const changes = [];
25456
- const addDeletionIfExists = (name, type) => {
25457
- const normalizedName = name.endsWith(".") ? name : `${name}.`;
25458
- const record = recordSets.find(
25459
- (rs) => rs.Name === normalizedName && rs.Type === type
25460
- );
25461
- if (record?.ResourceRecords) {
25462
- changes.push({
25463
- Action: "DELETE",
25464
- ResourceRecordSet: record
25465
- });
25466
- }
25467
- };
25468
- for (const token of dkimTokens) {
25469
- addDeletionIfExists(`${token}._domainkey.${domain}`, "CNAME");
25470
- }
25471
- addDeletionIfExists(`_dmarc.${domain}`, "TXT");
25472
- if (customTrackingDomain) {
25473
- addDeletionIfExists(customTrackingDomain, "CNAME");
25474
- }
25475
- if (mailFromDomain) {
25476
- addDeletionIfExists(mailFromDomain, "MX");
25477
- addDeletionIfExists(mailFromDomain, "TXT");
25478
- }
25479
- if (changes.length === 0) {
25480
- return;
25481
- }
25482
- await client2.send(
25483
- new ChangeResourceRecordSetsCommand({
25484
- HostedZoneId: hostedZoneId,
25485
- ChangeBatch: {
25486
- Changes: changes
25487
- }
25488
- })
25489
- );
25490
- }
25491
- var init_route53 = __esm({
25492
- "src/utils/route53.ts"() {
25493
- "use strict";
25494
- init_esm_shims();
25495
- }
25496
- });
25497
-
25498
- // src/utils/dns/cloudflare.ts
25499
- var cloudflare_exports = {};
25500
- __export(cloudflare_exports, {
25501
- CloudflareDNSClient: () => CloudflareDNSClient
25502
- });
25503
- function normalizeRecordValue(value) {
25504
- return value.replace(/^"/, "").replace(/"$/, "").trim();
25505
- }
25506
- function valuesMatch(type, actual, expected) {
25507
- const normalizedActual = normalizeRecordValue(actual);
25508
- const normalizedExpected = normalizeRecordValue(expected);
25509
- if (type === "MX") {
25510
- return normalizedActual.toLowerCase() === normalizedExpected.toLowerCase();
25511
- }
25512
- return normalizedActual === normalizedExpected;
25513
- }
25514
- var CLOUDFLARE_API_BASE, CloudflareDNSClient;
25515
- var init_cloudflare = __esm({
25516
- "src/utils/dns/cloudflare.ts"() {
25517
- "use strict";
25518
- init_esm_shims();
25519
- CLOUDFLARE_API_BASE = "https://api.cloudflare.com/client/v4";
25520
- CloudflareDNSClient = class {
25521
- zoneId;
25522
- apiToken;
25523
- constructor(zoneId, apiToken) {
25524
- this.zoneId = zoneId;
25525
- this.apiToken = apiToken;
25526
- }
25527
- async request(endpoint, method = "GET", body) {
25528
- const response = await fetch(
25529
- `${CLOUDFLARE_API_BASE}/zones/${this.zoneId}${endpoint}`,
25530
- {
25531
- method,
25532
- headers: {
25533
- Authorization: `Bearer ${this.apiToken}`,
25534
- "Content-Type": "application/json"
25535
- },
25536
- body: body ? JSON.stringify(body) : void 0
25537
- }
25538
- );
25539
- return response.json();
25540
- }
25541
- async createRecord(name, type, content, priority) {
25542
- const recordContent = type === "TXT" ? `"${content}"` : content;
25543
- const body = {
25544
- name,
25545
- type,
25546
- content: recordContent,
25547
- ttl: 1800,
25548
- proxied: false
25549
- // Must not be proxied for email records
25550
- };
25551
- if (priority !== void 0) {
25552
- body.priority = priority;
25553
- }
25554
- const result = await this.request(
25555
- "/dns_records",
25556
- "POST",
25557
- body
25558
- );
25559
- return result.success;
25560
- }
25561
- async findRecord(name, type) {
25562
- const result = await this.request(
25563
- `/dns_records?name=${encodeURIComponent(name)}&type=${type}`
25564
- );
25565
- if (result.success && result.result.length > 0) {
25566
- return result.result[0];
25567
- }
25568
- return null;
25569
- }
25570
- /**
25571
- * All records matching name+type. Unlike `findRecord`, this returns every
25572
- * match — callers that need to know whether a name ALREADY has mail records
25573
- * cannot use a single-result lookup.
25574
- */
25575
- async listRecords(name, type) {
25576
- const result = await this.request(
25577
- `/dns_records?name=${encodeURIComponent(name)}&type=${type}`
25578
- );
25579
- if (!(result.success && result.result)) {
25580
- return [];
25581
- }
25582
- return result.result.map((r6) => ({
25583
- content: r6.content,
25584
- priority: r6.priority
25585
- }));
25586
- }
25587
- async deleteRecord(recordId) {
25588
- const result = await this.request(
25589
- `/dns_records/${recordId}`,
25590
- "DELETE"
25591
- );
25592
- return result.success;
25593
- }
25594
- /**
25595
- * Delete an existing record by name+type, if one exists. `createRecord`
25596
- * always POSTs a new record rather than upserting, so callers that need
25597
- * to change an existing record's value (e.g. swapping the tracking CNAME
25598
- * target) must delete the old one first to avoid ending up with two.
25599
- */
25600
- async deleteRecordIfExists(name, type) {
25601
- const existing = await this.findRecord(name, type);
25602
- if (existing) {
25603
- await this.deleteRecord(existing.id);
25604
- }
25605
- }
25606
- /**
25607
- * Delete records matching name + type + exact value. Value matching is the
25608
- * point: a name can carry several TXT or MX records, and deleting "the first
25609
- * one" can remove a record Wraps did not create.
25610
- */
25611
- async deleteRecordsByValue(name, type, value) {
25612
- let result;
25613
- try {
25614
- result = await this.request(
25615
- `/dns_records?name=${encodeURIComponent(name)}&type=${type}`
25616
- );
25617
- } catch (error) {
25618
- return {
25619
- deleted: 0,
25620
- error: error instanceof Error ? error.message : "Unknown error"
25621
- };
25622
- }
25623
- if (!result.success) {
25624
- return {
25625
- deleted: 0,
25626
- error: result.errors?.map((e6) => e6.message).join("; ") || "Failed to list DNS records"
25627
- };
25628
- }
25629
- const matches = result.result.filter(
25630
- (record) => valuesMatch(type, record.content, value)
25631
- );
25632
- let deleted = 0;
25633
- for (const record of matches) {
25634
- if (await this.deleteRecord(record.id)) {
25635
- deleted++;
25636
- }
25637
- }
25638
- return { deleted };
25085
+ /**
25086
+ * Delete records matching name + type + exact value. Value matching is the
25087
+ * point: a name can carry several TXT or MX records, and deleting "the first
25088
+ * one" can remove a record Wraps did not create.
25089
+ */
25090
+ async deleteRecordsByValue(name, type, value) {
25091
+ let result;
25092
+ try {
25093
+ result = await this.request(
25094
+ `/dns_records?name=${encodeURIComponent(name)}&type=${type}`
25095
+ );
25096
+ } catch (error) {
25097
+ return {
25098
+ deleted: 0,
25099
+ error: error instanceof Error ? error.message : "Unknown error"
25100
+ };
25101
+ }
25102
+ if (!result.success) {
25103
+ return {
25104
+ deleted: 0,
25105
+ error: result.errors?.map((e6) => e6.message).join("; ") || "Failed to list DNS records"
25106
+ };
25107
+ }
25108
+ const matches = result.result.filter(
25109
+ (record) => valuesMatch(type, record.content, value)
25110
+ );
25111
+ let deleted = 0;
25112
+ for (const record of matches) {
25113
+ if (await this.deleteRecord(record.id)) {
25114
+ deleted++;
25115
+ }
25116
+ }
25117
+ return { deleted };
25639
25118
  }
25640
25119
  /**
25641
25120
  * Create arbitrary DNS records from a list
@@ -26227,174 +25706,896 @@ var init_vercel = __esm({
26227
25706
  ]
26228
25707
  };
26229
25708
  }
26230
- }
26231
- async deleteEmailRecords(data) {
26232
- const { domain, dkimTokens, mailFromDomain } = data;
26233
- const errors2 = [];
26234
- let recordsCreated = 0;
26235
- try {
26236
- for (const token of dkimTokens) {
26237
- const name = `${token}._domainkey.${domain}`;
26238
- const record = await this.findRecord(name, "CNAME");
26239
- if (record) {
26240
- const success = await this.deleteRecord(record.id);
26241
- if (success) {
26242
- recordsCreated++;
26243
- } else {
26244
- errors2.push(`Failed to delete DKIM record: ${name}`);
26245
- }
26246
- }
26247
- }
26248
- const dmarcRecord = await this.findRecord(`_dmarc.${domain}`, "TXT");
26249
- if (dmarcRecord) {
26250
- const success = await this.deleteRecord(dmarcRecord.id);
26251
- if (success) {
26252
- recordsCreated++;
26253
- } else {
26254
- errors2.push("Failed to delete DMARC record");
26255
- }
26256
- }
26257
- if (mailFromDomain) {
26258
- const mxRecord = await this.findRecord(mailFromDomain, "MX");
26259
- if (mxRecord) {
26260
- const success = await this.deleteRecord(mxRecord.id);
26261
- if (success) {
26262
- recordsCreated++;
26263
- } else {
26264
- errors2.push(`Failed to delete MX record for ${mailFromDomain}`);
26265
- }
26266
- }
26267
- const spfRecord = await this.findRecord(mailFromDomain, "TXT");
26268
- if (spfRecord) {
26269
- const success = await this.deleteRecord(spfRecord.id);
26270
- if (success) {
26271
- recordsCreated++;
26272
- } else {
26273
- errors2.push(`Failed to delete SPF record for ${mailFromDomain}`);
26274
- }
26275
- }
26276
- }
26277
- return {
26278
- success: errors2.length === 0,
26279
- recordsCreated,
26280
- errors: errors2.length > 0 ? errors2 : void 0
26281
- };
26282
- } catch (error) {
26283
- return {
26284
- success: false,
26285
- recordsCreated,
26286
- errors: [
26287
- ...errors2,
26288
- error instanceof Error ? error.message : "Unknown error"
26289
- ]
26290
- };
25709
+ }
25710
+ async deleteEmailRecords(data) {
25711
+ const { domain, dkimTokens, mailFromDomain } = data;
25712
+ const errors2 = [];
25713
+ let recordsCreated = 0;
25714
+ try {
25715
+ for (const token of dkimTokens) {
25716
+ const name = `${token}._domainkey.${domain}`;
25717
+ const record = await this.findRecord(name, "CNAME");
25718
+ if (record) {
25719
+ const success = await this.deleteRecord(record.id);
25720
+ if (success) {
25721
+ recordsCreated++;
25722
+ } else {
25723
+ errors2.push(`Failed to delete DKIM record: ${name}`);
25724
+ }
25725
+ }
25726
+ }
25727
+ const dmarcRecord = await this.findRecord(`_dmarc.${domain}`, "TXT");
25728
+ if (dmarcRecord) {
25729
+ const success = await this.deleteRecord(dmarcRecord.id);
25730
+ if (success) {
25731
+ recordsCreated++;
25732
+ } else {
25733
+ errors2.push("Failed to delete DMARC record");
25734
+ }
25735
+ }
25736
+ if (mailFromDomain) {
25737
+ const mxRecord = await this.findRecord(mailFromDomain, "MX");
25738
+ if (mxRecord) {
25739
+ const success = await this.deleteRecord(mxRecord.id);
25740
+ if (success) {
25741
+ recordsCreated++;
25742
+ } else {
25743
+ errors2.push(`Failed to delete MX record for ${mailFromDomain}`);
25744
+ }
25745
+ }
25746
+ const spfRecord = await this.findRecord(mailFromDomain, "TXT");
25747
+ if (spfRecord) {
25748
+ const success = await this.deleteRecord(spfRecord.id);
25749
+ if (success) {
25750
+ recordsCreated++;
25751
+ } else {
25752
+ errors2.push(`Failed to delete SPF record for ${mailFromDomain}`);
25753
+ }
25754
+ }
25755
+ }
25756
+ return {
25757
+ success: errors2.length === 0,
25758
+ recordsCreated,
25759
+ errors: errors2.length > 0 ? errors2 : void 0
25760
+ };
25761
+ } catch (error) {
25762
+ return {
25763
+ success: false,
25764
+ recordsCreated,
25765
+ errors: [
25766
+ ...errors2,
25767
+ error instanceof Error ? error.message : "Unknown error"
25768
+ ]
25769
+ };
25770
+ }
25771
+ }
25772
+ /**
25773
+ * Get all CAA records for the domain
25774
+ */
25775
+ async getCAARecords() {
25776
+ const result = await this.request(
25777
+ `/v4/domains/${this.domain}/records`
25778
+ );
25779
+ if (result.error || !result.records) {
25780
+ return [];
25781
+ }
25782
+ return result.records.filter((r6) => r6.type === "CAA").map((r6) => {
25783
+ const match = r6.value.match(/^(\d+)\s+(\w+)\s+"?([^"]+)"?$/);
25784
+ if (match) {
25785
+ return {
25786
+ flags: Number.parseInt(match[1], 10),
25787
+ tag: match[2],
25788
+ value: match[3]
25789
+ };
25790
+ }
25791
+ return null;
25792
+ }).filter(
25793
+ (r6) => r6 !== null
25794
+ );
25795
+ }
25796
+ /**
25797
+ * Check if Amazon is allowed to issue certificates based on CAA records
25798
+ * Returns true if:
25799
+ * - No CAA records exist (any CA can issue)
25800
+ * - CAA records exist and include amazon.com or amazontrust.com
25801
+ */
25802
+ async isAmazonCAAAllowed() {
25803
+ const caaRecords = await this.getCAARecords();
25804
+ const issueRecords = caaRecords.filter(
25805
+ (r6) => r6.tag === "issue" || r6.tag === "issuewild"
25806
+ );
25807
+ if (issueRecords.length === 0) {
25808
+ return { allowed: true, hasCAA: false, existingCAs: [] };
25809
+ }
25810
+ const existingCAs = issueRecords.map((r6) => r6.value);
25811
+ const amazonAllowed = existingCAs.some(
25812
+ (ca) => ca.includes("amazon.com") || ca.includes("amazontrust.com")
25813
+ );
25814
+ return { allowed: amazonAllowed, hasCAA: true, existingCAs };
25815
+ }
25816
+ /**
25817
+ * Add a CAA record to allow Amazon to issue certificates
25818
+ */
25819
+ async addAmazonCAARecord() {
25820
+ const body = {
25821
+ name: "@",
25822
+ // Root domain
25823
+ type: "CAA",
25824
+ value: '0 issue "amazon.com"',
25825
+ ttl: 1800
25826
+ };
25827
+ const result = await this.request(
25828
+ `/v2/domains/${this.domain}/records`,
25829
+ "POST",
25830
+ body
25831
+ );
25832
+ return !result.error;
25833
+ }
25834
+ async verifyRecords(data) {
25835
+ const { domain, dkimTokens, mailFromDomain, region } = data;
25836
+ const missing = [];
25837
+ const incorrect = [];
25838
+ for (const token of dkimTokens) {
25839
+ const name = `${token}._domainkey.${domain}`;
25840
+ const expectedValue = `${token}.dkim.amazonses.com`;
25841
+ const record = await this.findRecord(name, "CNAME");
25842
+ if (!record) {
25843
+ missing.push(`DKIM: ${name}`);
25844
+ } else if (record.value !== expectedValue) {
25845
+ incorrect.push(`DKIM: ${name} (expected ${expectedValue})`);
25846
+ }
25847
+ }
25848
+ const spfRecord = await this.findRecord(domain, "TXT");
25849
+ if (!spfRecord) {
25850
+ missing.push(`SPF: ${domain}`);
25851
+ } else if (!spfRecord.value.includes("include:amazonses.com")) {
25852
+ incorrect.push(`SPF: ${domain} (missing amazonses.com include)`);
25853
+ }
25854
+ const dmarcRecord = await this.findRecord(`_dmarc.${domain}`, "TXT");
25855
+ if (!dmarcRecord) {
25856
+ missing.push(`DMARC: _dmarc.${domain}`);
25857
+ }
25858
+ if (mailFromDomain) {
25859
+ const mxRecord = await this.findRecord(mailFromDomain, "MX");
25860
+ if (!mxRecord) {
25861
+ missing.push(`MX: ${mailFromDomain}`);
25862
+ } else if (!mxRecord.value.includes(`feedback-smtp.${region}.amazonses.com`)) {
25863
+ incorrect.push(`MX: ${mailFromDomain}`);
25864
+ }
25865
+ const mailFromSpf = await this.findRecord(mailFromDomain, "TXT");
25866
+ if (!mailFromSpf) {
25867
+ missing.push(`SPF: ${mailFromDomain}`);
25868
+ }
25869
+ }
25870
+ return {
25871
+ verified: missing.length === 0 && incorrect.length === 0,
25872
+ missing,
25873
+ incorrect
25874
+ };
25875
+ }
25876
+ };
25877
+ }
25878
+ });
25879
+
25880
+ // src/utils/dns/email-dns-cleanup.ts
25881
+ var email_dns_cleanup_exports = {};
25882
+ __export(email_dns_cleanup_exports, {
25883
+ deleteEmailDNSRecordsForProvider: () => deleteEmailDNSRecordsForProvider
25884
+ });
25885
+ import {
25886
+ ChangeResourceRecordSetsCommand,
25887
+ ListResourceRecordSetsCommand,
25888
+ Route53Client
25889
+ } from "@aws-sdk/client-route-53";
25890
+ async function deleteViaValueMatchedClient(client2, records) {
25891
+ const deleted = [];
25892
+ const skipped = [];
25893
+ const errors2 = [];
25894
+ for (const record of records) {
25895
+ const label = `${record.type} ${record.name}`;
25896
+ const result = await client2.deleteRecordsByValue(
25897
+ record.name,
25898
+ record.type,
25899
+ record.value
25900
+ );
25901
+ if (result.error) {
25902
+ errors2.push(`${label}: ${result.error}`);
25903
+ } else if (result.deleted > 0) {
25904
+ for (let i6 = 0; i6 < result.deleted; i6++) {
25905
+ deleted.push(label);
25906
+ }
25907
+ } else {
25908
+ skipped.push({
25909
+ record: label,
25910
+ reason: "no record with the value Wraps created"
25911
+ });
25912
+ }
25913
+ }
25914
+ return { deleted, skipped, supported: true, errors: errors2 };
25915
+ }
25916
+ async function listRoute53RecordSet(client2, hostedZoneId, name, type) {
25917
+ const targetName = normalizeRoute53Name(name);
25918
+ let startRecordName = name;
25919
+ let startRecordType;
25920
+ const MAX_PAGES = 50;
25921
+ for (let page = 0; page < MAX_PAGES; page++) {
25922
+ const response = await client2.send(
25923
+ new ListResourceRecordSetsCommand({
25924
+ HostedZoneId: hostedZoneId,
25925
+ StartRecordName: startRecordName,
25926
+ StartRecordType: startRecordType
25927
+ })
25928
+ );
25929
+ const rrsets = response.ResourceRecordSets ?? [];
25930
+ let movedPastTarget = false;
25931
+ for (const rrset of rrsets) {
25932
+ if (!(rrset.Name && rrset.Type)) {
25933
+ continue;
25934
+ }
25935
+ if (normalizeRoute53Name(rrset.Name) !== targetName) {
25936
+ movedPastTarget = true;
25937
+ break;
25938
+ }
25939
+ if (rrset.Type === type) {
25940
+ return {
25941
+ ttl: rrset.TTL,
25942
+ values: (rrset.ResourceRecords ?? []).map((r6) => r6.Value).filter((v) => Boolean(v))
25943
+ };
25944
+ }
25945
+ }
25946
+ if (movedPastTarget || !response.IsTruncated) {
25947
+ break;
25948
+ }
25949
+ startRecordName = response.NextRecordName;
25950
+ startRecordType = response.NextRecordType;
25951
+ if (!startRecordName) {
25952
+ break;
25953
+ }
25954
+ }
25955
+ }
25956
+ function buildRoute53RemovalChange(params) {
25957
+ const { name, type, sesValue, existing } = params;
25958
+ const values = existing?.values ?? [];
25959
+ if (!values.includes(sesValue)) {
25960
+ return {
25961
+ change: null,
25962
+ skippedReason: `The ${type} record set at ${name} does not contain the value Wraps created \u2014 leaving it in place.`
25963
+ };
25964
+ }
25965
+ const remaining = values.filter((value) => value !== sesValue);
25966
+ if (remaining.length === 0) {
25967
+ return {
25968
+ change: {
25969
+ Action: "DELETE",
25970
+ ResourceRecordSet: {
25971
+ Name: name,
25972
+ Type: type,
25973
+ TTL: existing?.ttl ?? 1800,
25974
+ ResourceRecords: values.map((value) => ({ Value: value }))
25975
+ }
25976
+ }
25977
+ };
25978
+ }
25979
+ return {
25980
+ change: {
25981
+ Action: "UPSERT",
25982
+ ResourceRecordSet: {
25983
+ Name: name,
25984
+ Type: type,
25985
+ TTL: existing?.ttl ?? 1800,
25986
+ ResourceRecords: remaining.map((value) => ({ Value: value }))
25987
+ }
25988
+ }
25989
+ };
25990
+ }
25991
+ function route53Value(record) {
25992
+ if (record.type === "MX") {
25993
+ return `${record.priority} ${record.value}`;
25994
+ }
25995
+ if (record.type === "TXT") {
25996
+ return `"${record.value}"`;
25997
+ }
25998
+ return record.value;
25999
+ }
26000
+ async function deleteRoute53EmailRecords(hostedZoneId, region, records) {
26001
+ const client2 = new Route53Client({ region });
26002
+ const mxTxtNames = /* @__PURE__ */ new Set();
26003
+ const cnameNames = /* @__PURE__ */ new Set();
26004
+ for (const record of records) {
26005
+ if (record.type === "CNAME") {
26006
+ cnameNames.add(record.name);
26007
+ } else {
26008
+ mxTxtNames.add(record.name);
26009
+ }
26010
+ }
26011
+ const mxTxtByName = /* @__PURE__ */ new Map();
26012
+ const cnameByName = /* @__PURE__ */ new Map();
26013
+ try {
26014
+ for (const name of mxTxtNames) {
26015
+ mxTxtByName.set(
26016
+ name,
26017
+ await listExistingRoute53InboundRecords(client2, hostedZoneId, name)
26018
+ );
26019
+ }
26020
+ for (const name of cnameNames) {
26021
+ cnameByName.set(
26022
+ name,
26023
+ await listRoute53RecordSet(client2, hostedZoneId, name, "CNAME")
26024
+ );
26025
+ }
26026
+ } catch (error) {
26027
+ return {
26028
+ deleted: [],
26029
+ skipped: [],
26030
+ supported: true,
26031
+ errors: [error instanceof Error ? error.message : "Unknown error"]
26032
+ };
26033
+ }
26034
+ const changes = [];
26035
+ const deleted = [];
26036
+ const skipped = [];
26037
+ for (const record of records) {
26038
+ const label = `${record.type} ${record.name}`;
26039
+ const existing = record.type === "CNAME" ? cnameByName.get(record.name) : mxTxtByName.get(record.name)?.get(record.type);
26040
+ const result = buildRoute53RemovalChange({
26041
+ name: record.name,
26042
+ type: record.type,
26043
+ sesValue: route53Value(record),
26044
+ existing
26045
+ });
26046
+ if (result.change) {
26047
+ changes.push(result.change);
26048
+ deleted.push(label);
26049
+ } else if (result.skippedReason) {
26050
+ skipped.push({ record: label, reason: result.skippedReason });
26051
+ }
26052
+ }
26053
+ if (changes.length === 0) {
26054
+ return { deleted, skipped, supported: true, errors: [] };
26055
+ }
26056
+ try {
26057
+ await client2.send(
26058
+ new ChangeResourceRecordSetsCommand({
26059
+ HostedZoneId: hostedZoneId,
26060
+ ChangeBatch: { Changes: changes }
26061
+ })
26062
+ );
26063
+ return { deleted, skipped, supported: true, errors: [] };
26064
+ } catch (error) {
26065
+ return {
26066
+ deleted: [],
26067
+ skipped,
26068
+ supported: true,
26069
+ errors: [error instanceof Error ? error.message : "Unknown error"]
26070
+ };
26071
+ }
26072
+ }
26073
+ async function deleteEmailDNSRecordsForProvider(credentials, data) {
26074
+ const records = buildEmailDNSRecords(data).filter(
26075
+ (record) => record.category !== "spf"
26076
+ );
26077
+ switch (credentials.provider) {
26078
+ case "cloudflare": {
26079
+ const client2 = new CloudflareDNSClient(
26080
+ credentials.zoneId,
26081
+ credentials.token
26082
+ );
26083
+ return deleteViaValueMatchedClient(client2, records);
26084
+ }
26085
+ case "vercel": {
26086
+ const client2 = new VercelDNSClient(
26087
+ data.domain,
26088
+ credentials.token,
26089
+ credentials.teamId
26090
+ );
26091
+ return deleteViaValueMatchedClient(client2, records);
26092
+ }
26093
+ case "route53":
26094
+ return deleteRoute53EmailRecords(
26095
+ credentials.hostedZoneId,
26096
+ data.region,
26097
+ records
26098
+ );
26099
+ case "manual":
26100
+ return { deleted: [], skipped: [], supported: false, errors: [] };
26101
+ }
26102
+ }
26103
+ var init_email_dns_cleanup = __esm({
26104
+ "src/utils/dns/email-dns-cleanup.ts"() {
26105
+ "use strict";
26106
+ init_esm_shims();
26107
+ init_cloudflare();
26108
+ init_create_records();
26109
+ init_vercel();
26110
+ }
26111
+ });
26112
+
26113
+ // src/utils/route53.ts
26114
+ var route53_exports = {};
26115
+ __export(route53_exports, {
26116
+ createDNSRecords: () => createDNSRecords,
26117
+ createSelectedDNSRecords: () => createSelectedDNSRecords,
26118
+ deleteDNSRecords: () => deleteDNSRecords,
26119
+ findHostedZone: () => findHostedZone,
26120
+ mergeSPFRecord: () => mergeSPFRecord,
26121
+ previewDNSChanges: () => previewDNSChanges
26122
+ });
26123
+ import {
26124
+ ChangeResourceRecordSetsCommand as ChangeResourceRecordSetsCommand2,
26125
+ ListHostedZonesByNameCommand,
26126
+ ListResourceRecordSetsCommand as ListResourceRecordSetsCommand2,
26127
+ Route53Client as Route53Client2
26128
+ } from "@aws-sdk/client-route-53";
26129
+ async function getExistingTXTRecords(client2, hostedZoneId, domain) {
26130
+ try {
26131
+ const response = await client2.send(
26132
+ new ListResourceRecordSetsCommand2({
26133
+ HostedZoneId: hostedZoneId,
26134
+ StartRecordName: domain,
26135
+ StartRecordType: "TXT",
26136
+ MaxItems: 100
26137
+ })
26138
+ );
26139
+ const txtRecordSet = response.ResourceRecordSets?.find(
26140
+ (rs) => rs.Type === "TXT" && (rs.Name === domain || rs.Name === `${domain}.`)
26141
+ );
26142
+ if (!txtRecordSet?.ResourceRecords) {
26143
+ return { allValues: [], spfValue: null, ttl: 1800 };
26144
+ }
26145
+ const allValues = [];
26146
+ let spfValue = null;
26147
+ for (const record of txtRecordSet.ResourceRecords) {
26148
+ const value = record.Value || "";
26149
+ allValues.push(value);
26150
+ const unquoted = value.replace(/^"|"$/g, "");
26151
+ if (unquoted.startsWith("v=spf1")) {
26152
+ spfValue = unquoted;
26153
+ }
26154
+ }
26155
+ return {
26156
+ allValues,
26157
+ spfValue,
26158
+ ttl: txtRecordSet.TTL || 1800
26159
+ };
26160
+ } catch (_error) {
26161
+ return { allValues: [], spfValue: null, ttl: 1800 };
26162
+ }
26163
+ }
26164
+ function mergeSPFRecord(existingSPF) {
26165
+ const sesInclude = "include:amazonses.com";
26166
+ if (existingSPF.includes(sesInclude)) {
26167
+ return existingSPF;
26168
+ }
26169
+ const allMatch = existingSPF.match(/\s([~+?-]?all)$/);
26170
+ if (allMatch) {
26171
+ const beforeAll = existingSPF.slice(0, allMatch.index);
26172
+ const allPart = allMatch[1];
26173
+ return `${beforeAll} ${sesInclude} ${allPart}`;
26174
+ }
26175
+ return `${existingSPF} ${sesInclude}`;
26176
+ }
26177
+ async function getExistingRecords(client2, hostedZoneId) {
26178
+ try {
26179
+ const response = await client2.send(
26180
+ new ListResourceRecordSetsCommand2({
26181
+ HostedZoneId: hostedZoneId,
26182
+ MaxItems: 500
26183
+ })
26184
+ );
26185
+ return response.ResourceRecordSets || [];
26186
+ } catch (_error) {
26187
+ return [];
26188
+ }
26189
+ }
26190
+ function findExistingRecord(records, name, type) {
26191
+ const normalizedName = name.endsWith(".") ? name : `${name}.`;
26192
+ return records.find((r6) => r6.Name === normalizedName && r6.Type === type) || null;
26193
+ }
26194
+ function getRecordValue(record) {
26195
+ if (!record?.ResourceRecords?.length) {
26196
+ return null;
26197
+ }
26198
+ return record.ResourceRecords.map((r6) => r6.Value || "").join(", ");
26199
+ }
26200
+ async function previewDNSChanges(hostedZoneId, domain, dkimTokens, region, customTrackingDomain, mailFromDomain) {
26201
+ const client2 = new Route53Client2({ region });
26202
+ const existingRecords = await getExistingRecords(client2, hostedZoneId);
26203
+ const existingTXT = await getExistingTXTRecords(client2, hostedZoneId, domain);
26204
+ const records = [];
26205
+ for (const token of dkimTokens) {
26206
+ const name = `${token}._domainkey.${domain}`;
26207
+ const proposedValue = `${token}.dkim.amazonses.com`;
26208
+ const existing = findExistingRecord(existingRecords, name, "CNAME");
26209
+ const existingValue = getRecordValue(existing);
26210
+ let status2 = "new";
26211
+ if (existingValue === proposedValue) {
26212
+ status2 = "no_change";
26213
+ } else if (existingValue) {
26214
+ status2 = "update";
26215
+ }
26216
+ records.push({
26217
+ name,
26218
+ type: "CNAME",
26219
+ proposedValue,
26220
+ existingValue,
26221
+ status: status2,
26222
+ category: "dkim"
26223
+ });
26224
+ }
26225
+ const proposedSPF = existingTXT.spfValue ? mergeSPFRecord(existingTXT.spfValue) : "v=spf1 include:amazonses.com ~all";
26226
+ let spfStatus = "new";
26227
+ if (existingTXT.spfValue) {
26228
+ if (existingTXT.spfValue === proposedSPF) {
26229
+ spfStatus = "no_change";
26230
+ } else if (existingTXT.spfValue.includes("include:amazonses.com")) {
26231
+ spfStatus = "no_change";
26232
+ } else {
26233
+ spfStatus = "update";
26234
+ }
26235
+ }
26236
+ records.push({
26237
+ name: domain,
26238
+ type: "TXT",
26239
+ proposedValue: `"${proposedSPF}"`,
26240
+ existingValue: existingTXT.spfValue ? `"${existingTXT.spfValue}"` : null,
26241
+ status: spfStatus,
26242
+ category: "spf",
26243
+ conflictReason: void 0
26244
+ });
26245
+ const dmarcName = `_dmarc.${domain}`;
26246
+ const dmarcRuaDomain = mailFromDomain || domain;
26247
+ const proposedDMARC = `"v=DMARC1; p=quarantine; sp=quarantine; np=reject; rua=mailto:postmaster@${dmarcRuaDomain}"`;
26248
+ const existingDMARC = findExistingRecord(existingRecords, dmarcName, "TXT");
26249
+ const existingDMARCValue = getRecordValue(existingDMARC);
26250
+ let dmarcStatus = "new";
26251
+ let dmarcConflictReason;
26252
+ if (existingDMARCValue) {
26253
+ if (existingDMARCValue === proposedDMARC) {
26254
+ dmarcStatus = "no_change";
26255
+ } else {
26256
+ dmarcStatus = "conflict";
26257
+ dmarcConflictReason = "Existing DMARC policy will be replaced";
26258
+ }
26259
+ }
26260
+ records.push({
26261
+ name: dmarcName,
26262
+ type: "TXT",
26263
+ proposedValue: proposedDMARC,
26264
+ existingValue: existingDMARCValue,
26265
+ status: dmarcStatus,
26266
+ category: "dmarc",
26267
+ conflictReason: dmarcConflictReason
26268
+ });
26269
+ if (customTrackingDomain) {
26270
+ const proposedTracking = `r.${region}.awstrack.me`;
26271
+ const existingTracking = findExistingRecord(
26272
+ existingRecords,
26273
+ customTrackingDomain,
26274
+ "CNAME"
26275
+ );
26276
+ const existingTrackingValue = getRecordValue(existingTracking);
26277
+ let trackingStatus = "new";
26278
+ let trackingConflictReason;
26279
+ if (existingTrackingValue === proposedTracking) {
26280
+ trackingStatus = "no_change";
26281
+ } else if (existingTrackingValue) {
26282
+ trackingStatus = "conflict";
26283
+ trackingConflictReason = `Points to ${existingTrackingValue}, will be replaced`;
26284
+ }
26285
+ records.push({
26286
+ name: customTrackingDomain,
26287
+ type: "CNAME",
26288
+ proposedValue: proposedTracking,
26289
+ existingValue: existingTrackingValue,
26290
+ status: trackingStatus,
26291
+ category: "tracking",
26292
+ conflictReason: trackingConflictReason
26293
+ });
26294
+ }
26295
+ if (mailFromDomain) {
26296
+ const proposedMX = `10 feedback-smtp.${region}.amazonses.com`;
26297
+ const existingMX = findExistingRecord(
26298
+ existingRecords,
26299
+ mailFromDomain,
26300
+ "MX"
26301
+ );
26302
+ const existingMXValue = getRecordValue(existingMX);
26303
+ let mxStatus = "new";
26304
+ if (existingMXValue === proposedMX) {
26305
+ mxStatus = "no_change";
26306
+ } else if (existingMXValue) {
26307
+ mxStatus = "update";
26308
+ }
26309
+ records.push({
26310
+ name: mailFromDomain,
26311
+ type: "MX",
26312
+ proposedValue: proposedMX,
26313
+ existingValue: existingMXValue,
26314
+ status: mxStatus,
26315
+ category: "mailfrom_mx"
26316
+ });
26317
+ const mailFromSPF = '"v=spf1 include:amazonses.com ~all"';
26318
+ const existingMailFromTXT = await getExistingTXTRecords(
26319
+ client2,
26320
+ hostedZoneId,
26321
+ mailFromDomain
26322
+ );
26323
+ let mailFromSpfStatus = "new";
26324
+ if (existingMailFromTXT.spfValue?.includes("include:amazonses.com")) {
26325
+ mailFromSpfStatus = "no_change";
26326
+ } else if (existingMailFromTXT.spfValue) {
26327
+ mailFromSpfStatus = "update";
26328
+ }
26329
+ records.push({
26330
+ name: mailFromDomain,
26331
+ type: "TXT",
26332
+ proposedValue: mailFromSPF,
26333
+ existingValue: existingMailFromTXT.spfValue ? `"${existingMailFromTXT.spfValue}"` : null,
26334
+ status: mailFromSpfStatus,
26335
+ category: "mailfrom_spf"
26336
+ });
26337
+ }
26338
+ const conflictCount = records.filter((r6) => r6.status === "conflict").length;
26339
+ const newCount = records.filter((r6) => r6.status === "new").length;
26340
+ const updateCount = records.filter((r6) => r6.status === "update").length;
26341
+ const noChangeCount = records.filter((r6) => r6.status === "no_change").length;
26342
+ return {
26343
+ records,
26344
+ hasConflicts: conflictCount > 0,
26345
+ conflictCount,
26346
+ newCount,
26347
+ updateCount,
26348
+ noChangeCount
26349
+ };
26350
+ }
26351
+ async function createSelectedDNSRecords(hostedZoneId, domain, dkimTokens, region, selectedCategories, customTrackingDomain, mailFromDomain, trackingCnameTarget) {
26352
+ const client2 = new Route53Client2({ region });
26353
+ const changes = [];
26354
+ if (selectedCategories.has("dkim")) {
26355
+ for (const token of dkimTokens) {
26356
+ changes.push({
26357
+ Action: "UPSERT",
26358
+ ResourceRecordSet: {
26359
+ Name: `${token}._domainkey.${domain}`,
26360
+ Type: "CNAME",
26361
+ TTL: 1800,
26362
+ ResourceRecords: [{ Value: `${token}.dkim.amazonses.com` }]
26363
+ }
26364
+ });
26365
+ }
26366
+ }
26367
+ if (selectedCategories.has("spf")) {
26368
+ const existingTXT = await getExistingTXTRecords(
26369
+ client2,
26370
+ hostedZoneId,
26371
+ domain
26372
+ );
26373
+ const newTXTValues = [];
26374
+ if (existingTXT.spfValue) {
26375
+ const mergedSPF = mergeSPFRecord(existingTXT.spfValue);
26376
+ newTXTValues.push(`"${mergedSPF}"`);
26377
+ for (const value of existingTXT.allValues) {
26378
+ const unquoted = value.replace(/^"|"$/g, "");
26379
+ if (!unquoted.startsWith("v=spf1")) {
26380
+ newTXTValues.push(value);
26291
26381
  }
26292
26382
  }
26293
- /**
26294
- * Get all CAA records for the domain
26295
- */
26296
- async getCAARecords() {
26297
- const result = await this.request(
26298
- `/v4/domains/${this.domain}/records`
26299
- );
26300
- if (result.error || !result.records) {
26301
- return [];
26302
- }
26303
- return result.records.filter((r6) => r6.type === "CAA").map((r6) => {
26304
- const match = r6.value.match(/^(\d+)\s+(\w+)\s+"?([^"]+)"?$/);
26305
- if (match) {
26306
- return {
26307
- flags: Number.parseInt(match[1], 10),
26308
- tag: match[2],
26309
- value: match[3]
26310
- };
26383
+ } else if (existingTXT.allValues.length > 0) {
26384
+ newTXTValues.push('"v=spf1 include:amazonses.com ~all"');
26385
+ newTXTValues.push(...existingTXT.allValues);
26386
+ } else {
26387
+ newTXTValues.push('"v=spf1 include:amazonses.com ~all"');
26388
+ }
26389
+ changes.push({
26390
+ Action: "UPSERT",
26391
+ ResourceRecordSet: {
26392
+ Name: domain,
26393
+ Type: "TXT",
26394
+ TTL: existingTXT.ttl,
26395
+ ResourceRecords: newTXTValues.map((v) => ({ Value: v }))
26396
+ }
26397
+ });
26398
+ }
26399
+ if (selectedCategories.has("dmarc")) {
26400
+ const dmarcRuaDomain = mailFromDomain || domain;
26401
+ changes.push({
26402
+ Action: "UPSERT",
26403
+ ResourceRecordSet: {
26404
+ Name: `_dmarc.${domain}`,
26405
+ Type: "TXT",
26406
+ TTL: 1800,
26407
+ ResourceRecords: [
26408
+ {
26409
+ Value: `"v=DMARC1; p=quarantine; sp=quarantine; np=reject; rua=mailto:postmaster@${dmarcRuaDomain}"`
26311
26410
  }
26312
- return null;
26313
- }).filter(
26314
- (r6) => r6 !== null
26315
- );
26411
+ ]
26412
+ }
26413
+ });
26414
+ }
26415
+ if (selectedCategories.has("tracking") && customTrackingDomain) {
26416
+ changes.push({
26417
+ Action: "UPSERT",
26418
+ ResourceRecordSet: {
26419
+ Name: customTrackingDomain,
26420
+ Type: "CNAME",
26421
+ TTL: 1800,
26422
+ ResourceRecords: [
26423
+ { Value: trackingCnameTarget ?? `r.${region}.awstrack.me` }
26424
+ ]
26425
+ }
26426
+ });
26427
+ }
26428
+ if (selectedCategories.has("mailfrom_mx") && mailFromDomain) {
26429
+ changes.push({
26430
+ Action: "UPSERT",
26431
+ ResourceRecordSet: {
26432
+ Name: mailFromDomain,
26433
+ Type: "MX",
26434
+ TTL: 1800,
26435
+ ResourceRecords: [
26436
+ { Value: `10 feedback-smtp.${region}.amazonses.com` }
26437
+ ]
26438
+ }
26439
+ });
26440
+ }
26441
+ if (selectedCategories.has("mailfrom_spf") && mailFromDomain) {
26442
+ changes.push({
26443
+ Action: "UPSERT",
26444
+ ResourceRecordSet: {
26445
+ Name: mailFromDomain,
26446
+ Type: "TXT",
26447
+ TTL: 1800,
26448
+ ResourceRecords: [{ Value: '"v=spf1 include:amazonses.com ~all"' }]
26449
+ }
26450
+ });
26451
+ }
26452
+ if (changes.length === 0) {
26453
+ return;
26454
+ }
26455
+ await client2.send(
26456
+ new ChangeResourceRecordSetsCommand2({
26457
+ HostedZoneId: hostedZoneId,
26458
+ ChangeBatch: {
26459
+ Changes: changes
26460
+ }
26461
+ })
26462
+ );
26463
+ }
26464
+ async function findHostedZone(domain, region) {
26465
+ const client2 = new Route53Client2({ region });
26466
+ try {
26467
+ const response = await client2.send(
26468
+ new ListHostedZonesByNameCommand({
26469
+ DNSName: domain,
26470
+ MaxItems: 1
26471
+ })
26472
+ );
26473
+ const zone = response.HostedZones?.[0];
26474
+ if (zone && zone.Name === `${domain}.` && zone.Id) {
26475
+ return {
26476
+ id: zone.Id.replace("/hostedzone/", ""),
26477
+ name: zone.Name
26478
+ };
26479
+ }
26480
+ return null;
26481
+ } catch (_error) {
26482
+ return null;
26483
+ }
26484
+ }
26485
+ async function createDNSRecords(hostedZoneId, domain, dkimTokens, region, customTrackingDomain, mailFromDomain, cloudFrontDomain) {
26486
+ const client2 = new Route53Client2({ region });
26487
+ const changes = [];
26488
+ for (const token of dkimTokens) {
26489
+ changes.push({
26490
+ Action: "UPSERT",
26491
+ ResourceRecordSet: {
26492
+ Name: `${token}._domainkey.${domain}`,
26493
+ Type: "CNAME",
26494
+ TTL: 1800,
26495
+ ResourceRecords: [{ Value: `${token}.dkim.amazonses.com` }]
26316
26496
  }
26317
- /**
26318
- * Check if Amazon is allowed to issue certificates based on CAA records
26319
- * Returns true if:
26320
- * - No CAA records exist (any CA can issue)
26321
- * - CAA records exist and include amazon.com or amazontrust.com
26322
- */
26323
- async isAmazonCAAAllowed() {
26324
- const caaRecords = await this.getCAARecords();
26325
- const issueRecords = caaRecords.filter(
26326
- (r6) => r6.tag === "issue" || r6.tag === "issuewild"
26327
- );
26328
- if (issueRecords.length === 0) {
26329
- return { allowed: true, hasCAA: false, existingCAs: [] };
26497
+ });
26498
+ }
26499
+ const existingTXT = await getExistingTXTRecords(client2, hostedZoneId, domain);
26500
+ const newTXTValues = [];
26501
+ if (existingTXT.spfValue) {
26502
+ const mergedSPF = mergeSPFRecord(existingTXT.spfValue);
26503
+ newTXTValues.push(`"${mergedSPF}"`);
26504
+ for (const value of existingTXT.allValues) {
26505
+ const unquoted = value.replace(/^"|"$/g, "");
26506
+ if (!unquoted.startsWith("v=spf1")) {
26507
+ newTXTValues.push(value);
26508
+ }
26509
+ }
26510
+ } else if (existingTXT.allValues.length > 0) {
26511
+ newTXTValues.push('"v=spf1 include:amazonses.com ~all"');
26512
+ newTXTValues.push(...existingTXT.allValues);
26513
+ } else {
26514
+ newTXTValues.push('"v=spf1 include:amazonses.com ~all"');
26515
+ }
26516
+ changes.push({
26517
+ Action: "UPSERT",
26518
+ ResourceRecordSet: {
26519
+ Name: domain,
26520
+ Type: "TXT",
26521
+ TTL: existingTXT.ttl,
26522
+ ResourceRecords: newTXTValues.map((v) => ({ Value: v }))
26523
+ }
26524
+ });
26525
+ const dmarcRuaDomain = mailFromDomain || domain;
26526
+ changes.push({
26527
+ Action: "UPSERT",
26528
+ ResourceRecordSet: {
26529
+ Name: `_dmarc.${domain}`,
26530
+ Type: "TXT",
26531
+ TTL: 1800,
26532
+ ResourceRecords: [
26533
+ {
26534
+ Value: `"v=DMARC1; p=quarantine; sp=quarantine; np=reject; rua=mailto:postmaster@${dmarcRuaDomain}"`
26330
26535
  }
26331
- const existingCAs = issueRecords.map((r6) => r6.value);
26332
- const amazonAllowed = existingCAs.some(
26333
- (ca) => ca.includes("amazon.com") || ca.includes("amazontrust.com")
26334
- );
26335
- return { allowed: amazonAllowed, hasCAA: true, existingCAs };
26536
+ ]
26537
+ }
26538
+ });
26539
+ if (customTrackingDomain) {
26540
+ const targetDomain = cloudFrontDomain || `r.${region}.awstrack.me`;
26541
+ changes.push({
26542
+ Action: "UPSERT",
26543
+ ResourceRecordSet: {
26544
+ Name: customTrackingDomain,
26545
+ Type: "CNAME",
26546
+ TTL: 1800,
26547
+ ResourceRecords: [{ Value: targetDomain }]
26336
26548
  }
26337
- /**
26338
- * Add a CAA record to allow Amazon to issue certificates
26339
- */
26340
- async addAmazonCAARecord() {
26341
- const body = {
26342
- name: "@",
26343
- // Root domain
26344
- type: "CAA",
26345
- value: '0 issue "amazon.com"',
26346
- ttl: 1800
26347
- };
26348
- const result = await this.request(
26349
- `/v2/domains/${this.domain}/records`,
26350
- "POST",
26351
- body
26352
- );
26353
- return !result.error;
26549
+ });
26550
+ }
26551
+ if (mailFromDomain) {
26552
+ changes.push({
26553
+ Action: "UPSERT",
26554
+ ResourceRecordSet: {
26555
+ Name: mailFromDomain,
26556
+ Type: "MX",
26557
+ TTL: 1800,
26558
+ ResourceRecords: [
26559
+ { Value: `10 feedback-smtp.${region}.amazonses.com` }
26560
+ ]
26354
26561
  }
26355
- async verifyRecords(data) {
26356
- const { domain, dkimTokens, mailFromDomain, region } = data;
26357
- const missing = [];
26358
- const incorrect = [];
26359
- for (const token of dkimTokens) {
26360
- const name = `${token}._domainkey.${domain}`;
26361
- const expectedValue = `${token}.dkim.amazonses.com`;
26362
- const record = await this.findRecord(name, "CNAME");
26363
- if (!record) {
26364
- missing.push(`DKIM: ${name}`);
26365
- } else if (record.value !== expectedValue) {
26366
- incorrect.push(`DKIM: ${name} (expected ${expectedValue})`);
26367
- }
26368
- }
26369
- const spfRecord = await this.findRecord(domain, "TXT");
26370
- if (!spfRecord) {
26371
- missing.push(`SPF: ${domain}`);
26372
- } else if (!spfRecord.value.includes("include:amazonses.com")) {
26373
- incorrect.push(`SPF: ${domain} (missing amazonses.com include)`);
26374
- }
26375
- const dmarcRecord = await this.findRecord(`_dmarc.${domain}`, "TXT");
26376
- if (!dmarcRecord) {
26377
- missing.push(`DMARC: _dmarc.${domain}`);
26378
- }
26379
- if (mailFromDomain) {
26380
- const mxRecord = await this.findRecord(mailFromDomain, "MX");
26381
- if (!mxRecord) {
26382
- missing.push(`MX: ${mailFromDomain}`);
26383
- } else if (!mxRecord.value.includes(`feedback-smtp.${region}.amazonses.com`)) {
26384
- incorrect.push(`MX: ${mailFromDomain}`);
26385
- }
26386
- const mailFromSpf = await this.findRecord(mailFromDomain, "TXT");
26387
- if (!mailFromSpf) {
26388
- missing.push(`SPF: ${mailFromDomain}`);
26389
- }
26390
- }
26391
- return {
26392
- verified: missing.length === 0 && incorrect.length === 0,
26393
- missing,
26394
- incorrect
26395
- };
26562
+ });
26563
+ changes.push({
26564
+ Action: "UPSERT",
26565
+ ResourceRecordSet: {
26566
+ Name: mailFromDomain,
26567
+ Type: "TXT",
26568
+ TTL: 1800,
26569
+ ResourceRecords: [{ Value: '"v=spf1 include:amazonses.com ~all"' }]
26396
26570
  }
26397
- };
26571
+ });
26572
+ }
26573
+ await client2.send(
26574
+ new ChangeResourceRecordSetsCommand2({
26575
+ HostedZoneId: hostedZoneId,
26576
+ ChangeBatch: {
26577
+ Changes: changes
26578
+ }
26579
+ })
26580
+ );
26581
+ }
26582
+ async function deleteDNSRecords(hostedZoneId, domain, dkimTokens, region, customTrackingDomain, mailFromDomain) {
26583
+ const { deleteEmailDNSRecordsForProvider: deleteEmailDNSRecordsForProvider2 } = await Promise.resolve().then(() => (init_email_dns_cleanup(), email_dns_cleanup_exports));
26584
+ return deleteEmailDNSRecordsForProvider2(
26585
+ { provider: "route53", hostedZoneId },
26586
+ {
26587
+ domain,
26588
+ dkimTokens,
26589
+ region,
26590
+ customTrackingDomain,
26591
+ mailFromDomain
26592
+ }
26593
+ );
26594
+ }
26595
+ var init_route53 = __esm({
26596
+ "src/utils/route53.ts"() {
26597
+ "use strict";
26598
+ init_esm_shims();
26398
26599
  }
26399
26600
  });
26400
26601
 
@@ -26415,9 +26616,9 @@ __export(create_records_exports, {
26415
26616
  normalizeRoute53Name: () => normalizeRoute53Name
26416
26617
  });
26417
26618
  import {
26418
- ChangeResourceRecordSetsCommand as ChangeResourceRecordSetsCommand2,
26419
- ListResourceRecordSetsCommand as ListResourceRecordSetsCommand2,
26420
- Route53Client as Route53Client2
26619
+ ChangeResourceRecordSetsCommand as ChangeResourceRecordSetsCommand3,
26620
+ ListResourceRecordSetsCommand as ListResourceRecordSetsCommand3,
26621
+ Route53Client as Route53Client3
26421
26622
  } from "@aws-sdk/client-route-53";
26422
26623
  import pc7 from "picocolors";
26423
26624
  function buildEmailDNSRecords(data) {
@@ -26634,7 +26835,7 @@ async function listExistingRoute53InboundRecords(client2, hostedZoneId, name) {
26634
26835
  const MAX_PAGES = 50;
26635
26836
  for (let page = 0; page < MAX_PAGES; page++) {
26636
26837
  const response = await client2.send(
26637
- new ListResourceRecordSetsCommand2({
26838
+ new ListResourceRecordSetsCommand3({
26638
26839
  HostedZoneId: hostedZoneId,
26639
26840
  StartRecordName: startRecordName,
26640
26841
  StartRecordType: startRecordType
@@ -26814,7 +27015,7 @@ async function createInboundDNSRecordsForProvider(credentials, receivingDomain,
26814
27015
  const records = buildInboundDNSRecords(receivingDomain, region);
26815
27016
  switch (credentials.provider) {
26816
27017
  case "route53": {
26817
- const client2 = new Route53Client2({ region });
27018
+ const client2 = new Route53Client3({ region });
26818
27019
  let existing;
26819
27020
  try {
26820
27021
  existing = await listExistingRoute53InboundRecords(
@@ -26862,7 +27063,7 @@ async function createInboundDNSRecordsForProvider(credentials, receivingDomain,
26862
27063
  }
26863
27064
  try {
26864
27065
  await client2.send(
26865
- new ChangeResourceRecordSetsCommand2({
27066
+ new ChangeResourceRecordSetsCommand3({
26866
27067
  HostedZoneId: credentials.hostedZoneId,
26867
27068
  ChangeBatch: { Changes: changes }
26868
27069
  })
@@ -31180,10 +31381,10 @@ var init_credentials = __esm({
31180
31381
 
31181
31382
  // src/utils/dns/inbound-dns-cleanup.ts
31182
31383
  import {
31183
- ChangeResourceRecordSetsCommand as ChangeResourceRecordSetsCommand3,
31184
- Route53Client as Route53Client3
31384
+ ChangeResourceRecordSetsCommand as ChangeResourceRecordSetsCommand4,
31385
+ Route53Client as Route53Client4
31185
31386
  } from "@aws-sdk/client-route-53";
31186
- async function deleteViaValueMatchedClient(client2, records) {
31387
+ async function deleteViaValueMatchedClient2(client2, records) {
31187
31388
  const deleted = [];
31188
31389
  const skipped = [];
31189
31390
  const errors2 = [];
@@ -31292,7 +31493,7 @@ function buildRoute53InboundTXTRemoval(record, existing) {
31292
31493
  return { change: null };
31293
31494
  }
31294
31495
  async function deleteRoute53InboundRecords(hostedZoneId, receivingDomain, region, records) {
31295
- const client2 = new Route53Client3({ region });
31496
+ const client2 = new Route53Client4({ region });
31296
31497
  let existing;
31297
31498
  try {
31298
31499
  existing = await listExistingRoute53InboundRecords(
@@ -31345,7 +31546,7 @@ async function deleteRoute53InboundRecords(hostedZoneId, receivingDomain, region
31345
31546
  }
31346
31547
  try {
31347
31548
  await client2.send(
31348
- new ChangeResourceRecordSetsCommand3({
31549
+ new ChangeResourceRecordSetsCommand4({
31349
31550
  HostedZoneId: hostedZoneId,
31350
31551
  ChangeBatch: { Changes: changes }
31351
31552
  })
@@ -31368,7 +31569,7 @@ async function deleteInboundDNSRecordsForProvider(credentials, receivingDomain,
31368
31569
  credentials.zoneId,
31369
31570
  credentials.token
31370
31571
  );
31371
- return deleteViaValueMatchedClient(client2, records);
31572
+ return deleteViaValueMatchedClient2(client2, records);
31372
31573
  }
31373
31574
  case "vercel": {
31374
31575
  const client2 = new VercelDNSClient(
@@ -31376,7 +31577,7 @@ async function deleteInboundDNSRecordsForProvider(credentials, receivingDomain,
31376
31577
  credentials.token,
31377
31578
  credentials.teamId
31378
31579
  );
31379
- return deleteViaValueMatchedClient(client2, records);
31580
+ return deleteViaValueMatchedClient2(client2, records);
31380
31581
  }
31381
31582
  case "route53":
31382
31583
  return deleteRoute53InboundRecords(
@@ -31400,7 +31601,7 @@ var init_inbound_dns_cleanup = __esm({
31400
31601
  });
31401
31602
 
31402
31603
  // src/utils/dns/inbound-preflight.ts
31403
- import { Route53Client as Route53Client4 } from "@aws-sdk/client-route-53";
31604
+ import { Route53Client as Route53Client5 } from "@aws-sdk/client-route-53";
31404
31605
  import * as clack24 from "@clack/prompts";
31405
31606
  function stripQuotes(value) {
31406
31607
  return value.replace(/^"|"$/g, "");
@@ -31478,7 +31679,7 @@ async function checkInboundDNSPreflight(credentials, receivingDomain, region, pa
31478
31679
  }
31479
31680
  }
31480
31681
  try {
31481
- const client2 = new Route53Client4({ region });
31682
+ const client2 = new Route53Client5({ region });
31482
31683
  const existing = await listExistingRoute53InboundRecords(
31483
31684
  client2,
31484
31685
  credentials.hostedZoneId,
@@ -31577,6 +31778,7 @@ __export(dns_exports, {
31577
31778
  checkInboundDNSPreflight: () => checkInboundDNSPreflight,
31578
31779
  createDNSRecordsForProvider: () => createDNSRecordsForProvider,
31579
31780
  createInboundDNSRecordsForProvider: () => createInboundDNSRecordsForProvider,
31781
+ deleteEmailDNSRecordsForProvider: () => deleteEmailDNSRecordsForProvider,
31580
31782
  deleteInboundDNSRecordsForProvider: () => deleteInboundDNSRecordsForProvider,
31581
31783
  describeInboundDNSConflict: () => describeInboundDNSConflict,
31582
31784
  detectAvailableDNSProviders: () => detectAvailableDNSProviders,
@@ -31599,6 +31801,7 @@ var init_dns = __esm({
31599
31801
  init_cloudflare();
31600
31802
  init_create_records();
31601
31803
  init_credentials();
31804
+ init_email_dns_cleanup();
31602
31805
  init_inbound_dns_cleanup();
31603
31806
  init_inbound_preflight();
31604
31807
  init_vercel();
@@ -34712,13 +34915,13 @@ async function emptyS3Bucket(bucketName, region) {
34712
34915
  }
34713
34916
  async function deleteCdnDNSRecords(hostedZoneId, customDomain) {
34714
34917
  const {
34715
- Route53Client: Route53Client6,
34716
- ListResourceRecordSetsCommand: ListResourceRecordSetsCommand3,
34717
- ChangeResourceRecordSetsCommand: ChangeResourceRecordSetsCommand5
34918
+ Route53Client: Route53Client7,
34919
+ ListResourceRecordSetsCommand: ListResourceRecordSetsCommand4,
34920
+ ChangeResourceRecordSetsCommand: ChangeResourceRecordSetsCommand6
34718
34921
  } = await import("@aws-sdk/client-route-53");
34719
- const route533 = new Route53Client6({ region: "us-east-1" });
34922
+ const route533 = new Route53Client7({ region: "us-east-1" });
34720
34923
  const listResponse = await route533.send(
34721
- new ListResourceRecordSetsCommand3({
34924
+ new ListResourceRecordSetsCommand4({
34722
34925
  HostedZoneId: hostedZoneId
34723
34926
  })
34724
34927
  );
@@ -34736,7 +34939,7 @@ async function deleteCdnDNSRecords(hostedZoneId, customDomain) {
34736
34939
  ResourceRecordSet: record
34737
34940
  }));
34738
34941
  await route533.send(
34739
- new ChangeResourceRecordSetsCommand5({
34942
+ new ChangeResourceRecordSetsCommand6({
34740
34943
  HostedZoneId: hostedZoneId,
34741
34944
  ChangeBatch: { Changes: changes }
34742
34945
  })
@@ -36109,13 +36312,13 @@ ${pc12.yellow(pc12.bold("Configuration Notes:"))}`);
36109
36312
  try {
36110
36313
  progress.start("Checking existing DNS records");
36111
36314
  const {
36112
- Route53Client: Route53Client6,
36113
- ListResourceRecordSetsCommand: ListResourceRecordSetsCommand3,
36114
- ChangeResourceRecordSetsCommand: ChangeResourceRecordSetsCommand5
36315
+ Route53Client: Route53Client7,
36316
+ ListResourceRecordSetsCommand: ListResourceRecordSetsCommand4,
36317
+ ChangeResourceRecordSetsCommand: ChangeResourceRecordSetsCommand6
36115
36318
  } = await import("@aws-sdk/client-route-53");
36116
- const route533 = new Route53Client6({ region: "us-east-1" });
36319
+ const route533 = new Route53Client7({ region: "us-east-1" });
36117
36320
  const existingRecords = await route533.send(
36118
- new ListResourceRecordSetsCommand3({
36321
+ new ListResourceRecordSetsCommand4({
36119
36322
  HostedZoneId: hostedZone.id,
36120
36323
  StartRecordName: outputs.customDomain,
36121
36324
  StartRecordType: "CNAME",
@@ -36203,7 +36406,7 @@ ${pc12.yellow(pc12.bold("Configuration Notes:"))}`);
36203
36406
  }));
36204
36407
  if (changes.length > 0) {
36205
36408
  await route533.send(
36206
- new ChangeResourceRecordSetsCommand5({
36409
+ new ChangeResourceRecordSetsCommand6({
36207
36410
  HostedZoneId: hostedZone.id,
36208
36411
  ChangeBatch: { Changes: changes }
36209
36412
  })
@@ -41493,9 +41696,9 @@ var IP_BLACKLISTS = [
41493
41696
  { name: "ScientificSpam", zone: "bl.scientificspam.net", priority: "medium" },
41494
41697
  { name: "Swinog IP", zone: "dnsrbl.swinog.ch", priority: "medium" },
41495
41698
  { name: "Kempt.net", zone: "dnsbl.kempt.net", priority: "low" },
41496
- { name: "Anonmails", zone: "spam.dnsbl.anonmails.de", priority: "low" },
41699
+ { name: "Anonmails", zone: "spam.dnsbl.anonmails.de", priority: "low" }
41497
41700
  // Reputation lists
41498
- { name: "Composite BL", zone: "cbl.anti-spam.org.cn", priority: "low" }
41701
+ // cbl.anti-spam.org.cn removed: dead zone, wildcards every query to 208.98.x.x
41499
41702
  ];
41500
41703
  var DEFAULT_TIMEOUT3 = 5e3;
41501
41704
  var SPF_LOOKUP_LIMIT = 10;
@@ -41958,8 +42161,11 @@ var TEST_RETURN_CODES = /* @__PURE__ */ new Set([
41958
42161
  "127.255.255.255"
41959
42162
  // Reserved/test
41960
42163
  ]);
42164
+ function isValidListingResponse(returnCode) {
42165
+ return returnCode.startsWith("127.");
42166
+ }
41961
42167
  function isTestResponse(returnCode) {
41962
- return TEST_RETURN_CODES.has(returnCode);
42168
+ return !isValidListingResponse(returnCode) || TEST_RETURN_CODES.has(returnCode);
41963
42169
  }
41964
42170
  async function checkBlacklist(options) {
41965
42171
  const { quick = false, skip = false, domain, ips = [] } = options;
@@ -43260,7 +43466,7 @@ async function parseAndValidateSpf(domain, record, result) {
43260
43466
  result.valid = false;
43261
43467
  } else if (allMechanism === "?all") {
43262
43468
  result.warnings.push(
43263
- "SPF uses ?all (neutral) - consider using -all or ~all"
43469
+ "SPF uses ?all (neutral) - consider using ~all or -all"
43264
43470
  );
43265
43471
  } else if (!allMechanism) {
43266
43472
  result.warnings.push(
@@ -43396,7 +43602,7 @@ var GRADE_BANDS = {
43396
43602
  function calculateScore(checks) {
43397
43603
  const deductions = [];
43398
43604
  const bonuses = [];
43399
- collectSpfIssues(checks.spf, deductions);
43605
+ collectSpfIssues(checks.spf, checks.dmarc, deductions);
43400
43606
  collectDkimIssues(checks, deductions, bonuses);
43401
43607
  collectDmarcIssues(checks.dmarc, deductions, bonuses);
43402
43608
  collectInfraIssues(checks, deductions, bonuses);
@@ -43497,7 +43703,22 @@ function determineGrade(checks) {
43497
43703
  }
43498
43704
  return "F";
43499
43705
  }
43500
- function collectSpfIssues(spf, deductions) {
43706
+ function dmarcEnforces(dmarc) {
43707
+ return dmarc.exists && dmarc.valid && !dmarc.testing && (dmarc.policy === "quarantine" || dmarc.policy === "reject");
43708
+ }
43709
+ var AUTHORIZING_MECHANISM = /^(?:a|mx|ptr)(?:[:/]|$)|^(?:ip4|ip6|include|exists):/i;
43710
+ function authorizesSenders(spf) {
43711
+ return (spf.record ?? "").trim().split(/\s+/).slice(1).some((term) => {
43712
+ if (/^redirect=/i.test(term)) {
43713
+ return true;
43714
+ }
43715
+ return AUTHORIZING_MECHANISM.test(term.replace(/^[+\-~?]/, ""));
43716
+ });
43717
+ }
43718
+ function hardfailWithoutEnforcingDmarc(spf, dmarc) {
43719
+ return spf.allMechanism === "-all" && authorizesSenders(spf) && !dmarcEnforces(dmarc);
43720
+ }
43721
+ function collectSpfIssues(spf, dmarc, deductions) {
43501
43722
  if (!spf.exists) {
43502
43723
  deductions.push({ check: "spf", points: 5, reason: "No SPF record" });
43503
43724
  return;
@@ -43536,11 +43757,11 @@ function collectSpfIssues(spf, deductions) {
43536
43757
  points: 3,
43537
43758
  reason: "SPF ?all is too permissive"
43538
43759
  });
43539
- } else if (spf.allMechanism === "~all") {
43760
+ } else if (hardfailWithoutEnforcingDmarc(spf, dmarc)) {
43540
43761
  deductions.push({
43541
43762
  check: "spf",
43542
43763
  points: 2,
43543
- reason: "SPF ~all (softfail) instead of -all (hardfail)"
43764
+ reason: "SPF -all (hardfail) without an enforcing DMARC policy \u2014 forwarded mail is rejected before DKIM is checked"
43544
43765
  });
43545
43766
  }
43546
43767
  if (spf.lookupCount > 10) {
@@ -44081,7 +44302,7 @@ function displayResults2(result, options) {
44081
44302
  console.log();
44082
44303
  console.log(pc21.bold("AUTHENTICATION"));
44083
44304
  console.log();
44084
- displaySpfResult(spf, options.verbose);
44305
+ displaySpfResult(spf, dmarc, options.verbose);
44085
44306
  displayDkimResult(dkim, options.verbose);
44086
44307
  displayDmarcResult(dmarc);
44087
44308
  displayBimiResult(bimi, result.domain);
@@ -44221,7 +44442,7 @@ function displayScoreBox(domain, score, grade, quick) {
44221
44442
  console.log(pc21.dim("\u2502") + " ".repeat(width - 2) + pc21.dim("\u2502"));
44222
44443
  console.log(pc21.dim(`\u2570${"\u2500".repeat(width - 2)}\u256F`));
44223
44444
  }
44224
- function displaySpfResult(spf, verbose) {
44445
+ function displaySpfResult(spf, dmarc, verbose) {
44225
44446
  const status2 = spf.exists && spf.valid ? pc21.green("\u2713") : spf.exists ? pc21.yellow("\u26A0") : pc21.red("\u2717");
44226
44447
  console.log(
44227
44448
  ` ${status2} ${pc21.dim("SPF")} ${spf.record || "Not found"}`
@@ -44237,9 +44458,9 @@ function displaySpfResult(spf, verbose) {
44237
44458
  " " + formatSpfLookupTree(spf.lookupTree).split("\n").join("\n ")
44238
44459
  );
44239
44460
  }
44240
- if (spf.allMechanism === "~all") {
44461
+ if (hardfailWithoutEnforcingDmarc(spf, dmarc)) {
44241
44462
  console.log(
44242
- ` ${pc21.dim("Softfail (~all) \u2014 consider using -all for strict enforcement")}`
44463
+ ` ${pc21.yellow("Hardfail (-all) with no enforcing DMARC \u2014 forwarded mail is rejected before DKIM is checked")}`
44243
44464
  );
44244
44465
  }
44245
44466
  }
@@ -45272,6 +45493,17 @@ async function getEmailIdentityInfo(domain, region) {
45272
45493
  throw error;
45273
45494
  }
45274
45495
  }
45496
+ function reportEmailDNSCleanup(result) {
45497
+ for (const label of result.deleted) {
45498
+ clack22.log.success(`Deleted DNS record: ${label}`);
45499
+ }
45500
+ for (const skip of result.skipped) {
45501
+ clack22.log.warn(`Left DNS record in place: ${skip.record} (${skip.reason})`);
45502
+ }
45503
+ for (const err of result.errors) {
45504
+ clack22.log.warn(`DNS cleanup error: ${err}`);
45505
+ }
45506
+ }
45275
45507
  function selectAgentsToDestroy(emailConfig) {
45276
45508
  const agentsConfig = emailConfig?.agents;
45277
45509
  if (!agentsConfig?.enabled) {
@@ -45419,7 +45651,7 @@ async function emailDestroy(options) {
45419
45651
  shouldCleanDNS = true;
45420
45652
  } else {
45421
45653
  const cleanDNS = await clack22.confirm({
45422
- message: `Found Route53 hosted zone for ${pc24.cyan(domain)}. Delete DNS records (DKIM, DMARC, MAIL FROM)?`,
45654
+ message: `Found Route53 hosted zone for ${pc24.cyan(domain)}. Delete the DNS records Wraps created (DKIM, DMARC, tracking CNAME, MAIL FROM)? Records Wraps did not create are left in place.`,
45423
45655
  initialValue: true
45424
45656
  });
45425
45657
  if (clack22.isCancel(cleanDNS)) {
@@ -45495,16 +45727,18 @@ async function emailDestroy(options) {
45495
45727
  }
45496
45728
  if (shouldCleanDNS && hostedZone && domain && dkimTokens.length > 0) {
45497
45729
  try {
45498
- await progress.execute(`Deleting DNS records for ${domain}`, async () => {
45499
- await deleteDNSRecords(
45730
+ const result = await progress.execute(
45731
+ `Deleting DNS records for ${domain}`,
45732
+ () => deleteDNSRecords(
45500
45733
  hostedZone.id,
45501
45734
  domain,
45502
45735
  dkimTokens,
45503
45736
  region,
45504
45737
  emailConfig?.tracking?.customRedirectDomain,
45505
45738
  mailFromDomain
45506
- );
45507
- });
45739
+ )
45740
+ );
45741
+ reportEmailDNSCleanup(result);
45508
45742
  } catch (error) {
45509
45743
  const msg = error instanceof Error ? error.message : String(error);
45510
45744
  clack22.log.warn(`Could not delete DNS records: ${msg}`);
@@ -45769,8 +46003,8 @@ import {
45769
46003
  UpdateDistributionCommand
45770
46004
  } from "@aws-sdk/client-cloudfront";
45771
46005
  import {
45772
- ChangeResourceRecordSetsCommand as ChangeResourceRecordSetsCommand4,
45773
- Route53Client as Route53Client5
46006
+ ChangeResourceRecordSetsCommand as ChangeResourceRecordSetsCommand5,
46007
+ Route53Client as Route53Client6
45774
46008
  } from "@aws-sdk/client-route-53";
45775
46009
  var US_EAST_1 = "us-east-1";
45776
46010
  var CERT_POLL_ATTEMPTS = 10;
@@ -45971,9 +46205,9 @@ async function pushValidationRecord(provider, sendingDomain, sesRegion, record)
45971
46205
  return result.success;
45972
46206
  }
45973
46207
  if (credResult.credentials.provider === "route53") {
45974
- const route533 = new Route53Client5({ region: sesRegion });
46208
+ const route533 = new Route53Client6({ region: sesRegion });
45975
46209
  await route533.send(
45976
- new ChangeResourceRecordSetsCommand4({
46210
+ new ChangeResourceRecordSetsCommand5({
45977
46211
  HostedZoneId: credResult.credentials.hostedZoneId,
45978
46212
  ChangeBatch: {
45979
46213
  Changes: [
@@ -49225,9 +49459,9 @@ async function inboundInit(options) {
49225
49459
  region,
49226
49460
  domain
49227
49461
  );
49228
- if (result.success && result.recordsCreated > 0) {
49462
+ if (result.success) {
49229
49463
  progress.succeed(
49230
- `Created ${result.recordsCreated} DNS records in ${getDNSProviderDisplayName2(dnsProvider)}`
49464
+ result.recordsCreated > 0 ? `Created ${result.recordsCreated} DNS records in ${getDNSProviderDisplayName2(dnsProvider)}` : `DNS already configured for ${receivingDomain}`
49231
49465
  );
49232
49466
  dnsAutoCreated = true;
49233
49467
  } else {
@@ -49356,6 +49590,12 @@ Deploy first: ${pc28.cyan("wraps email inbound init")}
49356
49590
  clack27.log.warn(
49357
49591
  `This will remove inbound email for ${pc28.cyan(inboundConfig.receivingDomain || "")}`
49358
49592
  );
49593
+ const replyThreading = emailService.config.replyThreading;
49594
+ if (replyThreading?.enabled && replyThreading.domains.length > 0) {
49595
+ clack27.log.warn(
49596
+ `Reply threading will stop working for ${replyThreading.domains.map((d6) => pc28.cyan(d6.domain)).join(", ")} once inbound infrastructure is removed. Run ${pc28.cyan("wraps email reply destroy")} to clean those up.`
49597
+ );
49598
+ }
49359
49599
  if (allInboundDomains.length > 0) {
49360
49600
  const { buildInboundDNSRecords: buildRecordsForDisplay } = await Promise.resolve().then(() => (init_dns(), dns_exports));
49361
49601
  clack27.log.info(pc28.bold("DNS records that will be deleted:"));
@@ -50036,9 +50276,9 @@ Deploy first: ${pc28.cyan("wraps email inbound init")}
50036
50276
  region,
50037
50277
  parentDomain
50038
50278
  );
50039
- if (result.success && result.recordsCreated > 0) {
50279
+ if (result.success) {
50040
50280
  progress.succeed(
50041
- `Created ${result.recordsCreated} DNS records in ${getDNSProviderDisplayName2(dnsProvider)}`
50281
+ result.recordsCreated > 0 ? `Created ${result.recordsCreated} DNS records in ${getDNSProviderDisplayName2(dnsProvider)}` : `DNS already configured for ${receivingDomain}`
50042
50282
  );
50043
50283
  dnsAutoCreated = true;
50044
50284
  if (replyThreadingEnabled) {
@@ -51854,9 +52094,9 @@ to diagnose, or retry the init.`,
51854
52094
  region,
51855
52095
  domain
51856
52096
  );
51857
- if (result.success && result.recordsCreated > 0) {
52097
+ if (result.success) {
51858
52098
  progress.succeed(
51859
- `Created ${result.recordsCreated} DNS records for r.mail.${domain} via ${getDNSProviderDisplayName2(dnsProvider)}`
52099
+ result.recordsCreated > 0 ? `Created ${result.recordsCreated} DNS records for r.mail.${domain} via ${getDNSProviderDisplayName2(dnsProvider)}` : `DNS already configured for r.mail.${domain}`
51860
52100
  );
51861
52101
  dnsAutoCreated = true;
51862
52102
  }
@@ -52327,17 +52567,65 @@ function stripDomainFromReplyThreadingMetadata(params) {
52327
52567
  domains: remaining
52328
52568
  };
52329
52569
  }
52570
+ var UNSUPPORTED_REPLY_DNS_CLEANUP = {
52571
+ deleted: [],
52572
+ skipped: [],
52573
+ supported: false,
52574
+ errors: []
52575
+ };
52330
52576
  async function cleanupDomainInfra(params) {
52331
- const { domain, region, progress } = params;
52577
+ const { domain, region, progress, metadata } = params;
52332
52578
  await progress.execute(
52333
52579
  `Removing r.mail.${domain} from receipt rule`,
52334
52580
  async () => {
52335
52581
  await removeDomainFromReceiptRule(region, `r.mail.${domain}`);
52336
52582
  }
52337
52583
  );
52338
- clack34.log.info(
52339
- `Remove MX/SPF DNS records for ${pc35.cyan(`r.mail.${domain}`)} from your DNS provider.`
52340
- );
52584
+ const dnsProvider = metadata.services.email?.dnsProvider;
52585
+ if (!dnsProvider || dnsProvider === "manual") {
52586
+ return UNSUPPORTED_REPLY_DNS_CLEANUP;
52587
+ }
52588
+ try {
52589
+ const { getDNSCredentials: getDNSCredentials2, deleteInboundDNSRecordsForProvider: deleteInboundDNSRecordsForProvider2 } = await Promise.resolve().then(() => (init_dns(), dns_exports));
52590
+ const credentialResult = await getDNSCredentials2(
52591
+ dnsProvider,
52592
+ domain,
52593
+ region
52594
+ );
52595
+ if (!(credentialResult.valid && credentialResult.credentials)) {
52596
+ clack34.log.warn(
52597
+ `Could not validate ${dnsProvider} credentials to delete DNS records for r.mail.${domain}: ${credentialResult.error || "unknown error"}`
52598
+ );
52599
+ return UNSUPPORTED_REPLY_DNS_CLEANUP;
52600
+ }
52601
+ return await deleteInboundDNSRecordsForProvider2(
52602
+ credentialResult.credentials,
52603
+ `r.mail.${domain}`,
52604
+ region,
52605
+ domain
52606
+ );
52607
+ } catch (error) {
52608
+ clack34.log.warn(
52609
+ `Failed to delete DNS records for r.mail.${domain}: ${error instanceof Error ? error.message : String(error)}`
52610
+ );
52611
+ return UNSUPPORTED_REPLY_DNS_CLEANUP;
52612
+ }
52613
+ }
52614
+ function reportReplyDNSCleanup(result, domain) {
52615
+ for (const label of result.deleted) {
52616
+ clack34.log.success(`Deleted DNS record: ${label}`);
52617
+ }
52618
+ for (const skip of result.skipped) {
52619
+ clack34.log.warn(`Left DNS record in place: ${skip.record} (${skip.reason})`);
52620
+ }
52621
+ for (const err of result.errors) {
52622
+ clack34.log.warn(`DNS cleanup error: ${err}`);
52623
+ }
52624
+ if (!result.supported) {
52625
+ clack34.log.info(
52626
+ `Remove MX/SPF DNS records for ${pc35.cyan(`r.mail.${domain}`)} from your DNS provider.`
52627
+ );
52628
+ }
52341
52629
  }
52342
52630
  async function replyDestroy(options) {
52343
52631
  if (!isJsonMode()) {
@@ -52447,20 +52735,33 @@ async function replyDestroy(options) {
52447
52735
  }
52448
52736
  );
52449
52737
  }
52738
+ const dnsCleanupByDomain = [];
52450
52739
  for (const domain of targets) {
52451
- await cleanupDomainInfra({ domain, region, progress });
52740
+ const result = await cleanupDomainInfra({
52741
+ domain,
52742
+ region,
52743
+ progress,
52744
+ metadata
52745
+ });
52746
+ dnsCleanupByDomain.push({ domain, result });
52452
52747
  }
52453
52748
  await saveConnectionMetadata(metadata);
52454
52749
  if (isJsonMode()) {
52455
52750
  jsonSuccess("email.reply.destroy", {
52456
52751
  removed: targets,
52457
- region
52752
+ region,
52753
+ dnsDeleted: dnsCleanupByDomain.flatMap((d6) => d6.result.deleted),
52754
+ dnsSkipped: dnsCleanupByDomain.flatMap((d6) => d6.result.skipped),
52755
+ dnsSupported: dnsCleanupByDomain.some((d6) => d6.result.supported)
52458
52756
  });
52459
52757
  return;
52460
52758
  }
52461
52759
  console.log();
52462
52760
  clack34.log.success(`Removed reply threading for ${targets.join(", ")}.`);
52463
52761
  console.log();
52762
+ for (const { domain, result } of dnsCleanupByDomain) {
52763
+ reportReplyDNSCleanup(result, domain);
52764
+ }
52464
52765
  }
52465
52766
  async function replyDecode(addressInput, options) {
52466
52767
  const progress = new DeploymentProgress();