pompelmi 1.12.1 → 1.14.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.
@@ -0,0 +1,48 @@
1
+ 'use strict';
2
+
3
+ const { PompelmiService } = require('./pompelmi.service');
4
+
5
+ const POMPELMI_OPTIONS = 'POMPELMI_OPTIONS';
6
+
7
+ class PompelmiModule {
8
+ static forRoot(options = {}) {
9
+ return {
10
+ module: PompelmiModule,
11
+ providers: [
12
+ { provide: POMPELMI_OPTIONS, useValue: options },
13
+ {
14
+ provide: PompelmiService,
15
+ useFactory: (opts) => new PompelmiService(opts),
16
+ inject: [POMPELMI_OPTIONS],
17
+ },
18
+ ],
19
+ exports: [PompelmiService],
20
+ };
21
+ }
22
+
23
+ static forRootAsync(asyncOptions = {}) {
24
+ const asyncProviders = [];
25
+ if (asyncOptions.useFactory) {
26
+ asyncProviders.push({
27
+ provide: POMPELMI_OPTIONS,
28
+ useFactory: asyncOptions.useFactory,
29
+ inject: asyncOptions.inject || [],
30
+ });
31
+ }
32
+ return {
33
+ module: PompelmiModule,
34
+ imports: asyncOptions.imports || [],
35
+ providers: [
36
+ ...asyncProviders,
37
+ {
38
+ provide: PompelmiService,
39
+ useFactory: (opts) => new PompelmiService(opts),
40
+ inject: [POMPELMI_OPTIONS],
41
+ },
42
+ ],
43
+ exports: [PompelmiService],
44
+ };
45
+ }
46
+ }
47
+
48
+ module.exports = { PompelmiModule, POMPELMI_OPTIONS };
@@ -0,0 +1,28 @@
1
+ 'use strict';
2
+
3
+ const { scan, scanBuffer, scanStream, Verdict } = require('pompelmi');
4
+
5
+ class PompelmiService {
6
+ constructor(options = {}) {
7
+ this.options = options;
8
+ }
9
+
10
+ scan(filePath) {
11
+ return scan(filePath, this.options);
12
+ }
13
+
14
+ scanBuffer(buffer) {
15
+ return scanBuffer(buffer, this.options);
16
+ }
17
+
18
+ scanStream(stream) {
19
+ return scanStream(stream, this.options);
20
+ }
21
+
22
+ async isMalware(buffer) {
23
+ const result = await scanBuffer(buffer, this.options);
24
+ return result === Verdict.Malicious;
25
+ }
26
+ }
27
+
28
+ module.exports = { PompelmiService };
@@ -0,0 +1,12 @@
1
+ {
2
+ "compilerOptions": {
3
+ "module": "commonjs",
4
+ "target": "ES2020",
5
+ "lib": ["ES2020"],
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "experimentalDecorators": true,
9
+ "emitDecoratorMetadata": true,
10
+ "skipLibCheck": true
11
+ }
12
+ }
@@ -0,0 +1,83 @@
1
+ # @pompelmi/nextjs
2
+
3
+ Next.js middleware for [pompelmi](https://pompelmi.app) — in-process ClamAV virus scanning with zero extra dependencies.
4
+
5
+ Supports both the **App Router** (Next.js 13+) and the **Pages Router** (Next.js ≤ 12).
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install pompelmi @pompelmi/nextjs
11
+ ```
12
+
13
+ ClamAV must be available on the server — either via the system `clamscan` binary or a running `clamd` daemon.
14
+
15
+ ## App Router (Next.js 13+)
16
+
17
+ ```js
18
+ // app/api/upload/route.js
19
+ import { withPompelmi } from '@pompelmi/nextjs'
20
+
21
+ export const POST = withPompelmi(async (req) => {
22
+ const formData = await req.formData()
23
+ const file = formData.get('file')
24
+ // req.pompelmiVerdict is set — Verdict.Clean is guaranteed here
25
+ return Response.json({ ok: true })
26
+ }, {
27
+ host: 'localhost',
28
+ port: 3310,
29
+ })
30
+ ```
31
+
32
+ With TypeScript:
33
+
34
+ ```ts
35
+ // app/api/upload/route.ts
36
+ import { withPompelmi } from '@pompelmi/nextjs'
37
+
38
+ export const POST = withPompelmi(async (req: Request) => {
39
+ return Response.json({ ok: true })
40
+ }, { host: 'localhost', port: 3310 })
41
+ ```
42
+
43
+ ## Pages Router (Next.js ≤ 12)
44
+
45
+ ```js
46
+ // pages/api/upload.js
47
+ import { withPompelmiHandler } from '@pompelmi/nextjs'
48
+
49
+ async function handler(req, res) {
50
+ // req.pompelmiVerdict is set — Verdict.Clean is guaranteed here
51
+ res.json({ ok: true })
52
+ }
53
+
54
+ export default withPompelmiHandler(handler, {
55
+ host: 'localhost',
56
+ port: 3310,
57
+ })
58
+ ```
59
+
60
+ ## Options
61
+
62
+ All options are forwarded to `pompelmi.scanBuffer()`:
63
+
64
+ | Option | Type | Default | Description |
65
+ |---|---|---|---|
66
+ | `host` | `string` | — | clamd hostname (TCP mode) |
67
+ | `port` | `number` | `3310` | clamd port |
68
+ | `socket` | `string` | — | UNIX socket path |
69
+ | `timeout` | `number` | `15000` | Connection timeout in ms |
70
+ | `retries` | `number` | `0` | Auto-retry count on failure |
71
+
72
+ When neither `host` nor `socket` is provided, pompelmi falls back to the local `clamscan` binary.
73
+
74
+ ## Behaviour
75
+
76
+ - The raw request body is buffered and scanned **before** your handler runs.
77
+ - If the body is malicious, a **400 JSON** response is returned immediately: `{ "error": "Malicious file detected" }`.
78
+ - `req.pompelmiVerdict` is set to the `Verdict` symbol so your handler can inspect it if needed.
79
+ - Scan errors (e.g. clamd unreachable) are **not** blocking — the request proceeds with `Verdict.ScanError`.
80
+
81
+ ## License
82
+
83
+ ISC
@@ -0,0 +1,45 @@
1
+ import type { VerdictValue, ScanOptions } from 'pompelmi';
2
+
3
+ /** Options accepted by withPompelmi / withPompelmiHandler */
4
+ export interface PompelmiNextOptions extends ScanOptions {}
5
+
6
+ /**
7
+ * App Router (Next.js 13+) wrapper.
8
+ * Scans the raw request body before the handler runs.
9
+ * Returns HTTP 400 if the body is malicious.
10
+ *
11
+ * @example
12
+ * // app/api/upload/route.ts
13
+ * import { withPompelmi } from '@pompelmi/nextjs'
14
+ *
15
+ * export const POST = withPompelmi(async (req) => {
16
+ * return Response.json({ ok: true })
17
+ * }, { host: 'localhost', port: 3310 })
18
+ */
19
+ export declare function withPompelmi(
20
+ handler: (req: Request, ctx?: unknown) => Promise<Response>,
21
+ options?: PompelmiNextOptions
22
+ ): (req: Request, ctx?: unknown) => Promise<Response>;
23
+
24
+ /**
25
+ * Pages Router (Next.js ≤ 12) wrapper.
26
+ * Scans the raw request body before the handler runs.
27
+ * Sends HTTP 400 if the body is malicious.
28
+ *
29
+ * @example
30
+ * // pages/api/upload.ts
31
+ * import { withPompelmiHandler } from '@pompelmi/nextjs'
32
+ * import type { NextApiRequest, NextApiResponse } from 'next'
33
+ *
34
+ * async function handler(req: NextApiRequest, res: NextApiResponse) {
35
+ * res.json({ ok: true })
36
+ * }
37
+ *
38
+ * export default withPompelmiHandler(handler, { host: 'localhost', port: 3310 })
39
+ */
40
+ export declare function withPompelmiHandler(
41
+ handler: (req: import('http').IncomingMessage, res: import('http').ServerResponse) => void | Promise<void>,
42
+ options?: PompelmiNextOptions
43
+ ): (req: import('http').IncomingMessage, res: import('http').ServerResponse) => Promise<void>;
44
+
45
+ export { Verdict } from 'pompelmi';
@@ -0,0 +1,130 @@
1
+ 'use strict';
2
+
3
+ const { scanBuffer, Verdict } = require('pompelmi');
4
+
5
+ /**
6
+ * Build a pompelmi scan options object from the plugin options,
7
+ * excluding non-scan keys.
8
+ */
9
+ function buildScanOpts(options) {
10
+ const keys = ['host', 'port', 'socket', 'timeout', 'retries', 'retryDelay'];
11
+ const out = {};
12
+ for (const k of keys) {
13
+ if (options[k] !== undefined) out[k] = options[k];
14
+ }
15
+ return out;
16
+ }
17
+
18
+ /**
19
+ * Read a Request body as a Buffer.
20
+ * Supports Web API Request (App Router) and Node.js IncomingMessage (Pages Router).
21
+ */
22
+ async function bodyBuffer(req) {
23
+ if (typeof req.arrayBuffer === 'function') {
24
+ // Web API Request (Next.js App Router)
25
+ const ab = await req.arrayBuffer();
26
+ return Buffer.from(ab);
27
+ }
28
+ // Node.js IncomingMessage (Pages Router)
29
+ return new Promise((resolve, reject) => {
30
+ const chunks = [];
31
+ req.on('data', c => chunks.push(c));
32
+ req.on('end', () => resolve(Buffer.concat(chunks)));
33
+ req.on('error', reject);
34
+ });
35
+ }
36
+
37
+ /**
38
+ * App Router wrapper (Next.js 13+).
39
+ *
40
+ * Wraps a route handler so that the raw request body is scanned before the
41
+ * handler runs. req.pompelmiVerdict is set to the scan verdict symbol.
42
+ * If the body is malicious a 400 JSON response is returned immediately.
43
+ *
44
+ * @param {Function} handler - async (req, ctx) => Response
45
+ * @param {object} options - ScanOptions (host, port, socket, …)
46
+ * @returns {Function} Next.js App Router handler
47
+ *
48
+ * @example
49
+ * // app/api/upload/route.js
50
+ * import { withPompelmi } from '@pompelmi/nextjs'
51
+ *
52
+ * export const POST = withPompelmi(async (req) => {
53
+ * return Response.json({ ok: true })
54
+ * }, { host: 'localhost', port: 3310 })
55
+ */
56
+ function withPompelmi(handler, options = {}) {
57
+ const scanOpts = buildScanOpts(options);
58
+
59
+ return async function pompelmiHandler(req, ctx) {
60
+ let buf;
61
+ try {
62
+ buf = await bodyBuffer(req);
63
+ } catch (_) {
64
+ return new Response(JSON.stringify({ error: 'Failed to read request body' }), {
65
+ status: 400,
66
+ headers: { 'Content-Type': 'application/json' },
67
+ });
68
+ }
69
+
70
+ const verdict = await scanBuffer(buf, scanOpts);
71
+
72
+ if (verdict === Verdict.Malicious) {
73
+ return new Response(JSON.stringify({ error: 'Malicious file detected' }), {
74
+ status: 400,
75
+ headers: { 'Content-Type': 'application/json' },
76
+ });
77
+ }
78
+
79
+ // Attach verdict so the handler can inspect it if needed
80
+ req.pompelmiVerdict = verdict;
81
+ return handler(req, ctx);
82
+ };
83
+ }
84
+
85
+ /**
86
+ * Pages Router wrapper (Next.js ≤ 12 / Pages API routes).
87
+ *
88
+ * Wraps a Next.js API handler so that the raw request body is scanned before
89
+ * the handler runs. req.pompelmiVerdict is set to the scan verdict symbol.
90
+ * If the body is malicious a 400 JSON response is sent immediately.
91
+ *
92
+ * @param {Function} handler - (req, res) => void | Promise<void>
93
+ * @param {object} options - ScanOptions (host, port, socket, …)
94
+ * @returns {Function} Next.js Pages API handler
95
+ *
96
+ * @example
97
+ * // pages/api/upload.js
98
+ * import { withPompelmiHandler } from '@pompelmi/nextjs'
99
+ *
100
+ * async function handler(req, res) {
101
+ * res.json({ ok: true })
102
+ * }
103
+ *
104
+ * export default withPompelmiHandler(handler, { host: 'localhost', port: 3310 })
105
+ */
106
+ function withPompelmiHandler(handler, options = {}) {
107
+ const scanOpts = buildScanOpts(options);
108
+
109
+ return async function pompelmiPagesHandler(req, res) {
110
+ let buf;
111
+ try {
112
+ buf = await bodyBuffer(req);
113
+ } catch (_) {
114
+ res.status(400).json({ error: 'Failed to read request body' });
115
+ return;
116
+ }
117
+
118
+ const verdict = await scanBuffer(buf, scanOpts);
119
+
120
+ if (verdict === Verdict.Malicious) {
121
+ res.status(400).json({ error: 'Malicious file detected' });
122
+ return;
123
+ }
124
+
125
+ req.pompelmiVerdict = verdict;
126
+ return handler(req, res);
127
+ };
128
+ }
129
+
130
+ module.exports = { withPompelmi, withPompelmiHandler, Verdict };
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@pompelmi/nextjs",
3
+ "version": "1.0.0",
4
+ "description": "Next.js 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/nextjs"
12
+ },
13
+ "keywords": [
14
+ "nextjs",
15
+ "next",
16
+ "clamav",
17
+ "antivirus",
18
+ "virus-scan",
19
+ "malware",
20
+ "file-upload",
21
+ "security",
22
+ "pompelmi",
23
+ "app-router",
24
+ "pages-router"
25
+ ],
26
+ "main": "./index.js",
27
+ "types": "./index.d.ts",
28
+ "scripts": {
29
+ "test": "node --test test/index.test.js"
30
+ },
31
+ "peerDependencies": {
32
+ "next": ">=13",
33
+ "pompelmi": ">=1.13.0"
34
+ },
35
+ "publishConfig": {
36
+ "registry": "https://registry.npmjs.org/",
37
+ "access": "public"
38
+ }
39
+ }