@netloc8/nextjs 0.2.0 → 1.0.1
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 +187 -31
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +1 -2
- package/dist/proxy.d.mts +6 -1
- package/dist/proxy.d.mts.map +1 -1
- package/dist/proxy.mjs +1 -127
- package/dist/proxy.mjs.map +1 -1
- package/dist/server.d.mts +8 -7
- package/dist/server.d.mts.map +1 -1
- package/dist/server.mjs +1 -140
- package/dist/server.mjs.map +1 -1
- package/package.json +34 -3
package/README.md
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
# @netloc8/nextjs
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@netloc8/nextjs)
|
|
4
|
+
[](https://www.npmjs.com/package/@netloc8/nextjs)
|
|
5
|
+
[](../../LICENSE)
|
|
6
|
+
|
|
7
|
+
Add **IP geolocation** to any **Next.js** application. Detect visitor country,
|
|
8
|
+
city, timezone, and EU membership in both Server and Client Components — with
|
|
9
|
+
a server-side proxy that resolves geo data before your page renders.
|
|
10
|
+
|
|
11
|
+
Show cookie consent to EU users (GDPR), redirect by locale, gate by region (CCPA),
|
|
12
|
+
display local timezone — all with SSR support and automatic browser timezone
|
|
13
|
+
reconciliation.
|
|
7
14
|
|
|
8
15
|
## Install
|
|
9
16
|
|
|
@@ -13,80 +20,216 @@ bun add @netloc8/nextjs
|
|
|
13
20
|
|
|
14
21
|
**Peer dependencies:** `next >= 16`, `react >= 19`
|
|
15
22
|
|
|
23
|
+
[Documentation](https://netloc8.com/docs) · [API Reference](https://netloc8.com/docs/api)
|
|
24
|
+
|
|
16
25
|
## Quick Start
|
|
17
26
|
|
|
18
|
-
### 1.
|
|
27
|
+
### 1. Add geo to a Client Component (simplest)
|
|
28
|
+
|
|
29
|
+
Wrap your root layout with the Provider and use the `useGeo()` hook in any
|
|
30
|
+
client component. Get your API key from the [NetLoc8 dashboard](https://netloc8.com) — the page renders immediately and geo data loads in the background:
|
|
31
|
+
|
|
32
|
+
```tsx
|
|
33
|
+
// app/layout.tsx
|
|
34
|
+
import { NetLoc8Provider } from '@netloc8/nextjs';
|
|
35
|
+
|
|
36
|
+
export default function RootLayout({ children }) {
|
|
37
|
+
return (
|
|
38
|
+
<html>
|
|
39
|
+
<body>
|
|
40
|
+
<NetLoc8Provider apiKey={process.env.NEXT_PUBLIC_NETLOC8_API_KEY}>
|
|
41
|
+
{children}
|
|
42
|
+
</NetLoc8Provider>
|
|
43
|
+
</body>
|
|
44
|
+
</html>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
```tsx
|
|
50
|
+
// app/components/LocationBanner.tsx
|
|
51
|
+
'use client';
|
|
52
|
+
import { useGeo } from '@netloc8/nextjs';
|
|
53
|
+
|
|
54
|
+
export function LocationBanner() {
|
|
55
|
+
const { geo, isLoading, error } = useGeo();
|
|
56
|
+
|
|
57
|
+
if (isLoading) return <p>Detecting location…</p>;
|
|
58
|
+
if (error) return <p>Geo unavailable</p>;
|
|
59
|
+
|
|
60
|
+
return <p>Hello from {geo.location?.city}, {geo.location?.country?.name}</p>;
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
> **That's it.** No proxy, no server setup, no environment variables. Geo data
|
|
65
|
+
> loads asynchronously via a publishable key — faster than SSR because the
|
|
66
|
+
> browser calls the NetLoc8 edge API directly, eliminating the server hop.
|
|
67
|
+
>
|
|
68
|
+
> **Never use secret keys here.** The `apiKey` prop is included in your
|
|
69
|
+
> client bundle. Always use a publishable key (`pk_...`). Keep secret
|
|
70
|
+
> keys (`sk_...`) server-side only.
|
|
71
|
+
>
|
|
72
|
+
> **Trade-off:** On first visit, geo-dependent content appears after the API
|
|
73
|
+
> call completes. Use the `loading` prop on the Provider and `<GeoGate>` to
|
|
74
|
+
> show skeleton UI while data loads. On repeat visits, the `__netloc8` cookie
|
|
75
|
+
> provides geo data synchronously — no content shift.
|
|
19
76
|
|
|
20
|
-
|
|
77
|
+
### 2. Server-side rendering (SSR proxy)
|
|
78
|
+
|
|
79
|
+
For apps that need geo data available on first render (SEO, personalized SSR),
|
|
80
|
+
set up the proxy and use `getGeo()` in Server Components:
|
|
21
81
|
|
|
22
82
|
```typescript
|
|
83
|
+
// proxy.ts
|
|
23
84
|
import { createProxy } from '@netloc8/nextjs/proxy';
|
|
24
85
|
|
|
25
86
|
export default createProxy();
|
|
26
87
|
```
|
|
27
88
|
|
|
28
|
-
### 2. Read geo data in Server Components
|
|
29
|
-
|
|
30
89
|
```typescript
|
|
90
|
+
// app/page.tsx
|
|
31
91
|
import { getGeo } from '@netloc8/nextjs/server';
|
|
32
92
|
|
|
33
93
|
export default async function Page() {
|
|
34
94
|
const geo = await getGeo();
|
|
35
|
-
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<div>
|
|
98
|
+
<p>Hello from {geo.location?.city}, {geo.location?.country?.name}</p>
|
|
99
|
+
<p>Timezone: {geo.location?.timezone}</p>
|
|
100
|
+
<p>{geo.location?.country?.flag}</p>
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
36
103
|
}
|
|
37
104
|
```
|
|
38
105
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
Wrap your layout with `<NetLoc8Provider>`:
|
|
106
|
+
To make SSR geo data available in client components, pass it to the Provider:
|
|
42
107
|
|
|
43
108
|
```tsx
|
|
44
109
|
import { getGeo } from '@netloc8/nextjs/server';
|
|
45
110
|
import { NetLoc8Provider } from '@netloc8/nextjs';
|
|
46
111
|
|
|
47
|
-
export default async function RootLayout(
|
|
112
|
+
export default async function RootLayout({ children }) {
|
|
48
113
|
const geo = await getGeo();
|
|
49
114
|
|
|
50
115
|
return (
|
|
51
|
-
<
|
|
52
|
-
|
|
53
|
-
|
|
116
|
+
<html>
|
|
117
|
+
<body>
|
|
118
|
+
<NetLoc8Provider geo={geo}>
|
|
119
|
+
{children}
|
|
120
|
+
</NetLoc8Provider>
|
|
121
|
+
</body>
|
|
122
|
+
</html>
|
|
54
123
|
);
|
|
55
124
|
}
|
|
56
125
|
```
|
|
57
126
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
```tsx
|
|
61
|
-
'use client';
|
|
62
|
-
import { useGeo } from '@netloc8/nextjs';
|
|
63
|
-
|
|
64
|
-
export function LocationBanner() {
|
|
65
|
-
const geo = useGeo();
|
|
66
|
-
return <p>Timezone: {geo.timezone}</p>;
|
|
67
|
-
}
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
### 4. Conditional rendering with GeoGate
|
|
127
|
+
### 3. Conditional rendering with GeoGate
|
|
71
128
|
|
|
72
129
|
```tsx
|
|
73
130
|
import { GeoGate } from '@netloc8/nextjs';
|
|
74
131
|
|
|
75
|
-
|
|
132
|
+
{/* EU users — show cookie consent */}
|
|
133
|
+
<GeoGate eu={true} loading={<Skeleton />}>
|
|
76
134
|
<CookieConsentBanner />
|
|
77
135
|
</GeoGate>
|
|
78
136
|
|
|
137
|
+
{/* Specific countries with fallback */}
|
|
79
138
|
<GeoGate country={['US', 'CA']} fallback={<p>Not available in your region</p>}>
|
|
80
139
|
<SpecialOffer />
|
|
81
140
|
</GeoGate>
|
|
82
141
|
```
|
|
83
142
|
|
|
143
|
+
### 4. Geo-based redirects
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
import { createProxy, withGeoRedirect } from '@netloc8/nextjs/proxy';
|
|
147
|
+
|
|
148
|
+
export default createProxy({
|
|
149
|
+
handler: withGeoRedirect({
|
|
150
|
+
defaultLocale: 'en',
|
|
151
|
+
localeMap: {
|
|
152
|
+
'DE': 'de',
|
|
153
|
+
'FR': 'fr',
|
|
154
|
+
'ES': 'es',
|
|
155
|
+
},
|
|
156
|
+
excludePaths: ['/api', '/_next'],
|
|
157
|
+
}),
|
|
158
|
+
});
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Preventing Content Shift
|
|
162
|
+
|
|
163
|
+
In direct mode, geo data loads asynchronously on first visit. Use these
|
|
164
|
+
patterns to prevent layout shift:
|
|
165
|
+
|
|
166
|
+
```tsx
|
|
167
|
+
{/* GeoGate loading prop — recommended for conditional sections */}
|
|
168
|
+
<GeoGate eu={true} loading={<div className="h-12 animate-pulse bg-gray-200 rounded" />}>
|
|
169
|
+
<CookieConsentBanner />
|
|
170
|
+
</GeoGate>
|
|
171
|
+
|
|
172
|
+
{/* useGeo isLoading — for inline geo content */}
|
|
173
|
+
function CityName() {
|
|
174
|
+
const { geo, isLoading } = useGeo();
|
|
175
|
+
if (isLoading) return <span className="h-4 w-24 animate-pulse bg-gray-200 rounded inline-block" />;
|
|
176
|
+
return <span>{geo.location?.city}</span>;
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
> **On repeat visits:** The `__netloc8` cookie provides geo data
|
|
181
|
+
> synchronously — `isLoading` starts as `false`, no skeleton flash.
|
|
182
|
+
|
|
183
|
+
## The `useGeo()` Hook
|
|
184
|
+
|
|
185
|
+
In client components, `useGeo()` returns `{ geo, isLoading, error }`:
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
const { geo, isLoading, error } = useGeo();
|
|
189
|
+
|
|
190
|
+
if (isLoading) return <Skeleton />;
|
|
191
|
+
if (error) return <p>Geo unavailable</p>;
|
|
192
|
+
|
|
193
|
+
geo.query?.value; // "203.0.113.42"
|
|
194
|
+
geo.location?.country?.code; // "US"
|
|
195
|
+
geo.location?.country?.name; // "United States"
|
|
196
|
+
geo.location?.country?.flag; // "🇺🇸"
|
|
197
|
+
geo.location?.country?.unions; // ["EU"] or []
|
|
198
|
+
geo.location?.region?.code; // "CA"
|
|
199
|
+
geo.location?.region?.name; // "California"
|
|
200
|
+
geo.location?.city; // "Mountain View"
|
|
201
|
+
geo.location?.timezone; // "America/Los_Angeles"
|
|
202
|
+
geo.location?.utcOffset; // "-07:00"
|
|
203
|
+
geo.location?.geoConfidence; // 1.0
|
|
204
|
+
geo.network?.asn; // "AS15169"
|
|
205
|
+
geo.network?.organization; // "Google LLC"
|
|
206
|
+
geo.meta?.precision; // "city"
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### EU Detection
|
|
210
|
+
|
|
211
|
+
```typescript
|
|
212
|
+
import { isEU } from '@netloc8/nextjs';
|
|
213
|
+
|
|
214
|
+
if (isEU(geo)) {
|
|
215
|
+
showCookieConsent();
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
See [`@netloc8/core`](../core/) for the full `Geo` type reference.
|
|
220
|
+
|
|
221
|
+
## RUM Telemetry
|
|
222
|
+
|
|
223
|
+
RUM is **enabled by default** when using the Provider. Core Web Vitals, Navigation
|
|
224
|
+
Timing, and JS errors are beaconed via `navigator.sendBeacon()` on page hide. Zero
|
|
225
|
+
latency impact, no PII. Opt out with `rum={false}` on the Provider.
|
|
226
|
+
|
|
84
227
|
## Environment Variables
|
|
85
228
|
|
|
86
229
|
| Variable | Required | Description |
|
|
87
230
|
|----------|----------|-------------|
|
|
88
231
|
| `NETLOC8_API_KEY` | Yes | Secret API key (`sk_...`) for the proxy |
|
|
89
|
-
| `NETLOC8_API_URL` | No | API base URL (defaults to `https://netloc8.com`) |
|
|
232
|
+
| `NETLOC8_API_URL` | No | API base URL (defaults to `https://api.netloc8.com`) |
|
|
90
233
|
| `NETLOC8_TEST_IP` | No | Override IP in development |
|
|
91
234
|
|
|
92
235
|
## Exports
|
|
@@ -101,6 +244,19 @@ import { GeoGate } from '@netloc8/nextjs';
|
|
|
101
244
|
| `@netloc8/nextjs` | `useGeo` | Re-exported from `@netloc8/react` |
|
|
102
245
|
| `@netloc8/nextjs` | `GeoGate` | Re-exported from `@netloc8/react` |
|
|
103
246
|
| `@netloc8/nextjs` | `GeoContext` | Re-exported from `@netloc8/react` |
|
|
247
|
+
| `@netloc8/nextjs` | `GeoContextValue` | Type: `{ geo, isLoading, error }` |
|
|
248
|
+
|
|
249
|
+
## How the Proxy Works
|
|
250
|
+
|
|
251
|
+
The proxy runs on every incoming request:
|
|
252
|
+
|
|
253
|
+
1. **IP detection** — reads `cf-connecting-ip`, `x-forwarded-for`, or `x-real-ip`
|
|
254
|
+
2. **Cookie check** — if `__netloc8` cookie exists and IP hasn't changed, uses cached data
|
|
255
|
+
3. **Platform headers** — reads Vercel/Cloudflare/CloudFront geo headers when available
|
|
256
|
+
4. **API call** — calls `api.netloc8.com/v1/ip/{ip}` with your secret key if platform lacks country data
|
|
257
|
+
5. **Reconciliation** — deep-merges all sources (cookie → platform → API) with API as highest priority
|
|
258
|
+
6. **Header transport** — sets 25 `x-netloc8-*` headers for downstream Server Components
|
|
259
|
+
7. **Cookie write** — persists geo data in `__netloc8` cookie for subsequent requests
|
|
104
260
|
|
|
105
261
|
## License
|
|
106
262
|
|
package/dist/index.d.mts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
|
|
2
|
-
import { GeoContext, GeoGate, GeoGateProps, NetLoc8Provider, useGeo } from "@netloc8/react";
|
|
3
|
-
export { GeoContext, GeoGate, type GeoGateProps, NetLoc8Provider, useGeo };
|
|
2
|
+
import { GeoContext, GeoContextValue, GeoGate, GeoGateProps, NetLoc8Provider, NetLoc8ProviderProps, useGeo } from "@netloc8/react";
|
|
3
|
+
export { GeoContext, type GeoContextValue, GeoGate, type GeoGateProps, NetLoc8Provider, type NetLoc8ProviderProps, useGeo };
|
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,2 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import
|
|
3
|
-
export { GeoContext, GeoGate, NetLoc8Provider, useGeo };
|
|
2
|
+
import{GeoContext as e,GeoGate as t,NetLoc8Provider as n,useGeo as r}from"@netloc8/react";export{e as GeoContext,t as GeoGate,n as NetLoc8Provider,r as useGeo};
|
package/dist/proxy.d.mts
CHANGED
|
@@ -15,6 +15,11 @@ interface GeoRedirectOptions {
|
|
|
15
15
|
excludePaths?: string[];
|
|
16
16
|
}
|
|
17
17
|
/**
|
|
18
|
+
* Read x-netloc8-* request headers back into a Geo object.
|
|
19
|
+
* Used by server.ts to reconstruct Geo on the server side.
|
|
20
|
+
*/
|
|
21
|
+
declare function readGeoHeaders(headers: Headers): Geo;
|
|
22
|
+
/**
|
|
18
23
|
* Create a Next.js 16 proxy function that resolves geolocation for every
|
|
19
24
|
* matching request.
|
|
20
25
|
*
|
|
@@ -27,5 +32,5 @@ declare function createProxy(options?: CreateProxyOptions): (request: NextReques
|
|
|
27
32
|
*/
|
|
28
33
|
declare function withGeoRedirect(options: GeoRedirectOptions): (request: NextRequest, geo: Geo) => NextResponse | undefined;
|
|
29
34
|
//#endregion
|
|
30
|
-
export { createProxy, withGeoRedirect };
|
|
35
|
+
export { createProxy, readGeoHeaders, withGeoRedirect };
|
|
31
36
|
//# sourceMappingURL=proxy.d.mts.map
|
package/dist/proxy.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"proxy.d.mts","names":[],"sources":["../src/proxy.ts"],"mappings":";;;;UAmBU,kBAAA;EACN,OAAA;EACA,MAAA;EACA,MAAA;EACA,MAAA;EACA,OAAA,IACI,OAAA,EAAS,WAAA,EACT,GAAA,EAAK,GAAA,KACJ,YAAA,eAA2B,OAAA,CAAQ,YAAA;AAAA;AAAA,UAGlC,kBAAA;EACN,aAAA;EACA,SAAA,EAAW,MAAA;EACX,YAAA;AAAA;;;;;;;;
|
|
1
|
+
{"version":3,"file":"proxy.d.mts","names":[],"sources":["../src/proxy.ts"],"mappings":";;;;UAmBU,kBAAA;EACN,OAAA;EACA,MAAA;EACA,MAAA;EACA,MAAA;EACA,OAAA,IACI,OAAA,EAAS,WAAA,EACT,GAAA,EAAK,GAAA,KACJ,YAAA,eAA2B,OAAA,CAAQ,YAAA;AAAA;AAAA,UAGlC,kBAAA;EACN,aAAA;EACA,SAAA,EAAW,MAAA;EACX,YAAA;AAAA;;;;;iBAiPY,cAAA,CAAe,OAAA,EAAS,OAAA,GAAU,GAAA;;;;;;;;iBA2BlC,WAAA,CAAY,OAAA,GAAU,kBAAA,IACjC,OAAA,EAAS,WAAA,KAAgB,OAAA,CAAQ,YAAA;AAnRM;;;AAAA,iBAkY5B,eAAA,CACZ,OAAA,EAAS,kBAAA,IACT,OAAA,EAAS,WAAA,EAAa,GAAA,EAAK,GAAA,KAAQ,YAAA"}
|
package/dist/proxy.mjs
CHANGED
|
@@ -1,128 +1,2 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { COOKIE_NAME, COOKIE_OPTIONS, fetchGeo, getClientIp, getGeoFromPlatformHeaders, isPublicIp, normalizeApiResponse, parseCookie, reconcileGeo, serializeCookie } from "@netloc8/core";
|
|
3
|
-
//#region src/proxy.ts
|
|
4
|
-
/**
|
|
5
|
-
* Header-to-Geo field mapping for setting request headers.
|
|
6
|
-
*/
|
|
7
|
-
const GEO_HEADER_MAP = [
|
|
8
|
-
["ip", "x-netloc8-ip"],
|
|
9
|
-
["ipVersion", "x-netloc8-ip-version"],
|
|
10
|
-
["continent", "x-netloc8-continent"],
|
|
11
|
-
["continentName", "x-netloc8-continent-name"],
|
|
12
|
-
["country", "x-netloc8-country"],
|
|
13
|
-
["countryName", "x-netloc8-country-name"],
|
|
14
|
-
["isEU", "x-netloc8-is-eu"],
|
|
15
|
-
["region", "x-netloc8-region"],
|
|
16
|
-
["regionName", "x-netloc8-region-name"],
|
|
17
|
-
["city", "x-netloc8-city"],
|
|
18
|
-
["postalCode", "x-netloc8-postal-code"],
|
|
19
|
-
["latitude", "x-netloc8-latitude"],
|
|
20
|
-
["longitude", "x-netloc8-longitude"],
|
|
21
|
-
["timezone", "x-netloc8-timezone"],
|
|
22
|
-
["accuracyRadius", "x-netloc8-accuracy-radius"],
|
|
23
|
-
["precision", "x-netloc8-precision"],
|
|
24
|
-
["isLimited", "x-netloc8-is-limited"],
|
|
25
|
-
["limitReason", "x-netloc8-limit-reason"],
|
|
26
|
-
["timezoneFromClient", "x-netloc8-timezone-from-client"]
|
|
27
|
-
];
|
|
28
|
-
/**
|
|
29
|
-
* Set x-netloc8-* request headers from a Geo object.
|
|
30
|
-
*/
|
|
31
|
-
function setGeoHeaders(requestHeaders, geo) {
|
|
32
|
-
for (const [field, header] of GEO_HEADER_MAP) {
|
|
33
|
-
const value = geo[field];
|
|
34
|
-
if (value !== void 0 && value !== null) requestHeaders.set(header, encodeURIComponent(String(value)));
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
/**
|
|
38
|
-
* Create a Next.js 16 proxy function that resolves geolocation for every
|
|
39
|
-
* matching request.
|
|
40
|
-
*
|
|
41
|
-
* Returns a standard proxy function that can be exported directly from the
|
|
42
|
-
* user's proxy.ts / proxy.js file, or composed with other proxy logic.
|
|
43
|
-
*/
|
|
44
|
-
function createProxy(options) {
|
|
45
|
-
return async (request) => {
|
|
46
|
-
const apiKey = options?.apiKey ?? process.env.NETLOC8_API_KEY;
|
|
47
|
-
const apiUrl = options?.apiUrl ?? process.env.NETLOC8_API_URL;
|
|
48
|
-
const timeout = options?.timeout ?? 1500;
|
|
49
|
-
const requestHeaders = new Headers(request.headers);
|
|
50
|
-
for (const [, headerName] of GEO_HEADER_MAP) requestHeaders.delete(headerName);
|
|
51
|
-
let clientIp;
|
|
52
|
-
if (process.env.NODE_ENV !== "production") clientIp = options?.testIp ?? process.env.NETLOC8_TEST_IP;
|
|
53
|
-
if (!clientIp) clientIp = getClientIp(request.headers);
|
|
54
|
-
const cookieValue = request.cookies.get(COOKIE_NAME)?.value;
|
|
55
|
-
const cookieGeo = parseCookie(cookieValue);
|
|
56
|
-
const cookieTimezone = cookieGeo.timezoneFromClient === true && cookieGeo.ip === clientIp ? {
|
|
57
|
-
timezone: cookieGeo.timezone,
|
|
58
|
-
timezoneFromClient: cookieGeo.timezoneFromClient
|
|
59
|
-
} : void 0;
|
|
60
|
-
const platformGeo = getGeoFromPlatformHeaders(request.headers);
|
|
61
|
-
let apiGeo;
|
|
62
|
-
if (clientIp && isPublicIp(clientIp) && !platformGeo.timezone && !cookieTimezone) {
|
|
63
|
-
const raw = await fetchGeo(clientIp, {
|
|
64
|
-
apiKey,
|
|
65
|
-
apiUrl,
|
|
66
|
-
timeout,
|
|
67
|
-
clientId: `@netloc8/nextjs/0.2.0`
|
|
68
|
-
});
|
|
69
|
-
if (raw) apiGeo = normalizeApiResponse(raw, clientIp);
|
|
70
|
-
}
|
|
71
|
-
const geo = reconcileGeo({
|
|
72
|
-
cookie: cookieGeo.ip ? cookieGeo : void 0,
|
|
73
|
-
platform: platformGeo,
|
|
74
|
-
api: apiGeo,
|
|
75
|
-
ip: clientIp
|
|
76
|
-
});
|
|
77
|
-
if (cookieTimezone) {
|
|
78
|
-
geo.timezone = cookieTimezone.timezone;
|
|
79
|
-
geo.timezoneFromClient = cookieTimezone.timezoneFromClient;
|
|
80
|
-
}
|
|
81
|
-
setGeoHeaders(requestHeaders, geo);
|
|
82
|
-
let handlerResponse;
|
|
83
|
-
if (options?.handler) {
|
|
84
|
-
const sanitizedRequest = new Request(request.nextUrl.toString(), {
|
|
85
|
-
method: request.method ?? "GET",
|
|
86
|
-
headers: requestHeaders,
|
|
87
|
-
body: request.body,
|
|
88
|
-
duplex: "half"
|
|
89
|
-
});
|
|
90
|
-
handlerResponse = await options.handler(Object.assign(sanitizedRequest, {
|
|
91
|
-
nextUrl: request.nextUrl,
|
|
92
|
-
cookies: request.cookies
|
|
93
|
-
}), geo);
|
|
94
|
-
}
|
|
95
|
-
const response = handlerResponse ?? NextResponse.next({ request: { headers: requestHeaders } });
|
|
96
|
-
if (!cookieValue || cookieGeo.ip !== clientIp) response.cookies.set(COOKIE_NAME, serializeCookie(geo), {
|
|
97
|
-
path: COOKIE_OPTIONS.path,
|
|
98
|
-
httpOnly: COOKIE_OPTIONS.httpOnly,
|
|
99
|
-
secure: COOKIE_OPTIONS.secure,
|
|
100
|
-
sameSite: COOKIE_OPTIONS.sameSite,
|
|
101
|
-
maxAge: COOKIE_OPTIONS.maxAge
|
|
102
|
-
});
|
|
103
|
-
return response;
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
/**
|
|
107
|
-
* Create a geo-redirect handler for use with createProxy.
|
|
108
|
-
*/
|
|
109
|
-
function withGeoRedirect(options) {
|
|
110
|
-
const { defaultLocale, localeMap, excludePaths = [] } = options;
|
|
111
|
-
const validLocales = new Set(Object.values(localeMap));
|
|
112
|
-
validLocales.add(defaultLocale);
|
|
113
|
-
return (request, geo) => {
|
|
114
|
-
const pathname = request.nextUrl.pathname;
|
|
115
|
-
for (const prefix of excludePaths) if (pathname.startsWith(prefix)) return;
|
|
116
|
-
const currentPrefix = pathname.split("/").filter(Boolean)[0];
|
|
117
|
-
if (currentPrefix && validLocales.has(currentPrefix)) return;
|
|
118
|
-
const locale = geo.country && localeMap[geo.country] || defaultLocale;
|
|
119
|
-
if (locale === defaultLocale) return;
|
|
120
|
-
const url = request.nextUrl.clone();
|
|
121
|
-
url.pathname = `/${locale}${pathname}`;
|
|
122
|
-
return NextResponse.redirect(url, 307);
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
//#endregion
|
|
126
|
-
export { createProxy, withGeoRedirect };
|
|
127
|
-
|
|
1
|
+
import{NextResponse as e}from"next/server";import{COOKIE_NAME as t,COOKIE_OPTIONS as n,fetchGeo as r,getClientIp as i,getGeoFromPlatformHeaders as a,isPublicIp as o,normalizeApiResponse as s,parseCookie as c,reconcileGeo as l,serializeCookie as u}from"@netloc8/core";const d=[{header:`x-netloc8-ip`,get:e=>e.query?.value,set:(e,t)=>{e.query||={},e.query.value=t},type:`string`},{header:`x-netloc8-ip-version`,get:e=>e.query?.ipVersion,set:(e,t)=>{let n=parseFloat(t);Number.isFinite(n)&&(e.query||={},e.query.ipVersion=n)},type:`number`},{header:`x-netloc8-continent-code`,get:e=>e.location?.continent?.code,set:(e,t)=>{e.location||={},e.location.continent||(e.location.continent={}),e.location.continent.code=t},type:`string`},{header:`x-netloc8-continent-name`,get:e=>e.location?.continent?.name,set:(e,t)=>{e.location||={},e.location.continent||(e.location.continent={}),e.location.continent.name=t},type:`string`},{header:`x-netloc8-country-code`,get:e=>e.location?.country?.code,set:(e,t)=>{e.location||={},e.location.country||(e.location.country={}),e.location.country.code=t},type:`string`},{header:`x-netloc8-country-name`,get:e=>e.location?.country?.name,set:(e,t)=>{e.location||={},e.location.country||(e.location.country={}),e.location.country.name=t},type:`string`},{header:`x-netloc8-country-flag`,get:e=>e.location?.country?.flag,set:(e,t)=>{e.location||={},e.location.country||(e.location.country={}),e.location.country.flag=t},type:`string`},{header:`x-netloc8-country-unions`,get:e=>e.location?.country?.unions,set:(e,t)=>{e.location||={},e.location.country||(e.location.country={});try{let n=JSON.parse(t);Array.isArray(n)&&(e.location.country.unions=n)}catch{}},type:`json`},{header:`x-netloc8-region-code`,get:e=>e.location?.region?.code,set:(e,t)=>{e.location||={},e.location.region||(e.location.region={}),e.location.region.code=t},type:`string`},{header:`x-netloc8-region-name`,get:e=>e.location?.region?.name,set:(e,t)=>{e.location||={},e.location.region||(e.location.region={}),e.location.region.name=t},type:`string`},{header:`x-netloc8-district`,get:e=>e.location?.district,set:(e,t)=>{e.location||={},e.location.district=t},type:`string`},{header:`x-netloc8-city`,get:e=>e.location?.city,set:(e,t)=>{e.location||={},e.location.city=t},type:`string`},{header:`x-netloc8-postal-code`,get:e=>e.location?.postalCode,set:(e,t)=>{e.location||={},e.location.postalCode=t},type:`string`},{header:`x-netloc8-latitude`,get:e=>e.location?.coordinates?.latitude,set:(e,t)=>{e.location||={},e.location.coordinates||(e.location.coordinates={});let n=parseFloat(t);Number.isFinite(n)&&(e.location.coordinates.latitude=n)},type:`number`},{header:`x-netloc8-longitude`,get:e=>e.location?.coordinates?.longitude,set:(e,t)=>{e.location||={},e.location.coordinates||(e.location.coordinates={});let n=parseFloat(t);Number.isFinite(n)&&(e.location.coordinates.longitude=n)},type:`number`},{header:`x-netloc8-accuracy-radius`,get:e=>e.location?.coordinates?.accuracyRadius,set:(e,t)=>{e.location||={},e.location.coordinates||(e.location.coordinates={});let n=parseFloat(t);Number.isFinite(n)&&(e.location.coordinates.accuracyRadius=n)},type:`number`},{header:`x-netloc8-timezone`,get:e=>e.location?.timezone,set:(e,t)=>{e.location||={},e.location.timezone=t},type:`string`},{header:`x-netloc8-utc-offset`,get:e=>e.location?.utcOffset,set:(e,t)=>{e.location||={},e.location.utcOffset=t},type:`string`},{header:`x-netloc8-geo-confidence`,get:e=>e.location?.geoConfidence,set:(e,t)=>{let n=parseFloat(t);Number.isFinite(n)&&(e.location||={},e.location.geoConfidence=n)},type:`number`},{header:`x-netloc8-asn`,get:e=>e.network?.asn,set:(e,t)=>{e.network||={},e.network.asn=t},type:`string`},{header:`x-netloc8-asn-org`,get:e=>e.network?.organization,set:(e,t)=>{e.network||={},e.network.organization=t},type:`string`},{header:`x-netloc8-asn-domain`,get:e=>e.network?.domain,set:(e,t)=>{e.network||={},e.network.domain=t},type:`string`},{header:`x-netloc8-precision`,get:e=>e.meta?.precision,set:(e,t)=>{e.meta||={},e.meta.precision=t},type:`string`},{header:`x-netloc8-degraded`,get:e=>e.meta?.degraded,set:(e,t)=>{e.meta||={},e.meta.degraded=t===`true`},type:`boolean`},{header:`x-netloc8-timezone-from-client`,get:e=>e.location?.timezoneFromClient,set:(e,t)=>{e.location||={},e.location.timezoneFromClient=t===`true`},type:`boolean`}];function f(e,t){for(let n of d){let r=n.get(t);r!=null&&(n.type===`json`?e.set(n.header,encodeURIComponent(JSON.stringify(r))):e.set(n.header,encodeURIComponent(String(r))))}}function p(e){let t={};for(let n of d){let r=e.get(n.header);if(r!==null)try{let e=decodeURIComponent(r);n.set(t,e)}catch{}}return t}function m(p){return async m=>{let h=p?.apiKey??process.env.NETLOC8_API_KEY,g=p?.apiUrl??process.env.NETLOC8_API_URL,_=p?.timeout??1500,v=new Headers(m.headers);for(let e of d)v.delete(e.header);let y;process.env.NODE_ENV!==`production`&&(y=p?.testIp??process.env.NETLOC8_TEST_IP),y||=i(m.headers);let b=m.cookies.get(t)?.value,x=c(b),S=x.location?.timezoneFromClient===!0&&x.query?.value===y?{timezone:x.location.timezone,timezoneFromClient:x.location.timezoneFromClient}:void 0,C=a(m.headers),w;if(y&&o(y)&&!C.location?.country?.code&&!S){let e=await r(y,{apiKey:h,apiUrl:g,timeout:_,clientId:`@netloc8/nextjs/1.0.1`});e&&(w=s(e,y))}let T=l({cookie:x.query?.value?x:void 0,platform:C,api:w,ip:y});S&&(T.location||={},T.location.timezone=S.timezone,T.location.timezoneFromClient=S.timezoneFromClient),f(v,T);let E;if(p?.handler){let e=new Request(m.nextUrl.toString(),{method:m.method??`GET`,headers:v,body:m.body,duplex:`half`});E=await p.handler(Object.assign(e,{nextUrl:m.nextUrl,cookies:m.cookies}),T)}let D=E??e.next({request:{headers:v}});return(!b||x.query?.value!==y)&&D.cookies.set(t,u(T),{path:n.path,httpOnly:n.httpOnly,secure:n.secure,sameSite:n.sameSite,maxAge:n.maxAge}),D}}function h(t){let{defaultLocale:n,localeMap:r,excludePaths:i=[]}=t,a=new Set(Object.values(r));return a.add(n),(t,o)=>{let s=t.nextUrl.pathname;for(let e of i)if(s.startsWith(e))return;let c=s.split(`/`).filter(Boolean)[0];if(c&&a.has(c))return;let l=o.location?.country?.code,u=l&&r[l]||n;if(u===n)return;let d=t.nextUrl.clone();return d.pathname=`/${u}${s}`,e.redirect(d,307)}}export{m as createProxy,p as readGeoHeaders,h as withGeoRedirect};
|
|
128
2
|
//# sourceMappingURL=proxy.mjs.map
|
package/dist/proxy.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"proxy.mjs","names":[],"sources":["../src/proxy.ts"],"sourcesContent":["declare const __PKG_NAME__: string;\ndeclare const __PKG_VERSION__: string;\n\nimport type { Geo } from '@netloc8/core';\nimport type { NextRequest } from 'next/server';\nimport { NextResponse } from 'next/server';\nimport {\n getClientIp,\n isPublicIp,\n getGeoFromPlatformHeaders,\n fetchGeo,\n normalizeApiResponse,\n parseCookie,\n serializeCookie,\n reconcileGeo,\n COOKIE_NAME,\n COOKIE_OPTIONS,\n} from '@netloc8/core';\n\ninterface CreateProxyOptions {\n timeout?: number;\n apiKey?: string;\n apiUrl?: string;\n testIp?: string;\n handler?: (\n request: NextRequest,\n geo: Geo\n ) => NextResponse | undefined | Promise<NextResponse | undefined>;\n}\n\ninterface GeoRedirectOptions {\n defaultLocale: string;\n localeMap: Record<string, string>;\n excludePaths?: string[];\n}\n\n/**\n * Header-to-Geo field mapping for setting request headers.\n */\nconst GEO_HEADER_MAP: Array<[keyof Geo, string]> = [\n ['ip', 'x-netloc8-ip'],\n ['ipVersion', 'x-netloc8-ip-version'],\n ['continent', 'x-netloc8-continent'],\n ['continentName', 'x-netloc8-continent-name'],\n ['country', 'x-netloc8-country'],\n ['countryName', 'x-netloc8-country-name'],\n ['isEU', 'x-netloc8-is-eu'],\n ['region', 'x-netloc8-region'],\n ['regionName', 'x-netloc8-region-name'],\n ['city', 'x-netloc8-city'],\n ['postalCode', 'x-netloc8-postal-code'],\n ['latitude', 'x-netloc8-latitude'],\n ['longitude', 'x-netloc8-longitude'],\n ['timezone', 'x-netloc8-timezone'],\n ['accuracyRadius', 'x-netloc8-accuracy-radius'],\n ['precision', 'x-netloc8-precision'],\n ['isLimited', 'x-netloc8-is-limited'],\n ['limitReason', 'x-netloc8-limit-reason'],\n ['timezoneFromClient', 'x-netloc8-timezone-from-client'],\n];\n\n/**\n * Set x-netloc8-* request headers from a Geo object.\n */\nfunction setGeoHeaders(requestHeaders: Headers, geo: Geo): void {\n for (const [field, header] of GEO_HEADER_MAP) {\n const value = geo[field];\n if (value !== undefined && value !== null) {\n requestHeaders.set(header, encodeURIComponent(String(value)));\n }\n }\n}\n\n/**\n * Create a Next.js 16 proxy function that resolves geolocation for every\n * matching request.\n *\n * Returns a standard proxy function that can be exported directly from the\n * user's proxy.ts / proxy.js file, or composed with other proxy logic.\n */\nexport function createProxy(options?: CreateProxyOptions):\n (request: NextRequest) => Promise<NextResponse> {\n\n return async (request: NextRequest): Promise<NextResponse> => {\n const apiKey = options?.apiKey ?? process.env.NETLOC8_API_KEY;\n const apiUrl = options?.apiUrl ?? process.env.NETLOC8_API_URL;\n const timeout = options?.timeout ?? 1500;\n\n // Security: Remove any incoming spoofed headers\n const requestHeaders = new Headers(request.headers);\n for (const [, headerName] of GEO_HEADER_MAP) {\n requestHeaders.delete(headerName);\n }\n\n // 1. Determine client IP\n let clientIp: string | undefined;\n\n if (process.env.NODE_ENV !== 'production') {\n clientIp = options?.testIp ?? process.env.NETLOC8_TEST_IP;\n }\n\n if (!clientIp) {\n clientIp = getClientIp(request.headers);\n }\n\n // 2. Check the cookie cache (fast path)\n const cookieValue = request.cookies.get(COOKIE_NAME)?.value;\n const cookieGeo = parseCookie(cookieValue);\n\n // Cookie fast path: only trust timezone/timezoneFromClient from the\n // client-controlled cookie. Re-resolve other geo fields to prevent\n // spoofing of country/region/city via cookie manipulation.\n const cookieTimezone = (\n cookieGeo.timezoneFromClient === true &&\n cookieGeo.ip === clientIp\n ) ? { timezone: cookieGeo.timezone, timezoneFromClient: cookieGeo.timezoneFromClient } : undefined;\n\n // 3. Extract platform headers (zero-cost)\n const platformGeo = getGeoFromPlatformHeaders(request.headers);\n\n // 4. Decide whether to call the API\n let apiGeo: Geo | undefined;\n\n if (clientIp && isPublicIp(clientIp) && !platformGeo.timezone && !cookieTimezone) {\n const raw = await fetchGeo(clientIp, { apiKey, apiUrl, timeout, clientId: typeof __PKG_NAME__ !== 'undefined' ? `${__PKG_NAME__}/${__PKG_VERSION__}` : undefined });\n if (raw) {\n apiGeo = normalizeApiResponse(raw, clientIp);\n }\n }\n\n // 5. Reconcile all sources — cookie is lowest priority in\n // reconcileGeo, so platform headers and API data overwrite it.\n // Pass the full cookie so self-hosted deployments (no platform\n // headers, API call skipped) still have city/country/region.\n const geo = reconcileGeo({\n cookie: cookieGeo.ip ? cookieGeo : undefined,\n platform: platformGeo,\n api: apiGeo,\n ip: clientIp,\n });\n\n // Apply trusted cookie timezone if available\n if (cookieTimezone) {\n geo.timezone = cookieTimezone.timezone;\n geo.timezoneFromClient = cookieTimezone.timezoneFromClient;\n }\n\n // 6. Set request headers\n setGeoHeaders(requestHeaders, geo);\n\n // 7. Build the response — use sanitized headers in the handler\n let handlerResponse: NextResponse | undefined;\n if (options?.handler) {\n const sanitizedRequest = new Request(request.nextUrl.toString(), {\n method: request.method ?? 'GET',\n headers: requestHeaders,\n body: request.body,\n // @ts-expect-error -- NextRequest supports duplex but TS doesn't expose it\n duplex: 'half',\n });\n handlerResponse = await options.handler(\n Object.assign(sanitizedRequest, { nextUrl: request.nextUrl, cookies: request.cookies }) as NextRequest,\n geo\n );\n }\n\n const response = handlerResponse ?? NextResponse.next({\n request: { headers: requestHeaders },\n });\n\n // 8. Set/update the cookie if needed\n if (!cookieValue || cookieGeo.ip !== clientIp) {\n response.cookies.set(COOKIE_NAME, serializeCookie(geo), {\n path: COOKIE_OPTIONS.path,\n httpOnly: COOKIE_OPTIONS.httpOnly,\n secure: COOKIE_OPTIONS.secure,\n sameSite: COOKIE_OPTIONS.sameSite,\n maxAge: COOKIE_OPTIONS.maxAge,\n });\n }\n\n return response;\n };\n}\n\n/**\n * Create a geo-redirect handler for use with createProxy.\n */\nexport function withGeoRedirect(\n options: GeoRedirectOptions\n): (request: NextRequest, geo: Geo) => NextResponse | undefined {\n const { defaultLocale, localeMap, excludePaths = [] } = options;\n const validLocales = new Set(Object.values(localeMap));\n validLocales.add(defaultLocale);\n\n return (request: NextRequest, geo: Geo): NextResponse | undefined => {\n const pathname = request.nextUrl.pathname;\n\n // Skip excluded paths\n for (const prefix of excludePaths) {\n if (pathname.startsWith(prefix)) {\n return undefined;\n }\n }\n\n // Extract current locale prefix from path\n const segments = pathname.split('/').filter(Boolean);\n const currentPrefix = segments[0];\n\n // If path already has a valid locale prefix, don't redirect\n if (currentPrefix && validLocales.has(currentPrefix)) {\n return undefined;\n }\n\n // Look up locale for the user's country\n const locale = (geo.country && localeMap[geo.country]) || defaultLocale;\n\n // If resolved locale is the default and path has no locale prefix, no redirect needed\n if (locale === defaultLocale) {\n return undefined;\n }\n\n // Redirect to locale-prefixed path\n const url = request.nextUrl.clone();\n url.pathname = `/${locale}${pathname}`;\n return NextResponse.redirect(url, 307);\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"proxy.mjs","names":[],"sources":["../src/proxy.ts"],"sourcesContent":["declare const __PKG_NAME__: string;\ndeclare const __PKG_VERSION__: string;\n\nimport type { Geo } from '@netloc8/core';\nimport type { NextRequest } from 'next/server';\nimport { NextResponse } from 'next/server';\nimport {\n getClientIp,\n isPublicIp,\n getGeoFromPlatformHeaders,\n fetchGeo,\n normalizeApiResponse,\n parseCookie,\n serializeCookie,\n reconcileGeo,\n COOKIE_NAME,\n COOKIE_OPTIONS,\n} from '@netloc8/core';\n\ninterface CreateProxyOptions {\n timeout?: number;\n apiKey?: string;\n apiUrl?: string;\n testIp?: string;\n handler?: (\n request: NextRequest,\n geo: Geo\n ) => NextResponse | undefined | Promise<NextResponse | undefined>;\n}\n\ninterface GeoRedirectOptions {\n defaultLocale: string;\n localeMap: Record<string, string>;\n excludePaths?: string[];\n}\n\n// --- Header transport ---\n// These map nested Geo paths to/from x-netloc8-* request headers\n// for the proxy → Server Component transport layer.\n\ninterface HeaderEntry {\n header: string;\n get: (geo: Geo) => string | number | boolean | string[] | undefined;\n set: (geo: Geo, raw: string) => void;\n type: 'string' | 'number' | 'boolean' | 'json';\n}\n\nconst HEADER_ENTRIES: HeaderEntry[] = [\n {\n header: 'x-netloc8-ip',\n get: (g) => g.query?.value,\n set: (g, v) => { if (!g.query) g.query = {}; g.query.value = v; },\n type: 'string',\n },\n {\n header: 'x-netloc8-ip-version',\n get: (g) => g.query?.ipVersion,\n set: (g, v) => { const n = parseFloat(v); if (!Number.isFinite(n)) return; if (!g.query) g.query = {}; g.query.ipVersion = n; },\n type: 'number',\n },\n {\n header: 'x-netloc8-continent-code',\n get: (g) => g.location?.continent?.code,\n set: (g, v) => {\n if (!g.location) g.location = {};\n if (!g.location.continent) g.location.continent = {};\n g.location.continent.code = v;\n },\n type: 'string',\n },\n {\n header: 'x-netloc8-continent-name',\n get: (g) => g.location?.continent?.name,\n set: (g, v) => {\n if (!g.location) g.location = {};\n if (!g.location.continent) g.location.continent = {};\n g.location.continent.name = v;\n },\n type: 'string',\n },\n {\n header: 'x-netloc8-country-code',\n get: (g) => g.location?.country?.code,\n set: (g, v) => {\n if (!g.location) g.location = {};\n if (!g.location.country) g.location.country = {};\n g.location.country.code = v;\n },\n type: 'string',\n },\n {\n header: 'x-netloc8-country-name',\n get: (g) => g.location?.country?.name,\n set: (g, v) => {\n if (!g.location) g.location = {};\n if (!g.location.country) g.location.country = {};\n g.location.country.name = v;\n },\n type: 'string',\n },\n {\n header: 'x-netloc8-country-flag',\n get: (g) => g.location?.country?.flag,\n set: (g, v) => {\n if (!g.location) g.location = {};\n if (!g.location.country) g.location.country = {};\n g.location.country.flag = v;\n },\n type: 'string',\n },\n {\n header: 'x-netloc8-country-unions',\n get: (g) => g.location?.country?.unions,\n set: (g, v) => {\n if (!g.location) g.location = {};\n if (!g.location.country) g.location.country = {};\n try {\n const parsed = JSON.parse(v);\n if (Array.isArray(parsed)) {\n g.location.country.unions = parsed;\n }\n } catch {\n // Skip malformed JSON\n }\n },\n type: 'json',\n },\n {\n header: 'x-netloc8-region-code',\n get: (g) => g.location?.region?.code,\n set: (g, v) => {\n if (!g.location) g.location = {};\n if (!g.location.region) g.location.region = {};\n g.location.region.code = v;\n },\n type: 'string',\n },\n {\n header: 'x-netloc8-region-name',\n get: (g) => g.location?.region?.name,\n set: (g, v) => {\n if (!g.location) g.location = {};\n if (!g.location.region) g.location.region = {};\n g.location.region.name = v;\n },\n type: 'string',\n },\n {\n header: 'x-netloc8-district',\n get: (g) => g.location?.district,\n set: (g, v) => { if (!g.location) g.location = {}; g.location.district = v; },\n type: 'string',\n },\n {\n header: 'x-netloc8-city',\n get: (g) => g.location?.city,\n set: (g, v) => { if (!g.location) g.location = {}; g.location.city = v; },\n type: 'string',\n },\n {\n header: 'x-netloc8-postal-code',\n get: (g) => g.location?.postalCode,\n set: (g, v) => { if (!g.location) g.location = {}; g.location.postalCode = v; },\n type: 'string',\n },\n {\n header: 'x-netloc8-latitude',\n get: (g) => g.location?.coordinates?.latitude,\n set: (g, v) => {\n if (!g.location) g.location = {};\n if (!g.location.coordinates) g.location.coordinates = {};\n const n = parseFloat(v); if (!Number.isFinite(n)) return;\n g.location.coordinates.latitude = n;\n },\n type: 'number',\n },\n {\n header: 'x-netloc8-longitude',\n get: (g) => g.location?.coordinates?.longitude,\n set: (g, v) => {\n if (!g.location) g.location = {};\n if (!g.location.coordinates) g.location.coordinates = {};\n const n = parseFloat(v); if (!Number.isFinite(n)) return;\n g.location.coordinates.longitude = n;\n },\n type: 'number',\n },\n {\n header: 'x-netloc8-accuracy-radius',\n get: (g) => g.location?.coordinates?.accuracyRadius,\n set: (g, v) => {\n if (!g.location) g.location = {};\n if (!g.location.coordinates) g.location.coordinates = {};\n const n = parseFloat(v); if (!Number.isFinite(n)) return;\n g.location.coordinates.accuracyRadius = n;\n },\n type: 'number',\n },\n {\n header: 'x-netloc8-timezone',\n get: (g) => g.location?.timezone,\n set: (g, v) => { if (!g.location) g.location = {}; g.location.timezone = v; },\n type: 'string',\n },\n {\n header: 'x-netloc8-utc-offset',\n get: (g) => g.location?.utcOffset,\n set: (g, v) => { if (!g.location) g.location = {}; g.location.utcOffset = v; },\n type: 'string',\n },\n {\n header: 'x-netloc8-geo-confidence',\n get: (g) => g.location?.geoConfidence,\n set: (g, v) => { const n = parseFloat(v); if (!Number.isFinite(n)) return; if (!g.location) g.location = {}; g.location.geoConfidence = n; },\n type: 'number',\n },\n {\n header: 'x-netloc8-asn',\n get: (g) => g.network?.asn,\n set: (g, v) => { if (!g.network) g.network = {}; g.network.asn = v; },\n type: 'string',\n },\n {\n header: 'x-netloc8-asn-org',\n get: (g) => g.network?.organization,\n set: (g, v) => { if (!g.network) g.network = {}; g.network.organization = v; },\n type: 'string',\n },\n {\n header: 'x-netloc8-asn-domain',\n get: (g) => g.network?.domain,\n set: (g, v) => { if (!g.network) g.network = {}; g.network.domain = v; },\n type: 'string',\n },\n {\n header: 'x-netloc8-precision',\n get: (g) => g.meta?.precision,\n set: (g, v) => { if (!g.meta) g.meta = {}; g.meta.precision = v; },\n type: 'string',\n },\n {\n header: 'x-netloc8-degraded',\n get: (g) => g.meta?.degraded,\n set: (g, v) => { if (!g.meta) g.meta = {}; g.meta.degraded = v === 'true'; },\n type: 'boolean',\n },\n {\n header: 'x-netloc8-timezone-from-client',\n get: (g) => g.location?.timezoneFromClient,\n set: (g, v) => { if (!g.location) g.location = {}; g.location.timezoneFromClient = v === 'true'; },\n type: 'boolean',\n },\n];\n\n/**\n * Set x-netloc8-* request headers from a Geo object.\n */\nfunction setGeoHeaders(requestHeaders: Headers, geo: Geo): void {\n for (const entry of HEADER_ENTRIES) {\n const value = entry.get(geo);\n if (value !== undefined && value !== null) {\n if (entry.type === 'json') {\n requestHeaders.set(entry.header, encodeURIComponent(JSON.stringify(value)));\n } else {\n requestHeaders.set(entry.header, encodeURIComponent(String(value)));\n }\n }\n }\n}\n\n/**\n * Read x-netloc8-* request headers back into a Geo object.\n * Used by server.ts to reconstruct Geo on the server side.\n */\nexport function readGeoHeaders(headers: Headers): Geo {\n const geo: Geo = {};\n\n for (const entry of HEADER_ENTRIES) {\n const raw = headers.get(entry.header);\n if (raw === null) {\n continue;\n }\n\n try {\n const decoded = decodeURIComponent(raw);\n entry.set(geo, decoded);\n } catch {\n // Skip this header if decodeURIComponent throws\n }\n }\n\n return geo;\n}\n\n/**\n * Create a Next.js 16 proxy function that resolves geolocation for every\n * matching request.\n *\n * Returns a standard proxy function that can be exported directly from the\n * user's proxy.ts / proxy.js file, or composed with other proxy logic.\n */\nexport function createProxy(options?: CreateProxyOptions):\n (request: NextRequest) => Promise<NextResponse> {\n\n return async (request: NextRequest): Promise<NextResponse> => {\n const apiKey = options?.apiKey ?? process.env.NETLOC8_API_KEY;\n const apiUrl = options?.apiUrl ?? process.env.NETLOC8_API_URL;\n const timeout = options?.timeout ?? 1500;\n\n // Security: Remove any incoming spoofed headers\n const requestHeaders = new Headers(request.headers);\n for (const entry of HEADER_ENTRIES) {\n requestHeaders.delete(entry.header);\n }\n\n // 1. Determine client IP\n let clientIp: string | undefined;\n\n if (process.env.NODE_ENV !== 'production') {\n clientIp = options?.testIp ?? process.env.NETLOC8_TEST_IP;\n }\n\n if (!clientIp) {\n clientIp = getClientIp(request.headers);\n }\n\n // 2. Check the cookie cache (fast path)\n const cookieValue = request.cookies.get(COOKIE_NAME)?.value;\n const cookieGeo = parseCookie(cookieValue);\n\n // Cookie fast path: only trust timezone/timezoneFromClient from the\n // client-controlled cookie. Re-resolve other geo fields to prevent\n // spoofing of country/region/city via cookie manipulation.\n const cookieTimezone = (\n cookieGeo.location?.timezoneFromClient === true &&\n cookieGeo.query?.value === clientIp\n ) ? {\n timezone: cookieGeo.location.timezone,\n timezoneFromClient: cookieGeo.location.timezoneFromClient,\n } : undefined;\n\n // 3. Extract platform headers (zero-cost)\n const platformGeo = getGeoFromPlatformHeaders(request.headers);\n\n // 4. Decide whether to call the API\n let apiGeo: Geo | undefined;\n\n if (clientIp && isPublicIp(clientIp) && !platformGeo.location?.country?.code && !cookieTimezone) {\n const raw = await fetchGeo(clientIp, { apiKey, apiUrl, timeout, clientId: typeof __PKG_NAME__ !== 'undefined' ? `${__PKG_NAME__}/${__PKG_VERSION__}` : undefined });\n if (raw) {\n apiGeo = normalizeApiResponse(raw, clientIp);\n }\n }\n\n // 5. Reconcile all sources — cookie is lowest priority in\n // reconcileGeo, so platform headers and API data overwrite it.\n // Pass the full cookie so self-hosted deployments (no platform\n // headers, API call skipped) still have city/country/region.\n const geo = reconcileGeo({\n cookie: cookieGeo.query?.value ? cookieGeo : undefined,\n platform: platformGeo,\n api: apiGeo,\n ip: clientIp,\n });\n\n // Apply trusted cookie timezone if available\n if (cookieTimezone) {\n if (!geo.location) geo.location = {};\n geo.location.timezone = cookieTimezone.timezone;\n geo.location.timezoneFromClient = cookieTimezone.timezoneFromClient;\n }\n\n // 6. Set request headers\n setGeoHeaders(requestHeaders, geo);\n\n // 7. Build the response — use sanitized headers in the handler\n let handlerResponse: NextResponse | undefined;\n if (options?.handler) {\n const sanitizedRequest = new Request(request.nextUrl.toString(), {\n method: request.method ?? 'GET',\n headers: requestHeaders,\n body: request.body,\n // @ts-expect-error -- NextRequest supports duplex but TS doesn't expose it\n duplex: 'half',\n });\n handlerResponse = await options.handler(\n Object.assign(sanitizedRequest, { nextUrl: request.nextUrl, cookies: request.cookies }) as NextRequest,\n geo\n );\n }\n\n const response = handlerResponse ?? NextResponse.next({\n request: { headers: requestHeaders },\n });\n\n // 8. Set/update the cookie if needed\n if (!cookieValue || cookieGeo.query?.value !== clientIp) {\n response.cookies.set(COOKIE_NAME, serializeCookie(geo), {\n path: COOKIE_OPTIONS.path,\n httpOnly: COOKIE_OPTIONS.httpOnly,\n secure: COOKIE_OPTIONS.secure,\n sameSite: COOKIE_OPTIONS.sameSite,\n maxAge: COOKIE_OPTIONS.maxAge,\n });\n }\n\n return response;\n };\n}\n\n/**\n * Create a geo-redirect handler for use with createProxy.\n */\nexport function withGeoRedirect(\n options: GeoRedirectOptions\n): (request: NextRequest, geo: Geo) => NextResponse | undefined {\n const { defaultLocale, localeMap, excludePaths = [] } = options;\n const validLocales = new Set(Object.values(localeMap));\n validLocales.add(defaultLocale);\n\n return (request: NextRequest, geo: Geo): NextResponse | undefined => {\n const pathname = request.nextUrl.pathname;\n\n // Skip excluded paths\n for (const prefix of excludePaths) {\n if (pathname.startsWith(prefix)) {\n return undefined;\n }\n }\n\n // Extract current locale prefix from path\n const segments = pathname.split('/').filter(Boolean);\n const currentPrefix = segments[0];\n\n // If path already has a valid locale prefix, don't redirect\n if (currentPrefix && validLocales.has(currentPrefix)) {\n return undefined;\n }\n\n // Look up locale for the user's country\n const countryCode = geo.location?.country?.code;\n const locale = (countryCode && localeMap[countryCode]) || defaultLocale;\n\n // If resolved locale is the default and path has no locale prefix, no redirect needed\n if (locale === defaultLocale) {\n return undefined;\n }\n\n // Redirect to locale-prefixed path\n const url = request.nextUrl.clone();\n url.pathname = `/${locale}${pathname}`;\n return NextResponse.redirect(url, 307);\n };\n}\n"],"mappings":"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"}
|
package/dist/server.d.mts
CHANGED
|
@@ -2,18 +2,19 @@ import { Geo } from "@netloc8/core";
|
|
|
2
2
|
|
|
3
3
|
//#region src/server.d.ts
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
6
|
-
* Must be called from a Server Component, Route Handler, or Server Action.
|
|
5
|
+
* Read geo data in a Server Component, Route Handler, or Server Action.
|
|
7
6
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* Reads x-netloc8-* headers set by `createProxy()` and reconstructs a
|
|
8
|
+
* full Geo object.
|
|
10
9
|
*/
|
|
11
10
|
declare function getGeo(): Promise<Geo>;
|
|
12
11
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* Read timezone in a Server Component, Route Handler, or Server Action.
|
|
13
|
+
*
|
|
14
|
+
* 1. Reads the timezone from the geo headers set by the proxy.
|
|
15
|
+
* 2. If empty, fetches from the API using the request IP.
|
|
15
16
|
*/
|
|
16
|
-
declare function getTimezone(): Promise<string |
|
|
17
|
+
declare function getTimezone(): Promise<string | null>;
|
|
17
18
|
//#endregion
|
|
18
19
|
export { getGeo, getTimezone };
|
|
19
20
|
//# sourceMappingURL=server.d.mts.map
|
package/dist/server.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.mts","names":[],"sources":["../src/server.ts"],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"server.d.mts","names":[],"sources":["../src/server.ts"],"mappings":";;;;;AAWA;;;;iBAAsB,MAAA,CAAA,GAAU,OAAA,CAAQ,GAAA;AAWxC;;;;;;AAAA,iBAAsB,WAAA,CAAA,GAAe,OAAA"}
|
package/dist/server.mjs
CHANGED
|
@@ -1,141 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
/**
|
|
3
|
-
* Header-to-Geo field mapping.
|
|
4
|
-
*/
|
|
5
|
-
const HEADER_MAP = [
|
|
6
|
-
[
|
|
7
|
-
"x-netloc8-ip",
|
|
8
|
-
"ip",
|
|
9
|
-
"string"
|
|
10
|
-
],
|
|
11
|
-
[
|
|
12
|
-
"x-netloc8-ip-version",
|
|
13
|
-
"ipVersion",
|
|
14
|
-
"number"
|
|
15
|
-
],
|
|
16
|
-
[
|
|
17
|
-
"x-netloc8-continent",
|
|
18
|
-
"continent",
|
|
19
|
-
"string"
|
|
20
|
-
],
|
|
21
|
-
[
|
|
22
|
-
"x-netloc8-continent-name",
|
|
23
|
-
"continentName",
|
|
24
|
-
"string"
|
|
25
|
-
],
|
|
26
|
-
[
|
|
27
|
-
"x-netloc8-country",
|
|
28
|
-
"country",
|
|
29
|
-
"string"
|
|
30
|
-
],
|
|
31
|
-
[
|
|
32
|
-
"x-netloc8-country-name",
|
|
33
|
-
"countryName",
|
|
34
|
-
"string"
|
|
35
|
-
],
|
|
36
|
-
[
|
|
37
|
-
"x-netloc8-is-eu",
|
|
38
|
-
"isEU",
|
|
39
|
-
"boolean"
|
|
40
|
-
],
|
|
41
|
-
[
|
|
42
|
-
"x-netloc8-region",
|
|
43
|
-
"region",
|
|
44
|
-
"string"
|
|
45
|
-
],
|
|
46
|
-
[
|
|
47
|
-
"x-netloc8-region-name",
|
|
48
|
-
"regionName",
|
|
49
|
-
"string"
|
|
50
|
-
],
|
|
51
|
-
[
|
|
52
|
-
"x-netloc8-city",
|
|
53
|
-
"city",
|
|
54
|
-
"string"
|
|
55
|
-
],
|
|
56
|
-
[
|
|
57
|
-
"x-netloc8-postal-code",
|
|
58
|
-
"postalCode",
|
|
59
|
-
"string"
|
|
60
|
-
],
|
|
61
|
-
[
|
|
62
|
-
"x-netloc8-latitude",
|
|
63
|
-
"latitude",
|
|
64
|
-
"number"
|
|
65
|
-
],
|
|
66
|
-
[
|
|
67
|
-
"x-netloc8-longitude",
|
|
68
|
-
"longitude",
|
|
69
|
-
"number"
|
|
70
|
-
],
|
|
71
|
-
[
|
|
72
|
-
"x-netloc8-timezone",
|
|
73
|
-
"timezone",
|
|
74
|
-
"string"
|
|
75
|
-
],
|
|
76
|
-
[
|
|
77
|
-
"x-netloc8-accuracy-radius",
|
|
78
|
-
"accuracyRadius",
|
|
79
|
-
"number"
|
|
80
|
-
],
|
|
81
|
-
[
|
|
82
|
-
"x-netloc8-precision",
|
|
83
|
-
"precision",
|
|
84
|
-
"string"
|
|
85
|
-
],
|
|
86
|
-
[
|
|
87
|
-
"x-netloc8-is-limited",
|
|
88
|
-
"isLimited",
|
|
89
|
-
"boolean"
|
|
90
|
-
],
|
|
91
|
-
[
|
|
92
|
-
"x-netloc8-limit-reason",
|
|
93
|
-
"limitReason",
|
|
94
|
-
"string"
|
|
95
|
-
],
|
|
96
|
-
[
|
|
97
|
-
"x-netloc8-timezone-from-client",
|
|
98
|
-
"timezoneFromClient",
|
|
99
|
-
"boolean"
|
|
100
|
-
]
|
|
101
|
-
];
|
|
102
|
-
/**
|
|
103
|
-
* Get the full geolocation data for the current request.
|
|
104
|
-
* Must be called from a Server Component, Route Handler, or Server Action.
|
|
105
|
-
*
|
|
106
|
-
* Returns the Geo object populated by the proxy. If the proxy did not run
|
|
107
|
-
* (e.g. the route is excluded from the matcher), returns an empty object.
|
|
108
|
-
*/
|
|
109
|
-
async function getGeo() {
|
|
110
|
-
try {
|
|
111
|
-
const { headers } = await import("next/headers");
|
|
112
|
-
const h = await headers();
|
|
113
|
-
const geo = {};
|
|
114
|
-
for (const [header, field, type] of HEADER_MAP) {
|
|
115
|
-
const raw = h.get(header);
|
|
116
|
-
if (raw === null) continue;
|
|
117
|
-
try {
|
|
118
|
-
const decoded = decodeURIComponent(raw);
|
|
119
|
-
if (type === "number") {
|
|
120
|
-
const num = parseFloat(decoded);
|
|
121
|
-
if (!isNaN(num)) geo[field] = num;
|
|
122
|
-
} else if (type === "boolean") geo[field] = decoded === "true";
|
|
123
|
-
else geo[field] = decoded;
|
|
124
|
-
} catch {}
|
|
125
|
-
}
|
|
126
|
-
return geo;
|
|
127
|
-
} catch {
|
|
128
|
-
return {};
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
/**
|
|
132
|
-
* Get the timezone for the current request.
|
|
133
|
-
* Shorthand for (await getGeo()).timezone.
|
|
134
|
-
*/
|
|
135
|
-
async function getTimezone() {
|
|
136
|
-
return (await getGeo()).timezone;
|
|
137
|
-
}
|
|
138
|
-
//#endregion
|
|
139
|
-
export { getGeo, getTimezone };
|
|
140
|
-
|
|
1
|
+
import{readGeoHeaders as e}from"./proxy.mjs";import{DEFAULT_API_URL as t,fetchTimezone as n}from"@netloc8/core";import{headers as r}from"next/headers";async function i(){return e(await r())}async function a(){let e=await i(),r=e.location?.timezone;if(r)return r;let a=e.query?.value;return a?await n(a,{apiUrl:process.env.NETLOC8_API_URL??t,apiKey:process.env.NETLOC8_API_KEY}):null}export{i as getGeo,a as getTimezone};
|
|
141
2
|
//# sourceMappingURL=server.mjs.map
|
package/dist/server.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.mjs","names":[],"sources":["../src/server.ts"],"sourcesContent":["import type { Geo } from '@netloc8/core';\
|
|
1
|
+
{"version":3,"file":"server.mjs","names":[],"sources":["../src/server.ts"],"sourcesContent":["import type { Geo } from '@netloc8/core';\nimport { headers } from 'next/headers';\nimport { DEFAULT_API_URL, fetchTimezone } from '@netloc8/core';\nimport { readGeoHeaders } from './proxy';\n\n/**\n * Read geo data in a Server Component, Route Handler, or Server Action.\n *\n * Reads x-netloc8-* headers set by `createProxy()` and reconstructs a\n * full Geo object.\n */\nexport async function getGeo(): Promise<Geo> {\n const headerStore = await headers();\n return readGeoHeaders(headerStore);\n}\n\n/**\n * Read timezone in a Server Component, Route Handler, or Server Action.\n *\n * 1. Reads the timezone from the geo headers set by the proxy.\n * 2. If empty, fetches from the API using the request IP.\n */\nexport async function getTimezone(): Promise<string | null> {\n const geo = await getGeo();\n const timezone = geo.location?.timezone;\n\n if (timezone) {\n return timezone;\n }\n\n // Fall back to API\n const ip = geo.query?.value;\n if (ip) {\n return await fetchTimezone(ip, {\n apiUrl: process.env.NETLOC8_API_URL ?? DEFAULT_API_URL,\n apiKey: process.env.NETLOC8_API_KEY,\n });\n }\n\n return null;\n}\n"],"mappings":"uJAWA,eAAsB,GAAuB,CAEzC,OAAO,EADa,MAAM,GAAS,CACD,CAStC,eAAsB,GAAsC,CACxD,IAAM,EAAM,MAAM,GAAQ,CACpB,EAAW,EAAI,UAAU,SAE/B,GAAI,EACA,OAAO,EAIX,IAAM,EAAK,EAAI,OAAO,MAQtB,OAPI,EACO,MAAM,EAAc,EAAI,CAC3B,OAAQ,QAAQ,IAAI,iBAAmB,EACvC,OAAQ,QAAQ,IAAI,gBACvB,CAAC,CAGC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@netloc8/nextjs",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Next.js geolocation plugin. Server-side proxy, getGeo() for Server Components, useGeo() hook, GeoGate, and locale-based redirects.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"nextjs",
|
|
7
|
+
"next",
|
|
8
|
+
"next-plugin",
|
|
9
|
+
"geolocation",
|
|
10
|
+
"ip-geolocation",
|
|
11
|
+
"geoip",
|
|
12
|
+
"server-components",
|
|
13
|
+
"app-router",
|
|
14
|
+
"middleware",
|
|
15
|
+
"proxy",
|
|
16
|
+
"ssr",
|
|
17
|
+
"timezone",
|
|
18
|
+
"eu-detection",
|
|
19
|
+
"gdpr",
|
|
20
|
+
"locale-redirect",
|
|
21
|
+
"geo-blocking",
|
|
22
|
+
"cookie-consent",
|
|
23
|
+
"location-detection",
|
|
24
|
+
"netloc8"
|
|
25
|
+
],
|
|
26
|
+
"homepage": "https://netloc8.com",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "https://github.com/netloc8/netloc8-js",
|
|
30
|
+
"directory": "packages/nextjs"
|
|
31
|
+
},
|
|
4
32
|
"type": "module",
|
|
5
33
|
"license": "Elastic-2.0",
|
|
6
34
|
"exports": {
|
|
@@ -35,7 +63,10 @@
|
|
|
35
63
|
"react": ">=19.0.0"
|
|
36
64
|
},
|
|
37
65
|
"dependencies": {
|
|
38
|
-
"@netloc8/core": "0.
|
|
39
|
-
"@netloc8/react": "0.
|
|
66
|
+
"@netloc8/core": "1.0.0",
|
|
67
|
+
"@netloc8/react": "1.0.0"
|
|
68
|
+
},
|
|
69
|
+
"engines": {
|
|
70
|
+
"node": ">=18"
|
|
40
71
|
}
|
|
41
72
|
}
|