vpndetection-express 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mslm Dev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,158 @@
1
+ # [<img src="https://s3.vpndetection.io/vpndetection-public/brand/mark.svg" alt="VPNDetection" width="24"/>](https://vpndetection.io/) VPNDetection Express Middleware
2
+
3
+ [![npm](https://img.shields.io/npm/v/vpndetection-express.svg)](https://www.npmjs.com/package/vpndetection-express)
4
+ [![license](https://img.shields.io/npm/l/vpndetection-express.svg)](LICENSE)
5
+
6
+ The official [Express](https://expressjs.com) middleware for the [VPNDetection](https://vpndetection.io) API.
7
+
8
+ It classifies the visitor behind each request — VPN, residential proxy, Tor, hosting, CDN, relay — and hands the answer to your handlers. Blocking is opt-in.
9
+
10
+ ## Getting Started
11
+
12
+ ```bash
13
+ npm install vpndetection-express
14
+ ```
15
+
16
+ Requires Node.js 22 or newer and Express 4.18 or newer. TypeScript types are included.
17
+
18
+ You need an API key. Create one in the [console](https://app.vpndetection.io); the free tier's allowance is counted per source address, and a server is a single source address, so a key is what makes this usable in production rather than optional.
19
+
20
+ ```js
21
+ import express from 'express';
22
+ import { vpndetection } from 'vpndetection-express';
23
+
24
+ const app = express();
25
+
26
+ app.set('trust proxy', true); // see "Where the client address comes from" below
27
+ app.use(vpndetection({ apiKey: process.env.VPNDETECTION_API_KEY }));
28
+
29
+ app.get('/', (req, res) => {
30
+ const { result } = req.vpndetection;
31
+ res.send(result.isVpn ? 'Hello, VPN user' : 'Hello');
32
+ });
33
+ ```
34
+
35
+ By default nothing is blocked. Every request gets a `req.vpndetection` and your own code decides what that means — which is usually what you want, because whether a VPN visitor is a problem depends entirely on what they are doing.
36
+
37
+ ## Blocking
38
+
39
+ Pass a `blockCondition` and a matching request is answered with `403` and never reaches your handlers.
40
+
41
+ ```js
42
+ app.use(vpndetection({
43
+ apiKey: process.env.VPNDETECTION_API_KEY,
44
+ blockCondition: { isVpn: true },
45
+ }));
46
+ ```
47
+
48
+ A condition is written in the shape of a result, and only the members you name are considered. That lets it reach the evidence, not just the flags:
49
+
50
+ ```js
51
+ blockCondition: { isVpn: true, vpn: { provider: 'nordvpn' } } // one provider
52
+ blockCondition: { isResproxy: true, resproxy: { hits: { gte: 5 } } } // a numeric threshold
53
+ blockCondition: { vpn: { confidence: ['high', 'medium'] } } // any of these
54
+ blockCondition: [{ isTor: true }, { isResproxy: true }] // a list is OR
55
+ ```
56
+
57
+ Values are matched by equality, strings without regard to case. An array means any-of. `{ gte, gt, lte, lt }` compares numbers, and every bound you give must hold, so two of them are a range. Members you set to `false` or `null` are ignored, so a condition states the signals you act on; one that constrains nothing would match every request, and is refused when the middleware is created rather than silently blocking all your traffic.
58
+
59
+ Replace the refusal with `onBlocked`:
60
+
61
+ ```js
62
+ app.use(vpndetection({
63
+ apiKey: process.env.VPNDETECTION_API_KEY,
64
+ blockCondition: { isVpn: true },
65
+ onBlocked: (req, res) => res.status(403).render('no-vpn'),
66
+ }));
67
+ ```
68
+
69
+ ## Where the client address comes from
70
+
71
+ This is the setting that decides whether any of the above works, and it is the one thing only you can get right.
72
+
73
+ By default the middleware uses `req.ip`, which is Express's own accessor. **Express resolves `req.ip` to the socket peer unless you set `trust proxy`.** So if your app sits behind nginx, a load balancer, or a CDN and you have not set it, every visitor arrives wearing your proxy's address — which is a datacenter address, so a hosting rule would block all of them.
74
+
75
+ If you are behind a proxy you control, setting Express's own option is the right fix and everything else here follows from it:
76
+
77
+ ```js
78
+ app.set('trust proxy', true);
79
+ ```
80
+
81
+ For an edge that writes the address into its own header, name the header:
82
+
83
+ ```js
84
+ import { headerIpSelector } from 'vpndetection-express';
85
+
86
+ app.use(vpndetection({
87
+ apiKey: process.env.VPNDETECTION_API_KEY,
88
+ ipSelector: headerIpSelector('CF-Connecting-IP'), // or True-Client-IP, or your own
89
+ }));
90
+ ```
91
+
92
+ `xffIpSelector()` reads `X-Forwarded-For` directly. Be aware that the left-most entry is whatever the caller sent, because proxies append to that header — it is only trustworthy when an edge you control overwrites it. If you know how many proxies sit in front, count from the right instead: `xffIpSelector({ depth: 1 })` is the address your nearest proxy saw.
93
+
94
+ Anything else, pass your own function. It receives the Express request and returns an address:
95
+
96
+ ```js
97
+ ipSelector: (req) => req.headers['x-real-ip'] ?? req.ip,
98
+ ```
99
+
100
+ If the address resolves to a private one, the middleware says so once on `console.warn`. That is expected on localhost and is the signal to fix your configuration anywhere else.
101
+
102
+ ## When a lookup fails
103
+
104
+ The request is let through, and the reason is recorded on `req.vpndetection.error`. Our outage should not become yours, so a network failure, an exhausted quota or a rejected key all fail open.
105
+
106
+ ```js
107
+ app.get('/', (req, res) => {
108
+ const { result, error } = req.vpndetection;
109
+ if (error) {
110
+ req.log.warn({ kind: error.kind }, 'vpndetection unavailable');
111
+ }
112
+ res.send(result?.isVpn ? 'Hello, VPN user' : 'Hello');
113
+ });
114
+ ```
115
+
116
+ Pass `failClosed: true` to block instead. Private addresses are answered locally and never fail, so this will not lock you out in development.
117
+
118
+ ## Cost and latency
119
+
120
+ Answers are cached per middleware for an hour, so a returning visitor costs nothing, and private addresses never leave the process. A cache miss is one request to our API, bounded at 2500 ms by default and not retried — on a request path, failing open quickly beats holding a visitor while we try again. Both are adjustable, along with the cache itself:
121
+
122
+ ```js
123
+ vpndetection({ apiKey: KEY, timeoutMs: 1000, retries: 1, cache: { max: 50000, ttlMs: 600000 } })
124
+ ```
125
+
126
+ Mount it on the routes that matter rather than the whole app, or skip what you do not care about:
127
+
128
+ ```js
129
+ app.use(vpndetection({ apiKey: KEY, skip: (req) => req.path.startsWith('/static') }));
130
+ ```
131
+
132
+ If you already hold a `VPNDetection` client, pass it as `client` and the middleware will share it rather than building a second cache.
133
+
134
+ Beyond a few million distinct visitors a day, stop calling the API per request: [download the dataset](https://vpndetection.io/databases) and look addresses up locally instead.
135
+
136
+ ## Absent is not false
137
+
138
+ Only `ip` and `isVpn` come back on every plan. A field your plan does not include is `undefined`, which means "not in your plan" rather than "checked, and no".
139
+
140
+ ```js
141
+ req.vpndetection.result.isHosting ?? false // when you only want the flag
142
+ ```
143
+
144
+ A `blockCondition` naming a member your plan does not serve can never match, so the middleware warns once instead of failing silently. Set `onMissingField: 'throw'` to make it an error.
145
+
146
+ ## Other Libraries
147
+
148
+ There are official VPNDetection client libraries available for many languages including PHP, Python, Go, Java, Ruby, and many popular frameworks such as Django, Rails, and Laravel. See our GitHub at https://github.com/vpndetection-io for more.
149
+
150
+ ## About VPNDetection
151
+
152
+ VPN Detection API: Accurate anonymity detection identifying VPNs, residential proxies, hosting servers, Tor nodes, CDNs, relays and more.
153
+
154
+ [<img src="https://s3.vpndetection.io/vpndetection-public/brand/mark.svg" alt="VPNDetection" width="96"/>](https://vpndetection.io/)
155
+
156
+ ## License
157
+
158
+ This project is licensed under the [MIT License](LICENSE).
@@ -0,0 +1,57 @@
1
+ import type { IpSelector, Lookup, MiddlewareOptions } from 'vpndetection/middleware';
2
+ import type { Request, RequestHandler, Response } from 'express';
3
+ export type { BlockCondition, IpSelector, Lookup, NumericBound } from 'vpndetection/middleware';
4
+ declare global {
5
+ namespace Express {
6
+ interface Request {
7
+ /**
8
+ * What the middleware found out about this visitor. Absent when the
9
+ * middleware has not run for this route, or when `skip` claimed it.
10
+ */
11
+ vpndetection?: Lookup;
12
+ }
13
+ }
14
+ }
15
+ export interface Options extends MiddlewareOptions<Request> {
16
+ /**
17
+ * How a blocked request is answered. Defaults to `403` with a short JSON
18
+ * body. Whatever you pass must end the response.
19
+ */
20
+ onBlocked?: (req: Request, res: Response, lookup: Lookup) => void;
21
+ }
22
+ /**
23
+ * Classify the visitor and hang the answer off `req.vpndetection`.
24
+ *
25
+ * Without a `blockCondition` this only enriches the request and never refuses
26
+ * one, leaving the decision to your own handlers. With one, a matching request
27
+ * is answered by `onBlocked` and never reaches them.
28
+ *
29
+ * A lookup that fails - network, quota, an outage of ours - lets the request
30
+ * through and records why on `req.vpndetection.error`, unless you set
31
+ * `failClosed`.
32
+ */
33
+ export declare function vpndetection(options?: Options): RequestHandler;
34
+ /**
35
+ * `req.ip`, which is the socket peer unless you have set Express's `trust
36
+ * proxy`. Behind a load balancer without it, every visitor looks like the load
37
+ * balancer - so if you are behind one, set it or pick another selector.
38
+ */
39
+ export declare const defaultIpSelector: IpSelector<Request>;
40
+ /**
41
+ * An address from `X-Forwarded-For`.
42
+ *
43
+ * **The left-most entry is whatever the caller sent**, since proxies append, so
44
+ * this is only trustworthy when an edge you control overwrites the header. When
45
+ * you know how many proxies sit in front, count from the right instead:
46
+ * `xffIpSelector({ depth: 1 })` is the address your nearest proxy saw.
47
+ */
48
+ export declare const xffIpSelector: (options?: {
49
+ depth?: number;
50
+ }) => IpSelector<Request>;
51
+ /**
52
+ * An address from a single-value header your edge writes -
53
+ * `headerIpSelector('CF-Connecting-IP')` behind Cloudflare,
54
+ * `headerIpSelector('True-Client-IP')` behind Akamai. Falls back to `req.ip`
55
+ * when the header is absent.
56
+ */
57
+ export declare const headerIpSelector: (name: string) => IpSelector<Request>;
package/dist/index.js ADDED
@@ -0,0 +1,67 @@
1
+ import { bindSelectors, createCore } from 'vpndetection/middleware';
2
+ /**
3
+ * Classify the visitor and hang the answer off `req.vpndetection`.
4
+ *
5
+ * Without a `blockCondition` this only enriches the request and never refuses
6
+ * one, leaving the decision to your own handlers. With one, a matching request
7
+ * is answered by `onBlocked` and never reaches them.
8
+ *
9
+ * A lookup that fails - network, quota, an outage of ours - lets the request
10
+ * through and records why on `req.vpndetection.error`, unless you set
11
+ * `failClosed`.
12
+ */
13
+ export function vpndetection(options = {}) {
14
+ const core = createCore(options, defaultIpSelector);
15
+ const onBlocked = options.onBlocked ?? refuse;
16
+ return (req, res, next) => {
17
+ core.evaluate(req).then((lookup) => {
18
+ if (lookup === undefined) {
19
+ next();
20
+ return;
21
+ }
22
+ req.vpndetection = lookup;
23
+ if (lookup.blocked) {
24
+ onBlocked(req, res, lookup);
25
+ return;
26
+ }
27
+ next();
28
+ }, next);
29
+ };
30
+ }
31
+ const view = (req) => ({
32
+ header: (name) => {
33
+ const value = req.headers[name.toLowerCase()];
34
+ return Array.isArray(value) ? value[0] : value;
35
+ },
36
+ frameworkIp: () => req.ip,
37
+ });
38
+ // Each of these is annotated rather than inferred: the inferred shape reaches
39
+ // through `@types/express` into its own transitive types, which TypeScript
40
+ // refuses to name in a declaration file a consumer would have to resolve.
41
+ const selectors = bindSelectors(view);
42
+ /**
43
+ * `req.ip`, which is the socket peer unless you have set Express's `trust
44
+ * proxy`. Behind a load balancer without it, every visitor looks like the load
45
+ * balancer - so if you are behind one, set it or pick another selector.
46
+ */
47
+ export const defaultIpSelector = selectors.defaultIpSelector;
48
+ /**
49
+ * An address from `X-Forwarded-For`.
50
+ *
51
+ * **The left-most entry is whatever the caller sent**, since proxies append, so
52
+ * this is only trustworthy when an edge you control overwrites the header. When
53
+ * you know how many proxies sit in front, count from the right instead:
54
+ * `xffIpSelector({ depth: 1 })` is the address your nearest proxy saw.
55
+ */
56
+ export const xffIpSelector = selectors.xffIpSelector;
57
+ /**
58
+ * An address from a single-value header your edge writes -
59
+ * `headerIpSelector('CF-Connecting-IP')` behind Cloudflare,
60
+ * `headerIpSelector('True-Client-IP')` behind Akamai. Falls back to `req.ip`
61
+ * when the header is absent.
62
+ */
63
+ export const headerIpSelector = selectors.headerIpSelector;
64
+ function refuse(_req, res) {
65
+ res.status(403).json({ error: 'access denied' });
66
+ }
67
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AA2BpE;;;;;;;;;;GAUG;AACH,MAAM,UAAU,YAAY,CAAC,UAAmB,EAAE;IAC9C,MAAM,IAAI,GAAG,UAAU,CAAU,OAAO,EAAE,iBAAiB,CAAC,CAAC;IAC7D,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,MAAM,CAAC;IAC9C,OAAO,CAAC,GAAY,EAAE,GAAa,EAAE,IAAkB,EAAE,EAAE;QACvD,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE;YAC/B,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;gBACvB,IAAI,EAAE,CAAC;gBACP,OAAO;YACX,CAAC;YACD,GAAG,CAAC,YAAY,GAAG,MAAM,CAAC;YAC1B,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;gBACjB,SAAS,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,CAAC,CAAC;gBAC5B,OAAO;YACX,CAAC;YACD,IAAI,EAAE,CAAC;QACX,CAAC,EAAE,IAAI,CAAC,CAAC;IACb,CAAC,CAAC;AACN,CAAC;AAED,MAAM,IAAI,GAAG,CAAC,GAAY,EAAE,EAAE,CAAC,CAAC;IAC5B,MAAM,EAAE,CAAC,IAAY,EAAE,EAAE;QACrB,MAAM,KAAK,GAAG,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC;QAC9C,OAAO,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;IACnD,CAAC;IACD,WAAW,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,EAAE;CAC5B,CAAC,CAAC;AAEH,8EAA8E;AAC9E,2EAA2E;AAC3E,0EAA0E;AAC1E,MAAM,SAAS,GAAG,aAAa,CAAU,IAAI,CAAC,CAAC;AAE/C;;;;GAIG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAwB,SAAS,CAAC,iBAAiB,CAAC;AAElF;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,aAAa,GACpB,SAAS,CAAC,aAAa,CAAC;AAE9B;;;;;GAKG;AACH,MAAM,CAAC,MAAM,gBAAgB,GACvB,SAAS,CAAC,gBAAgB,CAAC;AAEjC,SAAS,MAAM,CAAC,IAAa,EAAE,GAAa;IACxC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,eAAe,EAAE,CAAC,CAAC;AACrD,CAAC"}
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "vpndetection-express",
3
+ "version": "1.0.0",
4
+ "description": "Official Express middleware for the VPNDetection API. Detect VPNs, proxies, Tor, hosting and CDN visitors on every request.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "homepage": "https://vpndetection.io",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/vpndetection-io/sdk-nodejs-expressjs.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/vpndetection-io/sdk-nodejs-expressjs/issues"
14
+ },
15
+ "keywords": [
16
+ "express",
17
+ "express-middleware",
18
+ "middleware",
19
+ "vpn",
20
+ "vpn-detection",
21
+ "proxy-detection",
22
+ "tor",
23
+ "ip-intelligence",
24
+ "fraud-prevention",
25
+ "bot-detection",
26
+ "residential-proxy",
27
+ "hosting"
28
+ ],
29
+ "main": "./dist/index.js",
30
+ "types": "./dist/index.d.ts",
31
+ "exports": {
32
+ ".": {
33
+ "types": "./dist/index.d.ts",
34
+ "default": "./dist/index.js"
35
+ },
36
+ "./package.json": "./package.json"
37
+ },
38
+ "files": [
39
+ "dist",
40
+ "README.md",
41
+ "LICENSE"
42
+ ],
43
+ "engines": {
44
+ "node": ">=22"
45
+ },
46
+ "dependencies": {
47
+ "vpndetection": "^1.8.0"
48
+ },
49
+ "peerDependencies": {
50
+ "express": ">=4.18"
51
+ },
52
+ "devDependencies": {
53
+ "@types/express": "^5.0.0",
54
+ "@types/node": "^26.2.0",
55
+ "express": "^5.2.1",
56
+ "typescript": "^5.9.3"
57
+ },
58
+ "scripts": {
59
+ "build": "rm -rf dist && tsc",
60
+ "test": "pnpm run build && node --test test/*.test.mjs"
61
+ }
62
+ }