diff --git a/src/context-types.ts b/src/context-types.ts index a6d60f4..1f84a2a 100644 --- a/src/context-types.ts +++ b/src/context-types.ts @@ -92,16 +92,6 @@ export interface SidebarSessionStore { } | undefined } -/** One loader entry's options slice (the connection row's resolved config). */ -export interface SidebarLoaderEntry { - options: { name: string; config?: unknown } -} - -/** The loader face used to read the connection row's trustedHosts config. */ -export interface SidebarLoader { - entries(): Iterable -} - /** * The web runtime service face (mirror of @deepseek-ai/dsh-web-app's * WebRuntimeValues): the bind-derived trust list the /api gateway's fence diff --git a/src/index.ts b/src/index.ts index ca7f557..f9e0622 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,9 +4,10 @@ * preview route, the /sidebar/bundle lazy-chunk route (client code splits), * and the terminal WebSocket upgrade. Every route passes the same * browser-trust fence as the /api gateway — Host-header loopback or the - * web runtime's `trustedHosts` (LAN IP literals derived per boot plus - * `--trusted-host` authorities) — read live per request so the fence never - * drifts from the deployment's. + * web runtime's `trustedHosts` (LAN IP literals sampled at boot plus + * `--trusted-host` authorities), read per request from the live service + * value so the fence tracks the same trust source the /api gateway derives + * its list from. * * All operations are conversation-scoped: requests carry a sessionId, the * session's authoritative cwd comes from the session store, and terminal @@ -425,9 +426,10 @@ export function apply(ctx: Context, config?: SidebarConfig): void { // restore it before any terminal can spawn (idempotent). ensureSpawnHelper() const resolved = resolveSidebarConfig(config) - // The web runtime's bind-derived trust list (LAN literals plus - // --trusted-host authorities) — the same list the /api gateway fence - // accepts. Read per request so config reloads apply without a restart. + // The web runtime's bind-derived trust list (boot-sampled LAN literals + // plus --trusted-host authorities) — the authoritative source the /api + // gateway fence derives its list from. Read per request from the live + // service value; a replaced list takes effect without a plugin restart. const fence = (req: SidebarHttpRequest): boolean => isTrustedApiRequest(req, ctx.webRuntime.trustedHosts) const ptyManager = new PtyManager(defaultShell(), resolved.terminalsPerSession) // The agent-owned terminal registry: parallel to the UI-tab ptyManager, diff --git a/tests/trusted-hosts.spec.ts b/tests/trusted-hosts.spec.ts index af78f57..3bc7432 100644 --- a/tests/trusted-hosts.spec.ts +++ b/tests/trusted-hosts.spec.ts @@ -95,7 +95,7 @@ function remoteDomainRequest(): IncomingMessage { host: 'example.com', 'sec-fetch-site': 'same-origin', origin: 'https://example.com', - }) + }, '{"sessionId":"test-session"}') } /** A LAN-IP request with the browser markers a same-origin fetch sends. */ @@ -104,7 +104,7 @@ function lanRequest(): IncomingMessage { host: '192.0.2.1:3080', 'sec-fetch-site': 'same-origin', origin: 'http://192.0.2.1:3080', - }) + }, '{"sessionId":"test-session"}') } describe('remote-access trust (webRuntime.trustedHosts)', () => { @@ -113,7 +113,7 @@ describe('remote-access trust (webRuntime.trustedHosts)', () => { try { const res = fakeRes() await api(remoteDomainRequest(), res as unknown as ServerResponse) - expect(res.status).not.toBe(403) + expect(res.status).toBe(200) } finally { cleanup() } @@ -124,7 +124,7 @@ describe('remote-access trust (webRuntime.trustedHosts)', () => { try { const res = fakeRes() await api(lanRequest(), res as unknown as ServerResponse) - expect(res.status).not.toBe(403) + expect(res.status).toBe(200) } finally { cleanup() } @@ -140,8 +140,8 @@ describe('remote-access trust (webRuntime.trustedHosts)', () => { await api(req('POST', '/sidebar/api/session.cwd', { host: '127.0.0.1:3080', 'sec-fetch-site': 'same-origin', - }), loopback as unknown as ServerResponse) - expect(loopback.status).not.toBe(403) + }, '{"sessionId":"test-session"}'), loopback as unknown as ServerResponse) + expect(loopback.status).toBe(200) } finally { cleanup() } @@ -151,11 +151,13 @@ describe('remote-access trust (webRuntime.trustedHosts)', () => { const { api, cleanup } = mount(['example.com']) try { const res = fakeRes() + // Same-origin origin: only the explicit cross-site marker can reject — + // this pins the marker branch (a mismatched origin would also 403). await api(req('POST', '/sidebar/api/session.cwd', { host: 'example.com', 'sec-fetch-site': 'cross-site', - origin: 'https://evil.example.net', - }), res as unknown as ServerResponse) + origin: 'https://example.com', + }, '{"sessionId":"test-session"}'), res as unknown as ServerResponse) expect(res.status).toBe(403) } finally { cleanup() @@ -173,7 +175,7 @@ describe('remote-access trust (webRuntime.trustedHosts)', () => { setTrustedHosts(['example.com']) const after = fakeRes() await api(remoteDomainRequest(), after as unknown as ServerResponse) - expect(after.status).not.toBe(403) + expect(after.status).toBe(200) } finally { cleanup() }