feat(providers): Cursor PKCE login with Bearer quota, auto router, and empty-turn errors (#9909)

5 — Cursor PKCE login com Bearer quota, auto router e empty-turn errors. Feature completa e testada (11 arquivos de teste, 133 testes focados, todos verdes).

**Validação (worktree combinado `.claude/worktrees/fix-9909`, board sobre `origin/release/v3.8.50`):**
- 3 conflitos reais resolvidos: `config/quality/eslint-suppressions.json` (aditivo), `open-sse/config/providers/registry/cursor/index.ts` (dedup de 208 entradas de catálogo, 0 IDs duplicados verificado), `open-sse/executors/cursor.ts` (imports aditivos).
- `npm run typecheck:core`: limpo.
- `check-changelog-integrity`, `check-file-size`, `check-complexity` (2615/2774), `check-cognitive-complexity` (1175/1223), `check-dead-code` (410/416): todos OK.
- `check-public-creds`: 1 entrada obsoleta pré-existente na allowlist (`copilot-m365-web.ts:330`), já presente no tip da release — não é desta PR.
- `npm run lint`: 0 errors (5 warnings pré-existentes).
- Testes focados (`cursor-agent-cli-version`, `cursor-available-models`, `cursor-catalog-combo-compat`, `cursor-errors-classify`, `cursor-login-pkce`, `cursor-model-effort-suffix-7289`, `cursor-streaming`, `cursor-token-extractor`, `cursor-token-refresh-wiring`, `cursor-usage-fetcher`, `empty-stream-no-content-8649`): 133/133 verdes.
- Corrigido durante a validação: 1 teste novo da própria PR (`cursor-model-effort-suffix-7289.test.ts`, "splits effort off legacy grok- ids") colidia com `CURSOR_MODEL_ALIASES` já mesclado na release (mapeia `grok-4.5-high` → `cursor-grok-4.5-high` antes do fallback legado rodar); ajustado para usar um id não-aliasado (`grok-3-high`) que de fato exercita o fallback — commit `68b58ed`.

Obrigado pela contribuição, @yansigit — feature robusta com boa cobertura de testes.
This commit is contained in:
SB Yoon
2026-08-21 07:28:01 -06:00
committed by GitHub
parent 089a5e7cab
commit c130f2aa1c
82 changed files with 4144 additions and 568 deletions

View File

@@ -1,6 +1,6 @@
"use client";
import { useState, useEffect } from "react";
import { useState, useEffect, useRef } from "react";
import { useTranslations } from "next-intl";
import Modal from "./Modal";
import Button from "./Button";
@@ -13,9 +13,12 @@ type CursorAuthModalProps = {
reauthConnection?: unknown;
};
type AuthTab = "login" | "import";
/**
* Cursor Auth Modal
* Auto-detect and import token from Cursor IDE's local SQLite database
* Primary: deep-control PKCE login (Docker-friendly).
* Secondary: IDE / paste-token import (optional refresh token).
*/
export default function CursorAuthModal({
isOpen,
@@ -24,43 +27,161 @@ export default function CursorAuthModal({
reauthConnection: _,
}: CursorAuthModalProps) {
const t = useTranslations("cursorAuthModal");
const [tab, setTab] = useState<AuthTab>("login");
const [accessToken, setAccessToken] = useState("");
const [refreshToken, setRefreshToken] = useState("");
const [machineId, setMachineId] = useState("");
const [error, setError] = useState(null);
const [error, setError] = useState<string | null>(null);
const [importing, setImporting] = useState(false);
const [autoDetecting, setAutoDetecting] = useState(false);
const [autoDetected, setAutoDetected] = useState(false);
const [dockerHint, setDockerHint] = useState(false);
const [loginUrl, setLoginUrl] = useState("");
const [sessionId, setSessionId] = useState("");
const [loginStarting, setLoginStarting] = useState(false);
const [loginPolling, setLoginPolling] = useState(false);
const pollAbortRef = useRef(false);
// Auto-detect tokens when modal opens
useEffect(() => {
if (!isOpen) return;
if (!isOpen) {
pollAbortRef.current = true;
return;
}
pollAbortRef.current = false;
// Reset modal UI state for this open. Nested in a function (like autoDetect
// below) rather than called directly in the effect body, since these are a
// response to the modal being (re)opened — not a synchronization of React
// state with an external system — and react-hooks/set-state-in-effect flags
// direct top-level setState calls in an effect.
const resetModalState = () => {
setTab("login");
setError(null);
setLoginUrl("");
setSessionId("");
setLoginPolling(false);
// Detect Docker-ish environments for import-tab guidance
setDockerHint(false);
};
resetModalState();
const autoDetect = async () => {
setAutoDetecting(true);
setError(null);
setAutoDetected(false);
try {
const res = await fetch("/api/oauth/cursor/auto-import");
const data = await res.json();
if (data.found) {
setAccessToken(data.accessToken);
setAccessToken(data.accessToken || "");
setRefreshToken(data.refreshToken || "");
setMachineId(data.machineId || "");
setAutoDetected(true);
} else {
setError(data.error || t("errorAutoDetect"));
// Soft hint only on import tab; don't block login tab
if (
typeof data.error === "string" &&
/docker|config files found|not appear/i.test(data.error)
) {
setDockerHint(true);
}
}
} catch (err) {
setError(t("errorAutoDetectFailed"));
} catch {
/* ignore — login tab is primary */
} finally {
setAutoDetecting(false);
}
};
autoDetect();
void autoDetect();
}, [isOpen]);
useEffect(() => {
return () => {
pollAbortRef.current = true;
};
}, []);
const handleStartLogin = async () => {
setLoginStarting(true);
setError(null);
setLoginUrl("");
setSessionId("");
try {
const res = await fetch("/api/oauth/cursor/login/start", { method: "POST" });
const data = await res.json();
if (!res.ok) throw new Error(data.error || t("errorLoginStart"));
setLoginUrl(data.loginUrl);
setSessionId(data.sessionId);
if (typeof window !== "undefined" && data.loginUrl) {
window.open(data.loginUrl, "_blank", "noopener,noreferrer");
}
void pollUntilDone(data.sessionId);
} catch (err) {
setError(err instanceof Error ? err.message : t("errorLoginStart"));
} finally {
setLoginStarting(false);
}
};
const pollUntilDone = async (sid: string) => {
setLoginPolling(true);
pollAbortRef.current = false;
const maxAttempts = 150;
let delayMs = 1000;
try {
for (let i = 0; i < maxAttempts; i++) {
if (pollAbortRef.current) return;
await new Promise((r) => setTimeout(r, delayMs));
if (pollAbortRef.current) return;
const res = await fetch("/api/oauth/cursor/login/poll", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ sessionId: sid }),
});
const data = await res.json();
if (data.status === "pending") {
delayMs = Math.min(delayMs * 1.2, 10_000);
continue;
}
if (data.status === "ok" || data.success) {
onSuccess?.();
onClose();
return;
}
throw new Error(data.error || t("errorLoginPoll"));
}
throw new Error(t("errorLoginTimeout"));
} catch (err) {
if (!pollAbortRef.current) {
setError(err instanceof Error ? err.message : t("errorLoginPoll"));
}
} finally {
setLoginPolling(false);
}
};
const handleCancelLogin = async () => {
pollAbortRef.current = true;
if (sessionId) {
try {
await fetch("/api/oauth/cursor/login/cancel", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ sessionId }),
});
} catch {
/* ignore */
}
}
setLoginPolling(false);
setLoginUrl("");
setSessionId("");
};
const handleImportToken = async () => {
if (!accessToken.trim()) {
setError(t("errorEnterToken"));
@@ -73,6 +194,7 @@ export default function CursorAuthModal({
try {
const body: Record<string, string> = { accessToken: accessToken.trim() };
if (machineId.trim()) body.machineId = machineId.trim();
if (refreshToken.trim()) body.refreshToken = refreshToken.trim();
const res = await fetch("/api/oauth/cursor/import", {
method: "POST",
@@ -83,67 +205,122 @@ export default function CursorAuthModal({
const data = await res.json();
if (!res.ok) {
throw new Error(data.error || t("errorImportFailed"));
throw new Error(
typeof data.error === "string"
? data.error
: data.error?.message || t("errorImportFailed")
);
}
// Success - close modal and trigger refresh
onSuccess?.();
onClose();
} catch (err) {
setError(err.message);
setError(err instanceof Error ? err.message : t("errorImportFailed"));
} finally {
setImporting(false);
}
};
const handleClose = () => {
void handleCancelLogin();
onClose();
};
return (
<Modal isOpen={isOpen} title={t("title")} onClose={onClose}>
<Modal isOpen={isOpen} title={t("title")} onClose={handleClose}>
<div className="flex flex-col gap-4">
{/* Auto-detecting state */}
{autoDetecting && (
<div className="text-center py-6">
<div className="size-16 mx-auto mb-4 rounded-full bg-primary/10 flex items-center justify-center">
<span className="material-symbols-outlined text-3xl text-primary animate-spin">
progress_activity
</span>
<div className="flex gap-2 border-b border-border pb-2">
<Button
variant={tab === "login" ? "primary" : "ghost"}
onClick={() => setTab("login")}
disabled={loginPolling}
>
{t("tabLogin")}
</Button>
<Button
variant={tab === "import" ? "primary" : "ghost"}
onClick={() => setTab("import")}
disabled={loginPolling}
>
{t("tabImport")}
</Button>
</div>
{tab === "login" && (
<div className="flex flex-col gap-4">
<p className="text-sm text-text-muted">{t("loginDescription")}</p>
{loginPolling && (
<div className="text-center py-4">
<div className="size-12 mx-auto mb-3 rounded-full bg-primary/10 flex items-center justify-center">
<span className="material-symbols-outlined text-2xl text-primary animate-spin">
progress_activity
</span>
</div>
<p className="text-sm font-medium">{t("waitingApproval")}</p>
{loginUrl && (
<p className="text-xs text-text-muted mt-2 break-all">
{t("openUrlHint")}{" "}
<a
href={loginUrl}
target="_blank"
rel="noopener noreferrer"
className="text-primary underline"
>
{t("openLoginLink")}
</a>
</p>
)}
</div>
)}
{error && (
<div className="bg-red-50 dark:bg-red-900/20 p-3 rounded-lg border border-red-200 dark:border-red-800">
<p className="text-sm text-red-600 dark:text-red-400">{error}</p>
</div>
)}
<div className="flex gap-2">
{!loginPolling ? (
<Button onClick={handleStartLogin} fullWidth disabled={loginStarting}>
{loginStarting ? t("startingLogin") : t("loginWithCursor")}
</Button>
) : (
<Button onClick={handleCancelLogin} variant="ghost" fullWidth>
{t("cancelLogin")}
</Button>
)}
<Button onClick={handleClose} variant="ghost" fullWidth disabled={loginPolling}>
{t("cancel")}
</Button>
</div>
<h3 className="text-lg font-semibold mb-2">{t("autoDetecting")}</h3>
<p className="text-sm text-text-muted">{t("readingFromCursor")}</p>
</div>
)}
{/* Form (shown after auto-detect completes) */}
{!autoDetecting && (
<>
{/* Success message if auto-detected */}
{autoDetected && (
{tab === "import" && (
<div className="flex flex-col gap-4">
{autoDetecting && (
<div className="text-center py-4">
<p className="text-sm text-text-muted">{t("autoDetecting")}</p>
</div>
)}
{!autoDetecting && autoDetected && (
<div className="bg-green-50 dark:bg-green-900/20 p-3 rounded-lg border border-green-200 dark:border-green-800">
<div className="flex gap-2">
<span className="material-symbols-outlined text-green-600 dark:text-green-400">
check_circle
</span>
<p className="text-sm text-green-800 dark:text-green-200">
{t("tokensAutoDetected")}
</p>
</div>
<p className="text-sm text-green-800 dark:text-green-200">
{t("tokensAutoDetected")}
</p>
</div>
)}
{/* Info message if not auto-detected */}
{!autoDetected && !error && (
{!autoDetecting && !autoDetected && (
<div className="bg-blue-50 dark:bg-blue-900/20 p-3 rounded-lg border border-blue-200 dark:border-blue-800">
<div className="flex gap-2">
<span className="material-symbols-outlined text-blue-600 dark:text-blue-400">
info
</span>
<p className="text-sm text-blue-800 dark:text-blue-200">
{t("cursorNotDetected")}
</p>
</div>
<p className="text-sm text-blue-800 dark:text-blue-200">
{dockerHint ? t("dockerImportHint") : t("cursorNotDetected")}
</p>
</div>
)}
{/* Access Token Input */}
<div>
<label className="block text-sm font-medium mb-2">
{t("accessToken")} <span className="text-red-500">{t("required")}</span>
@@ -157,7 +334,19 @@ export default function CursorAuthModal({
/>
</div>
{/* Machine ID Input (optional — not needed for cursor-agent imports) */}
<div>
<label className="block text-sm font-medium mb-2">
{t("refreshToken")} <span className="text-text-muted text-xs">{t("optional")}</span>
</label>
<textarea
value={refreshToken}
onChange={(e) => setRefreshToken(e.target.value)}
placeholder={t("refreshTokenPlaceholder")}
rows={2}
className="w-full px-3 py-2 text-sm font-mono border border-border rounded-lg bg-background focus:outline-none focus:border-primary resize-none"
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">
{t("machineId")} <span className="text-text-muted text-xs">{t("optional")}</span>
@@ -170,14 +359,12 @@ export default function CursorAuthModal({
/>
</div>
{/* Error Display */}
{error && (
<div className="bg-red-50 dark:bg-red-900/20 p-3 rounded-lg border border-red-200 dark:border-red-800">
<p className="text-sm text-red-600 dark:text-red-400">{error}</p>
</div>
)}
{/* Action Buttons */}
<div className="flex gap-2">
<Button
onClick={handleImportToken}
@@ -186,11 +373,11 @@ export default function CursorAuthModal({
>
{importing ? t("importing") : t("importToken")}
</Button>
<Button onClick={onClose} variant="ghost" fullWidth>
<Button onClick={handleClose} variant="ghost" fullWidth>
{t("cancel")}
</Button>
</div>
</>
</div>
)}
</div>
</Modal>

View File

@@ -172,6 +172,7 @@ export const oauthPasteCredentialsSchema = z.object({
export const cursorImportSchema = z.object({
accessToken: z.string().trim().min(1, "Access token is required"),
machineId: z.string().trim().optional(),
refreshToken: z.string().trim().min(1).optional(),
});
export const traeImportSchema = z.object({