@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/README.md +1 -1
- package/dist/api-lambda.zip +0 -0
- package/dist/cli.js +679 -53
- package/dist/cli.js.map +1 -1
- package/dist/console/assets/index-BP4FpEpn.css +1 -0
- package/dist/console/assets/{index-BiEvZUP2.js → index-P0x5JCC4.js} +58 -58
- 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/0063_greedy_legion.sql +17 -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/0063_snapshot.json +7282 -0
- package/dist/selfhost-migrations/meta/_journal.json +29 -1
- package/package.json +38 -37
- 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());
|
|
@@ -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(
|
|
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.
|
|
16720
|
+
if (dmarc.testing && dmarc.policy !== "none") {
|
|
16690
16721
|
deductions.push({
|
|
16691
16722
|
check: "dmarc",
|
|
16692
16723
|
points: 2,
|
|
16693
|
-
reason:
|
|
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({
|
|
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 =
|
|
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 (
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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(
|
|
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
|
|
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
|
|
26902
|
-
return { html, text:
|
|
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:
|
|
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(
|
|
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,
|