Options
All scan functions accept an optional ScanOptions object as their last
argument. Every property is optional.
| Property | Type | Default | Description |
|---|---|---|---|
socket |
string |
— | UNIX domain socket path (e.g. /run/clamav/clamd.sock). Takes precedence over host and port. |
host |
string |
— | clamd hostname or IP address. Setting this enables TCP mode. |
port |
number |
3310 |
clamd TCP port. |
timeout |
number |
15000 |
Socket idle timeout in milliseconds. Applies to clamd mode only (TCP and UNIX socket). Has no effect in local clamscan mode. |
retries |
number |
0 |
Number of automatic retry attempts on connection error. Default 0 means no retries. |
retryDelay |
number |
1000 |
Milliseconds to wait between retry attempts. |
Mode selection
The connection mode is selected automatically from the options you provide:
- If
socketis set, pompelmi connects via UNIX domain socket. - Otherwise, if
hostis set (orportalone), pompelmi connects via TCP. - If neither is set, pompelmi spawns a local
clamscanprocess.
Verdicts
All scan functions resolve to one of three Symbol constants exported from pompelmi.
Always compare with === — never against raw strings.
| Constant | .description |
Local exit code | clamd response | Meaning |
|---|---|---|---|---|
Verdict.Clean |
'Clean' |
0 | stream: OK |
No threats detected. |
Verdict.Malicious |
'Malicious' |
1 | stream: <name> FOUND |
A virus or malware signature was matched. |
Verdict.ScanError |
'ScanError' |
2 | anything else | The scan failed. Treat the file as untrusted. |
Why Symbols?
String-based verdicts are fragile: a typo such as 'Cleaan' compiles
silently but always evaluates to false. With Symbols, any unknown
property on Verdict is undefined, making the mistake
visible immediately. Symbols also prevent accidental equality with third-party
string values.
Using .description for logging
const { scan, Verdict } = require('pompelmi');
const result = await scan('/uploads/report.pdf', { host: '127.0.0.1', port: 3310 });
// Compare with ===
if (result === Verdict.Malicious) { /* reject */ }
// Use .description for logging / JSON
console.log(result.description); // 'Clean'
logger.info({ verdict: result.description }); // { verdict: 'Clean' }
scan(filePath, [options])
Scans a file on the local filesystem. In clamd mode (TCP or UNIX socket), the file
is read and streamed to clamd using the INSTREAM protocol. In local mode, a
clamscan child process is spawned.
Signature
scan(filePath: string, options?: ScanOptions): Promise<symbol>
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
filePath |
string |
Yes | Absolute or relative path to the file to scan. Must exist before the call. |
options |
ScanOptions |
No | Connection and timeout options. Omit to use local clamscan. |
Return value
A Promise that resolves to Verdict.Clean, Verdict.Malicious, or Verdict.ScanError.
Rejection reasons
| Message pattern | Backend | Cause |
|---|---|---|
filePath must be a string |
both | Argument is not a string. |
File not found: <path> |
both | fs.existsSync(filePath) returned false. |
ENOENT |
local | clamscan binary not found in PATH. |
Unexpected exit code: <n> |
local | ClamAV exited with an unrecognised exit code. |
ECONNREFUSED |
clamd | Nothing is listening on the specified host/port. |
clamd connection timed out after Nms |
clamd | Socket idle for longer than options.timeout. |
Example
const { scan, Verdict } = require('pompelmi');
const result = await scan('/var/uploads/report.pdf', {
host: '127.0.0.1',
port: 3310,
});
switch (result) {
case Verdict.Clean: /* proceed */ break;
case Verdict.Malicious: /* reject file */ break;
case Verdict.ScanError: /* treat as untrusted */ break;
}
console.log(result.description); // 'Clean' | 'Malicious' | 'ScanError'
scanBuffer(buffer, [options])
Scans an in-memory Buffer. In clamd mode (TCP or UNIX socket),
the buffer is piped directly to clamd — no disk I/O at all.
In local mode, the buffer is written to a temporary file in os.tmpdir()
and deleted in a finally block after the scan completes.
Signature
scanBuffer(buffer: Buffer, options?: ScanOptions): Promise<symbol>
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
buffer |
Buffer |
Yes | The in-memory file data to scan. |
options |
ScanOptions |
No | Connection and timeout options. |
Extra validation errors
| Message | Cause |
|---|---|
buffer must be a Buffer |
Argument is not a Buffer instance. |
buffer is empty |
Buffer has zero length. |
Example — multer memoryStorage
const express = require('express');
const multer = require('multer');
const { scanBuffer, Verdict } = require('pompelmi');
const upload = multer({ storage: multer.memoryStorage() });
app.post('/upload', upload.single('file'), async (req, res) => {
const result = await scanBuffer(req.file.buffer, {
host: process.env.CLAMAV_HOST,
port: 3310,
});
if (result === Verdict.Malicious) {
return res.status(400).json({ error: 'Malware detected' });
}
// buffer never touched disk during the scan
await fs.promises.writeFile('/data/' + req.file.originalname, req.file.buffer);
res.json({ ok: true });
});
scanStream(stream, [options])
Scans a Node.js Readable stream. In clamd mode, the stream is piped
directly to clamd via the INSTREAM protocol — no disk I/O. In local mode,
the stream is piped to a temporary file in os.tmpdir() and deleted
in a finally block.
Signature
scanStream(stream: Readable, options?: ScanOptions): Promise<symbol>
Extra validation error
| Message | Cause |
|---|---|
stream must be a Readable |
Argument is not a Node.js Readable instance. |
Example — S3 GetObject
const { scanStream, Verdict } = require('pompelmi');
const { S3Client, GetObjectCommand } = require('@aws-sdk/client-s3');
const s3 = new S3Client({ region: 'us-east-1' });
const { Body } = await s3.send(new GetObjectCommand({
Bucket: 'my-uploads',
Key: 'incoming/document.docx',
}));
const result = await scanStream(Body, {
host: '127.0.0.1',
port: 3310,
timeout: 30000,
});
if (result === Verdict.Malicious) {
console.error('Malware found in S3 object');
}
scanDirectory(dirPath, [options])
Recursively scans every file under dirPath. Per-file errors are
collected and never thrown. The function always resolves with a summary object.
Signature
scanDirectory(
dirPath: string,
options?: ScanOptions
): Promise<{ clean: string[]; malicious: string[]; errors: string[] }>
Return value
| Property | Type | Description |
|---|---|---|
clean |
string[] |
Absolute paths of files that returned Verdict.Clean. |
malicious |
string[] |
Absolute paths of files that returned Verdict.Malicious. |
errors |
string[] |
Absolute paths of files that returned Verdict.ScanError or threw. |
Example
const { scanDirectory } = require('pompelmi');
const report = await scanDirectory('/var/uploads', {
host: '127.0.0.1',
port: 3310,
});
console.log(`Clean: ${report.clean.length}`);
console.log(`Malicious: ${report.malicious.length}`);
if (report.malicious.length > 0) {
console.error('Infected files:', report.malicious);
}
middleware([options])
Returns an Express/Fastify middleware function. Designed to run after multer
(or any other file parser that populates req.file or req.files).
If a malicious file is detected, the middleware responds immediately with HTTP 403
and does not call next().
Signature
middleware(options?: MiddlewareOptions): RequestHandler
MiddlewareOptions
MiddlewareOptions extends ScanOptions with one additional property:
| Property | Type | Default | Description |
|---|---|---|---|
uploadField |
string |
'file' |
The multer field name to scan. Ignored when req.files is an array. |
Example
const express = require('express');
const multer = require('multer');
const { middleware } = require('pompelmi');
const app = express();
const upload = multer({ storage: multer.memoryStorage() });
app.post(
'/upload',
upload.single('file'),
middleware({ host: '127.0.0.1', port: 3310 }),
(req, res) => {
// Only reached when the file is clean
res.json({ ok: true });
}
);
403 Forbidden with JSON body
{ "error": "Malware detected" } and stops the chain. On scan error
it calls next(err) so your error handler can decide.
scanS3(params, [options])
Streams an S3 object via GetObjectCommand directly to
scanStream() — no disk I/O. The AWS SDK is an optional peer
dependency; pompelmi does not bundle it.
Install the AWS SDK
npm install @aws-sdk/client-s3
If the SDK is not installed at runtime, scanS3() throws:
Install AWS SDK: npm install @aws-sdk/client-s3
Signature
scanS3(
params: { bucket: string; key: string; region?: string; credentials?: AwsCredentialIdentity },
options?: ScanOptions
): Promise<symbol>
Params
| Property | Type | Required | Description |
|---|---|---|---|
bucket |
string |
Yes | S3 bucket name. |
key |
string |
Yes | S3 object key. |
region |
string |
No | AWS region. Defaults to AWS_REGION env var or SDK default. |
credentials |
AwsCredentialIdentity |
No | Explicit AWS credentials. Omit to use the default credential chain (IAM role, env vars, etc.). |
Example
const { scanS3, Verdict } = require('pompelmi');
const result = await scanS3(
{ bucket: 'my-uploads', key: 'incoming/report.pdf', region: 'us-east-1' },
{ host: '127.0.0.1', port: 3310 }
);
if (result === Verdict.Malicious) {
console.error('Malware detected in S3 object');
}
createPool([options])
Creates a persistent connection pool to clamd. Connections are reused across calls; when all slots are busy, new requests queue until a slot is free. Auto-reconnects on connection drop.
Signature
createPool(options?: PoolOptions): ClamdPool
PoolOptions
PoolOptions extends ScanOptions with:
| Property | Type | Default | Description |
|---|---|---|---|
size |
number |
5 |
Number of persistent connections to maintain. |
ClamdPool methods
| Method | Description |
|---|---|
pool.scan(filePath) |
Scans a file path using a pooled connection. |
pool.scanBuffer(buffer) |
Scans an in-memory Buffer using a pooled connection. |
pool.scanStream(stream) |
Scans a Readable stream using a pooled connection. |
pool.destroy() |
Closes all connections and releases resources. Call on app shutdown. |
Example — scanning many buffers concurrently
const { createPool, Verdict } = require('pompelmi');
const pool = createPool({ host: '127.0.0.1', port: 3310, size: 5 });
// Fan-out: scan 20 files simultaneously; pool queues the overflow
const results = await Promise.all(
buffers.map(buf => pool.scanBuffer(buf))
);
const infected = results.filter(r => r === Verdict.Malicious).length;
console.log(`${infected} infected file(s) out of ${buffers.length}`);
// Shut down cleanly
await pool.destroy();
watch(dirPath, [options], callbacks)
Watches a directory for new or changed files and scans each one automatically.
Built on fs.watch with a 300 ms debounce. Returns an
FSWatcher handle; call .close() to stop watching.
Signature
watch(
dirPath: string,
options: ScanOptions,
callbacks: {
onClean: (filePath: string) => void;
onMalicious: (filePath: string) => void;
onError: (err: Error, filePath?: string) => void;
}
): FSWatcher
Example
const { watch } = require('pompelmi');
const watcher = watch(
'/var/incoming',
{ host: '127.0.0.1', port: 3310 },
{
onClean: (fp) => console.log('Clean:', fp),
onMalicious: (fp) => {
console.error('INFECTED:', fp);
fs.unlink(fp, () => {}); // quarantine or delete
},
onError: (err, fp) => console.error('Scan error', fp, err.message),
}
);
// Stop watching after 1 hour
setTimeout(() => watcher.close(), 3_600_000);
notify(webhookUrl, scanResult, [options])
Sends a POST request to webhookUrl when a scan result is available.
By default only fires when the verdict is Malicious.
Supports HMAC-SHA256 request signing via the X-Pompelmi-Signature
header. Uses Node.js built-in https/http — zero extra
dependencies.
Signature
notify(
webhookUrl: string,
scanResult: { file?: string | null; verdict: symbol | string; viruses?: string[] },
options?: NotifyOptions
): Promise<void>
NotifyOptions
| Property | Type | Default | Description |
|---|---|---|---|
onlyOnMalicious |
boolean |
true |
Skip the request when the verdict is not Malicious. |
secret |
string |
— | HMAC-SHA256 key. When provided, adds X-Pompelmi-Signature: sha256=<hex> to the request. |
Webhook payload
{
"file": "/uploads/invoice.pdf",
"verdict": "Malicious",
"viruses": ["Eicar-Test-Signature"],
"timestamp": "2026-05-04T12:00:00.000Z",
"hostname": "api-server-01"
}
Example
const { scan, notify, Verdict } = require('pompelmi');
const verdict = await scan('/uploads/invoice.pdf', { host: '127.0.0.1', port: 3310 });
await notify('https://hooks.example.com/security', {
file: '/uploads/invoice.pdf',
verdict,
viruses: [],
}, {
onlyOnMalicious: true,
secret: process.env.WEBHOOK_SECRET,
});
createScanner([options])
Returns an EventEmitter-based scanner with scan(filePath)
and scanDirectory(dirPath) methods. Emits 'clean',
'malicious', 'scanError', and 'error'
events per file. Ideal for streaming pipelines and upload processing loops.
Options are forwarded to the underlying scan() call.
Signature
createScanner(options?: ScanOptions): ScanEmitter
Events
| Event | Arguments | Description |
|---|---|---|
'clean' |
(filePath: string) |
File scanned clean. |
'malicious' |
(filePath: string, viruses: string[]) |
Virus or malware detected. |
'scanError' |
(filePath: string) |
Scan returned Verdict.ScanError — treat the file as untrusted. |
'error' |
(err: Error) |
Unexpected infrastructure error (connection refused, file not found, etc.). |
Methods
| Method | Description |
|---|---|
scanner.scan(filePath) |
Scan a single file; emits one of the four events above. |
scanner.scanDirectory(dirPath) |
Recursively scan every file under dirPath; emits events per file. |
Example
const { createScanner } = require('pompelmi');
const scanner = createScanner({ host: 'localhost', port: 3310 });
scanner.on('malicious', (file, viruses) => {
console.error('VIRUS DETECTED:', file, viruses);
fs.unlink(file, () => {});
});
scanner.on('clean', (file) => console.log('OK:', file));
scanner.on('scanError', (file) => console.warn('Scan error — treat as untrusted:', file));
scanner.on('error', (err) => console.error('Infrastructure error:', err.message));
// Scan a single file
scanner.scan('/uploads/report.pdf');
// Scan an entire directory
scanner.scanDirectory('/var/incoming');
createCache(options?)
Creates a SHA256-based scan result cache. Returns a ScanCache object whose
.scan() and .scanBuffer() methods wrap the underlying scan
functions, returning cached verdicts for previously-seen file hashes.
See the full Cache reference for all options and methods.
const { createCache } = require('pompelmi');
const cache = createCache({ ttl: 3600000, maxSize: 1000 });
const verdict = await cache.scanBuffer(buffer, { host: 'localhost', port: 3310 });
console.log(cache.stats()); // { hits, misses, size, hitRate }
createPolicy(rules?)
Creates a unified scan policy combining size limits, MIME/extension allowlists, encrypted
archive rejection, and virus scanning. Returns a ScanPolicy with
.check(), .middleware(), and .nestGuard().
See the full Policy reference.
const { createPolicy } = require('pompelmi');
const policy = createPolicy({
maxSize: 10 * 1024 * 1024,
allowedMimeTypes: ['image/jpeg', 'image/png', 'application/pdf'],
allowedExtensions: ['.jpg', '.jpeg', '.png', '.pdf'],
scan: { host: 'localhost', port: 3310 },
onScannerUnavailable: 'reject',
});
// Use directly
const result = await policy.check(buffer, { filename: 'doc.pdf', mimeType: 'application/pdf' });
// Or as Express middleware
app.post('/upload', multer().single('file'), policy.middleware(), handler);
createMultiEngine(options?)
Creates a multi-engine scanner that runs files through ClamAV and/or VirusTotal in parallel, combining verdicts with a configurable consensus mode. See the full Multi-Engine reference.
const { createMultiEngine } = require('pompelmi');
const scanner = createMultiEngine({
engines: [
{ type: 'clamav', host: 'localhost', port: 3310 },
{ type: 'virustotal', apiKey: process.env.VIRUSTOTAL_API_KEY, threshold: 2 },
],
consensus: 'any', // 'any' | 'all' | 'majority'
});
const result = await scanner.scanBuffer(buffer);
// { verdict: Verdict.Clean, consensus: 'any', engines: [ ... ] }
Error types
All errors are plain Error instances. There are no custom error classes.
Distinguish them by err.message.
| Message | Source function | Recovery |
|---|---|---|
filePath must be a string |
scan() |
Pass a string path. |
File not found: <path> |
scan() |
Verify the file exists before calling. |
buffer must be a Buffer |
scanBuffer() |
Pass a Node.js Buffer. |
buffer is empty |
scanBuffer() |
Check that the buffer has length > 0 before calling. |
stream must be a Readable |
scanStream() |
Pass a Node.js Readable stream. |
ENOENT |
scan() local mode |
Install ClamAV and ensure clamscan is in PATH. |
Unexpected exit code: <n> |
scan() local mode |
Check ClamAV logs at /var/log/clamav/. |
Process killed by signal: <SIGNAL> |
scan() local mode |
Investigate system resources; retry the scan. |
ECONNREFUSED |
all clamd-mode functions | Wait for clamd to be healthy, then retry. |
clamd connection timed out after Nms |
all clamd-mode functions | Increase timeout, or check container CPU/memory. |
Install AWS SDK: npm install @aws-sdk/client-s3 |
scanS3() |
Run npm install @aws-sdk/client-s3. |
TypeScript
TypeScript declarations are bundled with pompelmi v1.11.0. No @types/pompelmi
package is required. Import as normal:
import { scan, scanBuffer, scanStream, scanDirectory,
middleware, scanS3, createPool, watch,
notify, createScanner,
Verdict } from 'pompelmi';
import type { ScanOptions, NotifyOptions, ScanEmitter } from 'pompelmi';
Full types/index.d.ts structure
export declare const Verdict: Readonly<{
Clean: unique symbol;
Malicious: unique symbol;
ScanError: unique symbol;
}>;
export type ScanVerdict = typeof Verdict[keyof typeof Verdict];
export interface ScanOptions {
socket?: string;
host?: string;
port?: number;
timeout?: number;
retries?: number;
retryDelay?: number;
}
export interface MiddlewareOptions extends ScanOptions {
uploadField?: string;
}
export interface PoolOptions extends ScanOptions {
size?: number;
}
export interface ScanDirectoryResult {
clean: string[];
malicious: string[];
errors: string[];
}
export interface ClamdPool {
scan(filePath: string): Promise<ScanVerdict>;
scanBuffer(buffer: Buffer): Promise<ScanVerdict>;
scanStream(stream: NodeJS.ReadableStream): Promise<ScanVerdict>;
destroy(): Promise<void>;
}
export function scan(filePath: string, options?: ScanOptions): Promise<ScanVerdict>;
export function scanBuffer(buffer: Buffer, options?: ScanOptions): Promise<ScanVerdict>;
export function scanStream(stream: NodeJS.ReadableStream, options?: ScanOptions): Promise<ScanVerdict>;
export function scanDirectory(dirPath: string, options?: ScanOptions): Promise<ScanDirectoryResult>;
export function middleware(options?: MiddlewareOptions): (req: any, res: any, next: any) => void;
export function scanS3(
params: { bucket: string; key: string; region?: string; credentials?: object },
options?: ScanOptions
): Promise<ScanVerdict>;
export function createPool(options?: PoolOptions): ClamdPool;
export function watch(
dirPath: string,
options: ScanOptions,
callbacks: {
onClean: (filePath: string) => void;
onMalicious: (filePath: string) => void;
onError: (err: Error, filePath?: string) => void;
}
): import('fs').FSWatcher;
@types
package or maintain a local .d.ts file. If you see TypeScript
errors after upgrading, ensure your tsconfig.json has
"moduleResolution": "bundler" or "node16".