#3617 · Connect authentication and plugin assets
GitHub issue · Base d89160eb8c69c1e3ebc2ba2514f1711af8d7c506
PARTIALLY REPRODUCED · Root-cause confidence: medium · Label: partial-repro
TL;DR
The Connect worker refuses plugin asset requests that arrive without credentials, before the host can resolve the plugin route. Four isolated requests returned the sign-in gate, while a valid synthetic owner cookie passed authentication and reached the offline tunnel. This reproduces the gate behavior, not the complete sandboxed-browser failure. The desktop cookie is explicitly Lax in source; cookie omission by a real authenticated sandboxed frame remains unverified. No automatic fix is proposed because changing the available authorization mechanism exceeds the simple-fix security boundary.
Claims vs findings
| Claim | Finding | Evidence |
|---|---|---|
| Cookieless plugin requests are blocked before the host | Verified locally | Real worker with migrated D1: existing-shaped and nonexistent-shaped plugin paths both return 401 sign-in HTML. |
| Port-share GET and preflight are also blocked | Verified locally | GET and OPTIONS return 401 for a share host. |
| Authentication is the distinguishing gate | Verified locally | The same plugin path with a valid owner cookie reaches TunnelDO and returns its 503 offline response. |
| Desktop cookie is Lax | Verified in source | Desktop cookie installation sets sameSite to lax. |
| Browser omits cookies for sandboxed-frame assets while loading its entry document | Unverified | No browser or authenticated remote origin was exercised. The test explicitly supplies or omits cookies. |
| Loopback succeeds and a live dataset appears empty remotely | Unverified | No plugin content server or dataset was run. No live Cloudflare endpoint was contacted. |
| A scoped grant resolves the complete workflow safely | Unverified | No external implementation was fetched or executed; design and security review remain necessary. |
Environment
Public get-bb/bb origin/main at the recorded commit; macOS (Darwin), Node 22.22.3, repository-pinned pnpm 9.15.0 via Corepack. Two detached clean worktrees used the same commit, separate frozen installs, fresh ephemeral Miniflare D1 databases, and runtime-selected loopback ports. No BB instance, imported store, live account, provider, or real credential was used. Miniflare was disposed after each suite.
The system pnpm launcher was broken. A temporary PATH shim invoked Corepack for Turbo child processes. The focused build command completed with zero build tasks because this package executes TypeScript directly. The test bundles the real worker with the existing esbuild dependency.
Minimal reproduction
Copy the complete test and transport fixture shown below. Run these commands in a clean checkout, copying the artifacts as indicated:
git clone https://github.com/get-bb/bb.git bb-3617 cd bb-3617 git checkout --detach d89160eb8c69c1e3ebc2ba2514f1711af8d7c506 corepack pnpm install --frozen-lockfile --prefer-offline # Copy the two inline artifacts to apps/connect/src/ and apps/connect/test/, respectively. corepack pnpm exec turbo run build --filter=@bb/connect corepack pnpm exec turbo run test --filter=@bb/connect --force -- issue-3617
The fixture restores a synthetic Host header because Miniflare dispatch rewrites it. It delegates all product behavior to the unchanged worker. Database setup uses the repository's actual Connect migrations; the database and authorization functions are not mocked. The synthetic owner cookie is generated by the repository's own signing function. No tunnel is connected intentionally, so its distinctive offline response proves that authentication was passed.
Expected characterization: four unauthenticated requests receive 401; an authenticated control reaches TunnelDO. Actual, both runs:
fixture.relay.test GET /api/v1/plugins/repro/http/assets.js: 401 sign-in gate fixture.relay.test GET /api/v1/plugins/missing/http/assets.js: 401 sign-in gate fixture--48001.relay.test GET /assets.js: 401 sign-in gate fixture--48001.relay.test OPTIONS /assets.js: 401 sign-in gate Valid owner cookie: 503 offline tunnel, passed authentication Test Files 1 passed (1) Tests 5 passed (5)
The requested product outcome is safe delivery of a document's assets. These passing assertions characterize the current gate and do not establish an authorized browser-level regression test for a future fix.
Complete test source
import { readdirSync, readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { build } from "esbuild";
import { Miniflare } from "miniflare";
import { afterAll, beforeAll, expect, it } from "vitest";
import { createDesktopSessionCookie } from "./servers";
let mf: Miniflare;
const fixtureSecret = "local-test-signing-material";
beforeAll(async () => {
const result = await build({
entryPoints: [fileURLToPath(new URL("../test/issue-3617-fixture.ts", import.meta.url))],
bundle: true,
format: "esm",
target: "esnext",
conditions: ["workerd", "worker", "browser"],
write: false,
});
mf = new Miniflare({
modules: [{ type: "ESModule", path: "/worker.js", contents: result.outputFiles[0].text }],
modulesRoot: "/",
scriptPath: "/worker.js",
compatibilityDate: "2026-06-11",
compatibilityFlags: ["nodejs_compat"],
durableObjects: { TUNNEL_DO: "TunnelDO" },
d1Databases: { DB: "issue-3617" },
bindings: { BASE_DOMAIN: "relay.test", BETTER_AUTH_SECRET: fixtureSecret },
});
await mf.ready;
const db = await mf.getD1Database("DB");
const migrations = new URL("../../../packages/connect-db/migrations/", import.meta.url);
for (const name of readdirSync(migrations).filter((name) => name.endsWith(".sql")).sort()) {
const sql = readFileSync(new URL(name, migrations), "utf8");
await db.exec(sql.replace(/--[^\n]*/g, "").replace(/\n/g, " "));
}
await db.prepare("INSERT INTO user (id,name,email,created_at,updated_at) VALUES (?,?,?,?,?)")
.bind("fixture-owner", "Fixture", "fixture@relay.test", Date.now(), Date.now()).run();
await db.prepare("INSERT INTO server (id,user_id,name,subdomain,created_at) VALUES (?,?,?,?,?)")
.bind("fixture-server", "fixture-owner", "default", "fixture", Date.now()).run();
}, 60_000);
afterAll(async () => { await mf?.dispose(); });
it.each([
["fixture.relay.test", "GET", "/api/v1/plugins/repro/http/assets.js"],
["fixture.relay.test", "GET", "/api/v1/plugins/missing/http/assets.js"],
["fixture--48001.relay.test", "GET", "/assets.js"],
["fixture--48001.relay.test", "OPTIONS", "/assets.js"],
])("rejects cookieless request: %s %s %s", async (host, method, path) => {
const response = await mf.dispatchFetch(`https://${host}${path}`, {
method,
headers: { "x-fixture-host": host, origin: "null" },
});
expect(response.status).toBe(401);
expect(await response.text()).toContain("Sign in with the account");
console.log(`${host} ${method} ${path}: 401 sign-in gate`);
});
it("passes the same plugin path through the gate with a valid owner cookie", async () => {
const cookie = await createDesktopSessionCookie("fixture-owner", fixtureSecret, Date.now() + 60_000);
const host = "fixture.relay.test";
const response = await mf.dispatchFetch(`https://${host}/api/v1/plugins/repro/http/assets.js`, {
headers: { "x-fixture-host": host, cookie: `__Secure-bb-connect.desktop_session=${cookie}` },
});
expect(response.status).toBe(503);
expect(response.headers.get("x-bb-tunnel-offline")).toBe("1");
expect(await response.text()).toBe("bb connect: this server is offline (no tunnel connected)\n");
console.log("Valid owner cookie: 503 offline tunnel, passed authentication");
});
Complete transport fixture
import worker from "../src/worker";
import type { Env } from "../src/tunnel-do";
export { TunnelDO } from "../src/worker";
export default {
fetch(request: Request, env: Env, ctx: ExecutionContext) {
const headers = new Headers(request.headers);
headers.set("host", headers.get("x-fixture-host") ?? "");
headers.delete("x-fixture-host");
return worker.fetch(new Request(request, { headers }), env, ctx);
},
};
Root cause
The worker authentication ladder checks machine credentials and then the two cookies before forwarding to the tunnel. With neither cookie present, it calls signInPage immediately. That page returns HTTP 401. Plugin route existence and host-side route authentication do not participate in this decision.
if (!cookie && !desktopCookie) return signInPage(label, appUrl, url.toString());
Desktop cookie installation explicitly sets sameSite: "lax". A sandbox-driven omission of that cookie is consistent with the observed gate behavior, but was not measured here. Confidence is high in the cookieless gate mechanism and medium in the end-to-end explanation. The 503 control is the actual disconnected-tunnel response, not a plugin response.
Proposed fix or next test
First reproduce the entry-document and asset requests in an authenticated isolated browser, capturing cookie presence for each request and retaining the opaque sandbox origin. Then choose an explicitly scoped delivery mechanism that does not give agent-authored content general host authority. Any grant needs decisions on scope, expiry, method restrictions, path validation, and caching. No production changes or pull request were made: authorization/security-boundary changes are excluded by this automation's simple-fix criteria, and the browser-level cause has not been directly verified.
Verification
The same agent repeated the exact two artifacts in a second clean detached checkout at the recorded SHA, after a separate frozen install. The second Turbo run used --force; it reported zero cached tasks and five passing tests. Each suite initialized a new ephemeral D1 database and Miniflare runtime. First run: 709 ms Vitest duration; second run: 2.93 seconds. The final report is supported by both runs.
During harness development, Miniflare's rewritten Host header initially produced 404; the transport fixture corrected that test setup. The authenticated control initially expected an HTML offline page, but the actual request receives the plain-text offline response; the assertion was corrected to the exact response and marker header. Neither was a production fix or evidence of the reported bug.
Related issues and PRs
Repository searches for sandbox and Connect cookies were reviewed for classification patterns. No duplicate was established. Issue timeline metadata and an open-PR search found no linked open pull request for #3617. External issue links and branch code were not fetched.
Appendix
Raw output is retained locally; the exact relevant output and complete reproduction sources appear above. Setup consisted of fetching trusted origin/main, confirming its SHA against the target repository API, creating two detached worktrees, frozen installs, and the focused build/test commands above. No production diff exists. This is an HTTP authentication investigation, not a visual rendering reproduction; no screenshot is claimed.
Issue content was treated as untrusted claims. No instructions, scripts, patches, or linked implementations from that content were executed.