Skip to content

Commit 4c9bb12

Browse files
committed
chore: add changeset for IP core upgrade
1 parent dc4ee64 commit 4c9bb12

File tree

8 files changed

+123
-49
lines changed

8 files changed

+123
-49
lines changed

.changeset/silver-views-type.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@ip-kit/express': patch
3+
'@ip-kit/core': minor
4+
---
5+
6+
Improve IP parsing, CIDR matching and trust proxy logic. Introduce structured IP model for security-grade IP resolution.

.github/workflows/release.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
name: Release
22

33
on:
4+
push:
5+
branches:
6+
- main
7+
paths:
8+
- '.changeset/**'
49
push:
510
tags:
6-
- 'v*.*.*'
11+
- 'v*.*.*'
712
workflow_dispatch:
813

914
permissions:

packages/core/src/ip/cidr.test.ts

Lines changed: 46 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,78 @@
11
import { describe, it, expect } from 'vitest'
2-
import { ipInCidr } from './cidr'
2+
import { parseCidr, ipInCidr } from './cidr'
33

4-
describe('ipInCidr', () => {
4+
describe('CIDR matching', () => {
55
describe('IPv4', () => {
66
it('matches IP inside CIDR', () => {
7-
expect(ipInCidr('192.168.1.10', '192.168.1.0/24')).toBe(true)
7+
const cidr = parseCidr('192.168.1.0/24')
8+
expect(cidr).not.toBeNull()
9+
10+
expect(ipInCidr('192.168.1.10', cidr!)).toBe(true)
811
})
912

1013
it('does not match IP outside CIDR', () => {
11-
expect(ipInCidr('192.168.2.10', '192.168.1.0/24')).toBe(false)
14+
const cidr = parseCidr('192.168.1.0/24')
15+
expect(cidr).not.toBeNull()
16+
17+
expect(ipInCidr('192.168.2.10', cidr!)).toBe(false)
1218
})
1319

1420
it('matches /32 correctly', () => {
15-
expect(ipInCidr('8.8.8.8', '8.8.8.8/32')).toBe(true)
16-
expect(ipInCidr('8.8.8.9', '8.8.8.8/32')).toBe(false)
21+
const cidr = parseCidr('8.8.8.8/32')
22+
expect(cidr).not.toBeNull()
23+
24+
expect(ipInCidr('8.8.8.8', cidr!)).toBe(true)
25+
expect(ipInCidr('8.8.8.9', cidr!)).toBe(false)
1726
})
1827
})
1928

2029
describe('IPv6', () => {
2130
it('matches IPv6 inside CIDR', () => {
22-
expect(ipInCidr('2001:db8::1', '2001:db8::/32')).toBe(true)
31+
const cidr = parseCidr('2001:db8::/32')
32+
expect(cidr).not.toBeNull()
33+
34+
expect(ipInCidr('2001:db8::1', cidr!)).toBe(true)
2335
})
2436

2537
it('does not match IPv6 outside CIDR', () => {
26-
expect(ipInCidr('2001:dead::1', '2001:db8::/32')).toBe(false)
38+
const cidr = parseCidr('2001:db8::/32')
39+
expect(cidr).not.toBeNull()
40+
41+
expect(ipInCidr('2001:dead::1', cidr!)).toBe(false)
2742
})
2843

2944
it('matches /128 correctly', () => {
30-
expect(ipInCidr('::1', '::1/128')).toBe(true)
45+
const cidr = parseCidr('::1/128')
46+
expect(cidr).not.toBeNull()
47+
48+
expect(ipInCidr('::1', cidr!)).toBe(true)
49+
expect(ipInCidr('::2', cidr!)).toBe(false)
3150
})
3251
})
3352

3453
describe('invalid input', () => {
35-
it('returns false for invalid IP', () => {
36-
expect(ipInCidr('unknown', '192.168.0.0/16')).toBe(false)
54+
it('returns null for invalid CIDR string', () => {
55+
expect(parseCidr('invalid')).toBeNull()
56+
expect(parseCidr('192.168.0.0')).toBeNull()
57+
expect(parseCidr('192.168.0.0/99')).toBeNull()
3758
})
3859

39-
it('returns false for invalid CIDR', () => {
40-
expect(ipInCidr('192.168.1.1', 'invalid')).toBe(false)
60+
it('returns false for invalid IP', () => {
61+
const cidr = parseCidr('192.168.0.0/16')
62+
expect(cidr).not.toBeNull()
63+
64+
expect(ipInCidr('unknown', cidr!)).toBe(false)
4165
})
4266

43-
it('returns false for invalid prefix', () => {
44-
expect(ipInCidr('192.168.1.1', '192.168.0.0/99')).toBe(false)
67+
it('returns false for mismatched IP version', () => {
68+
const ipv4 = parseCidr('192.168.0.0/16')
69+
const ipv6 = parseCidr('2001:db8::/32')
70+
71+
expect(ipv4).not.toBeNull()
72+
expect(ipv6).not.toBeNull()
73+
74+
expect(ipInCidr('::1', ipv4!)).toBe(false)
75+
expect(ipInCidr('192.168.0.1', ipv6!)).toBe(false)
4576
})
4677
})
4778
})

packages/core/src/ip/is-private.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { ipInCidr } from './cidr'
1+
import { parseCidr, ipInCidr, type ParsedCidr } from './cidr'
22

3-
const PRIVATE_CIDRS = [
3+
const PRIVATE_CIDRS: ParsedCidr[] = [
44
// IPv4
55
'10.0.0.0/8',
66
'172.16.0.0/12',
@@ -12,6 +12,8 @@ const PRIVATE_CIDRS = [
1212
'fc00::/7',
1313
'fe80::/10',
1414
]
15+
.map(parseCidr)
16+
.filter((v): v is ParsedCidr => v !== null)
1517

1618
export function isPrivateIp(ip: string): boolean {
1719
return PRIVATE_CIDRS.some((cidr) => ipInCidr(ip, cidr))

packages/core/src/trust/presets.test.ts

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,30 @@ import { createTrustProxy } from './trust-proxy'
44

55
describe('trustPresets', () => {
66
it('loopback trusts localhost', () => {
7-
const trust = createTrustProxy(trustPresets.loopback)
7+
const proxy = createTrustProxy(trustPresets.loopback)
88

9-
expect(trust('127.0.0.1')).toBe(true)
10-
expect(trust('::1')).toBe(true)
11-
expect(trust('8.8.8.8')).toBe(false)
9+
expect(proxy.fn('127.0.0.1')).toBe(true)
10+
expect(proxy.fn('::1')).toBe(true)
11+
expect(proxy.fn('8.8.8.8')).toBe(false)
12+
13+
expect(proxy.test('127.0.0.1')).toEqual({
14+
trusted: true,
15+
reason: 'LOOPBACK',
16+
})
1217
})
1318

1419
it('private trusts private networks', () => {
15-
const trust = createTrustProxy(trustPresets.private)
20+
const proxy = createTrustProxy(trustPresets.private)
1621

17-
expect(trust('10.0.0.1')).toBe(true)
18-
expect(trust('192.168.1.1')).toBe(true)
19-
expect(trust('8.8.8.8')).toBe(false)
22+
expect(proxy.fn('10.0.0.1')).toBe(true)
23+
expect(proxy.fn('192.168.1.1')).toBe(true)
24+
expect(proxy.fn('8.8.8.8')).toBe(false)
2025
})
2126

22-
it('cloudflare trusts CF IPs', () => {
23-
const trust = createTrustProxy(trustPresets.cloudflare)
27+
it('cloudflare trusts Cloudflare IPs', () => {
28+
const proxy = createTrustProxy(trustPresets.cloudflare)
2429

25-
expect(trust('173.245.48.10')).toBe(true)
26-
expect(trust('8.8.8.8')).toBe(false)
30+
expect(proxy.fn('173.245.48.10')).toBe(true)
31+
expect(proxy.fn('8.8.8.8')).toBe(false)
2732
})
2833
})

packages/core/src/trust/presets.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ export const trustPresets = {
77
name: 'LOOPBACK',
88
fn: (ip) => ip === '127.0.0.1' || ip === '::1',
99
},
10-
1110
private: {
1211
mode: 'fn',
1312
name: 'PRIVATE_NETWORK',
@@ -21,7 +20,6 @@ export const trustPresets = {
2120
'fe80::/10',
2221
]),
2322
},
24-
2523
cloudflare: {
2624
mode: 'fn',
2725
name: 'CLOUDFLARE',

packages/core/src/trust/trust-proxy.test.ts

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,32 +3,59 @@ import { createTrustProxy } from './trust-proxy'
33

44
describe('createTrustProxy', () => {
55
it('mode=all trusts everything', () => {
6-
const trust = createTrustProxy({ mode: 'all' })
6+
const proxy = createTrustProxy({ mode: 'all' })
77

8-
expect(trust('8.8.8.8')).toBe(true)
9-
expect(trust('127.0.0.1')).toBe(true)
8+
expect(proxy.fn('8.8.8.8')).toBe(true)
9+
expect(proxy.fn('127.0.0.1')).toBe(true)
10+
11+
expect(proxy.test('8.8.8.8')).toEqual({
12+
trusted: true,
13+
reason: 'ALL_TRUSTED',
14+
})
1015
})
1116

1217
it('mode=none trusts nothing', () => {
13-
const trust = createTrustProxy({ mode: 'none' })
18+
const proxy = createTrustProxy({ mode: 'none' })
19+
20+
expect(proxy.fn('8.8.8.8')).toBe(false)
21+
expect(proxy.fn('127.0.0.1')).toBe(false)
1422

15-
expect(trust('8.8.8.8')).toBe(false)
16-
expect(trust('127.0.0.1')).toBe(false)
23+
expect(proxy.test('8.8.8.8')).toEqual({
24+
trusted: false,
25+
reason: 'NONE_TRUSTED',
26+
})
1727
})
1828

19-
it('mode=fn delegates to function', () => {
20-
const trust = createTrustProxy({
29+
it('mode=fn delegates to custom function', () => {
30+
const proxy = createTrustProxy({
2131
mode: 'fn',
32+
name: 'LOOPBACK_ONLY',
2233
fn: (ip) => ip === '127.0.0.1',
2334
})
2435

25-
expect(trust('127.0.0.1')).toBe(true)
26-
expect(trust('8.8.8.8')).toBe(false)
36+
expect(proxy.fn('127.0.0.1')).toBe(true)
37+
expect(proxy.fn('8.8.8.8')).toBe(false)
38+
39+
expect(proxy.test('127.0.0.1')).toEqual({
40+
trusted: true,
41+
reason: 'LOOPBACK_ONLY',
42+
})
43+
44+
expect(proxy.test('8.8.8.8')).toEqual({
45+
trusted: false,
46+
reason: 'LOOPBACK_ONLY',
47+
})
2748
})
2849

2950
it('mode=fn without fn returns false', () => {
30-
const trust = createTrustProxy({ mode: 'fn' })
51+
const proxy = createTrustProxy({ mode: 'fn' })
3152

32-
expect(trust('127.0.0.1')).toBe(false)
53+
expect(proxy.fn('127.0.0.1')).toBe(false)
54+
expect(proxy.fn('8.8.8.8')).toBe(false)
55+
56+
expect(proxy.test('127.0.0.1')).toEqual({
57+
trusted: false,
58+
reason: 'CUSTOM_FN',
59+
})
3360
})
3461
})

packages/express/src/middleware.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ export interface IpKitExpressOptions {
1010
trustProxy?: TrustProxyConfig | keyof typeof trustPresets
1111
}
1212

13-
export function ipKitExpress(options: IpKitExpressOptions = {}) {
14-
const trust =
13+
export function ipKit(options: IpKitExpressOptions = {}) {
14+
const proxy =
1515
typeof options.trustProxy === 'string'
1616
? createTrustProxy(trustPresets[options.trustProxy])
1717
: options.trustProxy
@@ -23,9 +23,9 @@ export function ipKitExpress(options: IpKitExpressOptions = {}) {
2323
_res: Response,
2424
next: NextFunction
2525
) {
26-
const result = extractIp(req, { trustProxy: trust })
27-
28-
req.ipKit = result
26+
req.ipKit = extractIp(req, {
27+
trustProxy: proxy.fn,
28+
})
2929

3030
next()
3131
}

0 commit comments

Comments
 (0)