Archived
- WorkspaceFiles: writeFile, deleteFile, moveFile with path safety - writeFile: text/binary, auto-create dirs, overwrite option - deleteFile: idempotent, uses lstat (removes symlinks not targets) - moveFile: unix mv semantics, overwrite defaults to false - All mutations auto-refreshFiles() in File Explorer - Symlink escape prevention via realpath(dirname) check - PluginPromptEditor: insertText, getText, getSelection, onPaste, onKeyDown, focus - Uses CM6 EditorView.domEventHandlers() via Compartment (not raw DOM) - Handlers registered before mount are preserved and applied on mount - First-to-consume-wins ordering for multi-plugin scenarios - insertText replaces selection (not inserts after) - PluginAttachments: insertFileReference, getAttachedFiles, removeFileReference - insertFileReference validates file exists before inserting @path - Does not auto-focus editor (unlike prompt.insertText) - @file regex requires file extension to avoid matching emails - Server endpoints: PUT /file, DELETE /file, POST /file/move - All work for local and federated machines - Tests: 31 unit tests, 9 integration tests, 5 client tests - Docs: 3 new sections in plugins.md
43 lines
2.0 KiB
TypeScript
43 lines
2.0 KiB
TypeScript
import { realpath } from "node:fs/promises";
|
|
import { isAbsolute, join, relative, sep } from "node:path";
|
|
|
|
export async function resolveInsideWorkspace(rootPath: string, relativePath: string | undefined): Promise<{ root: string; target: string; relativePath: string }> {
|
|
const requested = normalizeRelativePath(relativePath);
|
|
const root = await realpath(rootPath);
|
|
const joined = join(root, requested);
|
|
const target = await realpath(joined).catch((error: unknown) => {
|
|
if (isNodeErrorWithCode(error, "ENOENT")) throw new Error("Path does not exist");
|
|
throw error;
|
|
});
|
|
ensureInside(root, target);
|
|
return { root, target, relativePath: requested };
|
|
}
|
|
|
|
export async function resolveParentInsideWorkspace(rootPath: string, relativePath: string): Promise<{ root: string; target: string; relativePath: string }> {
|
|
const requested = normalizeRelativePath(relativePath);
|
|
const root = await realpath(rootPath);
|
|
const target = join(root, requested);
|
|
ensureInside(root, target);
|
|
return { root, target, relativePath: requested };
|
|
}
|
|
|
|
export function normalizeRelativePath(input: string | undefined): string {
|
|
const value = input ?? "";
|
|
if (value === "" || value === ".") return "";
|
|
if (isAbsolute(value)) throw new Error("Absolute paths are not allowed");
|
|
const parts = value.split(/[\\/]+/).filter((part) => part !== "" && part !== ".");
|
|
if (parts.some((part) => part === "..")) throw new Error("Path traversal is not allowed");
|
|
return parts.join("/");
|
|
}
|
|
|
|
export function isNodeErrorWithCode(error: unknown, code: string): error is NodeJS.ErrnoException {
|
|
return typeof error === "object" && error !== null && "code" in error && error.code === code;
|
|
}
|
|
|
|
export function ensureInside(root: string, target: string): void {
|
|
const rel = relative(root, target);
|
|
if (rel === "") return;
|
|
if (rel.startsWith("..") || isAbsolute(rel)) throw new Error("Path escapes workspace");
|
|
if (sep !== "/" && rel.split(sep).includes("..")) throw new Error("Path escapes workspace");
|
|
}
|