@netloc8/nextjs 0.1.3 → 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/README.md CHANGED
@@ -1,9 +1,16 @@
1
1
  # @netloc8/nextjs
2
2
 
3
- Next.js integration for the NetLoc8 geolocation SDK. Adds a proxy for
4
- server-side IP resolution and helper functions for reading geo data in
5
- Server Components, Route Handlers, and Server Actions. Re-exports all
6
- React bindings (`NetLoc8Provider`, `useGeo`, `GeoGate`).
3
+ [![npm version](https://img.shields.io/npm/v/@netloc8/nextjs)](https://www.npmjs.com/package/@netloc8/nextjs)
4
+ [![npm downloads](https://img.shields.io/npm/dm/@netloc8/nextjs)](https://www.npmjs.com/package/@netloc8/nextjs)
5
+ [![License: ELv2](https://img.shields.io/badge/license-ELv2-blue)](../../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. Set up the proxy
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
- Create `proxy.ts` in your Next.js app root:
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
- return <p>Hello from {geo.city}, {geo.country}</p>;
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
- ### 3. Read geo data in Client Components
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( { children } ) {
112
+ export default async function RootLayout({ children }) {
48
113
  const geo = await getGeo();
49
114
 
50
115
  return (
51
- <NetLoc8Provider initialGeo={geo}>
52
- {children}
53
- </NetLoc8Provider>
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
- Then use the hook in any client component:
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
- <GeoGate eu={true}>
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, NetLoc8Provider, useGeo } from "@netloc8/react";
3
- export { GeoContext, GeoGate, 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/proxy.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { NextRequest, NextResponse } from "next/server";
2
- import { Geo } from "@netloc8/netloc8-js";
2
+ import { Geo } from "@netloc8/core";
3
3
 
4
4
  //#region src/proxy.d.ts
5
5
  interface CreateProxyOptions {
@@ -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
@@ -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;;;;;;;;iBA+CY,WAAA,CAAY,OAAA,GAAU,kBAAA,IACjC,OAAA,EAAS,WAAA,KAAgB,OAAA,CAAQ,YAAA;;;;iBA2GtB,eAAA,CACZ,OAAA,EAAS,kBAAA,IACT,OAAA,EAAS,WAAA,EAAa,GAAA,EAAK,GAAA,KAAQ,YAAA"}
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,38 +1,282 @@
1
1
  import { NextResponse } from "next/server";
2
- import { COOKIE_NAME, COOKIE_OPTIONS, fetchGeo, getClientIp, getGeoFromPlatformHeaders, isPublicIp, normalizeApiResponse, parseCookie, reconcileGeo, serializeCookie } from "@netloc8/netloc8-js";
2
+ import { COOKIE_NAME, COOKIE_OPTIONS, fetchGeo, getClientIp, getGeoFromPlatformHeaders, isPublicIp, normalizeApiResponse, parseCookie, reconcileGeo, serializeCookie } from "@netloc8/core";
3
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"]
4
+ const HEADER_ENTRIES = [
5
+ {
6
+ header: "x-netloc8-ip",
7
+ get: (g) => g.query?.value,
8
+ set: (g, v) => {
9
+ if (!g.query) g.query = {};
10
+ g.query.value = v;
11
+ },
12
+ type: "string"
13
+ },
14
+ {
15
+ header: "x-netloc8-ip-version",
16
+ get: (g) => g.query?.ipVersion,
17
+ set: (g, v) => {
18
+ const n = parseFloat(v);
19
+ if (!Number.isFinite(n)) return;
20
+ if (!g.query) g.query = {};
21
+ g.query.ipVersion = n;
22
+ },
23
+ type: "number"
24
+ },
25
+ {
26
+ header: "x-netloc8-continent-code",
27
+ get: (g) => g.location?.continent?.code,
28
+ set: (g, v) => {
29
+ if (!g.location) g.location = {};
30
+ if (!g.location.continent) g.location.continent = {};
31
+ g.location.continent.code = v;
32
+ },
33
+ type: "string"
34
+ },
35
+ {
36
+ header: "x-netloc8-continent-name",
37
+ get: (g) => g.location?.continent?.name,
38
+ set: (g, v) => {
39
+ if (!g.location) g.location = {};
40
+ if (!g.location.continent) g.location.continent = {};
41
+ g.location.continent.name = v;
42
+ },
43
+ type: "string"
44
+ },
45
+ {
46
+ header: "x-netloc8-country-code",
47
+ get: (g) => g.location?.country?.code,
48
+ set: (g, v) => {
49
+ if (!g.location) g.location = {};
50
+ if (!g.location.country) g.location.country = {};
51
+ g.location.country.code = v;
52
+ },
53
+ type: "string"
54
+ },
55
+ {
56
+ header: "x-netloc8-country-name",
57
+ get: (g) => g.location?.country?.name,
58
+ set: (g, v) => {
59
+ if (!g.location) g.location = {};
60
+ if (!g.location.country) g.location.country = {};
61
+ g.location.country.name = v;
62
+ },
63
+ type: "string"
64
+ },
65
+ {
66
+ header: "x-netloc8-country-flag",
67
+ get: (g) => g.location?.country?.flag,
68
+ set: (g, v) => {
69
+ if (!g.location) g.location = {};
70
+ if (!g.location.country) g.location.country = {};
71
+ g.location.country.flag = v;
72
+ },
73
+ type: "string"
74
+ },
75
+ {
76
+ header: "x-netloc8-country-unions",
77
+ get: (g) => g.location?.country?.unions,
78
+ set: (g, v) => {
79
+ if (!g.location) g.location = {};
80
+ if (!g.location.country) g.location.country = {};
81
+ try {
82
+ const parsed = JSON.parse(v);
83
+ if (Array.isArray(parsed)) g.location.country.unions = parsed;
84
+ } catch {}
85
+ },
86
+ type: "json"
87
+ },
88
+ {
89
+ header: "x-netloc8-region-code",
90
+ get: (g) => g.location?.region?.code,
91
+ set: (g, v) => {
92
+ if (!g.location) g.location = {};
93
+ if (!g.location.region) g.location.region = {};
94
+ g.location.region.code = v;
95
+ },
96
+ type: "string"
97
+ },
98
+ {
99
+ header: "x-netloc8-region-name",
100
+ get: (g) => g.location?.region?.name,
101
+ set: (g, v) => {
102
+ if (!g.location) g.location = {};
103
+ if (!g.location.region) g.location.region = {};
104
+ g.location.region.name = v;
105
+ },
106
+ type: "string"
107
+ },
108
+ {
109
+ header: "x-netloc8-district",
110
+ get: (g) => g.location?.district,
111
+ set: (g, v) => {
112
+ if (!g.location) g.location = {};
113
+ g.location.district = v;
114
+ },
115
+ type: "string"
116
+ },
117
+ {
118
+ header: "x-netloc8-city",
119
+ get: (g) => g.location?.city,
120
+ set: (g, v) => {
121
+ if (!g.location) g.location = {};
122
+ g.location.city = v;
123
+ },
124
+ type: "string"
125
+ },
126
+ {
127
+ header: "x-netloc8-postal-code",
128
+ get: (g) => g.location?.postalCode,
129
+ set: (g, v) => {
130
+ if (!g.location) g.location = {};
131
+ g.location.postalCode = v;
132
+ },
133
+ type: "string"
134
+ },
135
+ {
136
+ header: "x-netloc8-latitude",
137
+ get: (g) => g.location?.coordinates?.latitude,
138
+ set: (g, v) => {
139
+ if (!g.location) g.location = {};
140
+ if (!g.location.coordinates) g.location.coordinates = {};
141
+ const n = parseFloat(v);
142
+ if (!Number.isFinite(n)) return;
143
+ g.location.coordinates.latitude = n;
144
+ },
145
+ type: "number"
146
+ },
147
+ {
148
+ header: "x-netloc8-longitude",
149
+ get: (g) => g.location?.coordinates?.longitude,
150
+ set: (g, v) => {
151
+ if (!g.location) g.location = {};
152
+ if (!g.location.coordinates) g.location.coordinates = {};
153
+ const n = parseFloat(v);
154
+ if (!Number.isFinite(n)) return;
155
+ g.location.coordinates.longitude = n;
156
+ },
157
+ type: "number"
158
+ },
159
+ {
160
+ header: "x-netloc8-accuracy-radius",
161
+ get: (g) => g.location?.coordinates?.accuracyRadius,
162
+ set: (g, v) => {
163
+ if (!g.location) g.location = {};
164
+ if (!g.location.coordinates) g.location.coordinates = {};
165
+ const n = parseFloat(v);
166
+ if (!Number.isFinite(n)) return;
167
+ g.location.coordinates.accuracyRadius = n;
168
+ },
169
+ type: "number"
170
+ },
171
+ {
172
+ header: "x-netloc8-timezone",
173
+ get: (g) => g.location?.timezone,
174
+ set: (g, v) => {
175
+ if (!g.location) g.location = {};
176
+ g.location.timezone = v;
177
+ },
178
+ type: "string"
179
+ },
180
+ {
181
+ header: "x-netloc8-utc-offset",
182
+ get: (g) => g.location?.utcOffset,
183
+ set: (g, v) => {
184
+ if (!g.location) g.location = {};
185
+ g.location.utcOffset = v;
186
+ },
187
+ type: "string"
188
+ },
189
+ {
190
+ header: "x-netloc8-geo-confidence",
191
+ get: (g) => g.location?.geoConfidence,
192
+ set: (g, v) => {
193
+ const n = parseFloat(v);
194
+ if (!Number.isFinite(n)) return;
195
+ if (!g.location) g.location = {};
196
+ g.location.geoConfidence = n;
197
+ },
198
+ type: "number"
199
+ },
200
+ {
201
+ header: "x-netloc8-asn",
202
+ get: (g) => g.network?.asn,
203
+ set: (g, v) => {
204
+ if (!g.network) g.network = {};
205
+ g.network.asn = v;
206
+ },
207
+ type: "string"
208
+ },
209
+ {
210
+ header: "x-netloc8-asn-org",
211
+ get: (g) => g.network?.organization,
212
+ set: (g, v) => {
213
+ if (!g.network) g.network = {};
214
+ g.network.organization = v;
215
+ },
216
+ type: "string"
217
+ },
218
+ {
219
+ header: "x-netloc8-asn-domain",
220
+ get: (g) => g.network?.domain,
221
+ set: (g, v) => {
222
+ if (!g.network) g.network = {};
223
+ g.network.domain = v;
224
+ },
225
+ type: "string"
226
+ },
227
+ {
228
+ header: "x-netloc8-precision",
229
+ get: (g) => g.meta?.precision,
230
+ set: (g, v) => {
231
+ if (!g.meta) g.meta = {};
232
+ g.meta.precision = v;
233
+ },
234
+ type: "string"
235
+ },
236
+ {
237
+ header: "x-netloc8-degraded",
238
+ get: (g) => g.meta?.degraded,
239
+ set: (g, v) => {
240
+ if (!g.meta) g.meta = {};
241
+ g.meta.degraded = v === "true";
242
+ },
243
+ type: "boolean"
244
+ },
245
+ {
246
+ header: "x-netloc8-timezone-from-client",
247
+ get: (g) => g.location?.timezoneFromClient,
248
+ set: (g, v) => {
249
+ if (!g.location) g.location = {};
250
+ g.location.timezoneFromClient = v === "true";
251
+ },
252
+ type: "boolean"
253
+ }
27
254
  ];
28
255
  /**
29
256
  * Set x-netloc8-* request headers from a Geo object.
30
257
  */
31
258
  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)));
259
+ for (const entry of HEADER_ENTRIES) {
260
+ const value = entry.get(geo);
261
+ if (value !== void 0 && value !== null) if (entry.type === "json") requestHeaders.set(entry.header, encodeURIComponent(JSON.stringify(value)));
262
+ else requestHeaders.set(entry.header, encodeURIComponent(String(value)));
263
+ }
264
+ }
265
+ /**
266
+ * Read x-netloc8-* request headers back into a Geo object.
267
+ * Used by server.ts to reconstruct Geo on the server side.
268
+ */
269
+ function readGeoHeaders(headers) {
270
+ const geo = {};
271
+ for (const entry of HEADER_ENTRIES) {
272
+ const raw = headers.get(entry.header);
273
+ if (raw === null) continue;
274
+ try {
275
+ const decoded = decodeURIComponent(raw);
276
+ entry.set(geo, decoded);
277
+ } catch {}
35
278
  }
279
+ return geo;
36
280
  }
37
281
  /**
38
282
  * Create a Next.js 16 proxy function that resolves geolocation for every
@@ -47,36 +291,37 @@ function createProxy(options) {
47
291
  const apiUrl = options?.apiUrl ?? process.env.NETLOC8_API_URL;
48
292
  const timeout = options?.timeout ?? 1500;
49
293
  const requestHeaders = new Headers(request.headers);
50
- for (const [, headerName] of GEO_HEADER_MAP) requestHeaders.delete(headerName);
294
+ for (const entry of HEADER_ENTRIES) requestHeaders.delete(entry.header);
51
295
  let clientIp;
52
296
  if (process.env.NODE_ENV !== "production") clientIp = options?.testIp ?? process.env.NETLOC8_TEST_IP;
53
297
  if (!clientIp) clientIp = getClientIp(request.headers);
54
298
  const cookieValue = request.cookies.get(COOKIE_NAME)?.value;
55
299
  const cookieGeo = parseCookie(cookieValue);
56
- const cookieTimezone = cookieGeo.timezoneFromClient === true && cookieGeo.ip === clientIp ? {
57
- timezone: cookieGeo.timezone,
58
- timezoneFromClient: cookieGeo.timezoneFromClient
300
+ const cookieTimezone = cookieGeo.location?.timezoneFromClient === true && cookieGeo.query?.value === clientIp ? {
301
+ timezone: cookieGeo.location.timezone,
302
+ timezoneFromClient: cookieGeo.location.timezoneFromClient
59
303
  } : void 0;
60
304
  const platformGeo = getGeoFromPlatformHeaders(request.headers);
61
305
  let apiGeo;
62
- if (clientIp && isPublicIp(clientIp) && !platformGeo.timezone && !cookieTimezone) {
306
+ if (clientIp && isPublicIp(clientIp) && !platformGeo.location?.country?.code && !cookieTimezone) {
63
307
  const raw = await fetchGeo(clientIp, {
64
308
  apiKey,
65
309
  apiUrl,
66
310
  timeout,
67
- clientId: `@netloc8/nextjs/0.1.3`
311
+ clientId: `@netloc8/nextjs/1.0.0`
68
312
  });
69
313
  if (raw) apiGeo = normalizeApiResponse(raw, clientIp);
70
314
  }
71
315
  const geo = reconcileGeo({
72
- cookie: cookieGeo.ip ? cookieGeo : void 0,
316
+ cookie: cookieGeo.query?.value ? cookieGeo : void 0,
73
317
  platform: platformGeo,
74
318
  api: apiGeo,
75
319
  ip: clientIp
76
320
  });
77
321
  if (cookieTimezone) {
78
- geo.timezone = cookieTimezone.timezone;
79
- geo.timezoneFromClient = cookieTimezone.timezoneFromClient;
322
+ if (!geo.location) geo.location = {};
323
+ geo.location.timezone = cookieTimezone.timezone;
324
+ geo.location.timezoneFromClient = cookieTimezone.timezoneFromClient;
80
325
  }
81
326
  setGeoHeaders(requestHeaders, geo);
82
327
  let handlerResponse;
@@ -93,7 +338,7 @@ function createProxy(options) {
93
338
  }), geo);
94
339
  }
95
340
  const response = handlerResponse ?? NextResponse.next({ request: { headers: requestHeaders } });
96
- if (!cookieValue || cookieGeo.ip !== clientIp) response.cookies.set(COOKIE_NAME, serializeCookie(geo), {
341
+ if (!cookieValue || cookieGeo.query?.value !== clientIp) response.cookies.set(COOKIE_NAME, serializeCookie(geo), {
97
342
  path: COOKIE_OPTIONS.path,
98
343
  httpOnly: COOKIE_OPTIONS.httpOnly,
99
344
  secure: COOKIE_OPTIONS.secure,
@@ -115,7 +360,8 @@ function withGeoRedirect(options) {
115
360
  for (const prefix of excludePaths) if (pathname.startsWith(prefix)) return;
116
361
  const currentPrefix = pathname.split("/").filter(Boolean)[0];
117
362
  if (currentPrefix && validLocales.has(currentPrefix)) return;
118
- const locale = geo.country && localeMap[geo.country] || defaultLocale;
363
+ const countryCode = geo.location?.country?.code;
364
+ const locale = countryCode && localeMap[countryCode] || defaultLocale;
119
365
  if (locale === defaultLocale) return;
120
366
  const url = request.nextUrl.clone();
121
367
  url.pathname = `/${locale}${pathname}`;
@@ -123,6 +369,6 @@ function withGeoRedirect(options) {
123
369
  };
124
370
  }
125
371
  //#endregion
126
- export { createProxy, withGeoRedirect };
372
+ export { createProxy, readGeoHeaders, withGeoRedirect };
127
373
 
128
374
  //# sourceMappingURL=proxy.mjs.map
@@ -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/netloc8-js';\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/netloc8-js';\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":";;;;;;AAuCA,MAAM,iBAA6C;CAC/C,CAAC,MAAM,eAAe;CACtB,CAAC,aAAa,uBAAuB;CACrC,CAAC,aAAa,sBAAsB;CACpC,CAAC,iBAAiB,2BAA2B;CAC7C,CAAC,WAAW,oBAAoB;CAChC,CAAC,eAAe,yBAAyB;CACzC,CAAC,QAAQ,kBAAkB;CAC3B,CAAC,UAAU,mBAAmB;CAC9B,CAAC,cAAc,wBAAwB;CACvC,CAAC,QAAQ,iBAAiB;CAC1B,CAAC,cAAc,wBAAwB;CACvC,CAAC,YAAY,qBAAqB;CAClC,CAAC,aAAa,sBAAsB;CACpC,CAAC,YAAY,qBAAqB;CAClC,CAAC,kBAAkB,4BAA4B;CAC/C,CAAC,aAAa,sBAAsB;CACpC,CAAC,aAAa,uBAAuB;CACrC,CAAC,eAAe,yBAAyB;CACzC,CAAC,sBAAsB,iCAAiC;CAC3D;;;;AAKD,SAAS,cAAc,gBAAyB,KAAgB;AAC5D,MAAK,MAAM,CAAC,OAAO,WAAW,gBAAgB;EAC1C,MAAM,QAAQ,IAAI;AAClB,MAAI,UAAU,KAAA,KAAa,UAAU,KACjC,gBAAe,IAAI,QAAQ,mBAAmB,OAAO,MAAM,CAAC,CAAC;;;;;;;;;;AAYzE,SAAgB,YAAY,SACwB;AAEhD,QAAO,OAAO,YAAgD;EAC1D,MAAM,SAAS,SAAS,UAAU,QAAQ,IAAI;EAC9C,MAAM,SAAS,SAAS,UAAU,QAAQ,IAAI;EAC9C,MAAM,UAAU,SAAS,WAAW;EAGpC,MAAM,iBAAiB,IAAI,QAAQ,QAAQ,QAAQ;AACnD,OAAK,MAAM,GAAG,eAAe,eACzB,gBAAe,OAAO,WAAW;EAIrC,IAAI;AAEJ,MAAI,QAAQ,IAAI,aAAa,aACzB,YAAW,SAAS,UAAU,QAAQ,IAAI;AAG9C,MAAI,CAAC,SACD,YAAW,YAAY,QAAQ,QAAQ;EAI3C,MAAM,cAAc,QAAQ,QAAQ,IAAI,YAAY,EAAE;EACtD,MAAM,YAAY,YAAY,YAAY;EAK1C,MAAM,iBACF,UAAU,uBAAuB,QACjC,UAAU,OAAO,WACjB;GAAE,UAAU,UAAU;GAAU,oBAAoB,UAAU;GAAoB,GAAG,KAAA;EAGzF,MAAM,cAAc,0BAA0B,QAAQ,QAAQ;EAG9D,IAAI;AAEJ,MAAI,YAAY,WAAW,SAAS,IAAI,CAAC,YAAY,YAAY,CAAC,gBAAgB;GAC9E,MAAM,MAAM,MAAM,SAAS,UAAU;IAAE;IAAQ;IAAQ;IAAS,UAAgD;IAAkD,CAAC;AACnK,OAAI,IACA,UAAS,qBAAqB,KAAK,SAAS;;EAQpD,MAAM,MAAM,aAAa;GACrB,QAAQ,UAAU,KAAK,YAAY,KAAA;GACnC,UAAU;GACV,KAAK;GACL,IAAI;GACP,CAAC;AAGF,MAAI,gBAAgB;AAChB,OAAI,WAAW,eAAe;AAC9B,OAAI,qBAAqB,eAAe;;AAI5C,gBAAc,gBAAgB,IAAI;EAGlC,IAAI;AACJ,MAAI,SAAS,SAAS;GAClB,MAAM,mBAAmB,IAAI,QAAQ,QAAQ,QAAQ,UAAU,EAAE;IAC7D,QAAQ,QAAQ,UAAU;IAC1B,SAAS;IACT,MAAM,QAAQ;IAEd,QAAQ;IACX,CAAC;AACF,qBAAkB,MAAM,QAAQ,QAC5B,OAAO,OAAO,kBAAkB;IAAE,SAAS,QAAQ;IAAS,SAAS,QAAQ;IAAS,CAAC,EACvF,IACH;;EAGL,MAAM,WAAW,mBAAmB,aAAa,KAAK,EAClD,SAAS,EAAE,SAAS,gBAAgB,EACvC,CAAC;AAGF,MAAI,CAAC,eAAe,UAAU,OAAO,SACjC,UAAS,QAAQ,IAAI,aAAa,gBAAgB,IAAI,EAAE;GACpD,MAAM,eAAe;GACrB,UAAU,eAAe;GACzB,QAAQ,eAAe;GACvB,UAAU,eAAe;GACzB,QAAQ,eAAe;GAC1B,CAAC;AAGN,SAAO;;;;;;AAOf,SAAgB,gBACZ,SAC4D;CAC5D,MAAM,EAAE,eAAe,WAAW,eAAe,EAAE,KAAK;CACxD,MAAM,eAAe,IAAI,IAAI,OAAO,OAAO,UAAU,CAAC;AACtD,cAAa,IAAI,cAAc;AAE/B,SAAQ,SAAsB,QAAuC;EACjE,MAAM,WAAW,QAAQ,QAAQ;AAGjC,OAAK,MAAM,UAAU,aACjB,KAAI,SAAS,WAAW,OAAO,CAC3B;EAMR,MAAM,gBADW,SAAS,MAAM,IAAI,CAAC,OAAO,QAAQ,CACrB;AAG/B,MAAI,iBAAiB,aAAa,IAAI,cAAc,CAChD;EAIJ,MAAM,SAAU,IAAI,WAAW,UAAU,IAAI,YAAa;AAG1D,MAAI,WAAW,cACX;EAIJ,MAAM,MAAM,QAAQ,QAAQ,OAAO;AACnC,MAAI,WAAW,IAAI,SAAS;AAC5B,SAAO,aAAa,SAAS,KAAK,IAAI"}
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
@@ -1,19 +1,20 @@
1
- import { Geo } from "@netloc8/netloc8-js";
1
+ import { Geo } from "@netloc8/core";
2
2
 
3
3
  //#region src/server.d.ts
4
4
  /**
5
- * Get the full geolocation data for the current request.
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
- * Returns the Geo object populated by the proxy. If the proxy did not run
9
- * (e.g. the route is excluded from the matcher), returns an empty object.
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
- * Get the timezone for the current request.
14
- * Shorthand for (await getGeo()).timezone.
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 | undefined>;
17
+ declare function getTimezone(): Promise<string | null>;
17
18
  //#endregion
18
19
  export { getGeo, getTimezone };
19
20
  //# sourceMappingURL=server.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.mts","names":[],"sources":["../src/server.ts"],"mappings":";;;;;AAkCA;;;;;iBAAsB,MAAA,CAAA,GAAU,OAAA,CAAQ,GAAA;;;;;iBAwClB,WAAA,CAAA,GAAe,OAAA"}
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,139 +1,32 @@
1
+ import { readGeoHeaders } from "./proxy.mjs";
2
+ import { DEFAULT_API_URL, fetchTimezone } from "@netloc8/core";
3
+ import { headers } from "next/headers";
1
4
  //#region src/server.ts
2
5
  /**
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.
6
+ * Read geo data in a Server Component, Route Handler, or Server Action.
105
7
  *
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.
8
+ * Reads x-netloc8-* headers set by `createProxy()` and reconstructs a
9
+ * full Geo object.
108
10
  */
109
11
  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
- }
12
+ return readGeoHeaders(await headers());
130
13
  }
131
14
  /**
132
- * Get the timezone for the current request.
133
- * Shorthand for (await getGeo()).timezone.
15
+ * Read timezone in a Server Component, Route Handler, or Server Action.
16
+ *
17
+ * 1. Reads the timezone from the geo headers set by the proxy.
18
+ * 2. If empty, fetches from the API using the request IP.
134
19
  */
135
20
  async function getTimezone() {
136
- return (await getGeo()).timezone;
21
+ const geo = await getGeo();
22
+ const timezone = geo.location?.timezone;
23
+ if (timezone) return timezone;
24
+ const ip = geo.query?.value;
25
+ if (ip) return await fetchTimezone(ip, {
26
+ apiUrl: process.env.NETLOC8_API_URL ?? DEFAULT_API_URL,
27
+ apiKey: process.env.NETLOC8_API_KEY
28
+ });
29
+ return null;
137
30
  }
138
31
  //#endregion
139
32
  export { getGeo, getTimezone };
@@ -1 +1 @@
1
- {"version":3,"file":"server.mjs","names":[],"sources":["../src/server.ts"],"sourcesContent":["import type { Geo } from '@netloc8/netloc8-js';\n\n/**\n * Header-to-Geo field mapping.\n */\nconst HEADER_MAP: Array<[string, keyof Geo, 'string' | 'number' | 'boolean']> = [\n ['x-netloc8-ip', 'ip', 'string'],\n ['x-netloc8-ip-version', 'ipVersion', 'number'],\n ['x-netloc8-continent', 'continent', 'string'],\n ['x-netloc8-continent-name', 'continentName', 'string'],\n ['x-netloc8-country', 'country', 'string'],\n ['x-netloc8-country-name', 'countryName', 'string'],\n ['x-netloc8-is-eu', 'isEU', 'boolean'],\n ['x-netloc8-region', 'region', 'string'],\n ['x-netloc8-region-name', 'regionName', 'string'],\n ['x-netloc8-city', 'city', 'string'],\n ['x-netloc8-postal-code', 'postalCode', 'string'],\n ['x-netloc8-latitude', 'latitude', 'number'],\n ['x-netloc8-longitude', 'longitude', 'number'],\n ['x-netloc8-timezone', 'timezone', 'string'],\n ['x-netloc8-accuracy-radius', 'accuracyRadius', 'number'],\n ['x-netloc8-precision', 'precision', 'string'],\n ['x-netloc8-is-limited', 'isLimited', 'boolean'],\n ['x-netloc8-limit-reason', 'limitReason', 'string'],\n ['x-netloc8-timezone-from-client', 'timezoneFromClient', 'boolean'],\n];\n\n/**\n * Get the full geolocation data for the current request.\n * Must be called from a Server Component, Route Handler, or Server Action.\n *\n * Returns the Geo object populated by the proxy. If the proxy did not run\n * (e.g. the route is excluded from the matcher), returns an empty object.\n */\nexport async function getGeo(): Promise<Geo> {\n try {\n const { headers } = await import('next/headers');\n const h = await headers();\n const geo: Geo = {};\n\n for (const [header, field, type] of HEADER_MAP) {\n const raw = h.get(header);\n if (raw === null) {\n continue;\n }\n\n try {\n const decoded = decodeURIComponent(raw);\n\n if (type === 'number') {\n const num = parseFloat(decoded);\n if (!isNaN(num)) {\n (geo as Record<string, unknown>)[field] = num;\n }\n } else if (type === 'boolean') {\n (geo as Record<string, unknown>)[field] = decoded === 'true';\n } else {\n (geo as Record<string, unknown>)[field] = decoded;\n }\n } catch {\n // Skip this header if decodeURIComponent throws\n }\n }\n\n return geo;\n } catch {\n return {};\n }\n}\n\n/**\n * Get the timezone for the current request.\n * Shorthand for (await getGeo()).timezone.\n */\nexport async function getTimezone(): Promise<string | undefined> {\n const geo = await getGeo();\n return geo.timezone;\n}\n"],"mappings":";;;;AAKA,MAAM,aAA0E;CAC5E;EAAC;EAAgB;EAAM;EAAS;CAChC;EAAC;EAAwB;EAAa;EAAS;CAC/C;EAAC;EAAuB;EAAa;EAAS;CAC9C;EAAC;EAA4B;EAAiB;EAAS;CACvD;EAAC;EAAqB;EAAW;EAAS;CAC1C;EAAC;EAA0B;EAAe;EAAS;CACnD;EAAC;EAAmB;EAAQ;EAAU;CACtC;EAAC;EAAoB;EAAU;EAAS;CACxC;EAAC;EAAyB;EAAc;EAAS;CACjD;EAAC;EAAkB;EAAQ;EAAS;CACpC;EAAC;EAAyB;EAAc;EAAS;CACjD;EAAC;EAAsB;EAAY;EAAS;CAC5C;EAAC;EAAuB;EAAa;EAAS;CAC9C;EAAC;EAAsB;EAAY;EAAS;CAC5C;EAAC;EAA6B;EAAkB;EAAS;CACzD;EAAC;EAAuB;EAAa;EAAS;CAC9C;EAAC;EAAwB;EAAa;EAAU;CAChD;EAAC;EAA0B;EAAe;EAAS;CACnD;EAAC;EAAkC;EAAsB;EAAU;CACtE;;;;;;;;AASD,eAAsB,SAAuB;AACzC,KAAI;EACA,MAAM,EAAE,YAAY,MAAM,OAAO;EACjC,MAAM,IAAI,MAAM,SAAS;EACzB,MAAM,MAAW,EAAE;AAEnB,OAAK,MAAM,CAAC,QAAQ,OAAO,SAAS,YAAY;GAC5C,MAAM,MAAM,EAAE,IAAI,OAAO;AACzB,OAAI,QAAQ,KACR;AAGJ,OAAI;IACA,MAAM,UAAU,mBAAmB,IAAI;AAEvC,QAAI,SAAS,UAAU;KACnB,MAAM,MAAM,WAAW,QAAQ;AAC/B,SAAI,CAAC,MAAM,IAAI,CACV,KAAgC,SAAS;eAEvC,SAAS,UACf,KAAgC,SAAS,YAAY;QAErD,KAAgC,SAAS;WAE1C;;AAKZ,SAAO;SACH;AACJ,SAAO,EAAE;;;;;;;AAQjB,eAAsB,cAA2C;AAE7D,SADY,MAAM,QAAQ,EACf"}
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":";;;;;;;;;;AAWA,eAAsB,SAAuB;AAEzC,QAAO,eADa,MAAM,SAAS,CACD;;;;;;;;AAStC,eAAsB,cAAsC;CACxD,MAAM,MAAM,MAAM,QAAQ;CAC1B,MAAM,WAAW,IAAI,UAAU;AAE/B,KAAI,SACA,QAAO;CAIX,MAAM,KAAK,IAAI,OAAO;AACtB,KAAI,GACA,QAAO,MAAM,cAAc,IAAI;EAC3B,QAAQ,QAAQ,IAAI,mBAAmB;EACvC,QAAQ,QAAQ,IAAI;EACvB,CAAC;AAGN,QAAO"}
package/package.json CHANGED
@@ -1,6 +1,34 @@
1
1
  {
2
2
  "name": "@netloc8/nextjs",
3
- "version": "0.1.3",
3
+ "version": "1.0.0",
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/netloc8-js": "0.1.0",
39
- "@netloc8/react": "0.1.0"
66
+ "@netloc8/core": "1.0.0",
67
+ "@netloc8/react": "1.0.0"
68
+ },
69
+ "engines": {
70
+ "node": ">=18"
40
71
  }
41
72
  }