fix: prevent base64 misinterpretation of numeric workspace IDs
Numeric string IDs like "164" are valid base64 that decodes to garbage (Hebrew character ""), causing silent workspace lookup failures. Skip base64 encoding/decoding for numeric IDs and only use it for legacy path-based workspace identifiers.
This commit is contained in:
@@ -24,7 +24,12 @@ describe("parseHostAgentRouteFromPathname", () => {
|
||||
});
|
||||
|
||||
describe("workspace route parsing", () => {
|
||||
it("encodes workspace IDs as base64url (no padding)", () => {
|
||||
it("encodes numeric workspace IDs without base64", () => {
|
||||
expect(encodeWorkspaceIdForPathSegment("164")).toBe("164");
|
||||
expect(decodeWorkspaceIdFromPathSegment("164")).toBe("164");
|
||||
});
|
||||
|
||||
it("encodes path-based workspace IDs as base64url (legacy)", () => {
|
||||
expect(encodeWorkspaceIdForPathSegment("/tmp/repo")).toBe("L3RtcC9yZXBv");
|
||||
expect(decodeWorkspaceIdFromPathSegment("L3RtcC9yZXBv")).toBe("/tmp/repo");
|
||||
});
|
||||
@@ -41,7 +46,14 @@ describe("workspace route parsing", () => {
|
||||
expect(decodeFilePathFromPathSegment(encoded)).toBe("src/index.ts");
|
||||
});
|
||||
|
||||
it("parses workspace route", () => {
|
||||
it("parses workspace route with numeric ID", () => {
|
||||
expect(parseHostWorkspaceRouteFromPathname("/h/local/workspace/164")).toEqual({
|
||||
serverId: "local",
|
||||
workspaceId: "164",
|
||||
});
|
||||
});
|
||||
|
||||
it("parses workspace route with legacy base64 path", () => {
|
||||
expect(parseHostWorkspaceRouteFromPathname("/h/local/workspace/L3RtcC9yZXBv")).toEqual({
|
||||
serverId: "local",
|
||||
workspaceId: "/tmp/repo",
|
||||
@@ -54,7 +66,11 @@ describe("workspace route parsing", () => {
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("builds base64url workspace routes", () => {
|
||||
it("builds numeric workspace routes without base64", () => {
|
||||
expect(buildHostWorkspaceRoute("local", "164")).toBe("/h/local/workspace/164");
|
||||
});
|
||||
|
||||
it("builds base64url workspace routes for legacy paths", () => {
|
||||
expect(buildHostWorkspaceRoute("local", "/tmp/repo")).toBe("/h/local/workspace/L3RtcC9yZXBv");
|
||||
});
|
||||
|
||||
@@ -65,7 +81,7 @@ describe("workspace route parsing", () => {
|
||||
it("parses workspace open intent from pathname query", () => {
|
||||
expect(
|
||||
parseHostWorkspaceOpenIntentFromPathname(
|
||||
"/h/local/workspace/L3RtcC9yZXBv?open=agent%3Aagent-1",
|
||||
"/h/local/workspace/164?open=agent%3Aagent-1",
|
||||
),
|
||||
).toEqual({
|
||||
kind: "agent",
|
||||
@@ -90,14 +106,23 @@ describe("workspace route parsing", () => {
|
||||
});
|
||||
|
||||
it("uses the plain workspace route when workspace context is provided", () => {
|
||||
expect(buildHostAgentDetailRoute("local", "agent-1", "/tmp/repo")).toBe(
|
||||
"/h/local/workspace/L3RtcC9yZXBv?open=agent%3Aagent-1",
|
||||
expect(buildHostAgentDetailRoute("local", "agent-1", "164")).toBe(
|
||||
"/h/local/workspace/164?open=agent%3Aagent-1",
|
||||
);
|
||||
});
|
||||
|
||||
it("builds workspace routes with a one-shot open intent", () => {
|
||||
expect(buildHostWorkspaceOpenRoute("local", "/tmp/repo", "draft:new")).toBe(
|
||||
"/h/local/workspace/L3RtcC9yZXBv?open=draft%3Anew",
|
||||
expect(buildHostWorkspaceOpenRoute("local", "164", "draft:new")).toBe(
|
||||
"/h/local/workspace/164?open=draft%3Anew",
|
||||
);
|
||||
});
|
||||
|
||||
it("round-trips numeric IDs through encode/decode", () => {
|
||||
const ids = ["1", "40", "164", "9999"];
|
||||
for (const id of ids) {
|
||||
const encoded = encodeWorkspaceIdForPathSegment(id);
|
||||
const decoded = decodeWorkspaceIdFromPathSegment(encoded);
|
||||
expect(decoded).toBe(id);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -163,7 +163,13 @@ export function encodeWorkspaceIdForPathSegment(workspaceId: string): string {
|
||||
if (!normalized) {
|
||||
return "";
|
||||
}
|
||||
return toBase64UrlNoPad(normalizeWorkspaceId(normalized));
|
||||
// Numeric string IDs are URL-safe and don't need encoding.
|
||||
// Legacy path-based IDs still get base64-encoded for safety.
|
||||
const id = normalizeWorkspaceId(normalized);
|
||||
if (isPathLikeWorkspaceIdentity(id)) {
|
||||
return toBase64UrlNoPad(id);
|
||||
}
|
||||
return encodeURIComponent(id);
|
||||
}
|
||||
|
||||
export function decodeWorkspaceIdFromPathSegment(workspaceIdSegment: string): string | null {
|
||||
@@ -183,6 +189,12 @@ export function decodeWorkspaceIdFromPathSegment(workspaceIdSegment: string): st
|
||||
return normalizeWorkspaceId(decoded);
|
||||
}
|
||||
|
||||
// If the segment looks like a plain numeric ID, return it directly.
|
||||
// Do NOT attempt base64 decode on short alphanumeric strings.
|
||||
if (/^\d+$/.test(decoded)) {
|
||||
return decoded;
|
||||
}
|
||||
|
||||
const base64Decoded = tryDecodeBase64UrlNoPadUtf8(decoded);
|
||||
if (base64Decoded) {
|
||||
return normalizeWorkspaceId(base64Decoded);
|
||||
|
||||
Reference in New Issue
Block a user