Files
OmniRoute/open-sse/services/kiroRegion.ts
Diego Rodrigues de Sa e Souza 778c3aeab9 fix(kiro): probe IdC region during profileArn discovery, cross-region (recovers #6099) (#6840)
* fix(kiro): route Amazon Q runtime by profileArn region for cross-region IdC

Enterprise AWS IAM Identity Center accounts whose IdC instance lives outside the two
Amazon Q Developer profile regions (us-east-1 / eu-central-1) - e.g. eu-north-1
(Stockholm), start URL https://d-XXXX.awsapps.com/start - showed no limits and returned
502 on every request.

Root cause: the backend used the IdC/OIDC token region (providerSpecificData.region, e.g.
eu-north-1) for every CodeWhisperer runtime call, hitting q.eu-north-1.amazonaws.com - a
host that does not exist as a Q Developer runtime endpoint. Per AWS docs ("Supported
Regions for the Q Developer console and Q Developer profile"), the Q Developer *profile*
(which produces the profileArn and hosts generateAssistantResponse / GetUsageLimits /
ListAvailableModels / ListAvailableProfiles) is only hosted in us-east-1 and eu-central-1,
regardless of the IdC region; "data is stored in the Region where you create the Amazon Q
Developer profile."

Fix (new open-sse/services/kiroRegion.ts) decouples the two regions:
- providerSpecificData.region stays the IdC/OIDC region, used ONLY for
  oidc.{region}.amazonaws.com token mint/refresh.
- The runtime region is derived from the profileArn (resolveKiroRuntimeRegion):
  profileArn region -> a valid stored profile region -> us-east-1. A stored IdC region
  that is not a Q profile region (eu-north-1) is ignored for runtime.
- Profile discovery (discoverKiroProfileArnAcrossRegions) probes the Q profile regions
  (EU IdC -> eu-central-1 first) with the cross-region SSO token instead of q.{idcRegion}.

Wired into: executors/kiro.ts (generateAssistantResponse targets the profile region),
services/usage/kiro.ts (getKiroUsage multi-region discovery + profileArn runtime region so
Limits resolves), services/kiroModels.ts (ListAvailableModels), and
src/lib/oauth/providers/kiro.ts (login-time postExchange profile discovery).

Adds tests/unit/kiro-idc-cross-region.test.ts (15 cases). All Kiro suites pass (60 tests).

* 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.

---------

Co-authored-by: artickc <artur1992123@mail.ru>
2026-07-11 04:25:47 -03:00

186 lines
9.1 KiB
TypeScript

/**
* Shared Amazon Q Developer (Kiro / AWS CodeWhisperer) region resolution.
*
* 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"):
*
* • 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:
* • 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
// src/lib/oauth/constants/oauth.ts) so this open-sse module has no cross-tree import just to
// validate a string. Guards against SSRF via region injection (GHSA-6mwv-4mrm-5p3m): the value
// is interpolated into upstream URLs.
export const AWS_REGION_PATTERN = /^[a-z]{2}-[a-z]+-\d{1,2}$/;
/**
* 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;
/**
* CodeWhisperer / Amazon Q runtime host for a region. us-east-1 keeps the legacy
* codewhisperer.us-east-1 host (AWS Builder ID home region); other regions use the regional
* Amazon Q endpoint `q.{region}.amazonaws.com` — codewhisperer.{region}.amazonaws.com does not
* resolve for non-us-east-1 regions.
*/
export function kiroRuntimeHost(region: string): string {
return region === "us-east-1"
? "https://codewhisperer.us-east-1.amazonaws.com"
: `https://q.${region}.amazonaws.com`;
}
/** Extract the region from a CodeWhisperer profile ARN (`arn:aws:codewhisperer:{region}:...`). */
export function regionFromKiroProfileArn(profileArn?: string | null): string | undefined {
if (typeof profileArn !== "string") return undefined;
return profileArn.toLowerCase().match(/^arn:aws:codewhisperer:([a-z0-9-]+):/)?.[1];
}
function normalizeRegion(region: unknown): string {
return typeof region === "string" ? region.trim().toLowerCase() : "";
}
/**
* Resolve the RUNTIME region for CodeWhisperer / Amazon Q calls.
*
* Priority:
* 1. The region embedded in the `profileArn` — authoritative, this is where the Q Developer
* profile (and thus the runtime) actually lives.
* 2. A stored region ONLY when it is a valid Q Developer profile region (us-east-1 /
* eu-central-1). A stored IdC region that is not a Q profile region (e.g. eu-north-1) is
* deliberately IGNORED for runtime — it is a token/OIDC region, not a runtime region.
* 3. us-east-1 (CodeWhisperer home region) as the final fallback.
*/
export function resolveKiroRuntimeRegion(
providerSpecificData: { region?: unknown; profileArn?: unknown } | null | undefined
): string {
const fromArn = regionFromKiroProfileArn(
typeof providerSpecificData?.profileArn === "string"
? providerSpecificData.profileArn
: undefined
);
if (fromArn) return fromArn;
const stored = normalizeRegion(providerSpecificData?.region);
if (stored && (KIRO_PROFILE_REGIONS as readonly string[]).includes(stored)) return stored;
return "us-east-1";
}
/**
* 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 stored = normalizeRegion(storedRegion);
const preferEu = /^(eu|af|me|il)-/.test(stored);
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;
}
async function listKiroProfileArnForRegion(
accessToken: string,
region: string,
fetchImpl: typeof fetch
): Promise<string | undefined> {
// Defensive: region comes from a hardcoded allowlist here, but validate before it is
// interpolated into the runtime host (SSRF guard, GHSA-6mwv-4mrm-5p3m).
if (!AWS_REGION_PATTERN.test(region)) return undefined;
try {
const response = await fetchImpl(`${kiroRuntimeHost(region)}/`, {
method: "POST",
headers: {
"Content-Type": "application/x-amz-json-1.0",
Accept: "application/json",
"x-amz-target": "AmazonCodeWhispererService.ListAvailableProfiles",
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify({ maxResults: 10 }),
// Never let a hung/region-mismatched profile lookup block login or the quota refresh.
signal: AbortSignal.timeout(10000),
});
if (!response.ok) return undefined;
const data = (await response.json()) as { profiles?: unknown };
const profiles = Array.isArray(data?.profiles) ? data.profiles : [];
// Prefer a profile whose ARN region matches the region we queried; else take the first.
const matched =
profiles.find((profile: unknown) => {
const arn = (profile as { arn?: unknown })?.arn;
return typeof arn === "string" && regionFromKiroProfileArn(arn) === region;
}) || profiles[0];
const arn = (matched as { arn?: unknown })?.arn;
return typeof arn === "string" && arn.length > 0 ? arn : undefined;
} catch {
return undefined;
}
}
/**
* Discover a Kiro/CodeWhisperer profile ARN by probing the Q Developer profile regions
* (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,
storedRegion?: string | null,
fetchImpl?: typeof fetch
): Promise<string | undefined> {
const token = typeof accessToken === "string" ? accessToken.trim() : "";
if (!token) return undefined;
// Resolve fetch at call time (not module-load) so callers/tests that swap globalThis.fetch
// are honored when no explicit implementation is injected.
const doFetch = fetchImpl ?? globalThis.fetch;
for (const region of buildKiroProfileDiscoveryRegions(storedRegion)) {
const arn = await listKiroProfileArnForRegion(token, region, doFetch);
if (arn) return arn;
}
return undefined;
}