@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 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)
@@ -0,0 +1,3 @@
1
+
2
+ import { GeoContext, GeoGate, NetLoc8Provider, useGeo } from "@netloc8/react";
3
+ export { GeoContext, GeoGate, NetLoc8Provider, useGeo };
package/dist/index.mjs ADDED
@@ -0,0 +1,3 @@
1
+ "use client";
2
+ import { GeoContext, GeoGate, NetLoc8Provider, useGeo } from "@netloc8/react";
3
+ export { GeoContext, GeoGate, NetLoc8Provider, useGeo };
@@ -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"}
@@ -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
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "main": "../dist/proxy.mjs",
3
+ "types": "../dist/proxy.d.mts"
4
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "main": "../dist/server.mjs",
3
+ "types": "../dist/server.d.mts"
4
+ }