From 6d59948c0bd5449e2ea8f18c6c00257a16fa073d Mon Sep 17 00:00:00 2001 From: Lawrence Hook Date: Sat, 2 May 2026 15:36:27 -0400 Subject: [PATCH] Migrate SES sends from v1 to SESv2 to avoid fast-xml-parser conflict MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- server/package-lock.json | 65 --------------------------- server/package.json | 1 - server/src/services/email.js | 86 ++++++++++++++++++++---------------- 3 files changed, 47 insertions(+), 105 deletions(-) diff --git a/server/package-lock.json b/server/package-lock.json index f094634..e641875 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -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", diff --git a/server/package.json b/server/package.json index b21f956..b155d9f 100644 --- a/server/package.json +++ b/server/package.json @@ -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", diff --git a/server/src/services/email.js b/server/src/services/email.js index 04e127a..76e9411 100644 --- a/server/src/services/email.js +++ b/server/src/services/email.js @@ -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 } = { ? ' Premium' : ''; 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: ` @@ -58,33 +61,35 @@ async function sendMagicLinkEmail(email, magicLinkUrl, { isPremium = false } = { `.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: ` @@ -112,35 +117,37 @@ async function sendWelcomeEmail(email) { `.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: ` @@ -180,14 +187,15 @@ async function sendCancellationEmail(email) { `.trim(), - Charset: 'UTF-8', + Charset: 'UTF-8', + }, }, }, }, }; const command = new SendEmailCommand(params); - return sesClient.send(command); + return sesV2Client.send(command); } async function validateEmail(email) {