Migrate SES sends from v1 to SESv2 to avoid fast-xml-parser conflict

The fast-xml-parser >=5.3.8 override (added for CVE) is incompatible
with @aws-sdk/client-ses, whose XML response parser registers entities
named "#xD" and "#10" — names that 5.x rejects with
[EntityReplacer] Invalid character '#' in entity name.

SESv2 uses REST/JSON, sidestepping the parser entirely. validateEmail
already used SESv2; this consolidates the three send functions onto
the same client and drops @aws-sdk/client-ses.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Lawrence Hook
2026-05-02 15:36:27 -04:00
parent 574879ca9e
commit 6d59948c0b
3 changed files with 47 additions and 105 deletions
-65
View File
@@ -8,7 +8,6 @@
"name": "rys-premium-server",
"version": "1.0.0",
"dependencies": {
"@aws-sdk/client-ses": "^3.1000.0",
"@aws-sdk/client-sesv2": "^3.1000.0",
"better-sqlite3": "^11.0.0",
"cors": "^2.8.5",
@@ -151,57 +150,6 @@
"node": ">=14.0.0"
}
},
"node_modules/@aws-sdk/client-ses": {
"version": "3.1035.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/client-ses/-/client-ses-3.1035.0.tgz",
"integrity": "sha512-fpyLQVNuBXwkSvySrBsi6P/qS/ZhNhSifY25Hiluo0EP6Zec7OM8SGWR5ECq9M3I87MyHvp1GrzuoWg+9/Fk5g==",
"license": "Apache-2.0",
"dependencies": {
"@aws-crypto/sha256-browser": "5.2.0",
"@aws-crypto/sha256-js": "5.2.0",
"@aws-sdk/core": "^3.974.4",
"@aws-sdk/credential-provider-node": "^3.972.35",
"@aws-sdk/middleware-host-header": "^3.972.10",
"@aws-sdk/middleware-logger": "^3.972.10",
"@aws-sdk/middleware-recursion-detection": "^3.972.11",
"@aws-sdk/middleware-user-agent": "^3.972.34",
"@aws-sdk/region-config-resolver": "^3.972.13",
"@aws-sdk/types": "^3.973.8",
"@aws-sdk/util-endpoints": "^3.996.8",
"@aws-sdk/util-user-agent-browser": "^3.972.10",
"@aws-sdk/util-user-agent-node": "^3.973.20",
"@smithy/config-resolver": "^4.4.17",
"@smithy/core": "^3.23.16",
"@smithy/fetch-http-handler": "^5.3.17",
"@smithy/hash-node": "^4.2.14",
"@smithy/invalid-dependency": "^4.2.14",
"@smithy/middleware-content-length": "^4.2.14",
"@smithy/middleware-endpoint": "^4.4.31",
"@smithy/middleware-retry": "^4.5.4",
"@smithy/middleware-serde": "^4.2.19",
"@smithy/middleware-stack": "^4.2.14",
"@smithy/node-config-provider": "^4.3.14",
"@smithy/node-http-handler": "^4.6.0",
"@smithy/protocol-http": "^5.3.14",
"@smithy/smithy-client": "^4.12.12",
"@smithy/types": "^4.14.1",
"@smithy/url-parser": "^4.2.14",
"@smithy/util-base64": "^4.3.2",
"@smithy/util-body-length-browser": "^4.2.2",
"@smithy/util-body-length-node": "^4.2.3",
"@smithy/util-defaults-mode-browser": "^4.3.48",
"@smithy/util-defaults-mode-node": "^4.2.53",
"@smithy/util-endpoints": "^3.4.2",
"@smithy/util-middleware": "^4.2.14",
"@smithy/util-retry": "^4.3.3",
"@smithy/util-utf8": "^4.2.2",
"@smithy/util-waiter": "^4.2.16",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@aws-sdk/client-sesv2": {
"version": "3.1035.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/client-sesv2/-/client-sesv2-3.1035.0.tgz",
@@ -1330,19 +1278,6 @@
"node": ">=18.0.0"
}
},
"node_modules/@smithy/util-waiter": {
"version": "4.2.16",
"resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.2.16.tgz",
"integrity": "sha512-GtclrKoZ3Lt7jPQ7aTIYKfjY92OgceScftVnkTsG8e1KV8rkvZgN+ny6YSRhd9hxB8rZtwVbmln7NTvE5O3GmQ==",
"license": "Apache-2.0",
"dependencies": {
"@smithy/types": "^4.14.1",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@smithy/uuid": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.2.tgz",
-1
View File
@@ -9,7 +9,6 @@
"test": "node --test --test-concurrency=1 tests/*.test.js"
},
"dependencies": {
"@aws-sdk/client-ses": "^3.1000.0",
"@aws-sdk/client-sesv2": "^3.1000.0",
"better-sqlite3": "^11.0.0",
"cors": "^2.8.5",
+47 -39
View File
@@ -1,8 +1,10 @@
const { SESClient, SendEmailCommand } = require('@aws-sdk/client-ses');
const { SESv2Client, GetEmailAddressInsightsCommand } = require('@aws-sdk/client-sesv2');
const {
SESv2Client,
SendEmailCommand,
GetEmailAddressInsightsCommand,
} = require('@aws-sdk/client-sesv2');
const config = require('../config');
const sesClient = new SESClient({ region: config.AWS_REGION });
const sesV2Client = new SESv2Client({ region: config.AWS_REGION });
async function sendMagicLinkEmail(email, magicLinkUrl, { isPremium = false } = {}) {
@@ -10,21 +12,22 @@ async function sendMagicLinkEmail(email, magicLinkUrl, { isPremium = false } = {
? ' <span style="font-size: 12px; font-weight: 500; color: #0600fb; letter-spacing: 0.02em;">Premium</span>'
: '';
const params = {
Source: config.EMAIL_FROM,
FromEmailAddress: config.EMAIL_FROM,
Destination: {
ToAddresses: [email],
},
Message: {
Subject: {
Data: 'Sign in to RYS',
Charset: 'UTF-8',
},
Body: {
Text: {
Data: `Here's your sign-in link for RYS:\n\n${magicLinkUrl}\n\nLink expires in 15 minutes. If you didn't request this, just ignore it.\n\n— Lawrence`,
Content: {
Simple: {
Subject: {
Data: 'Sign in to RYS',
Charset: 'UTF-8',
},
Html: {
Body: {
Text: {
Data: `Here's your sign-in link for RYS:\n\n${magicLinkUrl}\n\nLink expires in 15 minutes. If you didn't request this, just ignore it.\n\n— Lawrence`,
Charset: 'UTF-8',
},
Html: {
Data: `
<!DOCTYPE html>
<html>
@@ -58,33 +61,35 @@ async function sendMagicLinkEmail(email, magicLinkUrl, { isPremium = false } = {
</body>
</html>
`.trim(),
Charset: 'UTF-8',
Charset: 'UTF-8',
},
},
},
},
};
const command = new SendEmailCommand(params);
return sesClient.send(command);
return sesV2Client.send(command);
}
async function sendWelcomeEmail(email) {
const params = {
Source: config.EMAIL_FROM,
FromEmailAddress: config.EMAIL_FROM,
Destination: {
ToAddresses: [email],
},
Message: {
Subject: {
Data: 'Welcome to RYS Premium',
Charset: 'UTF-8',
},
Body: {
Text: {
Data: `Welcome to RYS Premium!\n\nThank you for subscribing. Your premium features are now active.\n\n— Lawrence`,
Content: {
Simple: {
Subject: {
Data: 'Welcome to RYS Premium',
Charset: 'UTF-8',
},
Html: {
Body: {
Text: {
Data: `Welcome to RYS Premium!\n\nThank you for subscribing. Your premium features are now active.\n\n— Lawrence`,
Charset: 'UTF-8',
},
Html: {
Data: `
<!DOCTYPE html>
<html>
@@ -112,35 +117,37 @@ async function sendWelcomeEmail(email) {
</body>
</html>
`.trim(),
Charset: 'UTF-8',
Charset: 'UTF-8',
},
},
},
},
};
const command = new SendEmailCommand(params);
return sesClient.send(command);
return sesV2Client.send(command);
}
const FEEDBACK_URL = 'https://docs.google.com/forms/d/1AzQQxTWgG6M5N87jinvXKQkGS6Mehzg19XV4mjteTK0';
async function sendCancellationEmail(email) {
const params = {
Source: config.EMAIL_FROM,
FromEmailAddress: config.EMAIL_FROM,
Destination: {
ToAddresses: [email],
},
Message: {
Subject: {
Data: 'Your RYS Premium subscription has been canceled',
Charset: 'UTF-8',
},
Body: {
Text: {
Data: `Your RYS Premium subscription has been canceled.\n\nThe free version of RYS is still yours to use anytime.\n\nIf you have a moment, we'd love to hear what we could do better: ${FEEDBACK_URL}\n\nThanks for giving Premium a try.\n\n— Lawrence`,
Content: {
Simple: {
Subject: {
Data: 'Your RYS Premium subscription has been canceled',
Charset: 'UTF-8',
},
Html: {
Body: {
Text: {
Data: `Your RYS Premium subscription has been canceled.\n\nThe free version of RYS is still yours to use anytime.\n\nIf you have a moment, we'd love to hear what we could do better: ${FEEDBACK_URL}\n\nThanks for giving Premium a try.\n\n— Lawrence`,
Charset: 'UTF-8',
},
Html: {
Data: `
<!DOCTYPE html>
<html>
@@ -180,14 +187,15 @@ async function sendCancellationEmail(email) {
</body>
</html>
`.trim(),
Charset: 'UTF-8',
Charset: 'UTF-8',
},
},
},
},
};
const command = new SendEmailCommand(params);
return sesClient.send(command);
return sesV2Client.send(command);
}
async function validateEmail(email) {