fix(auth): validate stopped workspace restore
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user