vpndetection-next 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 +21 -0
- package/README.md +170 -0
- package/dist/index.d.ts +104 -0
- package/dist/index.js +149 -0
- package/dist/index.js.map +1 -0
- package/package.json +62 -0
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,170 @@
|
|
|
1
|
+
# [<img src="https://s3.vpndetection.io/vpndetection-public/brand/mark.svg" alt="VPNDetection" width="24"/>](https://vpndetection.io/) VPNDetection Next.js Middleware
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/vpndetection-next)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
|
|
6
|
+
The official [Next.js](https://nextjs.org) proxy 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 forwards the answer to your app, where Server Components and Route Handlers can read it. Blocking is opt-in.
|
|
9
|
+
|
|
10
|
+
## Getting Started
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install vpndetection-next
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Requires Node.js 22 or newer and Next.js 15 or newer.
|
|
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 deployment is a single source address, so a key is what makes this usable in production rather than optional.
|
|
19
|
+
|
|
20
|
+
Next 16 renamed the file convention from `middleware.ts` to `proxy.ts`, and the export from `middleware` to `proxy`. Both work on 16 — `middleware.ts` logs a deprecation — so use whichever your version supports.
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
// proxy.ts (middleware.ts on Next 15)
|
|
24
|
+
import { createProxy } from 'vpndetection-next';
|
|
25
|
+
|
|
26
|
+
export const proxy = createProxy({ apiKey: process.env.VPNDETECTION_API_KEY });
|
|
27
|
+
|
|
28
|
+
export const config = { matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'] };
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Then read the answer anywhere downstream:
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
// app/page.tsx
|
|
35
|
+
import { headers } from 'next/headers';
|
|
36
|
+
import { readLookup } from 'vpndetection-next';
|
|
37
|
+
|
|
38
|
+
export default async function Page() {
|
|
39
|
+
const lookup = readLookup(await headers());
|
|
40
|
+
return <main>{lookup?.isVpn ? 'Hello, VPN user' : 'Hello'}</main>;
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
By default nothing is blocked. Your own code decides what the answer means — which is usually what you want, because whether a VPN visitor is a problem depends entirely on what they are doing.
|
|
45
|
+
|
|
46
|
+
**Set a `matcher`.** Without one the proxy runs on every request including static assets, which costs a lookup each.
|
|
47
|
+
|
|
48
|
+
## Blocking
|
|
49
|
+
|
|
50
|
+
Pass a `blockCondition` and a matching request is answered with `403` and never reaches your app.
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
export const proxy = createProxy({
|
|
54
|
+
apiKey: process.env.VPNDETECTION_API_KEY,
|
|
55
|
+
blockCondition: { isVpn: true },
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
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:
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
blockCondition: { isVpn: true, vpn: { provider: 'nordvpn' } } // one provider
|
|
63
|
+
blockCondition: { isResproxy: true, resproxy: { hits: { gte: 5 } } } // a numeric threshold
|
|
64
|
+
blockCondition: { vpn: { confidence: ['high', 'medium'] } } // any of these
|
|
65
|
+
blockCondition: [{ isTor: true }, { isResproxy: true }] // a list is OR
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
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 proxy is created rather than silently blocking all your traffic.
|
|
69
|
+
|
|
70
|
+
Replace the refusal with `onBlocked` — a redirect is usually friendlier than a 403:
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
onBlocked: (request) => NextResponse.redirect(new URL('/no-vpn', request.url)),
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Doing other work in the same proxy
|
|
77
|
+
|
|
78
|
+
Most apps already have a proxy. Use `createGuard` and call it from yours:
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
import { NextResponse } from 'next/server';
|
|
82
|
+
import { createGuard } from 'vpndetection-next';
|
|
83
|
+
|
|
84
|
+
const guard = createGuard({ apiKey: process.env.VPNDETECTION_API_KEY });
|
|
85
|
+
|
|
86
|
+
export async function proxy(request: NextRequest) {
|
|
87
|
+
const lookup = await guard(request);
|
|
88
|
+
if (lookup?.result?.isTor) {
|
|
89
|
+
return NextResponse.redirect(new URL('/verify', request.url));
|
|
90
|
+
}
|
|
91
|
+
return guard.forward(request, lookup);
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
`guard.forward` continues to your app carrying the answer. Return it rather than a bare `NextResponse.next()`, or nothing downstream can read the lookup.
|
|
96
|
+
|
|
97
|
+
## Where the client address comes from
|
|
98
|
+
|
|
99
|
+
This is the setting that decides whether any of the above works, and Next makes it more your problem than most frameworks do.
|
|
100
|
+
|
|
101
|
+
**Next has no socket peer to offer.** `NextRequest.ip` was removed in Next 15, and a proxy runs ahead of the server, so a forwarded header is the only address available — this is the one place where the default is a header rather than a connection.
|
|
102
|
+
|
|
103
|
+
The default reads the left-most `X-Forwarded-For` entry. That is correct when the platform in front of you **overwrites** the header, which Vercel does and any ingress you control can be configured to. Behind something that **appends**, the left-most entry is whatever the visitor sent, and detection silently does nothing.
|
|
104
|
+
|
|
105
|
+
For a platform that writes its own header, name it:
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
import { headerIpSelector } from 'vpndetection-next';
|
|
109
|
+
|
|
110
|
+
createProxy({
|
|
111
|
+
apiKey: process.env.VPNDETECTION_API_KEY,
|
|
112
|
+
ipSelector: headerIpSelector('CF-Connecting-IP'), // or True-Client-IP, or your own
|
|
113
|
+
});
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
If you know how many proxies sit in front, count from the right: `xffIpSelector({ depth: 1 })` is the address your nearest proxy saw. Anything else, pass your own function — it receives the `NextRequest` and returns an address.
|
|
117
|
+
|
|
118
|
+
If the address resolves to a private one, or to nothing at all, the proxy says so once on `console.warn`. That is expected running locally and is the signal to fix your configuration anywhere else.
|
|
119
|
+
|
|
120
|
+
## The forwarded header
|
|
121
|
+
|
|
122
|
+
The answer travels to your app on `x-vpndetection`, because a proxy runs in its own context and a request header is the only thing that crosses into it.
|
|
123
|
+
|
|
124
|
+
**Any copy the caller sent is deleted before ours is written.** That overwrite is the entire basis of trusting it downstream — without it a visitor could set the header themselves and tell your app whatever they liked about their own address. It is the same rule an ingress follows for a real-IP header, and it holds even when the request was skipped.
|
|
125
|
+
|
|
126
|
+
Turn it off with `forward: false` if you only ever block and never read the answer.
|
|
127
|
+
|
|
128
|
+
## When a lookup fails
|
|
129
|
+
|
|
130
|
+
The request is let through, and the reason is on `lookup.error`. Our outage should not become yours, so a network failure, an exhausted quota or a rejected key all fail open. Pass `failClosed: true` to block instead.
|
|
131
|
+
|
|
132
|
+
## Cost and latency
|
|
133
|
+
|
|
134
|
+
Answers are cached for an hour per proxy instance, 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.
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
createProxy({ apiKey: KEY, timeoutMs: 1000, retries: 1, cache: { max: 50000, ttlMs: 600000 } })
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
A serverless deployment gets less from the cache than a long-running one, because the instance holding it is short-lived. Narrow the `matcher`, or `skip` what you do not care about:
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
skip: (request) => request.nextUrl.pathname.startsWith('/api/health'),
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
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.
|
|
147
|
+
|
|
148
|
+
## Absent is not false
|
|
149
|
+
|
|
150
|
+
Only `ip` and `isVpn` come back on every plan. A field your plan does not include is absent, which means "not in your plan" rather than "checked, and no".
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
lookup.result?.is_hosting ?? false // when you only want the flag
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
A `blockCondition` naming a member your plan does not serve can never match, so the proxy warns once instead of failing silently. Set `onMissingField: 'throw'` to make it an error.
|
|
157
|
+
|
|
158
|
+
## Other Libraries
|
|
159
|
+
|
|
160
|
+
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.
|
|
161
|
+
|
|
162
|
+
## About VPNDetection
|
|
163
|
+
|
|
164
|
+
VPN Detection API: Accurate anonymity detection identifying VPNs, residential proxies, hosting servers, Tor nodes, CDNs, relays and more.
|
|
165
|
+
|
|
166
|
+
[<img src="https://s3.vpndetection.io/vpndetection-public/brand/mark.svg" alt="VPNDetection" width="96"/>](https://vpndetection.io/)
|
|
167
|
+
|
|
168
|
+
## License
|
|
169
|
+
|
|
170
|
+
This project is licensed under the [MIT License](LICENSE).
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { IpSelector, Lookup, MiddlewareOptions } from 'vpndetection/middleware';
|
|
2
|
+
import { NextResponse } from 'next/server.js';
|
|
3
|
+
import type { NextRequest } from 'next/server.js';
|
|
4
|
+
export type { BlockCondition, IpSelector, Lookup, NumericBound } from 'vpndetection/middleware';
|
|
5
|
+
/**
|
|
6
|
+
* The header the answer is forwarded on, so a Server Component or Route
|
|
7
|
+
* Handler can read what the proxy already looked up.
|
|
8
|
+
*/
|
|
9
|
+
export declare const LOOKUP_HEADER = "x-vpndetection";
|
|
10
|
+
/** What `readLookup` gives back. The detail objects are carried through as served. */
|
|
11
|
+
export interface ForwardedLookup {
|
|
12
|
+
ip?: string;
|
|
13
|
+
isVpn?: boolean;
|
|
14
|
+
blocked: boolean;
|
|
15
|
+
error?: string;
|
|
16
|
+
result?: Record<string, unknown>;
|
|
17
|
+
}
|
|
18
|
+
export interface Options extends MiddlewareOptions<NextRequest> {
|
|
19
|
+
/**
|
|
20
|
+
* How a blocked request is answered. Defaults to `403` with a short JSON
|
|
21
|
+
* body. Return a `NextResponse`.
|
|
22
|
+
*/
|
|
23
|
+
onBlocked?: (request: NextRequest, lookup: Lookup) => NextResponse | Promise<NextResponse>;
|
|
24
|
+
/**
|
|
25
|
+
* Forward the answer to your app on {@link LOOKUP_HEADER}, so a Server
|
|
26
|
+
* Component or Route Handler can read it with `readLookup`. Defaults to
|
|
27
|
+
* true. The proxy runs in its own context, so a header is the only way the
|
|
28
|
+
* answer reaches the rest of your app.
|
|
29
|
+
*/
|
|
30
|
+
forward?: boolean;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* A ready-made `proxy` export.
|
|
34
|
+
*
|
|
35
|
+
* ```ts
|
|
36
|
+
* // proxy.ts
|
|
37
|
+
* export const proxy = createProxy({ apiKey: process.env.VPNDETECTION_API_KEY });
|
|
38
|
+
* ```
|
|
39
|
+
*
|
|
40
|
+
* If your proxy already does other work, use {@link createGuard} instead and
|
|
41
|
+
* call it from your own function.
|
|
42
|
+
*/
|
|
43
|
+
export declare function createProxy(options?: Options): (request: NextRequest) => Promise<NextResponse>;
|
|
44
|
+
/**
|
|
45
|
+
* Classify the visitor, for a proxy that does other work too.
|
|
46
|
+
*
|
|
47
|
+
* ```ts
|
|
48
|
+
* const guard = createGuard({ apiKey: process.env.VPNDETECTION_API_KEY });
|
|
49
|
+
*
|
|
50
|
+
* export async function proxy(request: NextRequest) {
|
|
51
|
+
* const lookup = await guard(request);
|
|
52
|
+
* if (lookup?.result?.isVpn) {
|
|
53
|
+
* return NextResponse.redirect(new URL('/no-vpn', request.url));
|
|
54
|
+
* }
|
|
55
|
+
* return guard.forward(request, lookup);
|
|
56
|
+
* }
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
export declare function createGuard(options?: Options): {
|
|
60
|
+
(request: NextRequest): Promise<Lookup | undefined>;
|
|
61
|
+
forward(request: NextRequest, lookup: Lookup | undefined): NextResponse;
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Read what the proxy found, from a Server Component or Route Handler.
|
|
65
|
+
*
|
|
66
|
+
* ```ts
|
|
67
|
+
* import { headers } from 'next/headers';
|
|
68
|
+
* const lookup = readLookup(await headers());
|
|
69
|
+
* ```
|
|
70
|
+
*
|
|
71
|
+
* Answers `undefined` when the proxy did not run for this route, when `skip`
|
|
72
|
+
* claimed the request, or when `forward` was turned off.
|
|
73
|
+
*/
|
|
74
|
+
export declare function readLookup(source: Headers | {
|
|
75
|
+
get(name: string): string | null;
|
|
76
|
+
}): ForwardedLookup | undefined;
|
|
77
|
+
/**
|
|
78
|
+
* The left-most `X-Forwarded-For` entry.
|
|
79
|
+
*
|
|
80
|
+
* **Next has no socket peer to offer.** `NextRequest.ip` was removed in Next
|
|
81
|
+
* 15, and a proxy runs ahead of the server, so a forwarded header is the only
|
|
82
|
+
* address available - which makes this the one adapter whose default is a
|
|
83
|
+
* header rather than a connection.
|
|
84
|
+
*
|
|
85
|
+
* That is safe exactly when the platform in front of you OVERWRITES the header
|
|
86
|
+
* (Vercel does, and so does any ingress you control that is configured to).
|
|
87
|
+
* Behind something that APPENDS, the left-most entry is whatever the visitor
|
|
88
|
+
* sent: use `headerIpSelector` for your platform's own header, or
|
|
89
|
+
* `xffIpSelector({ depth })` to count from the right.
|
|
90
|
+
*/
|
|
91
|
+
export declare const defaultIpSelector: IpSelector<NextRequest>;
|
|
92
|
+
/**
|
|
93
|
+
* An address from `X-Forwarded-For`, left-most by default. `depth: 1` is the
|
|
94
|
+
* address your nearest proxy saw, for a known number of trusted hops.
|
|
95
|
+
*/
|
|
96
|
+
export declare const xffIpSelector: (options?: {
|
|
97
|
+
depth?: number;
|
|
98
|
+
}) => IpSelector<NextRequest>;
|
|
99
|
+
/**
|
|
100
|
+
* An address from a single-value header your platform writes -
|
|
101
|
+
* `headerIpSelector('CF-Connecting-IP')` behind Cloudflare,
|
|
102
|
+
* `headerIpSelector('True-Client-IP')` behind Akamai.
|
|
103
|
+
*/
|
|
104
|
+
export declare const headerIpSelector: (name: string) => IpSelector<NextRequest>;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { bindSelectors, createCore } from 'vpndetection/middleware';
|
|
2
|
+
import { NextResponse } from 'next/server.js';
|
|
3
|
+
/**
|
|
4
|
+
* The header the answer is forwarded on, so a Server Component or Route
|
|
5
|
+
* Handler can read what the proxy already looked up.
|
|
6
|
+
*/
|
|
7
|
+
export const LOOKUP_HEADER = 'x-vpndetection';
|
|
8
|
+
/**
|
|
9
|
+
* A ready-made `proxy` export.
|
|
10
|
+
*
|
|
11
|
+
* ```ts
|
|
12
|
+
* // proxy.ts
|
|
13
|
+
* export const proxy = createProxy({ apiKey: process.env.VPNDETECTION_API_KEY });
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* If your proxy already does other work, use {@link createGuard} instead and
|
|
17
|
+
* call it from your own function.
|
|
18
|
+
*/
|
|
19
|
+
export function createProxy(options = {}) {
|
|
20
|
+
const guard = createGuard(options);
|
|
21
|
+
const onBlocked = options.onBlocked ?? refuse;
|
|
22
|
+
return async function proxy(request) {
|
|
23
|
+
const lookup = await guard(request);
|
|
24
|
+
if (lookup?.blocked) {
|
|
25
|
+
return onBlocked(request, lookup);
|
|
26
|
+
}
|
|
27
|
+
return guard.forward(request, lookup);
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Classify the visitor, for a proxy that does other work too.
|
|
32
|
+
*
|
|
33
|
+
* ```ts
|
|
34
|
+
* const guard = createGuard({ apiKey: process.env.VPNDETECTION_API_KEY });
|
|
35
|
+
*
|
|
36
|
+
* export async function proxy(request: NextRequest) {
|
|
37
|
+
* const lookup = await guard(request);
|
|
38
|
+
* if (lookup?.result?.isVpn) {
|
|
39
|
+
* return NextResponse.redirect(new URL('/no-vpn', request.url));
|
|
40
|
+
* }
|
|
41
|
+
* return guard.forward(request, lookup);
|
|
42
|
+
* }
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export function createGuard(options = {}) {
|
|
46
|
+
const core = createCore(options, defaultIpSelector);
|
|
47
|
+
const forwarding = options.forward !== false;
|
|
48
|
+
const guard = async (request) => core.evaluate(request);
|
|
49
|
+
/**
|
|
50
|
+
* Continue to your app, carrying the answer on {@link LOOKUP_HEADER}.
|
|
51
|
+
*
|
|
52
|
+
* **Any copy the caller sent is deleted first**, which is the entire basis
|
|
53
|
+
* of trusting it downstream: without that overwrite a visitor could set the
|
|
54
|
+
* header themselves and tell your app whatever they liked about their own
|
|
55
|
+
* address.
|
|
56
|
+
*/
|
|
57
|
+
guard.forward = (request, lookup) => {
|
|
58
|
+
const headers = new Headers(request.headers);
|
|
59
|
+
headers.delete(LOOKUP_HEADER);
|
|
60
|
+
if (forwarding && lookup !== undefined) {
|
|
61
|
+
headers.set(LOOKUP_HEADER, JSON.stringify(toForwarded(lookup)));
|
|
62
|
+
}
|
|
63
|
+
return NextResponse.next({ request: { headers: headers } });
|
|
64
|
+
};
|
|
65
|
+
return guard;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Read what the proxy found, from a Server Component or Route Handler.
|
|
69
|
+
*
|
|
70
|
+
* ```ts
|
|
71
|
+
* import { headers } from 'next/headers';
|
|
72
|
+
* const lookup = readLookup(await headers());
|
|
73
|
+
* ```
|
|
74
|
+
*
|
|
75
|
+
* Answers `undefined` when the proxy did not run for this route, when `skip`
|
|
76
|
+
* claimed the request, or when `forward` was turned off.
|
|
77
|
+
*/
|
|
78
|
+
export function readLookup(source) {
|
|
79
|
+
const raw = source.get(LOOKUP_HEADER);
|
|
80
|
+
if (raw === null || raw === '') {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
return JSON.parse(raw);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const view = (request) => ({
|
|
91
|
+
header: (name) => request.headers.get(name) ?? undefined,
|
|
92
|
+
frameworkIp: () => firstForwarded(request),
|
|
93
|
+
});
|
|
94
|
+
// Each of these is annotated rather than inferred: the inferred shape reaches
|
|
95
|
+
// through `next`'s own types into its transitive ones, which TypeScript refuses
|
|
96
|
+
// to name in a declaration file a consumer would have to resolve.
|
|
97
|
+
const selectors = bindSelectors(view);
|
|
98
|
+
/**
|
|
99
|
+
* The left-most `X-Forwarded-For` entry.
|
|
100
|
+
*
|
|
101
|
+
* **Next has no socket peer to offer.** `NextRequest.ip` was removed in Next
|
|
102
|
+
* 15, and a proxy runs ahead of the server, so a forwarded header is the only
|
|
103
|
+
* address available - which makes this the one adapter whose default is a
|
|
104
|
+
* header rather than a connection.
|
|
105
|
+
*
|
|
106
|
+
* That is safe exactly when the platform in front of you OVERWRITES the header
|
|
107
|
+
* (Vercel does, and so does any ingress you control that is configured to).
|
|
108
|
+
* Behind something that APPENDS, the left-most entry is whatever the visitor
|
|
109
|
+
* sent: use `headerIpSelector` for your platform's own header, or
|
|
110
|
+
* `xffIpSelector({ depth })` to count from the right.
|
|
111
|
+
*/
|
|
112
|
+
export const defaultIpSelector = selectors.defaultIpSelector;
|
|
113
|
+
/**
|
|
114
|
+
* An address from `X-Forwarded-For`, left-most by default. `depth: 1` is the
|
|
115
|
+
* address your nearest proxy saw, for a known number of trusted hops.
|
|
116
|
+
*/
|
|
117
|
+
export const xffIpSelector = selectors.xffIpSelector;
|
|
118
|
+
/**
|
|
119
|
+
* An address from a single-value header your platform writes -
|
|
120
|
+
* `headerIpSelector('CF-Connecting-IP')` behind Cloudflare,
|
|
121
|
+
* `headerIpSelector('True-Client-IP')` behind Akamai.
|
|
122
|
+
*/
|
|
123
|
+
export const headerIpSelector = selectors.headerIpSelector;
|
|
124
|
+
function firstForwarded(request) {
|
|
125
|
+
const chain = request.headers.get('x-forwarded-for');
|
|
126
|
+
if (chain === null) {
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
const first = chain.split(',')[0]?.trim();
|
|
130
|
+
return first === '' ? undefined : first;
|
|
131
|
+
}
|
|
132
|
+
// Deliberately compact. This rides on a request header, which has a size limit
|
|
133
|
+
// every proxy in front of you enforces, so it carries the decision and the
|
|
134
|
+
// served body rather than a second copy of the idiomatic view.
|
|
135
|
+
function toForwarded(lookup) {
|
|
136
|
+
return {
|
|
137
|
+
...(lookup.ip === undefined ? {} : { ip: lookup.ip }),
|
|
138
|
+
...(lookup.result === undefined ? {} : {
|
|
139
|
+
isVpn: lookup.result.isVpn,
|
|
140
|
+
result: lookup.result.raw,
|
|
141
|
+
}),
|
|
142
|
+
...(lookup.error === undefined ? {} : { error: lookup.error.kind }),
|
|
143
|
+
blocked: lookup.blocked,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
function refuse() {
|
|
147
|
+
return NextResponse.json({ error: 'access denied' }, { status: 403 });
|
|
148
|
+
}
|
|
149
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vpndetection-next",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Official Next.js proxy/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-nextjs.git"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/vpndetection-io/sdk-nodejs-nextjs/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"nextjs",
|
|
17
|
+
"next",
|
|
18
|
+
"proxy",
|
|
19
|
+
"middleware",
|
|
20
|
+
"vpn",
|
|
21
|
+
"vpn-detection",
|
|
22
|
+
"proxy-detection",
|
|
23
|
+
"tor",
|
|
24
|
+
"ip-intelligence",
|
|
25
|
+
"fraud-prevention",
|
|
26
|
+
"bot-detection",
|
|
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
|
+
"next": ">=15"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@types/node": "^26.2.0",
|
|
54
|
+
"next": "16.3.4",
|
|
55
|
+
"react": "19.2.0",
|
|
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
|
+
}
|