@ipgeotrace/browser 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,77 @@
1
+ # @ipgeotrace/browser
2
+
3
+ The browser [IPGeoTrace](https://ipgeotrace.com) client. Answers one question — *where is the
4
+ person looking at this page?* — via `GET /resolve/me`, which sees the browser's real connection IP.
5
+
6
+ Sign up and grab your publishable key at [ipgeotrace.com](https://ipgeotrace.com).
7
+
8
+ Use it to personalize on the client: auto-select the visitor's country, price in their local
9
+ currency, prefill a phone code, default a timezone or language.
10
+
11
+ Framework-agnostic: React, Vue, Svelte, and vanilla all use it the same way. The optional reactive
12
+ wrappers (`@ipgeotrace/react`, etc.) are thin sugar on top; you don't need them.
13
+
14
+ ## Install
15
+
16
+ ```sh
17
+ npm add @ipgeotrace/browser
18
+ ```
19
+
20
+ ## Usage
21
+
22
+ ```ts
23
+ import { createBrowserClient } from '@ipgeotrace/browser';
24
+
25
+ const client = createBrowserClient({ publishableKey: 'pk_live_...' });
26
+
27
+ const result = await client.me();
28
+ if (result.ok) {
29
+ console.log(result.value.country?.name, result.value.location?.timeZone);
30
+ }
31
+ ```
32
+
33
+ That's it — `me()` calls the API directly from the browser. It's `async` because it makes a network
34
+ request; `await` it and check `result.ok`.
35
+
36
+ ## The key is a publishable key
37
+
38
+ You put this key in your client-side code on purpose. A publishable key is **safe to expose**
39
+ because it is restricted: domain-locked to your site, scoped to `resolve` / `me` only (no batch, no
40
+ account access), and rate-limited. This is the same model as a Google Maps browser key. Never ship
41
+ your secret server key (`@ipgeotrace/client`) to the browser — that one can run batch lookups and
42
+ spend your whole quota.
43
+
44
+ > Publishable keys are a planned API feature (domain allowlist + resolve-only scope). Until they
45
+ > ship, treat this package as targeting that model. See
46
+ > [../../API-CONTRACT.md](../../API-CONTRACT.md#publishable-keys-planned).
47
+
48
+ ## Options
49
+
50
+ ```ts
51
+ createBrowserClient({
52
+ publishableKey: 'pk_live_...', // required
53
+ baseUrl: undefined, // override the API host (staging / self-hosted)
54
+ timeoutMs: 10_000,
55
+ fetch: globalThis.fetch,
56
+ });
57
+ ```
58
+
59
+ `me()` is always a live call — it is never cached. The caller's IP can change at any time (VPN,
60
+ Wi-Fi ↔ cellular, moving networks) and the browser never knows its own IP, so there is nothing safe
61
+ to key a cache on. Every `me()` reflects the visitor's location right now. It returns the same
62
+ `Result<GeoResponse>` shape as `@ipgeotrace/client`.
63
+
64
+ ## React, in ~30 lines (no extra package needed)
65
+
66
+ ```tsx
67
+ function useVisitorGeo() {
68
+ const [state, setState] = useState<GeoResponse | null>(null);
69
+ useEffect(() => {
70
+ const client = createBrowserClient({ publishableKey: 'pk_live_...' });
71
+ const controller = new AbortController();
72
+ client.me(controller.signal).then((r) => r.ok && setState(r.value));
73
+ return () => controller.abort();
74
+ }, []);
75
+ return state;
76
+ }
77
+ ```
package/dist/index.cjs ADDED
@@ -0,0 +1,92 @@
1
+ 'use strict';
2
+
3
+ var client = require('@ipgeotrace/client');
4
+
5
+ // src/client.ts
6
+ var PRODUCTION_BASE_URL = "https://lookup.ipgeotrace.com";
7
+ var API_VERSION = "v1";
8
+ var DEFAULT_TIMEOUT_MS = 1e4;
9
+ var KNOWN_ERROR_CODES = new Set(Object.values(client.GeoErrorCodes));
10
+ function createBrowserClient(options) {
11
+ if (!options.publishableKey) {
12
+ throw new Error("IPGeoTrace: a publishableKey is required.");
13
+ }
14
+ const fetchImpl = options.fetch ?? globalThis.fetch;
15
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
16
+ const url = `${(options.baseUrl ?? PRODUCTION_BASE_URL).replace(/\/+$/, "")}/${API_VERSION}/resolve/me`;
17
+ const headers = { Accept: "application/json", "X-Api-Key": options.publishableKey };
18
+ return {
19
+ async me(signal) {
20
+ const controller = new AbortController();
21
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
22
+ const requestSignal = combineSignals(signal, controller.signal);
23
+ try {
24
+ const response = await fetchImpl(url, { method: "GET", headers, signal: requestSignal });
25
+ clearTimeout(timer);
26
+ if (!response.ok) return { ok: false, error: await readError(response) };
27
+ const value = await response.json();
28
+ return { ok: true, value };
29
+ } catch (cause) {
30
+ clearTimeout(timer);
31
+ if (signal?.aborted) throw cause;
32
+ const timedOut = controller.signal.aborted;
33
+ return {
34
+ ok: false,
35
+ error: {
36
+ code: timedOut ? client.GeoErrorCodes.Timeout : client.GeoErrorCodes.NetworkError,
37
+ message: timedOut ? "The request timed out." : "The request could not reach the API."
38
+ }
39
+ };
40
+ }
41
+ }
42
+ };
43
+ }
44
+ async function readError(response) {
45
+ let body = {};
46
+ try {
47
+ body = await response.json();
48
+ } catch {
49
+ body = {};
50
+ }
51
+ const bodyCode = body.code !== void 0 && KNOWN_ERROR_CODES.has(body.code) ? body.code : void 0;
52
+ return {
53
+ code: bodyCode ?? statusToCode(response.status),
54
+ message: body.error ?? response.statusText ?? "The request failed.",
55
+ statusCode: response.status
56
+ };
57
+ }
58
+ function statusToCode(status) {
59
+ switch (status) {
60
+ case 400:
61
+ return client.GeoErrorCodes.BadRequest;
62
+ case 401:
63
+ return client.GeoErrorCodes.Unauthorized;
64
+ case 403:
65
+ return client.GeoErrorCodes.Forbidden;
66
+ case 404:
67
+ return client.GeoErrorCodes.NotFound;
68
+ case 429:
69
+ return client.GeoErrorCodes.RateLimited;
70
+ case 503:
71
+ return client.GeoErrorCodes.ServiceUnavailable;
72
+ default:
73
+ return client.GeoErrorCodes.Unknown;
74
+ }
75
+ }
76
+ function combineSignals(external, internal) {
77
+ if (!external) return internal;
78
+ const controller = new AbortController();
79
+ const abort = () => controller.abort();
80
+ if (external.aborted || internal.aborted) controller.abort();
81
+ external.addEventListener("abort", abort, { once: true });
82
+ internal.addEventListener("abort", abort, { once: true });
83
+ return controller.signal;
84
+ }
85
+
86
+ Object.defineProperty(exports, "GeoErrorCodes", {
87
+ enumerable: true,
88
+ get: function () { return client.GeoErrorCodes; }
89
+ });
90
+ exports.createBrowserClient = createBrowserClient;
91
+ //# sourceMappingURL=index.cjs.map
92
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/client.ts"],"names":["GeoErrorCodes"],"mappings":"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","file":"index.cjs","sourcesContent":["import { GeoErrorCodes, type GeoError, type GeoErrorCode, type GeoResponse, type Result } from '@ipgeotrace/client';\n\nconst PRODUCTION_BASE_URL = 'https://lookup.ipgeotrace.com';\nconst API_VERSION = 'v1';\nconst DEFAULT_TIMEOUT_MS = 10_000;\nconst KNOWN_ERROR_CODES = new Set<string>(Object.values(GeoErrorCodes));\n\nexport interface BrowserClientOptions {\n publishableKey: string;\n baseUrl?: string;\n timeoutMs?: number;\n fetch?: typeof fetch;\n}\n\nexport interface BrowserClient {\n me(signal?: AbortSignal): Promise<Result<GeoResponse>>;\n}\n\nexport function createBrowserClient(options: BrowserClientOptions): BrowserClient {\n if (!options.publishableKey) {\n throw new Error('IPGeoTrace: a publishableKey is required.');\n }\n\n const fetchImpl = options.fetch ?? globalThis.fetch;\n const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const url = `${(options.baseUrl ?? PRODUCTION_BASE_URL).replace(/\\/+$/, '')}/${API_VERSION}/resolve/me`;\n const headers: Record<string, string> = { Accept: 'application/json', 'X-Api-Key': options.publishableKey };\n\n return {\n async me(signal?: AbortSignal): Promise<Result<GeoResponse>> {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n const requestSignal = combineSignals(signal, controller.signal);\n\n try {\n const response = await fetchImpl(url, { method: 'GET', headers, signal: requestSignal });\n clearTimeout(timer);\n\n if (!response.ok) return { ok: false, error: await readError(response) };\n\n const value = (await response.json()) as GeoResponse;\n return { ok: true, value };\n } catch (cause) {\n clearTimeout(timer);\n if (signal?.aborted) throw cause;\n const timedOut = controller.signal.aborted;\n return {\n ok: false,\n error: {\n code: timedOut ? GeoErrorCodes.Timeout : GeoErrorCodes.NetworkError,\n message: timedOut ? 'The request timed out.' : 'The request could not reach the API.',\n },\n };\n }\n },\n };\n}\n\nasync function readError(response: Response): Promise<GeoError> {\n let body: { error?: string; code?: string } = {};\n try {\n body = (await response.json()) as { error?: string; code?: string };\n } catch {\n body = {};\n }\n const bodyCode =\n body.code !== undefined && KNOWN_ERROR_CODES.has(body.code)\n ? (body.code as GeoErrorCode)\n : undefined;\n return {\n code: bodyCode ?? statusToCode(response.status),\n message: body.error ?? response.statusText ?? 'The request failed.',\n statusCode: response.status,\n };\n}\n\nfunction statusToCode(status: number): GeoErrorCode {\n switch (status) {\n case 400:\n return GeoErrorCodes.BadRequest;\n case 401:\n return GeoErrorCodes.Unauthorized;\n case 403:\n return GeoErrorCodes.Forbidden;\n case 404:\n return GeoErrorCodes.NotFound;\n case 429:\n return GeoErrorCodes.RateLimited;\n case 503:\n return GeoErrorCodes.ServiceUnavailable;\n default:\n return GeoErrorCodes.Unknown;\n }\n}\n\nfunction combineSignals(external: AbortSignal | undefined, internal: AbortSignal): AbortSignal {\n if (!external) return internal;\n const controller = new AbortController();\n const abort = () => controller.abort();\n if (external.aborted || internal.aborted) controller.abort();\n external.addEventListener('abort', abort, { once: true });\n internal.addEventListener('abort', abort, { once: true });\n return controller.signal;\n}\n"]}
@@ -0,0 +1,15 @@
1
+ import { Result, GeoResponse } from '@ipgeotrace/client';
2
+ export { GeoError, GeoErrorCode, GeoErrorCodes, GeoResponse, Result } from '@ipgeotrace/client';
3
+
4
+ interface BrowserClientOptions {
5
+ publishableKey: string;
6
+ baseUrl?: string;
7
+ timeoutMs?: number;
8
+ fetch?: typeof fetch;
9
+ }
10
+ interface BrowserClient {
11
+ me(signal?: AbortSignal): Promise<Result<GeoResponse>>;
12
+ }
13
+ declare function createBrowserClient(options: BrowserClientOptions): BrowserClient;
14
+
15
+ export { type BrowserClient, type BrowserClientOptions, createBrowserClient };
@@ -0,0 +1,15 @@
1
+ import { Result, GeoResponse } from '@ipgeotrace/client';
2
+ export { GeoError, GeoErrorCode, GeoErrorCodes, GeoResponse, Result } from '@ipgeotrace/client';
3
+
4
+ interface BrowserClientOptions {
5
+ publishableKey: string;
6
+ baseUrl?: string;
7
+ timeoutMs?: number;
8
+ fetch?: typeof fetch;
9
+ }
10
+ interface BrowserClient {
11
+ me(signal?: AbortSignal): Promise<Result<GeoResponse>>;
12
+ }
13
+ declare function createBrowserClient(options: BrowserClientOptions): BrowserClient;
14
+
15
+ export { type BrowserClient, type BrowserClientOptions, createBrowserClient };
package/dist/index.js ADDED
@@ -0,0 +1,87 @@
1
+ import { GeoErrorCodes } from '@ipgeotrace/client';
2
+ export { GeoErrorCodes } from '@ipgeotrace/client';
3
+
4
+ // src/client.ts
5
+ var PRODUCTION_BASE_URL = "https://lookup.ipgeotrace.com";
6
+ var API_VERSION = "v1";
7
+ var DEFAULT_TIMEOUT_MS = 1e4;
8
+ var KNOWN_ERROR_CODES = new Set(Object.values(GeoErrorCodes));
9
+ function createBrowserClient(options) {
10
+ if (!options.publishableKey) {
11
+ throw new Error("IPGeoTrace: a publishableKey is required.");
12
+ }
13
+ const fetchImpl = options.fetch ?? globalThis.fetch;
14
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
15
+ const url = `${(options.baseUrl ?? PRODUCTION_BASE_URL).replace(/\/+$/, "")}/${API_VERSION}/resolve/me`;
16
+ const headers = { Accept: "application/json", "X-Api-Key": options.publishableKey };
17
+ return {
18
+ async me(signal) {
19
+ const controller = new AbortController();
20
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
21
+ const requestSignal = combineSignals(signal, controller.signal);
22
+ try {
23
+ const response = await fetchImpl(url, { method: "GET", headers, signal: requestSignal });
24
+ clearTimeout(timer);
25
+ if (!response.ok) return { ok: false, error: await readError(response) };
26
+ const value = await response.json();
27
+ return { ok: true, value };
28
+ } catch (cause) {
29
+ clearTimeout(timer);
30
+ if (signal?.aborted) throw cause;
31
+ const timedOut = controller.signal.aborted;
32
+ return {
33
+ ok: false,
34
+ error: {
35
+ code: timedOut ? GeoErrorCodes.Timeout : GeoErrorCodes.NetworkError,
36
+ message: timedOut ? "The request timed out." : "The request could not reach the API."
37
+ }
38
+ };
39
+ }
40
+ }
41
+ };
42
+ }
43
+ async function readError(response) {
44
+ let body = {};
45
+ try {
46
+ body = await response.json();
47
+ } catch {
48
+ body = {};
49
+ }
50
+ const bodyCode = body.code !== void 0 && KNOWN_ERROR_CODES.has(body.code) ? body.code : void 0;
51
+ return {
52
+ code: bodyCode ?? statusToCode(response.status),
53
+ message: body.error ?? response.statusText ?? "The request failed.",
54
+ statusCode: response.status
55
+ };
56
+ }
57
+ function statusToCode(status) {
58
+ switch (status) {
59
+ case 400:
60
+ return GeoErrorCodes.BadRequest;
61
+ case 401:
62
+ return GeoErrorCodes.Unauthorized;
63
+ case 403:
64
+ return GeoErrorCodes.Forbidden;
65
+ case 404:
66
+ return GeoErrorCodes.NotFound;
67
+ case 429:
68
+ return GeoErrorCodes.RateLimited;
69
+ case 503:
70
+ return GeoErrorCodes.ServiceUnavailable;
71
+ default:
72
+ return GeoErrorCodes.Unknown;
73
+ }
74
+ }
75
+ function combineSignals(external, internal) {
76
+ if (!external) return internal;
77
+ const controller = new AbortController();
78
+ const abort = () => controller.abort();
79
+ if (external.aborted || internal.aborted) controller.abort();
80
+ external.addEventListener("abort", abort, { once: true });
81
+ internal.addEventListener("abort", abort, { once: true });
82
+ return controller.signal;
83
+ }
84
+
85
+ export { createBrowserClient };
86
+ //# sourceMappingURL=index.js.map
87
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/client.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import { GeoErrorCodes, type GeoError, type GeoErrorCode, type GeoResponse, type Result } from '@ipgeotrace/client';\n\nconst PRODUCTION_BASE_URL = 'https://lookup.ipgeotrace.com';\nconst API_VERSION = 'v1';\nconst DEFAULT_TIMEOUT_MS = 10_000;\nconst KNOWN_ERROR_CODES = new Set<string>(Object.values(GeoErrorCodes));\n\nexport interface BrowserClientOptions {\n publishableKey: string;\n baseUrl?: string;\n timeoutMs?: number;\n fetch?: typeof fetch;\n}\n\nexport interface BrowserClient {\n me(signal?: AbortSignal): Promise<Result<GeoResponse>>;\n}\n\nexport function createBrowserClient(options: BrowserClientOptions): BrowserClient {\n if (!options.publishableKey) {\n throw new Error('IPGeoTrace: a publishableKey is required.');\n }\n\n const fetchImpl = options.fetch ?? globalThis.fetch;\n const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const url = `${(options.baseUrl ?? PRODUCTION_BASE_URL).replace(/\\/+$/, '')}/${API_VERSION}/resolve/me`;\n const headers: Record<string, string> = { Accept: 'application/json', 'X-Api-Key': options.publishableKey };\n\n return {\n async me(signal?: AbortSignal): Promise<Result<GeoResponse>> {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n const requestSignal = combineSignals(signal, controller.signal);\n\n try {\n const response = await fetchImpl(url, { method: 'GET', headers, signal: requestSignal });\n clearTimeout(timer);\n\n if (!response.ok) return { ok: false, error: await readError(response) };\n\n const value = (await response.json()) as GeoResponse;\n return { ok: true, value };\n } catch (cause) {\n clearTimeout(timer);\n if (signal?.aborted) throw cause;\n const timedOut = controller.signal.aborted;\n return {\n ok: false,\n error: {\n code: timedOut ? GeoErrorCodes.Timeout : GeoErrorCodes.NetworkError,\n message: timedOut ? 'The request timed out.' : 'The request could not reach the API.',\n },\n };\n }\n },\n };\n}\n\nasync function readError(response: Response): Promise<GeoError> {\n let body: { error?: string; code?: string } = {};\n try {\n body = (await response.json()) as { error?: string; code?: string };\n } catch {\n body = {};\n }\n const bodyCode =\n body.code !== undefined && KNOWN_ERROR_CODES.has(body.code)\n ? (body.code as GeoErrorCode)\n : undefined;\n return {\n code: bodyCode ?? statusToCode(response.status),\n message: body.error ?? response.statusText ?? 'The request failed.',\n statusCode: response.status,\n };\n}\n\nfunction statusToCode(status: number): GeoErrorCode {\n switch (status) {\n case 400:\n return GeoErrorCodes.BadRequest;\n case 401:\n return GeoErrorCodes.Unauthorized;\n case 403:\n return GeoErrorCodes.Forbidden;\n case 404:\n return GeoErrorCodes.NotFound;\n case 429:\n return GeoErrorCodes.RateLimited;\n case 503:\n return GeoErrorCodes.ServiceUnavailable;\n default:\n return GeoErrorCodes.Unknown;\n }\n}\n\nfunction combineSignals(external: AbortSignal | undefined, internal: AbortSignal): AbortSignal {\n if (!external) return internal;\n const controller = new AbortController();\n const abort = () => controller.abort();\n if (external.aborted || internal.aborted) controller.abort();\n external.addEventListener('abort', abort, { once: true });\n internal.addEventListener('abort', abort, { once: true });\n return controller.signal;\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@ipgeotrace/browser",
3
+ "version": "0.1.0",
4
+ "description": "Browser IPGeoTrace client. Resolve the current visitor's location with a publishable key.",
5
+ "keywords": [
6
+ "ipgeotrace",
7
+ "geolocation",
8
+ "geoip",
9
+ "browser",
10
+ "typescript"
11
+ ],
12
+ "license": "MIT",
13
+ "homepage": "https://ipgeotrace.com",
14
+ "type": "module",
15
+ "main": "./dist/index.cjs",
16
+ "module": "./dist/index.js",
17
+ "types": "./dist/index.d.ts",
18
+ "exports": {
19
+ ".": {
20
+ "types": "./dist/index.d.ts",
21
+ "import": "./dist/index.js",
22
+ "require": "./dist/index.cjs"
23
+ }
24
+ },
25
+ "files": [
26
+ "dist"
27
+ ],
28
+ "sideEffects": false,
29
+ "dependencies": {
30
+ "@ipgeotrace/client": "0.1.0"
31
+ },
32
+ "devDependencies": {
33
+ "tsup": "^8.3.5",
34
+ "typescript": "^5.6.3"
35
+ },
36
+ "scripts": {
37
+ "build": "tsup",
38
+ "dev": "tsup --watch",
39
+ "typecheck": "tsc --noEmit",
40
+ "clean": "rm -rf dist"
41
+ }
42
+ }