proteum 2.2.2 → 2.2.6
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/AGENTS.md +5 -5
- package/README.md +4 -1
- package/agents/project/AGENTS.md +9 -8
- package/agents/project/diagnostics.md +10 -8
- package/agents/project/optimizations.md +2 -2
- package/agents/project/root/AGENTS.md +8 -7
- package/agents/project/tests/AGENTS.md +3 -2
- package/cli/app/index.ts +19 -9
- package/cli/commands/check.ts +7 -3
- package/cli/commands/configure.ts +14 -9
- package/cli/commands/e2e.ts +204 -0
- package/cli/commands/typecheck.ts +7 -3
- package/cli/presentation/commands.ts +37 -7
- package/cli/runtime/command.ts +2 -2
- package/cli/runtime/commands.ts +59 -0
- package/cli/scaffold/index.ts +1 -1
- package/cli/utils/agents.ts +175 -80
- package/cli/utils/check.ts +32 -4
- package/docs/dev-sessions.md +11 -2
- package/docs/diagnostics.md +2 -1
- package/package.json +1 -1
- package/scripts/update-codex-agents.ts +2 -2
- package/server/services/router/request/index.ts +2 -1
- package/server/services/router/request/ip.test.cjs +60 -0
- package/server/services/router/request/ip.ts +71 -0
|
@@ -17,6 +17,7 @@ import type { HttpMethod, HttpHeaders } from '..';
|
|
|
17
17
|
import ApiClient from './api';
|
|
18
18
|
import ServerResponse from '../response';
|
|
19
19
|
import type { TAnyRouter } from '..';
|
|
20
|
+
import { resolveRequestIp } from './ip';
|
|
20
21
|
|
|
21
22
|
/*----------------------------------
|
|
22
23
|
- TYPES
|
|
@@ -108,7 +109,7 @@ export default class ServerRequest<TRouter extends TAnyRouter = TAnyRouter> exte
|
|
|
108
109
|
this.domain = res.req.hostname;
|
|
109
110
|
this.cookies = res.req.cookies;
|
|
110
111
|
|
|
111
|
-
this.ip = res.req
|
|
112
|
+
this.ip = resolveRequestIp(res.req);
|
|
112
113
|
|
|
113
114
|
this.data = data || {};
|
|
114
115
|
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const assert = require('node:assert/strict');
|
|
2
|
+
const test = require('node:test');
|
|
3
|
+
|
|
4
|
+
const { resolveRequestIp } = require('./ip.ts');
|
|
5
|
+
|
|
6
|
+
const request = (headers, ip = '192.0.2.10') => ({
|
|
7
|
+
headers,
|
|
8
|
+
ip,
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test('Cloudflare client IP wins over Express fallback IP', () => {
|
|
12
|
+
assert.equal(
|
|
13
|
+
resolveRequestIp(request({ 'cf-connecting-ip': '203.0.113.10' }, '192.0.2.10')),
|
|
14
|
+
'203.0.113.10',
|
|
15
|
+
);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('True-Client-IP wins when Cloudflare IP is absent', () => {
|
|
19
|
+
assert.equal(
|
|
20
|
+
resolveRequestIp(request({ 'true-client-ip': '198.51.100.24' }, '192.0.2.10')),
|
|
21
|
+
'198.51.100.24',
|
|
22
|
+
);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('Fastly-Client-IP wins when Cloudflare and True-Client-IP are absent', () => {
|
|
26
|
+
assert.equal(
|
|
27
|
+
resolveRequestIp(request({ 'fastly-client-ip': '51.182.144.154' }, '192.0.2.10')),
|
|
28
|
+
'51.182.144.154',
|
|
29
|
+
);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('invalid and spoof-looking values are ignored before falling back to Express IP', () => {
|
|
33
|
+
assert.equal(
|
|
34
|
+
resolveRequestIp(
|
|
35
|
+
request(
|
|
36
|
+
{
|
|
37
|
+
'cf-connecting-ip': 'not-an-ip',
|
|
38
|
+
'true-client-ip': '203.0.113.10, 198.51.100.24',
|
|
39
|
+
'fastly-client-ip': 'unknown',
|
|
40
|
+
'x-forwarded-for': '203.0.113.200',
|
|
41
|
+
forwarded: 'for=203.0.113.201',
|
|
42
|
+
},
|
|
43
|
+
'192.0.2.10',
|
|
44
|
+
),
|
|
45
|
+
),
|
|
46
|
+
'192.0.2.10',
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('IPv4-mapped IPv6 and bracketed IPv6 candidates normalize correctly', () => {
|
|
51
|
+
assert.equal(
|
|
52
|
+
resolveRequestIp(request({ 'fastly-client-ip': ' ::ffff:51.182.144.154 ' }, '192.0.2.10')),
|
|
53
|
+
'51.182.144.154',
|
|
54
|
+
);
|
|
55
|
+
assert.equal(resolveRequestIp(request({ 'true-client-ip': '[2001:db8::12]' }, '192.0.2.10')), '2001:db8::12');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('IPv4 port suffix candidates normalize correctly', () => {
|
|
59
|
+
assert.equal(resolveRequestIp(request({ 'fastly-client-ip': '51.182.144.154:443' }, '192.0.2.10')), '51.182.144.154');
|
|
60
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/*----------------------------------
|
|
2
|
+
- DEPENDANCES
|
|
3
|
+
----------------------------------*/
|
|
4
|
+
|
|
5
|
+
// Npm
|
|
6
|
+
import type express from 'express';
|
|
7
|
+
import { isIP } from 'net';
|
|
8
|
+
|
|
9
|
+
/*----------------------------------
|
|
10
|
+
- TYPES
|
|
11
|
+
----------------------------------*/
|
|
12
|
+
|
|
13
|
+
type THeaderValue = string | string[] | undefined;
|
|
14
|
+
|
|
15
|
+
/*----------------------------------
|
|
16
|
+
- CONSTANTS
|
|
17
|
+
----------------------------------*/
|
|
18
|
+
|
|
19
|
+
const trustedClientIpHeaders = ['cf-connecting-ip', 'true-client-ip', 'fastly-client-ip'] as const;
|
|
20
|
+
const bracketedIpPattern = /^\[([^\]]+)\](?::\d+)?$/;
|
|
21
|
+
const ipv4WithPortPattern = /^(\d{1,3}(?:\.\d{1,3}){3}):\d+$/;
|
|
22
|
+
const ipv4MappedIpv6Pattern = /^::ffff:(\d{1,3}(?:\.\d{1,3}){3})$/i;
|
|
23
|
+
|
|
24
|
+
/*----------------------------------
|
|
25
|
+
- HELPERS
|
|
26
|
+
----------------------------------*/
|
|
27
|
+
|
|
28
|
+
const readHeader = (headers: Record<string, THeaderValue>, name: string): string | null => {
|
|
29
|
+
const normalizedName = name.toLowerCase();
|
|
30
|
+
const directValue = headers[name] ?? headers[normalizedName] ?? headers[name.toUpperCase()];
|
|
31
|
+
const value =
|
|
32
|
+
directValue !== undefined
|
|
33
|
+
? directValue
|
|
34
|
+
: Object.entries(headers).find(([key]) => key.toLowerCase() === normalizedName)?.[1];
|
|
35
|
+
|
|
36
|
+
if (Array.isArray(value)) {
|
|
37
|
+
const firstValue = value.find((entry) => typeof entry === 'string' && entry.trim());
|
|
38
|
+
return typeof firstValue === 'string' ? firstValue.trim() : null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const normalizeRequestIpCandidate = (value: string | null | undefined): string | null => {
|
|
45
|
+
let candidate = typeof value === 'string' ? value.trim() : '';
|
|
46
|
+
if (!candidate) return null;
|
|
47
|
+
|
|
48
|
+
const bracketedIp = bracketedIpPattern.exec(candidate);
|
|
49
|
+
if (bracketedIp) candidate = bracketedIp[1].trim();
|
|
50
|
+
|
|
51
|
+
const ipv4MappedIpv6 = ipv4MappedIpv6Pattern.exec(candidate);
|
|
52
|
+
if (ipv4MappedIpv6 && isIP(ipv4MappedIpv6[1]) === 4) {
|
|
53
|
+
candidate = ipv4MappedIpv6[1];
|
|
54
|
+
} else {
|
|
55
|
+
const ipv4WithPort = ipv4WithPortPattern.exec(candidate);
|
|
56
|
+
if (ipv4WithPort && isIP(ipv4WithPort[1]) === 4) candidate = ipv4WithPort[1];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return isIP(candidate) ? candidate : null;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const resolveRequestIp = (req: Pick<express.Request, 'headers' | 'ip'>): string | undefined => {
|
|
63
|
+
const headers = req.headers as Record<string, THeaderValue>;
|
|
64
|
+
|
|
65
|
+
for (const headerName of trustedClientIpHeaders) {
|
|
66
|
+
const ip = normalizeRequestIpCandidate(readHeader(headers, headerName));
|
|
67
|
+
if (ip) return ip;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return normalizeRequestIpCandidate(req.ip) || undefined;
|
|
71
|
+
};
|