diff --git a/src/app/(dashboard)/dashboard/cli-tools/components/CodexToolCard.js b/src/app/(dashboard)/dashboard/cli-tools/components/CodexToolCard.js
index ecb1f05..80df981 100644
--- a/src/app/(dashboard)/dashboard/cli-tools/components/CodexToolCard.js
+++ b/src/app/(dashboard)/dashboard/cli-tools/components/CodexToolCard.js
@@ -168,7 +168,7 @@ wire_api = "responses"
- { e.target.style.display = "none"; }} />
+ { e.target.style.display = "none"; }} />
diff --git a/src/app/(dashboard)/dashboard/cli-tools/components/DefaultToolCard.js b/src/app/(dashboard)/dashboard/cli-tools/components/DefaultToolCard.js
index ad58149..ad985c2 100644
--- a/src/app/(dashboard)/dashboard/cli-tools/components/DefaultToolCard.js
+++ b/src/app/(dashboard)/dashboard/cli-tools/components/DefaultToolCard.js
@@ -235,8 +235,8 @@ export default function DefaultToolCard({ toolId, tool, isExpanded, onToggle, ba
alt={tool.name}
width={40}
height={40}
- className="size-12 object-contain rounded-xl bg-gray-500"
- style={{ width: "auto", height: "auto" }}
+ className="size-12 object-contain rounded-xl bg-gray-500 max-w-[48px] max-h-[48px]"
+ sizes="48px"
onError={(e) => { e.target.style.display = "none"; }}
/>
);
@@ -250,8 +250,8 @@ export default function DefaultToolCard({ toolId, tool, isExpanded, onToggle, ba
alt={tool.name}
width={40}
height={40}
- className="size-10 object-contain rounded-xl"
- style={{ width: "auto", height: "auto" }}
+ className="size-10 object-contain rounded-xl max-w-[40px] max-h-[40px]"
+ sizes="40px"
onError={(e) => { e.target.style.display = "none"; }}
/>
);
diff --git a/src/app/(dashboard)/dashboard/providers/[id]/page.js b/src/app/(dashboard)/dashboard/providers/[id]/page.js
index b4bee0c..cfe37df 100644
--- a/src/app/(dashboard)/dashboard/providers/[id]/page.js
+++ b/src/app/(dashboard)/dashboard/providers/[id]/page.js
@@ -5,7 +5,7 @@ import PropTypes from "prop-types";
import { useParams } from "next/navigation";
import Link from "next/link";
import Image from "next/image";
-import { Card, Button, Badge, Input, Modal, CardSkeleton, OAuthModal, Toggle } from "@/shared/components";
+import { Card, Button, Badge, Input, Modal, CardSkeleton, OAuthModal, KiroOAuthWrapper, Toggle } from "@/shared/components";
import { OAUTH_PROVIDERS, APIKEY_PROVIDERS, getProviderAlias } from "@/shared/constants/providers";
import { getModelsByProviderId } from "@/shared/constants/models";
import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";
@@ -275,8 +275,8 @@ export default function ProviderDetailPage() {
alt={providerInfo.name}
width={48}
height={48}
- className="object-contain rounded-lg"
- style={{ width: "auto", height: "auto" }}
+ className="object-contain rounded-lg max-w-[48px] max-h-[48px]"
+ sizes="48px"
onError={(e) => { e.currentTarget.style.display = "none"; }}
/>
@@ -344,13 +344,22 @@ export default function ProviderDetailPage() {
{/* Modals */}
-
setShowOAuthModal(false)}
- />
+ {providerId === "kiro" ? (
+ setShowOAuthModal(false)}
+ />
+ ) : (
+ setShowOAuthModal(false)}
+ />
+ )}
setImgError(true)}
/>
)}
diff --git a/src/app/api/oauth/kiro/import/route.js b/src/app/api/oauth/kiro/import/route.js
new file mode 100644
index 0000000..c1db94b
--- /dev/null
+++ b/src/app/api/oauth/kiro/import/route.js
@@ -0,0 +1,76 @@
+import { NextResponse } from "next/server";
+import { KiroService } from "@/lib/oauth/services/kiro";
+import { createProviderConnection, isCloudEnabled } from "@/models";
+import { getConsistentMachineId } from "@/shared/utils/machineId";
+import { syncToCloud } from "@/app/api/sync/cloud/route";
+
+/**
+ * POST /api/oauth/kiro/import
+ * Import and validate refresh token from Kiro IDE
+ */
+export async function POST(request) {
+ try {
+ const { refreshToken } = await request.json();
+
+ if (!refreshToken || typeof refreshToken !== "string") {
+ return NextResponse.json(
+ { error: "Refresh token is required" },
+ { status: 400 }
+ );
+ }
+
+ const kiroService = new KiroService();
+
+ // Validate and refresh token
+ const tokenData = await kiroService.validateImportToken(refreshToken.trim());
+
+ // Extract email from JWT if available
+ const email = kiroService.extractEmailFromJWT(tokenData.accessToken);
+
+ // Save to database
+ const connection = await createProviderConnection({
+ provider: "kiro",
+ authType: "oauth",
+ accessToken: tokenData.accessToken,
+ refreshToken: tokenData.refreshToken,
+ expiresAt: new Date(Date.now() + tokenData.expiresIn * 1000).toISOString(),
+ email: email || null,
+ providerSpecificData: {
+ profileArn: tokenData.profileArn,
+ authMethod: "imported",
+ provider: "Imported",
+ },
+ testStatus: "active",
+ });
+
+ // Auto sync to Cloud if enabled
+ await syncToCloudIfEnabled();
+
+ return NextResponse.json({
+ success: true,
+ connection: {
+ id: connection.id,
+ provider: connection.provider,
+ email: connection.email,
+ },
+ });
+ } catch (error) {
+ console.log("Kiro import token error:", error);
+ return NextResponse.json({ error: error.message }, { status: 500 });
+ }
+}
+
+/**
+ * Sync to Cloud if enabled
+ */
+async function syncToCloudIfEnabled() {
+ try {
+ const cloudEnabled = await isCloudEnabled();
+ if (!cloudEnabled) return;
+
+ const machineId = await getConsistentMachineId();
+ await syncToCloud(machineId);
+ } catch (error) {
+ console.log("Error syncing to cloud after Kiro import:", error);
+ }
+}
diff --git a/src/app/api/oauth/kiro/social-authorize/route.js b/src/app/api/oauth/kiro/social-authorize/route.js
new file mode 100644
index 0000000..e5e1281
--- /dev/null
+++ b/src/app/api/oauth/kiro/social-authorize/route.js
@@ -0,0 +1,43 @@
+import { NextResponse } from "next/server";
+import { generatePKCE } from "@/lib/oauth/utils/pkce";
+import { KiroService } from "@/lib/oauth/services/kiro";
+
+/**
+ * GET /api/oauth/kiro/social-authorize
+ * Generate Google/GitHub social login URL for manual callback flow
+ * Uses kiro:// custom protocol as required by AWS Cognito
+ */
+export async function GET(request) {
+ try {
+ const { searchParams } = new URL(request.url);
+ const provider = searchParams.get("provider"); // "google" or "github"
+
+ if (!provider || !["google", "github"].includes(provider)) {
+ return NextResponse.json(
+ { error: "Invalid provider. Use 'google' or 'github'" },
+ { status: 400 }
+ );
+ }
+
+ // Generate PKCE for social auth
+ const { codeVerifier, codeChallenge, state } = generatePKCE();
+
+ const kiroService = new KiroService();
+ const authUrl = kiroService.buildSocialLoginUrl(
+ provider,
+ codeChallenge,
+ state
+ );
+
+ return NextResponse.json({
+ authUrl,
+ state,
+ codeVerifier,
+ codeChallenge,
+ provider,
+ });
+ } catch (error) {
+ console.log("Kiro social authorize error:", error);
+ return NextResponse.json({ error: error.message }, { status: 500 });
+ }
+}
diff --git a/src/app/api/oauth/kiro/social-exchange/route.js b/src/app/api/oauth/kiro/social-exchange/route.js
new file mode 100644
index 0000000..5e91618
--- /dev/null
+++ b/src/app/api/oauth/kiro/social-exchange/route.js
@@ -0,0 +1,87 @@
+import { NextResponse } from "next/server";
+import { KiroService } from "@/lib/oauth/services/kiro";
+import { createProviderConnection, isCloudEnabled } from "@/models";
+import { getConsistentMachineId } from "@/shared/utils/machineId";
+import { syncToCloud } from "@/app/api/sync/cloud/route";
+
+/**
+ * POST /api/oauth/kiro/social-exchange
+ * Exchange authorization code for tokens (Google/GitHub social login)
+ * Callback URL will be in format: kiro://kiro.kiroAgent/authenticate-success?code=XXX&state=YYY
+ */
+export async function POST(request) {
+ try {
+ const { code, codeVerifier, provider } = await request.json();
+
+ if (!code || !codeVerifier) {
+ return NextResponse.json(
+ { error: "Missing required fields" },
+ { status: 400 }
+ );
+ }
+
+ if (!provider || !["google", "github"].includes(provider)) {
+ return NextResponse.json(
+ { error: "Invalid provider" },
+ { status: 400 }
+ );
+ }
+
+ const kiroService = new KiroService();
+
+ // Exchange code for tokens (redirect_uri handled internally)
+ const tokenData = await kiroService.exchangeSocialCode(
+ code,
+ codeVerifier
+ );
+
+ // Extract email from JWT if available
+ const email = kiroService.extractEmailFromJWT(tokenData.accessToken);
+
+ // Save to database
+ const connection = await createProviderConnection({
+ provider: "kiro",
+ authType: "oauth",
+ accessToken: tokenData.accessToken,
+ refreshToken: tokenData.refreshToken,
+ expiresAt: new Date(Date.now() + tokenData.expiresIn * 1000).toISOString(),
+ email: email || null,
+ providerSpecificData: {
+ profileArn: tokenData.profileArn,
+ authMethod: provider, // "google" or "github"
+ provider: provider.charAt(0).toUpperCase() + provider.slice(1),
+ },
+ testStatus: "active",
+ });
+
+ // Auto sync to Cloud if enabled
+ await syncToCloudIfEnabled();
+
+ return NextResponse.json({
+ success: true,
+ connection: {
+ id: connection.id,
+ provider: connection.provider,
+ email: connection.email,
+ },
+ });
+ } catch (error) {
+ console.log("Kiro social exchange error:", error);
+ return NextResponse.json({ error: error.message }, { status: 500 });
+ }
+}
+
+/**
+ * Sync to Cloud if enabled
+ */
+async function syncToCloudIfEnabled() {
+ try {
+ const cloudEnabled = await isCloudEnabled();
+ if (!cloudEnabled) return;
+
+ const machineId = await getConsistentMachineId();
+ await syncToCloud(machineId);
+ } catch (error) {
+ console.log("Error syncing to cloud after Kiro OAuth:", error);
+ }
+}
diff --git a/src/app/landing/components/FlowAnimation.js b/src/app/landing/components/FlowAnimation.js
index 08d53ce..482e7a1 100644
--- a/src/app/landing/components/FlowAnimation.js
+++ b/src/app/landing/components/FlowAnimation.js
@@ -48,7 +48,8 @@ export default function FlowAnimation() {
alt={tool.name}
width={48}
height={48}
- className="object-contain rounded-xl"
+ className="object-contain rounded-xl max-w-[48px] max-h-[48px]"
+ sizes="48px"
/>
diff --git a/src/lib/oauth/constants/oauth.js b/src/lib/oauth/constants/oauth.js
index d557dba..d08a221 100644
--- a/src/lib/oauth/constants/oauth.js
+++ b/src/lib/oauth/constants/oauth.js
@@ -113,22 +113,33 @@ export const GITHUB_CONFIG = {
editorPluginVersion: "copilot-chat/0.26.7",
};
-// Kiro OAuth Configuration (AWS SSO OIDC Device Code Flow)
+// Kiro OAuth Configuration
+// Supports multiple auth methods:
+// 1. AWS Builder ID (Device Code Flow)
+// 2. AWS IAM Identity Center/IDC (Device Code Flow with custom startUrl/region)
+// 3. Google/GitHub Social Login (Authorization Code Flow - manual callback)
+// 4. Import Token (paste refresh token from Kiro IDE)
export const KIRO_CONFIG = {
- // AWS SSO OIDC endpoints for Builder ID
+ // AWS SSO OIDC endpoints for Builder ID/IDC (Device Code Flow)
ssoOidcEndpoint: "https://oidc.us-east-1.amazonaws.com",
registerClientUrl: "https://oidc.us-east-1.amazonaws.com/client/register",
deviceAuthUrl: "https://oidc.us-east-1.amazonaws.com/device_authorization",
tokenUrl: "https://oidc.us-east-1.amazonaws.com/token",
- refreshTokenUrl: "https://prod.us-east-1.auth.desktop.kiro.dev/refreshToken",
- // AWS Builder ID start URL
+ // AWS Builder ID default start URL
startUrl: "https://view.awsapps.com/start",
// Client registration params
- clientName: "kiro-cli",
+ clientName: "kiro-oauth-client",
clientType: "public",
scopes: ["codewhisperer:completions", "codewhisperer:analysis", "codewhisperer:conversations"],
grantTypes: ["urn:ietf:params:oauth:grant-type:device_code", "refresh_token"],
issuerUrl: "https://identitycenter.amazonaws.com/ssoins-722374e8c3c8e6c6",
+ // Social auth endpoints (Google/GitHub via AWS Cognito)
+ socialAuthEndpoint: "https://prod.us-east-1.auth.desktop.kiro.dev",
+ socialLoginUrl: "https://prod.us-east-1.auth.desktop.kiro.dev/login",
+ socialTokenUrl: "https://prod.us-east-1.auth.desktop.kiro.dev/oauth/token",
+ socialRefreshUrl: "https://prod.us-east-1.auth.desktop.kiro.dev/refreshToken",
+ // Auth methods
+ authMethods: ["builder-id", "idc", "google", "github", "import"],
};
// OAuth timeout (5 minutes)
diff --git a/src/lib/oauth/services/index.js b/src/lib/oauth/services/index.js
index b3d8284..3912864 100644
--- a/src/lib/oauth/services/index.js
+++ b/src/lib/oauth/services/index.js
@@ -11,4 +11,5 @@ export { IFlowService } from "./iflow.js";
export { AntigravityService } from "./antigravity.js";
export { OpenAIService } from "./openai.js";
export { GitHubService } from "./github.js";
+export { KiroService } from "./kiro.js";
diff --git a/src/lib/oauth/services/kiro.js b/src/lib/oauth/services/kiro.js
new file mode 100644
index 0000000..e65b0ab
--- /dev/null
+++ b/src/lib/oauth/services/kiro.js
@@ -0,0 +1,276 @@
+import { KIRO_CONFIG } from "../constants/oauth.js";
+
+/**
+ * Kiro OAuth Service
+ * Supports multiple authentication methods:
+ * 1. AWS Builder ID (Device Code Flow)
+ * 2. AWS IAM Identity Center/IDC (Device Code Flow)
+ * 3. Google/GitHub Social Login (Authorization Code Flow + Manual Callback)
+ * 4. Import Token (Manual refresh token paste)
+ */
+
+const KIRO_AUTH_SERVICE = "https://prod.us-east-1.auth.desktop.kiro.dev";
+
+export class KiroService {
+ /**
+ * Register OIDC client with AWS SSO
+ * Returns clientId and clientSecret for device code flow
+ */
+ async registerClient(region = "us-east-1") {
+ const endpoint = `https://oidc.${region}.amazonaws.com/client/register`;
+
+ const response = await fetch(endpoint, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ clientName: KIRO_CONFIG.clientName,
+ clientType: KIRO_CONFIG.clientType,
+ scopes: KIRO_CONFIG.scopes,
+ grantTypes: KIRO_CONFIG.grantTypes,
+ issuerUrl: KIRO_CONFIG.issuerUrl,
+ }),
+ });
+
+ if (!response.ok) {
+ const error = await response.text();
+ throw new Error(`Failed to register client: ${error}`);
+ }
+
+ const data = await response.json();
+ return {
+ clientId: data.clientId,
+ clientSecret: data.clientSecret,
+ clientSecretExpiresAt: data.clientSecretExpiresAt,
+ };
+ }
+
+ /**
+ * Start device authorization for AWS Builder ID or IDC
+ */
+ async startDeviceAuthorization(clientId, clientSecret, startUrl, region = "us-east-1") {
+ const endpoint = `https://oidc.${region}.amazonaws.com/device_authorization`;
+
+ const response = await fetch(endpoint, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ clientId,
+ clientSecret,
+ startUrl,
+ }),
+ });
+
+ if (!response.ok) {
+ const error = await response.text();
+ throw new Error(`Failed to start device authorization: ${error}`);
+ }
+
+ const data = await response.json();
+ return {
+ deviceCode: data.deviceCode,
+ userCode: data.userCode,
+ verificationUri: data.verificationUri,
+ verificationUriComplete: data.verificationUriComplete,
+ expiresIn: data.expiresIn,
+ interval: data.interval || 5,
+ };
+ }
+
+ /**
+ * Poll for token using device code (AWS Builder ID/IDC)
+ */
+ async pollDeviceToken(clientId, clientSecret, deviceCode, region = "us-east-1") {
+ const endpoint = `https://oidc.${region}.amazonaws.com/token`;
+
+ const response = await fetch(endpoint, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ clientId,
+ clientSecret,
+ deviceCode,
+ grantType: "urn:ietf:params:oauth:grant-type:device_code",
+ }),
+ });
+
+ const data = await response.json();
+
+ // Handle pending/slow_down/errors
+ if (!response.ok || data.error) {
+ return {
+ success: false,
+ error: data.error,
+ errorDescription: data.error_description,
+ pending: data.error === "authorization_pending" || data.error === "slow_down",
+ };
+ }
+
+ return {
+ success: true,
+ tokens: {
+ accessToken: data.accessToken,
+ refreshToken: data.refreshToken,
+ expiresIn: data.expiresIn,
+ tokenType: data.tokenType,
+ },
+ };
+ }
+
+ /**
+ * Build Google/GitHub social login URL
+ * Returns authorization URL for manual callback flow
+ * Uses kiro:// custom protocol as required by AWS Cognito whitelist
+ */
+ buildSocialLoginUrl(provider, codeChallenge, state) {
+ const idp = provider === "google" ? "Google" : "Github";
+ // AWS Cognito only whitelists kiro:// protocol, not localhost
+ const redirectUri = "kiro://kiro.kiroAgent/authenticate-success";
+ return `${KIRO_AUTH_SERVICE}/login?idp=${idp}&redirect_uri=${encodeURIComponent(redirectUri)}&code_challenge=${codeChallenge}&code_challenge_method=S256&state=${state}&prompt=select_account`;
+ }
+
+ /**
+ * Exchange authorization code for tokens (Social Login)
+ * Must use same redirect_uri as authorization request
+ */
+ async exchangeSocialCode(code, codeVerifier) {
+ // Must match the redirect_uri used in buildSocialLoginUrl
+ const redirectUri = "kiro://kiro.kiroAgent/authenticate-success";
+
+ const response = await fetch(`${KIRO_AUTH_SERVICE}/oauth/token`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ code,
+ code_verifier: codeVerifier,
+ redirect_uri: redirectUri,
+ }),
+ });
+
+ if (!response.ok) {
+ const error = await response.text();
+ throw new Error(`Token exchange failed: ${error}`);
+ }
+
+ const data = await response.json();
+ return {
+ accessToken: data.accessToken,
+ refreshToken: data.refreshToken,
+ profileArn: data.profileArn,
+ expiresIn: data.expiresIn || 3600,
+ };
+ }
+
+ /**
+ * Refresh token using refresh token
+ */
+ async refreshToken(refreshToken, providerSpecificData = {}) {
+ const { authMethod, clientId, clientSecret, region } = providerSpecificData;
+
+ // AWS SSO OIDC refresh (Builder ID or IDC)
+ if (clientId && clientSecret) {
+ const endpoint = `https://oidc.${region || "us-east-1"}.amazonaws.com/token`;
+
+ const response = await fetch(endpoint, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ clientId,
+ clientSecret,
+ refreshToken,
+ grantType: "refresh_token",
+ }),
+ });
+
+ if (!response.ok) {
+ const error = await response.text();
+ throw new Error(`Token refresh failed: ${error}`);
+ }
+
+ const data = await response.json();
+ return {
+ accessToken: data.accessToken,
+ refreshToken: data.refreshToken || refreshToken,
+ expiresIn: data.expiresIn,
+ };
+ }
+
+ // Social auth refresh (Google/GitHub)
+ const response = await fetch(`${KIRO_AUTH_SERVICE}/refreshToken`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ refreshToken,
+ }),
+ });
+
+ if (!response.ok) {
+ const error = await response.text();
+ throw new Error(`Token refresh failed: ${error}`);
+ }
+
+ const data = await response.json();
+ return {
+ accessToken: data.accessToken,
+ refreshToken: data.refreshToken || refreshToken,
+ profileArn: data.profileArn,
+ expiresIn: data.expiresIn || 3600,
+ };
+ }
+
+ /**
+ * Validate and import refresh token
+ */
+ async validateImportToken(refreshToken) {
+ // Validate token format
+ if (!refreshToken.startsWith("aorAAAAAG")) {
+ throw new Error("Invalid token format. Token should start with aorAAAAAG...");
+ }
+
+ // Try to refresh to validate
+ try {
+ const result = await this.refreshToken(refreshToken);
+ return {
+ accessToken: result.accessToken,
+ refreshToken: result.refreshToken || refreshToken,
+ profileArn: result.profileArn,
+ expiresIn: result.expiresIn,
+ authMethod: "imported",
+ };
+ } catch (error) {
+ throw new Error(`Token validation failed: ${error.message}`);
+ }
+ }
+
+ /**
+ * Fetch user email from access token (optional, for display)
+ */
+ extractEmailFromJWT(accessToken) {
+ try {
+ const parts = accessToken.split(".");
+ if (parts.length !== 3) return null;
+
+ // Decode payload (add padding if needed)
+ let payload = parts[1];
+ while (payload.length % 4) {
+ payload += "=";
+ }
+
+ const decoded = JSON.parse(atob(payload.replace(/-/g, "+").replace(/_/g, "/")));
+ return decoded.email || decoded.preferred_username || decoded.sub;
+ } catch {
+ return null;
+ }
+ }
+}
diff --git a/src/shared/components/Header.js b/src/shared/components/Header.js
index 88bba08..d805f20 100644
--- a/src/shared/components/Header.js
+++ b/src/shared/components/Header.js
@@ -94,8 +94,8 @@ export default function Header({ onMenuClick, showMenuButton = true }) {
alt={crumb.label}
width={28}
height={28}
- className="object-contain rounded"
- style={{ width: "auto", height: "auto" }}
+ className="object-contain rounded max-w-[28px] max-h-[28px]"
+ sizes="28px"
onError={(e) => { e.currentTarget.style.display = "none"; }}
/>
)}
diff --git a/src/shared/components/KiroAuthModal.js b/src/shared/components/KiroAuthModal.js
new file mode 100644
index 0000000..9e9789a
--- /dev/null
+++ b/src/shared/components/KiroAuthModal.js
@@ -0,0 +1,325 @@
+"use client";
+
+import { useState } from "react";
+import PropTypes from "prop-types";
+import { Modal, Button, Input } from "@/shared/components";
+import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";
+
+/**
+ * Kiro Auth Method Selection Modal
+ * Allows user to choose between multiple Kiro authentication methods:
+ * 1. AWS Builder ID (Device Code)
+ * 2. AWS IAM Identity Center/IDC (Device Code with custom startUrl/region)
+ * 3. Google Social Login (Manual callback)
+ * 4. GitHub Social Login (Manual callback)
+ * 5. Import Token (Paste refresh token)
+ */
+export default function KiroAuthModal({ isOpen, onMethodSelect, onClose }) {
+ const [selectedMethod, setSelectedMethod] = useState(null);
+ const [idcStartUrl, setIdcStartUrl] = useState("");
+ const [idcRegion, setIdcRegion] = useState("us-east-1");
+ const [refreshToken, setRefreshToken] = useState("");
+ const [error, setError] = useState(null);
+ const [importing, setImporting] = useState(false);
+ const { copied, copy } = useCopyToClipboard();
+
+ const handleMethodSelect = (method) => {
+ setSelectedMethod(method);
+ setError(null);
+ };
+
+ const handleBack = () => {
+ setSelectedMethod(null);
+ setError(null);
+ };
+
+ const handleImportToken = async () => {
+ if (!refreshToken.trim()) {
+ setError("Please enter a refresh token");
+ return;
+ }
+
+ setImporting(true);
+ setError(null);
+
+ try {
+ const res = await fetch("/api/oauth/kiro/import", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ refreshToken: refreshToken.trim() }),
+ });
+
+ const data = await res.json();
+
+ if (!res.ok) {
+ throw new Error(data.error || "Import failed");
+ }
+
+ // Success - close modal
+ onClose();
+ } catch (err) {
+ setError(err.message);
+ } finally {
+ setImporting(false);
+ }
+ };
+
+ const handleIdcContinue = () => {
+ if (!idcStartUrl.trim()) {
+ setError("Please enter your IDC start URL");
+ return;
+ }
+ onMethodSelect("idc", { startUrl: idcStartUrl.trim(), region: idcRegion });
+ };
+
+ const handleSocialLogin = (provider) => {
+ onMethodSelect("social", { provider });
+ };
+
+ return (
+
+
+ {/* Method Selection */}
+ {!selectedMethod && (
+
+
+ Choose your authentication method:
+
+
+ {/* AWS Builder ID */}
+
+
+ {/* AWS IAM Identity Center (IDC) - HIDDEN */}
+
+
+ {/* Google Social Login - HIDDEN */}
+
+
+ {/* GitHub Social Login - HIDDEN */}
+
+
+ {/* Import Token */}
+
+
+ )}
+
+ {/* IDC Configuration */}
+ {selectedMethod === "idc" && (
+
+
+
+
setIdcStartUrl(e.target.value)}
+ placeholder="https://your-org.awsapps.com/start"
+ className="font-mono text-sm"
+ />
+
+ Your organization's AWS IAM Identity Center URL
+
+
+
+
+
+
setIdcRegion(e.target.value)}
+ placeholder="us-east-1"
+ className="font-mono text-sm"
+ />
+
+ AWS region for your Identity Center (default: us-east-1)
+
+
+
+ {error && (
+
{error}
+ )}
+
+
+
+
+
+
+ )}
+
+ {/* Social Login Info (Google) */}
+ {selectedMethod === "social-google" && (
+
+
+
+
info
+
+
+ Manual Callback Required
+
+
+ After login, you'll need to copy the callback URL from your browser and paste it back here.
+
+
+
+
+
+
+
+
+
+
+ )}
+
+ {/* Social Login Info (GitHub) */}
+ {selectedMethod === "social-github" && (
+
+
+
+
info
+
+
+ Manual Callback Required
+
+
+ After login, you'll need to copy the callback URL from your browser and paste it back here.
+
+
+
+
+
+
+
+
+
+
+ )}
+
+ {/* Import Token */}
+ {selectedMethod === "import" && (
+
+
+
+ 💡 Please login to Kiro IDE first.
+
+
+
+
+
+
setRefreshToken(e.target.value)}
+ placeholder="aorAAAAAG..."
+ className="font-mono text-sm"
+ type="password"
+ />
+
+ Find it in Kiro IDE at: ~/.aws/sso/cache/kiro-auth-token.json
+
+
+
+ {error && (
+
+ )}
+
+
+
+
+
+
+ )}
+
+
+ );
+}
+
+KiroAuthModal.propTypes = {
+ isOpen: PropTypes.bool.isRequired,
+ onMethodSelect: PropTypes.func.isRequired,
+ onClose: PropTypes.func.isRequired,
+};
diff --git a/src/shared/components/KiroOAuthWrapper.js b/src/shared/components/KiroOAuthWrapper.js
new file mode 100644
index 0000000..be53ccd
--- /dev/null
+++ b/src/shared/components/KiroOAuthWrapper.js
@@ -0,0 +1,101 @@
+"use client";
+
+import { useState, useCallback } from "react";
+import PropTypes from "prop-types";
+import OAuthModal from "./OAuthModal";
+import KiroAuthModal from "./KiroAuthModal";
+import KiroSocialOAuthModal from "./KiroSocialOAuthModal";
+
+/**
+ * Kiro OAuth Wrapper
+ * Orchestrates between method selection, device code flow, and social login flow
+ */
+export default function KiroOAuthWrapper({ isOpen, providerInfo, onSuccess, onClose }) {
+ const [authMethod, setAuthMethod] = useState(null); // null | "builder-id" | "idc" | "social" | "import"
+ const [socialProvider, setSocialProvider] = useState(null); // "google" | "github"
+ const [idcConfig, setIdcConfig] = useState(null);
+
+ const handleMethodSelect = useCallback((method, config) => {
+ if (method === "builder-id") {
+ // Use device code flow (AWS Builder ID)
+ setAuthMethod("builder-id");
+ } else if (method === "idc") {
+ // Use device code flow with IDC config
+ setAuthMethod("idc");
+ setIdcConfig(config);
+ } else if (method === "social") {
+ // Use social login with manual callback
+ setAuthMethod("social");
+ setSocialProvider(config.provider);
+ } else if (method === "import") {
+ // Import handled in KiroAuthModal, just close
+ onSuccess?.();
+ }
+ }, [onSuccess]);
+
+ const handleBack = () => {
+ setAuthMethod(null);
+ setSocialProvider(null);
+ setIdcConfig(null);
+ };
+
+ const handleSocialSuccess = () => {
+ setAuthMethod(null);
+ setSocialProvider(null);
+ onSuccess?.();
+ };
+
+ const handleDeviceSuccess = () => {
+ setAuthMethod(null);
+ setIdcConfig(null);
+ onSuccess?.();
+ };
+
+ // Show method selection first
+ if (!authMethod) {
+ return (
+
+ );
+ }
+
+ // Show device code flow (Builder ID or IDC)
+ if (authMethod === "builder-id" || authMethod === "idc") {
+ return (
+
+ );
+ }
+
+ // Show social login flow (Google/GitHub with manual callback)
+ if (authMethod === "social" && socialProvider) {
+ return (
+
+ );
+ }
+
+ return null;
+}
+
+KiroOAuthWrapper.propTypes = {
+ isOpen: PropTypes.bool.isRequired,
+ providerInfo: PropTypes.shape({
+ name: PropTypes.string,
+ }),
+ onSuccess: PropTypes.func,
+ onClose: PropTypes.func.isRequired,
+};
diff --git a/src/shared/components/KiroSocialOAuthModal.js b/src/shared/components/KiroSocialOAuthModal.js
new file mode 100644
index 0000000..323d4b1
--- /dev/null
+++ b/src/shared/components/KiroSocialOAuthModal.js
@@ -0,0 +1,205 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import PropTypes from "prop-types";
+import { Modal, Button, Input } from "@/shared/components";
+import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";
+
+/**
+ * Kiro Social OAuth Modal (Google/GitHub)
+ * Handles manual callback URL flow for social login
+ */
+export default function KiroSocialOAuthModal({ isOpen, provider, onSuccess, onClose }) {
+ const [step, setStep] = useState("loading"); // loading | input | success | error
+ const [authUrl, setAuthUrl] = useState("");
+ const [authData, setAuthData] = useState(null);
+ const [callbackUrl, setCallbackUrl] = useState("");
+ const [error, setError] = useState(null);
+ const { copied, copy } = useCopyToClipboard();
+
+ // Initialize auth flow
+ useEffect(() => {
+ if (!isOpen || !provider) return;
+
+ const initAuth = async () => {
+ try {
+ setError(null);
+ setStep("loading");
+
+ const res = await fetch(`/api/oauth/kiro/social-authorize?provider=${provider}`);
+ const data = await res.json();
+
+ if (!res.ok) {
+ throw new Error(data.error);
+ }
+
+ setAuthData(data);
+ setAuthUrl(data.authUrl);
+ setStep("input");
+
+ // Auto-open browser
+ window.open(data.authUrl, "_blank");
+ } catch (err) {
+ setError(err.message);
+ setStep("error");
+ }
+ };
+
+ initAuth();
+ }, [isOpen, provider]);
+
+ const handleManualSubmit = async () => {
+ try {
+ setError(null);
+
+ // Parse callback URL - can be either kiro:// or http://localhost format
+ let url;
+ try {
+ url = new URL(callbackUrl);
+ } catch (e) {
+ // If URL parsing fails, might be malformed
+ throw new Error("Invalid callback URL format");
+ }
+
+ const code = url.searchParams.get("code");
+ const state = url.searchParams.get("state");
+ const errorParam = url.searchParams.get("error");
+
+ if (errorParam) {
+ throw new Error(url.searchParams.get("error_description") || errorParam);
+ }
+
+ if (!code) {
+ throw new Error("No authorization code found in URL");
+ }
+
+ // Exchange code for tokens
+ const res = await fetch("/api/oauth/kiro/social-exchange", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ code,
+ codeVerifier: authData.codeVerifier,
+ provider,
+ }),
+ });
+
+ const data = await res.json();
+ if (!res.ok) throw new Error(data.error);
+
+ setStep("success");
+ onSuccess?.();
+ } catch (err) {
+ setError(err.message);
+ setStep("error");
+ }
+ };
+
+ const providerName = provider === "google" ? "Google" : "GitHub";
+
+ return (
+
+
+ {/* Loading */}
+ {step === "loading" && (
+
+
+
+ progress_activity
+
+
+
Initializing...
+
+ Setting up {providerName} authentication
+
+
+ )}
+
+ {/* Manual Input Step */}
+ {step === "input" && (
+ <>
+
+
+
Step 1: Open this URL in your browser
+
+
+
+
+
+
+
+
Step 2: Paste the callback URL here
+
+ After authorization, copy the full URL from your browser address bar.
+
+
setCallbackUrl(e.target.value)}
+ placeholder="kiro://kiro.kiroAgent/authenticate-success?code=..."
+ className="font-mono text-xs"
+ />
+
+
+
+
+
+
+
+ >
+ )}
+
+ {/* Success */}
+ {step === "success" && (
+
+
+ check_circle
+
+
Connected Successfully!
+
+ Your Kiro account via {providerName} has been connected.
+
+
+
+ )}
+
+ {/* Error */}
+ {step === "error" && (
+
+
+ error
+
+
Connection Failed
+
{error}
+
+
+
+
+
+ )}
+
+
+ );
+}
+
+KiroSocialOAuthModal.propTypes = {
+ isOpen: PropTypes.bool.isRequired,
+ provider: PropTypes.oneOf(["google", "github"]).isRequired,
+ onSuccess: PropTypes.func,
+ onClose: PropTypes.func.isRequired,
+};
diff --git a/src/shared/components/index.js b/src/shared/components/index.js
index cf98333..99d674b 100644
--- a/src/shared/components/index.js
+++ b/src/shared/components/index.js
@@ -18,7 +18,11 @@ export { default as ModelSelectModal } from "./ModelSelectModal";
export { default as ManualConfigModal } from "./ManualConfigModal";
export { default as UsageStats } from "./UsageStats";
export { default as RequestLogger } from "./RequestLogger";
+export { default as KiroAuthModal } from "./KiroAuthModal";
+export { default as KiroOAuthWrapper } from "./KiroOAuthWrapper";
+export { default as KiroSocialOAuthModal } from "./KiroSocialOAuthModal";
// Layouts
export * from "./layouts";
+