From 31c0f8d4f550074c85e1e2545754cd865eb4f081 Mon Sep 17 00:00:00 2001 From: Devin Date: Mon, 13 Apr 2026 16:43:48 -0700 Subject: [PATCH 1/6] change runtime url --- src/sandbox/base.ts | 28 +++++++------ src/sandbox/ws.ts | 13 +++++- src/services/sandboxes.ts | 45 ++++++++++++++++++++- tests/sandbox/e2e/list-contract.test.ts | 8 ++-- tests/sandbox/e2e/runtime-transport.test.ts | 24 +++++++++++ tests/sandbox/e2e/sandbox-contract.test.ts | 24 +++++------ 6 files changed, 111 insertions(+), 31 deletions(-) diff --git a/src/sandbox/base.ts b/src/sandbox/base.ts index 1be43a7..b1b020a 100644 --- a/src/sandbox/base.ts +++ b/src/sandbox/base.ts @@ -199,18 +199,9 @@ export class RuntimeTransport { init?: RequestInit, params?: RuntimeParams ): Promise { - const url = new URL(path, this.normalizeBaseUrl(connection.baseUrl)); - if (params) { - for (const [key, value] of Object.entries(params)) { - if (value !== undefined) { - url.searchParams.append(key, String(value)); - } - } - } - const target = resolveRuntimeTransportTarget( connection.baseUrl, - `${url.pathname}${url.search}`, + this.buildRequestPath(path, params), this.runtimeProxyOverride ); const headers = this.buildHeaders(connection, init?.headers, target.hostHeader); @@ -307,7 +298,20 @@ export class RuntimeTransport { return headers; } - private normalizeBaseUrl(baseUrl: string): string { - return baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`; + private buildRequestPath(path: string, params?: RuntimeParams): string { + const trimmed = path.trim(); + const [rawPath, rawQuery = ""] = trimmed.split("?", 2); + const queryParams = new URLSearchParams(rawQuery); + + if (params) { + for (const [key, value] of Object.entries(params)) { + if (value !== undefined) { + queryParams.append(key, String(value)); + } + } + } + + const query = queryParams.toString(); + return query ? `${rawPath}?${query}` : rawPath; } } diff --git a/src/sandbox/ws.ts b/src/sandbox/ws.ts index 0ac09b3..dc517cb 100644 --- a/src/sandbox/ws.ts +++ b/src/sandbox/ws.ts @@ -90,12 +90,23 @@ const RETRYABLE_NETWORK_CODES = new Set([ const hasScheme = (value: string): boolean => /^[a-z][a-z0-9+.-]*:\/\//i.test(value); +const normalizeRuntimeRelativePath = (path: string): string => { + const trimmed = path.trim(); + if (!trimmed) { + return ""; + } + return trimmed.replace(/^\/+/, ""); +}; + export const resolveRuntimeTransportTarget = ( baseUrl: string, path: string, runtimeProxyOverride?: string ): RuntimeTransportTarget => { - const url = new URL(path, baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`); + const url = new URL( + normalizeRuntimeRelativePath(path), + baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/` + ); if (!runtimeProxyOverride) { return { diff --git a/src/services/sandboxes.ts b/src/services/sandboxes.ts index 7d6b485..ad9aa96 100644 --- a/src/services/sandboxes.ts +++ b/src/services/sandboxes.ts @@ -135,11 +135,52 @@ type SandboxRuntimeState = { runtime: SandboxDetail["runtime"]; }; +const runtimeSessionIdFromPath = (rawPath: string): string | null => { + const segments = rawPath + .trim() + .replace(/^\/+|\/+$/g, "") + .split("/") + .filter(Boolean); + if (segments.length < 2 || segments[0] !== "sandbox" || !segments[1]) { + return null; + } + return segments[1]; +}; + +const resolveSandboxRuntimeSessionHost = ( + runtime: SandboxDetail["runtime"], + baseUrl: URL +): string => { + const sessionIdFromBasePath = runtimeSessionIdFromPath(baseUrl.pathname); + if (sessionIdFromBasePath && baseUrl.hostname) { + return `${sessionIdFromBasePath}.${baseUrl.hostname}`; + } + + const runtimeHost = runtime.host?.trim() || ""; + if (runtimeHost) { + try { + const parsedHost = new URL(runtimeHost); + const sessionIdFromHostPath = runtimeSessionIdFromPath(parsedHost.pathname); + if (sessionIdFromHostPath && parsedHost.hostname) { + return `${sessionIdFromHostPath}.${parsedHost.hostname}`; + } + if (parsedHost.hostname) { + return parsedHost.hostname; + } + } catch { + return runtimeHost; + } + } + + return baseUrl.hostname; +}; + const buildSandboxExposedUrl = (runtime: SandboxDetail["runtime"], port: number): string => { const baseUrl = new URL(runtime.baseUrl); + const sessionHost = resolveSandboxRuntimeSessionHost(runtime, baseUrl); const authority = baseUrl.port - ? `${port}-${runtime.host}:${baseUrl.port}` - : `${port}-${runtime.host}`; + ? `${port}-${sessionHost}:${baseUrl.port}` + : `${port}-${sessionHost}`; return new URL("/", `${baseUrl.protocol}//${authority}`).toString(); }; diff --git a/tests/sandbox/e2e/list-contract.test.ts b/tests/sandbox/e2e/list-contract.test.ts index bf65ed7..ca45b1b 100644 --- a/tests/sandbox/e2e/list-contract.test.ts +++ b/tests/sandbox/e2e/list-contract.test.ts @@ -27,15 +27,15 @@ describe("sandbox control list contract", () => { proxyBytesUsed: 3, runtime: { transport: "regional_proxy" as const, - host: "runtime.example.com", - baseUrl: "https://runtime.example.com", + host: "https://runtime.example.com/sandbox/sbx_123", + baseUrl: "https://runtime.example.com/sandbox/sbx_123", }, exposedPorts: [ { port: 3000, auth: true, - url: "https://3000-runtime.example.com/", - browserUrl: "https://3000-runtime.example.com/_hb/auth?grant=token&next=%2F", + url: "https://3000-sbx_123.runtime.example.com/", + browserUrl: "https://3000-sbx_123.runtime.example.com/_hb/auth?grant=token&next=%2F", browserUrlExpiresAt: "2026-03-12T00:00:01Z", }, ], diff --git a/tests/sandbox/e2e/runtime-transport.test.ts b/tests/sandbox/e2e/runtime-transport.test.ts index 7f11022..e6d2228 100644 --- a/tests/sandbox/e2e/runtime-transport.test.ts +++ b/tests/sandbox/e2e/runtime-transport.test.ts @@ -53,4 +53,28 @@ describe("sandbox runtime transport target", () => { hostHeader: "session.example.dev:8443", }); }); + + test("preserves runtime base path prefixes", () => { + const target = resolveRuntimeTransportTarget( + "https://region.example.dev/sandbox/sbx_123", + "/sandbox/exec?foo=bar" + ); + + expect(target).toEqual({ + url: "https://region.example.dev/sandbox/sbx_123/sandbox/exec?foo=bar", + }); + }); + + test("preserves runtime base path prefixes for websocket targets with overrides", () => { + const target = toWebSocketUrl( + "https://region.example.dev/sandbox/sbx_123", + "/sandbox/pty/pty_123/ws?sessionId=sandbox_123", + "http://127.0.0.1:8090" + ); + + expect(target).toEqual({ + url: "ws://127.0.0.1:8090/sandbox/sbx_123/sandbox/pty/pty_123/ws?sessionId=sandbox_123", + hostHeader: "region.example.dev", + }); + }); }); diff --git a/tests/sandbox/e2e/sandbox-contract.test.ts b/tests/sandbox/e2e/sandbox-contract.test.ts index aa9d695..b513053 100644 --- a/tests/sandbox/e2e/sandbox-contract.test.ts +++ b/tests/sandbox/e2e/sandbox-contract.test.ts @@ -29,8 +29,8 @@ const wireSandboxDetail = (overrides: Record = {}): Record { { port: 3000, auth: true, - url: "https://3000-runtime.example.com/", - browserUrl: "https://3000-runtime.example.com/_hb/auth?grant=token&next=%2F", + url: "https://3000-sbx_123.runtime.example.com/", + browserUrl: "https://3000-sbx_123.runtime.example.com/_hb/auth?grant=token&next=%2F", browserUrlExpiresAt: "2026-03-12T13:00:00Z", }, ], @@ -99,7 +99,7 @@ describe("sandbox control and runtime contract", () => { memoryMiB: 2048, diskMiB: 8192, }); - expect(sandbox.getExposedUrl(3000)).toBe("https://3000-runtime.example.com/"); + expect(sandbox.getExposedUrl(3000)).toBe("https://3000-sbx_123.runtime.example.com/"); }); test("create forwards mounts for snapshot launches", async () => { @@ -141,8 +141,8 @@ describe("sandbox control and runtime contract", () => { .mockResolvedValueOnce({ port: 3000, auth: true, - url: "https://3000-runtime.example.com/", - browserUrl: "https://3000-runtime.example.com/_hb/auth?grant=token&next=%2F", + url: "https://3000-sbx_123.runtime.example.com/", + browserUrl: "https://3000-sbx_123.runtime.example.com/_hb/auth?grant=token&next=%2F", browserUrlExpiresAt: "2026-03-12T13:00:00Z", }) .mockResolvedValueOnce({ @@ -199,8 +199,8 @@ describe("sandbox control and runtime contract", () => { diskSizeMiB: 8192, runtime: { transport: "regional_proxy", - host: "runtime.example.com", - baseUrl: "https://runtime.example.com", + host: "https://runtime.example.com/sandbox/sbx_123", + baseUrl: "https://runtime.example.com/sandbox/sbx_123", }, exposedPorts: [], }, @@ -236,7 +236,7 @@ describe("sandbox control and runtime contract", () => { }); const files = new SandboxFilesApi({ requestJSON } as any, async () => ({ sandboxId: "sbx_123", - baseUrl: "https://runtime.example.com", + baseUrl: "https://runtime.example.com/sandbox/sbx_123", token: "runtime-token", })); @@ -285,7 +285,7 @@ describe("sandbox control and runtime contract", () => { {} as any, async () => ({ sandboxId: "sbx_123", - baseUrl: "https://runtime.example.com", + baseUrl: "https://runtime.example.com/sandbox/sbx_123", token: "runtime-token", }), { @@ -302,7 +302,7 @@ describe("sandbox control and runtime contract", () => { await terminal.attach(10); expect(toWebSocketUrlSpy).toHaveBeenCalledWith( - "https://runtime.example.com", + "https://runtime.example.com/sandbox/sbx_123", "/sandbox/pty/pty_123/ws?sessionId=sbx_123&cursor=10", undefined ); From fd8c02187e44dba27059d8c0e763d15679be4a32 Mon Sep 17 00:00:00 2001 From: Devin Date: Mon, 13 Apr 2026 17:49:09 -0700 Subject: [PATCH 2/6] update path routing --- src/sandbox/ws.ts | 39 +++++++++++++++++++-- tests/sandbox/e2e/runtime-transport.test.ts | 4 +-- 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/src/sandbox/ws.ts b/src/sandbox/ws.ts index dc517cb..956491f 100644 --- a/src/sandbox/ws.ts +++ b/src/sandbox/ws.ts @@ -90,12 +90,45 @@ const RETRYABLE_NETWORK_CODES = new Set([ const hasScheme = (value: string): boolean => /^[a-z][a-z0-9+.-]*:\/\//i.test(value); -const normalizeRuntimeRelativePath = (path: string): string => { +const hasSessionScopedRuntimeBasePath = (pathname: string): boolean => { + const segments = pathname + .trim() + .replace(/^\/+|\/+$/g, "") + .split("/") + .filter(Boolean); + return segments.length >= 2 && segments[0] === "sandbox" && Boolean(segments[1]); +}; + +const stripRuntimeSandboxPrefix = (pathname: string): string => { + if (pathname.startsWith("/sandbox/")) { + return `/${pathname.slice("/sandbox/".length)}`; + } + if (pathname === "/sandbox") { + return "/"; + } + if (pathname.startsWith("sandbox/")) { + return pathname.slice("sandbox/".length); + } + if (pathname === "sandbox") { + return ""; + } + return pathname; +}; + +const normalizeRuntimeRelativePath = (baseUrl: string, path: string): string => { const trimmed = path.trim(); if (!trimmed) { return ""; } - return trimmed.replace(/^\/+/, ""); + + const parsedPath = new URL(trimmed, "http://runtime.local"); + let normalizedPath = parsedPath.pathname; + if (hasSessionScopedRuntimeBasePath(new URL(baseUrl).pathname)) { + normalizedPath = stripRuntimeSandboxPrefix(normalizedPath); + } + + const relativePath = normalizedPath.replace(/^\/+/, ""); + return `${relativePath}${parsedPath.search}${parsedPath.hash}`; }; export const resolveRuntimeTransportTarget = ( @@ -104,7 +137,7 @@ export const resolveRuntimeTransportTarget = ( runtimeProxyOverride?: string ): RuntimeTransportTarget => { const url = new URL( - normalizeRuntimeRelativePath(path), + normalizeRuntimeRelativePath(baseUrl, path), baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/` ); diff --git a/tests/sandbox/e2e/runtime-transport.test.ts b/tests/sandbox/e2e/runtime-transport.test.ts index e6d2228..c5f39a9 100644 --- a/tests/sandbox/e2e/runtime-transport.test.ts +++ b/tests/sandbox/e2e/runtime-transport.test.ts @@ -61,7 +61,7 @@ describe("sandbox runtime transport target", () => { ); expect(target).toEqual({ - url: "https://region.example.dev/sandbox/sbx_123/sandbox/exec?foo=bar", + url: "https://region.example.dev/sandbox/sbx_123/exec?foo=bar", }); }); @@ -73,7 +73,7 @@ describe("sandbox runtime transport target", () => { ); expect(target).toEqual({ - url: "ws://127.0.0.1:8090/sandbox/sbx_123/sandbox/pty/pty_123/ws?sessionId=sandbox_123", + url: "ws://127.0.0.1:8090/sandbox/sbx_123/pty/pty_123/ws?sessionId=sandbox_123", hostHeader: "region.example.dev", }); }); From 73187007bfa06fdd0512d99e4f0c295aaa637704 Mon Sep 17 00:00:00 2001 From: Devin Date: Mon, 13 Apr 2026 18:14:02 -0700 Subject: [PATCH 3/6] determine runtime before parse --- src/sandbox/base.ts | 3 +- src/sandbox/files.ts | 3 +- src/sandbox/terminal.ts | 3 +- src/sandbox/ws.ts | 73 ++++++++++++++------- tests/sandbox/e2e/list-contract.test.ts | 2 +- tests/sandbox/e2e/runtime-transport.test.ts | 22 +++++++ tests/sandbox/e2e/sandbox-contract.test.ts | 7 +- 7 files changed, 84 insertions(+), 29 deletions(-) diff --git a/src/sandbox/base.ts b/src/sandbox/base.ts index b1b020a..21f6228 100644 --- a/src/sandbox/base.ts +++ b/src/sandbox/base.ts @@ -202,7 +202,8 @@ export class RuntimeTransport { const target = resolveRuntimeTransportTarget( connection.baseUrl, this.buildRequestPath(path, params), - this.runtimeProxyOverride + this.runtimeProxyOverride, + connection.sandboxId ); const headers = this.buildHeaders(connection, init?.headers, target.hostHeader); const controller = new AbortController(); diff --git a/src/sandbox/files.ts b/src/sandbox/files.ts index 81858b1..5c467b0 100644 --- a/src/sandbox/files.ts +++ b/src/sandbox/files.ts @@ -277,7 +277,8 @@ class RuntimeFileWatchHandle { `/sandbox/files/watch/${this.status.id}/ws?sessionId=${encodeURIComponent( connectionInfo.sandboxId )}${cursor !== undefined ? `&cursor=${encodeURIComponent(String(cursor))}` : ""}`, - this.runtimeProxyOverride + this.runtimeProxyOverride, + connectionInfo.sandboxId ); const headers: Record = { diff --git a/src/sandbox/terminal.ts b/src/sandbox/terminal.ts index 1446679..2363ae4 100644 --- a/src/sandbox/terminal.ts +++ b/src/sandbox/terminal.ts @@ -278,7 +278,8 @@ export class SandboxTerminalHandle { const target = toWebSocketUrl( connectionInfo.baseUrl, `/sandbox/pty/${this.id}/ws?${query.toString()}`, - this.runtimeProxyOverride + this.runtimeProxyOverride, + connectionInfo.sandboxId ); const headers: Record = { diff --git a/src/sandbox/ws.ts b/src/sandbox/ws.ts index 956491f..c48356a 100644 --- a/src/sandbox/ws.ts +++ b/src/sandbox/ws.ts @@ -90,42 +90,69 @@ const RETRYABLE_NETWORK_CODES = new Set([ const hasScheme = (value: string): boolean => /^[a-z][a-z0-9+.-]*:\/\//i.test(value); -const hasSessionScopedRuntimeBasePath = (pathname: string): boolean => { - const segments = pathname +const runtimeBaseUrlSessionId = (runtimeBaseUrl: string): string | null => { + const segments = new URL(runtimeBaseUrl).pathname .trim() .replace(/^\/+|\/+$/g, "") .split("/") .filter(Boolean); - return segments.length >= 2 && segments[0] === "sandbox" && Boolean(segments[1]); + if (segments.length < 2 || segments[0] !== "sandbox" || !segments[1]) { + return null; + } + return segments[1]; }; -const stripRuntimeSandboxPrefix = (pathname: string): string => { - if (pathname.startsWith("/sandbox/")) { - return `/${pathname.slice("/sandbox/".length)}`; +const shouldPrependSandboxToRuntimeAPI = ( + runtimeBaseUrl: string, + sandboxId?: string +): boolean => { + const pathSessionId = runtimeBaseUrlSessionId(runtimeBaseUrl); + if (!pathSessionId) { + return true; } - if (pathname === "/sandbox") { - return "/"; + if (sandboxId?.trim() && sandboxId.trim() !== pathSessionId) { + // Base URL shape is authoritative even if local metadata is stale. + return false; } - if (pathname.startsWith("sandbox/")) { - return pathname.slice("sandbox/".length); + return false; +}; + +const normalizeRuntimeAPIPath = (pathname: string, prependSandbox: boolean): string => { + const trimmed = pathname.trim(); + if (!trimmed) { + return prependSandbox ? "/sandbox" : "/"; } - if (pathname === "sandbox") { - return ""; + + const absolute = trimmed.startsWith("/") ? trimmed : `/${trimmed}`; + if (prependSandbox) { + if (absolute === "/sandbox" || absolute.startsWith("/sandbox/")) { + return absolute; + } + return `/sandbox${absolute}`; } - return pathname; + + if (absolute === "/sandbox") { + return "/"; + } + if (absolute.startsWith("/sandbox/")) { + return `/${absolute.slice("/sandbox/".length)}`; + } + return absolute; }; -const normalizeRuntimeRelativePath = (baseUrl: string, path: string): string => { +const normalizeRuntimeRelativePath = ( + baseUrl: string, + path: string, + sandboxId?: string +): string => { const trimmed = path.trim(); if (!trimmed) { return ""; } const parsedPath = new URL(trimmed, "http://runtime.local"); - let normalizedPath = parsedPath.pathname; - if (hasSessionScopedRuntimeBasePath(new URL(baseUrl).pathname)) { - normalizedPath = stripRuntimeSandboxPrefix(normalizedPath); - } + const prependSandbox = shouldPrependSandboxToRuntimeAPI(baseUrl, sandboxId); + const normalizedPath = normalizeRuntimeAPIPath(parsedPath.pathname, prependSandbox); const relativePath = normalizedPath.replace(/^\/+/, ""); return `${relativePath}${parsedPath.search}${parsedPath.hash}`; @@ -134,10 +161,11 @@ const normalizeRuntimeRelativePath = (baseUrl: string, path: string): string => export const resolveRuntimeTransportTarget = ( baseUrl: string, path: string, - runtimeProxyOverride?: string + runtimeProxyOverride?: string, + sandboxId?: string ): RuntimeTransportTarget => { const url = new URL( - normalizeRuntimeRelativePath(baseUrl, path), + normalizeRuntimeRelativePath(baseUrl, path, sandboxId), baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/` ); @@ -168,9 +196,10 @@ export const resolveRuntimeTransportTarget = ( export const toWebSocketUrl = ( baseUrl: string, path: string, - runtimeProxyOverride?: string + runtimeProxyOverride?: string, + sandboxId?: string ): RuntimeTransportTarget => { - const target = resolveRuntimeTransportTarget(baseUrl, path, runtimeProxyOverride); + const target = resolveRuntimeTransportTarget(baseUrl, path, runtimeProxyOverride, sandboxId); const url = new URL(target.url); if (url.protocol === "https:") { url.protocol = "wss:"; diff --git a/tests/sandbox/e2e/list-contract.test.ts b/tests/sandbox/e2e/list-contract.test.ts index ca45b1b..a4edb82 100644 --- a/tests/sandbox/e2e/list-contract.test.ts +++ b/tests/sandbox/e2e/list-contract.test.ts @@ -27,7 +27,7 @@ describe("sandbox control list contract", () => { proxyBytesUsed: 3, runtime: { transport: "regional_proxy" as const, - host: "https://runtime.example.com/sandbox/sbx_123", + host: "https://runtime.example.com", baseUrl: "https://runtime.example.com/sandbox/sbx_123", }, exposedPorts: [ diff --git a/tests/sandbox/e2e/runtime-transport.test.ts b/tests/sandbox/e2e/runtime-transport.test.ts index c5f39a9..94be667 100644 --- a/tests/sandbox/e2e/runtime-transport.test.ts +++ b/tests/sandbox/e2e/runtime-transport.test.ts @@ -28,6 +28,17 @@ describe("sandbox runtime transport target", () => { }); }); + test("prepends /sandbox for session-host runtimes when callers pass relative runtime paths", () => { + const target = resolveRuntimeTransportTarget( + "https://session.example.dev:8443", + "/exec?foo=bar" + ); + + expect(target).toEqual({ + url: "https://session.example.dev:8443/sandbox/exec?foo=bar", + }); + }); + test("applies an explicit runtime proxy override and preserves the original host header", () => { const target = resolveRuntimeTransportTarget( "https://session.example.dev:8443", @@ -65,6 +76,17 @@ describe("sandbox runtime transport target", () => { }); }); + test("does not double-prefix /sandbox for region-path runtimes when callers pass relative runtime paths", () => { + const target = resolveRuntimeTransportTarget( + "https://region.example.dev/sandbox/sbx_123", + "/exec?foo=bar" + ); + + expect(target).toEqual({ + url: "https://region.example.dev/sandbox/sbx_123/exec?foo=bar", + }); + }); + test("preserves runtime base path prefixes for websocket targets with overrides", () => { const target = toWebSocketUrl( "https://region.example.dev/sandbox/sbx_123", diff --git a/tests/sandbox/e2e/sandbox-contract.test.ts b/tests/sandbox/e2e/sandbox-contract.test.ts index b513053..5703812 100644 --- a/tests/sandbox/e2e/sandbox-contract.test.ts +++ b/tests/sandbox/e2e/sandbox-contract.test.ts @@ -29,7 +29,7 @@ const wireSandboxDetail = (overrides: Record = {}): Record { diskSizeMiB: 8192, runtime: { transport: "regional_proxy", - host: "https://runtime.example.com/sandbox/sbx_123", + host: "https://runtime.example.com", baseUrl: "https://runtime.example.com/sandbox/sbx_123", }, exposedPorts: [], @@ -304,7 +304,8 @@ describe("sandbox control and runtime contract", () => { expect(toWebSocketUrlSpy).toHaveBeenCalledWith( "https://runtime.example.com/sandbox/sbx_123", "/sandbox/pty/pty_123/ws?sessionId=sbx_123&cursor=10", - undefined + undefined, + "sbx_123" ); }); }); From 40b980ac54c107729c58f72b3c9e23c6a3d9e0c7 Mon Sep 17 00:00:00 2001 From: Devin Deng Date: Tue, 14 Apr 2026 03:13:30 +0000 Subject: [PATCH 4/6] bump version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7ecfc25..0ed69a6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@hyperbrowser/sdk", - "version": "0.89.2", + "version": "0.89.3", "description": "Node SDK for Hyperbrowser API", "author": "", "repository": { From 320797cb4f0462f723b17d97b1387c42d6301475 Mon Sep 17 00:00:00 2001 From: Devin Deng Date: Tue, 14 Apr 2026 03:42:28 +0000 Subject: [PATCH 5/6] remove dupe function --- src/sandbox/runtime-path.ts | 15 +++++++++++++++ src/sandbox/ws.ts | 13 +------------ src/services/sandboxes.ts | 13 +------------ 3 files changed, 17 insertions(+), 24 deletions(-) create mode 100644 src/sandbox/runtime-path.ts diff --git a/src/sandbox/runtime-path.ts b/src/sandbox/runtime-path.ts new file mode 100644 index 0000000..ddd4f16 --- /dev/null +++ b/src/sandbox/runtime-path.ts @@ -0,0 +1,15 @@ +export const runtimeSessionIdFromPath = (rawPath: string): string | null => { + const segments = rawPath + .trim() + .replace(/^\/+|\/+$/g, "") + .split("/") + .filter(Boolean); + if (segments.length < 2 || segments[0] !== "sandbox" || !segments[1]?.trim()) { + return null; + } + return segments[1].trim(); +}; + +export const runtimeBaseUrlSessionId = (runtimeBaseUrl: string): string | null => { + return runtimeSessionIdFromPath(new URL(runtimeBaseUrl).pathname); +}; diff --git a/src/sandbox/ws.ts b/src/sandbox/ws.ts index c48356a..3d607cd 100644 --- a/src/sandbox/ws.ts +++ b/src/sandbox/ws.ts @@ -1,6 +1,7 @@ import type { IncomingMessage } from "http"; import WebSocket from "ws"; import { HyperbrowserError } from "../client"; +import { runtimeBaseUrlSessionId } from "./runtime-path"; export class AsyncEventQueue implements AsyncIterable { private readonly values: T[] = []; @@ -90,18 +91,6 @@ const RETRYABLE_NETWORK_CODES = new Set([ const hasScheme = (value: string): boolean => /^[a-z][a-z0-9+.-]*:\/\//i.test(value); -const runtimeBaseUrlSessionId = (runtimeBaseUrl: string): string | null => { - const segments = new URL(runtimeBaseUrl).pathname - .trim() - .replace(/^\/+|\/+$/g, "") - .split("/") - .filter(Boolean); - if (segments.length < 2 || segments[0] !== "sandbox" || !segments[1]) { - return null; - } - return segments[1]; -}; - const shouldPrependSandboxToRuntimeAPI = ( runtimeBaseUrl: string, sandboxId?: string diff --git a/src/services/sandboxes.ts b/src/services/sandboxes.ts index ad9aa96..826b4c8 100644 --- a/src/services/sandboxes.ts +++ b/src/services/sandboxes.ts @@ -1,6 +1,7 @@ import { HyperbrowserError } from "../client"; import { SandboxFilesApi } from "../sandbox/files"; import { RuntimeConnection, RuntimeTransport } from "../sandbox/base"; +import { runtimeSessionIdFromPath } from "../sandbox/runtime-path"; import { SandboxProcessHandle, SandboxProcessesApi } from "../sandbox/process"; import { SandboxTerminalApi } from "../sandbox/terminal"; import { BasicResponse } from "../types/session"; @@ -135,18 +136,6 @@ type SandboxRuntimeState = { runtime: SandboxDetail["runtime"]; }; -const runtimeSessionIdFromPath = (rawPath: string): string | null => { - const segments = rawPath - .trim() - .replace(/^\/+|\/+$/g, "") - .split("/") - .filter(Boolean); - if (segments.length < 2 || segments[0] !== "sandbox" || !segments[1]) { - return null; - } - return segments[1]; -}; - const resolveSandboxRuntimeSessionHost = ( runtime: SandboxDetail["runtime"], baseUrl: URL From 7343c0f9464b767bf4926e37b488f4d01089f371 Mon Sep 17 00:00:00 2001 From: Devin Deng Date: Tue, 14 Apr 2026 03:44:46 +0000 Subject: [PATCH 6/6] remove dead code --- src/sandbox/base.ts | 3 +- src/sandbox/files.ts | 3 +- src/sandbox/terminal.ts | 3 +- src/sandbox/ws.ts | 33 ++++++---------------- tests/sandbox/e2e/sandbox-contract.test.ts | 3 +- 5 files changed, 12 insertions(+), 33 deletions(-) diff --git a/src/sandbox/base.ts b/src/sandbox/base.ts index 21f6228..b1b020a 100644 --- a/src/sandbox/base.ts +++ b/src/sandbox/base.ts @@ -202,8 +202,7 @@ export class RuntimeTransport { const target = resolveRuntimeTransportTarget( connection.baseUrl, this.buildRequestPath(path, params), - this.runtimeProxyOverride, - connection.sandboxId + this.runtimeProxyOverride ); const headers = this.buildHeaders(connection, init?.headers, target.hostHeader); const controller = new AbortController(); diff --git a/src/sandbox/files.ts b/src/sandbox/files.ts index 5c467b0..81858b1 100644 --- a/src/sandbox/files.ts +++ b/src/sandbox/files.ts @@ -277,8 +277,7 @@ class RuntimeFileWatchHandle { `/sandbox/files/watch/${this.status.id}/ws?sessionId=${encodeURIComponent( connectionInfo.sandboxId )}${cursor !== undefined ? `&cursor=${encodeURIComponent(String(cursor))}` : ""}`, - this.runtimeProxyOverride, - connectionInfo.sandboxId + this.runtimeProxyOverride ); const headers: Record = { diff --git a/src/sandbox/terminal.ts b/src/sandbox/terminal.ts index 2363ae4..1446679 100644 --- a/src/sandbox/terminal.ts +++ b/src/sandbox/terminal.ts @@ -278,8 +278,7 @@ export class SandboxTerminalHandle { const target = toWebSocketUrl( connectionInfo.baseUrl, `/sandbox/pty/${this.id}/ws?${query.toString()}`, - this.runtimeProxyOverride, - connectionInfo.sandboxId + this.runtimeProxyOverride ); const headers: Record = { diff --git a/src/sandbox/ws.ts b/src/sandbox/ws.ts index 3d607cd..61f83d2 100644 --- a/src/sandbox/ws.ts +++ b/src/sandbox/ws.ts @@ -91,19 +91,8 @@ const RETRYABLE_NETWORK_CODES = new Set([ const hasScheme = (value: string): boolean => /^[a-z][a-z0-9+.-]*:\/\//i.test(value); -const shouldPrependSandboxToRuntimeAPI = ( - runtimeBaseUrl: string, - sandboxId?: string -): boolean => { - const pathSessionId = runtimeBaseUrlSessionId(runtimeBaseUrl); - if (!pathSessionId) { - return true; - } - if (sandboxId?.trim() && sandboxId.trim() !== pathSessionId) { - // Base URL shape is authoritative even if local metadata is stale. - return false; - } - return false; +const shouldPrependSandboxToRuntimeAPI = (runtimeBaseUrl: string): boolean => { + return runtimeBaseUrlSessionId(runtimeBaseUrl) === null; }; const normalizeRuntimeAPIPath = (pathname: string, prependSandbox: boolean): string => { @@ -129,18 +118,14 @@ const normalizeRuntimeAPIPath = (pathname: string, prependSandbox: boolean): str return absolute; }; -const normalizeRuntimeRelativePath = ( - baseUrl: string, - path: string, - sandboxId?: string -): string => { +const normalizeRuntimeRelativePath = (baseUrl: string, path: string): string => { const trimmed = path.trim(); if (!trimmed) { return ""; } const parsedPath = new URL(trimmed, "http://runtime.local"); - const prependSandbox = shouldPrependSandboxToRuntimeAPI(baseUrl, sandboxId); + const prependSandbox = shouldPrependSandboxToRuntimeAPI(baseUrl); const normalizedPath = normalizeRuntimeAPIPath(parsedPath.pathname, prependSandbox); const relativePath = normalizedPath.replace(/^\/+/, ""); @@ -150,11 +135,10 @@ const normalizeRuntimeRelativePath = ( export const resolveRuntimeTransportTarget = ( baseUrl: string, path: string, - runtimeProxyOverride?: string, - sandboxId?: string + runtimeProxyOverride?: string ): RuntimeTransportTarget => { const url = new URL( - normalizeRuntimeRelativePath(baseUrl, path, sandboxId), + normalizeRuntimeRelativePath(baseUrl, path), baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/` ); @@ -185,10 +169,9 @@ export const resolveRuntimeTransportTarget = ( export const toWebSocketUrl = ( baseUrl: string, path: string, - runtimeProxyOverride?: string, - sandboxId?: string + runtimeProxyOverride?: string ): RuntimeTransportTarget => { - const target = resolveRuntimeTransportTarget(baseUrl, path, runtimeProxyOverride, sandboxId); + const target = resolveRuntimeTransportTarget(baseUrl, path, runtimeProxyOverride); const url = new URL(target.url); if (url.protocol === "https:") { url.protocol = "wss:"; diff --git a/tests/sandbox/e2e/sandbox-contract.test.ts b/tests/sandbox/e2e/sandbox-contract.test.ts index 5703812..c1e6541 100644 --- a/tests/sandbox/e2e/sandbox-contract.test.ts +++ b/tests/sandbox/e2e/sandbox-contract.test.ts @@ -304,8 +304,7 @@ describe("sandbox control and runtime contract", () => { expect(toWebSocketUrlSpy).toHaveBeenCalledWith( "https://runtime.example.com/sandbox/sbx_123", "/sandbox/pty/pty_123/ws?sessionId=sbx_123&cursor=10", - undefined, - "sbx_123" + undefined ); }); });