hono-ip 1.0.1 → 2.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/LICENSE ADDED
@@ -0,0 +1,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright [yyyy] [name of copyright owner]
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
package/README.md CHANGED
@@ -1,119 +1,197 @@
1
1
  # hono-ip
2
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.
3
+ Resolve the real client IP in [Hono](https://hono.dev) without getting spoofed.
4
4
 
5
- Works on **Node.js** and **Bun** out of the box. No regex. No dependencies.
5
+ Works on Node, Bun, Deno, and Cloudflare Workers. Three dependencies, no regex parsing of security-sensitive headers, branded types end-to-end.
6
6
 
7
- ## Why?
7
+ ## Why this exists
8
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.
9
+ Most "get the client IP" libraries try a list of headers in a fixed order and return the first one that looks like an address. That's the bug. `X-Real-IP`, `X-Client-IP`, `True-Client-IP` - any client can send any of those, and most servers will believe them. The result is silent IP spoofing affecting rate limits, audit logs, fraud signals, and geofencing.
10
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.
11
+ `hono-ip` inverts the model. The operator declares the topology - "I'm behind Cloudflare," "I'm behind nginx with one trusted hop," "I'm direct" - and the middleware reads only what's been declared trustworthy. There is no fallback chain that can be tricked. There is no "try every header" mode.
12
12
 
13
- ## Quick start
13
+ ## Install
14
14
 
15
15
  ```bash
16
16
  npm install hono-ip
17
17
  ```
18
18
 
19
+ ## A minimal example
20
+
19
21
  ```ts
20
22
  import { Hono } from "hono";
21
- import { ipMiddleware } from "hono-ip";
23
+ import { ipMiddleware, cloudflare } from "hono-ip";
22
24
 
23
25
  const app = new Hono();
26
+ app.use(ipMiddleware({ strategy: cloudflare() }));
24
27
 
25
- app.use(ipMiddleware());
28
+ app.get("/", (c) => c.text(`Hello ${c.var.ip ?? "stranger"}`));
29
+ ```
26
30
 
27
- app.get("/", (c) => {
28
- const ip = c.get("ip"); // string | null
29
- return c.text(`Hello, ${ip ?? "stranger"}`);
30
- });
31
+ `c.var.ip` is typed as `IpAddress | null`, where `IpAddress` is a branded string. You cannot pass a raw, unvalidated string where an `IpAddress` is expected - the validator is the only constructor.
32
+
33
+ ## Strategies
34
+
35
+ A strategy tells the middleware exactly where to look and how to interpret what it finds. Pick one that matches your deployment.
36
+
37
+ ### Behind a known CDN
38
+
39
+ Presets exist for the common ones. Each reads a single platform-specific header and trusts it directly, because the platform strips client-supplied copies before your code runs.
40
+
41
+ ```ts
42
+ import { ipMiddleware, cloudflare, fly, vercel } from "hono-ip";
43
+
44
+ app.use(ipMiddleware({ strategy: cloudflare() })); // CF-Connecting-IP
45
+ app.use(ipMiddleware({ strategy: fly() })); // Fly-Client-IP
46
+ app.use(ipMiddleware({ strategy: vercel() })); // X-Real-IP behind Vercel
31
47
  ```
32
48
 
33
- That's it. Every route after the middleware can read `c.get("ip")` or `c.var.ip`.
49
+ For any other platform that sets a single trusted header, use `single-header` directly:
50
+
51
+ ```ts
52
+ app.use(ipMiddleware({
53
+ strategy: { kind: "single-header", header: "x-azure-clientip" },
54
+ }));
55
+ ```
34
56
 
35
- ## Going deeper
57
+ ### Behind a reverse proxy you control
36
58
 
37
- ### Trusted proxies
59
+ Walks `X-Forwarded-For` right-to-left, skipping any address that matches your trusted CIDR ranges, and returns the first untrusted hop. That hop is, by definition, the closest address your proxy chain didn't add.
38
60
 
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:
61
+ ```ts
62
+ import { ipMiddleware, behindReverseProxy } from "hono-ip";
40
63
 
64
+ app.use(ipMiddleware({
65
+ strategy: behindReverseProxy({
66
+ trustedProxies: ["loopback", "uniquelocal", "10.42.0.0/16"],
67
+ }),
68
+ }));
41
69
  ```
42
- X-Forwarded-For: 6.6.6.6, <actual client>, <your proxy>
43
- attacker injected this
70
+
71
+ `trustedProxies` accepts CIDR strings, named presets (`loopback`, `linklocal`, `uniquelocal`, `private`, `cloudflare`), or a custom predicate `(ip) => boolean` for fully dynamic trust (e.g. Kubernetes pod CIDRs resolved at startup). Validation happens at compile time - invalid CIDRs throw before the middleware ever runs.
72
+
73
+ ### RFC 7239 Forwarded header
74
+
75
+ If your infrastructure emits the standardized `Forwarded` header instead of (or alongside) XFF:
76
+
77
+ ```ts
78
+ app.use(ipMiddleware({
79
+ strategy: {
80
+ kind: "forwarded-rightmost-untrusted",
81
+ trustedProxies: ["loopback", "uniquelocal"],
82
+ },
83
+ }));
44
84
  ```
45
85
 
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:
86
+ Parsing uses `forwarded-parse`, the only widely-vetted spec-compliant parser. Malformed headers - including [sabotage attempts](https://adam-p.ca/blog/2022/03/forwarded-header-sabotage/) using unclosed quotes - cause the entire header to be discarded rather than partially interpreted.
87
+
88
+ ### Direct connections
89
+
90
+ When there's no proxy, fall back to the runtime's connection info. You import the helper for your runtime to keep the package free of cross-runtime imports.
47
91
 
48
92
  ```ts
49
- app.use(
50
- ipMiddleware({
51
- trustedProxies: new Set(["10.0.0.1", "10.0.0.2"]),
52
- })
53
- );
93
+ import { ipMiddleware, direct } from "hono-ip";
94
+ import { getConnInfo } from "@hono/node-server/conninfo"; // or hono/bun, hono/deno
95
+
96
+ app.use(ipMiddleware({ strategy: direct(getConnInfo) }));
54
97
  ```
55
98
 
56
- ### Runtime-level connection info
99
+ ### Hybrid deployments
57
100
 
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:
101
+ Compose strategies with `first-of`. Each child carries its own trust configuration, so the composition stays safe.
59
102
 
60
103
  ```ts
61
- // Bun
104
+ import { ipMiddleware } from "hono-ip";
62
105
  import { getConnInfo } from "hono/bun";
63
106
 
64
- // Node.js
65
- // import { getConnInfo } from "@hono/node-server/conninfo";
107
+ app.use(ipMiddleware({
108
+ strategy: {
109
+ kind: "first-of",
110
+ strategies: [
111
+ { kind: "single-header", header: "cf-connecting-ip" },
112
+ { kind: "conn-info", getConnInfo },
113
+ ],
114
+ },
115
+ }));
116
+ ```
117
+
118
+ ## Audit and observability
119
+
120
+ Every resolution is recorded as a tagged outcome. The middleware writes both the IP and the full outcome to the context, so you can log exactly which strategy succeeded, which hop index was chosen, or which failure mode occurred.
66
121
 
67
- app.use(ipMiddleware({ getConnInfo }));
122
+ ```ts
123
+ app.use(ipMiddleware({
124
+ strategy: behindReverseProxy({ trustedProxies: ["loopback"] }),
125
+ onFailure: (outcome) => {
126
+ // outcome.reason: "no-header" | "header-empty" | "all-hops-trusted"
127
+ // | "header-too-large" | "too-many-entries" | ...
128
+ logger.warn({ outcome }, "ip resolution failed");
129
+ },
130
+ }));
131
+
132
+ app.get("/debug", (c) => {
133
+ const outcome = c.var.ipOutcome;
134
+ if (outcome.ok) {
135
+ return c.json({ ip: outcome.ip, source: outcome.source, hop: outcome.hopIndex });
136
+ }
137
+ return c.json({ failed: outcome.reason }, 400);
138
+ });
68
139
  ```
69
140
 
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.
141
+ For endpoints that genuinely cannot serve a request without a client IP - rate limiters, fraud scoring, geofencing - set `required: true` to short-circuit with `400` when resolution fails.
71
142
 
72
- ### Using `getClientIp` directly
143
+ ```ts
144
+ app.use("/api/*", ipMiddleware({
145
+ strategy: cloudflare(),
146
+ required: true,
147
+ }));
148
+ ```
149
+
150
+ ## Custom variable names
73
151
 
74
- You don't have to use the middleware. The core function is exported on its own:
152
+ The variable name flows through the type system. Whatever name you pass becomes a typed property on `c.var`.
75
153
 
76
154
  ```ts
77
- import { getClientIp } from "hono-ip";
155
+ app.use(ipMiddleware({ strategy: cloudflare(), variable: "clientIp" }));
78
156
 
79
- app.get("/ip", (c) => {
80
- const ip = getClientIp(c, {
81
- trustedProxies: new Set(["10.0.0.1"]),
82
- });
83
- return c.json({ ip });
157
+ app.get("/", (c) => {
158
+ c.var.clientIp; // IpAddress | null, fully typed
84
159
  });
85
160
  ```
86
161
 
87
- ### Custom context variable name
162
+ ## Direct API
88
163
 
89
- If `"ip"` collides with something in your app:
164
+ The middleware is a thin wrapper around composable primitives. Use them directly when you need finer control:
90
165
 
91
166
  ```ts
92
- app.use(ipMiddleware({ attributeName: "clientIp" }));
93
-
94
- // later
95
- c.get("clientIp");
167
+ import {
168
+ parseIp, // (raw: string) => IpAddress | null
169
+ extractIp, // unwraps "[::1]:443", "1.2.3.4:80", brackets, zone IDs
170
+ isIpV4, isIpV6, // type predicates narrowing IpAddress -> IpV4 / IpV6
171
+ parseXForwardedFor,
172
+ parseForwarded, // RFC 7239, returns left-to-right chain
173
+ compileTrust, // build a reusable CIDR matcher from preset names + CIDRs
174
+ compileStrategy, // pre-build a resolver, run it against any Hono context
175
+ } from "hono-ip";
96
176
  ```
97
177
 
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 |
178
+ All parsers return discriminated unions. All validators return branded types or `null`. There are no thrown exceptions on the request path.
179
+
180
+ ## What's enforced for you
181
+
182
+ IPv6 addresses are normalized; zone identifiers (`fe80::1%eth0`) are stripped before validation. IPv4-mapped IPv6 (`::ffff:1.2.3.4`) is collapsed to the v4 form so rate-limit keys are consistent. Only four-part-decimal IPv4 is accepted - odd forms like `0xc0.168.1.1` are rejected to prevent parser-mismatch vulnerabilities. Headers larger than 8 KiB or containing more than 50 entries are refused outright, neutralizing header-flood attacks. Trusted-proxy CIDR ranges are compiled once at middleware construction; the per-request hot path is a header read, a bounded split, and a linear scan with bitmask comparisons.
183
+
184
+ ## Strategy reference
185
+
186
+ | Strategy | When to use | Reads | Trust model |
187
+ |---|---|---|---|
188
+ | `single-header` | Behind a CDN that sets and strips its own header | One specific header | Implicit - the platform is the perimeter |
189
+ | `xff-rightmost-untrusted` | Behind your own reverse proxy chain | `X-Forwarded-For` | CIDR ranges you declare |
190
+ | `forwarded-rightmost-untrusted` | Modern proxy emitting RFC 7239 | `Forwarded` | CIDR ranges you declare |
191
+ | `xff-leftmost-insecure` | Migration only - flagged in audit logs | `X-Forwarded-For` | None (spoofable) |
192
+ | `conn-info` | Direct connections, no proxy | TCP socket | Inherent (cannot be spoofed) |
193
+ | `first-of` | Hybrid environments | Children in order | Each child carries its own |
117
194
 
118
195
  ## License
119
- Apache-2.0
196
+
197
+ [Apache-2.0](LICENSE)
@@ -0,0 +1,8 @@
1
+ export type { IpAddress, IpV4, IpV6, IpKind, Cidr, ResolutionOutcome, ResolutionSource, ResolutionFailure, } from "./types.js";
2
+ export { parseIp, extractIp, isIpV4, isIpV6, ipKind, isCidr, } from "./validate.js";
3
+ export { compileTrust, PRESETS, type TrustedProxiesInput, type PresetName, } from "./trust.js";
4
+ export { parseXForwardedFor, parseForwarded } from "./parsers.js";
5
+ export type { Strategy } from "./strategies.js";
6
+ export { cloudflare, fly, vercel, behindReverseProxy, direct, } from "./presets.js";
7
+ export { ipMiddleware, type IpMiddlewareOptions, type IpVariables, } from "./middleware.js";
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,SAAS,EACT,IAAI,EACJ,IAAI,EACJ,MAAM,EACN,IAAI,EACJ,iBAAiB,EACjB,gBAAgB,EAChB,iBAAiB,GAClB,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,OAAO,EACP,SAAS,EACT,MAAM,EACN,MAAM,EACN,MAAM,EACN,MAAM,GACP,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,YAAY,EACZ,OAAO,EACP,KAAK,mBAAmB,EACxB,KAAK,UAAU,GAChB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAClE,YAAY,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EACL,UAAU,EACV,GAAG,EACH,MAAM,EACN,kBAAkB,EAClB,MAAM,GACP,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,YAAY,EACZ,KAAK,mBAAmB,EACxB,KAAK,WAAW,GACjB,MAAM,iBAAiB,CAAC"}
package/dist/index.js CHANGED
@@ -1,4 +1,20 @@
1
- import{createMiddleware as c}from"hono/factory";import{isIP as g}from"node:net";function f(n){if(!n||typeof n!=="string")return!1;return g(n)!==0}function b(n){return g(n)===4}function R(n){return g(n)===6}function W(n){if(n.startsWith("[")){let t=n.indexOf("]");if(t===-1)return n;return n.slice(1,t)}return n}function V(n){let t=n.lastIndexOf(":");if(t===-1)return n;let r=n.slice(0,t);if(g(r)===4)return r;return n}function P(n){if(!n||typeof n!=="string")return null;let t=n.trim();if(!t)return null;if(t.startsWith("["))t=W(t);else t=V(t);return f(t)?t:null}function x(n,t){if(!n||typeof n!=="string")return null;let r=n.split(","),i=[];for(let u=0;u<r.length;u++){let e=P(r[u]);if(e)i.push(e)}if(i.length===0)return null;if(!t?.trustedProxies)return i[0];for(let u=i.length-1;u>=0;u--)if(!t.trustedProxies.has(i[u]))return i[u];return i[0]}var X=/for=(?:"([^"]+)"|([^;\s,]+))/i;function O(n){if(!n||typeof n!=="string")return null;let t=X.exec(n);if(!t)return null;return P(t[1]??t[2])}var h=["x-client-ip","cf-connecting-ip","fastly-client-ip","true-client-ip","x-real-ip","x-cluster-client-ip"],_=["x-appengine-user-ip","cf-pseudo-ipv4"];function o(n,t){let r=n.req.header(t);if(!r)return null;let i=r.trim();return f(i)?i:null}function j(n,t){let r=o(n,"x-client-ip");if(r)return r;let i=x(n.req.header("x-forwarded-for"),t);if(i)return i;for(let l of h){if(l==="x-client-ip")continue;let s=o(n,l);if(s)return s}let u=o(n,"x-forwarded");if(u)return u;let e=o(n,"forwarded-for");if(e)return e;let y=O(n.req.header("forwarded"));if(y)return y;for(let l of _){let s=o(n,l);if(s)return s}if(t?.getConnInfo)try{let s=t.getConnInfo(n)?.remote?.address;if(s&&f(s))return s}catch{}return console.log("No IP found"),null}function D(n={}){let t=n.attributeName??"ip";return c(async(r,i)=>{r.set(t,j(r,n)),await i()})}export{O as parseForwarded,R as isIpV6,b as isIpV4,f as isIp,D as ipMiddleware,x as getFirstFromXForwardedFor,j as getClientIp,P as extractIp};
1
+ export {
2
+ vercel,
3
+ parseXForwardedFor,
4
+ parseIp,
5
+ parseForwarded,
6
+ isIpV6,
7
+ isIpV4,
8
+ isCidr,
9
+ ipMiddleware,
10
+ ipKind,
11
+ fly,
12
+ extractIp,
13
+ direct,
14
+ compileTrust,
15
+ cloudflare,
16
+ behindReverseProxy,
17
+ PRESETS
18
+ };
2
19
 
3
- //# debugId=5B4660AD822724D264756E2164756E21
4
- //# sourceMappingURL=index.js.map
20
+ //# debugId=211C0359C15CA00664756E2164756E21
package/dist/index.js.map CHANGED
@@ -1,11 +1,9 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["..\\src\\index.ts", "..\\src\\ip.ts"],
3
+ "sources": [],
4
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
- "import { isIP as _netIsIP } from \"node:net\";\n\nexport function isIp(value: string | null | undefined): value is string {\n if (!value || typeof value !== \"string\") return false;\n return _netIsIP(value) !== 0;\n}\n\nexport function isIpV4(value: string): boolean {\n return _netIsIP(value) === 4;\n}\n\nexport function isIpV6(value: string): boolean {\n return _netIsIP(value) === 6;\n}\n\nfunction unwrapIPv6(raw: string): string {\n if (raw.startsWith(\"[\")) {\n const closeBracket = raw.indexOf(\"]\");\n if (closeBracket === -1) return raw;\n return raw.slice(1, closeBracket);\n }\n return raw;\n}\n\nfunction stripIPv4Port(raw: string): string {\n const colon = raw.lastIndexOf(\":\");\n if (colon === -1) return raw;\n const maybeIp = raw.slice(0, colon);\n if (_netIsIP(maybeIp) === 4) return maybeIp;\n return raw;\n}\n\nexport function extractIp(raw: string | null | undefined): string | null {\n if (!raw || typeof raw !== \"string\") return null;\n let ip = raw.trim();\n if (!ip) return null;\n\n if (ip.startsWith(\"[\")) {\n ip = unwrapIPv6(ip);\n } else {\n ip = stripIPv4Port(ip);\n }\n\n return isIp(ip) ? ip : null;\n}\n\nexport interface XffOptions {\n trustedProxies?: ReadonlySet<string>;\n}\n\nexport function getFirstFromXForwardedFor(\n value: string | null | undefined,\n opts?: XffOptions,\n): string | null {\n if (!value || typeof value !== \"string\") return null;\n\n const parts = value.split(\",\");\n const cleaned: string[] = [];\n for (let i = 0; i < parts.length; i++) {\n const ip = extractIp(parts[i]);\n if (ip) cleaned.push(ip);\n }\n\n if (cleaned.length === 0) return null;\n\n if (!opts?.trustedProxies) return cleaned[0]!;\n\n for (let i = cleaned.length - 1; i >= 0; i--) {\n if (!opts.trustedProxies.has(cleaned[i]!)) return cleaned[i]!;\n }\n\n return cleaned[0]!;\n}\n\nconst FORWARDED_FOR_RE = /for=(?:\"([^\"]+)\"|([^;\\s,]+))/i;\n\nexport function parseForwarded(value: string | null | undefined): string | null {\n if (!value || typeof value !== \"string\") return null;\n const m = FORWARDED_FOR_RE.exec(value);\n if (!m) return null;\n return extractIp(m[1] ?? m[2]);\n}"
7
5
  ],
8
- "mappings": "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",
9
- "debugId": "5B4660AD822724D264756E2164756E21",
6
+ "mappings": "",
7
+ "debugId": "211C0359C15CA00664756E2164756E21",
10
8
  "names": []
11
9
  }
@@ -0,0 +1,20 @@
1
+ import type { Env, MiddlewareHandler } from "hono";
2
+ import { type Strategy } from "./strategies.js";
3
+ import type { IpAddress, ResolutionOutcome } from "./types.js";
4
+ export interface IpMiddlewareOptions<K extends string = "ip"> {
5
+ readonly strategy: Strategy;
6
+ readonly variable?: K;
7
+ readonly onFailure?: (outcome: Extract<ResolutionOutcome, {
8
+ ok: false;
9
+ }>) => void;
10
+ readonly required?: boolean;
11
+ }
12
+ export type IpVariables<K extends string = "ip"> = {
13
+ readonly [P in K]: IpAddress | null;
14
+ } & {
15
+ readonly [P in `${K}Outcome`]: ResolutionOutcome;
16
+ };
17
+ export declare function ipMiddleware<K extends string = "ip">(opts: IpMiddlewareOptions<K>): MiddlewareHandler<{
18
+ Variables: IpVariables<K>;
19
+ } & Env>;
20
+ //# sourceMappingURL=middleware.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../src/middleware.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,GAAG,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AACnD,OAAO,EAAmB,KAAK,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AACjE,OAAO,KAAK,EAAE,SAAS,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAE/D,MAAM,WAAW,mBAAmB,CAAC,CAAC,SAAS,MAAM,GAAG,IAAI;IAC1D,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;IAC5B,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;IACtB,QAAQ,CAAC,SAAS,CAAC,EAAE,CACnB,OAAO,EAAE,OAAO,CAAC,iBAAiB,EAAE;QAAE,EAAE,EAAE,KAAK,CAAA;KAAE,CAAC,KAC/C,IAAI,CAAC;IACV,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED,MAAM,MAAM,WAAW,CAAC,CAAC,SAAS,MAAM,GAAG,IAAI,IAAI;IACjD,QAAQ,EAAE,CAAC,IAAI,CAAC,GAAG,SAAS,GAAG,IAAI;CACpC,GAAG;IACF,QAAQ,EAAE,CAAC,IAAI,GAAG,CAAC,SAAS,GAAG,iBAAiB;CACjD,CAAC;AAEF,wBAAgB,YAAY,CAAC,CAAC,SAAS,MAAM,GAAG,IAAI,EAClD,IAAI,EAAE,mBAAmB,CAAC,CAAC,CAAC,GAC3B,iBAAiB,CAAC;IAAE,SAAS,EAAE,WAAW,CAAC,CAAC,CAAC,CAAA;CAAE,GAAG,GAAG,CAAC,CAoBxD"}
@@ -0,0 +1,11 @@
1
+ import { type IpAddress, type NonEmptyReadonlyArray } from "./types.js";
2
+ export type ChainParseResult = {
3
+ readonly ok: true;
4
+ readonly chain: NonEmptyReadonlyArray<IpAddress>;
5
+ } | {
6
+ readonly ok: false;
7
+ readonly reason: "too-large" | "too-many" | "empty";
8
+ };
9
+ export declare function parseXForwardedFor(raw: string | undefined): ChainParseResult;
10
+ export declare function parseForwarded(raw: string | undefined): ChainParseResult;
11
+ //# sourceMappingURL=parsers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parsers.d.ts","sourceRoot":"","sources":["../src/parsers.ts"],"names":[],"mappings":"AAEA,OAAO,EAGL,KAAK,SAAS,EACd,KAAK,qBAAqB,EAC3B,MAAM,YAAY,CAAC;AAEpB,MAAM,MAAM,gBAAgB,GACxB;IACE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAClB,QAAQ,CAAC,KAAK,EAAE,qBAAqB,CAAC,SAAS,CAAC,CAAC;CAClD,GACD;IACE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IACnB,QAAQ,CAAC,MAAM,EAAE,WAAW,GAAG,UAAU,GAAG,OAAO,CAAC;CACrD,CAAC;AAON,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,gBAAgB,CAiB5E;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,gBAAgB,CAyBxE"}
@@ -0,0 +1,18 @@
1
+ import type { Context } from "hono";
2
+ import type { Strategy } from "./strategies.js";
3
+ export declare const cloudflare: () => Strategy;
4
+ export declare const fly: () => Strategy;
5
+ export declare const vercel: () => Strategy;
6
+ export declare const behindReverseProxy: (opts?: {
7
+ trustedProxies?: Strategy & {
8
+ kind: "xff-rightmost-untrusted";
9
+ } extends {
10
+ trustedProxies: infer T;
11
+ } ? T : never;
12
+ }) => Strategy;
13
+ export declare const direct: (getConnInfo: (c: Context) => {
14
+ remote?: {
15
+ address?: string;
16
+ };
17
+ }) => Strategy;
18
+ //# sourceMappingURL=presets.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"presets.d.ts","sourceRoot":"","sources":["../src/presets.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AACpC,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAEhD,eAAO,MAAM,UAAU,QAAO,QAG5B,CAAC;AAEH,eAAO,MAAM,GAAG,QAAO,QAGrB,CAAC;AAEH,eAAO,MAAM,MAAM,QAAO,QAGxB,CAAC;AAEH,eAAO,MAAM,kBAAkB,GAC7B,OAAM;IACJ,cAAc,CAAC,EAAE,QAAQ,GAAG;QAAE,IAAI,EAAE,yBAAyB,CAAA;KAAE,SAAS;QACtE,cAAc,EAAE,MAAM,CAAC,CAAC;KACzB,GACG,CAAC,GACD,KAAK,CAAC;CACN,KACL,QAGD,CAAC;AAEH,eAAO,MAAM,MAAM,GACjB,aAAa,CAAC,CAAC,EAAE,OAAO,KAAK;IAAE,MAAM,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,KAC7D,QAGD,CAAC"}
@@ -0,0 +1,29 @@
1
+ import type { Context } from "hono";
2
+ import { type TrustedProxiesInput } from "./trust.js";
3
+ import type { ResolutionOutcome } from "./types.js";
4
+ export type Strategy = {
5
+ readonly kind: "single-header";
6
+ readonly header: string;
7
+ } | {
8
+ readonly kind: "xff-rightmost-untrusted";
9
+ readonly trustedProxies: TrustedProxiesInput;
10
+ } | {
11
+ readonly kind: "xff-leftmost-insecure";
12
+ } | {
13
+ readonly kind: "forwarded-rightmost-untrusted";
14
+ readonly trustedProxies: TrustedProxiesInput;
15
+ } | {
16
+ readonly kind: "conn-info";
17
+ readonly getConnInfo: (c: Context) => {
18
+ remote?: {
19
+ address?: string;
20
+ };
21
+ };
22
+ } | {
23
+ readonly kind: "first-of";
24
+ readonly strategies: readonly Strategy[];
25
+ };
26
+ type Resolver = (c: Context) => ResolutionOutcome;
27
+ export declare function compileStrategy(strategy: Strategy): Resolver;
28
+ export {};
29
+ //# sourceMappingURL=strategies.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"strategies.d.ts","sourceRoot":"","sources":["../src/strategies.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAGpC,OAAO,EAEL,KAAK,mBAAmB,EAEzB,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EAAa,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAE/D,MAAM,MAAM,QAAQ,GAChB;IAAE,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAC3D;IACE,QAAQ,CAAC,IAAI,EAAE,yBAAyB,CAAC;IACzC,QAAQ,CAAC,cAAc,EAAE,mBAAmB,CAAC;CAC9C,GACD;IAAE,QAAQ,CAAC,IAAI,EAAE,uBAAuB,CAAA;CAAE,GAC1C;IACE,QAAQ,CAAC,IAAI,EAAE,+BAA+B,CAAC;IAC/C,QAAQ,CAAC,cAAc,EAAE,mBAAmB,CAAC;CAC9C,GACD;IACE,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAC3B,QAAQ,CAAC,WAAW,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK;QAAE,MAAM,CAAC,EAAE;YAAE,OAAO,CAAC,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,CAAC;CACzE,GACD;IAAE,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAAC,QAAQ,CAAC,UAAU,EAAE,SAAS,QAAQ,EAAE,CAAA;CAAE,CAAC;AAE5E,KAAK,QAAQ,GAAG,CAAC,CAAC,EAAE,OAAO,KAAK,iBAAiB,CAAC;AAElD,wBAAgB,eAAe,CAAC,QAAQ,EAAE,QAAQ,GAAG,QAAQ,CA6B5D"}
@@ -0,0 +1,13 @@
1
+ import type { IpAddress } from "./types.js";
2
+ export declare const PRESETS: {
3
+ readonly loopback: readonly ["127.0.0.0/8", "::1/128"];
4
+ readonly linklocal: readonly ["169.254.0.0/16", "fe80::/10"];
5
+ readonly uniquelocal: readonly ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "fc00::/7"];
6
+ readonly private: readonly ["127.0.0.0/8", "::1/128", "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "fc00::/7", "fe80::/10"];
7
+ readonly cloudflare: readonly ["173.245.48.0/20", "103.21.244.0/22", "103.22.200.0/22", "103.31.4.0/22", "141.101.64.0/18", "108.162.192.0/18", "190.93.240.0/20", "188.114.96.0/20", "197.234.240.0/22", "198.41.128.0/17", "162.158.0.0/15", "104.16.0.0/13", "104.24.0.0/14", "172.64.0.0/13", "131.0.72.0/22", "2400:cb00::/32", "2606:4700::/32", "2803:f800::/32", "2405:b500::/32", "2405:8100::/32", "2a06:98c0::/29", "2c0f:f248::/32"];
8
+ };
9
+ export type PresetName = keyof typeof PRESETS;
10
+ export type TrustedProxiesInput = ReadonlyArray<string | PresetName> | ((ip: IpAddress) => boolean) | "all" | "none";
11
+ export type TrustFn = (ip: IpAddress) => boolean;
12
+ export declare function compileTrust(input: TrustedProxiesInput): TrustFn;
13
+ //# sourceMappingURL=trust.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"trust.d.ts","sourceRoot":"","sources":["../src/trust.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAQ,MAAM,YAAY,CAAC;AAGlD,eAAO,MAAM,OAAO;;;;;;CAqCkC,CAAC;AAEvD,MAAM,MAAM,UAAU,GAAG,MAAM,OAAO,OAAO,CAAC;AAE9C,MAAM,MAAM,mBAAmB,GAC3B,aAAa,CAAC,MAAM,GAAG,UAAU,CAAC,GAClC,CAAC,CAAC,EAAE,EAAE,SAAS,KAAK,OAAO,CAAC,GAC5B,KAAK,GACL,MAAM,CAAC;AAIX,MAAM,MAAM,OAAO,GAAG,CAAC,EAAE,EAAE,SAAS,KAAK,OAAO,CAAC;AAEjD,wBAAgB,YAAY,CAAC,KAAK,EAAE,mBAAmB,GAAG,OAAO,CAoChE"}
@@ -0,0 +1,34 @@
1
+ declare const IpAddressBrand: unique symbol;
2
+ declare const IpV4Brand: unique symbol;
3
+ declare const IpV6Brand: unique symbol;
4
+ declare const CidrBrand: unique symbol;
5
+ export type IpAddress = string & {
6
+ readonly [IpAddressBrand]: true;
7
+ };
8
+ export type IpV4 = IpAddress & {
9
+ readonly [IpV4Brand]: true;
10
+ };
11
+ export type IpV6 = IpAddress & {
12
+ readonly [IpV6Brand]: true;
13
+ };
14
+ export type Cidr = string & {
15
+ readonly [CidrBrand]: true;
16
+ };
17
+ export type IpKind = "ipv4" | "ipv6";
18
+ export type NonEmptyReadonlyArray<T> = readonly [T, ...T[]];
19
+ export type ResolutionOutcome = {
20
+ readonly ok: true;
21
+ readonly ip: IpAddress;
22
+ readonly source: ResolutionSource;
23
+ readonly hopIndex?: number;
24
+ } | {
25
+ readonly ok: false;
26
+ readonly reason: ResolutionFailure;
27
+ readonly attempted: ResolutionSource;
28
+ };
29
+ export type ResolutionSource = "single-header" | "xff-rightmost-untrusted" | "xff-leftmost-explicit-insecure" | "forwarded-rightmost-untrusted" | "conn-info" | "none";
30
+ export type ResolutionFailure = "no-header" | "header-empty" | "header-malformed" | "all-hops-trusted" | "no-untrusted-hop" | "conn-info-unavailable" | "header-too-large" | "too-many-entries";
31
+ export declare const MAX_HEADER_BYTES: number;
32
+ export declare const MAX_CHAIN_ENTRIES = 50;
33
+ export {};
34
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,CAAC,MAAM,cAAc,EAAE,OAAO,MAAM,CAAC;AAC5C,OAAO,CAAC,MAAM,SAAS,EAAE,OAAO,MAAM,CAAC;AACvC,OAAO,CAAC,MAAM,SAAS,EAAE,OAAO,MAAM,CAAC;AACvC,OAAO,CAAC,MAAM,SAAS,EAAE,OAAO,MAAM,CAAC;AAEvC,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG;IAAE,QAAQ,CAAC,CAAC,cAAc,CAAC,EAAE,IAAI,CAAA;CAAE,CAAC;AACrE,MAAM,MAAM,IAAI,GAAG,SAAS,GAAG;IAAE,QAAQ,CAAC,CAAC,SAAS,CAAC,EAAE,IAAI,CAAA;CAAE,CAAC;AAC9D,MAAM,MAAM,IAAI,GAAG,SAAS,GAAG;IAAE,QAAQ,CAAC,CAAC,SAAS,CAAC,EAAE,IAAI,CAAA;CAAE,CAAC;AAC9D,MAAM,MAAM,IAAI,GAAG,MAAM,GAAG;IAAE,QAAQ,CAAC,CAAC,SAAS,CAAC,EAAE,IAAI,CAAA;CAAE,CAAC;AAE3D,MAAM,MAAM,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAErC,MAAM,MAAM,qBAAqB,CAAC,CAAC,IAAI,SAAS,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC;AAE5D,MAAM,MAAM,iBAAiB,GACzB;IACE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAClB,QAAQ,CAAC,EAAE,EAAE,SAAS,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAC;IAClC,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC5B,GACD;IACE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IACnB,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAC;IACnC,QAAQ,CAAC,SAAS,EAAE,gBAAgB,CAAC;CACtC,CAAC;AAEN,MAAM,MAAM,gBAAgB,GACxB,eAAe,GACf,yBAAyB,GACzB,gCAAgC,GAChC,+BAA+B,GAC/B,WAAW,GACX,MAAM,CAAC;AAEX,MAAM,MAAM,iBAAiB,GACzB,WAAW,GACX,cAAc,GACd,kBAAkB,GAClB,kBAAkB,GAClB,kBAAkB,GAClB,uBAAuB,GACvB,kBAAkB,GAClB,kBAAkB,CAAC;AAEvB,eAAO,MAAM,gBAAgB,QAAW,CAAC;AACzC,eAAO,MAAM,iBAAiB,KAAK,CAAC"}
@@ -0,0 +1,8 @@
1
+ import type { IpAddress, IpV4, IpV6, IpKind, Cidr } from "./types.js";
2
+ export declare function parseIp(raw: string): IpAddress | null;
3
+ export declare function isIpV4(ip: IpAddress): ip is IpV4;
4
+ export declare function isIpV6(ip: IpAddress): ip is IpV6;
5
+ export declare function ipKind(ip: IpAddress): IpKind;
6
+ export declare function extractIp(raw: string): IpAddress | null;
7
+ export declare function isCidr(value: string): value is Cidr;
8
+ //# sourceMappingURL=validate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../src/validate.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAEtE,wBAAgB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAmBrD;AAED,wBAAgB,MAAM,CAAC,EAAE,EAAE,SAAS,GAAG,EAAE,IAAI,IAAI,CAEhD;AAED,wBAAgB,MAAM,CAAC,EAAE,EAAE,SAAS,GAAG,EAAE,IAAI,IAAI,CAEhD;AAED,wBAAgB,MAAM,CAAC,EAAE,EAAE,SAAS,GAAG,MAAM,CAE5C;AAED,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAqBvD;AAED,wBAAgB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,IAAI,CAEnD"}
package/package.json CHANGED
@@ -1,36 +1,68 @@
1
1
  {
2
2
  "name": "hono-ip",
3
- "version": "1.0.1",
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.",
3
+ "version": "2.0.0",
4
+ "description": "Tiny and fast middleware for Hono that figures out your client's real IP address",
5
5
  "keywords": [
6
6
  "hono",
7
7
  "ip",
8
- "middleware"
8
+ "middleware",
9
+ "client-ip",
10
+ "x-forwarded-for",
11
+ "forwarded",
12
+ "rfc7239",
13
+ "cloudflare",
14
+ "proxy",
15
+ "cidr",
16
+ "trusted-proxies"
9
17
  ],
10
- "module": "dist/index.js",
18
+ "type": "module",
19
+ "main": "./dist/index.js",
20
+ "module": "./dist/index.js",
21
+ "types": "./dist/index.d.ts",
11
22
  "exports": {
12
23
  ".": {
24
+ "types": "./dist/index.d.ts",
13
25
  "import": "./dist/index.js"
14
- }
26
+ },
27
+ "./package.json": "./package.json"
15
28
  },
16
- "type": "module",
17
29
  "sideEffects": false,
18
30
  "files": [
19
- "dist"
31
+ "dist",
32
+ "README.md",
33
+ "LICENSE"
20
34
  ],
35
+ "engines": {
36
+ "node": ">=18"
37
+ },
21
38
  "scripts": {
22
- "build": "bun run build.ts"
39
+ "build": "bun run build.ts",
40
+ "typecheck": "tsc --noEmit",
41
+ "lint": "biome check src",
42
+ "format": "biome format --write src",
43
+ "prepublishOnly": "bun run build"
23
44
  },
24
45
  "license": "Apache-2.0",
25
46
  "author": "orielhaim",
26
47
  "repository": {
27
48
  "type": "git",
28
- "url": "https://github.com/orielhaim/hono-ip"
49
+ "url": "git+https://github.com/orielhaim/hono-ip.git"
29
50
  },
30
- "devDependencies": {
31
- "@types/bun": "^1.3.9"
51
+ "bugs": {
52
+ "url": "https://github.com/orielhaim/hono-ip/issues"
53
+ },
54
+ "homepage": "https://github.com/orielhaim/hono-ip#readme",
55
+ "peerDependencies": {
56
+ "hono": "^4.0.0"
32
57
  },
33
58
  "dependencies": {
34
- "hono": "^4.12.3"
59
+ "forwarded-parse": "^2.1.2",
60
+ "ipaddr.js": "^2.4.0"
61
+ },
62
+ "devDependencies": {
63
+ "@biomejs/biome": "2.4.14",
64
+ "@types/bun": "^1.3.13",
65
+ "hono": "^4.12.16",
66
+ "typescript": "^6.0.3"
35
67
  }
36
- }
68
+ }