heliumts 0.5.0 → 0.5.2
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/dist/client/rpcClient.d.ts +4 -0
- package/dist/client/rpcClient.d.ts.map +1 -1
- package/dist/client/rpcClient.js +206 -41
- package/dist/client/rpcClient.js.map +1 -1
- package/dist/server/config.d.ts +56 -0
- package/dist/server/config.d.ts.map +1 -1
- package/dist/server/config.js +3 -0
- package/dist/server/config.js.map +1 -1
- package/dist/server/devServer.d.ts.map +1 -1
- package/dist/server/devServer.js +48 -3
- package/dist/server/devServer.js.map +1 -1
- package/dist/server/httpRouter.js +29 -5
- package/dist/server/httpRouter.js.map +1 -1
- package/dist/server/prodServer.d.ts.map +1 -1
- package/dist/server/prodServer.js +116 -7
- package/dist/server/prodServer.js.map +1 -1
- package/dist/server/rpcRegistry.d.ts +2 -0
- package/dist/server/rpcRegistry.d.ts.map +1 -1
- package/dist/server/rpcRegistry.js +44 -3
- package/dist/server/rpcRegistry.js.map +1 -1
- package/dist/server/security.d.ts +5 -0
- package/dist/server/security.d.ts.map +1 -1
- package/dist/server/security.js +25 -14
- package/dist/server/security.js.map +1 -1
- package/dist/server/serializer.d.ts +2 -1
- package/dist/server/serializer.d.ts.map +1 -1
- package/dist/server/serializer.js +20 -4
- package/dist/server/serializer.js.map +1 -1
- package/dist/utils/ipExtractor.d.ts.map +1 -1
- package/dist/utils/ipExtractor.js +20 -16
- package/dist/utils/ipExtractor.js.map +1 -1
- package/package.json +1 -1
package/dist/server/security.js
CHANGED
|
@@ -1,22 +1,25 @@
|
|
|
1
1
|
import crypto from "crypto";
|
|
2
2
|
import { log } from "../utils/logger.js";
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
const GLOBAL_CONFIG_KEY = Symbol.for("helium.server.securityConfig");
|
|
6
|
-
let SERVER_SECRET;
|
|
7
|
-
// SECURITY_CONFIG stores the RPC-specific security settings
|
|
3
|
+
// Module-scoped secrets — not accessible via globalThis enumeration
|
|
4
|
+
let SERVER_SECRET = "";
|
|
8
5
|
let SECURITY_CONFIG;
|
|
6
|
+
let isInitialized = false;
|
|
9
7
|
export function initializeSecurity(config) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
8
|
+
if (!isInitialized) {
|
|
9
|
+
const envSecret = process.env.HELIUM_SECRET;
|
|
10
|
+
if (!envSecret) {
|
|
11
|
+
SERVER_SECRET = crypto.randomBytes(32).toString("hex");
|
|
12
|
+
if (process.env.NODE_ENV === "production") {
|
|
13
|
+
log("warn", "HELIUM_SECRET is not set. A random secret was generated. Tokens will NOT verify across cluster instances or restarts. Set HELIUM_SECRET for production.");
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
SERVER_SECRET = envSecret;
|
|
18
|
+
}
|
|
19
|
+
SECURITY_CONFIG = config;
|
|
20
|
+
isInitialized = true;
|
|
21
|
+
log("info", "Security module initialized");
|
|
17
22
|
}
|
|
18
|
-
SERVER_SECRET = globalThis[GLOBAL_SECRET_KEY];
|
|
19
|
-
SECURITY_CONFIG = globalThis[GLOBAL_CONFIG_KEY];
|
|
20
23
|
}
|
|
21
24
|
export function generateConnectionToken() {
|
|
22
25
|
const timestamp = Date.now().toString();
|
|
@@ -56,4 +59,12 @@ export function verifyConnectionToken(token) {
|
|
|
56
59
|
}
|
|
57
60
|
return isValid;
|
|
58
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* Reset security state. Only for testing.
|
|
64
|
+
* @internal
|
|
65
|
+
*/
|
|
66
|
+
export function resetSecurity() {
|
|
67
|
+
SERVER_SECRET = "";
|
|
68
|
+
isInitialized = false;
|
|
69
|
+
}
|
|
59
70
|
//# sourceMappingURL=security.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"security.js","sourceRoot":"","sources":["../../src/server/security.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,QAAQ,CAAC;AAE5B,OAAO,EAAE,GAAG,EAAE,MAAM,oBAAoB,CAAC;AAGzC,
|
|
1
|
+
{"version":3,"file":"security.js","sourceRoot":"","sources":["../../src/server/security.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,QAAQ,CAAC;AAE5B,OAAO,EAAE,GAAG,EAAE,MAAM,oBAAoB,CAAC;AAGzC,oEAAoE;AACpE,IAAI,aAAa,GAAW,EAAE,CAAC;AAC/B,IAAI,eAAkD,CAAC;AACvD,IAAI,aAAa,GAAG,KAAK,CAAC;AAE1B,MAAM,UAAU,kBAAkB,CAAC,MAAyC;IACxE,IAAI,CAAC,aAAa,EAAE,CAAC;QACjB,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC;QAC5C,IAAI,CAAC,SAAS,EAAE,CAAC;YACb,aAAa,GAAG,MAAM,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;YACvD,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,YAAY,EAAE,CAAC;gBACxC,GAAG,CAAC,MAAM,EAAE,yJAAyJ,CAAC,CAAC;YAC3K,CAAC;QACL,CAAC;aAAM,CAAC;YACJ,aAAa,GAAG,SAAS,CAAC;QAC9B,CAAC;QACD,eAAe,GAAG,MAAM,CAAC;QACzB,aAAa,GAAG,IAAI,CAAC;QACrB,GAAG,CAAC,MAAM,EAAE,6BAA6B,CAAC,CAAC;IAC/C,CAAC;AACL,CAAC;AAED,MAAM,UAAU,uBAAuB;IACnC,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC;IACxC,MAAM,IAAI,GAAG,MAAM,CAAC,UAAU,CAAC,QAAQ,EAAE,aAAa,CAAC,CAAC;IACxD,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;IACvB,MAAM,SAAS,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IACrC,OAAO,GAAG,SAAS,IAAI,SAAS,EAAE,CAAC;AACvC,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,KAAa;IAC/C,IAAI,CAAC,KAAK,EAAE,CAAC;QACT,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;QAC7B,OAAO,KAAK,CAAC;IACjB,CAAC;IAED,MAAM,CAAC,SAAS,EAAE,SAAS,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAChD,IAAI,CAAC,SAAS,IAAI,CAAC,SAAS,EAAE,CAAC;QAC3B,GAAG,CAAC,MAAM,EAAE,sBAAsB,CAAC,CAAC;QACpC,OAAO,KAAK,CAAC;IACjB,CAAC;IAED,4BAA4B;IAC5B,MAAM,EAAE,GAAG,QAAQ,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;IACnC,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACvB,MAAM,UAAU,GAAG,eAAe,EAAE,eAAe,IAAI,KAAK,CAAC;IAC7D,IAAI,KAAK,CAAC,EAAE,CAAC,IAAI,GAAG,GAAG,EAAE,GAAG,UAAU,IAAI,EAAE,GAAG,GAAG,GAAG,IAAI,EAAE,CAAC;QACxD,GAAG,CAAC,MAAM,EAAE,sBAAsB,EAAE,UAAU,GAAG,WAAW,GAAG,GAAG,EAAE,iBAAiB,UAAU,EAAE,CAAC,CAAC;QACnG,OAAO,KAAK,CAAC;IACjB,CAAC;IAED,MAAM,IAAI,GAAG,MAAM,CAAC,UAAU,CAAC,QAAQ,EAAE,aAAa,CAAC,CAAC;IACxD,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;IACvB,MAAM,iBAAiB,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAE7C,IAAI,SAAS,CAAC,MAAM,KAAK,iBAAiB,CAAC,MAAM,EAAE,CAAC;QAChD,GAAG,CAAC,MAAM,EAAE,2BAA2B,CAAC,CAAC;QACzC,OAAO,KAAK,CAAC;IACjB,CAAC;IAED,MAAM,OAAO,GAAG,MAAM,CAAC,eAAe,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC,CAAC;IAC/F,IAAI,CAAC,OAAO,EAAE,CAAC;QACX,GAAG,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC;IACrC,CAAC;IACD,OAAO,OAAO,CAAC;AACnB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,aAAa;IACzB,aAAa,GAAG,EAAE,CAAC;IACnB,aAAa,GAAG,KAAK,CAAC;AAC1B,CAAC","sourcesContent":["import crypto from \"crypto\";\n\nimport { log } from \"../utils/logger.js\";\nimport type { HeliumRpcSecurityConfig } from \"./config.js\";\n\n// Module-scoped secrets — not accessible via globalThis enumeration\nlet SERVER_SECRET: string = \"\";\nlet SECURITY_CONFIG: Required<HeliumRpcSecurityConfig>;\nlet isInitialized = false;\n\nexport function initializeSecurity(config: Required<HeliumRpcSecurityConfig>): void {\n if (!isInitialized) {\n const envSecret = process.env.HELIUM_SECRET;\n if (!envSecret) {\n SERVER_SECRET = crypto.randomBytes(32).toString(\"hex\");\n if (process.env.NODE_ENV === \"production\") {\n log(\"warn\", \"HELIUM_SECRET is not set. A random secret was generated. Tokens will NOT verify across cluster instances or restarts. Set HELIUM_SECRET for production.\");\n }\n } else {\n SERVER_SECRET = envSecret;\n }\n SECURITY_CONFIG = config;\n isInitialized = true;\n log(\"info\", \"Security module initialized\");\n }\n}\n\nexport function generateConnectionToken(): string {\n const timestamp = Date.now().toString();\n const hmac = crypto.createHmac(\"sha256\", SERVER_SECRET);\n hmac.update(timestamp);\n const signature = hmac.digest(\"hex\");\n return `${timestamp}.${signature}`;\n}\n\nexport function verifyConnectionToken(token: string): boolean {\n if (!token) {\n log(\"warn\", \"Token missing\");\n return false;\n }\n\n const [timestamp, signature] = token.split(\".\");\n if (!timestamp || !signature) {\n log(\"warn\", \"Invalid token format\");\n return false;\n }\n\n // Check if token is expired\n const ts = parseInt(timestamp, 10);\n const now = Date.now();\n const validityMs = SECURITY_CONFIG?.tokenValidityMs ?? 10000;\n if (isNaN(ts) || now - ts > validityMs || ts > now + 1000) {\n log(\"warn\", `Token expired. TS: ${ts}, Now: ${now}, Diff: ${now - ts}, ValidityMs: ${validityMs}`);\n return false;\n }\n\n const hmac = crypto.createHmac(\"sha256\", SERVER_SECRET);\n hmac.update(timestamp);\n const expectedSignature = hmac.digest(\"hex\");\n\n if (signature.length !== expectedSignature.length) {\n log(\"warn\", \"Signature length mismatch\");\n return false;\n }\n\n const isValid = crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expectedSignature));\n if (!isValid) {\n log(\"warn\", \"Invalid signature\");\n }\n return isValid;\n}\n\n/**\n * Reset security state. Only for testing.\n * @internal\n */\nexport function resetSecurity(): void {\n SERVER_SECRET = \"\";\n isInitialized = false;\n}"]}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* serialized correctly (hiding private fields, virtuals, etc.) and efficiently.
|
|
5
5
|
*
|
|
6
6
|
* It preserves MessagePack-supported types like Date and Uint8Array (Buffer).
|
|
7
|
+
* Circular references are replaced with null to avoid runtime crashes.
|
|
7
8
|
*/
|
|
8
|
-
export declare function prepareForMsgpack(value:
|
|
9
|
+
export declare function prepareForMsgpack(value: unknown): unknown;
|
|
9
10
|
//# sourceMappingURL=serializer.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serializer.d.ts","sourceRoot":"","sources":["../../src/server/serializer.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"serializer.d.ts","sourceRoot":"","sources":["../../src/server/serializer.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAGzD"}
|
|
@@ -4,14 +4,24 @@
|
|
|
4
4
|
* serialized correctly (hiding private fields, virtuals, etc.) and efficiently.
|
|
5
5
|
*
|
|
6
6
|
* It preserves MessagePack-supported types like Date and Uint8Array (Buffer).
|
|
7
|
+
* Circular references are replaced with null to avoid runtime crashes.
|
|
7
8
|
*/
|
|
8
9
|
export function prepareForMsgpack(value) {
|
|
10
|
+
const stack = new WeakSet();
|
|
11
|
+
return prepareForMsgpackInner(value, stack);
|
|
12
|
+
}
|
|
13
|
+
function prepareForMsgpackInner(value, stack) {
|
|
9
14
|
if (value === null || value === undefined) {
|
|
10
15
|
return value;
|
|
11
16
|
}
|
|
12
17
|
if (typeof value !== "object") {
|
|
13
18
|
return value;
|
|
14
19
|
}
|
|
20
|
+
// Security: circular reference detection (only true cycles, not shared refs)
|
|
21
|
+
if (stack.has(value)) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
stack.add(value);
|
|
15
25
|
// Preserve MessagePack supported types
|
|
16
26
|
if (value instanceof Date) {
|
|
17
27
|
return value;
|
|
@@ -20,21 +30,27 @@ export function prepareForMsgpack(value) {
|
|
|
20
30
|
return value;
|
|
21
31
|
}
|
|
22
32
|
// Handle toJSON (e.g. Mongoose documents, custom classes)
|
|
23
|
-
|
|
24
|
-
|
|
33
|
+
const valueWithToJson = value;
|
|
34
|
+
if (typeof valueWithToJson.toJSON === "function") {
|
|
35
|
+
const result = prepareForMsgpackInner(valueWithToJson.toJSON(), stack);
|
|
36
|
+
stack.delete(value);
|
|
37
|
+
return result;
|
|
25
38
|
}
|
|
26
39
|
// Handle Array
|
|
27
40
|
if (Array.isArray(value)) {
|
|
28
|
-
|
|
41
|
+
const result = value.map((item) => prepareForMsgpackInner(item, stack));
|
|
42
|
+
stack.delete(value);
|
|
43
|
+
return result;
|
|
29
44
|
}
|
|
30
45
|
// Handle Plain Objects
|
|
31
46
|
// We create a new object to avoid mutating the original
|
|
32
47
|
const newObj = {};
|
|
33
48
|
for (const key in value) {
|
|
34
49
|
if (Object.prototype.hasOwnProperty.call(value, key)) {
|
|
35
|
-
newObj[key] =
|
|
50
|
+
newObj[key] = prepareForMsgpackInner(value[key], stack);
|
|
36
51
|
}
|
|
37
52
|
}
|
|
53
|
+
stack.delete(value);
|
|
38
54
|
return newObj;
|
|
39
55
|
}
|
|
40
56
|
//# sourceMappingURL=serializer.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serializer.js","sourceRoot":"","sources":["../../src/server/serializer.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"serializer.js","sourceRoot":"","sources":["../../src/server/serializer.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,UAAU,iBAAiB,CAAC,KAAc;IAC5C,MAAM,KAAK,GAAG,IAAI,OAAO,EAAU,CAAC;IACpC,OAAO,sBAAsB,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;AAChD,CAAC;AAED,SAAS,sBAAsB,CAAC,KAAc,EAAE,KAAsB;IAClE,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACxC,OAAO,KAAK,CAAC;IACjB,CAAC;IACD,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC5B,OAAO,KAAK,CAAC;IACjB,CAAC;IAED,6EAA6E;IAC7E,IAAI,KAAK,CAAC,GAAG,CAAC,KAAe,CAAC,EAAE,CAAC;QAC7B,OAAO,IAAI,CAAC;IAChB,CAAC;IACD,KAAK,CAAC,GAAG,CAAC,KAAe,CAAC,CAAC;IAE3B,uCAAuC;IACvC,IAAI,KAAK,YAAY,IAAI,EAAE,CAAC;QACxB,OAAO,KAAK,CAAC;IACjB,CAAC;IACD,IAAI,KAAK,YAAY,UAAU,EAAE,CAAC;QAC9B,OAAO,KAAK,CAAC;IACjB,CAAC;IAED,0DAA0D;IAC1D,MAAM,eAAe,GAAG,KAAmC,CAAC;IAC5D,IAAI,OAAO,eAAe,CAAC,MAAM,KAAK,UAAU,EAAE,CAAC;QAC/C,MAAM,MAAM,GAAG,sBAAsB,CAAC,eAAe,CAAC,MAAM,EAAE,EAAE,KAAK,CAAC,CAAC;QACvE,KAAK,CAAC,MAAM,CAAC,KAAe,CAAC,CAAC;QAC9B,OAAO,MAAM,CAAC;IAClB,CAAC;IAED,eAAe;IACf,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACvB,MAAM,MAAM,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,sBAAsB,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC;QACxE,KAAK,CAAC,MAAM,CAAC,KAAe,CAAC,CAAC;QAC9B,OAAO,MAAM,CAAC;IAClB,CAAC;IAED,uBAAuB;IACvB,wDAAwD;IACxD,MAAM,MAAM,GAA4B,EAAE,CAAC;IAC3C,KAAK,MAAM,GAAG,IAAI,KAAK,EAAE,CAAC;QACtB,IAAI,MAAM,CAAC,SAAS,CAAC,cAAc,CAAC,IAAI,CAAC,KAAK,EAAE,GAAG,CAAC,EAAE,CAAC;YACnD,MAAM,CAAC,GAAG,CAAC,GAAG,sBAAsB,CAAE,KAAiC,CAAC,GAAG,CAAC,EAAE,KAAK,CAAC,CAAC;QACzF,CAAC;IACL,CAAC;IACD,KAAK,CAAC,MAAM,CAAC,KAAe,CAAC,CAAC;IAC9B,OAAO,MAAM,CAAC;AAClB,CAAC","sourcesContent":["/**\n * Prepares data for MessagePack serialization by respecting .toJSON() methods.\n * This ensures that objects like ORM entities (Mongoose, Prisma, etc.) are\n * serialized correctly (hiding private fields, virtuals, etc.) and efficiently.\n *\n * It preserves MessagePack-supported types like Date and Uint8Array (Buffer).\n * Circular references are replaced with null to avoid runtime crashes.\n */\nexport function prepareForMsgpack(value: unknown): unknown {\n const stack = new WeakSet<object>();\n return prepareForMsgpackInner(value, stack);\n}\n\nfunction prepareForMsgpackInner(value: unknown, stack: WeakSet<object>): unknown {\n if (value === null || value === undefined) {\n return value;\n }\n if (typeof value !== \"object\") {\n return value;\n }\n\n // Security: circular reference detection (only true cycles, not shared refs)\n if (stack.has(value as object)) {\n return null;\n }\n stack.add(value as object);\n\n // Preserve MessagePack supported types\n if (value instanceof Date) {\n return value;\n }\n if (value instanceof Uint8Array) {\n return value;\n }\n\n // Handle toJSON (e.g. Mongoose documents, custom classes)\n const valueWithToJson = value as { toJSON?: () => unknown };\n if (typeof valueWithToJson.toJSON === \"function\") {\n const result = prepareForMsgpackInner(valueWithToJson.toJSON(), stack);\n stack.delete(value as object);\n return result;\n }\n\n // Handle Array\n if (Array.isArray(value)) {\n const result = value.map((item) => prepareForMsgpackInner(item, stack));\n stack.delete(value as object);\n return result;\n }\n\n // Handle Plain Objects\n // We create a new object to avoid mutating the original\n const newObj: Record<string, unknown> = {};\n for (const key in value) {\n if (Object.prototype.hasOwnProperty.call(value, key)) {\n newObj[key] = prepareForMsgpackInner((value as Record<string, unknown>)[key], stack);\n }\n }\n stack.delete(value as object);\n return newObj;\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ipExtractor.d.ts","sourceRoot":"","sources":["../../src/utils/ipExtractor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAE7B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,eAAe,GAAE,MAAU,GAAG,MAAM,
|
|
1
|
+
{"version":3,"file":"ipExtractor.d.ts","sourceRoot":"","sources":["../../src/utils/ipExtractor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAE7B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,eAAe,GAAE,MAAU,GAAG,MAAM,CAgD9F;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,eAAe,GAAE,MAAU,GAAG,MAAM,CAqCvG"}
|
|
@@ -43,22 +43,11 @@ export function extractClientIP(req, trustProxyDepth = 0) {
|
|
|
43
43
|
if (trustProxyDepth === 0) {
|
|
44
44
|
return req.socket.remoteAddress || "unknown";
|
|
45
45
|
}
|
|
46
|
-
// 1
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
// 2. Check True-Client-IP (Cloudflare Enterprise, Akamai)
|
|
52
|
-
const trueClientIP = req.headers["true-client-ip"];
|
|
53
|
-
if (trueClientIP && typeof trueClientIP === "string" && trueClientIP.trim().length > 0) {
|
|
54
|
-
return trueClientIP.trim();
|
|
55
|
-
}
|
|
56
|
-
// 3. Check X-Real-IP (Nginx, other proxies)
|
|
57
|
-
const xRealIP = req.headers["x-real-ip"];
|
|
58
|
-
if (xRealIP && typeof xRealIP === "string" && xRealIP.trim().length > 0) {
|
|
59
|
-
return xRealIP.trim();
|
|
60
|
-
}
|
|
61
|
-
// 4. Check X-Forwarded-For (Standard, but requires parsing)
|
|
46
|
+
// Security: When trustProxyDepth >= 1, extract IP using X-Forwarded-For first.
|
|
47
|
+
// Single-value headers (CF-Connecting-IP, True-Client-IP, X-Real-IP) are only
|
|
48
|
+
// checked as a fallback since they can be spoofed by direct clients when no
|
|
49
|
+
// trusted proxy IP allowlist is configured.
|
|
50
|
+
// 1. Check X-Forwarded-For (Standard, most reliable with depth-based extraction)
|
|
62
51
|
const forwardedFor = req.headers["x-forwarded-for"];
|
|
63
52
|
if (forwardedFor) {
|
|
64
53
|
const forwardedIPs = (Array.isArray(forwardedFor) ? forwardedFor.join(",") : forwardedFor)
|
|
@@ -73,6 +62,21 @@ export function extractClientIP(req, trustProxyDepth = 0) {
|
|
|
73
62
|
}
|
|
74
63
|
}
|
|
75
64
|
}
|
|
65
|
+
// 2. Fallback: Check CF-Connecting-IP (Cloudflare)
|
|
66
|
+
const cfConnectingIP = req.headers["cf-connecting-ip"];
|
|
67
|
+
if (cfConnectingIP && typeof cfConnectingIP === "string" && cfConnectingIP.trim().length > 0) {
|
|
68
|
+
return cfConnectingIP.trim();
|
|
69
|
+
}
|
|
70
|
+
// 3. Fallback: Check True-Client-IP (Cloudflare Enterprise, Akamai)
|
|
71
|
+
const trueClientIP = req.headers["true-client-ip"];
|
|
72
|
+
if (trueClientIP && typeof trueClientIP === "string" && trueClientIP.trim().length > 0) {
|
|
73
|
+
return trueClientIP.trim();
|
|
74
|
+
}
|
|
75
|
+
// 4. Fallback: Check X-Real-IP (Nginx, other proxies)
|
|
76
|
+
const xRealIP = req.headers["x-real-ip"];
|
|
77
|
+
if (xRealIP && typeof xRealIP === "string" && xRealIP.trim().length > 0) {
|
|
78
|
+
return xRealIP.trim();
|
|
79
|
+
}
|
|
76
80
|
// 5. Fall back to direct connection
|
|
77
81
|
return req.socket.remoteAddress || "unknown";
|
|
78
82
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ipExtractor.js","sourceRoot":"","sources":["../../src/utils/ipExtractor.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,MAAM,UAAU,eAAe,CAAC,GAAyB,EAAE,kBAA0B,CAAC;IAClF,+DAA+D;IAC/D,IAAI,eAAe,KAAK,CAAC,EAAE,CAAC;QACxB,OAAO,GAAG,CAAC,MAAM,CAAC,aAAa,IAAI,SAAS,CAAC;IACjD,CAAC;IAED,
|
|
1
|
+
{"version":3,"file":"ipExtractor.js","sourceRoot":"","sources":["../../src/utils/ipExtractor.ts"],"names":[],"mappings":"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","sourcesContent":["import type http from \"http\";\n\n/**\n * Extracts the client IP address from an HTTP request, checking multiple headers\n * and taking into account proxy configurations.\n *\n * Checks headers in order of reliability:\n * 1. CF-Connecting-IP (Cloudflare)\n * 2. True-Client-IP (Cloudflare Enterprise, Akamai)\n * 3. X-Real-IP (Nginx, other proxies)\n * 4. X-Forwarded-For (Standard, but can be spoofed)\n * 5. req.socket.remoteAddress (Direct connection)\n *\n * When behind proxies, the X-Forwarded-For header contains a chain of IP addresses.\n * Format: \"client, proxy1, proxy2, ...\"\n *\n * @param req - The HTTP request object\n * @param trustProxyDepth - Number of proxy levels to trust\n * - 0: Only use req.socket.remoteAddress (no proxy trust)\n * - 1: Trust 1 proxy level (recommended for Vercel, Netlify, Railway)\n * - 2+: Trust multiple proxy levels (for complex setups)\n *\n * Examples:\n * - trustProxyDepth=0: Direct connection, no proxies\n * All proxy headers ignored\n * Result: req.socket.remoteAddress\n *\n * - trustProxyDepth=1: Behind one proxy (e.g., Vercel, Netlify)\n * X-Forwarded-For: \"203.0.113.1, 198.51.100.1\"\n * Result: \"203.0.113.1\" (client IP)\n *\n * - trustProxyDepth=2: Behind two proxies (e.g., Cloudflare -> Load Balancer)\n * X-Forwarded-For: \"203.0.113.1, 198.51.100.1, 192.0.2.1\"\n * Result: \"203.0.113.1\" (client IP)\n *\n * Common configurations:\n * - Vercel/Netlify/Railway: trustProxyDepth=1\n * - Cloudflare -> Origin: trustProxyDepth=1 (use CF-Connecting-IP)\n * - AWS ALB -> EC2: trustProxyDepth=1\n * - Nginx -> Node: trustProxyDepth=1 (use X-Real-IP)\n * - Cloudflare -> Nginx -> Node: trustProxyDepth=2\n */\nexport function extractClientIP(req: http.IncomingMessage, trustProxyDepth: number = 0): string {\n // If not trusting any proxies, return the direct connection IP\n if (trustProxyDepth === 0) {\n return req.socket.remoteAddress || \"unknown\";\n }\n\n // Security: When trustProxyDepth >= 1, extract IP using X-Forwarded-For first.\n // Single-value headers (CF-Connecting-IP, True-Client-IP, X-Real-IP) are only\n // checked as a fallback since they can be spoofed by direct clients when no\n // trusted proxy IP allowlist is configured.\n\n // 1. Check X-Forwarded-For (Standard, most reliable with depth-based extraction)\n const forwardedFor = req.headers[\"x-forwarded-for\"];\n if (forwardedFor) {\n const forwardedIPs = (Array.isArray(forwardedFor) ? forwardedFor.join(\",\") : forwardedFor)\n .split(\",\")\n .map((ip) => ip.trim())\n .filter((ip) => ip.length > 0);\n\n if (forwardedIPs.length > 0) {\n // Verify we have enough IPs in the chain for the trust depth\n if (forwardedIPs.length >= trustProxyDepth) {\n // Return the client IP (first in the chain)\n return forwardedIPs[0];\n }\n }\n }\n\n // 2. Fallback: Check CF-Connecting-IP (Cloudflare)\n const cfConnectingIP = req.headers[\"cf-connecting-ip\"];\n if (cfConnectingIP && typeof cfConnectingIP === \"string\" && cfConnectingIP.trim().length > 0) {\n return cfConnectingIP.trim();\n }\n\n // 3. Fallback: Check True-Client-IP (Cloudflare Enterprise, Akamai)\n const trueClientIP = req.headers[\"true-client-ip\"];\n if (trueClientIP && typeof trueClientIP === \"string\" && trueClientIP.trim().length > 0) {\n return trueClientIP.trim();\n }\n\n // 4. Fallback: Check X-Real-IP (Nginx, other proxies)\n const xRealIP = req.headers[\"x-real-ip\"];\n if (xRealIP && typeof xRealIP === \"string\" && xRealIP.trim().length > 0) {\n return xRealIP.trim();\n }\n\n // 5. Fall back to direct connection\n return req.socket.remoteAddress || \"unknown\";\n}\n\n/**\n * Alternative extraction method that works from the right (trusts the rightmost IPs).\n * This is useful when you want to trust the last N proxies in the chain.\n *\n * Note: This only applies to X-Forwarded-For parsing. Single-value headers like\n * CF-Connecting-IP are still checked first.\n *\n * @param req - The HTTP request object\n * @param trustProxyDepth - Number of proxy levels to trust from the right\n *\n * Example:\n * X-Forwarded-For: \"203.0.113.1, 198.51.100.1, 192.0.2.1\"\n * trustProxyDepth=1: Result is \"198.51.100.1\" (skip the last trusted proxy)\n * trustProxyDepth=2: Result is \"203.0.113.1\" (skip the last 2 trusted proxies)\n */\nexport function extractClientIPFromRight(req: http.IncomingMessage, trustProxyDepth: number = 0): string {\n if (trustProxyDepth === 0) {\n return req.socket.remoteAddress || \"unknown\";\n }\n\n // Check single-value headers first (same as extractClientIP)\n const cfConnectingIP = req.headers[\"cf-connecting-ip\"];\n if (cfConnectingIP && typeof cfConnectingIP === \"string\" && cfConnectingIP.trim().length > 0) {\n return cfConnectingIP.trim();\n }\n\n const trueClientIP = req.headers[\"true-client-ip\"];\n if (trueClientIP && typeof trueClientIP === \"string\" && trueClientIP.trim().length > 0) {\n return trueClientIP.trim();\n }\n\n const xRealIP = req.headers[\"x-real-ip\"];\n if (xRealIP && typeof xRealIP === \"string\" && xRealIP.trim().length > 0) {\n return xRealIP.trim();\n }\n\n // For X-Forwarded-For, use right-based extraction\n const forwardedFor = req.headers[\"x-forwarded-for\"];\n if (forwardedFor) {\n const forwardedIPs = (Array.isArray(forwardedFor) ? forwardedFor.join(\",\") : forwardedFor)\n .split(\",\")\n .map((ip) => ip.trim())\n .filter((ip) => ip.length > 0);\n\n if (forwardedIPs.length > 0) {\n // Calculate which IP to trust by skipping the rightmost N trusted proxies\n const clientIPIndex = Math.max(0, forwardedIPs.length - trustProxyDepth - 1);\n return forwardedIPs[clientIPIndex];\n }\n }\n\n return req.socket.remoteAddress || \"unknown\";\n}\n"]}
|