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.
- package/README.md +44 -1
- package/bin/pompelmi.js +36 -0
- package/package.json +2 -2
- package/packages/fastify/README.md +115 -0
- package/packages/fastify/index.d.ts +27 -0
- package/packages/fastify/index.js +56 -0
- package/packages/fastify/package.json +40 -0
- package/packages/nestjs/README.md +150 -0
- package/packages/nestjs/index.d.ts +37 -0
- package/packages/nestjs/package.json +38 -0
- package/packages/nestjs/src/index.js +8 -0
- package/packages/nestjs/src/pompelmi.guard.js +19 -0
- package/packages/nestjs/src/pompelmi.interceptor.js +24 -0
- package/packages/nestjs/src/pompelmi.module.js +48 -0
- package/packages/nestjs/src/pompelmi.service.js +28 -0
- package/packages/nestjs/tsconfig.json +12 -0
- package/packages/nextjs/README.md +83 -0
- package/packages/nextjs/index.d.ts +45 -0
- package/packages/nextjs/index.js +130 -0
- package/packages/nextjs/package.json +39 -0
- package/src/Dashboard.js +370 -0
- package/src/ShareCard.js +124 -0
- package/src/index.js +3 -1
- package/types/index.d.ts +51 -0
- package/pr_info.tmp +0 -2
|
@@ -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,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
|
+
}
|