fix: sanitize workspace API envelopes

This commit is contained in:
2026-08-09 20:38:54 +02:00
parent 80aa989523
commit a681c431fb
2 changed files with 103 additions and 9 deletions
+54 -8
View File
@@ -200,6 +200,33 @@ function object(value: unknown): Record<string, unknown> | undefined {
: undefined;
}
function exactObject(value: unknown, keys: readonly string[]): Record<string, unknown> | undefined {
const source = object(value);
return source && Object.keys(source).every((key) => keys.includes(key)) ? source : undefined;
}
function workspaceRevision(value: unknown, expectedId: string): WorkspaceRevision | undefined {
const source = exactObject(value, ["id", "commit", "blob", "snapshotPath", "state"]);
if (!source) return undefined;
const { id, commit, blob, snapshotPath, state } = source;
if (
id !== expectedId
|| typeof id !== "string" || !/^[a-z][a-z0-9-]{2,62}$/.test(id)
|| typeof commit !== "string" || !/^[0-9a-f]{40}$/.test(commit)
|| typeof blob !== "string" || !/^[0-9a-f]{40}$/.test(blob)
|| typeof snapshotPath !== "string" || snapshotPath.length === 0
|| snapshotPath.trim() !== snapshotPath || /[\u0000-\u001f\u007f]/u.test(snapshotPath)
|| (state !== "operational" && state !== "migration_required")
) return undefined;
return { id, commit, blob, snapshotPath, state };
}
function requireWorkspaceRevision(value: unknown, expectedId: string): WorkspaceRevision {
const revision = workspaceRevision(value, expectedId);
if (!revision) throw new Error("Workspace API returned an invalid workspace revision");
return revision;
}
function conflictRevision(value: unknown): WorkspaceConflictRevision | undefined {
const source = object(value);
const commit = source?.commit;
@@ -258,9 +285,10 @@ export const getWorkspace = async (id: string): Promise<WorkspaceRecord> => {
const response = await apiFetch<unknown>(`/workspaces/${encodeURIComponent(id)}`);
const source = object(response);
if (!source) throw new Error("Workspace API returned an invalid workspace record");
const workspace = requireCanonicalWorkspace(source.workspace);
return {
workspace: requireCanonicalWorkspace(source.workspace),
revision: source.revision as WorkspaceRevision,
workspace,
revision: requireWorkspaceRevision(source.revision, workspace.workspace.id),
};
};
export const getWorkspaceRegistryStatus = () => apiFetch<WorkspaceRegistryStatus>("/workspace-registry/status");
@@ -276,20 +304,38 @@ export const validateWorkspace = async (workspace: CanonicalWorkspace) => {
};
export const testWorkspace = (id: string) =>
apiFetch<WorkspaceDiagnostics>(`/workspaces/${encodeURIComponent(id)}/test`, { method: "POST" });
export const publishWorkspace = (request: PublishWorkspaceRequest) => {
export const publishWorkspace = async (request: PublishWorkspaceRequest) => {
const safeRequest: PublishWorkspaceRequest = request.action === "delete"
? request
: { ...request, workspace: requireCanonicalWorkspace(request.workspace) };
return apiFetch<{ revision: WorkspaceRevision } | undefined>("/workspaces/publish", {
const response = await apiFetch<unknown>("/workspaces/publish", {
method: "POST", body: JSON.stringify(safeRequest),
});
if (response === undefined) return undefined;
const source = exactObject(response, ["revision"]);
const expectedId = safeRequest.action === "delete" ? safeRequest.id : safeRequest.workspace.workspace.id;
if (!source) throw new Error("Workspace API returned an invalid publish result");
return { revision: requireWorkspaceRevision(source.revision, expectedId) };
};
export const exportWorkspace = (id: string) =>
apiFetchBlob(`/workspaces/${encodeURIComponent(id)}/export`);
export const importWorkspace = (bundle: File) => {
export const importWorkspace = async (bundle: File) => {
const body = new FormData();
body.set("bundle", bundle);
return apiFetch<{ draft: { workspace: CanonicalWorkspace; contract?: unknown } }>("/workspaces/import", {
method: "POST", body,
});
const response = await apiFetch<unknown>("/workspaces/import", { method: "POST", body });
const source = exactObject(response, ["draft"]);
const draft = exactObject(source?.draft, ["workspace", "contract"]);
if (!source || !draft) throw new Error("Workspace API returned an invalid imported workspace draft");
let workspace: CanonicalWorkspace;
try {
workspace = requireCanonicalWorkspace(draft.workspace);
} catch {
throw new Error("Workspace API returned an invalid imported workspace draft");
}
return {
draft: {
workspace,
...(draft.contract === undefined ? {} : { contract: draft.contract }),
},
};
};