@wraps.dev/cli 2.23.3 → 2.25.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
@@ -9,12 +9,12 @@ var __export = (target, all6) => {
9
9
  __defProp(target, name, { get: all6[name], enumerable: true });
10
10
  };
11
11
 
12
- // ../../node_modules/.pnpm/tsup@8.5.1_jiti@2.6.1_postcss@8.5.14_tsx@4.20.6_typescript@5.9.3_yaml@2.8.4/node_modules/tsup/assets/esm_shims.js
12
+ // ../../node_modules/.pnpm/tsup@8.5.1_jiti@2.7.0_postcss@8.5.15_tsx@4.20.6_typescript@5.9.3_yaml@2.8.4/node_modules/tsup/assets/esm_shims.js
13
13
  import path from "path";
14
14
  import { fileURLToPath } from "url";
15
15
  var getFilename, getDirname, __dirname;
16
16
  var init_esm_shims = __esm({
17
- "../../node_modules/.pnpm/tsup@8.5.1_jiti@2.6.1_postcss@8.5.14_tsx@4.20.6_typescript@5.9.3_yaml@2.8.4/node_modules/tsup/assets/esm_shims.js"() {
17
+ "../../node_modules/.pnpm/tsup@8.5.1_jiti@2.7.0_postcss@8.5.15_tsx@4.20.6_typescript@5.9.3_yaml@2.8.4/node_modules/tsup/assets/esm_shims.js"() {
18
18
  "use strict";
19
19
  getFilename = () => fileURLToPath(import.meta.url);
20
20
  getDirname = () => path.dirname(getFilename());
@@ -3047,7 +3047,7 @@ async function previewDNSChanges(hostedZoneId, domain, dkimTokens, region, custo
3047
3047
  });
3048
3048
  const dmarcName = `_dmarc.${domain}`;
3049
3049
  const dmarcRuaDomain = mailFromDomain || domain;
3050
- const proposedDMARC = `"v=DMARC1; p=quarantine; rua=mailto:postmaster@${dmarcRuaDomain}"`;
3050
+ const proposedDMARC = `"v=DMARC1; p=quarantine; sp=quarantine; np=reject; rua=mailto:postmaster@${dmarcRuaDomain}"`;
3051
3051
  const existingDMARC = findExistingRecord(existingRecords, dmarcName, "TXT");
3052
3052
  const existingDMARCValue = getRecordValue(existingDMARC);
3053
3053
  let dmarcStatus = "new";
@@ -3209,7 +3209,7 @@ async function createSelectedDNSRecords(hostedZoneId, domain, dkimTokens, region
3209
3209
  TTL: 1800,
3210
3210
  ResourceRecords: [
3211
3211
  {
3212
- Value: `"v=DMARC1; p=quarantine; rua=mailto:postmaster@${dmarcRuaDomain}"`
3212
+ Value: `"v=DMARC1; p=quarantine; sp=quarantine; np=reject; rua=mailto:postmaster@${dmarcRuaDomain}"`
3213
3213
  }
3214
3214
  ]
3215
3215
  }
@@ -3332,7 +3332,7 @@ async function createDNSRecords(hostedZoneId, domain, dkimTokens, region, custom
3332
3332
  TTL: 1800,
3333
3333
  ResourceRecords: [
3334
3334
  {
3335
- Value: `"v=DMARC1; p=quarantine; rua=mailto:postmaster@${dmarcRuaDomain}"`
3335
+ Value: `"v=DMARC1; p=quarantine; sp=quarantine; np=reject; rua=mailto:postmaster@${dmarcRuaDomain}"`
3336
3336
  }
3337
3337
  ]
3338
3338
  }
@@ -3556,7 +3556,7 @@ var init_cloudflare = __esm({
3556
3556
  const dmarcSuccess = await this.createRecord(
3557
3557
  `_dmarc.${domain}`,
3558
3558
  "TXT",
3559
- `v=DMARC1; p=quarantine; rua=mailto:postmaster@${mailFromDomain || domain}`
3559
+ `v=DMARC1; p=quarantine; sp=quarantine; np=reject; rua=mailto:postmaster@${mailFromDomain || domain}`
3560
3560
  );
3561
3561
  if (dmarcSuccess) {
3562
3562
  recordsCreated++;
@@ -3942,7 +3942,7 @@ var init_vercel = __esm({
3942
3942
  const dmarcSuccess = await this.createRecord(
3943
3943
  `_dmarc.${domain}`,
3944
3944
  "TXT",
3945
- `v=DMARC1; p=quarantine; rua=mailto:postmaster@${mailFromDomain || domain}`
3945
+ `v=DMARC1; p=quarantine; sp=quarantine; np=reject; rua=mailto:postmaster@${mailFromDomain || domain}`
3946
3946
  );
3947
3947
  if (dmarcSuccess) {
3948
3948
  recordsCreated++;
@@ -4211,7 +4211,7 @@ function buildEmailDNSRecords(data) {
4211
4211
  records.push({
4212
4212
  name: `_dmarc.${domain}`,
4213
4213
  type: "TXT",
4214
- value: `v=DMARC1; p=quarantine; rua=mailto:postmaster@${dmarcRuaDomain}`,
4214
+ value: `v=DMARC1; p=quarantine; sp=quarantine; np=reject; rua=mailto:postmaster@${dmarcRuaDomain}`,
4215
4215
  category: "dmarc"
4216
4216
  });
4217
4217
  if (customTrackingDomain) {
@@ -6263,7 +6263,7 @@ Verification should complete within a few minutes.`,
6263
6263
  ),
6264
6264
  "",
6265
6265
  pc10.bold("DMARC Record (TXT):"),
6266
- ` ${pc10.cyan(`_dmarc.${domain}`)} ${pc10.dim("TXT")} "v=DMARC1; p=quarantine; rua=mailto:postmaster@${dmarcRuaDomain}"`
6266
+ ` ${pc10.cyan(`_dmarc.${domain}`)} ${pc10.dim("TXT")} "v=DMARC1; p=quarantine; sp=quarantine; np=reject; rua=mailto:postmaster@${dmarcRuaDomain}"`
6267
6267
  );
6268
6268
  if (outputs.mailFromDomain) {
6269
6269
  dnsLines.push(
@@ -6492,7 +6492,7 @@ ${domainStrings.join("\n")}`);
6492
6492
  ),
6493
6493
  "",
6494
6494
  pc10.bold("DMARC Record (TXT):"),
6495
- ` ${pc10.cyan(`_dmarc.${domain.domain}`)} ${pc10.dim("TXT")} "v=DMARC1; p=quarantine; rua=mailto:postmaster@${dmarcRuaDomain}"`
6495
+ ` ${pc10.cyan(`_dmarc.${domain.domain}`)} ${pc10.dim("TXT")} "v=DMARC1; p=quarantine; sp=quarantine; np=reject; rua=mailto:postmaster@${dmarcRuaDomain}"`
6496
6496
  );
6497
6497
  }
6498
6498
  if (domain.mailFromDomain && domain.mailFromStatus !== "SUCCESS") {
@@ -15408,6 +15408,9 @@ async function checkDmarc(domain) {
15408
15408
  valid: false,
15409
15409
  policy: null,
15410
15410
  subdomainPolicy: null,
15411
+ nonExistentSubdomainPolicy: null,
15412
+ testing: false,
15413
+ psd: null,
15411
15414
  percentage: 100,
15412
15415
  reportingEnabled: false,
15413
15416
  ruaAddresses: [],
@@ -15460,6 +15463,21 @@ function parseDmarcRecord(record, result) {
15460
15463
  } else {
15461
15464
  result.subdomainPolicy = result.policy;
15462
15465
  }
15466
+ const np = tags.get("np");
15467
+ if (np) {
15468
+ if (["none", "quarantine", "reject"].includes(np)) {
15469
+ result.nonExistentSubdomainPolicy = np;
15470
+ } else {
15471
+ result.warnings.push(`Invalid non-existent subdomain policy (np): ${np}`);
15472
+ }
15473
+ }
15474
+ if (tags.get("t") === "y") {
15475
+ result.testing = true;
15476
+ }
15477
+ const psd = tags.get("psd");
15478
+ if (psd && ["y", "n", "u"].includes(psd)) {
15479
+ result.psd = psd;
15480
+ }
15463
15481
  const pct = tags.get("pct");
15464
15482
  if (pct) {
15465
15483
  const pctNum = Number.parseInt(pct, 10);
@@ -15468,7 +15486,9 @@ function parseDmarcRecord(record, result) {
15468
15486
  } else {
15469
15487
  result.percentage = pctNum;
15470
15488
  if (pctNum < 100) {
15471
- result.warnings.push(`Only ${pctNum}% of messages subject to policy`);
15489
+ result.warnings.push(
15490
+ `pct=${pctNum} is deprecated (DMARCbis receivers ignore it; enforcement is all-or-nothing). Use t=y while testing instead.`
15491
+ );
15472
15492
  }
15473
15493
  }
15474
15494
  }
@@ -15533,6 +15553,17 @@ function parseDmarcRecord(record, result) {
15533
15553
  if (result.subdomainPolicy === "none" && result.policy !== "none") {
15534
15554
  result.warnings.push("Subdomain policy is less strict than domain policy");
15535
15555
  }
15556
+ const enforcing = result.policy === "quarantine" || result.policy === "reject";
15557
+ if (result.testing && result.policy !== "none") {
15558
+ result.warnings.push(
15559
+ "t=y (testing) is set \u2014 receivers will not enforce the policy"
15560
+ );
15561
+ }
15562
+ if (enforcing && !result.nonExistentSubdomainPolicy) {
15563
+ result.warnings.push(
15564
+ "No np= set \u2014 add np=reject to block spoofing from non-existent subdomains (DMARCbis)"
15565
+ );
15566
+ }
15536
15567
  if (!result.reportingEnabled) {
15537
15568
  result.warnings.push("No aggregate reporting configured (rua=)");
15538
15569
  }
@@ -16686,11 +16717,18 @@ function collectDmarcIssues(dmarc, deductions, bonuses) {
16686
16717
  reason: "DMARC policy is none (not enforcing)"
16687
16718
  });
16688
16719
  }
16689
- if (dmarc.percentage < 100) {
16720
+ if (dmarc.testing && dmarc.policy !== "none") {
16690
16721
  deductions.push({
16691
16722
  check: "dmarc",
16692
16723
  points: 2,
16693
- reason: `DMARC pct=${dmarc.percentage} (not 100%)`
16724
+ reason: "DMARC testing mode (t=y) \u2014 receivers won't enforce the policy"
16725
+ });
16726
+ }
16727
+ if (dmarc.percentage < 100) {
16728
+ deductions.push({
16729
+ check: "dmarc",
16730
+ points: 1,
16731
+ reason: `DMARC pct=${dmarc.percentage} is deprecated (DMARCbis receivers ignore it; enforcement is inconsistent)`
16694
16732
  });
16695
16733
  }
16696
16734
  if (!dmarc.reportingEnabled) {
@@ -16707,6 +16745,13 @@ function collectDmarcIssues(dmarc, deductions, bonuses) {
16707
16745
  reason: "DMARC strict alignment configured"
16708
16746
  });
16709
16747
  }
16748
+ if ((dmarc.policy === "quarantine" || dmarc.policy === "reject") && dmarc.nonExistentSubdomainPolicy === "reject") {
16749
+ bonuses.push({
16750
+ check: "dmarc",
16751
+ points: 1,
16752
+ reason: "np=reject blocks non-existent subdomain spoofing (DMARCbis)"
16753
+ });
16754
+ }
16710
16755
  }
16711
16756
  function collectInfraIssues(checks, deductions, bonuses) {
16712
16757
  if (checks.mx.exists) {
@@ -17839,8 +17884,19 @@ async function createEventBridgeResources(config2) {
17839
17884
  const target = new aws7.cloudwatch.EventTarget("wraps-email-events-target", {
17840
17885
  rule: rule.name,
17841
17886
  eventBusName,
17842
- arn: config2.queueArn
17887
+ arn: config2.queueArn,
17888
+ deadLetterConfig: { arn: config2.dlqArn }
17843
17889
  });
17890
+ const archive = new aws7.cloudwatch.EventArchive(
17891
+ "wraps-email-events-archive",
17892
+ {
17893
+ name: "wraps-email-events-archive",
17894
+ eventSourceArn: config2.eventBusArn,
17895
+ description: "Wraps: 30-day archive of SES events for outage replay (see wraps email doctor)",
17896
+ retentionDays: 30,
17897
+ eventPattern: JSON.stringify({ source: ["aws.ses"] })
17898
+ }
17899
+ );
17844
17900
  let webhookConnection;
17845
17901
  let webhookApiDestination;
17846
17902
  let webhookTarget;
@@ -17911,7 +17967,8 @@ async function createEventBridgeResources(config2) {
17911
17967
  rule: rule.name,
17912
17968
  eventBusName,
17913
17969
  arn: webhookApiDestination.arn,
17914
- roleArn: webhookRole.arn
17970
+ roleArn: webhookRole.arn,
17971
+ deadLetterConfig: { arn: config2.dlqArn }
17915
17972
  });
17916
17973
  }
17917
17974
  let userWebhookConnection;
@@ -17929,9 +17986,33 @@ async function createEventBridgeResources(config2) {
17929
17986
  userWebhookApiDestination = userWebhookResources.apiDestination;
17930
17987
  userWebhookTarget = userWebhookResources.target;
17931
17988
  }
17989
+ const alarmActions = config2.alertTopicArn ? [config2.alertTopicArn] : void 0;
17990
+ const failedInvocationsAlarm = new aws7.cloudwatch.MetricAlarm(
17991
+ "wraps-email-events-delivery-failures",
17992
+ {
17993
+ name: "wraps-email-events-delivery-failures",
17994
+ namespace: "AWS/Events",
17995
+ metricName: "FailedInvocations",
17996
+ dimensions: { RuleName: "wraps-email-events-to-sqs" },
17997
+ statistic: "Sum",
17998
+ period: 300,
17999
+ evaluationPeriods: 3,
18000
+ threshold: 1,
18001
+ comparisonOperator: "GreaterThanOrEqualToThreshold",
18002
+ treatMissingData: "notBreaching",
18003
+ alarmDescription: "SES event delivery from EventBridge is failing (dead target or deauthorized webhook). Event history is being lost \u2014 run `wraps email doctor`.",
18004
+ ...alarmActions ? { alarmActions, okActions: alarmActions } : {},
18005
+ tags: {
18006
+ ManagedBy: "wraps-cli",
18007
+ Service: "email"
18008
+ }
18009
+ }
18010
+ );
17932
18011
  return {
17933
18012
  rule,
17934
18013
  target,
18014
+ failedInvocationsAlarm,
18015
+ archive,
17935
18016
  webhookConnection,
17936
18017
  webhookApiDestination,
17937
18018
  webhookTarget,
@@ -18376,6 +18457,30 @@ async function createSQSResources(config2) {
18376
18457
  ...SQS_TIMEOUTS,
18377
18458
  ...dlqUrl ? { import: dlqUrl } : {}
18378
18459
  });
18460
+ const ruleArn = `arn:aws:events:${config2.region}:${config2.accountId}:rule/wraps-email-events-to-sqs`;
18461
+ new aws11.sqs.QueuePolicy("wraps-email-events-dlq-policy", {
18462
+ queueUrl: dlq.url,
18463
+ policy: dlq.arn.apply(
18464
+ (dlqArn) => JSON.stringify({
18465
+ Version: "2012-10-17",
18466
+ Statement: [
18467
+ {
18468
+ Effect: "Allow",
18469
+ Principal: {
18470
+ Service: "events.amazonaws.com"
18471
+ },
18472
+ Action: "sqs:SendMessage",
18473
+ Resource: dlqArn,
18474
+ Condition: {
18475
+ ArnEquals: {
18476
+ "aws:SourceArn": ruleArn
18477
+ }
18478
+ }
18479
+ }
18480
+ ]
18481
+ })
18482
+ )
18483
+ });
18379
18484
  const queueConfig = {
18380
18485
  name: queueName,
18381
18486
  visibilityTimeoutSeconds: 300,
@@ -18505,13 +18610,27 @@ async function deployEmailStack(config2) {
18505
18610
  }
18506
18611
  let sqsResources;
18507
18612
  if (emailConfig.eventTracking?.enabled) {
18508
- sqsResources = await createSQSResources({ region: config2.region });
18613
+ sqsResources = await createSQSResources({
18614
+ region: config2.region,
18615
+ accountId
18616
+ });
18617
+ }
18618
+ let alertingResources;
18619
+ if (emailConfig.alerts?.enabled) {
18620
+ alertingResources = await createAlertingResources({
18621
+ alertConfig: emailConfig.alerts,
18622
+ configSetName: sesResources?.configSet.configurationSetName,
18623
+ dlqName: sqsResources ? "wraps-email-events-dlq" : void 0,
18624
+ region: config2.region
18625
+ });
18509
18626
  }
18510
18627
  if (emailConfig.eventTracking?.enabled && sesResources && sqsResources) {
18511
18628
  await createEventBridgeResources({
18512
18629
  eventBusArn: sesResources.eventBus.arn,
18513
18630
  queueArn: sqsResources.queue.arn,
18514
18631
  queueUrl: sqsResources.queue.url,
18632
+ dlqArn: sqsResources.dlq.arn,
18633
+ alertTopicArn: alertingResources?.topic.arn,
18515
18634
  // Include webhook config if provided (for Wraps platform integration)
18516
18635
  webhook: config2.webhook,
18517
18636
  // Include user webhook config if provided
@@ -18552,15 +18671,6 @@ async function deployEmailStack(config2) {
18552
18671
  region: config2.region
18553
18672
  });
18554
18673
  }
18555
- let alertingResources;
18556
- if (emailConfig.alerts?.enabled) {
18557
- alertingResources = await createAlertingResources({
18558
- alertConfig: emailConfig.alerts,
18559
- configSetName: sesResources?.configSet.configurationSetName,
18560
- dlqName: sqsResources ? "wraps-email-events-dlq" : void 0,
18561
- region: config2.region
18562
- });
18563
- }
18564
18674
  let inboundResources;
18565
18675
  let replySecretsOutput;
18566
18676
  if (emailConfig.inbound?.enabled) {
@@ -19509,6 +19619,26 @@ async function emailDestroy(options) {
19509
19619
  const domain = emailConfig?.domain;
19510
19620
  const storedStackName = emailService?.pulumiStackName;
19511
19621
  if (!(options.force || options.preview)) {
19622
+ const additionalDomainNames = (emailConfig?.additionalDomains ?? []).map((d) => d.domain).filter(Boolean);
19623
+ const domainList = [domain, ...additionalDomainNames].filter(Boolean);
19624
+ const summaryLines = [
19625
+ domainList.length > 0 ? `Domain(s): ${domainList.join(", ")}` : "Domain(s): none configured",
19626
+ "SES configuration sets and event destinations for the above domain(s)",
19627
+ "Event pipeline: wraps-email-events / wraps-email-events-dlq queues, wraps-email-event-processor Lambda, EventBridge rule wraps-email-events-to-sqs",
19628
+ "",
19629
+ pc20.yellow(
19630
+ "Event streaming stops: the Wraps dashboard email timeline and event history for THIS ENTIRE AWS ACCOUNT stop receiving events. Historical events in DynamoDB are destroyed with the table. This cannot be undone."
19631
+ )
19632
+ ];
19633
+ if (emailService?.webhookSecret) {
19634
+ summaryLines.push(
19635
+ "",
19636
+ pc20.yellow(
19637
+ "This account is connected to the Wraps platform \u2014 the dashboard will stop receiving events for it."
19638
+ )
19639
+ );
19640
+ }
19641
+ clack18.note(summaryLines.join("\n"), "What will be destroyed");
19512
19642
  const confirmed = await clack18.confirm({
19513
19643
  message: pc20.red(
19514
19644
  "Are you sure you want to destroy all email infrastructure?"
@@ -19519,6 +19649,19 @@ async function emailDestroy(options) {
19519
19649
  clack18.cancel("Destruction cancelled.");
19520
19650
  process.exit(0);
19521
19651
  }
19652
+ if (emailService?.webhookSecret) {
19653
+ const typed = await clack18.text({
19654
+ message: `Type the AWS account id (${identity.accountId}) to confirm destruction of a platform-connected account:`
19655
+ });
19656
+ if (clack18.isCancel(typed) || typed !== identity.accountId) {
19657
+ clack18.cancel("Destruction cancelled \u2014 account id did not match.");
19658
+ process.exit(0);
19659
+ }
19660
+ }
19661
+ } else if (options.force && emailService?.webhookSecret) {
19662
+ clack18.log.warn(
19663
+ `--force skipped the typed account-id confirmation for platform-connected account ${identity.accountId}.`
19664
+ );
19522
19665
  }
19523
19666
  let shouldCleanDNS = false;
19524
19667
  let hostedZone = null;
@@ -19764,12 +19907,6 @@ Run ${pc20.cyan("wraps email init")} to deploy infrastructure again.
19764
19907
  // src/commands/email/doctor.ts
19765
19908
  init_esm_shims();
19766
19909
  init_events();
19767
- init_aws();
19768
- init_fs();
19769
- init_json_output();
19770
- init_metadata();
19771
- init_output();
19772
- init_scanner();
19773
19910
  import { DeleteTableCommand, DynamoDBClient as DynamoDBClient2 } from "@aws-sdk/client-dynamodb";
19774
19911
  import {
19775
19912
  DeleteRoleCommand,
@@ -19785,6 +19922,458 @@ import { DeleteTopicCommand, SNSClient as SNSClient2 } from "@aws-sdk/client-sns
19785
19922
  import * as clack19 from "@clack/prompts";
19786
19923
  import * as pulumi17 from "@pulumi/pulumi";
19787
19924
  import pc21 from "picocolors";
19925
+
19926
+ // src/utils/email/event-pipeline-check.ts
19927
+ init_esm_shims();
19928
+ init_errors();
19929
+ init_config_set_slug();
19930
+ var EVENT_DESTINATION_NAME = "wraps-email-eventbridge";
19931
+ var CONFIG_SET_FALLBACK = "wraps-email-tracking";
19932
+ var RULE_NAME = "wraps-email-events-to-sqs";
19933
+ var QUEUE_NAME = "wraps-email-events";
19934
+ var DLQ_NAME = "wraps-email-events-dlq";
19935
+ var LAMBDA_FUNCTION_NAME = "wraps-email-event-processor";
19936
+ var WEBHOOK_DESTINATION_NAME = "wraps-webhook-destination";
19937
+ var WEBHOOK_CONNECTION_NAME = "wraps-webhook-connection";
19938
+ var HISTORY_TABLE_NAME = "wraps-email-history";
19939
+ var UPGRADE_HINT = "wraps email upgrade";
19940
+ function summarizeError(error) {
19941
+ return error instanceof Error ? error.message : String(error);
19942
+ }
19943
+ function isQueueNotFoundError(error) {
19944
+ return isAWSNotFoundError(error) || error instanceof Error && error.name === "QueueDoesNotExist";
19945
+ }
19946
+ async function checkConfigSet(configSetName, region, optional) {
19947
+ const hop = `SES config set ${configSetName}`;
19948
+ try {
19949
+ const { SESv2Client: SESv2Client10, GetConfigurationSetEventDestinationsCommand: GetConfigurationSetEventDestinationsCommand2 } = await import("@aws-sdk/client-sesv2");
19950
+ const client = new SESv2Client10({ region });
19951
+ const response = await client.send(
19952
+ new GetConfigurationSetEventDestinationsCommand2({
19953
+ ConfigurationSetName: configSetName
19954
+ })
19955
+ );
19956
+ const destination = response.EventDestinations?.find(
19957
+ (d) => d.Name === EVENT_DESTINATION_NAME
19958
+ );
19959
+ if (destination?.Enabled) {
19960
+ return {
19961
+ hop,
19962
+ status: "pass",
19963
+ details: `${EVENT_DESTINATION_NAME} enabled`
19964
+ };
19965
+ }
19966
+ return {
19967
+ hop,
19968
+ status: "fail",
19969
+ details: `SES emits no events for ${configSetName} \u2014 redeploy with \`${UPGRADE_HINT}\``
19970
+ };
19971
+ } catch (error) {
19972
+ if (isAWSNotFoundError(error)) {
19973
+ if (optional) {
19974
+ return null;
19975
+ }
19976
+ return {
19977
+ hop,
19978
+ status: "fail",
19979
+ details: `Configuration set not found \u2014 redeploy with \`${UPGRADE_HINT}\``
19980
+ };
19981
+ }
19982
+ return {
19983
+ hop,
19984
+ status: "warn",
19985
+ details: `Could not check event destinations: ${summarizeError(error)}`
19986
+ };
19987
+ }
19988
+ }
19989
+ async function checkRule(region) {
19990
+ const hop = `EventBridge rule ${RULE_NAME}`;
19991
+ try {
19992
+ const { EventBridgeClient, DescribeRuleCommand } = await import("@aws-sdk/client-eventbridge");
19993
+ const client = new EventBridgeClient({ region });
19994
+ const response = await client.send(
19995
+ new DescribeRuleCommand({ Name: RULE_NAME })
19996
+ );
19997
+ if (response.State === "ENABLED") {
19998
+ return {
19999
+ exists: true,
20000
+ check: { hop, status: "pass", details: "Rule enabled" }
20001
+ };
20002
+ }
20003
+ return {
20004
+ exists: true,
20005
+ check: {
20006
+ hop,
20007
+ status: "fail",
20008
+ details: `Rule is ${response.State ?? "in an unknown state"} \u2014 run \`${UPGRADE_HINT}\` to re-enable`
20009
+ }
20010
+ };
20011
+ } catch (error) {
20012
+ if (isAWSNotFoundError(error)) {
20013
+ return {
20014
+ exists: false,
20015
+ check: {
20016
+ hop,
20017
+ status: "fail",
20018
+ details: `Rule not found \u2014 SES events have nowhere to go; run \`${UPGRADE_HINT}\``
20019
+ }
20020
+ };
20021
+ }
20022
+ return {
20023
+ exists: false,
20024
+ check: {
20025
+ hop,
20026
+ status: "warn",
20027
+ details: `Could not check rule: ${summarizeError(error)}`
20028
+ }
20029
+ };
20030
+ }
20031
+ }
20032
+ async function checkQueueExists(queueName, region, failDetails) {
20033
+ const hop = `SQS queue ${queueName}`;
20034
+ try {
20035
+ const { SQSClient, GetQueueUrlCommand } = await import("@aws-sdk/client-sqs");
20036
+ const client = new SQSClient({ region });
20037
+ await client.send(new GetQueueUrlCommand({ QueueName: queueName }));
20038
+ return { hop, status: "pass", details: "Queue exists" };
20039
+ } catch (error) {
20040
+ if (isQueueNotFoundError(error)) {
20041
+ return { hop, status: "fail", details: failDetails };
20042
+ }
20043
+ return {
20044
+ hop,
20045
+ status: "warn",
20046
+ details: `Could not check queue: ${summarizeError(error)}`
20047
+ };
20048
+ }
20049
+ }
20050
+ function findDuplicateTargetChecks(targets) {
20051
+ const idsByArn = /* @__PURE__ */ new Map();
20052
+ for (const target of targets) {
20053
+ if (!target.Arn) {
20054
+ continue;
20055
+ }
20056
+ const ids = idsByArn.get(target.Arn) ?? [];
20057
+ ids.push(target.Id ?? "unknown");
20058
+ idsByArn.set(target.Arn, ids);
20059
+ }
20060
+ const checks = [];
20061
+ for (const [arn, ids] of idsByArn) {
20062
+ if (ids.length > 1) {
20063
+ checks.push({
20064
+ hop: "EventBridge rule targets",
20065
+ status: "warn",
20066
+ details: `Duplicate targets for ${arn}: ${ids.join(", ")}`
20067
+ });
20068
+ }
20069
+ }
20070
+ return checks;
20071
+ }
20072
+ function checkSqsTargetOnRule(targets, region) {
20073
+ const failDetails = `events are being dropped \u2014 run \`${UPGRADE_HINT}\` to recreate the queue`;
20074
+ const sqsTarget = targets.find((t) => t.Arn?.split(":")[2] === "sqs");
20075
+ if (!sqsTarget) {
20076
+ return Promise.resolve({
20077
+ hop: `SQS queue ${QUEUE_NAME}`,
20078
+ status: "fail",
20079
+ details: `No SQS target on rule \u2014 ${failDetails}`
20080
+ });
20081
+ }
20082
+ return checkQueueExists(QUEUE_NAME, region, failDetails);
20083
+ }
20084
+ function checkPlatformWebhookTargetPresence(webhookTargetPresent, expectPlatformWebhook) {
20085
+ if (expectPlatformWebhook && !webhookTargetPresent) {
20086
+ return {
20087
+ hop: "Platform webhook target",
20088
+ status: "fail",
20089
+ details: `platform webhook target missing but metadata says connected \u2014 dashboard receives no events; run \`${UPGRADE_HINT}\``
20090
+ };
20091
+ }
20092
+ if (!expectPlatformWebhook && webhookTargetPresent) {
20093
+ return {
20094
+ hop: "Platform webhook target",
20095
+ status: "warn",
20096
+ details: "Webhook target exists on the rule but metadata has no webhookSecret \u2014 stack/metadata mismatch, possibly a lost metadata file"
20097
+ };
20098
+ }
20099
+ if (expectPlatformWebhook && webhookTargetPresent) {
20100
+ return {
20101
+ hop: "Platform webhook target",
20102
+ status: "pass",
20103
+ details: "Platform webhook target present"
20104
+ };
20105
+ }
20106
+ return null;
20107
+ }
20108
+ async function checkRuleTargets(region, expectPlatformWebhook) {
20109
+ try {
20110
+ const { EventBridgeClient, ListTargetsByRuleCommand } = await import("@aws-sdk/client-eventbridge");
20111
+ const client = new EventBridgeClient({ region });
20112
+ const response = await client.send(
20113
+ new ListTargetsByRuleCommand({ Rule: RULE_NAME })
20114
+ );
20115
+ const targets = response.Targets ?? [];
20116
+ const checks = [...findDuplicateTargetChecks(targets)];
20117
+ checks.push(await checkSqsTargetOnRule(targets, region));
20118
+ const webhookTargetPresent = targets.some(
20119
+ (t) => t.Arn?.includes(WEBHOOK_DESTINATION_NAME)
20120
+ );
20121
+ const webhookCheck = checkPlatformWebhookTargetPresence(
20122
+ webhookTargetPresent,
20123
+ expectPlatformWebhook
20124
+ );
20125
+ if (webhookCheck) {
20126
+ checks.push(webhookCheck);
20127
+ }
20128
+ return { checks, webhookTargetPresent };
20129
+ } catch (error) {
20130
+ return {
20131
+ checks: [
20132
+ {
20133
+ hop: "EventBridge rule targets",
20134
+ status: "warn",
20135
+ details: `Could not list rule targets: ${summarizeError(error)}`
20136
+ }
20137
+ ],
20138
+ webhookTargetPresent: false
20139
+ };
20140
+ }
20141
+ }
20142
+ async function checkWebhookDestination(region) {
20143
+ const hop = `EventBridge API destination ${WEBHOOK_DESTINATION_NAME}`;
20144
+ try {
20145
+ const { EventBridgeClient, DescribeApiDestinationCommand } = await import("@aws-sdk/client-eventbridge");
20146
+ const client = new EventBridgeClient({ region });
20147
+ const response = await client.send(
20148
+ new DescribeApiDestinationCommand({ Name: WEBHOOK_DESTINATION_NAME })
20149
+ );
20150
+ if (response.ApiDestinationState === "ACTIVE") {
20151
+ return { hop, status: "pass", details: "Active" };
20152
+ }
20153
+ return {
20154
+ hop,
20155
+ status: "fail",
20156
+ details: `API destination is ${response.ApiDestinationState ?? "in an unknown state"} \u2014 re-run \`${UPGRADE_HINT}\` to re-authorize`
20157
+ };
20158
+ } catch (error) {
20159
+ if (isAWSNotFoundError(error)) {
20160
+ return {
20161
+ hop,
20162
+ status: "fail",
20163
+ details: `API destination not found \u2014 re-run \`${UPGRADE_HINT}\``
20164
+ };
20165
+ }
20166
+ return {
20167
+ hop,
20168
+ status: "warn",
20169
+ details: `Could not check API destination: ${summarizeError(error)}`
20170
+ };
20171
+ }
20172
+ }
20173
+ async function checkWebhookConnection(region) {
20174
+ const hop = `EventBridge connection ${WEBHOOK_CONNECTION_NAME}`;
20175
+ try {
20176
+ const { EventBridgeClient, DescribeConnectionCommand } = await import("@aws-sdk/client-eventbridge");
20177
+ const client = new EventBridgeClient({ region });
20178
+ const response = await client.send(
20179
+ new DescribeConnectionCommand({ Name: WEBHOOK_CONNECTION_NAME })
20180
+ );
20181
+ if (response.ConnectionState === "AUTHORIZED") {
20182
+ return { hop, status: "pass", details: "Authorized" };
20183
+ }
20184
+ return {
20185
+ hop,
20186
+ status: "fail",
20187
+ details: `Connection is ${response.ConnectionState ?? "in an unknown state"} \u2014 re-run \`${UPGRADE_HINT}\` to re-authorize`
20188
+ };
20189
+ } catch (error) {
20190
+ if (isAWSNotFoundError(error)) {
20191
+ return {
20192
+ hop,
20193
+ status: "fail",
20194
+ details: `Connection not found \u2014 re-run \`${UPGRADE_HINT}\``
20195
+ };
20196
+ }
20197
+ return {
20198
+ hop,
20199
+ status: "warn",
20200
+ details: `Could not check connection: ${summarizeError(error)}`
20201
+ };
20202
+ }
20203
+ }
20204
+ async function checkDLQ(region) {
20205
+ const hop = `SQS DLQ ${DLQ_NAME}`;
20206
+ try {
20207
+ const { SQSClient, GetQueueUrlCommand, GetQueueAttributesCommand } = await import("@aws-sdk/client-sqs");
20208
+ const client = new SQSClient({ region });
20209
+ const urlResponse = await client.send(
20210
+ new GetQueueUrlCommand({ QueueName: DLQ_NAME })
20211
+ );
20212
+ const attrResponse = await client.send(
20213
+ new GetQueueAttributesCommand({
20214
+ QueueUrl: urlResponse.QueueUrl,
20215
+ AttributeNames: ["ApproximateNumberOfMessages"]
20216
+ })
20217
+ );
20218
+ const count = Number(
20219
+ attrResponse.Attributes?.ApproximateNumberOfMessages ?? "0"
20220
+ );
20221
+ if (count > 0) {
20222
+ return {
20223
+ hop,
20224
+ status: "warn",
20225
+ details: `${count} dead-lettered event(s) \u2014 investigate before they age out`
20226
+ };
20227
+ }
20228
+ return { hop, status: "pass", details: "Empty" };
20229
+ } catch (error) {
20230
+ if (isQueueNotFoundError(error)) {
20231
+ return {
20232
+ hop,
20233
+ status: "fail",
20234
+ details: `Dead-letter queue missing \u2014 run \`${UPGRADE_HINT}\` to recreate it`
20235
+ };
20236
+ }
20237
+ return {
20238
+ hop,
20239
+ status: "warn",
20240
+ details: `Could not check DLQ: ${summarizeError(error)}`
20241
+ };
20242
+ }
20243
+ }
20244
+ async function checkEventSourceMapping(region) {
20245
+ const hop = `Lambda event source mapping ${LAMBDA_FUNCTION_NAME}`;
20246
+ try {
20247
+ const { LambdaClient: LambdaClient3, ListEventSourceMappingsCommand } = await import("@aws-sdk/client-lambda");
20248
+ const client = new LambdaClient3({ region });
20249
+ const response = await client.send(
20250
+ new ListEventSourceMappingsCommand({ FunctionName: LAMBDA_FUNCTION_NAME })
20251
+ );
20252
+ const mapping = response.EventSourceMappings?.find(
20253
+ (m) => m.EventSourceArn?.endsWith(`:${QUEUE_NAME}`)
20254
+ );
20255
+ if (!mapping) {
20256
+ return {
20257
+ hop,
20258
+ status: "fail",
20259
+ details: `No event source mapping from ${QUEUE_NAME} \u2014 Lambda never runs; run \`${UPGRADE_HINT}\``
20260
+ };
20261
+ }
20262
+ if (mapping.State === "Enabled") {
20263
+ return { hop, status: "pass", details: "Mapping enabled" };
20264
+ }
20265
+ return {
20266
+ hop,
20267
+ status: "fail",
20268
+ details: `Mapping is ${mapping.State ?? "in an unknown state"} \u2014 run \`${UPGRADE_HINT}\` to re-enable`
20269
+ };
20270
+ } catch (error) {
20271
+ if (isAWSNotFoundError(error)) {
20272
+ return {
20273
+ hop,
20274
+ status: "fail",
20275
+ details: `Lambda function not found \u2014 run \`${UPGRADE_HINT}\``
20276
+ };
20277
+ }
20278
+ return {
20279
+ hop,
20280
+ status: "warn",
20281
+ details: `Could not check event source mapping: ${summarizeError(error)}`
20282
+ };
20283
+ }
20284
+ }
20285
+ async function checkHistoryTable(region) {
20286
+ const hop = `DynamoDB table ${HISTORY_TABLE_NAME}`;
20287
+ try {
20288
+ const { DynamoDBClient: DynamoDBClient7, DescribeTableCommand: DescribeTableCommand2 } = await import("@aws-sdk/client-dynamodb");
20289
+ const client = new DynamoDBClient7({ region });
20290
+ const response = await client.send(
20291
+ new DescribeTableCommand2({ TableName: HISTORY_TABLE_NAME })
20292
+ );
20293
+ const status2 = response.Table?.TableStatus;
20294
+ if (status2 === "ACTIVE") {
20295
+ return { hop, status: "pass", details: "Active" };
20296
+ }
20297
+ return {
20298
+ hop,
20299
+ status: "fail",
20300
+ details: `Table status is ${status2 ?? "unknown"} \u2014 event log may be unavailable; run \`${UPGRADE_HINT}\``
20301
+ };
20302
+ } catch (error) {
20303
+ if (isAWSNotFoundError(error)) {
20304
+ return {
20305
+ hop,
20306
+ status: "fail",
20307
+ details: `Table not found \u2014 event history is not being recorded; run \`${UPGRADE_HINT}\``
20308
+ };
20309
+ }
20310
+ return {
20311
+ hop,
20312
+ status: "warn",
20313
+ details: `Could not check table: ${summarizeError(error)}`
20314
+ };
20315
+ }
20316
+ }
20317
+ async function checkEventPipeline(params) {
20318
+ const { region, domains, expectPlatformWebhook } = params;
20319
+ const checks = [];
20320
+ const uniqueConfigSetNames = Array.from(
20321
+ new Set(domains.map((d) => domainToConfigSetName(d)))
20322
+ );
20323
+ if (uniqueConfigSetNames.length === 0) {
20324
+ const check2 = await checkConfigSet(CONFIG_SET_FALLBACK, region, false);
20325
+ if (check2) {
20326
+ checks.push(check2);
20327
+ }
20328
+ } else {
20329
+ for (const name of uniqueConfigSetNames) {
20330
+ const check2 = await checkConfigSet(name, region, false);
20331
+ if (check2) {
20332
+ checks.push(check2);
20333
+ }
20334
+ }
20335
+ const fallbackCheck = await checkConfigSet(
20336
+ CONFIG_SET_FALLBACK,
20337
+ region,
20338
+ true
20339
+ );
20340
+ if (fallbackCheck) {
20341
+ checks.push(fallbackCheck);
20342
+ }
20343
+ }
20344
+ const ruleResult = await checkRule(region);
20345
+ checks.push(ruleResult.check);
20346
+ let webhookTargetPresent = false;
20347
+ if (ruleResult.exists) {
20348
+ const targetsResult = await checkRuleTargets(region, expectPlatformWebhook);
20349
+ checks.push(...targetsResult.checks);
20350
+ webhookTargetPresent = targetsResult.webhookTargetPresent;
20351
+ }
20352
+ if (webhookTargetPresent) {
20353
+ checks.push(await checkWebhookDestination(region));
20354
+ checks.push(await checkWebhookConnection(region));
20355
+ }
20356
+ checks.push(await checkDLQ(region));
20357
+ checks.push(await checkEventSourceMapping(region));
20358
+ checks.push(await checkHistoryTable(region));
20359
+ return checks;
20360
+ }
20361
+
20362
+ // src/commands/email/doctor.ts
20363
+ init_aws();
20364
+ init_fs();
20365
+ init_json_output();
20366
+ init_metadata();
20367
+ init_output();
20368
+ init_scanner();
20369
+ function pipelineChecksToDoctorResults(pipelineChecks) {
20370
+ return pipelineChecks.map((check2) => ({
20371
+ status: check2.status,
20372
+ category: "Event Pipeline",
20373
+ name: check2.hop,
20374
+ details: check2.details
20375
+ }));
20376
+ }
19788
20377
  function runResourceDiagnostics(wrapsResources, hasStack) {
19789
20378
  const results = [];
19790
20379
  const orphanSuffix = hasStack ? void 0 : " (orphan \u2014 no Pulumi state)";
@@ -19880,11 +20469,11 @@ async function emailDoctor(options) {
19880
20469
  async () => validateAWSCredentials()
19881
20470
  );
19882
20471
  let region = options.region || await getAWSRegion();
20472
+ const emailConnections = await findConnectionsWithService(
20473
+ identity.accountId,
20474
+ "email"
20475
+ );
19883
20476
  if (!(options.region || process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION)) {
19884
- const emailConnections = await findConnectionsWithService(
19885
- identity.accountId,
19886
- "email"
19887
- );
19888
20477
  if (emailConnections.length === 1) {
19889
20478
  region = emailConnections[0].region;
19890
20479
  } else if (emailConnections.length > 1 && !isJsonMode()) {
@@ -19919,8 +20508,23 @@ async function emailDoctor(options) {
19919
20508
  } catch (_error) {
19920
20509
  hasStack = false;
19921
20510
  }
20511
+ const emailConnection = emailConnections.find((c) => c.region === region);
20512
+ let pipelineResults = [];
20513
+ if (emailConnection) {
20514
+ const emailService = emailConnection.services.email;
20515
+ const domains = emailService?.config ? getAllTrackedDomains(emailConnection).map((d) => d.domain) : [];
20516
+ const expectPlatformWebhook = Boolean(emailService?.webhookSecret);
20517
+ const pipelineChecks = await progress.execute(
20518
+ "Checking event pipeline",
20519
+ async () => checkEventPipeline({ region, domains, expectPlatformWebhook })
20520
+ );
20521
+ pipelineResults = pipelineChecksToDoctorResults(pipelineChecks);
20522
+ }
19922
20523
  progress.stop();
19923
- const results = runResourceDiagnostics(wrapsResources, hasStack);
20524
+ const results = [
20525
+ ...runResourceDiagnostics(wrapsResources, hasStack),
20526
+ ...pipelineResults
20527
+ ];
19924
20528
  if (isJsonMode()) {
19925
20529
  jsonSuccess("email.doctor", {
19926
20530
  region,
@@ -19935,7 +20539,7 @@ async function emailDoctor(options) {
19935
20539
  });
19936
20540
  return;
19937
20541
  }
19938
- if (totalResources === 0) {
20542
+ if (results.length === 0) {
19939
20543
  clack19.log.info("No wraps-* resources found in this region.");
19940
20544
  clack19.outro(pc21.dim("Your AWS account is clean."));
19941
20545
  } else {
@@ -21922,7 +22526,7 @@ import {
21922
22526
  UpdateReceiptRuleCommand
21923
22527
  } from "@aws-sdk/client-ses";
21924
22528
  var RULE_SET_NAME = "wraps-inbound-rules";
21925
- var RULE_NAME = "wraps-inbound-catch-all";
22529
+ var RULE_NAME2 = "wraps-inbound-catch-all";
21926
22530
  function createSESClient(region) {
21927
22531
  return new SESClient4({ region });
21928
22532
  }
@@ -21947,13 +22551,13 @@ async function createReceiptRule(region, receivingDomain, s3BucketName) {
21947
22551
  await ses.send(
21948
22552
  new DescribeReceiptRuleCommand({
21949
22553
  RuleSetName: RULE_SET_NAME,
21950
- RuleName: RULE_NAME
22554
+ RuleName: RULE_NAME2
21951
22555
  })
21952
22556
  );
21953
22557
  await ses.send(
21954
22558
  new DeleteReceiptRuleCommand({
21955
22559
  RuleSetName: RULE_SET_NAME,
21956
- RuleName: RULE_NAME
22560
+ RuleName: RULE_NAME2
21957
22561
  })
21958
22562
  );
21959
22563
  } catch (error) {
@@ -21965,7 +22569,7 @@ async function createReceiptRule(region, receivingDomain, s3BucketName) {
21965
22569
  new CreateReceiptRuleCommand({
21966
22570
  RuleSetName: RULE_SET_NAME,
21967
22571
  Rule: {
21968
- Name: RULE_NAME,
22572
+ Name: RULE_NAME2,
21969
22573
  Enabled: true,
21970
22574
  ScanEnabled: true,
21971
22575
  // Enable spam and virus scanning
@@ -22013,7 +22617,7 @@ async function deleteReceiptRule(region) {
22013
22617
  await ses.send(
22014
22618
  new DeleteReceiptRuleCommand({
22015
22619
  RuleSetName: RULE_SET_NAME,
22016
- RuleName: RULE_NAME
22620
+ RuleName: RULE_NAME2
22017
22621
  })
22018
22622
  );
22019
22623
  } catch (error) {
@@ -22048,7 +22652,7 @@ async function addDomainToReceiptRule(region, domain, s3BucketName) {
22048
22652
  const response = await ses.send(
22049
22653
  new DescribeReceiptRuleCommand({
22050
22654
  RuleSetName: RULE_SET_NAME,
22051
- RuleName: RULE_NAME
22655
+ RuleName: RULE_NAME2
22052
22656
  })
22053
22657
  );
22054
22658
  const existingRecipients = response.Rule?.Recipients ?? [];
@@ -22059,7 +22663,7 @@ async function addDomainToReceiptRule(region, domain, s3BucketName) {
22059
22663
  new UpdateReceiptRuleCommand({
22060
22664
  RuleSetName: RULE_SET_NAME,
22061
22665
  Rule: {
22062
- Name: RULE_NAME,
22666
+ Name: RULE_NAME2,
22063
22667
  Enabled: response.Rule?.Enabled,
22064
22668
  ScanEnabled: response.Rule?.ScanEnabled,
22065
22669
  TlsPolicy: response.Rule?.TlsPolicy,
@@ -22084,7 +22688,7 @@ async function removeDomainFromReceiptRule(region, domain) {
22084
22688
  const response = await ses.send(
22085
22689
  new DescribeReceiptRuleCommand({
22086
22690
  RuleSetName: RULE_SET_NAME,
22087
- RuleName: RULE_NAME
22691
+ RuleName: RULE_NAME2
22088
22692
  })
22089
22693
  );
22090
22694
  const existingRecipients = response.Rule?.Recipients ?? [];
@@ -22097,7 +22701,7 @@ async function removeDomainFromReceiptRule(region, domain) {
22097
22701
  new UpdateReceiptRuleCommand({
22098
22702
  RuleSetName: RULE_SET_NAME,
22099
22703
  Rule: {
22100
- Name: RULE_NAME,
22704
+ Name: RULE_NAME2,
22101
22705
  Enabled: response.Rule?.Enabled,
22102
22706
  ScanEnabled: response.Rule?.ScanEnabled,
22103
22707
  TlsPolicy: response.Rule?.TlsPolicy,
@@ -22119,7 +22723,7 @@ async function getReceiptRuleDomains(region) {
22119
22723
  const response = await ses.send(
22120
22724
  new DescribeReceiptRuleCommand({
22121
22725
  RuleSetName: RULE_SET_NAME,
22122
- RuleName: RULE_NAME
22726
+ RuleName: RULE_NAME2
22123
22727
  })
22124
22728
  );
22125
22729
  return response.Rule?.Recipients ?? [];
@@ -26840,7 +27444,7 @@ init_esm_shims();
26840
27444
  // src/utils/email/template-mustache-case.ts
26841
27445
  init_esm_shims();
26842
27446
  var HANDLEBARS_BLOCK_HELPERS = /* @__PURE__ */ new Set(["if", "unless", "each", "with"]);
26843
- function normalizePlainTextMustaches(text12, canonicalVars) {
27447
+ function normalizePlainTextMustaches(text13, canonicalVars) {
26844
27448
  const canonicalByLower = /* @__PURE__ */ new Map();
26845
27449
  for (const name of canonicalVars) {
26846
27450
  canonicalByLower.set(name.toLowerCase(), name);
@@ -26848,7 +27452,7 @@ function normalizePlainTextMustaches(text12, canonicalVars) {
26848
27452
  function restoreIdentifier(name) {
26849
27453
  return canonicalByLower.get(name.toLowerCase()) ?? name;
26850
27454
  }
26851
- return text12.replace(
27455
+ return text13.replace(
26852
27456
  /\{\{([#/]?)([A-Z][A-Z0-9_]*(?:\.[A-Z0-9_]+)*)((?:\s+[A-Z0-9_.]+)*)\s*\}\}/g,
26853
27457
  (match, sigil, name, args) => {
26854
27458
  const lower = name.toLowerCase();
@@ -26898,8 +27502,8 @@ async function renderTemplateWithProxy(Component) {
26898
27502
  match = regex.exec(html);
26899
27503
  }
26900
27504
  const canonicalVars = /* @__PURE__ */ new Set([...htmlVarNames, ...accessedProps]);
26901
- const text12 = normalizePlainTextMustaches(rawText, canonicalVars);
26902
- return { html, text: text12, accessedProps };
27505
+ const text13 = normalizePlainTextMustaches(rawText, canonicalVars);
27506
+ return { html, text: text13, accessedProps };
26903
27507
  }
26904
27508
 
26905
27509
  // src/commands/email/templates/push.ts
@@ -27138,11 +27742,11 @@ async function compileTemplate(filePath, slug, source, sourceHash, wrapsDir) {
27138
27742
  "Template must have a default export (React component function)"
27139
27743
  );
27140
27744
  }
27141
- const { html, text: text12, accessedProps } = await renderTemplateWithProxy(Component);
27745
+ const { html, text: text13, accessedProps } = await renderTemplateWithProxy(Component);
27142
27746
  const variables = mergeVariables(extractVariables(html), accessedProps);
27143
27747
  const sesSubject = transformVariablesForSes(subject);
27144
27748
  const sesHtml = transformVariablesForSes(html);
27145
- const sesText = transformVariablesForSes(text12);
27749
+ const sesText = transformVariablesForSes(text13);
27146
27750
  const sesTemplateName = slug.replace(/[^a-zA-Z0-9-_]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").substring(0, 64);
27147
27751
  return {
27148
27752
  slug,
@@ -29545,6 +30149,28 @@ ${pc37.green("\u2713")} ${pc37.bold("Upgrade complete!")}
29545
30149
  }
29546
30150
  }
29547
30151
  await saveConnectionMetadata(metadata);
30152
+ try {
30153
+ const domains = getAllTrackedDomains(metadata).map((d) => d.domain);
30154
+ const pipelineChecks = await checkEventPipeline({
30155
+ region: outputs.region ?? region,
30156
+ domains,
30157
+ expectPlatformWebhook: Boolean(metadata.services.email?.webhookSecret)
30158
+ });
30159
+ const issues = pipelineChecks.filter((check2) => check2.status !== "pass");
30160
+ if (issues.length > 0 && !isJsonMode()) {
30161
+ for (const issue of issues) {
30162
+ clack36.log.warn(
30163
+ `Post-deploy pipeline check: ${issue.hop} \u2014 ${issue.details}`
30164
+ );
30165
+ }
30166
+ }
30167
+ } catch (error) {
30168
+ if (!isJsonMode()) {
30169
+ clack36.log.warn(
30170
+ `Post-deploy pipeline check failed to run: ${error instanceof Error ? error.message : String(error)}`
30171
+ );
30172
+ }
30173
+ }
29548
30174
  if (isJsonMode()) {
29549
30175
  jsonSuccess("email.upgrade", {
29550
30176
  upgraded: true,