From daba405e1692e3dc011191b01328d9f409861e64 Mon Sep 17 00:00:00 2001 From: MagMueller Date: Tue, 28 Jul 2026 00:00:28 -0700 Subject: [PATCH 1/5] feat(browser): support scoped fetch proxy --- packages/bcode-browser/src/fetch-use.ts | 73 +++++++++++-------- packages/bcode-browser/test/fetch-use.test.ts | 49 ++++++++++++- 2 files changed, 87 insertions(+), 35 deletions(-) diff --git a/packages/bcode-browser/src/fetch-use.ts b/packages/bcode-browser/src/fetch-use.ts index 83562b9828..0b3ce16b05 100644 --- a/packages/bcode-browser/src/fetch-use.ts +++ b/packages/bcode-browser/src/fetch-use.ts @@ -1,12 +1,13 @@ // FetchUse — Effect service that proxies HTTP through Browser Use's fetch-use // cloud (Chrome JA4, HTTP/2 header order, session cookies). Decisions §3.3. -// `enabled` is true when BROWSER_USE_API_KEY is set; webfetch.ts combines -// this with the user's `experimental.fetch_use` opencode.json setting. +// `enabled` is true when either a direct Browser Use API key or a scoped proxy +// token is set; webfetch.ts combines this with the user's +// `experimental.fetch_use` opencode.json setting. import { Context, Effect, Layer } from "effect" import { HttpClient, HttpClientRequest } from "effect/unstable/http" -const ENDPOINT = "https://fetch.browser-use.com/fetch" +const DEFAULT_ENDPOINT = "https://fetch.browser-use.com/fetch" export interface FetchResult { readonly body: ArrayBuffer @@ -27,33 +28,43 @@ export class Service extends Context.Service Effect.Effect }>()("@browser-use/FetchUse") {} -export const layer = Layer.effect( - Service, - Effect.gen(function* () { - const http = yield* HttpClient.HttpClient - const apiKey = process.env.BROWSER_USE_API_KEY ?? "" - return Service.of({ - enabled: apiKey.length > 0, - fetch: (url, { timeoutMs }) => - Effect.gen(function* () { - const request = yield* HttpClientRequest.post(ENDPOINT).pipe( - HttpClientRequest.setHeaders({ "Content-Type": "application/json", "X-Browser-Use-API-Key": apiKey }), - HttpClientRequest.bodyJson({ url, timeout_ms: timeoutMs }), - ) - const response = yield* HttpClient.filterStatusOk(http).execute(request) - const data = (yield* response.json) as unknown as FetchUseRaw - if (data.error) return yield* Effect.fail(new Error(`fetch-use: ${data.error}`)) - // Mirror native path's filterStatusOk: surface upstream HTTP errors as failures. - if (data.status_code >= 400) return yield* Effect.fail(new Error(`fetch-use: HTTP ${data.status_code}`)) - const body = data.is_binary && data.body_base64 - ? (new Uint8Array(Buffer.from(data.body_base64, "base64")).buffer as ArrayBuffer) - : (new TextEncoder().encode(data.body ?? "").buffer as ArrayBuffer) - const ct = - Object.entries(data.headers ?? {}).find(([k]) => k.toLowerCase() === "content-type")?.[1]?.[0] ?? "" - return { body, contentType: ct } - }).pipe(Effect.mapError((e) => (e instanceof Error ? e : new Error(String(e))))), - }) - }), -) +export const makeLayer = (options: { endpoint: string; apiKey: string; proxyToken: string }) => + Layer.effect( + Service, + Effect.gen(function* () { + const http = yield* HttpClient.HttpClient + return Service.of({ + enabled: options.proxyToken.length > 0 || options.apiKey.length > 0, + fetch: (url, { timeoutMs }) => + Effect.gen(function* () { + const auth = options.proxyToken + ? { Authorization: `Bearer ${options.proxyToken}` } + : { "X-Browser-Use-API-Key": options.apiKey } + const request = yield* HttpClientRequest.post(options.endpoint).pipe( + HttpClientRequest.setHeaders({ "Content-Type": "application/json", ...auth }), + HttpClientRequest.bodyJson({ url, timeout_ms: timeoutMs }), + ) + const response = yield* HttpClient.filterStatusOk(http).execute(request) + const data = (yield* response.json) as unknown as FetchUseRaw + if (data.error) return yield* Effect.fail(new Error(`fetch-use: ${data.error}`)) + // Mirror native path's filterStatusOk: surface upstream HTTP errors as failures. + if (data.status_code >= 400) return yield* Effect.fail(new Error(`fetch-use: HTTP ${data.status_code}`)) + const body = + data.is_binary && data.body_base64 + ? (new Uint8Array(Buffer.from(data.body_base64, "base64")).buffer as ArrayBuffer) + : (new TextEncoder().encode(data.body ?? "").buffer as ArrayBuffer) + const ct = + Object.entries(data.headers ?? {}).find(([k]) => k.toLowerCase() === "content-type")?.[1]?.[0] ?? "" + return { body, contentType: ct } + }).pipe(Effect.mapError((e) => (e instanceof Error ? e : new Error(String(e))))), + }) + }), + ) + +export const layer = makeLayer({ + endpoint: process.env.BROWSER_USE_FETCH_URL ?? DEFAULT_ENDPOINT, + apiKey: process.env.BROWSER_USE_API_KEY ?? "", + proxyToken: process.env.BROWSER_USE_FETCH_TOKEN ?? "", +}) export * as FetchUse from "./fetch-use" diff --git a/packages/bcode-browser/test/fetch-use.test.ts b/packages/bcode-browser/test/fetch-use.test.ts index 753118d579..4538a6f29a 100644 --- a/packages/bcode-browser/test/fetch-use.test.ts +++ b/packages/bcode-browser/test/fetch-use.test.ts @@ -1,6 +1,6 @@ // FetchUse smoke tests. // -// Unit: layer is constructible, `enabled` reflects BROWSER_USE_API_KEY presence. +// Unit: layer is constructible, `enabled` reflects direct and proxy credentials. // Live: when the key is set, end-to-end POST to fetch.browser-use.com returns // body bytes + content-type. Skipped without the key. Config-based // opt-in (experimental.fetch_use=true) is enforced in webfetch.ts, @@ -11,16 +11,16 @@ import { Effect, Layer } from "effect" import { FetchHttpClient } from "effect/unstable/http" import { FetchUse } from "../src/fetch-use" -const haveKey = !!process.env.BROWSER_USE_API_KEY +const haveCredential = !!process.env.BROWSER_USE_API_KEY || !!process.env.BROWSER_USE_FETCH_TOKEN test("layer constructs and exposes `enabled` reflecting env", async () => { const enabled = await Effect.gen(function* () { return (yield* FetchUse.Service).enabled }).pipe(Effect.provide(FetchUse.layer.pipe(Layer.provide(FetchHttpClient.layer))), Effect.runPromise) - expect(enabled).toBe(haveKey) + expect(enabled).toBe(haveCredential) }) -test.skipIf(!haveKey)("live: fetches httpbin and returns body + content-type", async () => { +test.skipIf(!process.env.BROWSER_USE_API_KEY)("live: fetches httpbin and returns body + content-type", async () => { const result = await Effect.gen(function* () { return yield* (yield* FetchUse.Service).fetch("https://httpbin.org/get", { timeoutMs: 30_000 }) }).pipe(Effect.provide(FetchUse.layer.pipe(Layer.provide(FetchHttpClient.layer))), Effect.runPromise) @@ -28,3 +28,44 @@ test.skipIf(!haveKey)("live: fetches httpbin and returns body + content-type", a expect(result.contentType).toContain("application/json") expect(JSON.parse(new TextDecoder().decode(result.body)).url).toBe("https://httpbin.org/get") }) + +test("scoped proxy token uses bearer auth instead of the API-key header", async () => { + const request = { authorization: "", apiKey: "", body: {} as Record } + const server = Bun.serve({ + port: 0, + fetch: async (incoming) => { + request.authorization = incoming.headers.get("authorization") ?? "" + request.apiKey = incoming.headers.get("x-browser-use-api-key") ?? "" + request.body = (await incoming.json()) as Record + return Response.json({ + status_code: 200, + headers: { "content-type": ["text/plain"] }, + body: "proxied", + }) + }, + }) + + try { + const result = await Effect.gen(function* () { + const service = yield* FetchUse.Service + expect(service.enabled).toBe(true) + return yield* service.fetch("https://example.com/page", { timeoutMs: 12_345 }) + }).pipe( + Effect.provide( + FetchUse.makeLayer({ + endpoint: server.url.toString(), + apiKey: "", + proxyToken: "v4rt_test", + }).pipe(Layer.provide(FetchHttpClient.layer)), + ), + Effect.runPromise, + ) + + expect(request.authorization).toBe("Bearer v4rt_test") + expect(request.apiKey).toBe("") + expect(request.body).toEqual({ url: "https://example.com/page", timeout_ms: 12_345 }) + expect(new TextDecoder().decode(result.body)).toBe("proxied") + } finally { + server.stop(true) + } +}) From ead6d39a0cd91d7747d38fa45761c76992bba7ff Mon Sep 17 00:00:00 2001 From: MagMueller Date: Tue, 28 Jul 2026 00:03:15 -0700 Subject: [PATCH 2/5] fix(browser): reject partial fetch proxy --- packages/bcode-browser/src/fetch-use.ts | 16 +++++++++++----- packages/bcode-browser/test/fetch-use.test.ts | 18 +++++++++++++++++- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/packages/bcode-browser/src/fetch-use.ts b/packages/bcode-browser/src/fetch-use.ts index 0b3ce16b05..3b37ef3306 100644 --- a/packages/bcode-browser/src/fetch-use.ts +++ b/packages/bcode-browser/src/fetch-use.ts @@ -28,19 +28,25 @@ export class Service extends Context.Service Effect.Effect }>()("@browser-use/FetchUse") {} -export const makeLayer = (options: { endpoint: string; apiKey: string; proxyToken: string }) => +export const makeLayer = (options: { proxyUrl: string; apiKey: string; proxyToken: string }) => Layer.effect( Service, Effect.gen(function* () { const http = yield* HttpClient.HttpClient + const hasProxySetting = options.proxyUrl.length > 0 || options.proxyToken.length > 0 + const proxyEnabled = options.proxyUrl.length > 0 && options.proxyToken.length > 0 + const directEnabled = !hasProxySetting && options.apiKey.length > 0 return Service.of({ - enabled: options.proxyToken.length > 0 || options.apiKey.length > 0, + enabled: proxyEnabled || directEnabled, fetch: (url, { timeoutMs }) => Effect.gen(function* () { - const auth = options.proxyToken + if (!proxyEnabled && !directEnabled) { + return yield* Effect.fail(new Error("fetch-use credentials are missing or partially configured")) + } + const auth = proxyEnabled ? { Authorization: `Bearer ${options.proxyToken}` } : { "X-Browser-Use-API-Key": options.apiKey } - const request = yield* HttpClientRequest.post(options.endpoint).pipe( + const request = yield* HttpClientRequest.post(proxyEnabled ? options.proxyUrl : DEFAULT_ENDPOINT).pipe( HttpClientRequest.setHeaders({ "Content-Type": "application/json", ...auth }), HttpClientRequest.bodyJson({ url, timeout_ms: timeoutMs }), ) @@ -62,7 +68,7 @@ export const makeLayer = (options: { endpoint: string; apiKey: string; proxyToke ) export const layer = makeLayer({ - endpoint: process.env.BROWSER_USE_FETCH_URL ?? DEFAULT_ENDPOINT, + proxyUrl: process.env.BROWSER_USE_FETCH_URL ?? "", apiKey: process.env.BROWSER_USE_API_KEY ?? "", proxyToken: process.env.BROWSER_USE_FETCH_TOKEN ?? "", }) diff --git a/packages/bcode-browser/test/fetch-use.test.ts b/packages/bcode-browser/test/fetch-use.test.ts index 4538a6f29a..a27765435b 100644 --- a/packages/bcode-browser/test/fetch-use.test.ts +++ b/packages/bcode-browser/test/fetch-use.test.ts @@ -53,7 +53,7 @@ test("scoped proxy token uses bearer auth instead of the API-key header", async }).pipe( Effect.provide( FetchUse.makeLayer({ - endpoint: server.url.toString(), + proxyUrl: server.url.toString(), apiKey: "", proxyToken: "v4rt_test", }).pipe(Layer.provide(FetchHttpClient.layer)), @@ -69,3 +69,19 @@ test("scoped proxy token uses bearer auth instead of the API-key header", async server.stop(true) } }) + +test.each([ + { proxyUrl: "https://proxy.example/fetch", proxyToken: "", apiKey: "bu_secret" }, + { proxyUrl: "", proxyToken: "v4rt_secret", apiKey: "bu_secret" }, +])("partial proxy configuration fails closed", async (options) => { + const result = await Effect.gen(function* () { + const service = yield* FetchUse.Service + expect(service.enabled).toBe(false) + return yield* Effect.flip(service.fetch("https://example.com", { timeoutMs: 1_000 })) + }).pipe( + Effect.provide(FetchUse.makeLayer(options).pipe(Layer.provide(FetchHttpClient.layer))), + Effect.runPromise, + ) + + expect(result.message).toContain("partially configured") +}) From 6d5811635b04690533315c0967dec264757ab4c8 Mon Sep 17 00:00:00 2001 From: MagMueller Date: Tue, 28 Jul 2026 00:10:16 -0700 Subject: [PATCH 3/5] test(browser): match fetch proxy config --- packages/bcode-browser/test/fetch-use.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/bcode-browser/test/fetch-use.test.ts b/packages/bcode-browser/test/fetch-use.test.ts index a27765435b..dc6e20ec84 100644 --- a/packages/bcode-browser/test/fetch-use.test.ts +++ b/packages/bcode-browser/test/fetch-use.test.ts @@ -11,7 +11,10 @@ import { Effect, Layer } from "effect" import { FetchHttpClient } from "effect/unstable/http" import { FetchUse } from "../src/fetch-use" -const haveCredential = !!process.env.BROWSER_USE_API_KEY || !!process.env.BROWSER_USE_FETCH_TOKEN +const haveProxySetting = !!process.env.BROWSER_USE_FETCH_URL || !!process.env.BROWSER_USE_FETCH_TOKEN +const haveCredential = + (!!process.env.BROWSER_USE_FETCH_URL && !!process.env.BROWSER_USE_FETCH_TOKEN) || + (!haveProxySetting && !!process.env.BROWSER_USE_API_KEY) test("layer constructs and exposes `enabled` reflecting env", async () => { const enabled = await Effect.gen(function* () { From eede9973443ce9d63f83aa1f0226e53c3df480ac Mon Sep 17 00:00:00 2001 From: MagMueller Date: Tue, 28 Jul 2026 08:59:05 -0700 Subject: [PATCH 4/5] fix(browser): prevent fetch proxy fallback --- packages/bcode-browser/src/fetch-use.ts | 8 ++++---- packages/bcode-browser/test/fetch-use.test.ts | 11 +++++------ 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/packages/bcode-browser/src/fetch-use.ts b/packages/bcode-browser/src/fetch-use.ts index 3b37ef3306..8d8f87fe31 100644 --- a/packages/bcode-browser/src/fetch-use.ts +++ b/packages/bcode-browser/src/fetch-use.ts @@ -1,8 +1,8 @@ // FetchUse — Effect service that proxies HTTP through Browser Use's fetch-use // cloud (Chrome JA4, HTTP/2 header order, session cookies). Decisions §3.3. -// `enabled` is true when either a direct Browser Use API key or a scoped proxy -// token is set; webfetch.ts combines this with the user's -// `experimental.fetch_use` opencode.json setting. +// `enabled` selects this route when direct credentials are complete or any +// scoped-proxy setting is present. Partial proxy config stays selected and +// errors instead of silently falling back to native HTTP. import { Context, Effect, Layer } from "effect" import { HttpClient, HttpClientRequest } from "effect/unstable/http" @@ -37,7 +37,7 @@ export const makeLayer = (options: { proxyUrl: string; apiKey: string; proxyToke const proxyEnabled = options.proxyUrl.length > 0 && options.proxyToken.length > 0 const directEnabled = !hasProxySetting && options.apiKey.length > 0 return Service.of({ - enabled: proxyEnabled || directEnabled, + enabled: hasProxySetting || directEnabled, fetch: (url, { timeoutMs }) => Effect.gen(function* () { if (!proxyEnabled && !directEnabled) { diff --git a/packages/bcode-browser/test/fetch-use.test.ts b/packages/bcode-browser/test/fetch-use.test.ts index dc6e20ec84..af6e7ba95a 100644 --- a/packages/bcode-browser/test/fetch-use.test.ts +++ b/packages/bcode-browser/test/fetch-use.test.ts @@ -12,15 +12,13 @@ import { FetchHttpClient } from "effect/unstable/http" import { FetchUse } from "../src/fetch-use" const haveProxySetting = !!process.env.BROWSER_USE_FETCH_URL || !!process.env.BROWSER_USE_FETCH_TOKEN -const haveCredential = - (!!process.env.BROWSER_USE_FETCH_URL && !!process.env.BROWSER_USE_FETCH_TOKEN) || - (!haveProxySetting && !!process.env.BROWSER_USE_API_KEY) +const fetchUseSelected = haveProxySetting || !!process.env.BROWSER_USE_API_KEY -test("layer constructs and exposes `enabled` reflecting env", async () => { +test("layer selects fetch-use for direct or scoped-proxy configuration", async () => { const enabled = await Effect.gen(function* () { return (yield* FetchUse.Service).enabled }).pipe(Effect.provide(FetchUse.layer.pipe(Layer.provide(FetchHttpClient.layer))), Effect.runPromise) - expect(enabled).toBe(haveCredential) + expect(enabled).toBe(fetchUseSelected) }) test.skipIf(!process.env.BROWSER_USE_API_KEY)("live: fetches httpbin and returns body + content-type", async () => { @@ -79,7 +77,8 @@ test.each([ ])("partial proxy configuration fails closed", async (options) => { const result = await Effect.gen(function* () { const service = yield* FetchUse.Service - expect(service.enabled).toBe(false) + // Keep this route selected so webfetch cannot silently use native HTTP. + expect(service.enabled).toBe(true) return yield* Effect.flip(service.fetch("https://example.com", { timeoutMs: 1_000 })) }).pipe( Effect.provide(FetchUse.makeLayer(options).pipe(Layer.provide(FetchHttpClient.layer))), From 27ce8380589aa8b9fca4ec490b6dabca156f2656 Mon Sep 17 00:00:00 2001 From: MagMueller Date: Tue, 28 Jul 2026 09:12:22 -0700 Subject: [PATCH 5/5] test(browser): use explicit placeholder credentials --- packages/bcode-browser/test/fetch-use.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/bcode-browser/test/fetch-use.test.ts b/packages/bcode-browser/test/fetch-use.test.ts index af6e7ba95a..876fac977d 100644 --- a/packages/bcode-browser/test/fetch-use.test.ts +++ b/packages/bcode-browser/test/fetch-use.test.ts @@ -56,13 +56,13 @@ test("scoped proxy token uses bearer auth instead of the API-key header", async FetchUse.makeLayer({ proxyUrl: server.url.toString(), apiKey: "", - proxyToken: "v4rt_test", + proxyToken: "test-proxy-token", }).pipe(Layer.provide(FetchHttpClient.layer)), ), Effect.runPromise, ) - expect(request.authorization).toBe("Bearer v4rt_test") + expect(request.authorization).toBe("Bearer test-proxy-token") expect(request.apiKey).toBe("") expect(request.body).toEqual({ url: "https://example.com/page", timeout_ms: 12_345 }) expect(new TextDecoder().decode(result.body)).toBe("proxied") @@ -72,8 +72,8 @@ test("scoped proxy token uses bearer auth instead of the API-key header", async }) test.each([ - { proxyUrl: "https://proxy.example/fetch", proxyToken: "", apiKey: "bu_secret" }, - { proxyUrl: "", proxyToken: "v4rt_secret", apiKey: "bu_secret" }, + { proxyUrl: "https://proxy.example/fetch", proxyToken: "", apiKey: "test-direct-key" }, + { proxyUrl: "", proxyToken: "test-proxy-token", apiKey: "test-direct-key" }, ])("partial proxy configuration fails closed", async (options) => { const result = await Effect.gen(function* () { const service = yield* FetchUse.Service