From f8c2f23ae654080d575518670b57079ca72d7c41 Mon Sep 17 00:00:00 2001 From: artickc Date: Fri, 3 Jul 2026 16:38:35 +0300 Subject: [PATCH] fix(kiro): probe the IdC region too during profileArn discovery (any IdC region) Make profile discovery general for an IdC in ANY of the ~30 IdC-supported AWS regions (us-west-2, ap-southeast-2, me-central-1, af-south-1, ...), not just eu-north-1. buildKiroProfileDiscoveryRegions now probes the two documented Q Developer profile regions FIRST (us-east-1 / eu-central-1, EU-first for EMEA IdC regions to cut latency), then appends the IdC/stored region itself as a forward-compatible fallback: if AWS ever co-locates the profile with the IdC or expands the profile-region list, a same-region probe still finds it. Probing a region with no profile simply returns nothing and we fall through. The profileArn's own region remains authoritative for every runtime call (resolveKiroRuntimeRegion), so a newly-issued ARN in any region is honored automatically. Adds ap-southeast-2 (APAC) cross-region coverage and updates the discovery-order tests. --- open-sse/services/kiroRegion.ts | 92 +++++++++++++----------- tests/unit/kiro-idc-cross-region.test.ts | 52 ++++++++++++-- 2 files changed, 97 insertions(+), 47 deletions(-) diff --git a/open-sse/services/kiroRegion.ts b/open-sse/services/kiroRegion.ts index 8f1eee3f61..e87495cf33 100644 --- a/open-sse/services/kiroRegion.ts +++ b/open-sse/services/kiroRegion.ts @@ -1,28 +1,31 @@ /** * Shared Amazon Q Developer (Kiro / AWS CodeWhisperer) region resolution. * - * CRITICAL AWS constraint — verified against the AWS docs - * "Amazon Q Developer Pro Region support" → "Supported Regions for the Q Developer console and - * Q Developer profile": + * TWO DISTINCT REGIONS — verified against the AWS docs "Amazon Q Developer Pro Region support" + * ("Supported Regions for the Q Developer console and Q Developer profile"): * - * • An IAM Identity Center instance (and therefore the SSO OIDC token endpoint - * `oidc.{region}.amazonaws.com`) may live in MANY regions, e.g. eu-north-1 (Stockholm), - * us-west-1, ap-southeast-2, ... - * • The Amazon Q Developer PROFILE — which produces the `profileArn` and hosts EVERY - * CodeWhisperer runtime call (generateAssistantResponse, GetUsageLimits, ListAvailableModels, - * ListAvailableProfiles) — is only hosted in **us-east-1** and **eu-central-1**. - * • "Regardless of the IAM Identity Center Region, data is stored in the Region where you create - * the Amazon Q Developer profile." → the IdC region and the runtime region are frequently - * DIFFERENT (e.g. IdC in eu-north-1 → profile in eu-central-1). + * • IdC / OIDC / token region — `providerSpecificData.region`. May be ANY of the ~30 IdC- + * supported AWS regions (us-east-1, us-west-2, ca-central-1, sa-east-1, eu-west-1/2/3, + * eu-central-1/2, eu-north-1, eu-south-1/2, ap-south-1/2, ap-east-1/2, ap-southeast-1..7, + * ap-northeast-1/2/3, me-central-1, me-south-1, af-south-1, il-central-1, …). Used ONLY for + * `oidc.{region}.amazonaws.com` token mint/refresh (see tokenRefresh.ts / oauth providers). + * • Q Developer PROFILE / RUNTIME region — where the `profileArn` lives and every CodeWhisperer + * runtime call is served (generateAssistantResponse, GetUsageLimits, ListAvailableModels, + * ListAvailableProfiles). AWS currently hosts the profile ONLY in us-east-1 and eu-central-1, + * REGARDLESS of the IdC region ("Regardless of the IAM Identity Center Region, data is stored + * in the Region where you create the Amazon Q Developer profile"). The AWS docs' own example: + * an IdC in us-west-1 → profile in us-east-1. * * Consequences enforced here: - * • `providerSpecificData.region` is the IdC/OIDC/token region. It must ONLY be used for - * `oidc.{region}.amazonaws.com` token mint/refresh (see tokenRefresh.ts / oauth providers). - * • The RUNTIME region is the region embedded in the `profileArn` (us-east-1 / eu-central-1), - * NOT the IdC region. Routing a runtime call to `q.eu-north-1.amazonaws.com` — a host that - * does not exist as a Q Developer runtime endpoint — is the root cause of the - * "Kiro IAM shows no limits + every request returns 502" failure for enterprise IdC accounts - * whose IdC lives outside us-east-1 / eu-central-1. + * • The RUNTIME region is the region embedded in the `profileArn` (authoritative — whatever + * region AWS actually hosts the profile in), NOT the IdC region. Routing a runtime call to + * `q.{idcRegion}.amazonaws.com` for a non-profile IdC region (e.g. q.eu-north-1, which does + * not exist as a Q Developer runtime endpoint) is the root cause of the "Kiro IAM shows no + * limits + every request returns 502" failure. + * • profileArn discovery works for an IdC in ANY region: it probes the known profile regions + * (us-east-1 / eu-central-1) with the cross-region SSO token, AND the IdC's own region as a + * forward-compatible fallback (in case AWS ever co-locates or expands profile regions). The + * discovered ARN's region then drives every runtime call. */ // Canonical AWS region shape — kept local (identical to AWS_REGION_PATTERN in @@ -32,9 +35,11 @@ export const AWS_REGION_PATTERN = /^[a-z]{2}-[a-z]+-\d{1,2}$/; /** - * Regions where the Amazon Q Developer *profile* (identity-aware / IdC Pro tier) can exist, and - * therefore the only regions that host a Q Developer runtime endpoint for profile-bound calls. - * Order is the default probe order (us-east-1 is CodeWhisperer's home region). + * Regions where the Amazon Q Developer *profile* is currently hosted (AWS docs: "Supported + * Regions for the Q Developer console and Q Developer profile"). These are the guaranteed + * discovery targets and the only regions trusted as a runtime fallback when no profileArn is + * known. The profileArn's own region is always honored above this list, so a future AWS + * profile-region expansion works automatically once an ARN is discovered. */ export const KIRO_PROFILE_REGIONS = ["us-east-1", "eu-central-1"] as const; @@ -88,25 +93,27 @@ export function resolveKiroRuntimeRegion( } /** - * Build the ordered list of Q Developer profile regions to probe for `ListAvailableProfiles`. - * The IdC/token region is only useful here as a hint for geographic proximity — the actual - * profile always lives in one of KIRO_PROFILE_REGIONS, so those are the only regions probed. + * Build the ordered list of regions to probe for `ListAvailableProfiles`. + * + * The Amazon Q Developer profile (and thus every runtime endpoint) is currently hosted only in + * KIRO_PROFILE_REGIONS (us-east-1 / eu-central-1) regardless of the IdC region, so those are + * probed FIRST — EU-first when the IdC region is in EMEA (eu-, af-, me-, il- prefixes) to + * minimize latency. The IdC/stored region is then appended as a forward-compatible fallback: if + * AWS ever co-locates the profile with the IdC, or expands the profile-region list, a same-region + * probe still finds it. It is only appended when it is a valid AWS region distinct from the known + * profile regions; probing a region with no profile simply returns nothing and we fall through. + * This makes discovery work for an IdC in ANY region (us-west-2, ap-southeast-2, me-central-1, + * af-south-1, …), not just eu-north-1. */ export function buildKiroProfileDiscoveryRegions(storedRegion?: string | null): string[] { - const regions: string[] = []; const stored = normalizeRegion(storedRegion); - - // If the IdC/token region happens to be a Q profile region itself, probe it first. - if (stored && (KIRO_PROFILE_REGIONS as readonly string[]).includes(stored)) { - regions.push(stored); - } - - // Otherwise order the two known profile regions by rough geographic proximity to the IdC - // region so an EU IdC (e.g. eu-north-1) hits eu-central-1 first. const preferEu = /^(eu|af|me|il)-/.test(stored); - const ordered = preferEu ? ["eu-central-1", "us-east-1"] : ["us-east-1", "eu-central-1"]; - for (const r of ordered) { - if (!regions.includes(r)) regions.push(r); + const regions: string[] = preferEu + ? ["eu-central-1", "us-east-1"] + : ["us-east-1", "eu-central-1"]; + + if (stored && AWS_REGION_PATTERN.test(stored) && !regions.includes(stored)) { + regions.push(stored); } return regions; } @@ -151,11 +158,12 @@ async function listKiroProfileArnForRegion( /** * Discover a Kiro/CodeWhisperer profile ARN by probing the Q Developer profile regions - * (us-east-1 / eu-central-1) with the account's access token. The SSO bearer token minted from - * the IdC region works cross-region against the Q Developer profile's region (AWS's documented - * multi-region IdC ⇄ profile setup). Returns the first ARN found (its embedded region is the - * authoritative runtime region), or undefined when no profile is available (e.g. AWS Builder ID - * accounts, or an org/token with no Kiro entitlement). Best-effort: never throws. + * (us-east-1 / eu-central-1) AND the IdC/stored region with the account's access token. The SSO + * bearer token minted from the IdC region works cross-region against the Q Developer profile's + * region (AWS's documented multi-region IdC ⇄ profile setup), so an IdC in ANY region resolves. + * Returns the first ARN found (its embedded region is the authoritative runtime region), or + * undefined when no profile is available (e.g. AWS Builder ID accounts, or an org/token with no + * Kiro entitlement). Best-effort: never throws. */ export async function discoverKiroProfileArnAcrossRegions( accessToken: string | null | undefined, diff --git a/tests/unit/kiro-idc-cross-region.test.ts b/tests/unit/kiro-idc-cross-region.test.ts index fd8a375fec..6e34c44bb9 100644 --- a/tests/unit/kiro-idc-cross-region.test.ts +++ b/tests/unit/kiro-idc-cross-region.test.ts @@ -79,14 +79,32 @@ test("the executor's resolveKiroRegion routes an eu-north-1 IdC account to the p assert.equal(kiroRuntimeHost("eu-central-1"), "https://q.eu-central-1.amazonaws.com"); }); -test("buildKiroProfileDiscoveryRegions: EU IdC probes eu-central-1 first, never eu-north-1", () => { +test("buildKiroProfileDiscoveryRegions: EU IdC probes the profile regions first, then the IdC region", () => { const regions = buildKiroProfileDiscoveryRegions("eu-north-1"); - assert.deepEqual(regions, ["eu-central-1", "us-east-1"]); - assert.ok(!regions.includes("eu-north-1"), "must not probe the nonexistent q.eu-north-1 host"); + assert.deepEqual(regions, ["eu-central-1", "us-east-1", "eu-north-1"]); + // The profile regions (fast path) are tried BEFORE the IdC-region fallback. + assert.ok(regions.indexOf("eu-central-1") < regions.indexOf("eu-north-1")); + assert.ok(regions.indexOf("us-east-1") < regions.indexOf("eu-north-1")); + // Another EMEA IdC region → still EU-first, IdC region appended as fallback. + assert.deepEqual(buildKiroProfileDiscoveryRegions("me-central-1"), [ + "eu-central-1", + "us-east-1", + "me-central-1", + ]); }); -test("buildKiroProfileDiscoveryRegions: non-EU IdC probes us-east-1 first", () => { - assert.deepEqual(buildKiroProfileDiscoveryRegions("us-west-2"), ["us-east-1", "eu-central-1"]); +test("buildKiroProfileDiscoveryRegions: non-EU IdC probes us-east-1 first, then the IdC region", () => { + assert.deepEqual(buildKiroProfileDiscoveryRegions("us-west-2"), [ + "us-east-1", + "eu-central-1", + "us-west-2", + ]); + assert.deepEqual(buildKiroProfileDiscoveryRegions("ap-southeast-2"), [ + "us-east-1", + "eu-central-1", + "ap-southeast-2", + ]); + // No stored region → just the two profile regions. assert.deepEqual(buildKiroProfileDiscoveryRegions(undefined), ["us-east-1", "eu-central-1"]); }); @@ -121,6 +139,30 @@ test("discoverKiroProfileArnAcrossRegions: eu-north-1 IdC finds the eu-central-1 ); }); +test("discoverKiroProfileArnAcrossRegions: a non-EU (ap-southeast-2) IdC resolves a us-east-1 profile", async () => { + // Proves the fix is general, not eu-north-1-specific: an APAC IdC's profile lives in a Q + // profile region (us-east-1 here) and is found via the cross-region SSO token. + const US_EAST_ARN = "arn:aws:codewhisperer:us-east-1:111111111111:profile/APAC"; + const requested: string[] = []; + const fetchImpl = (async (input: RequestInfo | URL) => { + const url = String(input); + requested.push(url); + if (url.includes("us-east-1")) { + return new Response(JSON.stringify({ profiles: [{ arn: US_EAST_ARN }] }), { status: 200 }); + } + return new Response(JSON.stringify({ profiles: [] }), { status: 200 }); + }) as unknown as typeof fetch; + + const arn = await discoverKiroProfileArnAcrossRegions("sso-token", "ap-southeast-2", fetchImpl); + assert.equal(arn, US_EAST_ARN); + assert.equal( + resolveKiroRuntimeRegion({ region: "ap-southeast-2", profileArn: arn }), + "us-east-1" + ); + // The us-east-1 profile region is probed before the ap-southeast-2 IdC-region fallback. + assert.ok(requested.some((u) => u.startsWith("https://codewhisperer.us-east-1.amazonaws.com/"))); +}); + test("discoverKiroProfileArnAcrossRegions: no token / no profile yields undefined without throwing", async () => { assert.equal(await discoverKiroProfileArnAcrossRegions("", "eu-north-1"), undefined); const emptyFetch = (async () =>