Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions open-sse/config/kiroConstants.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,46 @@ export function resolveDefaultProfileArn(authMethod) {
return social ? KIRO_DEFAULT_PROFILE_ARNS.social : KIRO_DEFAULT_PROFILE_ARNS["builder-id"];
}

// ─── Regional CodeWhisperer endpoints ───────────────────────────────────────
// Kiro historically assumed us-east-1 everywhere. IAM Identity Center (IdC)
// accounts can be homed in another region (e.g. eu-central-1); their token is
// rejected with 403 "bearer token invalid" by the us-east-1 endpoints, and
// their CodeWhisperer profile is only visible from the regional Amazon Q host.
// These helpers thread `providerSpecificData.region` through, defaulting to
// us-east-1 so existing Builder ID / social accounts are unaffected.

export const KIRO_DEFAULT_REGION = "us-east-1";

/** Normalize a Kiro region, falling back to us-east-1. */
export function resolveKiroRegion(region) {
const r = typeof region === "string" ? region.trim() : "";
return r || KIRO_DEFAULT_REGION;
}

/**
* Data-plane (generateAssistantResponse) URL for a region.
* Returns null for us-east-1 so callers keep the historical registry baseUrls
* (runtime.kiro.dev → codewhisperer → q). Other regions use the regional
* Amazon Q endpoint, the only host that resolves + accepts their token.
*/
export function resolveKiroDataPlaneUrl(region) {
const r = resolveKiroRegion(region);
if (r === KIRO_DEFAULT_REGION) return null;
return `https://q.${r}.amazonaws.com/generateAssistantResponse`;
}

/**
* Control-plane host (ListAvailableProfiles / GetUsageLimits) for a region.
* us-east-1 keeps the historical codewhisperer host; other regions use the
* regional Amazon Q host.
*/
export function resolveKiroControlPlaneHost(region) {
const r = resolveKiroRegion(region);
return r === KIRO_DEFAULT_REGION
? "https://codewhisperer.us-east-1.amazonaws.com"
: `https://q.${r}.amazonaws.com`;
}

export const KIRO_THINKING_BUDGET_DEFAULT = 16000;

export const KIRO_AGENTIC_SYSTEM_PROMPT = `
Expand Down
8 changes: 8 additions & 0 deletions open-sse/executors/kiro.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { BaseExecutor } from "./base.js";
import { PROVIDERS } from "../config/providers.js";
import { v4 as uuidv4 } from "uuid";
import { refreshKiroToken } from "../services/tokenRefresh.js";
import { resolveKiroDataPlaneUrl } from "../config/kiroConstants.js";
import { SSE_DONE, SSE_HEADERS } from "../utils/sseConstants.js";

/**
Expand Down Expand Up @@ -51,6 +52,13 @@ export class KiroExecutor extends BaseExecutor {
* (kiro.dev first) since its token is what that gateway accepts.
*/
getOrderedBaseUrls(credentials) {
// IAM Identity Center accounts can be homed outside us-east-1. Their token is
// rejected by the hardcoded us-east-1 registry baseUrls (403 "bearer token
// invalid"), so route to the account's regional Amazon Q endpoint. us-east-1
// (Builder ID / social / unset) keeps the registry baseUrls + 429 rotation.
const regional = resolveKiroDataPlaneUrl(credentials?.providerSpecificData?.region);
if (regional) return [regional];

const baseUrls = this.getBaseUrls();
const isApiKey = credentials?.providerSpecificData?.authMethod === "api_key";
if (!isApiKey) return baseUrls;
Expand Down
2 changes: 1 addition & 1 deletion open-sse/services/tokenRefresh/providers.js
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@ async function resolveKiroProfileArnPatch(providerSpecificData, accessToken, ref
let profileArn = refreshedArn?.trim?.() || null;
if (!profileArn) {
const { fetchKiroProfileArn } = await import("../../../src/lib/oauth/providers.js");
profileArn = await fetchKiroProfileArn(accessToken);
profileArn = await fetchKiroProfileArn(accessToken, providerSpecificData?.region);
}
return profileArn ? { providerSpecificData: { profileArn } } : {};
}
Expand Down
16 changes: 12 additions & 4 deletions open-sse/services/usage/kiro.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*/

import { proxyAwareFetch } from "../../utils/proxyFetch.js";
import { resolveDefaultProfileArn } from "../../config/kiroConstants.js";
import { resolveDefaultProfileArn, resolveKiroRegion, resolveKiroControlPlaneHost, KIRO_DEFAULT_REGION } from "../../config/kiroConstants.js";
import { U, parseResetTime } from "./shared.js";

/**
Expand Down Expand Up @@ -70,12 +70,20 @@ export async function getKiroUsage(accessToken, providerSpecificData, proxyOptio
resourceType: "AGENTIC_REQUEST",
});

// Region-aware usage hosts. us-east-1 keeps the historical codewhisperer/q
// hosts from the registry; other regions (IdC accounts) use the regional
// Amazon Q host so the GetUsageLimits call isn't rejected with 403.
const isDefaultRegion = resolveKiroRegion(providerSpecificData?.region) === KIRO_DEFAULT_REGION;
const cwHost = isDefaultRegion ? U("kiro").cwHost : resolveKiroControlPlaneHost(providerSpecificData?.region);
const qHost = isDefaultRegion ? U("kiro").qHost : resolveKiroControlPlaneHost(providerSpecificData?.region);
const limitsPath = U("kiro").limitsPath;

// For compatibility, try multiple known Kiro usage endpoints
const attempts = [
{
name: "codewhisperer-get",
run: async () => proxyAwareFetch(
`${U("kiro").cwHost}${U("kiro").limitsPath}?${getUsageParams.toString()}`,
`${cwHost}${limitsPath}?${getUsageParams.toString()}`,
{
method: "GET",
headers: {
Expand All @@ -91,7 +99,7 @@ export async function getKiroUsage(accessToken, providerSpecificData, proxyOptio
},
{
name: "codewhisperer-post",
run: async () => proxyAwareFetch(U("kiro").cwHost, {
run: async () => proxyAwareFetch(cwHost, {
method: "POST",
headers: {
"Authorization": `Bearer ${accessToken}`,
Expand All @@ -115,7 +123,7 @@ export async function getKiroUsage(accessToken, providerSpecificData, proxyOptio
...(profileArn ? { profileArn } : {}),
resourceType: "AGENTIC_REQUEST",
});
return proxyAwareFetch(`${U("kiro").qHost}${U("kiro").limitsPath}?${params}`, {
return proxyAwareFetch(`${qHost}${limitsPath}?${params}`, {
method: "GET",
headers: {
"Authorization": `Bearer ${accessToken}`,
Expand Down
11 changes: 9 additions & 2 deletions src/lib/oauth/providerHelpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,17 @@ function extractEmailFromAccessToken(accessToken) {
return payload.email || payload.preferred_username || payload.sub || undefined;
}

export async function fetchKiroProfileArn(accessToken) {
export async function fetchKiroProfileArn(accessToken, region) {
if (!accessToken) return null;
// IdC accounts homed outside us-east-1 only expose their CodeWhisperer profile
// via the regional Amazon Q host; us-east-1 returns an empty profile list for
// them. Default to the historical us-east-1 codewhisperer host.
const normalizedRegion = typeof region === "string" && region.trim() ? region.trim() : "us-east-1";
const host = normalizedRegion === "us-east-1"
? "https://codewhisperer.us-east-1.amazonaws.com"
: `https://q.${normalizedRegion}.amazonaws.com`;
try {
const response = await fetch("https://codewhisperer.us-east-1.amazonaws.com/ListAvailableProfiles", {
const response = await fetch(`${host}/ListAvailableProfiles`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Expand Down
2 changes: 1 addition & 1 deletion src/lib/oauth/providers.js
Original file line number Diff line number Diff line change
Expand Up @@ -1362,7 +1362,7 @@ export async function pollForToken(providerName, deviceCode, codeVerifier, extra
const tokens = provider.mapTokens(result.data, extra);
// Kiro IDC/Builder-ID tokens lack profileArn; resolve it to avoid 403
if (providerName === "kiro" && !tokens.providerSpecificData?.profileArn) {
const profileArn = await fetchKiroProfileArn(tokens.accessToken);
const profileArn = await fetchKiroProfileArn(tokens.accessToken, tokens.providerSpecificData?.region);
if (profileArn) tokens.providerSpecificData.profileArn = profileArn;
}
return { success: true, tokens };
Expand Down
91 changes: 91 additions & 0 deletions tests/unit/kiro-region.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { describe, it, expect, vi, afterEach } from "vitest";
import {
resolveKiroRegion,
resolveKiroDataPlaneUrl,
resolveKiroControlPlaneHost,
KIRO_DEFAULT_REGION,
} from "../../open-sse/config/kiroConstants.js";
import { KiroExecutor } from "../../open-sse/executors/kiro.js";
import { fetchKiroProfileArn } from "../../src/lib/oauth/providerHelpers.js";

/**
* Regression tests for multi-region Kiro / CodeWhisperer support.
*
* Background: Kiro assumed us-east-1 for the data plane, profile resolution and
* usage. IAM Identity Center accounts homed elsewhere (e.g. eu-central-1) are
* rejected with 403 "bearer token invalid" by the us-east-1 hosts, and their
* CodeWhisperer profile is only visible from the regional Amazon Q endpoint.
* The region (stored in providerSpecificData.region by the IdC device flow) now
* drives those endpoints, defaulting to us-east-1 for existing accounts.
*/
describe("kiro region helpers", () => {
it("defaults to us-east-1 and trims input", () => {
expect(resolveKiroRegion()).toBe(KIRO_DEFAULT_REGION);
expect(resolveKiroRegion("")).toBe("us-east-1");
expect(resolveKiroRegion(" eu-central-1 ")).toBe("eu-central-1");
});

it("returns null data-plane URL for us-east-1 (keep registry baseUrls)", () => {
expect(resolveKiroDataPlaneUrl()).toBeNull();
expect(resolveKiroDataPlaneUrl("us-east-1")).toBeNull();
});

it("returns the regional Amazon Q data-plane URL for other regions", () => {
expect(resolveKiroDataPlaneUrl("eu-central-1")).toBe(
"https://q.eu-central-1.amazonaws.com/generateAssistantResponse"
);
});

it("resolves the control-plane host per region", () => {
expect(resolveKiroControlPlaneHost("us-east-1")).toBe(
"https://codewhisperer.us-east-1.amazonaws.com"
);
expect(resolveKiroControlPlaneHost("eu-central-1")).toBe(
"https://q.eu-central-1.amazonaws.com"
);
});
});

describe("KiroExecutor.buildUrl region routing", () => {
it("keeps the us-east-1 registry baseUrl when no/default region", () => {
const ex = new KiroExecutor();
const url = ex.buildUrl("claude-sonnet-4.5", true, 0, { providerSpecificData: {} });
expect(url).toBe("https://runtime.us-east-1.kiro.dev/generateAssistantResponse");
});

it("routes to the regional Amazon Q endpoint for a non-default region", () => {
const ex = new KiroExecutor();
const url = ex.buildUrl("claude-opus-4.8", true, 0, {
providerSpecificData: { region: "eu-central-1" },
});
expect(url).toBe("https://q.eu-central-1.amazonaws.com/generateAssistantResponse");
});
});

describe("fetchKiroProfileArn region host", () => {
afterEach(() => vi.restoreAllMocks());

it("queries us-east-1 codewhisperer by default", async () => {
const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue({
ok: true,
json: async () => ({ profiles: [{ arn: "arn:aws:codewhisperer:us-east-1:1:profile/X" }] }),
});
const arn = await fetchKiroProfileArn("token");
expect(arn).toBe("arn:aws:codewhisperer:us-east-1:1:profile/X");
expect(fetchMock.mock.calls[0][0]).toBe(
"https://codewhisperer.us-east-1.amazonaws.com/ListAvailableProfiles"
);
});

it("queries the regional Amazon Q host for a non-default region", async () => {
const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue({
ok: true,
json: async () => ({ profiles: [{ arn: "arn:aws:codewhisperer:eu-central-1:2:profile/Y" }] }),
});
const arn = await fetchKiroProfileArn("token", "eu-central-1");
expect(arn).toBe("arn:aws:codewhisperer:eu-central-1:2:profile/Y");
expect(fetchMock.mock.calls[0][0]).toBe(
"https://q.eu-central-1.amazonaws.com/ListAvailableProfiles"
);
});
});