@@ -257,6 +257,46 @@ describe("HttpApi Server.listen", () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Regression for #25698 (Ope): the app's SDK call to
|
||||||
|
// `client.pty.connectToken({ ptyID })` originally omitted `directory`, so
|
||||||
|
// the server resolved the PTY in its own cwd context — where the project
|
||||||
|
// PTY isn't registered — and returned 404. The fix is to always pass
|
||||||
|
// `directory` from the app side; this test locks in two contracts:
|
||||||
|
// 1. Mint without directory cannot find a PTY registered in another dir.
|
||||||
|
// 2. Mint with the project directory succeeds; the resulting ticket
|
||||||
|
// consumes cleanly when the WS upgrade carries the same directory.
|
||||||
|
testPty("PTY connect token requires matching directory across mint and connect", async () => {
|
||||||
|
await using tmp = await tmpdir({ git: true, config: { formatter: false, lsp: false } })
|
||||||
|
const listener = await startListener()
|
||||||
|
try {
|
||||||
|
const info = await createCat(listener, tmp.path)
|
||||||
|
|
||||||
|
// Mint without directory — server uses its own cwd, can't find the PTY.
|
||||||
|
const ambiguous = await fetch(new URL(PtyPaths.connectToken.replace(":ptyID", info.id), listener.url), {
|
||||||
|
method: "POST",
|
||||||
|
headers: { authorization: authorization(), "x-opencode-ticket": "1" },
|
||||||
|
})
|
||||||
|
expect(ambiguous.status).toBe(404)
|
||||||
|
|
||||||
|
// Mint with the project directory — succeeds, ticket binds to that scope.
|
||||||
|
const scoped = await fetch(
|
||||||
|
new URL(`${PtyPaths.connectToken.replace(":ptyID", info.id)}?directory=${encodeURIComponent(tmp.path)}`, listener.url),
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: { authorization: authorization(), "x-opencode-ticket": "1" },
|
||||||
|
},
|
||||||
|
)
|
||||||
|
expect(scoped.status).toBe(200)
|
||||||
|
const mint = (await scoped.json()) as { ticket: string }
|
||||||
|
|
||||||
|
// Same directory on the WS upgrade → consume succeeds.
|
||||||
|
const ws = await openSocket(socketURL(listener, info.id, tmp.path, mint.ticket))
|
||||||
|
ws.close(1000)
|
||||||
|
} finally {
|
||||||
|
await stop(listener, "timed out cleaning up directory-scope listener").catch(() => undefined)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
testPty("keeps PTY websocket tickets optional when server auth is disabled", async () => {
|
testPty("keeps PTY websocket tickets optional when server auth is disabled", async () => {
|
||||||
await using tmp = await tmpdir({ git: true, config: { formatter: false, lsp: false } })
|
await using tmp = await tmpdir({ git: true, config: { formatter: false, lsp: false } })
|
||||||
const listener = await startNoAuthListener()
|
const listener = await startNoAuthListener()
|
||||||
|
|||||||
@@ -184,6 +184,52 @@ describe("HttpApi UI fallback", () => {
|
|||||||
expect(await response.text()).toBe("console.log('ok')")
|
expect(await response.text()).toBe("console.log('ok')")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Regression for #25698 (Ope): upstream `transfer-encoding: chunked` was
|
||||||
|
// forwarded through the proxy while the proxy itself re-frames the body,
|
||||||
|
// causing browsers to fail with `ERR_INVALID_CHUNKED_ENCODING`.
|
||||||
|
test("strips upstream transfer-encoding header from proxied assets", async () => {
|
||||||
|
Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true
|
||||||
|
Flag.OPENCODE_DISABLE_EMBEDDED_WEB_UI = true
|
||||||
|
|
||||||
|
const response = await Effect.runPromise(
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const fs = yield* AppFileSystem.Service
|
||||||
|
const client = yield* HttpClient.HttpClient
|
||||||
|
return yield* serveUIEffect(HttpServerRequest.fromWeb(new Request("http://localhost/")), {
|
||||||
|
fs,
|
||||||
|
client,
|
||||||
|
})
|
||||||
|
}).pipe(
|
||||||
|
Effect.provide(
|
||||||
|
Layer.mergeAll(
|
||||||
|
AppFileSystem.defaultLayer,
|
||||||
|
Layer.succeed(
|
||||||
|
HttpClient.HttpClient,
|
||||||
|
HttpClient.make((request) =>
|
||||||
|
Effect.succeed(
|
||||||
|
HttpClientResponse.fromWeb(
|
||||||
|
request,
|
||||||
|
new Response("<html>opencode</html>", {
|
||||||
|
headers: {
|
||||||
|
"transfer-encoding": "chunked",
|
||||||
|
"content-type": "text/html",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Effect.map(HttpServerResponse.toWeb),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response.status).toBe(200)
|
||||||
|
expect(response.headers.get("transfer-encoding")).toBeNull()
|
||||||
|
expect(await response.text()).toBe("<html>opencode</html>")
|
||||||
|
})
|
||||||
|
|
||||||
test("serves embedded UI assets when Bun can read them but access reports missing", async () => {
|
test("serves embedded UI assets when Bun can read them but access reports missing", async () => {
|
||||||
Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true
|
Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true
|
||||||
let readPath: string | undefined
|
let readPath: string | undefined
|
||||||
@@ -257,6 +303,25 @@ describe("HttpApi UI fallback", () => {
|
|||||||
expect(response.status).toBe(200)
|
expect(response.status).toBe(200)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Regression for #25698 (Ope): the browser fetches the PWA manifest and
|
||||||
|
// its icons via flows that don't carry app-managed credentials (the
|
||||||
|
// `<link rel="manifest">` request is not under page-auth control), so the
|
||||||
|
// server returning 401 breaks PWA install. These specific public assets
|
||||||
|
// should bypass auth.
|
||||||
|
test("serves the PWA manifest without auth even when a server password is set", async () => {
|
||||||
|
Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true
|
||||||
|
Flag.OPENCODE_DISABLE_EMBEDDED_WEB_UI = true
|
||||||
|
|
||||||
|
for (const path of ["/site.webmanifest", "/web-app-manifest-192x192.png", "/web-app-manifest-512x512.png"]) {
|
||||||
|
const response = await uiApp({
|
||||||
|
password: "secret",
|
||||||
|
username: "opencode",
|
||||||
|
client: httpClient(new Response("ok")),
|
||||||
|
}).request(path)
|
||||||
|
expect(response.status).not.toBe(401)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
test("allows web UI preflight without auth", async () => {
|
test("allows web UI preflight without auth", async () => {
|
||||||
Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true
|
Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user