From 5aad9826b698381c6e19a1a69f772c85d17cee2d Mon Sep 17 00:00:00 2001 From: elkaix Date: Mon, 17 Aug 2026 12:47:02 -0400 Subject: [PATCH] fix(cli): keep OAuth query parameters when opening the browser on Windows --- .changeset/windows-browser-url.md | 5 +++ apps/pythinker-code/src/utils/open-url.ts | 33 +++++++++++++++---- .../test/utils/open-url.test.ts | 28 ++++++++++++++++ 3 files changed, 59 insertions(+), 7 deletions(-) create mode 100644 .changeset/windows-browser-url.md create mode 100644 apps/pythinker-code/test/utils/open-url.test.ts diff --git a/.changeset/windows-browser-url.md b/.changeset/windows-browser-url.md new file mode 100644 index 00000000..fe6d8441 --- /dev/null +++ b/.changeset/windows-browser-url.md @@ -0,0 +1,5 @@ +--- +'@pymodel/pythinker-code': patch +--- + +Open the browser on Windows through `rundll32` instead of `cmd /c start`. `cmd` cut every URL at the first `&`, so OAuth logins reached the provider with only the first query parameter and failed with an invalid authorize request. diff --git a/apps/pythinker-code/src/utils/open-url.ts b/apps/pythinker-code/src/utils/open-url.ts index 4112d9c6..10b1a887 100644 --- a/apps/pythinker-code/src/utils/open-url.ts +++ b/apps/pythinker-code/src/utils/open-url.ts @@ -1,11 +1,30 @@ import { execFile } from 'node:child_process'; +export interface OpenUrlCommand { + readonly command: string; + readonly args: readonly string[]; +} + +/** + * Windows uses `rundll32` rather than `cmd /c start` because `cmd` re-parses + * its arguments and cuts a URL at the first `&`, which strips every OAuth + * query parameter after `client_id`. + */ +export function openUrlCommandFor( + url: string, + platform: NodeJS.Platform = process.platform, +): OpenUrlCommand { + switch (platform) { + case 'darwin': + return { command: 'open', args: [url] }; + case 'win32': + return { command: 'rundll32', args: ['url.dll,FileProtocolHandler', url] }; + default: + return { command: 'xdg-open', args: [url] }; + } +} + export function openUrl(url: string): void { - const command: [string, string[]] = - process.platform === 'darwin' - ? ['open', [url]] - : process.platform === 'win32' - ? ['cmd', ['/c', 'start', '', url]] - : ['xdg-open', [url]]; - execFile(command[0], command[1], () => {}); + const { command, args } = openUrlCommandFor(url); + execFile(command, [...args], () => {}); } diff --git a/apps/pythinker-code/test/utils/open-url.test.ts b/apps/pythinker-code/test/utils/open-url.test.ts new file mode 100644 index 00000000..2d35a9d5 --- /dev/null +++ b/apps/pythinker-code/test/utils/open-url.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, it } from 'vitest'; + +import { openUrlCommandFor } from '#/utils/open-url'; + +const AUTHORIZE_URL = + 'https://auth.openai.com/oauth/authorize?client_id=app_test&response_type=code&state=abc'; + +describe('openUrlCommandFor', () => { + it('keeps every query parameter on Windows', () => { + const { command, args } = openUrlCommandFor(AUTHORIZE_URL, 'win32'); + expect(command).toBe('rundll32'); + // `cmd /c start` cuts the URL at the first `&`, so the launcher must not + // hand the URL to a command interpreter. + expect(command).not.toBe('cmd'); + expect(args.at(-1)).toBe(AUTHORIZE_URL); + }); + + it('uses the platform launcher elsewhere', () => { + expect(openUrlCommandFor(AUTHORIZE_URL, 'darwin')).toEqual({ + command: 'open', + args: [AUTHORIZE_URL], + }); + expect(openUrlCommandFor(AUTHORIZE_URL, 'linux')).toEqual({ + command: 'xdg-open', + args: [AUTHORIZE_URL], + }); + }); +});