refactor: transition to opaque workspace identifiers
This commit is contained in:
@@ -24,13 +24,13 @@ describe("parseHostAgentRouteFromPathname", () => {
|
||||
});
|
||||
|
||||
describe("workspace route parsing", () => {
|
||||
it("encodes numeric workspace IDs without base64", () => {
|
||||
it("keeps URL-safe workspace IDs unencoded", () => {
|
||||
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");
|
||||
it("encodes non-URL-safe workspace IDs as base64url", () => {
|
||||
expect(encodeWorkspaceIdForPathSegment("/tmp/repo")).toBe("b64_L3RtcC9yZXBv");
|
||||
expect(decodeWorkspaceIdFromPathSegment("L3RtcC9yZXBv")).toBe("/tmp/repo");
|
||||
});
|
||||
|
||||
@@ -46,7 +46,7 @@ describe("workspace route parsing", () => {
|
||||
expect(decodeFilePathFromPathSegment(encoded)).toBe("src/index.ts");
|
||||
});
|
||||
|
||||
it("parses workspace route with numeric ID", () => {
|
||||
it("parses workspace route with a plain workspace id", () => {
|
||||
expect(parseHostWorkspaceRouteFromPathname("/h/local/workspace/164")).toEqual({
|
||||
serverId: "local",
|
||||
workspaceId: "164",
|
||||
@@ -66,12 +66,14 @@ describe("workspace route parsing", () => {
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("builds numeric workspace routes without base64", () => {
|
||||
it("builds plain workspace routes for URL-safe ids", () => {
|
||||
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");
|
||||
expect(buildHostWorkspaceRoute("local", "/tmp/repo")).toBe(
|
||||
"/h/local/workspace/b64_L3RtcC9yZXBv",
|
||||
);
|
||||
});
|
||||
|
||||
it("builds host root routes", () => {
|
||||
@@ -115,12 +117,19 @@ describe("workspace route parsing", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("round-trips numeric IDs through encode/decode", () => {
|
||||
const ids = ["1", "40", "164", "9999"];
|
||||
it("round-trips URL-safe IDs through encode/decode", () => {
|
||||
const ids = ["1", "40", "164", "9999", "workspace-1", "opaque_id.v2~test"];
|
||||
for (const id of ids) {
|
||||
const encoded = encodeWorkspaceIdForPathSegment(id);
|
||||
const decoded = decodeWorkspaceIdFromPathSegment(encoded);
|
||||
expect(decoded).toBe(id);
|
||||
}
|
||||
});
|
||||
|
||||
it("round-trips opaque IDs with reserved characters through base64 encoding", () => {
|
||||
const id = " team/setup:id#1 ";
|
||||
const encoded = encodeWorkspaceIdForPathSegment(id);
|
||||
expect(encoded).toBe("b64_dGVhbS9zZXR1cDppZCMx");
|
||||
expect(decodeWorkspaceIdFromPathSegment(encoded)).toBe("team/setup:id#1");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Buffer } from "buffer";
|
||||
|
||||
type NullableString = string | null | undefined;
|
||||
const BASE64_WORKSPACE_ID_PREFIX = "b64_";
|
||||
|
||||
function stripSearchAndHash(pathname: string): string {
|
||||
const hashIndex = pathname.indexOf("#");
|
||||
@@ -87,12 +88,16 @@ function tryDecodeBase64UrlNoPadUtf8(input: string): string | null {
|
||||
return decoded;
|
||||
}
|
||||
|
||||
function isPathLikeWorkspaceIdentity(value: string): boolean {
|
||||
return value.includes("/") || value.includes("\\") || /^[A-Za-z]:[\\/]/.test(value);
|
||||
function normalizeWorkspaceId(value: string): string {
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
function normalizeWorkspaceId(value: string): string {
|
||||
return value.trim().replace(/\\/g, "/").replace(/\/+$/, "");
|
||||
function isUrlSafeWorkspaceId(value: string): boolean {
|
||||
return /^[A-Za-z0-9._~-]+$/.test(value);
|
||||
}
|
||||
|
||||
function isLegacyPathLikeWorkspaceValue(value: string): boolean {
|
||||
return value.includes("/") || value.includes("\\") || /^[A-Za-z]:[\\/]/.test(value);
|
||||
}
|
||||
|
||||
export type WorkspaceOpenIntent =
|
||||
@@ -163,13 +168,11 @@ export function encodeWorkspaceIdForPathSegment(workspaceId: string): string {
|
||||
if (!normalized) {
|
||||
return "";
|
||||
}
|
||||
// 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);
|
||||
if (isUrlSafeWorkspaceId(id)) {
|
||||
return id;
|
||||
}
|
||||
return encodeURIComponent(id);
|
||||
return `${BASE64_WORKSPACE_ID_PREFIX}${toBase64UrlNoPad(id)}`;
|
||||
}
|
||||
|
||||
export function decodeWorkspaceIdFromPathSegment(workspaceIdSegment: string): string | null {
|
||||
@@ -178,32 +181,25 @@ export function decodeWorkspaceIdFromPathSegment(workspaceIdSegment: string): st
|
||||
return null;
|
||||
}
|
||||
|
||||
// Decode %2F etc first (legacy scheme), but keep the raw segment to decide if base64 applies.
|
||||
const decoded = trimNonEmpty(decodeSegment(normalizedSegment));
|
||||
if (!decoded) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Legacy: if it already looks like a path after decoding, keep it.
|
||||
if (decoded.includes("/") || decoded.includes("\\")) {
|
||||
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;
|
||||
if (decoded.startsWith(BASE64_WORKSPACE_ID_PREFIX)) {
|
||||
const encodedPayload = decoded.slice(BASE64_WORKSPACE_ID_PREFIX.length);
|
||||
const prefixedDecoded =
|
||||
tryDecodeBase64UrlNoPadUtf8(encodedPayload) ?? decodeBase64UrlNoPadUtf8(encodedPayload);
|
||||
return prefixedDecoded ? normalizeWorkspaceId(prefixedDecoded) : null;
|
||||
}
|
||||
|
||||
const base64Decoded = tryDecodeBase64UrlNoPadUtf8(decoded);
|
||||
if (base64Decoded) {
|
||||
if (base64Decoded && isLegacyPathLikeWorkspaceValue(base64Decoded)) {
|
||||
return normalizeWorkspaceId(base64Decoded);
|
||||
}
|
||||
|
||||
// Some older links use non-canonical base64url (non-zero pad bits). Accept
|
||||
// decoded values only when they clearly represent filesystem paths.
|
||||
const relaxedBase64Decoded = decodeBase64UrlNoPadUtf8(decoded);
|
||||
if (relaxedBase64Decoded && isPathLikeWorkspaceIdentity(relaxedBase64Decoded)) {
|
||||
if (relaxedBase64Decoded && isLegacyPathLikeWorkspaceValue(relaxedBase64Decoded)) {
|
||||
return normalizeWorkspaceId(relaxedBase64Decoded);
|
||||
}
|
||||
|
||||
|
||||
@@ -50,9 +50,9 @@ describe("buildNotificationRoute", () => {
|
||||
buildNotificationRoute({
|
||||
serverId: "srv-1",
|
||||
agentId: "agent-1",
|
||||
workspaceId: "/tmp/repo",
|
||||
workspaceId: "ws-main",
|
||||
}),
|
||||
).toBe("/h/srv-1/workspace/L3RtcC9yZXBv?open=agent%3Aagent-1");
|
||||
).toBe("/h/srv-1/workspace/ws-main?open=agent%3Aagent-1");
|
||||
});
|
||||
|
||||
it("routes directly to server-scoped agent path when both ids are present", () => {
|
||||
|
||||
@@ -10,9 +10,10 @@ import type {
|
||||
|
||||
function workspace(overrides: Partial<SidebarWorkspaceEntry> = {}): SidebarWorkspaceEntry {
|
||||
return {
|
||||
workspaceKey: "srv:/repo",
|
||||
workspaceKey: "srv:ws-root",
|
||||
serverId: "srv",
|
||||
workspaceId: "/repo",
|
||||
workspaceId: "ws-root",
|
||||
workspaceDirectory: "/repo",
|
||||
projectKind: "git",
|
||||
workspaceKind: "checkout",
|
||||
name: "paseo",
|
||||
@@ -41,7 +42,7 @@ function project(overrides: Partial<SidebarProjectEntry> = {}): SidebarProjectEn
|
||||
describe("buildSidebarProjectRowModel", () => {
|
||||
it("flattens non-git projects with one workspace into a direct workspace row model", () => {
|
||||
const flattenedWorkspace = workspace({
|
||||
workspaceId: "/repo/non-git",
|
||||
workspaceId: "ws-non-git",
|
||||
workspaceKind: "checkout",
|
||||
statusBucket: "running",
|
||||
});
|
||||
@@ -66,7 +67,7 @@ describe("buildSidebarProjectRowModel", () => {
|
||||
it("marks flattened non-git project rows as selected when their workspace is active", () => {
|
||||
const flattenedWorkspace = workspace({
|
||||
serverId: "srv-2",
|
||||
workspaceId: "/repo/non-git",
|
||||
workspaceId: "ws-non-git",
|
||||
});
|
||||
|
||||
const result = buildSidebarProjectRowModel({
|
||||
@@ -78,7 +79,7 @@ describe("buildSidebarProjectRowModel", () => {
|
||||
serverId: "srv-2",
|
||||
activeWorkspaceSelection: {
|
||||
serverId: "srv-2",
|
||||
workspaceId: "/repo/non-git",
|
||||
workspaceId: "ws-non-git",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -90,7 +91,7 @@ describe("buildSidebarProjectRowModel", () => {
|
||||
|
||||
it("keeps single-workspace git projects as sections with the new worktree action", () => {
|
||||
const onlyWorkspace = workspace({
|
||||
workspaceId: "/repo/main",
|
||||
workspaceId: "ws-main",
|
||||
workspaceKind: "checkout",
|
||||
});
|
||||
|
||||
@@ -114,8 +115,8 @@ describe("buildSidebarProjectRowModel", () => {
|
||||
project: project({
|
||||
projectKind: "git",
|
||||
workspaces: [
|
||||
workspace({ workspaceId: "/repo/main", workspaceKind: "checkout" }),
|
||||
workspace({ workspaceId: "/repo/feature", workspaceKind: "worktree" }),
|
||||
workspace({ workspaceId: "ws-main", workspaceKind: "checkout" }),
|
||||
workspace({ workspaceId: "ws-feature", workspaceKind: "worktree" }),
|
||||
],
|
||||
}),
|
||||
collapsed: true,
|
||||
@@ -144,8 +145,8 @@ describe("isSidebarProjectFlattened", () => {
|
||||
isSidebarProjectFlattened(
|
||||
project({
|
||||
workspaces: [
|
||||
workspace({ workspaceId: "/repo/main" }),
|
||||
workspace({ workspaceId: "/repo/feat" }),
|
||||
workspace({ workspaceId: "ws-main" }),
|
||||
workspace({ workspaceId: "ws-feat" }),
|
||||
],
|
||||
}),
|
||||
),
|
||||
|
||||
@@ -6,14 +6,20 @@ import type {
|
||||
|
||||
import { buildSidebarShortcutModel } from "./sidebar-shortcuts";
|
||||
|
||||
function workspace(serverId: string, cwd: string): SidebarWorkspaceEntry {
|
||||
function workspace(input: {
|
||||
serverId: string;
|
||||
workspaceId: string;
|
||||
workspaceDirectory: string;
|
||||
name: string;
|
||||
}): SidebarWorkspaceEntry {
|
||||
return {
|
||||
workspaceKey: `${serverId}:${cwd}`,
|
||||
serverId,
|
||||
workspaceId: cwd,
|
||||
workspaceKey: `${input.serverId}:${input.workspaceId}`,
|
||||
serverId: input.serverId,
|
||||
workspaceId: input.workspaceId,
|
||||
workspaceDirectory: input.workspaceDirectory,
|
||||
projectKind: "git",
|
||||
workspaceKind: "checkout",
|
||||
name: cwd,
|
||||
name: input.name,
|
||||
statusBucket: "done",
|
||||
diffStat: null,
|
||||
scripts: [],
|
||||
@@ -26,7 +32,7 @@ function project(projectKey: string, workspaces: SidebarWorkspaceEntry[]): Sideb
|
||||
projectKey,
|
||||
projectName: projectKey,
|
||||
projectKind: "git",
|
||||
iconWorkingDir: workspaces[0]?.workspaceId ?? "",
|
||||
iconWorkingDir: workspaces[0]?.workspaceDirectory ?? "",
|
||||
statusBucket: "done",
|
||||
activeCount: 0,
|
||||
totalWorkspaces: workspaces.length,
|
||||
@@ -37,8 +43,34 @@ function project(projectKey: string, workspaces: SidebarWorkspaceEntry[]): Sideb
|
||||
describe("buildSidebarShortcutModel", () => {
|
||||
it("builds shortcut targets in visual order and excludes collapsed projects", () => {
|
||||
const projects = [
|
||||
project("p1", [workspace("s1", "/repo/main"), workspace("s1", "/repo/feat-a")]),
|
||||
project("p2", [workspace("s1", "/repo2/main"), workspace("s1", "/repo2/feat-a")]),
|
||||
project("p1", [
|
||||
workspace({
|
||||
serverId: "s1",
|
||||
workspaceId: "ws-main",
|
||||
workspaceDirectory: "/repo/main",
|
||||
name: "main",
|
||||
}),
|
||||
workspace({
|
||||
serverId: "s1",
|
||||
workspaceId: "ws-feat-a",
|
||||
workspaceDirectory: "/repo/feat-a",
|
||||
name: "feat-a",
|
||||
}),
|
||||
]),
|
||||
project("p2", [
|
||||
workspace({
|
||||
serverId: "s1",
|
||||
workspaceId: "ws-repo2-main",
|
||||
workspaceDirectory: "/repo2/main",
|
||||
name: "main",
|
||||
}),
|
||||
workspace({
|
||||
serverId: "s1",
|
||||
workspaceId: "ws-repo2-feat-a",
|
||||
workspaceDirectory: "/repo2/feat-a",
|
||||
name: "feat-a",
|
||||
}),
|
||||
]),
|
||||
];
|
||||
|
||||
const model = buildSidebarShortcutModel({
|
||||
@@ -47,21 +79,26 @@ describe("buildSidebarShortcutModel", () => {
|
||||
});
|
||||
|
||||
expect(model.visibleTargets).toEqual([
|
||||
{ serverId: "s1", workspaceId: "/repo/main" },
|
||||
{ serverId: "s1", workspaceId: "/repo/feat-a" },
|
||||
{ serverId: "s1", workspaceId: "ws-main" },
|
||||
{ serverId: "s1", workspaceId: "ws-feat-a" },
|
||||
]);
|
||||
expect(model.shortcutTargets).toEqual([
|
||||
{ serverId: "s1", workspaceId: "/repo/main" },
|
||||
{ serverId: "s1", workspaceId: "/repo/feat-a" },
|
||||
{ serverId: "s1", workspaceId: "ws-main" },
|
||||
{ serverId: "s1", workspaceId: "ws-feat-a" },
|
||||
]);
|
||||
expect(model.shortcutIndexByWorkspaceKey.get("s1:/repo/main")).toBe(1);
|
||||
expect(model.shortcutIndexByWorkspaceKey.get("s1:/repo/feat-a")).toBe(2);
|
||||
expect(model.shortcutIndexByWorkspaceKey.get("s1:/repo2/main")).toBeUndefined();
|
||||
expect(model.shortcutIndexByWorkspaceKey.get("s1:ws-main")).toBe(1);
|
||||
expect(model.shortcutIndexByWorkspaceKey.get("s1:ws-feat-a")).toBe(2);
|
||||
expect(model.shortcutIndexByWorkspaceKey.get("s1:ws-repo2-main")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("limits shortcuts to 9", () => {
|
||||
const workspaces = Array.from({ length: 20 }, (_, index) =>
|
||||
workspace("s", `/repo/w${index + 1}`),
|
||||
workspace({
|
||||
serverId: "s",
|
||||
workspaceId: `ws-${index + 1}`,
|
||||
workspaceDirectory: `/repo/w${index + 1}`,
|
||||
name: `w${index + 1}`,
|
||||
}),
|
||||
);
|
||||
const projects = [project("p", workspaces)];
|
||||
|
||||
@@ -71,14 +108,23 @@ describe("buildSidebarShortcutModel", () => {
|
||||
});
|
||||
|
||||
expect(model.visibleTargets).toHaveLength(20);
|
||||
expect(model.visibleTargets[19]).toEqual({ serverId: "s", workspaceId: "/repo/w20" });
|
||||
expect(model.visibleTargets[19]).toEqual({ serverId: "s", workspaceId: "ws-20" });
|
||||
expect(model.shortcutTargets).toHaveLength(9);
|
||||
expect(model.shortcutTargets[0]).toEqual({ serverId: "s", workspaceId: "/repo/w1" });
|
||||
expect(model.shortcutTargets[8]).toEqual({ serverId: "s", workspaceId: "/repo/w9" });
|
||||
expect(model.shortcutTargets[0]).toEqual({ serverId: "s", workspaceId: "ws-1" });
|
||||
expect(model.shortcutTargets[8]).toEqual({ serverId: "s", workspaceId: "ws-9" });
|
||||
});
|
||||
|
||||
it("still excludes collapsed single-workspace git projects because they are not flattened", () => {
|
||||
const projects = [project("p1", [workspace("s1", "/repo/main")])];
|
||||
const projects = [
|
||||
project("p1", [
|
||||
workspace({
|
||||
serverId: "s1",
|
||||
workspaceId: "ws-main",
|
||||
workspaceDirectory: "/repo/main",
|
||||
name: "main",
|
||||
}),
|
||||
]),
|
||||
];
|
||||
|
||||
const model = buildSidebarShortcutModel({
|
||||
projects,
|
||||
|
||||
@@ -27,8 +27,10 @@ function createWorkspace(
|
||||
}
|
||||
|
||||
describe("resolveWorkspaceRouteId", () => {
|
||||
it("normalizes route workspace ids", () => {
|
||||
expect(resolveWorkspaceRouteId({ routeWorkspaceId: " /tmp/repo/ " })).toBe("/tmp/repo");
|
||||
it("trims route workspace ids without path normalization", () => {
|
||||
expect(resolveWorkspaceRouteId({ routeWorkspaceId: " C:\\tmp\\repo\\ " })).toBe(
|
||||
"C:\\tmp\\repo\\",
|
||||
);
|
||||
});
|
||||
|
||||
it("returns null for empty values", () => {
|
||||
@@ -87,12 +89,12 @@ describe("resolveWorkspaceMapKeyByIdentity", () => {
|
||||
expect(
|
||||
resolveWorkspaceMapKeyByIdentity({
|
||||
workspaces,
|
||||
workspaceIdentity: "workspace-1",
|
||||
workspaceId: "workspace-1",
|
||||
}),
|
||||
).toBe("workspace-1");
|
||||
});
|
||||
|
||||
it("resolves a workspace directory identity to the canonical map key", () => {
|
||||
it("does not resolve workspace directories when an id is required", () => {
|
||||
const workspaces = new Map<string, WorkspaceDescriptor>([
|
||||
[
|
||||
"workspace-1",
|
||||
@@ -106,9 +108,9 @@ describe("resolveWorkspaceMapKeyByIdentity", () => {
|
||||
expect(
|
||||
resolveWorkspaceMapKeyByIdentity({
|
||||
workspaces,
|
||||
workspaceIdentity: "C:/repo/feature",
|
||||
workspaceId: "C:/repo/feature",
|
||||
}),
|
||||
).toBe("workspace-1");
|
||||
).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { WorkspaceDescriptor } from "@/stores/session-store";
|
||||
import { normalizeWorkspaceIdentity } from "@/utils/workspace-identity";
|
||||
import { normalizeWorkspaceOpaqueId, normalizeWorkspacePath } from "@/utils/workspace-identity";
|
||||
|
||||
export type WorkspaceAuthorityResult = {
|
||||
workspaceId: string;
|
||||
@@ -23,20 +23,20 @@ export type WorkspaceExecutionAuthorityResult =
|
||||
export function resolveWorkspaceRouteId(input: {
|
||||
routeWorkspaceId: string | null | undefined;
|
||||
}): string | null {
|
||||
return normalizeWorkspaceIdentity(input.routeWorkspaceId);
|
||||
return normalizeWorkspaceOpaqueId(input.routeWorkspaceId);
|
||||
}
|
||||
|
||||
export function resolveWorkspaceIdByExecutionDirectory(input: {
|
||||
workspaces: Iterable<WorkspaceDescriptor> | null | undefined;
|
||||
workspaceDirectory: string | null | undefined;
|
||||
}): string | null {
|
||||
const normalizedWorkspaceDirectory = normalizeWorkspaceIdentity(input.workspaceDirectory);
|
||||
const normalizedWorkspaceDirectory = normalizeWorkspacePath(input.workspaceDirectory);
|
||||
if (!normalizedWorkspaceDirectory) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (const workspace of input.workspaces ?? []) {
|
||||
if (normalizeWorkspaceIdentity(workspace.workspaceDirectory) === normalizedWorkspaceDirectory) {
|
||||
if (normalizeWorkspacePath(workspace.workspaceDirectory) === normalizedWorkspaceDirectory) {
|
||||
return workspace.id;
|
||||
}
|
||||
}
|
||||
@@ -46,10 +46,10 @@ export function resolveWorkspaceIdByExecutionDirectory(input: {
|
||||
|
||||
export function resolveWorkspaceMapKeyByIdentity(input: {
|
||||
workspaces: Map<string, WorkspaceDescriptor> | null | undefined;
|
||||
workspaceIdentity: string | null | undefined;
|
||||
workspaceId: string | null | undefined;
|
||||
}): string | null {
|
||||
const normalizedWorkspaceIdentity = normalizeWorkspaceIdentity(input.workspaceIdentity);
|
||||
if (!normalizedWorkspaceIdentity) {
|
||||
const normalizedWorkspaceId = normalizeWorkspaceOpaqueId(input.workspaceId);
|
||||
if (!normalizedWorkspaceId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -58,15 +58,12 @@ export function resolveWorkspaceMapKeyByIdentity(input: {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (workspaces.has(normalizedWorkspaceIdentity)) {
|
||||
return normalizedWorkspaceIdentity;
|
||||
if (workspaces.has(normalizedWorkspaceId)) {
|
||||
return normalizedWorkspaceId;
|
||||
}
|
||||
|
||||
for (const [workspaceKey, workspace] of workspaces) {
|
||||
if (
|
||||
normalizeWorkspaceIdentity(workspace.id) === normalizedWorkspaceIdentity ||
|
||||
normalizeWorkspaceIdentity(workspace.workspaceDirectory) === normalizedWorkspaceIdentity
|
||||
) {
|
||||
if (normalizeWorkspaceOpaqueId(workspace.id) === normalizedWorkspaceId) {
|
||||
return workspaceKey;
|
||||
}
|
||||
}
|
||||
@@ -90,7 +87,7 @@ export function getWorkspaceExecutionAuthority(
|
||||
: (() => {
|
||||
const workspaceKey = resolveWorkspaceMapKeyByIdentity({
|
||||
workspaces: input.workspaces,
|
||||
workspaceIdentity: input.workspaceId,
|
||||
workspaceId: input.workspaceId,
|
||||
});
|
||||
if (!workspaceKey) {
|
||||
return null;
|
||||
@@ -99,7 +96,7 @@ export function getWorkspaceExecutionAuthority(
|
||||
})();
|
||||
|
||||
if ("workspaces" in input) {
|
||||
const normalizedWorkspaceId = normalizeWorkspaceIdentity(input.workspaceId);
|
||||
const normalizedWorkspaceId = normalizeWorkspaceOpaqueId(input.workspaceId);
|
||||
if (!normalizedWorkspaceId) {
|
||||
return {
|
||||
ok: false,
|
||||
@@ -120,7 +117,7 @@ export function getWorkspaceExecutionAuthority(
|
||||
};
|
||||
}
|
||||
|
||||
const workspaceDirectory = normalizeWorkspaceIdentity(workspace.workspaceDirectory);
|
||||
const workspaceDirectory = normalizeWorkspacePath(workspace.workspaceDirectory);
|
||||
if (!workspaceDirectory) {
|
||||
return {
|
||||
ok: false,
|
||||
@@ -156,24 +153,10 @@ export function requireWorkspaceExecutionAuthority(
|
||||
return result.authority;
|
||||
}
|
||||
|
||||
export function requireWorkspaceRecordId(workspaceId: string): number {
|
||||
const normalizedWorkspaceId = normalizeWorkspaceIdentity(workspaceId);
|
||||
if (!normalizedWorkspaceId) {
|
||||
throw new Error("Workspace ID is required");
|
||||
}
|
||||
|
||||
const parsedWorkspaceId = Number(normalizedWorkspaceId);
|
||||
if (!Number.isInteger(parsedWorkspaceId)) {
|
||||
throw new Error(`Workspace ID is not a persisted record ID: ${workspaceId}`);
|
||||
}
|
||||
|
||||
return parsedWorkspaceId;
|
||||
}
|
||||
|
||||
export function resolveWorkspaceExecutionDirectory(input: {
|
||||
workspaceDirectory: string | null | undefined;
|
||||
}): string | null {
|
||||
return normalizeWorkspaceIdentity(input.workspaceDirectory);
|
||||
return normalizeWorkspacePath(input.workspaceDirectory);
|
||||
}
|
||||
|
||||
export function requireWorkspaceExecutionDirectory(input: {
|
||||
@@ -206,5 +189,3 @@ export function resolveWorkspaceExecutionAuthority(
|
||||
const result = getWorkspaceExecutionAuthority(input);
|
||||
return result.ok ? result.authority : null;
|
||||
}
|
||||
|
||||
export const parseWorkspaceRecordId = requireWorkspaceRecordId;
|
||||
|
||||
@@ -6,7 +6,11 @@ function trimNonEmpty(value: string | null | undefined): string | null {
|
||||
return trimmed.length > 0 ? trimmed : null;
|
||||
}
|
||||
|
||||
export function normalizeWorkspaceIdentity(value: string | null | undefined): string | null {
|
||||
export function normalizeWorkspaceOpaqueId(value: string | null | undefined): string | null {
|
||||
return trimNonEmpty(value);
|
||||
}
|
||||
|
||||
export function normalizeWorkspacePath(value: string | null | undefined): string | null {
|
||||
const trimmed = trimNonEmpty(value);
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
|
||||
@@ -24,7 +24,7 @@ export function normalizeWorkspaceTabTarget(
|
||||
}
|
||||
if (value.kind === "setup") {
|
||||
const workspaceId = trimNonEmpty(value.workspaceId);
|
||||
return workspaceId ? { kind: "setup", workspaceId: workspaceId.replace(/\\/g, "/") } : null;
|
||||
return workspaceId ? { kind: "setup", workspaceId } : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user