fix: sanitize workspace API envelopes
This commit is contained in:
@@ -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 }),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user