From abc0add031e16d538f45156041169f7b504bf8dc Mon Sep 17 00:00:00 2001 From: Volodymyr Saakian Date: Fri, 3 Jul 2026 11:05:32 +0700 Subject: [PATCH] fix(kiro): route IdC auth to regional CodeWhisperer surface (#2297) IAM Identity Center (authMethod=idc) tokens failed every request with 403 "bearer token invalid". Treat idc like api_key/external_idp: - executors/kiro.js: route idc to *.amazonaws.com CodeWhisperer surface, region-aware from credentials.region instead of hardcoded us-east-1. - openai-to-kiro.js / claude-to-kiro.js: send resolved profileArn or empty for idc/external_idp, never the shared builder-id placeholder ARN. Co-authored-by: Cursor --- open-sse/executors/kiro.js | 17 +++++++++++++++-- open-sse/translator/request/claude-to-kiro.js | 7 +++++-- open-sse/translator/request/openai-to-kiro.js | 9 ++++++++- 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/open-sse/executors/kiro.js b/open-sse/executors/kiro.js index 6e2f803b..e6dde985 100644 --- a/open-sse/executors/kiro.js +++ b/open-sse/executors/kiro.js @@ -64,9 +64,22 @@ export class KiroExecutor extends BaseExecutor { getOrderedBaseUrls(credentials) { const baseUrls = this.getBaseUrls(); const authMethod = credentials?.providerSpecificData?.authMethod; - const isCodeWhispererSurface = authMethod === "api_key" || authMethod === "external_idp"; + // IAM Identity Center (idc) tokens are AWS SSO access tokens — the same + // family as external_idp/api_key. The kiro.dev gateway rejects them with + // 403 "bearer token invalid", so they must hit the CodeWhisperer + // *.amazonaws.com surface, and in the region the token was minted in + // (the baseUrls are hardcoded us-east-1). + const isCodeWhispererSurface = + authMethod === "api_key" || authMethod === "external_idp" || authMethod === "idc"; if (!isCodeWhispererSurface) return baseUrls; - const amazon = baseUrls.filter((u) => u.includes("amazonaws.com")); + + const region = (credentials?.providerSpecificData?.region || "us-east-1").trim(); + const regionalize = (u) => + region && region !== "us-east-1" && u.includes("amazonaws.com") + ? u.replace(/([a-z]+)\.[a-z0-9-]+\.amazonaws\.com/, `$1.${region}.amazonaws.com`) + : u; + + const amazon = baseUrls.filter((u) => u.includes("amazonaws.com")).map(regionalize); const others = baseUrls.filter((u) => !u.includes("amazonaws.com")); return amazon.length > 0 ? [...amazon, ...others] : baseUrls; } diff --git a/open-sse/translator/request/claude-to-kiro.js b/open-sse/translator/request/claude-to-kiro.js index 8a38e4a9..5d891c11 100644 --- a/open-sse/translator/request/claude-to-kiro.js +++ b/open-sse/translator/request/claude-to-kiro.js @@ -393,9 +393,12 @@ export function claudeToKiroRequest(model, body, stream, credentials) { reconcileOrphanedToolResults(history, currentMessage); } - // API-key auth must never use the shared default ARN (403); OAuth/social fall back to it. + // api_key / idc / external_idp must never use the shared default ARN (belongs + // to another account → 403 "bearer token invalid"); OAuth/social fall back to it. const authMethod = credentials?.providerSpecificData?.authMethod; - const profileArn = authMethod === "api_key" + const accountBoundAuth = + authMethod === "api_key" || authMethod === "idc" || authMethod === "external_idp"; + const profileArn = accountBoundAuth ? (credentials?.providerSpecificData?.profileArn || "") : (credentials?.providerSpecificData?.profileArn || resolveDefaultProfileArn(authMethod)); diff --git a/open-sse/translator/request/openai-to-kiro.js b/open-sse/translator/request/openai-to-kiro.js index e3e7f475..ee886666 100644 --- a/open-sse/translator/request/openai-to-kiro.js +++ b/open-sse/translator/request/openai-to-kiro.js @@ -530,8 +530,15 @@ export function openaiToKiroRequest(model, body, stream, credentials) { // (the ARN doesn't belong to the key's account). So for api_key, only send a // profileArn that was actually resolved for this connection — never the default. // OAuth/social keep the default fallback (their tokens accept it). + // api_key / idc / external_idp carry an account-specific (or token-bound) + // profile. The shared builder-id/social default ARN belongs to a different + // account and triggers 403 "bearer token invalid", so never fall back to it — + // send the resolved ARN, or an empty string so CodeWhisperer uses the token's + // own default profile. Only OAuth/social keep the shared placeholder. const authMethod = credentials?.providerSpecificData?.authMethod; - const profileArn = authMethod === "api_key" + const accountBoundAuth = + authMethod === "api_key" || authMethod === "idc" || authMethod === "external_idp"; + const profileArn = accountBoundAuth ? (credentials?.providerSpecificData?.profileArn || "") : (credentials?.providerSpecificData?.profileArn || resolveDefaultProfileArn(authMethod));