History

Harden browser opener commands

Rhys Sullivan committed 4 months ago

Commit 10ffa121 parent 622971f3 files · +60 −14Browse files

Unified · Split

3 files
  1. apps/cli/src/device-login.test.ts +34 −0
  2. apps/cli/src/device-login.ts +25 −3
  3. apps/cli/src/main.ts +1 −11
apps/cli/src/device-login.test.ts+34−0
@@ -0,0 +1,34 @@
1import { describe, expect, it } from "@effect/vitest";
2
3import { browserOpenCommand } from "./device-login";
4
5describe("browserOpenCommand", () => {
6 it("opens Windows browser URLs without cmd.exe", () => {
7 const command = browserOpenCommand(
8 "https://executor.example/login?next=a%20b&token=abc123",
9 "win32",
10 );
11
12 expect(command).toEqual([
13 "rundll32.exe",
14 ["url.dll,FileProtocolHandler", "https://executor.example/login?next=a%20b&token=abc123"],
15 ]);
16 });
17
18 it("passes the browser URL as one argument on every platform", () => {
19 expect(browserOpenCommand("https://executor.example/login?x=1&y=2", "darwin")).toEqual([
20 "open",
21 ["https://executor.example/login?x=1&y=2"],
22 ]);
23 expect(browserOpenCommand("https://executor.example/login?x=1&y=2", "linux")).toEqual([
24 "xdg-open",
25 ["https://executor.example/login?x=1&y=2"],
26 ]);
27 });
28
29 it("refuses non-browser URL schemes", () => {
30 expect(browserOpenCommand("javascript:alert(1)", "win32")).toBeUndefined();
31 expect(browserOpenCommand("file:///C:/Windows/System32/calc.exe", "win32")).toBeUndefined();
32 expect(browserOpenCommand("not a url", "win32")).toBeUndefined();
33 });
34});
apps/cli/src/device-login.ts+25−3
@@ -311,11 +311,33 @@ export const refreshDeviceTokens = async (input: { · 310 unmodified lines
311 };311 };
312};312};
313 313
314export type BrowserOpenCommand = readonly [command: string, args: ReadonlyArray<string>];
315
316const normalizeBrowserOpenUrl = (url: string): string | undefined => {
317 try {
318 const parsed = new URL(url);
319 return parsed.protocol === "http:" || parsed.protocol === "https:" ? parsed.href : undefined;
320 } catch {
321 return undefined;
322 }
323};
324
325export const browserOpenCommand = (
326 url: string,
327 platform: typeof process.platform = process.platform,
328): BrowserOpenCommand | undefined => {
329 const normalizedUrl = normalizeBrowserOpenUrl(url);
330 if (!normalizedUrl) return undefined;
331 if (platform === "darwin") return ["open", [normalizedUrl]];
332 if (platform === "win32") return ["rundll32.exe", ["url.dll,FileProtocolHandler", normalizedUrl]];
333 return ["xdg-open", [normalizedUrl]];
334};
335
314/** Best-effort open the verification URL in the user's default browser. */336/** Best-effort open the verification URL in the user's default browser. */
315export const openBrowser = (url: string): void => {337export const openBrowser = (url: string): void => {
316 const platform = process.platform;338 const openCommand = browserOpenCommand(url);
317 const command = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";339 if (!openCommand) return;
318 const args = platform === "win32" ? ["/c", "start", "", url] : [url];340 const [command, args] = openCommand;
319 try {341 try {
320 const child = spawn(command, args, { stdio: "ignore", detached: true });342 const child = spawn(command, args, { stdio: "ignore", detached: true });
321 // Swallow async spawn failures (e.g. the opener binary is missing), the343 // Swallow async spawn failures (e.g. the opener binary is missing), the
apps/cli/src/main.ts+1−11
@@ -2,7 +2,6 @@ · 1 unmodified line
2// before any import (e.g. `@executor-js/local` → libSQL) eagerly loads them.2// before any import (e.g. `@executor-js/local` → libSQL) eagerly loads them.
3import "./native-bindings";3import "./native-bindings";
4 4
5import { execFile } from "node:child_process";
6import { randomUUID } from "node:crypto";5import { randomUUID } from "node:crypto";
7import { existsSync, realpathSync } from "node:fs";6import { existsSync, realpathSync } from "node:fs";
8import { dirname, join, resolve } from "node:path";7import { dirname, join, resolve } from "node:path";
@@ -2821,16 +2820,7 @@ const installCommand = Command.make( · 2812 unmodified lines
2821 */2820 */
2822const openInBrowser = (url: string): Effect.Effect<void> =>2821const openInBrowser = (url: string): Effect.Effect<void> =>
2823 Effect.sync(() => {2822 Effect.sync(() => {
2824 const [cmd, args]: readonly [string, ReadonlyArray<string>] =2823 openBrowser(url);
2825 process.platform === "darwin"
2826 ? ["open", [url]]
2827 : process.platform === "win32"
2828 ? ["cmd", ["/c", "start", "", url]]
2829 : ["xdg-open", [url]];
2830 // best-effort: ignore failures (e.g. no opener on headless Linux). The URL
2831 // was printed above for manual open; execFile's callback absorbs the error,
2832 // so there's no unhandled 'error' event to crash the CLI.
2833 execFile(cmd, [...args], () => {});
2834 });2824 });
2835 2825
2836const printNoRunningLocalWebApp = (): void => {2826const printNoRunningLocalWebApp = (): void => {

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t