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
clamscanas 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:
- The file path is checked with
fs.existsSync. If missing, the Promise rejects immediately. - Node's built-in
child_process.spawnlaunchesclamscan --no-summary <filePath>as a child process. - pompelmi listens for the
closeevent on the child process. - The exit code is looked up in the
SCAN_RESULTSmap:0 → Verdict.Clean,1 → Verdict.Malicious,2 → Verdict.ScanError. - 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
clamscanbinary must be inPATH. -
Virus definitions must be kept up to date manually.
ClamAV's detection quality depends on fresh definitions. Run
freshclamregularly or set up a cron job. -
Scanning speed depends on file size.
Each call spawns a new
clamscanprocess, 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.