From 83258bfeb816b2cbce6b6928f58ef6a1c625dbbe Mon Sep 17 00:00:00 2001 From: purva Date: Sat, 2 May 2026 17:52:21 +0530 Subject: [PATCH 1/3] feat: support standard AWS env vars and default credential chain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace non-standard AWS_ACCESS_KEY / AWS_SECRET_KEY with the AWS-standard AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY. The old names are kept as fallbacks in the runtimeEnv for backward compatibility. Both vars are now optional. When omitted, the credentials object is not passed to SESv2Client, STSClient, or SNSClient — the AWS SDK then falls back to its default provider chain (IAM roles, ECS task roles, instance profiles, etc.), which is the recommended approach for cloud-native deployments. Closes #316 Co-Authored-By: Claude Sonnet 4.6 --- .env.example | 4 ++-- .env.selfhost.example | 8 +++++--- apps/web/.env.test.example | 4 ++-- apps/web/src/env.js | 8 ++++---- apps/web/src/server/aws/ses.ts | 14 ++++++-------- apps/web/src/server/aws/sns.ts | 7 +++---- apps/web/src/test/setup/setup-env.ts | 4 ++-- 7 files changed, 24 insertions(+), 25 deletions(-) diff --git a/.env.example b/.env.example index 79539fe0..eeea5d0c 100644 --- a/.env.example +++ b/.env.example @@ -10,8 +10,8 @@ SMTP_USER=test_userdadad@example.com # Example SMTP user AWS_DEFAULT_REGION="us-east-1" -AWS_SECRET_KEY="some-secret-key" -AWS_ACCESS_KEY="some-access-key" +AWS_ACCESS_KEY_ID="some-access-key" +AWS_SECRET_ACCESS_KEY="some-secret-key" AWS_SES_ENDPOINT="http://localhost:3003/api/ses" AWS_SNS_ENDPOINT="http://localhost:3003/api/sns" diff --git a/.env.selfhost.example b/.env.selfhost.example index d294bd34..976d7782 100644 --- a/.env.selfhost.example +++ b/.env.selfhost.example @@ -25,10 +25,12 @@ GITHUB_SECRET="" GOOGLE_CLIENT_ID="" GOOGLE_CLIENT_SECRET="" -# AWS details - required +# AWS details +# Provide static credentials OR rely on the AWS default credential chain +# (IAM role, ECS task role, instance profile, etc.) by omitting these vars. AWS_DEFAULT_REGION="us-east-1" -AWS_SECRET_KEY="" -AWS_ACCESS_KEY="" +AWS_ACCESS_KEY_ID="" +AWS_SECRET_ACCESS_KEY="" diff --git a/apps/web/.env.test.example b/apps/web/.env.test.example index 1f62dec1..58459ee8 100644 --- a/apps/web/.env.test.example +++ b/apps/web/.env.test.example @@ -5,8 +5,8 @@ NEXTAUTH_SECRET=test-secret DATABASE_URL=postgresql://usesend:password@127.0.0.1:54329/usesend_test REDIS_URL=redis://127.0.0.1:6380/15 -AWS_ACCESS_KEY=test-access-key -AWS_SECRET_KEY=test-secret-key +AWS_ACCESS_KEY_ID=test-access-key +AWS_SECRET_ACCESS_KEY=test-secret-key AWS_DEFAULT_REGION=us-east-1 NEXT_PUBLIC_IS_CLOUD=true diff --git a/apps/web/src/env.js b/apps/web/src/env.js index 9c37cdde..b472fc77 100644 --- a/apps/web/src/env.js +++ b/apps/web/src/env.js @@ -31,8 +31,8 @@ export const env = createEnv({ ), GITHUB_ID: z.string().optional(), GITHUB_SECRET: z.string().optional(), - AWS_ACCESS_KEY: z.string(), - AWS_SECRET_KEY: z.string(), + AWS_ACCESS_KEY_ID: z.string().optional(), + AWS_SECRET_ACCESS_KEY: z.string().optional(), USESEND_API_KEY: z.string().optional(), UNSEND_API_KEY: z.string().optional(), GOOGLE_CLIENT_ID: z.string().optional(), @@ -99,8 +99,8 @@ export const env = createEnv({ NEXTAUTH_URL: process.env.NEXTAUTH_URL, GITHUB_ID: process.env.GITHUB_ID, GITHUB_SECRET: process.env.GITHUB_SECRET, - AWS_ACCESS_KEY: process.env.AWS_ACCESS_KEY, - AWS_SECRET_KEY: process.env.AWS_SECRET_KEY, + AWS_ACCESS_KEY_ID: process.env.AWS_ACCESS_KEY_ID ?? process.env.AWS_ACCESS_KEY, + AWS_SECRET_ACCESS_KEY: process.env.AWS_SECRET_ACCESS_KEY ?? process.env.AWS_SECRET_KEY, USESEND_API_KEY: process.env.USESEND_API_KEY, UNSEND_API_KEY: process.env.UNSEND_API_KEY, GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID, diff --git a/apps/web/src/server/aws/ses.ts b/apps/web/src/server/aws/ses.ts index 0fda94c8..4b0a6a87 100644 --- a/apps/web/src/server/aws/ses.ts +++ b/apps/web/src/server/aws/ses.ts @@ -30,10 +30,9 @@ async function getAccountId(region: string) { const stsClient = new STSClient({ region: region, - credentials: { - accessKeyId: env.AWS_ACCESS_KEY, - secretAccessKey: env.AWS_SECRET_KEY, - }, + ...(env.AWS_ACCESS_KEY_ID && env.AWS_SECRET_ACCESS_KEY + ? { credentials: { accessKeyId: env.AWS_ACCESS_KEY_ID, secretAccessKey: env.AWS_SECRET_ACCESS_KEY } } + : {}), }); const command = new GetCallerIdentityCommand({}); const response = await stsClient.send(command); @@ -50,10 +49,9 @@ function getSesClient(region: string) { return new SESv2Client({ region: region, endpoint: env.AWS_SES_ENDPOINT, - credentials: { - accessKeyId: env.AWS_ACCESS_KEY, - secretAccessKey: env.AWS_SECRET_KEY, - }, + ...(env.AWS_ACCESS_KEY_ID && env.AWS_SECRET_ACCESS_KEY + ? { credentials: { accessKeyId: env.AWS_ACCESS_KEY_ID, secretAccessKey: env.AWS_SECRET_ACCESS_KEY } } + : {}), }); } diff --git a/apps/web/src/server/aws/sns.ts b/apps/web/src/server/aws/sns.ts index 0b99b135..f810952b 100644 --- a/apps/web/src/server/aws/sns.ts +++ b/apps/web/src/server/aws/sns.ts @@ -10,10 +10,9 @@ function getSnsClient(region: string) { return new SNSClient({ endpoint: env.AWS_SNS_ENDPOINT, region: region, - credentials: { - accessKeyId: env.AWS_ACCESS_KEY, - secretAccessKey: env.AWS_SECRET_KEY, - }, + ...(env.AWS_ACCESS_KEY_ID && env.AWS_SECRET_ACCESS_KEY + ? { credentials: { accessKeyId: env.AWS_ACCESS_KEY_ID, secretAccessKey: env.AWS_SECRET_ACCESS_KEY } } + : {}), }); } diff --git a/apps/web/src/test/setup/setup-env.ts b/apps/web/src/test/setup/setup-env.ts index 96d56b45..c6f58790 100644 --- a/apps/web/src/test/setup/setup-env.ts +++ b/apps/web/src/test/setup/setup-env.ts @@ -4,8 +4,8 @@ const defaultEnv: Record = { NEXTAUTH_SECRET: "test-secret", DATABASE_URL: "postgresql://usesend:password@127.0.0.1:54329/usesend_test", REDIS_URL: "redis://127.0.0.1:6380/15", - AWS_ACCESS_KEY: "test-access-key", - AWS_SECRET_KEY: "test-secret-key", + AWS_ACCESS_KEY_ID: "test-access-key", + AWS_SECRET_ACCESS_KEY: "test-secret-key", AWS_DEFAULT_REGION: "us-east-1", NEXT_PUBLIC_IS_CLOUD: "true", API_RATE_LIMIT: "2", From 4277fb860fb54af1501d0db589c9b438d20211b5 Mon Sep 17 00:00:00 2001 From: Purva Kandalgaonkar <136103488+purva-8@users.noreply.github.com> Date: Sat, 2 May 2026 18:02:27 +0530 Subject: [PATCH 2/3] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- apps/web/src/env.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/src/env.js b/apps/web/src/env.js index b472fc77..8e73959a 100644 --- a/apps/web/src/env.js +++ b/apps/web/src/env.js @@ -99,8 +99,8 @@ export const env = createEnv({ NEXTAUTH_URL: process.env.NEXTAUTH_URL, GITHUB_ID: process.env.GITHUB_ID, GITHUB_SECRET: process.env.GITHUB_SECRET, - AWS_ACCESS_KEY_ID: process.env.AWS_ACCESS_KEY_ID ?? process.env.AWS_ACCESS_KEY, - AWS_SECRET_ACCESS_KEY: process.env.AWS_SECRET_ACCESS_KEY ?? process.env.AWS_SECRET_KEY, + AWS_ACCESS_KEY_ID: process.env.AWS_ACCESS_KEY_ID || process.env.AWS_ACCESS_KEY, + AWS_SECRET_ACCESS_KEY: process.env.AWS_SECRET_ACCESS_KEY || process.env.AWS_SECRET_KEY, USESEND_API_KEY: process.env.USESEND_API_KEY, UNSEND_API_KEY: process.env.UNSEND_API_KEY, GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID, From 5c816be7a3c1fdfd95a632fdabff07eb8b390a3e Mon Sep 17 00:00:00 2001 From: purva Date: Sat, 2 May 2026 18:07:27 +0530 Subject: [PATCH 3/3] refactor: extract shared getAwsCredentialOptions helper and add partial-config guard - Move the credential spread logic into a single credentials.ts helper so SESv2Client, STSClient, and SNSClient all share one implementation - Throw a clear error if only one of AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY is set, preventing silent fallback to the default provider chain with a half-configured environment Co-Authored-By: Claude Sonnet 4.6 --- apps/web/src/server/aws/credentials.ts | 22 ++++++++++++++++++++++ apps/web/src/server/aws/ses.ts | 9 +++------ apps/web/src/server/aws/sns.ts | 5 ++--- 3 files changed, 27 insertions(+), 9 deletions(-) create mode 100644 apps/web/src/server/aws/credentials.ts diff --git a/apps/web/src/server/aws/credentials.ts b/apps/web/src/server/aws/credentials.ts new file mode 100644 index 00000000..551dee82 --- /dev/null +++ b/apps/web/src/server/aws/credentials.ts @@ -0,0 +1,22 @@ +import { env } from "~/env"; + +export function getAwsCredentialOptions() { + const hasKey = !!env.AWS_ACCESS_KEY_ID; + const hasSecret = !!env.AWS_SECRET_ACCESS_KEY; + + if (hasKey !== hasSecret) { + throw new Error( + "AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY must both be set or both be omitted" + ); + } + + if (hasKey) { + return { + credentials: { + accessKeyId: env.AWS_ACCESS_KEY_ID!, + secretAccessKey: env.AWS_SECRET_ACCESS_KEY!, + }, + }; + } + return {}; +} diff --git a/apps/web/src/server/aws/ses.ts b/apps/web/src/server/aws/ses.ts index 4b0a6a87..7c19e17d 100644 --- a/apps/web/src/server/aws/ses.ts +++ b/apps/web/src/server/aws/ses.ts @@ -17,6 +17,7 @@ import { STSClient, GetCallerIdentityCommand } from "@aws-sdk/client-sts"; import { generateKeyPairSync } from "crypto"; import nodemailer from "nodemailer"; import { env } from "~/env"; +import { getAwsCredentialOptions } from "./credentials"; import { EmailContent } from "~/types"; import { logger } from "../logger/log"; import { buildHeaders } from "~/server/utils/email-headers"; @@ -30,9 +31,7 @@ async function getAccountId(region: string) { const stsClient = new STSClient({ region: region, - ...(env.AWS_ACCESS_KEY_ID && env.AWS_SECRET_ACCESS_KEY - ? { credentials: { accessKeyId: env.AWS_ACCESS_KEY_ID, secretAccessKey: env.AWS_SECRET_ACCESS_KEY } } - : {}), + ...getAwsCredentialOptions(), }); const command = new GetCallerIdentityCommand({}); const response = await stsClient.send(command); @@ -49,9 +48,7 @@ function getSesClient(region: string) { return new SESv2Client({ region: region, endpoint: env.AWS_SES_ENDPOINT, - ...(env.AWS_ACCESS_KEY_ID && env.AWS_SECRET_ACCESS_KEY - ? { credentials: { accessKeyId: env.AWS_ACCESS_KEY_ID, secretAccessKey: env.AWS_SECRET_ACCESS_KEY } } - : {}), + ...getAwsCredentialOptions(), }); } diff --git a/apps/web/src/server/aws/sns.ts b/apps/web/src/server/aws/sns.ts index f810952b..00ad0224 100644 --- a/apps/web/src/server/aws/sns.ts +++ b/apps/web/src/server/aws/sns.ts @@ -5,14 +5,13 @@ import { DeleteTopicCommand, } from "@aws-sdk/client-sns"; import { env } from "~/env"; +import { getAwsCredentialOptions } from "./credentials"; function getSnsClient(region: string) { return new SNSClient({ endpoint: env.AWS_SNS_ENDPOINT, region: region, - ...(env.AWS_ACCESS_KEY_ID && env.AWS_SECRET_ACCESS_KEY - ? { credentials: { accessKeyId: env.AWS_ACCESS_KEY_ID, secretAccessKey: env.AWS_SECRET_ACCESS_KEY } } - : {}), + ...getAwsCredentialOptions(), }); }