@wraps.dev/cli 2.14.8 → 2.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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";
@@ -19098,7 +19188,8 @@ import {
19098
19188
  DescribeActiveReceiptRuleSetCommand,
19099
19189
  DescribeReceiptRuleCommand,
19100
19190
  SESClient as SESClient3,
19101
- SetActiveReceiptRuleSetCommand
19191
+ SetActiveReceiptRuleSetCommand,
19192
+ UpdateReceiptRuleCommand
19102
19193
  } from "@aws-sdk/client-ses";
19103
19194
  var RULE_SET_NAME = "wraps-inbound-rules";
19104
19195
  var RULE_NAME = "wraps-inbound-catch-all";
@@ -19221,6 +19312,77 @@ async function deleteReceiptRuleSet(region) {
19221
19312
  throw error;
19222
19313
  }
19223
19314
  }
19315
+ async function addDomainToReceiptRule(region, domain, s3BucketName) {
19316
+ const ses = createSESClient(region);
19317
+ try {
19318
+ const response = await ses.send(
19319
+ new DescribeReceiptRuleCommand({
19320
+ RuleSetName: RULE_SET_NAME,
19321
+ RuleName: RULE_NAME
19322
+ })
19323
+ );
19324
+ const existingRecipients = response.Rule?.Recipients ?? [];
19325
+ if (existingRecipients.includes(domain)) {
19326
+ return;
19327
+ }
19328
+ await ses.send(
19329
+ new UpdateReceiptRuleCommand({
19330
+ RuleSetName: RULE_SET_NAME,
19331
+ Rule: {
19332
+ Name: RULE_NAME,
19333
+ Enabled: response.Rule?.Enabled,
19334
+ ScanEnabled: response.Rule?.ScanEnabled,
19335
+ TlsPolicy: response.Rule?.TlsPolicy,
19336
+ Actions: response.Rule?.Actions,
19337
+ Recipients: [...existingRecipients, domain]
19338
+ }
19339
+ })
19340
+ );
19341
+ } catch (error) {
19342
+ if (error instanceof Error && (error.name === "RuleDoesNotExistException" || error.name === "RuleSetDoesNotExistException")) {
19343
+ await createReceiptRuleSet(region);
19344
+ await createReceiptRule(region, domain, s3BucketName);
19345
+ await setActiveReceiptRuleSet(region, RULE_SET_NAME);
19346
+ return;
19347
+ }
19348
+ throw error;
19349
+ }
19350
+ }
19351
+ async function removeDomainFromReceiptRule(region, domain) {
19352
+ const ses = createSESClient(region);
19353
+ try {
19354
+ const response = await ses.send(
19355
+ new DescribeReceiptRuleCommand({
19356
+ RuleSetName: RULE_SET_NAME,
19357
+ RuleName: RULE_NAME
19358
+ })
19359
+ );
19360
+ const existingRecipients = response.Rule?.Recipients ?? [];
19361
+ const updated = existingRecipients.filter((r) => r !== domain);
19362
+ if (updated.length === 0) {
19363
+ await deleteReceiptRule(region);
19364
+ return;
19365
+ }
19366
+ await ses.send(
19367
+ new UpdateReceiptRuleCommand({
19368
+ RuleSetName: RULE_SET_NAME,
19369
+ Rule: {
19370
+ Name: RULE_NAME,
19371
+ Enabled: response.Rule?.Enabled,
19372
+ ScanEnabled: response.Rule?.ScanEnabled,
19373
+ TlsPolicy: response.Rule?.TlsPolicy,
19374
+ Actions: response.Rule?.Actions,
19375
+ Recipients: updated
19376
+ }
19377
+ })
19378
+ );
19379
+ } catch (error) {
19380
+ if (error instanceof Error && (error.name === "RuleDoesNotExistException" || error.name === "RuleSetDoesNotExistException")) {
19381
+ return;
19382
+ }
19383
+ throw error;
19384
+ }
19385
+ }
19224
19386
 
19225
19387
  // src/commands/email/inbound.ts
19226
19388
  init_aws();
@@ -19299,7 +19461,15 @@ async function inboundInit(options) {
19299
19461
  bucketName: `wraps-inbound-${identity.accountId}-${region}`,
19300
19462
  webhookUrl,
19301
19463
  webhookSecret
19302
- }
19464
+ },
19465
+ inboundDomains: [
19466
+ {
19467
+ subdomain,
19468
+ receivingDomain,
19469
+ parentDomain: domain,
19470
+ addedAt: (/* @__PURE__ */ new Date()).toISOString()
19471
+ }
19472
+ ]
19303
19473
  };
19304
19474
  const stackConfig = buildEmailStackConfig(metadata, region, {
19305
19475
  emailConfig: updatedEmailConfig
@@ -19540,7 +19710,8 @@ Deploy first: ${pc21.cyan("wraps email inbound init")}
19540
19710
  const stackName = emailService.pulumiStackName || `wraps-${identity.accountId}-${region}`;
19541
19711
  const updatedEmailConfig = {
19542
19712
  ...emailService.config,
19543
- inbound: void 0
19713
+ inbound: void 0,
19714
+ inboundDomains: void 0
19544
19715
  };
19545
19716
  const stackConfig = buildEmailStackConfig(metadata, region, {
19546
19717
  emailConfig: updatedEmailConfig
@@ -19609,13 +19780,18 @@ Enable it: ${pc21.cyan("wraps email inbound init")}
19609
19780
  `);
19610
19781
  return;
19611
19782
  }
19612
- const inbound = metadata.services.email.config.inbound;
19783
+ const emailConfig = metadata.services.email.config;
19784
+ const inbound = emailConfig.inbound;
19785
+ const inboundDomains = emailConfig.inboundDomains ?? [];
19613
19786
  const activeRuleSet = await getActiveReceiptRuleSet(region);
19614
- const receivingDomain = inbound.receivingDomain || `${inbound.subdomain}.${metadata.services.email.config.domain}`;
19787
+ const domainList = inboundDomains.length > 0 ? inboundDomains.map((d) => d.receivingDomain) : [
19788
+ inbound.receivingDomain || `${inbound.subdomain}.${emailConfig.domain}`
19789
+ ];
19615
19790
  if (isJsonMode()) {
19616
19791
  jsonSuccess("email.inbound.status", {
19617
19792
  enabled: true,
19618
- receivingDomain,
19793
+ receivingDomains: domainList,
19794
+ receivingDomain: domainList[0],
19619
19795
  bucketName: inbound.bucketName || "",
19620
19796
  region,
19621
19797
  webhookUrl: inbound.webhookUrl || null,
@@ -19627,7 +19803,16 @@ Enable it: ${pc21.cyan("wraps email inbound init")}
19627
19803
  console.log();
19628
19804
  console.log(pc21.bold(" Inbound Email Configuration"));
19629
19805
  console.log();
19630
- console.log(` ${pc21.dim("Receiving domain:")} ${pc21.cyan(receivingDomain)}`);
19806
+ if (domainList.length === 1) {
19807
+ console.log(
19808
+ ` ${pc21.dim("Receiving domain:")} ${pc21.cyan(domainList[0])}`
19809
+ );
19810
+ } else {
19811
+ console.log(` ${pc21.dim("Receiving domains:")}`);
19812
+ for (const d of domainList) {
19813
+ console.log(` ${pc21.cyan(d)}`);
19814
+ }
19815
+ }
19631
19816
  console.log(
19632
19817
  ` ${pc21.dim("S3 bucket:")} ${pc21.cyan(inbound.bucketName || "")}`
19633
19818
  );
@@ -19661,61 +19846,77 @@ Enable it: ${pc21.cyan("wraps email inbound init")}
19661
19846
  `);
19662
19847
  process.exit(1);
19663
19848
  }
19664
- const inbound = metadata.services.email.config.inbound;
19665
- const receivingDomain = inbound.receivingDomain || `${inbound.subdomain}.${metadata.services.email.config.domain}`;
19849
+ const emailConfig = metadata.services.email.config;
19850
+ const inbound = emailConfig.inbound;
19851
+ const inboundDomains = emailConfig.inboundDomains ?? [];
19852
+ const domainList = inboundDomains.length > 0 ? inboundDomains.map((d) => d.receivingDomain) : [
19853
+ inbound.receivingDomain || `${inbound.subdomain}.${emailConfig.domain}`
19854
+ ];
19666
19855
  let allPassed = true;
19856
+ const domainChecks = {};
19667
19857
  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
- }
19858
+ for (const receivingDomain of domainList) {
19859
+ if (domainList.length > 1) {
19860
+ clack20.log.info(pc21.bold(`Checking ${pc21.cyan(receivingDomain)}`));
19678
19861
  }
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 };
19862
+ const mxResult = await progress.execute(
19863
+ `Checking MX record for ${receivingDomain}`,
19864
+ async () => {
19865
+ try {
19866
+ const records = await dns2.resolveMx(receivingDomain);
19867
+ const hasSES = records.some(
19868
+ (r) => r.exchange.includes("inbound-smtp")
19869
+ );
19870
+ return { found: true, hasSES, records };
19871
+ } catch {
19872
+ return { found: false, hasSES: false, records: [] };
19873
+ }
19706
19874
  }
19875
+ );
19876
+ if (mxResult.hasSES) {
19877
+ clack20.log.success(
19878
+ `MX record: ${pc21.green("verified")} \u2192 inbound-smtp.${region}.amazonaws.com`
19879
+ );
19880
+ } else if (mxResult.found) {
19881
+ clack20.log.warn(
19882
+ `MX record found but not pointing to SES. Expected: ${pc21.cyan(`10 inbound-smtp.${region}.amazonaws.com`)}`
19883
+ );
19884
+ allPassed = false;
19885
+ } else {
19886
+ clack20.log.error(
19887
+ `MX record: ${pc21.red("not found")}. Add: ${pc21.cyan(`${receivingDomain} MX 10 inbound-smtp.${region}.amazonaws.com`)}`
19888
+ );
19889
+ allPassed = false;
19707
19890
  }
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")}`
19891
+ const spfResult = await progress.execute(
19892
+ `Checking SPF record for ${receivingDomain}`,
19893
+ async () => {
19894
+ try {
19895
+ const records = await dns2.resolveTxt(receivingDomain);
19896
+ const flat = records.map((r) => r.join(""));
19897
+ const spf = flat.find((r) => r.startsWith("v=spf1"));
19898
+ const hasSES = spf?.includes("amazonses.com") ?? false;
19899
+ return { found: !!spf, hasSES, value: spf };
19900
+ } catch {
19901
+ return { found: false, hasSES: false, value: null };
19902
+ }
19903
+ }
19717
19904
  );
19718
- allPassed = false;
19905
+ if (spfResult.hasSES) {
19906
+ clack20.log.success(`SPF record: ${pc21.green("verified")}`);
19907
+ } else if (spfResult.found) {
19908
+ clack20.log.warn("SPF record exists but missing amazonses.com include");
19909
+ allPassed = false;
19910
+ } else {
19911
+ clack20.log.error(
19912
+ `SPF record: ${pc21.red("not found")}. Add TXT: ${pc21.cyan("v=spf1 include:amazonses.com ~all")}`
19913
+ );
19914
+ allPassed = false;
19915
+ }
19916
+ domainChecks[receivingDomain] = {
19917
+ mx: { found: mxResult.found, verified: mxResult.hasSES },
19918
+ spf: { found: spfResult.found, verified: spfResult.hasSES }
19919
+ };
19719
19920
  }
19720
19921
  const activeRuleSet = await getActiveReceiptRuleSet(region);
19721
19922
  if (activeRuleSet === RULE_SET_NAME) {
@@ -19728,13 +19929,11 @@ Enable it: ${pc21.cyan("wraps email inbound init")}
19728
19929
  }
19729
19930
  if (isJsonMode()) {
19730
19931
  jsonSuccess("email.inbound.verify", {
19731
- receivingDomain,
19932
+ receivingDomains: domainList,
19933
+ receivingDomain: domainList[0],
19732
19934
  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
- }
19935
+ domainChecks,
19936
+ receiptRuleSet: { active: activeRuleSet === RULE_SET_NAME }
19738
19937
  });
19739
19938
  return;
19740
19939
  }
@@ -19891,6 +20090,304 @@ Enable it: ${pc21.cyan("wraps email inbound init")}
19891
20090
  clack20.log.success(pc21.bold("Inbound email is working correctly!"));
19892
20091
  console.log();
19893
20092
  }
20093
+ async function inboundAdd(options) {
20094
+ if (!isJsonMode()) {
20095
+ clack20.intro(pc21.bold("Add Inbound Receiving Domain"));
20096
+ }
20097
+ const progress = new DeploymentProgress();
20098
+ const identity = await progress.execute(
20099
+ "Validating AWS credentials",
20100
+ async () => validateAWSCredentials()
20101
+ );
20102
+ const region = options.region || await getAWSRegion();
20103
+ if (!SES_RECEIVING_REGIONS.includes(
20104
+ region
20105
+ )) {
20106
+ throw errors.inboundRegionNotSupported(region);
20107
+ }
20108
+ const metadata = await loadConnectionMetadata(identity.accountId, region);
20109
+ if (!metadata?.services?.email?.config?.inbound?.enabled) {
20110
+ clack20.log.error("Inbound email infrastructure is not deployed.");
20111
+ console.log(
20112
+ `
20113
+ Deploy first: ${pc21.cyan("wraps email inbound init")}
20114
+ `
20115
+ );
20116
+ process.exit(1);
20117
+ }
20118
+ const emailConfig = metadata.services.email.config;
20119
+ const primaryDomain = emailConfig.domain || "";
20120
+ const allDomains = [primaryDomain];
20121
+ for (const d of emailConfig.additionalDomains ?? []) {
20122
+ if (!allDomains.includes(d.domain)) {
20123
+ allDomains.push(d.domain);
20124
+ }
20125
+ }
20126
+ let parentDomain = options.domain;
20127
+ if (!parentDomain) {
20128
+ if (options.yes) {
20129
+ parentDomain = primaryDomain;
20130
+ } else if (allDomains.length === 1) {
20131
+ parentDomain = allDomains[0];
20132
+ } else {
20133
+ const selected = await clack20.select({
20134
+ message: "Which domain should the inbound subdomain be under?",
20135
+ options: allDomains.map((d) => ({
20136
+ value: d,
20137
+ label: d,
20138
+ hint: d === primaryDomain ? "primary" : void 0
20139
+ }))
20140
+ });
20141
+ if (clack20.isCancel(selected)) {
20142
+ clack20.cancel("Operation cancelled.");
20143
+ process.exit(0);
20144
+ }
20145
+ parentDomain = selected;
20146
+ }
20147
+ }
20148
+ const subdomain = options.subdomain || (options.yes ? "inbound" : await promptInboundSubdomain(parentDomain));
20149
+ const receivingDomain = `${subdomain}.${parentDomain}`;
20150
+ const existingDomains = emailConfig.inboundDomains ?? [];
20151
+ if (existingDomains.some((d) => d.receivingDomain === receivingDomain)) {
20152
+ clack20.log.warn(
20153
+ `${pc21.cyan(receivingDomain)} is already configured as an inbound domain.`
20154
+ );
20155
+ return;
20156
+ }
20157
+ clack20.log.info(`Adding receiving domain: ${pc21.cyan(receivingDomain)}`);
20158
+ const bucketName = emailConfig.inbound?.bucketName || `wraps-inbound-${identity.accountId}-${region}`;
20159
+ await progress.execute("Updating SES receipt rule", async () => {
20160
+ await addDomainToReceiptRule(region, receivingDomain, bucketName);
20161
+ });
20162
+ let dnsAutoCreated = false;
20163
+ const {
20164
+ detectAvailableDNSProviders: detectAvailableDNSProviders2,
20165
+ getDNSCredentials: getDNSCredentials2,
20166
+ createInboundDNSRecordsForProvider: createInboundDNSRecordsForProvider2,
20167
+ getDNSProviderDisplayName: getDNSProviderDisplayName2,
20168
+ getDNSProviderTokenUrl: getDNSProviderTokenUrl2,
20169
+ buildInboundDNSRecords: buildRecords,
20170
+ formatDNSRecordsForDisplay: formatRecords
20171
+ } = await Promise.resolve().then(() => (init_dns(), dns_exports));
20172
+ const { promptDNSProvider: promptDNSProvider2, promptContinueManualDNS: promptContinueManualDNS2 } = await Promise.resolve().then(() => (init_prompts(), prompts_exports));
20173
+ const existingDnsProvider = metadata.services.email.dnsProvider;
20174
+ let dnsProvider = existingDnsProvider;
20175
+ if (!dnsProvider || dnsProvider === "manual") {
20176
+ progress.start("Detecting DNS providers");
20177
+ const availableProviders = await detectAvailableDNSProviders2(
20178
+ parentDomain,
20179
+ region
20180
+ );
20181
+ progress.stop();
20182
+ dnsProvider = options.yes ? "manual" : await promptDNSProvider2(parentDomain, availableProviders);
20183
+ }
20184
+ if (dnsProvider !== "manual") {
20185
+ progress.start(
20186
+ `Validating ${getDNSProviderDisplayName2(dnsProvider)} credentials`
20187
+ );
20188
+ const credentialResult = await getDNSCredentials2(
20189
+ dnsProvider,
20190
+ parentDomain,
20191
+ region
20192
+ );
20193
+ progress.stop();
20194
+ if (credentialResult.valid && credentialResult.credentials) {
20195
+ const records = buildRecords(receivingDomain, region);
20196
+ clack20.log.info(pc21.bold("DNS records to create:"));
20197
+ for (const record of records) {
20198
+ const value = record.priority ? `${record.priority} ${record.value}` : record.value;
20199
+ clack20.log.info(pc21.dim(` ${record.type} ${record.name} \u2192 ${value}`));
20200
+ }
20201
+ progress.start(
20202
+ `Creating DNS records in ${getDNSProviderDisplayName2(dnsProvider)}`
20203
+ );
20204
+ const result = await createInboundDNSRecordsForProvider2(
20205
+ credentialResult.credentials,
20206
+ receivingDomain,
20207
+ region,
20208
+ parentDomain
20209
+ );
20210
+ if (result.success && result.recordsCreated > 0) {
20211
+ progress.succeed(
20212
+ `Created ${result.recordsCreated} DNS records in ${getDNSProviderDisplayName2(dnsProvider)}`
20213
+ );
20214
+ dnsAutoCreated = true;
20215
+ } else {
20216
+ progress.fail("Failed to create some DNS records");
20217
+ if (result.errors) {
20218
+ for (const err of result.errors) {
20219
+ clack20.log.warn(err);
20220
+ }
20221
+ }
20222
+ }
20223
+ } else {
20224
+ clack20.log.warn(
20225
+ credentialResult.error || "Could not validate credentials"
20226
+ );
20227
+ if (dnsProvider === "vercel" || dnsProvider === "cloudflare") {
20228
+ clack20.log.info(
20229
+ `Set the ${dnsProvider === "vercel" ? "VERCEL_TOKEN" : "CLOUDFLARE_API_TOKEN"} environment variable to enable automatic DNS management.`
20230
+ );
20231
+ clack20.log.info(
20232
+ `You can create a token at: ${pc21.cyan(getDNSProviderTokenUrl2(dnsProvider))}`
20233
+ );
20234
+ }
20235
+ if (!options.yes) {
20236
+ const continueManual = await promptContinueManualDNS2();
20237
+ if (continueManual) {
20238
+ dnsProvider = "manual";
20239
+ }
20240
+ }
20241
+ }
20242
+ }
20243
+ if (!dnsAutoCreated) {
20244
+ const dnsRecords = buildRecords(receivingDomain, region);
20245
+ const formattedRecords = formatRecords(dnsRecords);
20246
+ console.log();
20247
+ clack20.log.info(pc21.bold("DNS Records Required"));
20248
+ console.log(
20249
+ ` Add these records to your DNS provider for ${pc21.cyan(receivingDomain)}:
20250
+ `
20251
+ );
20252
+ for (const record of formattedRecords) {
20253
+ console.log(` ${pc21.dim(record.type.padEnd(6))} ${record.name}`);
20254
+ console.log(` ${pc21.dim("\u2192")} ${pc21.green(record.value)}
20255
+ `);
20256
+ }
20257
+ }
20258
+ await progress.execute("Saving configuration", async () => {
20259
+ addInboundDomainToMetadata(metadata, {
20260
+ subdomain,
20261
+ receivingDomain,
20262
+ parentDomain,
20263
+ addedAt: (/* @__PURE__ */ new Date()).toISOString()
20264
+ });
20265
+ await saveConnectionMetadata(metadata);
20266
+ });
20267
+ if (isJsonMode()) {
20268
+ jsonSuccess("email.inbound.add", {
20269
+ receivingDomain,
20270
+ subdomain,
20271
+ parentDomain,
20272
+ dnsAutoCreated,
20273
+ region
20274
+ });
20275
+ return;
20276
+ }
20277
+ console.log();
20278
+ clack20.log.success(
20279
+ `${pc21.bold("Added inbound domain:")} ${pc21.cyan(receivingDomain)}`
20280
+ );
20281
+ console.log();
20282
+ if (!dnsAutoCreated) {
20283
+ console.log(
20284
+ ` ${pc21.dim("1.")} Add DNS records above to your DNS provider`
20285
+ );
20286
+ console.log(
20287
+ ` ${pc21.dim("2.")} Verify: ${pc21.cyan("wraps email inbound verify")}`
20288
+ );
20289
+ } else {
20290
+ console.log(
20291
+ ` Verify: ${pc21.cyan("wraps email inbound verify")}`
20292
+ );
20293
+ }
20294
+ console.log();
20295
+ }
20296
+ async function inboundRemove(options) {
20297
+ if (!isJsonMode()) {
20298
+ clack20.intro(pc21.bold("Remove Inbound Receiving Domain"));
20299
+ }
20300
+ const progress = new DeploymentProgress();
20301
+ const identity = await progress.execute(
20302
+ "Validating AWS credentials",
20303
+ async () => validateAWSCredentials()
20304
+ );
20305
+ const region = options.region || await getAWSRegion();
20306
+ const metadata = await loadConnectionMetadata(identity.accountId, region);
20307
+ if (!metadata?.services?.email?.config?.inbound?.enabled) {
20308
+ clack20.log.error("Inbound email infrastructure is not deployed.");
20309
+ console.log(
20310
+ `
20311
+ Deploy first: ${pc21.cyan("wraps email inbound init")}
20312
+ `
20313
+ );
20314
+ process.exit(1);
20315
+ }
20316
+ const emailConfig = metadata.services.email.config;
20317
+ const inboundDomains = emailConfig.inboundDomains ?? [];
20318
+ if (inboundDomains.length === 0) {
20319
+ clack20.log.warn("No inbound domains configured.");
20320
+ return;
20321
+ }
20322
+ let domainToRemove = options.domain;
20323
+ if (!domainToRemove) {
20324
+ if (inboundDomains.length === 1) {
20325
+ domainToRemove = inboundDomains[0].receivingDomain;
20326
+ } else {
20327
+ const selected = await clack20.select({
20328
+ message: "Which inbound domain do you want to remove?",
20329
+ options: inboundDomains.map((d) => ({
20330
+ value: d.receivingDomain,
20331
+ label: d.receivingDomain,
20332
+ hint: `added ${d.addedAt.split("T")[0]}`
20333
+ }))
20334
+ });
20335
+ if (clack20.isCancel(selected)) {
20336
+ clack20.cancel("Operation cancelled.");
20337
+ process.exit(0);
20338
+ }
20339
+ domainToRemove = selected;
20340
+ }
20341
+ }
20342
+ if (!inboundDomains.some((d) => d.receivingDomain === domainToRemove)) {
20343
+ clack20.log.error(
20344
+ `${pc21.cyan(domainToRemove)} is not in the inbound domains list.`
20345
+ );
20346
+ return;
20347
+ }
20348
+ if (inboundDomains.length === 1) {
20349
+ clack20.log.error(
20350
+ "Cannot remove the last inbound domain. Use " + pc21.cyan("wraps email inbound destroy") + " to remove all inbound infrastructure."
20351
+ );
20352
+ return;
20353
+ }
20354
+ if (!options.yes) {
20355
+ const confirmed = await clack20.confirm({
20356
+ message: `Remove inbound domain ${pc21.cyan(domainToRemove)}?`,
20357
+ initialValue: false
20358
+ });
20359
+ if (clack20.isCancel(confirmed) || !confirmed) {
20360
+ clack20.cancel("Operation cancelled.");
20361
+ process.exit(0);
20362
+ }
20363
+ }
20364
+ await progress.execute("Updating SES receipt rule", async () => {
20365
+ await removeDomainFromReceiptRule(region, domainToRemove);
20366
+ });
20367
+ await progress.execute("Saving configuration", async () => {
20368
+ removeInboundDomainFromMetadata(metadata, domainToRemove);
20369
+ await saveConnectionMetadata(metadata);
20370
+ });
20371
+ if (isJsonMode()) {
20372
+ jsonSuccess("email.inbound.remove", {
20373
+ removedDomain: domainToRemove,
20374
+ remainingDomains: (emailConfig.inboundDomains ?? []).map(
20375
+ (d) => d.receivingDomain
20376
+ ),
20377
+ region
20378
+ });
20379
+ return;
20380
+ }
20381
+ console.log();
20382
+ clack20.log.success(
20383
+ `${pc21.bold("Removed inbound domain:")} ${pc21.cyan(domainToRemove)}`
20384
+ );
20385
+ console.log();
20386
+ console.log(
20387
+ ` ${pc21.dim("Remember to remove the MX and SPF DNS records for")} ${pc21.cyan(domainToRemove)}`
20388
+ );
20389
+ console.log();
20390
+ }
19894
20391
 
19895
20392
  // src/commands/email/init.ts
19896
20393
  init_esm_shims();
@@ -24461,6 +24958,13 @@ ${pc30.bold("Add these DNS records to your DNS provider:")}
24461
24958
  if (outputs.httpsTrackingEnabled && outputs.acmCertificateValidationRecords) {
24462
24959
  acmValidationRecords.push(...outputs.acmCertificateValidationRecords);
24463
24960
  }
24961
+ if (acmValidationRecords.length === 0 && outputs.httpsTrackingPending && outputs.customTrackingDomain) {
24962
+ const { getCertificateValidationRecords: getCertificateValidationRecords2 } = await Promise.resolve().then(() => (init_acm(), acm_exports));
24963
+ const directRecords = await getCertificateValidationRecords2(
24964
+ outputs.customTrackingDomain
24965
+ );
24966
+ acmValidationRecords.push(...directRecords);
24967
+ }
24464
24968
  let acmDnsAutoCreated = false;
24465
24969
  if (outputs.httpsTrackingPending && acmValidationRecords.length > 0 && outputs.customTrackingDomain) {
24466
24970
  const trackingDnsProvider = metadata.services.email?.dnsProvider;
@@ -27140,7 +27644,7 @@ import {
27140
27644
  IAMClient as IAMClient2,
27141
27645
  PutRolePolicyCommand
27142
27646
  } 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";
27647
+ import { confirm as confirm14, intro as intro32, isCancel as isCancel20, log as log32, outro as outro19, select as select15 } from "@clack/prompts";
27144
27648
  import * as pulumi21 from "@pulumi/pulumi";
27145
27649
  import pc36 from "picocolors";
27146
27650
  init_events();
@@ -27483,7 +27987,7 @@ async function resolveOrganization() {
27483
27987
  if (orgs.length === 1) {
27484
27988
  return orgs[0];
27485
27989
  }
27486
- const selected = await select14({
27990
+ const selected = await select15({
27487
27991
  message: "Which organization should this AWS account connect to?",
27488
27992
  options: orgs.map((org) => ({
27489
27993
  value: org.id,
@@ -27779,7 +28283,7 @@ Run ${pc36.cyan("wraps email init")} or ${pc36.cyan("wraps sms init")} first.
27779
28283
  log32.info(
27780
28284
  `Already connected to Wraps Platform (AWS Account: ${pc36.cyan(metadata.accountId)})`
27781
28285
  );
27782
- const action = await select14({
28286
+ const action = await select15({
27783
28287
  message: "What would you like to do?",
27784
28288
  options: [
27785
28289
  {
@@ -36139,6 +36643,11 @@ args.options([
36139
36643
  name: "org",
36140
36644
  description: "Organization slug",
36141
36645
  defaultValue: void 0
36646
+ },
36647
+ {
36648
+ name: "subdomain",
36649
+ description: "Subdomain for inbound email (e.g., inbound, support)",
36650
+ defaultValue: void 0
36142
36651
  }
36143
36652
  ]);
36144
36653
  var flags = args.parse(process.argv);
@@ -36453,13 +36962,30 @@ Usage: ${pc53.cyan("wraps email verify --domain yourapp.com")}
36453
36962
  json: flags.json
36454
36963
  });
36455
36964
  break;
36965
+ case "add":
36966
+ await inboundAdd({
36967
+ region: flags.region,
36968
+ subdomain: flags.subdomain,
36969
+ domain: flags.domain,
36970
+ yes: flags.yes,
36971
+ json: flags.json
36972
+ });
36973
+ break;
36974
+ case "remove":
36975
+ await inboundRemove({
36976
+ region: flags.region,
36977
+ domain: flags.domain,
36978
+ yes: flags.yes,
36979
+ json: flags.json
36980
+ });
36981
+ break;
36456
36982
  default:
36457
36983
  clack50.log.error(
36458
36984
  `Unknown inbound command: ${inboundSubCommand || "(none)"}`
36459
36985
  );
36460
36986
  console.log(
36461
36987
  `
36462
- Available commands: ${pc53.cyan("init")}, ${pc53.cyan("destroy")}, ${pc53.cyan("status")}, ${pc53.cyan("verify")}, ${pc53.cyan("test")}
36988
+ Available commands: ${pc53.cyan("init")}, ${pc53.cyan("destroy")}, ${pc53.cyan("status")}, ${pc53.cyan("verify")}, ${pc53.cyan("test")}, ${pc53.cyan("add")}, ${pc53.cyan("remove")}
36463
36989
  `
36464
36990
  );
36465
36991
  throw new Error(