@netloc8/netloc8-js 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,71 @@
1
+ # @netloc8/netloc8-js
2
+
3
+ Zero-dependency core library for the NetLoc8 geolocation SDK. Provides the
4
+ API client, IP detection, platform header parsing, cookie management,
5
+ and geo-source reconciliation.
6
+
7
+ > **Tip:** If you're using Next.js, install [`@netloc8/nextjs`](../nextjs/)
8
+ > instead — it re-exports everything from this package. For React SPAs,
9
+ > see [`@netloc8/react`](../react/).
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ bun add @netloc8/netloc8-js
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ ### Fetch geo data for an IP
20
+
21
+ ```typescript
22
+ import { fetchGeo } from '@netloc8/netloc8-js';
23
+
24
+ const geo = await fetchGeo( '203.0.113.42', {
25
+ apiKey: 'sk_...',
26
+ } );
27
+
28
+ console.log( geo );
29
+ // { ip, country, region, city, timezone, ... }
30
+ ```
31
+
32
+ ### Fetch only the timezone
33
+
34
+ ```typescript
35
+ import { fetchTimezone } from '@netloc8/netloc8-js';
36
+
37
+ const tz = await fetchTimezone( '203.0.113.42', {
38
+ apiKey: 'sk_...',
39
+ } );
40
+
41
+ console.log( tz ); // "America/Chicago"
42
+ ```
43
+
44
+ ### Client-side (browser) — fetch for the caller's own IP
45
+
46
+ ```typescript
47
+ import { fetchMyGeo, fetchMyTimezone } from '@netloc8/netloc8-js';
48
+
49
+ const geo = await fetchMyGeo( { apiKey: 'pk_...' } );
50
+ const tz = await fetchMyTimezone( { apiKey: 'pk_...' } );
51
+ ```
52
+
53
+ ## Exports
54
+
55
+ | Export | Description |
56
+ |--------|-------------|
57
+ | `fetchGeo` | Full geo lookup by IP |
58
+ | `fetchTimezone` | Timezone-only lookup by IP |
59
+ | `fetchMyGeo` | Full geo lookup for the caller's own IP (browser) |
60
+ | `fetchMyTimezone` | Timezone-only lookup for the caller's own IP (browser) |
61
+ | `getClientIp` | Extract client IP from request headers |
62
+ | `isPublicIp` | Check whether an IP is publicly routable |
63
+ | `getGeoFromPlatformHeaders` | Parse Vercel / Cloudflare / CloudFront geo headers |
64
+ | `parseCookie` / `serializeCookie` | Read/write the `__netloc8` cookie |
65
+ | `reconcileGeo` | Merge cookie, platform, and API geo sources |
66
+ | `normalizeApiResponse` | Normalize raw API JSON into a `Geo` object |
67
+ | `Geo` (type) | The shared geolocation type used across all packages |
68
+
69
+ ## License
70
+
71
+ [Elastic License 2.0 (ELv2)](../../LICENSE)
@@ -0,0 +1,147 @@
1
+ //#region src/types.d.ts
2
+ interface Geo {
3
+ ip?: string;
4
+ ipVersion?: number;
5
+ continent?: string;
6
+ continentName?: string;
7
+ country?: string;
8
+ countryName?: string;
9
+ isEU?: boolean;
10
+ region?: string;
11
+ regionName?: string;
12
+ city?: string;
13
+ postalCode?: string;
14
+ latitude?: number;
15
+ longitude?: number;
16
+ timezone?: string;
17
+ accuracyRadius?: number;
18
+ precision?: string;
19
+ isLimited?: boolean;
20
+ limitReason?: string;
21
+ timezoneFromClient?: boolean;
22
+ }
23
+ interface FetchGeoOptions {
24
+ apiKey?: string;
25
+ apiUrl?: string;
26
+ timeout?: number;
27
+ clientId?: string;
28
+ }
29
+ interface CookieOptions {
30
+ path?: string;
31
+ httpOnly?: boolean;
32
+ secure?: boolean;
33
+ sameSite?: "strict" | "lax" | "none";
34
+ maxAge?: number;
35
+ }
36
+ //#endregion
37
+ //#region src/api.d.ts
38
+ /**
39
+ * Fetch geolocation data for an IP address from the NetLoc8 API.
40
+ *
41
+ * Returns the raw API JSON or null on error/timeout.
42
+ * Never throws — errors are swallowed with a console.warn for debugging.
43
+ */
44
+ declare function fetchGeo(ipAddress: string, options?: FetchGeoOptions): Promise<Record<string, unknown> | null>;
45
+ /**
46
+ * Fetch only the timezone for an IP address.
47
+ *
48
+ * Returns the IANA timezone string or null.
49
+ */
50
+ declare function fetchTimezone(ipAddress: string, options?: FetchGeoOptions): Promise<string | null>;
51
+ /**
52
+ * Fetch geolocation data for the caller's own IP from the NetLoc8 API.
53
+ *
54
+ * Calls GET /api/v1/ip/me which auto-detects the caller's IP and returns
55
+ * the full Geo response. Intended for browser-side usage with a publishable
56
+ * key (pk_).
57
+ *
58
+ * Returns the raw API JSON or null on error/timeout.
59
+ * Never throws — errors are swallowed with a console.warn for debugging.
60
+ */
61
+ declare function fetchMyGeo(options?: FetchGeoOptions): Promise<Record<string, unknown> | null>;
62
+ /**
63
+ * Fetch only the timezone for the caller's own IP.
64
+ *
65
+ * Calls GET /api/v1/ip/me/timezone which auto-detects the caller's IP.
66
+ * Intended for browser-side usage with a publishable key (pk_).
67
+ *
68
+ * Returns the IANA timezone string or null.
69
+ */
70
+ declare function fetchMyTimezone(options?: FetchGeoOptions): Promise<string | null>;
71
+ //#endregion
72
+ //#region src/ip.d.ts
73
+ /**
74
+ * Normalize an IP string: strip ::ffff: prefix, brackets, lowercase.
75
+ */
76
+ declare function normalizeIp(ip: string | undefined): string | undefined;
77
+ /**
78
+ * Return true if the IP is publicly routable
79
+ * (not RFC1918, loopback, link-local, CGNAT, ULA).
80
+ */
81
+ declare function isPublicIp(ip: string): boolean;
82
+ /**
83
+ * Determine the most likely real client IP from request headers.
84
+ * Returns the first public IP found in the priority chain.
85
+ */
86
+ declare function getClientIp(headers: Headers): string | undefined;
87
+ //#endregion
88
+ //#region src/platform.d.ts
89
+ /**
90
+ * Extract geo information from platform/CDN request headers.
91
+ * Supports Vercel, Cloudflare, and CloudFront.
92
+ *
93
+ * Returns a partial Geo object — fields not provided by the platform are absent.
94
+ */
95
+ declare function getGeoFromPlatformHeaders(headers: Headers): Partial<Geo>;
96
+ //#endregion
97
+ //#region src/cookie.d.ts
98
+ /** Cookie name used by the plugin. */
99
+ declare const COOKIE_NAME = "__netloc8_geo";
100
+ /** Default cookie options. `secure` is disabled in development for http://localhost. */
101
+ declare const COOKIE_OPTIONS: CookieOptions;
102
+ /**
103
+ * Serialize a Geo object into a cookie value string (JSON, URI-encoded).
104
+ */
105
+ declare function serializeCookie(geo: Geo): string;
106
+ /**
107
+ * Parse a cookie value string back into a Geo object.
108
+ * Returns an empty object if corrupt or unparseable.
109
+ */
110
+ declare function parseCookie(value: string | undefined): Partial<Geo>;
111
+ //#endregion
112
+ //#region src/normalize.d.ts
113
+ /**
114
+ * Normalize a NetLoc8 API response into the SDK's Geo type.
115
+ *
116
+ * The API returns a flat structure that matches Geo closely.
117
+ * This function copies the fields and adds timezoneFromClient.
118
+ */
119
+ declare function normalizeApiResponse(raw: Record<string, unknown>, ip?: string): Geo;
120
+ //#endregion
121
+ //#region src/reconcile.d.ts
122
+ interface ReconcileSources {
123
+ cookie?: Partial<Geo>;
124
+ platform?: Partial<Geo>;
125
+ api?: Partial<Geo>;
126
+ ip?: string;
127
+ }
128
+ /**
129
+ * Merge geo data from multiple sources into a single authoritative Geo object.
130
+ *
131
+ * Source priority (highest to lowest):
132
+ * 1. Fresh API response (most complete)
133
+ * 2. Platform headers (zero-cost, may be partial)
134
+ * 3. Cookie (including timezoneFromClient) (stale, client-controlled)
135
+ *
136
+ * Cookie location fields are never treated as fully authoritative; they are
137
+ * only used as a fallback when no better data is available. The browser-
138
+ * confirmed timezone (`timezoneFromClient`) is trusted when the IP matches.
139
+ */
140
+ declare function reconcileGeo(sources: ReconcileSources): Geo;
141
+ //#endregion
142
+ //#region src/constants.d.ts
143
+ /** SDK client identifier sent as X-NetLoc8-Client on every API request. */
144
+ declare const CLIENT_ID: string;
145
+ //#endregion
146
+ export { CLIENT_ID, COOKIE_NAME, COOKIE_OPTIONS, CookieOptions, FetchGeoOptions, Geo, ReconcileSources, fetchGeo, fetchMyGeo, fetchMyTimezone, fetchTimezone, getClientIp, getGeoFromPlatformHeaders, isPublicIp, normalizeApiResponse, normalizeIp, parseCookie, reconcileGeo, serializeCookie };
147
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/api.ts","../src/ip.ts","../src/platform.ts","../src/cookie.ts","../src/normalize.ts","../src/reconcile.ts","../src/constants.ts"],"mappings":";UAAiB,GAAA;EACb,EAAA;EACA,SAAA;EACA,SAAA;EACA,aAAA;EACA,OAAA;EACA,WAAA;EACA,IAAA;EACA,MAAA;EACA,UAAA;EACA,IAAA;EACA,UAAA;EACA,QAAA;EACA,SAAA;EACA,QAAA;EACA,cAAA;EACA,SAAA;EACA,SAAA;EACA,WAAA;EACA,kBAAA;AAAA;AAAA,UAGa,eAAA;EACb,MAAA;EACA,MAAA;EACA,OAAA;EACA,QAAA;AAAA;AAAA,UAGa,aAAA;EACb,IAAA;EACA,QAAA;EACA,MAAA;EACA,QAAA;EACA,MAAA;AAAA;;;AAlCJ;;;;;;AAAA,iBCSsB,QAAA,CAClB,SAAA,UACA,OAAA,GAAU,eAAA,GACX,OAAA,CAAQ,MAAA;;;;;;iBAwCW,aAAA,CAClB,SAAA,UACA,OAAA,GAAU,eAAA,GACX,OAAA;;;;;;;;;;;iBA6CmB,UAAA,CAClB,OAAA,GAAU,eAAA,GACX,OAAA,CAAQ,MAAA;;ADhFX;;;;;;;iBC2HsB,eAAA,CAClB,OAAA,GAAU,eAAA,GACX,OAAA;;;;ADnJH;;iBEGgB,WAAA,CAAY,EAAA;;;;;iBAwBZ,UAAA,CAAW,EAAA;;;;;iBAoFX,WAAA,CAAY,OAAA,EAAS,OAAA;;;AF/GrC;;;;;;AAAA,iBGQgB,yBAAA,CAA0B,OAAA,EAAS,OAAA,GAAU,OAAA,CAAQ,GAAA;;;AHRrE;AAAA,cIGa,WAAA;;cAGA,cAAA,EAAgB,aAAA;;;;iBAWb,eAAA,CAAgB,GAAA,EAAK,GAAA;;;;;iBAQrB,WAAA,CAAY,KAAA,uBAA4B,OAAA,CAAQ,GAAA;;;AJzBhE;;;;;;AAAA,iBKQgB,oBAAA,CACZ,GAAA,EAAK,MAAA,mBACL,EAAA,YACD,GAAA;;;UCTc,gBAAA;EACb,MAAA,GAAS,OAAA,CAAQ,GAAA;EACjB,QAAA,GAAW,OAAA,CAAQ,GAAA;EACnB,GAAA,GAAM,OAAA,CAAQ,GAAA;EACd,EAAA;AAAA;;;;;;;;;;;;;iBAeY,YAAA,CAAa,OAAA,EAAS,gBAAA,GAAmB,GAAA;;;;cCjB5C,SAAA"}
package/dist/index.mjs ADDED
@@ -0,0 +1,379 @@
1
+ //#region src/constants.ts
2
+ /** SDK client identifier sent as X-NetLoc8-Client on every API request. */
3
+ const CLIENT_ID = `@netloc8/netloc8-js/0.1.0`;
4
+ //#endregion
5
+ //#region src/api.ts
6
+ /**
7
+ * Fetch geolocation data for an IP address from the NetLoc8 API.
8
+ *
9
+ * Returns the raw API JSON or null on error/timeout.
10
+ * Never throws — errors are swallowed with a console.warn for debugging.
11
+ */
12
+ async function fetchGeo(ipAddress, options) {
13
+ const apiKey = options?.apiKey ?? process.env.NETLOC8_API_KEY;
14
+ const apiUrl = options?.apiUrl ?? process.env.NETLOC8_API_URL ?? "https://netloc8.com";
15
+ const timeout = options?.timeout ?? 1500;
16
+ const clientId = options?.clientId ?? CLIENT_ID;
17
+ if (!apiKey) {
18
+ console.warn("[netloc8] No API key provided. Set NETLOC8_API_KEY or pass apiKey in options.");
19
+ return null;
20
+ }
21
+ const url = `${apiUrl}/api/v1/ip/${encodeURIComponent(ipAddress)}`;
22
+ try {
23
+ const response = await fetch(url, {
24
+ method: "GET",
25
+ headers: {
26
+ "X-API-Key": apiKey,
27
+ "X-NetLoc8-Client": clientId,
28
+ "Accept": "application/json"
29
+ },
30
+ signal: AbortSignal.timeout(timeout)
31
+ });
32
+ if (!response.ok) return null;
33
+ return await response.json();
34
+ } catch (error) {
35
+ console.warn(`[netloc8] Geo lookup failed for ${ipAddress}: ${error.message}`);
36
+ return null;
37
+ }
38
+ }
39
+ /**
40
+ * Fetch only the timezone for an IP address.
41
+ *
42
+ * Returns the IANA timezone string or null.
43
+ */
44
+ async function fetchTimezone(ipAddress, options) {
45
+ const apiKey = options?.apiKey ?? process.env.NETLOC8_API_KEY;
46
+ const apiUrl = options?.apiUrl ?? process.env.NETLOC8_API_URL ?? "https://netloc8.com";
47
+ const timeout = options?.timeout ?? 1500;
48
+ const clientId = options?.clientId ?? CLIENT_ID;
49
+ if (!apiKey) {
50
+ console.warn("[netloc8] No API key provided. Set NETLOC8_API_KEY or pass apiKey in options.");
51
+ return null;
52
+ }
53
+ const url = `${apiUrl}/api/v1/ip/${encodeURIComponent(ipAddress)}/timezone`;
54
+ try {
55
+ const response = await fetch(url, {
56
+ method: "GET",
57
+ headers: {
58
+ "X-API-Key": apiKey,
59
+ "X-NetLoc8-Client": clientId,
60
+ "Accept": "application/json"
61
+ },
62
+ signal: AbortSignal.timeout(timeout)
63
+ });
64
+ if (!response.ok) return null;
65
+ return await response.json();
66
+ } catch (error) {
67
+ console.warn(`[netloc8] Timezone lookup failed for ${ipAddress}: ${error.message}`);
68
+ return null;
69
+ }
70
+ }
71
+ /**
72
+ * Fetch geolocation data for the caller's own IP from the NetLoc8 API.
73
+ *
74
+ * Calls GET /api/v1/ip/me which auto-detects the caller's IP and returns
75
+ * the full Geo response. Intended for browser-side usage with a publishable
76
+ * key (pk_).
77
+ *
78
+ * Returns the raw API JSON or null on error/timeout.
79
+ * Never throws — errors are swallowed with a console.warn for debugging.
80
+ */
81
+ async function fetchMyGeo(options) {
82
+ const apiKey = options?.apiKey ?? process.env.NETLOC8_API_KEY;
83
+ const apiUrl = options?.apiUrl ?? process.env.NETLOC8_API_URL ?? "https://netloc8.com";
84
+ const timeout = options?.timeout ?? 1500;
85
+ const clientId = options?.clientId ?? CLIENT_ID;
86
+ if (!apiKey) {
87
+ console.warn("[netloc8] No API key provided. Set NETLOC8_API_KEY or pass apiKey in options.");
88
+ return null;
89
+ }
90
+ const url = `${apiUrl}/api/v1/ip/me`;
91
+ try {
92
+ const response = await fetch(url, {
93
+ method: "GET",
94
+ headers: {
95
+ "X-API-Key": apiKey,
96
+ "X-NetLoc8-Client": clientId,
97
+ "Accept": "application/json"
98
+ },
99
+ signal: AbortSignal.timeout(timeout)
100
+ });
101
+ if (!response.ok) return null;
102
+ return await response.json();
103
+ } catch (error) {
104
+ console.warn(`[netloc8] Self geo lookup failed: ${error.message}`);
105
+ return null;
106
+ }
107
+ }
108
+ /**
109
+ * Fetch only the timezone for the caller's own IP.
110
+ *
111
+ * Calls GET /api/v1/ip/me/timezone which auto-detects the caller's IP.
112
+ * Intended for browser-side usage with a publishable key (pk_).
113
+ *
114
+ * Returns the IANA timezone string or null.
115
+ */
116
+ async function fetchMyTimezone(options) {
117
+ const apiKey = options?.apiKey ?? process.env.NETLOC8_API_KEY;
118
+ const apiUrl = options?.apiUrl ?? process.env.NETLOC8_API_URL ?? "https://netloc8.com";
119
+ const timeout = options?.timeout ?? 1500;
120
+ const clientId = options?.clientId ?? CLIENT_ID;
121
+ if (!apiKey) {
122
+ console.warn("[netloc8] No API key provided. Set NETLOC8_API_KEY or pass apiKey in options.");
123
+ return null;
124
+ }
125
+ const url = `${apiUrl}/api/v1/ip/me/timezone`;
126
+ try {
127
+ const response = await fetch(url, {
128
+ method: "GET",
129
+ headers: {
130
+ "X-API-Key": apiKey,
131
+ "X-NetLoc8-Client": clientId,
132
+ "Accept": "application/json"
133
+ },
134
+ signal: AbortSignal.timeout(timeout)
135
+ });
136
+ if (!response.ok) return null;
137
+ return await response.json();
138
+ } catch (error) {
139
+ console.warn(`[netloc8] Self timezone lookup failed: ${error.message}`);
140
+ return null;
141
+ }
142
+ }
143
+ //#endregion
144
+ //#region src/ip.ts
145
+ /**
146
+ * Normalize an IP string: strip ::ffff: prefix, brackets, lowercase.
147
+ */
148
+ function normalizeIp(ip) {
149
+ if (!ip) return;
150
+ let normalized = ip.trim();
151
+ if (normalized.startsWith("[") && normalized.endsWith("]")) normalized = normalized.slice(1, -1);
152
+ if (normalized.toLowerCase().startsWith("::ffff:")) normalized = normalized.slice(7);
153
+ return normalized.toLowerCase();
154
+ }
155
+ /**
156
+ * Return true if the IP is publicly routable
157
+ * (not RFC1918, loopback, link-local, CGNAT, ULA).
158
+ */
159
+ function isPublicIp(ip) {
160
+ const normalized = normalizeIp(ip);
161
+ if (!normalized) return false;
162
+ if (!normalized.includes(":")) {
163
+ const parts = normalized.split(".").map(Number);
164
+ if (parts.length !== 4) return false;
165
+ if (parts.some((p) => !Number.isInteger(p) || p < 0 || p > 255)) return false;
166
+ if (parts[0] === 127) return false;
167
+ if (parts[0] === 10) return false;
168
+ if (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) return false;
169
+ if (parts[0] === 192 && parts[1] === 168) return false;
170
+ if (parts[0] === 100 && parts[1] >= 64 && parts[1] <= 127) return false;
171
+ if (parts[0] === 169 && parts[1] === 254) return false;
172
+ if (parts.every((p) => p === 0)) return false;
173
+ return true;
174
+ }
175
+ if (normalized === "::1") return false;
176
+ if (normalized.startsWith("fc") || normalized.startsWith("fd")) return false;
177
+ if (normalized.startsWith("fe80")) return false;
178
+ if (normalized === "::") return false;
179
+ return true;
180
+ }
181
+ /**
182
+ * Determine the most likely real client IP from request headers.
183
+ * Returns the first public IP found in the priority chain.
184
+ */
185
+ function getClientIp(headers) {
186
+ const xff = headers.get("x-forwarded-for");
187
+ if (xff) {
188
+ const publicIp = xff.split(",").map((s) => normalizeIp(s.trim())).filter(Boolean).find((ip) => isPublicIp(ip));
189
+ if (publicIp) return publicIp;
190
+ }
191
+ const singleHeaders = [
192
+ "cf-connecting-ip",
193
+ "true-client-ip",
194
+ "x-real-ip",
195
+ "x-client-ip",
196
+ "fastly-client-ip",
197
+ "fly-client-ip"
198
+ ];
199
+ for (const header of singleHeaders) {
200
+ const value = headers.get(header);
201
+ if (value) {
202
+ const normalized = normalizeIp(value);
203
+ if (normalized && isPublicIp(normalized)) return normalized;
204
+ }
205
+ }
206
+ if (xff) {
207
+ const ips = xff.split(",").map((s) => normalizeIp(s.trim())).filter(Boolean);
208
+ if (ips.length > 0) return ips[0];
209
+ }
210
+ for (const header of singleHeaders) {
211
+ const value = headers.get(header);
212
+ if (value) {
213
+ const normalized = normalizeIp(value);
214
+ if (normalized) return normalized;
215
+ }
216
+ }
217
+ }
218
+ //#endregion
219
+ //#region src/platform.ts
220
+ /**
221
+ * Extract geo information from platform/CDN request headers.
222
+ * Supports Vercel, Cloudflare, and CloudFront.
223
+ *
224
+ * Returns a partial Geo object — fields not provided by the platform are absent.
225
+ */
226
+ function getGeoFromPlatformHeaders(headers) {
227
+ const geo = {};
228
+ const vercelCountry = headers.get("x-vercel-ip-country");
229
+ if (vercelCountry) geo.country = vercelCountry;
230
+ const vercelRegion = headers.get("x-vercel-ip-country-region");
231
+ if (vercelRegion) geo.region = vercelRegion;
232
+ const vercelCity = headers.get("x-vercel-ip-city");
233
+ if (vercelCity !== null) try {
234
+ geo.city = decodeURIComponent(vercelCity);
235
+ } catch {
236
+ geo.city = vercelCity;
237
+ }
238
+ const vercelLat = headers.get("x-vercel-ip-latitude");
239
+ if (vercelLat) {
240
+ const lat = parseFloat(vercelLat);
241
+ if (isFinite(lat)) geo.latitude = lat;
242
+ }
243
+ const vercelLng = headers.get("x-vercel-ip-longitude");
244
+ if (vercelLng) {
245
+ const lng = parseFloat(vercelLng);
246
+ if (isFinite(lng)) geo.longitude = lng;
247
+ }
248
+ const vercelTz = headers.get("x-vercel-ip-timezone");
249
+ if (vercelTz) geo.timezone = vercelTz;
250
+ const cfCountry = headers.get("cf-ipcountry");
251
+ if (cfCountry && !geo.country) geo.country = cfCountry;
252
+ const cfrontCountry = headers.get("cloudfront-viewer-country");
253
+ if (cfrontCountry && !geo.country) geo.country = cfrontCountry;
254
+ return geo;
255
+ }
256
+ //#endregion
257
+ //#region src/cookie.ts
258
+ /** Cookie name used by the plugin. */
259
+ const COOKIE_NAME = "__netloc8_geo";
260
+ /** Default cookie options. `secure` is disabled in development for http://localhost. */
261
+ const COOKIE_OPTIONS = {
262
+ path: "/",
263
+ httpOnly: false,
264
+ secure: process.env.NODE_ENV === "production",
265
+ sameSite: "lax",
266
+ maxAge: 2592e3
267
+ };
268
+ /**
269
+ * Serialize a Geo object into a cookie value string (JSON, URI-encoded).
270
+ */
271
+ function serializeCookie(geo) {
272
+ return encodeURIComponent(JSON.stringify(geo));
273
+ }
274
+ /**
275
+ * Parse a cookie value string back into a Geo object.
276
+ * Returns an empty object if corrupt or unparseable.
277
+ */
278
+ function parseCookie(value) {
279
+ if (!value) return {};
280
+ try {
281
+ const decoded = decodeURIComponent(value);
282
+ const parsed = JSON.parse(decoded);
283
+ if (typeof parsed !== "object" || parsed === null) return {};
284
+ const safeGeo = {};
285
+ if (typeof parsed.ip === "string") safeGeo.ip = parsed.ip;
286
+ if (typeof parsed.ipVersion === "number") safeGeo.ipVersion = parsed.ipVersion;
287
+ if (typeof parsed.continent === "string") safeGeo.continent = parsed.continent;
288
+ if (typeof parsed.continentName === "string") safeGeo.continentName = parsed.continentName;
289
+ if (typeof parsed.country === "string") safeGeo.country = parsed.country;
290
+ if (typeof parsed.countryName === "string") safeGeo.countryName = parsed.countryName;
291
+ if (typeof parsed.isEU === "boolean") safeGeo.isEU = parsed.isEU;
292
+ if (typeof parsed.region === "string") safeGeo.region = parsed.region;
293
+ if (typeof parsed.regionName === "string") safeGeo.regionName = parsed.regionName;
294
+ if (typeof parsed.city === "string") safeGeo.city = parsed.city;
295
+ if (typeof parsed.postalCode === "string") safeGeo.postalCode = parsed.postalCode;
296
+ if (typeof parsed.latitude === "number") safeGeo.latitude = parsed.latitude;
297
+ if (typeof parsed.longitude === "number") safeGeo.longitude = parsed.longitude;
298
+ if (typeof parsed.timezone === "string") safeGeo.timezone = parsed.timezone;
299
+ if (typeof parsed.accuracyRadius === "number") safeGeo.accuracyRadius = parsed.accuracyRadius;
300
+ if (typeof parsed.precision === "string") safeGeo.precision = parsed.precision;
301
+ if (typeof parsed.isLimited === "boolean") safeGeo.isLimited = parsed.isLimited;
302
+ if (typeof parsed.limitReason === "string") safeGeo.limitReason = parsed.limitReason;
303
+ if (typeof parsed.timezoneFromClient === "boolean") safeGeo.timezoneFromClient = parsed.timezoneFromClient;
304
+ return safeGeo;
305
+ } catch {
306
+ return {};
307
+ }
308
+ }
309
+ //#endregion
310
+ //#region src/normalize.ts
311
+ /**
312
+ * Normalize a NetLoc8 API response into the SDK's Geo type.
313
+ *
314
+ * The API returns a flat structure that matches Geo closely.
315
+ * This function copies the fields and adds timezoneFromClient.
316
+ */
317
+ function normalizeApiResponse(raw, ip) {
318
+ return {
319
+ ip: raw.ip ?? ip,
320
+ ipVersion: raw.ipVersion,
321
+ continent: raw.continent,
322
+ continentName: raw.continentName,
323
+ country: raw.country,
324
+ countryName: raw.countryName,
325
+ isEU: raw.isEU,
326
+ region: raw.region,
327
+ regionName: raw.regionName,
328
+ city: raw.city,
329
+ postalCode: raw.postalCode,
330
+ latitude: raw.latitude,
331
+ longitude: raw.longitude,
332
+ timezone: raw.timezone,
333
+ accuracyRadius: raw.accuracyRadius,
334
+ precision: raw.precision,
335
+ isLimited: raw.isLimited,
336
+ limitReason: raw.limitReason,
337
+ timezoneFromClient: false
338
+ };
339
+ }
340
+ //#endregion
341
+ //#region src/reconcile.ts
342
+ /**
343
+ * Merge geo data from multiple sources into a single authoritative Geo object.
344
+ *
345
+ * Source priority (highest to lowest):
346
+ * 1. Fresh API response (most complete)
347
+ * 2. Platform headers (zero-cost, may be partial)
348
+ * 3. Cookie (including timezoneFromClient) (stale, client-controlled)
349
+ *
350
+ * Cookie location fields are never treated as fully authoritative; they are
351
+ * only used as a fallback when no better data is available. The browser-
352
+ * confirmed timezone (`timezoneFromClient`) is trusted when the IP matches.
353
+ */
354
+ function reconcileGeo(sources) {
355
+ const { cookie, platform, api, ip } = sources;
356
+ const geo = { ip };
357
+ if (cookie) Object.assign(geo, stripUndefined(cookie));
358
+ if (platform) Object.assign(geo, stripUndefined(platform));
359
+ if (api) Object.assign(geo, stripUndefined(api));
360
+ if (cookie?.timezoneFromClient === true && cookie?.ip !== ip && cookie?.timezone) {
361
+ geo.timezone = cookie.timezone;
362
+ geo.timezoneFromClient = false;
363
+ }
364
+ geo.ip = ip;
365
+ return geo;
366
+ }
367
+ /**
368
+ * Remove undefined values from an object so Object.assign doesn't overwrite
369
+ * existing values with undefined.
370
+ */
371
+ function stripUndefined(obj) {
372
+ const result = {};
373
+ for (const [key, value] of Object.entries(obj)) if (value !== void 0) result[key] = value;
374
+ return result;
375
+ }
376
+ //#endregion
377
+ export { CLIENT_ID, COOKIE_NAME, COOKIE_OPTIONS, fetchGeo, fetchMyGeo, fetchMyTimezone, fetchTimezone, getClientIp, getGeoFromPlatformHeaders, isPublicIp, normalizeApiResponse, normalizeIp, parseCookie, reconcileGeo, serializeCookie };
378
+
379
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/constants.ts","../src/api.ts","../src/ip.ts","../src/platform.ts","../src/cookie.ts","../src/normalize.ts","../src/reconcile.ts"],"sourcesContent":["declare const __PKG_NAME__: string;\ndeclare const __PKG_VERSION__: string;\n\n/** SDK client identifier sent as X-NetLoc8-Client on every API request. */\nexport const CLIENT_ID: string = `${typeof __PKG_NAME__ !== 'undefined' ? __PKG_NAME__ : '@netloc8/netloc8-js'}/${typeof __PKG_VERSION__ !== 'undefined' ? __PKG_VERSION__ : 'dev'}`;\n","import type { FetchGeoOptions } from './types';\nimport { CLIENT_ID } from './constants';\n\n/**\n * Fetch geolocation data for an IP address from the NetLoc8 API.\n *\n * Returns the raw API JSON or null on error/timeout.\n * Never throws — errors are swallowed with a console.warn for debugging.\n */\nexport async function fetchGeo(\n ipAddress: string,\n options?: FetchGeoOptions\n): Promise<Record<string, unknown> | null> {\n const apiKey = options?.apiKey ?? process.env.NETLOC8_API_KEY;\n const apiUrl = options?.apiUrl ?? process.env.NETLOC8_API_URL ?? 'https://netloc8.com';\n const timeout = options?.timeout ?? 1500;\n const clientId = options?.clientId ?? CLIENT_ID;\n\n if (!apiKey) {\n console.warn('[netloc8] No API key provided. Set NETLOC8_API_KEY or pass apiKey in options.');\n return null;\n }\n\n const url = `${apiUrl}/api/v1/ip/${encodeURIComponent(ipAddress)}`;\n\n try {\n const response = await fetch(url, {\n method: 'GET',\n headers: {\n 'X-API-Key': apiKey,\n 'X-NetLoc8-Client': clientId,\n 'Accept': 'application/json',\n },\n signal: AbortSignal.timeout(timeout),\n });\n\n if (!response.ok) {\n return null;\n }\n\n return await response.json() as Record<string, unknown>;\n } catch (error) {\n console.warn(`[netloc8] Geo lookup failed for ${ipAddress}: ${(error as Error).message}`);\n return null;\n }\n}\n\n/**\n * Fetch only the timezone for an IP address.\n *\n * Returns the IANA timezone string or null.\n */\nexport async function fetchTimezone(\n ipAddress: string,\n options?: FetchGeoOptions\n): Promise<string | null> {\n const apiKey = options?.apiKey ?? process.env.NETLOC8_API_KEY;\n const apiUrl = options?.apiUrl ?? process.env.NETLOC8_API_URL ?? 'https://netloc8.com';\n const timeout = options?.timeout ?? 1500;\n const clientId = options?.clientId ?? CLIENT_ID;\n\n if (!apiKey) {\n console.warn('[netloc8] No API key provided. Set NETLOC8_API_KEY or pass apiKey in options.');\n return null;\n }\n\n const url = `${apiUrl}/api/v1/ip/${encodeURIComponent(ipAddress)}/timezone`;\n\n try {\n const response = await fetch(url, {\n method: 'GET',\n headers: {\n 'X-API-Key': apiKey,\n 'X-NetLoc8-Client': clientId,\n 'Accept': 'application/json',\n },\n signal: AbortSignal.timeout(timeout),\n });\n\n if (!response.ok) {\n return null;\n }\n\n return await response.json() as string;\n } catch (error) {\n console.warn(`[netloc8] Timezone lookup failed for ${ipAddress}: ${(error as Error).message}`);\n return null;\n }\n}\n\n/**\n * Fetch geolocation data for the caller's own IP from the NetLoc8 API.\n *\n * Calls GET /api/v1/ip/me which auto-detects the caller's IP and returns\n * the full Geo response. Intended for browser-side usage with a publishable\n * key (pk_).\n *\n * Returns the raw API JSON or null on error/timeout.\n * Never throws — errors are swallowed with a console.warn for debugging.\n */\nexport async function fetchMyGeo(\n options?: FetchGeoOptions\n): Promise<Record<string, unknown> | null> {\n const apiKey = options?.apiKey ?? process.env.NETLOC8_API_KEY;\n const apiUrl = options?.apiUrl ?? process.env.NETLOC8_API_URL ?? 'https://netloc8.com';\n const timeout = options?.timeout ?? 1500;\n const clientId = options?.clientId ?? CLIENT_ID;\n\n if (!apiKey) {\n console.warn('[netloc8] No API key provided. Set NETLOC8_API_KEY or pass apiKey in options.');\n return null;\n }\n\n const url = `${apiUrl}/api/v1/ip/me`;\n\n try {\n const response = await fetch(url, {\n method: 'GET',\n headers: {\n 'X-API-Key': apiKey,\n 'X-NetLoc8-Client': clientId,\n 'Accept': 'application/json',\n },\n signal: AbortSignal.timeout(timeout),\n });\n\n if (!response.ok) {\n return null;\n }\n\n return await response.json() as Record<string, unknown>;\n } catch (error) {\n console.warn(`[netloc8] Self geo lookup failed: ${(error as Error).message}`);\n return null;\n }\n}\n\n/**\n * Fetch only the timezone for the caller's own IP.\n *\n * Calls GET /api/v1/ip/me/timezone which auto-detects the caller's IP.\n * Intended for browser-side usage with a publishable key (pk_).\n *\n * Returns the IANA timezone string or null.\n */\nexport async function fetchMyTimezone(\n options?: FetchGeoOptions\n): Promise<string | null> {\n const apiKey = options?.apiKey ?? process.env.NETLOC8_API_KEY;\n const apiUrl = options?.apiUrl ?? process.env.NETLOC8_API_URL ?? 'https://netloc8.com';\n const timeout = options?.timeout ?? 1500;\n const clientId = options?.clientId ?? CLIENT_ID;\n\n if (!apiKey) {\n console.warn('[netloc8] No API key provided. Set NETLOC8_API_KEY or pass apiKey in options.');\n return null;\n }\n\n const url = `${apiUrl}/api/v1/ip/me/timezone`;\n\n try {\n const response = await fetch(url, {\n method: 'GET',\n headers: {\n 'X-API-Key': apiKey,\n 'X-NetLoc8-Client': clientId,\n 'Accept': 'application/json',\n },\n signal: AbortSignal.timeout(timeout),\n });\n\n if (!response.ok) {\n return null;\n }\n\n return await response.json() as string;\n } catch (error) {\n console.warn(`[netloc8] Self timezone lookup failed: ${(error as Error).message}`);\n return null;\n }\n}\n","/**\n * Normalize an IP string: strip ::ffff: prefix, brackets, lowercase.\n */\nexport function normalizeIp(ip: string | undefined): string | undefined {\n if (!ip) {\n return undefined;\n }\n\n let normalized = ip.trim();\n\n // Strip brackets (IPv6 in URLs)\n if (normalized.startsWith('[') && normalized.endsWith(']')) {\n normalized = normalized.slice(1, -1);\n }\n\n // Strip IPv4-mapped IPv6 prefix\n if (normalized.toLowerCase().startsWith('::ffff:')) {\n normalized = normalized.slice(7);\n }\n\n return normalized.toLowerCase();\n}\n\n/**\n * Return true if the IP is publicly routable\n * (not RFC1918, loopback, link-local, CGNAT, ULA).\n */\nexport function isPublicIp(ip: string): boolean {\n const normalized = normalizeIp(ip);\n if (!normalized) {\n return false;\n }\n\n // IPv4 checks\n if (!normalized.includes(':')) {\n const parts = normalized.split('.').map(Number);\n if (parts.length !== 4) {\n return false;\n }\n\n // Validate each octet is an integer in 0-255\n if (parts.some(p => !Number.isInteger(p) || p < 0 || p > 255)) {\n return false;\n }\n\n // Loopback: 127.0.0.0/8\n if (parts[0] === 127) {\n return false;\n }\n\n // RFC1918: 10.0.0.0/8\n if (parts[0] === 10) {\n return false;\n }\n\n // RFC1918: 172.16.0.0/12\n if (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) {\n return false;\n }\n\n // RFC1918: 192.168.0.0/16\n if (parts[0] === 192 && parts[1] === 168) {\n return false;\n }\n\n // CGNAT: 100.64.0.0/10\n if (parts[0] === 100 && parts[1] >= 64 && parts[1] <= 127) {\n return false;\n }\n\n // Link-local: 169.254.0.0/16\n if (parts[0] === 169 && parts[1] === 254) {\n return false;\n }\n\n // 0.0.0.0\n if (parts.every(p => p === 0)) {\n return false;\n }\n\n return true;\n }\n\n // IPv6 checks\n // Loopback: ::1\n if (normalized === '::1') {\n return false;\n }\n\n // ULA: fc00::/7\n if (normalized.startsWith('fc') || normalized.startsWith('fd')) {\n return false;\n }\n\n // Link-local: fe80::/10\n if (normalized.startsWith('fe80')) {\n return false;\n }\n\n // Unspecified: ::\n if (normalized === '::') {\n return false;\n }\n\n return true;\n}\n\n/**\n * Determine the most likely real client IP from request headers.\n * Returns the first public IP found in the priority chain.\n */\nexport function getClientIp(headers: Headers): string | undefined {\n // 1. x-forwarded-for — split by comma, return first public IP\n const xff = headers.get('x-forwarded-for');\n if (xff) {\n const ips = xff.split(',').map(s => normalizeIp(s.trim())).filter(Boolean) as string[];\n const publicIp = ips.find(ip => isPublicIp(ip));\n if (publicIp) {\n return publicIp;\n }\n }\n\n // 2. Single-IP headers in priority order\n const singleHeaders = [\n 'cf-connecting-ip',\n 'true-client-ip',\n 'x-real-ip',\n 'x-client-ip',\n 'fastly-client-ip',\n 'fly-client-ip',\n ];\n\n for (const header of singleHeaders) {\n const value = headers.get(header);\n if (value) {\n const normalized = normalizeIp(value);\n if (normalized && isPublicIp(normalized)) {\n return normalized;\n }\n }\n }\n\n // 3. Last resort: return first candidate from xff even if private\n if (xff) {\n const ips = xff.split(',').map(s => normalizeIp(s.trim())).filter(Boolean) as string[];\n if (ips.length > 0) {\n return ips[0];\n }\n }\n\n // 4. Check single-IP headers again, accepting private IPs\n for (const header of singleHeaders) {\n const value = headers.get(header);\n if (value) {\n const normalized = normalizeIp(value);\n if (normalized) {\n return normalized;\n }\n }\n }\n\n return undefined;\n}\n","import type { Geo } from './types';\n\n/**\n * Extract geo information from platform/CDN request headers.\n * Supports Vercel, Cloudflare, and CloudFront.\n *\n * Returns a partial Geo object — fields not provided by the platform are absent.\n */\nexport function getGeoFromPlatformHeaders(headers: Headers): Partial<Geo> {\n const geo: Partial<Geo> = {};\n\n // Vercel headers\n const vercelCountry = headers.get('x-vercel-ip-country');\n if (vercelCountry) {\n geo.country = vercelCountry;\n }\n\n const vercelRegion = headers.get('x-vercel-ip-country-region');\n if (vercelRegion) {\n geo.region = vercelRegion;\n }\n\n const vercelCity = headers.get('x-vercel-ip-city');\n if (vercelCity !== null) {\n try {\n geo.city = decodeURIComponent(vercelCity);\n } catch {\n geo.city = vercelCity;\n }\n }\n\n const vercelLat = headers.get('x-vercel-ip-latitude');\n if (vercelLat) {\n const lat = parseFloat(vercelLat);\n if (isFinite(lat)) {\n geo.latitude = lat;\n }\n }\n\n const vercelLng = headers.get('x-vercel-ip-longitude');\n if (vercelLng) {\n const lng = parseFloat(vercelLng);\n if (isFinite(lng)) {\n geo.longitude = lng;\n }\n }\n\n const vercelTz = headers.get('x-vercel-ip-timezone');\n if (vercelTz) {\n geo.timezone = vercelTz;\n }\n\n // Cloudflare headers\n const cfCountry = headers.get('cf-ipcountry');\n if (cfCountry && !geo.country) {\n geo.country = cfCountry;\n }\n\n // CloudFront headers\n const cfrontCountry = headers.get('cloudfront-viewer-country');\n if (cfrontCountry && !geo.country) {\n geo.country = cfrontCountry;\n }\n\n return geo;\n}\n","import type { Geo, CookieOptions } from './types';\n\n/** Cookie name used by the plugin. */\nexport const COOKIE_NAME = '__netloc8_geo';\n\n/** Default cookie options. `secure` is disabled in development for http://localhost. */\nexport const COOKIE_OPTIONS: CookieOptions = {\n path: '/',\n httpOnly: false,\n secure: process.env.NODE_ENV === 'production',\n sameSite: 'lax',\n maxAge: 2_592_000, // 30 days\n};\n\n/**\n * Serialize a Geo object into a cookie value string (JSON, URI-encoded).\n */\nexport function serializeCookie(geo: Geo): string {\n return encodeURIComponent(JSON.stringify(geo));\n}\n\n/**\n * Parse a cookie value string back into a Geo object.\n * Returns an empty object if corrupt or unparseable.\n */\nexport function parseCookie(value: string | undefined): Partial<Geo> {\n if (!value) {\n return {};\n }\n\n try {\n const decoded = decodeURIComponent(value);\n const parsed = JSON.parse(decoded);\n\n if (typeof parsed !== 'object' || parsed === null) {\n return {};\n }\n\n // Explicitly pick known properties to avoid prototype pollution or unexpected data\n const safeGeo: Partial<Geo> = {};\n\n if (typeof parsed.ip === 'string') safeGeo.ip = parsed.ip;\n if (typeof parsed.ipVersion === 'number') safeGeo.ipVersion = parsed.ipVersion;\n if (typeof parsed.continent === 'string') safeGeo.continent = parsed.continent;\n if (typeof parsed.continentName === 'string') safeGeo.continentName = parsed.continentName;\n if (typeof parsed.country === 'string') safeGeo.country = parsed.country;\n if (typeof parsed.countryName === 'string') safeGeo.countryName = parsed.countryName;\n if (typeof parsed.isEU === 'boolean') safeGeo.isEU = parsed.isEU;\n if (typeof parsed.region === 'string') safeGeo.region = parsed.region;\n if (typeof parsed.regionName === 'string') safeGeo.regionName = parsed.regionName;\n if (typeof parsed.city === 'string') safeGeo.city = parsed.city;\n if (typeof parsed.postalCode === 'string') safeGeo.postalCode = parsed.postalCode;\n if (typeof parsed.latitude === 'number') safeGeo.latitude = parsed.latitude;\n if (typeof parsed.longitude === 'number') safeGeo.longitude = parsed.longitude;\n if (typeof parsed.timezone === 'string') safeGeo.timezone = parsed.timezone;\n if (typeof parsed.accuracyRadius === 'number') safeGeo.accuracyRadius = parsed.accuracyRadius;\n if (typeof parsed.precision === 'string') safeGeo.precision = parsed.precision;\n if (typeof parsed.isLimited === 'boolean') safeGeo.isLimited = parsed.isLimited;\n if (typeof parsed.limitReason === 'string') safeGeo.limitReason = parsed.limitReason;\n if (typeof parsed.timezoneFromClient === 'boolean') safeGeo.timezoneFromClient = parsed.timezoneFromClient;\n\n return safeGeo;\n } catch {\n return {};\n }\n}\n","import type { Geo } from './types';\n\n/**\n * Normalize a NetLoc8 API response into the SDK's Geo type.\n *\n * The API returns a flat structure that matches Geo closely.\n * This function copies the fields and adds timezoneFromClient.\n */\nexport function normalizeApiResponse(\n raw: Record<string, unknown>,\n ip?: string\n): Geo {\n return {\n ip: (raw.ip as string) ?? ip,\n ipVersion: raw.ipVersion as number | undefined,\n continent: raw.continent as string | undefined,\n continentName: raw.continentName as string | undefined,\n country: raw.country as string | undefined,\n countryName: raw.countryName as string | undefined,\n isEU: raw.isEU as boolean | undefined,\n region: raw.region as string | undefined,\n regionName: raw.regionName as string | undefined,\n city: raw.city as string | undefined,\n postalCode: raw.postalCode as string | undefined,\n latitude: raw.latitude as number | undefined,\n longitude: raw.longitude as number | undefined,\n timezone: raw.timezone as string | undefined,\n accuracyRadius: raw.accuracyRadius as number | undefined,\n precision: raw.precision as string | undefined,\n isLimited: raw.isLimited as boolean | undefined,\n limitReason: raw.limitReason as string | undefined,\n timezoneFromClient: false,\n };\n}\n","import type { Geo } from './types';\n\nexport interface ReconcileSources {\n cookie?: Partial<Geo>;\n platform?: Partial<Geo>;\n api?: Partial<Geo>;\n ip?: string;\n}\n\n/**\n * Merge geo data from multiple sources into a single authoritative Geo object.\n *\n * Source priority (highest to lowest):\n * 1. Fresh API response (most complete)\n * 2. Platform headers (zero-cost, may be partial)\n * 3. Cookie (including timezoneFromClient) (stale, client-controlled)\n *\n * Cookie location fields are never treated as fully authoritative; they are\n * only used as a fallback when no better data is available. The browser-\n * confirmed timezone (`timezoneFromClient`) is trusted when the IP matches.\n */\nexport function reconcileGeo(sources: ReconcileSources): Geo {\n const { cookie, platform, api, ip } = sources;\n\n // Build merged geo from lowest to highest priority\n const geo: Geo = { ip };\n\n // Layer in cookie fields (lowest priority — stale but useful fallback)\n if (cookie) {\n Object.assign(geo, stripUndefined(cookie));\n }\n\n // Layer in platform headers (partial — may only have country)\n if (platform) {\n Object.assign(geo, stripUndefined(platform));\n }\n\n // Layer in API response (most complete — overwrites platform fields)\n if (api) {\n Object.assign(geo, stripUndefined(api));\n }\n\n // If cookie had timezoneFromClient but IP changed,\n // keep the browser timezone but mark it as stale\n if (\n cookie?.timezoneFromClient === true &&\n cookie?.ip !== ip &&\n cookie?.timezone\n ) {\n geo.timezone = cookie.timezone;\n geo.timezoneFromClient = false;\n }\n\n // Ensure IP is set\n geo.ip = ip;\n\n return geo;\n}\n\n/**\n * Remove undefined values from an object so Object.assign doesn't overwrite\n * existing values with undefined.\n */\nfunction stripUndefined(obj: Partial<Geo>): Partial<Geo> {\n const result: Partial<Geo> = {};\n for (const [key, value] of Object.entries(obj)) {\n if (value !== undefined) {\n (result as Record<string, unknown>)[key] = value;\n }\n }\n return result;\n}\n"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@netloc8/netloc8-js",
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
+ },
13
+ "files": [
14
+ "dist/"
15
+ ],
16
+ "scripts": {
17
+ "build": "tsdown",
18
+ "clean": "rm -rf dist",
19
+ "test": "bun test src/"
20
+ },
21
+ "engines": {
22
+ "node": ">=18"
23
+ }
24
+ }