Skip to content

Commit 054636d

Browse files
committed
Fix AWS SDK credentials for Amplify: use APP_* env vars
Amplify reserves the AWS_* env var prefix, so SDK clients had no credentials in production. Added shared credentials helper that reads APP_ACCESS_KEY_ID/APP_SECRET_ACCESS_KEY and passes them explicitly to all 8 SDK client creation points (Polly, Bedrock, DynamoDB).
1 parent ef8c494 commit 054636d

9 files changed

Lines changed: 63 additions & 18 deletions

File tree

app/api/chat/conversation/route.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
BedrockRuntimeClient,
1919
InvokeModelCommand,
2020
} from "@aws-sdk/client-bedrock-runtime";
21+
import { getAppCredentials } from "../../../lib/aws/credentials";
2122

2223
/* ------------------------------------------------------------------ */
2324
/* Types */
@@ -58,7 +59,8 @@ interface ConversationResponse {
5859
const BEDROCK_REGION = process.env.BEDROCK_REGION || "us-east-1";
5960

6061
function getBedrockClient(): BedrockRuntimeClient {
61-
return new BedrockRuntimeClient({ region: BEDROCK_REGION });
62+
const credentials = getAppCredentials();
63+
return new BedrockRuntimeClient({ region: BEDROCK_REGION, ...(credentials && { credentials }) });
6264
}
6365

6466
/* ------------------------------------------------------------------ */

app/api/chat/generate-words/route.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
BedrockRuntimeClient,
1818
InvokeModelCommand,
1919
} from "@aws-sdk/client-bedrock-runtime";
20+
import { getAppCredentials } from "../../../lib/aws/credentials";
2021

2122
/* ------------------------------------------------------------------ */
2223
/* Types */
@@ -42,7 +43,8 @@ interface GeneratedItem {
4243
const BEDROCK_REGION = process.env.BEDROCK_REGION || "us-east-1";
4344

4445
function getBedrockClient(): BedrockRuntimeClient {
45-
return new BedrockRuntimeClient({ region: BEDROCK_REGION });
46+
const credentials = getAppCredentials();
47+
return new BedrockRuntimeClient({ region: BEDROCK_REGION, ...(credentials && { credentials }) });
4648
}
4749

4850
/* ------------------------------------------------------------------ */

app/api/report/clinical/route.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
InvokeModelCommand,
2727
} from "@aws-sdk/client-bedrock-runtime";
2828
import type { BiomarkerAggregate } from "../../../types/biomarker";
29+
import { getAppCredentials } from "../../../lib/aws/credentials";
2930

3031
interface ClinicalRequestBody {
3132
sessionId: string;
@@ -45,10 +46,9 @@ interface ClinicalResponse {
4546

4647
const BEDROCK_REGION = process.env.BEDROCK_REGION || "us-east-1";
4748

48-
// Don't pass explicit credentials — the SDK default credential provider chain
49-
// handles Lambda IAM roles (with session tokens) and local dev env vars.
5049
function getBedrockClient(): BedrockRuntimeClient {
51-
return new BedrockRuntimeClient({ region: BEDROCK_REGION });
50+
const credentials = getAppCredentials();
51+
return new BedrockRuntimeClient({ region: BEDROCK_REGION, ...(credentials && { credentials }) });
5252
}
5353

5454
function buildMockReport(

app/api/report/summary/route.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
InvokeModelCommand,
1919
} from "@aws-sdk/client-bedrock-runtime";
2020
import type { BiomarkerAggregate } from "../../../types/biomarker";
21+
import { getAppCredentials } from "../../../lib/aws/credentials";
2122

2223
interface SummaryRequestBody {
2324
sessionId: string;
@@ -26,10 +27,9 @@ interface SummaryRequestBody {
2627

2728
const BEDROCK_REGION = process.env.BEDROCK_REGION || "us-east-1";
2829

29-
// Don't pass explicit credentials — the SDK default credential provider chain
30-
// handles Lambda IAM roles (with session tokens) and local dev env vars.
3130
function getBedrockClient(): BedrockRuntimeClient {
32-
return new BedrockRuntimeClient({ region: BEDROCK_REGION });
31+
const credentials = getAppCredentials();
32+
return new BedrockRuntimeClient({ region: BEDROCK_REGION, ...(credentials && { credentials }) });
3333
}
3434

3535
function buildMockSummary(biomarkers: BiomarkerAggregate): string {

app/api/sync/route.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,12 @@ import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
1919
import { DynamoDBDocumentClient, PutCommand } from "@aws-sdk/lib-dynamodb";
2020
import type { SessionSyncPayload } from "../../types/session";
2121
import type { BiomarkerAggregate } from "../../types/biomarker";
22+
import { getAppCredentials, getAppRegion } from "../../lib/aws/credentials";
2223

23-
// Don't pass explicit credentials — let the SDK use its default credential
24-
// provider chain (Lambda IAM role on Amplify, env vars or ~/.aws on local dev).
24+
const appCredentials = getAppCredentials();
2525
const dynamoClient = new DynamoDBClient({
26-
region: process.env.AWS_REGION ?? "ap-south-1",
26+
region: getAppRegion("ap-south-1"),
27+
...(appCredentials && { credentials: appCredentials }),
2728
});
2829
const docClient = DynamoDBDocumentClient.from(dynamoClient);
2930

app/api/tts/route.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,18 @@ import {
1818
SynthesizeSpeechCommand,
1919
type VoiceId,
2020
} from "@aws-sdk/client-polly";
21+
import { getAppCredentials, getAppRegion } from "../../lib/aws/credentials";
2122

2223
interface TtsRequestBody {
2324
text: string;
2425
voiceId?: string;
2526
}
2627

27-
// Use POLLY_REGION if set, otherwise fall back to AWS_REGION (auto-set on Lambda)
28-
const POLLY_REGION = process.env.POLLY_REGION || process.env.AWS_REGION || "ap-south-1";
28+
const POLLY_REGION = process.env.POLLY_REGION || getAppRegion("ap-south-1");
2929

30-
// Don't pass explicit credentials — the SDK default credential provider chain
31-
// handles Lambda IAM roles (with session tokens) and local dev env vars.
3230
function getPollyClient(): PollyClient {
33-
return new PollyClient({ region: POLLY_REGION });
31+
const credentials = getAppCredentials();
32+
return new PollyClient({ region: POLLY_REGION, ...(credentials && { credentials }) });
3433
}
3534

3635
export async function POST(req: NextRequest) {

app/lib/auth/dynamodb.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
*/
1111

1212
import { AUTH_CONFIG } from "./config";
13+
import { getAppCredentials, getAppRegion } from "../aws/credentials";
1314

1415
// ─── Types ───────────────────────────────────────────────────────────
1516
export interface AuthUser {
@@ -41,11 +42,12 @@ function shouldUseDynamo(): boolean {
4142
if (
4243
process.env.NODE_ENV === "development" &&
4344
!process.env.AWS_ACCESS_KEY_ID &&
45+
!process.env.APP_ACCESS_KEY_ID &&
4446
!process.env.AWS_REGION
4547
) {
4648
return false;
4749
}
48-
// In production (Amplify), always try DynamoDB — IAM role provides creds
50+
// In production (Amplify), always try DynamoDB
4951
return true;
5052
}
5153

@@ -123,8 +125,10 @@ async function getDynamoClient() {
123125
const { DynamoDBClient } = await import("@aws-sdk/client-dynamodb");
124126
const { DynamoDBDocumentClient } = await import("@aws-sdk/lib-dynamodb");
125127

128+
const credentials = getAppCredentials();
126129
const client = new DynamoDBClient({
127-
region: process.env.AWS_REGION || "ap-south-1",
130+
region: getAppRegion("ap-south-1"),
131+
...(credentials && { credentials }),
128132
});
129133
return DynamoDBDocumentClient.from(client);
130134
}

app/lib/aws/credentials.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/**
2+
* Shared AWS credential provider for Amplify deployments.
3+
*
4+
* Amplify reserves the "AWS_*" env var prefix, so we use custom-named
5+
* variables: APP_ACCESS_KEY_ID, APP_SECRET_ACCESS_KEY, APP_REGION.
6+
* The SDK default credential chain is tried first (covers Lambda IAM
7+
* roles and local dev with AWS_* env vars). If that fails, the custom
8+
* env vars are used as an explicit fallback.
9+
*/
10+
11+
import type { AwsCredentialIdentity } from "@aws-sdk/types";
12+
13+
/**
14+
* Returns explicit credentials from APP_* env vars if set,
15+
* or undefined to let the SDK use its default provider chain.
16+
*/
17+
export function getAppCredentials(): AwsCredentialIdentity | undefined {
18+
const accessKeyId = process.env.APP_ACCESS_KEY_ID;
19+
const secretAccessKey = process.env.APP_SECRET_ACCESS_KEY;
20+
21+
if (accessKeyId && secretAccessKey) {
22+
return { accessKeyId, secretAccessKey };
23+
}
24+
25+
return undefined;
26+
}
27+
28+
/**
29+
* Returns the AWS region from APP_REGION, AWS_REGION, or the given default.
30+
*/
31+
export function getAppRegion(fallback = "ap-south-1"): string {
32+
return process.env.APP_REGION || process.env.AWS_REGION || fallback;
33+
}

next.config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ const nextConfig: NextConfig = {
3434
DYNAMODB_CHILD_PROFILES_TABLE: process.env.DYNAMODB_CHILD_PROFILES_TABLE ?? "",
3535
DYNAMODB_SESSION_SUMMARIES_TABLE: process.env.DYNAMODB_SESSION_SUMMARIES_TABLE ?? "",
3636
DYNAMODB_FEED_POSTS_TABLE: process.env.DYNAMODB_FEED_POSTS_TABLE ?? "",
37+
// Amplify blocks AWS_* prefix — use custom names for SDK credentials
38+
APP_ACCESS_KEY_ID: process.env.APP_ACCESS_KEY_ID ?? "",
39+
APP_SECRET_ACCESS_KEY: process.env.APP_SECRET_ACCESS_KEY ?? "",
40+
APP_REGION: process.env.APP_REGION ?? "",
3741
},
3842

3943
// Required for SharedArrayBuffer (ONNX WASM multi-threading)

0 commit comments

Comments
 (0)