pompelmi 1.2.1 → 1.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,8 +6,8 @@
6
6
  "Bash(echo \"EXIT_CODE:$?\")",
7
7
  "Bash(tee /tmp/pompelmi_test_out.txt)",
8
8
  "Bash(echo \"done: $?\")",
9
- "Bash(ls /Users/tommy/SonoTommy/pompelmi/*.md)",
10
- "Bash(ls /Users/tommy/SonoTommy/pompelmi/LICENSE*)",
9
+ "Bash(ls /Users/tommy/pompelmi/pompelmi/*.md)",
10
+ "Bash(ls /Users/tommy/pompelmi/pompelmi/LICENSE*)",
11
11
  "WebSearch",
12
12
  "WebFetch(domain:pompelmi.app)",
13
13
  "WebFetch(domain:news.ycombinator.com)",
@@ -23,7 +23,28 @@
23
23
  "WebFetch(domain:cdn.brandfetch.io)",
24
24
  "WebFetch(domain:cooperpress.com)",
25
25
  "WebFetch(domain:wirexsystems.com)",
26
- "WebFetch(domain:www.zlti.com)"
26
+ "WebFetch(domain:www.zlti.com)",
27
+ "Bash(gh run:*)",
28
+ "Bash(gh api:*)",
29
+ "WebFetch(domain:api.github.com)",
30
+ "WebFetch(domain:raw.githubusercontent.com)",
31
+ "Bash(git -C /Users/tommy/pompelmi/pompelmi status)",
32
+ "Bash(git -C /Users/tommy/pompelmi/pompelmi log --oneline -5)",
33
+ "Bash(git pull:*)",
34
+ "Bash(git add:*)",
35
+ "Bash(git commit -m ':*)",
36
+ "Bash(git push:*)",
37
+ "Bash(grep -E '\\\\.\\(png|svg|ico|webp\\)$')",
38
+ "Bash(ls -d /Users/tommy/pompelmi/pompelmi/*/)",
39
+ "Bash(node --test test/unit.test.js)",
40
+ "Bash(echo \"exit:$?\")",
41
+ "Read(//tmp/**)",
42
+ "Bash(tee /Users/tommy/pompelmi/pompelmi/test_out.txt)",
43
+ "Bash(npm install:*)",
44
+ "Bash(npm ls:*)",
45
+ "Bash(wait)",
46
+ "Bash(git rebase *)",
47
+ "Bash(python3 *)"
27
48
  ]
28
49
  }
29
50
  }
package/README.md CHANGED
@@ -1,103 +1,174 @@
1
1
  <p align="center">
2
- <img src="./src/grapefruit.png" width="96" alt="pompelmi logo">
2
+ <img src="./src/grapefruit.png" width="88" alt="pompelmi logo">
3
3
  </p>
4
4
 
5
5
  <h1 align="center">pompelmi</h1>
6
6
 
7
- <p align="center"><strong>ClamAV for humans</strong></p>
7
+ <p align="center"><strong>ClamAV antivirus scanning for Node.js — clean, typed, zero dependencies.</strong></p>
8
8
 
9
9
  <p align="center">
10
10
  <a href="https://www.npmjs.com/package/pompelmi"><img src="https://img.shields.io/npm/v/pompelmi.svg" alt="npm version"></a>
11
+ <a href="https://www.npmjs.com/package/pompelmi"><img src="https://img.shields.io/npm/dw/pompelmi" alt="npm weekly downloads"></a>
12
+ <a href="https://github.com/pompelmi/pompelmi"><img src="https://img.shields.io/github/stars/pompelmi/pompelmi?style=social" alt="GitHub stars"></a>
13
+ <img src="https://img.shields.io/badge/docker-available-blue?logo=docker" alt="Docker available">
11
14
  <img src="https://img.shields.io/badge/license-ISC-blue.svg" alt="license">
12
- <img src="https://img.shields.io/badge/platform-macOS%20%7C%20Linux%20%7C%20Windows-lightgrey.svg" alt="platform">
13
- <a href="https://www.npmjs.com/package/pompelmi?activeTab=dependencies"><img src="https://img.shields.io/badge/dependencies-0-brightgreen" alt="zero dependencies"></a>
15
+ <img src="https://img.shields.io/badge/dependencies-0-brightgreen" alt="zero dependencies">
14
16
  </p>
15
17
 
16
18
  ---
17
19
 
18
- A minimal Node.js wrapper around [ClamAV](https://www.clamav.net/) that scans any file and returns a typed `Verdict` Symbol: `Verdict.Clean`, `Verdict.Malicious`, or `Verdict.ScanError`. No daemons. No cloud. No native bindings. Zero runtime dependencies.
19
-
20
- ## Table of contents
21
-
22
- - [Quickstart](#quickstart)
23
- - [How it works](#how-it-works)
24
- - [API](#api)
25
- - [pompelmi.scan()](#pompelmiscanfilepath-options)
26
- - [Docker / remote scanning](#docker--remote-scanning)
27
- - [Examples](#examples)
28
- - [Internal utilities](#internal-utilities)
29
- - [ClamAVInstaller()](#clamavinstaller)
30
- - [updateClamAVDatabase()](#updateclamavdatabase)
31
- - [Supported platforms](#supported-platforms)
32
- - [Installing ClamAV manually](#installing-clamav-manually)
33
- - [Testing](#testing)
34
- - [Contributing](#contributing)
35
- - [Security](#security)
36
- - [License](#license)
20
+ ## Overview
21
+
22
+ pompelmi is a minimal Node.js wrapper around [ClamAV](https://www.clamav.net/) that exposes a single async function — `scan()` — and returns one of three typed verdict Symbols: `Verdict.Clean`, `Verdict.Malicious`, or `Verdict.ScanError`.
23
+
24
+ It supports two scanning modes:
25
+
26
+ - **Local** — spawns `clamscan` as a child process and maps its exit code to a verdict. No stdout parsing, no regex.
27
+ - **Remote / Docker** — streams the file to a running `clamd` daemon over TCP using the ClamAV `INSTREAM` protocol.
28
+
29
+ No cloud. No daemon required for local mode. No native bindings. Zero runtime dependencies.
30
+
31
+ ---
32
+
33
+ ## Features
34
+
35
+ - Single `scan(filePath, [options])` function — works locally or against a remote clamd instance
36
+ - Symbol-based verdicts (`Verdict.Clean` / `Verdict.Malicious` / `Verdict.ScanError`) — typo-proof comparisons
37
+ - Full TCP/clamd support via the INSTREAM protocol with configurable host, port, and timeout
38
+ - Built-in helpers to install ClamAV and update virus definitions programmatically
39
+ - Works with Express, Fastify, and any other Node.js HTTP framework
40
+ - Zero runtime dependencies — ships nothing but source code
41
+ - Tested with EICAR standard antivirus test files
42
+ - CommonJS module; TypeScript type declarations available inline
43
+
44
+ ---
45
+
46
+ ## Requirements
47
+
48
+ - **Node.js** — any LTS release (no native addons, no C++ bindings)
49
+ - **ClamAV** — must be installed on the host or reachable over TCP
50
+
51
+ pompelmi does not bundle or automatically download ClamAV. Install it once per machine (see [Installing ClamAV](#installing-clamav)).
37
52
 
38
53
  ---
39
54
 
40
- ## Quickstart
55
+ ## Installation
41
56
 
42
57
  ```bash
58
+ # npm
43
59
  npm install pompelmi
60
+
61
+ # yarn
62
+ yarn add pompelmi
63
+
64
+ # pnpm
65
+ pnpm add pompelmi
66
+ ```
67
+
68
+ ### Docker
69
+
70
+ Run ClamAV as a sidecar and point pompelmi at it — no local install needed on the application host.
71
+
72
+ ```yaml
73
+ # docker-compose.yml
74
+ services:
75
+ clamav:
76
+ image: clamav/clamav:stable
77
+ ports:
78
+ - "3310:3310"
79
+ ```
80
+
81
+ ```js
82
+ const result = await scan('/path/to/upload.zip', {
83
+ host: '127.0.0.1',
84
+ port: 3310,
85
+ });
44
86
  ```
45
87
 
88
+ See [Docker / remote scanning](#docker--remote-scanning) for details.
89
+
90
+ ---
91
+
92
+ ## Usage
93
+
94
+ ### Basic scan
95
+
46
96
  ```js
47
97
  const { scan, Verdict } = require('pompelmi');
48
98
 
49
- const result = await scan('/path/to/file.zip');
99
+ const result = await scan('/path/to/file.pdf');
50
100
 
51
- if (result === Verdict.Malicious) {
52
- throw new Error('File rejected: malware detected');
53
- }
101
+ if (result === Verdict.Clean) console.log('File is safe.');
102
+ if (result === Verdict.Malicious) throw new Error('Malware detected file rejected.');
103
+ if (result === Verdict.ScanError) console.warn('Scan incomplete — treat file as untrusted.');
54
104
  ```
55
105
 
56
- ## How it works
106
+ ### Express file upload
57
107
 
58
- 1. **Validate** — pompelmi checks that the argument is a string and that the file exists before spawning anything.
59
- 2. **Scan** pompelmi spawns `clamscan --no-summary <filePath>` as a child process and reads the exit code.
60
- 3. **Map** — the exit code is mapped to a result string. Unknown codes and spawn errors reject the Promise.
108
+ ```js
109
+ const express = require('express');
110
+ const multer = require('multer');
111
+ const fs = require('fs');
112
+ const { scan, Verdict } = require('pompelmi');
61
113
 
62
- No stdout parsing. No regex. No surprises.
114
+ const upload = multer({ dest: './uploads' });
115
+ const app = express();
63
116
 
64
- ## API
117
+ app.post('/upload', upload.single('file'), async (req, res) => {
118
+ const filePath = req.file.path;
65
119
 
66
- ### `pompelmi.scan(filePath, [options])`
120
+ try {
121
+ const result = await scan(filePath);
67
122
 
68
- ```ts
69
- scan(filePath: string, options?: { host?: string; port?: number; timeout?: number }): Promise<symbol>
70
- // resolves to one of: Verdict.Clean | Verdict.Malicious | Verdict.ScanError
123
+ if (result === Verdict.Malicious) {
124
+ fs.unlinkSync(filePath);
125
+ return res.status(422).json({ error: 'Malicious file rejected.' });
126
+ }
127
+ if (result === Verdict.ScanError) {
128
+ fs.unlinkSync(filePath);
129
+ return res.status(422).json({ error: 'Scan incomplete — file rejected as precaution.' });
130
+ }
131
+
132
+ return res.json({ ok: true, file: req.file.filename });
133
+ } catch (err) {
134
+ fs.unlink(filePath, () => {});
135
+ return res.status(500).json({ error: `Scan failed: ${err.message}` });
136
+ }
137
+ });
138
+
139
+ app.listen(3000);
71
140
  ```
72
141
 
73
- | Parameter | Type | Description |
74
- |------------|----------|-----------------------------------------|
75
- | `filePath` | `string` | Absolute or relative path to the file. |
76
- | `options` | `object` | Optional. Omit to use the local `clamscan` CLI. Pass `host` / `port` to scan via a clamd TCP socket instead. See [docs/api.html](./docs/api.html) for the full reference. |
142
+ ### Fastify file upload
77
143
 
78
- **Resolves** to one of:
144
+ ```js
145
+ const Fastify = require('fastify');
146
+ const { pipeline } = require('stream/promises');
147
+ const fs = require('fs');
148
+ const path = require('path');
149
+ const { scan, Verdict } = require('pompelmi');
79
150
 
80
- | Result | ClamAV exit code | Meaning |
81
- |--------------------|:---:|------------------------------------------------------------------------------------------------------|
82
- | `Verdict.Clean` | 0 | No threats found. |
83
- | `Verdict.Malicious` | 1 | A known virus or malware signature was matched. |
84
- | `Verdict.ScanError` | 2 | The scan itself failed (I/O error, encrypted archive, permission denied). File status is unknown — treat as untrusted. |
151
+ const app = Fastify({ logger: true });
152
+ app.register(require('@fastify/multipart'));
85
153
 
86
- > **Reading the verdict as a string** — each Verdict Symbol carries a `.description` property
87
- > (`Verdict.Clean.description === 'Clean'`) for logging or serialisation without comparing against
88
- > raw strings in application logic.
154
+ app.post('/upload', async (req, reply) => {
155
+ const data = await req.file();
156
+ const filePath = path.join('./uploads', `${Date.now()}-${data.filename}`);
89
157
 
90
- **Rejects** with an `Error` in these cases:
158
+ await pipeline(data.file, fs.createWriteStream(filePath));
91
159
 
92
- | Condition | Error message |
93
- |-----------|---------------|
94
- | `filePath` is not a string | `filePath must be a string` |
95
- | File does not exist | `File not found: <path>` |
96
- | `clamscan` is not in PATH | `ENOENT` (from the OS) |
97
- | ClamAV returns an unknown exit code | `Unexpected exit code: N` |
98
- | `clamscan` process is killed by a signal | `Process killed by signal: <SIGNAL>` |
160
+ const result = await scan(filePath);
99
161
 
100
- **Example full error handling:**
162
+ if (result !== Verdict.Clean) {
163
+ fs.unlinkSync(filePath);
164
+ return reply.code(422).send({ error: result.description });
165
+ }
166
+
167
+ return reply.send({ ok: true });
168
+ });
169
+ ```
170
+
171
+ ### Full error handling
101
172
 
102
173
  ```js
103
174
  const { scan, Verdict } = require('pompelmi');
@@ -108,110 +179,134 @@ async function safeScan(filePath) {
108
179
  const result = await scan(path.resolve(filePath));
109
180
 
110
181
  if (result === Verdict.ScanError) {
111
- // The scan could not completetreat the file as untrusted.
112
- console.warn('Scan incomplete, rejecting file as precaution.');
182
+ // clamscan exited with code 2I/O error, encrypted archive, etc.
183
+ console.warn('Scan could not complete — rejecting file as precaution.');
113
184
  return null;
114
185
  }
115
186
 
116
187
  return result; // Verdict.Clean or Verdict.Malicious
117
188
  } catch (err) {
189
+ // filePath not a string, file not found, clamscan not in PATH, etc.
118
190
  console.error('Scan failed:', err.message);
119
191
  return null;
120
192
  }
121
193
  }
122
194
  ```
123
195
 
124
- ## Docker / remote scanning
196
+ ### Scan multiple files concurrently
197
+
198
+ ```js
199
+ const { scan } = require('pompelmi');
200
+ const files = ['/uploads/a.pdf', '/uploads/b.zip', '/uploads/c.png'];
201
+
202
+ const results = await Promise.all(files.map((f) => scan(f)));
203
+ ```
204
+
205
+ ---
206
+
207
+ ## Docker / Remote Scanning
125
208
 
126
- If ClamAV runs in a Docker container (or anywhere on the network), pass `host` and `port`everything else stays the same.
209
+ Pass `host` and `port` to switch from the local `clamscan` CLI to the clamd TCP daemon. Everything elsethe returned verdicts, error types — is identical.
127
210
 
128
211
  ```js
129
- const result = await pompelmi.scan('/path/to/upload.zip', {
130
- host: '127.0.0.1',
131
- port: 3310,
212
+ const result = await scan('/path/to/file.zip', {
213
+ host: '127.0.0.1',
214
+ port: 3310,
215
+ timeout: 30_000, // socket idle timeout, ms — default 15 000
132
216
  });
133
217
  ```
134
218
 
135
- See [docs/docker.md](./docs/docker.md) for the `docker-compose.yml` snippet and first-boot notes.
219
+ pompelmi uses the ClamAV `INSTREAM` protocol: the file is streamed in 64 KB chunks, each prefixed with a 4-byte big-endian length header, terminated by four zero bytes. The response line (`stream: OK`, `stream: <name> FOUND`, or an error) is mapped to the same verdict Symbols.
136
220
 
137
221
  ---
138
222
 
139
- ## Examples
223
+ ## Configuration
140
224
 
141
- The [`examples/`](./examples/) directory contains standalone, runnable scripts for common use cases. Each file can be run directly with `node examples/<name>.js`.
142
-
143
- - [`basic-scan.js`](examples/basic-scan.js) — Scan a single file and log the Verdict
144
- - [`scan-on-upload-express.js`](examples/scan-on-upload-express.js) — Express route: receive upload, scan before saving
145
- - [`scan-on-upload-fastify.js`](examples/scan-on-upload-fastify.js) — Fastify route: same pattern
146
- - [`scan-with-options.js`](examples/scan-with-options.js) — Scan via a remote clamd instance with custom host, port, and timeout
147
- - [`handle-scan-error.js`](examples/handle-scan-error.js) — Gracefully handle every Verdict including ScanError and hard rejections
148
- - [`delete-on-malicious.js`](examples/delete-on-malicious.js) — Auto-delete file if Verdict.Malicious
149
- - [`quarantine-on-malicious.js`](examples/quarantine-on-malicious.js) — Move infected file to a `quarantine/` folder
150
- - [`scan-multiple-files.js`](examples/scan-multiple-files.js) — Scan an array of files concurrently with `Promise.all`
151
- - [`scan-directory.js`](examples/scan-directory.js) — Walk a directory recursively and scan every file
152
- - [`scan-buffer.js`](examples/scan-buffer.js) — Scan an in-memory Buffer via a temp-file shim
153
- - [`install-clamav.js`](examples/install-clamav.js) — Programmatically trigger ClamAV installation
154
- - [`update-virus-database.js`](examples/update-virus-database.js) — Programmatically run freshclam / DB update
155
- - [`scan-with-timeout.js`](examples/scan-with-timeout.js) — Timeout patterns for both local clamscan and remote clamd
156
- - [`scan-pdf.js`](examples/scan-pdf.js) — Scan a PDF upload with extension validation
157
- - [`scan-image.js`](examples/scan-image.js) — Scan an image upload with extension validation
158
- - [`scan-zip.js`](examples/scan-zip.js) — Scan a ZIP archive upload (ClamAV recurses automatically)
159
- - [`rest-api-server.js`](examples/rest-api-server.js) — Minimal HTTP server exposing `POST /scan`
160
- - [`s3-scan-before-upload.js`](examples/s3-scan-before-upload.js) — Scan locally, then upload to AWS S3 only if clean
161
- - [`cli-scan.js`](examples/cli-scan.js) — Accept file paths as CLI arguments, print verdicts, exit non-zero on threats
162
- - [`typescript-usage.ts`](examples/typescript-usage.ts) — TypeScript example with inline type declarations
225
+ pompelmi has no configuration file or environment variables. All options are passed directly to `scan()`.
163
226
 
164
- ---
227
+ | Option | Type | Default | Description |
228
+ |-----------|----------|-----------------|----------------------------------------|
229
+ | `host` | `string` | — | clamd hostname. Enables TCP mode when set. |
230
+ | `port` | `number` | `3310` | clamd port. |
231
+ | `timeout` | `number` | `15000` | Socket idle timeout in milliseconds (TCP mode only). |
165
232
 
166
- ## Internal utilities
233
+ When neither `host` nor `port` is provided, pompelmi spawns `clamscan --no-summary <filePath>` locally.
167
234
 
168
- These modules are not part of the public npm API but are used internally to set up the ClamAV environment on a fresh machine.
235
+ ---
169
236
 
170
- ### `ClamAVInstaller()`
237
+ ## API Reference
171
238
 
172
- Installs ClamAV using the platform's native package manager. Skips silently if ClamAV is already installed.
239
+ ### `scan(filePath, [options])`
173
240
 
174
241
  ```ts
175
- ClamAVInstaller(): Promise<string>
242
+ scan(
243
+ filePath: string,
244
+ options?: { host?: string; port?: number; timeout?: number }
245
+ ): Promise<symbol>
176
246
  ```
177
247
 
178
- - Resolves with a status message string on success or skip.
179
- - Rejects if the install process exits with a non-zero code or if spawning the package manager fails.
248
+ **Returns** a Promise that resolves to one of:
249
+
250
+ | Verdict | ClamAV exit code / response | Meaning |
251
+ |---------------------|-----------------------------|-------------------------------------------------------------------------|
252
+ | `Verdict.Clean` | `0` / `stream: OK` | No threats found. |
253
+ | `Verdict.Malicious` | `1` / `<name> FOUND` | A known virus or malware signature was matched. |
254
+ | `Verdict.ScanError` | `2` / other response | Scan failed — I/O error, encrypted archive, permission denied. Treat file as untrusted. |
255
+
256
+ **Rejects** with an `Error` in these cases:
257
+
258
+ | Condition | Error message |
259
+ |---------------------------------------|-----------------------------------|
260
+ | `filePath` is not a string | `filePath must be a string` |
261
+ | File does not exist | `File not found: <path>` |
262
+ | `clamscan` not in PATH | `ENOENT` (from the OS) |
263
+ | ClamAV returns an unknown exit code | `Unexpected exit code: N` |
264
+ | Process killed by signal | `Process killed by signal: <SIG>` |
265
+ | clamd connection timed out | `clamd connection timed out after Nms` |
266
+
267
+ Each `Verdict` Symbol exposes a `.description` property for safe serialisation:
180
268
 
181
- | Platform | Package manager | Command |
182
- |----------|----------------|---------|
183
- | macOS | Homebrew | `brew install clamav` |
184
- | Linux | apt-get | `sudo apt-get install -y clamav clamav-daemon` |
185
- | Windows | Chocolatey | `choco install clamav -y` |
269
+ ```js
270
+ Verdict.Clean.description // 'Clean'
271
+ Verdict.Malicious.description // 'Malicious'
272
+ Verdict.ScanError.description // 'ScanError'
273
+ ```
186
274
 
187
- ### `updateClamAVDatabase()`
275
+ ---
188
276
 
189
- Downloads or updates the ClamAV virus definition database by running `freshclam`. Skips if `main.cvd` is already present on disk.
277
+ ### `ClamAVInstaller()` _(internal)_
278
+
279
+ Installs ClamAV using the platform's native package manager. Resolves immediately if ClamAV is already installed.
190
280
 
191
281
  ```ts
192
- updateClamAVDatabase(): Promise<string>
282
+ ClamAVInstaller(): Promise<string>
193
283
  ```
194
284
 
195
- - Resolves with a status message string on success or skip.
196
- - Rejects if `freshclam` exits with a non-zero code or if spawning fails.
285
+ | Platform | Package manager | Command |
286
+ |----------|-----------------|-------------------------------------------|
287
+ | macOS | Homebrew | `brew install clamav` |
288
+ | Linux | apt-get | `sudo apt-get install -y clamav clamav-daemon` |
289
+ | Windows | Chocolatey | `choco install clamav -y` |
290
+
291
+ ---
292
+
293
+ ### `updateClamAVDatabase()` _(internal)_
197
294
 
198
- | Platform | Database path |
199
- |----------|--------------|
200
- | macOS | `/usr/local/share/clamav/main.cvd` |
201
- | Linux | `/var/lib/clamav/main.cvd` |
202
- | Windows | `C:\ProgramData\ClamAV\main.cvd` |
295
+ Runs `freshclam` to download or refresh the virus definition database. Skips if the database file is already present.
203
296
 
204
- ## Supported platforms
297
+ ```ts
298
+ updateClamAVDatabase(): Promise<string>
299
+ ```
205
300
 
206
- | OS | ClamAV install | DB path checked |
207
- |---------|---------------------------|------------------------------------|
208
- | macOS | `brew install clamav` | `/usr/local/share/clamav/main.cvd` |
209
- | Linux | `apt-get install clamav` | `/var/lib/clamav/main.cvd` |
210
- | Windows | `choco install clamav -y` | `C:\ProgramData\ClamAV\main.cvd` |
301
+ | Platform | Database path |
302
+ |----------|---------------------------------------|
303
+ | macOS | `/usr/local/share/clamav/main.cvd` |
304
+ | Linux | `/var/lib/clamav/main.cvd` |
305
+ | Windows | `C:\ProgramData\ClamAV\main.cvd` |
211
306
 
212
- ClamAV must be installed on the host system. pompelmi does not bundle or download it.
307
+ ---
213
308
 
214
- ## Installing ClamAV manually
309
+ ## Installing ClamAV
215
310
 
216
311
  ```bash
217
312
  # macOS
@@ -224,29 +319,67 @@ sudo apt-get install -y clamav clamav-daemon && sudo freshclam
224
319
  choco install clamav -y
225
320
  ```
226
321
 
227
- ## Testing
322
+ ---
323
+
324
+ ## Examples
325
+
326
+ The [`examples/`](./examples/) directory contains standalone runnable scripts. Each can be run directly with `node examples/<name>.js`.
327
+
328
+ | File | Description |
329
+ |------|-------------|
330
+ | [`basic-scan.js`](examples/basic-scan.js) | Scan a single file and log the verdict |
331
+ | [`scan-on-upload-express.js`](examples/scan-on-upload-express.js) | Express route: scan before saving |
332
+ | [`scan-on-upload-fastify.js`](examples/scan-on-upload-fastify.js) | Fastify route: same pattern |
333
+ | [`scan-with-options.js`](examples/scan-with-options.js) | Remote clamd with custom host, port, timeout |
334
+ | [`handle-scan-error.js`](examples/handle-scan-error.js) | Handle every verdict including hard rejections |
335
+ | [`delete-on-malicious.js`](examples/delete-on-malicious.js) | Auto-delete file if malicious |
336
+ | [`quarantine-on-malicious.js`](examples/quarantine-on-malicious.js) | Move infected file to a quarantine folder |
337
+ | [`scan-multiple-files.js`](examples/scan-multiple-files.js) | Concurrent scans with `Promise.all` |
338
+ | [`scan-directory.js`](examples/scan-directory.js) | Recursively scan every file in a directory |
339
+ | [`scan-buffer.js`](examples/scan-buffer.js) | Scan an in-memory Buffer via a temp-file shim |
340
+ | [`rest-api-server.js`](examples/rest-api-server.js) | Minimal HTTP server exposing `POST /scan` |
341
+ | [`s3-scan-before-upload.js`](examples/s3-scan-before-upload.js) | Scan locally, then upload to S3 only if clean |
342
+ | [`cli-scan.js`](examples/cli-scan.js) | CLI tool: scan file paths, exit non-zero on threats |
343
+ | [`scan-with-timeout.js`](examples/scan-with-timeout.js) | Timeout patterns for local and remote scanning |
344
+ | [`scan-pdf.js`](examples/scan-pdf.js) | PDF upload with extension validation |
345
+ | [`scan-image.js`](examples/scan-image.js) | Image upload with extension validation |
346
+ | [`scan-zip.js`](examples/scan-zip.js) | ZIP archive scan (ClamAV recurses automatically) |
347
+ | [`install-clamav.js`](examples/install-clamav.js) | Programmatic ClamAV installation |
348
+ | [`update-virus-database.js`](examples/update-virus-database.js) | Programmatic virus DB update |
349
+ | [`typescript-usage.ts`](examples/typescript-usage.ts) | TypeScript example with inline type declarations |
350
+
351
+ ---
352
+
353
+ ## Contributing
228
354
 
229
355
  ```bash
356
+ # 1. Clone and install dev dependencies
357
+ git clone https://github.com/pompelmi/pompelmi.git
358
+ cd pompelmi
359
+ npm install
360
+
361
+ # 2. Run the test suite
230
362
  npm test
363
+
364
+ # 3. Lint
365
+ npm run lint
231
366
  ```
232
367
 
233
- The test suite has two parts:
368
+ **Tests**
234
369
 
235
- - **Unit tests** (`test/unit.test.js`)run with Node's built-in test runner. Mock `nativeSpawn` from `src/spawn.js` and platform dependencies via require-cache injection; no ClamAV installation required.
236
- - **Integration tests** (`test/scan.test.js`) — spawn real `clamscan` processes against EICAR test files. Skipped automatically if `clamscan` is not found in PATH.
370
+ - `test/unit.test.js` — runs with Node's built-in test runner. Mocks `nativeSpawn` and platform dependencies; ClamAV is not required.
371
+ - `test/scan.test.js` — integration tests that spawn real `clamscan` against EICAR test files. Skipped automatically when `clamscan` is not in `PATH`.
237
372
 
238
- ## Contributing
373
+ **Submitting changes**
239
374
 
240
- 1. Fork the repository at [https://github.com/pompelmi/pompelmi](https://github.com/pompelmi/pompelmi).
375
+ 1. Fork the repository.
241
376
  2. Create a feature branch: `git checkout -b feat/your-change`.
242
- 3. Make your changes and run `npm test` to verify.
377
+ 3. Make your changes and confirm `npm test` passes.
243
378
  4. Open a pull request against `main`.
244
379
 
245
- Please read [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md) before contributing.
380
+ Please read [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md) before contributing. To report a security vulnerability, see [SECURITY.md](./SECURITY.md).
246
381
 
247
- ## Security
248
-
249
- To report a vulnerability, see [SECURITY.md](./SECURITY.md).
382
+ ---
250
383
 
251
384
  ## License
252
385
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pompelmi",
3
- "version": "1.2.1",
3
+ "version": "1.2.3",
4
4
  "description": "ClamAV for humans — scan any file and get back Clean, Malicious, or ScanError. No daemons. No cloud. No native bindings.",
5
5
  "license": "ISC",
6
6
  "author": "pompelmi contributors",