Cloudflare Workers

@pompelmi/cloudflare is the official pompelmi adapter for Cloudflare Workers. It scans file uploads for malware by streaming them to a remote ClamAV (clamd) instance using the INSTREAM protocol.

The package uses Web APIs onlyfetch, FormData, ArrayBuffer, and Cloudflare's connect() socket API from cloudflare:sockets. No Node.js built-ins, no native bindings.

Requirements

Cloudflare Workers cannot run clamd locally. You need a publicly reachable clamd instance. Options:

Option Description
VPS + open port Run clamd on a virtual machine and expose port 3310. Add firewall rules to restrict access to Cloudflare's egress IPs.
Cloudflare Tunnel Use cloudflared to expose a private clamd instance without opening a port. Most secure option.
Security note: Never expose clamd directly to the public internet without access controls. Restrict clamd access using firewall rules or Cloudflare Tunnel.

Installation

npm i @pompelmi/cloudflare

Basic Usage

Scan an ArrayBuffer directly from a multipart form upload:

import { scanBuffer } from '@pompelmi/cloudflare';

export default {
  async fetch(request, env) {
    const formData = await request.formData();
    const file = formData.get('file');
    const buffer = await file.arrayBuffer();

    const result = await scanBuffer(buffer, {
      host: env.CLAMAV_HOST,
      port: parseInt(env.CLAMAV_PORT),
    });

    if (result !== 'clean') {
      return new Response('File rejected', { status: 422 });
    }

    return new Response('OK');
  },
};

scanRequest helper

scanRequest(request, options) handles the full form-read → scan → response cycle. It returns null for clean files, a Response(422) for malicious files, and a Response(500) on scan errors.

import { scanRequest } from '@pompelmi/cloudflare';

export default {
  async fetch(request, env) {
    const rejection = await scanRequest(request, {
      host: env.CLAMAV_HOST,
      port: parseInt(env.CLAMAV_PORT),
    });
    if (rejection) return rejection;

    // File is clean — proceed with your upload logic
    return new Response('File accepted');
  },
};

To scan a specific form field other than file:

const rejection = await scanRequest(request, {
  host: env.CLAMAV_HOST,
  port: parseInt(env.CLAMAV_PORT),
  field: 'attachment',
});

Wrangler Configuration

Copy wrangler.toml.example from the package and fill in your clamd details:

name = "my-worker"
main = "src/worker.js"
compatibility_date = "2024-01-01"

[vars]
CLAMAV_HOST = "your-clamd-host.example.com"
CLAMAV_PORT = "3310"

Use Wrangler secrets for production to avoid committing credentials:

wrangler secret put CLAMAV_HOST
wrangler secret put CLAMAV_PORT

API Reference

scanBuffer(buffer, options)

ParameterTypeDescription
bufferArrayBufferThe file bytes to scan
options.hoststringclamd hostname or IP (required)
options.portnumberclamd port, typically 3310 (required)
options.timeoutnumberRead timeout in ms (default: 15000)

Returns Promise<'clean' | 'malicious' | 'error'>.

scanRequest(request, options)

ParameterTypeDescription
requestRequestCloudflare Workers Request object
options.hoststringclamd hostname or IP (required)
options.portnumberclamd port (required)
options.fieldstringForm field name (default: 'file')
options.timeoutnumberRead timeout in ms (default: 15000)

Returns Promise<Response | null>null if clean, HTTP Response otherwise.

Remote clamd Setup

Start a clamd instance accessible over TCP using Docker:

docker run -d \
  --name clamd \
  -p 3310:3310 \
  clamav/clamav:latest

For a production deployment, use the Docker guide and restrict clamd to your Cloudflare Worker's outbound IP range.

For a zero-port-forwarding setup, use Cloudflare Tunnel to securely expose a private clamd instance.