diff --git a/.changeset/email-headers.md b/.changeset/email-headers.md new file mode 100644 index 0000000..6ac8b77 --- /dev/null +++ b/.changeset/email-headers.md @@ -0,0 +1,8 @@ +--- +"@seamless-auth/messaging": minor +"@seamless-auth/messaging-aws": minor +--- + +Email messages can carry custom headers, such as `List-Unsubscribe` and `List-Unsubscribe-Post` for one-click unsubscribe. Header names and values are validated so a caller cannot inject further headers, and the headers the transport sets from the message itself (From, To, Subject) are refused. + +The AWS email transport now sends through SES v2 (`@aws-sdk/client-sesv2`), since the v1 SendEmail call cannot carry custom headers. The IAM action is still `ses:SendEmail`. A `sesClient` passed in config must now be an SES v2 client, or anything that accepts the v2 `SendEmailCommand`. diff --git a/package-lock.json b/package-lock.json index f844595..e5616a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -154,57 +154,6 @@ "node": ">=14.0.0" } }, - "node_modules/@aws-sdk/client-ses": { - "version": "3.1034.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-ses/-/client-ses-3.1034.0.tgz", - "integrity": "sha512-yOkp3yqiVUrQpKfR9dmiEvRzQYJkm+6zAQjvVj12zvXoZtPvavvOau591gCZjau2xVe6RTpdlMSUkdm3fnTNXg==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.974.3", - "@aws-sdk/credential-provider-node": "^3.972.34", - "@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.33", - "@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.19", - "@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-sns": { "version": "3.1034.0", "resolved": "https://registry.npmjs.org/@aws-sdk/client-sns/-/client-sns-3.1034.0.tgz", @@ -256,40 +205,43 @@ } }, "node_modules/@aws-sdk/core": { - "version": "3.974.3", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.3.tgz", - "integrity": "sha512-W3aJJm2clu8OmsrwMOMnfof13O6LGnbknnZIQeSRbxjqKah2nVvkjbUBBZVhWrt08KC69H7WsINTdrxC/2SXQw==", + "version": "3.979.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.979.1.tgz", + "integrity": "sha512-ayOlV1VhLNMiq8PW9ZkVobG4gkldnUoHjG69yy2b8j+VYFlhz/uAL7ZdYQAoQ+eShtGUdNDnnG5rxDg6yUJrOg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.8", - "@aws-sdk/xml-builder": "^3.972.18", - "@smithy/core": "^3.23.16", - "@smithy/node-config-provider": "^4.3.14", - "@smithy/property-provider": "^4.2.14", - "@smithy/protocol-http": "^5.3.14", - "@smithy/signature-v4": "^5.3.14", - "@smithy/smithy-client": "^4.12.12", - "@smithy/types": "^4.14.1", - "@smithy/util-base64": "^4.3.2", - "@smithy/util-middleware": "^4.2.14", - "@smithy/util-retry": "^4.3.3", - "@smithy/util-utf8": "^4.2.2", + "@aws-sdk/types": "^3.975.0", + "@aws-sdk/xml-builder": "^3.972.42", + "@aws/lambda-invoke-store": "^0.3.0", + "@smithy/core": "^3.38.0", + "@smithy/signature-v4": "^5.7.5", + "@smithy/types": "^4.19.0", + "bowser": "^2.11.0", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, + "node_modules/@aws-sdk/core/node_modules/@aws/lambda-invoke-store": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.3.0.tgz", + "integrity": "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.972.29", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.29.tgz", - "integrity": "sha512-rf+AlUxgTeSzQ/4zoS0D+Bt7XvgpY48PnWG8Yg/N9fdMgyK2Jaqa+6tLZp4MYMIMHkGrfAxnbSeb2YLMGFMg6g==", + "version": "3.972.80", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.80.tgz", + "integrity": "sha512-uO5TiSvb6j1w9RTe5z28OaOmsrOxVQUPaC5+wZl50RfOXcrYD/KgnS8I9lfyGXc7sGbkCopK6VnhNIAoLqfewQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.3", - "@aws-sdk/types": "^3.973.8", - "@smithy/property-provider": "^4.2.14", - "@smithy/types": "^4.14.1", + "@aws-sdk/core": "^3.979.1", + "@aws-sdk/types": "^3.975.0", + "@smithy/core": "^3.38.0", + "@smithy/types": "^4.19.0", "tslib": "^2.6.2" }, "engines": { @@ -297,20 +249,17 @@ } }, "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.972.31", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.31.tgz", - "integrity": "sha512-TR2/lQ3qKFj2EOrsiASzemsNEz2uzZ/SUBf48+U4Cr9a/FZlHfH/hwAeBJNBp1gMyJNxROJZhT3dn1cO+jnYfQ==", + "version": "3.972.82", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.82.tgz", + "integrity": "sha512-vQzlDHlC24Yz42p+rT4NBoP/5VdetkBoujbOLq9h6QYWC8THuZSPYnXIR9OP3XcOrdMgPBBipOrdssfxnIiWUg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.3", - "@aws-sdk/types": "^3.973.8", - "@smithy/fetch-http-handler": "^5.3.17", - "@smithy/node-http-handler": "^4.6.0", - "@smithy/property-provider": "^4.2.14", - "@smithy/protocol-http": "^5.3.14", - "@smithy/smithy-client": "^4.12.12", - "@smithy/types": "^4.14.1", - "@smithy/util-stream": "^4.5.24", + "@aws-sdk/core": "^3.979.1", + "@aws-sdk/types": "^3.975.0", + "@smithy/core": "^3.38.0", + "@smithy/fetch-http-handler": "^5.8.0", + "@smithy/node-http-handler": "^4.14.0", + "@smithy/types": "^4.19.0", "tslib": "^2.6.2" }, "engines": { @@ -318,24 +267,23 @@ } }, "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.972.33", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.33.tgz", - "integrity": "sha512-UwdbJbOrgnOxZbshaNZ4DzX35h5wQd33MNYTGzWhN3ORG9lG9KQbDX6l6tDJSAdaGTktJoZPSritmUoW1rYkRA==", + "version": "3.973.25", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.973.25.tgz", + "integrity": "sha512-b5xl71uCRdWsu7P1VeGpiGuCf6zfYETsSNOv8AmYVTrsgbvTJRXdmmOPVfxkvGGVNsiKkGctClQW3jAn0SZLug==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.3", - "@aws-sdk/credential-provider-env": "^3.972.29", - "@aws-sdk/credential-provider-http": "^3.972.31", - "@aws-sdk/credential-provider-login": "^3.972.33", - "@aws-sdk/credential-provider-process": "^3.972.29", - "@aws-sdk/credential-provider-sso": "^3.972.33", - "@aws-sdk/credential-provider-web-identity": "^3.972.33", - "@aws-sdk/nested-clients": "^3.997.1", - "@aws-sdk/types": "^3.973.8", - "@smithy/credential-provider-imds": "^4.2.14", - "@smithy/property-provider": "^4.2.14", - "@smithy/shared-ini-file-loader": "^4.4.9", - "@smithy/types": "^4.14.1", + "@aws-sdk/core": "^3.979.1", + "@aws-sdk/credential-provider-env": "^3.972.80", + "@aws-sdk/credential-provider-http": "^3.972.82", + "@aws-sdk/credential-provider-login": "^3.972.87", + "@aws-sdk/credential-provider-process": "^3.972.80", + "@aws-sdk/credential-provider-sso": "^3.973.24", + "@aws-sdk/credential-provider-web-identity": "^3.972.86", + "@aws-sdk/nested-clients": "^3.997.54", + "@aws-sdk/types": "^3.975.0", + "@smithy/core": "^3.38.0", + "@smithy/credential-provider-imds": "^4.5.2", + "@smithy/types": "^4.19.0", "tslib": "^2.6.2" }, "engines": { @@ -343,18 +291,16 @@ } }, "node_modules/@aws-sdk/credential-provider-login": { - "version": "3.972.33", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.33.tgz", - "integrity": "sha512-WyZuPVoDM1HGNl41eVg8HSSXIB+FGkuuK63GhDbh4TMdfWU03AciWvF/QqOVWvJtWVYaLddANJ+aUklVr2ieuw==", + "version": "3.972.87", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.87.tgz", + "integrity": "sha512-C4ZFmcwM+Zj14Z8huVUk54Gv4uzT1ZdzG9cY3Gdh8FePakq7Bss1OFsgZjAhWFOkFOGpBvFBeu0OWjWf8HNKRQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.3", - "@aws-sdk/nested-clients": "^3.997.1", - "@aws-sdk/types": "^3.973.8", - "@smithy/property-provider": "^4.2.14", - "@smithy/protocol-http": "^5.3.14", - "@smithy/shared-ini-file-loader": "^4.4.9", - "@smithy/types": "^4.14.1", + "@aws-sdk/core": "^3.979.1", + "@aws-sdk/nested-clients": "^3.997.54", + "@aws-sdk/types": "^3.975.0", + "@smithy/core": "^3.38.0", + "@smithy/types": "^4.19.0", "tslib": "^2.6.2" }, "engines": { @@ -362,22 +308,21 @@ } }, "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.972.34", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.34.tgz", - "integrity": "sha512-sPcisURibKU4x0PCWJkWF1KJYm49Cph9dCn/PAnG5FU0wq5Id3g2v7RuEWAtNlKv1Af4gUJYBVGOeNpSEEx41A==", + "version": "3.972.92", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.92.tgz", + "integrity": "sha512-jC3zmOo7Qxlk+oBivvSsS1Ph7sFGm0QfuhqDGbqgRXi4cxUYMG+dWtBwS2XYYXgc8TIVXJiwgQ65sbNpbYupTA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/credential-provider-env": "^3.972.29", - "@aws-sdk/credential-provider-http": "^3.972.31", - "@aws-sdk/credential-provider-ini": "^3.972.33", - "@aws-sdk/credential-provider-process": "^3.972.29", - "@aws-sdk/credential-provider-sso": "^3.972.33", - "@aws-sdk/credential-provider-web-identity": "^3.972.33", - "@aws-sdk/types": "^3.973.8", - "@smithy/credential-provider-imds": "^4.2.14", - "@smithy/property-provider": "^4.2.14", - "@smithy/shared-ini-file-loader": "^4.4.9", - "@smithy/types": "^4.14.1", + "@aws-sdk/credential-provider-env": "^3.972.80", + "@aws-sdk/credential-provider-http": "^3.972.82", + "@aws-sdk/credential-provider-ini": "^3.973.25", + "@aws-sdk/credential-provider-process": "^3.972.80", + "@aws-sdk/credential-provider-sso": "^3.973.24", + "@aws-sdk/credential-provider-web-identity": "^3.972.86", + "@aws-sdk/types": "^3.975.0", + "@smithy/core": "^3.38.0", + "@smithy/credential-provider-imds": "^4.5.2", + "@smithy/types": "^4.19.0", "tslib": "^2.6.2" }, "engines": { @@ -385,16 +330,15 @@ } }, "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.972.29", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.29.tgz", - "integrity": "sha512-DURisqWS3bUgiwMXTmzymVNGlcRW0FnbPZ3SZknhmxnCXm3n9idkTJ6T+Uir359KRKtJNFLRViskk8HsSVLi1w==", + "version": "3.972.80", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.80.tgz", + "integrity": "sha512-xqfixO5aclje7tbQJDias+iNND5wp/3QZixCdZcVQpgWPTZ0V5DfTW9A804A5Z7ezu7sH3WRO+vzUjtiJBA8yg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.3", - "@aws-sdk/types": "^3.973.8", - "@smithy/property-provider": "^4.2.14", - "@smithy/shared-ini-file-loader": "^4.4.9", - "@smithy/types": "^4.14.1", + "@aws-sdk/core": "^3.979.1", + "@aws-sdk/types": "^3.975.0", + "@smithy/core": "^3.38.0", + "@smithy/types": "^4.19.0", "tslib": "^2.6.2" }, "engines": { @@ -402,18 +346,17 @@ } }, "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.972.33", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.33.tgz", - "integrity": "sha512-9y9obU4IQWru9f+NiiscUeyCe5ZmQav4FKEb1qfUNrik/C3BzBGUnHQWyPEyXjOX9cb+vx1TYx0qZBtinKdzTA==", + "version": "3.973.24", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.973.24.tgz", + "integrity": "sha512-nlg3BKG03Cgzvv/dMnhxvDJPhAhQSpaN0RUdwgU7ziwkasoev7iI8el8Xy4Rd5sqzgaDUbV269fOXck5SargqA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.3", - "@aws-sdk/nested-clients": "^3.997.1", - "@aws-sdk/token-providers": "3.1034.0", - "@aws-sdk/types": "^3.973.8", - "@smithy/property-provider": "^4.2.14", - "@smithy/shared-ini-file-loader": "^4.4.9", - "@smithy/types": "^4.14.1", + "@aws-sdk/core": "^3.979.1", + "@aws-sdk/nested-clients": "^3.997.54", + "@aws-sdk/token-providers": "3.1161.0", + "@aws-sdk/types": "^3.975.0", + "@smithy/core": "^3.38.0", + "@smithy/types": "^4.19.0", "tslib": "^2.6.2" }, "engines": { @@ -421,17 +364,16 @@ } }, "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.972.33", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.33.tgz", - "integrity": "sha512-RazhlN0YAkna2T2p2v4YuuRlVBVRNo8V0SL+9JePTWDndEUAeOBAjYeQfAMbtDyCh120+zA0Op6V0jS4dw2+iw==", + "version": "3.972.86", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.86.tgz", + "integrity": "sha512-zKTyLJBEyfQBVlYqT6Jt+mYV4r8kGbWM7dn6v4Y4MRARizhYafjDXP/iMu76UMRhToSPNmIKX1XAb24gtpBFIA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.3", - "@aws-sdk/nested-clients": "^3.997.1", - "@aws-sdk/types": "^3.973.8", - "@smithy/property-provider": "^4.2.14", - "@smithy/shared-ini-file-loader": "^4.4.9", - "@smithy/types": "^4.14.1", + "@aws-sdk/core": "^3.979.1", + "@aws-sdk/nested-clients": "^3.997.54", + "@aws-sdk/types": "^3.975.0", + "@smithy/core": "^3.38.0", + "@smithy/types": "^4.19.0", "tslib": "^2.6.2" }, "engines": { @@ -483,31 +425,6 @@ "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/middleware-sdk-s3": { - "version": "3.972.32", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.32.tgz", - "integrity": "sha512-dc2O2x0V5pGJhmdQYQveUIFtMZsur7GrGuSgoKM4oQJuEcfvwnJ3sj+ip6WnxR5l6TrX5zkl4KgcgswOy3wAzQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.974.3", - "@aws-sdk/types": "^3.973.8", - "@aws-sdk/util-arn-parser": "^3.972.3", - "@smithy/core": "^3.23.16", - "@smithy/node-config-provider": "^4.3.14", - "@smithy/protocol-http": "^5.3.14", - "@smithy/signature-v4": "^5.3.14", - "@smithy/smithy-client": "^4.12.12", - "@smithy/types": "^4.14.1", - "@smithy/util-config-provider": "^4.2.2", - "@smithy/util-middleware": "^4.2.14", - "@smithy/util-stream": "^4.5.24", - "@smithy/util-utf8": "^4.2.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, "node_modules/@aws-sdk/middleware-user-agent": { "version": "3.972.33", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.33.tgz", @@ -528,49 +445,18 @@ } }, "node_modules/@aws-sdk/nested-clients": { - "version": "3.997.1", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.1.tgz", - "integrity": "sha512-Afc9hc2WZs3X4Jb8dnxyuYiZsLoWRO51roTCRf497gPnAKN2WRdXANu1vaVCTzwnDMOYFXb/cYv4ZSjxqAqcKA==", + "version": "3.997.54", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.54.tgz", + "integrity": "sha512-/OczlavsS7U5sOodRO10Ug4R/AKFVZiXTstKipWe68EfjPkmdDFNXNl9FCJyXWkg1DG6RBm3qAuVB8FNW9O8KQ==", "license": "Apache-2.0", "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.974.3", - "@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.33", - "@aws-sdk/region-config-resolver": "^3.972.13", - "@aws-sdk/signature-v4-multi-region": "^3.996.20", - "@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.19", - "@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", + "@aws-sdk/core": "^3.979.1", + "@aws-sdk/signature-v4-multi-region": "^3.996.51", + "@aws-sdk/types": "^3.975.0", + "@smithy/core": "^3.38.0", + "@smithy/fetch-http-handler": "^5.8.0", + "@smithy/node-http-handler": "^4.14.0", + "@smithy/types": "^4.19.0", "tslib": "^2.6.2" }, "engines": { @@ -594,16 +480,14 @@ } }, "node_modules/@aws-sdk/signature-v4-multi-region": { - "version": "3.996.20", - "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.20.tgz", - "integrity": "sha512-MEj6DhEcaO8RgVtFCJ+xpCQnZC3Iesr09avdY75qkMQfckQULu447IegK7Rs1MCGerVBfKnJQ4q+pQq9hI5lng==", + "version": "3.996.51", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.51.tgz", + "integrity": "sha512-+aHvUugVBuMPXxgkPfWnTMyGdE/yWS8GHi5+qQ37HowS0YqzrbuaDLznj+Dtmtu5jgCdqz36gXmujOToUhNNWw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-sdk-s3": "^3.972.32", - "@aws-sdk/types": "^3.973.8", - "@smithy/protocol-http": "^5.3.14", - "@smithy/signature-v4": "^5.3.14", - "@smithy/types": "^4.14.1", + "@aws-sdk/types": "^3.975.0", + "@smithy/signature-v4": "^5.7.5", + "@smithy/types": "^4.19.0", "tslib": "^2.6.2" }, "engines": { @@ -611,17 +495,16 @@ } }, "node_modules/@aws-sdk/token-providers": { - "version": "3.1034.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1034.0.tgz", - "integrity": "sha512-8E+KGcD4ET0H9FXJ2/ZWbfFnQNYEkTZZYJxAs1lkdJlve1AYuqaydInIFfvNgoz5GbYtzbK8/ugsSMu5wPm6kA==", + "version": "3.1161.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1161.0.tgz", + "integrity": "sha512-ypbXSofNcSHwf/KA40bFCXJb0MLaAKrfT7sot4dFaA+2lsuXNah4zLRJIAU7Qmvs9PFJ6rnY0bZFEC+mE7attA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.3", - "@aws-sdk/nested-clients": "^3.997.1", - "@aws-sdk/types": "^3.973.8", - "@smithy/property-provider": "^4.2.14", - "@smithy/shared-ini-file-loader": "^4.4.9", - "@smithy/types": "^4.14.1", + "@aws-sdk/core": "^3.979.1", + "@aws-sdk/nested-clients": "^3.997.54", + "@aws-sdk/types": "^3.975.0", + "@smithy/core": "^3.38.0", + "@smithy/types": "^4.19.0", "tslib": "^2.6.2" }, "engines": { @@ -629,24 +512,12 @@ } }, "node_modules/@aws-sdk/types": { - "version": "3.973.8", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.8.tgz", - "integrity": "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/util-arn-parser": { - "version": "3.972.3", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.972.3.tgz", - "integrity": "sha512-HzSD8PMFrvgi2Kserxuff5VitNq2sgf3w9qxmskKDiDTThWfVteJxuCS9JXiPIPtmCrp+7N9asfIaVhBFORllA==", + "version": "3.975.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.975.0.tgz", + "integrity": "sha512-WQlhs6juByzWOzj5fwxOx0mPJaEqMcG/sDpl3Id61B3XhKE63xY+0QWcnG+sWR/sCfprmreyUo1lM7IKZmHMzw==", "license": "Apache-2.0", "dependencies": { + "@smithy/types": "^4.19.0", "tslib": "^2.6.2" }, "engines": { @@ -719,13 +590,12 @@ } }, "node_modules/@aws-sdk/xml-builder": { - "version": "3.972.18", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.18.tgz", - "integrity": "sha512-BMDNVG1ETXRhl1tnisQiYBef3RShJ1kfZA7x7afivTFMLirfHNTb6U71K569HNXhSXbQZsweHvSDZ6euBw8hPA==", + "version": "3.972.42", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.42.tgz", + "integrity": "sha512-smX8/gNP8Kjq+CWTBMZQLQ912vOQjiWExzwzvMO+eujPB5RY8IbtF0AGdUUXHKN7KoDWK9dIcuW0WG269kiP6w==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.14.1", - "fast-xml-parser": "5.5.8", + "@smithy/types": "^4.19.0", "tslib": "^2.6.2" }, "engines": { @@ -1437,20 +1307,12 @@ } }, "node_modules/@smithy/core": { - "version": "3.23.16", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.23.16.tgz", - "integrity": "sha512-JStomOrINQA1VqNEopLsgcdgwd42au7mykKqVr30XFw89wLt9sDxJDi4djVPRwQmmzyTGy/uOvTc2ultMpFi1w==", + "version": "3.38.1", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.38.1.tgz", + "integrity": "sha512-U/mB2XiwTLaf+NSgLSWwJfptsMT/H8aBi0aTAA/E5e2/TagFeGCqAyhKGG0w0A9IWTkm4qYEmLoD/HB6aYfnRA==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.14", - "@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-middleware": "^4.2.14", - "@smithy/util-stream": "^4.5.24", - "@smithy/util-utf8": "^4.2.2", - "@smithy/uuid": "^1.1.2", + "@smithy/types": "^4.19.0", "tslib": "^2.6.2" }, "engines": { @@ -1458,15 +1320,13 @@ } }, "node_modules/@smithy/credential-provider-imds": { - "version": "4.2.14", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.14.tgz", - "integrity": "sha512-Au28zBN48ZAoXdooGUHemuVBrkE+Ie6RPmGNIAJsFqj33Vhb6xAgRifUydZ2aY+M+KaMAETAlKk5NC5h1G7wpg==", + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.5.2.tgz", + "integrity": "sha512-A9uSdn72ozbRUSit0eib0TW7nXuNPlaeM0zcGkJ+nE6tFcSDbnmtwoxbTCFBukVQcszDAyvsd7+rTduPTXpygg==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.14", - "@smithy/property-provider": "^4.2.14", - "@smithy/types": "^4.14.1", - "@smithy/url-parser": "^4.2.14", + "@smithy/core": "^3.33.2", + "@smithy/types": "^4.17.2", "tslib": "^2.6.2" }, "engines": { @@ -1474,15 +1334,13 @@ } }, "node_modules/@smithy/fetch-http-handler": { - "version": "5.3.17", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.17.tgz", - "integrity": "sha512-bXOvQzaSm6MnmLaWA1elgfQcAtN4UP3vXqV97bHuoOrHQOJiLT3ds6o9eo5bqd0TJfRFpzdGnDQdW3FACiAVdw==", + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.8.0.tgz", + "integrity": "sha512-ycSJu3tFAQ4v04CBB0agqFMVsSQ1iG3yw+SpgxRqKfaURpQD4CZ8Wn0zPMmSnOuTpTh65Vz+EA0rMrw089wvkA==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.14", - "@smithy/querystring-builder": "^4.2.14", - "@smithy/types": "^4.14.1", - "@smithy/util-base64": "^4.3.2", + "@smithy/core": "^3.33.3", + "@smithy/types": "^4.18.0", "tslib": "^2.6.2" }, "engines": { @@ -1627,14 +1485,13 @@ } }, "node_modules/@smithy/node-http-handler": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.6.0.tgz", - "integrity": "sha512-P734cAoTFtuGfWa/R3jgBnGlURt2w9bYEBwQNMKf58sRM9RShirB2mKwLsVP+jlG/wxpCu8abv8NxdUts8tdLA==", + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.14.1.tgz", + "integrity": "sha512-/T6E1jzCL0fErS9uq8JqES/5yYQQ4ppVmmYHwXw2nlov8KFzLb+3QqHR5iAun1BsuAB50IPxZliV8YMgghLwhA==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.14", - "@smithy/querystring-builder": "^4.2.14", - "@smithy/types": "^4.14.1", + "@smithy/core": "^3.38.1", + "@smithy/types": "^4.19.0", "tslib": "^2.6.2" }, "engines": { @@ -1667,20 +1524,6 @@ "node": ">=18.0.0" } }, - "node_modules/@smithy/querystring-builder": { - "version": "4.2.14", - "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.14.tgz", - "integrity": "sha512-XYA5Z0IqTeF+5XDdh4BBmSA0HvbgVZIyv4cmOoUheDNR57K1HgBp9ukUMx3Cr3XpDHHpLBnexPE3LAtDsZkj2A==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.14.1", - "@smithy/util-uri-escape": "^4.2.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/@smithy/querystring-parser": { "version": "4.2.14", "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.14.tgz", @@ -1720,18 +1563,13 @@ } }, "node_modules/@smithy/signature-v4": { - "version": "5.3.14", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.14.tgz", - "integrity": "sha512-1D9Y/nmlVjCeSivCbhZ7hgEpmHyY1h0GvpSZt3l0xcD9JjmjVC1CHOozS6+Gh+/ldMH8JuJ6cujObQqfayAVFA==", + "version": "5.7.5", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.7.5.tgz", + "integrity": "sha512-WPjdr5oWOk1LArpI4pavu+fSo+/evi0Re1olxGvqwuZopd4gYR1UGCi+Soyn/IjmafqbWs+ZC+3AKxJS3ZpwUg==", "license": "Apache-2.0", "dependencies": { - "@smithy/is-array-buffer": "^4.2.2", - "@smithy/protocol-http": "^5.3.14", - "@smithy/types": "^4.14.1", - "@smithy/util-hex-encoding": "^4.2.2", - "@smithy/util-middleware": "^4.2.14", - "@smithy/util-uri-escape": "^4.2.2", - "@smithy/util-utf8": "^4.2.2", + "@smithy/core": "^3.38.0", + "@smithy/types": "^4.19.0", "tslib": "^2.6.2" }, "engines": { @@ -1757,9 +1595,9 @@ } }, "node_modules/@smithy/types": { - "version": "4.14.1", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.1.tgz", - "integrity": "sha512-59b5HtSVrVR/eYNei3BUj3DCPKD/G7EtDDe7OEJE7i7FtQFugYo6MxbotS8mVJkLNVf8gYaAlEBwwtJ9HzhWSg==", + "version": "4.19.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.19.0.tgz", + "integrity": "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -1950,18 +1788,6 @@ "node": ">=18.0.0" } }, - "node_modules/@smithy/util-uri-escape": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.2.tgz", - "integrity": "sha512-2kAStBlvq+lTXHyAZYfJRb/DfS3rsinLiwb+69SstC9Vb0s9vNWkRwpnj918Pfi85mzi42sOqdV72OLxWAISnw==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/@smithy/util-utf8": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.2.tgz", @@ -1975,19 +1801,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", @@ -2888,41 +2701,6 @@ "dev": true, "license": "MIT" }, - "node_modules/fast-xml-builder": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.5.tgz", - "integrity": "sha512-4TJn/8FKLeslLAH3dnohXqE3QSoxkhvaMzepOIZytwJXZO69Bfz0HBdDHzOTOon6G59Zrk6VQ2bEiv1t61rfkA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "dependencies": { - "path-expression-matcher": "^1.1.3" - } - }, - "node_modules/fast-xml-parser": { - "version": "5.5.8", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.5.8.tgz", - "integrity": "sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "dependencies": { - "fast-xml-builder": "^1.1.4", - "path-expression-matcher": "^1.2.0", - "strnum": "^2.2.0" - }, - "bin": { - "fxparser": "src/cli/cli.js" - } - }, "node_modules/fastq": { "version": "1.20.3", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.3.tgz", @@ -3754,21 +3532,6 @@ "node": ">=8" } }, - "node_modules/path-expression-matcher": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz", - "integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -4207,18 +3970,6 @@ "node": ">=4" } }, - "node_modules/strnum": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.3.tgz", - "integrity": "sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT" - }, "node_modules/term-size": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.1.tgz", @@ -4430,7 +4181,7 @@ "version": "0.2.0", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/client-ses": "^3.917.0", + "@aws-sdk/client-sesv2": "^3.1034.0", "@aws-sdk/client-sns": "^3.917.0", "@seamless-auth/messaging": "^0.2.0" }, @@ -4438,6 +4189,57 @@ "node": ">=18" } }, + "packages/aws/node_modules/@aws-sdk/client-sesv2": { + "version": "3.1034.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sesv2/-/client-sesv2-3.1034.0.tgz", + "integrity": "sha512-dXlOzN++LHyXTDbCZzkbMLQTKrGuedogE4Xn1JPeZ5Cvh9RI1kBcPSDj6j328ngZUJFPW7ySTucU3W5sOOmMNw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.974.3", + "@aws-sdk/credential-provider-node": "^3.972.34", + "@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.33", + "@aws-sdk/region-config-resolver": "^3.972.13", + "@aws-sdk/signature-v4-multi-region": "^3.996.20", + "@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.19", + "@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", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, "packages/core": { "name": "@seamless-auth/messaging", "version": "0.2.0", diff --git a/packages/aws/package.json b/packages/aws/package.json index 8380434..2967cb4 100644 --- a/packages/aws/package.json +++ b/packages/aws/package.json @@ -26,7 +26,7 @@ "clean": "rm -rf dist" }, "dependencies": { - "@aws-sdk/client-ses": "^3.917.0", + "@aws-sdk/client-sesv2": "^3.1034.0", "@aws-sdk/client-sns": "^3.917.0", "@seamless-auth/messaging": "^0.2.0" }, diff --git a/packages/aws/src/index.ts b/packages/aws/src/index.ts index 9a27972..f0fa077 100644 --- a/packages/aws/src/index.ts +++ b/packages/aws/src/index.ts @@ -6,10 +6,10 @@ import { } from "@aws-sdk/client-sns"; import { SendEmailCommand, - SESClient, + SESv2Client, type SendEmailCommandOutput, type SendEmailCommandInput, -} from "@aws-sdk/client-ses"; +} from "@aws-sdk/client-sesv2"; import type { DeliveryResult, EmailMessage, @@ -56,35 +56,45 @@ class AwsSesEmailTransport implements EmailTransport { assertNonEmptyString(config.fromEmail, "config.fromEmail"); assertLikelyEmail(config.fromEmail, "config.fromEmail"); this.config = config; - this.client = config.sesClient ?? new SESClient({ region: config.region }); + this.client = config.sesClient ?? new SESv2Client({ region: config.region }); } async send(message: EmailMessage): Promise { assertEmailMessage(message); + const headers = Object.entries(message.headers ?? {}).map(([Name, Value]) => ({ + Name, + Value, + })); + + // SES v2, because the v1 SendEmail call has no way to carry custom headers. + // Simple content is still authorised by ses:SendEmail, as before. const input: SendEmailCommandInput = { - Source: message.from ?? this.config.fromEmail, + FromEmailAddress: message.from ?? this.config.fromEmail, Destination: { ToAddresses: [message.to], }, - Message: { - Subject: { - Charset: "UTF-8", - Data: message.subject, - }, - Body: { - ...(message.html - ? { - Html: { - Charset: "UTF-8", - Data: message.html, - }, - } - : {}), - Text: { + Content: { + Simple: { + Subject: { Charset: "UTF-8", - Data: message.text, + Data: message.subject, + }, + Body: { + ...(message.html + ? { + Html: { + Charset: "UTF-8", + Data: message.html, + }, + } + : {}), + Text: { + Charset: "UTF-8", + Data: message.text, + }, }, + ...(headers.length > 0 ? { Headers: headers } : {}), }, }, }; diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index c4d903d..02b6712 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -14,6 +14,12 @@ export interface EmailMessage { subject: string; text: string; html?: string; + /** + * Extra headers to send with the message, such as `List-Unsubscribe`. Names + * and values are passed through as given; a transport that cannot send + * custom headers ignores them. + */ + headers?: Record; } export interface SmsMessage { diff --git a/packages/core/src/validation.ts b/packages/core/src/validation.ts index 5a8d954..d58d756 100644 --- a/packages/core/src/validation.ts +++ b/packages/core/src/validation.ts @@ -48,6 +48,45 @@ export function assertEmailMessage(message: EmailMessage): void { if (message.html !== undefined) { assertNonEmptyString(message.html, "message.html"); } + + if (message.headers !== undefined) { + assertEmailHeaders(message.headers); + } +} + +// RFC 5322 field names are printable ASCII without a colon. A line break in +// either part would let a caller inject further headers, or a body. +const HEADER_NAME = /^[!-9;-~]+$/; +const LINE_BREAK = /[\r\n]/; + +// Set by the transport from the message's own fields, so a header naming one +// would be a second, conflicting copy. +const RESERVED_HEADERS = new Set(["from", "to", "cc", "bcc", "subject", "content-type"]); + +export function assertEmailHeaders(headers: Record): void { + if (typeof headers !== "object" || headers === null || Array.isArray(headers)) { + throw new MessagingError("INVALID_INPUT", `"message.headers" must be an object.`); + } + + for (const [name, value] of Object.entries(headers)) { + if (!HEADER_NAME.test(name)) { + throw new MessagingError("INVALID_INPUT", `"${name}" is not a valid header name.`); + } + + if (RESERVED_HEADERS.has(name.toLowerCase())) { + throw new MessagingError( + "INVALID_INPUT", + `"${name}" is set from the message itself and cannot be passed as a header.`, + ); + } + + if (typeof value !== "string" || value.trim().length === 0 || LINE_BREAK.test(value)) { + throw new MessagingError( + "INVALID_INPUT", + `The "${name}" header must be a non-empty string on a single line.`, + ); + } + } } export function assertSmsMessage(message: SmsMessage): void { diff --git a/tests/providers.test.mjs b/tests/providers.test.mjs index ae1a771..6467e87 100644 --- a/tests/providers.test.mjs +++ b/tests/providers.test.mjs @@ -30,10 +30,63 @@ test("AWS SES provider sends normalized email payloads", async () => { assert.equal(result.provider, "aws-ses"); assert.equal(result.channel, "email"); assert.equal(result.messageId, "ses-123"); - assert.equal(capturedCommand.input.Source, "noreply@example.com"); + assert.equal(capturedCommand.input.FromEmailAddress, "noreply@example.com"); assert.deepEqual(capturedCommand.input.Destination.ToAddresses, ["user@example.com"]); - assert.equal(capturedCommand.input.Message.Subject.Data, "Verify your email"); - assert.equal(capturedCommand.input.Message.Body.Text.Data, "Code: ABCDEF"); + assert.equal(capturedCommand.input.Content.Simple.Subject.Data, "Verify your email"); + assert.equal(capturedCommand.input.Content.Simple.Body.Text.Data, "Code: ABCDEF"); + assert.equal(capturedCommand.input.Content.Simple.Headers, undefined); +}); + +test("AWS SES provider passes custom headers through", async () => { + let capturedCommand; + + const provider = createAwsEmailTransport({ + region: "us-east-1", + fromEmail: "noreply@example.com", + sesClient: { + async send(command) { + capturedCommand = command; + return { MessageId: "ses-456" }; + }, + }, + }); + + await provider.send({ + to: "user@example.com", + subject: "What's new", + text: "A note.", + headers: { + "List-Unsubscribe": "", + "List-Unsubscribe-Post": "List-Unsubscribe=One-Click", + }, + }); + + assert.deepEqual(capturedCommand.input.Content.Simple.Headers, [ + { Name: "List-Unsubscribe", Value: "" }, + { Name: "List-Unsubscribe-Post", Value: "List-Unsubscribe=One-Click" }, + ]); +}); + +test("AWS SES provider refuses headers that would inject or collide", async () => { + const provider = createAwsEmailTransport({ + region: "us-east-1", + fromEmail: "noreply@example.com", + sesClient: { + async send() { + throw new Error("should not be sent"); + }, + }, + }); + const base = { to: "user@example.com", subject: "Hi", text: "Hello" }; + + for (const headers of [ + { "List-Unsubscribe": "\r\nBcc: x@example.com" }, + { "Bad Name": "value" }, + { Subject: "Another subject" }, + { "X-Empty": " " }, + ]) { + await assert.rejects(provider.send({ ...base, headers }), { code: "INVALID_INPUT" }); + } }); test("AWS SNS provider sends SMS with sender ID when configured", async () => { @@ -149,7 +202,7 @@ test("Mixed provider composition works for SES email plus Twilio SMS", async () assert.equal(smsResult.provider, "twilio"); assert.equal(deliveries[0].type, "email"); assert.equal(deliveries[1].type, "sms"); - assert.match(deliveries[0].input.Message.Subject.Data, /sign-in link/i); + assert.match(deliveries[0].input.Content.Simple.Subject.Data, /sign-in link/i); assert.match(deliveries[1].input.body, /123456/); }); @@ -287,9 +340,12 @@ test("Enrollment invites link to the sign-in page", async () => { }); assert.equal(result.provider, "aws-ses"); - assert.match(delivered[0].Message.Subject.Data, /add a passkey/i); - assert.match(delivered[0].Message.Body.Text.Data, /https:\/\/app\.example\.com\/login/); - assert.match(delivered[0].Message.Body.Html.Data, /href="https:\/\/app\.example\.com\/login"/); + assert.match(delivered[0].Content.Simple.Subject.Data, /add a passkey/i); + assert.match(delivered[0].Content.Simple.Body.Text.Data, /https:\/\/app\.example\.com\/login/); + assert.match( + delivered[0].Content.Simple.Body.Html.Data, + /href="https:\/\/app\.example\.com\/login"/, + ); }); test("Enrollment invites honour a custom handler and an override", async () => { @@ -323,7 +379,10 @@ test("Enrollment invites honour a custom handler and an override", async () => { to: "user@example.com", signInUrl: "https://app.example.com/login", }); - assert.equal(delivered[0].Message.Subject.Data, "[Seamless Demo] passkeys for user@example.com"); + assert.equal( + delivered[0].Content.Simple.Subject.Data, + "[Seamless Demo] passkeys for user@example.com", + ); }); test("Enrollment invites reject a sign-in URL that is not a URL", async () => {