From efabdb76f16652241fa47f427970d44a7fdf9664 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Fri, 25 Sep 2026 17:33:30 -0500 Subject: [PATCH] refactor(core): share a browser opener across opencode and tui Move browser launching into @opencode-ai/core/open so the CLI, plugins, MCP browser service, and TUI stop importing open directly. openUrl only accepts http and https targets. The MCP OAuth provider applies the same check before forwarding an authorization URL. Bump open to 11 and let core own the dependency, with a root override so the tree resolves to a single copy. --- bun.lock | 24 +++++--------- package.json | 3 +- packages/core/package.json | 1 + packages/core/src/open.ts | 8 +++++ packages/core/test/open.test.ts | 16 ++++++++++ packages/opencode/package.json | 1 - packages/opencode/src/cli/cmd/account.ts | 4 +-- packages/opencode/src/cli/cmd/web.ts | 6 ++-- packages/opencode/src/mcp/browser.ts | 4 +-- packages/opencode/src/mcp/oauth-provider.ts | 4 +++ packages/opencode/src/plugin/digitalocean.ts | 4 +-- .../opencode/src/plugin/snowflake-cortex.ts | 4 +-- .../opencode/test/mcp/oauth-provider.test.ts | 31 +++++++++++++++++++ packages/tui/package.json | 1 - packages/tui/src/app.tsx | 4 +-- .../tui/src/component/dialog-retry-action.tsx | 4 +-- packages/tui/src/ui/link.tsx | 4 +-- 17 files changed, 87 insertions(+), 36 deletions(-) create mode 100644 packages/core/src/open.ts create mode 100644 packages/core/test/open.test.ts diff --git a/bun.lock b/bun.lock index 79aa7d7e3857..5cd6bc034270 100644 --- a/bun.lock +++ b/bun.lock @@ -352,6 +352,7 @@ "mime-types": "3.0.2", "minimatch": "10.2.5", "npm-package-arg": "13.0.2", + "open": "11.0.4", "semver": "^7.6.3", "turndown": "7.2.0", "venice-ai-sdk-provider": "2.1.1", @@ -645,7 +646,6 @@ "mime-types": "3.0.2", "minimatch": "10.0.3", "npm-package-arg": "13.0.2", - "open": "10.1.2", "opencode-gitlab-auth": "2.1.0", "opencode-poe-auth": "0.0.1", "opentui-spinner": "catalog:", @@ -952,7 +952,6 @@ "diff": "catalog:", "effect": "catalog:", "fuzzysort": "catalog:", - "open": "10.1.2", "opentui-spinner": "catalog:", "remeda": "catalog:", "solid-js": "catalog:", @@ -1086,6 +1085,7 @@ "@opentui/solid": "catalog:", "@types/bun": "catalog:", "@types/node": "catalog:", + "open": "11.0.4", }, "catalog": { "@cloudflare/workers-types": "4.20251008.0", @@ -3456,7 +3456,7 @@ "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], - "default-browser": ["default-browser@5.5.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw=="], + "default-browser": ["default-browser@5.5.1", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-m1pAzaJgZ/gssEqlOhJkPJp8Xly7QyW6xcrkUa2KKcDeDSEMP7X8xipU3snUcfisTQx0w1AGae+9UtJSfVnXGw=="], "default-browser-id": ["default-browser-id@5.0.1", "", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="], @@ -4592,7 +4592,7 @@ "oniguruma-to-es": ["oniguruma-to-es@4.3.6", "", { "dependencies": { "oniguruma-parser": "^0.12.2", "regex": "^6.1.0", "regex-recursion": "^6.0.2" } }, "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA=="], - "open": ["open@10.1.2", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "is-wsl": "^3.1.0" } }, "sha512-cxN6aIDPz6rm8hbebcP7vrQNhvRcveZoJU72Y7vskh4oIm+BZwBECnx5nTmrlres1Qapvx27Qo1Auukpf8PKXw=="], + "open": ["open@11.0.4", "", { "dependencies": { "default-browser": "^5.5.1", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.2.1", "wsl-utils": "^1.0.0" } }, "sha512-++Zlftm0kVLPmzC06t6epuWmcRMDbI4z5P3NNX979WA/k23+NtSOynEGzsVfZwguKw2mi5umVgnBlJQMwRz4Pg=="], "openai": ["openai@5.11.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.23.8" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-+AuTc5pVjlnTuA9zvn8rA/k+1RluPIx9AD4eDcnutv6JNwHHZxIhkFy+tmMKCvmMFDQzfA/r1ujvPWB19DQkYg=="], @@ -4758,7 +4758,7 @@ "postject": ["postject@1.0.0-alpha.6", "", { "dependencies": { "commander": "^9.4.0" }, "bin": { "postject": "dist/cli.js" } }, "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A=="], - "powershell-utils": ["powershell-utils@0.1.0", "", {}, "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A=="], + "powershell-utils": ["powershell-utils@0.2.1", "", {}, "sha512-C+y9x90UElAddDZmV4qOx9W53B61PO7cIqWz2dQsWlwswuq4mr8NEwytdGKboYbQlGZ3awrkTeNvcZiZNHnQ8A=="], "preact": ["preact@11.0.0-beta.0", "", {}, "sha512-IcODoASASYwJ9kxz7+MJeiJhvLriwSb4y4mHIyxdgaRZp6kPUud7xytrk/6GZw8U3y6EFJaRb5wi9SrEK+8+lg=="], @@ -5550,7 +5550,7 @@ "ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="], - "wsl-utils": ["wsl-utils@0.3.1", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg=="], + "wsl-utils": ["wsl-utils@1.0.0", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-Hl0ZOAs672vg+06kfujwRhoS6/jehvULrlFkuF2dRu6pHgA8U06h3xqNIqNNU1LTXPcedxByAR4GS6pwQK0mgA=="], "xdg-basedir": ["xdg-basedir@5.1.0", "", {}, "sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ=="], @@ -5928,8 +5928,6 @@ "@hey-api/json-schema-ref-parser/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], - "@hey-api/openapi-ts/open": ["open@11.0.0", "", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.1.0", "wsl-utils": "^0.3.0" } }, "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw=="], - "@hey-api/openapi-ts/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], "@img/sharp-wasm32/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], @@ -6414,8 +6412,6 @@ "opencode/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], - "opencode-gitlab-auth/open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="], - "openid-client/jose": ["jose@4.15.9", "", {}, "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA=="], "openid-client/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], @@ -6488,8 +6484,6 @@ "sst/jose": ["jose@5.2.3", "", {}, "sha512-KUXdbctm1uHVL8BYhnyHkgp3zDX5KW8ZhAKVFEfUbU2P8Alpzjb+48hHvjOdQIyPshoblhzsuqOwEEAbtHVirA=="], - "storybook/open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="], - "string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], @@ -6560,6 +6554,8 @@ "wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "wsl-utils/powershell-utils": ["powershell-utils@0.1.0", "", {}, "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A=="], + "yaml-language-server/request-light": ["request-light@0.5.8", "", {}, "sha512-3Zjgh+8b5fhRJBQZoy+zbVKpAQGLyka0MPgW3zruTF4dFFJ8Fqcfu9YsAvi/rvdcaTeWG3MkbZv4WKxAn/84Lg=="], "yaml-language-server/yaml": ["yaml@2.7.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ=="], @@ -7172,8 +7168,6 @@ "motion/framer-motion/motion-utils": ["motion-utils@12.39.0", "", {}, "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ=="], - "opencode-gitlab-auth/open/wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="], - "opencode/@ai-sdk/cerebras/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.54", "", { "dependencies": { "@ai-sdk/provider": "3.0.12", "@ai-sdk/provider-utils": "4.0.33" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-OyXt0zK8y2/ZIyWlbxTv2r1M7AK227S+Gl4BYOEF42q0wz1n5m4fwR8L4Fy/MQ4Ho6xje47MPsFcRdIqIyP6Rw=="], "opencode/@ai-sdk/cerebras/@ai-sdk/provider": ["@ai-sdk/provider@3.0.12", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-sj9DWTJ2Ze0WR9qsiOPqoqzNx3OxL6iMxHImbhvoe9qOspekbzxNDMiJ4TIGfYHYh9w4OmBjz3prvqhzTi96+Q=="], @@ -7200,8 +7194,6 @@ "send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - "storybook/open/wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="], - "string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "tw-to-css/tailwindcss/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], diff --git a/package.json b/package.json index a3f9544410da..59bf0501f096 100644 --- a/package.json +++ b/package.json @@ -142,7 +142,8 @@ "@opentui/keymap": "catalog:", "@opentui/solid": "catalog:", "@types/bun": "catalog:", - "@types/node": "catalog:" + "@types/node": "catalog:", + "open": "11.0.4" }, "patchedDependencies": { "@dnd-kit/dom@0.5.0": "patches/@dnd-kit%2Fdom@0.5.0.patch", diff --git a/packages/core/package.json b/packages/core/package.json index 0a7e0ea07a62..fbcbfc34e629 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -119,6 +119,7 @@ "mime-types": "3.0.2", "minimatch": "10.2.5", "npm-package-arg": "13.0.2", + "open": "11.0.4", "semver": "^7.6.3", "turndown": "7.2.0", "venice-ai-sdk-provider": "2.1.1", diff --git a/packages/core/src/open.ts b/packages/core/src/open.ts new file mode 100644 index 000000000000..296982f3afc3 --- /dev/null +++ b/packages/core/src/open.ts @@ -0,0 +1,8 @@ +import open from "open" + +export function openUrl(input: string) { + const url = URL.canParse(input) ? new URL(input) : undefined + if (!url || (url.protocol !== "http:" && url.protocol !== "https:")) + return Promise.reject(new Error(`Only http and https links can be opened in the browser: ${input}`)) + return open(url.href) +} diff --git a/packages/core/test/open.test.ts b/packages/core/test/open.test.ts new file mode 100644 index 000000000000..b40222eb6d26 --- /dev/null +++ b/packages/core/test/open.test.ts @@ -0,0 +1,16 @@ +import { describe, expect, test } from "bun:test" +import { openUrl } from "@opencode-ai/core/open" + +describe("openUrl", () => { + test("rejects values that are not URLs", async () => { + await expect(openUrl("not a url")).rejects.toThrow("Only http and https links") + await expect(openUrl("")).rejects.toThrow("Only http and https links") + }) + + test("rejects non-http schemes", async () => { + await expect(openUrl("file:///etc/hosts")).rejects.toThrow("Only http and https links") + await expect(openUrl("javascript:alert(1)")).rejects.toThrow("Only http and https links") + await expect(openUrl("ms-msdt:/id PCWDiagnostic")).rejects.toThrow("Only http and https links") + await expect(openUrl("\\\\server\\share\\file.html")).rejects.toThrow("Only http and https links") + }) +}) diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 8b6e8ae73d13..faf8423bd4b9 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -131,7 +131,6 @@ "mime-types": "3.0.2", "minimatch": "10.0.3", "npm-package-arg": "13.0.2", - "open": "10.1.2", "opencode-gitlab-auth": "2.1.0", "opencode-poe-auth": "0.0.1", "opentui-spinner": "catalog:", diff --git a/packages/opencode/src/cli/cmd/account.ts b/packages/opencode/src/cli/cmd/account.ts index f439c187314d..d15bb1d0176e 100644 --- a/packages/opencode/src/cli/cmd/account.ts +++ b/packages/opencode/src/cli/cmd/account.ts @@ -5,9 +5,9 @@ import { Account } from "@/account/account" import { AccountID, OrgID, PollExpired, type PollResult, type AccountError } from "@/account/schema" import { effectCmd } from "../effect-cmd" import * as Prompt from "../effect/prompt" -import open from "open" +import { openUrl } from "@opencode-ai/core/open" -const openBrowser = (url: string) => Effect.promise(() => open(url).catch(() => undefined)) +const openBrowser = (url: string) => Effect.promise(() => openUrl(url).catch(() => undefined)) const println = (msg: string) => Effect.sync(() => UI.println(msg)) diff --git a/packages/opencode/src/cli/cmd/web.ts b/packages/opencode/src/cli/cmd/web.ts index 69a981aada49..8842b7ca37c7 100644 --- a/packages/opencode/src/cli/cmd/web.ts +++ b/packages/opencode/src/cli/cmd/web.ts @@ -3,7 +3,7 @@ import { UI } from "../ui" import { effectCmd } from "../effect-cmd" import { withNetworkOptions, resolveNetworkOptions } from "../network" import { Flag } from "@opencode-ai/core/flag/flag" -import open from "open" +import { openUrl } from "@opencode-ai/core/open" import { networkInterfaces } from "os" function getNetworkIPs() { @@ -72,11 +72,11 @@ export const WebCommand = effectCmd({ } // Open localhost in browser - open(localhostUrl).catch(() => {}) + openUrl(localhostUrl).catch(() => {}) } else { const displayUrl = server.url.toString() UI.println(UI.Style.TEXT_INFO_BOLD + " Web interface: ", UI.Style.TEXT_NORMAL, displayUrl) - open(displayUrl).catch(() => {}) + openUrl(displayUrl).catch(() => {}) } yield* Effect.never diff --git a/packages/opencode/src/mcp/browser.ts b/packages/opencode/src/mcp/browser.ts index 5760d8cbf9a0..73aecc846bab 100644 --- a/packages/opencode/src/mcp/browser.ts +++ b/packages/opencode/src/mcp/browser.ts @@ -1,6 +1,6 @@ import { LayerNode } from "@opencode-ai/core/effect/layer-node" +import { openUrl } from "@opencode-ai/core/open" import { Context, Effect, Layer } from "effect" -import open from "open" export interface Interface { readonly open: (url: string) => Effect.Effect @@ -13,7 +13,7 @@ const layer = Layer.succeed( Service.of({ open: Effect.fn("McpBrowser.open")(function* (url: string) { const subprocess = yield* Effect.tryPromise({ - try: () => open(url), + try: () => openUrl(url), catch: (error) => (error instanceof Error ? error : new Error(String(error))), }) yield* Effect.callback((resume) => { diff --git a/packages/opencode/src/mcp/oauth-provider.ts b/packages/opencode/src/mcp/oauth-provider.ts index 596bfe1d551f..64a9912a4ecf 100644 --- a/packages/opencode/src/mcp/oauth-provider.ts +++ b/packages/opencode/src/mcp/oauth-provider.ts @@ -125,6 +125,10 @@ export class McpOAuthProvider implements OAuthClientProvider { } async redirectToAuthorization(authorizationUrl: URL): Promise { + if (authorizationUrl.protocol !== "http:" && authorizationUrl.protocol !== "https:") + throw new Error( + `MCP server "${this.serverUrl}" returned a ${authorizationUrl.protocol} authorization URL; only http and https are supported`, + ) await this.callbacks.onRedirect(authorizationUrl) } diff --git a/packages/opencode/src/plugin/digitalocean.ts b/packages/opencode/src/plugin/digitalocean.ts index af241781e61b..6553fd5f25cd 100644 --- a/packages/opencode/src/plugin/digitalocean.ts +++ b/packages/opencode/src/plugin/digitalocean.ts @@ -3,7 +3,7 @@ import type { Model } from "@opencode-ai/sdk/v2" import { InstallationVersion } from "@opencode-ai/core/installation/version" import { OauthCallbackPage } from "@opencode-ai/core/oauth/page" import { createServer } from "http" -import open from "open" +import { openUrl } from "@opencode-ai/core/open" const DO_OAUTH_CLIENT_ID = "b1a6c5158156caac821fd1b30253ca8acb52454a48fa744420e41889cb589f82" const DO_AUTHORIZE_URL = "https://cloud.digitalocean.com/v1/oauth/authorize" @@ -279,7 +279,7 @@ export async function DigitalOceanAuthPlugin(input: PluginInput): Promise const state = generateState() const callbackPromise = waitForOAuthCallback(state) const url = buildAuthorizeUrl(state) - await open(url).catch(() => undefined) + await openUrl(url).catch(() => undefined) return { url, instructions: diff --git a/packages/opencode/src/plugin/snowflake-cortex.ts b/packages/opencode/src/plugin/snowflake-cortex.ts index 09f107ed7b96..c5c4df126ad9 100644 --- a/packages/opencode/src/plugin/snowflake-cortex.ts +++ b/packages/opencode/src/plugin/snowflake-cortex.ts @@ -3,7 +3,7 @@ import { OAUTH_DUMMY_KEY } from "../auth" import { InstallationVersion } from "@opencode-ai/core/installation/version" import { OauthCallbackPage } from "@opencode-ai/core/oauth/page" import { createServer } from "http" -import open from "open" +import { openUrl } from "@opencode-ai/core/open" const OAUTH_CLIENT_ID = "LOCAL_APPLICATION" const OAUTH_CALLBACK_HOST = "127.0.0.1" @@ -470,7 +470,7 @@ export async function SnowflakeCortexAuthPlugin(_input: PluginInput): Promise undefined) + await openUrl(url).catch(() => undefined) return { url, diff --git a/packages/opencode/test/mcp/oauth-provider.test.ts b/packages/opencode/test/mcp/oauth-provider.test.ts index 249c49e8f91d..fc5aa7bc270d 100644 --- a/packages/opencode/test/mcp/oauth-provider.test.ts +++ b/packages/opencode/test/mcp/oauth-provider.test.ts @@ -34,6 +34,37 @@ describe("McpOAuthProvider.redirectUrl", () => { }) }) +describe("McpOAuthProvider.redirectToAuthorization", () => { + test("forwards http and https authorization URLs", async () => { + const seen: string[] = [] + const provider = new McpOAuthProvider( + "test-server", + "https://mcp.example.com/mcp", + {}, + { onRedirect: (url) => void seen.push(url.href) }, + stubAuth, + ) + await provider.redirectToAuthorization(new URL("https://auth.example.com/authorize?state=1")) + await provider.redirectToAuthorization(new URL("http://127.0.0.1:8080/authorize")) + expect(seen).toEqual(["https://auth.example.com/authorize?state=1", "http://127.0.0.1:8080/authorize"]) + }) + + test("rejects authorization URLs that are not http or https", async () => { + const seen: string[] = [] + const provider = new McpOAuthProvider( + "test-server", + "https://mcp.example.com/mcp", + {}, + { onRedirect: (url) => void seen.push(url.href) }, + stubAuth, + ) + await expect(provider.redirectToAuthorization(new URL("file:///tmp/authorize"))).rejects.toThrow( + "returned a file: authorization URL", + ) + expect(seen).toEqual([]) + }) +}) + describe("McpOAuthProvider.clientMetadata", () => { test("includes redirect_uris from redirectUrl", () => { const provider = makeProvider({ callbackPort: 6620 }) diff --git a/packages/tui/package.json b/packages/tui/package.json index c62751124388..d6e61a99f574 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -59,7 +59,6 @@ "diff": "catalog:", "effect": "catalog:", "fuzzysort": "catalog:", - "open": "10.1.2", "opentui-spinner": "catalog:", "remeda": "catalog:", "strip-ansi": "7.1.2", diff --git a/packages/tui/src/app.tsx b/packages/tui/src/app.tsx index 6852311f9e84..c331ed609a50 100644 --- a/packages/tui/src/app.tsx +++ b/packages/tui/src/app.tsx @@ -63,7 +63,7 @@ import { isDefaultTitle } from "./util/session" import { KVProvider, useKV } from "./context/kv" import * as Model from "./util/model" import { ArgsProvider, useArgs, type Args } from "./context/args" -import open from "open" +import { openUrl } from "@opencode-ai/core/open" import { PromptRefProvider, usePromptRef } from "./context/prompt" import { TuiConfigProvider, useTuiConfig, type TuiConfig } from "./config" import { createTuiApiAdapters } from "./plugin/adapters" @@ -821,7 +821,7 @@ function App(props: { onSnapshot?: () => Promise; pluginHost: TuiPlugi name: "docs.open", title: "Open docs", run: () => { - open("https://opencode.ai/docs").catch(() => {}) + openUrl("https://opencode.ai/docs").catch(() => {}) dialog.clear() }, category: "System", diff --git a/packages/tui/src/component/dialog-retry-action.tsx b/packages/tui/src/component/dialog-retry-action.tsx index b52a6e9b9f69..853979af902c 100644 --- a/packages/tui/src/component/dialog-retry-action.tsx +++ b/packages/tui/src/component/dialog-retry-action.tsx @@ -1,5 +1,5 @@ import { RGBA, TextAttributes } from "@opentui/core" -import open from "open" +import { openUrl } from "@opencode-ai/core/open" import { createSignal } from "solid-js" import { selectedForeground, useTheme } from "../context/theme" import { useDialog, type DialogContext } from "../ui/dialog" @@ -21,7 +21,7 @@ export type DialogRetryActionProps = { } function runAction(props: DialogRetryActionProps, dialog: ReturnType) { - if (props.link) open(props.link).catch(() => {}) + if (props.link) openUrl(props.link).catch(() => {}) props.onClose?.() dialog.clear() } diff --git a/packages/tui/src/ui/link.tsx b/packages/tui/src/ui/link.tsx index cfd78bc3335c..aff00000c3ca 100644 --- a/packages/tui/src/ui/link.tsx +++ b/packages/tui/src/ui/link.tsx @@ -1,6 +1,6 @@ import type { JSX } from "solid-js" import type { RGBA } from "@opentui/core" -import open from "open" +import { openUrl } from "@opencode-ai/core/open" export interface LinkProps { href: string @@ -25,7 +25,7 @@ export function Link(props: LinkProps) { width={props.width} wrapMode={props.wrapMode} onMouseUp={() => { - open(props.href).catch(() => {}) + openUrl(props.href).catch(() => {}) }} > {displayText}