Intelligence

Artifacts

Browse the repository, read documents, and manage the governance folders. Source, runtime, and infrastructure are read-only.

Repository
README.md
CONSTITUTION_COMPLIANCE_AUDIT_V1.mdREADME.md
repositories/aaf-holdings/hq01/lib/files/manager.ts
9.2 KB
import fs from "node:fs";
import path from "node:path";
import { CONTENT_ROOT } from "@/lib/content/config";
import { sanitizeFilename } from "@/lib/uploads/upload";
import { ALLOWED_EXTENSIONS, MANAGED_ROOTS, MAX_UPLOAD_BYTES } from "./config";

/**
 * The document file-manager — create/delete folders and documents within the
 * managed governance areas. Server-side only. This is the single place that
 * performs these mutations, and every path passes through {@link resolveManaged}
 * so nothing outside the managed roots can ever be written or removed.
 */

export class FileOpError extends Error {
  constructor(
    message: string,
    readonly status = 400,
  ) {
    super(message);
    this.name = "FileOpError";
  }
}

/** Strip leading separators and normalise; reject any traversal. */
function normalizeRel(relPath: string): string {
  const norm = path
    .normalize(relPath ?? "")
    .replace(/^([/\\])+/, "")
    .replace(/[/\\]+$/, "");
  return norm.split(path.sep).join("/");
}

/** The managed root a path belongs to, or null if it is outside all of them. */
export function managedRootOf(relPath: string): string | null {
  const norm = normalizeRel(relPath);
  if (!norm) return null;
  const first = norm.split("/")[0];
  return (MANAGED_ROOTS as readonly string[]).includes(first) ? first : null;
}

export function isManaged(relPath: string): boolean {
  try {
    resolveManaged(relPath);
    return true;
  } catch {
    return false;
  }
}

/**
 * Resolve a workspace-relative path to an absolute path, asserting it lives
 * inside one of the managed roots. Throws {@link FileOpError} otherwise.
 *
 * @param allowRoot when false, the path may not BE a top-level managed root
 *   (used by delete, so a root like `constitutions/` can never be removed).
 */
export function resolveManaged(
  relPath: string,
  { allowRoot = true }: { allowRoot?: boolean } = {},
): string {
  const norm = normalizeRel(relPath);
  if (!norm || norm.split("/").includes("..")) {
    throw new FileOpError("Invalid path.", 400);
  }
  const root = managedRootOf(norm);
  if (!root) {
    throw new FileOpError("Path is outside the manageable area.", 403);
  }

  const abs = path.join(CONTENT_ROOT, norm);
  const rootAbs = path.join(CONTENT_ROOT, root);
  const rootWithSep = rootAbs.endsWith(path.sep) ? rootAbs : rootAbs + path.sep;

  if (abs !== rootAbs && !abs.startsWith(rootWithSep)) {
    throw new FileOpError("Path escapes the managed root.", 403);
  }
  if (!allowRoot && abs === rootAbs) {
    throw new FileOpError("A top-level folder cannot be removed.", 403);
  }
  return abs;
}

function relOf(abs: string): string {
  return path.relative(CONTENT_ROOT, abs).split(path.sep).join("/");
}

/** Sanitise a folder name to a single safe path segment. */
export function sanitizeFoldername(raw: string): string {
  const base = (raw ?? "").split(/[/\\]/).pop() ?? "";
  let name = base
    .normalize("NFKD")
    .replace(/[^A-Za-z0-9._ -]+/g, "-")
    .replace(/\s+/g, "-")
    .replace(/-{2,}/g, "-")
    .replace(/^[.\-_]+/, "")
    .replace(/[.\-_]+$/, "");
  return name.slice(0, 60);
}

export interface DirEntry {
  name: string;
  type: "file" | "dir";
  path: string;
  ext?: string;
  size?: number;
  /** For folders: number of descendant files (used in delete confirmations). */
  files?: number;
  /** For folders: number of descendant sub-folders. */
  folders?: number;
}

export interface ManagedDir {
  path: string;
  isRoot: boolean;
  parent: string | null;
  entries: DirEntry[];
}

/** List the immediate contents of a managed folder, for the manager UI. */
export function listManagedDir(relPath: string): ManagedDir {
  const abs = resolveManaged(relPath);
  let stat: fs.Stats;
  try {
    stat = fs.statSync(abs);
  } catch {
    throw new FileOpError("Folder not found.", 404);
  }
  if (!stat.isDirectory()) throw new FileOpError("Not a folder.", 400);

  const norm = relOf(abs);
  const entries: DirEntry[] = fs
    .readdirSync(abs, { withFileTypes: true })
    .filter((e) => !e.name.startsWith("."))
    .map((e) => {
      const childRel = `${norm}/${e.name}`;
      if (e.isDirectory()) {
        const c = countTree(path.join(abs, e.name));
        return {
          name: e.name,
          type: "dir" as const,
          path: childRel,
          files: c.files,
          folders: c.folders,
        };
      }
      let size: number | undefined;
      try {
        size = fs.statSync(path.join(abs, e.name)).size;
      } catch {
        size = undefined;
      }
      return {
        name: e.name,
        type: "file" as const,
        path: childRel,
        ext: path.extname(e.name).toLowerCase(),
        size,
      };
    })
    .sort((a, b) => {
      if (a.type !== b.type) return a.type === "dir" ? -1 : 1;
      return a.name.localeCompare(b.name);
    });

  const isRoot = managedRootOf(norm) === norm;
  return {
    path: norm,
    isRoot,
    parent: isRoot ? null : norm.split("/").slice(0, -1).join("/"),
    entries,
  };
}

/** Create a sub-folder inside a managed folder. Returns its relative path. */
export function createFolder(parentRel: string, name: string): string {
  const parentAbs = resolveManaged(parentRel);
  let pstat: fs.Stats;
  try {
    pstat = fs.statSync(parentAbs);
  } catch {
    throw new FileOpError("Parent folder not found.", 404);
  }
  if (!pstat.isDirectory()) throw new FileOpError("Parent is not a folder.", 400);

  const safe = sanitizeFoldername(name);
  if (!safe) throw new FileOpError("Enter a valid folder name.", 400);

  const targetAbs = path.join(parentAbs, safe);
  // Re-validate the composed target stays inside the managed area.
  resolveManaged(relOf(targetAbs));
  if (fs.existsSync(targetAbs)) {
    throw new FileOpError("A folder with that name already exists.", 409);
  }
  fs.mkdirSync(targetAbs);
  return relOf(targetAbs);
}

/** Count of files and folders beneath an absolute path (excludes the path itself). */
function countTree(abs: string): { files: number; folders: number } {
  let files = 0;
  let folders = 0;
  const walk = (dir: string) => {
    let entries: fs.Dirent[];
    try {
      entries = fs.readdirSync(dir, { withFileTypes: true });
    } catch {
      return;
    }
    for (const e of entries) {
      if (e.isDirectory()) {
        folders += 1;
        walk(path.join(dir, e.name));
      } else {
        files += 1;
      }
    }
  };
  walk(abs);
  return { files, folders };
}

/** Recursively delete a managed sub-folder (never a top-level root). */
export function deleteFolder(relPath: string): { files: number; folders: number } {
  const abs = resolveManaged(relPath, { allowRoot: false });
  let stat: fs.Stats;
  try {
    stat = fs.statSync(abs);
  } catch {
    throw new FileOpError("Folder not found.", 404);
  }
  if (!stat.isDirectory()) throw new FileOpError("Not a folder.", 400);

  const counts = countTree(abs);
  fs.rmSync(abs, { recursive: true, force: true });
  return counts;
}

/** Delete a single managed document. */
export function deleteDocument(relPath: string): void {
  const abs = resolveManaged(relPath, { allowRoot: false });
  let stat: fs.Stats;
  try {
    stat = fs.statSync(abs);
  } catch {
    throw new FileOpError("File not found.", 404);
  }
  if (!stat.isFile()) throw new FileOpError("Not a file.", 400);
  fs.unlinkSync(abs);
}

export interface SavedDocument {
  name: string;
  relativePath: string;
  size: number;
}

/** Validate and save an uploaded document into a managed folder. */
export function saveToFolder(
  dirRel: string,
  filename: string,
  size: number,
  data: Buffer,
): SavedDocument {
  const dirAbs = resolveManaged(dirRel);
  let dstat: fs.Stats;
  try {
    dstat = fs.statSync(dirAbs);
  } catch {
    throw new FileOpError("Target folder not found.", 404);
  }
  if (!dstat.isDirectory()) throw new FileOpError("Target is not a folder.", 400);

  if (size <= 0 || data.length === 0) throw new FileOpError("The file is empty.", 400);
  if (size > MAX_UPLOAD_BYTES || data.length > MAX_UPLOAD_BYTES) {
    throw new FileOpError("File exceeds the 25MB limit.", 413);
  }

  const safeName = sanitizeFilename(filename);
  const ext = path.extname(safeName).toLowerCase();
  if (!ALLOWED_EXTENSIONS.has(ext)) {
    throw new FileOpError(
      `Unsupported file type "${ext || "(none)"}". Allowed: ${[...ALLOWED_EXTENSIONS].join(", ")}.`,
      415,
    );
  }

  const stem = safeName.slice(0, safeName.length - ext.length);
  const dirWithSep = dirAbs.endsWith(path.sep) ? dirAbs : dirAbs + path.sep;

  for (let attempt = 0; attempt < 1000; attempt += 1) {
    const finalName = attempt === 0 ? safeName : `${stem}-${attempt}${ext}`;
    const target = path.join(dirAbs, finalName);
    if (!target.startsWith(dirWithSep)) {
      throw new FileOpError("Resolved path escapes the folder.", 400);
    }
    try {
      const fd = fs.openSync(target, "wx");
      try {
        fs.writeFileSync(fd, data);
      } finally {
        fs.closeSync(fd);
      }
      return { name: finalName, relativePath: relOf(target), size: data.length };
    } catch (err) {
      if ((err as NodeJS.ErrnoException)?.code === "EEXIST") continue;
      throw new FileOpError(
        `Failed to write file: ${err instanceof Error ? err.message : String(err)}`,
        500,
      );
    }
  }
  throw new FileOpError("Could not find a free filename.", 409);
}

root · /srv/aaf