fix(kiro): route enterprise IAM Identity Center accounts to their regional endpoint (#3631)

Integrated into release/v3.8.22
This commit is contained in:
NOXX - Commiter
2026-06-11 17:36:36 +03:00
committed by GitHub
parent c5b21412c0
commit 8ecfe757ea
4 changed files with 218 additions and 4 deletions

View File

@@ -168,6 +168,37 @@ function ensureKiroUsage(state: KiroStreamState) {
};
}
/**
* Resolve the AWS region for a Kiro/CodeWhisperer connection. Enterprise AWS IAM Identity
* Center accounts are region-bound: the access token, the Q Developer profile ARN and the
* runtime endpoint must all match the region the IdC instance lives in (e.g. eu-central-1).
* A request signed for one region is rejected by another ("bearer token is invalid"), and a
* regional profileArn sent to us-east-1 fails with "Improperly formed request". Falls back to
* the region embedded in the profileArn, then us-east-1 (the AWS Builder ID default).
*/
export function resolveKiroRegion(
credentials: { providerSpecificData?: unknown } | null | undefined
): string {
const psd = (credentials?.providerSpecificData || {}) as Record<string, unknown>;
const region = typeof psd.region === "string" ? psd.region.trim().toLowerCase() : "";
if (region) return region;
const arn = typeof psd.profileArn === "string" ? psd.profileArn.toLowerCase() : "";
const match = arn.match(/^arn:aws:codewhisperer:([a-z0-9-]+):/);
return match ? match[1] : "us-east-1";
}
/**
* CodeWhisperer/Amazon Q runtime host for a region. us-east-1 keeps the legacy
* codewhisperer.us-east-1 host (AWS Builder ID); 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`;
}
/**
* KiroExecutor - Executor for Kiro AI (AWS CodeWhisperer)
* Uses AWS CodeWhisperer streaming API with AWS EventStream binary format
@@ -228,7 +259,11 @@ export class KiroExecutor extends BaseExecutor {
log,
upstreamExtraHeaders,
}: ExecuteInput) {
const url = this.buildUrl(model, stream, 0);
// Route to the region-specific CodeWhisperer/Amazon Q endpoint. Enterprise IAM Identity
// Center accounts (e.g. eu-central-1) are rejected by the default us-east-1 host; only the
// regional endpoint accepts the region-bound token + profileArn.
const region = resolveKiroRegion(credentials);
const url = `${kiroRuntimeHost(region)}/generateAssistantResponse`;
const headers = this.buildHeaders(credentials, stream);
mergeUpstreamExtraHeaders(headers, upstreamExtraHeaders);
const transformedBody = await this.transformRequest(model, body, stream, credentials);

View File

@@ -3005,6 +3005,22 @@ async function getKiroUsage(accessToken?: string, providerSpecificData?: JsonRec
return { message: "Kiro connected. Profile ARN not available for quota tracking." };
}
// Enterprise IAM Identity Center accounts are region-bound: the profileArn, token and
// endpoint must all match the region. Derive the region from the stored region (preferred)
// or the profileArn, then route to the regional Amazon Q endpoint (us-east-1 keeps the
// legacy codewhisperer host; codewhisperer.{region} does not resolve for other regions).
const regionFromArn =
typeof profileArn === "string"
? profileArn.toLowerCase().match(/^arn:aws:codewhisperer:([a-z0-9-]+):/)?.[1]
: undefined;
const region =
(typeof providerSpecificData?.region === "string" &&
providerSpecificData.region.trim().toLowerCase()) ||
regionFromArn ||
"us-east-1";
const usageBaseUrl =
region === "us-east-1" ? CODEWHISPERER_BASE_URL : `https://q.${region}.amazonaws.com`;
// Kiro uses AWS CodeWhisperer GetUsageLimits API
const payload = {
origin: "AI_EDITOR",
@@ -3012,7 +3028,7 @@ async function getKiroUsage(accessToken?: string, providerSpecificData?: JsonRec
resourceType: "AGENTIC_REQUEST",
};
const response = await fetch(CODEWHISPERER_BASE_URL, {
const response = await fetch(usageBaseUrl, {
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,

View File

@@ -76,7 +76,7 @@ export const kiro = {
};
},
pollToken: async (config, deviceCode, codeVerifier, extraData) => {
const tokenRegion = extraData?._region || "us-east-1";
const tokenRegion = String(extraData?._region || "us-east-1").toLowerCase();
const tokenUrl = `https://oidc.${tokenRegion}.amazonaws.com/token`;
const response = await fetch(tokenUrl, {
@@ -123,7 +123,46 @@ export const kiro = {
},
};
},
mapTokens: (tokens) => ({
// Enterprise IAM Identity Center accounts require a region-bound Q Developer profileArn on every
// CodeWhisperer call; without it AWS returns 403 "User is not authorized to make this call". The
// device-code flow does not return one, so discover it here via ListAvailableProfiles against the
// same regional endpoint the token was issued for. Best-effort: AWS Builder ID accounts have no
// profile and this simply yields none; failures never block login.
postExchange: async (tokenData) => {
const accessToken = tokenData?.access_token;
if (!accessToken) return null;
const region = String(tokenData?._region || "us-east-1").toLowerCase();
const runtimeHost =
region === "us-east-1"
? "https://codewhisperer.us-east-1.amazonaws.com"
: `https://q.${region}.amazonaws.com`;
try {
const profRes = await fetch(`${runtimeHost}/`, {
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 }),
signal: AbortSignal.timeout(10000),
});
if (!profRes.ok) return null;
const profData = await profRes.json();
const profiles = Array.isArray(profData?.profiles) ? profData.profiles : [];
const matched =
profiles.find(
(p: { arn?: string }) => typeof p?.arn === "string" && p.arn.includes(`:${region}:`)
) || profiles[0];
const arn = matched && typeof matched.arn === "string" ? matched.arn : undefined;
return arn ? { profileArn: arn } : null;
} catch {
// Best-effort profile discovery — never block login on it.
return null;
}
},
mapTokens: (tokens, extra) => ({
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token,
expiresIn: tokens.expires_in,
@@ -131,6 +170,7 @@ export const kiro = {
clientId: tokens._clientId,
clientSecret: tokens._clientSecret,
region: tokens._region,
...(extra?.profileArn ? { profileArn: extra.profileArn } : {}),
},
}),
};

View File

@@ -0,0 +1,123 @@
/**
* Kiro enterprise IAM Identity Center region routing.
*
* Kiro/CodeWhisperer access tokens and Q Developer profile ARNs are region-bound. Enterprise
* IAM Identity Center accounts whose profile lives outside us-east-1 (e.g. eu-central-1) must
* route to the regional Amazon Q endpoint (q.{region}.amazonaws.com) with the region-matched
* profileArn — sending them to the default us-east-1 host fails with 403/400. These tests cover
* the region resolver, the regional host mapping, and the profileArn discovery added to the
* device-code login.
*/
import test from "node:test";
import assert from "node:assert/strict";
import { resolveKiroRegion, kiroRuntimeHost } from "../../open-sse/executors/kiro.ts";
import { kiro } from "@/lib/oauth/providers/kiro";
test("resolveKiroRegion prefers the stored region", () => {
assert.equal(resolveKiroRegion({ providerSpecificData: { region: "eu-central-1" } }), "eu-central-1");
});
test("resolveKiroRegion falls back to the region in the profileArn", () => {
assert.equal(
resolveKiroRegion({
providerSpecificData: {
profileArn: "arn:aws:codewhisperer:ap-southeast-2:123456789012:profile/ABC123",
},
}),
"ap-southeast-2"
);
});
test("resolveKiroRegion defaults to us-east-1 when nothing is set", () => {
assert.equal(resolveKiroRegion({ providerSpecificData: {} }), "us-east-1");
assert.equal(resolveKiroRegion(null), "us-east-1");
assert.equal(resolveKiroRegion(undefined), "us-east-1");
});
test("resolveKiroRegion normalizes case and whitespace", () => {
assert.equal(
resolveKiroRegion({ providerSpecificData: { region: " EU-CENTRAL-1 " } }),
"eu-central-1"
);
});
test("kiroRuntimeHost keeps the legacy host for us-east-1 and uses regional Q hosts otherwise", () => {
assert.equal(kiroRuntimeHost("us-east-1"), "https://codewhisperer.us-east-1.amazonaws.com");
assert.equal(kiroRuntimeHost("eu-central-1"), "https://q.eu-central-1.amazonaws.com");
assert.equal(kiroRuntimeHost("ap-southeast-2"), "https://q.ap-southeast-2.amazonaws.com");
});
test("kiro.postExchange discovers the region-matched profileArn via ListAvailableProfiles", async () => {
const originalFetch = global.fetch;
let requestedUrl = "";
let requestedTarget = "";
global.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
requestedUrl = String(input);
requestedTarget = String((init?.headers as Record<string, string>)?.["x-amz-target"] || "");
return new Response(
JSON.stringify({
profiles: [
{ arn: "arn:aws:codewhisperer:us-east-1:111111111111:profile/USEAST" },
{ arn: "arn:aws:codewhisperer:eu-central-1:820374639727:profile/RX4VNUHGHGAQ" },
],
}),
{ status: 200 }
);
}) as typeof fetch;
try {
const extra = await kiro.postExchange({ access_token: "token", _region: "eu-central-1" });
assert.equal(requestedUrl, "https://q.eu-central-1.amazonaws.com/");
assert.equal(requestedTarget, "AmazonCodeWhispererService.ListAvailableProfiles");
assert.deepEqual(extra, {
profileArn: "arn:aws:codewhisperer:eu-central-1:820374639727:profile/RX4VNUHGHGAQ",
});
} finally {
global.fetch = originalFetch;
}
});
test("kiro.postExchange returns null when no profile is available (AWS Builder ID)", async () => {
const originalFetch = global.fetch;
global.fetch = (async () =>
new Response(JSON.stringify({ profiles: [] }), { status: 200 })) as typeof fetch;
try {
assert.equal(await kiro.postExchange({ access_token: "token", _region: "us-east-1" }), null);
} finally {
global.fetch = originalFetch;
}
});
test("kiro.postExchange never throws on network failure", async () => {
const originalFetch = global.fetch;
global.fetch = (async () => {
throw new Error("network down");
}) as typeof fetch;
try {
assert.equal(await kiro.postExchange({ access_token: "token", _region: "eu-central-1" }), null);
} finally {
global.fetch = originalFetch;
}
});
test("kiro.mapTokens stores the discovered profileArn from postExchange extra", () => {
const mapped = kiro.mapTokens(
{ access_token: "at", refresh_token: "rt", expires_in: 3600, _region: "eu-central-1" },
{ profileArn: "arn:aws:codewhisperer:eu-central-1:820374639727:profile/RX4VNUHGHGAQ" }
);
assert.equal(
mapped.providerSpecificData.profileArn,
"arn:aws:codewhisperer:eu-central-1:820374639727:profile/RX4VNUHGHGAQ"
);
});
test("kiro.mapTokens omits profileArn when postExchange found none", () => {
const mapped = kiro.mapTokens(
{ access_token: "at", refresh_token: "rt", expires_in: 3600, _region: "us-east-1" },
null
);
assert.equal("profileArn" in mapped.providerSpecificData, false);
});