fix(auth): validate stopped workspace restore

This commit is contained in:
2026-08-18 00:06:28 +02:00
parent e8b9995ed0
commit 0651f3316f
6 changed files with 300 additions and 11 deletions
+61
View File
@@ -31,6 +31,11 @@ export interface WorkspaceRevision {
snapshotPath: string;
}
export interface StoredWorkspaceIntegrity {
state: "uninitialized" | "active";
workspaces: number;
}
export interface SessionRevisionLease {
workspace: WorkspaceDescriptor;
revision: WorkspaceRevision;
@@ -180,6 +185,48 @@ export class WorkspaceRegistry {
});
}
/**
* Validate only persisted local registry state. Restore uses this path while the installation
* is stopped: it must neither contact Git nor turn a never-used registry into initialized state.
*/
async verifyStoredState(): Promise<StoredWorkspaceIntegrity> {
await this.repository.ensureLayout();
return await this.lock.run(async () => {
try {
const stateEntries = await readdir(this.repository.statePath, { withFileTypes: true });
if (stateEntries.some((entry) => (
entry.name !== "active.json" || !entry.isFile() || entry.isSymbolicLink()
))) throw new Error("workspace state directory is partial");
const snapshotEntries = await readdir(this.repository.snapshotsPath, { withFileTypes: true });
for (const entry of snapshotEntries) {
const isRuntime = entry.name === "runtime";
const isSnapshot = /^[0-9a-f]{40}$/.test(entry.name);
if ((!isRuntime && !isSnapshot) || !entry.isDirectory() || entry.isSymbolicLink()) {
throw new Error("workspace snapshot directory is partial");
}
}
const hasActiveState = stateEntries.length === 1;
if (!hasActiveState) {
if (snapshotEntries.some((entry) => entry.name !== "runtime") || this.storedPathExists(this.repository.repoPath)) {
throw new Error("workspace registry is partially initialized");
}
return { state: "uninitialized", workspaces: 0 };
}
const active = await this.activeState();
for (const entry of snapshotEntries) {
if (entry.name === "runtime" || entry.name === active.head) continue;
await this.snapshotState(entry.name);
}
return { state: "active", workspaces: active.revisions.length };
} catch (error) {
throw workspaceError(error);
}
});
}
async read(id: string): Promise<{ workspace: WorkspaceDescriptor; revision: WorkspaceRevision }> {
const state = await this.activeState();
const revision = state.revisions.find((candidate) => candidate.id === id);
@@ -737,6 +784,20 @@ export class WorkspaceRegistry {
}
}
private storedPathExists(path: string): boolean {
try {
const entry = lstatSync(path);
if (!entry.isDirectory() || entry.isSymbolicLink()) {
throw new WorkspaceRegistryError("workspace_invalid", "Workspace registry path is invalid");
}
return true;
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") return false;
if (error instanceof WorkspaceRegistryError) throw error;
throw new WorkspaceRegistryError("workspace_invalid", "Workspace registry path is unavailable");
}
}
private pathIsMissing(path: string): boolean {
try {
lstatSync(path);