All files / src/lib/dsn fs-utils.ts

100% Statements 11/11
100% Branches 12/12
100% Functions 3/3
100% Lines 11/11

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88                                                    1796x 1794x 1794x               2x                           1796x 5x                                                       1247x 1247x 76x   1169x 1169x      
/**
 * File System Utilities for DSN Detection
 *
 * Shared utilities for handling file system errors during scanning.
 */
 
import { stat } from "node:fs/promises";
// biome-ignore lint/performance/noNamespaceImport: Sentry SDK recommends namespace import
import * as Sentry from "@sentry/node-core/light";
 
/**
 * Check if an error is an expected file system error that should be silently ignored.
 *
 * Expected errors during file scanning:
 * - ENOENT: File or directory does not exist
 * - EACCES: Permission denied (e.g., no read access)
 * - EPERM: Operation not permitted (e.g., file locked, or system-level restriction)
 * - EISDIR: Path is a directory, not a file (e.g., `.env/` directory instead of `.env` file)
 * - ENOTDIR: A path component is not a directory (e.g., `/file.txt/child`)
 *
 * All other errors are unexpected and should be reported to Sentry.
 *
 * @param error - The error to check
 * @returns True if the error is expected and should be ignored
 */
function isIgnorableFileError(error: unknown): boolean {
  if (error instanceof Error && "code" in error) {
    const code = (error as NodeJS.ErrnoException).code;
    return (
      code === "ENOENT" ||
      code === "EACCES" ||
      code === "EPERM" ||
      code === "EISDIR" ||
      code === "ENOTDIR"
    );
  }
  return false;
}
 
/**
 * Handle a file system error by either ignoring it (for expected errors)
 * or capturing it to Sentry (for unexpected errors).
 *
 * @param error - The error that occurred
 * @param context - Additional context for Sentry (e.g., file path, operation)
 */
export function handleFileError(
  error: unknown,
  context: { operation: string; path?: string }
): void {
  if (!isIgnorableFileError(error)) {
    Sentry.captureException(error, {
      tags: {
        operation: context.operation,
      },
      extra: {
        path: context.path,
      },
    });
  }
}
 
/**
 * Check if a path points to a regular file (not a FIFO, socket, device, etc.).
 *
 * Named pipes (FIFOs) — commonly used by 1Password to stream secrets via
 * symlinked `.env` files — cause `readFile()` to block indefinitely
 * waiting for a writer. This guard uses `stat()`, which follows symlinks
 * and inspects file type without performing the blocking read, so a
 * symlink → FIFO is correctly detected.
 *
 * @param filePath - Absolute path to check
 * @param operation - Logical operation name for unexpected stat error reporting
 * @returns True if the path is a regular file safe to read, false otherwise
 */
export async function isRegularFile(
  filePath: string,
  operation = "isRegularFile"
): Promise<boolean> {
  try {
    const stats = await stat(filePath);
    return stats.isFile();
  } catch (error) {
    handleFileError(error, { operation, path: filePath });
    return false;
  }
}