import { createHash, randomBytes } from "node:crypto"; import { createServer, type Server, type ServerResponse } from "node:http"; import type { OAuthCredentials, OAuthLoginCallbacks, OAuthProviderInterface } from "@earendil-works/pi-ai/oauth"; import type { KiroAuthMethod, KiroOAuthConfig } from "./config.js"; import { readKiroCliLoginConfig } from "./model-discovery.js"; import { redactSensitiveString } from "./debug-logger.js"; import type { DebugLogger } from "./debug-logger.js"; import { isRecord, nonEmptyString, type JsonRecord, positiveFiniteNumber as numericSeconds, KIRO_PROFILE_ARN_HEADER, readJsonResponse, applyProfileArnToModels, resolveOAuthProviderIdentity } from "./shared/index.js"; interface KiroCredentials extends OAuthCredentials { clientId?: string; clientSecret?: string; region?: string; profileArn?: string; authMethod?: KiroAuthMethod | string; provider?: string; } export type KiroOAuthFlow = "login" | "refresh"; export interface KiroOAuthFailureDetails { providerId: string; flow: KiroOAuthFlow; status?: number; errorCode?: string; reason: "missing_refresh_token" | "missing_client_metadata" | "missing_required_fields" | "unsupported_auth_method" | "invalid_callback" | "state_mismatch" | "token_rejected" | "authorization_denied" | "device_code_expired" | "client_rejected" | "rate_limited" | "http_error" | "request_failed"; permanent: boolean; retryable: boolean; source: "extension"; } export class KiroOAuthFailureError extends Error { readonly details: KiroOAuthFailureDetails; readonly kiroOAuth: KiroOAuthFailureDetails; constructor(message: string, details: KiroOAuthFailureDetails, options?: { cause?: unknown }) { super(redactSensitiveString(message), options?.cause === undefined ? undefined : { cause: options.cause }); this.name = details.flow === "refresh" ? "OAuthRefreshFailureError" : "KiroOAuthFailureError"; this.details = { ...details }; this.kiroOAuth = { ...details }; } } const DEFAULT_TOKEN_EXPIRES_IN_SECONDS = 3600; const DEFAULT_OAUTH_REQUEST_TIMEOUT_MS = 300_000; const DEFAULT_BUILDER_ID_START_URL = "https://view.awsapps.com/start"; const AUTH_METHODS = ["builder-id", "google", "github"] as const satisfies readonly KiroAuthMethod[]; const SOCIAL_AUTH_METHODS = ["google", "github"] as const satisfies readonly KiroAuthMethod[]; const SOCIAL_IDP_BY_METHOD: Record<(typeof SOCIAL_AUTH_METHODS)[number], string> = { google: "Google", github: "Github", }; const PKCE_VERIFIER_BYTES = 32; const DEFAULT_SOCIAL_PORTAL_URL = "https://app.kiro.dev/signin"; const DEFAULT_SOCIAL_PORTAL_REDIRECT_URI = "http://localhost:3128"; const DEFAULT_SOCIAL_CALLBACK_PATH = "/oauth/callback"; const DEFAULT_SOCIAL_CALLBACK_PORT_SPAN = 20; const LOCAL_CALLBACK_SUCCESS_HTML = "Kiro authentication complete

Kiro authentication complete

You can return to Pi.

"; const LOCAL_CALLBACK_ERROR_HTML = "Kiro authentication failed

Kiro authentication failed

Return to Pi and paste the callback URL manually.

"; interface LocalCallbackServerHandle { redirectBaseUri: string; callbackUrl: string; waitForCallback(): Promise; cancelWait(): void; close(): Promise; } function positiveInteger(value: unknown, fallback: number): number { return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : fallback; } function configString(config: KiroOAuthConfig, key: keyof KiroOAuthConfig, fallback: string): string { return nonEmptyString(config[key]) ?? fallback; } function responseString(body: JsonRecord, ...keys: string[]): string | undefined { for (const key of keys) { const value = nonEmptyString(body[key]); if (value) return value; } return undefined; } function tokenExpiresAt(body: JsonRecord): number { const explicitExpiresAt = responseString(body, "expiresAt", "expires_at"); if (explicitExpiresAt) { const parsed = Date.parse(explicitExpiresAt); if (Number.isFinite(parsed) && parsed > Date.now()) return parsed; } return expiresAt(body.expiresIn ?? body.expires_in); } function isKiroAuthMethod(value: unknown): value is KiroAuthMethod { return typeof value === "string" && AUTH_METHODS.includes(value as KiroAuthMethod); } function normalizeStoredAuthMethod(value: unknown): KiroAuthMethod | undefined { if (value === undefined || value === null || value === "") return "builder-id"; return isKiroAuthMethod(value) ? value : undefined; } function base64UrlRandom(bytes: number): string { return randomBytes(bytes).toString("base64url"); } function createPkce(): { codeVerifier: string; codeChallenge: string; state: string } { const codeVerifier = base64UrlRandom(PKCE_VERIFIER_BYTES); return { codeVerifier, codeChallenge: createHash("sha256").update(codeVerifier).digest("base64url"), state: base64UrlRandom(PKCE_VERIFIER_BYTES), }; } function endpoint(region: string, path: string): string { return `https://oidc.${region}.amazonaws.com/${path.replace(/^\/+/, "")}`; } function oauthRequestTimeoutMs(config?: Pick): number { const configuredTimeout = config?.requestTimeoutMs; return typeof configuredTimeout === "number" && Number.isFinite(configuredTimeout) && configuredTimeout > 0 ? Math.floor(configuredTimeout) : DEFAULT_OAUTH_REQUEST_TIMEOUT_MS; } async function fetchOAuthWithTimeout(input: RequestInfo | URL, init: RequestInit, config?: Pick): Promise { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), oauthRequestTimeoutMs(config)); (timeout as ReturnType & { unref?: () => void }).unref?.(); try { return await fetch(input, { ...init, signal: controller.signal }); } finally { clearTimeout(timeout); } } const OIDC_JSON_HEADERS: Record = { "Content-Type": "application/json", Accept: "application/json" }; const SOCIAL_JSON_HEADERS: Record = { "Content-Type": "application/json", Accept: "application/json", "User-Agent": "Kiro-CLI" }; const OAUTH_NON_OBJECT_MESSAGE = "Kiro OAuth returned a non-object JSON response."; type OAuthFailureFactory = (input: { cause?: unknown; status?: number; body?: JsonRecord; reason?: KiroOAuthFailureDetails["reason"]; permanent?: boolean; }) => KiroOAuthFailureError; async function postJson( config: Pick | undefined, url: string, headers: Record, payload: unknown, failure: OAuthFailureFactory, acceptBody?: (body: JsonRecord, response: Response) => boolean, ): Promise { let response: Response; try { response = await fetchOAuthWithTimeout(url, { method: "POST", headers, body: JSON.stringify(payload), }, config); } catch (error) { throw failure({ cause: error, reason: "request_failed", permanent: false }); } const body = await readJsonResponse(response, OAUTH_NON_OBJECT_MESSAGE); const accepted = acceptBody ? acceptBody(body, response) : response.ok; if (!accepted) throw failure({ status: response.status, body }); return body; } function oauthErrorParts(body: JsonRecord | undefined): { code?: string; description?: string } { const nested = isRecord(body?.error) ? body.error : undefined; return { code: nonEmptyString(body?.error) ?? nonEmptyString(nested?.code) ?? nonEmptyString(nested?.type), description: nonEmptyString(body?.error_description) ?? nonEmptyString(body?.message) ?? nonEmptyString(nested?.message), }; } function classifyOAuthReason(flow: KiroOAuthFlow, status: number | undefined, errorCode: string | undefined, description: string | undefined): Pick { const combined = [errorCode, description].filter((value): value is string => Boolean(value)).join(" "); if (/authorization[_-]?pending/i.test(combined)) return { reason: "http_error", permanent: false }; if (/slow[_-]?down|rate[_-]?limit|thrott/i.test(combined) || status === 429) return { reason: "rate_limited", permanent: false }; if (/access[_-]?denied|authorization[_-]?denied|user[_-]?denied/i.test(combined)) return { reason: "authorization_denied", permanent: true }; if (/expired[_-]?token|device.*expired|expired.*device/i.test(combined)) return { reason: "device_code_expired", permanent: true }; if (/invalid[_-]?client|unauthorized[_-]?client/i.test(combined)) return { reason: "client_rejected", permanent: true }; if (/invalid[_-]?grant|refresh[_ -]?token.*(expired|revoked|invalid|reused|not found)|token.*(expired|revoked|invalid|rejected)/i.test(combined)) { return { reason: "token_rejected", permanent: true }; } if (flow === "refresh" && (status === 400 || status === 401)) return { reason: "token_rejected", permanent: true }; return { reason: status === undefined ? "request_failed" : "http_error", permanent: false }; } function oauthFailureMessage(prefix: string, details: KiroOAuthFailureDetails): string { const status = details.status === undefined ? "" : ` HTTP ${details.status};`; const code = details.errorCode ? ` code=${redactSensitiveString(details.errorCode)};` : ""; const action = details.permanent ? "reauthentication is required" : "failure is retryable"; return `${prefix}:${status}${code} reason=${details.reason}; ${action}.`; } export function classifyKiroOAuthFailure( flow: KiroOAuthFlow, prefix: string, input: { providerId?: string; status?: number; body?: JsonRecord; cause?: unknown; reason?: KiroOAuthFailureDetails["reason"]; permanent?: boolean; }, ): KiroOAuthFailureError { const { code: errorCode, description } = oauthErrorParts(input.body); const classified = input.reason && input.permanent !== undefined ? { reason: input.reason, permanent: input.permanent } : classifyOAuthReason(flow, input.status, errorCode, description); const details: KiroOAuthFailureDetails = { providerId: input.providerId ?? "kiro", flow, status: input.status, errorCode: errorCode ? redactSensitiveString(errorCode) : undefined, reason: classified.reason, permanent: classified.permanent, retryable: !classified.permanent, source: "extension", }; if (details.status === undefined) delete details.status; if (details.errorCode === undefined) delete details.errorCode; return new KiroOAuthFailureError(oauthFailureMessage(prefix, details), details, { cause: input.cause }); } function configuredKiroOAuthFailure( config: KiroOAuthConfig, flow: KiroOAuthFlow, prefix: string, input: Parameters[2], ): KiroOAuthFailureError { return classifyKiroOAuthFailure(flow, prefix, { ...input, providerId: nonEmptyString(config.providerId) ?? "kiro" }); } function expiresAt(expiresIn: unknown): number { return Date.now() + numericSeconds(expiresIn, DEFAULT_TOKEN_EXPIRES_IN_SECONDS) * 1000; } async function registerClient(config: KiroOAuthConfig): Promise<{ clientId: string; clientSecret: string }> { const payload: JsonRecord = { clientName: config.clientName, clientType: config.clientType, scopes: config.scopes, grantTypes: config.grantTypes, }; if (config.issuerUrl && !config.skipIssuerUrlForRegistration) payload.issuerUrl = config.issuerUrl; const failure: OAuthFailureFactory = (input) => configuredKiroOAuthFailure(config, "login", "Kiro client registration failed", input); const body = await postJson(config, endpoint(config.region, "client/register"), OIDC_JSON_HEADERS, payload, failure); const clientId = nonEmptyString(body.clientId); const clientSecret = nonEmptyString(body.clientSecret); if (!clientId || !clientSecret) throw failure({ reason: "missing_required_fields", permanent: false }); return { clientId, clientSecret }; } async function requestDeviceCode(config: KiroOAuthConfig, client: { clientId: string; clientSecret: string }): Promise { const failure: OAuthFailureFactory = (input) => configuredKiroOAuthFailure(config, "login", "Kiro device authorization failed", input); return postJson(config, endpoint(config.region, "device_authorization"), OIDC_JSON_HEADERS, { clientId: client.clientId, clientSecret: client.clientSecret, startUrl: config.startUrl }, failure); } async function pollDeviceToken(config: KiroOAuthConfig, client: { clientId: string; clientSecret: string }, deviceCode: string): Promise { const failure: OAuthFailureFactory = (input) => configuredKiroOAuthFailure(config, "login", "Kiro token polling failed", input); return postJson( config, endpoint(config.region, "token"), OIDC_JSON_HEADERS, { clientId: client.clientId, clientSecret: client.clientSecret, deviceCode, grantType: "urn:ietf:params:oauth:grant-type:device_code", }, failure, (body, response) => response.ok || body.error === "authorization_pending" || body.error === "slow_down", ); } function wait(ms: number, signal?: AbortSignal): Promise { return new Promise((resolve, reject) => { if (signal?.aborted) { reject(signal.reason instanceof Error ? signal.reason : new Error("Kiro OAuth login aborted.")); return; } let settled = false; const cleanup = (): void => signal?.removeEventListener("abort", abort); const finish = (): void => { if (settled) return; settled = true; cleanup(); resolve(); }; const abort = (): void => { if (settled) return; settled = true; clearTimeout(timeout); cleanup(); reject(signal?.reason instanceof Error ? signal.reason : new Error("Kiro OAuth login aborted.")); }; const timeout = setTimeout(finish, ms); signal?.addEventListener("abort", abort, { once: true }); }); } function localCallbackPath(config: KiroOAuthConfig): string { const configuredPath = configString(config, "socialCallbackPath", DEFAULT_SOCIAL_CALLBACK_PATH); return configuredPath.startsWith("/") ? configuredPath : `/${configuredPath}`; } function hasExplicitPortalConfig(config: KiroOAuthConfig): boolean { return Boolean(nonEmptyString((config as Partial).socialPortalUrl) && nonEmptyString((config as Partial).socialPortalRedirectUri)); } function getPortalRedirectUrl(config: KiroOAuthConfig): URL | null { if (!hasExplicitPortalConfig(config)) return null; const raw = configString(config, "socialPortalRedirectUri", DEFAULT_SOCIAL_PORTAL_REDIRECT_URI); try { const parsed = new URL(raw); return parsed.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]", "::1"].includes(parsed.hostname.toLowerCase()) ? parsed : null; } catch { return null; } } function makeCallbackUrl(baseUri: string, callbackPath: string, authMethod: (typeof SOCIAL_AUTH_METHODS)[number]): string { const url = new URL(callbackPath, baseUri.endsWith("/") ? baseUri : `${baseUri}/`); url.searchParams.set("login_option", authMethod); return url.toString(); } function responseWithHtml(response: ServerResponse, status: number, html: string, location?: string): void { const headers: Record = { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store", Connection: "close", }; if (location) headers.Location = location; response.writeHead(status, headers); response.end(html); } async function listen(server: Server, host: string, port: number): Promise { await new Promise((resolve, reject) => { const onError = (error: Error) => { server.off("error", onError); reject(error); }; server.once("error", onError); server.listen(port, host, () => { server.off("error", onError); resolve(); }); }); } async function closeServer(server: Server): Promise { await new Promise((resolve) => { server.close(() => resolve()); }); } async function startLocalCallbackServer(config: KiroOAuthConfig, authMethod: (typeof SOCIAL_AUTH_METHODS)[number]): Promise { const redirectUrl = getPortalRedirectUrl(config); if (!redirectUrl) return null; const callbackPath = localCallbackPath(config); const requestedPort = redirectUrl.port ? Number.parseInt(redirectUrl.port, 10) : 0; const firstPort = positiveInteger(requestedPort, 0); const lastPort = firstPort > 0 ? firstPort + DEFAULT_SOCIAL_CALLBACK_PORT_SPAN : 0; const listenHost = redirectUrl.hostname === "localhost" ? "localhost" : redirectUrl.hostname; let settled = false; let settleWait: ((value: string | null) => void) | null = null; let boundPort = firstPort; const callbackPromise = new Promise((resolve) => { settleWait = (value) => { if (settled) return; settled = true; resolve(value); }; }); const server = createServer((request, response) => { const requestUrl = new URL(request.url ?? "/", `http://${redirectUrl.hostname || "localhost"}:${boundPort || 0}`); if (requestUrl.pathname !== callbackPath) { responseWithHtml(response, 404, LOCAL_CALLBACK_ERROR_HTML); return; } const fullUrl = `${redirectUrl.protocol}//${redirectUrl.hostname}:${boundPort}${request.url ?? ""}`; settleWait?.(fullUrl); responseWithHtml(response, 302, LOCAL_CALLBACK_SUCCESS_HTML, `${DEFAULT_SOCIAL_PORTAL_URL}?auth_status=success&redirect_from=kirocli`); }); let lastError: Error | null = null; const ports = firstPort > 0 ? Array.from({ length: lastPort - firstPort + 1 }, (_, index) => firstPort + index) : [0]; for (const port of ports) { try { await listen(server, listenHost, port); const address = server.address(); boundPort = typeof address === "object" && address ? address.port : port; lastError = null; break; } catch (error) { lastError = error instanceof Error ? error : new Error(String(error)); if ((lastError as Error & { code?: string }).code !== "EADDRINUSE") break; } } if (lastError) { await closeServer(server).catch(() => undefined); return null; } const redirectBaseUri = `${redirectUrl.protocol}//${redirectUrl.hostname}:${boundPort}`; return { redirectBaseUri, callbackUrl: makeCallbackUrl(redirectBaseUri, callbackPath, authMethod), waitForCallback: async () => callbackPromise, cancelWait: () => settleWait?.(null), close: async () => closeServer(server), }; } export function resolveBuilderIdLoginConfig(config: KiroOAuthConfig, cliLogin = readKiroCliLoginConfig()): KiroOAuthConfig { // Kiro CLI stores account-specific Builder ID routing metadata. Reuse only // start URL and region; bearer credentials remain isolated in CLI storage. if (config.startUrl !== DEFAULT_BUILDER_ID_START_URL || !cliLogin) return config; return { ...config, region: cliLogin.region, startUrl: cliLogin.startUrl }; } async function loginWithBuilderId(config: KiroOAuthConfig, callbacks: OAuthLoginCallbacks, logger: DebugLogger): Promise { const loginConfig = resolveBuilderIdLoginConfig(config); const client = await registerClient(loginConfig); const device = await requestDeviceCode(loginConfig, client); const deviceCode = nonEmptyString(device.deviceCode); const userCode = nonEmptyString(device.userCode); const verificationUri = nonEmptyString(device.verificationUri); const verificationUriComplete = nonEmptyString(device.verificationUriComplete) ?? verificationUri; if (!deviceCode || !userCode || !verificationUri) throw configuredKiroOAuthFailure(config, "login", "Kiro device authorization failed", { reason: "missing_required_fields", permanent: false }); callbacks.onAuth({ url: verificationUriComplete ?? verificationUri, instructions: `Open the Kiro authorization URL and enter code ${userCode}.`, }); const expiresIn = numericSeconds(device.expiresIn, 600); const intervalMs = Math.max(1, numericSeconds(device.interval, 5)) * 1000; const deadline = Date.now() + expiresIn * 1000; let nextIntervalMs = intervalMs; while (Date.now() < deadline) { await wait(nextIntervalMs, callbacks.signal); const token = await pollDeviceToken(loginConfig, client, deviceCode); const access = nonEmptyString(token.accessToken); if (access) { const refresh = nonEmptyString(token.refreshToken); if (!refresh) throw configuredKiroOAuthFailure(config, "login", "Kiro token polling failed", { reason: "missing_required_fields", permanent: false }); return { access, refresh, expires: expiresAt(token.expiresIn), clientId: client.clientId, clientSecret: client.clientSecret, region: loginConfig.region, authMethod: "builder-id", }; } const error = nonEmptyString(token.error); if (error === "slow_down") nextIntervalMs += intervalMs; callbacks.onProgress?.(error === "slow_down" ? "Kiro authorization is pending; slowing polling interval." : "Waiting for Kiro authorization to complete."); } logger.warn("oauth_device_code_expired", { provider: nonEmptyString(config.providerId) ?? "kiro" }); throw configuredKiroOAuthFailure(config, "login", "Kiro OAuth device code expired before authorization completed", { body: { error: "expired_token" } }); } async function selectAuthMethod(config: KiroOAuthConfig, callbacks: OAuthLoginCallbacks): Promise { if (!callbacks.onSelect) return "builder-id"; const selected = await callbacks.onSelect({ message: "Choose a Kiro sign-in method.", options: AUTH_METHODS.map((method) => ({ id: method, label: config.methodLabels[method] })), }); if (selected === undefined) { throw configuredKiroOAuthFailure(config, "login", "Kiro OAuth login was cancelled", { reason: "authorization_denied", permanent: true }); } if (!isKiroAuthMethod(selected)) { throw configuredKiroOAuthFailure(config, "login", "Kiro OAuth login method is not supported", { reason: "unsupported_auth_method", permanent: true }); } return selected; } function applyPkceParams(url: URL, codeChallenge: string, state: string): void { url.searchParams.set("code_challenge", codeChallenge); url.searchParams.set("code_challenge_method", "S256"); url.searchParams.set("state", state); } function buildLegacySocialAuthorizeUrl(config: KiroOAuthConfig, authMethod: (typeof SOCIAL_AUTH_METHODS)[number], codeChallenge: string, state: string, redirectUri: string): string { const url = new URL(config.socialAuthorizeUrl); url.searchParams.set("idp", SOCIAL_IDP_BY_METHOD[authMethod]); url.searchParams.set("redirect_uri", redirectUri); applyPkceParams(url, codeChallenge, state); url.searchParams.set("prompt", "select_account"); return url.toString(); } function buildPortalAuthorizeUrl(config: KiroOAuthConfig, codeChallenge: string, state: string, redirectBaseUri: string): string { const url = new URL(configString(config, "socialPortalUrl", DEFAULT_SOCIAL_PORTAL_URL)); applyPkceParams(url, codeChallenge, state); url.searchParams.set("redirect_uri", redirectBaseUri); url.searchParams.set("redirect_from", "kirocli"); return url.toString(); } async function requestManualCallback(callbacks: OAuthLoginCallbacks, config: KiroOAuthConfig, authMethod: KiroAuthMethod, placeholderRedirectUri: string): Promise { if (callbacks.onManualCodeInput) return callbacks.onManualCodeInput(); return callbacks.onPrompt({ message: `Paste the full Kiro ${config.methodLabels[authMethod]} callback URL.`, placeholder: `${placeholderRedirectUri}${placeholderRedirectUri.includes("?") ? "&" : "?"}code=...&state=...`, }); } function assertCallbackUrlMatchesConfig(callbackUrl: URL, configuredRedirectUri: string, providerId: string): void { let expected: URL; try { expected = new URL(configuredRedirectUri); } catch { throw classifyKiroOAuthFailure("login", "Kiro social OAuth redirect URI is invalid", { providerId, reason: "invalid_callback", permanent: true }); } if ( callbackUrl.protocol !== expected.protocol || callbackUrl.hostname.toLowerCase() !== expected.hostname.toLowerCase() || callbackUrl.port !== expected.port || callbackUrl.pathname !== expected.pathname || callbackUrl.username || callbackUrl.password || callbackUrl.hash ) { throw classifyKiroOAuthFailure("login", "Kiro social OAuth callback URL is invalid", { providerId, reason: "invalid_callback", permanent: true }); } } function parseSocialCallback(input: string, expectedState: string, configuredRedirectUri: string, authMethod: (typeof SOCIAL_AUTH_METHODS)[number], providerId: string): string { let callbackUrl: URL; try { callbackUrl = new URL(input.trim()); } catch { throw classifyKiroOAuthFailure("login", "Kiro social OAuth requires the full callback URL", { providerId, reason: "invalid_callback", permanent: true }); } assertCallbackUrlMatchesConfig(callbackUrl, configuredRedirectUri, providerId); const loginOption = nonEmptyString(callbackUrl.searchParams.get("login_option")); if (loginOption && loginOption.toLowerCase() !== authMethod) { throw classifyKiroOAuthFailure("login", "Kiro social OAuth callback was for a different sign-in method", { providerId, reason: "invalid_callback", permanent: true }); } const error = nonEmptyString(callbackUrl.searchParams.get("error")); if (error) throw classifyKiroOAuthFailure("login", "Kiro social OAuth authorization was denied", { providerId, body: { error } }); const states = callbackUrl.searchParams.getAll("state"); if (states.length !== 1 || states[0] !== expectedState) { throw classifyKiroOAuthFailure("login", "Kiro social OAuth callback state did not match", { providerId, reason: "state_mismatch", permanent: true }); } const codes = callbackUrl.searchParams.getAll("code"); const code = codes.length === 1 ? nonEmptyString(codes[0]) : undefined; if (!code) throw classifyKiroOAuthFailure("login", "Kiro social OAuth callback did not include an authorization code", { providerId, reason: "invalid_callback", permanent: true }); return code; } function buildProfileRequestOverride(profileArn: string | undefined): { request?: { headers: Record } } { return profileArn ? { request: { headers: { [KIRO_PROFILE_ARN_HEADER]: profileArn } } } : {}; } async function exchangeSocialCode(config: KiroOAuthConfig, authMethod: (typeof SOCIAL_AUTH_METHODS)[number], code: string, codeVerifier: string, redirectUri: string): Promise { const failure: OAuthFailureFactory = (input) => configuredKiroOAuthFailure(config, "login", "Kiro social token exchange failed", input); const body = await postJson(config, config.socialTokenUrl, SOCIAL_JSON_HEADERS, { code, code_verifier: codeVerifier, redirect_uri: redirectUri }, failure); const access = responseString(body, "accessToken", "access_token"); const refresh = responseString(body, "refreshToken", "refresh_token"); if (!access || !refresh) throw failure({ reason: "missing_required_fields", permanent: false }); const profileArn = responseString(body, "profileArn", "profile_arn"); return { access, refresh, expires: tokenExpiresAt(body), profileArn, authMethod, provider: SOCIAL_IDP_BY_METHOD[authMethod], ...buildProfileRequestOverride(profileArn), }; } async function resolveCallbackInput(callbacks: OAuthLoginCallbacks, config: KiroOAuthConfig, authMethod: (typeof SOCIAL_AUTH_METHODS)[number], callbackServer: LocalCallbackServerHandle | null, placeholderRedirectUri: string): Promise { if (!callbackServer) return requestManualCallback(callbacks, config, authMethod, placeholderRedirectUri); const serverResultPromise = callbackServer.waitForCallback().then((value) => value ? { source: "server" as const, value } : null); const manualResultPromise = requestManualCallback(callbacks, config, authMethod, callbackServer.callbackUrl).then((value) => ({ source: "manual" as const, value })); const firstResult = await Promise.race([serverResultPromise, manualResultPromise]); if (firstResult) { if (firstResult.source === "manual") callbackServer.cancelWait(); return firstResult.value; } return requestManualCallback(callbacks, config, authMethod, callbackServer.callbackUrl); } async function loginWithSocial(config: KiroOAuthConfig, authMethod: (typeof SOCIAL_AUTH_METHODS)[number], callbacks: OAuthLoginCallbacks, logger: DebugLogger): Promise { const pkce = createPkce(); let expectedState: string | undefined = pkce.state; let codeVerifier: string | undefined = pkce.codeVerifier; let callbackServer: LocalCallbackServerHandle | null = null; try { callbackServer = await startLocalCallbackServer(config, authMethod); const redirectBaseUri = callbackServer?.redirectBaseUri; const callbackRedirectUri = callbackServer?.callbackUrl ?? config.socialRedirectUri; callbacks.onAuth({ url: redirectBaseUri ? buildPortalAuthorizeUrl(config, pkce.codeChallenge, pkce.state, redirectBaseUri) : buildLegacySocialAuthorizeUrl(config, authMethod, pkce.codeChallenge, pkce.state, callbackRedirectUri), instructions: redirectBaseUri ? `Complete Kiro ${config.methodLabels[authMethod]} sign-in in the browser. Pi will capture the localhost callback automatically; paste the final callback URL only if capture fails.` : `Complete Kiro ${config.methodLabels[authMethod]} sign-in, then paste the full callback URL from the browser or app prompt.`, }); callbacks.onProgress?.("Waiting for Kiro authentication callback..."); const callbackInput = await resolveCallbackInput(callbacks, config, authMethod, callbackServer, callbackRedirectUri); const state = expectedState; const verifier = codeVerifier; expectedState = undefined; codeVerifier = undefined; if (!state || !verifier) throw configuredKiroOAuthFailure(config, "login", "Kiro social OAuth verifier was already used", { reason: "invalid_callback", permanent: true }); const code = parseSocialCallback(callbackInput, state, callbackRedirectUri, authMethod, nonEmptyString(config.providerId) ?? "kiro"); callbacks.onProgress?.("Exchanging Kiro authorization code..."); return exchangeSocialCode(config, authMethod, code, verifier, callbackRedirectUri); } finally { expectedState = undefined; codeVerifier = undefined; try { await callbackServer?.close(); } catch (error) { logger.warn("oauth_callback_server_close_failed", { provider: nonEmptyString(config.providerId) ?? "kiro", error }); } } } async function loginKiro(config: KiroOAuthConfig, callbacks: OAuthLoginCallbacks, logger: DebugLogger): Promise { const authMethod = await selectAuthMethod(config, callbacks); if (authMethod === "builder-id") return loginWithBuilderId(config, callbacks, logger); return loginWithSocial(config, authMethod, callbacks, logger); } async function refreshWithOidc(credentials: KiroCredentials, providerId: string, config?: Pick): Promise { const region = credentials.region ?? "us-east-1"; const clientId = nonEmptyString(credentials.clientId); const clientSecret = nonEmptyString(credentials.clientSecret); if (!clientId || !clientSecret) throw classifyKiroOAuthFailure("refresh", "Kiro OIDC refresh requires client metadata", { providerId, reason: "missing_client_metadata", permanent: true }); const failure: OAuthFailureFactory = (input) => classifyKiroOAuthFailure("refresh", "Kiro token refresh failed", { ...input, providerId }); const body = await postJson(config, endpoint(region, "token"), OIDC_JSON_HEADERS, { clientId, clientSecret, refreshToken: credentials.refresh, grantType: "refresh_token" }, failure); const access = nonEmptyString(body.accessToken); if (!access) throw failure({ reason: "missing_required_fields", permanent: false }); return { ...credentials, access, refresh: nonEmptyString(body.refreshToken) ?? credentials.refresh, expires: expiresAt(body.expiresIn), authMethod: "builder-id", }; } async function refreshWithSocialEndpoint(config: KiroOAuthConfig, credentials: KiroCredentials, authMethod: (typeof SOCIAL_AUTH_METHODS)[number]): Promise { const failure: OAuthFailureFactory = (input) => configuredKiroOAuthFailure(config, "refresh", "Kiro social token refresh failed", input); const body = await postJson(config, config.socialRefreshUrl, SOCIAL_JSON_HEADERS, { refreshToken: credentials.refresh }, failure); const access = responseString(body, "accessToken", "access_token"); if (!access) throw failure({ reason: "missing_required_fields", permanent: false }); const profileArn = responseString(body, "profileArn", "profile_arn") ?? credentials.profileArn; return { ...credentials, access, refresh: responseString(body, "refreshToken", "refresh_token") ?? credentials.refresh, expires: tokenExpiresAt(body), profileArn, authMethod, provider: SOCIAL_IDP_BY_METHOD[authMethod], ...buildProfileRequestOverride(profileArn), }; } export interface KiroOAuthProviderOptions { providerId?: string; displayName?: string; } export function createKiroOAuthProvider(config: KiroOAuthConfig, logger: DebugLogger, options: KiroOAuthProviderOptions = {}): OAuthProviderInterface { const { providerId, displayName } = resolveOAuthProviderIdentity(options, config); const providerConfig: KiroOAuthConfig = { ...config, providerId }; return { id: providerId, name: displayName, async login(callbacks) { return loginKiro(providerConfig, callbacks, logger); }, async refreshToken(credentials) { const kiroCredentials = credentials as KiroCredentials; if (!kiroCredentials.refresh) throw classifyKiroOAuthFailure("refresh", "Kiro OAuth refresh requires a refresh token", { providerId, reason: "missing_refresh_token", permanent: true }); const authMethod = normalizeStoredAuthMethod(kiroCredentials.authMethod); if (!authMethod) throw classifyKiroOAuthFailure("refresh", "Kiro OAuth credential auth method is not supported", { providerId, reason: "unsupported_auth_method", permanent: true }); if (authMethod === "builder-id") return refreshWithOidc(kiroCredentials, providerId, providerConfig); return refreshWithSocialEndpoint(providerConfig, kiroCredentials, authMethod); }, getApiKey(credentials) { return credentials.access; }, modifyModels: applyProfileArnToModels, }; }