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:

  1. If socket is set, pompelmi connects via UNIX domain socket.
  2. Otherwise, if host is set (or port alone), pompelmi connects via TCP.
  3. If neither is set, pompelmi spawns a local clamscan process.

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');
}
See S3 Integration for a full Lambda trigger example and IAM policy setup.

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 });
  }
);
On detection, the middleware sends 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');
}
See S3 Integration for IAM policy setup, Lambda trigger examples, and the pre-upload scan pattern.

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 are now bundled — no need to install a separate @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".