mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-02 18:18:43 +00:00
fix(cliproxy): harden kiro device-code auth flow consistency
This commit is contained in:
@@ -15,19 +15,19 @@ import { AccountInfo } from '../account-manager';
|
|||||||
* - Gemini: Authorization Code Flow with local callback server on port 8085
|
* - Gemini: Authorization Code Flow with local callback server on port 8085
|
||||||
* - Codex: Authorization Code Flow with local callback server on port 1455
|
* - Codex: Authorization Code Flow with local callback server on port 1455
|
||||||
* - Agy: Authorization Code Flow with local callback server on port 51121
|
* - Agy: Authorization Code Flow with local callback server on port 51121
|
||||||
* - Kiro: Authorization Code Flow with local callback server on port 9876
|
|
||||||
* - iFlow: Authorization Code Flow with local callback server on port 11451
|
* - iFlow: Authorization Code Flow with local callback server on port 11451
|
||||||
* - Claude: Authorization Code Flow with local callback server on port 54545 (Anthropic OAuth)
|
* - Claude: Authorization Code Flow with local callback server on port 54545 (Anthropic OAuth)
|
||||||
|
* - Kiro: Device Code Flow (polling-based, NO callback port needed)
|
||||||
* - Qwen: Device Code Flow (polling-based, NO callback port needed)
|
* - Qwen: Device Code Flow (polling-based, NO callback port needed)
|
||||||
* - GHCP: Device Code Flow (polling-based, NO callback port needed)
|
* - GHCP: Device Code Flow (polling-based, NO callback port needed)
|
||||||
*/
|
*/
|
||||||
export const OAUTH_CALLBACK_PORTS: Partial<Record<CLIProxyProvider, number>> = {
|
export const OAUTH_CALLBACK_PORTS: Partial<Record<CLIProxyProvider, number>> = {
|
||||||
gemini: 8085,
|
gemini: 8085,
|
||||||
kiro: 9876,
|
|
||||||
codex: 1455,
|
codex: 1455,
|
||||||
agy: 51121,
|
agy: 51121,
|
||||||
iflow: 11451,
|
iflow: 11451,
|
||||||
claude: 54545,
|
claude: 54545,
|
||||||
|
// kiro: Device Code Flow - no callback port
|
||||||
// qwen: Device Code Flow - no callback port
|
// qwen: Device Code Flow - no callback port
|
||||||
// ghcp: Device Code Flow - no callback port
|
// ghcp: Device Code Flow - no callback port
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
* - Codex: 1455
|
* - Codex: 1455
|
||||||
* - Agy: 51121
|
* - Agy: 51121
|
||||||
* - iFlow: 11451
|
* - iFlow: 11451
|
||||||
* - Kiro: 9876
|
* - Kiro: Device Code Flow (no port needed)
|
||||||
* - Claude: 54545
|
* - Claude: 54545
|
||||||
* - Qwen: Device Code Flow (no port needed)
|
* - Qwen: Device Code Flow (no port needed)
|
||||||
* - GHCP: Device Code Flow (no port needed)
|
* - GHCP: Device Code Flow (no port needed)
|
||||||
@@ -26,40 +26,44 @@ import {
|
|||||||
} from '../utils/port-utils';
|
} from '../utils/port-utils';
|
||||||
import { CLIProxyProvider } from '../cliproxy/types';
|
import { CLIProxyProvider } from '../cliproxy/types';
|
||||||
import { CLIPROXY_PROFILES } from '../auth/profile-detector';
|
import { CLIPROXY_PROFILES } from '../auth/profile-detector';
|
||||||
|
import {
|
||||||
|
CLIPROXY_PROVIDER_IDS,
|
||||||
|
getOAuthCallbackPort,
|
||||||
|
getOAuthFlowType,
|
||||||
|
type OAuthFlowType as ProviderOAuthFlowType,
|
||||||
|
} from '../cliproxy/provider-capabilities';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* OAuth callback ports for each provider
|
* Build provider-indexed records from canonical provider capabilities.
|
||||||
* Extracted from CLIProxyAPI source
|
* Keeps diagnostics in sync with runtime OAuth flow metadata.
|
||||||
*/
|
*/
|
||||||
export const OAUTH_CALLBACK_PORTS: Record<CLIProxyProvider, number | null> = {
|
function buildProviderMap<T>(
|
||||||
gemini: 8085,
|
valueFor: (provider: CLIProxyProvider) => T
|
||||||
codex: 1455,
|
): Record<CLIProxyProvider, T> {
|
||||||
agy: 51121,
|
return CLIPROXY_PROVIDER_IDS.reduce(
|
||||||
qwen: null, // Device Code Flow - no callback port
|
(acc, provider) => {
|
||||||
iflow: 11451, // Authorization Code Flow
|
acc[provider] = valueFor(provider);
|
||||||
kiro: 9876, // Authorization Code Flow
|
return acc;
|
||||||
ghcp: null, // Device Code Flow - no callback port
|
},
|
||||||
claude: 54545, // Authorization Code Flow (Anthropic OAuth)
|
{} as Record<CLIProxyProvider, T>
|
||||||
};
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const OAUTH_CALLBACK_PORTS: Record<CLIProxyProvider, number | null> = buildProviderMap(
|
||||||
|
(provider) => getOAuthCallbackPort(provider)
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* OAuth flow types
|
* OAuth flow types
|
||||||
*/
|
*/
|
||||||
export type OAuthFlowType = 'authorization_code' | 'device_code';
|
export type OAuthFlowType = ProviderOAuthFlowType;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* OAuth flow type per provider
|
* OAuth flow type per provider
|
||||||
*/
|
*/
|
||||||
export const OAUTH_FLOW_TYPES: Record<CLIProxyProvider, OAuthFlowType> = {
|
export const OAUTH_FLOW_TYPES: Record<CLIProxyProvider, OAuthFlowType> = buildProviderMap(
|
||||||
gemini: 'authorization_code',
|
(provider) => getOAuthFlowType(provider)
|
||||||
codex: 'authorization_code',
|
);
|
||||||
agy: 'authorization_code',
|
|
||||||
qwen: 'device_code',
|
|
||||||
iflow: 'authorization_code',
|
|
||||||
kiro: 'authorization_code',
|
|
||||||
ghcp: 'device_code',
|
|
||||||
claude: 'authorization_code',
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Port diagnostic result
|
* Port diagnostic result
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import {
|
|||||||
CLIPROXY_CALLBACK_PROVIDER_MAP,
|
CLIPROXY_CALLBACK_PROVIDER_MAP,
|
||||||
CLIPROXY_AUTH_URL_PROVIDER_MAP,
|
CLIPROXY_AUTH_URL_PROVIDER_MAP,
|
||||||
} from '../../cliproxy/auth/auth-types';
|
} from '../../cliproxy/auth/auth-types';
|
||||||
|
import { getOAuthFlowType } from '../../cliproxy/provider-capabilities';
|
||||||
import type { CLIProxyProvider } from '../../cliproxy/types';
|
import type { CLIProxyProvider } from '../../cliproxy/types';
|
||||||
import { CLIPROXY_PROFILES } from '../../auth/profile-detector';
|
import { CLIPROXY_PROFILES } from '../../auth/profile-detector';
|
||||||
|
|
||||||
@@ -46,6 +47,13 @@ const router = Router();
|
|||||||
// Valid providers list - derived from canonical CLIPROXY_PROFILES
|
// Valid providers list - derived from canonical CLIPROXY_PROFILES
|
||||||
const validProviders: CLIProxyProvider[] = [...CLIPROXY_PROFILES];
|
const validProviders: CLIProxyProvider[] = [...CLIPROXY_PROFILES];
|
||||||
|
|
||||||
|
export function getStartUrlUnsupportedReason(provider: CLIProxyProvider): string | null {
|
||||||
|
if (getOAuthFlowType(provider) === 'device_code') {
|
||||||
|
return `Provider '${provider}' uses Device Code flow. Use /api/cliproxy/auth/${provider}/start instead.`;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /api/cliproxy/auth - Get auth status for built-in CLIProxy profiles
|
* GET /api/cliproxy/auth - Get auth status for built-in CLIProxy profiles
|
||||||
* Also fetches CLIProxyAPI stats to update lastUsedAt for active providers
|
* Also fetches CLIProxyAPI stats to update lastUsedAt for active providers
|
||||||
@@ -557,6 +565,11 @@ router.post('/:provider/start-url', async (req: Request, res: Response): Promise
|
|||||||
res.status(400).json({ error: `Invalid provider: ${provider}` });
|
res.status(400).json({ error: `Invalid provider: ${provider}` });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const unsupportedReason = getStartUrlUnsupportedReason(provider as CLIProxyProvider);
|
||||||
|
if (unsupportedReason) {
|
||||||
|
res.status(400).json({ error: unsupportedReason });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authUrlProvider =
|
const authUrlProvider =
|
||||||
|
|||||||
@@ -2,11 +2,17 @@ import { describe, expect, it } from 'bun:test';
|
|||||||
import {
|
import {
|
||||||
CLIPROXY_PROVIDER_IDS,
|
CLIPROXY_PROVIDER_IDS,
|
||||||
getOAuthCallbackPort,
|
getOAuthCallbackPort,
|
||||||
|
getOAuthFlowType,
|
||||||
getProviderDisplayName,
|
getProviderDisplayName,
|
||||||
getProvidersByOAuthFlow,
|
getProvidersByOAuthFlow,
|
||||||
isCLIProxyProvider,
|
isCLIProxyProvider,
|
||||||
mapExternalProviderName,
|
mapExternalProviderName,
|
||||||
} from '../../../src/cliproxy/provider-capabilities';
|
} from '../../../src/cliproxy/provider-capabilities';
|
||||||
|
import {
|
||||||
|
OAUTH_CALLBACK_PORTS as DIAGNOSTIC_CALLBACK_PORTS,
|
||||||
|
OAUTH_FLOW_TYPES,
|
||||||
|
} from '../../../src/management/oauth-port-diagnostics';
|
||||||
|
import { OAUTH_CALLBACK_PORTS as AUTH_CALLBACK_PORTS } from '../../../src/cliproxy/auth/auth-types';
|
||||||
|
|
||||||
describe('provider-capabilities', () => {
|
describe('provider-capabilities', () => {
|
||||||
it('keeps canonical provider IDs backward-compatible', () => {
|
it('keeps canonical provider IDs backward-compatible', () => {
|
||||||
@@ -56,4 +62,17 @@ describe('provider-capabilities', () => {
|
|||||||
expect(getOAuthCallbackPort('gemini')).toBe(8085);
|
expect(getOAuthCallbackPort('gemini')).toBe(8085);
|
||||||
expect(getProviderDisplayName('agy')).toBe('AntiGravity');
|
expect(getProviderDisplayName('agy')).toBe('AntiGravity');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('keeps diagnostics flow metadata in sync with provider capabilities', () => {
|
||||||
|
for (const provider of CLIPROXY_PROVIDER_IDS) {
|
||||||
|
expect(OAUTH_FLOW_TYPES[provider]).toBe(getOAuthFlowType(provider));
|
||||||
|
expect(DIAGNOSTIC_CALLBACK_PORTS[provider]).toBe(getOAuthCallbackPort(provider));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not define callback ports for device code providers in auth constants', () => {
|
||||||
|
for (const provider of getProvidersByOAuthFlow('device_code')) {
|
||||||
|
expect(AUTH_CALLBACK_PORTS[provider]).toBeUndefined();
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { describe, expect, it } from 'bun:test';
|
||||||
|
import { getStartUrlUnsupportedReason } from '../../../src/web-server/routes/cliproxy-auth-routes';
|
||||||
|
|
||||||
|
describe('cliproxy-auth-routes start-url guard', () => {
|
||||||
|
it('rejects device code providers', () => {
|
||||||
|
expect(getStartUrlUnsupportedReason('kiro')).toContain("Provider 'kiro' uses Device Code flow");
|
||||||
|
expect(getStartUrlUnsupportedReason('ghcp')).toContain("Provider 'ghcp' uses Device Code flow");
|
||||||
|
expect(getStartUrlUnsupportedReason('qwen')).toContain("Provider 'qwen' uses Device Code flow");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows authorization code providers', () => {
|
||||||
|
expect(getStartUrlUnsupportedReason('gemini')).toBeNull();
|
||||||
|
expect(getStartUrlUnsupportedReason('codex')).toBeNull();
|
||||||
|
expect(getStartUrlUnsupportedReason('claude')).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Add Account Dialog Component
|
* Add Account Dialog Component
|
||||||
* Uses /start-url to get OAuth URL + polls for completion via management API.
|
* Uses /start-url to get OAuth URL + polls for completion via management API.
|
||||||
* For Device Code flows (ghcp, qwen): Uses /start endpoint which spawns CLIProxy
|
* For Device Code flows (ghcp, qwen, kiro): Uses /start endpoint which spawns CLIProxy
|
||||||
* binary and emits WebSocket events. DeviceCodeDialog handles user code display.
|
* binary and emits WebSocket events. DeviceCodeDialog handles user code display.
|
||||||
* Shows auth URL + callback paste field. Polling auto-closes on success.
|
* Shows auth URL + callback paste field. Polling auto-closes on success.
|
||||||
* For Kiro: Also shows "Import from IDE" option.
|
* For Kiro: Also shows "Import from IDE" option.
|
||||||
@@ -22,7 +22,7 @@ import { Loader2, ExternalLink, User, Download, Copy, Check } from 'lucide-react
|
|||||||
import { useKiroImport } from '@/hooks/use-cliproxy';
|
import { useKiroImport } from '@/hooks/use-cliproxy';
|
||||||
import { useCliproxyAuthFlow } from '@/hooks/use-cliproxy-auth-flow';
|
import { useCliproxyAuthFlow } from '@/hooks/use-cliproxy-auth-flow';
|
||||||
import { applyDefaultPreset } from '@/lib/preset-utils';
|
import { applyDefaultPreset } from '@/lib/preset-utils';
|
||||||
import { isDeviceCodeProvider } from '@/lib/provider-config';
|
import { isDeviceCodeProvider, isNicknameRequiredProvider } from '@/lib/provider-config';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
interface AddAccountDialogProps {
|
interface AddAccountDialogProps {
|
||||||
@@ -44,18 +44,23 @@ export function AddAccountDialog({
|
|||||||
const [nickname, setNickname] = useState('');
|
const [nickname, setNickname] = useState('');
|
||||||
const [callbackUrl, setCallbackUrl] = useState('');
|
const [callbackUrl, setCallbackUrl] = useState('');
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
|
const [localError, setLocalError] = useState<string | null>(null);
|
||||||
const wasAuthenticatingRef = useRef(false);
|
const wasAuthenticatingRef = useRef(false);
|
||||||
const authFlow = useCliproxyAuthFlow();
|
const authFlow = useCliproxyAuthFlow();
|
||||||
const kiroImportMutation = useKiroImport();
|
const kiroImportMutation = useKiroImport();
|
||||||
|
|
||||||
const isKiro = provider === 'kiro';
|
const isKiro = provider === 'kiro';
|
||||||
const isDeviceCode = isDeviceCodeProvider(provider);
|
const isDeviceCode = isDeviceCodeProvider(provider);
|
||||||
|
const requiresNickname = isNicknameRequiredProvider(provider);
|
||||||
const isPending = authFlow.isAuthenticating || kiroImportMutation.isPending;
|
const isPending = authFlow.isAuthenticating || kiroImportMutation.isPending;
|
||||||
|
const nicknameTrimmed = nickname.trim();
|
||||||
|
const errorMessage = localError || authFlow.error;
|
||||||
|
|
||||||
const resetAndClose = () => {
|
const resetAndClose = () => {
|
||||||
setNickname('');
|
setNickname('');
|
||||||
setCallbackUrl('');
|
setCallbackUrl('');
|
||||||
setCopied(false);
|
setCopied(false);
|
||||||
|
setLocalError(null);
|
||||||
wasAuthenticatingRef.current = false;
|
wasAuthenticatingRef.current = false;
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
@@ -103,13 +108,18 @@ export function AddAccountDialog({
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Authenticate via /start-url + polling only.
|
* Start auth flow using provider capabilities.
|
||||||
* Does NOT call /start (which spawns a local CLIProxy binary that kills running instances).
|
* - Device code providers use /start and rely on WebSocket events for code display.
|
||||||
* /start-url uses the management API to get auth URL, then polls for completion.
|
* - Authorization code providers use /start-url and polling.
|
||||||
*/
|
*/
|
||||||
const handleAuthenticate = () => {
|
const handleAuthenticate = () => {
|
||||||
|
if (requiresNickname && !nicknameTrimmed) {
|
||||||
|
setLocalError(`Nickname is required for ${displayName} accounts.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setLocalError(null);
|
||||||
wasAuthenticatingRef.current = true;
|
wasAuthenticatingRef.current = true;
|
||||||
authFlow.startAuth(provider, { nickname: nickname.trim() || undefined });
|
authFlow.startAuth(provider, { nickname: nicknameTrimmed || undefined });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleKiroImport = () => {
|
const handleKiroImport = () => {
|
||||||
@@ -157,20 +167,27 @@ export function AddAccountDialog({
|
|||||||
{/* Nickname input - only show before auth starts */}
|
{/* Nickname input - only show before auth starts */}
|
||||||
{!showAuthUI && (
|
{!showAuthUI && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="nickname">Nickname (optional)</Label>
|
<Label htmlFor="nickname">
|
||||||
|
{requiresNickname ? 'Nickname (required)' : 'Nickname (optional)'}
|
||||||
|
</Label>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<User className="w-4 h-4 text-muted-foreground" />
|
<User className="w-4 h-4 text-muted-foreground" />
|
||||||
<Input
|
<Input
|
||||||
id="nickname"
|
id="nickname"
|
||||||
value={nickname}
|
value={nickname}
|
||||||
onChange={(e) => setNickname(e.target.value)}
|
onChange={(e) => {
|
||||||
|
setNickname(e.target.value);
|
||||||
|
setLocalError(null);
|
||||||
|
}}
|
||||||
placeholder="e.g., work, personal"
|
placeholder="e.g., work, personal"
|
||||||
disabled={isPending}
|
disabled={isPending}
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
A friendly name to identify this account. Auto-generated from email if left empty.
|
{requiresNickname
|
||||||
|
? 'Required for this provider. Use a unique friendly name (e.g., work, personal).'
|
||||||
|
: 'A friendly name to identify this account. Auto-generated from email if left empty.'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -191,11 +208,6 @@ export function AddAccountDialog({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Error display */}
|
|
||||||
{authFlow.error && !authFlow.authUrl && (
|
|
||||||
<p className="text-xs text-center text-destructive">{authFlow.error}</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Auth URL section - only for Authorization Code flows, NOT Device Code */}
|
{/* Auth URL section - only for Authorization Code flows, NOT Device Code */}
|
||||||
{authFlow.authUrl && !authFlow.isDeviceCodeFlow && (
|
{authFlow.authUrl && !authFlow.isDeviceCodeFlow && (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
@@ -273,6 +285,9 @@ export function AddAccountDialog({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Persist error visibility outside auth-only UI states */}
|
||||||
|
{errorMessage && <p className="text-xs text-center text-destructive">{errorMessage}</p>}
|
||||||
|
|
||||||
{/* Kiro import loading */}
|
{/* Kiro import loading */}
|
||||||
{kiroImportMutation.isPending && (
|
{kiroImportMutation.isPending && (
|
||||||
<p className="text-sm text-center text-muted-foreground">
|
<p className="text-sm text-center text-muted-foreground">
|
||||||
@@ -302,7 +317,10 @@ export function AddAccountDialog({
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{!showAuthUI && (
|
{!showAuthUI && (
|
||||||
<Button onClick={handleAuthenticate} disabled={isPending}>
|
<Button
|
||||||
|
onClick={handleAuthenticate}
|
||||||
|
disabled={isPending || (requiresNickname && !nicknameTrimmed)}
|
||||||
|
>
|
||||||
<ExternalLink className="w-4 h-4 mr-2" />
|
<ExternalLink className="w-4 h-4 mr-2" />
|
||||||
Authenticate
|
Authenticate
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ interface AuthFlowState {
|
|||||||
oauthState: string | null;
|
oauthState: string | null;
|
||||||
/** Whether callback is being submitted */
|
/** Whether callback is being submitted */
|
||||||
isSubmittingCallback: boolean;
|
isSubmittingCallback: boolean;
|
||||||
/** Whether this is a device code flow (ghcp, qwen) - dialog handled separately via WebSocket */
|
/** Whether this is a device code flow (ghcp, qwen, kiro) - dialog handled separately via WebSocket */
|
||||||
isDeviceCodeFlow: boolean;
|
isDeviceCodeFlow: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
*
|
*
|
||||||
* Listens for WebSocket device code events and manages dialog state.
|
* Listens for WebSocket device code events and manages dialog state.
|
||||||
* Similar to useProjectSelection but for Device Code OAuth flows
|
* Similar to useProjectSelection but for Device Code OAuth flows
|
||||||
* (GitHub Copilot, Qwen, etc.)
|
* (GitHub Copilot, Qwen, Kiro, etc.)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState, useEffect, useCallback, useMemo } from 'react';
|
import { useState, useEffect, useCallback, useMemo } from 'react';
|
||||||
@@ -26,6 +26,7 @@ interface DeviceCodeState {
|
|||||||
/** Provider display names for user-friendly messages */
|
/** Provider display names for user-friendly messages */
|
||||||
const PROVIDER_DISPLAY_NAMES: Record<string, string> = {
|
const PROVIDER_DISPLAY_NAMES: Record<string, string> = {
|
||||||
ghcp: 'GitHub Copilot',
|
ghcp: 'GitHub Copilot',
|
||||||
|
kiro: 'Kiro (AWS)',
|
||||||
qwen: 'Qwen Code',
|
qwen: 'Qwen Code',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -82,3 +82,11 @@ export const DEVICE_CODE_PROVIDERS: CLIProxyProvider[] = ['ghcp', 'kiro', 'qwen'
|
|||||||
export function isDeviceCodeProvider(provider: string): boolean {
|
export function isDeviceCodeProvider(provider: string): boolean {
|
||||||
return DEVICE_CODE_PROVIDERS.includes(provider as CLIProxyProvider);
|
return DEVICE_CODE_PROVIDERS.includes(provider as CLIProxyProvider);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Providers that require nickname because token payload may not include email. */
|
||||||
|
export const NICKNAME_REQUIRED_PROVIDERS: CLIProxyProvider[] = ['ghcp', 'kiro'];
|
||||||
|
|
||||||
|
/** Check if provider requires user-supplied nickname in auth flow */
|
||||||
|
export function isNicknameRequiredProvider(provider: string): boolean {
|
||||||
|
return NICKNAME_REQUIRED_PROVIDERS.includes(provider as CLIProxyProvider);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user