fix(mcp): surface a short launch URL and log Windows opener failures for OAuth
Two independent defects broke /mcp reauth against S256-only providers on
Windows boxes whose PATH no longer references System32:
1. openPath spawned bare rundll32 and swallowed the
`Executable not found in $PATH` throw with a bare `catch {}`, so the MCP
controller's outer try/catch was dead and the transcript unconditionally
claimed "Opening browser automatically...".
2. TUI#prepareLine silently truncates any composed row wider than the
viewport. MCPAuthorizationLinkPrompt rendered `Copy URL: <full URL>` as a
single ~271-column line whose trailing parameter is
code_challenge_method=S256. On the reporter's 270-col terminal the cut
landed inside that parameter, dropping the method while keeping
code_challenge — which RFC 7636 §4.3 treats as plain PKCE, which Linear
correctly rejects with "The plain PKCE method is not allowed. Use S256
instead."
OAuthCallbackFlow now hosts a `GET /launch` route on the same loopback
callback server it already runs; the route 302-redirects to the pending
authorization URL and is advertised as `OAuthAuthInfo.launchUrl` — a
~30-char copy target no viewport can meaningfully truncate. The MCP OAuth
fallback, /login, setup wizard, auth-broker CLI, and login-dialog all
prefer the launch URL for the visible copy target, keep the full URL in
the OSC 8 hyperlink for click-through, and the MCP flow additionally
stages the copy target on the clipboard via OSC 52 (same pattern the
setup wizard uses).
openPath now resolves rundll32.exe through %SystemRoot%\System32 (with a
C:\Windows fallback when SystemRoot is unset) and logs both synchronous
spawn throws and non-zero exits via the shared logger, so silent
misconfigurations show up in ~/.omp/logs/omp.*.log. The dead try/catch
around openPath in the MCP controller is removed.
Fixes #4418
This commit is contained in:
@@ -222,8 +222,17 @@ async function runLocalLogin(provider: OAuthProvider): Promise<void> {
|
||||
// for non-paste-code providers, so this is defense-in-depth on the same gate.
|
||||
const usesManualInput = PASTE_CODE_LOGIN_PROVIDERS.has(provider);
|
||||
await storage.login(provider, {
|
||||
onAuth({ url, instructions }) {
|
||||
process.stdout.write(`\nOpen this URL in your browser:\n${url}\n`);
|
||||
onAuth({ url, launchUrl, instructions }) {
|
||||
process.stdout.write("\nOpen this URL in your browser:\n");
|
||||
// Advertise the short launch URL first when the flow exposes one — it
|
||||
// survives narrow terminals that would truncate the trailing
|
||||
// `code_challenge_method=S256` (or worse, drop `state`/`code_challenge`
|
||||
// entirely) from the full authorize URL. The full URL still prints
|
||||
// beneath it so headless callers can capture it programmatically.
|
||||
if (launchUrl && launchUrl !== url) {
|
||||
process.stdout.write(`${launchUrl}\n(redirects to)\n`);
|
||||
}
|
||||
process.stdout.write(`${url}\n`);
|
||||
if (instructions) process.stdout.write(`${instructions}\n`);
|
||||
process.stdout.write("\n");
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user