mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-10 07:11:25 +03:00
fix(core): present client certificates on both the Bun and Node runtimes
This commit is contained in:
@@ -249,6 +249,7 @@
|
||||
"jose": "6.0.11",
|
||||
"jsonc-parser": "3.3.1",
|
||||
"mime-types": "3.0.2",
|
||||
"undici": "8.12.3",
|
||||
"which": "6.0.1",
|
||||
"zod": "catalog:",
|
||||
},
|
||||
|
||||
@@ -64,6 +64,12 @@
|
||||
"node": "./src/image/photon-wasm.node.ts",
|
||||
"default": "./src/image/photon-wasm.bun.ts"
|
||||
},
|
||||
"#client-tls": {
|
||||
"workerd": "./src/effect/client-tls/client-tls.workerd.ts",
|
||||
"bun": "./src/effect/client-tls/client-tls.bun.ts",
|
||||
"node": "./src/effect/client-tls/client-tls.node.ts",
|
||||
"default": "./src/effect/client-tls/client-tls.bun.ts"
|
||||
},
|
||||
"#v1-migration": {
|
||||
"types": "./src/database/v1-migration.bun.ts",
|
||||
"bun": "./src/database/v1-migration.bun.ts",
|
||||
@@ -129,6 +135,7 @@
|
||||
"jose": "6.0.11",
|
||||
"jsonc-parser": "3.3.1",
|
||||
"mime-types": "3.0.2",
|
||||
"undici": "8.12.3",
|
||||
"which": "6.0.1",
|
||||
"zod": "catalog:"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,7 @@ export * as AISDK from "./aisdk.js"
|
||||
import { makeLocationNode } from "@opencode/util/effect/app-node"
|
||||
import { httpFetch } from "@opencode/util/effect/app-node-platform"
|
||||
import { APICallError } from "@ai-sdk/provider"
|
||||
import { ClientTls } from "#client-tls"
|
||||
import type {
|
||||
JSONSchema7,
|
||||
JSONValue,
|
||||
@@ -161,9 +162,12 @@ function prepareOptions(model: RuntimeInfo, pkg: string, outbound: Fetch) {
|
||||
}
|
||||
}
|
||||
|
||||
const https = String(input instanceof Request ? input.url : input).startsWith("https:")
|
||||
const certified = tls && https ? ClientTls.sender(tls) : undefined
|
||||
if (tls && https && !certified) throw new Error("Client certificates aren't supported on this runtime")
|
||||
// A followed redirect keeps the TLS options, so it would present the client certificate to another host.
|
||||
if (tls) Object.assign(opts, { tls, redirect: "error" })
|
||||
const send: Fetch = isFetch(customFetch) ? customFetch : outbound
|
||||
if (certified) Object.assign(opts, certified.init, { redirect: "error" })
|
||||
const send: Fetch = certified ? certified.fetch : isFetch(customFetch) ? customFetch : outbound
|
||||
const middleware = httpMiddleware.getStore()
|
||||
const res = await (
|
||||
middleware
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { LLMClient, RequestExecutor } from "@opencode/ai/route"
|
||||
import { Effect, Layer, Option } from "effect"
|
||||
import { FetchHttpClient, HttpClient } from "effect/http"
|
||||
import { FetchHttpClient, HttpClient, HttpClientError } from "effect/http"
|
||||
import { Socket } from "effect/socket"
|
||||
import { makeGlobalNode } from "@opencode/util/effect/app-node"
|
||||
import { httpClient } from "@opencode/util/effect/app-node-platform"
|
||||
import { ClientTls } from "#client-tls"
|
||||
import { WebSocketConstructor } from "./websocket-constructor.js"
|
||||
|
||||
// Bun's fetch ends a request after five quiet minutes, before provider routes' own header and chunk timeouts.
|
||||
@@ -18,8 +19,8 @@ const withoutBunTimeout = Layer.effect(
|
||||
}),
|
||||
)
|
||||
|
||||
// Presents the model's client certificate (provider `settings.tls`) on its HTTPS requests. The TLS handshake only
|
||||
// sends it to servers that request one.
|
||||
// Presents the model's client certificate (provider `settings.tls`) on its HTTPS requests, through this runtime's
|
||||
// certificate-capable fetch. The TLS handshake only sends it to servers that request one.
|
||||
const withClientCertificate = Layer.effect(
|
||||
HttpClient.HttpClient,
|
||||
Effect.gen(function* () {
|
||||
@@ -28,18 +29,25 @@ const withClientCertificate = Layer.effect(
|
||||
Effect.gen(function* () {
|
||||
const tls = yield* RequestExecutor.CurrentTls
|
||||
if (!tls || !request.url.startsWith("https:")) return yield* effect
|
||||
const init: BunFetchRequestInit = Option.getOrElse(
|
||||
yield* Effect.serviceOption(FetchHttpClient.RequestInit),
|
||||
() => ({}),
|
||||
)
|
||||
return yield* effect.pipe(
|
||||
const sender = ClientTls.sender(tls)
|
||||
if (!sender)
|
||||
return yield* new HttpClientError.HttpClientError({
|
||||
reason: new HttpClientError.TransportError({
|
||||
request,
|
||||
description: "Client certificates aren't supported on this runtime",
|
||||
}),
|
||||
})
|
||||
const init: RequestInit = Option.getOrElse(yield* Effect.serviceOption(FetchHttpClient.RequestInit), () => ({}))
|
||||
return yield* HttpClient.execute(request).pipe(
|
||||
Effect.provide(FetchHttpClient.layer),
|
||||
Effect.provideService(FetchHttpClient.Fetch, sender.fetch),
|
||||
Effect.provideService(FetchHttpClient.RequestInit, {
|
||||
...init,
|
||||
tls: { ...init.tls, ...tls },
|
||||
...sender.init,
|
||||
// A followed redirect keeps the TLS options, so it would present the certificate to whatever host the
|
||||
// server names.
|
||||
redirect: init.redirect === "manual" ? "manual" : "error",
|
||||
} as BunFetchRequestInit),
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import type { Sender, Tls } from "./client-tls.js"
|
||||
|
||||
// Bun's fetch takes the client certificate per request.
|
||||
export function sender(tls: Tls): Sender | undefined {
|
||||
return { fetch: globalThis.fetch, init: { tls } }
|
||||
}
|
||||
|
||||
export * as ClientTls from "./client-tls.bun.js"
|
||||
@@ -0,0 +1,17 @@
|
||||
// The deep path keeps Vite, which runs under Bun where "undici" is a built-in, from leaving the import external.
|
||||
import { Agent, fetch } from "undici/index.js"
|
||||
import type { Sender, Tls } from "./client-tls.js"
|
||||
|
||||
// Node's fetch only takes a client certificate through an undici dispatcher, and a dispatcher from the npm package
|
||||
// only works with that package's own fetch, not Node's bundled one. One agent per certificate keeps its connections.
|
||||
const agents = new Map<string, Agent>()
|
||||
|
||||
export function sender(tls: Tls): Sender | undefined {
|
||||
const id = JSON.stringify([tls.cert, tls.key, tls.passphrase])
|
||||
const agent = agents.get(id) ?? new Agent({ connect: { cert: tls.cert, key: tls.key, passphrase: tls.passphrase } })
|
||||
agents.set(id, agent)
|
||||
// SAFETY: undici's fetch implements the WHATWG fetch contract that FetchHttpClient and the AI SDK call.
|
||||
return { fetch: fetch as unknown as typeof globalThis.fetch, init: { dispatcher: agent } }
|
||||
}
|
||||
|
||||
export * as ClientTls from "./client-tls.node.js"
|
||||
@@ -0,0 +1,12 @@
|
||||
/** PEM client certificate and private key, with the passphrase of an encrypted key. */
|
||||
export interface Tls {
|
||||
readonly cert: string
|
||||
readonly key: string
|
||||
readonly passphrase?: string
|
||||
}
|
||||
|
||||
/** The `fetch` and request options that present a client certificate on this runtime. */
|
||||
export interface Sender {
|
||||
readonly fetch: typeof globalThis.fetch
|
||||
readonly init: Record<string, unknown>
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import type { Sender, Tls } from "./client-tls.js"
|
||||
|
||||
// Workers' fetch can't present a client certificate. Embedders use an mTLS certificate binding's fetch instead.
|
||||
export function sender(_tls: Tls): Sender | undefined {
|
||||
return undefined
|
||||
}
|
||||
|
||||
export * as ClientTls from "./client-tls.workerd.js"
|
||||
@@ -117,6 +117,7 @@ TLS handshake only sends it to servers that ask for one.
|
||||
|
||||
- `cert` and `key` are PEM text. Use `{file:...}` to read them from files. A file that can't be read leaves `tls` unset.
|
||||
- `passphrase` is only needed for an encrypted key.
|
||||
- Works on the Bun and Node builds. A server on Cloudflare Workers can't present a client certificate through `fetch`.
|
||||
- Requests with a client certificate don't follow redirects, so the certificate isn't presented to another host.
|
||||
|
||||
Every model request of that provider presents the certificate: chat, compaction, image and decision models. A provider
|
||||
|
||||
Reference in New Issue
Block a user