What pompelmi is

pompelmi is a Node.js library that provides antivirus file scanning by delegating to ClamAV, the open-source antivirus engine. It exposes a single async function, scan(filePath), which returns a Promise resolving to one of three Symbol constants: Verdict.Clean, Verdict.Malicious, or Verdict.ScanError.

The name pompelmi is the Italian plural of pompelmo (grapefruit). It was chosen because grapefruits are bright, sharp, and slightly bitter — much like a good security check.

Why it exists

Most Node.js antivirus integrations either require running a persistent ClamAV daemon (clamd), parse raw stdout from clamscan, or wrap a native C extension. pompelmi takes the simplest possible path:

  • Spawn clamscan as a child process.
  • Read the exit code when it finishes.
  • Map that code to a Symbol constant and resolve the Promise.

No daemon. No stdout parsing. No native bindings. The result is a library that is trivial to install, audit, and reason about.

How it works

When you call pompelmi.scan('/path/to/file'), the following happens:

  1. The file path is checked with fs.existsSync. If missing, the Promise rejects immediately.
  2. Node's built-in child_process.spawn launches clamscan --no-summary <filePath> as a child process.
  3. pompelmi listens for the close event on the child process.
  4. The exit code is looked up in the SCAN_RESULTS map: 0 → Verdict.Clean, 1 → Verdict.Malicious, 2 → Verdict.ScanError.
  5. The Promise resolves with the matching Symbol constant, or rejects if the code is unrecognised.

The --no-summary flag suppresses ClamAV's final statistics output, keeping stdout clean. pompelmi never reads stdout at all; only the exit code matters.

Architecture

The data flow from a calling application to a scan result:

your code
    |
    | pompelmi.scan(filePath)
    v
ClamAVScanner.js
    |
    | child_process.spawn('clamscan', ['--no-summary', filePath])
    v
clamscan (system binary)
    |
    | exit code: 0 / 1 / 2
    v
SCAN_RESULTS map  (config.js)
    |
    | Verdict.Clean / Verdict.Malicious / Verdict.ScanError
    v
Promise resolved → your code

The installer and updater helpers follow the same pattern: they spawn the appropriate system package manager or freshclam and report success or failure via the exit code.

Modules

File Exports Purpose
index.js pompelmi Public entry point. Re-exports { scan }.
ClamAVScanner.js scan(filePath) Spawns clamscan, maps exit code, returns Promise.
ClamAVInstaller.js ClamAVInstaller() Installs ClamAV using the platform's package manager.
ClamAVDatabaseUpdater.js updateClamAVDatabase() Runs freshclam to refresh virus definitions.
InstallerCommand.js getInstallerCommand(), getUpdaterCommand() Returns the correct [binary, args] pair for the current platform.
config.js INSTALLER_COMMANDS, UPDATER_COMMANDS, DB_PATHS, SCAN_RESULTS Frozen config object. Single source of truth for all commands and Verdict Symbol mappings.
constants.js PLATFORM Exports process.platform so it can be mocked in tests.

Dependencies

pompelmi has zero runtime dependencies. It uses Node's built-in child_process module to spawn clamscan — no third-party packages are required at runtime. There is nothing to audit, nothing to patch, and no supply-chain surface beyond pompelmi itself.

Dev dependencies

Package Purpose
eslint, @eslint/js, globals Static analysis. Configured in eslint.config.mjs.

Design decisions

Why not use the clamd daemon?

The ClamAV daemon (clamd) provides faster repeated scanning because it keeps virus definitions in memory. However, it requires managing a background service, a socket or TCP connection, and additional configuration. pompelmi prioritises simplicity: spawn a process, get a result, done. For high-throughput scanning pipelines, switching to a clamd-based approach is reasonable, but that is outside pompelmi's scope.

Why return a Symbol instead of a string or boolean?

A boolean (true/false) would collapse the three-state result into two, losing the Verdict.ScanError case which represents a distinct and actionable condition: the scan failed and the file's safety is unknown. A plain string is error-prone — a typo such as 'Cleaan' compiles silently but always evaluates to false. A Symbol constant makes any unknown value undefined immediately, preventing silent mis-comparisons. Each Symbol carries a .description string for logging and serialisation.

Why freeze the config object?

config.js exports a frozen object. This prevents accidental mutation of shared configuration at runtime. Because config values are looked up on every scan, a mutation bug could silently change behaviour across all scans in a process.

Why no third-party spawn wrapper?

Node's built-in child_process.spawn does everything pompelmi needs: launch a process, listen for the close event, read the exit code. Third-party wrappers like cross-spawn or execa add dependency surface for problems pompelmi does not have. Keeping it to built-ins means zero runtime deps and nothing to audit or patch.

Limitations

  • Requires ClamAV on the host. pompelmi does not bundle or download ClamAV. The clamscan binary must be in PATH.
  • Virus definitions must be kept up to date manually. ClamAV's detection quality depends on fresh definitions. Run freshclam regularly or set up a cron job.
  • Scanning speed depends on file size. Each call spawns a new clamscan process, which loads definitions from disk every time. For bulk scanning, consider batching calls or switching to the clamd socket.
  • No directory scanning. scan() accepts a single file path. ClamAV supports recursive directory scanning, but pompelmi does not currently expose that option.
  • No streaming support. The file must exist on disk. Scanning a stream or buffer directly is not supported.