@wraps.dev/cli 2.19.8 → 2.20.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
@@ -6057,7 +6057,9 @@ function getAllTrackedDomains(metadata) {
6057
6057
  managed: true,
6058
6058
  purpose: d.purpose,
6059
6059
  mailFromDomain: d.mailFromDomain,
6060
- addedAt: d.addedAt
6060
+ addedAt: d.addedAt,
6061
+ configSetName: d.configSetName,
6062
+ trackingConfig: d.trackingConfig
6061
6063
  });
6062
6064
  }
6063
6065
  return result;
@@ -7255,6 +7257,34 @@ var init_dist = __esm({
7255
7257
  }
7256
7258
  });
7257
7259
 
7260
+ // src/utils/email/config-set-slug.ts
7261
+ var config_set_slug_exports = {};
7262
+ __export(config_set_slug_exports, {
7263
+ domainToConfigSetName: () => domainToConfigSetName
7264
+ });
7265
+ import { createHash } from "crypto";
7266
+ function domainToConfigSetName(domain) {
7267
+ const slug = domain.toLowerCase().replace(/\./g, "-");
7268
+ const needsHash = domain.includes("-") || slug.length > MAX_SLUG;
7269
+ if (!needsHash) {
7270
+ return `${PREFIX}${slug}`;
7271
+ }
7272
+ const hash = createHash("sha256").update(domain).digest("hex").slice(0, 8);
7273
+ const slugPart = slug.length > MAX_SLUG_WITH_HASH ? slug.slice(0, MAX_SLUG_WITH_HASH) : slug;
7274
+ return `${PREFIX}${slugPart}-${hash}`;
7275
+ }
7276
+ var PREFIX, MAX_LENGTH, MAX_SLUG, MAX_SLUG_WITH_HASH;
7277
+ var init_config_set_slug = __esm({
7278
+ "src/utils/email/config-set-slug.ts"() {
7279
+ "use strict";
7280
+ init_esm_shims();
7281
+ PREFIX = "wraps-email-";
7282
+ MAX_LENGTH = 64;
7283
+ MAX_SLUG = MAX_LENGTH - PREFIX.length;
7284
+ MAX_SLUG_WITH_HASH = MAX_SLUG - 9;
7285
+ }
7286
+ });
7287
+
7258
7288
  // src/infrastructure/resources/eventbridge-user-webhook.ts
7259
7289
  var eventbridge_user_webhook_exports = {};
7260
7290
  __export(eventbridge_user_webhook_exports, {
@@ -7929,20 +7959,20 @@ async function createMailManagerArchive(config2) {
7929
7959
  let archiveId;
7930
7960
  let archiveArn;
7931
7961
  try {
7932
- const listResult = await mailManagerClient.send(new ListArchivesCommand({}));
7962
+ const listResult = await mailManagerClient.send(
7963
+ new ListArchivesCommand({})
7964
+ );
7933
7965
  const existing = listResult.Archives?.find(
7934
7966
  (a) => a.ArchiveState === ArchiveState.ACTIVE && a.ArchiveName !== void 0 && namePattern.test(a.ArchiveName)
7935
7967
  );
7936
7968
  if (existing?.ArchiveId) {
7937
- console.log(`Using existing Mail Manager archive: ${existing.ArchiveName}`);
7938
7969
  archiveId = existing.ArchiveId;
7939
7970
  const getResult = await mailManagerClient.send(
7940
7971
  new GetArchiveCommand({ ArchiveId: archiveId })
7941
7972
  );
7942
7973
  archiveArn = getResult.ArchiveArn;
7943
7974
  }
7944
- } catch (error) {
7945
- console.log("Error checking for existing archive:", error);
7975
+ } catch {
7946
7976
  }
7947
7977
  if (!archiveId) {
7948
7978
  for (let attempt = 1; attempt <= MAX_NAME_ATTEMPTS; attempt++) {
@@ -7966,13 +7996,9 @@ async function createMailManagerArchive(config2) {
7966
7996
  "Failed to create Mail Manager Archive: No ArchiveId returned"
7967
7997
  );
7968
7998
  }
7969
- console.log(`Created new Mail Manager archive: ${archiveName}`);
7970
7999
  break;
7971
8000
  } catch (error) {
7972
8001
  if (error instanceof Error && error.name === "ConflictException" && attempt < MAX_NAME_ATTEMPTS) {
7973
- console.log(
7974
- `Archive '${archiveName}' is unavailable, trying '${baseArchiveName}-${attempt + 1}'...`
7975
- );
7976
8002
  continue;
7977
8003
  }
7978
8004
  throw error;
@@ -9572,7 +9598,7 @@ Run ${pc27.cyan(`wraps email domains verify --domain ${domain} --wait`)} to keep
9572
9598
  }
9573
9599
  }
9574
9600
  },
9575
- ConfigurationSetName: "wraps-email-tracking"
9601
+ ConfigurationSetName: domainToConfigSetName(domain)
9576
9602
  })
9577
9603
  );
9578
9604
  return response.MessageId;
@@ -9686,6 +9712,7 @@ var init_test = __esm({
9686
9712
  init_events();
9687
9713
  init_ses_simulator();
9688
9714
  init_verification();
9715
+ init_config_set_slug();
9689
9716
  init_aws();
9690
9717
  init_errors();
9691
9718
  init_json_output();
@@ -17403,6 +17430,7 @@ import pc18 from "picocolors";
17403
17430
  // src/infrastructure/email-stack.ts
17404
17431
  init_esm_shims();
17405
17432
  init_dist();
17433
+ init_config_set_slug();
17406
17434
  import * as aws19 from "@pulumi/aws";
17407
17435
 
17408
17436
  // src/infrastructure/resources/alerting.ts
@@ -17963,6 +17991,7 @@ init_lambda();
17963
17991
 
17964
17992
  // src/infrastructure/resources/ses.ts
17965
17993
  init_esm_shims();
17994
+ init_config_set_slug();
17966
17995
  import * as aws9 from "@pulumi/aws";
17967
17996
  async function configurationSetExists(configSetName, region) {
17968
17997
  try {
@@ -18014,8 +18043,9 @@ async function emailIdentityExists(emailIdentity, region) {
18014
18043
  }
18015
18044
  }
18016
18045
  async function createSESResources(config2) {
18046
+ const configSetName = config2.domain ? domainToConfigSetName(config2.domain) : "wraps-email-tracking";
18017
18047
  const configSetOptions = {
18018
- configurationSetName: "wraps-email-tracking",
18048
+ configurationSetName: configSetName,
18019
18049
  deliveryOptions: config2.tlsRequired ? {
18020
18050
  tlsPolicy: "REQUIRE"
18021
18051
  // Require TLS 1.2+ for all emails
@@ -18040,7 +18070,6 @@ async function createSESResources(config2) {
18040
18070
  httpsPolicy: config2.trackingConfig.httpsEnabled ? "REQUIRE" : "OPTIONAL"
18041
18071
  };
18042
18072
  }
18043
- const configSetName = "wraps-email-tracking";
18044
18073
  const exists = await configurationSetExists(configSetName, config2.region);
18045
18074
  const configSet = exists && !config2.skipResourceImports ? new aws9.sesv2.ConfigurationSet(configSetName, configSetOptions, {
18046
18075
  import: configSetName
@@ -18050,6 +18079,20 @@ async function createSESResources(config2) {
18050
18079
  });
18051
18080
  if (config2.eventTrackingEnabled) {
18052
18081
  const eventDestName = "wraps-email-eventbridge";
18082
+ const opensEnabled = config2.trackingConfig?.opens ?? true;
18083
+ const clicksEnabled = config2.trackingConfig?.clicks ?? true;
18084
+ const matchingEventTypes = [
18085
+ "SEND",
18086
+ "DELIVERY",
18087
+ ...opensEnabled ? ["OPEN"] : [],
18088
+ ...clicksEnabled ? ["CLICK"] : [],
18089
+ "BOUNCE",
18090
+ "COMPLAINT",
18091
+ "REJECT",
18092
+ "RENDERING_FAILURE",
18093
+ "DELIVERY_DELAY",
18094
+ "SUBSCRIPTION"
18095
+ ];
18053
18096
  new aws9.sesv2.ConfigurationSetEventDestination(
18054
18097
  "wraps-email-all-events",
18055
18098
  {
@@ -18057,18 +18100,7 @@ async function createSESResources(config2) {
18057
18100
  eventDestinationName: eventDestName,
18058
18101
  eventDestination: {
18059
18102
  enabled: true,
18060
- matchingEventTypes: [
18061
- "SEND",
18062
- "DELIVERY",
18063
- "OPEN",
18064
- "CLICK",
18065
- "BOUNCE",
18066
- "COMPLAINT",
18067
- "REJECT",
18068
- "RENDERING_FAILURE",
18069
- "DELIVERY_DELAY",
18070
- "SUBSCRIPTION"
18071
- ],
18103
+ matchingEventTypes,
18072
18104
  eventBridgeDestination: {
18073
18105
  // SES requires default bus - cannot use custom bus
18074
18106
  eventBusArn: defaultEventBus.arn
@@ -18078,7 +18110,7 @@ async function createSESResources(config2) {
18078
18110
  {
18079
18111
  // Import existing resource if it already exists in AWS but not in Pulumi state.
18080
18112
  // Skip when skipResourceImports is true (resource already tracked in state).
18081
- import: config2.importExistingEventDestination && !config2.skipResourceImports ? `wraps-email-tracking|${eventDestName}` : void 0
18113
+ import: config2.importExistingEventDestination && !config2.skipResourceImports ? `${configSetName}|${eventDestName}` : void 0
18082
18114
  }
18083
18115
  );
18084
18116
  }
@@ -18349,7 +18381,7 @@ async function deployEmailStack(config2) {
18349
18381
  let sesResources;
18350
18382
  if (emailConfig.tracking?.enabled || emailConfig.eventTracking?.enabled) {
18351
18383
  const shouldImportEventDest = !config2.skipResourceImports && emailConfig.eventTracking?.enabled && await eventDestinationExists(
18352
- "wraps-email-tracking",
18384
+ domainToConfigSetName(emailConfig.domain ?? ""),
18353
18385
  "wraps-email-eventbridge",
18354
18386
  config2.region
18355
18387
  );
@@ -18433,7 +18465,7 @@ async function deployEmailStack(config2) {
18433
18465
  let smtpResources;
18434
18466
  if (emailConfig.smtpCredentials?.enabled && sesResources) {
18435
18467
  smtpResources = await createSMTPCredentials({
18436
- configSetName: "wraps-email-tracking",
18468
+ configSetName: domainToConfigSetName(emailConfig.domain ?? ""),
18437
18469
  region: config2.region
18438
18470
  });
18439
18471
  }
@@ -19222,17 +19254,18 @@ ${pc19.dim("\u2500\u2500\u2500 end Pulumi output \u2500\u2500\u2500")}
19222
19254
  tableName: outputs.tableName
19223
19255
  });
19224
19256
  if (selectedIdentities.length > 0 && emailConfig.tracking?.enabled) {
19257
+ const displayConfigSetName = outputs.configSetName ?? "wraps-email-<your-domain>";
19225
19258
  console.log(`
19226
19259
  ${pc19.bold("Next Steps:")}
19227
19260
  `);
19228
19261
  console.log(
19229
- `Update your code to use configuration set: ${pc19.cyan("wraps-email-tracking")}`
19262
+ `Update your code to use configuration set: ${pc19.cyan(displayConfigSetName)}`
19230
19263
  );
19231
19264
  console.log(`
19232
19265
  ${pc19.dim("Example:")}`);
19233
19266
  console.log(
19234
19267
  pc19.gray(` await ses.sendEmail({
19235
- ConfigurationSetName: 'wraps-email-tracking',
19268
+ ConfigurationSetName: '${displayConfigSetName}',
19236
19269
  // ... other parameters
19237
19270
  });`)
19238
19271
  );
@@ -19550,6 +19583,24 @@ async function emailDestroy(options) {
19550
19583
  "Some resources may not have been fully removed. You can re-run this command or clean up manually in the AWS console."
19551
19584
  );
19552
19585
  }
19586
+ if (destroyFailed) {
19587
+ try {
19588
+ const { SQSClient, GetQueueUrlCommand, DeleteQueueCommand } = await import("@aws-sdk/client-sqs");
19589
+ const sqsClient = new SQSClient({ region });
19590
+ for (const queueName of ["wraps-email-events", "wraps-email-events-dlq"]) {
19591
+ try {
19592
+ const { QueueUrl } = await sqsClient.send(
19593
+ new GetQueueUrlCommand({ QueueName: queueName })
19594
+ );
19595
+ if (QueueUrl) {
19596
+ await sqsClient.send(new DeleteQueueCommand({ QueueUrl }));
19597
+ }
19598
+ } catch {
19599
+ }
19600
+ }
19601
+ } catch {
19602
+ }
19603
+ }
19553
19604
  await deleteConnectionMetadata(identity.accountId, region);
19554
19605
  progress.stop();
19555
19606
  if (isJsonMode()) {
@@ -19923,8 +19974,14 @@ init_json_output();
19923
19974
  init_metadata();
19924
19975
  init_output();
19925
19976
  init_prompts();
19977
+ init_config_set_slug();
19926
19978
  import { Resolver as Resolver2 } from "dns/promises";
19927
- import { GetEmailIdentityCommand as GetEmailIdentityCommand2, SESv2Client as SESv2Client4 } from "@aws-sdk/client-sesv2";
19979
+ import {
19980
+ CreateConfigurationSetCommand,
19981
+ CreateConfigurationSetEventDestinationCommand,
19982
+ GetEmailIdentityCommand as GetEmailIdentityCommand2,
19983
+ SESv2Client as SESv2Client4
19984
+ } from "@aws-sdk/client-sesv2";
19928
19985
  import * as clack21 from "@clack/prompts";
19929
19986
  import pc23 from "picocolors";
19930
19987
  async function checkVerification(domain, sesClient, region) {
@@ -20382,13 +20439,57 @@ Run ${pc23.cyan("wraps email init")} first to deploy email infrastructure.
20382
20439
  if (!options.yes) {
20383
20440
  purpose = await promptDomainPurpose();
20384
20441
  }
20442
+ const configSetName = domainToConfigSetName(domain);
20443
+ const trackingConfig = {
20444
+ opens: purpose === "marketing" || purpose === "notifications",
20445
+ clicks: purpose === "marketing" || purpose === "notifications"
20446
+ };
20447
+ const baseEventTypes = [
20448
+ "SEND",
20449
+ "DELIVERY",
20450
+ "BOUNCE",
20451
+ "COMPLAINT",
20452
+ "REJECT",
20453
+ "RENDERING_FAILURE",
20454
+ "DELIVERY_DELAY",
20455
+ "SUBSCRIPTION"
20456
+ ];
20457
+ const matchingEventTypes = trackingConfig.opens ? [...baseEventTypes, "OPEN", "CLICK"] : [...baseEventTypes];
20458
+ const eventBusArn = `arn:aws:events:${region}:${identity.accountId}:event-bus/default`;
20459
+ await progress.execute("Creating tracking configuration", async () => {
20460
+ try {
20461
+ await sesClient.send(
20462
+ new CreateConfigurationSetCommand({
20463
+ ConfigurationSetName: configSetName,
20464
+ SuppressionOptions: { SuppressedReasons: ["BOUNCE", "COMPLAINT"] }
20465
+ })
20466
+ );
20467
+ } catch (err) {
20468
+ if (err.name !== "AlreadyExistsException") throw err;
20469
+ }
20470
+ try {
20471
+ await sesClient.send(
20472
+ new CreateConfigurationSetEventDestinationCommand({
20473
+ ConfigurationSetName: configSetName,
20474
+ EventDestinationName: "wraps-email-eventbridge",
20475
+ EventDestination: {
20476
+ Enabled: true,
20477
+ MatchingEventTypes: matchingEventTypes,
20478
+ EventBridgeDestination: { EventBusArn: eventBusArn }
20479
+ }
20480
+ })
20481
+ );
20482
+ } catch (err) {
20483
+ if (err.name !== "AlreadyExistsException") throw err;
20484
+ }
20485
+ });
20385
20486
  if (domainAlreadyExists) {
20386
20487
  const { PutEmailIdentityConfigurationSetAttributesCommand } = await import("@aws-sdk/client-sesv2");
20387
20488
  await progress.execute("Associating tracking configuration", async () => {
20388
20489
  await sesClient.send(
20389
20490
  new PutEmailIdentityConfigurationSetAttributesCommand({
20390
20491
  EmailIdentity: domain,
20391
- ConfigurationSetName: "wraps-email-tracking"
20492
+ ConfigurationSetName: configSetName
20392
20493
  })
20393
20494
  );
20394
20495
  });
@@ -20398,7 +20499,7 @@ Run ${pc23.cyan("wraps email init")} first to deploy email infrastructure.
20398
20499
  await sesClient.send(
20399
20500
  new CreateEmailIdentityCommand({
20400
20501
  EmailIdentity: domain,
20401
- ConfigurationSetName: "wraps-email-tracking",
20502
+ ConfigurationSetName: configSetName,
20402
20503
  DkimSigningAttributes: {
20403
20504
  NextSigningKeyLength: "RSA_2048_BIT"
20404
20505
  }
@@ -20511,6 +20612,8 @@ Run ${pc23.cyan("wraps email init")} first to deploy email infrastructure.
20511
20612
  domain,
20512
20613
  mailFromDomain,
20513
20614
  purpose,
20615
+ configSetName,
20616
+ trackingConfig,
20514
20617
  addedAt: (/* @__PURE__ */ new Date()).toISOString()
20515
20618
  };
20516
20619
  addDomainToMetadata(metadata, entry);
@@ -21424,7 +21527,7 @@ Deploy first: ${pc24.cyan("wraps email inbound init")}
21424
21527
  }
21425
21528
  const emailService = metadata.services.email;
21426
21529
  const inboundConfig = emailService.config.inbound;
21427
- if (!options.force) {
21530
+ if (!(options.force || options.preview)) {
21428
21531
  clack22.log.warn(
21429
21532
  `This will remove inbound email for ${pc24.cyan(inboundConfig.receivingDomain || "")}`
21430
21533
  );
@@ -21437,17 +21540,6 @@ Deploy first: ${pc24.cyan("wraps email inbound init")}
21437
21540
  process.exit(0);
21438
21541
  }
21439
21542
  }
21440
- await progress.execute("Removing SES receipt rules", async () => {
21441
- await deleteReceiptRule(region);
21442
- await deleteReceiptRuleSet(region);
21443
- });
21444
- await progress.execute(
21445
- "Preparing workspace",
21446
- async () => ensurePulumiWorkDir({
21447
- accountId: identity.accountId,
21448
- region
21449
- })
21450
- );
21451
21543
  const pulumiWorkDir = getPulumiWorkDir();
21452
21544
  const stackName = emailService.pulumiStackName || `wraps-${identity.accountId}-${region}`;
21453
21545
  const updatedEmailConfig = {
@@ -21458,7 +21550,8 @@ Deploy first: ${pc24.cyan("wraps email inbound init")}
21458
21550
  const stackConfig = buildEmailStackConfig(metadata, region, {
21459
21551
  emailConfig: updatedEmailConfig
21460
21552
  });
21461
- await progress.execute("Removing inbound infrastructure", async () => {
21553
+ const createStack = async () => {
21554
+ await ensurePulumiWorkDir({ accountId: identity.accountId, region });
21462
21555
  const stack = await pulumi18.automation.LocalWorkspace.createOrSelectStack(
21463
21556
  {
21464
21557
  stackName,
@@ -21473,6 +21566,39 @@ Deploy first: ${pc24.cyan("wraps email inbound init")}
21473
21566
  }
21474
21567
  );
21475
21568
  await stack.setConfig("aws:region", { value: region });
21569
+ return stack;
21570
+ };
21571
+ if (options.preview) {
21572
+ const previewResult = await progress.execute(
21573
+ "Generating infrastructure preview",
21574
+ async () => {
21575
+ const stack = await createStack();
21576
+ return previewWithResourceChanges(stack, { diff: true });
21577
+ }
21578
+ );
21579
+ displayPreview({
21580
+ changeSummary: previewResult.changeSummary,
21581
+ resourceChanges: previewResult.resourceChanges,
21582
+ commandName: "wraps email inbound destroy"
21583
+ });
21584
+ clack22.outro(
21585
+ pc24.green("Preview complete. Run without --preview to destroy.")
21586
+ );
21587
+ return;
21588
+ }
21589
+ await progress.execute("Removing SES receipt rules", async () => {
21590
+ await deleteReceiptRule(region);
21591
+ await deleteReceiptRuleSet(region);
21592
+ });
21593
+ await progress.execute(
21594
+ "Preparing workspace",
21595
+ async () => ensurePulumiWorkDir({
21596
+ accountId: identity.accountId,
21597
+ region
21598
+ })
21599
+ );
21600
+ await progress.execute("Removing inbound infrastructure", async () => {
21601
+ const stack = await createStack();
21476
21602
  await withLockRetry(
21477
21603
  () => withTimeout(
21478
21604
  stack.up({ onOutput: () => {
@@ -22502,6 +22628,36 @@ async function init2(options) {
22502
22628
  emailConfig.mailFromSubdomain = mailFromFull.endsWith(suffix) ? mailFromFull.slice(0, -suffix.length) || "mail" : "mail";
22503
22629
  }
22504
22630
  }
22631
+ if (!options.quick && preset !== "custom" && emailConfig.tracking?.enabled) {
22632
+ const purpose = await promptDomainPurpose();
22633
+ if (purpose === "transactional") {
22634
+ emailConfig.tracking = { ...emailConfig.tracking, opens: false, clicks: false };
22635
+ } else if (purpose === "marketing" || purpose === "notifications") {
22636
+ emailConfig.tracking = { ...emailConfig.tracking, opens: true, clicks: true };
22637
+ } else {
22638
+ const trackOpens = await clack26.confirm({
22639
+ message: "Track email opens?",
22640
+ initialValue: emailConfig.tracking.opens ?? true
22641
+ });
22642
+ if (clack26.isCancel(trackOpens)) {
22643
+ clack26.cancel("Operation cancelled.");
22644
+ process.exit(0);
22645
+ }
22646
+ const trackClicks = await clack26.confirm({
22647
+ message: "Track link clicks?",
22648
+ initialValue: emailConfig.tracking.clicks ?? true
22649
+ });
22650
+ if (clack26.isCancel(trackClicks)) {
22651
+ clack26.cancel("Operation cancelled.");
22652
+ process.exit(0);
22653
+ }
22654
+ emailConfig.tracking = {
22655
+ ...emailConfig.tracking,
22656
+ opens: trackOpens,
22657
+ clicks: trackClicks
22658
+ };
22659
+ }
22660
+ }
22505
22661
  let costSummary;
22506
22662
  if (!options.quick) {
22507
22663
  const estimatedVolume = await promptEstimatedVolume();
@@ -23372,6 +23528,59 @@ async function replyInit(options) {
23372
23528
  );
23373
23529
  }
23374
23530
  const stackName = emailService.pulumiStackName || `wraps-${identity.accountId}-${region}`;
23531
+ if (options.preview) {
23532
+ const previewResult = await progress.execute(
23533
+ "Generating infrastructure preview",
23534
+ async () => {
23535
+ const previewMetadata = JSON.parse(
23536
+ JSON.stringify(metadata)
23537
+ );
23538
+ const previewEmailConfig = previewMetadata.services.email.config;
23539
+ const rt = previewEmailConfig.replyThreading ?? {
23540
+ enabled: false,
23541
+ domains: []
23542
+ };
23543
+ for (const domain of targetDomains) {
23544
+ const filtered = rt.domains.filter((d) => d.domain !== domain);
23545
+ filtered.push({
23546
+ domain,
23547
+ initialSecret: randomBytes5(32).toString("base64"),
23548
+ currentKid: 1,
23549
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
23550
+ });
23551
+ rt.domains = filtered;
23552
+ }
23553
+ previewEmailConfig.replyThreading = {
23554
+ enabled: true,
23555
+ domains: rt.domains
23556
+ };
23557
+ const stackConfig = buildEmailStackConfig(previewMetadata, region);
23558
+ await ensurePulumiWorkDir({ accountId: identity.accountId, region });
23559
+ const stack = await pulumi20.automation.LocalWorkspace.createOrSelectStack(
23560
+ {
23561
+ stackName,
23562
+ projectName: "wraps-email",
23563
+ program: async () => {
23564
+ const result = await deployEmailStack(stackConfig);
23565
+ return result;
23566
+ }
23567
+ },
23568
+ {
23569
+ workDir: getPulumiWorkDir()
23570
+ }
23571
+ );
23572
+ await stack.setConfig("aws:region", { value: region });
23573
+ return previewWithResourceChanges(stack, { diff: true });
23574
+ }
23575
+ );
23576
+ displayPreview({
23577
+ changeSummary: previewResult.changeSummary,
23578
+ resourceChanges: previewResult.resourceChanges,
23579
+ commandName: "wraps email reply init"
23580
+ });
23581
+ clack27.outro(pc29.green("Preview complete. Run without --preview to deploy."));
23582
+ return;
23583
+ }
23375
23584
  const results = [];
23376
23585
  for (const domain of targetDomains) {
23377
23586
  const fresh = await loadConnectionMetadata(identity.accountId, region);
@@ -23713,7 +23922,7 @@ async function replyDestroy(options) {
23713
23922
  "https://wraps.dev/docs/guides/reply-threading"
23714
23923
  );
23715
23924
  }
23716
- if (!(options.force || isJsonMode())) {
23925
+ if (!(options.force || options.preview || isJsonMode())) {
23717
23926
  const confirmed = await clack27.confirm({
23718
23927
  message: `Remove reply threading for ${targets.join(", ")}?`,
23719
23928
  initialValue: false
@@ -23723,10 +23932,55 @@ async function replyDestroy(options) {
23723
23932
  return;
23724
23933
  }
23725
23934
  }
23935
+ const emailService = metadata.services.email;
23936
+ if (options.preview) {
23937
+ if (emailService) {
23938
+ const previewMetadata = JSON.parse(
23939
+ JSON.stringify(metadata)
23940
+ );
23941
+ for (const domain of targets) {
23942
+ stripDomainFromReplyThreadingMetadata({
23943
+ domain,
23944
+ metadata: previewMetadata
23945
+ });
23946
+ }
23947
+ const stackName = emailService.pulumiStackName || `wraps-${identity.accountId}-${region}`;
23948
+ const stackConfig = buildEmailStackConfig(previewMetadata, region);
23949
+ const previewResult = await progress.execute(
23950
+ "Generating infrastructure preview",
23951
+ async () => {
23952
+ await ensurePulumiWorkDir({ accountId: identity.accountId, region });
23953
+ const stack = await pulumi20.automation.LocalWorkspace.createOrSelectStack(
23954
+ {
23955
+ stackName,
23956
+ projectName: "wraps-email",
23957
+ program: async () => {
23958
+ const result = await deployEmailStack(stackConfig);
23959
+ return result;
23960
+ }
23961
+ },
23962
+ {
23963
+ workDir: getPulumiWorkDir()
23964
+ }
23965
+ );
23966
+ await stack.setConfig("aws:region", { value: region });
23967
+ return previewWithResourceChanges(stack, { diff: true });
23968
+ }
23969
+ );
23970
+ displayPreview({
23971
+ changeSummary: previewResult.changeSummary,
23972
+ resourceChanges: previewResult.resourceChanges,
23973
+ commandName: "wraps email reply destroy"
23974
+ });
23975
+ }
23976
+ clack27.outro(
23977
+ pc29.green("Preview complete. Run without --preview to destroy.")
23978
+ );
23979
+ return;
23980
+ }
23726
23981
  for (const domain of targets) {
23727
23982
  stripDomainFromReplyThreadingMetadata({ domain, metadata });
23728
23983
  }
23729
- const emailService = metadata.services.email;
23730
23984
  if (emailService) {
23731
23985
  const stackName = emailService.pulumiStackName || `wraps-${identity.accountId}-${region}`;
23732
23986
  const stackConfig = buildEmailStackConfig(metadata, region);
@@ -23819,6 +24073,7 @@ async function replyDecode(addressInput, options) {
23819
24073
  // src/commands/email/restore.ts
23820
24074
  init_esm_shims();
23821
24075
  init_events();
24076
+ init_config_set_slug();
23822
24077
  init_aws();
23823
24078
  init_errors();
23824
24079
  init_fs();
@@ -23882,7 +24137,10 @@ ${pc30.bold("The following Wraps resources will be removed:")}
23882
24137
  `
23883
24138
  );
23884
24139
  if (metadata.services.email?.config.tracking?.enabled) {
23885
- console.log(` ${pc30.cyan("\u2713")} Configuration Set (wraps-email-tracking)`);
24140
+ const configSetName = domainToConfigSetName(
24141
+ metadata.services.email.config.domain ?? ""
24142
+ );
24143
+ console.log(` ${pc30.cyan("\u2713")} Configuration Set (${configSetName})`);
23886
24144
  }
23887
24145
  if (metadata.services.email?.config.eventTracking?.dynamoDBHistory) {
23888
24146
  console.log(` ${pc30.cyan("\u2713")} DynamoDB Table (wraps-email-history)`);
@@ -25440,7 +25698,7 @@ function renderErrorPage(err) {
25440
25698
  // src/commands/email/templates/push.ts
25441
25699
  init_esm_shims();
25442
25700
  init_events();
25443
- import { createHash } from "crypto";
25701
+ import { createHash as createHash2 } from "crypto";
25444
25702
  import { existsSync as existsSync13 } from "fs";
25445
25703
  import { mkdir as mkdir6, readFile as readFile6, writeFile as writeFile8 } from "fs/promises";
25446
25704
  import { join as join15 } from "path";
@@ -26077,7 +26335,7 @@ async function pushToAPI(templates, token, progress, force) {
26077
26335
  return results;
26078
26336
  }
26079
26337
  function sha256(content) {
26080
- return createHash("sha256").update(content).digest("hex");
26338
+ return createHash2("sha256").update(content).digest("hex");
26081
26339
  }
26082
26340
 
26083
26341
  // src/cli.ts
@@ -26301,6 +26559,11 @@ ${pc35.bold("Current Configuration:")}
26301
26559
  value: "hosting-provider",
26302
26560
  label: "Change hosting provider",
26303
26561
  hint: metadata.provider === "vercel" ? `Currently: Vercel (${metadata.vercel?.teamSlug || "configured"})` : `Currently: ${metadata.provider} \u2192 Switch to Vercel OIDC, etc.`
26562
+ },
26563
+ {
26564
+ value: "per-domain-config-sets",
26565
+ label: "Per-domain configuration sets",
26566
+ hint: "Create dedicated SES config sets for each additional domain"
26304
26567
  }
26305
26568
  );
26306
26569
  if (options.action) {
@@ -27433,6 +27696,131 @@ ${pc35.bold("SMTP Credentials for Legacy Systems")}
27433
27696
  }
27434
27697
  break;
27435
27698
  }
27699
+ case "per-domain-config-sets": {
27700
+ const {
27701
+ SESv2Client: SESv2Client9,
27702
+ CreateConfigurationSetCommand: CreateConfigurationSetCommand2,
27703
+ CreateConfigurationSetEventDestinationCommand: CreateConfigurationSetEventDestinationCommand2,
27704
+ PutEmailIdentityConfigurationSetAttributesCommand,
27705
+ EventType
27706
+ } = await import("@aws-sdk/client-sesv2");
27707
+ const { domainToConfigSetName: domainToConfigSetName2 } = await Promise.resolve().then(() => (init_config_set_slug(), config_set_slug_exports));
27708
+ const sesClient = new SESv2Client9({ region });
27709
+ const accountId = identity.accountId;
27710
+ const additionalDomains = metadata.services.email?.config.additionalDomains ?? [];
27711
+ const unmigratedDomains = additionalDomains.filter(
27712
+ (d) => !d.configSetName
27713
+ );
27714
+ if (unmigratedDomains.length === 0) {
27715
+ clack34.log.info(
27716
+ "All additional domains are already migrated to per-domain config sets."
27717
+ );
27718
+ clack34.outro(pc35.green("\u2713 Nothing to migrate"));
27719
+ trackServiceUpgrade("email", {
27720
+ action: "per-domain-config-sets",
27721
+ duration_ms: Date.now() - startTime
27722
+ });
27723
+ return;
27724
+ }
27725
+ clack34.log.info(
27726
+ `Migrating ${unmigratedDomains.length} domain(s) to per-domain configuration sets...`
27727
+ );
27728
+ const eventBusArn = `arn:aws:events:${region}:${accountId}:event-bus/default`;
27729
+ for (let i = 0; i < unmigratedDomains.length; i++) {
27730
+ const d = unmigratedDomains[i];
27731
+ const configSetName = domainToConfigSetName2(d.domain);
27732
+ clack34.log.step(
27733
+ `Migrating ${pc35.cyan(d.domain)} \u2192 ${pc35.dim(configSetName)}`
27734
+ );
27735
+ await progress.execute(
27736
+ `Creating config set for ${d.domain}`,
27737
+ async () => {
27738
+ await sesClient.send(
27739
+ new CreateConfigurationSetCommand2({
27740
+ ConfigurationSetName: configSetName,
27741
+ SuppressionOptions: {
27742
+ SuppressedReasons: ["BOUNCE", "COMPLAINT"]
27743
+ }
27744
+ })
27745
+ );
27746
+ }
27747
+ );
27748
+ const allEvents = [
27749
+ EventType.SEND,
27750
+ EventType.DELIVERY,
27751
+ EventType.OPEN,
27752
+ EventType.CLICK,
27753
+ EventType.BOUNCE,
27754
+ EventType.COMPLAINT,
27755
+ EventType.REJECT,
27756
+ EventType.RENDERING_FAILURE,
27757
+ EventType.DELIVERY_DELAY,
27758
+ EventType.SUBSCRIPTION
27759
+ ];
27760
+ const matchingEventTypes = d.trackingConfig != null ? allEvents.filter((evt) => {
27761
+ if (evt === EventType.OPEN) return d.trackingConfig.opens;
27762
+ if (evt === EventType.CLICK) return d.trackingConfig.clicks;
27763
+ return true;
27764
+ }) : [...allEvents];
27765
+ await progress.execute(
27766
+ `Adding EventBridge destination for ${d.domain}`,
27767
+ async () => {
27768
+ await sesClient.send(
27769
+ new CreateConfigurationSetEventDestinationCommand2({
27770
+ ConfigurationSetName: configSetName,
27771
+ EventDestinationName: "wraps-eventbridge",
27772
+ EventDestination: {
27773
+ Enabled: true,
27774
+ MatchingEventTypes: matchingEventTypes,
27775
+ EventBridgeDestination: { EventBusArn: eventBusArn }
27776
+ }
27777
+ })
27778
+ );
27779
+ }
27780
+ );
27781
+ d.configSetName = configSetName;
27782
+ await saveConnectionMetadata(metadata);
27783
+ try {
27784
+ await progress.execute(
27785
+ `Reassigning identity ${d.domain}`,
27786
+ async () => {
27787
+ await sesClient.send(
27788
+ new PutEmailIdentityConfigurationSetAttributesCommand({
27789
+ EmailIdentity: d.domain,
27790
+ ConfigurationSetName: configSetName
27791
+ })
27792
+ );
27793
+ }
27794
+ );
27795
+ } catch (err) {
27796
+ const msg = err instanceof Error ? err.message : String(err);
27797
+ clack34.log.warn(
27798
+ `Failed to reassign identity for ${pc35.cyan(d.domain)}: ${msg}`
27799
+ );
27800
+ clack34.log.info(
27801
+ pc35.dim(
27802
+ `Config set was saved \u2014 re-run to retry identity reassignment for ${d.domain}`
27803
+ )
27804
+ );
27805
+ }
27806
+ if (i < unmigratedDomains.length - 1) {
27807
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
27808
+ }
27809
+ }
27810
+ clack34.log.info(
27811
+ `For the primary domain (${pc35.cyan(config2.domain ?? "")}) config set rename: re-run ${pc35.cyan("wraps email upgrade")} with a Pulumi stack update.`
27812
+ );
27813
+ clack34.outro(
27814
+ pc35.green(
27815
+ `\u2713 Per-domain config sets migration complete (${unmigratedDomains.length} domain(s))`
27816
+ )
27817
+ );
27818
+ trackServiceUpgrade("email", {
27819
+ action: "per-domain-config-sets",
27820
+ duration_ms: Date.now() - startTime
27821
+ });
27822
+ return;
27823
+ }
27436
27824
  }
27437
27825
  const newCostData = calculateCosts(updatedConfig, 5e4);
27438
27826
  const costDiff = newCostData.total.monthly - currentCostData.total.monthly;
@@ -28813,7 +29201,7 @@ function assignPositions(steps, transitions) {
28813
29201
 
28814
29202
  // src/utils/email/workflow-ts.ts
28815
29203
  init_esm_shims();
28816
- import { createHash as createHash2 } from "crypto";
29204
+ import { createHash as createHash3 } from "crypto";
28817
29205
  import { existsSync as existsSync15 } from "fs";
28818
29206
  import { mkdir as mkdir8, readdir as readdir4, readFile as readFile8, writeFile as writeFile10 } from "fs/promises";
28819
29207
  import { basename, join as join17 } from "path";
@@ -28839,7 +29227,7 @@ async function discoverWorkflows(dir, filter) {
28839
29227
  async function parseWorkflowTs(filePath, wrapsDir) {
28840
29228
  const { build: build2 } = await import("esbuild");
28841
29229
  const source = await readFile8(filePath, "utf-8");
28842
- const sourceHash = createHash2("sha256").update(source).digest("hex");
29230
+ const sourceHash = createHash3("sha256").update(source).digest("hex");
28843
29231
  const slug = basename(filePath, ".ts");
28844
29232
  const shimDir = join17(wrapsDir, ".wraps", "_shims");
28845
29233
  await mkdir8(shimDir, { recursive: true });
@@ -30494,7 +30882,7 @@ import {
30494
30882
  IAMClient as IAMClient3,
30495
30883
  PutRolePolicyCommand
30496
30884
  } from "@aws-sdk/client-iam";
30497
- import { confirm as confirm18, intro as intro34, isCancel as isCancel25, log as log36, outro as outro20, select as select17 } from "@clack/prompts";
30885
+ import { confirm as confirm18, intro as intro34, isCancel as isCancel25, log as log36, outro as outro22, select as select17 } from "@clack/prompts";
30498
30886
  import * as pulumi24 from "@pulumi/pulumi";
30499
30887
  import pc41 from "picocolors";
30500
30888
  init_events();
@@ -30856,7 +31244,7 @@ async function resolveOrganization() {
30856
31244
  }))
30857
31245
  });
30858
31246
  if (isCancel25(selected)) {
30859
- outro20("Operation cancelled");
31247
+ outro22("Operation cancelled");
30860
31248
  process.exit(0);
30861
31249
  }
30862
31250
  return orgs.find((o) => o.id === selected) || null;
@@ -30922,7 +31310,7 @@ async function authenticatedConnect(token, options) {
30922
31310
  initialValue: true
30923
31311
  });
30924
31312
  if (isCancel25(enableTracking) || !enableTracking) {
30925
- outro20("Platform connection cancelled.");
31313
+ outro22("Platform connection cancelled.");
30926
31314
  process.exit(0);
30927
31315
  }
30928
31316
  metadata.services.email.config = {
@@ -31013,7 +31401,7 @@ You can try the manual flow: ${pc41.cyan("wraps auth logout")} then ${pc41.cyan(
31013
31401
  webhookConnected: true
31014
31402
  });
31015
31403
  } else {
31016
- outro20(pc41.green("Platform connection complete!"));
31404
+ outro22(pc41.green("Platform connection complete!"));
31017
31405
  console.log();
31018
31406
  console.log(
31019
31407
  pc41.dim(
@@ -31117,7 +31505,7 @@ Run ${pc41.cyan("wraps email init")} or ${pc41.cyan("wraps sms init")} first.
31117
31505
  initialValue: true
31118
31506
  });
31119
31507
  if (isCancel25(enableEventTracking) || !enableEventTracking) {
31120
- outro20("Platform connection cancelled.");
31508
+ outro22("Platform connection cancelled.");
31121
31509
  process.exit(0);
31122
31510
  }
31123
31511
  metadata.services.email.config = {
@@ -31165,7 +31553,7 @@ Run ${pc41.cyan("wraps email init")} or ${pc41.cyan("wraps sms init")} first.
31165
31553
  ]
31166
31554
  });
31167
31555
  if (isCancel25(action)) {
31168
- outro20("Operation cancelled");
31556
+ outro22("Operation cancelled");
31169
31557
  process.exit(0);
31170
31558
  }
31171
31559
  if (action === "keep") {
@@ -31176,7 +31564,7 @@ Run ${pc41.cyan("wraps email init")} or ${pc41.cyan("wraps sms init")} first.
31176
31564
  initialValue: false
31177
31565
  });
31178
31566
  if (isCancel25(confirmDisconnect) || !confirmDisconnect) {
31179
- outro20("Disconnect cancelled");
31567
+ outro22("Disconnect cancelled");
31180
31568
  process.exit(0);
31181
31569
  }
31182
31570
  metadata.services.email.webhookSecret = void 0;
@@ -31274,7 +31662,7 @@ Run ${pc41.cyan("wraps email init")} or ${pc41.cyan("wraps sms init")} first.
31274
31662
  }
31275
31663
  await saveConnectionMetadata(metadata);
31276
31664
  progress.stop();
31277
- outro20(pc41.green("Platform connection complete!"));
31665
+ outro22(pc41.green("Platform connection complete!"));
31278
31666
  if (webhookSecret && needsDeployment) {
31279
31667
  console.log(`
31280
31668
  ${pc41.bold("Webhook Secret")} ${pc41.dim("(save this!)")}`);
@@ -31387,7 +31775,7 @@ import {
31387
31775
  GetRoleCommand as GetRoleCommand2,
31388
31776
  IAMClient as IAMClient4
31389
31777
  } from "@aws-sdk/client-iam";
31390
- import { confirm as confirm19, intro as intro36, isCancel as isCancel26, log as log37, outro as outro21 } from "@clack/prompts";
31778
+ import { confirm as confirm19, intro as intro36, isCancel as isCancel26, log as log37, outro as outro23 } from "@clack/prompts";
31391
31779
  import pc43 from "picocolors";
31392
31780
  async function updateRole(options) {
31393
31781
  const startTime = Date.now();
@@ -31457,7 +31845,7 @@ Run ${pc43.cyan("wraps email init")} to deploy infrastructure first.
31457
31845
  initialValue: true
31458
31846
  });
31459
31847
  if (isCancel26(shouldContinue) || !shouldContinue) {
31460
- outro21(`${actionLabel} cancelled`);
31848
+ outro23(`${actionLabel} cancelled`);
31461
31849
  process.exit(0);
31462
31850
  }
31463
31851
  }
@@ -31537,7 +31925,7 @@ Run ${pc43.cyan("wraps email init")} to deploy infrastructure first.
31537
31925
  });
31538
31926
  return;
31539
31927
  }
31540
- outro21(pc43.green(`\u2713 Platform access role ${actionVerb} successfully`));
31928
+ outro23(pc43.green(`\u2713 Platform access role ${actionVerb} successfully`));
31541
31929
  console.log(`
31542
31930
  ${pc43.bold("Permissions:")}`);
31543
31931
  console.log(`
@@ -33096,6 +33484,7 @@ function createMetricsRouter(config2) {
33096
33484
 
33097
33485
  // src/console/routes/settings.ts
33098
33486
  init_esm_shims();
33487
+ init_config_set_slug();
33099
33488
  init_metadata();
33100
33489
  import dns4 from "dns/promises";
33101
33490
  import { Router as createRouter6 } from "express";
@@ -33232,8 +33621,8 @@ function createSettingsRouter(config2) {
33232
33621
  error: "No Wraps infrastructure found for this account and region"
33233
33622
  });
33234
33623
  }
33235
- const configSetName = "wraps-email-tracking";
33236
33624
  const domain = metadata.services.email?.config.domain;
33625
+ const configSetName = domainToConfigSetName(domain ?? "");
33237
33626
  const settings = await fetchEmailSettings(
33238
33627
  config2.roleArn,
33239
33628
  config2.region,
@@ -33335,7 +33724,9 @@ function createSettingsRouter(config2) {
33335
33724
  error: "No Wraps infrastructure found for this account and region"
33336
33725
  });
33337
33726
  }
33338
- const configSetName = "wraps-email-tracking";
33727
+ const configSetName = domainToConfigSetName(
33728
+ metadata.services.email?.config.domain ?? ""
33729
+ );
33339
33730
  console.log(
33340
33731
  `[Settings] Updating sending options for ${configSetName}: ${enabled}`
33341
33732
  );
@@ -33372,7 +33763,9 @@ function createSettingsRouter(config2) {
33372
33763
  error: "No Wraps infrastructure found for this account and region"
33373
33764
  });
33374
33765
  }
33375
- const configSetName = "wraps-email-tracking";
33766
+ const configSetName = domainToConfigSetName(
33767
+ metadata.services.email?.config.domain ?? ""
33768
+ );
33376
33769
  console.log(
33377
33770
  `[Settings] Updating reputation options for ${configSetName}: ${enabled}`
33378
33771
  );
@@ -33415,7 +33808,9 @@ function createSettingsRouter(config2) {
33415
33808
  error: "No Wraps infrastructure found for this account and region"
33416
33809
  });
33417
33810
  }
33418
- const configSetName = "wraps-email-tracking";
33811
+ const configSetName = domainToConfigSetName(
33812
+ metadata.services.email?.config.domain ?? ""
33813
+ );
33419
33814
  console.log(
33420
33815
  `[Settings] Updating tracking domain for ${configSetName}: ${domain}`
33421
33816
  );
@@ -36464,7 +36859,7 @@ ${pc48.yellow(pc48.bold("Important Notes:"))}`);
36464
36859
  clack45.log.warn(warning);
36465
36860
  }
36466
36861
  }
36467
- if (!options.yes) {
36862
+ if (!(options.yes || options.preview)) {
36468
36863
  const confirmed = await confirmDeploy();
36469
36864
  if (!confirmed) {
36470
36865
  clack45.cancel("Deployment cancelled.");
@@ -36477,43 +36872,77 @@ ${pc48.yellow(pc48.bold("Important Notes:"))}`);
36477
36872
  vercel: vercelConfig,
36478
36873
  smsConfig
36479
36874
  };
36875
+ const createStack = async () => {
36876
+ await ensurePulumiWorkDir({ accountId: identity.accountId, region });
36877
+ const stack = await pulumi29.automation.LocalWorkspace.createOrSelectStack(
36878
+ {
36879
+ stackName: `wraps-sms-${identity.accountId}-${region}`,
36880
+ projectName: "wraps-sms",
36881
+ program: async () => {
36882
+ const result = await deploySMSStack(stackConfig);
36883
+ return {
36884
+ roleArn: result.roleArn,
36885
+ phoneNumber: result.phoneNumber,
36886
+ phoneNumberArn: result.phoneNumberArn,
36887
+ configSetName: result.configSetName,
36888
+ tableName: result.tableName,
36889
+ region: result.region,
36890
+ lambdaFunctions: result.lambdaFunctions,
36891
+ snsTopicArn: result.snsTopicArn,
36892
+ queueUrl: result.queueUrl,
36893
+ dlqUrl: result.dlqUrl,
36894
+ optOutListArn: result.optOutListArn
36895
+ };
36896
+ }
36897
+ },
36898
+ {
36899
+ workDir: getPulumiWorkDir(),
36900
+ envVars: {
36901
+ PULUMI_CONFIG_PASSPHRASE: "",
36902
+ AWS_REGION: region
36903
+ },
36904
+ secretsProvider: "passphrase"
36905
+ }
36906
+ );
36907
+ await stack.setConfig("aws:region", { value: region });
36908
+ return stack;
36909
+ };
36910
+ if (options.preview) {
36911
+ try {
36912
+ const previewResult = await progress.execute(
36913
+ "Generating infrastructure preview",
36914
+ async () => {
36915
+ const stack = await createStack();
36916
+ return previewWithResourceChanges(stack, { diff: true });
36917
+ }
36918
+ );
36919
+ displayPreview({
36920
+ changeSummary: previewResult.changeSummary,
36921
+ resourceChanges: previewResult.resourceChanges,
36922
+ costEstimate: getSMSCostSummary(smsConfig, 0),
36923
+ commandName: "wraps sms init"
36924
+ });
36925
+ clack45.outro(
36926
+ pc48.green("Preview complete. Run without --preview to deploy.")
36927
+ );
36928
+ trackServiceInit("sms", true, {
36929
+ provider,
36930
+ region,
36931
+ preview: true,
36932
+ duration_ms: Date.now() - startTime
36933
+ });
36934
+ } catch (error) {
36935
+ trackError("PREVIEW_FAILED", "sms:init", { step: "preview" });
36936
+ throw error;
36937
+ }
36938
+ return;
36939
+ }
36480
36940
  let outputs;
36481
36941
  try {
36482
36942
  outputs = await progress.execute(
36483
36943
  "Deploying SMS infrastructure (this may take 2-3 minutes)",
36484
36944
  async () => {
36485
- await ensurePulumiWorkDir({ accountId: identity.accountId, region });
36486
- const stack = await pulumi29.automation.LocalWorkspace.createOrSelectStack(
36487
- {
36488
- stackName: `wraps-sms-${identity.accountId}-${region}`,
36489
- projectName: "wraps-sms",
36490
- program: async () => {
36491
- const result = await deploySMSStack(stackConfig);
36492
- return {
36493
- roleArn: result.roleArn,
36494
- phoneNumber: result.phoneNumber,
36495
- phoneNumberArn: result.phoneNumberArn,
36496
- configSetName: result.configSetName,
36497
- tableName: result.tableName,
36498
- region: result.region,
36499
- lambdaFunctions: result.lambdaFunctions,
36500
- snsTopicArn: result.snsTopicArn,
36501
- queueUrl: result.queueUrl,
36502
- dlqUrl: result.dlqUrl,
36503
- optOutListArn: result.optOutListArn
36504
- };
36505
- }
36506
- },
36507
- {
36508
- workDir: getPulumiWorkDir(),
36509
- envVars: {
36510
- PULUMI_CONFIG_PASSPHRASE: "",
36511
- AWS_REGION: region
36512
- },
36513
- secretsProvider: "passphrase"
36514
- }
36515
- );
36516
- await stack.setConfig("aws:region", { value: region });
36945
+ const stack = await createStack();
36517
36946
  const upResult = await withLockRetry(
36518
36947
  () => stack.up({ onOutput: console.log }),
36519
36948
  { accountId: identity.accountId, region, autoConfirm: options.yes }
@@ -38180,7 +38609,7 @@ ${pc53.bold("Cost Impact:")}`);
38180
38609
  );
38181
38610
  }
38182
38611
  console.log("");
38183
- if (!options.yes) {
38612
+ if (!(options.yes || options.preview)) {
38184
38613
  const confirmed = await clack50.confirm({
38185
38614
  message: "Proceed with upgrade?",
38186
38615
  initialValue: true
@@ -38202,46 +38631,79 @@ ${pc53.bold("Cost Impact:")}`);
38202
38631
  vercel: vercelConfig,
38203
38632
  smsConfig: updatedConfig
38204
38633
  };
38634
+ const stackName = metadata.services.sms?.pulumiStackName || `wraps-sms-${identity.accountId}-${region}`;
38635
+ const createStack = async () => {
38636
+ await ensurePulumiWorkDir({ accountId: identity.accountId, region });
38637
+ const stack = await pulumi32.automation.LocalWorkspace.createOrSelectStack(
38638
+ {
38639
+ stackName,
38640
+ projectName: "wraps-sms",
38641
+ program: async () => {
38642
+ const result = await deploySMSStack(stackConfig);
38643
+ return {
38644
+ roleArn: result.roleArn,
38645
+ phoneNumber: result.phoneNumber,
38646
+ phoneNumberArn: result.phoneNumberArn,
38647
+ configSetName: result.configSetName,
38648
+ tableName: result.tableName,
38649
+ region: result.region,
38650
+ lambdaFunctions: result.lambdaFunctions,
38651
+ snsTopicArn: result.snsTopicArn,
38652
+ queueUrl: result.queueUrl,
38653
+ dlqUrl: result.dlqUrl,
38654
+ optOutListArn: result.optOutListArn
38655
+ };
38656
+ }
38657
+ },
38658
+ {
38659
+ workDir: getPulumiWorkDir(),
38660
+ envVars: {
38661
+ PULUMI_CONFIG_PASSPHRASE: "",
38662
+ AWS_REGION: region
38663
+ },
38664
+ secretsProvider: "passphrase"
38665
+ }
38666
+ );
38667
+ await stack.workspace.selectStack(stackName);
38668
+ await stack.setConfig("aws:region", { value: region });
38669
+ return stack;
38670
+ };
38671
+ if (options.preview) {
38672
+ try {
38673
+ const previewResult = await progress.execute(
38674
+ "Generating infrastructure preview",
38675
+ async () => {
38676
+ const stack = await createStack();
38677
+ await stack.refresh({ onOutput: () => {
38678
+ } });
38679
+ return previewWithResourceChanges(stack, { diff: true });
38680
+ }
38681
+ );
38682
+ displayPreview({
38683
+ changeSummary: previewResult.changeSummary,
38684
+ resourceChanges: previewResult.resourceChanges,
38685
+ commandName: "wraps sms upgrade"
38686
+ });
38687
+ clack50.outro(
38688
+ pc53.green("Preview complete. Run without --preview to upgrade.")
38689
+ );
38690
+ trackServiceUpgrade("sms", {
38691
+ region,
38692
+ preview: true,
38693
+ duration_ms: Date.now() - startTime
38694
+ });
38695
+ } catch (error) {
38696
+ trackError("PREVIEW_FAILED", "sms:upgrade", { step: "preview" });
38697
+ throw error;
38698
+ }
38699
+ return;
38700
+ }
38205
38701
  let outputs;
38206
38702
  try {
38207
38703
  outputs = await progress.execute(
38208
38704
  "Updating SMS infrastructure (this may take 2-3 minutes)",
38209
38705
  async () => {
38210
- await ensurePulumiWorkDir({ accountId: identity.accountId, region });
38211
- const stack = await pulumi32.automation.LocalWorkspace.createOrSelectStack(
38212
- {
38213
- stackName: metadata.services.sms?.pulumiStackName || `wraps-sms-${identity.accountId}-${region}`,
38214
- projectName: "wraps-sms",
38215
- program: async () => {
38216
- const result = await deploySMSStack(stackConfig);
38217
- return {
38218
- roleArn: result.roleArn,
38219
- phoneNumber: result.phoneNumber,
38220
- phoneNumberArn: result.phoneNumberArn,
38221
- configSetName: result.configSetName,
38222
- tableName: result.tableName,
38223
- region: result.region,
38224
- lambdaFunctions: result.lambdaFunctions,
38225
- snsTopicArn: result.snsTopicArn,
38226
- queueUrl: result.queueUrl,
38227
- dlqUrl: result.dlqUrl,
38228
- optOutListArn: result.optOutListArn
38229
- };
38230
- }
38231
- },
38232
- {
38233
- workDir: getPulumiWorkDir(),
38234
- envVars: {
38235
- PULUMI_CONFIG_PASSPHRASE: "",
38236
- AWS_REGION: region
38237
- },
38238
- secretsProvider: "passphrase"
38239
- }
38240
- );
38241
- await stack.workspace.selectStack(
38242
- metadata.services.sms?.pulumiStackName || `wraps-sms-${identity.accountId}-${region}`
38243
- );
38244
- await stack.setConfig("aws:region", { value: region });
38706
+ const stack = await createStack();
38245
38707
  await stack.refresh({ onOutput: () => {
38246
38708
  } });
38247
38709
  const upResult = await stack.up({ onOutput: () => {
@@ -39567,7 +40029,8 @@ if (!primaryCommand) {
39567
40029
  provider: flags.provider,
39568
40030
  region: flags.region,
39569
40031
  preset: flags.preset,
39570
- yes: flags.yes
40032
+ yes: flags.yes,
40033
+ preview: flags.preview
39571
40034
  });
39572
40035
  break;
39573
40036
  case "cdn-init":
@@ -39752,6 +40215,7 @@ Usage: ${pc59.cyan("wraps email verify --domain yourapp.com")}
39752
40215
  await inboundDestroy({
39753
40216
  region: flags.region,
39754
40217
  force: flags.force,
40218
+ preview: flags.preview,
39755
40219
  json: flags.json
39756
40220
  });
39757
40221
  break;
@@ -39816,6 +40280,7 @@ Available commands: ${pc59.cyan("init")}, ${pc59.cyan("destroy")}, ${pc59.cyan("
39816
40280
  domain: flags.domain,
39817
40281
  all: flags.all,
39818
40282
  yes: flags.yes,
40283
+ preview: flags.preview,
39819
40284
  json: flags.json
39820
40285
  });
39821
40286
  break;
@@ -39839,6 +40304,7 @@ Available commands: ${pc59.cyan("init")}, ${pc59.cyan("destroy")}, ${pc59.cyan("
39839
40304
  domain: flags.domain,
39840
40305
  all: flags.all,
39841
40306
  force: flags.force,
40307
+ preview: flags.preview,
39842
40308
  json: flags.json
39843
40309
  });
39844
40310
  break;
@@ -40065,6 +40531,7 @@ Run ${pc59.cyan("wraps --help")} for available commands.
40065
40531
  region: flags.region,
40066
40532
  preset: flags.preset,
40067
40533
  yes: flags.yes,
40534
+ preview: flags.preview,
40068
40535
  json: flags.json
40069
40536
  });
40070
40537
  break;
@@ -40087,6 +40554,7 @@ Run ${pc59.cyan("wraps --help")} for available commands.
40087
40554
  await smsUpgrade({
40088
40555
  region: flags.region,
40089
40556
  yes: flags.yes,
40557
+ preview: flags.preview,
40090
40558
  json: flags.json
40091
40559
  });
40092
40560
  break;