@wraps.dev/cli 2.14.8 → 2.15.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
@@ -4102,6 +4102,7 @@ var init_route53 = __esm({
4102
4102
  var metadata_exports = {};
4103
4103
  __export(metadata_exports, {
4104
4104
  addDomainToMetadata: () => addDomainToMetadata,
4105
+ addInboundDomainToMetadata: () => addInboundDomainToMetadata,
4105
4106
  addServiceToConnection: () => addServiceToConnection,
4106
4107
  applyConfigUpdates: () => applyConfigUpdates,
4107
4108
  buildEmailStackConfig: () => buildEmailStackConfig,
@@ -4117,7 +4118,9 @@ __export(metadata_exports, {
4117
4118
  hasService: () => hasService,
4118
4119
  listConnections: () => listConnections,
4119
4120
  loadConnectionMetadata: () => loadConnectionMetadata,
4121
+ migrateInboundToMultiDomain: () => migrateInboundToMultiDomain,
4120
4122
  removeDomainFromMetadata: () => removeDomainFromMetadata,
4123
+ removeInboundDomainFromMetadata: () => removeInboundDomainFromMetadata,
4121
4124
  removeServiceFromConnection: () => removeServiceFromConnection,
4122
4125
  saveConnectionMetadata: () => saveConnectionMetadata,
4123
4126
  updateEmailConfig: () => updateEmailConfig,
@@ -4180,6 +4183,9 @@ async function loadConnectionMetadata(accountId, region) {
4180
4183
  }
4181
4184
  localData = data;
4182
4185
  }
4186
+ if (localData && migrateInboundToMultiDomain(localData)) {
4187
+ await saveConnectionMetadataLocal(localData);
4188
+ }
4183
4189
  } catch (error) {
4184
4190
  console.error(
4185
4191
  "Error loading connection metadata:",
@@ -4599,6 +4605,61 @@ function getAllTrackedDomains(metadata) {
4599
4605
  }
4600
4606
  return result;
4601
4607
  }
4608
+ function migrateInboundToMultiDomain(metadata) {
4609
+ const emailConfig = metadata.services.email?.config;
4610
+ if (!emailConfig?.inbound?.enabled) {
4611
+ return false;
4612
+ }
4613
+ if (emailConfig.inboundDomains && emailConfig.inboundDomains.length > 0) {
4614
+ return false;
4615
+ }
4616
+ const inbound = emailConfig.inbound;
4617
+ const receivingDomain = inbound.receivingDomain || (inbound.subdomain && emailConfig.domain ? `${inbound.subdomain}.${emailConfig.domain}` : null);
4618
+ if (!receivingDomain) {
4619
+ return false;
4620
+ }
4621
+ const parentDomain = emailConfig.domain || "";
4622
+ const subdomain = inbound.subdomain || "inbound";
4623
+ emailConfig.inboundDomains = [
4624
+ {
4625
+ subdomain,
4626
+ receivingDomain,
4627
+ parentDomain,
4628
+ addedAt: metadata.services.email?.deployedAt || (/* @__PURE__ */ new Date()).toISOString()
4629
+ }
4630
+ ];
4631
+ return true;
4632
+ }
4633
+ function addInboundDomainToMetadata(metadata, entry) {
4634
+ if (!metadata.services.email) {
4635
+ throw new Error("Email service not configured in metadata");
4636
+ }
4637
+ const config2 = metadata.services.email.config;
4638
+ const existing = config2.inboundDomains ?? [];
4639
+ const idx = existing.findIndex(
4640
+ (d) => d.receivingDomain === entry.receivingDomain
4641
+ );
4642
+ if (idx >= 0) {
4643
+ existing[idx] = entry;
4644
+ } else {
4645
+ existing.push(entry);
4646
+ }
4647
+ config2.inboundDomains = existing;
4648
+ metadata.timestamp = (/* @__PURE__ */ new Date()).toISOString();
4649
+ }
4650
+ function removeInboundDomainFromMetadata(metadata, receivingDomain) {
4651
+ if (!metadata.services.email) {
4652
+ return;
4653
+ }
4654
+ const config2 = metadata.services.email.config;
4655
+ if (!config2.inboundDomains) {
4656
+ return;
4657
+ }
4658
+ config2.inboundDomains = config2.inboundDomains.filter(
4659
+ (d) => d.receivingDomain !== receivingDomain
4660
+ );
4661
+ metadata.timestamp = (/* @__PURE__ */ new Date()).toISOString();
4662
+ }
4602
4663
  var init_metadata = __esm({
4603
4664
  "src/utils/shared/metadata.ts"() {
4604
4665
  "use strict";
@@ -5781,14 +5842,18 @@ var init_lambda = __esm({
5781
5842
  var acm_exports = {};
5782
5843
  __export(acm_exports, {
5783
5844
  checkCertificateValidation: () => checkCertificateValidation,
5784
- createACMCertificate: () => createACMCertificate
5845
+ createACMCertificate: () => createACMCertificate,
5846
+ getCertificateValidationRecords: () => getCertificateValidationRecords
5785
5847
  });
5786
- import { ACMClient as ACMClient2, DescribeCertificateCommand as DescribeCertificateCommand2 } from "@aws-sdk/client-acm";
5848
+ import {
5849
+ ACMClient as ACMClient2,
5850
+ DescribeCertificateCommand as DescribeCertificateCommand2,
5851
+ ListCertificatesCommand
5852
+ } from "@aws-sdk/client-acm";
5787
5853
  import * as aws12 from "@pulumi/aws";
5788
5854
  async function checkCertificateValidation(domain) {
5789
5855
  try {
5790
5856
  const acm3 = new ACMClient2({ region: "us-east-1" });
5791
- const { ListCertificatesCommand } = await import("@aws-sdk/client-acm");
5792
5857
  const listResponse = await acm3.send(
5793
5858
  new ListCertificatesCommand({
5794
5859
  CertificateStatuses: ["ISSUED"]
@@ -5864,6 +5929,31 @@ async function createACMCertificate(config2) {
5864
5929
  validationRecords
5865
5930
  };
5866
5931
  }
5932
+ async function getCertificateValidationRecords(domain) {
5933
+ const acm3 = new ACMClient2({ region: "us-east-1" });
5934
+ const listResponse = await acm3.send(
5935
+ new ListCertificatesCommand({
5936
+ CertificateStatuses: ["PENDING_VALIDATION", "ISSUED"]
5937
+ })
5938
+ );
5939
+ const cert = listResponse.CertificateSummaryList?.find(
5940
+ (c) => c.DomainName === domain
5941
+ );
5942
+ if (!cert?.CertificateArn) {
5943
+ return [];
5944
+ }
5945
+ const describeResponse = await acm3.send(
5946
+ new DescribeCertificateCommand2({
5947
+ CertificateArn: cert.CertificateArn
5948
+ })
5949
+ );
5950
+ const options = describeResponse.Certificate?.DomainValidationOptions ?? [];
5951
+ return options.filter((opt) => opt.ResourceRecord?.Name && opt.ResourceRecord?.Value).map((opt) => ({
5952
+ name: opt.ResourceRecord.Name,
5953
+ type: opt.ResourceRecord.Type ?? "CNAME",
5954
+ value: opt.ResourceRecord.Value
5955
+ }));
5956
+ }
5867
5957
  var init_acm = __esm({
5868
5958
  "src/infrastructure/resources/acm.ts"() {
5869
5959
  "use strict";
@@ -6638,7 +6728,7 @@ var init_cloudflare = __esm({
6638
6728
  };
6639
6729
  }
6640
6730
  async createEmailRecords(data) {
6641
- const { domain, dkimTokens, mailFromDomain, region } = data;
6731
+ const { domain, dkimTokens, mailFromDomain, customTrackingDomain, region } = data;
6642
6732
  const errors2 = [];
6643
6733
  let recordsCreated = 0;
6644
6734
  try {
@@ -6675,6 +6765,20 @@ var init_cloudflare = __esm({
6675
6765
  } else {
6676
6766
  errors2.push(`Failed to create DMARC record for ${domain}`);
6677
6767
  }
6768
+ if (customTrackingDomain) {
6769
+ const trackingSuccess = await this.createRecord(
6770
+ customTrackingDomain,
6771
+ "CNAME",
6772
+ `r.${region}.awstrack.me`
6773
+ );
6774
+ if (trackingSuccess) {
6775
+ recordsCreated++;
6776
+ } else {
6777
+ errors2.push(
6778
+ `Failed to create tracking CNAME for ${customTrackingDomain}`
6779
+ );
6780
+ }
6781
+ }
6678
6782
  if (mailFromDomain) {
6679
6783
  const mxSuccess = await this.createRecord(
6680
6784
  mailFromDomain,
@@ -7006,7 +7110,7 @@ var init_vercel = __esm({
7006
7110
  };
7007
7111
  }
7008
7112
  async createEmailRecords(data) {
7009
- const { domain, dkimTokens, mailFromDomain, region } = data;
7113
+ const { domain, dkimTokens, mailFromDomain, customTrackingDomain, region } = data;
7010
7114
  const errors2 = [];
7011
7115
  let recordsCreated = 0;
7012
7116
  try {
@@ -7043,6 +7147,20 @@ var init_vercel = __esm({
7043
7147
  } else {
7044
7148
  errors2.push(`Failed to create DMARC record for ${domain}`);
7045
7149
  }
7150
+ if (customTrackingDomain) {
7151
+ const trackingSuccess = await this.createRecord(
7152
+ customTrackingDomain,
7153
+ "CNAME",
7154
+ `r.${region}.awstrack.me`
7155
+ );
7156
+ if (trackingSuccess) {
7157
+ recordsCreated++;
7158
+ } else {
7159
+ errors2.push(
7160
+ `Failed to create tracking CNAME for ${customTrackingDomain}`
7161
+ );
7162
+ }
7163
+ }
7046
7164
  if (mailFromDomain) {
7047
7165
  const mxSuccess = await this.createRecord(
7048
7166
  mailFromDomain,
@@ -7258,7 +7376,7 @@ import {
7258
7376
  Route53Client as Route53Client2
7259
7377
  } from "@aws-sdk/client-route-53";
7260
7378
  function buildEmailDNSRecords(data) {
7261
- const { domain, dkimTokens, mailFromDomain, region } = data;
7379
+ const { domain, dkimTokens, mailFromDomain, customTrackingDomain, region } = data;
7262
7380
  const records = [];
7263
7381
  for (const token of dkimTokens) {
7264
7382
  records.push({
@@ -7281,6 +7399,14 @@ function buildEmailDNSRecords(data) {
7281
7399
  value: `v=DMARC1; p=quarantine; rua=mailto:postmaster@${dmarcRuaDomain}`,
7282
7400
  category: "dmarc"
7283
7401
  });
7402
+ if (customTrackingDomain) {
7403
+ records.push({
7404
+ name: customTrackingDomain,
7405
+ type: "CNAME",
7406
+ value: `r.${region}.awstrack.me`,
7407
+ category: "tracking"
7408
+ });
7409
+ }
7284
7410
  if (mailFromDomain) {
7285
7411
  records.push({
7286
7412
  name: mailFromDomain,
@@ -7341,8 +7467,7 @@ async function createDNSRecordsForProvider(credentials, data, selectedCategories
7341
7467
  data.dkimTokens,
7342
7468
  data.region,
7343
7469
  categories,
7344
- void 0,
7345
- // customTrackingDomain - not used here
7470
+ data.customTrackingDomain,
7346
7471
  data.mailFromDomain
7347
7472
  );
7348
7473
  let recordsCreated = 0;
@@ -19098,7 +19223,8 @@ import {
19098
19223
  DescribeActiveReceiptRuleSetCommand,
19099
19224
  DescribeReceiptRuleCommand,
19100
19225
  SESClient as SESClient3,
19101
- SetActiveReceiptRuleSetCommand
19226
+ SetActiveReceiptRuleSetCommand,
19227
+ UpdateReceiptRuleCommand
19102
19228
  } from "@aws-sdk/client-ses";
19103
19229
  var RULE_SET_NAME = "wraps-inbound-rules";
19104
19230
  var RULE_NAME = "wraps-inbound-catch-all";
@@ -19221,6 +19347,77 @@ async function deleteReceiptRuleSet(region) {
19221
19347
  throw error;
19222
19348
  }
19223
19349
  }
19350
+ async function addDomainToReceiptRule(region, domain, s3BucketName) {
19351
+ const ses = createSESClient(region);
19352
+ try {
19353
+ const response = await ses.send(
19354
+ new DescribeReceiptRuleCommand({
19355
+ RuleSetName: RULE_SET_NAME,
19356
+ RuleName: RULE_NAME
19357
+ })
19358
+ );
19359
+ const existingRecipients = response.Rule?.Recipients ?? [];
19360
+ if (existingRecipients.includes(domain)) {
19361
+ return;
19362
+ }
19363
+ await ses.send(
19364
+ new UpdateReceiptRuleCommand({
19365
+ RuleSetName: RULE_SET_NAME,
19366
+ Rule: {
19367
+ Name: RULE_NAME,
19368
+ Enabled: response.Rule?.Enabled,
19369
+ ScanEnabled: response.Rule?.ScanEnabled,
19370
+ TlsPolicy: response.Rule?.TlsPolicy,
19371
+ Actions: response.Rule?.Actions,
19372
+ Recipients: [...existingRecipients, domain]
19373
+ }
19374
+ })
19375
+ );
19376
+ } catch (error) {
19377
+ if (error instanceof Error && (error.name === "RuleDoesNotExistException" || error.name === "RuleSetDoesNotExistException")) {
19378
+ await createReceiptRuleSet(region);
19379
+ await createReceiptRule(region, domain, s3BucketName);
19380
+ await setActiveReceiptRuleSet(region, RULE_SET_NAME);
19381
+ return;
19382
+ }
19383
+ throw error;
19384
+ }
19385
+ }
19386
+ async function removeDomainFromReceiptRule(region, domain) {
19387
+ const ses = createSESClient(region);
19388
+ try {
19389
+ const response = await ses.send(
19390
+ new DescribeReceiptRuleCommand({
19391
+ RuleSetName: RULE_SET_NAME,
19392
+ RuleName: RULE_NAME
19393
+ })
19394
+ );
19395
+ const existingRecipients = response.Rule?.Recipients ?? [];
19396
+ const updated = existingRecipients.filter((r) => r !== domain);
19397
+ if (updated.length === 0) {
19398
+ await deleteReceiptRule(region);
19399
+ return;
19400
+ }
19401
+ await ses.send(
19402
+ new UpdateReceiptRuleCommand({
19403
+ RuleSetName: RULE_SET_NAME,
19404
+ Rule: {
19405
+ Name: RULE_NAME,
19406
+ Enabled: response.Rule?.Enabled,
19407
+ ScanEnabled: response.Rule?.ScanEnabled,
19408
+ TlsPolicy: response.Rule?.TlsPolicy,
19409
+ Actions: response.Rule?.Actions,
19410
+ Recipients: updated
19411
+ }
19412
+ })
19413
+ );
19414
+ } catch (error) {
19415
+ if (error instanceof Error && (error.name === "RuleDoesNotExistException" || error.name === "RuleSetDoesNotExistException")) {
19416
+ return;
19417
+ }
19418
+ throw error;
19419
+ }
19420
+ }
19224
19421
 
19225
19422
  // src/commands/email/inbound.ts
19226
19423
  init_aws();
@@ -19299,7 +19496,15 @@ async function inboundInit(options) {
19299
19496
  bucketName: `wraps-inbound-${identity.accountId}-${region}`,
19300
19497
  webhookUrl,
19301
19498
  webhookSecret
19302
- }
19499
+ },
19500
+ inboundDomains: [
19501
+ {
19502
+ subdomain,
19503
+ receivingDomain,
19504
+ parentDomain: domain,
19505
+ addedAt: (/* @__PURE__ */ new Date()).toISOString()
19506
+ }
19507
+ ]
19303
19508
  };
19304
19509
  const stackConfig = buildEmailStackConfig(metadata, region, {
19305
19510
  emailConfig: updatedEmailConfig
@@ -19540,7 +19745,8 @@ Deploy first: ${pc21.cyan("wraps email inbound init")}
19540
19745
  const stackName = emailService.pulumiStackName || `wraps-${identity.accountId}-${region}`;
19541
19746
  const updatedEmailConfig = {
19542
19747
  ...emailService.config,
19543
- inbound: void 0
19748
+ inbound: void 0,
19749
+ inboundDomains: void 0
19544
19750
  };
19545
19751
  const stackConfig = buildEmailStackConfig(metadata, region, {
19546
19752
  emailConfig: updatedEmailConfig
@@ -19609,13 +19815,18 @@ Enable it: ${pc21.cyan("wraps email inbound init")}
19609
19815
  `);
19610
19816
  return;
19611
19817
  }
19612
- const inbound = metadata.services.email.config.inbound;
19818
+ const emailConfig = metadata.services.email.config;
19819
+ const inbound = emailConfig.inbound;
19820
+ const inboundDomains = emailConfig.inboundDomains ?? [];
19613
19821
  const activeRuleSet = await getActiveReceiptRuleSet(region);
19614
- const receivingDomain = inbound.receivingDomain || `${inbound.subdomain}.${metadata.services.email.config.domain}`;
19822
+ const domainList = inboundDomains.length > 0 ? inboundDomains.map((d) => d.receivingDomain) : [
19823
+ inbound.receivingDomain || `${inbound.subdomain}.${emailConfig.domain}`
19824
+ ];
19615
19825
  if (isJsonMode()) {
19616
19826
  jsonSuccess("email.inbound.status", {
19617
19827
  enabled: true,
19618
- receivingDomain,
19828
+ receivingDomains: domainList,
19829
+ receivingDomain: domainList[0],
19619
19830
  bucketName: inbound.bucketName || "",
19620
19831
  region,
19621
19832
  webhookUrl: inbound.webhookUrl || null,
@@ -19627,7 +19838,14 @@ Enable it: ${pc21.cyan("wraps email inbound init")}
19627
19838
  console.log();
19628
19839
  console.log(pc21.bold(" Inbound Email Configuration"));
19629
19840
  console.log();
19630
- console.log(` ${pc21.dim("Receiving domain:")} ${pc21.cyan(receivingDomain)}`);
19841
+ if (domainList.length === 1) {
19842
+ console.log(` ${pc21.dim("Receiving domain:")} ${pc21.cyan(domainList[0])}`);
19843
+ } else {
19844
+ console.log(` ${pc21.dim("Receiving domains:")}`);
19845
+ for (const d of domainList) {
19846
+ console.log(` ${pc21.cyan(d)}`);
19847
+ }
19848
+ }
19631
19849
  console.log(
19632
19850
  ` ${pc21.dim("S3 bucket:")} ${pc21.cyan(inbound.bucketName || "")}`
19633
19851
  );
@@ -19661,61 +19879,77 @@ Enable it: ${pc21.cyan("wraps email inbound init")}
19661
19879
  `);
19662
19880
  process.exit(1);
19663
19881
  }
19664
- const inbound = metadata.services.email.config.inbound;
19665
- const receivingDomain = inbound.receivingDomain || `${inbound.subdomain}.${metadata.services.email.config.domain}`;
19882
+ const emailConfig = metadata.services.email.config;
19883
+ const inbound = emailConfig.inbound;
19884
+ const inboundDomains = emailConfig.inboundDomains ?? [];
19885
+ const domainList = inboundDomains.length > 0 ? inboundDomains.map((d) => d.receivingDomain) : [
19886
+ inbound.receivingDomain || `${inbound.subdomain}.${emailConfig.domain}`
19887
+ ];
19666
19888
  let allPassed = true;
19889
+ const domainChecks = {};
19667
19890
  console.log();
19668
- const mxResult = await progress.execute(
19669
- `Checking MX record for ${receivingDomain}`,
19670
- async () => {
19671
- try {
19672
- const records = await dns2.resolveMx(receivingDomain);
19673
- const hasSES = records.some((r) => r.exchange.includes("inbound-smtp"));
19674
- return { found: true, hasSES, records };
19675
- } catch {
19676
- return { found: false, hasSES: false, records: [] };
19677
- }
19891
+ for (const receivingDomain of domainList) {
19892
+ if (domainList.length > 1) {
19893
+ clack20.log.info(pc21.bold(`Checking ${pc21.cyan(receivingDomain)}`));
19678
19894
  }
19679
- );
19680
- if (mxResult.hasSES) {
19681
- clack20.log.success(
19682
- `MX record: ${pc21.green("verified")} \u2192 inbound-smtp.${region}.amazonaws.com`
19683
- );
19684
- } else if (mxResult.found) {
19685
- clack20.log.warn(
19686
- `MX record found but not pointing to SES. Expected: ${pc21.cyan(`10 inbound-smtp.${region}.amazonaws.com`)}`
19687
- );
19688
- allPassed = false;
19689
- } else {
19690
- clack20.log.error(
19691
- `MX record: ${pc21.red("not found")}. Add: ${pc21.cyan(`${receivingDomain} MX 10 inbound-smtp.${region}.amazonaws.com`)}`
19692
- );
19693
- allPassed = false;
19694
- }
19695
- const spfResult = await progress.execute(
19696
- `Checking SPF record for ${receivingDomain}`,
19697
- async () => {
19698
- try {
19699
- const records = await dns2.resolveTxt(receivingDomain);
19700
- const flat = records.map((r) => r.join(""));
19701
- const spf = flat.find((r) => r.startsWith("v=spf1"));
19702
- const hasSES = spf?.includes("amazonses.com") ?? false;
19703
- return { found: !!spf, hasSES, value: spf };
19704
- } catch {
19705
- return { found: false, hasSES: false, value: null };
19895
+ const mxResult = await progress.execute(
19896
+ `Checking MX record for ${receivingDomain}`,
19897
+ async () => {
19898
+ try {
19899
+ const records = await dns2.resolveMx(receivingDomain);
19900
+ const hasSES = records.some(
19901
+ (r) => r.exchange.includes("inbound-smtp")
19902
+ );
19903
+ return { found: true, hasSES, records };
19904
+ } catch {
19905
+ return { found: false, hasSES: false, records: [] };
19906
+ }
19706
19907
  }
19908
+ );
19909
+ if (mxResult.hasSES) {
19910
+ clack20.log.success(
19911
+ `MX record: ${pc21.green("verified")} \u2192 inbound-smtp.${region}.amazonaws.com`
19912
+ );
19913
+ } else if (mxResult.found) {
19914
+ clack20.log.warn(
19915
+ `MX record found but not pointing to SES. Expected: ${pc21.cyan(`10 inbound-smtp.${region}.amazonaws.com`)}`
19916
+ );
19917
+ allPassed = false;
19918
+ } else {
19919
+ clack20.log.error(
19920
+ `MX record: ${pc21.red("not found")}. Add: ${pc21.cyan(`${receivingDomain} MX 10 inbound-smtp.${region}.amazonaws.com`)}`
19921
+ );
19922
+ allPassed = false;
19707
19923
  }
19708
- );
19709
- if (spfResult.hasSES) {
19710
- clack20.log.success(`SPF record: ${pc21.green("verified")}`);
19711
- } else if (spfResult.found) {
19712
- clack20.log.warn("SPF record exists but missing amazonses.com include");
19713
- allPassed = false;
19714
- } else {
19715
- clack20.log.error(
19716
- `SPF record: ${pc21.red("not found")}. Add TXT: ${pc21.cyan("v=spf1 include:amazonses.com ~all")}`
19924
+ const spfResult = await progress.execute(
19925
+ `Checking SPF record for ${receivingDomain}`,
19926
+ async () => {
19927
+ try {
19928
+ const records = await dns2.resolveTxt(receivingDomain);
19929
+ const flat = records.map((r) => r.join(""));
19930
+ const spf = flat.find((r) => r.startsWith("v=spf1"));
19931
+ const hasSES = spf?.includes("amazonses.com") ?? false;
19932
+ return { found: !!spf, hasSES, value: spf };
19933
+ } catch {
19934
+ return { found: false, hasSES: false, value: null };
19935
+ }
19936
+ }
19717
19937
  );
19718
- allPassed = false;
19938
+ if (spfResult.hasSES) {
19939
+ clack20.log.success(`SPF record: ${pc21.green("verified")}`);
19940
+ } else if (spfResult.found) {
19941
+ clack20.log.warn("SPF record exists but missing amazonses.com include");
19942
+ allPassed = false;
19943
+ } else {
19944
+ clack20.log.error(
19945
+ `SPF record: ${pc21.red("not found")}. Add TXT: ${pc21.cyan("v=spf1 include:amazonses.com ~all")}`
19946
+ );
19947
+ allPassed = false;
19948
+ }
19949
+ domainChecks[receivingDomain] = {
19950
+ mx: { found: mxResult.found, verified: mxResult.hasSES },
19951
+ spf: { found: spfResult.found, verified: spfResult.hasSES }
19952
+ };
19719
19953
  }
19720
19954
  const activeRuleSet = await getActiveReceiptRuleSet(region);
19721
19955
  if (activeRuleSet === RULE_SET_NAME) {
@@ -19728,13 +19962,11 @@ Enable it: ${pc21.cyan("wraps email inbound init")}
19728
19962
  }
19729
19963
  if (isJsonMode()) {
19730
19964
  jsonSuccess("email.inbound.verify", {
19731
- receivingDomain,
19965
+ receivingDomains: domainList,
19966
+ receivingDomain: domainList[0],
19732
19967
  allPassed,
19733
- checks: {
19734
- mx: { found: mxResult.found, verified: mxResult.hasSES },
19735
- spf: { found: spfResult.found, verified: spfResult.hasSES },
19736
- receiptRuleSet: { active: activeRuleSet === RULE_SET_NAME }
19737
- }
19968
+ domainChecks,
19969
+ receiptRuleSet: { active: activeRuleSet === RULE_SET_NAME }
19738
19970
  });
19739
19971
  return;
19740
19972
  }
@@ -19891,6 +20123,296 @@ Enable it: ${pc21.cyan("wraps email inbound init")}
19891
20123
  clack20.log.success(pc21.bold("Inbound email is working correctly!"));
19892
20124
  console.log();
19893
20125
  }
20126
+ async function inboundAdd(options) {
20127
+ if (!isJsonMode()) {
20128
+ clack20.intro(pc21.bold("Add Inbound Receiving Domain"));
20129
+ }
20130
+ const progress = new DeploymentProgress();
20131
+ const identity = await progress.execute(
20132
+ "Validating AWS credentials",
20133
+ async () => validateAWSCredentials()
20134
+ );
20135
+ const region = options.region || await getAWSRegion();
20136
+ if (!SES_RECEIVING_REGIONS.includes(
20137
+ region
20138
+ )) {
20139
+ throw errors.inboundRegionNotSupported(region);
20140
+ }
20141
+ const metadata = await loadConnectionMetadata(identity.accountId, region);
20142
+ if (!metadata?.services?.email?.config?.inbound?.enabled) {
20143
+ clack20.log.error("Inbound email infrastructure is not deployed.");
20144
+ console.log(`
20145
+ Deploy first: ${pc21.cyan("wraps email inbound init")}
20146
+ `);
20147
+ process.exit(1);
20148
+ }
20149
+ const emailConfig = metadata.services.email.config;
20150
+ const primaryDomain = emailConfig.domain || "";
20151
+ const allDomains = [primaryDomain];
20152
+ for (const d of emailConfig.additionalDomains ?? []) {
20153
+ if (!allDomains.includes(d.domain)) {
20154
+ allDomains.push(d.domain);
20155
+ }
20156
+ }
20157
+ let parentDomain = options.domain;
20158
+ if (!parentDomain) {
20159
+ if (options.yes) {
20160
+ parentDomain = primaryDomain;
20161
+ } else if (allDomains.length === 1) {
20162
+ parentDomain = allDomains[0];
20163
+ } else {
20164
+ const selected = await clack20.select({
20165
+ message: "Which domain should the inbound subdomain be under?",
20166
+ options: allDomains.map((d) => ({
20167
+ value: d,
20168
+ label: d,
20169
+ hint: d === primaryDomain ? "primary" : void 0
20170
+ }))
20171
+ });
20172
+ if (clack20.isCancel(selected)) {
20173
+ clack20.cancel("Operation cancelled.");
20174
+ process.exit(0);
20175
+ }
20176
+ parentDomain = selected;
20177
+ }
20178
+ }
20179
+ const subdomain = options.subdomain || (options.yes ? "inbound" : await promptInboundSubdomain(parentDomain));
20180
+ const receivingDomain = `${subdomain}.${parentDomain}`;
20181
+ const existingDomains = emailConfig.inboundDomains ?? [];
20182
+ if (existingDomains.some((d) => d.receivingDomain === receivingDomain)) {
20183
+ clack20.log.warn(
20184
+ `${pc21.cyan(receivingDomain)} is already configured as an inbound domain.`
20185
+ );
20186
+ return;
20187
+ }
20188
+ clack20.log.info(`Adding receiving domain: ${pc21.cyan(receivingDomain)}`);
20189
+ const bucketName = emailConfig.inbound?.bucketName || `wraps-inbound-${identity.accountId}-${region}`;
20190
+ await progress.execute("Updating SES receipt rule", async () => {
20191
+ await addDomainToReceiptRule(region, receivingDomain, bucketName);
20192
+ });
20193
+ let dnsAutoCreated = false;
20194
+ const {
20195
+ detectAvailableDNSProviders: detectAvailableDNSProviders2,
20196
+ getDNSCredentials: getDNSCredentials2,
20197
+ createInboundDNSRecordsForProvider: createInboundDNSRecordsForProvider2,
20198
+ getDNSProviderDisplayName: getDNSProviderDisplayName2,
20199
+ getDNSProviderTokenUrl: getDNSProviderTokenUrl2,
20200
+ buildInboundDNSRecords: buildRecords,
20201
+ formatDNSRecordsForDisplay: formatRecords
20202
+ } = await Promise.resolve().then(() => (init_dns(), dns_exports));
20203
+ const { promptDNSProvider: promptDNSProvider2, promptContinueManualDNS: promptContinueManualDNS2 } = await Promise.resolve().then(() => (init_prompts(), prompts_exports));
20204
+ const existingDnsProvider = metadata.services.email.dnsProvider;
20205
+ let dnsProvider = existingDnsProvider;
20206
+ if (!dnsProvider || dnsProvider === "manual") {
20207
+ progress.start("Detecting DNS providers");
20208
+ const availableProviders = await detectAvailableDNSProviders2(
20209
+ parentDomain,
20210
+ region
20211
+ );
20212
+ progress.stop();
20213
+ dnsProvider = options.yes ? "manual" : await promptDNSProvider2(parentDomain, availableProviders);
20214
+ }
20215
+ if (dnsProvider !== "manual") {
20216
+ progress.start(
20217
+ `Validating ${getDNSProviderDisplayName2(dnsProvider)} credentials`
20218
+ );
20219
+ const credentialResult = await getDNSCredentials2(
20220
+ dnsProvider,
20221
+ parentDomain,
20222
+ region
20223
+ );
20224
+ progress.stop();
20225
+ if (credentialResult.valid && credentialResult.credentials) {
20226
+ const records = buildRecords(receivingDomain, region);
20227
+ clack20.log.info(pc21.bold("DNS records to create:"));
20228
+ for (const record of records) {
20229
+ const value = record.priority ? `${record.priority} ${record.value}` : record.value;
20230
+ clack20.log.info(pc21.dim(` ${record.type} ${record.name} \u2192 ${value}`));
20231
+ }
20232
+ progress.start(
20233
+ `Creating DNS records in ${getDNSProviderDisplayName2(dnsProvider)}`
20234
+ );
20235
+ const result = await createInboundDNSRecordsForProvider2(
20236
+ credentialResult.credentials,
20237
+ receivingDomain,
20238
+ region,
20239
+ parentDomain
20240
+ );
20241
+ if (result.success && result.recordsCreated > 0) {
20242
+ progress.succeed(
20243
+ `Created ${result.recordsCreated} DNS records in ${getDNSProviderDisplayName2(dnsProvider)}`
20244
+ );
20245
+ dnsAutoCreated = true;
20246
+ } else {
20247
+ progress.fail("Failed to create some DNS records");
20248
+ if (result.errors) {
20249
+ for (const err of result.errors) {
20250
+ clack20.log.warn(err);
20251
+ }
20252
+ }
20253
+ }
20254
+ } else {
20255
+ clack20.log.warn(
20256
+ credentialResult.error || "Could not validate credentials"
20257
+ );
20258
+ if (dnsProvider === "vercel" || dnsProvider === "cloudflare") {
20259
+ clack20.log.info(
20260
+ `Set the ${dnsProvider === "vercel" ? "VERCEL_TOKEN" : "CLOUDFLARE_API_TOKEN"} environment variable to enable automatic DNS management.`
20261
+ );
20262
+ clack20.log.info(
20263
+ `You can create a token at: ${pc21.cyan(getDNSProviderTokenUrl2(dnsProvider))}`
20264
+ );
20265
+ }
20266
+ if (!options.yes) {
20267
+ const continueManual = await promptContinueManualDNS2();
20268
+ if (continueManual) {
20269
+ dnsProvider = "manual";
20270
+ }
20271
+ }
20272
+ }
20273
+ }
20274
+ if (!dnsAutoCreated) {
20275
+ const dnsRecords = buildRecords(receivingDomain, region);
20276
+ const formattedRecords = formatRecords(dnsRecords);
20277
+ console.log();
20278
+ clack20.log.info(pc21.bold("DNS Records Required"));
20279
+ console.log(
20280
+ ` Add these records to your DNS provider for ${pc21.cyan(receivingDomain)}:
20281
+ `
20282
+ );
20283
+ for (const record of formattedRecords) {
20284
+ console.log(` ${pc21.dim(record.type.padEnd(6))} ${record.name}`);
20285
+ console.log(` ${pc21.dim("\u2192")} ${pc21.green(record.value)}
20286
+ `);
20287
+ }
20288
+ }
20289
+ await progress.execute("Saving configuration", async () => {
20290
+ addInboundDomainToMetadata(metadata, {
20291
+ subdomain,
20292
+ receivingDomain,
20293
+ parentDomain,
20294
+ addedAt: (/* @__PURE__ */ new Date()).toISOString()
20295
+ });
20296
+ await saveConnectionMetadata(metadata);
20297
+ });
20298
+ if (isJsonMode()) {
20299
+ jsonSuccess("email.inbound.add", {
20300
+ receivingDomain,
20301
+ subdomain,
20302
+ parentDomain,
20303
+ dnsAutoCreated,
20304
+ region
20305
+ });
20306
+ return;
20307
+ }
20308
+ console.log();
20309
+ clack20.log.success(
20310
+ `${pc21.bold("Added inbound domain:")} ${pc21.cyan(receivingDomain)}`
20311
+ );
20312
+ console.log();
20313
+ if (dnsAutoCreated) {
20314
+ console.log(` Verify: ${pc21.cyan("wraps email inbound verify")}`);
20315
+ } else {
20316
+ console.log(` ${pc21.dim("1.")} Add DNS records above to your DNS provider`);
20317
+ console.log(
20318
+ ` ${pc21.dim("2.")} Verify: ${pc21.cyan("wraps email inbound verify")}`
20319
+ );
20320
+ }
20321
+ console.log();
20322
+ }
20323
+ async function inboundRemove(options) {
20324
+ if (!isJsonMode()) {
20325
+ clack20.intro(pc21.bold("Remove Inbound Receiving Domain"));
20326
+ }
20327
+ const progress = new DeploymentProgress();
20328
+ const identity = await progress.execute(
20329
+ "Validating AWS credentials",
20330
+ async () => validateAWSCredentials()
20331
+ );
20332
+ const region = options.region || await getAWSRegion();
20333
+ const metadata = await loadConnectionMetadata(identity.accountId, region);
20334
+ if (!metadata?.services?.email?.config?.inbound?.enabled) {
20335
+ clack20.log.error("Inbound email infrastructure is not deployed.");
20336
+ console.log(`
20337
+ Deploy first: ${pc21.cyan("wraps email inbound init")}
20338
+ `);
20339
+ process.exit(1);
20340
+ }
20341
+ const emailConfig = metadata.services.email.config;
20342
+ const inboundDomains = emailConfig.inboundDomains ?? [];
20343
+ if (inboundDomains.length === 0) {
20344
+ clack20.log.warn("No inbound domains configured.");
20345
+ return;
20346
+ }
20347
+ let domainToRemove = options.domain;
20348
+ if (!domainToRemove) {
20349
+ if (inboundDomains.length === 1) {
20350
+ domainToRemove = inboundDomains[0].receivingDomain;
20351
+ } else {
20352
+ const selected = await clack20.select({
20353
+ message: "Which inbound domain do you want to remove?",
20354
+ options: inboundDomains.map((d) => ({
20355
+ value: d.receivingDomain,
20356
+ label: d.receivingDomain,
20357
+ hint: `added ${d.addedAt.split("T")[0]}`
20358
+ }))
20359
+ });
20360
+ if (clack20.isCancel(selected)) {
20361
+ clack20.cancel("Operation cancelled.");
20362
+ process.exit(0);
20363
+ }
20364
+ domainToRemove = selected;
20365
+ }
20366
+ }
20367
+ if (!inboundDomains.some((d) => d.receivingDomain === domainToRemove)) {
20368
+ clack20.log.error(
20369
+ `${pc21.cyan(domainToRemove)} is not in the inbound domains list.`
20370
+ );
20371
+ return;
20372
+ }
20373
+ if (inboundDomains.length === 1) {
20374
+ clack20.log.error(
20375
+ "Cannot remove the last inbound domain. Use " + pc21.cyan("wraps email inbound destroy") + " to remove all inbound infrastructure."
20376
+ );
20377
+ return;
20378
+ }
20379
+ if (!options.yes) {
20380
+ const confirmed = await clack20.confirm({
20381
+ message: `Remove inbound domain ${pc21.cyan(domainToRemove)}?`,
20382
+ initialValue: false
20383
+ });
20384
+ if (clack20.isCancel(confirmed) || !confirmed) {
20385
+ clack20.cancel("Operation cancelled.");
20386
+ process.exit(0);
20387
+ }
20388
+ }
20389
+ await progress.execute("Updating SES receipt rule", async () => {
20390
+ await removeDomainFromReceiptRule(region, domainToRemove);
20391
+ });
20392
+ await progress.execute("Saving configuration", async () => {
20393
+ removeInboundDomainFromMetadata(metadata, domainToRemove);
20394
+ await saveConnectionMetadata(metadata);
20395
+ });
20396
+ if (isJsonMode()) {
20397
+ jsonSuccess("email.inbound.remove", {
20398
+ removedDomain: domainToRemove,
20399
+ remainingDomains: (emailConfig.inboundDomains ?? []).map(
20400
+ (d) => d.receivingDomain
20401
+ ),
20402
+ region
20403
+ });
20404
+ return;
20405
+ }
20406
+ console.log();
20407
+ clack20.log.success(
20408
+ `${pc21.bold("Removed inbound domain:")} ${pc21.cyan(domainToRemove)}`
20409
+ );
20410
+ console.log();
20411
+ console.log(
20412
+ ` ${pc21.dim("Remember to remove the MX and SPF DNS records for")} ${pc21.cyan(domainToRemove)}`
20413
+ );
20414
+ console.log();
20415
+ }
19894
20416
 
19895
20417
  // src/commands/email/init.ts
19896
20418
  init_esm_shims();
@@ -20459,6 +20981,7 @@ ${pc24.yellow(pc24.bold("Configuration Warnings:"))}`);
20459
20981
  domain: outputs.domain,
20460
20982
  dkimTokens: outputs.dkimTokens,
20461
20983
  mailFromDomain: outputs.mailFromDomain,
20984
+ customTrackingDomain: outputs.customTrackingDomain,
20462
20985
  region
20463
20986
  },
20464
20987
  selectedCategories
@@ -20491,6 +21014,7 @@ ${pc24.yellow(pc24.bold("Configuration Warnings:"))}`);
20491
21014
  domain: outputs.domain,
20492
21015
  dkimTokens: outputs.dkimTokens,
20493
21016
  mailFromDomain: outputs.mailFromDomain,
21017
+ customTrackingDomain: outputs.customTrackingDomain,
20494
21018
  region
20495
21019
  };
20496
21020
  const records = buildEmailDNSRecords2(recordData);
@@ -24367,6 +24891,7 @@ ${pc30.bold("Cost Impact:")}`);
24367
24891
  domain: outputs.domain,
24368
24892
  dkimTokens: outputs.dkimTokens,
24369
24893
  mailFromDomain,
24894
+ customTrackingDomain: outputs.customTrackingDomain,
24370
24895
  region
24371
24896
  };
24372
24897
  try {
@@ -24417,6 +24942,7 @@ ${pc30.bold("Cost Impact:")}`);
24417
24942
  domain: outputs.domain,
24418
24943
  dkimTokens: outputs.dkimTokens,
24419
24944
  mailFromDomain,
24945
+ customTrackingDomain: outputs.customTrackingDomain,
24420
24946
  region
24421
24947
  };
24422
24948
  const dnsRecords = buildEmailDNSRecords(dnsData);
@@ -24461,6 +24987,13 @@ ${pc30.bold("Add these DNS records to your DNS provider:")}
24461
24987
  if (outputs.httpsTrackingEnabled && outputs.acmCertificateValidationRecords) {
24462
24988
  acmValidationRecords.push(...outputs.acmCertificateValidationRecords);
24463
24989
  }
24990
+ if (acmValidationRecords.length === 0 && outputs.httpsTrackingPending && outputs.customTrackingDomain) {
24991
+ const { getCertificateValidationRecords: getCertificateValidationRecords2 } = await Promise.resolve().then(() => (init_acm(), acm_exports));
24992
+ const directRecords = await getCertificateValidationRecords2(
24993
+ outputs.customTrackingDomain
24994
+ );
24995
+ acmValidationRecords.push(...directRecords);
24996
+ }
24464
24997
  let acmDnsAutoCreated = false;
24465
24998
  if (outputs.httpsTrackingPending && acmValidationRecords.length > 0 && outputs.customTrackingDomain) {
24466
24999
  const trackingDnsProvider = metadata.services.email?.dnsProvider;
@@ -27140,7 +27673,7 @@ import {
27140
27673
  IAMClient as IAMClient2,
27141
27674
  PutRolePolicyCommand
27142
27675
  } from "@aws-sdk/client-iam";
27143
- import { confirm as confirm14, intro as intro32, isCancel as isCancel20, log as log32, outro as outro19, select as select14 } from "@clack/prompts";
27676
+ import { confirm as confirm14, intro as intro32, isCancel as isCancel20, log as log32, outro as outro19, select as select15 } from "@clack/prompts";
27144
27677
  import * as pulumi21 from "@pulumi/pulumi";
27145
27678
  import pc36 from "picocolors";
27146
27679
  init_events();
@@ -27483,7 +28016,7 @@ async function resolveOrganization() {
27483
28016
  if (orgs.length === 1) {
27484
28017
  return orgs[0];
27485
28018
  }
27486
- const selected = await select14({
28019
+ const selected = await select15({
27487
28020
  message: "Which organization should this AWS account connect to?",
27488
28021
  options: orgs.map((org) => ({
27489
28022
  value: org.id,
@@ -27779,7 +28312,7 @@ Run ${pc36.cyan("wraps email init")} or ${pc36.cyan("wraps sms init")} first.
27779
28312
  log32.info(
27780
28313
  `Already connected to Wraps Platform (AWS Account: ${pc36.cyan(metadata.accountId)})`
27781
28314
  );
27782
- const action = await select14({
28315
+ const action = await select15({
27783
28316
  message: "What would you like to do?",
27784
28317
  options: [
27785
28318
  {
@@ -36139,6 +36672,11 @@ args.options([
36139
36672
  name: "org",
36140
36673
  description: "Organization slug",
36141
36674
  defaultValue: void 0
36675
+ },
36676
+ {
36677
+ name: "subdomain",
36678
+ description: "Subdomain for inbound email (e.g., inbound, support)",
36679
+ defaultValue: void 0
36142
36680
  }
36143
36681
  ]);
36144
36682
  var flags = args.parse(process.argv);
@@ -36453,13 +36991,30 @@ Usage: ${pc53.cyan("wraps email verify --domain yourapp.com")}
36453
36991
  json: flags.json
36454
36992
  });
36455
36993
  break;
36994
+ case "add":
36995
+ await inboundAdd({
36996
+ region: flags.region,
36997
+ subdomain: flags.subdomain,
36998
+ domain: flags.domain,
36999
+ yes: flags.yes,
37000
+ json: flags.json
37001
+ });
37002
+ break;
37003
+ case "remove":
37004
+ await inboundRemove({
37005
+ region: flags.region,
37006
+ domain: flags.domain,
37007
+ yes: flags.yes,
37008
+ json: flags.json
37009
+ });
37010
+ break;
36456
37011
  default:
36457
37012
  clack50.log.error(
36458
37013
  `Unknown inbound command: ${inboundSubCommand || "(none)"}`
36459
37014
  );
36460
37015
  console.log(
36461
37016
  `
36462
- Available commands: ${pc53.cyan("init")}, ${pc53.cyan("destroy")}, ${pc53.cyan("status")}, ${pc53.cyan("verify")}, ${pc53.cyan("test")}
37017
+ Available commands: ${pc53.cyan("init")}, ${pc53.cyan("destroy")}, ${pc53.cyan("status")}, ${pc53.cyan("verify")}, ${pc53.cyan("test")}, ${pc53.cyan("add")}, ${pc53.cyan("remove")}
36463
37018
  `
36464
37019
  );
36465
37020
  throw new Error(