@wraps.dev/cli 2.11.2 → 2.11.3

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
@@ -4880,9 +4880,36 @@ var init_lambda = __esm({
4880
4880
  // src/infrastructure/resources/acm.ts
4881
4881
  var acm_exports = {};
4882
4882
  __export(acm_exports, {
4883
+ checkCertificateValidation: () => checkCertificateValidation,
4883
4884
  createACMCertificate: () => createACMCertificate
4884
4885
  });
4886
+ import { ACMClient as ACMClient2, DescribeCertificateCommand as DescribeCertificateCommand2 } from "@aws-sdk/client-acm";
4885
4887
  import * as aws12 from "@pulumi/aws";
4888
+ async function checkCertificateValidation(domain) {
4889
+ try {
4890
+ const acm3 = new ACMClient2({ region: "us-east-1" });
4891
+ const { ListCertificatesCommand } = await import("@aws-sdk/client-acm");
4892
+ const listResponse = await acm3.send(
4893
+ new ListCertificatesCommand({
4894
+ CertificateStatuses: ["ISSUED"]
4895
+ })
4896
+ );
4897
+ const cert = listResponse.CertificateSummaryList?.find(
4898
+ (c) => c.DomainName === domain
4899
+ );
4900
+ if (cert?.CertificateArn) {
4901
+ const describeResponse = await acm3.send(
4902
+ new DescribeCertificateCommand2({
4903
+ CertificateArn: cert.CertificateArn
4904
+ })
4905
+ );
4906
+ return describeResponse.Certificate?.Status === "ISSUED";
4907
+ }
4908
+ return false;
4909
+ } catch (error) {
4910
+ return false;
4911
+ }
4912
+ }
4886
4913
  async function createACMCertificate(config2) {
4887
4914
  const usEast1Provider = new aws12.Provider("acm-us-east-1", {
4888
4915
  region: "us-east-1"
@@ -5618,6 +5645,10 @@ var init_eventbridge_inbound = __esm({
5618
5645
  });
5619
5646
 
5620
5647
  // src/utils/dns/cloudflare.ts
5648
+ var cloudflare_exports = {};
5649
+ __export(cloudflare_exports, {
5650
+ CloudflareDNSClient: () => CloudflareDNSClient
5651
+ });
5621
5652
  var CLOUDFLARE_API_BASE, CloudflareDNSClient;
5622
5653
  var init_cloudflare = __esm({
5623
5654
  "src/utils/dns/cloudflare.ts"() {
@@ -5844,6 +5875,92 @@ var init_cloudflare = __esm({
5844
5875
  };
5845
5876
  }
5846
5877
  }
5878
+ /**
5879
+ * Get the zone name (domain) for this zone ID
5880
+ */
5881
+ async getZoneName() {
5882
+ try {
5883
+ const response = await fetch(
5884
+ `${CLOUDFLARE_API_BASE}/zones/${this.zoneId}`,
5885
+ {
5886
+ headers: {
5887
+ Authorization: `Bearer ${this.apiToken}`,
5888
+ "Content-Type": "application/json"
5889
+ }
5890
+ }
5891
+ );
5892
+ const data = await response.json();
5893
+ return data.success ? data.result.name : null;
5894
+ } catch {
5895
+ return null;
5896
+ }
5897
+ }
5898
+ /**
5899
+ * Get all CAA records for the zone
5900
+ */
5901
+ async getCAARecords() {
5902
+ const zoneName = await this.getZoneName();
5903
+ if (!zoneName) return [];
5904
+ const result = await this.request(
5905
+ "/dns_records?type=CAA"
5906
+ );
5907
+ if (!(result.success && result.result)) {
5908
+ return [];
5909
+ }
5910
+ return result.result.filter((r) => r.type === "CAA").map((r) => {
5911
+ const match = r.content.match(/^(\d+)\s+(\w+)\s+"?([^"]+)"?$/);
5912
+ if (match) {
5913
+ return {
5914
+ flags: Number.parseInt(match[1], 10),
5915
+ tag: match[2],
5916
+ value: match[3]
5917
+ };
5918
+ }
5919
+ return null;
5920
+ }).filter(
5921
+ (r) => r !== null
5922
+ );
5923
+ }
5924
+ /**
5925
+ * Check if Amazon is allowed to issue certificates based on CAA records
5926
+ */
5927
+ async isAmazonCAAAllowed() {
5928
+ const caaRecords = await this.getCAARecords();
5929
+ const issueRecords = caaRecords.filter(
5930
+ (r) => r.tag === "issue" || r.tag === "issuewild"
5931
+ );
5932
+ if (issueRecords.length === 0) {
5933
+ return { allowed: true, hasCAA: false, existingCAs: [] };
5934
+ }
5935
+ const existingCAs = issueRecords.map((r) => r.value);
5936
+ const amazonAllowed = existingCAs.some(
5937
+ (ca) => ca.includes("amazon.com") || ca.includes("amazontrust.com")
5938
+ );
5939
+ return { allowed: amazonAllowed, hasCAA: true, existingCAs };
5940
+ }
5941
+ /**
5942
+ * Add a CAA record to allow Amazon to issue certificates
5943
+ */
5944
+ async addAmazonCAARecord() {
5945
+ const zoneName = await this.getZoneName();
5946
+ if (!zoneName) return false;
5947
+ const body = {
5948
+ name: zoneName,
5949
+ type: "CAA",
5950
+ data: {
5951
+ flags: 0,
5952
+ tag: "issue",
5953
+ value: "amazon.com"
5954
+ },
5955
+ ttl: 1800
5956
+ };
5957
+ const result = await this.request(
5958
+ "/dns_records",
5959
+ "POST",
5960
+ body
5961
+ );
5962
+ return result.success;
5963
+ }
5847
5964
  async verifyRecords(data) {
5848
5965
  const { domain, dkimTokens, mailFromDomain, region } = data;
5849
5966
  const missing = [];
@@ -5891,6 +6008,10 @@ var init_cloudflare = __esm({
5891
6008
  });
5892
6009
 
5893
6010
  // src/utils/dns/vercel.ts
6011
+ var vercel_exports = {};
6012
+ __export(vercel_exports, {
6013
+ VercelDNSClient: () => VercelDNSClient
6014
+ });
5894
6015
  var VERCEL_API_BASE, VercelDNSClient;
5895
6016
  var init_vercel = __esm({
5896
6017
  "src/utils/dns/vercel.ts"() {
@@ -6122,6 +6243,68 @@ var init_vercel = __esm({
6122
6243
  };
6123
6244
  }
6124
6245
  }
6246
+ /**
6247
+ * Get all CAA records for the domain
6248
+ */
6249
+ async getCAARecords() {
6250
+ const result = await this.request(
6251
+ `/v4/domains/${this.domain}/records`
6252
+ );
6253
+ if (result.error || !result.records) {
6254
+ return [];
6255
+ }
6256
+ return result.records.filter((r) => r.type === "CAA").map((r) => {
6257
+ const match = r.value.match(/^(\d+)\s+(\w+)\s+"?([^"]+)"?$/);
6258
+ if (match) {
6259
+ return {
6260
+ flags: Number.parseInt(match[1], 10),
6261
+ tag: match[2],
6262
+ value: match[3]
6263
+ };
6264
+ }
6265
+ return null;
6266
+ }).filter(
6267
+ (r) => r !== null
6268
+ );
6269
+ }
6270
+ /**
6271
+ * Check if Amazon is allowed to issue certificates based on CAA records
6272
+ * Returns true if:
6273
+ * - No CAA records exist (any CA can issue)
6274
+ * - CAA records exist and include amazon.com or amazontrust.com
6275
+ */
6276
+ async isAmazonCAAAllowed() {
6277
+ const caaRecords = await this.getCAARecords();
6278
+ const issueRecords = caaRecords.filter(
6279
+ (r) => r.tag === "issue" || r.tag === "issuewild"
6280
+ );
6281
+ if (issueRecords.length === 0) {
6282
+ return { allowed: true, hasCAA: false, existingCAs: [] };
6283
+ }
6284
+ const existingCAs = issueRecords.map((r) => r.value);
6285
+ const amazonAllowed = existingCAs.some(
6286
+ (ca) => ca.includes("amazon.com") || ca.includes("amazontrust.com")
6287
+ );
6288
+ return { allowed: amazonAllowed, hasCAA: true, existingCAs };
6289
+ }
6290
+ /**
6291
+ * Add a CAA record to allow Amazon to issue certificates
6292
+ */
6293
+ async addAmazonCAARecord() {
6294
+ const body = {
6295
+ name: "@",
6296
+ // Root domain
6297
+ type: "CAA",
6298
+ value: '0 issue "amazon.com"',
6299
+ ttl: 1800
6300
+ };
6301
+ const result = await this.request(
6302
+ `/v2/domains/${this.domain}/records`,
6303
+ "POST",
6304
+ body
6305
+ );
6306
+ return !result.error;
6307
+ }
6125
6308
  async verifyRecords(data) {
6126
6309
  const { domain, dkimTokens, mailFromDomain, region } = data;
6127
6310
  const missing = [];
@@ -6687,6 +6870,76 @@ var init_dns = __esm({
6687
6870
  }
6688
6871
  });
6689
6872
 
6873
+ // src/utils/dns/caa.ts
6874
+ var caa_exports = {};
6875
+ __export(caa_exports, {
6876
+ ensureAmazonCAAAllowed: () => ensureAmazonCAAAllowed
6877
+ });
6878
+ async function ensureAmazonCAAAllowed(credentials, domain) {
6879
+ if (credentials.provider === "manual" || credentials.provider === "route53") {
6880
+ return { success: true, wasAlreadyAllowed: true, recordCreated: false };
6881
+ }
6882
+ try {
6883
+ if (credentials.provider === "vercel") {
6884
+ const { VercelDNSClient: VercelDNSClient2 } = await Promise.resolve().then(() => (init_vercel(), vercel_exports));
6885
+ const client = new VercelDNSClient2(
6886
+ domain,
6887
+ credentials.token,
6888
+ credentials.teamId
6889
+ );
6890
+ const caaStatus = await client.isAmazonCAAAllowed();
6891
+ if (caaStatus.hasCAA && caaStatus.allowed) {
6892
+ return { success: true, wasAlreadyAllowed: true, recordCreated: false };
6893
+ }
6894
+ const added = await client.addAmazonCAARecord();
6895
+ if (added) {
6896
+ return { success: true, wasAlreadyAllowed: false, recordCreated: true };
6897
+ }
6898
+ return {
6899
+ success: false,
6900
+ wasAlreadyAllowed: false,
6901
+ recordCreated: false,
6902
+ error: "Failed to create CAA record in Vercel DNS"
6903
+ };
6904
+ }
6905
+ if (credentials.provider === "cloudflare") {
6906
+ const { CloudflareDNSClient: CloudflareDNSClient2 } = await Promise.resolve().then(() => (init_cloudflare(), cloudflare_exports));
6907
+ const client = new CloudflareDNSClient2(
6908
+ credentials.zoneId,
6909
+ credentials.token
6910
+ );
6911
+ const caaStatus = await client.isAmazonCAAAllowed();
6912
+ if (caaStatus.allowed) {
6913
+ return { success: true, wasAlreadyAllowed: true, recordCreated: false };
6914
+ }
6915
+ const added = await client.addAmazonCAARecord();
6916
+ if (added) {
6917
+ return { success: true, wasAlreadyAllowed: false, recordCreated: true };
6918
+ }
6919
+ return {
6920
+ success: false,
6921
+ wasAlreadyAllowed: false,
6922
+ recordCreated: false,
6923
+ error: "Failed to create CAA record in Cloudflare DNS"
6924
+ };
6925
+ }
6926
+ return { success: true, wasAlreadyAllowed: true, recordCreated: false };
6927
+ } catch (error) {
6928
+ return {
6929
+ success: false,
6930
+ wasAlreadyAllowed: false,
6931
+ recordCreated: false,
6932
+ error: error instanceof Error ? error.message : "Unknown error"
6933
+ };
6934
+ }
6935
+ }
6936
+ var init_caa = __esm({
6937
+ "src/utils/dns/caa.ts"() {
6938
+ "use strict";
6939
+ init_esm_shims();
6940
+ }
6941
+ });
6942
+
6690
6943
  // src/utils/shared/assume-role.ts
6691
6944
  var assume_role_exports = {};
6692
6945
  __export(assume_role_exports, {
@@ -10753,10 +11006,10 @@ async function cdnSync(options) {
10753
11006
  });
10754
11007
  const stackOutputs = await checkStack.outputs();
10755
11008
  if (stackOutputs.acmCertificateArn?.value) {
10756
- const { ACMClient: ACMClient2, DescribeCertificateCommand: DescribeCertificateCommand2 } = await import("@aws-sdk/client-acm");
10757
- const acmClient = new ACMClient2({ region: "us-east-1" });
11009
+ const { ACMClient: ACMClient3, DescribeCertificateCommand: DescribeCertificateCommand3 } = await import("@aws-sdk/client-acm");
11010
+ const acmClient = new ACMClient3({ region: "us-east-1" });
10758
11011
  const certResponse = await acmClient.send(
10759
- new DescribeCertificateCommand2({
11012
+ new DescribeCertificateCommand3({
10760
11013
  CertificateArn: stackOutputs.acmCertificateArn.value
10761
11014
  })
10762
11015
  );
@@ -10941,12 +11194,12 @@ Current configuration:
10941
11194
  process.exit(0);
10942
11195
  }
10943
11196
  progress.start("Checking certificate validation status");
10944
- const { ACMClient: ACMClient2, DescribeCertificateCommand: DescribeCertificateCommand2 } = await import("@aws-sdk/client-acm");
10945
- const acmClient = new ACMClient2({ region: "us-east-1" });
11197
+ const { ACMClient: ACMClient3, DescribeCertificateCommand: DescribeCertificateCommand3 } = await import("@aws-sdk/client-acm");
11198
+ const acmClient = new ACMClient3({ region: "us-east-1" });
10946
11199
  let certStatus;
10947
11200
  try {
10948
11201
  const certResponse = await acmClient.send(
10949
- new DescribeCertificateCommand2({
11202
+ new DescribeCertificateCommand3({
10950
11203
  CertificateArn: stackOutputs.acmCertificateArn.value
10951
11204
  })
10952
11205
  );
@@ -11142,10 +11395,10 @@ async function checkDNSRecord(hostname, expectedValue) {
11142
11395
  }
11143
11396
  async function checkCertificateStatus(certificateArn) {
11144
11397
  try {
11145
- const { ACMClient: ACMClient2, DescribeCertificateCommand: DescribeCertificateCommand2 } = await import("@aws-sdk/client-acm");
11146
- const acm3 = new ACMClient2({ region: "us-east-1" });
11398
+ const { ACMClient: ACMClient3, DescribeCertificateCommand: DescribeCertificateCommand3 } = await import("@aws-sdk/client-acm");
11399
+ const acm3 = new ACMClient3({ region: "us-east-1" });
11147
11400
  const result = await acm3.send(
11148
- new DescribeCertificateCommand2({ CertificateArn: certificateArn })
11401
+ new DescribeCertificateCommand3({ CertificateArn: certificateArn })
11149
11402
  );
11150
11403
  const cert = result.Certificate;
11151
11404
  const validationStatus = cert?.DomainValidationOptions?.[0]?.ValidationStatus;
@@ -15296,6 +15549,7 @@ async function deployEmailStack(config2) {
15296
15549
  });
15297
15550
  let cloudFrontResources;
15298
15551
  let acmResources;
15552
+ let skipCloudFront = false;
15299
15553
  if (emailConfig.tracking?.enabled && emailConfig.tracking.customRedirectDomain && emailConfig.tracking.httpsEnabled) {
15300
15554
  const { findHostedZone: findHostedZone2 } = await Promise.resolve().then(() => (init_route53(), route53_exports));
15301
15555
  const hostedZone = await findHostedZone2(
@@ -15307,16 +15561,27 @@ async function deployEmailStack(config2) {
15307
15561
  domain: emailConfig.tracking.customRedirectDomain,
15308
15562
  hostedZoneId: hostedZone?.id
15309
15563
  });
15310
- const { createCloudFrontTracking: createCloudFrontTracking2 } = await Promise.resolve().then(() => (init_cloudfront(), cloudfront_exports));
15311
- const certificateArn = acmResources.certificateValidation ? acmResources.certificateValidation.certificateArn : acmResources.certificate.arn;
15312
- cloudFrontResources = await createCloudFrontTracking2({
15313
- customTrackingDomain: emailConfig.tracking.customRedirectDomain,
15314
- region: config2.region,
15315
- certificateArn,
15316
- hostedZoneId: hostedZone?.id,
15317
- // Pass hosted zone ID for automatic DNS record creation
15318
- wafEnabled: emailConfig.tracking.wafEnabled
15319
- });
15564
+ if (!hostedZone) {
15565
+ const { checkCertificateValidation: checkCertificateValidation2 } = await Promise.resolve().then(() => (init_acm(), acm_exports));
15566
+ const isValidated = await checkCertificateValidation2(
15567
+ emailConfig.tracking.customRedirectDomain
15568
+ );
15569
+ if (!isValidated) {
15570
+ skipCloudFront = true;
15571
+ }
15572
+ }
15573
+ if (!skipCloudFront) {
15574
+ const { createCloudFrontTracking: createCloudFrontTracking2 } = await Promise.resolve().then(() => (init_cloudfront(), cloudfront_exports));
15575
+ const certificateArn = acmResources.certificateValidation ? acmResources.certificateValidation.certificateArn : acmResources.certificate.arn;
15576
+ cloudFrontResources = await createCloudFrontTracking2({
15577
+ customTrackingDomain: emailConfig.tracking.customRedirectDomain,
15578
+ region: config2.region,
15579
+ certificateArn,
15580
+ hostedZoneId: hostedZone?.id,
15581
+ // Pass hosted zone ID for automatic DNS record creation
15582
+ wafEnabled: emailConfig.tracking.wafEnabled
15583
+ });
15584
+ }
15320
15585
  }
15321
15586
  let sesResources;
15322
15587
  if (emailConfig.tracking?.enabled || emailConfig.eventTracking?.enabled) {
@@ -15329,11 +15594,19 @@ async function deployEmailStack(config2) {
15329
15594
  if (!mailFromDomain && emailConfig.mailFromSubdomain && emailConfig.domain) {
15330
15595
  mailFromDomain = `${emailConfig.mailFromSubdomain}.${emailConfig.domain}`;
15331
15596
  }
15597
+ const effectiveTrackingConfig = skipCloudFront && emailConfig.tracking ? {
15598
+ enabled: emailConfig.tracking.enabled,
15599
+ opens: emailConfig.tracking.opens,
15600
+ clicks: emailConfig.tracking.clicks,
15601
+ customRedirectDomain: emailConfig.tracking.customRedirectDomain,
15602
+ httpsEnabled: false
15603
+ // Use OPTIONAL until CloudFront is ready
15604
+ } : emailConfig.tracking;
15332
15605
  sesResources = await createSESResources({
15333
15606
  domain: emailConfig.domain,
15334
15607
  mailFromDomain,
15335
15608
  region: config2.region,
15336
- trackingConfig: emailConfig.tracking,
15609
+ trackingConfig: effectiveTrackingConfig,
15337
15610
  eventTypes: emailConfig.eventTracking?.events,
15338
15611
  eventTrackingEnabled: emailConfig.eventTracking?.enabled,
15339
15612
  // Pass flag to create EventBridge destination
@@ -15461,6 +15734,8 @@ async function deployEmailStack(config2) {
15461
15734
  dlqUrl: sqsResources?.dlq.url,
15462
15735
  customTrackingDomain: sesResources?.customTrackingDomain,
15463
15736
  httpsTrackingEnabled: emailConfig.tracking?.httpsEnabled,
15737
+ httpsTrackingPending: skipCloudFront,
15738
+ // True if HTTPS requested but cert not validated yet
15464
15739
  cloudFrontDomain: cloudFrontResources?.domainName,
15465
15740
  acmCertificateValidationRecords: acmResources?.validationRecords,
15466
15741
  mailFromDomain: sesResources?.mailFromDomain,
@@ -21408,6 +21683,56 @@ ${pc28.bold("Cost Impact:")}`);
21408
21683
  const stackConfig = buildEmailStackConfig(metadata, region, {
21409
21684
  emailConfig: updatedConfig
21410
21685
  });
21686
+ if (updatedConfig.tracking?.httpsEnabled && updatedConfig.tracking.customRedirectDomain) {
21687
+ const trackingDomainParts = updatedConfig.tracking.customRedirectDomain.split(".");
21688
+ const parentDomain = trackingDomainParts.length > 2 ? trackingDomainParts.slice(-2).join(".") : updatedConfig.tracking.customRedirectDomain;
21689
+ let dnsProvider = metadata.services.email?.dnsProvider;
21690
+ if (!dnsProvider) {
21691
+ const availableProviders = await progress.execute(
21692
+ "Detecting DNS provider for CAA check",
21693
+ async () => await detectAvailableDNSProviders(parentDomain, region)
21694
+ );
21695
+ const detectedProvider = availableProviders.find(
21696
+ (p) => p.detected && p.provider !== "manual"
21697
+ );
21698
+ if (detectedProvider) {
21699
+ dnsProvider = detectedProvider.provider;
21700
+ if (metadata.services.email) {
21701
+ metadata.services.email.dnsProvider = dnsProvider;
21702
+ }
21703
+ }
21704
+ }
21705
+ if (dnsProvider && dnsProvider !== "manual" && dnsProvider !== "route53") {
21706
+ const credResult = await getDNSCredentials(
21707
+ dnsProvider,
21708
+ parentDomain,
21709
+ region
21710
+ );
21711
+ if (credResult.valid && credResult.credentials) {
21712
+ const { ensureAmazonCAAAllowed: ensureAmazonCAAAllowed2 } = await Promise.resolve().then(() => (init_caa(), caa_exports));
21713
+ const caaResult = await progress.execute(
21714
+ "Checking CAA records for certificate issuance",
21715
+ async () => await ensureAmazonCAAAllowed2(credResult.credentials, parentDomain)
21716
+ );
21717
+ if (caaResult.recordCreated) {
21718
+ progress.info(
21719
+ `Added CAA record to allow Amazon certificate issuance for ${pc28.cyan(parentDomain)}`
21720
+ );
21721
+ await new Promise((resolve) => setTimeout(resolve, 2e3));
21722
+ } else if (!caaResult.success) {
21723
+ clack27.log.warn(
21724
+ `Could not verify CAA records: ${caaResult.error || "Unknown error"}`
21725
+ );
21726
+ clack27.log.info(
21727
+ pc28.dim(
21728
+ "If certificate issuance fails, you may need to add a CAA record manually:"
21729
+ )
21730
+ );
21731
+ clack27.log.info(pc28.dim(` ${parentDomain} CAA 0 issue "amazon.com"`));
21732
+ }
21733
+ }
21734
+ }
21735
+ }
21411
21736
  if (options.preview) {
21412
21737
  try {
21413
21738
  const previewResult = await progress.execute(
@@ -21508,6 +21833,7 @@ ${pc28.bold("Cost Impact:")}`);
21508
21833
  dkimTokens: result.dkimTokens,
21509
21834
  customTrackingDomain: result.customTrackingDomain,
21510
21835
  httpsTrackingEnabled: result.httpsTrackingEnabled,
21836
+ httpsTrackingPending: result.httpsTrackingPending,
21511
21837
  cloudFrontDomain: result.cloudFrontDomain,
21512
21838
  acmCertificateValidationRecords: result.acmCertificateValidationRecords,
21513
21839
  archiveArn: result.archiveArn,
@@ -21549,6 +21875,7 @@ ${pc28.bold("Cost Impact:")}`);
21549
21875
  dkimTokens: pulumiOutputs.dkimTokens?.value,
21550
21876
  customTrackingDomain: pulumiOutputs.customTrackingDomain?.value,
21551
21877
  httpsTrackingEnabled: pulumiOutputs.httpsTrackingEnabled?.value,
21878
+ httpsTrackingPending: pulumiOutputs.httpsTrackingPending?.value,
21552
21879
  cloudFrontDomain: pulumiOutputs.cloudFrontDomain?.value,
21553
21880
  acmCertificateValidationRecords: pulumiOutputs.acmCertificateValidationRecords?.value,
21554
21881
  archiveArn: pulumiOutputs.archiveArn?.value,
@@ -21703,14 +22030,92 @@ ${pc28.bold("Add these DNS records to your DNS provider:")}
21703
22030
  if (outputs.httpsTrackingEnabled && outputs.acmCertificateValidationRecords) {
21704
22031
  acmValidationRecords.push(...outputs.acmCertificateValidationRecords);
21705
22032
  }
21706
- const needsCertificateValidation = outputs.httpsTrackingEnabled && acmValidationRecords.length > 0 && !outputs.cloudFrontDomain;
22033
+ let acmDnsAutoCreated = false;
22034
+ if (outputs.httpsTrackingPending && acmValidationRecords.length > 0 && outputs.customTrackingDomain) {
22035
+ const trackingDnsProvider = metadata.services.email?.dnsProvider;
22036
+ if (trackingDnsProvider && trackingDnsProvider !== "manual") {
22037
+ const trackingDomainParts = outputs.customTrackingDomain.split(".");
22038
+ const parentDomain = trackingDomainParts.length > 2 ? trackingDomainParts.slice(-2).join(".") : outputs.customTrackingDomain;
22039
+ const credResult = await progress.execute(
22040
+ `Validating ${getDNSProviderDisplayName(trackingDnsProvider)} credentials for ACM validation`,
22041
+ async () => await getDNSCredentials(trackingDnsProvider, parentDomain, region)
22042
+ );
22043
+ if (credResult.valid && credResult.credentials) {
22044
+ try {
22045
+ progress.start(
22046
+ `Creating ACM validation DNS record in ${getDNSProviderDisplayName(trackingDnsProvider)}`
22047
+ );
22048
+ if (credResult.credentials.provider === "vercel") {
22049
+ const { VercelDNSClient: VercelDNSClient2 } = await Promise.resolve().then(() => (init_vercel(), vercel_exports));
22050
+ const client = new VercelDNSClient2(
22051
+ parentDomain,
22052
+ credResult.credentials.token,
22053
+ credResult.credentials.teamId
22054
+ );
22055
+ const result = await client.createRecords(
22056
+ acmValidationRecords.map((r) => ({
22057
+ name: r.name,
22058
+ type: r.type,
22059
+ value: r.value
22060
+ }))
22061
+ );
22062
+ if (result.success) {
22063
+ progress.succeed(
22064
+ `Created ACM validation DNS record in ${getDNSProviderDisplayName(trackingDnsProvider)}`
22065
+ );
22066
+ acmDnsAutoCreated = true;
22067
+ progress.info(
22068
+ "Certificate validation usually takes 5-30 minutes. Run this command again after validation completes."
22069
+ );
22070
+ } else {
22071
+ progress.fail(
22072
+ `Failed to create ACM validation record: ${result.errors?.join(", ")}`
22073
+ );
22074
+ }
22075
+ } else if (credResult.credentials.provider === "cloudflare") {
22076
+ const { CloudflareDNSClient: CloudflareDNSClient2 } = await Promise.resolve().then(() => (init_cloudflare(), cloudflare_exports));
22077
+ const client = new CloudflareDNSClient2(
22078
+ credResult.credentials.zoneId,
22079
+ credResult.credentials.token
22080
+ );
22081
+ const result = await client.createRecords(
22082
+ acmValidationRecords.map((r) => ({
22083
+ name: r.name,
22084
+ type: r.type,
22085
+ value: r.value
22086
+ }))
22087
+ );
22088
+ if (result.success) {
22089
+ progress.succeed(
22090
+ `Created ACM validation DNS record in ${getDNSProviderDisplayName(trackingDnsProvider)}`
22091
+ );
22092
+ acmDnsAutoCreated = true;
22093
+ progress.info(
22094
+ "Certificate validation usually takes 5-30 minutes. Run this command again after validation completes."
22095
+ );
22096
+ } else {
22097
+ progress.fail(
22098
+ `Failed to create ACM validation record: ${result.errors?.join(", ")}`
22099
+ );
22100
+ }
22101
+ }
22102
+ } catch (error) {
22103
+ progress.fail(
22104
+ `Failed to create ACM validation record: ${error.message}`
22105
+ );
22106
+ }
22107
+ }
22108
+ }
22109
+ }
22110
+ const needsCertificateValidation = outputs.httpsTrackingPending || outputs.httpsTrackingEnabled && acmValidationRecords.length > 0 && !outputs.cloudFrontDomain;
21707
22111
  displaySuccess({
21708
22112
  roleArn: outputs.roleArn,
21709
22113
  configSetName: outputs.configSetName,
21710
22114
  region: outputs.region,
21711
22115
  tableName: outputs.tableName,
21712
22116
  trackingDomainDnsRecords: trackingDomainDnsRecords.length > 0 ? trackingDomainDnsRecords : void 0,
21713
- acmValidationRecords: acmValidationRecords.length > 0 ? acmValidationRecords : void 0,
22117
+ // Only show ACM validation records if they weren't auto-created
22118
+ acmValidationRecords: acmValidationRecords.length > 0 && !acmDnsAutoCreated ? acmValidationRecords : void 0,
21714
22119
  customTrackingDomain: outputs.customTrackingDomain,
21715
22120
  httpsTrackingEnabled: outputs.httpsTrackingEnabled
21716
22121
  });
@@ -21730,16 +22135,27 @@ ${pc28.green("\u2713")} ${pc28.bold("Upgrade complete!")}
21730
22135
  }
21731
22136
  if (needsCertificateValidation) {
21732
22137
  console.log(pc28.bold("\u26A0\uFE0F HTTPS Tracking - Next Steps:\n"));
21733
- console.log(
21734
- " 1. Add the SSL certificate validation DNS record shown above to your DNS provider"
21735
- );
21736
- console.log(
21737
- " 2. Wait for DNS propagation and certificate validation (5-30 minutes)"
21738
- );
21739
- console.log(
21740
- ` 3. Run ${pc28.cyan("wraps email upgrade")} again to complete CloudFront setup
22138
+ if (acmDnsAutoCreated) {
22139
+ console.log(
22140
+ ` 1. ${pc28.green("\u2713")} ACM validation DNS record created automatically`
22141
+ );
22142
+ console.log(" 2. Wait for certificate validation (5-30 minutes)");
22143
+ console.log(
22144
+ ` 3. Run ${pc28.cyan("wraps email upgrade")} again to complete CloudFront setup
21741
22145
  `
21742
- );
22146
+ );
22147
+ } else {
22148
+ console.log(
22149
+ " 1. Add the SSL certificate validation DNS record shown above to your DNS provider"
22150
+ );
22151
+ console.log(
22152
+ " 2. Wait for DNS propagation and certificate validation (5-30 minutes)"
22153
+ );
22154
+ console.log(
22155
+ ` 3. Run ${pc28.cyan("wraps email upgrade")} again to complete CloudFront setup
22156
+ `
22157
+ );
22158
+ }
21743
22159
  console.log(
21744
22160
  pc28.dim(
21745
22161
  " Note: CloudFront distribution will be created once the certificate is validated.\n"
@@ -24496,6 +24912,7 @@ async function platform() {
24496
24912
 
24497
24913
  // src/commands/platform/update-role.ts
24498
24914
  init_esm_shims();
24915
+ init_events();
24499
24916
  init_aws();
24500
24917
  init_metadata();
24501
24918
  import {
@@ -24506,6 +24923,7 @@ import {
24506
24923
  import { confirm as confirm13, intro as intro32, isCancel as isCancel19, log as log31, outro as outro19 } from "@clack/prompts";
24507
24924
  import pc35 from "picocolors";
24508
24925
  async function updateRole(options) {
24926
+ const startTime = Date.now();
24509
24927
  intro32(pc35.bold("Update Platform Access Role"));
24510
24928
  const progress = new DeploymentProgress();
24511
24929
  const identity = await progress.execute(
@@ -24633,6 +25051,11 @@ Run ${pc35.cyan("wraps email init")} to deploy infrastructure first.
24633
25051
  }
24634
25052
  progress.stop();
24635
25053
  const actionVerb = roleExists4 ? "updated" : "created";
25054
+ trackCommand("platform:update-role", {
25055
+ success: true,
25056
+ duration_ms: Date.now() - startTime,
25057
+ action: actionVerb
25058
+ });
24636
25059
  outro19(pc35.green(`\u2713 Platform access role ${actionVerb} successfully`));
24637
25060
  console.log(`
24638
25061
  ${pc35.bold("Permissions:")}`);