pompelmi 1.14.0 → 1.16.0

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.
package/README.md CHANGED
@@ -89,11 +89,15 @@ Most integrations require parsing ClamAV's stdout with regex, managing a clamd d
89
89
  - Symbol-based verdicts (`Verdict.Clean` / `Verdict.Malicious` / `Verdict.ScanError`) — typo-proof comparisons
90
90
  - Full clamd support via the INSTREAM protocol — TCP (`host`/`port`) or UNIX socket (`socket`) with configurable timeout
91
91
  - Built-in helpers to install ClamAV and update virus definitions programmatically
92
- - Works with Express, Fastify, NestJS, and any other Node.js HTTP framework
92
+ - Works with Express, Fastify, NestJS, Hono, Remix, SvelteKit, and any other Node.js HTTP framework
93
+ - Works with Node.js and Bun — uses `Bun.file()` for faster file reading when available
94
+ - Interactive demo at [pompelmi.app/demo](https://pompelmi.app/demo.html) — try before you install
93
95
  - Zero runtime dependencies — ships nothing but source code
94
96
  - Tested with EICAR standard antivirus test files
95
97
  - CommonJS module; TypeScript type declarations available inline
96
98
 
99
+ See [how pompelmi compares](./docs/comparison.html) to other Node.js ClamAV integrations.
100
+
97
101
  ---
98
102
 
99
103
  ## Framework Integrations
@@ -104,6 +108,10 @@ Official integration packages for popular frameworks:
104
108
  |---------|-----------|---------|
105
109
  | [@pompelmi/nestjs](./packages/nestjs/) | NestJS | `npm i @pompelmi/nestjs` |
106
110
  | [@pompelmi/fastify](./packages/fastify/) | Fastify | `npm i @pompelmi/fastify` |
111
+ | [@pompelmi/hono](./packages/hono/) | Hono | `npm i @pompelmi/hono` |
112
+ | [@pompelmi/remix](./packages/remix/) | Remix | `npm i @pompelmi/remix` |
113
+ | [@pompelmi/sveltekit](./packages/sveltekit/) | SvelteKit | `npm i @pompelmi/sveltekit` |
114
+ | [@pompelmi/testing](./packages/testing/) | Jest/Vitest/Node | `npm i -D @pompelmi/testing` |
107
115
 
108
116
  ### NestJS
109
117
 
@@ -132,11 +140,63 @@ const result = await fastify.pompelmi.scanBuffer(buffer);
132
140
  fastify.post('/upload', { preHandler: fastify.pompelmi.preHandler({ field: 'file' }) }, handler);
133
141
  ```
134
142
 
143
+ ### Hono (Node.js, Bun, Cloudflare Workers)
144
+
145
+ ```js
146
+ import { Hono } from 'hono'
147
+ import { pompelmiMiddleware } from '@pompelmi/hono'
148
+
149
+ const app = new Hono()
150
+
151
+ app.use('/upload/*', pompelmiMiddleware({
152
+ host: 'localhost',
153
+ port: 3310,
154
+ onInfected: (c, filename) => c.json({ error: 'Malware detected' }, 422),
155
+ }))
156
+
157
+ app.post('/upload', async (c) => c.json({ ok: true }))
158
+ ```
159
+
160
+ ### Remix
161
+
162
+ ```ts
163
+ import { unstable_parseMultipartFormData, json } from '@remix-run/node'
164
+ import { pompelmiUploadHandler } from '@pompelmi/remix'
165
+
166
+ export async function action({ request }) {
167
+ // Throws HTTP 422 automatically if malware is detected
168
+ const formData = await unstable_parseMultipartFormData(
169
+ request,
170
+ pompelmiUploadHandler({ host: 'localhost', port: 3310 })
171
+ )
172
+ const file = formData.get('file')
173
+ return json({ name: file.name, size: file.size, ok: true })
174
+ }
175
+ ```
176
+
177
+ ### SvelteKit
178
+
179
+ ```ts
180
+ // +page.server.ts
181
+ import { scanUpload } from '@pompelmi/sveltekit'
182
+ import type { Actions } from './$types'
183
+
184
+ export const actions: Actions = {
185
+ default: async ({ request }) => {
186
+ const formData = await request.formData()
187
+ // Throws HTTP 422 automatically if malware is detected
188
+ await scanUpload(formData.get('file') as File, { host: 'localhost', port: 3310 })
189
+ return { success: true }
190
+ }
191
+ }
192
+ ```
193
+
135
194
  ---
136
195
 
137
196
  ## Requirements
138
197
 
139
198
  - **Node.js** — any LTS release (no native addons, no C++ bindings)
199
+ - **Bun** — fully supported; uses `Bun.file()` for faster file reading
140
200
  - **ClamAV** — must be installed on the host or reachable over TCP
141
201
 
142
202
  pompelmi does not bundle or automatically download ClamAV. Install it once per machine (see [Installing ClamAV](#installing-clamav)).
@@ -156,6 +216,9 @@ yarn add pompelmi
156
216
 
157
217
  # pnpm
158
218
  pnpm add pompelmi
219
+
220
+ # bun
221
+ bun add pompelmi
159
222
  ```
160
223
 
161
224
  ### Docker
package/llms.txt CHANGED
@@ -20,6 +20,18 @@ Options: `{ host?: string, port?: number, timeout?: number }`
20
20
  - Local mode: spawns `clamscan`, maps exit codes to verdicts
21
21
  - TCP mode: streams to clamd via INSTREAM protocol (set `host`)
22
22
 
23
+ ## Framework integrations
24
+
25
+ Official packages for popular frameworks:
26
+
27
+ - `@pompelmi/nestjs` — NestJS module (`PompelmiModule`, `PompelmiService`, `PompelmiInterceptor`)
28
+ - `@pompelmi/fastify` — Fastify plugin (decorates `fastify.pompelmi` with scan/preHandler)
29
+ - `@pompelmi/hono` — Hono middleware (`pompelmiMiddleware`) for Node.js, Bun, Cloudflare Workers
30
+ - `@pompelmi/remix` — Remix upload handler (`pompelmiUploadHandler` for `unstable_parseMultipartFormData`)
31
+ - `@pompelmi/sveltekit` — SvelteKit helper (`scanUpload`, `scanFormData` for +page.server.ts and +server.ts)
32
+ - `@pompelmi/nextjs` — Next.js App Router / Pages Router helpers
33
+ - `@pompelmi/testing` — test utilities (`mockClean`, `mockInfected`, `withMockedPompelmi`)
34
+
23
35
  ## Installation
24
36
 
25
37
  npm install pompelmi
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pompelmi",
3
- "version": "1.14.0",
3
+ "version": "1.16.0",
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",
@@ -28,7 +28,10 @@
28
28
  "fastify",
29
29
  "nestjs",
30
30
  "multer",
31
- "zero-dependencies"
31
+ "zero-dependencies",
32
+ "remix",
33
+ "sveltekit",
34
+ "svelte"
32
35
  ],
33
36
  "type": "commonjs",
34
37
  "main": "./src/index.js",
@@ -37,7 +40,7 @@
37
40
  "pompelmi": "./bin/pompelmi.js"
38
41
  },
39
42
  "scripts": {
40
- "test": "node --test test/unit.test.js && node --test packages/nestjs/test/index.test.js && node --test packages/fastify/test/index.test.js && node --test packages/nextjs/test/index.test.js && node test/scan.test.js",
43
+ "test": "node --test test/unit.test.js && node --test packages/nestjs/test/index.test.js && node --test packages/fastify/test/index.test.js && node --test packages/nextjs/test/index.test.js && node --test packages/hono/test/index.test.js && node --test packages/testing/test/index.test.js && node --test packages/remix/test/index.test.js && node --test packages/sveltekit/test/index.test.js && node test/scan.test.js",
41
44
  "lint": "eslint src/"
42
45
  },
43
46
  "publishConfig": {
@@ -0,0 +1,160 @@
1
+ # @pompelmi/hono
2
+
3
+ Hono middleware for [pompelmi](https://pompelmi.app) — in-process ClamAV virus scanning with zero extra dependencies.
4
+
5
+ Works on **Node.js**, **Bun**, and **Cloudflare Workers** (simulation mode).
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install @pompelmi/hono pompelmi
11
+ # or
12
+ bun add @pompelmi/hono pompelmi
13
+ ```
14
+
15
+ ## Quick Start
16
+
17
+ ```js
18
+ import { Hono } from 'hono'
19
+ import { pompelmiMiddleware } from '@pompelmi/hono'
20
+
21
+ const app = new Hono()
22
+
23
+ app.use('/upload/*', pompelmiMiddleware({
24
+ host: 'localhost',
25
+ port: 3310,
26
+ }))
27
+
28
+ app.post('/upload', async (c) => {
29
+ // file is guaranteed clean here
30
+ return c.json({ ok: true })
31
+ })
32
+
33
+ export default app
34
+ ```
35
+
36
+ ## Custom infected handler
37
+
38
+ ```js
39
+ app.use('/upload/*', pompelmiMiddleware({
40
+ host: 'localhost',
41
+ port: 3310,
42
+ field: 'file',
43
+ onInfected: (c, filename) => {
44
+ console.warn(`Blocked malicious upload: ${filename}`)
45
+ return c.json({ error: 'Malware detected', filename }, 422)
46
+ },
47
+ }))
48
+ ```
49
+
50
+ ## Hono on Node.js
51
+
52
+ ```js
53
+ const { serve } = require('@hono/node-server')
54
+ const { Hono } = require('hono')
55
+ const { pompelmiMiddleware } = require('@pompelmi/hono')
56
+
57
+ const app = new Hono()
58
+
59
+ app.use('/upload/*', pompelmiMiddleware({
60
+ host: '127.0.0.1',
61
+ port: 3310,
62
+ }))
63
+
64
+ app.post('/upload', async (c) => {
65
+ const body = await c.req.parseBody()
66
+ const file = body['file']
67
+ return c.json({ name: file.name, size: file.size, ok: true })
68
+ })
69
+
70
+ serve({ fetch: app.fetch, port: 3000 }, () => {
71
+ console.log('Server running on http://localhost:3000')
72
+ })
73
+ ```
74
+
75
+ ## Hono on Bun
76
+
77
+ ```ts
78
+ import { Hono } from 'hono'
79
+ import { pompelmiMiddleware } from '@pompelmi/hono'
80
+
81
+ const app = new Hono()
82
+
83
+ app.use('/upload/*', pompelmiMiddleware({
84
+ socket: '/run/clamav/clamd.sock', // UNIX socket — faster on Bun
85
+ }))
86
+
87
+ app.post('/upload', async (c) => {
88
+ return c.json({ ok: true })
89
+ })
90
+
91
+ export default {
92
+ port: 3000,
93
+ fetch: app.fetch,
94
+ }
95
+ ```
96
+
97
+ ## Hono on Cloudflare Workers
98
+
99
+ > Note: clamd is not available inside Workers. Use pompelmi in a Node.js / Bun sidecar
100
+ > service and call it over HTTP, or use the UNIX socket approach with a co-located daemon.
101
+ >
102
+ > For Workers deployments without a sidecar, the middleware skips scanning gracefully
103
+ > and calls `next()` — you can gate the behaviour with an environment variable.
104
+
105
+ ```ts
106
+ import { Hono } from 'hono'
107
+ import { pompelmiMiddleware } from '@pompelmi/hono'
108
+
109
+ const app = new Hono<{ Bindings: { SCAN_HOST: string; SCAN_PORT: string } }>()
110
+
111
+ app.use('/upload/*', async (c, next) => {
112
+ if (!c.env.SCAN_HOST) return next() // skip if no clamd configured
113
+ return pompelmiMiddleware({
114
+ host: c.env.SCAN_HOST,
115
+ port: Number(c.env.SCAN_PORT) || 3310,
116
+ })(c, next)
117
+ })
118
+
119
+ app.post('/upload', async (c) => {
120
+ return c.json({ ok: true })
121
+ })
122
+
123
+ export default app
124
+ ```
125
+
126
+ ## Configuration Reference
127
+
128
+ All options are forwarded to pompelmi's `ScanOptions`:
129
+
130
+ | Option | Type | Default | Description |
131
+ |--------|------|---------|-------------|
132
+ | `field` | `string` | `'file'` | Form field name containing the uploaded file |
133
+ | `host` | `string` | — | clamd hostname (enables TCP mode) |
134
+ | `port` | `number` | `3310` | clamd port |
135
+ | `socket` | `string` | — | UNIX domain socket path |
136
+ | `timeout` | `number` | `15000` | Socket idle timeout in ms |
137
+ | `retries` | `number` | `0` | Number of retry attempts |
138
+ | `retryDelay` | `number` | `1000` | Delay between retries in ms |
139
+ | `onInfected` | `Function` | — | Called with `(c, filename)` when malware is detected |
140
+
141
+ ## TypeScript
142
+
143
+ ```ts
144
+ import { Hono } from 'hono'
145
+ import { pompelmiMiddleware } from '@pompelmi/hono'
146
+ import { Verdict } from 'pompelmi'
147
+
148
+ const app = new Hono()
149
+
150
+ app.use('/upload/*', pompelmiMiddleware({
151
+ host: 'localhost',
152
+ port: 3310,
153
+ onInfected: (c, filename) =>
154
+ c.json({ error: `${filename} is infected` }, 422),
155
+ }))
156
+ ```
157
+
158
+ ## License
159
+
160
+ ISC — see root [LICENSE](../../LICENSE).
@@ -0,0 +1,29 @@
1
+ import type { MiddlewareHandler, Context } from 'hono';
2
+ import type { ScanOptions } from 'pompelmi';
3
+
4
+ export interface PompelmiHonoOptions extends ScanOptions {
5
+ /** Form field name that contains the uploaded file (default: 'file') */
6
+ field?: string;
7
+ /**
8
+ * Called with (c, filename) when a malicious file is detected.
9
+ * Must return a Response (or Promise<Response>).
10
+ * If omitted, responds with HTTP 422 { error: 'Malware detected' }.
11
+ */
12
+ onInfected?: (c: Context, filename: string) => Response | Promise<Response>;
13
+ }
14
+
15
+ /**
16
+ * Hono middleware that scans an uploaded file before the route handler runs.
17
+ * Reads the file from the parsed multipart body field (default: 'file').
18
+ *
19
+ * @example
20
+ * import { Hono } from 'hono'
21
+ * import { pompelmiMiddleware } from '@pompelmi/hono'
22
+ *
23
+ * const app = new Hono()
24
+ * app.use('/upload/*', pompelmiMiddleware({ host: 'localhost', port: 3310 }))
25
+ * app.post('/upload', (c) => c.json({ ok: true }))
26
+ */
27
+ export function pompelmiMiddleware(options?: PompelmiHonoOptions): MiddlewareHandler;
28
+
29
+ export { Verdict } from 'pompelmi';
@@ -0,0 +1,74 @@
1
+ 'use strict';
2
+
3
+ const { scanBuffer, Verdict } = require('pompelmi');
4
+
5
+ const SCAN_KEYS = ['host', 'port', 'socket', 'timeout', 'retries', 'retryDelay'];
6
+
7
+ function buildScanOptions(options) {
8
+ const out = {};
9
+ for (const k of SCAN_KEYS) {
10
+ if (options[k] !== undefined) out[k] = options[k];
11
+ }
12
+ return out;
13
+ }
14
+
15
+ async function toBuffer(raw) {
16
+ if (Buffer.isBuffer(raw)) return raw;
17
+ if (raw instanceof Uint8Array) return Buffer.from(raw);
18
+ // Web API File / Blob (Hono on Bun, Cloudflare Workers, Node 20+)
19
+ if (raw && typeof raw.arrayBuffer === 'function') {
20
+ return Buffer.from(await raw.arrayBuffer());
21
+ }
22
+ if (typeof raw === 'string') return Buffer.from(raw);
23
+ return null;
24
+ }
25
+
26
+ /**
27
+ * Hono middleware that scans an uploaded file with pompelmi before the route
28
+ * handler runs. The file is read from the parsed multipart body.
29
+ *
30
+ * @param {object} [options]
31
+ * @param {string} [options.field='file'] - Form field name containing the file.
32
+ * @param {string} [options.host] - clamd hostname (enables TCP mode).
33
+ * @param {number} [options.port=3310] - clamd port.
34
+ * @param {string} [options.socket] - UNIX domain socket path.
35
+ * @param {number} [options.timeout=15000] - Socket idle timeout in ms.
36
+ * @param {number} [options.retries=0] - Number of retry attempts.
37
+ * @param {number} [options.retryDelay=1000] - Delay between retries in ms.
38
+ * @param {Function} [options.onInfected] - Called with (c, filename) when malware
39
+ * is detected; must return a Response.
40
+ * Defaults to 422 JSON error.
41
+ */
42
+ function pompelmiMiddleware(options) {
43
+ const { field = 'file', onInfected } = options || {};
44
+ const scanOptions = buildScanOptions(options || {});
45
+
46
+ return async function pompelmiHonoMiddleware(c, next) {
47
+ let body;
48
+ try {
49
+ body = await c.req.parseBody();
50
+ } catch {
51
+ return next();
52
+ }
53
+
54
+ const raw = body[field];
55
+ if (raw == null) return next();
56
+
57
+ const buffer = await toBuffer(raw);
58
+ if (!buffer || buffer.length === 0) return next();
59
+
60
+ const result = await scanBuffer(buffer, scanOptions);
61
+
62
+ if (result === Verdict.Malicious) {
63
+ const filename = (raw && typeof raw.name === 'string') ? raw.name : field;
64
+ if (typeof onInfected === 'function') {
65
+ return onInfected(c, filename);
66
+ }
67
+ return c.json({ error: 'Malware detected' }, 422);
68
+ }
69
+
70
+ return next();
71
+ };
72
+ }
73
+
74
+ module.exports = { pompelmiMiddleware, Verdict };
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@pompelmi/hono",
3
+ "version": "1.0.0",
4
+ "description": "Hono middleware for pompelmi — in-process ClamAV virus scanning with zero extra dependencies",
5
+ "license": "ISC",
6
+ "author": "pompelmi contributors",
7
+ "homepage": "https://pompelmi.app",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/pompelmi/pompelmi.git",
11
+ "directory": "packages/hono"
12
+ },
13
+ "keywords": [
14
+ "hono",
15
+ "middleware",
16
+ "clamav",
17
+ "antivirus",
18
+ "virus-scan",
19
+ "malware",
20
+ "file-upload",
21
+ "security",
22
+ "pompelmi",
23
+ "bun",
24
+ "edge",
25
+ "cloudflare-workers"
26
+ ],
27
+ "main": "./index.js",
28
+ "types": "./index.d.ts",
29
+ "scripts": {
30
+ "test": "node --test test/index.test.js"
31
+ },
32
+ "peerDependencies": {
33
+ "hono": ">=3",
34
+ "pompelmi": ">=1.14.0"
35
+ },
36
+ "publishConfig": {
37
+ "registry": "https://registry.npmjs.org/",
38
+ "access": "public"
39
+ }
40
+ }
@@ -0,0 +1,142 @@
1
+ # @pompelmi/remix
2
+
3
+ Remix upload handler for [pompelmi](https://pompelmi.app) — in-process ClamAV virus scanning with zero extra dependencies.
4
+
5
+ Works with **Remix v1 and v2** on Node.js, and is compatible with the `unstable_parseMultipartFormData` API.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install @pompelmi/remix pompelmi
11
+ ```
12
+
13
+ ## Quick Start
14
+
15
+ ```ts
16
+ import { unstable_parseMultipartFormData, json } from '@remix-run/node'
17
+ import { pompelmiUploadHandler } from '@pompelmi/remix'
18
+ import type { ActionFunctionArgs } from '@remix-run/node'
19
+
20
+ export async function action({ request }: ActionFunctionArgs) {
21
+ const formData = await unstable_parseMultipartFormData(
22
+ request,
23
+ pompelmiUploadHandler({ host: 'localhost', port: 3310 })
24
+ )
25
+
26
+ const file = formData.get('file') as File
27
+ return json({ name: file.name, size: file.size, ok: true })
28
+ }
29
+ ```
30
+
31
+ If a malicious file is uploaded, `pompelmiUploadHandler` throws a `Response` with HTTP **422** — Remix catches it automatically and returns it to the client.
32
+
33
+ ## With an inner handler
34
+
35
+ Chain with any Remix upload handler (e.g. `unstable_createFileUploadHandler`) to store clean files to disk:
36
+
37
+ ```ts
38
+ import {
39
+ unstable_parseMultipartFormData,
40
+ unstable_createFileUploadHandler,
41
+ json,
42
+ } from '@remix-run/node'
43
+ import { pompelmiUploadHandler } from '@pompelmi/remix'
44
+
45
+ const uploadToTmp = unstable_createFileUploadHandler({ directory: '/tmp/uploads' })
46
+
47
+ export async function action({ request }) {
48
+ const formData = await unstable_parseMultipartFormData(
49
+ request,
50
+ pompelmiUploadHandler({
51
+ host: 'localhost',
52
+ port: 3310,
53
+ inner: uploadToTmp, // called only if file is clean
54
+ })
55
+ )
56
+ const file = formData.get('file') // NodeOnDiskFile from inner handler
57
+ return json({ ok: true })
58
+ }
59
+ ```
60
+
61
+ ## Scan a specific field only
62
+
63
+ Use `field` to restrict scanning to a single form field. Other file fields are passed through to `inner` (or returned as `File` objects) without scanning:
64
+
65
+ ```ts
66
+ pompelmiUploadHandler({
67
+ host: 'localhost',
68
+ port: 3310,
69
+ field: 'avatar', // only scan the 'avatar' field
70
+ })
71
+ ```
72
+
73
+ ## Custom error response
74
+
75
+ ```ts
76
+ pompelmiUploadHandler({
77
+ host: 'localhost',
78
+ port: 3310,
79
+ onInfected: ({ filename }) => {
80
+ console.warn(`Blocked malicious upload: ${filename}`)
81
+ throw new Response(
82
+ JSON.stringify({ error: 'Malware detected', filename }),
83
+ { status: 422, headers: { 'Content-Type': 'application/json' } }
84
+ )
85
+ },
86
+ })
87
+ ```
88
+
89
+ ## Route example (full)
90
+
91
+ ```tsx
92
+ // app/routes/upload.tsx
93
+ import {
94
+ unstable_parseMultipartFormData,
95
+ json,
96
+ type ActionFunctionArgs,
97
+ } from '@remix-run/node'
98
+ import { Form, useActionData } from '@remix-run/react'
99
+ import { pompelmiUploadHandler } from '@pompelmi/remix'
100
+
101
+ export async function action({ request }: ActionFunctionArgs) {
102
+ // Throws HTTP 422 automatically if malware is detected
103
+ const formData = await unstable_parseMultipartFormData(
104
+ request,
105
+ pompelmiUploadHandler({ host: 'localhost', port: 3310 })
106
+ )
107
+
108
+ const file = formData.get('document') as File
109
+ if (!file) return json({ error: 'No file provided' }, { status: 400 })
110
+
111
+ return json({ name: file.name, size: file.size, ok: true })
112
+ }
113
+
114
+ export default function Upload() {
115
+ const data = useActionData<typeof action>()
116
+ return (
117
+ <Form method="post" encType="multipart/form-data">
118
+ <input type="file" name="document" />
119
+ <button type="submit">Upload</button>
120
+ {data?.ok && <p>Uploaded: {data.name} ({data.size} bytes)</p>}
121
+ </Form>
122
+ )
123
+ }
124
+ ```
125
+
126
+ ## Configuration Reference
127
+
128
+ | Option | Type | Default | Description |
129
+ |--------|------|---------|-------------|
130
+ | `field` | `string` | — | Only scan this field; others pass through unscanned |
131
+ | `inner` | `UploadHandler` | — | Inner handler for clean files (e.g. file-upload, memory) |
132
+ | `host` | `string` | — | clamd hostname (enables TCP mode) |
133
+ | `port` | `number` | `3310` | clamd port |
134
+ | `socket` | `string` | — | UNIX domain socket path |
135
+ | `timeout` | `number` | `15000` | Socket idle timeout in ms |
136
+ | `retries` | `number` | `0` | Retry attempts |
137
+ | `retryDelay` | `number` | `1000` | Delay between retries in ms |
138
+ | `onInfected` | `Function` | — | Called with `{ name, filename }` when malware detected |
139
+
140
+ ## License
141
+
142
+ ISC — see root [LICENSE](../../LICENSE).
@@ -0,0 +1,50 @@
1
+ import type { ScanOptions } from 'pompelmi';
2
+
3
+ export interface UploadHandlerArgs {
4
+ name: string;
5
+ filename: string | undefined;
6
+ contentType: string;
7
+ data: AsyncIterable<Uint8Array>;
8
+ }
9
+
10
+ export type RemixUploadHandler = (args: UploadHandlerArgs) => Promise<File | string | undefined>;
11
+
12
+ export interface PompelmiRemixOptions extends ScanOptions {
13
+ /**
14
+ * Only scan this form field name; other file fields are passed straight to `inner`.
15
+ * When omitted, all file fields are scanned.
16
+ */
17
+ field?: string;
18
+ /**
19
+ * Inner UploadHandler to call for clean files (e.g. unstable_createMemoryUploadHandler).
20
+ * When omitted, clean files are returned as Web API `File` objects.
21
+ */
22
+ inner?: RemixUploadHandler;
23
+ /**
24
+ * Called with `{ name, filename }` when a malicious file is detected.
25
+ * Must throw a Response (or return one that will be thrown by the caller).
26
+ * If omitted, throws an HTTP 422 Response automatically.
27
+ */
28
+ onInfected?: (args: { name: string; filename: string }) => never | Promise<never>;
29
+ }
30
+
31
+ /**
32
+ * Creates a Remix-compatible UploadHandler that scans each uploaded file with
33
+ * pompelmi before forwarding it to an optional inner handler.
34
+ *
35
+ * @example
36
+ * import { unstable_parseMultipartFormData } from '@remix-run/node'
37
+ * import { pompelmiUploadHandler } from '@pompelmi/remix'
38
+ *
39
+ * export async function action({ request }: ActionFunctionArgs) {
40
+ * const formData = await unstable_parseMultipartFormData(
41
+ * request,
42
+ * pompelmiUploadHandler({ host: 'localhost', port: 3310 })
43
+ * )
44
+ * const file = formData.get('file') as File
45
+ * return json({ name: file.name, size: file.size })
46
+ * }
47
+ */
48
+ export function pompelmiUploadHandler(options?: PompelmiRemixOptions): RemixUploadHandler;
49
+
50
+ export { Verdict } from 'pompelmi';