security: complete local access protections

This commit is contained in:
Rohit P
2026-07-24 18:44:39 -07:00
parent 3f5ac872ca
commit ac83bc0490
20 changed files with 361 additions and 32 deletions
+38
View File
@@ -0,0 +1,38 @@
import { afterEach, expect, it, vi } from "vitest";
import { getHealth, Session } from "./api";
afterEach(() => {
vi.unstubAllGlobals();
});
it("authenticates REST and session WebSocket calls with the launch token", async () => {
vi.stubGlobal("__COWORKER_API_TOKEN__", "launch-token");
const request = vi.fn(async (_url: string, init?: RequestInit) => {
expect(new Headers(init?.headers).get("X-OpenWorker-Token")).toBe("launch-token");
return { json: async () => ({ status: "ok" }) } as Response;
});
vi.stubGlobal("fetch", request);
class FakeWebSocket {
static readonly CONNECTING = 0;
static readonly OPEN = 1;
readyState = FakeWebSocket.CONNECTING;
onmessage: ((event: MessageEvent) => void) | null = null;
onopen: (() => void) | null = null;
onclose: (() => void) | null = null;
send = vi.fn();
constructor(
public readonly url: string,
public readonly protocols?: string | string[],
) {}
}
vi.stubGlobal("WebSocket", FakeWebSocket);
await getHealth();
expect(request).toHaveBeenCalledOnce();
const session = new Session("s1", "/workspace", "code", { onEvent: vi.fn() });
const socket = (session as unknown as { ws: FakeWebSocket }).ws;
expect(socket.protocols).toEqual(["openworker", "launch-token"]);
});
+27 -2
View File
@@ -1,5 +1,7 @@
import type { SessionInfo, WsEvent } from "./types";
declare const __COWORKER_DEV_TOKEN__: string;
// Endpoint resolution order: runtime-injected globals (Tauri sets `window.__COWORKER_HTTP__`
// for its dynamically-chosen sidecar port) → Vite env → the 127.0.0.1:8765 dev default. This
// keeps a single codebase: browser `npm run dev` hits 8765; the desktop shell hits its sidecar.
@@ -11,6 +13,29 @@ const wsBase = (): string =>
(globalThis as any).__COWORKER_WS__ ||
(import.meta as any).env?.VITE_COWORKER_WS ||
"ws://127.0.0.1:8765";
const apiToken = (): string =>
(globalThis as any).__COWORKER_API_TOKEN__ ||
(import.meta as any).env?.VITE_COWORKER_API_TOKEN ||
(typeof __COWORKER_DEV_TOKEN__ === "string" ? __COWORKER_DEV_TOKEN__ : "");
// All local REST calls pass through this module, so a module-local wrapper applies launch
// authentication without asking every endpoint helper to remember the security header.
const fetch = (
input: RequestInfo | URL,
init: RequestInit = {},
): Promise<Response> => {
const headers = new Headers(init.headers);
const token = apiToken();
if (token) headers.set("X-OpenWorker-Token", token);
return globalThis.fetch(input, { ...init, headers });
};
const openWebSocket = (url: string): WebSocket => {
const token = apiToken();
return token
? new WebSocket(url, ["openworker", token])
: new WebSocket(url);
};
export interface Health {
status: string;
@@ -1388,7 +1413,7 @@ export function connectEvents(
let closed = false;
const open = () => {
if (closed) return;
ws = new WebSocket(`${wsBase()}/ws/events`);
ws = openWebSocket(`${wsBase()}/ws/events`);
ws.onmessage = (e) => {
try {
onEvent(JSON.parse(e.data));
@@ -1715,7 +1740,7 @@ export class Session {
constructor(sessionId: string, workspace: string, agent: string, handlers: Handlers) {
const q = `?workspace=${encodeURIComponent(workspace)}&agent=${encodeURIComponent(agent)}`;
this.ws = new WebSocket(`${wsBase()}/ws/session/${sessionId}${q}`);
this.ws = openWebSocket(`${wsBase()}/ws/session/${sessionId}${q}`);
this.ws.onmessage = (e) => handlers.onEvent(JSON.parse(e.data));
this.ws.onopen = () => {
this.flush();