@netloc8/nextjs 0.1.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 +107 -0
- package/dist/index.d.mts +3 -0
- package/dist/index.mjs +3 -0
- package/dist/proxy.d.mts +31 -0
- package/dist/proxy.d.mts.map +1 -0
- package/dist/proxy.mjs +132 -0
- package/dist/proxy.mjs.map +1 -0
- package/dist/server.d.mts +19 -0
- package/dist/server.d.mts.map +1 -0
- package/dist/server.mjs +141 -0
- package/dist/server.mjs.map +1 -0
- package/package.json +41 -0
- package/proxy/package.json +4 -0
- package/server/package.json +4 -0
package/README.md
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# @netloc8/nextjs
|
|
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`).
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
bun add @netloc8/nextjs
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
**Peer dependencies:** `next >= 16`, `react >= 19`
|
|
15
|
+
|
|
16
|
+
## Quick Start
|
|
17
|
+
|
|
18
|
+
### 1. Set up the proxy
|
|
19
|
+
|
|
20
|
+
Create `proxy.ts` in your Next.js app root:
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
import { createProxy } from '@netloc8/nextjs/proxy';
|
|
24
|
+
|
|
25
|
+
export default createProxy();
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### 2. Read geo data in Server Components
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
import { getGeo } from '@netloc8/nextjs/server';
|
|
32
|
+
|
|
33
|
+
export default async function Page() {
|
|
34
|
+
const geo = await getGeo();
|
|
35
|
+
return <p>Hello from {geo.city}, {geo.country}</p>;
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### 3. Read geo data in Client Components
|
|
40
|
+
|
|
41
|
+
Wrap your layout with `<NetLoc8Provider>`:
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
import { getGeo } from '@netloc8/nextjs/server';
|
|
45
|
+
import { NetLoc8Provider } from '@netloc8/nextjs';
|
|
46
|
+
|
|
47
|
+
export default async function RootLayout( { children } ) {
|
|
48
|
+
const geo = await getGeo();
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<NetLoc8Provider initialGeo={geo}>
|
|
52
|
+
{children}
|
|
53
|
+
</NetLoc8Provider>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
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
|
|
71
|
+
|
|
72
|
+
```tsx
|
|
73
|
+
import { GeoGate } from '@netloc8/nextjs';
|
|
74
|
+
|
|
75
|
+
<GeoGate eu={true}>
|
|
76
|
+
<CookieConsentBanner />
|
|
77
|
+
</GeoGate>
|
|
78
|
+
|
|
79
|
+
<GeoGate country={['US', 'CA']} not fallback={<p>Not available in your region</p>}>
|
|
80
|
+
<SpecialOffer />
|
|
81
|
+
</GeoGate>
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Environment Variables
|
|
85
|
+
|
|
86
|
+
| Variable | Required | Description |
|
|
87
|
+
|----------|----------|-------------|
|
|
88
|
+
| `NETLOC8_API_KEY` | Yes | Secret API key (`sk_...`) for the proxy |
|
|
89
|
+
| `NETLOC8_API_URL` | No | API base URL (defaults to `https://netloc8.com`) |
|
|
90
|
+
| `NETLOC8_TEST_IP` | No | Override IP in development |
|
|
91
|
+
|
|
92
|
+
## Exports
|
|
93
|
+
|
|
94
|
+
| Subpath | Export | Description |
|
|
95
|
+
|---------|--------|-------------|
|
|
96
|
+
| `@netloc8/nextjs/proxy` | `createProxy` | Create the proxy function for `proxy.ts` |
|
|
97
|
+
| `@netloc8/nextjs/proxy` | `withGeoRedirect` | Geo-based locale redirect handler |
|
|
98
|
+
| `@netloc8/nextjs/server` | `getGeo` | Read geo data in server contexts |
|
|
99
|
+
| `@netloc8/nextjs/server` | `getTimezone` | Shorthand for timezone only |
|
|
100
|
+
| `@netloc8/nextjs` | `NetLoc8Provider` | Re-exported from `@netloc8/react` |
|
|
101
|
+
| `@netloc8/nextjs` | `useGeo` | Re-exported from `@netloc8/react` |
|
|
102
|
+
| `@netloc8/nextjs` | `GeoGate` | Re-exported from `@netloc8/react` |
|
|
103
|
+
| `@netloc8/nextjs` | `GeoContext` | Re-exported from `@netloc8/react` |
|
|
104
|
+
|
|
105
|
+
## License
|
|
106
|
+
|
|
107
|
+
[Elastic License 2.0 (ELv2)](../../LICENSE)
|
package/dist/index.d.mts
ADDED
package/dist/index.mjs
ADDED
package/dist/proxy.d.mts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { Geo } from "@netloc8/netloc8-js";
|
|
3
|
+
|
|
4
|
+
//#region src/proxy.d.ts
|
|
5
|
+
interface CreateProxyOptions {
|
|
6
|
+
timeout?: number;
|
|
7
|
+
apiKey?: string;
|
|
8
|
+
apiUrl?: string;
|
|
9
|
+
testIp?: string;
|
|
10
|
+
handler?: (request: NextRequest, geo: Geo) => NextResponse | undefined | Promise<NextResponse | undefined>;
|
|
11
|
+
}
|
|
12
|
+
interface GeoRedirectOptions {
|
|
13
|
+
defaultLocale: string;
|
|
14
|
+
localeMap: Record<string, string>;
|
|
15
|
+
excludePaths?: string[];
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Create a Next.js 16 proxy function that resolves geolocation for every
|
|
19
|
+
* matching request.
|
|
20
|
+
*
|
|
21
|
+
* Returns a standard proxy function that can be exported directly from the
|
|
22
|
+
* user's proxy.ts / proxy.js file, or composed with other proxy logic.
|
|
23
|
+
*/
|
|
24
|
+
declare function createProxy(options?: CreateProxyOptions): (request: NextRequest) => Promise<NextResponse>;
|
|
25
|
+
/**
|
|
26
|
+
* Create a geo-redirect handler for use with createProxy.
|
|
27
|
+
*/
|
|
28
|
+
declare function withGeoRedirect(options: GeoRedirectOptions): (request: NextRequest, geo: Geo) => NextResponse | undefined;
|
|
29
|
+
//#endregion
|
|
30
|
+
export { createProxy, withGeoRedirect };
|
|
31
|
+
//# sourceMappingURL=proxy.d.mts.map
|
|
@@ -0,0 +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;;;;iBA+GtB,eAAA,CACZ,OAAA,EAAS,kBAAA,IACT,OAAA,EAAS,WAAA,EAAa,GAAA,EAAK,GAAA,KAAQ,YAAA"}
|
package/dist/proxy.mjs
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { COOKIE_NAME, COOKIE_OPTIONS, fetchGeo, getClientIp, getGeoFromPlatformHeaders, isPublicIp, normalizeApiResponse, parseCookie, reconcileGeo, serializeCookie } from "@netloc8/netloc8-js";
|
|
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.1.0`
|
|
68
|
+
});
|
|
69
|
+
if (raw) apiGeo = normalizeApiResponse(raw, clientIp);
|
|
70
|
+
}
|
|
71
|
+
const geo = reconcileGeo({
|
|
72
|
+
cookie: cookieGeo.ip ? {
|
|
73
|
+
ip: cookieGeo.ip,
|
|
74
|
+
timezone: cookieGeo.timezone,
|
|
75
|
+
timezoneFromClient: cookieGeo.timezoneFromClient
|
|
76
|
+
} : void 0,
|
|
77
|
+
platform: platformGeo,
|
|
78
|
+
api: apiGeo,
|
|
79
|
+
ip: clientIp
|
|
80
|
+
});
|
|
81
|
+
if (cookieTimezone) {
|
|
82
|
+
geo.timezone = cookieTimezone.timezone;
|
|
83
|
+
geo.timezoneFromClient = cookieTimezone.timezoneFromClient;
|
|
84
|
+
}
|
|
85
|
+
setGeoHeaders(requestHeaders, geo);
|
|
86
|
+
let handlerResponse;
|
|
87
|
+
if (options?.handler) {
|
|
88
|
+
const sanitizedRequest = new Request(request.nextUrl.toString(), {
|
|
89
|
+
method: request.method ?? "GET",
|
|
90
|
+
headers: requestHeaders,
|
|
91
|
+
body: request.body,
|
|
92
|
+
duplex: "half"
|
|
93
|
+
});
|
|
94
|
+
handlerResponse = await options.handler(Object.assign(sanitizedRequest, {
|
|
95
|
+
nextUrl: request.nextUrl,
|
|
96
|
+
cookies: request.cookies
|
|
97
|
+
}), geo);
|
|
98
|
+
}
|
|
99
|
+
const response = handlerResponse ?? NextResponse.next({ request: { headers: requestHeaders } });
|
|
100
|
+
if (!cookieValue || cookieGeo.ip !== clientIp) response.cookies.set(COOKIE_NAME, serializeCookie(geo), {
|
|
101
|
+
path: COOKIE_OPTIONS.path,
|
|
102
|
+
httpOnly: COOKIE_OPTIONS.httpOnly,
|
|
103
|
+
secure: COOKIE_OPTIONS.secure,
|
|
104
|
+
sameSite: COOKIE_OPTIONS.sameSite,
|
|
105
|
+
maxAge: COOKIE_OPTIONS.maxAge
|
|
106
|
+
});
|
|
107
|
+
return response;
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Create a geo-redirect handler for use with createProxy.
|
|
112
|
+
*/
|
|
113
|
+
function withGeoRedirect(options) {
|
|
114
|
+
const { defaultLocale, localeMap, excludePaths = [] } = options;
|
|
115
|
+
const validLocales = new Set(Object.values(localeMap));
|
|
116
|
+
validLocales.add(defaultLocale);
|
|
117
|
+
return (request, geo) => {
|
|
118
|
+
const pathname = request.nextUrl.pathname;
|
|
119
|
+
for (const prefix of excludePaths) if (pathname.startsWith(prefix)) return;
|
|
120
|
+
const currentPrefix = pathname.split("/").filter(Boolean)[0];
|
|
121
|
+
if (currentPrefix && validLocales.has(currentPrefix)) return;
|
|
122
|
+
const locale = geo.country && localeMap[geo.country] || defaultLocale;
|
|
123
|
+
if (locale === defaultLocale) return;
|
|
124
|
+
const url = request.nextUrl.clone();
|
|
125
|
+
url.pathname = `/${locale}${pathname}`;
|
|
126
|
+
return NextResponse.redirect(url, 307);
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
//#endregion
|
|
130
|
+
export { createProxy, withGeoRedirect };
|
|
131
|
+
|
|
132
|
+
//# sourceMappingURL=proxy.mjs.map
|
|
@@ -0,0 +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 — only pass timezone fields from cookie\n // to prevent client-side spoofing of location data\n const trustedCookie = cookieGeo.ip ? {\n ip: cookieGeo.ip,\n timezone: cookieGeo.timezone,\n timezoneFromClient: cookieGeo.timezoneFromClient,\n } : undefined;\n\n const geo = reconcileGeo({\n cookie: trustedCookie,\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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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Geo } from "@netloc8/netloc8-js";
|
|
2
|
+
|
|
3
|
+
//#region src/server.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Get the full geolocation data for the current request.
|
|
6
|
+
* Must be called from a Server Component, Route Handler, or Server Action.
|
|
7
|
+
*
|
|
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.
|
|
10
|
+
*/
|
|
11
|
+
declare function getGeo(): Promise<Geo>;
|
|
12
|
+
/**
|
|
13
|
+
* Get the timezone for the current request.
|
|
14
|
+
* Shorthand for (await getGeo()).timezone.
|
|
15
|
+
*/
|
|
16
|
+
declare function getTimezone(): Promise<string | undefined>;
|
|
17
|
+
//#endregion
|
|
18
|
+
export { getGeo, getTimezone };
|
|
19
|
+
//# sourceMappingURL=server.d.mts.map
|
|
@@ -0,0 +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"}
|
package/dist/server.mjs
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
//#region src/server.ts
|
|
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
|
+
|
|
141
|
+
//# sourceMappingURL=server.mjs.map
|
|
@@ -0,0 +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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@netloc8/nextjs",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"license": "Elastic-2.0",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"bun": "./src/index.ts",
|
|
9
|
+
"import": "./dist/index.mjs",
|
|
10
|
+
"types": "./dist/index.d.mts"
|
|
11
|
+
},
|
|
12
|
+
"./proxy": {
|
|
13
|
+
"bun": "./src/proxy.ts",
|
|
14
|
+
"import": "./dist/proxy.mjs",
|
|
15
|
+
"types": "./dist/proxy.d.mts"
|
|
16
|
+
},
|
|
17
|
+
"./server": {
|
|
18
|
+
"bun": "./src/server.ts",
|
|
19
|
+
"import": "./dist/server.mjs",
|
|
20
|
+
"types": "./dist/server.d.mts"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist/",
|
|
25
|
+
"proxy/",
|
|
26
|
+
"server/"
|
|
27
|
+
],
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "tsdown",
|
|
30
|
+
"clean": "rm -rf dist",
|
|
31
|
+
"test": "bun test src/"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"next": ">=16.0.0",
|
|
35
|
+
"react": ">=19.0.0"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@netloc8/netloc8-js": "0.1.0",
|
|
39
|
+
"@netloc8/react": "0.1.0"
|
|
40
|
+
}
|
|
41
|
+
}
|