@wraps.dev/cli 2.11.4 → 2.11.6

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
@@ -1133,9 +1133,11 @@ var init_aws_detection = __esm({
1133
1133
  var errors_exports = {};
1134
1134
  __export(errors_exports, {
1135
1135
  WrapsError: () => WrapsError,
1136
+ classifyDNSError: () => classifyDNSError,
1136
1137
  errors: () => errors,
1137
1138
  handleCLIError: () => handleCLIError,
1138
1139
  isAWSError: () => isAWSError,
1140
+ isAWSNotFoundError: () => isAWSNotFoundError,
1139
1141
  isPulumiError: () => isPulumiError,
1140
1142
  parseAWSError: () => parseAWSError,
1141
1143
  parsePulumiError: () => parsePulumiError,
@@ -1162,6 +1164,23 @@ function isAWSError(error) {
1162
1164
  ];
1163
1165
  return awsErrorNames.includes(error.name) || "$metadata" in error;
1164
1166
  }
1167
+ function classifyDNSError(error) {
1168
+ if (!(error instanceof Error)) {
1169
+ return "unknown";
1170
+ }
1171
+ const code = error.code;
1172
+ if (code === "ENOTFOUND" || code === "ENODATA") {
1173
+ return "missing";
1174
+ }
1175
+ if (code === "ETIMEOUT" || code === "ESERVFAIL" || code === "ECONNREFUSED") {
1176
+ return "network";
1177
+ }
1178
+ return "unknown";
1179
+ }
1180
+ function isAWSNotFoundError(error) {
1181
+ if (!(error instanceof Error)) return false;
1182
+ return error.name === "NotFoundException" || error.name === "NoSuchEntityException" || error.name === "NoSuchEntity" || error.name === "ResourceNotFoundException" || error.$metadata?.httpStatusCode === 404;
1183
+ }
1165
1184
  function isPulumiError(error) {
1166
1185
  if (!(error instanceof Error)) {
1167
1186
  return false;
@@ -4444,6 +4463,100 @@ var init_metadata = __esm({
4444
4463
  }
4445
4464
  });
4446
4465
 
4466
+ // src/infrastructure/shared/resource-checks.ts
4467
+ async function roleExists(roleName) {
4468
+ try {
4469
+ const { IAMClient: IAMClient4, GetRoleCommand: GetRoleCommand3 } = await import("@aws-sdk/client-iam");
4470
+ const iam10 = new IAMClient4({
4471
+ region: process.env.AWS_REGION || "us-east-1"
4472
+ });
4473
+ await iam10.send(new GetRoleCommand3({ RoleName: roleName }));
4474
+ return true;
4475
+ } catch (error) {
4476
+ if (error instanceof Error && (error.name === "NoSuchEntityException" || error.Code === "NoSuchEntity" || error.Error?.Code === "NoSuchEntity")) {
4477
+ return false;
4478
+ }
4479
+ console.error("Error checking for existing IAM role:", error);
4480
+ return false;
4481
+ }
4482
+ }
4483
+ async function tableExists(tableName) {
4484
+ try {
4485
+ const { DynamoDBClient: DynamoDBClient6, DescribeTableCommand: DescribeTableCommand2 } = await import("@aws-sdk/client-dynamodb");
4486
+ const dynamodb3 = new DynamoDBClient6({
4487
+ region: process.env.AWS_REGION || "us-east-1"
4488
+ });
4489
+ await dynamodb3.send(new DescribeTableCommand2({ TableName: tableName }));
4490
+ return true;
4491
+ } catch (error) {
4492
+ if (error instanceof Error && error.name === "ResourceNotFoundException") {
4493
+ return false;
4494
+ }
4495
+ console.error("Error checking for existing DynamoDB table:", error);
4496
+ return false;
4497
+ }
4498
+ }
4499
+ async function sqsQueueExists(queueName) {
4500
+ try {
4501
+ const { SQSClient, GetQueueUrlCommand } = await import("@aws-sdk/client-sqs");
4502
+ const sqs5 = new SQSClient({
4503
+ region: process.env.AWS_REGION || "us-east-1"
4504
+ });
4505
+ const response = await sqs5.send(
4506
+ new GetQueueUrlCommand({ QueueName: queueName })
4507
+ );
4508
+ return response.QueueUrl || null;
4509
+ } catch {
4510
+ return null;
4511
+ }
4512
+ }
4513
+ async function snsTopicExists(topicName) {
4514
+ try {
4515
+ const { SNSClient: SNSClient2, ListTopicsCommand: ListTopicsCommand2 } = await import("@aws-sdk/client-sns");
4516
+ const sns3 = new SNSClient2({
4517
+ region: process.env.AWS_REGION || "us-east-1"
4518
+ });
4519
+ let nextToken;
4520
+ do {
4521
+ const response = await sns3.send(
4522
+ new ListTopicsCommand2({ NextToken: nextToken })
4523
+ );
4524
+ const found = response.Topics?.find(
4525
+ (t) => t.TopicArn?.endsWith(`:${topicName}`)
4526
+ );
4527
+ if (found?.TopicArn) {
4528
+ return found.TopicArn;
4529
+ }
4530
+ nextToken = response.NextToken;
4531
+ } while (nextToken);
4532
+ return null;
4533
+ } catch {
4534
+ return null;
4535
+ }
4536
+ }
4537
+ async function lambdaFunctionExists(functionName) {
4538
+ try {
4539
+ const { LambdaClient: LambdaClient2, GetFunctionCommand } = await import("@aws-sdk/client-lambda");
4540
+ const lambda4 = new LambdaClient2({
4541
+ region: process.env.AWS_REGION || "us-east-1"
4542
+ });
4543
+ await lambda4.send(new GetFunctionCommand({ FunctionName: functionName }));
4544
+ return true;
4545
+ } catch (error) {
4546
+ if (error instanceof Error && error.name === "ResourceNotFoundException") {
4547
+ return false;
4548
+ }
4549
+ console.error("Error checking for existing Lambda function:", error);
4550
+ return false;
4551
+ }
4552
+ }
4553
+ var init_resource_checks = __esm({
4554
+ "src/infrastructure/shared/resource-checks.ts"() {
4555
+ "use strict";
4556
+ init_esm_shims();
4557
+ }
4558
+ });
4559
+
4447
4560
  // ../core/dist/index.js
4448
4561
  var dist_exports = {};
4449
4562
  __export(dist_exports, {
@@ -4661,22 +4774,6 @@ function getPackageRoot() {
4661
4774
  }
4662
4775
  throw new Error("Could not find package.json");
4663
4776
  }
4664
- async function lambdaFunctionExists(functionName) {
4665
- try {
4666
- const { LambdaClient: LambdaClient2, GetFunctionCommand } = await import("@aws-sdk/client-lambda");
4667
- const lambda4 = new LambdaClient2({
4668
- region: process.env.AWS_REGION || "us-east-1"
4669
- });
4670
- await lambda4.send(new GetFunctionCommand({ FunctionName: functionName }));
4671
- return true;
4672
- } catch (error) {
4673
- if (error.name === "ResourceNotFoundException") {
4674
- return false;
4675
- }
4676
- console.error("Error checking for existing Lambda function:", error);
4677
- return false;
4678
- }
4679
- }
4680
4777
  async function findEventSourceMapping(functionName, queueArn) {
4681
4778
  try {
4682
4779
  const { LambdaClient: LambdaClient2, ListEventSourceMappingsCommand } = await import("@aws-sdk/client-lambda");
@@ -4873,6 +4970,7 @@ var init_lambda = __esm({
4873
4970
  "src/infrastructure/resources/lambda.ts"() {
4874
4971
  "use strict";
4875
4972
  init_esm_shims();
4973
+ init_resource_checks();
4876
4974
  nodeBuiltins = builtinModules.flatMap((m) => [m, `node:${m}`]);
4877
4975
  }
4878
4976
  });
@@ -7334,7 +7432,11 @@ async function login(options) {
7334
7432
  client_id: "wraps-cli"
7335
7433
  });
7336
7434
  if (codeError || !codeData) {
7337
- trackCommand("auth:login", { success: false, duration_ms: Date.now() - startTime, method: "device" });
7435
+ trackCommand("auth:login", {
7436
+ success: false,
7437
+ duration_ms: Date.now() - startTime,
7438
+ method: "device"
7439
+ });
7338
7440
  trackError("DEVICE_AUTH_FAILED", "auth:login", { step: "request_code" });
7339
7441
  clack.log.error("Failed to start device authorization.");
7340
7442
  process.exit(1);
@@ -7410,7 +7512,11 @@ async function login(options) {
7410
7512
  continue;
7411
7513
  }
7412
7514
  if (errorCode === "access_denied") {
7413
- trackCommand("auth:login", { success: false, duration_ms: Date.now() - startTime, method: "device" });
7515
+ trackCommand("auth:login", {
7516
+ success: false,
7517
+ duration_ms: Date.now() - startTime,
7518
+ method: "device"
7519
+ });
7414
7520
  trackError("ACCESS_DENIED", "auth:login", { step: "poll_token" });
7415
7521
  spinner8.stop("Denied.");
7416
7522
  clack.log.error("Authorization was denied.");
@@ -7421,7 +7527,11 @@ async function login(options) {
7421
7527
  }
7422
7528
  }
7423
7529
  }
7424
- trackCommand("auth:login", { success: false, duration_ms: Date.now() - startTime, method: "device" });
7530
+ trackCommand("auth:login", {
7531
+ success: false,
7532
+ duration_ms: Date.now() - startTime,
7533
+ method: "device"
7534
+ });
7425
7535
  trackError("DEVICE_CODE_EXPIRED", "auth:login", { step: "poll_token" });
7426
7536
  spinner8.stop("Expired.");
7427
7537
  clack.log.error("Device code expired. Run `wraps auth login` to try again.");
@@ -9134,12 +9244,13 @@ async function cdnDestroy(options) {
9134
9244
  return;
9135
9245
  } catch (error) {
9136
9246
  progress.stop();
9137
- if (error.message.includes("No CDN infrastructure found")) {
9247
+ const msg = error instanceof Error ? error.message : String(error);
9248
+ if (msg.includes("No CDN infrastructure found")) {
9138
9249
  clack9.log.warn("No CDN infrastructure found to preview");
9139
9250
  process.exit(0);
9140
9251
  }
9141
9252
  trackError("PREVIEW_FAILED", "storage destroy", { step: "preview" });
9142
- throw new Error(`Preview failed: ${error.message}`);
9253
+ throw new Error(`Preview failed: ${msg}`);
9143
9254
  }
9144
9255
  }
9145
9256
  if (shouldCleanDNS && hostedZone && customDomain) {
@@ -9151,7 +9262,8 @@ async function cdnDestroy(options) {
9151
9262
  }
9152
9263
  );
9153
9264
  } catch (error) {
9154
- clack9.log.warn(`Could not delete DNS records: ${error.message}`);
9265
+ const msg = error instanceof Error ? error.message : String(error);
9266
+ clack9.log.warn(`Could not delete DNS records: ${msg}`);
9155
9267
  clack9.log.info("You may need to delete them manually from Route53");
9156
9268
  }
9157
9269
  }
@@ -9164,7 +9276,8 @@ async function cdnDestroy(options) {
9164
9276
  }
9165
9277
  );
9166
9278
  } catch (error) {
9167
- clack9.log.info(`Note: ${error.message}`);
9279
+ const msg = error instanceof Error ? error.message : String(error);
9280
+ clack9.log.info(`Note: ${msg}`);
9168
9281
  }
9169
9282
  try {
9170
9283
  await progress.execute(
@@ -9188,7 +9301,8 @@ async function cdnDestroy(options) {
9188
9301
  );
9189
9302
  } catch (error) {
9190
9303
  progress.stop();
9191
- if (error.message.includes("No CDN infrastructure found")) {
9304
+ const msg = error instanceof Error ? error.message : String(error);
9305
+ if (msg.includes("No CDN infrastructure found")) {
9192
9306
  clack9.log.warn("No CDN infrastructure found");
9193
9307
  if (metadata) {
9194
9308
  removeServiceFromConnection(metadata, "cdn");
@@ -9196,7 +9310,7 @@ async function cdnDestroy(options) {
9196
9310
  }
9197
9311
  process.exit(0);
9198
9312
  }
9199
- if (error.message?.includes("stack is currently locked")) {
9313
+ if (msg.includes("stack is currently locked")) {
9200
9314
  trackError("STACK_LOCKED", "storage destroy", { step: "destroy" });
9201
9315
  throw errors.stackLocked();
9202
9316
  }
@@ -9635,6 +9749,9 @@ async function createCdnACMCertificate(config2) {
9635
9749
  };
9636
9750
  }
9637
9751
 
9752
+ // src/infrastructure/cdn-stack.ts
9753
+ init_resource_checks();
9754
+
9638
9755
  // src/infrastructure/vercel-oidc.ts
9639
9756
  init_esm_shims();
9640
9757
  import * as aws2 from "@pulumi/aws";
@@ -9696,22 +9813,6 @@ async function createVercelOIDC(config2) {
9696
9813
  }
9697
9814
 
9698
9815
  // src/infrastructure/cdn-stack.ts
9699
- async function roleExists(roleName) {
9700
- try {
9701
- const { IAMClient: IAMClient4, GetRoleCommand: GetRoleCommand3 } = await import("@aws-sdk/client-iam");
9702
- const iam10 = new IAMClient4({
9703
- region: process.env.AWS_REGION || "us-east-1"
9704
- });
9705
- await iam10.send(new GetRoleCommand3({ RoleName: roleName }));
9706
- return true;
9707
- } catch (error) {
9708
- if (error.name === "NoSuchEntityException" || error.Code === "NoSuchEntity" || error.Error?.Code === "NoSuchEntity") {
9709
- return false;
9710
- }
9711
- console.error("Error checking for existing IAM role:", error);
9712
- return false;
9713
- }
9714
- }
9715
9816
  async function createCdnIAMRole(config2) {
9716
9817
  let assumeRolePolicy;
9717
9818
  if (config2.provider === "vercel" && config2.oidcProvider) {
@@ -10480,10 +10581,11 @@ ${pc11.yellow(pc11.bold("Configuration Notes:"))}`);
10480
10581
  return;
10481
10582
  } catch (error) {
10482
10583
  trackError("PREVIEW_FAILED", "storage:init", { step: "preview" });
10483
- if (error.message?.includes("stack is currently locked")) {
10584
+ const msg = error instanceof Error ? error.message : String(error);
10585
+ if (msg.includes("stack is currently locked")) {
10484
10586
  throw errors.stackLocked();
10485
10587
  }
10486
- throw new Error(`Preview failed: ${error.message}`);
10588
+ throw new Error(`Preview failed: ${msg}`);
10487
10589
  }
10488
10590
  }
10489
10591
  let outputs;
@@ -10547,18 +10649,19 @@ ${pc11.yellow(pc11.bold("Configuration Notes:"))}`);
10547
10649
  }
10548
10650
  );
10549
10651
  } catch (error) {
10652
+ const msg = error instanceof Error ? error.message : String(error);
10550
10653
  trackServiceInit("cdn", false, {
10551
10654
  preset,
10552
10655
  provider,
10553
10656
  region,
10554
10657
  duration_ms: Date.now() - startTime
10555
10658
  });
10556
- if (error.message?.includes("stack is currently locked")) {
10659
+ if (msg.includes("stack is currently locked")) {
10557
10660
  trackError("STACK_LOCKED", "storage:init", { step: "deploy" });
10558
10661
  throw errors.stackLocked();
10559
10662
  }
10560
10663
  trackError("DEPLOYMENT_FAILED", "storage:init", { step: "deploy" });
10561
- throw new Error(`Pulumi deployment failed: ${error.message}`);
10664
+ throw new Error(`Pulumi deployment failed: ${msg}`);
10562
10665
  }
10563
10666
  if (metadata.services.cdn) {
10564
10667
  metadata.services.cdn.pulumiStackName = `wraps-cdn-${identity.accountId}-${region}`;
@@ -10691,7 +10794,8 @@ ${pc11.yellow(pc11.bold("Configuration Notes:"))}`);
10691
10794
  }
10692
10795
  } catch (error) {
10693
10796
  progress.stop();
10694
- clack10.log.warn(`Could not manage DNS records: ${error.message}`);
10797
+ const msg = error instanceof Error ? error.message : String(error);
10798
+ clack10.log.warn(`Could not manage DNS records: ${msg}`);
10695
10799
  }
10696
10800
  }
10697
10801
  }
@@ -11115,13 +11219,14 @@ async function cdnSync(options) {
11115
11219
  return result.summary;
11116
11220
  });
11117
11221
  } catch (error) {
11222
+ const msg = error instanceof Error ? error.message : String(error);
11118
11223
  trackCommand("storage:sync", {
11119
11224
  success: false,
11120
- error: error.message,
11225
+ error: msg,
11121
11226
  region,
11122
11227
  duration_ms: Date.now() - startTime
11123
11228
  });
11124
- clack12.log.error(`Sync failed: ${error.message}`);
11229
+ clack12.log.error(`Sync failed: ${msg}`);
11125
11230
  process.exit(1);
11126
11231
  }
11127
11232
  clack12.log.success(pc13.green("CDN infrastructure synced!"));
@@ -11245,7 +11350,8 @@ Current configuration:
11245
11350
  certStatus = certResponse.Certificate?.Status || "UNKNOWN";
11246
11351
  } catch (error) {
11247
11352
  progress.fail("Failed to check certificate status");
11248
- clack13.log.error(`Error: ${error.message}`);
11353
+ const msg = error instanceof Error ? error.message : String(error);
11354
+ clack13.log.error(`Error: ${msg}`);
11249
11355
  process.exit(1);
11250
11356
  }
11251
11357
  progress.stop();
@@ -11369,13 +11475,14 @@ Ready to add custom domain: ${pc14.cyan(pendingDomain)}`);
11369
11475
  }
11370
11476
  );
11371
11477
  } catch (error) {
11478
+ const msg = error instanceof Error ? error.message : String(error);
11372
11479
  trackCommand("storage:upgrade", {
11373
11480
  success: false,
11374
- error: error.message,
11481
+ error: msg,
11375
11482
  region,
11376
11483
  duration_ms: Date.now() - startTime
11377
11484
  });
11378
- clack13.log.error(`Upgrade failed: ${error.message}`);
11485
+ clack13.log.error(`Upgrade failed: ${msg}`);
11379
11486
  process.exit(1);
11380
11487
  }
11381
11488
  if (metadata.services.cdn) {
@@ -14224,16 +14331,17 @@ async function check(options) {
14224
14331
  process.exit(getExitCode(result.score.grade));
14225
14332
  } catch (error) {
14226
14333
  spinner8?.stop("Check failed");
14334
+ const msg = error instanceof Error ? error.message : String(error);
14227
14335
  if (options.json) {
14228
- console.log(JSON.stringify({ error: error.message }));
14336
+ console.log(JSON.stringify({ error: msg }));
14229
14337
  } else {
14230
- clack15.log.error(error.message);
14338
+ clack15.log.error(msg);
14231
14339
  }
14232
14340
  const duration = Date.now() - startTime;
14233
14341
  trackCommand("email:check", {
14234
14342
  success: false,
14235
14343
  duration_ms: duration,
14236
- error: error.message
14344
+ error: msg
14237
14345
  });
14238
14346
  process.exit(4);
14239
14347
  }
@@ -14843,23 +14951,8 @@ async function createAlertingResources(config2) {
14843
14951
 
14844
14952
  // src/infrastructure/resources/dynamodb.ts
14845
14953
  init_esm_shims();
14954
+ init_resource_checks();
14846
14955
  import * as aws5 from "@pulumi/aws";
14847
- async function tableExists(tableName) {
14848
- try {
14849
- const { DynamoDBClient: DynamoDBClient6, DescribeTableCommand: DescribeTableCommand2 } = await import("@aws-sdk/client-dynamodb");
14850
- const dynamodb3 = new DynamoDBClient6({
14851
- region: process.env.AWS_REGION || "us-east-1"
14852
- });
14853
- await dynamodb3.send(new DescribeTableCommand2({ TableName: tableName }));
14854
- return true;
14855
- } catch (error) {
14856
- if (error.name === "ResourceNotFoundException") {
14857
- return false;
14858
- }
14859
- console.error("Error checking for existing DynamoDB table:", error);
14860
- return false;
14861
- }
14862
- }
14863
14956
  async function createDynamoDBTables(_config) {
14864
14957
  const tableName = "wraps-email-history";
14865
14958
  const exists = await tableExists(tableName);
@@ -15056,24 +15149,9 @@ async function createEventBridgeResources(config2) {
15056
15149
 
15057
15150
  // src/infrastructure/resources/iam.ts
15058
15151
  init_esm_shims();
15152
+ init_resource_checks();
15059
15153
  import * as aws7 from "@pulumi/aws";
15060
15154
  import * as pulumi10 from "@pulumi/pulumi";
15061
- async function roleExists2(roleName) {
15062
- try {
15063
- const { IAMClient: IAMClient4, GetRoleCommand: GetRoleCommand3 } = await import("@aws-sdk/client-iam");
15064
- const iam10 = new IAMClient4({
15065
- region: process.env.AWS_REGION || "us-east-1"
15066
- });
15067
- await iam10.send(new GetRoleCommand3({ RoleName: roleName }));
15068
- return true;
15069
- } catch (error) {
15070
- if (error.name === "NoSuchEntityException" || error.Code === "NoSuchEntity" || error.Error?.Code === "NoSuchEntity") {
15071
- return false;
15072
- }
15073
- console.error("Error checking for existing IAM role:", error);
15074
- return false;
15075
- }
15076
- }
15077
15155
  async function createIAMRole(config2) {
15078
15156
  let assumeRolePolicy;
15079
15157
  if (config2.provider === "vercel" && config2.oidcProvider) {
@@ -15110,7 +15188,7 @@ async function createIAMRole(config2) {
15110
15188
  throw new Error("Other providers not yet implemented");
15111
15189
  }
15112
15190
  const roleName = "wraps-email-role";
15113
- const exists = await roleExists2(roleName);
15191
+ const exists = await roleExists(roleName);
15114
15192
  const role = exists ? new aws7.iam.Role(
15115
15193
  roleName,
15116
15194
  {
@@ -15955,10 +16033,11 @@ ${pc17.bold("Current Configuration:")}
15955
16033
  return;
15956
16034
  } catch (error) {
15957
16035
  trackError("PREVIEW_FAILED", "email:config", { step: "preview" });
15958
- if (error.message?.includes("stack is currently locked")) {
16036
+ const msg = error instanceof Error ? error.message : String(error);
16037
+ if (msg.includes("stack is currently locked")) {
15959
16038
  throw errors.stackLocked();
15960
16039
  }
15961
- throw new Error(`Preview failed: ${error.message}`);
16040
+ throw new Error(`Preview failed: ${msg}`);
15962
16041
  }
15963
16042
  }
15964
16043
  let outputs;
@@ -16016,16 +16095,17 @@ ${pc17.bold("Current Configuration:")}
16016
16095
  }
16017
16096
  );
16018
16097
  } catch (error) {
16098
+ const msg = error instanceof Error ? error.message : String(error);
16019
16099
  trackCommand("email:config", {
16020
16100
  success: false,
16021
16101
  duration_ms: Date.now() - startTime
16022
16102
  });
16023
- if (error.message?.includes("stack is currently locked")) {
16103
+ if (msg.includes("stack is currently locked")) {
16024
16104
  trackError("STACK_LOCKED", "email:config", { step: "update" });
16025
16105
  throw errors.stackLocked();
16026
16106
  }
16027
16107
  trackError("UPDATE_FAILED", "email:config", { step: "update" });
16028
- throw new Error(`Pulumi update failed: ${error.message}`);
16108
+ throw new Error(`Pulumi update failed: ${msg}`);
16029
16109
  }
16030
16110
  metadata.timestamp = (/* @__PURE__ */ new Date()).toISOString();
16031
16111
  await saveConnectionMetadata(metadata);
@@ -16456,10 +16536,11 @@ async function connect2(options) {
16456
16536
  return;
16457
16537
  } catch (error) {
16458
16538
  trackError("PREVIEW_FAILED", "email:connect", { step: "preview" });
16459
- if (error.message?.includes("stack is currently locked")) {
16539
+ const msg = error instanceof Error ? error.message : String(error);
16540
+ if (msg.includes("stack is currently locked")) {
16460
16541
  throw errors.stackLocked();
16461
16542
  }
16462
- throw new Error(`Preview failed: ${error.message}`);
16543
+ throw new Error(`Preview failed: ${msg}`);
16463
16544
  }
16464
16545
  }
16465
16546
  let outputs;
@@ -16515,17 +16596,18 @@ async function connect2(options) {
16515
16596
  }
16516
16597
  );
16517
16598
  } catch (error) {
16599
+ const msg = error instanceof Error ? error.message : String(error);
16518
16600
  trackServiceInit("email", false, {
16519
16601
  preset,
16520
16602
  provider,
16521
16603
  duration_ms: Date.now() - startTime
16522
16604
  });
16523
- if (error.message?.includes("stack is currently locked")) {
16605
+ if (msg.includes("stack is currently locked")) {
16524
16606
  trackError("STACK_LOCKED", "email:connect", { step: "deploy" });
16525
16607
  throw errors.stackLocked();
16526
16608
  }
16527
16609
  trackError("DEPLOYMENT_FAILED", "email:connect", { step: "deploy" });
16528
- throw new Error(`Pulumi deployment failed: ${error.message}`);
16610
+ throw new Error(`Pulumi deployment failed: ${msg}`);
16529
16611
  }
16530
16612
  if (outputs.domain && outputs.dkimTokens && outputs.dkimTokens.length > 0) {
16531
16613
  const { findHostedZone: findHostedZone2, createDNSRecords: createDNSRecords2 } = await Promise.resolve().then(() => (init_route53(), route53_exports));
@@ -16544,9 +16626,8 @@ async function connect2(options) {
16544
16626
  );
16545
16627
  progress.succeed("DNS records created in Route53");
16546
16628
  } catch (error) {
16547
- progress.fail(
16548
- `Failed to create DNS records automatically: ${error.message}`
16549
- );
16629
+ const msg = error instanceof Error ? error.message : String(error);
16630
+ progress.fail(`Failed to create DNS records automatically: ${msg}`);
16550
16631
  progress.info(
16551
16632
  "You can manually add the required DNS records shown below"
16552
16633
  );
@@ -16675,8 +16756,11 @@ async function getEmailIdentityInfo(domain, region) {
16675
16756
  dkimTokens: response.DkimAttributes?.Tokens || [],
16676
16757
  mailFromDomain: response.MailFromAttributes?.MailFromDomain
16677
16758
  };
16678
- } catch (_error) {
16679
- return { dkimTokens: [] };
16759
+ } catch (error) {
16760
+ if (isAWSError(error)) {
16761
+ return { dkimTokens: [] };
16762
+ }
16763
+ throw error;
16680
16764
  }
16681
16765
  }
16682
16766
  async function emailDestroy(options) {
@@ -16771,7 +16855,7 @@ async function emailDestroy(options) {
16771
16855
  stackName,
16772
16856
  workDir: getPulumiWorkDir()
16773
16857
  });
16774
- } catch (_error) {
16858
+ } catch {
16775
16859
  throw new Error("No email infrastructure found to preview");
16776
16860
  }
16777
16861
  const result = await previewWithResourceChanges(stack, {
@@ -16805,12 +16889,13 @@ async function emailDestroy(options) {
16805
16889
  return;
16806
16890
  } catch (error) {
16807
16891
  progress.stop();
16808
- if (error.message.includes("No email infrastructure found")) {
16892
+ const msg = error instanceof Error ? error.message : String(error);
16893
+ if (msg.includes("No email infrastructure found")) {
16809
16894
  clack18.log.warn("No email infrastructure found to preview");
16810
16895
  process.exit(0);
16811
16896
  }
16812
16897
  trackError("PREVIEW_FAILED", "email destroy", { step: "preview" });
16813
- throw new Error(`Preview failed: ${error.message}`);
16898
+ throw new Error(`Preview failed: ${msg}`);
16814
16899
  }
16815
16900
  }
16816
16901
  if (shouldCleanDNS && hostedZone && domain && dkimTokens.length > 0) {
@@ -16826,7 +16911,8 @@ async function emailDestroy(options) {
16826
16911
  );
16827
16912
  });
16828
16913
  } catch (error) {
16829
- clack18.log.warn(`Could not delete DNS records: ${error.message}`);
16914
+ const msg = error instanceof Error ? error.message : String(error);
16915
+ clack18.log.warn(`Could not delete DNS records: ${msg}`);
16830
16916
  clack18.log.info("You may need to delete them manually from Route53");
16831
16917
  }
16832
16918
  }
@@ -16842,7 +16928,7 @@ async function emailDestroy(options) {
16842
16928
  stackName,
16843
16929
  workDir: getPulumiWorkDir()
16844
16930
  });
16845
- } catch (_error) {
16931
+ } catch {
16846
16932
  throw new Error("No email infrastructure found to destroy");
16847
16933
  }
16848
16934
  await withTimeout(
@@ -16857,12 +16943,13 @@ async function emailDestroy(options) {
16857
16943
  );
16858
16944
  } catch (error) {
16859
16945
  progress.stop();
16860
- if (error.message.includes("No email infrastructure found")) {
16946
+ const msg = error instanceof Error ? error.message : String(error);
16947
+ if (msg.includes("No email infrastructure found")) {
16861
16948
  clack18.log.warn("No email infrastructure found");
16862
16949
  await deleteConnectionMetadata(identity.accountId, region);
16863
16950
  process.exit(0);
16864
16951
  }
16865
- if (error.message?.includes("stack is currently locked")) {
16952
+ if (msg.includes("stack is currently locked")) {
16866
16953
  trackError("STACK_LOCKED", "email destroy", { step: "destroy" });
16867
16954
  throw errors.stackLocked();
16868
16955
  }
@@ -16907,6 +16994,7 @@ init_client();
16907
16994
  init_events();
16908
16995
  init_dns();
16909
16996
  init_aws();
16997
+ init_errors();
16910
16998
  init_metadata();
16911
16999
  import { Resolver as Resolver2 } from "dns/promises";
16912
17000
  import { GetEmailIdentityCommand as GetEmailIdentityCommand2, SESv2Client as SESv2Client3 } from "@aws-sdk/client-sesv2";
@@ -16933,16 +17021,19 @@ async function verifyDomain(options) {
16933
17021
  );
16934
17022
  dkimTokens = identity.DkimAttributes?.Tokens || [];
16935
17023
  mailFromDomain = identity.MailFromAttributes?.MailFromDomain;
16936
- } catch (_error) {
16937
- progress.stop();
16938
- clack19.log.error(`Domain ${options.domain} not found in SES`);
16939
- console.log(
16940
- `
17024
+ } catch (error) {
17025
+ if (isAWSNotFoundError(error)) {
17026
+ progress.stop();
17027
+ clack19.log.error(`Domain ${options.domain} not found in SES`);
17028
+ console.log(
17029
+ `
16941
17030
  Run ${pc20.cyan(`wraps email init --domain ${options.domain}`)} to add this domain.
16942
17031
  `
16943
- );
16944
- process.exit(1);
16945
- return;
17032
+ );
17033
+ process.exit(1);
17034
+ return;
17035
+ }
17036
+ throw error;
16946
17037
  }
16947
17038
  const resolver = new Resolver2();
16948
17039
  resolver.setServers(["8.8.8.8", "1.1.1.1"]);
@@ -16959,12 +17050,24 @@ Run ${pc20.cyan(`wraps email init --domain ${options.domain}`)} to add this doma
16959
17050
  status: found ? "verified" : "incorrect",
16960
17051
  records
16961
17052
  });
16962
- } catch (_error) {
16963
- dnsResults.push({
16964
- name: dkimRecord,
16965
- type: "CNAME",
16966
- status: "missing"
16967
- });
17053
+ } catch (error) {
17054
+ const dnsClass = classifyDNSError(error);
17055
+ if (dnsClass === "missing") {
17056
+ dnsResults.push({
17057
+ name: dkimRecord,
17058
+ type: "CNAME",
17059
+ status: "missing"
17060
+ });
17061
+ } else if (dnsClass === "network") {
17062
+ dnsResults.push({
17063
+ name: dkimRecord,
17064
+ type: "CNAME",
17065
+ status: "missing",
17066
+ records: ["DNS lookup failed (network issue)"]
17067
+ });
17068
+ } else {
17069
+ throw error;
17070
+ }
16968
17071
  }
16969
17072
  }
16970
17073
  try {
@@ -16977,12 +17080,24 @@ Run ${pc20.cyan(`wraps email init --domain ${options.domain}`)} to add this doma
16977
17080
  status: hasAmazonSES ? "verified" : spfRecord ? "incorrect" : "missing",
16978
17081
  records: spfRecord ? [spfRecord] : void 0
16979
17082
  });
16980
- } catch (_error) {
16981
- dnsResults.push({
16982
- name: options.domain,
16983
- type: "TXT (SPF)",
16984
- status: "missing"
16985
- });
17083
+ } catch (error) {
17084
+ const dnsClass = classifyDNSError(error);
17085
+ if (dnsClass === "missing") {
17086
+ dnsResults.push({
17087
+ name: options.domain,
17088
+ type: "TXT (SPF)",
17089
+ status: "missing"
17090
+ });
17091
+ } else if (dnsClass === "network") {
17092
+ dnsResults.push({
17093
+ name: options.domain,
17094
+ type: "TXT (SPF)",
17095
+ status: "missing",
17096
+ records: ["DNS lookup failed (network issue)"]
17097
+ });
17098
+ } else {
17099
+ throw error;
17100
+ }
16986
17101
  }
16987
17102
  try {
16988
17103
  const records = await resolver.resolveTxt(`_dmarc.${options.domain}`);
@@ -16993,12 +17108,24 @@ Run ${pc20.cyan(`wraps email init --domain ${options.domain}`)} to add this doma
16993
17108
  status: dmarcRecord ? "verified" : "missing",
16994
17109
  records: dmarcRecord ? [dmarcRecord] : void 0
16995
17110
  });
16996
- } catch (_error) {
16997
- dnsResults.push({
16998
- name: `_dmarc.${options.domain}`,
16999
- type: "TXT (DMARC)",
17000
- status: "missing"
17001
- });
17111
+ } catch (error) {
17112
+ const dnsClass = classifyDNSError(error);
17113
+ if (dnsClass === "missing") {
17114
+ dnsResults.push({
17115
+ name: `_dmarc.${options.domain}`,
17116
+ type: "TXT (DMARC)",
17117
+ status: "missing"
17118
+ });
17119
+ } else if (dnsClass === "network") {
17120
+ dnsResults.push({
17121
+ name: `_dmarc.${options.domain}`,
17122
+ type: "TXT (DMARC)",
17123
+ status: "missing",
17124
+ records: ["DNS lookup failed (network issue)"]
17125
+ });
17126
+ } else {
17127
+ throw error;
17128
+ }
17002
17129
  }
17003
17130
  if (mailFromDomain) {
17004
17131
  try {
@@ -17013,12 +17140,24 @@ Run ${pc20.cyan(`wraps email init --domain ${options.domain}`)} to add this doma
17013
17140
  status: hasMx ? "verified" : mxRecords.length > 0 ? "incorrect" : "missing",
17014
17141
  records: mxRecords.map((r) => `${r.priority} ${r.exchange}`)
17015
17142
  });
17016
- } catch (_error) {
17017
- dnsResults.push({
17018
- name: mailFromDomain,
17019
- type: "MX",
17020
- status: "missing"
17021
- });
17143
+ } catch (error) {
17144
+ const dnsClass = classifyDNSError(error);
17145
+ if (dnsClass === "missing") {
17146
+ dnsResults.push({
17147
+ name: mailFromDomain,
17148
+ type: "MX",
17149
+ status: "missing"
17150
+ });
17151
+ } else if (dnsClass === "network") {
17152
+ dnsResults.push({
17153
+ name: mailFromDomain,
17154
+ type: "MX",
17155
+ status: "missing",
17156
+ records: ["DNS lookup failed (network issue)"]
17157
+ });
17158
+ } else {
17159
+ throw error;
17160
+ }
17022
17161
  }
17023
17162
  try {
17024
17163
  const records = await resolver.resolveTxt(mailFromDomain);
@@ -17030,12 +17169,24 @@ Run ${pc20.cyan(`wraps email init --domain ${options.domain}`)} to add this doma
17030
17169
  status: hasAmazonSES ? "verified" : spfRecord ? "incorrect" : "missing",
17031
17170
  records: spfRecord ? [spfRecord] : void 0
17032
17171
  });
17033
- } catch (_error) {
17034
- dnsResults.push({
17035
- name: mailFromDomain,
17036
- type: "TXT (SPF)",
17037
- status: "missing"
17038
- });
17172
+ } catch (error) {
17173
+ const dnsClass = classifyDNSError(error);
17174
+ if (dnsClass === "missing") {
17175
+ dnsResults.push({
17176
+ name: mailFromDomain,
17177
+ type: "TXT (SPF)",
17178
+ status: "missing"
17179
+ });
17180
+ } else if (dnsClass === "network") {
17181
+ dnsResults.push({
17182
+ name: mailFromDomain,
17183
+ type: "TXT (SPF)",
17184
+ status: "missing",
17185
+ records: ["DNS lookup failed (network issue)"]
17186
+ });
17187
+ } else {
17188
+ throw error;
17189
+ }
17039
17190
  }
17040
17191
  }
17041
17192
  progress.stop();
@@ -17177,7 +17328,7 @@ Run ${pc20.cyan(`wraps email domains verify --domain ${domain}`)} to check verif
17177
17328
  );
17178
17329
  return;
17179
17330
  } catch (error) {
17180
- if (error.name !== "NotFoundException") {
17331
+ if (!isAWSNotFoundError(error)) {
17181
17332
  throw error;
17182
17333
  }
17183
17334
  }
@@ -17490,7 +17641,7 @@ ${pc20.bold("DNS Records to add:")}
17490
17641
  } catch (error) {
17491
17642
  progress.stop();
17492
17643
  trackCommand("email:domains:get-dkim", { success: false });
17493
- if (error.name === "NotFoundException") {
17644
+ if (isAWSNotFoundError(error)) {
17494
17645
  clack19.log.error(`Domain ${options.domain} not found in SES`);
17495
17646
  console.log(
17496
17647
  `
@@ -17571,7 +17722,7 @@ Use ${pc20.cyan(`wraps email domains remove --domain ${options.domain} --force`)
17571
17722
  } catch (error) {
17572
17723
  progress.stop();
17573
17724
  trackCommand("email:domains:remove", { success: false });
17574
- if (error.name === "NotFoundException") {
17725
+ if (isAWSNotFoundError(error)) {
17575
17726
  clack19.log.error(`Domain ${options.domain} not found in SES`);
17576
17727
  process.exit(1);
17577
17728
  return;
@@ -18670,10 +18821,11 @@ ${pc22.yellow(pc22.bold("Configuration Warnings:"))}`);
18670
18821
  return;
18671
18822
  } catch (error) {
18672
18823
  trackError("PREVIEW_FAILED", "email:init", { step: "preview" });
18673
- if (error.message?.includes("stack is currently locked")) {
18824
+ const msg = error instanceof Error ? error.message : String(error);
18825
+ if (msg.includes("stack is currently locked")) {
18674
18826
  throw errors.stackLocked();
18675
18827
  }
18676
- throw new Error(`Preview failed: ${error.message}`);
18828
+ throw new Error(`Preview failed: ${msg}`);
18677
18829
  }
18678
18830
  }
18679
18831
  let outputs;
@@ -18744,13 +18896,14 @@ ${pc22.yellow(pc22.bold("Configuration Warnings:"))}`);
18744
18896
  }
18745
18897
  );
18746
18898
  } catch (error) {
18899
+ const msg = error instanceof Error ? error.message : String(error);
18747
18900
  trackServiceInit("email", false, {
18748
18901
  preset,
18749
18902
  provider,
18750
18903
  region,
18751
18904
  duration_ms: Date.now() - startTime
18752
18905
  });
18753
- if (error.message?.includes("stack is currently locked")) {
18906
+ if (msg.includes("stack is currently locked")) {
18754
18907
  trackError("STACK_LOCKED", "email:init", { step: "deploy" });
18755
18908
  throw errors.stackLocked();
18756
18909
  }
@@ -18781,7 +18934,7 @@ ${pc22.yellow(pc22.bold("Configuration Warnings:"))}`);
18781
18934
  }
18782
18935
  }
18783
18936
  trackError("DEPLOYMENT_FAILED", "email:init", { step: "deploy" });
18784
- throw new Error(`Pulumi deployment failed: ${error.message}`);
18937
+ throw new Error(`Pulumi deployment failed: ${msg}`);
18785
18938
  }
18786
18939
  if (metadata.services.email) {
18787
18940
  metadata.services.email.pulumiStackName = `wraps-${identity.accountId}-${region}`;
@@ -18879,7 +19032,8 @@ ${pc22.yellow(pc22.bold("Configuration Warnings:"))}`);
18879
19032
  }
18880
19033
  } catch (error) {
18881
19034
  progress.stop();
18882
- clack21.log.warn(`Could not manage DNS records: ${error.message}`);
19035
+ const msg = error instanceof Error ? error.message : String(error);
19036
+ clack21.log.warn(`Could not manage DNS records: ${msg}`);
18883
19037
  }
18884
19038
  } else {
18885
19039
  const recordData = {
@@ -19114,7 +19268,8 @@ ${pc23.bold("The following Wraps resources will be removed:")}
19114
19268
  return;
19115
19269
  } catch (error) {
19116
19270
  trackError("PREVIEW_FAILED", "email:restore", { step: "preview" });
19117
- throw new Error(`Preview failed: ${error.message}`);
19271
+ const msg = error instanceof Error ? error.message : String(error);
19272
+ throw new Error(`Preview failed: ${msg}`);
19118
19273
  }
19119
19274
  }
19120
19275
  return;
@@ -19149,7 +19304,8 @@ ${pc23.bold("The following Wraps resources will be removed:")}
19149
19304
  );
19150
19305
  } catch (error) {
19151
19306
  trackError("DESTROY_FAILED", "email:restore", { step: "destroy" });
19152
- throw new Error(`Failed to destroy Pulumi stack: ${error.message}`);
19307
+ const msg = error instanceof Error ? error.message : String(error);
19308
+ throw new Error(`Failed to destroy Pulumi stack: ${msg}`);
19153
19309
  }
19154
19310
  });
19155
19311
  }
@@ -19176,6 +19332,7 @@ init_esm_shims();
19176
19332
  init_client();
19177
19333
  init_events();
19178
19334
  init_aws();
19335
+ init_errors();
19179
19336
  init_fs();
19180
19337
  init_metadata();
19181
19338
  import * as clack23 from "@clack/prompts";
@@ -19220,15 +19377,19 @@ async function emailStatus(options) {
19220
19377
  workDir: getPulumiWorkDir()
19221
19378
  });
19222
19379
  stackOutputs = await stack.outputs();
19223
- } catch (_error) {
19224
- progress.stop();
19225
- clack23.log.error("No email infrastructure found");
19226
- console.log(
19227
- `
19380
+ } catch (error) {
19381
+ if (error instanceof Error && (error.message.includes("no stack named") || error.message.includes("not found"))) {
19382
+ progress.stop();
19383
+ clack23.log.error("No email infrastructure found");
19384
+ console.log(
19385
+ `
19228
19386
  Run ${pc24.cyan("wraps email init")} to deploy email infrastructure.
19229
19387
  `
19230
- );
19231
- process.exit(1);
19388
+ );
19389
+ process.exit(1);
19390
+ return;
19391
+ }
19392
+ throw error;
19232
19393
  }
19233
19394
  const domains = await listSESDomains(region);
19234
19395
  const { SESv2Client: SESv2Client6, GetEmailIdentityCommand: GetEmailIdentityCommand5 } = await import("@aws-sdk/client-sesv2");
@@ -19253,17 +19414,20 @@ Run ${pc24.cyan("wraps email init")} to deploy email infrastructure.
19253
19414
  isPrimary: tracked?.isPrimary,
19254
19415
  purpose: tracked?.purpose
19255
19416
  };
19256
- } catch (_error) {
19257
- return {
19258
- domain: d.domain,
19259
- status: d.verified ? "verified" : "pending",
19260
- dkimTokens: void 0,
19261
- mailFromDomain: void 0,
19262
- mailFromStatus: void 0,
19263
- managed: tracked?.managed,
19264
- isPrimary: tracked?.isPrimary,
19265
- purpose: tracked?.purpose
19266
- };
19417
+ } catch (error) {
19418
+ if (isAWSError(error)) {
19419
+ return {
19420
+ domain: d.domain,
19421
+ status: d.verified ? "verified" : "pending",
19422
+ dkimTokens: void 0,
19423
+ mailFromDomain: void 0,
19424
+ mailFromStatus: void 0,
19425
+ managed: tracked?.managed,
19426
+ isPrimary: tracked?.isPrimary,
19427
+ purpose: tracked?.purpose
19428
+ };
19429
+ }
19430
+ throw error;
19267
19431
  }
19268
19432
  })
19269
19433
  );
@@ -20686,12 +20850,19 @@ ${pc28.bold("Current Configuration:")}
20686
20850
  if (config2.domain) {
20687
20851
  console.log(` Sending Domain: ${pc28.cyan(config2.domain)}`);
20688
20852
  }
20853
+ const hasHttpsTrackingPending = config2.tracking?.httpsEnabled && config2.tracking?.customRedirectDomain;
20689
20854
  if (config2.tracking?.enabled) {
20690
20855
  console.log(` ${pc28.green("\u2713")} Open & Click Tracking`);
20691
20856
  if (config2.tracking.customRedirectDomain) {
20692
- console.log(
20693
- ` ${pc28.dim("\u2514\u2500")} Custom domain: ${pc28.cyan(config2.tracking.customRedirectDomain)}`
20694
- );
20857
+ if (hasHttpsTrackingPending) {
20858
+ console.log(
20859
+ ` ${pc28.dim("\u2514\u2500")} Custom domain: ${pc28.cyan(config2.tracking.customRedirectDomain)} ${pc28.yellow("(HTTPS pending - certificate validation required)")}`
20860
+ );
20861
+ } else {
20862
+ console.log(
20863
+ ` ${pc28.dim("\u2514\u2500")} Custom domain: ${pc28.cyan(config2.tracking.customRedirectDomain)}`
20864
+ );
20865
+ }
20695
20866
  }
20696
20867
  }
20697
20868
  if (config2.suppressionList?.enabled) {
@@ -20747,60 +20918,69 @@ ${pc28.bold("Current Configuration:")}
20747
20918
  Estimated Cost: ${pc28.cyan(`~${formatCost(currentCostData.total.monthly)}/mo`)}`
20748
20919
  );
20749
20920
  console.log("");
20921
+ const upgradeOptions = [];
20922
+ if (hasHttpsTrackingPending) {
20923
+ upgradeOptions.push({
20924
+ value: "finish-tracking-domain",
20925
+ label: "Finish setting up custom tracking domain",
20926
+ hint: `Complete HTTPS setup for ${config2.tracking.customRedirectDomain}`
20927
+ });
20928
+ }
20929
+ upgradeOptions.push(
20930
+ {
20931
+ value: "preset",
20932
+ label: "Upgrade to a different preset",
20933
+ hint: "Starter \u2192 Production \u2192 Enterprise"
20934
+ },
20935
+ {
20936
+ value: "archiving",
20937
+ label: config2.emailArchiving?.enabled ? "Change email archiving settings" : "Enable email archiving",
20938
+ hint: config2.emailArchiving?.enabled ? "Update retention or disable" : "Store full email content with HTML"
20939
+ },
20940
+ {
20941
+ value: "tracking-domain",
20942
+ label: "Add/change custom tracking domain",
20943
+ hint: "Use your own domain for email links"
20944
+ },
20945
+ {
20946
+ value: "retention",
20947
+ label: "Change email history retention",
20948
+ hint: "7 days, 30 days, 90 days, 6 months, 1 year, 18 months"
20949
+ },
20950
+ {
20951
+ value: "events",
20952
+ label: "Customize tracked event types",
20953
+ hint: "Choose which SES events to track"
20954
+ },
20955
+ {
20956
+ value: "dedicated-ip",
20957
+ label: "Enable dedicated IP address",
20958
+ hint: "Requires 100k+ emails/day ($50-100/mo)"
20959
+ },
20960
+ {
20961
+ value: "alerts",
20962
+ label: config2.alerts?.enabled ? "Manage reputation alerts" : "Enable reputation alerts",
20963
+ hint: config2.alerts?.enabled ? "Update thresholds or notification settings" : "Get notified before AWS suspends your account"
20964
+ },
20965
+ {
20966
+ value: "custom",
20967
+ label: "Custom configuration",
20968
+ hint: "Modify multiple settings at once"
20969
+ },
20970
+ {
20971
+ value: "wraps-dashboard",
20972
+ label: metadata.services.email?.webhookSecret ? "Manage Wraps Dashboard connection" : "Connect to Wraps Dashboard",
20973
+ hint: metadata.services.email?.webhookSecret ? "Regenerate secret or disconnect" : "Send events to dashboard for analytics"
20974
+ },
20975
+ {
20976
+ value: "smtp-credentials",
20977
+ label: metadata.services.email?.smtpCredentials?.enabled ? "Manage SMTP credentials" : "Enable SMTP credentials",
20978
+ hint: metadata.services.email?.smtpCredentials?.enabled ? "Rotate or disable credentials" : "Generate credentials for PHP, WordPress, etc."
20979
+ }
20980
+ );
20750
20981
  upgradeAction = await clack27.select({
20751
20982
  message: "What would you like to do?",
20752
- options: [
20753
- {
20754
- value: "preset",
20755
- label: "Upgrade to a different preset",
20756
- hint: "Starter \u2192 Production \u2192 Enterprise"
20757
- },
20758
- {
20759
- value: "archiving",
20760
- label: config2.emailArchiving?.enabled ? "Change email archiving settings" : "Enable email archiving",
20761
- hint: config2.emailArchiving?.enabled ? "Update retention or disable" : "Store full email content with HTML"
20762
- },
20763
- {
20764
- value: "tracking-domain",
20765
- label: "Add/change custom tracking domain",
20766
- hint: "Use your own domain for email links"
20767
- },
20768
- {
20769
- value: "retention",
20770
- label: "Change email history retention",
20771
- hint: "7 days, 30 days, 90 days, 6 months, 1 year, 18 months"
20772
- },
20773
- {
20774
- value: "events",
20775
- label: "Customize tracked event types",
20776
- hint: "Choose which SES events to track"
20777
- },
20778
- {
20779
- value: "dedicated-ip",
20780
- label: "Enable dedicated IP address",
20781
- hint: "Requires 100k+ emails/day ($50-100/mo)"
20782
- },
20783
- {
20784
- value: "alerts",
20785
- label: config2.alerts?.enabled ? "Manage reputation alerts" : "Enable reputation alerts",
20786
- hint: config2.alerts?.enabled ? "Update thresholds or notification settings" : "Get notified before AWS suspends your account"
20787
- },
20788
- {
20789
- value: "custom",
20790
- label: "Custom configuration",
20791
- hint: "Modify multiple settings at once"
20792
- },
20793
- {
20794
- value: "wraps-dashboard",
20795
- label: metadata.services.email?.webhookSecret ? "Manage Wraps Dashboard connection" : "Connect to Wraps Dashboard",
20796
- hint: metadata.services.email?.webhookSecret ? "Regenerate secret or disconnect" : "Send events to dashboard for analytics"
20797
- },
20798
- {
20799
- value: "smtp-credentials",
20800
- label: metadata.services.email?.smtpCredentials?.enabled ? "Manage SMTP credentials" : "Enable SMTP credentials",
20801
- hint: metadata.services.email?.smtpCredentials?.enabled ? "Rotate or disable credentials" : "Generate credentials for PHP, WordPress, etc."
20802
- }
20803
- ]
20983
+ options: upgradeOptions
20804
20984
  });
20805
20985
  if (clack27.isCancel(upgradeAction)) {
20806
20986
  clack27.cancel("Upgrade cancelled.");
@@ -20809,6 +20989,14 @@ ${pc28.bold("Current Configuration:")}
20809
20989
  let updatedConfig = { ...config2 };
20810
20990
  let newPreset = metadata.services.email?.preset;
20811
20991
  switch (upgradeAction) {
20992
+ case "finish-tracking-domain": {
20993
+ clack27.log.info(
20994
+ `Checking certificate status for ${pc28.cyan(config2.tracking.customRedirectDomain)}...`
20995
+ );
20996
+ updatedConfig = { ...config2 };
20997
+ newPreset = metadata.services.email?.preset;
20998
+ break;
20999
+ }
20812
21000
  case "preset": {
20813
21001
  const presets = getAllPresetInfo().filter(
20814
21002
  (p) => p.name.toLowerCase() !== "custom"
@@ -21863,11 +22051,12 @@ ${pc28.bold("Cost Impact:")}`);
21863
22051
  });
21864
22052
  return;
21865
22053
  } catch (error) {
22054
+ const msg = error instanceof Error ? error.message : String(error);
21866
22055
  trackError("PREVIEW_FAILED", "email:upgrade", { step: "preview" });
21867
- if (error.message?.includes("stack is currently locked")) {
22056
+ if (msg.includes("stack is currently locked")) {
21868
22057
  throw errors.stackLocked();
21869
22058
  }
21870
- throw new Error(`Preview failed: ${error.message}`);
22059
+ throw new Error(`Preview failed: ${msg}`);
21871
22060
  }
21872
22061
  }
21873
22062
  let outputs;
@@ -21949,18 +22138,19 @@ ${pc28.bold("Cost Impact:")}`);
21949
22138
  }
21950
22139
  );
21951
22140
  } catch (error) {
22141
+ const msg = error instanceof Error ? error.message : String(error);
21952
22142
  trackServiceUpgrade("email", {
21953
22143
  from_preset: metadata.services.email?.preset,
21954
22144
  to_preset: newPreset,
21955
22145
  action: typeof upgradeAction === "string" ? upgradeAction : void 0,
21956
22146
  duration_ms: Date.now() - startTime
21957
22147
  });
21958
- if (error.message?.includes("stack is currently locked")) {
22148
+ if (msg.includes("stack is currently locked")) {
21959
22149
  trackError("STACK_LOCKED", "email:upgrade", { step: "deploy" });
21960
22150
  throw errors.stackLocked();
21961
22151
  }
21962
22152
  trackError("UPGRADE_FAILED", "email:upgrade", { step: "deploy" });
21963
- throw new Error(`Pulumi upgrade failed: ${error.message}`);
22153
+ throw new Error(`Pulumi upgrade failed: ${msg}`);
21964
22154
  }
21965
22155
  let dnsAutoCreated = false;
21966
22156
  if (outputs.domain && outputs.dkimTokens && outputs.dkimTokens.length > 0) {
@@ -22018,9 +22208,8 @@ ${pc28.bold("Cost Impact:")}`);
22018
22208
  );
22019
22209
  }
22020
22210
  } catch (error) {
22021
- progress.fail(
22022
- `Failed to create DNS records automatically: ${error.message}`
22023
- );
22211
+ const msg = error instanceof Error ? error.message : String(error);
22212
+ progress.fail(`Failed to create DNS records automatically: ${msg}`);
22024
22213
  progress.info(
22025
22214
  "You can manually add the required DNS records shown below"
22026
22215
  );
@@ -22159,9 +22348,8 @@ ${pc28.bold("Add these DNS records to your DNS provider:")}
22159
22348
  }
22160
22349
  }
22161
22350
  } catch (error) {
22162
- progress.fail(
22163
- `Failed to create ACM validation record: ${error.message}`
22164
- );
22351
+ const msg = error instanceof Error ? error.message : String(error);
22352
+ progress.fail(`Failed to create ACM validation record: ${msg}`);
22165
22353
  }
22166
22354
  }
22167
22355
  }
@@ -24387,10 +24575,10 @@ var WRAPS_PLATFORM_ACCOUNT_ID = "905130073023";
24387
24575
  async function updatePlatformRole(metadata, progress, externalId) {
24388
24576
  const roleName = "wraps-console-access-role";
24389
24577
  const iam10 = new IAMClient2({ region: "us-east-1" });
24390
- let roleExists4 = false;
24578
+ let roleExists2 = false;
24391
24579
  try {
24392
24580
  await iam10.send(new GetRoleCommand({ RoleName: roleName }));
24393
- roleExists4 = true;
24581
+ roleExists2 = true;
24394
24582
  } catch (error) {
24395
24583
  const isNotFound = error instanceof Error && (error.name === "NoSuchEntityException" || error.name === "NoSuchEntity" || error.message.includes("NoSuchEntity"));
24396
24584
  if (!isNotFound) {
@@ -24400,7 +24588,7 @@ async function updatePlatformRole(metadata, progress, externalId) {
24400
24588
  const emailConfig = metadata.services.email?.config;
24401
24589
  const smsConfig = metadata.services.sms?.config;
24402
24590
  const policy = buildConsolePolicyDocument(emailConfig, smsConfig);
24403
- if (roleExists4) {
24591
+ if (roleExists2) {
24404
24592
  await progress.execute("Updating platform access role", async () => {
24405
24593
  await iam10.send(
24406
24594
  new PutRolePolicyCommand({
@@ -24858,17 +25046,17 @@ Run ${pc33.cyan("wraps email init")} or ${pc33.cyan("wraps sms init")} first.
24858
25046
  }
24859
25047
  const roleName = "wraps-console-access-role";
24860
25048
  const iam10 = new IAMClient2({ region: "us-east-1" });
24861
- let roleExists4 = false;
25049
+ let roleExists2 = false;
24862
25050
  try {
24863
25051
  await iam10.send(new GetRoleCommand({ RoleName: roleName }));
24864
- roleExists4 = true;
25052
+ roleExists2 = true;
24865
25053
  } catch (error) {
24866
25054
  const isNotFound = error instanceof Error && (error.name === "NoSuchEntityException" || error.name === "NoSuchEntity" || error.message.includes("NoSuchEntity"));
24867
25055
  if (!isNotFound) {
24868
25056
  throw error;
24869
25057
  }
24870
25058
  }
24871
- if (roleExists4) {
25059
+ if (roleExists2) {
24872
25060
  const emailConfig = metadata.services.email?.config;
24873
25061
  const smsConfig = metadata.services.sms?.config;
24874
25062
  const policy = buildConsolePolicyDocument(emailConfig, smsConfig);
@@ -25025,10 +25213,10 @@ Run ${pc35.cyan("wraps email init")} to deploy infrastructure first.
25025
25213
  }
25026
25214
  const roleName = "wraps-console-access-role";
25027
25215
  const iam10 = new IAMClient3({ region: "us-east-1" });
25028
- let roleExists4 = false;
25216
+ let roleExists2 = false;
25029
25217
  try {
25030
25218
  await iam10.send(new GetRoleCommand2({ RoleName: roleName }));
25031
- roleExists4 = true;
25219
+ roleExists2 = true;
25032
25220
  } catch (error) {
25033
25221
  const isNotFound = error instanceof Error && (error.name === "NoSuchEntityException" || error.name === "NoSuchEntity" || error.message.includes("NoSuchEntity"));
25034
25222
  if (!isNotFound) {
@@ -25036,7 +25224,7 @@ Run ${pc35.cyan("wraps email init")} to deploy infrastructure first.
25036
25224
  }
25037
25225
  }
25038
25226
  const externalId = metadata.platform?.externalId;
25039
- if (!(roleExists4 || externalId)) {
25227
+ if (!(roleExists2 || externalId)) {
25040
25228
  progress.stop();
25041
25229
  log31.warn(`IAM role ${pc35.cyan(roleName)} does not exist`);
25042
25230
  console.log(
@@ -25048,7 +25236,7 @@ Run ${pc35.cyan("wraps email init")} to deploy infrastructure first.
25048
25236
  );
25049
25237
  process.exit(0);
25050
25238
  }
25051
- if (roleExists4) {
25239
+ if (roleExists2) {
25052
25240
  progress.info(`Found IAM role: ${pc35.cyan(roleName)}`);
25053
25241
  } else {
25054
25242
  progress.info(
@@ -25057,7 +25245,7 @@ Run ${pc35.cyan("wraps email init")} to deploy infrastructure first.
25057
25245
  }
25058
25246
  if (!options.force) {
25059
25247
  progress.stop();
25060
- const actionLabel = roleExists4 ? "Update" : "Create";
25248
+ const actionLabel = roleExists2 ? "Update" : "Create";
25061
25249
  const shouldContinue = await confirm13({
25062
25250
  message: `${actionLabel} IAM role ${pc35.cyan(roleName)} with latest permissions?`,
25063
25251
  initialValue: true
@@ -25076,7 +25264,7 @@ Run ${pc35.cyan("wraps email init")} to deploy infrastructure first.
25076
25264
  const smsEnabled = !!smsConfig;
25077
25265
  const smsSendingEnabled = smsConfig && smsConfig.sendingEnabled !== false;
25078
25266
  const smsEventTracking = smsConfig?.eventTracking;
25079
- if (!roleExists4 && externalId) {
25267
+ if (!roleExists2 && externalId) {
25080
25268
  const WRAPS_PLATFORM_ACCOUNT_ID2 = "905130073023";
25081
25269
  await progress.execute("Creating IAM role", async () => {
25082
25270
  const trustPolicy = {
@@ -25129,7 +25317,7 @@ Run ${pc35.cyan("wraps email init")} to deploy infrastructure first.
25129
25317
  });
25130
25318
  }
25131
25319
  progress.stop();
25132
- const actionVerb = roleExists4 ? "updated" : "created";
25320
+ const actionVerb = roleExists2 ? "updated" : "created";
25133
25321
  trackCommand("platform:update-role", {
25134
25322
  success: true,
25135
25323
  duration_ms: Date.now() - startTime,
@@ -28313,38 +28501,9 @@ import pc39 from "picocolors";
28313
28501
 
28314
28502
  // src/infrastructure/sms-stack.ts
28315
28503
  init_esm_shims();
28504
+ init_resource_checks();
28316
28505
  import * as aws19 from "@pulumi/aws";
28317
28506
  import * as pulumi24 from "@pulumi/pulumi";
28318
- async function roleExists3(roleName) {
28319
- try {
28320
- const { IAMClient: IAMClient4, GetRoleCommand: GetRoleCommand3 } = await import("@aws-sdk/client-iam");
28321
- const iam10 = new IAMClient4({
28322
- region: process.env.AWS_REGION || "us-east-1"
28323
- });
28324
- await iam10.send(new GetRoleCommand3({ RoleName: roleName }));
28325
- return true;
28326
- } catch (error) {
28327
- if (error.name === "NoSuchEntityException" || error.Code === "NoSuchEntity" || error.Error?.Code === "NoSuchEntity") {
28328
- return false;
28329
- }
28330
- return false;
28331
- }
28332
- }
28333
- async function tableExists2(tableName) {
28334
- try {
28335
- const { DynamoDBClient: DynamoDBClient6, DescribeTableCommand: DescribeTableCommand2 } = await import("@aws-sdk/client-dynamodb");
28336
- const dynamodb3 = new DynamoDBClient6({
28337
- region: process.env.AWS_REGION || "us-east-1"
28338
- });
28339
- await dynamodb3.send(new DescribeTableCommand2({ TableName: tableName }));
28340
- return true;
28341
- } catch (error) {
28342
- if (error instanceof Error && "name" in error && error.name === "ResourceNotFoundException") {
28343
- return false;
28344
- }
28345
- return false;
28346
- }
28347
- }
28348
28507
  async function createSMSIAMRole(config2) {
28349
28508
  let assumeRolePolicy;
28350
28509
  if (config2.provider === "vercel" && config2.oidcProvider) {
@@ -28390,7 +28549,7 @@ async function createSMSIAMRole(config2) {
28390
28549
  throw new Error("Other providers not yet implemented");
28391
28550
  }
28392
28551
  const roleName = "wraps-sms-role";
28393
- const exists = await roleExists3(roleName);
28552
+ const exists = await roleExists(roleName);
28394
28553
  const role = exists ? new aws19.iam.Role(
28395
28554
  roleName,
28396
28555
  {
@@ -28608,20 +28767,6 @@ async function createSMSPhoneNumber(phoneNumberType, optOutList) {
28608
28767
  }
28609
28768
  );
28610
28769
  }
28611
- async function sqsQueueExists(queueName) {
28612
- try {
28613
- const { SQSClient, GetQueueUrlCommand } = await import("@aws-sdk/client-sqs");
28614
- const sqs5 = new SQSClient({
28615
- region: process.env.AWS_REGION || "us-east-1"
28616
- });
28617
- const response = await sqs5.send(
28618
- new GetQueueUrlCommand({ QueueName: queueName })
28619
- );
28620
- return response.QueueUrl || null;
28621
- } catch {
28622
- return null;
28623
- }
28624
- }
28625
28770
  async function createSMSSQSResources() {
28626
28771
  const dlqName = "wraps-sms-events-dlq";
28627
28772
  const queueName = "wraps-sms-events";
@@ -28671,30 +28816,6 @@ async function createSMSSQSResources() {
28671
28816
  });
28672
28817
  return { queue, dlq };
28673
28818
  }
28674
- async function snsTopicExists(topicName) {
28675
- try {
28676
- const { SNSClient: SNSClient2, ListTopicsCommand: ListTopicsCommand2 } = await import("@aws-sdk/client-sns");
28677
- const sns3 = new SNSClient2({
28678
- region: process.env.AWS_REGION || "us-east-1"
28679
- });
28680
- let nextToken;
28681
- do {
28682
- const response = await sns3.send(
28683
- new ListTopicsCommand2({ NextToken: nextToken })
28684
- );
28685
- const found = response.Topics?.find(
28686
- (t) => t.TopicArn?.endsWith(`:${topicName}`)
28687
- );
28688
- if (found?.TopicArn) {
28689
- return found.TopicArn;
28690
- }
28691
- nextToken = response.NextToken;
28692
- } while (nextToken);
28693
- return null;
28694
- } catch {
28695
- return null;
28696
- }
28697
- }
28698
28819
  async function createSMSSNSResources(config2) {
28699
28820
  const topicName = "wraps-sms-events";
28700
28821
  const topicArn = await snsTopicExists(topicName);
@@ -28762,7 +28883,7 @@ async function createSMSSNSResources(config2) {
28762
28883
  }
28763
28884
  async function createSMSDynamoDBTable() {
28764
28885
  const tableName = "wraps-sms-history";
28765
- const exists = await tableExists2(tableName);
28886
+ const exists = await tableExists(tableName);
28766
28887
  const tableConfig = {
28767
28888
  name: tableName,
28768
28889
  billingMode: "PAY_PER_REQUEST",
@@ -31698,18 +31819,19 @@ ${pc45.bold("Cost Impact:")}`);
31698
31819
  });
31699
31820
  }
31700
31821
  } catch (error) {
31822
+ const msg = error instanceof Error ? error.message : String(error);
31701
31823
  trackServiceUpgrade("sms", {
31702
31824
  from_preset: metadata.services.sms?.preset,
31703
31825
  to_preset: newPreset,
31704
31826
  action: typeof upgradeAction === "string" ? upgradeAction : void 0,
31705
31827
  duration_ms: Date.now() - startTime
31706
31828
  });
31707
- if (error.message?.includes("stack is currently locked")) {
31829
+ if (msg.includes("stack is currently locked")) {
31708
31830
  trackError("STACK_LOCKED", "sms:upgrade", { step: "deploy" });
31709
31831
  throw errors.stackLocked();
31710
31832
  }
31711
31833
  trackError("UPGRADE_FAILED", "sms:upgrade", { step: "deploy" });
31712
- throw new Error(`SMS upgrade failed: ${error.message}`);
31834
+ throw new Error(`SMS upgrade failed: ${msg}`);
31713
31835
  }
31714
31836
  updateServiceConfig(metadata, "sms", updatedConfig);
31715
31837
  if (metadata.services.sms) {