From a13107731f058906ae360e868c4373bb468756c8 Mon Sep 17 00:00:00 2001 From: backryun Date: Mon, 10 Aug 2026 12:28:21 +0900 Subject: [PATCH] fix(types): type Copilot WebSocket construction (#9974) --- open-sse/executors/copilot-web.ts | 33 ++++++++++++++++--------- tests/unit/copilot-web-executor.test.ts | 15 +++++++++-- 2 files changed, 34 insertions(+), 14 deletions(-) diff --git a/open-sse/executors/copilot-web.ts b/open-sse/executors/copilot-web.ts index dbeeaf6b06..b895dcc7c5 100644 --- a/open-sse/executors/copilot-web.ts +++ b/open-sse/executors/copilot-web.ts @@ -67,6 +67,11 @@ interface CopilotWsEvent { [key: string]: unknown; } +type NodeWebSocketConstructor = new ( + url: string | URL, + options?: { headers?: Record } +) => WebSocket; + // ─── Helpers ──────────────────────────────────────────────────────────────── export function getCopilotMode(model?: string): string { @@ -106,6 +111,12 @@ export function extractAccessToken(credential: string): string | null { return credential; } +/* @testonly */ export function buildCopilotWebSocketHeaders( + accessToken: string +): Record { + return { Authorization: `Bearer ${accessToken}` }; +} + /** * Map a token (or absence of one) to an in-memory session-pool key. * @@ -292,19 +303,17 @@ export class CopilotWebExecutor extends BaseExecutor { // Use Node.js built-in WebSocket if available, else dynamic import. // Pass the access token via Authorization header (not URL) to avoid // credential exposure in server logs. - let WS = globalThis.WebSocket; - if (!WS) { + const BrowserWebSocket = globalThis.WebSocket; + if (BrowserWebSocket) { + ws = new BrowserWebSocket(wsUrl); + } else { // @ts-ignore — ws module has no type declarations in this project - WS = (await import("ws")).default as unknown as typeof WebSocket; - if (accessToken) { - // @ts-ignore — ws module supports headers option in second arg - ws = new WS(wsUrl, { - headers: { Authorization: `Bearer ${accessToken}` }, - }) as WebSocket; - } - } - if (!ws) { - ws = new WS(wsUrl) as WebSocket; + const NodeWebSocket = (await import("ws")) + .default as unknown as NodeWebSocketConstructor; + ws = new NodeWebSocket( + wsUrl, + accessToken ? { headers: buildCopilotWebSocketHeaders(accessToken) } : undefined + ); } const timeout = setTimeout(() => abort("Copilot WebSocket timeout"), FETCH_TIMEOUT_MS); diff --git a/tests/unit/copilot-web-executor.test.ts b/tests/unit/copilot-web-executor.test.ts index 72d9f56582..19a2be5e7b 100644 --- a/tests/unit/copilot-web-executor.test.ts +++ b/tests/unit/copilot-web-executor.test.ts @@ -1,8 +1,13 @@ import test from "node:test"; import assert from "node:assert/strict"; -const { getCopilotMode, extractAccessToken, sessionPoolKey, solveHashcash } = - await import("../../open-sse/executors/copilot-web.ts"); +const { + buildCopilotWebSocketHeaders, + getCopilotMode, + extractAccessToken, + sessionPoolKey, + solveHashcash, +} = await import("../../open-sse/executors/copilot-web.ts"); test("getCopilotMode maps known models to their Copilot modes", () => { assert.equal(getCopilotMode("copilot"), "chat"); @@ -43,6 +48,12 @@ test("extractAccessToken returns null for empty input", () => { assert.equal(extractAccessToken(""), null); }); +test("Copilot WebSocket credentials use the Authorization header", () => { + assert.deepEqual(buildCopilotWebSocketHeaders("access-token"), { + Authorization: "Bearer access-token", + }); +}); + test("sessionPoolKey produces unique keys per token preventing session sharing", () => { const key1 = sessionPoolKey("token-user-alice"); const key2 = sessionPoolKey("token-user-bob");