Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 34 additions & 22 deletions bun.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@
"@types/node": "catalog:",
"bun-types": "1.4.2",
"effect": "catalog:",
"open": "11.0.4",
"solid-js": "catalog:"
},
"patchedDependencies": {
Expand Down
1 change: 0 additions & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@
"effect": "catalog:",
"immer": "11.1.4",
"jsonc-parser": "3.3.1",
"open": "10.1.2",
"solid-js": "catalog:",
"tree-sitter-bash": "0.25.0",
"tree-sitter-powershell": "0.25.10",
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/ui/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ export function requireInteractive(message: string) {
}

export const openUrl = Effect.fn("cli.prompt.open-url")(function* (url: string) {
const { default: open } = yield* Effect.promise(() => import("open"))
yield* Effect.promise(() => open(url)).pipe(Effect.ignore)
const browser = yield* Effect.promise(() => import("@opencode/util/open"))
yield* Effect.promise(() => browser.openUrl(url)).pipe(Effect.ignore)
})

export function handlePromptErrors<A, E, R>(effect: Effect.Effect<A, E, R>) {
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/mcp/oauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,8 @@ export const provider = (options: Options): OAuthClientProvider => {
saveTokens: (tokens) => options.store.saveTokens(tokens),
redirectToAuthorization: (url) => {
if (!redirect) throw refuse("user authorization")
if (url.protocol !== "http:" && url.protocol !== "https:")
throw new Error(`MCP server "${options.config.url}" returned a ${url.protocol} authorization URL; only http and https are supported`)
return redirect.open(url)
},
...(options.invalidate ? { invalidateCredentials: options.invalidate } : {}),
Expand Down
8 changes: 8 additions & 0 deletions packages/core/test/mcp-oauth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,14 @@ describe("MCP OAuth", () => {
await expect(authorize("not a URL")).rejects.toThrow(TypeError)
})

test("rejects an authorization endpoint that is not http or https", async () => {
const { server } = authorizationServer({ authorization_endpoint: "file:///tmp/authorize" })

await expect(Effect.runPromise(Effect.scoped(start(server))).finally(() => server.stop(true))).rejects.toThrow(
"returned a file: authorization URL",
)
})

test("sends the configured URL as the resource when the server publishes no metadata", async () => {
const { server, tokenRequests } = authorizationServer({})
const url = `${server.url.origin}/mcp`
Expand Down
1 change: 0 additions & 1 deletion packages/tui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,6 @@
"effect": "catalog:",
"fuzzysort": "catalog:",
"get-east-asian-width": "catalog:",
"open": "10.1.2",
"opentui-spinner": "catalog:",
"remeda": "catalog:",
"solid-js": "catalog:",
Expand Down
4 changes: 2 additions & 2 deletions packages/tui/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ import { Toast, ToastProvider, useToast } from "./ui/toast"
import { isFallbackTitle } from "@opencode/util/session-title-fallback"
import * as Model from "./util/model"
import { ArgsProvider, useArgs, type Args } from "./context/args"
import open from "open"
import { openUrl } from "@opencode/util/open"
import { PromptRefProvider, usePromptRef } from "./context/prompt"
import { Config, ConfigProvider, useConfig } from "./config"
import { newSessionLocation } from "./config/new-session-location"
Expand Down Expand Up @@ -1092,7 +1092,7 @@ function App() {
name: "docs.open",
title: "Open docs",
run: () => {
open("https://opencode.ai/docs").catch(() => {})
openUrl("https://opencode.ai/docs").catch(() => {})
dialog.clear()
},
category: "System",
Expand Down
6 changes: 3 additions & 3 deletions packages/tui/src/component/dialog-integration.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import type {
FormValue,
LocationRef,
} from "@opencode/client"
import open from "open"
import { openUrl } from "@opencode/util/open"
import { createEffect, createMemo, createSignal, onCleanup, onMount, Show } from "solid-js"
import { useClipboard } from "../context/clipboard"
import { useData } from "../context/data"
Expand Down Expand Up @@ -590,7 +590,7 @@ function OAuthAuto(props: {
title: "Open authorization URL",
group: "Dialog",
run: () => {
open(props.attempt.url).catch(() =>
openUrl(props.attempt.url).catch(() =>
toast.show({
message: "Could not open the browser. Copy the URL and continue manually.",
variant: "error",
Expand Down Expand Up @@ -985,7 +985,7 @@ async function externalAnswer(
() => <OAuthView title={formLabel(field) || title} message="Opening link…" />,
() => resolve(CANCELLED),
)
void open(field.url).then(
void openUrl(field.url).then(
() => resolve(true),
() => resolve(false),
)
Expand Down
4 changes: 2 additions & 2 deletions packages/tui/src/mini/footer.form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -228,8 +228,8 @@ export function RunFormBody(props: {
try {
if (props.openExternal) await props.openExternal(field.url)
else {
const { default: open } = await import("open")
await open(field.url)
const { openUrl } = await import("@opencode/util/open")
await openUrl(field.url)
}
setState((previous) => formSetExternalReady(previous, field.key))
} catch {
Expand Down
4 changes: 2 additions & 2 deletions packages/tui/src/routes/session/form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
type ScrollBoxRenderable,
type TextareaRenderable,
} from "@opentui/core"
import open from "open"
import { openUrl } from "@opencode/util/open"
import { useTheme } from "../../context/theme"
import type { FormAnswer, FormField, FormValue } from "@opencode/client"
import { useData, type FormWithLocation } from "../../context/data"
Expand Down Expand Up @@ -478,7 +478,7 @@ export function FormPrompt(props: { form: FormWithLocation }) {
const current = externalField()
if (!current) return
setStore("error", "")
void open(current.url)
void openUrl(current.url)
.then(() => setStore("externalReady", { ...store.externalReady, [current.key]: true }))
.catch(() => setStore("error", "Could not open the browser. Copy the URL and continue manually."))
}
Expand Down
4 changes: 2 additions & 2 deletions packages/tui/src/ui/link.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { JSX } from "solid-js"
import type { RGBA } from "@opentui/core"
import open from "open"
import { openUrl } from "@opencode/util/open"

export interface LinkProps {
href: string
Expand All @@ -25,7 +25,7 @@ export function Link(props: LinkProps) {
width={props.width}
wrapMode={props.wrapMode}
onMouseUp={() => {
open(props.href).catch(() => {})
openUrl(props.href).catch(() => {})
}}
>
<a href={props.href}>{displayText}</a>
Expand Down
4 changes: 2 additions & 2 deletions packages/tui/src/ui/working-directory-actions.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createSignal } from "solid-js"
import open from "open"
import { openPath } from "@opencode/util/open"
import { useRenderer } from "@opentui/solid"
import { useClipboard } from "../context/clipboard"
import { useDialog } from "./dialog"
Expand Down Expand Up @@ -39,7 +39,7 @@ export function useWorkingDirectoryActions(input: { directory: () => string | un
description: "in system file manager",
onSelect: (dialog) => {
dialog.clear()
void open(directory).catch(toast.error)
void openPath(directory).catch(toast.error)
},
},
...(input.onMove
Expand Down
1 change: 1 addition & 0 deletions packages/util/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
"mime-types": "3.0.2",
"minimatch": "10.2.5",
"npm-package-arg": "13.0.2",
"open": "11.0.4",
"pacote": "21.5.1"
},
"devDependencies": {
Expand Down
16 changes: 16 additions & 0 deletions packages/util/src/open.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { describe, expect, test } from "bun:test"
import { openUrl } from "./open.js"

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")
})
})
12 changes: 12 additions & 0 deletions packages/util/src/open.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
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)
}

export function openPath(path: string) {
return open(path)
}
Loading