@ipgeotrace/fastify 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,89 @@
1
+ # @ipgeotrace/fastify
2
+
3
+ Fastify plugin for [IPGeoTrace](https://ipgeotrace.com). Resolves the caller's location once per
4
+ request and hands it to you on `request.geo`. Built on [`@ipgeotrace/client`](../client) — the
5
+ secret key stays server-side.
6
+
7
+ Sign up and grab your API key at [ipgeotrace.com](https://ipgeotrace.com).
8
+
9
+ ## Install
10
+
11
+ ```sh
12
+ npm add @ipgeotrace/fastify fastify
13
+ ```
14
+
15
+ ## Usage
16
+
17
+ ```ts
18
+ import Fastify from 'fastify';
19
+ import { ipgeotrace, getGeo } from '@ipgeotrace/fastify';
20
+
21
+ const app = Fastify({ trustProxy: true }); // so request.ip reads X-Forwarded-For behind a proxy
22
+
23
+ await app.register(ipgeotrace, { apiKey: process.env.IPGEOTRACE_API_KEY! });
24
+
25
+ app.get('/checkout', (request, reply) => {
26
+ const geo = getGeo(request);
27
+ const currency = geo.status === 'resolved' ? geo.value?.country?.currency ?? 'USD' : 'USD';
28
+ reply.send({ currency });
29
+ });
30
+ ```
31
+
32
+ The plugin is registered app-wide (wrapped with `fastify-plugin`), so `request.geo` is available in
33
+ every route.
34
+
35
+ `request.geo` (and `getGeo(request)`) is a `GeoLookup` whose `status` tells you exactly what
36
+ happened:
37
+
38
+ - `resolved` — `value` carries the data.
39
+ - `skipped` — the caller's IP was missing, private, loopback, or link-local, so no API call was made.
40
+ - `failed` — `error` carries the reason (`rate_limited`, `quota_exceeded`, …).
41
+ - `not_attempted` — the plugin opted out for this request (`shouldResolve` returned false).
42
+
43
+ `getGeo()` is always safe to call and returns `not_attempted` when the plugin never ran.
44
+
45
+ ## Skipping requests
46
+
47
+ Health checks and internal endpoints should not spend lookups. Opt them out with `shouldResolve`:
48
+
49
+ ```ts
50
+ await app.register(ipgeotrace, {
51
+ apiKey: process.env.IPGEOTRACE_API_KEY!,
52
+ shouldResolve: (request) => !request.url.startsWith('/health') && !request.url.startsWith('/internal'),
53
+ });
54
+ ```
55
+
56
+ Private, loopback, and link-local addresses (including IPv4-mapped IPv6) are detected locally and
57
+ skipped without an API call or quota usage.
58
+
59
+ ## Choosing the caller's IP
60
+
61
+ By default the plugin uses `request.ip`, which respects Fastify's `trustProxy` setting. For full
62
+ control, supply your own selector:
63
+
64
+ ```ts
65
+ await app.register(ipgeotrace, {
66
+ apiKey: process.env.IPGEOTRACE_API_KEY!,
67
+ ipSelector: (request) => (request.headers['cf-connecting-ip'] as string) ?? request.ip,
68
+ });
69
+ ```
70
+
71
+ ## Configuration
72
+
73
+ Pass client options through `clientOptions`, or share a pre-built client across your app:
74
+
75
+ ```ts
76
+ import { IpGeoTraceClient } from '@ipgeotrace/client';
77
+
78
+ // inline client options
79
+ await app.register(ipgeotrace, {
80
+ apiKey: process.env.IPGEOTRACE_API_KEY!,
81
+ clientOptions: { cache: true, cacheTtlMs: 6 * 60 * 60_000, timeoutMs: 3_000 },
82
+ });
83
+
84
+ // or bring your own client (also usable directly in services)
85
+ const client = new IpGeoTraceClient({ apiKey: process.env.IPGEOTRACE_API_KEY!, cache: true });
86
+ await app.register(ipgeotrace, { client });
87
+ ```
88
+
89
+ See the [`@ipgeotrace/client` README](../client) for caching, retries, timeouts, and batch lookups.
package/dist/index.cjs ADDED
@@ -0,0 +1,50 @@
1
+ 'use strict';
2
+
3
+ var client = require('@ipgeotrace/client');
4
+ var fp = require('fastify-plugin');
5
+
6
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
7
+
8
+ var fp__default = /*#__PURE__*/_interopDefault(fp);
9
+
10
+ // src/plugin.ts
11
+ var plugin = async (fastify, options) => {
12
+ const client$1 = resolveClient(options);
13
+ const shouldResolve = options.shouldResolve ?? (() => true);
14
+ const ipSelector = options.ipSelector ?? ((req) => req.ip);
15
+ if (!fastify.hasRequestDecorator("geo")) {
16
+ fastify.decorateRequest("geo", void 0);
17
+ }
18
+ fastify.addHook("onRequest", async (req) => {
19
+ if (!shouldResolve(req)) {
20
+ req.geo = { status: "not_attempted" };
21
+ return;
22
+ }
23
+ const ip = ipSelector(req);
24
+ if (!ip || client.isPrivateOrReservedIp(ip)) {
25
+ req.geo = { status: "skipped" };
26
+ return;
27
+ }
28
+ const result = await client$1.resolve(ip);
29
+ req.geo = result.ok ? { status: "resolved", value: result.value } : { status: "failed", error: result.error };
30
+ });
31
+ };
32
+ var ipgeotrace = fp__default.default(plugin, {
33
+ fastify: ">=4.26.0 <6.0.0",
34
+ name: "@ipgeotrace/fastify"
35
+ });
36
+ function getGeo(req) {
37
+ return req.geo ?? { status: "not_attempted" };
38
+ }
39
+ function resolveClient(options) {
40
+ if (options.client) return options.client;
41
+ if (!options.apiKey) {
42
+ throw new Error("IPGeoTrace: pass either a client or an apiKey to the fastify plugin.");
43
+ }
44
+ return new client.IpGeoTraceClient({ apiKey: options.apiKey, ...options.clientOptions });
45
+ }
46
+
47
+ exports.getGeo = getGeo;
48
+ exports.ipgeotrace = ipgeotrace;
49
+ //# sourceMappingURL=index.cjs.map
50
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/plugin.ts"],"names":["client","isPrivateOrReservedIp","fp","IpGeoTraceClient"],"mappings":";;;;;;;;;;AAKA,IAAM,MAAA,GAAsD,OAAO,OAAA,EAAS,OAAA,KAAY;AACtF,EAAA,MAAMA,QAAA,GAAS,cAAc,OAAO,CAAA;AACpC,EAAA,MAAM,aAAA,GAAgB,OAAA,CAAQ,aAAA,KAAkB,MAAM,IAAA,CAAA;AACtD,EAAA,MAAM,UAAA,GAAa,OAAA,CAAQ,UAAA,KAAe,CAAC,QAAwB,GAAA,CAAI,EAAA,CAAA;AAEvE,EAAA,IAAI,CAAC,OAAA,CAAQ,mBAAA,CAAoB,KAAK,CAAA,EAAG;AACvC,IAAA,OAAA,CAAQ,eAAA,CAAgB,OAAO,MAAS,CAAA;AAAA,EAC1C;AAEA,EAAA,OAAA,CAAQ,OAAA,CAAQ,WAAA,EAAa,OAAO,GAAA,KAAwB;AAC1D,IAAA,IAAI,CAAC,aAAA,CAAc,GAAG,CAAA,EAAG;AACvB,MAAA,GAAA,CAAI,GAAA,GAAM,EAAE,MAAA,EAAQ,eAAA,EAAgB;AACpC,MAAA;AAAA,IACF;AAEA,IAAA,MAAM,EAAA,GAAK,WAAW,GAAG,CAAA;AACzB,IAAA,IAAI,CAAC,EAAA,IAAMC,4BAAA,CAAsB,EAAE,CAAA,EAAG;AACpC,MAAA,GAAA,CAAI,GAAA,GAAM,EAAE,MAAA,EAAQ,SAAA,EAAU;AAC9B,MAAA;AAAA,IACF;AAEA,IAAA,MAAM,MAAA,GAAS,MAAMD,QAAA,CAAO,OAAA,CAAQ,EAAE,CAAA;AACtC,IAAA,GAAA,CAAI,GAAA,GAAM,MAAA,CAAO,EAAA,GACb,EAAE,QAAQ,UAAA,EAAY,KAAA,EAAO,MAAA,CAAO,KAAA,KACpC,EAAE,MAAA,EAAQ,QAAA,EAAU,KAAA,EAAO,OAAO,KAAA,EAAM;AAAA,EAC9C,CAAC,CAAA;AACH,CAAA;AAEO,IAAM,UAAA,GAAaE,oBAAG,MAAA,EAAQ;AAAA,EACnC,OAAA,EAAS,iBAAA;AAAA,EACT,IAAA,EAAM;AACR,CAAC;AAEM,SAAS,OAAO,GAAA,EAAgC;AACrD,EAAA,OAAO,GAAA,CAAI,GAAA,IAAO,EAAE,MAAA,EAAQ,eAAA,EAAgB;AAC9C;AAEA,SAAS,cAAc,OAAA,EAAoD;AACzE,EAAA,IAAI,OAAA,CAAQ,MAAA,EAAQ,OAAO,OAAA,CAAQ,MAAA;AACnC,EAAA,IAAI,CAAC,QAAQ,MAAA,EAAQ;AACnB,IAAA,MAAM,IAAI,MAAM,sEAAsE,CAAA;AAAA,EACxF;AACA,EAAA,OAAO,IAAIC,wBAAiB,EAAE,MAAA,EAAQ,QAAQ,MAAA,EAAQ,GAAG,OAAA,CAAQ,aAAA,EAAe,CAAA;AAClF","file":"index.cjs","sourcesContent":["import { IpGeoTraceClient, isPrivateOrReservedIp, type GeoLookup } from '@ipgeotrace/client';\nimport type { FastifyPluginAsync, FastifyRequest } from 'fastify';\nimport fp from 'fastify-plugin';\nimport type { IpGeoTracePluginOptions } from './types.js';\n\nconst plugin: FastifyPluginAsync<IpGeoTracePluginOptions> = async (fastify, options) => {\n const client = resolveClient(options);\n const shouldResolve = options.shouldResolve ?? (() => true);\n const ipSelector = options.ipSelector ?? ((req: FastifyRequest) => req.ip);\n\n if (!fastify.hasRequestDecorator('geo')) {\n fastify.decorateRequest('geo', undefined);\n }\n\n fastify.addHook('onRequest', async (req: FastifyRequest) => {\n if (!shouldResolve(req)) {\n req.geo = { status: 'not_attempted' };\n return;\n }\n\n const ip = ipSelector(req);\n if (!ip || isPrivateOrReservedIp(ip)) {\n req.geo = { status: 'skipped' };\n return;\n }\n\n const result = await client.resolve(ip);\n req.geo = result.ok\n ? { status: 'resolved', value: result.value }\n : { status: 'failed', error: result.error };\n });\n};\n\nexport const ipgeotrace = fp(plugin, {\n fastify: '>=4.26.0 <6.0.0',\n name: '@ipgeotrace/fastify',\n});\n\nexport function getGeo(req: FastifyRequest): GeoLookup {\n return req.geo ?? { status: 'not_attempted' };\n}\n\nfunction resolveClient(options: IpGeoTracePluginOptions): IpGeoTraceClient {\n if (options.client) return options.client;\n if (!options.apiKey) {\n throw new Error('IPGeoTrace: pass either a client or an apiKey to the fastify plugin.');\n }\n return new IpGeoTraceClient({ apiKey: options.apiKey, ...options.clientOptions });\n}\n"]}
@@ -0,0 +1,21 @@
1
+ import { GeoLookup, IpGeoTraceClient, IpGeoTraceClientOptions } from '@ipgeotrace/client';
2
+ export { GeoError, GeoLookup, GeoLookupStatus, GeoResponse } from '@ipgeotrace/client';
3
+ import { FastifyRequest, FastifyPluginAsync } from 'fastify';
4
+
5
+ interface IpGeoTracePluginOptions {
6
+ apiKey?: string;
7
+ client?: IpGeoTraceClient;
8
+ clientOptions?: Omit<IpGeoTraceClientOptions, 'apiKey'>;
9
+ shouldResolve?: (req: FastifyRequest) => boolean;
10
+ ipSelector?: (req: FastifyRequest) => string | undefined;
11
+ }
12
+ declare module 'fastify' {
13
+ interface FastifyRequest {
14
+ geo?: GeoLookup;
15
+ }
16
+ }
17
+
18
+ declare const ipgeotrace: FastifyPluginAsync<IpGeoTracePluginOptions>;
19
+ declare function getGeo(req: FastifyRequest): GeoLookup;
20
+
21
+ export { type IpGeoTracePluginOptions, getGeo, ipgeotrace };
@@ -0,0 +1,21 @@
1
+ import { GeoLookup, IpGeoTraceClient, IpGeoTraceClientOptions } from '@ipgeotrace/client';
2
+ export { GeoError, GeoLookup, GeoLookupStatus, GeoResponse } from '@ipgeotrace/client';
3
+ import { FastifyRequest, FastifyPluginAsync } from 'fastify';
4
+
5
+ interface IpGeoTracePluginOptions {
6
+ apiKey?: string;
7
+ client?: IpGeoTraceClient;
8
+ clientOptions?: Omit<IpGeoTraceClientOptions, 'apiKey'>;
9
+ shouldResolve?: (req: FastifyRequest) => boolean;
10
+ ipSelector?: (req: FastifyRequest) => string | undefined;
11
+ }
12
+ declare module 'fastify' {
13
+ interface FastifyRequest {
14
+ geo?: GeoLookup;
15
+ }
16
+ }
17
+
18
+ declare const ipgeotrace: FastifyPluginAsync<IpGeoTracePluginOptions>;
19
+ declare function getGeo(req: FastifyRequest): GeoLookup;
20
+
21
+ export { type IpGeoTracePluginOptions, getGeo, ipgeotrace };
package/dist/index.js ADDED
@@ -0,0 +1,43 @@
1
+ import { isPrivateOrReservedIp, IpGeoTraceClient } from '@ipgeotrace/client';
2
+ import fp from 'fastify-plugin';
3
+
4
+ // src/plugin.ts
5
+ var plugin = async (fastify, options) => {
6
+ const client = resolveClient(options);
7
+ const shouldResolve = options.shouldResolve ?? (() => true);
8
+ const ipSelector = options.ipSelector ?? ((req) => req.ip);
9
+ if (!fastify.hasRequestDecorator("geo")) {
10
+ fastify.decorateRequest("geo", void 0);
11
+ }
12
+ fastify.addHook("onRequest", async (req) => {
13
+ if (!shouldResolve(req)) {
14
+ req.geo = { status: "not_attempted" };
15
+ return;
16
+ }
17
+ const ip = ipSelector(req);
18
+ if (!ip || isPrivateOrReservedIp(ip)) {
19
+ req.geo = { status: "skipped" };
20
+ return;
21
+ }
22
+ const result = await client.resolve(ip);
23
+ req.geo = result.ok ? { status: "resolved", value: result.value } : { status: "failed", error: result.error };
24
+ });
25
+ };
26
+ var ipgeotrace = fp(plugin, {
27
+ fastify: ">=4.26.0 <6.0.0",
28
+ name: "@ipgeotrace/fastify"
29
+ });
30
+ function getGeo(req) {
31
+ return req.geo ?? { status: "not_attempted" };
32
+ }
33
+ function resolveClient(options) {
34
+ if (options.client) return options.client;
35
+ if (!options.apiKey) {
36
+ throw new Error("IPGeoTrace: pass either a client or an apiKey to the fastify plugin.");
37
+ }
38
+ return new IpGeoTraceClient({ apiKey: options.apiKey, ...options.clientOptions });
39
+ }
40
+
41
+ export { getGeo, ipgeotrace };
42
+ //# sourceMappingURL=index.js.map
43
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/plugin.ts"],"names":[],"mappings":";;;;AAKA,IAAM,MAAA,GAAsD,OAAO,OAAA,EAAS,OAAA,KAAY;AACtF,EAAA,MAAM,MAAA,GAAS,cAAc,OAAO,CAAA;AACpC,EAAA,MAAM,aAAA,GAAgB,OAAA,CAAQ,aAAA,KAAkB,MAAM,IAAA,CAAA;AACtD,EAAA,MAAM,UAAA,GAAa,OAAA,CAAQ,UAAA,KAAe,CAAC,QAAwB,GAAA,CAAI,EAAA,CAAA;AAEvE,EAAA,IAAI,CAAC,OAAA,CAAQ,mBAAA,CAAoB,KAAK,CAAA,EAAG;AACvC,IAAA,OAAA,CAAQ,eAAA,CAAgB,OAAO,MAAS,CAAA;AAAA,EAC1C;AAEA,EAAA,OAAA,CAAQ,OAAA,CAAQ,WAAA,EAAa,OAAO,GAAA,KAAwB;AAC1D,IAAA,IAAI,CAAC,aAAA,CAAc,GAAG,CAAA,EAAG;AACvB,MAAA,GAAA,CAAI,GAAA,GAAM,EAAE,MAAA,EAAQ,eAAA,EAAgB;AACpC,MAAA;AAAA,IACF;AAEA,IAAA,MAAM,EAAA,GAAK,WAAW,GAAG,CAAA;AACzB,IAAA,IAAI,CAAC,EAAA,IAAM,qBAAA,CAAsB,EAAE,CAAA,EAAG;AACpC,MAAA,GAAA,CAAI,GAAA,GAAM,EAAE,MAAA,EAAQ,SAAA,EAAU;AAC9B,MAAA;AAAA,IACF;AAEA,IAAA,MAAM,MAAA,GAAS,MAAM,MAAA,CAAO,OAAA,CAAQ,EAAE,CAAA;AACtC,IAAA,GAAA,CAAI,GAAA,GAAM,MAAA,CAAO,EAAA,GACb,EAAE,QAAQ,UAAA,EAAY,KAAA,EAAO,MAAA,CAAO,KAAA,KACpC,EAAE,MAAA,EAAQ,QAAA,EAAU,KAAA,EAAO,OAAO,KAAA,EAAM;AAAA,EAC9C,CAAC,CAAA;AACH,CAAA;AAEO,IAAM,UAAA,GAAa,GAAG,MAAA,EAAQ;AAAA,EACnC,OAAA,EAAS,iBAAA;AAAA,EACT,IAAA,EAAM;AACR,CAAC;AAEM,SAAS,OAAO,GAAA,EAAgC;AACrD,EAAA,OAAO,GAAA,CAAI,GAAA,IAAO,EAAE,MAAA,EAAQ,eAAA,EAAgB;AAC9C;AAEA,SAAS,cAAc,OAAA,EAAoD;AACzE,EAAA,IAAI,OAAA,CAAQ,MAAA,EAAQ,OAAO,OAAA,CAAQ,MAAA;AACnC,EAAA,IAAI,CAAC,QAAQ,MAAA,EAAQ;AACnB,IAAA,MAAM,IAAI,MAAM,sEAAsE,CAAA;AAAA,EACxF;AACA,EAAA,OAAO,IAAI,iBAAiB,EAAE,MAAA,EAAQ,QAAQ,MAAA,EAAQ,GAAG,OAAA,CAAQ,aAAA,EAAe,CAAA;AAClF","file":"index.js","sourcesContent":["import { IpGeoTraceClient, isPrivateOrReservedIp, type GeoLookup } from '@ipgeotrace/client';\nimport type { FastifyPluginAsync, FastifyRequest } from 'fastify';\nimport fp from 'fastify-plugin';\nimport type { IpGeoTracePluginOptions } from './types.js';\n\nconst plugin: FastifyPluginAsync<IpGeoTracePluginOptions> = async (fastify, options) => {\n const client = resolveClient(options);\n const shouldResolve = options.shouldResolve ?? (() => true);\n const ipSelector = options.ipSelector ?? ((req: FastifyRequest) => req.ip);\n\n if (!fastify.hasRequestDecorator('geo')) {\n fastify.decorateRequest('geo', undefined);\n }\n\n fastify.addHook('onRequest', async (req: FastifyRequest) => {\n if (!shouldResolve(req)) {\n req.geo = { status: 'not_attempted' };\n return;\n }\n\n const ip = ipSelector(req);\n if (!ip || isPrivateOrReservedIp(ip)) {\n req.geo = { status: 'skipped' };\n return;\n }\n\n const result = await client.resolve(ip);\n req.geo = result.ok\n ? { status: 'resolved', value: result.value }\n : { status: 'failed', error: result.error };\n });\n};\n\nexport const ipgeotrace = fp(plugin, {\n fastify: '>=4.26.0 <6.0.0',\n name: '@ipgeotrace/fastify',\n});\n\nexport function getGeo(req: FastifyRequest): GeoLookup {\n return req.geo ?? { status: 'not_attempted' };\n}\n\nfunction resolveClient(options: IpGeoTracePluginOptions): IpGeoTraceClient {\n if (options.client) return options.client;\n if (!options.apiKey) {\n throw new Error('IPGeoTrace: pass either a client or an apiKey to the fastify plugin.');\n }\n return new IpGeoTraceClient({ apiKey: options.apiKey, ...options.clientOptions });\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@ipgeotrace/fastify",
3
+ "version": "0.1.0",
4
+ "description": "Fastify plugin for IPGeoTrace. Resolves the caller's location once per request and exposes it on request.geo.",
5
+ "keywords": [
6
+ "ipgeotrace",
7
+ "geolocation",
8
+ "geoip",
9
+ "fastify",
10
+ "plugin",
11
+ "typescript"
12
+ ],
13
+ "license": "MIT",
14
+ "homepage": "https://ipgeotrace.com",
15
+ "type": "module",
16
+ "main": "./dist/index.cjs",
17
+ "module": "./dist/index.js",
18
+ "types": "./dist/index.d.ts",
19
+ "exports": {
20
+ ".": {
21
+ "types": "./dist/index.d.ts",
22
+ "import": "./dist/index.js",
23
+ "require": "./dist/index.cjs"
24
+ }
25
+ },
26
+ "files": [
27
+ "dist"
28
+ ],
29
+ "sideEffects": false,
30
+ "engines": {
31
+ "node": ">=18.17"
32
+ },
33
+ "dependencies": {
34
+ "fastify-plugin": "^4.5.1 || ^5.0.1",
35
+ "@ipgeotrace/client": "0.1.0"
36
+ },
37
+ "peerDependencies": {
38
+ "fastify": "^4.26.0 || ^5.0.0"
39
+ },
40
+ "devDependencies": {
41
+ "fastify": "^5.0.0",
42
+ "tsup": "^8.3.5",
43
+ "typescript": "^5.6.3"
44
+ },
45
+ "scripts": {
46
+ "build": "tsup",
47
+ "dev": "tsup --watch",
48
+ "typecheck": "tsc --noEmit",
49
+ "clean": "rm -rf dist"
50
+ }
51
+ }