fix: canonicalize workspace tab routes and prevent orphan runs

This commit is contained in:
Mohamed Boudra
2026-03-04 17:39:19 +07:00
parent 7578846c27
commit 26ea214d64
16 changed files with 723 additions and 88 deletions
@@ -68,6 +68,18 @@ describe("workspace route parsing", () => {
});
});
it("prefers path tab segment and ignores query params for workspace tab routes", () => {
expect(
parseHostWorkspaceTabRouteFromPathname(
"/h/local/workspace/L3RtcC9yZXBv/tab/draft_from_path?tabId=draft_from_query&workspaceId=wrong"
)
).toEqual({
serverId: "local",
workspaceId: "/tmp/repo",
tabId: "draft_from_path",
});
});
it("builds base64url workspace routes", () => {
expect(buildHostWorkspaceRoute("local", "/tmp/repo")).toBe("/h/local/workspace/L3RtcC9yZXBv");
});
+30 -17
View File
@@ -2,6 +2,15 @@ import { Buffer } from "buffer";
type NullableString = string | null | undefined;
function stripSearchAndHash(pathname: string): string {
const hashIndex = pathname.indexOf("#");
const queryIndex = pathname.indexOf("?");
const end = [hashIndex, queryIndex]
.filter((index) => index >= 0)
.reduce((min, index) => Math.min(min, index), pathname.length);
return pathname.slice(0, end);
}
function trimNonEmpty(value: NullableString): string | null {
if (typeof value !== "string") {
return null;
@@ -155,7 +164,8 @@ export function decodeFilePathFromPathSegment(filePathSegment: string): string |
}
export function parseServerIdFromPathname(pathname: string): string | null {
const match = pathname.match(/^\/h\/([^/]+)(?:\/|$)/);
const pathOnly = stripSearchAndHash(pathname);
const match = pathOnly.match(/^\/h\/([^/]+)(?:\/|$)/);
if (!match) {
return null;
}
@@ -169,7 +179,8 @@ export function parseServerIdFromPathname(pathname: string): string | null {
export function parseHostAgentRouteFromPathname(
pathname: string
): { serverId: string; agentId: string } | null {
const match = pathname.match(/^\/h\/([^/]+)\/agent\/([^/]+)(?:\/|$)/);
const pathOnly = stripSearchAndHash(pathname);
const match = pathOnly.match(/^\/h\/([^/]+)\/agent\/([^/]+)(?:\/|$)/);
if (!match) {
return null;
}
@@ -191,35 +202,36 @@ export function parseHostAgentRouteFromPathname(
export function parseHostWorkspaceRouteFromPathname(
pathname: string
): { serverId: string; workspaceId: string } | null {
const pathOnly = stripSearchAndHash(pathname);
const prefix = "/h/";
if (!pathname.startsWith(prefix)) {
if (!pathOnly.startsWith(prefix)) {
return null;
}
const serverIdStart = prefix.length;
const serverIdEnd = pathname.indexOf("/", serverIdStart);
const serverIdEnd = pathOnly.indexOf("/", serverIdStart);
if (serverIdEnd < 0) {
return null;
}
const rawServerId = pathname.slice(serverIdStart, serverIdEnd);
const rawServerId = pathOnly.slice(serverIdStart, serverIdEnd);
const serverId = trimNonEmpty(decodeSegment(rawServerId));
if (!serverId) {
return null;
}
const workspacePrefix = "/workspace/";
if (!pathname.startsWith(workspacePrefix, serverIdEnd)) {
if (!pathOnly.startsWith(workspacePrefix, serverIdEnd)) {
return null;
}
const workspaceIdStart = serverIdEnd + workspacePrefix.length;
let workspaceIdEnd = pathname.length;
const tabIdx = pathname.lastIndexOf("/tab/");
let workspaceIdEnd = pathOnly.length;
const tabIdx = pathOnly.lastIndexOf("/tab/");
if (tabIdx >= 0 && tabIdx > workspaceIdStart) {
workspaceIdEnd = tabIdx;
}
const rawWorkspaceId = pathname.slice(workspaceIdStart, workspaceIdEnd).replace(/\/+$/, "");
const rawWorkspaceId = pathOnly.slice(workspaceIdStart, workspaceIdEnd).replace(/\/+$/, "");
const workspaceId = decodeWorkspaceIdFromPathSegment(rawWorkspaceId);
if (!workspaceId) {
return null;
@@ -230,44 +242,45 @@ export function parseHostWorkspaceRouteFromPathname(
export function parseHostWorkspaceTabRouteFromPathname(
pathname: string
): { serverId: string; workspaceId: string; tabId: string } | null {
const pathOnly = stripSearchAndHash(pathname);
const prefix = "/h/";
if (!pathname.startsWith(prefix)) {
if (!pathOnly.startsWith(prefix)) {
return null;
}
const serverIdStart = prefix.length;
const serverIdEnd = pathname.indexOf("/", serverIdStart);
const serverIdEnd = pathOnly.indexOf("/", serverIdStart);
if (serverIdEnd < 0) {
return null;
}
const rawServerId = pathname.slice(serverIdStart, serverIdEnd);
const rawServerId = pathOnly.slice(serverIdStart, serverIdEnd);
const serverId = trimNonEmpty(decodeSegment(rawServerId));
if (!serverId) {
return null;
}
const workspacePrefix = "/workspace/";
if (!pathname.startsWith(workspacePrefix, serverIdEnd)) {
if (!pathOnly.startsWith(workspacePrefix, serverIdEnd)) {
return null;
}
const workspaceIdStart = serverIdEnd + workspacePrefix.length;
const tabMarker = "/tab/";
const tabIdx = pathname.lastIndexOf(tabMarker);
const tabIdx = pathOnly.lastIndexOf(tabMarker);
if (tabIdx < 0 || tabIdx <= workspaceIdStart) {
return null;
}
const rawWorkspaceId = pathname.slice(workspaceIdStart, tabIdx).replace(/\/+$/, "");
const rawWorkspaceId = pathOnly.slice(workspaceIdStart, tabIdx).replace(/\/+$/, "");
const workspaceId = decodeWorkspaceIdFromPathSegment(rawWorkspaceId);
if (!workspaceId) {
return null;
}
const tabIdStart = tabIdx + tabMarker.length;
const tabIdEnd = pathname.indexOf("/", tabIdStart);
const tabIdEnd = pathOnly.indexOf("/", tabIdStart);
const rawTabId =
tabIdEnd < 0 ? pathname.slice(tabIdStart) : pathname.slice(tabIdStart, tabIdEnd);
tabIdEnd < 0 ? pathOnly.slice(tabIdStart) : pathOnly.slice(tabIdStart, tabIdEnd);
const tabId = trimNonEmpty(decodeSegment(rawTabId));
if (!tabId) {
return null;