diff --git a/bun.lock b/bun.lock index 79aa7d7e38..5cd6bc0342 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 a3f9544410..59bf0501f0 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 0a7e0ea07a..fbcbfc34e6 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 0000000000..296982f3af --- /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 0000000000..b40222eb6d --- /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 8b6e8ae73d..faf8423bd4 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 f439c18731..d15bb1d017 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 69a981aada..8842b7ca37 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 5760d8cbf9..73aecc846b 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 596bfe1d55..64a9912a4e 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 af241781e6..6553fd5f25 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 09f107ed7b..c5c4df126a 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 249c49e8f9..fc5aa7bc27 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 c627511243..d6e61a99f5 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 6852311f9e..c331ed609a 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 b52a6e9b9f..853979af90 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 cfd78bc333..aff00000c3 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}