hono-ip 1.0.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,119 @@
1
+ # hono-ip
2
+
3
+ A tiny, fast middleware for [Hono](https://hono.dev) that figures out your client's real IP address - even when your app sits behind proxies, load balancers, or CDNs.
4
+
5
+ Works on **Node.js** and **Bun** out of the box. No regex. No dependencies.
6
+
7
+ ## Why?
8
+
9
+ Getting the "real" client IP in a web app is surprisingly annoying. Your server sees the proxy's address, not the user's. Different infrastructure stacks stuff the original IP into different headers - Cloudflare uses `CF-Connecting-IP`, AWS might use `X-Forwarded-For`, Fastly has its own thing, and so on.
10
+
11
+ This middleware checks all the common headers in a sensible order, validates every candidate with native `net.isIP` and hands you back a single, trustworthy string.
12
+
13
+ ## Quick start
14
+
15
+ ```bash
16
+ npm install hono-ip
17
+ ```
18
+
19
+ ```ts
20
+ import { Hono } from "hono";
21
+ import { ipMiddleware } from "hono-ip";
22
+
23
+ const app = new Hono();
24
+
25
+ app.use(ipMiddleware());
26
+
27
+ app.get("/", (c) => {
28
+ const ip = c.get("ip"); // string | null
29
+ return c.text(`Hello, ${ip ?? "stranger"}`);
30
+ });
31
+ ```
32
+
33
+ That's it. Every route after the middleware can read `c.get("ip")` or `c.var.ip`.
34
+
35
+ ## Going deeper
36
+
37
+ ### Trusted proxies
38
+
39
+ By default, the middleware returns the **leftmost** IP from `X-Forwarded-For` - the classic approach. The problem is that a malicious client can prepend whatever they want to that header:
40
+
41
+ ```
42
+ X-Forwarded-For: 6.6.6.6, <actual client>, <your proxy>
43
+ ↑ attacker injected this
44
+ ```
45
+
46
+ If you know your proxy IPs, pass them in. The middleware will then walk `X-Forwarded-For` **from the right**, skipping trusted addresses, and return the first IP it doesn't recognise - which is the real client:
47
+
48
+ ```ts
49
+ app.use(
50
+ ipMiddleware({
51
+ trustedProxies: new Set(["10.0.0.1", "10.0.0.2"]),
52
+ })
53
+ );
54
+ ```
55
+
56
+ ### Runtime-level connection info
57
+
58
+ Headers can be spoofed. The one thing that _can't_ be faked is the TCP connection's remote address, which Hono exposes through its `getConnInfo` helper. Pass it in to use it as a final fallback:
59
+
60
+ ```ts
61
+ // Bun
62
+ import { getConnInfo } from "hono/bun";
63
+
64
+ // Node.js
65
+ // import { getConnInfo } from "@hono/node-server/conninfo";
66
+
67
+ app.use(ipMiddleware({ getConnInfo }));
68
+ ```
69
+
70
+ When no header yields a valid IP, the middleware will call `getConnInfo(c).remote.address` and use that instead. If `getConnInfo` throws (e.g. during tests where there's no real server), it's caught silently.
71
+
72
+ ### Using `getClientIp` directly
73
+
74
+ You don't have to use the middleware. The core function is exported on its own:
75
+
76
+ ```ts
77
+ import { getClientIp } from "hono-ip";
78
+
79
+ app.get("/ip", (c) => {
80
+ const ip = getClientIp(c, {
81
+ trustedProxies: new Set(["10.0.0.1"]),
82
+ });
83
+ return c.json({ ip });
84
+ });
85
+ ```
86
+
87
+ ### Custom context variable name
88
+
89
+ If `"ip"` collides with something in your app:
90
+
91
+ ```ts
92
+ app.use(ipMiddleware({ attributeName: "clientIp" }));
93
+
94
+ // later
95
+ c.get("clientIp");
96
+ ```
97
+
98
+ ## Resolution order
99
+
100
+ The middleware checks these sources top-to-bottom and returns the first valid IP it finds:
101
+
102
+ | Priority | Source | Notes |
103
+ |----------|--------|-------|
104
+ | 1 | `X-Client-IP` | Set by some proxies and load balancers |
105
+ | 2 | `X-Forwarded-For` | Leftmost valid IP, or rightmost untrusted if `trustedProxies` is set |
106
+ | 3 | `CF-Connecting-IP` | Cloudflare |
107
+ | 4 | `Fastly-Client-Ip` | Fastly |
108
+ | 5 | `True-Client-Ip` | Akamai, Cloudflare enterprise |
109
+ | 6 | `X-Real-IP` | Nginx default config |
110
+ | 7 | `X-Cluster-Client-IP` | Rackspace, Riverbed |
111
+ | 8 | `X-Forwarded` | Non-standard single-IP variant |
112
+ | 9 | `Forwarded-For` | Non-standard single-IP variant |
113
+ | 10 | `Forwarded` | RFC 7239 - parses `for="..."` value, handles bracketed IPv6 |
114
+ | 11 | `X-Appengine-User-Ip` | Google App Engine |
115
+ | 12 | `getConnInfo()` | Hono runtime adapter (Bun / Node / CF Workers / Deno / …) |
116
+ | 13 | `Cf-Pseudo-IPv4` | Cloudflare pseudo IPv4 for IPv6 visitors |
117
+
118
+ ## License
119
+ Apache-2.0
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ import{createMiddleware as U}from"hono/factory";import{isIp as T,getFirstFromXForwardedFor as V,parseForwarded as W}from"./ip";import{isIp as v,isIpV4 as y,isIpV6 as D,extractIp as H,getFirstFromXForwardedFor as M,parseForwarded as R}from"./ip";var Y=["x-client-ip","cf-connecting-ip","fastly-client-ip","true-client-ip","x-real-ip","x-cluster-client-ip"],Z=["x-appengine-user-ip","cf-pseudo-ipv4"];function J(b,u){let q=b.req.header(u);if(!q)return null;let z=q.trim();return T(z)?z:null}function $(b,u){let q=J(b,"x-client-ip");if(q)return q;let z=V(b.req.header("x-forwarded-for"),u);if(z)return z;for(let G of Y){if(G==="x-client-ip")continue;let j=J(b,G);if(j)return j}let N=J(b,"x-forwarded");if(N)return N;let O=J(b,"forwarded-for");if(O)return O;let Q=W(b.req.header("forwarded"));if(Q)return Q;for(let G of Z){let j=J(b,G);if(j)return j}if(u?.getConnInfo)try{let j=u.getConnInfo(b)?.remote?.address;if(j&&T(j))return j}catch{}return console.log("No IP found"),null}function P(b={}){let u=b.attributeName??"ip";return U(async(q,z)=>{q.set(u,$(q,b)),await z()})}export{R as parseForwarded,D as isIpV6,y as isIpV4,v as isIp,P as ipMiddleware,M as getFirstFromXForwardedFor,$ as getClientIp,H as extractIp};
2
+
3
+ //# debugId=5CD5D6EBA955279864756E2164756E21
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["..\\src\\index.ts"],
4
+ "sourcesContent": [
5
+ "import { createMiddleware } from \"hono/factory\";\nimport type { Context } from \"hono\";\nimport {\n isIp,\n extractIp,\n getFirstFromXForwardedFor,\n parseForwarded,\n type XffOptions,\n} from \"./ip\";\n\ndeclare module \"hono\" {\n interface ContextVariableMap {\n ip: string | null;\n }\n}\n\n// ──────────────────────────────────────────────\n// Header priority (same order as request-ip):\n// 1. X-Client-IP\n// 2. X-Forwarded-For (leftmost valid, or rightmost untrusted)\n// 3. CF-Connecting-IP\n// 4. Fastly-Client-Ip\n// 5. True-Client-Ip\n// 6. X-Real-IP\n// 7. X-Cluster-Client-IP\n// 8. X-Forwarded / Forwarded-For / Forwarded (RFC 7239)\n// 9. X-Appengine-User-Ip\n// 10. Hono getConnInfo (Bun / Node / CF Workers / …)\n// 11. Cf-Pseudo-IPv4\n// ──────────────────────────────────────────────\n\nconst SIMPLE_HEADERS = [\n \"x-client-ip\",\n \"cf-connecting-ip\",\n \"fastly-client-ip\",\n \"true-client-ip\",\n \"x-real-ip\",\n \"x-cluster-client-ip\",\n] as const;\n\nconst FALLBACK_HEADERS = [\n \"x-appengine-user-ip\",\n \"cf-pseudo-ipv4\",\n] as const;\n\nfunction headerIp(c: Context, name: string): string | null {\n const v = c.req.header(name);\n if (!v) return null;\n const ip = v.trim();\n return isIp(ip) ? ip : null;\n}\n\nexport interface IpMiddlewareOptions extends XffOptions {\n attributeName?: string;\n\n getConnInfo?: (c: Context) => { remote: { address?: string } };\n}\n\nexport function getClientIp(c: Context, opts?: IpMiddlewareOptions): string | null {\n const xClientIp = headerIp(c, \"x-client-ip\");\n if (xClientIp) return xClientIp;\n\n const xff = getFirstFromXForwardedFor(c.req.header(\"x-forwarded-for\"), opts);\n if (xff) return xff;\n\n for (const name of SIMPLE_HEADERS) {\n if (name === \"x-client-ip\") continue;\n const ip = headerIp(c, name);\n if (ip) return ip;\n }\n\n const xForwarded = headerIp(c, \"x-forwarded\");\n if (xForwarded) return xForwarded;\n\n const forwardedFor = headerIp(c, \"forwarded-for\");\n if (forwardedFor) return forwardedFor;\n\n const forwarded = parseForwarded(c.req.header(\"forwarded\"));\n if (forwarded) return forwarded;\n\n for (const name of FALLBACK_HEADERS) {\n const ip = headerIp(c, name);\n if (ip) return ip;\n }\n\n if (opts?.getConnInfo) {\n try {\n const info = opts.getConnInfo(c);\n const addr = info?.remote?.address;\n if (addr && isIp(addr)) return addr;\n } catch {}\n }\n\n console.log(\"No IP found\");\n return null;\n}\n\nexport function ipMiddleware(opts: IpMiddlewareOptions = {}) {\n const key = (opts.attributeName ?? \"ip\") as \"ip\";\n\n return createMiddleware(async (c, next) => {\n c.set(key, getClientIp(c, opts));\n await next();\n });\n}\n\nexport { isIp, isIpV4, isIpV6, extractIp, getFirstFromXForwardedFor, parseForwarded } from \"./ip\";"
6
+ ],
7
+ "mappings": "AAAA,2BAAS,qBAET,eACE,+BAEA,oBACA,aAoGF,eAAS,YAAM,YAAQ,eAAQ,+BAAW,oBAA2B,aA3ErE,IAAM,EAAiB,CACrB,cACA,mBACA,mBACA,iBACA,YACA,qBACF,EAEM,EAAmB,CACvB,sBACA,gBACF,EAEA,SAAS,CAAQ,CAAC,EAAY,EAA6B,CACzD,IAAM,EAAI,EAAE,IAAI,OAAO,CAAI,EAC3B,GAAI,CAAC,EAAG,OAAO,KACf,IAAM,EAAK,EAAE,KAAK,EAClB,OAAO,EAAK,CAAE,EAAI,EAAK,KASlB,SAAS,CAAW,CAAC,EAAY,EAA2C,CACjF,IAAM,EAAY,EAAS,EAAG,aAAa,EAC3C,GAAI,EAAW,OAAO,EAEtB,IAAM,EAAM,EAA0B,EAAE,IAAI,OAAO,iBAAiB,EAAG,CAAI,EAC3E,GAAI,EAAK,OAAO,EAEhB,QAAW,KAAQ,EAAgB,CACjC,GAAI,IAAS,cAAe,SAC5B,IAAM,EAAK,EAAS,EAAG,CAAI,EAC3B,GAAI,EAAI,OAAO,EAGjB,IAAM,EAAa,EAAS,EAAG,aAAa,EAC5C,GAAI,EAAY,OAAO,EAEvB,IAAM,EAAe,EAAS,EAAG,eAAe,EAChD,GAAI,EAAc,OAAO,EAEzB,IAAM,EAAY,EAAe,EAAE,IAAI,OAAO,WAAW,CAAC,EAC1D,GAAI,EAAW,OAAO,EAEtB,QAAW,KAAQ,EAAkB,CACnC,IAAM,EAAK,EAAS,EAAG,CAAI,EAC3B,GAAI,EAAI,OAAO,EAGjB,GAAI,GAAM,YACR,GAAI,CAEF,IAAM,EADO,EAAK,YAAY,CAAC,GACZ,QAAQ,QAC3B,GAAI,GAAQ,EAAK,CAAI,EAAG,OAAO,EAC/B,KAAM,EAIV,OADA,QAAQ,IAAI,aAAa,EAClB,KAGF,SAAS,CAAY,CAAC,EAA4B,CAAC,EAAG,CAC3D,IAAM,EAAO,EAAK,eAAiB,KAEnC,OAAO,EAAiB,MAAO,EAAG,IAAS,CACzC,EAAE,IAAI,EAAK,EAAY,EAAG,CAAI,CAAC,EAC/B,MAAM,EAAK,EACZ",
8
+ "debugId": "5CD5D6EBA955279864756E2164756E21",
9
+ "names": []
10
+ }
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "hono-ip",
3
+ "version": "1.0.0",
4
+ "description": "A tiny, fast middleware for Hono that figures out your client's real IP address - even when your app sits behind proxies, load balancers, or CDNs.",
5
+ "keywords": [
6
+ "hono",
7
+ "ip",
8
+ "middleware"
9
+ ],
10
+ "module": "dist/index.js",
11
+ "exports": {
12
+ ".": {
13
+ "import": "./dist/index.js"
14
+ }
15
+ },
16
+ "type": "module",
17
+ "sideEffects": false,
18
+ "files": [
19
+ "dist"
20
+ ],
21
+ "scripts": {
22
+ "build": "bun run build.ts"
23
+ },
24
+ "license": "Apache-2.0",
25
+ "author": "orielhaim",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "https://github.com/orielhaim/hono-ip"
29
+ },
30
+ "devDependencies": {
31
+ "@types/bun": "^1.3.9"
32
+ },
33
+ "dependencies": {
34
+ "hono": "^4.12.3"
35
+ }
36
+ }