@ipgeotrace/client 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 +96 -0
- package/dist/index.cjs +337 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +143 -0
- package/dist/index.d.ts +143 -0
- package/dist/index.js +327 -0
- package/dist/index.js.map +1 -0
- package/package.json +42 -0
package/README.md
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# @ipgeotrace/client
|
|
2
|
+
|
|
3
|
+
Isomorphic [IPGeoTrace](https://ipgeotrace.com) client for **Node 18+, edge runtimes, Deno, and
|
|
4
|
+
Bun**. Resolve a single IP or a batch of up to 100, with optional caching and automatic retries.
|
|
5
|
+
Zero runtime dependencies, `fetch`-based.
|
|
6
|
+
|
|
7
|
+
Sign up and grab your API key at [ipgeotrace.com](https://ipgeotrace.com).
|
|
8
|
+
|
|
9
|
+
This is the server-side, secret-key client — you supply the IP. For the browser (resolve the
|
|
10
|
+
current visitor, no secret key), use [`@ipgeotrace/browser`](../browser).
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
npm add @ipgeotrace/client
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Quick start
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { IpGeoTraceClient } from '@ipgeotrace/client';
|
|
22
|
+
|
|
23
|
+
const client = new IpGeoTraceClient({ apiKey: process.env.IPGEOTRACE_API_KEY! });
|
|
24
|
+
|
|
25
|
+
const result = await client.resolve('8.8.8.8');
|
|
26
|
+
if (result.ok) {
|
|
27
|
+
console.log(`${result.value.city?.name}, ${result.value.country?.name}`);
|
|
28
|
+
} else {
|
|
29
|
+
console.log(`failed: ${result.error.code}`);
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The IP is always supplied by you: a signup form, a webhook payload, a stored audit log. Invalid IPs
|
|
34
|
+
are caught locally (`invalid_ip`) with no wasted round trip.
|
|
35
|
+
|
|
36
|
+
## Results, not exceptions
|
|
37
|
+
|
|
38
|
+
Every call returns `Result<T>` — `{ ok: true, value } | { ok: false, error }`. Check `ok` and
|
|
39
|
+
TypeScript narrows `value` (or `error`) for you. The only thrown thing is caller cancellation via
|
|
40
|
+
an `AbortSignal`:
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
const controller = new AbortController();
|
|
44
|
+
const result = await client.resolve('8.8.8.8', controller.signal);
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Batch lookups
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
const batch = await client.resolveBatch(logins.map((l) => l.ip));
|
|
51
|
+
if (batch.ok) {
|
|
52
|
+
for (const item of batch.value.results) {
|
|
53
|
+
report.add(item.ip, item.found ? item.country?.name : item.error);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Results come back in request order, one item per address. A single bad address fails as its own
|
|
59
|
+
item (`found: false, error`) without failing the batch. With caching on, cached IPs are served
|
|
60
|
+
locally and only the misses are sent; if every IP is a hit, no request is made and
|
|
61
|
+
`batch.value.fromCache` is `true`.
|
|
62
|
+
|
|
63
|
+
## Configuration
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
const client = new IpGeoTraceClient({
|
|
67
|
+
apiKey: '...',
|
|
68
|
+
environment: 'production', // or 'sandbox'; ignored when baseUrl is set
|
|
69
|
+
baseUrl: undefined, // absolute override for self-hosted / staging
|
|
70
|
+
timeoutMs: 10_000,
|
|
71
|
+
maxRetries: 2, // retries 429 (rate_limited) and 503, honoring Retry-After
|
|
72
|
+
cache: true, // or supply your own GeoCache (Redis, etc.)
|
|
73
|
+
cacheTtlMs: 5 * 60_000,
|
|
74
|
+
fetch: globalThis.fetch, // override on Node < 18 or to inject a proxy agent
|
|
75
|
+
});
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Only successful lookups are cached, never errors. A throwing cache never breaks a lookup — the
|
|
79
|
+
client falls back to calling the API.
|
|
80
|
+
|
|
81
|
+
## Errors
|
|
82
|
+
|
|
83
|
+
On failure, `error` carries the reason:
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
interface GeoError {
|
|
87
|
+
code: GeoErrorCode; // GeoErrorCodes.*
|
|
88
|
+
message: string;
|
|
89
|
+
statusCode?: number;
|
|
90
|
+
retryAfterSeconds?: number;
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Codes: `unauthorized`, `invalid_ip`, `bad_request`, `not_found`, `rate_limited`, `quota_exceeded`,
|
|
95
|
+
`license_inactive`, `forbidden`, `service_unavailable`, `network_error`, `timeout`,
|
|
96
|
+
`invalid_response`, `unknown`.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/cache.ts
|
|
4
|
+
var MemoryGeoCache = class {
|
|
5
|
+
store = /* @__PURE__ */ new Map();
|
|
6
|
+
get(ip) {
|
|
7
|
+
const entry = this.store.get(ip);
|
|
8
|
+
if (!entry) return void 0;
|
|
9
|
+
if (entry.expiresAt <= Date.now()) {
|
|
10
|
+
this.store.delete(ip);
|
|
11
|
+
return void 0;
|
|
12
|
+
}
|
|
13
|
+
return entry.value;
|
|
14
|
+
}
|
|
15
|
+
set(ip, value, ttlMs) {
|
|
16
|
+
this.store.set(ip, { value, expiresAt: Date.now() + ttlMs });
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// src/errors.ts
|
|
21
|
+
var GeoErrorCodes = {
|
|
22
|
+
Unauthorized: "unauthorized",
|
|
23
|
+
InvalidIp: "invalid_ip",
|
|
24
|
+
BadRequest: "bad_request",
|
|
25
|
+
NotFound: "not_found",
|
|
26
|
+
RateLimited: "rate_limited",
|
|
27
|
+
QuotaExceeded: "quota_exceeded",
|
|
28
|
+
LicenseInactive: "license_inactive",
|
|
29
|
+
Forbidden: "forbidden",
|
|
30
|
+
ServiceUnavailable: "service_unavailable",
|
|
31
|
+
NetworkError: "network_error",
|
|
32
|
+
Timeout: "timeout",
|
|
33
|
+
InvalidResponse: "invalid_response",
|
|
34
|
+
Unknown: "unknown"
|
|
35
|
+
};
|
|
36
|
+
function statusToCode(status) {
|
|
37
|
+
switch (status) {
|
|
38
|
+
case 400:
|
|
39
|
+
return GeoErrorCodes.BadRequest;
|
|
40
|
+
case 401:
|
|
41
|
+
return GeoErrorCodes.Unauthorized;
|
|
42
|
+
case 403:
|
|
43
|
+
return GeoErrorCodes.Forbidden;
|
|
44
|
+
case 404:
|
|
45
|
+
return GeoErrorCodes.NotFound;
|
|
46
|
+
case 429:
|
|
47
|
+
return GeoErrorCodes.RateLimited;
|
|
48
|
+
case 503:
|
|
49
|
+
return GeoErrorCodes.ServiceUnavailable;
|
|
50
|
+
default:
|
|
51
|
+
return GeoErrorCodes.Unknown;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// src/internal/ip.ts
|
|
56
|
+
var IPV4 = /^(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}$/;
|
|
57
|
+
var IPV6 = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|::(ffff(:0{1,4})?:)?((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d))$/;
|
|
58
|
+
function isValidIp(ip) {
|
|
59
|
+
return IPV4.test(ip) || IPV6.test(ip);
|
|
60
|
+
}
|
|
61
|
+
function isPrivateOrReservedIp(ip) {
|
|
62
|
+
const normalized = ip.trim().toLowerCase();
|
|
63
|
+
return normalized.includes(":") ? isPrivateIpv6(normalized) : isPrivateIpv4(normalized);
|
|
64
|
+
}
|
|
65
|
+
function isPrivateIpv4(ip) {
|
|
66
|
+
const parts = ip.split(".");
|
|
67
|
+
if (parts.length !== 4) return false;
|
|
68
|
+
const octets = parts.map((part) => Number(part));
|
|
69
|
+
if (octets.some((o) => !Number.isInteger(o) || o < 0 || o > 255)) return false;
|
|
70
|
+
const a = octets[0];
|
|
71
|
+
const b = octets[1];
|
|
72
|
+
if (a === 0 || a === 10 || a === 127) return true;
|
|
73
|
+
if (a === 169 && b === 254) return true;
|
|
74
|
+
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
75
|
+
if (a === 192 && b === 168) return true;
|
|
76
|
+
if (a === 100 && b >= 64 && b <= 127) return true;
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
function isPrivateIpv6(ip) {
|
|
80
|
+
if (ip === "::1" || ip === "::") return true;
|
|
81
|
+
const mapped = ip.match(/^::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/);
|
|
82
|
+
if (mapped) return isPrivateIpv4(mapped[1]);
|
|
83
|
+
const hextet = parseInt(ip.split(":")[0] ?? "", 16);
|
|
84
|
+
if (Number.isNaN(hextet)) return false;
|
|
85
|
+
if (hextet >= 64512 && hextet <= 65023) return true;
|
|
86
|
+
if (hextet >= 65152 && hextet <= 65215) return true;
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// src/result.ts
|
|
91
|
+
function ok(value) {
|
|
92
|
+
return { ok: true, value };
|
|
93
|
+
}
|
|
94
|
+
function err(error) {
|
|
95
|
+
return { ok: false, error };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// src/internal/http.ts
|
|
99
|
+
var KNOWN_ERROR_CODES = new Set(Object.values(GeoErrorCodes));
|
|
100
|
+
async function send(config, method, path, body, signal) {
|
|
101
|
+
const url = `${config.baseUrl}${path}`;
|
|
102
|
+
let attempt = 0;
|
|
103
|
+
while (true) {
|
|
104
|
+
const timeoutController = new AbortController();
|
|
105
|
+
const timer = setTimeout(() => timeoutController.abort(), config.timeoutMs);
|
|
106
|
+
const requestSignal = combineSignals(signal, timeoutController.signal);
|
|
107
|
+
try {
|
|
108
|
+
const response = await config.fetchImpl(url, {
|
|
109
|
+
method,
|
|
110
|
+
headers: {
|
|
111
|
+
"X-Api-Key": config.apiKey,
|
|
112
|
+
Accept: "application/json",
|
|
113
|
+
...body === void 0 ? {} : { "Content-Type": "application/json" }
|
|
114
|
+
},
|
|
115
|
+
body: body === void 0 ? void 0 : JSON.stringify(body),
|
|
116
|
+
signal: requestSignal
|
|
117
|
+
});
|
|
118
|
+
clearTimeout(timer);
|
|
119
|
+
if (response.ok) return await parseJson(response);
|
|
120
|
+
const error = await readError(response);
|
|
121
|
+
if (shouldRetry(error.code) && attempt < config.maxRetries) {
|
|
122
|
+
attempt += 1;
|
|
123
|
+
await sleep(backoff(attempt, error.retryAfterSeconds));
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
return err(error);
|
|
127
|
+
} catch (cause) {
|
|
128
|
+
clearTimeout(timer);
|
|
129
|
+
if (signal?.aborted) throw cause;
|
|
130
|
+
const timedOut = timeoutController.signal.aborted;
|
|
131
|
+
const code = timedOut ? GeoErrorCodes.Timeout : GeoErrorCodes.NetworkError;
|
|
132
|
+
if (attempt < config.maxRetries) {
|
|
133
|
+
attempt += 1;
|
|
134
|
+
await sleep(backoff(attempt));
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
return err({
|
|
138
|
+
code,
|
|
139
|
+
message: timedOut ? "The request timed out." : "The request could not reach the API."
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
async function parseJson(response) {
|
|
145
|
+
try {
|
|
146
|
+
return ok(await response.json());
|
|
147
|
+
} catch {
|
|
148
|
+
return err({
|
|
149
|
+
code: GeoErrorCodes.InvalidResponse,
|
|
150
|
+
message: "The API returned a response that could not be read.",
|
|
151
|
+
statusCode: response.status
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
async function readError(response) {
|
|
156
|
+
let parsed = {};
|
|
157
|
+
try {
|
|
158
|
+
parsed = await response.json();
|
|
159
|
+
} catch {
|
|
160
|
+
parsed = {};
|
|
161
|
+
}
|
|
162
|
+
const bodyCode = parsed.code !== void 0 && KNOWN_ERROR_CODES.has(parsed.code) ? parsed.code : void 0;
|
|
163
|
+
const code = bodyCode ?? statusToCode(response.status);
|
|
164
|
+
return {
|
|
165
|
+
code,
|
|
166
|
+
message: parsed.error ?? response.statusText ?? "The request failed.",
|
|
167
|
+
statusCode: response.status,
|
|
168
|
+
retryAfterSeconds: parseRetryAfter(response.headers.get("retry-after"))
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
function shouldRetry(code) {
|
|
172
|
+
return code === GeoErrorCodes.RateLimited || code === GeoErrorCodes.ServiceUnavailable;
|
|
173
|
+
}
|
|
174
|
+
function parseRetryAfter(value) {
|
|
175
|
+
if (!value) return void 0;
|
|
176
|
+
const seconds = Number(value);
|
|
177
|
+
return Number.isFinite(seconds) && seconds >= 0 ? seconds : void 0;
|
|
178
|
+
}
|
|
179
|
+
var BASE_BACKOFF_MS = 200;
|
|
180
|
+
var MAX_BACKOFF_MS = 2e3;
|
|
181
|
+
function backoff(attempt, retryAfterSeconds) {
|
|
182
|
+
if (retryAfterSeconds !== void 0) return retryAfterSeconds * 1e3;
|
|
183
|
+
const exponential = Math.min(MAX_BACKOFF_MS, BASE_BACKOFF_MS * 2 ** (attempt - 1));
|
|
184
|
+
return exponential + Math.floor(Math.random() * BASE_BACKOFF_MS);
|
|
185
|
+
}
|
|
186
|
+
function sleep(ms) {
|
|
187
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
188
|
+
}
|
|
189
|
+
function combineSignals(external, internal) {
|
|
190
|
+
if (!external) return internal;
|
|
191
|
+
const controller = new AbortController();
|
|
192
|
+
const abort = () => controller.abort();
|
|
193
|
+
if (external.aborted || internal.aborted) controller.abort();
|
|
194
|
+
external.addEventListener("abort", abort, { once: true });
|
|
195
|
+
internal.addEventListener("abort", abort, { once: true });
|
|
196
|
+
return controller.signal;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// src/options.ts
|
|
200
|
+
var PRODUCTION_BASE_URL = "https://lookup.ipgeotrace.com";
|
|
201
|
+
var SANDBOX_BASE_URL = "https://lookup.dev.ipgeotrace.com";
|
|
202
|
+
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
203
|
+
var DEFAULT_MAX_RETRIES = 2;
|
|
204
|
+
var DEFAULT_CACHE_TTL_MS = 5 * 6e4;
|
|
205
|
+
function resolveConfig(options, defaultCache) {
|
|
206
|
+
if (!options.apiKey) throw new Error("IPGeoTrace: an apiKey is required.");
|
|
207
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
208
|
+
const maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
209
|
+
if (timeoutMs <= 0) throw new Error("IPGeoTrace: timeoutMs must be greater than zero.");
|
|
210
|
+
if (maxRetries < 0) throw new Error("IPGeoTrace: maxRetries cannot be negative.");
|
|
211
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
212
|
+
if (typeof fetchImpl !== "function") {
|
|
213
|
+
throw new Error("IPGeoTrace: no global fetch available; pass options.fetch (Node < 18).");
|
|
214
|
+
}
|
|
215
|
+
let cache;
|
|
216
|
+
if (options.cache === true) cache = defaultCache();
|
|
217
|
+
else if (options.cache) cache = options.cache;
|
|
218
|
+
return {
|
|
219
|
+
apiKey: options.apiKey,
|
|
220
|
+
baseUrl: resolveBaseUrl(options),
|
|
221
|
+
timeoutMs,
|
|
222
|
+
maxRetries,
|
|
223
|
+
cache,
|
|
224
|
+
cacheTtlMs: options.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS,
|
|
225
|
+
fetchImpl
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
function resolveBaseUrl(options) {
|
|
229
|
+
const base = options.baseUrl ?? (options.environment === "sandbox" ? SANDBOX_BASE_URL : PRODUCTION_BASE_URL);
|
|
230
|
+
return base.replace(/\/+$/, "");
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// src/client.ts
|
|
234
|
+
var MAX_BATCH_SIZE = 100;
|
|
235
|
+
var API_VERSION = "v1";
|
|
236
|
+
var IpGeoTraceClient = class {
|
|
237
|
+
config;
|
|
238
|
+
cache;
|
|
239
|
+
constructor(options) {
|
|
240
|
+
this.config = resolveConfig(options, () => new MemoryGeoCache());
|
|
241
|
+
this.cache = this.config.cache;
|
|
242
|
+
}
|
|
243
|
+
async resolve(ip, signal) {
|
|
244
|
+
if (!isValidIp(ip)) {
|
|
245
|
+
return err({ code: GeoErrorCodes.InvalidIp, message: `'${ip}' is not a valid IP address.` });
|
|
246
|
+
}
|
|
247
|
+
const cached = await this.readCache(ip);
|
|
248
|
+
if (cached) return ok(cached);
|
|
249
|
+
const result = await send(this.config, "GET", `/${API_VERSION}/resolve/${encodeURIComponent(ip)}`, void 0, signal);
|
|
250
|
+
if (result.ok) await this.writeCache(ip, result.value);
|
|
251
|
+
return result;
|
|
252
|
+
}
|
|
253
|
+
async resolveBatch(ips, signal) {
|
|
254
|
+
const requested = [...ips];
|
|
255
|
+
if (requested.length === 0) {
|
|
256
|
+
return err({ code: GeoErrorCodes.BadRequest, message: "At least one IP address is required." });
|
|
257
|
+
}
|
|
258
|
+
if (requested.length > MAX_BATCH_SIZE) {
|
|
259
|
+
return err({ code: GeoErrorCodes.BadRequest, message: `A batch may contain at most ${MAX_BATCH_SIZE} IP addresses.` });
|
|
260
|
+
}
|
|
261
|
+
const byIp = /* @__PURE__ */ new Map();
|
|
262
|
+
const misses = [];
|
|
263
|
+
for (const ip of requested) {
|
|
264
|
+
if (!isValidIp(ip)) {
|
|
265
|
+
byIp.set(ip, { ip, found: false, error: GeoErrorCodes.InvalidIp });
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
const cached = await this.readCache(ip);
|
|
269
|
+
if (cached) {
|
|
270
|
+
byIp.set(ip, toBatchItem(cached));
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
misses.push(ip);
|
|
274
|
+
}
|
|
275
|
+
let fromCache = true;
|
|
276
|
+
if (misses.length > 0) {
|
|
277
|
+
const response = await send(this.config, "POST", `/${API_VERSION}/resolve/batch`, { ips: misses }, signal);
|
|
278
|
+
if (!response.ok) return response;
|
|
279
|
+
fromCache = false;
|
|
280
|
+
const items = response.value.results;
|
|
281
|
+
for (let i = 0; i < items.length; i += 1) {
|
|
282
|
+
const item = items[i];
|
|
283
|
+
if (!item) continue;
|
|
284
|
+
const key = misses[i] ?? item.ip;
|
|
285
|
+
byIp.set(key, item);
|
|
286
|
+
if (item.found) await this.writeCache(key, toGeoResponse(item));
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
const results = requested.map((ip) => byIp.get(ip) ?? { ip, found: false, error: GeoErrorCodes.Unknown });
|
|
290
|
+
return ok({ results, fromCache });
|
|
291
|
+
}
|
|
292
|
+
async readCache(ip) {
|
|
293
|
+
if (!this.cache) return void 0;
|
|
294
|
+
try {
|
|
295
|
+
return await this.cache.get(ip) ?? void 0;
|
|
296
|
+
} catch {
|
|
297
|
+
return void 0;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
async writeCache(ip, value) {
|
|
301
|
+
if (!this.cache) return;
|
|
302
|
+
try {
|
|
303
|
+
await this.cache.set(ip, value, this.config.cacheTtlMs);
|
|
304
|
+
} catch {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
function toGeoResponse(item) {
|
|
310
|
+
return {
|
|
311
|
+
ip: item.ip,
|
|
312
|
+
ipVersion: item.ip.includes(":") ? 6 : 4,
|
|
313
|
+
continent: item.continent,
|
|
314
|
+
country: item.country,
|
|
315
|
+
region: item.region,
|
|
316
|
+
city: item.city,
|
|
317
|
+
location: item.location,
|
|
318
|
+
asn: item.asn,
|
|
319
|
+
isp: item.isp
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
function toBatchItem(response) {
|
|
323
|
+
const { ipVersion: _ipVersion, ...rest } = response;
|
|
324
|
+
return { ...rest, found: true };
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
exports.GeoErrorCodes = GeoErrorCodes;
|
|
328
|
+
exports.IpGeoTraceClient = IpGeoTraceClient;
|
|
329
|
+
exports.MemoryGeoCache = MemoryGeoCache;
|
|
330
|
+
exports.PRODUCTION_BASE_URL = PRODUCTION_BASE_URL;
|
|
331
|
+
exports.SANDBOX_BASE_URL = SANDBOX_BASE_URL;
|
|
332
|
+
exports.err = err;
|
|
333
|
+
exports.isPrivateOrReservedIp = isPrivateOrReservedIp;
|
|
334
|
+
exports.isValidIp = isValidIp;
|
|
335
|
+
exports.ok = ok;
|
|
336
|
+
//# sourceMappingURL=index.cjs.map
|
|
337
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cache.ts","../src/errors.ts","../src/internal/ip.ts","../src/result.ts","../src/internal/http.ts","../src/options.ts","../src/client.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["import type { GeoResponse } from './types.js';\n\nexport interface GeoCache {\n get(ip: string): Promise<GeoResponse | undefined> | GeoResponse | undefined;\n set(ip: string, value: GeoResponse, ttlMs: number): Promise<void> | void;\n}\n\ninterface Entry {\n value: GeoResponse;\n expiresAt: number;\n}\n\nexport class MemoryGeoCache implements GeoCache {\n private readonly store = new Map<string, Entry>();\n\n get(ip: string): GeoResponse | undefined {\n const entry = this.store.get(ip);\n if (!entry) return undefined;\n if (entry.expiresAt <= Date.now()) {\n this.store.delete(ip);\n return undefined;\n }\n return entry.value;\n }\n\n set(ip: string, value: GeoResponse, ttlMs: number): void {\n this.store.set(ip, { value, expiresAt: Date.now() + ttlMs });\n }\n}\n","export const GeoErrorCodes = {\n Unauthorized: 'unauthorized',\n InvalidIp: 'invalid_ip',\n BadRequest: 'bad_request',\n NotFound: 'not_found',\n RateLimited: 'rate_limited',\n QuotaExceeded: 'quota_exceeded',\n LicenseInactive: 'license_inactive',\n Forbidden: 'forbidden',\n ServiceUnavailable: 'service_unavailable',\n NetworkError: 'network_error',\n Timeout: 'timeout',\n InvalidResponse: 'invalid_response',\n Unknown: 'unknown',\n} as const;\n\nexport type GeoErrorCode = (typeof GeoErrorCodes)[keyof typeof GeoErrorCodes];\n\nexport interface GeoError {\n code: GeoErrorCode;\n message: string;\n statusCode?: number;\n retryAfterSeconds?: number;\n}\n\nexport function 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","const IPV4 =\n /^(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}$/;\n\nconst IPV6 =\n /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|::(ffff(:0{1,4})?:)?((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\.){3}(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\.){3}(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d))$/;\n\nexport function isValidIp(ip: string): boolean {\n return IPV4.test(ip) || IPV6.test(ip);\n}\n\nexport function isPrivateOrReservedIp(ip: string): boolean {\n const normalized = ip.trim().toLowerCase();\n return normalized.includes(':') ? isPrivateIpv6(normalized) : isPrivateIpv4(normalized);\n}\n\nfunction isPrivateIpv4(ip: string): boolean {\n const parts = ip.split('.');\n if (parts.length !== 4) return false;\n const octets = parts.map((part) => Number(part));\n if (octets.some((o) => !Number.isInteger(o) || o < 0 || o > 255)) return false;\n\n const a = octets[0]!;\n const b = octets[1]!;\n if (a === 0 || a === 10 || a === 127) return true;\n if (a === 169 && b === 254) return true;\n if (a === 172 && b >= 16 && b <= 31) return true;\n if (a === 192 && b === 168) return true;\n if (a === 100 && b >= 64 && b <= 127) return true;\n return false;\n}\n\nfunction isPrivateIpv6(ip: string): boolean {\n if (ip === '::1' || ip === '::') return true;\n\n const mapped = ip.match(/^::ffff:(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3})$/);\n if (mapped) return isPrivateIpv4(mapped[1]!);\n\n const hextet = parseInt(ip.split(':')[0] ?? '', 16);\n if (Number.isNaN(hextet)) return false;\n if (hextet >= 0xfc00 && hextet <= 0xfdff) return true;\n if (hextet >= 0xfe80 && hextet <= 0xfebf) return true;\n return false;\n}\n","import type { GeoError } from './errors.js';\n\nexport type Result<T> = { ok: true; value: T } | { ok: false; error: GeoError };\n\nexport function ok<T>(value: T): Result<T> {\n return { ok: true, value };\n}\n\nexport function err<T = never>(error: GeoError): Result<T> {\n return { ok: false, error };\n}\n","import { type GeoError, type GeoErrorCode, GeoErrorCodes, statusToCode } from '../errors.js';\nimport { err, ok, type Result } from '../result.js';\nimport type { ResolvedConfig } from '../options.js';\n\ninterface ApiErrorBody {\n error?: string;\n code?: string;\n}\n\nconst KNOWN_ERROR_CODES = new Set<string>(Object.values(GeoErrorCodes));\n\nexport async function send<T>(\n config: ResolvedConfig,\n method: 'GET' | 'POST',\n path: string,\n body: unknown,\n signal: AbortSignal | undefined,\n): Promise<Result<T>> {\n const url = `${config.baseUrl}${path}`;\n let attempt = 0;\n\n while (true) {\n const timeoutController = new AbortController();\n const timer = setTimeout(() => timeoutController.abort(), config.timeoutMs);\n const requestSignal = combineSignals(signal, timeoutController.signal);\n\n try {\n const response = await config.fetchImpl(url, {\n method,\n headers: {\n 'X-Api-Key': config.apiKey,\n Accept: 'application/json',\n ...(body === undefined ? {} : { 'Content-Type': 'application/json' }),\n },\n body: body === undefined ? undefined : JSON.stringify(body),\n signal: requestSignal,\n });\n clearTimeout(timer);\n\n if (response.ok) return await parseJson<T>(response);\n\n const error = await readError(response);\n if (shouldRetry(error.code) && attempt < config.maxRetries) {\n attempt += 1;\n await sleep(backoff(attempt, error.retryAfterSeconds));\n continue;\n }\n return err(error);\n } catch (cause) {\n clearTimeout(timer);\n if (signal?.aborted) throw cause;\n\n const timedOut = timeoutController.signal.aborted;\n const code = timedOut ? GeoErrorCodes.Timeout : GeoErrorCodes.NetworkError;\n if (attempt < config.maxRetries) {\n attempt += 1;\n await sleep(backoff(attempt));\n continue;\n }\n return err({\n code,\n message: timedOut ? 'The request timed out.' : 'The request could not reach the API.',\n });\n }\n }\n}\n\nasync function parseJson<T>(response: Response): Promise<Result<T>> {\n try {\n return ok((await response.json()) as T);\n } catch {\n return err({\n code: GeoErrorCodes.InvalidResponse,\n message: 'The API returned a response that could not be read.',\n statusCode: response.status,\n });\n }\n}\n\nasync function readError(response: Response): Promise<GeoError> {\n let parsed: ApiErrorBody = {};\n try {\n parsed = (await response.json()) as ApiErrorBody;\n } catch {\n parsed = {};\n }\n\n const bodyCode =\n parsed.code !== undefined && KNOWN_ERROR_CODES.has(parsed.code)\n ? (parsed.code as GeoErrorCode)\n : undefined;\n const code = bodyCode ?? statusToCode(response.status);\n return {\n code,\n message: parsed.error ?? response.statusText ?? 'The request failed.',\n statusCode: response.status,\n retryAfterSeconds: parseRetryAfter(response.headers.get('retry-after')),\n };\n}\n\nfunction shouldRetry(code: GeoErrorCode): boolean {\n return code === GeoErrorCodes.RateLimited || code === GeoErrorCodes.ServiceUnavailable;\n}\n\nfunction parseRetryAfter(value: string | null): number | undefined {\n if (!value) return undefined;\n const seconds = Number(value);\n return Number.isFinite(seconds) && seconds >= 0 ? seconds : undefined;\n}\n\nconst BASE_BACKOFF_MS = 200;\nconst MAX_BACKOFF_MS = 2_000;\n\nfunction backoff(attempt: number, retryAfterSeconds?: number): number {\n if (retryAfterSeconds !== undefined) return retryAfterSeconds * 1_000;\n const exponential = Math.min(MAX_BACKOFF_MS, BASE_BACKOFF_MS * 2 ** (attempt - 1));\n return exponential + Math.floor(Math.random() * BASE_BACKOFF_MS);\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nfunction combineSignals(\n external: AbortSignal | undefined,\n internal: AbortSignal,\n): 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","import type { GeoCache } from './cache.js';\n\nexport type IpGeoTraceEnvironment = 'production' | 'sandbox';\n\nexport const PRODUCTION_BASE_URL = 'https://lookup.ipgeotrace.com';\nexport const SANDBOX_BASE_URL = 'https://lookup.dev.ipgeotrace.com';\n\nexport interface IpGeoTraceClientOptions {\n apiKey: string;\n environment?: IpGeoTraceEnvironment;\n baseUrl?: string;\n timeoutMs?: number;\n maxRetries?: number;\n cache?: GeoCache | boolean;\n cacheTtlMs?: number;\n fetch?: typeof fetch;\n}\n\nexport interface ResolvedConfig {\n apiKey: string;\n baseUrl: string;\n timeoutMs: number;\n maxRetries: number;\n cache?: GeoCache;\n cacheTtlMs: number;\n fetchImpl: typeof fetch;\n}\n\nconst DEFAULT_TIMEOUT_MS = 10_000;\nconst DEFAULT_MAX_RETRIES = 2;\nconst DEFAULT_CACHE_TTL_MS = 5 * 60_000;\n\nexport function resolveConfig(\n options: IpGeoTraceClientOptions,\n defaultCache: () => GeoCache,\n): ResolvedConfig {\n if (!options.apiKey) throw new Error('IPGeoTrace: an apiKey is required.');\n\n const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;\n if (timeoutMs <= 0) throw new Error('IPGeoTrace: timeoutMs must be greater than zero.');\n if (maxRetries < 0) throw new Error('IPGeoTrace: maxRetries cannot be negative.');\n\n const fetchImpl = options.fetch ?? globalThis.fetch;\n if (typeof fetchImpl !== 'function') {\n throw new Error('IPGeoTrace: no global fetch available; pass options.fetch (Node < 18).');\n }\n\n let cache: GeoCache | undefined;\n if (options.cache === true) cache = defaultCache();\n else if (options.cache) cache = options.cache;\n\n return {\n apiKey: options.apiKey,\n baseUrl: resolveBaseUrl(options),\n timeoutMs,\n maxRetries,\n cache,\n cacheTtlMs: options.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS,\n fetchImpl,\n };\n}\n\nfunction resolveBaseUrl(options: IpGeoTraceClientOptions): string {\n const base = options.baseUrl ?? (options.environment === 'sandbox' ? SANDBOX_BASE_URL : PRODUCTION_BASE_URL);\n return base.replace(/\\/+$/, '');\n}\n","import { type GeoCache, MemoryGeoCache } from './cache.js';\nimport { GeoErrorCodes } from './errors.js';\nimport { isValidIp } from './internal/ip.js';\nimport { send } from './internal/http.js';\nimport {\n type IpGeoTraceClientOptions,\n resolveConfig,\n type ResolvedConfig,\n} from './options.js';\nimport { err, ok, type Result } from './result.js';\nimport type { BatchItem, BatchResponse, GeoResponse } from './types.js';\n\nconst MAX_BATCH_SIZE = 100;\nconst API_VERSION = 'v1';\n\nexport class IpGeoTraceClient {\n private readonly config: ResolvedConfig;\n private readonly cache?: GeoCache;\n\n constructor(options: IpGeoTraceClientOptions) {\n this.config = resolveConfig(options, () => new MemoryGeoCache());\n this.cache = this.config.cache;\n }\n\n async resolve(ip: string, signal?: AbortSignal): Promise<Result<GeoResponse>> {\n if (!isValidIp(ip)) {\n return err({ code: GeoErrorCodes.InvalidIp, message: `'${ip}' is not a valid IP address.` });\n }\n\n const cached = await this.readCache(ip);\n if (cached) return ok(cached);\n\n const result = await send<GeoResponse>(this.config, 'GET', `/${API_VERSION}/resolve/${encodeURIComponent(ip)}`, undefined, signal);\n if (result.ok) await this.writeCache(ip, result.value);\n return result;\n }\n\n async resolveBatch(ips: Iterable<string>, signal?: AbortSignal): Promise<Result<BatchResponse>> {\n const requested = [...ips];\n if (requested.length === 0) {\n return err({ code: GeoErrorCodes.BadRequest, message: 'At least one IP address is required.' });\n }\n if (requested.length > MAX_BATCH_SIZE) {\n return err({ code: GeoErrorCodes.BadRequest, message: `A batch may contain at most ${MAX_BATCH_SIZE} IP addresses.` });\n }\n\n const byIp = new Map<string, BatchItem>();\n const misses: string[] = [];\n for (const ip of requested) {\n if (!isValidIp(ip)) {\n byIp.set(ip, { ip, found: false, error: GeoErrorCodes.InvalidIp });\n continue;\n }\n const cached = await this.readCache(ip);\n if (cached) {\n byIp.set(ip, toBatchItem(cached));\n continue;\n }\n misses.push(ip);\n }\n\n let fromCache = true;\n if (misses.length > 0) {\n const response = await send<{ results: BatchItem[] }>(this.config, 'POST', `/${API_VERSION}/resolve/batch`, { ips: misses }, signal);\n if (!response.ok) return response;\n fromCache = false;\n const items = response.value.results;\n for (let i = 0; i < items.length; i += 1) {\n const item = items[i];\n if (!item) continue;\n const key = misses[i] ?? item.ip;\n byIp.set(key, item);\n if (item.found) await this.writeCache(key, toGeoResponse(item));\n }\n }\n\n const results = requested.map((ip) => byIp.get(ip) ?? { ip, found: false, error: GeoErrorCodes.Unknown });\n return ok({ results, fromCache });\n }\n\n private async readCache(ip: string): Promise<GeoResponse | undefined> {\n if (!this.cache) return undefined;\n try {\n return (await this.cache.get(ip)) ?? undefined;\n } catch {\n return undefined;\n }\n }\n\n private async writeCache(ip: string, value: GeoResponse): Promise<void> {\n if (!this.cache) return;\n try {\n await this.cache.set(ip, value, this.config.cacheTtlMs);\n } catch {\n return;\n }\n }\n}\n\nfunction toGeoResponse(item: BatchItem): GeoResponse {\n return {\n ip: item.ip,\n ipVersion: item.ip.includes(':') ? 6 : 4,\n continent: item.continent,\n country: item.country,\n region: item.region,\n city: item.city,\n location: item.location,\n asn: item.asn,\n isp: item.isp,\n };\n}\n\nfunction toBatchItem(response: GeoResponse): BatchItem {\n const { ipVersion: _ipVersion, ...rest } = response;\n return { ...rest, found: true };\n}\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
interface GeoResponse {
|
|
2
|
+
ip: string;
|
|
3
|
+
ipVersion: number;
|
|
4
|
+
continent?: Continent;
|
|
5
|
+
country?: Country;
|
|
6
|
+
region?: Region;
|
|
7
|
+
city?: City;
|
|
8
|
+
location?: GeoLocation;
|
|
9
|
+
asn?: Asn;
|
|
10
|
+
isp?: Isp;
|
|
11
|
+
}
|
|
12
|
+
interface Continent {
|
|
13
|
+
code?: string;
|
|
14
|
+
name?: string;
|
|
15
|
+
}
|
|
16
|
+
interface Country {
|
|
17
|
+
code?: string;
|
|
18
|
+
code3?: string;
|
|
19
|
+
name?: string;
|
|
20
|
+
currency?: string;
|
|
21
|
+
currencyName?: string;
|
|
22
|
+
phoneCode?: string;
|
|
23
|
+
languages?: string[];
|
|
24
|
+
}
|
|
25
|
+
interface Region {
|
|
26
|
+
code?: string;
|
|
27
|
+
name?: string;
|
|
28
|
+
}
|
|
29
|
+
interface City {
|
|
30
|
+
name?: string;
|
|
31
|
+
postalCode?: string;
|
|
32
|
+
population?: number;
|
|
33
|
+
}
|
|
34
|
+
interface GeoLocation {
|
|
35
|
+
latitude?: number;
|
|
36
|
+
longitude?: number;
|
|
37
|
+
accuracyRadiusKm?: number;
|
|
38
|
+
timeZone?: string;
|
|
39
|
+
gmtOffset?: number;
|
|
40
|
+
}
|
|
41
|
+
interface Asn {
|
|
42
|
+
number?: number;
|
|
43
|
+
name?: string;
|
|
44
|
+
type?: string;
|
|
45
|
+
}
|
|
46
|
+
interface Isp {
|
|
47
|
+
name?: string;
|
|
48
|
+
}
|
|
49
|
+
interface BatchResponse {
|
|
50
|
+
results: BatchItem[];
|
|
51
|
+
fromCache: boolean;
|
|
52
|
+
}
|
|
53
|
+
interface BatchItem {
|
|
54
|
+
ip: string;
|
|
55
|
+
found: boolean;
|
|
56
|
+
error?: string;
|
|
57
|
+
continent?: Continent;
|
|
58
|
+
country?: Country;
|
|
59
|
+
region?: Region;
|
|
60
|
+
city?: City;
|
|
61
|
+
location?: GeoLocation;
|
|
62
|
+
asn?: Asn;
|
|
63
|
+
isp?: Isp;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface GeoCache {
|
|
67
|
+
get(ip: string): Promise<GeoResponse | undefined> | GeoResponse | undefined;
|
|
68
|
+
set(ip: string, value: GeoResponse, ttlMs: number): Promise<void> | void;
|
|
69
|
+
}
|
|
70
|
+
declare class MemoryGeoCache implements GeoCache {
|
|
71
|
+
private readonly store;
|
|
72
|
+
get(ip: string): GeoResponse | undefined;
|
|
73
|
+
set(ip: string, value: GeoResponse, ttlMs: number): void;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
type IpGeoTraceEnvironment = 'production' | 'sandbox';
|
|
77
|
+
declare const PRODUCTION_BASE_URL = "https://lookup.ipgeotrace.com";
|
|
78
|
+
declare const SANDBOX_BASE_URL = "https://lookup.dev.ipgeotrace.com";
|
|
79
|
+
interface IpGeoTraceClientOptions {
|
|
80
|
+
apiKey: string;
|
|
81
|
+
environment?: IpGeoTraceEnvironment;
|
|
82
|
+
baseUrl?: string;
|
|
83
|
+
timeoutMs?: number;
|
|
84
|
+
maxRetries?: number;
|
|
85
|
+
cache?: GeoCache | boolean;
|
|
86
|
+
cacheTtlMs?: number;
|
|
87
|
+
fetch?: typeof fetch;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
declare const GeoErrorCodes: {
|
|
91
|
+
readonly Unauthorized: "unauthorized";
|
|
92
|
+
readonly InvalidIp: "invalid_ip";
|
|
93
|
+
readonly BadRequest: "bad_request";
|
|
94
|
+
readonly NotFound: "not_found";
|
|
95
|
+
readonly RateLimited: "rate_limited";
|
|
96
|
+
readonly QuotaExceeded: "quota_exceeded";
|
|
97
|
+
readonly LicenseInactive: "license_inactive";
|
|
98
|
+
readonly Forbidden: "forbidden";
|
|
99
|
+
readonly ServiceUnavailable: "service_unavailable";
|
|
100
|
+
readonly NetworkError: "network_error";
|
|
101
|
+
readonly Timeout: "timeout";
|
|
102
|
+
readonly InvalidResponse: "invalid_response";
|
|
103
|
+
readonly Unknown: "unknown";
|
|
104
|
+
};
|
|
105
|
+
type GeoErrorCode = (typeof GeoErrorCodes)[keyof typeof GeoErrorCodes];
|
|
106
|
+
interface GeoError {
|
|
107
|
+
code: GeoErrorCode;
|
|
108
|
+
message: string;
|
|
109
|
+
statusCode?: number;
|
|
110
|
+
retryAfterSeconds?: number;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
type Result<T> = {
|
|
114
|
+
ok: true;
|
|
115
|
+
value: T;
|
|
116
|
+
} | {
|
|
117
|
+
ok: false;
|
|
118
|
+
error: GeoError;
|
|
119
|
+
};
|
|
120
|
+
declare function ok<T>(value: T): Result<T>;
|
|
121
|
+
declare function err<T = never>(error: GeoError): Result<T>;
|
|
122
|
+
|
|
123
|
+
declare class IpGeoTraceClient {
|
|
124
|
+
private readonly config;
|
|
125
|
+
private readonly cache?;
|
|
126
|
+
constructor(options: IpGeoTraceClientOptions);
|
|
127
|
+
resolve(ip: string, signal?: AbortSignal): Promise<Result<GeoResponse>>;
|
|
128
|
+
resolveBatch(ips: Iterable<string>, signal?: AbortSignal): Promise<Result<BatchResponse>>;
|
|
129
|
+
private readCache;
|
|
130
|
+
private writeCache;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
declare function isValidIp(ip: string): boolean;
|
|
134
|
+
declare function isPrivateOrReservedIp(ip: string): boolean;
|
|
135
|
+
|
|
136
|
+
type GeoLookupStatus = 'resolved' | 'skipped' | 'failed' | 'not_attempted';
|
|
137
|
+
interface GeoLookup {
|
|
138
|
+
status: GeoLookupStatus;
|
|
139
|
+
value?: GeoResponse;
|
|
140
|
+
error?: GeoError;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export { type Asn, type BatchItem, type BatchResponse, type City, type Continent, type Country, type GeoCache, type GeoError, type GeoErrorCode, GeoErrorCodes, type GeoLocation, type GeoLookup, type GeoLookupStatus, type GeoResponse, IpGeoTraceClient, type IpGeoTraceClientOptions, type IpGeoTraceEnvironment, type Isp, MemoryGeoCache, PRODUCTION_BASE_URL, type Region, type Result, SANDBOX_BASE_URL, err, isPrivateOrReservedIp, isValidIp, ok };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
interface GeoResponse {
|
|
2
|
+
ip: string;
|
|
3
|
+
ipVersion: number;
|
|
4
|
+
continent?: Continent;
|
|
5
|
+
country?: Country;
|
|
6
|
+
region?: Region;
|
|
7
|
+
city?: City;
|
|
8
|
+
location?: GeoLocation;
|
|
9
|
+
asn?: Asn;
|
|
10
|
+
isp?: Isp;
|
|
11
|
+
}
|
|
12
|
+
interface Continent {
|
|
13
|
+
code?: string;
|
|
14
|
+
name?: string;
|
|
15
|
+
}
|
|
16
|
+
interface Country {
|
|
17
|
+
code?: string;
|
|
18
|
+
code3?: string;
|
|
19
|
+
name?: string;
|
|
20
|
+
currency?: string;
|
|
21
|
+
currencyName?: string;
|
|
22
|
+
phoneCode?: string;
|
|
23
|
+
languages?: string[];
|
|
24
|
+
}
|
|
25
|
+
interface Region {
|
|
26
|
+
code?: string;
|
|
27
|
+
name?: string;
|
|
28
|
+
}
|
|
29
|
+
interface City {
|
|
30
|
+
name?: string;
|
|
31
|
+
postalCode?: string;
|
|
32
|
+
population?: number;
|
|
33
|
+
}
|
|
34
|
+
interface GeoLocation {
|
|
35
|
+
latitude?: number;
|
|
36
|
+
longitude?: number;
|
|
37
|
+
accuracyRadiusKm?: number;
|
|
38
|
+
timeZone?: string;
|
|
39
|
+
gmtOffset?: number;
|
|
40
|
+
}
|
|
41
|
+
interface Asn {
|
|
42
|
+
number?: number;
|
|
43
|
+
name?: string;
|
|
44
|
+
type?: string;
|
|
45
|
+
}
|
|
46
|
+
interface Isp {
|
|
47
|
+
name?: string;
|
|
48
|
+
}
|
|
49
|
+
interface BatchResponse {
|
|
50
|
+
results: BatchItem[];
|
|
51
|
+
fromCache: boolean;
|
|
52
|
+
}
|
|
53
|
+
interface BatchItem {
|
|
54
|
+
ip: string;
|
|
55
|
+
found: boolean;
|
|
56
|
+
error?: string;
|
|
57
|
+
continent?: Continent;
|
|
58
|
+
country?: Country;
|
|
59
|
+
region?: Region;
|
|
60
|
+
city?: City;
|
|
61
|
+
location?: GeoLocation;
|
|
62
|
+
asn?: Asn;
|
|
63
|
+
isp?: Isp;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface GeoCache {
|
|
67
|
+
get(ip: string): Promise<GeoResponse | undefined> | GeoResponse | undefined;
|
|
68
|
+
set(ip: string, value: GeoResponse, ttlMs: number): Promise<void> | void;
|
|
69
|
+
}
|
|
70
|
+
declare class MemoryGeoCache implements GeoCache {
|
|
71
|
+
private readonly store;
|
|
72
|
+
get(ip: string): GeoResponse | undefined;
|
|
73
|
+
set(ip: string, value: GeoResponse, ttlMs: number): void;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
type IpGeoTraceEnvironment = 'production' | 'sandbox';
|
|
77
|
+
declare const PRODUCTION_BASE_URL = "https://lookup.ipgeotrace.com";
|
|
78
|
+
declare const SANDBOX_BASE_URL = "https://lookup.dev.ipgeotrace.com";
|
|
79
|
+
interface IpGeoTraceClientOptions {
|
|
80
|
+
apiKey: string;
|
|
81
|
+
environment?: IpGeoTraceEnvironment;
|
|
82
|
+
baseUrl?: string;
|
|
83
|
+
timeoutMs?: number;
|
|
84
|
+
maxRetries?: number;
|
|
85
|
+
cache?: GeoCache | boolean;
|
|
86
|
+
cacheTtlMs?: number;
|
|
87
|
+
fetch?: typeof fetch;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
declare const GeoErrorCodes: {
|
|
91
|
+
readonly Unauthorized: "unauthorized";
|
|
92
|
+
readonly InvalidIp: "invalid_ip";
|
|
93
|
+
readonly BadRequest: "bad_request";
|
|
94
|
+
readonly NotFound: "not_found";
|
|
95
|
+
readonly RateLimited: "rate_limited";
|
|
96
|
+
readonly QuotaExceeded: "quota_exceeded";
|
|
97
|
+
readonly LicenseInactive: "license_inactive";
|
|
98
|
+
readonly Forbidden: "forbidden";
|
|
99
|
+
readonly ServiceUnavailable: "service_unavailable";
|
|
100
|
+
readonly NetworkError: "network_error";
|
|
101
|
+
readonly Timeout: "timeout";
|
|
102
|
+
readonly InvalidResponse: "invalid_response";
|
|
103
|
+
readonly Unknown: "unknown";
|
|
104
|
+
};
|
|
105
|
+
type GeoErrorCode = (typeof GeoErrorCodes)[keyof typeof GeoErrorCodes];
|
|
106
|
+
interface GeoError {
|
|
107
|
+
code: GeoErrorCode;
|
|
108
|
+
message: string;
|
|
109
|
+
statusCode?: number;
|
|
110
|
+
retryAfterSeconds?: number;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
type Result<T> = {
|
|
114
|
+
ok: true;
|
|
115
|
+
value: T;
|
|
116
|
+
} | {
|
|
117
|
+
ok: false;
|
|
118
|
+
error: GeoError;
|
|
119
|
+
};
|
|
120
|
+
declare function ok<T>(value: T): Result<T>;
|
|
121
|
+
declare function err<T = never>(error: GeoError): Result<T>;
|
|
122
|
+
|
|
123
|
+
declare class IpGeoTraceClient {
|
|
124
|
+
private readonly config;
|
|
125
|
+
private readonly cache?;
|
|
126
|
+
constructor(options: IpGeoTraceClientOptions);
|
|
127
|
+
resolve(ip: string, signal?: AbortSignal): Promise<Result<GeoResponse>>;
|
|
128
|
+
resolveBatch(ips: Iterable<string>, signal?: AbortSignal): Promise<Result<BatchResponse>>;
|
|
129
|
+
private readCache;
|
|
130
|
+
private writeCache;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
declare function isValidIp(ip: string): boolean;
|
|
134
|
+
declare function isPrivateOrReservedIp(ip: string): boolean;
|
|
135
|
+
|
|
136
|
+
type GeoLookupStatus = 'resolved' | 'skipped' | 'failed' | 'not_attempted';
|
|
137
|
+
interface GeoLookup {
|
|
138
|
+
status: GeoLookupStatus;
|
|
139
|
+
value?: GeoResponse;
|
|
140
|
+
error?: GeoError;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export { type Asn, type BatchItem, type BatchResponse, type City, type Continent, type Country, type GeoCache, type GeoError, type GeoErrorCode, GeoErrorCodes, type GeoLocation, type GeoLookup, type GeoLookupStatus, type GeoResponse, IpGeoTraceClient, type IpGeoTraceClientOptions, type IpGeoTraceEnvironment, type Isp, MemoryGeoCache, PRODUCTION_BASE_URL, type Region, type Result, SANDBOX_BASE_URL, err, isPrivateOrReservedIp, isValidIp, ok };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
// src/cache.ts
|
|
2
|
+
var MemoryGeoCache = class {
|
|
3
|
+
store = /* @__PURE__ */ new Map();
|
|
4
|
+
get(ip) {
|
|
5
|
+
const entry = this.store.get(ip);
|
|
6
|
+
if (!entry) return void 0;
|
|
7
|
+
if (entry.expiresAt <= Date.now()) {
|
|
8
|
+
this.store.delete(ip);
|
|
9
|
+
return void 0;
|
|
10
|
+
}
|
|
11
|
+
return entry.value;
|
|
12
|
+
}
|
|
13
|
+
set(ip, value, ttlMs) {
|
|
14
|
+
this.store.set(ip, { value, expiresAt: Date.now() + ttlMs });
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// src/errors.ts
|
|
19
|
+
var GeoErrorCodes = {
|
|
20
|
+
Unauthorized: "unauthorized",
|
|
21
|
+
InvalidIp: "invalid_ip",
|
|
22
|
+
BadRequest: "bad_request",
|
|
23
|
+
NotFound: "not_found",
|
|
24
|
+
RateLimited: "rate_limited",
|
|
25
|
+
QuotaExceeded: "quota_exceeded",
|
|
26
|
+
LicenseInactive: "license_inactive",
|
|
27
|
+
Forbidden: "forbidden",
|
|
28
|
+
ServiceUnavailable: "service_unavailable",
|
|
29
|
+
NetworkError: "network_error",
|
|
30
|
+
Timeout: "timeout",
|
|
31
|
+
InvalidResponse: "invalid_response",
|
|
32
|
+
Unknown: "unknown"
|
|
33
|
+
};
|
|
34
|
+
function statusToCode(status) {
|
|
35
|
+
switch (status) {
|
|
36
|
+
case 400:
|
|
37
|
+
return GeoErrorCodes.BadRequest;
|
|
38
|
+
case 401:
|
|
39
|
+
return GeoErrorCodes.Unauthorized;
|
|
40
|
+
case 403:
|
|
41
|
+
return GeoErrorCodes.Forbidden;
|
|
42
|
+
case 404:
|
|
43
|
+
return GeoErrorCodes.NotFound;
|
|
44
|
+
case 429:
|
|
45
|
+
return GeoErrorCodes.RateLimited;
|
|
46
|
+
case 503:
|
|
47
|
+
return GeoErrorCodes.ServiceUnavailable;
|
|
48
|
+
default:
|
|
49
|
+
return GeoErrorCodes.Unknown;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// src/internal/ip.ts
|
|
54
|
+
var IPV4 = /^(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}$/;
|
|
55
|
+
var IPV6 = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|::(ffff(:0{1,4})?:)?((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d))$/;
|
|
56
|
+
function isValidIp(ip) {
|
|
57
|
+
return IPV4.test(ip) || IPV6.test(ip);
|
|
58
|
+
}
|
|
59
|
+
function isPrivateOrReservedIp(ip) {
|
|
60
|
+
const normalized = ip.trim().toLowerCase();
|
|
61
|
+
return normalized.includes(":") ? isPrivateIpv6(normalized) : isPrivateIpv4(normalized);
|
|
62
|
+
}
|
|
63
|
+
function isPrivateIpv4(ip) {
|
|
64
|
+
const parts = ip.split(".");
|
|
65
|
+
if (parts.length !== 4) return false;
|
|
66
|
+
const octets = parts.map((part) => Number(part));
|
|
67
|
+
if (octets.some((o) => !Number.isInteger(o) || o < 0 || o > 255)) return false;
|
|
68
|
+
const a = octets[0];
|
|
69
|
+
const b = octets[1];
|
|
70
|
+
if (a === 0 || a === 10 || a === 127) return true;
|
|
71
|
+
if (a === 169 && b === 254) return true;
|
|
72
|
+
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
73
|
+
if (a === 192 && b === 168) return true;
|
|
74
|
+
if (a === 100 && b >= 64 && b <= 127) return true;
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
function isPrivateIpv6(ip) {
|
|
78
|
+
if (ip === "::1" || ip === "::") return true;
|
|
79
|
+
const mapped = ip.match(/^::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/);
|
|
80
|
+
if (mapped) return isPrivateIpv4(mapped[1]);
|
|
81
|
+
const hextet = parseInt(ip.split(":")[0] ?? "", 16);
|
|
82
|
+
if (Number.isNaN(hextet)) return false;
|
|
83
|
+
if (hextet >= 64512 && hextet <= 65023) return true;
|
|
84
|
+
if (hextet >= 65152 && hextet <= 65215) return true;
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// src/result.ts
|
|
89
|
+
function ok(value) {
|
|
90
|
+
return { ok: true, value };
|
|
91
|
+
}
|
|
92
|
+
function err(error) {
|
|
93
|
+
return { ok: false, error };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// src/internal/http.ts
|
|
97
|
+
var KNOWN_ERROR_CODES = new Set(Object.values(GeoErrorCodes));
|
|
98
|
+
async function send(config, method, path, body, signal) {
|
|
99
|
+
const url = `${config.baseUrl}${path}`;
|
|
100
|
+
let attempt = 0;
|
|
101
|
+
while (true) {
|
|
102
|
+
const timeoutController = new AbortController();
|
|
103
|
+
const timer = setTimeout(() => timeoutController.abort(), config.timeoutMs);
|
|
104
|
+
const requestSignal = combineSignals(signal, timeoutController.signal);
|
|
105
|
+
try {
|
|
106
|
+
const response = await config.fetchImpl(url, {
|
|
107
|
+
method,
|
|
108
|
+
headers: {
|
|
109
|
+
"X-Api-Key": config.apiKey,
|
|
110
|
+
Accept: "application/json",
|
|
111
|
+
...body === void 0 ? {} : { "Content-Type": "application/json" }
|
|
112
|
+
},
|
|
113
|
+
body: body === void 0 ? void 0 : JSON.stringify(body),
|
|
114
|
+
signal: requestSignal
|
|
115
|
+
});
|
|
116
|
+
clearTimeout(timer);
|
|
117
|
+
if (response.ok) return await parseJson(response);
|
|
118
|
+
const error = await readError(response);
|
|
119
|
+
if (shouldRetry(error.code) && attempt < config.maxRetries) {
|
|
120
|
+
attempt += 1;
|
|
121
|
+
await sleep(backoff(attempt, error.retryAfterSeconds));
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
return err(error);
|
|
125
|
+
} catch (cause) {
|
|
126
|
+
clearTimeout(timer);
|
|
127
|
+
if (signal?.aborted) throw cause;
|
|
128
|
+
const timedOut = timeoutController.signal.aborted;
|
|
129
|
+
const code = timedOut ? GeoErrorCodes.Timeout : GeoErrorCodes.NetworkError;
|
|
130
|
+
if (attempt < config.maxRetries) {
|
|
131
|
+
attempt += 1;
|
|
132
|
+
await sleep(backoff(attempt));
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
return err({
|
|
136
|
+
code,
|
|
137
|
+
message: timedOut ? "The request timed out." : "The request could not reach the API."
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
async function parseJson(response) {
|
|
143
|
+
try {
|
|
144
|
+
return ok(await response.json());
|
|
145
|
+
} catch {
|
|
146
|
+
return err({
|
|
147
|
+
code: GeoErrorCodes.InvalidResponse,
|
|
148
|
+
message: "The API returned a response that could not be read.",
|
|
149
|
+
statusCode: response.status
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
async function readError(response) {
|
|
154
|
+
let parsed = {};
|
|
155
|
+
try {
|
|
156
|
+
parsed = await response.json();
|
|
157
|
+
} catch {
|
|
158
|
+
parsed = {};
|
|
159
|
+
}
|
|
160
|
+
const bodyCode = parsed.code !== void 0 && KNOWN_ERROR_CODES.has(parsed.code) ? parsed.code : void 0;
|
|
161
|
+
const code = bodyCode ?? statusToCode(response.status);
|
|
162
|
+
return {
|
|
163
|
+
code,
|
|
164
|
+
message: parsed.error ?? response.statusText ?? "The request failed.",
|
|
165
|
+
statusCode: response.status,
|
|
166
|
+
retryAfterSeconds: parseRetryAfter(response.headers.get("retry-after"))
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
function shouldRetry(code) {
|
|
170
|
+
return code === GeoErrorCodes.RateLimited || code === GeoErrorCodes.ServiceUnavailable;
|
|
171
|
+
}
|
|
172
|
+
function parseRetryAfter(value) {
|
|
173
|
+
if (!value) return void 0;
|
|
174
|
+
const seconds = Number(value);
|
|
175
|
+
return Number.isFinite(seconds) && seconds >= 0 ? seconds : void 0;
|
|
176
|
+
}
|
|
177
|
+
var BASE_BACKOFF_MS = 200;
|
|
178
|
+
var MAX_BACKOFF_MS = 2e3;
|
|
179
|
+
function backoff(attempt, retryAfterSeconds) {
|
|
180
|
+
if (retryAfterSeconds !== void 0) return retryAfterSeconds * 1e3;
|
|
181
|
+
const exponential = Math.min(MAX_BACKOFF_MS, BASE_BACKOFF_MS * 2 ** (attempt - 1));
|
|
182
|
+
return exponential + Math.floor(Math.random() * BASE_BACKOFF_MS);
|
|
183
|
+
}
|
|
184
|
+
function sleep(ms) {
|
|
185
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
186
|
+
}
|
|
187
|
+
function combineSignals(external, internal) {
|
|
188
|
+
if (!external) return internal;
|
|
189
|
+
const controller = new AbortController();
|
|
190
|
+
const abort = () => controller.abort();
|
|
191
|
+
if (external.aborted || internal.aborted) controller.abort();
|
|
192
|
+
external.addEventListener("abort", abort, { once: true });
|
|
193
|
+
internal.addEventListener("abort", abort, { once: true });
|
|
194
|
+
return controller.signal;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// src/options.ts
|
|
198
|
+
var PRODUCTION_BASE_URL = "https://lookup.ipgeotrace.com";
|
|
199
|
+
var SANDBOX_BASE_URL = "https://lookup.dev.ipgeotrace.com";
|
|
200
|
+
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
201
|
+
var DEFAULT_MAX_RETRIES = 2;
|
|
202
|
+
var DEFAULT_CACHE_TTL_MS = 5 * 6e4;
|
|
203
|
+
function resolveConfig(options, defaultCache) {
|
|
204
|
+
if (!options.apiKey) throw new Error("IPGeoTrace: an apiKey is required.");
|
|
205
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
206
|
+
const maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
207
|
+
if (timeoutMs <= 0) throw new Error("IPGeoTrace: timeoutMs must be greater than zero.");
|
|
208
|
+
if (maxRetries < 0) throw new Error("IPGeoTrace: maxRetries cannot be negative.");
|
|
209
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
210
|
+
if (typeof fetchImpl !== "function") {
|
|
211
|
+
throw new Error("IPGeoTrace: no global fetch available; pass options.fetch (Node < 18).");
|
|
212
|
+
}
|
|
213
|
+
let cache;
|
|
214
|
+
if (options.cache === true) cache = defaultCache();
|
|
215
|
+
else if (options.cache) cache = options.cache;
|
|
216
|
+
return {
|
|
217
|
+
apiKey: options.apiKey,
|
|
218
|
+
baseUrl: resolveBaseUrl(options),
|
|
219
|
+
timeoutMs,
|
|
220
|
+
maxRetries,
|
|
221
|
+
cache,
|
|
222
|
+
cacheTtlMs: options.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS,
|
|
223
|
+
fetchImpl
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
function resolveBaseUrl(options) {
|
|
227
|
+
const base = options.baseUrl ?? (options.environment === "sandbox" ? SANDBOX_BASE_URL : PRODUCTION_BASE_URL);
|
|
228
|
+
return base.replace(/\/+$/, "");
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// src/client.ts
|
|
232
|
+
var MAX_BATCH_SIZE = 100;
|
|
233
|
+
var API_VERSION = "v1";
|
|
234
|
+
var IpGeoTraceClient = class {
|
|
235
|
+
config;
|
|
236
|
+
cache;
|
|
237
|
+
constructor(options) {
|
|
238
|
+
this.config = resolveConfig(options, () => new MemoryGeoCache());
|
|
239
|
+
this.cache = this.config.cache;
|
|
240
|
+
}
|
|
241
|
+
async resolve(ip, signal) {
|
|
242
|
+
if (!isValidIp(ip)) {
|
|
243
|
+
return err({ code: GeoErrorCodes.InvalidIp, message: `'${ip}' is not a valid IP address.` });
|
|
244
|
+
}
|
|
245
|
+
const cached = await this.readCache(ip);
|
|
246
|
+
if (cached) return ok(cached);
|
|
247
|
+
const result = await send(this.config, "GET", `/${API_VERSION}/resolve/${encodeURIComponent(ip)}`, void 0, signal);
|
|
248
|
+
if (result.ok) await this.writeCache(ip, result.value);
|
|
249
|
+
return result;
|
|
250
|
+
}
|
|
251
|
+
async resolveBatch(ips, signal) {
|
|
252
|
+
const requested = [...ips];
|
|
253
|
+
if (requested.length === 0) {
|
|
254
|
+
return err({ code: GeoErrorCodes.BadRequest, message: "At least one IP address is required." });
|
|
255
|
+
}
|
|
256
|
+
if (requested.length > MAX_BATCH_SIZE) {
|
|
257
|
+
return err({ code: GeoErrorCodes.BadRequest, message: `A batch may contain at most ${MAX_BATCH_SIZE} IP addresses.` });
|
|
258
|
+
}
|
|
259
|
+
const byIp = /* @__PURE__ */ new Map();
|
|
260
|
+
const misses = [];
|
|
261
|
+
for (const ip of requested) {
|
|
262
|
+
if (!isValidIp(ip)) {
|
|
263
|
+
byIp.set(ip, { ip, found: false, error: GeoErrorCodes.InvalidIp });
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
const cached = await this.readCache(ip);
|
|
267
|
+
if (cached) {
|
|
268
|
+
byIp.set(ip, toBatchItem(cached));
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
misses.push(ip);
|
|
272
|
+
}
|
|
273
|
+
let fromCache = true;
|
|
274
|
+
if (misses.length > 0) {
|
|
275
|
+
const response = await send(this.config, "POST", `/${API_VERSION}/resolve/batch`, { ips: misses }, signal);
|
|
276
|
+
if (!response.ok) return response;
|
|
277
|
+
fromCache = false;
|
|
278
|
+
const items = response.value.results;
|
|
279
|
+
for (let i = 0; i < items.length; i += 1) {
|
|
280
|
+
const item = items[i];
|
|
281
|
+
if (!item) continue;
|
|
282
|
+
const key = misses[i] ?? item.ip;
|
|
283
|
+
byIp.set(key, item);
|
|
284
|
+
if (item.found) await this.writeCache(key, toGeoResponse(item));
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
const results = requested.map((ip) => byIp.get(ip) ?? { ip, found: false, error: GeoErrorCodes.Unknown });
|
|
288
|
+
return ok({ results, fromCache });
|
|
289
|
+
}
|
|
290
|
+
async readCache(ip) {
|
|
291
|
+
if (!this.cache) return void 0;
|
|
292
|
+
try {
|
|
293
|
+
return await this.cache.get(ip) ?? void 0;
|
|
294
|
+
} catch {
|
|
295
|
+
return void 0;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
async writeCache(ip, value) {
|
|
299
|
+
if (!this.cache) return;
|
|
300
|
+
try {
|
|
301
|
+
await this.cache.set(ip, value, this.config.cacheTtlMs);
|
|
302
|
+
} catch {
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
function toGeoResponse(item) {
|
|
308
|
+
return {
|
|
309
|
+
ip: item.ip,
|
|
310
|
+
ipVersion: item.ip.includes(":") ? 6 : 4,
|
|
311
|
+
continent: item.continent,
|
|
312
|
+
country: item.country,
|
|
313
|
+
region: item.region,
|
|
314
|
+
city: item.city,
|
|
315
|
+
location: item.location,
|
|
316
|
+
asn: item.asn,
|
|
317
|
+
isp: item.isp
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
function toBatchItem(response) {
|
|
321
|
+
const { ipVersion: _ipVersion, ...rest } = response;
|
|
322
|
+
return { ...rest, found: true };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export { GeoErrorCodes, IpGeoTraceClient, MemoryGeoCache, PRODUCTION_BASE_URL, SANDBOX_BASE_URL, err, isPrivateOrReservedIp, isValidIp, ok };
|
|
326
|
+
//# sourceMappingURL=index.js.map
|
|
327
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cache.ts","../src/errors.ts","../src/internal/ip.ts","../src/result.ts","../src/internal/http.ts","../src/options.ts","../src/client.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type { GeoResponse } from './types.js';\n\nexport interface GeoCache {\n get(ip: string): Promise<GeoResponse | undefined> | GeoResponse | undefined;\n set(ip: string, value: GeoResponse, ttlMs: number): Promise<void> | void;\n}\n\ninterface Entry {\n value: GeoResponse;\n expiresAt: number;\n}\n\nexport class MemoryGeoCache implements GeoCache {\n private readonly store = new Map<string, Entry>();\n\n get(ip: string): GeoResponse | undefined {\n const entry = this.store.get(ip);\n if (!entry) return undefined;\n if (entry.expiresAt <= Date.now()) {\n this.store.delete(ip);\n return undefined;\n }\n return entry.value;\n }\n\n set(ip: string, value: GeoResponse, ttlMs: number): void {\n this.store.set(ip, { value, expiresAt: Date.now() + ttlMs });\n }\n}\n","export const GeoErrorCodes = {\n Unauthorized: 'unauthorized',\n InvalidIp: 'invalid_ip',\n BadRequest: 'bad_request',\n NotFound: 'not_found',\n RateLimited: 'rate_limited',\n QuotaExceeded: 'quota_exceeded',\n LicenseInactive: 'license_inactive',\n Forbidden: 'forbidden',\n ServiceUnavailable: 'service_unavailable',\n NetworkError: 'network_error',\n Timeout: 'timeout',\n InvalidResponse: 'invalid_response',\n Unknown: 'unknown',\n} as const;\n\nexport type GeoErrorCode = (typeof GeoErrorCodes)[keyof typeof GeoErrorCodes];\n\nexport interface GeoError {\n code: GeoErrorCode;\n message: string;\n statusCode?: number;\n retryAfterSeconds?: number;\n}\n\nexport function 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","const IPV4 =\n /^(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}$/;\n\nconst IPV6 =\n /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|::(ffff(:0{1,4})?:)?((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\.){3}(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\.){3}(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d))$/;\n\nexport function isValidIp(ip: string): boolean {\n return IPV4.test(ip) || IPV6.test(ip);\n}\n\nexport function isPrivateOrReservedIp(ip: string): boolean {\n const normalized = ip.trim().toLowerCase();\n return normalized.includes(':') ? isPrivateIpv6(normalized) : isPrivateIpv4(normalized);\n}\n\nfunction isPrivateIpv4(ip: string): boolean {\n const parts = ip.split('.');\n if (parts.length !== 4) return false;\n const octets = parts.map((part) => Number(part));\n if (octets.some((o) => !Number.isInteger(o) || o < 0 || o > 255)) return false;\n\n const a = octets[0]!;\n const b = octets[1]!;\n if (a === 0 || a === 10 || a === 127) return true;\n if (a === 169 && b === 254) return true;\n if (a === 172 && b >= 16 && b <= 31) return true;\n if (a === 192 && b === 168) return true;\n if (a === 100 && b >= 64 && b <= 127) return true;\n return false;\n}\n\nfunction isPrivateIpv6(ip: string): boolean {\n if (ip === '::1' || ip === '::') return true;\n\n const mapped = ip.match(/^::ffff:(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3})$/);\n if (mapped) return isPrivateIpv4(mapped[1]!);\n\n const hextet = parseInt(ip.split(':')[0] ?? '', 16);\n if (Number.isNaN(hextet)) return false;\n if (hextet >= 0xfc00 && hextet <= 0xfdff) return true;\n if (hextet >= 0xfe80 && hextet <= 0xfebf) return true;\n return false;\n}\n","import type { GeoError } from './errors.js';\n\nexport type Result<T> = { ok: true; value: T } | { ok: false; error: GeoError };\n\nexport function ok<T>(value: T): Result<T> {\n return { ok: true, value };\n}\n\nexport function err<T = never>(error: GeoError): Result<T> {\n return { ok: false, error };\n}\n","import { type GeoError, type GeoErrorCode, GeoErrorCodes, statusToCode } from '../errors.js';\nimport { err, ok, type Result } from '../result.js';\nimport type { ResolvedConfig } from '../options.js';\n\ninterface ApiErrorBody {\n error?: string;\n code?: string;\n}\n\nconst KNOWN_ERROR_CODES = new Set<string>(Object.values(GeoErrorCodes));\n\nexport async function send<T>(\n config: ResolvedConfig,\n method: 'GET' | 'POST',\n path: string,\n body: unknown,\n signal: AbortSignal | undefined,\n): Promise<Result<T>> {\n const url = `${config.baseUrl}${path}`;\n let attempt = 0;\n\n while (true) {\n const timeoutController = new AbortController();\n const timer = setTimeout(() => timeoutController.abort(), config.timeoutMs);\n const requestSignal = combineSignals(signal, timeoutController.signal);\n\n try {\n const response = await config.fetchImpl(url, {\n method,\n headers: {\n 'X-Api-Key': config.apiKey,\n Accept: 'application/json',\n ...(body === undefined ? {} : { 'Content-Type': 'application/json' }),\n },\n body: body === undefined ? undefined : JSON.stringify(body),\n signal: requestSignal,\n });\n clearTimeout(timer);\n\n if (response.ok) return await parseJson<T>(response);\n\n const error = await readError(response);\n if (shouldRetry(error.code) && attempt < config.maxRetries) {\n attempt += 1;\n await sleep(backoff(attempt, error.retryAfterSeconds));\n continue;\n }\n return err(error);\n } catch (cause) {\n clearTimeout(timer);\n if (signal?.aborted) throw cause;\n\n const timedOut = timeoutController.signal.aborted;\n const code = timedOut ? GeoErrorCodes.Timeout : GeoErrorCodes.NetworkError;\n if (attempt < config.maxRetries) {\n attempt += 1;\n await sleep(backoff(attempt));\n continue;\n }\n return err({\n code,\n message: timedOut ? 'The request timed out.' : 'The request could not reach the API.',\n });\n }\n }\n}\n\nasync function parseJson<T>(response: Response): Promise<Result<T>> {\n try {\n return ok((await response.json()) as T);\n } catch {\n return err({\n code: GeoErrorCodes.InvalidResponse,\n message: 'The API returned a response that could not be read.',\n statusCode: response.status,\n });\n }\n}\n\nasync function readError(response: Response): Promise<GeoError> {\n let parsed: ApiErrorBody = {};\n try {\n parsed = (await response.json()) as ApiErrorBody;\n } catch {\n parsed = {};\n }\n\n const bodyCode =\n parsed.code !== undefined && KNOWN_ERROR_CODES.has(parsed.code)\n ? (parsed.code as GeoErrorCode)\n : undefined;\n const code = bodyCode ?? statusToCode(response.status);\n return {\n code,\n message: parsed.error ?? response.statusText ?? 'The request failed.',\n statusCode: response.status,\n retryAfterSeconds: parseRetryAfter(response.headers.get('retry-after')),\n };\n}\n\nfunction shouldRetry(code: GeoErrorCode): boolean {\n return code === GeoErrorCodes.RateLimited || code === GeoErrorCodes.ServiceUnavailable;\n}\n\nfunction parseRetryAfter(value: string | null): number | undefined {\n if (!value) return undefined;\n const seconds = Number(value);\n return Number.isFinite(seconds) && seconds >= 0 ? seconds : undefined;\n}\n\nconst BASE_BACKOFF_MS = 200;\nconst MAX_BACKOFF_MS = 2_000;\n\nfunction backoff(attempt: number, retryAfterSeconds?: number): number {\n if (retryAfterSeconds !== undefined) return retryAfterSeconds * 1_000;\n const exponential = Math.min(MAX_BACKOFF_MS, BASE_BACKOFF_MS * 2 ** (attempt - 1));\n return exponential + Math.floor(Math.random() * BASE_BACKOFF_MS);\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nfunction combineSignals(\n external: AbortSignal | undefined,\n internal: AbortSignal,\n): 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","import type { GeoCache } from './cache.js';\n\nexport type IpGeoTraceEnvironment = 'production' | 'sandbox';\n\nexport const PRODUCTION_BASE_URL = 'https://lookup.ipgeotrace.com';\nexport const SANDBOX_BASE_URL = 'https://lookup.dev.ipgeotrace.com';\n\nexport interface IpGeoTraceClientOptions {\n apiKey: string;\n environment?: IpGeoTraceEnvironment;\n baseUrl?: string;\n timeoutMs?: number;\n maxRetries?: number;\n cache?: GeoCache | boolean;\n cacheTtlMs?: number;\n fetch?: typeof fetch;\n}\n\nexport interface ResolvedConfig {\n apiKey: string;\n baseUrl: string;\n timeoutMs: number;\n maxRetries: number;\n cache?: GeoCache;\n cacheTtlMs: number;\n fetchImpl: typeof fetch;\n}\n\nconst DEFAULT_TIMEOUT_MS = 10_000;\nconst DEFAULT_MAX_RETRIES = 2;\nconst DEFAULT_CACHE_TTL_MS = 5 * 60_000;\n\nexport function resolveConfig(\n options: IpGeoTraceClientOptions,\n defaultCache: () => GeoCache,\n): ResolvedConfig {\n if (!options.apiKey) throw new Error('IPGeoTrace: an apiKey is required.');\n\n const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;\n if (timeoutMs <= 0) throw new Error('IPGeoTrace: timeoutMs must be greater than zero.');\n if (maxRetries < 0) throw new Error('IPGeoTrace: maxRetries cannot be negative.');\n\n const fetchImpl = options.fetch ?? globalThis.fetch;\n if (typeof fetchImpl !== 'function') {\n throw new Error('IPGeoTrace: no global fetch available; pass options.fetch (Node < 18).');\n }\n\n let cache: GeoCache | undefined;\n if (options.cache === true) cache = defaultCache();\n else if (options.cache) cache = options.cache;\n\n return {\n apiKey: options.apiKey,\n baseUrl: resolveBaseUrl(options),\n timeoutMs,\n maxRetries,\n cache,\n cacheTtlMs: options.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS,\n fetchImpl,\n };\n}\n\nfunction resolveBaseUrl(options: IpGeoTraceClientOptions): string {\n const base = options.baseUrl ?? (options.environment === 'sandbox' ? SANDBOX_BASE_URL : PRODUCTION_BASE_URL);\n return base.replace(/\\/+$/, '');\n}\n","import { type GeoCache, MemoryGeoCache } from './cache.js';\nimport { GeoErrorCodes } from './errors.js';\nimport { isValidIp } from './internal/ip.js';\nimport { send } from './internal/http.js';\nimport {\n type IpGeoTraceClientOptions,\n resolveConfig,\n type ResolvedConfig,\n} from './options.js';\nimport { err, ok, type Result } from './result.js';\nimport type { BatchItem, BatchResponse, GeoResponse } from './types.js';\n\nconst MAX_BATCH_SIZE = 100;\nconst API_VERSION = 'v1';\n\nexport class IpGeoTraceClient {\n private readonly config: ResolvedConfig;\n private readonly cache?: GeoCache;\n\n constructor(options: IpGeoTraceClientOptions) {\n this.config = resolveConfig(options, () => new MemoryGeoCache());\n this.cache = this.config.cache;\n }\n\n async resolve(ip: string, signal?: AbortSignal): Promise<Result<GeoResponse>> {\n if (!isValidIp(ip)) {\n return err({ code: GeoErrorCodes.InvalidIp, message: `'${ip}' is not a valid IP address.` });\n }\n\n const cached = await this.readCache(ip);\n if (cached) return ok(cached);\n\n const result = await send<GeoResponse>(this.config, 'GET', `/${API_VERSION}/resolve/${encodeURIComponent(ip)}`, undefined, signal);\n if (result.ok) await this.writeCache(ip, result.value);\n return result;\n }\n\n async resolveBatch(ips: Iterable<string>, signal?: AbortSignal): Promise<Result<BatchResponse>> {\n const requested = [...ips];\n if (requested.length === 0) {\n return err({ code: GeoErrorCodes.BadRequest, message: 'At least one IP address is required.' });\n }\n if (requested.length > MAX_BATCH_SIZE) {\n return err({ code: GeoErrorCodes.BadRequest, message: `A batch may contain at most ${MAX_BATCH_SIZE} IP addresses.` });\n }\n\n const byIp = new Map<string, BatchItem>();\n const misses: string[] = [];\n for (const ip of requested) {\n if (!isValidIp(ip)) {\n byIp.set(ip, { ip, found: false, error: GeoErrorCodes.InvalidIp });\n continue;\n }\n const cached = await this.readCache(ip);\n if (cached) {\n byIp.set(ip, toBatchItem(cached));\n continue;\n }\n misses.push(ip);\n }\n\n let fromCache = true;\n if (misses.length > 0) {\n const response = await send<{ results: BatchItem[] }>(this.config, 'POST', `/${API_VERSION}/resolve/batch`, { ips: misses }, signal);\n if (!response.ok) return response;\n fromCache = false;\n const items = response.value.results;\n for (let i = 0; i < items.length; i += 1) {\n const item = items[i];\n if (!item) continue;\n const key = misses[i] ?? item.ip;\n byIp.set(key, item);\n if (item.found) await this.writeCache(key, toGeoResponse(item));\n }\n }\n\n const results = requested.map((ip) => byIp.get(ip) ?? { ip, found: false, error: GeoErrorCodes.Unknown });\n return ok({ results, fromCache });\n }\n\n private async readCache(ip: string): Promise<GeoResponse | undefined> {\n if (!this.cache) return undefined;\n try {\n return (await this.cache.get(ip)) ?? undefined;\n } catch {\n return undefined;\n }\n }\n\n private async writeCache(ip: string, value: GeoResponse): Promise<void> {\n if (!this.cache) return;\n try {\n await this.cache.set(ip, value, this.config.cacheTtlMs);\n } catch {\n return;\n }\n }\n}\n\nfunction toGeoResponse(item: BatchItem): GeoResponse {\n return {\n ip: item.ip,\n ipVersion: item.ip.includes(':') ? 6 : 4,\n continent: item.continent,\n country: item.country,\n region: item.region,\n city: item.city,\n location: item.location,\n asn: item.asn,\n isp: item.isp,\n };\n}\n\nfunction toBatchItem(response: GeoResponse): BatchItem {\n const { ipVersion: _ipVersion, ...rest } = response;\n return { ...rest, found: true };\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ipgeotrace/client",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Isomorphic IPGeoTrace client for Node, edge, Deno, and Bun. Single and batch IP geolocation with caching and retries.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"ipgeotrace",
|
|
7
|
+
"geolocation",
|
|
8
|
+
"geoip",
|
|
9
|
+
"ip",
|
|
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
|
+
"engines": {
|
|
30
|
+
"node": ">=18.17"
|
|
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
|
+
}
|