@wraps.dev/cli 2.23.3 → 2.24.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/api-lambda.zip +0 -0
- package/dist/cli.js +623 -42
- package/dist/cli.js.map +1 -1
- package/dist/console/assets/{index-BiEvZUP2.js → index-5HGEQ1Ul.js} +58 -58
- package/dist/console/assets/index-BP4FpEpn.css +1 -0
- package/dist/console/index.html +2 -2
- package/dist/lambda/event-processor/.bundled +1 -1
- package/dist/lambda/event-processor/index.js +3 -2
- package/dist/lambda/inbound-processor/.bundled +1 -1
- package/dist/lambda/inbound-processor/index.js +60 -67
- package/dist/lambda/sms-event-processor/.bundled +1 -1
- package/dist/lambda/sms-event-processor/index.js +3 -2
- package/dist/selfhost-migrations/0060_harsh_nehzno.sql +1 -0
- package/dist/selfhost-migrations/0061_flashy_spirit.sql +1 -0
- package/dist/selfhost-migrations/0062_next_timeslip.sql +2 -0
- package/dist/selfhost-migrations/meta/0060_snapshot.json +7125 -0
- package/dist/selfhost-migrations/meta/0061_snapshot.json +7131 -0
- package/dist/selfhost-migrations/meta/0062_snapshot.json +7143 -0
- package/dist/selfhost-migrations/meta/_journal.json +22 -1
- package/package.json +37 -36
- package/dist/cli.d.ts +0 -1
- package/dist/console/assets/index-DTBHdBwz.css +0 -1
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.
|
|
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.
|
|
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());
|
|
@@ -17839,8 +17839,19 @@ async function createEventBridgeResources(config2) {
|
|
|
17839
17839
|
const target = new aws7.cloudwatch.EventTarget("wraps-email-events-target", {
|
|
17840
17840
|
rule: rule.name,
|
|
17841
17841
|
eventBusName,
|
|
17842
|
-
arn: config2.queueArn
|
|
17842
|
+
arn: config2.queueArn,
|
|
17843
|
+
deadLetterConfig: { arn: config2.dlqArn }
|
|
17843
17844
|
});
|
|
17845
|
+
const archive = new aws7.cloudwatch.EventArchive(
|
|
17846
|
+
"wraps-email-events-archive",
|
|
17847
|
+
{
|
|
17848
|
+
name: "wraps-email-events-archive",
|
|
17849
|
+
eventSourceArn: config2.eventBusArn,
|
|
17850
|
+
description: "Wraps: 30-day archive of SES events for outage replay (see wraps email doctor)",
|
|
17851
|
+
retentionDays: 30,
|
|
17852
|
+
eventPattern: JSON.stringify({ source: ["aws.ses"] })
|
|
17853
|
+
}
|
|
17854
|
+
);
|
|
17844
17855
|
let webhookConnection;
|
|
17845
17856
|
let webhookApiDestination;
|
|
17846
17857
|
let webhookTarget;
|
|
@@ -17911,7 +17922,8 @@ async function createEventBridgeResources(config2) {
|
|
|
17911
17922
|
rule: rule.name,
|
|
17912
17923
|
eventBusName,
|
|
17913
17924
|
arn: webhookApiDestination.arn,
|
|
17914
|
-
roleArn: webhookRole.arn
|
|
17925
|
+
roleArn: webhookRole.arn,
|
|
17926
|
+
deadLetterConfig: { arn: config2.dlqArn }
|
|
17915
17927
|
});
|
|
17916
17928
|
}
|
|
17917
17929
|
let userWebhookConnection;
|
|
@@ -17929,9 +17941,33 @@ async function createEventBridgeResources(config2) {
|
|
|
17929
17941
|
userWebhookApiDestination = userWebhookResources.apiDestination;
|
|
17930
17942
|
userWebhookTarget = userWebhookResources.target;
|
|
17931
17943
|
}
|
|
17944
|
+
const alarmActions = config2.alertTopicArn ? [config2.alertTopicArn] : void 0;
|
|
17945
|
+
const failedInvocationsAlarm = new aws7.cloudwatch.MetricAlarm(
|
|
17946
|
+
"wraps-email-events-delivery-failures",
|
|
17947
|
+
{
|
|
17948
|
+
name: "wraps-email-events-delivery-failures",
|
|
17949
|
+
namespace: "AWS/Events",
|
|
17950
|
+
metricName: "FailedInvocations",
|
|
17951
|
+
dimensions: { RuleName: "wraps-email-events-to-sqs" },
|
|
17952
|
+
statistic: "Sum",
|
|
17953
|
+
period: 300,
|
|
17954
|
+
evaluationPeriods: 3,
|
|
17955
|
+
threshold: 1,
|
|
17956
|
+
comparisonOperator: "GreaterThanOrEqualToThreshold",
|
|
17957
|
+
treatMissingData: "notBreaching",
|
|
17958
|
+
alarmDescription: "SES event delivery from EventBridge is failing (dead target or deauthorized webhook). Event history is being lost \u2014 run `wraps email doctor`.",
|
|
17959
|
+
...alarmActions ? { alarmActions, okActions: alarmActions } : {},
|
|
17960
|
+
tags: {
|
|
17961
|
+
ManagedBy: "wraps-cli",
|
|
17962
|
+
Service: "email"
|
|
17963
|
+
}
|
|
17964
|
+
}
|
|
17965
|
+
);
|
|
17932
17966
|
return {
|
|
17933
17967
|
rule,
|
|
17934
17968
|
target,
|
|
17969
|
+
failedInvocationsAlarm,
|
|
17970
|
+
archive,
|
|
17935
17971
|
webhookConnection,
|
|
17936
17972
|
webhookApiDestination,
|
|
17937
17973
|
webhookTarget,
|
|
@@ -18376,6 +18412,30 @@ async function createSQSResources(config2) {
|
|
|
18376
18412
|
...SQS_TIMEOUTS,
|
|
18377
18413
|
...dlqUrl ? { import: dlqUrl } : {}
|
|
18378
18414
|
});
|
|
18415
|
+
const ruleArn = `arn:aws:events:${config2.region}:${config2.accountId}:rule/wraps-email-events-to-sqs`;
|
|
18416
|
+
new aws11.sqs.QueuePolicy("wraps-email-events-dlq-policy", {
|
|
18417
|
+
queueUrl: dlq.url,
|
|
18418
|
+
policy: dlq.arn.apply(
|
|
18419
|
+
(dlqArn) => JSON.stringify({
|
|
18420
|
+
Version: "2012-10-17",
|
|
18421
|
+
Statement: [
|
|
18422
|
+
{
|
|
18423
|
+
Effect: "Allow",
|
|
18424
|
+
Principal: {
|
|
18425
|
+
Service: "events.amazonaws.com"
|
|
18426
|
+
},
|
|
18427
|
+
Action: "sqs:SendMessage",
|
|
18428
|
+
Resource: dlqArn,
|
|
18429
|
+
Condition: {
|
|
18430
|
+
ArnEquals: {
|
|
18431
|
+
"aws:SourceArn": ruleArn
|
|
18432
|
+
}
|
|
18433
|
+
}
|
|
18434
|
+
}
|
|
18435
|
+
]
|
|
18436
|
+
})
|
|
18437
|
+
)
|
|
18438
|
+
});
|
|
18379
18439
|
const queueConfig = {
|
|
18380
18440
|
name: queueName,
|
|
18381
18441
|
visibilityTimeoutSeconds: 300,
|
|
@@ -18505,13 +18565,27 @@ async function deployEmailStack(config2) {
|
|
|
18505
18565
|
}
|
|
18506
18566
|
let sqsResources;
|
|
18507
18567
|
if (emailConfig.eventTracking?.enabled) {
|
|
18508
|
-
sqsResources = await createSQSResources({
|
|
18568
|
+
sqsResources = await createSQSResources({
|
|
18569
|
+
region: config2.region,
|
|
18570
|
+
accountId
|
|
18571
|
+
});
|
|
18572
|
+
}
|
|
18573
|
+
let alertingResources;
|
|
18574
|
+
if (emailConfig.alerts?.enabled) {
|
|
18575
|
+
alertingResources = await createAlertingResources({
|
|
18576
|
+
alertConfig: emailConfig.alerts,
|
|
18577
|
+
configSetName: sesResources?.configSet.configurationSetName,
|
|
18578
|
+
dlqName: sqsResources ? "wraps-email-events-dlq" : void 0,
|
|
18579
|
+
region: config2.region
|
|
18580
|
+
});
|
|
18509
18581
|
}
|
|
18510
18582
|
if (emailConfig.eventTracking?.enabled && sesResources && sqsResources) {
|
|
18511
18583
|
await createEventBridgeResources({
|
|
18512
18584
|
eventBusArn: sesResources.eventBus.arn,
|
|
18513
18585
|
queueArn: sqsResources.queue.arn,
|
|
18514
18586
|
queueUrl: sqsResources.queue.url,
|
|
18587
|
+
dlqArn: sqsResources.dlq.arn,
|
|
18588
|
+
alertTopicArn: alertingResources?.topic.arn,
|
|
18515
18589
|
// Include webhook config if provided (for Wraps platform integration)
|
|
18516
18590
|
webhook: config2.webhook,
|
|
18517
18591
|
// Include user webhook config if provided
|
|
@@ -18552,15 +18626,6 @@ async function deployEmailStack(config2) {
|
|
|
18552
18626
|
region: config2.region
|
|
18553
18627
|
});
|
|
18554
18628
|
}
|
|
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
18629
|
let inboundResources;
|
|
18565
18630
|
let replySecretsOutput;
|
|
18566
18631
|
if (emailConfig.inbound?.enabled) {
|
|
@@ -19509,6 +19574,26 @@ async function emailDestroy(options) {
|
|
|
19509
19574
|
const domain = emailConfig?.domain;
|
|
19510
19575
|
const storedStackName = emailService?.pulumiStackName;
|
|
19511
19576
|
if (!(options.force || options.preview)) {
|
|
19577
|
+
const additionalDomainNames = (emailConfig?.additionalDomains ?? []).map((d) => d.domain).filter(Boolean);
|
|
19578
|
+
const domainList = [domain, ...additionalDomainNames].filter(Boolean);
|
|
19579
|
+
const summaryLines = [
|
|
19580
|
+
domainList.length > 0 ? `Domain(s): ${domainList.join(", ")}` : "Domain(s): none configured",
|
|
19581
|
+
"SES configuration sets and event destinations for the above domain(s)",
|
|
19582
|
+
"Event pipeline: wraps-email-events / wraps-email-events-dlq queues, wraps-email-event-processor Lambda, EventBridge rule wraps-email-events-to-sqs",
|
|
19583
|
+
"",
|
|
19584
|
+
pc20.yellow(
|
|
19585
|
+
"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."
|
|
19586
|
+
)
|
|
19587
|
+
];
|
|
19588
|
+
if (emailService?.webhookSecret) {
|
|
19589
|
+
summaryLines.push(
|
|
19590
|
+
"",
|
|
19591
|
+
pc20.yellow(
|
|
19592
|
+
"This account is connected to the Wraps platform \u2014 the dashboard will stop receiving events for it."
|
|
19593
|
+
)
|
|
19594
|
+
);
|
|
19595
|
+
}
|
|
19596
|
+
clack18.note(summaryLines.join("\n"), "What will be destroyed");
|
|
19512
19597
|
const confirmed = await clack18.confirm({
|
|
19513
19598
|
message: pc20.red(
|
|
19514
19599
|
"Are you sure you want to destroy all email infrastructure?"
|
|
@@ -19519,6 +19604,19 @@ async function emailDestroy(options) {
|
|
|
19519
19604
|
clack18.cancel("Destruction cancelled.");
|
|
19520
19605
|
process.exit(0);
|
|
19521
19606
|
}
|
|
19607
|
+
if (emailService?.webhookSecret) {
|
|
19608
|
+
const typed = await clack18.text({
|
|
19609
|
+
message: `Type the AWS account id (${identity.accountId}) to confirm destruction of a platform-connected account:`
|
|
19610
|
+
});
|
|
19611
|
+
if (clack18.isCancel(typed) || typed !== identity.accountId) {
|
|
19612
|
+
clack18.cancel("Destruction cancelled \u2014 account id did not match.");
|
|
19613
|
+
process.exit(0);
|
|
19614
|
+
}
|
|
19615
|
+
}
|
|
19616
|
+
} else if (options.force && emailService?.webhookSecret) {
|
|
19617
|
+
clack18.log.warn(
|
|
19618
|
+
`--force skipped the typed account-id confirmation for platform-connected account ${identity.accountId}.`
|
|
19619
|
+
);
|
|
19522
19620
|
}
|
|
19523
19621
|
let shouldCleanDNS = false;
|
|
19524
19622
|
let hostedZone = null;
|
|
@@ -19764,12 +19862,6 @@ Run ${pc20.cyan("wraps email init")} to deploy infrastructure again.
|
|
|
19764
19862
|
// src/commands/email/doctor.ts
|
|
19765
19863
|
init_esm_shims();
|
|
19766
19864
|
init_events();
|
|
19767
|
-
init_aws();
|
|
19768
|
-
init_fs();
|
|
19769
|
-
init_json_output();
|
|
19770
|
-
init_metadata();
|
|
19771
|
-
init_output();
|
|
19772
|
-
init_scanner();
|
|
19773
19865
|
import { DeleteTableCommand, DynamoDBClient as DynamoDBClient2 } from "@aws-sdk/client-dynamodb";
|
|
19774
19866
|
import {
|
|
19775
19867
|
DeleteRoleCommand,
|
|
@@ -19785,6 +19877,458 @@ import { DeleteTopicCommand, SNSClient as SNSClient2 } from "@aws-sdk/client-sns
|
|
|
19785
19877
|
import * as clack19 from "@clack/prompts";
|
|
19786
19878
|
import * as pulumi17 from "@pulumi/pulumi";
|
|
19787
19879
|
import pc21 from "picocolors";
|
|
19880
|
+
|
|
19881
|
+
// src/utils/email/event-pipeline-check.ts
|
|
19882
|
+
init_esm_shims();
|
|
19883
|
+
init_errors();
|
|
19884
|
+
init_config_set_slug();
|
|
19885
|
+
var EVENT_DESTINATION_NAME = "wraps-email-eventbridge";
|
|
19886
|
+
var CONFIG_SET_FALLBACK = "wraps-email-tracking";
|
|
19887
|
+
var RULE_NAME = "wraps-email-events-to-sqs";
|
|
19888
|
+
var QUEUE_NAME = "wraps-email-events";
|
|
19889
|
+
var DLQ_NAME = "wraps-email-events-dlq";
|
|
19890
|
+
var LAMBDA_FUNCTION_NAME = "wraps-email-event-processor";
|
|
19891
|
+
var WEBHOOK_DESTINATION_NAME = "wraps-webhook-destination";
|
|
19892
|
+
var WEBHOOK_CONNECTION_NAME = "wraps-webhook-connection";
|
|
19893
|
+
var HISTORY_TABLE_NAME = "wraps-email-history";
|
|
19894
|
+
var UPGRADE_HINT = "wraps email upgrade";
|
|
19895
|
+
function summarizeError(error) {
|
|
19896
|
+
return error instanceof Error ? error.message : String(error);
|
|
19897
|
+
}
|
|
19898
|
+
function isQueueNotFoundError(error) {
|
|
19899
|
+
return isAWSNotFoundError(error) || error instanceof Error && error.name === "QueueDoesNotExist";
|
|
19900
|
+
}
|
|
19901
|
+
async function checkConfigSet(configSetName, region, optional) {
|
|
19902
|
+
const hop = `SES config set ${configSetName}`;
|
|
19903
|
+
try {
|
|
19904
|
+
const { SESv2Client: SESv2Client10, GetConfigurationSetEventDestinationsCommand: GetConfigurationSetEventDestinationsCommand2 } = await import("@aws-sdk/client-sesv2");
|
|
19905
|
+
const client = new SESv2Client10({ region });
|
|
19906
|
+
const response = await client.send(
|
|
19907
|
+
new GetConfigurationSetEventDestinationsCommand2({
|
|
19908
|
+
ConfigurationSetName: configSetName
|
|
19909
|
+
})
|
|
19910
|
+
);
|
|
19911
|
+
const destination = response.EventDestinations?.find(
|
|
19912
|
+
(d) => d.Name === EVENT_DESTINATION_NAME
|
|
19913
|
+
);
|
|
19914
|
+
if (destination?.Enabled) {
|
|
19915
|
+
return {
|
|
19916
|
+
hop,
|
|
19917
|
+
status: "pass",
|
|
19918
|
+
details: `${EVENT_DESTINATION_NAME} enabled`
|
|
19919
|
+
};
|
|
19920
|
+
}
|
|
19921
|
+
return {
|
|
19922
|
+
hop,
|
|
19923
|
+
status: "fail",
|
|
19924
|
+
details: `SES emits no events for ${configSetName} \u2014 redeploy with \`${UPGRADE_HINT}\``
|
|
19925
|
+
};
|
|
19926
|
+
} catch (error) {
|
|
19927
|
+
if (isAWSNotFoundError(error)) {
|
|
19928
|
+
if (optional) {
|
|
19929
|
+
return null;
|
|
19930
|
+
}
|
|
19931
|
+
return {
|
|
19932
|
+
hop,
|
|
19933
|
+
status: "fail",
|
|
19934
|
+
details: `Configuration set not found \u2014 redeploy with \`${UPGRADE_HINT}\``
|
|
19935
|
+
};
|
|
19936
|
+
}
|
|
19937
|
+
return {
|
|
19938
|
+
hop,
|
|
19939
|
+
status: "warn",
|
|
19940
|
+
details: `Could not check event destinations: ${summarizeError(error)}`
|
|
19941
|
+
};
|
|
19942
|
+
}
|
|
19943
|
+
}
|
|
19944
|
+
async function checkRule(region) {
|
|
19945
|
+
const hop = `EventBridge rule ${RULE_NAME}`;
|
|
19946
|
+
try {
|
|
19947
|
+
const { EventBridgeClient, DescribeRuleCommand } = await import("@aws-sdk/client-eventbridge");
|
|
19948
|
+
const client = new EventBridgeClient({ region });
|
|
19949
|
+
const response = await client.send(
|
|
19950
|
+
new DescribeRuleCommand({ Name: RULE_NAME })
|
|
19951
|
+
);
|
|
19952
|
+
if (response.State === "ENABLED") {
|
|
19953
|
+
return {
|
|
19954
|
+
exists: true,
|
|
19955
|
+
check: { hop, status: "pass", details: "Rule enabled" }
|
|
19956
|
+
};
|
|
19957
|
+
}
|
|
19958
|
+
return {
|
|
19959
|
+
exists: true,
|
|
19960
|
+
check: {
|
|
19961
|
+
hop,
|
|
19962
|
+
status: "fail",
|
|
19963
|
+
details: `Rule is ${response.State ?? "in an unknown state"} \u2014 run \`${UPGRADE_HINT}\` to re-enable`
|
|
19964
|
+
}
|
|
19965
|
+
};
|
|
19966
|
+
} catch (error) {
|
|
19967
|
+
if (isAWSNotFoundError(error)) {
|
|
19968
|
+
return {
|
|
19969
|
+
exists: false,
|
|
19970
|
+
check: {
|
|
19971
|
+
hop,
|
|
19972
|
+
status: "fail",
|
|
19973
|
+
details: `Rule not found \u2014 SES events have nowhere to go; run \`${UPGRADE_HINT}\``
|
|
19974
|
+
}
|
|
19975
|
+
};
|
|
19976
|
+
}
|
|
19977
|
+
return {
|
|
19978
|
+
exists: false,
|
|
19979
|
+
check: {
|
|
19980
|
+
hop,
|
|
19981
|
+
status: "warn",
|
|
19982
|
+
details: `Could not check rule: ${summarizeError(error)}`
|
|
19983
|
+
}
|
|
19984
|
+
};
|
|
19985
|
+
}
|
|
19986
|
+
}
|
|
19987
|
+
async function checkQueueExists(queueName, region, failDetails) {
|
|
19988
|
+
const hop = `SQS queue ${queueName}`;
|
|
19989
|
+
try {
|
|
19990
|
+
const { SQSClient, GetQueueUrlCommand } = await import("@aws-sdk/client-sqs");
|
|
19991
|
+
const client = new SQSClient({ region });
|
|
19992
|
+
await client.send(new GetQueueUrlCommand({ QueueName: queueName }));
|
|
19993
|
+
return { hop, status: "pass", details: "Queue exists" };
|
|
19994
|
+
} catch (error) {
|
|
19995
|
+
if (isQueueNotFoundError(error)) {
|
|
19996
|
+
return { hop, status: "fail", details: failDetails };
|
|
19997
|
+
}
|
|
19998
|
+
return {
|
|
19999
|
+
hop,
|
|
20000
|
+
status: "warn",
|
|
20001
|
+
details: `Could not check queue: ${summarizeError(error)}`
|
|
20002
|
+
};
|
|
20003
|
+
}
|
|
20004
|
+
}
|
|
20005
|
+
function findDuplicateTargetChecks(targets) {
|
|
20006
|
+
const idsByArn = /* @__PURE__ */ new Map();
|
|
20007
|
+
for (const target of targets) {
|
|
20008
|
+
if (!target.Arn) {
|
|
20009
|
+
continue;
|
|
20010
|
+
}
|
|
20011
|
+
const ids = idsByArn.get(target.Arn) ?? [];
|
|
20012
|
+
ids.push(target.Id ?? "unknown");
|
|
20013
|
+
idsByArn.set(target.Arn, ids);
|
|
20014
|
+
}
|
|
20015
|
+
const checks = [];
|
|
20016
|
+
for (const [arn, ids] of idsByArn) {
|
|
20017
|
+
if (ids.length > 1) {
|
|
20018
|
+
checks.push({
|
|
20019
|
+
hop: "EventBridge rule targets",
|
|
20020
|
+
status: "warn",
|
|
20021
|
+
details: `Duplicate targets for ${arn}: ${ids.join(", ")}`
|
|
20022
|
+
});
|
|
20023
|
+
}
|
|
20024
|
+
}
|
|
20025
|
+
return checks;
|
|
20026
|
+
}
|
|
20027
|
+
function checkSqsTargetOnRule(targets, region) {
|
|
20028
|
+
const failDetails = `events are being dropped \u2014 run \`${UPGRADE_HINT}\` to recreate the queue`;
|
|
20029
|
+
const sqsTarget = targets.find((t) => t.Arn?.split(":")[2] === "sqs");
|
|
20030
|
+
if (!sqsTarget) {
|
|
20031
|
+
return Promise.resolve({
|
|
20032
|
+
hop: `SQS queue ${QUEUE_NAME}`,
|
|
20033
|
+
status: "fail",
|
|
20034
|
+
details: `No SQS target on rule \u2014 ${failDetails}`
|
|
20035
|
+
});
|
|
20036
|
+
}
|
|
20037
|
+
return checkQueueExists(QUEUE_NAME, region, failDetails);
|
|
20038
|
+
}
|
|
20039
|
+
function checkPlatformWebhookTargetPresence(webhookTargetPresent, expectPlatformWebhook) {
|
|
20040
|
+
if (expectPlatformWebhook && !webhookTargetPresent) {
|
|
20041
|
+
return {
|
|
20042
|
+
hop: "Platform webhook target",
|
|
20043
|
+
status: "fail",
|
|
20044
|
+
details: `platform webhook target missing but metadata says connected \u2014 dashboard receives no events; run \`${UPGRADE_HINT}\``
|
|
20045
|
+
};
|
|
20046
|
+
}
|
|
20047
|
+
if (!expectPlatformWebhook && webhookTargetPresent) {
|
|
20048
|
+
return {
|
|
20049
|
+
hop: "Platform webhook target",
|
|
20050
|
+
status: "warn",
|
|
20051
|
+
details: "Webhook target exists on the rule but metadata has no webhookSecret \u2014 stack/metadata mismatch, possibly a lost metadata file"
|
|
20052
|
+
};
|
|
20053
|
+
}
|
|
20054
|
+
if (expectPlatformWebhook && webhookTargetPresent) {
|
|
20055
|
+
return {
|
|
20056
|
+
hop: "Platform webhook target",
|
|
20057
|
+
status: "pass",
|
|
20058
|
+
details: "Platform webhook target present"
|
|
20059
|
+
};
|
|
20060
|
+
}
|
|
20061
|
+
return null;
|
|
20062
|
+
}
|
|
20063
|
+
async function checkRuleTargets(region, expectPlatformWebhook) {
|
|
20064
|
+
try {
|
|
20065
|
+
const { EventBridgeClient, ListTargetsByRuleCommand } = await import("@aws-sdk/client-eventbridge");
|
|
20066
|
+
const client = new EventBridgeClient({ region });
|
|
20067
|
+
const response = await client.send(
|
|
20068
|
+
new ListTargetsByRuleCommand({ Rule: RULE_NAME })
|
|
20069
|
+
);
|
|
20070
|
+
const targets = response.Targets ?? [];
|
|
20071
|
+
const checks = [...findDuplicateTargetChecks(targets)];
|
|
20072
|
+
checks.push(await checkSqsTargetOnRule(targets, region));
|
|
20073
|
+
const webhookTargetPresent = targets.some(
|
|
20074
|
+
(t) => t.Arn?.includes(WEBHOOK_DESTINATION_NAME)
|
|
20075
|
+
);
|
|
20076
|
+
const webhookCheck = checkPlatformWebhookTargetPresence(
|
|
20077
|
+
webhookTargetPresent,
|
|
20078
|
+
expectPlatformWebhook
|
|
20079
|
+
);
|
|
20080
|
+
if (webhookCheck) {
|
|
20081
|
+
checks.push(webhookCheck);
|
|
20082
|
+
}
|
|
20083
|
+
return { checks, webhookTargetPresent };
|
|
20084
|
+
} catch (error) {
|
|
20085
|
+
return {
|
|
20086
|
+
checks: [
|
|
20087
|
+
{
|
|
20088
|
+
hop: "EventBridge rule targets",
|
|
20089
|
+
status: "warn",
|
|
20090
|
+
details: `Could not list rule targets: ${summarizeError(error)}`
|
|
20091
|
+
}
|
|
20092
|
+
],
|
|
20093
|
+
webhookTargetPresent: false
|
|
20094
|
+
};
|
|
20095
|
+
}
|
|
20096
|
+
}
|
|
20097
|
+
async function checkWebhookDestination(region) {
|
|
20098
|
+
const hop = `EventBridge API destination ${WEBHOOK_DESTINATION_NAME}`;
|
|
20099
|
+
try {
|
|
20100
|
+
const { EventBridgeClient, DescribeApiDestinationCommand } = await import("@aws-sdk/client-eventbridge");
|
|
20101
|
+
const client = new EventBridgeClient({ region });
|
|
20102
|
+
const response = await client.send(
|
|
20103
|
+
new DescribeApiDestinationCommand({ Name: WEBHOOK_DESTINATION_NAME })
|
|
20104
|
+
);
|
|
20105
|
+
if (response.ApiDestinationState === "ACTIVE") {
|
|
20106
|
+
return { hop, status: "pass", details: "Active" };
|
|
20107
|
+
}
|
|
20108
|
+
return {
|
|
20109
|
+
hop,
|
|
20110
|
+
status: "fail",
|
|
20111
|
+
details: `API destination is ${response.ApiDestinationState ?? "in an unknown state"} \u2014 re-run \`${UPGRADE_HINT}\` to re-authorize`
|
|
20112
|
+
};
|
|
20113
|
+
} catch (error) {
|
|
20114
|
+
if (isAWSNotFoundError(error)) {
|
|
20115
|
+
return {
|
|
20116
|
+
hop,
|
|
20117
|
+
status: "fail",
|
|
20118
|
+
details: `API destination not found \u2014 re-run \`${UPGRADE_HINT}\``
|
|
20119
|
+
};
|
|
20120
|
+
}
|
|
20121
|
+
return {
|
|
20122
|
+
hop,
|
|
20123
|
+
status: "warn",
|
|
20124
|
+
details: `Could not check API destination: ${summarizeError(error)}`
|
|
20125
|
+
};
|
|
20126
|
+
}
|
|
20127
|
+
}
|
|
20128
|
+
async function checkWebhookConnection(region) {
|
|
20129
|
+
const hop = `EventBridge connection ${WEBHOOK_CONNECTION_NAME}`;
|
|
20130
|
+
try {
|
|
20131
|
+
const { EventBridgeClient, DescribeConnectionCommand } = await import("@aws-sdk/client-eventbridge");
|
|
20132
|
+
const client = new EventBridgeClient({ region });
|
|
20133
|
+
const response = await client.send(
|
|
20134
|
+
new DescribeConnectionCommand({ Name: WEBHOOK_CONNECTION_NAME })
|
|
20135
|
+
);
|
|
20136
|
+
if (response.ConnectionState === "AUTHORIZED") {
|
|
20137
|
+
return { hop, status: "pass", details: "Authorized" };
|
|
20138
|
+
}
|
|
20139
|
+
return {
|
|
20140
|
+
hop,
|
|
20141
|
+
status: "fail",
|
|
20142
|
+
details: `Connection is ${response.ConnectionState ?? "in an unknown state"} \u2014 re-run \`${UPGRADE_HINT}\` to re-authorize`
|
|
20143
|
+
};
|
|
20144
|
+
} catch (error) {
|
|
20145
|
+
if (isAWSNotFoundError(error)) {
|
|
20146
|
+
return {
|
|
20147
|
+
hop,
|
|
20148
|
+
status: "fail",
|
|
20149
|
+
details: `Connection not found \u2014 re-run \`${UPGRADE_HINT}\``
|
|
20150
|
+
};
|
|
20151
|
+
}
|
|
20152
|
+
return {
|
|
20153
|
+
hop,
|
|
20154
|
+
status: "warn",
|
|
20155
|
+
details: `Could not check connection: ${summarizeError(error)}`
|
|
20156
|
+
};
|
|
20157
|
+
}
|
|
20158
|
+
}
|
|
20159
|
+
async function checkDLQ(region) {
|
|
20160
|
+
const hop = `SQS DLQ ${DLQ_NAME}`;
|
|
20161
|
+
try {
|
|
20162
|
+
const { SQSClient, GetQueueUrlCommand, GetQueueAttributesCommand } = await import("@aws-sdk/client-sqs");
|
|
20163
|
+
const client = new SQSClient({ region });
|
|
20164
|
+
const urlResponse = await client.send(
|
|
20165
|
+
new GetQueueUrlCommand({ QueueName: DLQ_NAME })
|
|
20166
|
+
);
|
|
20167
|
+
const attrResponse = await client.send(
|
|
20168
|
+
new GetQueueAttributesCommand({
|
|
20169
|
+
QueueUrl: urlResponse.QueueUrl,
|
|
20170
|
+
AttributeNames: ["ApproximateNumberOfMessages"]
|
|
20171
|
+
})
|
|
20172
|
+
);
|
|
20173
|
+
const count = Number(
|
|
20174
|
+
attrResponse.Attributes?.ApproximateNumberOfMessages ?? "0"
|
|
20175
|
+
);
|
|
20176
|
+
if (count > 0) {
|
|
20177
|
+
return {
|
|
20178
|
+
hop,
|
|
20179
|
+
status: "warn",
|
|
20180
|
+
details: `${count} dead-lettered event(s) \u2014 investigate before they age out`
|
|
20181
|
+
};
|
|
20182
|
+
}
|
|
20183
|
+
return { hop, status: "pass", details: "Empty" };
|
|
20184
|
+
} catch (error) {
|
|
20185
|
+
if (isQueueNotFoundError(error)) {
|
|
20186
|
+
return {
|
|
20187
|
+
hop,
|
|
20188
|
+
status: "fail",
|
|
20189
|
+
details: `Dead-letter queue missing \u2014 run \`${UPGRADE_HINT}\` to recreate it`
|
|
20190
|
+
};
|
|
20191
|
+
}
|
|
20192
|
+
return {
|
|
20193
|
+
hop,
|
|
20194
|
+
status: "warn",
|
|
20195
|
+
details: `Could not check DLQ: ${summarizeError(error)}`
|
|
20196
|
+
};
|
|
20197
|
+
}
|
|
20198
|
+
}
|
|
20199
|
+
async function checkEventSourceMapping(region) {
|
|
20200
|
+
const hop = `Lambda event source mapping ${LAMBDA_FUNCTION_NAME}`;
|
|
20201
|
+
try {
|
|
20202
|
+
const { LambdaClient: LambdaClient3, ListEventSourceMappingsCommand } = await import("@aws-sdk/client-lambda");
|
|
20203
|
+
const client = new LambdaClient3({ region });
|
|
20204
|
+
const response = await client.send(
|
|
20205
|
+
new ListEventSourceMappingsCommand({ FunctionName: LAMBDA_FUNCTION_NAME })
|
|
20206
|
+
);
|
|
20207
|
+
const mapping = response.EventSourceMappings?.find(
|
|
20208
|
+
(m) => m.EventSourceArn?.endsWith(`:${QUEUE_NAME}`)
|
|
20209
|
+
);
|
|
20210
|
+
if (!mapping) {
|
|
20211
|
+
return {
|
|
20212
|
+
hop,
|
|
20213
|
+
status: "fail",
|
|
20214
|
+
details: `No event source mapping from ${QUEUE_NAME} \u2014 Lambda never runs; run \`${UPGRADE_HINT}\``
|
|
20215
|
+
};
|
|
20216
|
+
}
|
|
20217
|
+
if (mapping.State === "Enabled") {
|
|
20218
|
+
return { hop, status: "pass", details: "Mapping enabled" };
|
|
20219
|
+
}
|
|
20220
|
+
return {
|
|
20221
|
+
hop,
|
|
20222
|
+
status: "fail",
|
|
20223
|
+
details: `Mapping is ${mapping.State ?? "in an unknown state"} \u2014 run \`${UPGRADE_HINT}\` to re-enable`
|
|
20224
|
+
};
|
|
20225
|
+
} catch (error) {
|
|
20226
|
+
if (isAWSNotFoundError(error)) {
|
|
20227
|
+
return {
|
|
20228
|
+
hop,
|
|
20229
|
+
status: "fail",
|
|
20230
|
+
details: `Lambda function not found \u2014 run \`${UPGRADE_HINT}\``
|
|
20231
|
+
};
|
|
20232
|
+
}
|
|
20233
|
+
return {
|
|
20234
|
+
hop,
|
|
20235
|
+
status: "warn",
|
|
20236
|
+
details: `Could not check event source mapping: ${summarizeError(error)}`
|
|
20237
|
+
};
|
|
20238
|
+
}
|
|
20239
|
+
}
|
|
20240
|
+
async function checkHistoryTable(region) {
|
|
20241
|
+
const hop = `DynamoDB table ${HISTORY_TABLE_NAME}`;
|
|
20242
|
+
try {
|
|
20243
|
+
const { DynamoDBClient: DynamoDBClient7, DescribeTableCommand: DescribeTableCommand2 } = await import("@aws-sdk/client-dynamodb");
|
|
20244
|
+
const client = new DynamoDBClient7({ region });
|
|
20245
|
+
const response = await client.send(
|
|
20246
|
+
new DescribeTableCommand2({ TableName: HISTORY_TABLE_NAME })
|
|
20247
|
+
);
|
|
20248
|
+
const status2 = response.Table?.TableStatus;
|
|
20249
|
+
if (status2 === "ACTIVE") {
|
|
20250
|
+
return { hop, status: "pass", details: "Active" };
|
|
20251
|
+
}
|
|
20252
|
+
return {
|
|
20253
|
+
hop,
|
|
20254
|
+
status: "fail",
|
|
20255
|
+
details: `Table status is ${status2 ?? "unknown"} \u2014 event log may be unavailable; run \`${UPGRADE_HINT}\``
|
|
20256
|
+
};
|
|
20257
|
+
} catch (error) {
|
|
20258
|
+
if (isAWSNotFoundError(error)) {
|
|
20259
|
+
return {
|
|
20260
|
+
hop,
|
|
20261
|
+
status: "fail",
|
|
20262
|
+
details: `Table not found \u2014 event history is not being recorded; run \`${UPGRADE_HINT}\``
|
|
20263
|
+
};
|
|
20264
|
+
}
|
|
20265
|
+
return {
|
|
20266
|
+
hop,
|
|
20267
|
+
status: "warn",
|
|
20268
|
+
details: `Could not check table: ${summarizeError(error)}`
|
|
20269
|
+
};
|
|
20270
|
+
}
|
|
20271
|
+
}
|
|
20272
|
+
async function checkEventPipeline(params) {
|
|
20273
|
+
const { region, domains, expectPlatformWebhook } = params;
|
|
20274
|
+
const checks = [];
|
|
20275
|
+
const uniqueConfigSetNames = Array.from(
|
|
20276
|
+
new Set(domains.map((d) => domainToConfigSetName(d)))
|
|
20277
|
+
);
|
|
20278
|
+
if (uniqueConfigSetNames.length === 0) {
|
|
20279
|
+
const check2 = await checkConfigSet(CONFIG_SET_FALLBACK, region, false);
|
|
20280
|
+
if (check2) {
|
|
20281
|
+
checks.push(check2);
|
|
20282
|
+
}
|
|
20283
|
+
} else {
|
|
20284
|
+
for (const name of uniqueConfigSetNames) {
|
|
20285
|
+
const check2 = await checkConfigSet(name, region, false);
|
|
20286
|
+
if (check2) {
|
|
20287
|
+
checks.push(check2);
|
|
20288
|
+
}
|
|
20289
|
+
}
|
|
20290
|
+
const fallbackCheck = await checkConfigSet(
|
|
20291
|
+
CONFIG_SET_FALLBACK,
|
|
20292
|
+
region,
|
|
20293
|
+
true
|
|
20294
|
+
);
|
|
20295
|
+
if (fallbackCheck) {
|
|
20296
|
+
checks.push(fallbackCheck);
|
|
20297
|
+
}
|
|
20298
|
+
}
|
|
20299
|
+
const ruleResult = await checkRule(region);
|
|
20300
|
+
checks.push(ruleResult.check);
|
|
20301
|
+
let webhookTargetPresent = false;
|
|
20302
|
+
if (ruleResult.exists) {
|
|
20303
|
+
const targetsResult = await checkRuleTargets(region, expectPlatformWebhook);
|
|
20304
|
+
checks.push(...targetsResult.checks);
|
|
20305
|
+
webhookTargetPresent = targetsResult.webhookTargetPresent;
|
|
20306
|
+
}
|
|
20307
|
+
if (webhookTargetPresent) {
|
|
20308
|
+
checks.push(await checkWebhookDestination(region));
|
|
20309
|
+
checks.push(await checkWebhookConnection(region));
|
|
20310
|
+
}
|
|
20311
|
+
checks.push(await checkDLQ(region));
|
|
20312
|
+
checks.push(await checkEventSourceMapping(region));
|
|
20313
|
+
checks.push(await checkHistoryTable(region));
|
|
20314
|
+
return checks;
|
|
20315
|
+
}
|
|
20316
|
+
|
|
20317
|
+
// src/commands/email/doctor.ts
|
|
20318
|
+
init_aws();
|
|
20319
|
+
init_fs();
|
|
20320
|
+
init_json_output();
|
|
20321
|
+
init_metadata();
|
|
20322
|
+
init_output();
|
|
20323
|
+
init_scanner();
|
|
20324
|
+
function pipelineChecksToDoctorResults(pipelineChecks) {
|
|
20325
|
+
return pipelineChecks.map((check2) => ({
|
|
20326
|
+
status: check2.status,
|
|
20327
|
+
category: "Event Pipeline",
|
|
20328
|
+
name: check2.hop,
|
|
20329
|
+
details: check2.details
|
|
20330
|
+
}));
|
|
20331
|
+
}
|
|
19788
20332
|
function runResourceDiagnostics(wrapsResources, hasStack) {
|
|
19789
20333
|
const results = [];
|
|
19790
20334
|
const orphanSuffix = hasStack ? void 0 : " (orphan \u2014 no Pulumi state)";
|
|
@@ -19880,11 +20424,11 @@ async function emailDoctor(options) {
|
|
|
19880
20424
|
async () => validateAWSCredentials()
|
|
19881
20425
|
);
|
|
19882
20426
|
let region = options.region || await getAWSRegion();
|
|
20427
|
+
const emailConnections = await findConnectionsWithService(
|
|
20428
|
+
identity.accountId,
|
|
20429
|
+
"email"
|
|
20430
|
+
);
|
|
19883
20431
|
if (!(options.region || process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION)) {
|
|
19884
|
-
const emailConnections = await findConnectionsWithService(
|
|
19885
|
-
identity.accountId,
|
|
19886
|
-
"email"
|
|
19887
|
-
);
|
|
19888
20432
|
if (emailConnections.length === 1) {
|
|
19889
20433
|
region = emailConnections[0].region;
|
|
19890
20434
|
} else if (emailConnections.length > 1 && !isJsonMode()) {
|
|
@@ -19919,8 +20463,23 @@ async function emailDoctor(options) {
|
|
|
19919
20463
|
} catch (_error) {
|
|
19920
20464
|
hasStack = false;
|
|
19921
20465
|
}
|
|
20466
|
+
const emailConnection = emailConnections.find((c) => c.region === region);
|
|
20467
|
+
let pipelineResults = [];
|
|
20468
|
+
if (emailConnection) {
|
|
20469
|
+
const emailService = emailConnection.services.email;
|
|
20470
|
+
const domains = emailService?.config ? getAllTrackedDomains(emailConnection).map((d) => d.domain) : [];
|
|
20471
|
+
const expectPlatformWebhook = Boolean(emailService?.webhookSecret);
|
|
20472
|
+
const pipelineChecks = await progress.execute(
|
|
20473
|
+
"Checking event pipeline",
|
|
20474
|
+
async () => checkEventPipeline({ region, domains, expectPlatformWebhook })
|
|
20475
|
+
);
|
|
20476
|
+
pipelineResults = pipelineChecksToDoctorResults(pipelineChecks);
|
|
20477
|
+
}
|
|
19922
20478
|
progress.stop();
|
|
19923
|
-
const results =
|
|
20479
|
+
const results = [
|
|
20480
|
+
...runResourceDiagnostics(wrapsResources, hasStack),
|
|
20481
|
+
...pipelineResults
|
|
20482
|
+
];
|
|
19924
20483
|
if (isJsonMode()) {
|
|
19925
20484
|
jsonSuccess("email.doctor", {
|
|
19926
20485
|
region,
|
|
@@ -19935,7 +20494,7 @@ async function emailDoctor(options) {
|
|
|
19935
20494
|
});
|
|
19936
20495
|
return;
|
|
19937
20496
|
}
|
|
19938
|
-
if (
|
|
20497
|
+
if (results.length === 0) {
|
|
19939
20498
|
clack19.log.info("No wraps-* resources found in this region.");
|
|
19940
20499
|
clack19.outro(pc21.dim("Your AWS account is clean."));
|
|
19941
20500
|
} else {
|
|
@@ -21922,7 +22481,7 @@ import {
|
|
|
21922
22481
|
UpdateReceiptRuleCommand
|
|
21923
22482
|
} from "@aws-sdk/client-ses";
|
|
21924
22483
|
var RULE_SET_NAME = "wraps-inbound-rules";
|
|
21925
|
-
var
|
|
22484
|
+
var RULE_NAME2 = "wraps-inbound-catch-all";
|
|
21926
22485
|
function createSESClient(region) {
|
|
21927
22486
|
return new SESClient4({ region });
|
|
21928
22487
|
}
|
|
@@ -21947,13 +22506,13 @@ async function createReceiptRule(region, receivingDomain, s3BucketName) {
|
|
|
21947
22506
|
await ses.send(
|
|
21948
22507
|
new DescribeReceiptRuleCommand({
|
|
21949
22508
|
RuleSetName: RULE_SET_NAME,
|
|
21950
|
-
RuleName:
|
|
22509
|
+
RuleName: RULE_NAME2
|
|
21951
22510
|
})
|
|
21952
22511
|
);
|
|
21953
22512
|
await ses.send(
|
|
21954
22513
|
new DeleteReceiptRuleCommand({
|
|
21955
22514
|
RuleSetName: RULE_SET_NAME,
|
|
21956
|
-
RuleName:
|
|
22515
|
+
RuleName: RULE_NAME2
|
|
21957
22516
|
})
|
|
21958
22517
|
);
|
|
21959
22518
|
} catch (error) {
|
|
@@ -21965,7 +22524,7 @@ async function createReceiptRule(region, receivingDomain, s3BucketName) {
|
|
|
21965
22524
|
new CreateReceiptRuleCommand({
|
|
21966
22525
|
RuleSetName: RULE_SET_NAME,
|
|
21967
22526
|
Rule: {
|
|
21968
|
-
Name:
|
|
22527
|
+
Name: RULE_NAME2,
|
|
21969
22528
|
Enabled: true,
|
|
21970
22529
|
ScanEnabled: true,
|
|
21971
22530
|
// Enable spam and virus scanning
|
|
@@ -22013,7 +22572,7 @@ async function deleteReceiptRule(region) {
|
|
|
22013
22572
|
await ses.send(
|
|
22014
22573
|
new DeleteReceiptRuleCommand({
|
|
22015
22574
|
RuleSetName: RULE_SET_NAME,
|
|
22016
|
-
RuleName:
|
|
22575
|
+
RuleName: RULE_NAME2
|
|
22017
22576
|
})
|
|
22018
22577
|
);
|
|
22019
22578
|
} catch (error) {
|
|
@@ -22048,7 +22607,7 @@ async function addDomainToReceiptRule(region, domain, s3BucketName) {
|
|
|
22048
22607
|
const response = await ses.send(
|
|
22049
22608
|
new DescribeReceiptRuleCommand({
|
|
22050
22609
|
RuleSetName: RULE_SET_NAME,
|
|
22051
|
-
RuleName:
|
|
22610
|
+
RuleName: RULE_NAME2
|
|
22052
22611
|
})
|
|
22053
22612
|
);
|
|
22054
22613
|
const existingRecipients = response.Rule?.Recipients ?? [];
|
|
@@ -22059,7 +22618,7 @@ async function addDomainToReceiptRule(region, domain, s3BucketName) {
|
|
|
22059
22618
|
new UpdateReceiptRuleCommand({
|
|
22060
22619
|
RuleSetName: RULE_SET_NAME,
|
|
22061
22620
|
Rule: {
|
|
22062
|
-
Name:
|
|
22621
|
+
Name: RULE_NAME2,
|
|
22063
22622
|
Enabled: response.Rule?.Enabled,
|
|
22064
22623
|
ScanEnabled: response.Rule?.ScanEnabled,
|
|
22065
22624
|
TlsPolicy: response.Rule?.TlsPolicy,
|
|
@@ -22084,7 +22643,7 @@ async function removeDomainFromReceiptRule(region, domain) {
|
|
|
22084
22643
|
const response = await ses.send(
|
|
22085
22644
|
new DescribeReceiptRuleCommand({
|
|
22086
22645
|
RuleSetName: RULE_SET_NAME,
|
|
22087
|
-
RuleName:
|
|
22646
|
+
RuleName: RULE_NAME2
|
|
22088
22647
|
})
|
|
22089
22648
|
);
|
|
22090
22649
|
const existingRecipients = response.Rule?.Recipients ?? [];
|
|
@@ -22097,7 +22656,7 @@ async function removeDomainFromReceiptRule(region, domain) {
|
|
|
22097
22656
|
new UpdateReceiptRuleCommand({
|
|
22098
22657
|
RuleSetName: RULE_SET_NAME,
|
|
22099
22658
|
Rule: {
|
|
22100
|
-
Name:
|
|
22659
|
+
Name: RULE_NAME2,
|
|
22101
22660
|
Enabled: response.Rule?.Enabled,
|
|
22102
22661
|
ScanEnabled: response.Rule?.ScanEnabled,
|
|
22103
22662
|
TlsPolicy: response.Rule?.TlsPolicy,
|
|
@@ -22119,7 +22678,7 @@ async function getReceiptRuleDomains(region) {
|
|
|
22119
22678
|
const response = await ses.send(
|
|
22120
22679
|
new DescribeReceiptRuleCommand({
|
|
22121
22680
|
RuleSetName: RULE_SET_NAME,
|
|
22122
|
-
RuleName:
|
|
22681
|
+
RuleName: RULE_NAME2
|
|
22123
22682
|
})
|
|
22124
22683
|
);
|
|
22125
22684
|
return response.Rule?.Recipients ?? [];
|
|
@@ -26840,7 +27399,7 @@ init_esm_shims();
|
|
|
26840
27399
|
// src/utils/email/template-mustache-case.ts
|
|
26841
27400
|
init_esm_shims();
|
|
26842
27401
|
var HANDLEBARS_BLOCK_HELPERS = /* @__PURE__ */ new Set(["if", "unless", "each", "with"]);
|
|
26843
|
-
function normalizePlainTextMustaches(
|
|
27402
|
+
function normalizePlainTextMustaches(text13, canonicalVars) {
|
|
26844
27403
|
const canonicalByLower = /* @__PURE__ */ new Map();
|
|
26845
27404
|
for (const name of canonicalVars) {
|
|
26846
27405
|
canonicalByLower.set(name.toLowerCase(), name);
|
|
@@ -26848,7 +27407,7 @@ function normalizePlainTextMustaches(text12, canonicalVars) {
|
|
|
26848
27407
|
function restoreIdentifier(name) {
|
|
26849
27408
|
return canonicalByLower.get(name.toLowerCase()) ?? name;
|
|
26850
27409
|
}
|
|
26851
|
-
return
|
|
27410
|
+
return text13.replace(
|
|
26852
27411
|
/\{\{([#/]?)([A-Z][A-Z0-9_]*(?:\.[A-Z0-9_]+)*)((?:\s+[A-Z0-9_.]+)*)\s*\}\}/g,
|
|
26853
27412
|
(match, sigil, name, args) => {
|
|
26854
27413
|
const lower = name.toLowerCase();
|
|
@@ -26898,8 +27457,8 @@ async function renderTemplateWithProxy(Component) {
|
|
|
26898
27457
|
match = regex.exec(html);
|
|
26899
27458
|
}
|
|
26900
27459
|
const canonicalVars = /* @__PURE__ */ new Set([...htmlVarNames, ...accessedProps]);
|
|
26901
|
-
const
|
|
26902
|
-
return { html, text:
|
|
27460
|
+
const text13 = normalizePlainTextMustaches(rawText, canonicalVars);
|
|
27461
|
+
return { html, text: text13, accessedProps };
|
|
26903
27462
|
}
|
|
26904
27463
|
|
|
26905
27464
|
// src/commands/email/templates/push.ts
|
|
@@ -27138,11 +27697,11 @@ async function compileTemplate(filePath, slug, source, sourceHash, wrapsDir) {
|
|
|
27138
27697
|
"Template must have a default export (React component function)"
|
|
27139
27698
|
);
|
|
27140
27699
|
}
|
|
27141
|
-
const { html, text:
|
|
27700
|
+
const { html, text: text13, accessedProps } = await renderTemplateWithProxy(Component);
|
|
27142
27701
|
const variables = mergeVariables(extractVariables(html), accessedProps);
|
|
27143
27702
|
const sesSubject = transformVariablesForSes(subject);
|
|
27144
27703
|
const sesHtml = transformVariablesForSes(html);
|
|
27145
|
-
const sesText = transformVariablesForSes(
|
|
27704
|
+
const sesText = transformVariablesForSes(text13);
|
|
27146
27705
|
const sesTemplateName = slug.replace(/[^a-zA-Z0-9-_]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").substring(0, 64);
|
|
27147
27706
|
return {
|
|
27148
27707
|
slug,
|
|
@@ -29545,6 +30104,28 @@ ${pc37.green("\u2713")} ${pc37.bold("Upgrade complete!")}
|
|
|
29545
30104
|
}
|
|
29546
30105
|
}
|
|
29547
30106
|
await saveConnectionMetadata(metadata);
|
|
30107
|
+
try {
|
|
30108
|
+
const domains = getAllTrackedDomains(metadata).map((d) => d.domain);
|
|
30109
|
+
const pipelineChecks = await checkEventPipeline({
|
|
30110
|
+
region: outputs.region ?? region,
|
|
30111
|
+
domains,
|
|
30112
|
+
expectPlatformWebhook: Boolean(metadata.services.email?.webhookSecret)
|
|
30113
|
+
});
|
|
30114
|
+
const issues = pipelineChecks.filter((check2) => check2.status !== "pass");
|
|
30115
|
+
if (issues.length > 0 && !isJsonMode()) {
|
|
30116
|
+
for (const issue of issues) {
|
|
30117
|
+
clack36.log.warn(
|
|
30118
|
+
`Post-deploy pipeline check: ${issue.hop} \u2014 ${issue.details}`
|
|
30119
|
+
);
|
|
30120
|
+
}
|
|
30121
|
+
}
|
|
30122
|
+
} catch (error) {
|
|
30123
|
+
if (!isJsonMode()) {
|
|
30124
|
+
clack36.log.warn(
|
|
30125
|
+
`Post-deploy pipeline check failed to run: ${error instanceof Error ? error.message : String(error)}`
|
|
30126
|
+
);
|
|
30127
|
+
}
|
|
30128
|
+
}
|
|
29548
30129
|
if (isJsonMode()) {
|
|
29549
30130
|
jsonSuccess("email.upgrade", {
|
|
29550
30131
|
upgraded: true,
|