mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-03 00:17:47 +00:00
@@ -1,6 +1,6 @@
|
|||||||
# Provider Integration Flows
|
# Provider Integration Flows
|
||||||
|
|
||||||
Last Updated: 2026-03-30
|
Last Updated: 2026-05-07
|
||||||
|
|
||||||
Detailed provider integration flows including CLIProxyAPI, legacy GLMT compatibility transforms, remote CLIProxy, quota management, and authentication.
|
Detailed provider integration flows including CLIProxyAPI, legacy GLMT compatibility transforms, remote CLIProxy, quota management, and authentication.
|
||||||
|
|
||||||
@@ -44,6 +44,11 @@ Generated local CLIProxy configs also keep the management dashboard aligned with
|
|||||||
| - User enters code at github.com/login/device
|
| - User enters code at github.com/login/device
|
||||||
| - Polls for token completion
|
| - Polls for token completion
|
||||||
| |
|
| |
|
||||||
|
| +---> Browser URL Polling (no callback port)
|
||||||
|
| - Cursor
|
||||||
|
| - Opens provider login URL returned by CLIProxyAPIPlus
|
||||||
|
| - Polls auth state until token is saved
|
||||||
|
| |
|
||||||
| v
|
| v
|
||||||
| +------------------+
|
| +------------------+
|
||||||
| | OAuth Server | Browser-based auth
|
| | OAuth Server | Browser-based auth
|
||||||
@@ -81,6 +86,7 @@ Generated local CLIProxy configs also keep the management dashboard aligned with
|
|||||||
| Antigravity | `agy` | Authorization Code | 9876 | CLIProxyAPI |
|
| Antigravity | `agy` | Authorization Code | 9876 | CLIProxyAPI |
|
||||||
| Kiro (AWS) | `kiro` | Method-aware (default: Device Code) | 9876 | CLIProxyAPIPlus fork |
|
| Kiro (AWS) | `kiro` | Method-aware (default: Device Code) | 9876 | CLIProxyAPIPlus fork |
|
||||||
| GitHub Copilot | `ghcp` | Device Code | none | CLIProxyAPIPlus fork |
|
| GitHub Copilot | `ghcp` | Device Code | none | CLIProxyAPIPlus fork |
|
||||||
|
| Cursor | `cursor` | Browser URL polling | none | CLIProxyAPIPlus fork |
|
||||||
|
|
||||||
### Codex Duplicate-Email Account Identity
|
### Codex Duplicate-Email Account Identity
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { describe, expect, it } from 'bun:test';
|
|||||||
import {
|
import {
|
||||||
buildProviderAliasMap,
|
buildProviderAliasMap,
|
||||||
CLIPROXY_PROVIDER_IDS,
|
CLIPROXY_PROVIDER_IDS,
|
||||||
|
getDeviceCodeVerificationProviders,
|
||||||
getOAuthCallbackPort,
|
getOAuthCallbackPort,
|
||||||
getOAuthFlowType,
|
getOAuthFlowType,
|
||||||
PROVIDER_CAPABILITIES,
|
PROVIDER_CAPABILITIES,
|
||||||
@@ -76,6 +77,17 @@ describe('provider-capabilities', () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('separates browser URL auth providers from verification-code device flows', () => {
|
||||||
|
expect(getDeviceCodeVerificationProviders()).toEqual([
|
||||||
|
'qwen',
|
||||||
|
'kiro',
|
||||||
|
'ghcp',
|
||||||
|
'kimi',
|
||||||
|
'codebuddy',
|
||||||
|
'kilo',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
it('maps external provider aliases to canonical IDs', () => {
|
it('maps external provider aliases to canonical IDs', () => {
|
||||||
expect(mapExternalProviderName('gemini-cli')).toBe('gemini');
|
expect(mapExternalProviderName('gemini-cli')).toBe('gemini');
|
||||||
expect(mapExternalProviderName('antigravity')).toBe('agy');
|
expect(mapExternalProviderName('antigravity')).toBe('agy');
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ import { CLIPROXY_DEFAULT_PORT as BACKEND_CLIPROXY_DEFAULT_PORT } from '../port-
|
|||||||
import { DEFAULT_CURSOR_PORT as BACKEND_CURSOR_DEFAULT_PORT } from '../../../cursor/cursor-models';
|
import { DEFAULT_CURSOR_PORT as BACKEND_CURSOR_DEFAULT_PORT } from '../../../cursor/cursor-models';
|
||||||
import {
|
import {
|
||||||
CLIPROXY_PROVIDER_IDS as BACKEND_CLIPROXY_PROVIDER_IDS,
|
CLIPROXY_PROVIDER_IDS as BACKEND_CLIPROXY_PROVIDER_IDS,
|
||||||
|
getDeviceCodeVerificationProviders,
|
||||||
getProviderDescription as getBackendProviderDescription,
|
getProviderDescription as getBackendProviderDescription,
|
||||||
getProviderDisplayName as getBackendProviderDisplayName,
|
getProviderDisplayName as getBackendProviderDisplayName,
|
||||||
getProvidersByOAuthFlow,
|
|
||||||
} from '../../provider-capabilities';
|
} from '../../provider-capabilities';
|
||||||
import {
|
import {
|
||||||
CLIPROXY_DEFAULT_PORT as UI_CLIPROXY_DEFAULT_PORT,
|
CLIPROXY_DEFAULT_PORT as UI_CLIPROXY_DEFAULT_PORT,
|
||||||
@@ -20,9 +20,9 @@ import {
|
|||||||
import {
|
import {
|
||||||
CLIPROXY_PROVIDERS as UI_CLIPROXY_PROVIDERS,
|
CLIPROXY_PROVIDERS as UI_CLIPROXY_PROVIDERS,
|
||||||
CORE_CLIPROXY_PROVIDERS as UI_CORE_CLIPROXY_PROVIDERS,
|
CORE_CLIPROXY_PROVIDERS as UI_CORE_CLIPROXY_PROVIDERS,
|
||||||
DEVICE_CODE_PROVIDERS as UI_DEVICE_CODE_PROVIDERS,
|
|
||||||
PLUS_EXTRA_CLIPROXY_PROVIDERS as UI_PLUS_EXTRA_CLIPROXY_PROVIDERS,
|
PLUS_EXTRA_CLIPROXY_PROVIDERS as UI_PLUS_EXTRA_CLIPROXY_PROVIDERS,
|
||||||
PROVIDER_METADATA as UI_PROVIDER_METADATA,
|
PROVIDER_METADATA as UI_PROVIDER_METADATA,
|
||||||
|
VERIFICATION_CODE_AUTH_PROVIDERS as UI_VERIFICATION_CODE_AUTH_PROVIDERS,
|
||||||
} from '../../../../ui/src/lib/provider-config';
|
} from '../../../../ui/src/lib/provider-config';
|
||||||
import { PLUS_ONLY_PROVIDERS as BACKEND_PLUS_ONLY_PROVIDERS } from '../../types/index';
|
import { PLUS_ONLY_PROVIDERS as BACKEND_PLUS_ONLY_PROVIDERS } from '../../types/index';
|
||||||
|
|
||||||
@@ -43,9 +43,9 @@ describe('Default Port Sync', () => {
|
|||||||
expect(sorted(UI_CLIPROXY_PROVIDERS)).toEqual(sorted(BACKEND_CLIPROXY_PROVIDER_IDS));
|
expect(sorted(UI_CLIPROXY_PROVIDERS)).toEqual(sorted(BACKEND_CLIPROXY_PROVIDER_IDS));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Device code providers are synced between backend and UI', () => {
|
test('verification-code auth providers are synced between backend and UI', () => {
|
||||||
expect(sorted(UI_DEVICE_CODE_PROVIDERS)).toEqual(
|
expect(sorted(UI_VERIFICATION_CODE_AUTH_PROVIDERS)).toEqual(
|
||||||
sorted(getProvidersByOAuthFlow('device_code'))
|
sorted(getDeviceCodeVerificationProviders())
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -188,6 +188,12 @@ export const CLIPROXY_PROVIDER_IDS = Object.freeze(
|
|||||||
Object.keys(PROVIDER_CAPABILITIES) as CLIProxyProvider[]
|
Object.keys(PROVIDER_CAPABILITIES) as CLIProxyProvider[]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const BROWSER_URL_AUTH_PROVIDER_IDS = Object.freeze([
|
||||||
|
'cursor',
|
||||||
|
] as const satisfies readonly CLIProxyProvider[]);
|
||||||
|
|
||||||
|
const BROWSER_URL_AUTH_PROVIDER_SET = new Set<CLIProxyProvider>(BROWSER_URL_AUTH_PROVIDER_IDS);
|
||||||
|
|
||||||
/** Providers currently supported by quota status fetchers. */
|
/** Providers currently supported by quota status fetchers. */
|
||||||
export const QUOTA_SUPPORTED_PROVIDER_IDS = Object.freeze([
|
export const QUOTA_SUPPORTED_PROVIDER_IDS = Object.freeze([
|
||||||
'agy',
|
'agy',
|
||||||
@@ -278,6 +284,16 @@ export function getProvidersByOAuthFlow(flowType: OAuthFlowType): CLIProxyProvid
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isBrowserUrlAuthProvider(provider: CLIProxyProvider): boolean {
|
||||||
|
return BROWSER_URL_AUTH_PROVIDER_SET.has(provider);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDeviceCodeVerificationProviders(): CLIProxyProvider[] {
|
||||||
|
return getProvidersByOAuthFlow('device_code').filter(
|
||||||
|
(provider) => !isBrowserUrlAuthProvider(provider)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function getOAuthFlowType(provider: CLIProxyProvider): OAuthFlowType {
|
export function getOAuthFlowType(provider: CLIProxyProvider): OAuthFlowType {
|
||||||
return PROVIDER_CAPABILITIES[provider].oauthFlow;
|
return PROVIDER_CAPABILITIES[provider].oauthFlow;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,7 +56,11 @@ import {
|
|||||||
normalizeKiroAuthMethod,
|
normalizeKiroAuthMethod,
|
||||||
toKiroManagementMethod,
|
toKiroManagementMethod,
|
||||||
} from '../../cliproxy/auth/auth-types';
|
} from '../../cliproxy/auth/auth-types';
|
||||||
import { getOAuthFlowType, mapExternalProviderName } from '../../cliproxy/provider-capabilities';
|
import {
|
||||||
|
getOAuthFlowType,
|
||||||
|
isBrowserUrlAuthProvider,
|
||||||
|
mapExternalProviderName,
|
||||||
|
} 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';
|
||||||
import {
|
import {
|
||||||
@@ -286,6 +290,10 @@ export function getStartUrlUnsupportedReason(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isBrowserUrlAuthProvider(provider)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (getOAuthFlowType(provider) === 'device_code') {
|
if (getOAuthFlowType(provider) === 'device_code') {
|
||||||
return `Provider '${provider}' uses Device Code flow. Use /api/cliproxy/auth/${provider}/start instead.`;
|
return `Provider '${provider}' uses Device Code flow. Use /api/cliproxy/auth/${provider}/start instead.`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { describe, expect, it } from 'bun:test';
|
|||||||
import {
|
import {
|
||||||
getDeviceCodeProviderInstruction,
|
getDeviceCodeProviderInstruction,
|
||||||
getProviderDisplayName,
|
getProviderDisplayName,
|
||||||
|
isDeviceCodeProvider,
|
||||||
} from '../../../ui/src/lib/provider-config';
|
} from '../../../ui/src/lib/provider-config';
|
||||||
|
|
||||||
describe('provider-config fallbacks', () => {
|
describe('provider-config fallbacks', () => {
|
||||||
@@ -15,4 +16,10 @@ describe('provider-config fallbacks', () => {
|
|||||||
'Complete the authorization in your browser.'
|
'Complete the authorization in your browser.'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not classify Cursor browser URL auth as a verification-code device flow', () => {
|
||||||
|
expect(isDeviceCodeProvider('cursor')).toBe(false);
|
||||||
|
expect(isDeviceCodeProvider('ghcp')).toBe(true);
|
||||||
|
expect(isDeviceCodeProvider('qwen')).toBe(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
getCachedQuota,
|
getCachedQuota,
|
||||||
setCachedQuota,
|
setCachedQuota,
|
||||||
} from '../../../src/cliproxy/quota/quota-response-cache';
|
} from '../../../src/cliproxy/quota/quota-response-cache';
|
||||||
import { restoreFetch, mockFetch } from '../../mocks';
|
import { restoreFetch, mockFetch, getCapturedFetchRequests } from '../../mocks';
|
||||||
|
|
||||||
describe('cliproxy-auth-routes manual callback nickname persistence', () => {
|
describe('cliproxy-auth-routes manual callback nickname persistence', () => {
|
||||||
let server: Server;
|
let server: Server;
|
||||||
@@ -179,6 +179,34 @@ describe('cliproxy-auth-routes manual callback nickname persistence', () => {
|
|||||||
expect(registry.providers.kiro.accounts['github-ABC123']?.nickname).toBe('work');
|
expect(registry.providers.kiro.accounts['github-ABC123']?.nickname).toBe('work');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('starts Cursor auth through the browser URL management endpoint', async () => {
|
||||||
|
mockFetch([
|
||||||
|
{
|
||||||
|
url: /\/v0\/management\/cursor-auth-url\?is_webui=true$/,
|
||||||
|
response: {
|
||||||
|
url: 'https://cursor.com/loginDeepControl?state=cursor-state',
|
||||||
|
state: 'cursor-state',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const startResponse = await postJson('/api/cliproxy/auth/cursor/start-url', {
|
||||||
|
nickname: 'work',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(startResponse.status).toBe(200);
|
||||||
|
expect(startResponse.body).toEqual({
|
||||||
|
success: true,
|
||||||
|
authUrl: 'https://cursor.com/loginDeepControl?state=cursor-state',
|
||||||
|
state: 'cursor-state',
|
||||||
|
method: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const requests = getCapturedFetchRequests();
|
||||||
|
expect(requests).toHaveLength(1);
|
||||||
|
expect(requests[0]?.url).toContain('/v0/management/cursor-auth-url?is_webui=true');
|
||||||
|
});
|
||||||
|
|
||||||
it('returns wait after callback submission when the local token is not yet available', async () => {
|
it('returns wait after callback submission when the local token is not yet available', async () => {
|
||||||
mockFetch([
|
mockFetch([
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -13,15 +13,14 @@ describe('cliproxy-auth-routes start-url guard', () => {
|
|||||||
);
|
);
|
||||||
expect(getStartUrlUnsupportedReason('ghcp')).toContain("Provider 'ghcp' uses Device Code flow");
|
expect(getStartUrlUnsupportedReason('ghcp')).toContain("Provider 'ghcp' uses Device Code flow");
|
||||||
expect(getStartUrlUnsupportedReason('qwen')).toContain("Provider 'qwen' uses Device Code flow");
|
expect(getStartUrlUnsupportedReason('qwen')).toContain("Provider 'qwen' uses Device Code flow");
|
||||||
expect(getStartUrlUnsupportedReason('cursor')).toContain(
|
|
||||||
"Provider 'cursor' uses Device Code flow"
|
|
||||||
);
|
|
||||||
expect(getStartUrlUnsupportedReason('codebuddy')).toContain(
|
expect(getStartUrlUnsupportedReason('codebuddy')).toContain(
|
||||||
"Provider 'codebuddy' uses Device Code flow"
|
"Provider 'codebuddy' uses Device Code flow"
|
||||||
);
|
);
|
||||||
expect(getStartUrlUnsupportedReason('kilo')).toContain(
|
expect(getStartUrlUnsupportedReason('kilo')).toContain("Provider 'kilo' uses Device Code flow");
|
||||||
"Provider 'kilo' uses Device Code flow"
|
});
|
||||||
);
|
|
||||||
|
it('allows Cursor browser URL auth on start-url', () => {
|
||||||
|
expect(getStartUrlUnsupportedReason('cursor')).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('allows Kiro social methods on start-url', () => {
|
it('allows Kiro social methods on start-url', () => {
|
||||||
@@ -138,6 +137,8 @@ describe('cliproxy-auth-routes nickname validation', () => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
expect(getStartAuthNicknameError('kiro', 'work', existingAccounts, 'github-ABC123')).toBeNull();
|
expect(getStartAuthNicknameError('kiro', 'work', existingAccounts, 'github-ABC123')).toBeNull();
|
||||||
expect(getStartAuthNicknameError('kiro', 'github-ABC123', existingAccounts, 'github-ABC123')).toBeNull();
|
expect(
|
||||||
|
getStartAuthNicknameError('kiro', 'github-ABC123', existingAccounts, 'github-ABC123')
|
||||||
|
).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -293,8 +293,8 @@ export function AddAccountDialog({
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Start auth flow using provider capabilities.
|
* Start auth flow using provider capabilities.
|
||||||
* - Device code providers use /start and rely on WebSocket events for code display.
|
* - Verification-code providers use /start and WebSocket events for code display.
|
||||||
* - Authorization code providers use /start-url and polling.
|
* - Browser URL providers use /start-url and polling.
|
||||||
*/
|
*/
|
||||||
const handleAuthenticate = () => {
|
const handleAuthenticate = () => {
|
||||||
if (isPowerUserModePending) {
|
if (isPowerUserModePending) {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
import {
|
import {
|
||||||
CLIPROXY_PROVIDER_IDS,
|
CLIPROXY_PROVIDER_IDS,
|
||||||
PROVIDER_CAPABILITIES,
|
PROVIDER_CAPABILITIES,
|
||||||
getProvidersByOAuthFlow,
|
getDeviceCodeVerificationProviders,
|
||||||
} from '../../../src/cliproxy/provider-capabilities';
|
} from '../../../src/cliproxy/provider-capabilities';
|
||||||
import type { AiProviderFamilyId, AiProviderModelAlias } from '../../../src/cliproxy/ai-providers';
|
import type { AiProviderFamilyId, AiProviderModelAlias } from '../../../src/cliproxy/ai-providers';
|
||||||
import { PLUS_ONLY_PROVIDERS } from '../../../src/cliproxy/types';
|
import { PLUS_ONLY_PROVIDERS } from '../../../src/cliproxy/types';
|
||||||
@@ -346,13 +346,14 @@ export function getProviderDescription(provider: unknown): string {
|
|||||||
return PROVIDER_METADATA[normalized].description;
|
return PROVIDER_METADATA[normalized].description;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Providers whose add-account UX shows a verification code and uses `/start`. */
|
||||||
* Providers that use Device Code OAuth flow instead of Authorization Code flow.
|
export const VERIFICATION_CODE_AUTH_PROVIDERS: CLIProxyProvider[] = [
|
||||||
*/
|
...getDeviceCodeVerificationProviders(),
|
||||||
export const DEVICE_CODE_PROVIDERS: CLIProxyProvider[] = [
|
|
||||||
...getProvidersByOAuthFlow('device_code'),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/** Backward-compatible alias for verification-code auth providers. */
|
||||||
|
export const DEVICE_CODE_PROVIDERS = VERIFICATION_CODE_AUTH_PROVIDERS;
|
||||||
|
|
||||||
const DEVICE_CODE_PROVIDER_DISPLAY_NAMES: Readonly<Partial<Record<CLIProxyProvider, string>>> =
|
const DEVICE_CODE_PROVIDER_DISPLAY_NAMES: Readonly<Partial<Record<CLIProxyProvider, string>>> =
|
||||||
Object.freeze({
|
Object.freeze({
|
||||||
ghcp: 'GitHub Copilot',
|
ghcp: 'GitHub Copilot',
|
||||||
@@ -368,10 +369,10 @@ const DEVICE_CODE_PROVIDER_INSTRUCTIONS: Readonly<Partial<Record<CLIProxyProvide
|
|||||||
kimi: 'Sign in with your Kimi account and finish the device authorization.',
|
kimi: 'Sign in with your Kimi account and finish the device authorization.',
|
||||||
});
|
});
|
||||||
|
|
||||||
/** Check if provider uses Device Code flow */
|
/** Check if the add-account UI should use the verification-code auth dialog. */
|
||||||
export function isDeviceCodeProvider(provider: unknown): boolean {
|
export function isDeviceCodeProvider(provider: unknown): boolean {
|
||||||
const normalized = normalizeProviderInput(provider);
|
const normalized = normalizeProviderInput(provider);
|
||||||
return isValidProvider(normalized) && DEVICE_CODE_PROVIDERS.includes(normalized);
|
return isValidProvider(normalized) && VERIFICATION_CODE_AUTH_PROVIDERS.includes(normalized);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Provider display name tuned for device-code UX copy. */
|
/** Provider display name tuned for device-code UX copy. */
|
||||||
|
|||||||
@@ -45,6 +45,47 @@ describe('useCliproxyAuthFlow', () => {
|
|||||||
vi.restoreAllMocks();
|
vi.restoreAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('routes Cursor through start-url browser polling by default', async () => {
|
||||||
|
const fetchMock = vi.fn((input: RequestInfo | URL) => {
|
||||||
|
const url = String(input);
|
||||||
|
|
||||||
|
if (url.includes('/cursor/start-url')) {
|
||||||
|
return Promise.resolve(
|
||||||
|
createJsonResponse({
|
||||||
|
success: true,
|
||||||
|
authUrl: 'https://cursor.com/loginDeepControl?state=cursor-state',
|
||||||
|
state: 'cursor-state',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.includes('/status?state=cursor-state')) {
|
||||||
|
return Promise.resolve(createJsonResponse({ status: 'wait' }));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve(createJsonResponse({ success: true, account: { id: 'cursor-test' } }));
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.stubGlobal('fetch', fetchMock);
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useCliproxyAuthFlow(), { wrapper });
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.startAuth('cursor');
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(fetchMock).toHaveBeenCalledWith(
|
||||||
|
'/api/cliproxy/auth/cursor/start-url',
|
||||||
|
expect.objectContaining({ method: 'POST' })
|
||||||
|
);
|
||||||
|
expect(result.current.isDeviceCodeFlow).toBe(false);
|
||||||
|
expect(result.current.authUrl).toContain('cursor.com/loginDeepControl');
|
||||||
|
expect(window.open).toHaveBeenCalledWith(
|
||||||
|
'https://cursor.com/loginDeepControl?state=cursor-state',
|
||||||
|
'_blank'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('ignores stale poll completions from a superseded auth attempt', async () => {
|
it('ignores stale poll completions from a superseded auth attempt', async () => {
|
||||||
const firstPoll = createDeferred<Response>();
|
const firstPoll = createDeferred<Response>();
|
||||||
let startCount = 0;
|
let startCount = 0;
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
getRequestedUpstreamModelRuleErrors,
|
getRequestedUpstreamModelRuleErrors,
|
||||||
getRequestedModelId,
|
getRequestedModelId,
|
||||||
groupProvidersBySection,
|
groupProvidersBySection,
|
||||||
|
isDeviceCodeProvider,
|
||||||
isPlusExtraProvider,
|
isPlusExtraProvider,
|
||||||
parseRequestedUpstreamModelRules,
|
parseRequestedUpstreamModelRules,
|
||||||
PLUS_EXTRA_CLIPROXY_PROVIDERS,
|
PLUS_EXTRA_CLIPROXY_PROVIDERS,
|
||||||
@@ -90,6 +91,12 @@ describe('provider presentation metadata', () => {
|
|||||||
expect(grouped[1]?.items.map((entry) => entry.provider)).toEqual(['cursor']);
|
expect(grouped[1]?.items.map((entry) => entry.provider)).toEqual(['cursor']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('keeps Cursor out of verification-code device flow routing', () => {
|
||||||
|
expect(isDeviceCodeProvider('cursor')).toBe(false);
|
||||||
|
expect(isDeviceCodeProvider('ghcp')).toBe(true);
|
||||||
|
expect(isDeviceCodeProvider('qwen')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it('detects plus-extra providers inside composite variants', () => {
|
it('detects plus-extra providers inside composite variants', () => {
|
||||||
expect(
|
expect(
|
||||||
variantUsesPlusExtraProvider({
|
variantUsesPlusExtraProvider({
|
||||||
|
|||||||
Reference in New Issue
Block a user