@arcis/node 1.3.0 → 1.4.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/dist/core/{index.d.mts → constants.d.ts} +21 -70
- package/dist/core/constants.d.ts.map +1 -0
- package/dist/core/errors.d.ts +53 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/index.d.ts +6 -168
- package/dist/core/index.d.ts.map +1 -0
- package/dist/{types-BOkx5YJc.d.mts → core/types.d.ts} +27 -30
- package/dist/core/types.d.ts.map +1 -0
- package/dist/index.d.ts +71 -166
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +57 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +56 -3
- package/dist/index.mjs.map +1 -1
- package/dist/logging/index.d.ts +4 -36
- package/dist/logging/index.d.ts.map +1 -0
- package/dist/logging/{index.d.mts → redactor.d.ts} +5 -9
- package/dist/logging/redactor.d.ts.map +1 -0
- package/dist/middleware/bot-detection.d.ts +86 -0
- package/dist/middleware/bot-detection.d.ts.map +1 -0
- package/dist/middleware/cookies.d.ts +48 -0
- package/dist/middleware/cookies.d.ts.map +1 -0
- package/dist/middleware/cors.d.ts +65 -0
- package/dist/middleware/cors.d.ts.map +1 -0
- package/dist/middleware/csrf.d.ts +109 -0
- package/dist/middleware/csrf.d.ts.map +1 -0
- package/dist/middleware/error-handler.d.ts +43 -0
- package/dist/middleware/error-handler.d.ts.map +1 -0
- package/dist/middleware/headers.d.ts +29 -0
- package/dist/middleware/headers.d.ts.map +1 -0
- package/dist/middleware/hpp.d.ts +56 -0
- package/dist/middleware/hpp.d.ts.map +1 -0
- package/dist/middleware/index.d.ts +16 -3
- package/dist/middleware/index.d.ts.map +1 -0
- package/dist/middleware/index.js +6 -1
- package/dist/middleware/index.js.map +1 -1
- package/dist/middleware/index.mjs +6 -1
- package/dist/middleware/index.mjs.map +1 -1
- package/dist/middleware/main.d.ts +40 -0
- package/dist/middleware/main.d.ts.map +1 -0
- package/dist/middleware/rate-limit-sliding.d.ts +46 -0
- package/dist/middleware/rate-limit-sliding.d.ts.map +1 -0
- package/dist/middleware/rate-limit-token.d.ts +51 -0
- package/dist/middleware/rate-limit-token.d.ts.map +1 -0
- package/dist/middleware/rate-limit.d.ts +34 -0
- package/dist/middleware/rate-limit.d.ts.map +1 -0
- package/dist/sanitizers/command.d.ts +28 -0
- package/dist/sanitizers/command.d.ts.map +1 -0
- package/dist/sanitizers/encode.d.ts +46 -0
- package/dist/sanitizers/encode.d.ts.map +1 -0
- package/dist/sanitizers/headers.d.ts +46 -0
- package/dist/sanitizers/headers.d.ts.map +1 -0
- package/dist/sanitizers/index.d.ts +17 -22
- package/dist/sanitizers/index.d.ts.map +1 -0
- package/dist/sanitizers/jsonp.d.ts +34 -0
- package/dist/sanitizers/jsonp.d.ts.map +1 -0
- package/dist/sanitizers/nosql.d.ts +31 -0
- package/dist/sanitizers/nosql.d.ts.map +1 -0
- package/dist/sanitizers/path.d.ts +28 -0
- package/dist/sanitizers/path.d.ts.map +1 -0
- package/dist/sanitizers/pii.d.ts +80 -0
- package/dist/sanitizers/pii.d.ts.map +1 -0
- package/dist/sanitizers/prototype.d.ts +34 -0
- package/dist/sanitizers/prototype.d.ts.map +1 -0
- package/dist/sanitizers/sanitize.d.ts +51 -0
- package/dist/sanitizers/sanitize.d.ts.map +1 -0
- package/dist/sanitizers/sql.d.ts +28 -0
- package/dist/sanitizers/sql.d.ts.map +1 -0
- package/dist/sanitizers/ssti.d.ts +20 -0
- package/dist/sanitizers/ssti.d.ts.map +1 -0
- package/dist/sanitizers/utils.d.ts +19 -0
- package/dist/sanitizers/utils.d.ts.map +1 -0
- package/dist/sanitizers/xss.d.ts +35 -0
- package/dist/sanitizers/xss.d.ts.map +1 -0
- package/dist/sanitizers/xxe.d.ts +20 -0
- package/dist/sanitizers/xxe.d.ts.map +1 -0
- package/dist/stores/index.d.ts +6 -104
- package/dist/stores/index.d.ts.map +1 -0
- package/dist/stores/memory.d.ts +35 -0
- package/dist/stores/memory.d.ts.map +1 -0
- package/dist/stores/{index.d.mts → redis.d.ts} +6 -45
- package/dist/stores/redis.d.ts.map +1 -0
- package/dist/utils/duration.d.ts +34 -0
- package/dist/utils/duration.d.ts.map +1 -0
- package/dist/utils/fingerprint.d.ts +64 -0
- package/dist/utils/fingerprint.d.ts.map +1 -0
- package/dist/utils/index.d.ts +10 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +188 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/index.mjs +182 -0
- package/dist/utils/index.mjs.map +1 -0
- package/dist/utils/ip.d.ts +70 -0
- package/dist/utils/ip.d.ts.map +1 -0
- package/dist/validation/email.d.ts +82 -0
- package/dist/validation/email.d.ts.map +1 -0
- package/dist/validation/file.d.ts +90 -0
- package/dist/validation/file.d.ts.map +1 -0
- package/dist/validation/index.d.ts +10 -3
- package/dist/validation/index.d.ts.map +1 -0
- package/dist/validation/redirect.d.ts +64 -0
- package/dist/validation/redirect.d.ts.map +1 -0
- package/dist/validation/schema.d.ts +36 -0
- package/dist/validation/schema.d.ts.map +1 -0
- package/dist/validation/url.d.ts +65 -0
- package/dist/validation/url.d.ts.map +1 -0
- package/package.json +8 -6
- package/dist/encode-CrQCGlBq.d.mts +0 -484
- package/dist/encode-jl9sOwmA.d.ts +0 -484
- package/dist/index-BAhgn9V2.d.ts +0 -532
- package/dist/index-BGNKspqH.d.ts +0 -340
- package/dist/index-Cd02z-0j.d.mts +0 -340
- package/dist/index-DgJtWMSj.d.mts +0 -532
- package/dist/index.d.mts +0 -175
- package/dist/middleware/index.d.mts +0 -3
- package/dist/sanitizers/index.d.mts +0 -24
- package/dist/types-BOkx5YJc.d.ts +0 -279
- package/dist/validation/index.d.mts +0 -3
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var crypto = require('crypto');
|
|
4
|
+
|
|
5
|
+
// src/utils/duration.ts
|
|
6
|
+
var MAX_DURATION_MS = 4294967295;
|
|
7
|
+
var DURATION_REGEX = /^(\d+(?:\.\d+)?)\s*(ms|s|m|h|d)$/i;
|
|
8
|
+
var UNIT_TO_MS = {
|
|
9
|
+
ms: 1,
|
|
10
|
+
s: 1e3,
|
|
11
|
+
m: 6e4,
|
|
12
|
+
h: 36e5,
|
|
13
|
+
d: 864e5
|
|
14
|
+
};
|
|
15
|
+
function parseDuration(value) {
|
|
16
|
+
if (typeof value === "number") {
|
|
17
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
18
|
+
throw new Error(`Invalid duration: ${value}. Must be a non-negative finite number.`);
|
|
19
|
+
}
|
|
20
|
+
return Math.min(Math.floor(value), MAX_DURATION_MS);
|
|
21
|
+
}
|
|
22
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
23
|
+
throw new Error(`Invalid duration: "${value}". Expected a duration string (e.g. "5m", "2h") or number.`);
|
|
24
|
+
}
|
|
25
|
+
const match = value.trim().match(DURATION_REGEX);
|
|
26
|
+
if (!match) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
`Invalid duration: "${value}". Expected format: <number><unit> where unit is ms, s, m, h, or d.`
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
const amount = parseFloat(match[1]);
|
|
32
|
+
const unit = match[2].toLowerCase();
|
|
33
|
+
const ms = Math.floor(amount * UNIT_TO_MS[unit]);
|
|
34
|
+
if (ms < 0 || ms > MAX_DURATION_MS) {
|
|
35
|
+
throw new Error(`Duration "${value}" exceeds maximum allowed (${MAX_DURATION_MS}ms / ~49.7 days).`);
|
|
36
|
+
}
|
|
37
|
+
return ms;
|
|
38
|
+
}
|
|
39
|
+
function formatDuration(ms) {
|
|
40
|
+
if (!Number.isFinite(ms) || ms < 0) return "0ms";
|
|
41
|
+
if (ms < 1e3) return `${ms}ms`;
|
|
42
|
+
const days = Math.floor(ms / 864e5);
|
|
43
|
+
const hours = Math.floor(ms % 864e5 / 36e5);
|
|
44
|
+
const minutes = Math.floor(ms % 36e5 / 6e4);
|
|
45
|
+
const seconds = Math.floor(ms % 6e4 / 1e3);
|
|
46
|
+
const parts = [];
|
|
47
|
+
if (days > 0) parts.push(`${days}d`);
|
|
48
|
+
if (hours > 0) parts.push(`${hours}h`);
|
|
49
|
+
if (minutes > 0) parts.push(`${minutes}m`);
|
|
50
|
+
if (seconds > 0) parts.push(`${seconds}s`);
|
|
51
|
+
return parts.join(" ") || "0ms";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// src/utils/ip.ts
|
|
55
|
+
var PLATFORM_HEADERS = {
|
|
56
|
+
cloudflare: "cf-connecting-ip",
|
|
57
|
+
vercel: "x-real-ip",
|
|
58
|
+
flyio: "fly-client-ip",
|
|
59
|
+
render: "x-render-client-ip",
|
|
60
|
+
firebase: "x-appengine-user-ip",
|
|
61
|
+
"aws-alb": "x-forwarded-for"
|
|
62
|
+
};
|
|
63
|
+
function detectPlatform() {
|
|
64
|
+
const env = typeof process !== "undefined" ? process.env : {};
|
|
65
|
+
if (env.CF_PAGES || env.CF_WORKERS) return "cloudflare";
|
|
66
|
+
if (env.VERCEL) return "vercel";
|
|
67
|
+
if (env.FLY_APP_NAME) return "flyio";
|
|
68
|
+
if (env.RENDER) return "render";
|
|
69
|
+
if (env.FIREBASE_CONFIG || env.GCLOUD_PROJECT) return "firebase";
|
|
70
|
+
if (env.AWS_EXECUTION_ENV || env.AWS_LAMBDA_FUNCTION_NAME) return "aws-alb";
|
|
71
|
+
return "generic";
|
|
72
|
+
}
|
|
73
|
+
var _cachedPlatform = null;
|
|
74
|
+
function getCachedPlatform() {
|
|
75
|
+
if (_cachedPlatform === null) {
|
|
76
|
+
_cachedPlatform = detectPlatform();
|
|
77
|
+
}
|
|
78
|
+
return _cachedPlatform;
|
|
79
|
+
}
|
|
80
|
+
var MAX_IP_LENGTH = 45;
|
|
81
|
+
function sanitizeIp(ip) {
|
|
82
|
+
const trimmed = ip.trim();
|
|
83
|
+
if (trimmed.length > MAX_IP_LENGTH) return trimmed.slice(0, MAX_IP_LENGTH);
|
|
84
|
+
return trimmed;
|
|
85
|
+
}
|
|
86
|
+
function getHeader(req, name) {
|
|
87
|
+
const val = req.headers[name];
|
|
88
|
+
if (Array.isArray(val)) return val[0];
|
|
89
|
+
return val;
|
|
90
|
+
}
|
|
91
|
+
function parseForwardedFor(header, trustedProxyCount) {
|
|
92
|
+
const ips = header.split(",").map((ip) => ip.trim()).filter(Boolean);
|
|
93
|
+
if (ips.length === 0) return void 0;
|
|
94
|
+
const clientIndex = Math.max(0, ips.length - trustedProxyCount);
|
|
95
|
+
return ips[clientIndex] || void 0;
|
|
96
|
+
}
|
|
97
|
+
function detectClientIp(req, options = {}) {
|
|
98
|
+
const { platform = "auto", trustedProxyCount = 1 } = options;
|
|
99
|
+
const r = req;
|
|
100
|
+
const resolvedPlatform = platform === "auto" ? getCachedPlatform() : platform;
|
|
101
|
+
if (resolvedPlatform !== "generic" && resolvedPlatform in PLATFORM_HEADERS) {
|
|
102
|
+
const headerName = PLATFORM_HEADERS[resolvedPlatform];
|
|
103
|
+
if (headerName) {
|
|
104
|
+
if (resolvedPlatform === "aws-alb") {
|
|
105
|
+
const xff2 = getHeader(r, "x-forwarded-for");
|
|
106
|
+
if (xff2) {
|
|
107
|
+
const ip = parseForwardedFor(xff2, trustedProxyCount);
|
|
108
|
+
if (ip) return sanitizeIp(ip);
|
|
109
|
+
}
|
|
110
|
+
} else {
|
|
111
|
+
const ip = getHeader(r, headerName);
|
|
112
|
+
if (ip) return sanitizeIp(ip);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (r.ip) return sanitizeIp(r.ip);
|
|
117
|
+
const xff = getHeader(r, "x-forwarded-for");
|
|
118
|
+
if (xff) {
|
|
119
|
+
const ip = parseForwardedFor(xff, trustedProxyCount);
|
|
120
|
+
if (ip) return sanitizeIp(ip);
|
|
121
|
+
}
|
|
122
|
+
const realIp = getHeader(r, "x-real-ip");
|
|
123
|
+
if (realIp) return sanitizeIp(realIp);
|
|
124
|
+
const socketIp = r.socket?.remoteAddress ?? r.connection?.remoteAddress;
|
|
125
|
+
if (socketIp) return sanitizeIp(socketIp);
|
|
126
|
+
return "unknown";
|
|
127
|
+
}
|
|
128
|
+
function isPrivateIp(ip) {
|
|
129
|
+
const normalized = ip.startsWith("::ffff:") ? ip.slice(7) : ip;
|
|
130
|
+
if (/^127\./.test(normalized)) return true;
|
|
131
|
+
if (/^10\./.test(normalized)) return true;
|
|
132
|
+
if (/^172\.(1[6-9]|2\d|3[01])\./.test(normalized)) return true;
|
|
133
|
+
if (/^192\.168\./.test(normalized)) return true;
|
|
134
|
+
if (/^169\.254\./.test(normalized)) return true;
|
|
135
|
+
if (/^0\./.test(normalized)) return true;
|
|
136
|
+
if (ip === "::1") return true;
|
|
137
|
+
if (/^fe80:/i.test(ip)) return true;
|
|
138
|
+
if (/^fc00:/i.test(ip)) return true;
|
|
139
|
+
if (/^fd/i.test(ip)) return true;
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
function getHeader2(req, name) {
|
|
143
|
+
const val = req.headers[name];
|
|
144
|
+
if (Array.isArray(val)) return val[0] ?? "";
|
|
145
|
+
return val ?? "";
|
|
146
|
+
}
|
|
147
|
+
function fingerprint(req, options = {}) {
|
|
148
|
+
const {
|
|
149
|
+
ip = true,
|
|
150
|
+
userAgent = true,
|
|
151
|
+
accept = true,
|
|
152
|
+
acceptLanguage = true,
|
|
153
|
+
acceptEncoding = true,
|
|
154
|
+
custom = [],
|
|
155
|
+
ipOptions
|
|
156
|
+
} = options;
|
|
157
|
+
const components = [];
|
|
158
|
+
if (ip) {
|
|
159
|
+
components.push(`ip:${detectClientIp(req, ipOptions)}`);
|
|
160
|
+
}
|
|
161
|
+
if (userAgent) {
|
|
162
|
+
components.push(`ua:${getHeader2(req, "user-agent")}`);
|
|
163
|
+
}
|
|
164
|
+
if (accept) {
|
|
165
|
+
components.push(`accept:${getHeader2(req, "accept")}`);
|
|
166
|
+
}
|
|
167
|
+
if (acceptLanguage) {
|
|
168
|
+
components.push(`lang:${getHeader2(req, "accept-language")}`);
|
|
169
|
+
}
|
|
170
|
+
if (acceptEncoding) {
|
|
171
|
+
components.push(`enc:${getHeader2(req, "accept-encoding")}`);
|
|
172
|
+
}
|
|
173
|
+
for (const c of custom) {
|
|
174
|
+
if (c !== null && c !== void 0) components.push(`custom:${c}`);
|
|
175
|
+
}
|
|
176
|
+
components.sort();
|
|
177
|
+
const hash = crypto.createHash("sha256");
|
|
178
|
+
hash.update(components.join("|"));
|
|
179
|
+
return hash.digest("hex");
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
exports.detectClientIp = detectClientIp;
|
|
183
|
+
exports.fingerprint = fingerprint;
|
|
184
|
+
exports.formatDuration = formatDuration;
|
|
185
|
+
exports.isPrivateIp = isPrivateIp;
|
|
186
|
+
exports.parseDuration = parseDuration;
|
|
187
|
+
//# sourceMappingURL=index.js.map
|
|
188
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/utils/duration.ts","../../src/utils/ip.ts","../../src/utils/fingerprint.ts"],"names":["xff","getHeader","createHash"],"mappings":";;;;;AAcA,IAAM,eAAA,GAAkB,UAAA;AAExB,IAAM,cAAA,GAAiB,mCAAA;AAEvB,IAAM,UAAA,GAAqC;AAAA,EACzC,EAAA,EAAI,CAAA;AAAA,EACJ,CAAA,EAAG,GAAA;AAAA,EACH,CAAA,EAAG,GAAA;AAAA,EACH,CAAA,EAAG,IAAA;AAAA,EACH,CAAA,EAAG;AACL,CAAA;AAeO,SAAS,cAAc,KAAA,EAAgC;AAC5D,EAAA,IAAI,OAAO,UAAU,QAAA,EAAU;AAC7B,IAAA,IAAI,CAAC,MAAA,CAAO,QAAA,CAAS,KAAK,CAAA,IAAK,QAAQ,CAAA,EAAG;AACxC,MAAA,MAAM,IAAI,KAAA,CAAM,CAAA,kBAAA,EAAqB,KAAK,CAAA,uCAAA,CAAyC,CAAA;AAAA,IACrF;AACA,IAAA,OAAO,KAAK,GAAA,CAAI,IAAA,CAAK,KAAA,CAAM,KAAK,GAAG,eAAe,CAAA;AAAA,EACpD;AAEA,EAAA,IAAI,OAAO,KAAA,KAAU,QAAA,IAAY,KAAA,CAAM,IAAA,OAAW,EAAA,EAAI;AACpD,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,mBAAA,EAAsB,KAAK,CAAA,0DAAA,CAA4D,CAAA;AAAA,EACzG;AAEA,EAAA,MAAM,KAAA,GAAQ,KAAA,CAAM,IAAA,EAAK,CAAE,MAAM,cAAc,CAAA;AAC/C,EAAA,IAAI,CAAC,KAAA,EAAO;AACV,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,sBAAsB,KAAK,CAAA,mEAAA;AAAA,KAC7B;AAAA,EACF;AAEA,EAAA,MAAM,MAAA,GAAS,UAAA,CAAW,KAAA,CAAM,CAAC,CAAC,CAAA;AAClC,EAAA,MAAM,IAAA,GAAO,KAAA,CAAM,CAAC,CAAA,CAAE,WAAA,EAAY;AAClC,EAAA,MAAM,KAAK,IAAA,CAAK,KAAA,CAAM,MAAA,GAAS,UAAA,CAAW,IAAI,CAAC,CAAA;AAE/C,EAAA,IAAI,EAAA,GAAK,CAAA,IAAK,EAAA,GAAK,eAAA,EAAiB;AAClC,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,UAAA,EAAa,KAAK,CAAA,2BAAA,EAA8B,eAAe,CAAA,iBAAA,CAAmB,CAAA;AAAA,EACpG;AAEA,EAAA,OAAO,EAAA;AACT;AAQO,SAAS,eAAe,EAAA,EAAoB;AACjD,EAAA,IAAI,CAAC,MAAA,CAAO,QAAA,CAAS,EAAE,CAAA,IAAK,EAAA,GAAK,GAAG,OAAO,KAAA;AAE3C,EAAA,IAAI,EAAA,GAAK,GAAA,EAAM,OAAO,CAAA,EAAG,EAAE,CAAA,EAAA,CAAA;AAE3B,EAAA,MAAM,IAAA,GAAO,IAAA,CAAK,KAAA,CAAM,EAAA,GAAK,KAAU,CAAA;AACvC,EAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,KAAA,CAAO,EAAA,GAAK,QAAc,IAAS,CAAA;AACtD,EAAA,MAAM,OAAA,GAAU,IAAA,CAAK,KAAA,CAAO,EAAA,GAAK,OAAa,GAAM,CAAA;AACpD,EAAA,MAAM,OAAA,GAAU,IAAA,CAAK,KAAA,CAAO,EAAA,GAAK,MAAU,GAAK,CAAA;AAEhD,EAAA,MAAM,QAAkB,EAAC;AACzB,EAAA,IAAI,OAAO,CAAA,EAAG,KAAA,CAAM,IAAA,CAAK,CAAA,EAAG,IAAI,CAAA,CAAA,CAAG,CAAA;AACnC,EAAA,IAAI,QAAQ,CAAA,EAAG,KAAA,CAAM,IAAA,CAAK,CAAA,EAAG,KAAK,CAAA,CAAA,CAAG,CAAA;AACrC,EAAA,IAAI,UAAU,CAAA,EAAG,KAAA,CAAM,IAAA,CAAK,CAAA,EAAG,OAAO,CAAA,CAAA,CAAG,CAAA;AACzC,EAAA,IAAI,UAAU,CAAA,EAAG,KAAA,CAAM,IAAA,CAAK,CAAA,EAAG,OAAO,CAAA,CAAA,CAAG,CAAA;AAEzC,EAAA,OAAO,KAAA,CAAM,IAAA,CAAK,GAAG,CAAA,IAAK,KAAA;AAC5B;;;AC/CA,IAAM,gBAAA,GAA0E;AAAA,EAC9E,UAAA,EAAY,kBAAA;AAAA,EACZ,MAAA,EAAQ,WAAA;AAAA,EACR,KAAA,EAAO,eAAA;AAAA,EACP,MAAA,EAAQ,oBAAA;AAAA,EACR,QAAA,EAAU,qBAAA;AAAA,EACV,SAAA,EAAW;AACb,CAAA;AAKA,SAAS,cAAA,GAA2B;AAClC,EAAA,MAAM,MAAM,OAAO,OAAA,KAAY,WAAA,GAAc,OAAA,CAAQ,MAAM,EAAC;AAE5D,EAAA,IAAI,GAAA,CAAI,QAAA,IAAY,GAAA,CAAI,UAAA,EAAY,OAAO,YAAA;AAC3C,EAAA,IAAI,GAAA,CAAI,QAAQ,OAAO,QAAA;AACvB,EAAA,IAAI,GAAA,CAAI,cAAc,OAAO,OAAA;AAC7B,EAAA,IAAI,GAAA,CAAI,QAAQ,OAAO,QAAA;AACvB,EAAA,IAAI,GAAA,CAAI,eAAA,IAAmB,GAAA,CAAI,cAAA,EAAgB,OAAO,UAAA;AACtD,EAAA,IAAI,GAAA,CAAI,iBAAA,IAAqB,GAAA,CAAI,wBAAA,EAA0B,OAAO,SAAA;AAElE,EAAA,OAAO,SAAA;AACT;AAGA,IAAI,eAAA,GAAmC,IAAA;AAEvC,SAAS,iBAAA,GAA8B;AACrC,EAAA,IAAI,oBAAoB,IAAA,EAAM;AAC5B,IAAA,eAAA,GAAkB,cAAA,EAAe;AAAA,EACnC;AACA,EAAA,OAAO,eAAA;AACT;AAGA,IAAM,aAAA,GAAgB,EAAA;AAMtB,SAAS,WAAW,EAAA,EAAoB;AACtC,EAAA,MAAM,OAAA,GAAU,GAAG,IAAA,EAAK;AACxB,EAAA,IAAI,QAAQ,MAAA,GAAS,aAAA,SAAsB,OAAA,CAAQ,KAAA,CAAM,GAAG,aAAa,CAAA;AACzE,EAAA,OAAO,OAAA;AACT;AAKA,SAAS,SAAA,CAAU,KAAkB,IAAA,EAAkC;AACrE,EAAA,MAAM,GAAA,GAAM,GAAA,CAAI,OAAA,CAAQ,IAAI,CAAA;AAC5B,EAAA,IAAI,MAAM,OAAA,CAAQ,GAAG,CAAA,EAAG,OAAO,IAAI,CAAC,CAAA;AACpC,EAAA,OAAO,GAAA;AACT;AAOA,SAAS,iBAAA,CAAkB,QAAgB,iBAAA,EAA+C;AACxF,EAAA,MAAM,GAAA,GAAM,MAAA,CAAO,KAAA,CAAM,GAAG,CAAA,CAAE,GAAA,CAAI,CAAA,EAAA,KAAM,EAAA,CAAG,IAAA,EAAM,CAAA,CAAE,MAAA,CAAO,OAAO,CAAA;AACjE,EAAA,IAAI,GAAA,CAAI,MAAA,KAAW,CAAA,EAAG,OAAO,MAAA;AAG7B,EAAA,MAAM,cAAc,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,GAAA,CAAI,SAAS,iBAAiB,CAAA;AAC9D,EAAA,OAAO,GAAA,CAAI,WAAW,CAAA,IAAK,MAAA;AAC7B;AA6BO,SAAS,cAAA,CACd,GAAA,EACA,OAAA,GAA2B,EAAC,EACpB;AACR,EAAA,MAAM,EAAE,QAAA,GAAW,MAAA,EAAQ,iBAAA,GAAoB,GAAE,GAAI,OAAA;AACrD,EAAA,MAAM,CAAA,GAAI,GAAA;AAEV,EAAA,MAAM,gBAAA,GAAmB,QAAA,KAAa,MAAA,GAAS,iBAAA,EAAkB,GAAI,QAAA;AAGrE,EAAA,IAAI,gBAAA,KAAqB,SAAA,IAAa,gBAAA,IAAoB,gBAAA,EAAkB;AAC1E,IAAA,MAAM,UAAA,GAAa,iBAAiB,gBAAiD,CAAA;AACrF,IAAA,IAAI,UAAA,EAAY;AACd,MAAA,IAAI,qBAAqB,SAAA,EAAW;AAElC,QAAA,MAAMA,IAAAA,GAAM,SAAA,CAAU,CAAA,EAAG,iBAAiB,CAAA;AAC1C,QAAA,IAAIA,IAAAA,EAAK;AACP,UAAA,MAAM,EAAA,GAAK,iBAAA,CAAkBA,IAAAA,EAAK,iBAAiB,CAAA;AACnD,UAAA,IAAI,EAAA,EAAI,OAAO,UAAA,CAAW,EAAE,CAAA;AAAA,QAC9B;AAAA,MACF,CAAA,MAAO;AACL,QAAA,MAAM,EAAA,GAAK,SAAA,CAAU,CAAA,EAAG,UAAU,CAAA;AAClC,QAAA,IAAI,EAAA,EAAI,OAAO,UAAA,CAAW,EAAE,CAAA;AAAA,MAC9B;AAAA,IACF;AAAA,EACF;AAGA,EAAA,IAAI,CAAA,CAAE,EAAA,EAAI,OAAO,UAAA,CAAW,EAAE,EAAE,CAAA;AAGhC,EAAA,MAAM,GAAA,GAAM,SAAA,CAAU,CAAA,EAAG,iBAAiB,CAAA;AAC1C,EAAA,IAAI,GAAA,EAAK;AACP,IAAA,MAAM,EAAA,GAAK,iBAAA,CAAkB,GAAA,EAAK,iBAAiB,CAAA;AACnD,IAAA,IAAI,EAAA,EAAI,OAAO,UAAA,CAAW,EAAE,CAAA;AAAA,EAC9B;AAGA,EAAA,MAAM,MAAA,GAAS,SAAA,CAAU,CAAA,EAAG,WAAW,CAAA;AACvC,EAAA,IAAI,MAAA,EAAQ,OAAO,UAAA,CAAW,MAAM,CAAA;AAGpC,EAAA,MAAM,QAAA,GAAW,CAAA,CAAE,MAAA,EAAQ,aAAA,IAAiB,EAAE,UAAA,EAAY,aAAA;AAC1D,EAAA,IAAI,QAAA,EAAU,OAAO,UAAA,CAAW,QAAQ,CAAA;AAExC,EAAA,OAAO,SAAA;AACT;AAOO,SAAS,YAAY,EAAA,EAAqB;AAE/C,EAAA,MAAM,UAAA,GAAa,GAAG,UAAA,CAAW,SAAS,IAAI,EAAA,CAAG,KAAA,CAAM,CAAC,CAAA,GAAI,EAAA;AAG5D,EAAA,IAAI,QAAA,CAAS,IAAA,CAAK,UAAU,CAAA,EAAG,OAAO,IAAA;AACtC,EAAA,IAAI,OAAA,CAAQ,IAAA,CAAK,UAAU,CAAA,EAAG,OAAO,IAAA;AACrC,EAAA,IAAI,4BAAA,CAA6B,IAAA,CAAK,UAAU,CAAA,EAAG,OAAO,IAAA;AAC1D,EAAA,IAAI,aAAA,CAAc,IAAA,CAAK,UAAU,CAAA,EAAG,OAAO,IAAA;AAC3C,EAAA,IAAI,aAAA,CAAc,IAAA,CAAK,UAAU,CAAA,EAAG,OAAO,IAAA;AAC3C,EAAA,IAAI,MAAA,CAAO,IAAA,CAAK,UAAU,CAAA,EAAG,OAAO,IAAA;AAGpC,EAAA,IAAI,EAAA,KAAO,OAAO,OAAO,IAAA;AACzB,EAAA,IAAI,SAAA,CAAU,IAAA,CAAK,EAAE,CAAA,EAAG,OAAO,IAAA;AAC/B,EAAA,IAAI,SAAA,CAAU,IAAA,CAAK,EAAE,CAAA,EAAG,OAAO,IAAA;AAC/B,EAAA,IAAI,MAAA,CAAO,IAAA,CAAK,EAAE,CAAA,EAAG,OAAO,IAAA;AAE5B,EAAA,OAAO,KAAA;AACT;AC/KA,SAASC,UAAAA,CAAU,KAAkB,IAAA,EAAsB;AACzD,EAAA,MAAM,GAAA,GAAM,GAAA,CAAI,OAAA,CAAQ,IAAI,CAAA;AAC5B,EAAA,IAAI,MAAM,OAAA,CAAQ,GAAG,GAAG,OAAO,GAAA,CAAI,CAAC,CAAA,IAAK,EAAA;AACzC,EAAA,OAAO,GAAA,IAAO,EAAA;AAChB;AAyBO,SAAS,WAAA,CAAY,GAAA,EAAkB,OAAA,GAA8B,EAAC,EAAW;AACtF,EAAA,MAAM;AAAA,IACJ,EAAA,GAAK,IAAA;AAAA,IACL,SAAA,GAAY,IAAA;AAAA,IACZ,MAAA,GAAS,IAAA;AAAA,IACT,cAAA,GAAiB,IAAA;AAAA,IACjB,cAAA,GAAiB,IAAA;AAAA,IACjB,SAAS,EAAC;AAAA,IACV;AAAA,GACF,GAAI,OAAA;AAEJ,EAAA,MAAM,aAAuB,EAAC;AAE9B,EAAA,IAAI,EAAA,EAAI;AACN,IAAA,UAAA,CAAW,KAAK,CAAA,GAAA,EAAM,cAAA,CAAe,GAAA,EAAK,SAAS,CAAC,CAAA,CAAE,CAAA;AAAA,EACxD;AACA,EAAA,IAAI,SAAA,EAAW;AACb,IAAA,UAAA,CAAW,KAAK,CAAA,GAAA,EAAMA,UAAAA,CAAU,GAAA,EAAK,YAAY,CAAC,CAAA,CAAE,CAAA;AAAA,EACtD;AACA,EAAA,IAAI,MAAA,EAAQ;AACV,IAAA,UAAA,CAAW,KAAK,CAAA,OAAA,EAAUA,UAAAA,CAAU,GAAA,EAAK,QAAQ,CAAC,CAAA,CAAE,CAAA;AAAA,EACtD;AACA,EAAA,IAAI,cAAA,EAAgB;AAClB,IAAA,UAAA,CAAW,KAAK,CAAA,KAAA,EAAQA,UAAAA,CAAU,GAAA,EAAK,iBAAiB,CAAC,CAAA,CAAE,CAAA;AAAA,EAC7D;AACA,EAAA,IAAI,cAAA,EAAgB;AAClB,IAAA,UAAA,CAAW,KAAK,CAAA,IAAA,EAAOA,UAAAA,CAAU,GAAA,EAAK,iBAAiB,CAAC,CAAA,CAAE,CAAA;AAAA,EAC5D;AAEA,EAAA,KAAA,MAAW,KAAK,MAAA,EAAQ;AACtB,IAAA,IAAI,CAAA,KAAM,QAAQ,CAAA,KAAM,MAAA,aAAsB,IAAA,CAAK,CAAA,OAAA,EAAU,CAAC,CAAA,CAAE,CAAA;AAAA,EAClE;AAGA,EAAA,UAAA,CAAW,IAAA,EAAK;AAEhB,EAAA,MAAM,IAAA,GAAOC,kBAAW,QAAQ,CAAA;AAChC,EAAA,IAAA,CAAK,MAAA,CAAO,UAAA,CAAW,IAAA,CAAK,GAAG,CAAC,CAAA;AAChC,EAAA,OAAO,IAAA,CAAK,OAAO,KAAK,CAAA;AAC1B","file":"index.js","sourcesContent":["/**\n * @module @arcis/node/utils/duration\n * Parse human-readable duration strings into milliseconds.\n *\n * Supports: ms, s, m, h, d\n *\n * @example\n * parseDuration('5m') // 300000\n * parseDuration('2h') // 7200000\n * parseDuration(60000) // 60000 (passthrough)\n * parseDuration('500ms') // 500\n */\n\n/** Maximum duration: ~49.7 days (uint32 max in ms) */\nconst MAX_DURATION_MS = 4_294_967_295;\n\nconst DURATION_REGEX = /^(\\d+(?:\\.\\d+)?)\\s*(ms|s|m|h|d)$/i;\n\nconst UNIT_TO_MS: Record<string, number> = {\n ms: 1,\n s: 1_000,\n m: 60_000,\n h: 3_600_000,\n d: 86_400_000,\n};\n\n/**\n * Parse a duration string or number into milliseconds.\n *\n * @param value - Duration string (e.g. \"5m\", \"2h\", \"30s\") or number (ms)\n * @returns Duration in milliseconds\n * @throws {Error} If the value is not a valid duration\n *\n * @example\n * parseDuration('15m') // 900000\n * parseDuration('1d') // 86400000\n * parseDuration('500ms') // 500\n * parseDuration(60000) // 60000\n */\nexport function parseDuration(value: string | number): number {\n if (typeof value === 'number') {\n if (!Number.isFinite(value) || value < 0) {\n throw new Error(`Invalid duration: ${value}. Must be a non-negative finite number.`);\n }\n return Math.min(Math.floor(value), MAX_DURATION_MS);\n }\n\n if (typeof value !== 'string' || value.trim() === '') {\n throw new Error(`Invalid duration: \"${value}\". Expected a duration string (e.g. \"5m\", \"2h\") or number.`);\n }\n\n const match = value.trim().match(DURATION_REGEX);\n if (!match) {\n throw new Error(\n `Invalid duration: \"${value}\". Expected format: <number><unit> where unit is ms, s, m, h, or d.`\n );\n }\n\n const amount = parseFloat(match[1]);\n const unit = match[2].toLowerCase();\n const ms = Math.floor(amount * UNIT_TO_MS[unit]);\n\n if (ms < 0 || ms > MAX_DURATION_MS) {\n throw new Error(`Duration \"${value}\" exceeds maximum allowed (${MAX_DURATION_MS}ms / ~49.7 days).`);\n }\n\n return ms;\n}\n\n/**\n * Format milliseconds into a human-readable duration string.\n *\n * @param ms - Duration in milliseconds\n * @returns Human-readable string (e.g. \"5m\", \"2h 30m\")\n */\nexport function formatDuration(ms: number): string {\n if (!Number.isFinite(ms) || ms < 0) return '0ms';\n\n if (ms < 1000) return `${ms}ms`;\n\n const days = Math.floor(ms / 86_400_000);\n const hours = Math.floor((ms % 86_400_000) / 3_600_000);\n const minutes = Math.floor((ms % 3_600_000) / 60_000);\n const seconds = Math.floor((ms % 60_000) / 1_000);\n\n const parts: string[] = [];\n if (days > 0) parts.push(`${days}d`);\n if (hours > 0) parts.push(`${hours}h`);\n if (minutes > 0) parts.push(`${minutes}m`);\n if (seconds > 0) parts.push(`${seconds}s`);\n\n return parts.join(' ') || '0ms';\n}\n","/**\n * @module @arcis/node/utils/ip\n * Platform-aware client IP detection.\n *\n * Prevents IP spoofing by reading platform-specific headers\n * instead of blindly trusting X-Forwarded-For.\n *\n * @example\n * // Auto-detect platform from environment\n * const ip = detectClientIp(req);\n *\n * // Explicit platform\n * const ip = detectClientIp(req, { platform: 'cloudflare' });\n */\n\nimport type { IncomingMessage } from 'http';\n\nexport type Platform =\n | 'auto'\n | 'cloudflare'\n | 'vercel'\n | 'flyio'\n | 'render'\n | 'firebase'\n | 'aws-alb'\n | 'generic';\n\nexport interface DetectIpOptions {\n /** Platform to use for header selection. Default: 'auto' */\n platform?: Platform;\n /** Number of trusted proxies (for X-Forwarded-For parsing). Default: 1 */\n trustedProxyCount?: number;\n}\n\ninterface RequestLike {\n headers: Record<string, string | string[] | undefined>;\n socket?: { remoteAddress?: string };\n connection?: { remoteAddress?: string };\n ip?: string;\n}\n\n/**\n * Platform-specific header configurations.\n * Each platform sets a trusted header that cannot be spoofed by the client.\n */\nconst PLATFORM_HEADERS: Record<Exclude<Platform, 'auto' | 'generic'>, string> = {\n cloudflare: 'cf-connecting-ip',\n vercel: 'x-real-ip',\n flyio: 'fly-client-ip',\n render: 'x-render-client-ip',\n firebase: 'x-appengine-user-ip',\n 'aws-alb': 'x-forwarded-for',\n};\n\n/**\n * Auto-detect the platform from environment variables.\n */\nfunction detectPlatform(): Platform {\n const env = typeof process !== 'undefined' ? process.env : {};\n\n if (env.CF_PAGES || env.CF_WORKERS) return 'cloudflare';\n if (env.VERCEL) return 'vercel';\n if (env.FLY_APP_NAME) return 'flyio';\n if (env.RENDER) return 'render';\n if (env.FIREBASE_CONFIG || env.GCLOUD_PROJECT) return 'firebase';\n if (env.AWS_EXECUTION_ENV || env.AWS_LAMBDA_FUNCTION_NAME) return 'aws-alb';\n\n return 'generic';\n}\n\n// Cache the detected platform — it won't change during process lifetime\nlet _cachedPlatform: Platform | null = null;\n\nfunction getCachedPlatform(): Platform {\n if (_cachedPlatform === null) {\n _cachedPlatform = detectPlatform();\n }\n return _cachedPlatform;\n}\n\n/** Max IP string length (IPv6 max = 45 chars) */\nconst MAX_IP_LENGTH = 45;\n\n/**\n * Sanitize an IP string: trim, truncate, strip control characters.\n * Prevents unbounded strings from being used as map keys.\n */\nfunction sanitizeIp(ip: string): string {\n const trimmed = ip.trim();\n if (trimmed.length > MAX_IP_LENGTH) return trimmed.slice(0, MAX_IP_LENGTH);\n return trimmed;\n}\n\n/**\n * Get a header value from the request, handling string arrays.\n */\nfunction getHeader(req: RequestLike, name: string): string | undefined {\n const val = req.headers[name];\n if (Array.isArray(val)) return val[0];\n return val;\n}\n\n/**\n * Parse the rightmost trusted IP from X-Forwarded-For.\n * Reading from the right prevents client spoofing — the rightmost entry\n * is the one added by the closest trusted proxy.\n */\nfunction parseForwardedFor(header: string, trustedProxyCount: number): string | undefined {\n const ips = header.split(',').map(ip => ip.trim()).filter(Boolean);\n if (ips.length === 0) return undefined;\n\n // The client IP is at position (length - trustedProxyCount)\n const clientIndex = Math.max(0, ips.length - trustedProxyCount);\n return ips[clientIndex] || undefined;\n}\n\n/**\n * Detect the real client IP address from a request.\n *\n * Uses platform-specific headers when available to prevent IP spoofing.\n * Falls back to X-Forwarded-For (parsed from the right) and then\n * the socket remote address.\n *\n * @param req - HTTP request object (Express, raw http, etc.)\n * @param options - Detection options\n * @returns Client IP address, or 'unknown' if unresolvable\n *\n * @example\n * // Auto-detect platform\n * app.use((req, res, next) => {\n * const clientIp = detectClientIp(req);\n * console.log('Client IP:', clientIp);\n * next();\n * });\n *\n * @example\n * // Behind Cloudflare\n * const ip = detectClientIp(req, { platform: 'cloudflare' });\n *\n * @example\n * // Behind 2 proxies (e.g. CDN + load balancer)\n * const ip = detectClientIp(req, { trustedProxyCount: 2 });\n */\nexport function detectClientIp(\n req: RequestLike | IncomingMessage,\n options: DetectIpOptions = {}\n): string {\n const { platform = 'auto', trustedProxyCount = 1 } = options;\n const r = req as RequestLike;\n\n const resolvedPlatform = platform === 'auto' ? getCachedPlatform() : platform;\n\n // 1. Try platform-specific header (most trusted)\n if (resolvedPlatform !== 'generic' && resolvedPlatform in PLATFORM_HEADERS) {\n const headerName = PLATFORM_HEADERS[resolvedPlatform as keyof typeof PLATFORM_HEADERS];\n if (headerName) {\n if (resolvedPlatform === 'aws-alb') {\n // AWS ALB: parse X-Forwarded-For from the right\n const xff = getHeader(r, 'x-forwarded-for');\n if (xff) {\n const ip = parseForwardedFor(xff, trustedProxyCount);\n if (ip) return sanitizeIp(ip);\n }\n } else {\n const ip = getHeader(r, headerName);\n if (ip) return sanitizeIp(ip);\n }\n }\n }\n\n // 2. Try Express req.ip (respects trust proxy setting)\n if (r.ip) return sanitizeIp(r.ip);\n\n // 3. Try X-Forwarded-For (parsed from the right for safety)\n const xff = getHeader(r, 'x-forwarded-for');\n if (xff) {\n const ip = parseForwardedFor(xff, trustedProxyCount);\n if (ip) return sanitizeIp(ip);\n }\n\n // 4. Try X-Real-IP\n const realIp = getHeader(r, 'x-real-ip');\n if (realIp) return sanitizeIp(realIp);\n\n // 5. Socket remote address\n const socketIp = r.socket?.remoteAddress ?? r.connection?.remoteAddress;\n if (socketIp) return sanitizeIp(socketIp);\n\n return 'unknown';\n}\n\n/**\n * Check if an IP address is a private/internal address.\n *\n * Detects: loopback, private ranges (RFC 1918), link-local, IPv6 equivalents.\n */\nexport function isPrivateIp(ip: string): boolean {\n // Strip IPv4-mapped IPv6 prefix (::ffff:127.0.0.1 -> 127.0.0.1)\n const normalized = ip.startsWith('::ffff:') ? ip.slice(7) : ip;\n\n // IPv4 private ranges\n if (/^127\\./.test(normalized)) return true; // Loopback\n if (/^10\\./.test(normalized)) return true; // Class A private\n if (/^172\\.(1[6-9]|2\\d|3[01])\\./.test(normalized)) return true; // Class B private\n if (/^192\\.168\\./.test(normalized)) return true; // Class C private\n if (/^169\\.254\\./.test(normalized)) return true; // Link-local\n if (/^0\\./.test(normalized)) return true; // Current network\n\n // IPv6\n if (ip === '::1') return true; // Loopback\n if (/^fe80:/i.test(ip)) return true; // Link-local\n if (/^fc00:/i.test(ip)) return true; // Unique local\n if (/^fd/i.test(ip)) return true; // Unique local\n\n return false;\n}\n\n/** Reset cached platform (for testing). */\nexport function _resetPlatformCache(): void {\n _cachedPlatform = null;\n}\n","/**\n * @module @arcis/node/utils/fingerprint\n * Deterministic request fingerprinting via SHA-256.\n *\n * Generates a stable hash from request characteristics for\n * rate limiting keys, abuse detection, and analytics.\n *\n * @example\n * const fp = await fingerprint(req);\n * // \"a3f2b8c1d4e5...\"\n */\n\nimport { createHash } from 'crypto';\nimport { detectClientIp } from './ip';\nimport type { DetectIpOptions } from './ip';\n\nexport interface FingerprintOptions {\n /** Include IP address in fingerprint. Default: true */\n ip?: boolean;\n /** Include User-Agent header. Default: true */\n userAgent?: boolean;\n /** Include Accept header. Default: true */\n accept?: boolean;\n /** Include Accept-Language header. Default: true */\n acceptLanguage?: boolean;\n /** Include Accept-Encoding header. Default: true */\n acceptEncoding?: boolean;\n /** Additional custom components to include */\n custom?: string[];\n /** IP detection options */\n ipOptions?: DetectIpOptions;\n}\n\ninterface RequestLike {\n headers: Record<string, string | string[] | undefined>;\n socket?: { remoteAddress?: string };\n connection?: { remoteAddress?: string };\n ip?: string;\n}\n\nfunction getHeader(req: RequestLike, name: string): string {\n const val = req.headers[name];\n if (Array.isArray(val)) return val[0] ?? '';\n return val ?? '';\n}\n\n/**\n * Generate a deterministic fingerprint for a request.\n *\n * Creates a SHA-256 hash from configurable request components.\n * The fingerprint is stable across requests from the same client\n * (same IP, browser, language settings).\n *\n * @param req - HTTP request object\n * @param options - Fingerprint configuration\n * @returns Hex-encoded SHA-256 hash (64 characters)\n *\n * @example\n * // Default fingerprint (IP + UA + Accept headers)\n * const fp = fingerprint(req);\n *\n * @example\n * // IP-only fingerprint (for simple rate limiting)\n * const fp = fingerprint(req, { userAgent: false, accept: false, acceptLanguage: false, acceptEncoding: false });\n *\n * @example\n * // With custom components\n * const fp = fingerprint(req, { custom: [req.body?.userId] });\n */\nexport function fingerprint(req: RequestLike, options: FingerprintOptions = {}): string {\n const {\n ip = true,\n userAgent = true,\n accept = true,\n acceptLanguage = true,\n acceptEncoding = true,\n custom = [],\n ipOptions,\n } = options;\n\n const components: string[] = [];\n\n if (ip) {\n components.push(`ip:${detectClientIp(req, ipOptions)}`);\n }\n if (userAgent) {\n components.push(`ua:${getHeader(req, 'user-agent')}`);\n }\n if (accept) {\n components.push(`accept:${getHeader(req, 'accept')}`);\n }\n if (acceptLanguage) {\n components.push(`lang:${getHeader(req, 'accept-language')}`);\n }\n if (acceptEncoding) {\n components.push(`enc:${getHeader(req, 'accept-encoding')}`);\n }\n\n for (const c of custom) {\n if (c !== null && c !== undefined) components.push(`custom:${c}`);\n }\n\n // Sort for deterministic ordering\n components.sort();\n\n const hash = createHash('sha256');\n hash.update(components.join('|'));\n return hash.digest('hex');\n}\n"]}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { createHash } from 'crypto';
|
|
2
|
+
|
|
3
|
+
// src/utils/duration.ts
|
|
4
|
+
var MAX_DURATION_MS = 4294967295;
|
|
5
|
+
var DURATION_REGEX = /^(\d+(?:\.\d+)?)\s*(ms|s|m|h|d)$/i;
|
|
6
|
+
var UNIT_TO_MS = {
|
|
7
|
+
ms: 1,
|
|
8
|
+
s: 1e3,
|
|
9
|
+
m: 6e4,
|
|
10
|
+
h: 36e5,
|
|
11
|
+
d: 864e5
|
|
12
|
+
};
|
|
13
|
+
function parseDuration(value) {
|
|
14
|
+
if (typeof value === "number") {
|
|
15
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
16
|
+
throw new Error(`Invalid duration: ${value}. Must be a non-negative finite number.`);
|
|
17
|
+
}
|
|
18
|
+
return Math.min(Math.floor(value), MAX_DURATION_MS);
|
|
19
|
+
}
|
|
20
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
21
|
+
throw new Error(`Invalid duration: "${value}". Expected a duration string (e.g. "5m", "2h") or number.`);
|
|
22
|
+
}
|
|
23
|
+
const match = value.trim().match(DURATION_REGEX);
|
|
24
|
+
if (!match) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
`Invalid duration: "${value}". Expected format: <number><unit> where unit is ms, s, m, h, or d.`
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
const amount = parseFloat(match[1]);
|
|
30
|
+
const unit = match[2].toLowerCase();
|
|
31
|
+
const ms = Math.floor(amount * UNIT_TO_MS[unit]);
|
|
32
|
+
if (ms < 0 || ms > MAX_DURATION_MS) {
|
|
33
|
+
throw new Error(`Duration "${value}" exceeds maximum allowed (${MAX_DURATION_MS}ms / ~49.7 days).`);
|
|
34
|
+
}
|
|
35
|
+
return ms;
|
|
36
|
+
}
|
|
37
|
+
function formatDuration(ms) {
|
|
38
|
+
if (!Number.isFinite(ms) || ms < 0) return "0ms";
|
|
39
|
+
if (ms < 1e3) return `${ms}ms`;
|
|
40
|
+
const days = Math.floor(ms / 864e5);
|
|
41
|
+
const hours = Math.floor(ms % 864e5 / 36e5);
|
|
42
|
+
const minutes = Math.floor(ms % 36e5 / 6e4);
|
|
43
|
+
const seconds = Math.floor(ms % 6e4 / 1e3);
|
|
44
|
+
const parts = [];
|
|
45
|
+
if (days > 0) parts.push(`${days}d`);
|
|
46
|
+
if (hours > 0) parts.push(`${hours}h`);
|
|
47
|
+
if (minutes > 0) parts.push(`${minutes}m`);
|
|
48
|
+
if (seconds > 0) parts.push(`${seconds}s`);
|
|
49
|
+
return parts.join(" ") || "0ms";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// src/utils/ip.ts
|
|
53
|
+
var PLATFORM_HEADERS = {
|
|
54
|
+
cloudflare: "cf-connecting-ip",
|
|
55
|
+
vercel: "x-real-ip",
|
|
56
|
+
flyio: "fly-client-ip",
|
|
57
|
+
render: "x-render-client-ip",
|
|
58
|
+
firebase: "x-appengine-user-ip",
|
|
59
|
+
"aws-alb": "x-forwarded-for"
|
|
60
|
+
};
|
|
61
|
+
function detectPlatform() {
|
|
62
|
+
const env = typeof process !== "undefined" ? process.env : {};
|
|
63
|
+
if (env.CF_PAGES || env.CF_WORKERS) return "cloudflare";
|
|
64
|
+
if (env.VERCEL) return "vercel";
|
|
65
|
+
if (env.FLY_APP_NAME) return "flyio";
|
|
66
|
+
if (env.RENDER) return "render";
|
|
67
|
+
if (env.FIREBASE_CONFIG || env.GCLOUD_PROJECT) return "firebase";
|
|
68
|
+
if (env.AWS_EXECUTION_ENV || env.AWS_LAMBDA_FUNCTION_NAME) return "aws-alb";
|
|
69
|
+
return "generic";
|
|
70
|
+
}
|
|
71
|
+
var _cachedPlatform = null;
|
|
72
|
+
function getCachedPlatform() {
|
|
73
|
+
if (_cachedPlatform === null) {
|
|
74
|
+
_cachedPlatform = detectPlatform();
|
|
75
|
+
}
|
|
76
|
+
return _cachedPlatform;
|
|
77
|
+
}
|
|
78
|
+
var MAX_IP_LENGTH = 45;
|
|
79
|
+
function sanitizeIp(ip) {
|
|
80
|
+
const trimmed = ip.trim();
|
|
81
|
+
if (trimmed.length > MAX_IP_LENGTH) return trimmed.slice(0, MAX_IP_LENGTH);
|
|
82
|
+
return trimmed;
|
|
83
|
+
}
|
|
84
|
+
function getHeader(req, name) {
|
|
85
|
+
const val = req.headers[name];
|
|
86
|
+
if (Array.isArray(val)) return val[0];
|
|
87
|
+
return val;
|
|
88
|
+
}
|
|
89
|
+
function parseForwardedFor(header, trustedProxyCount) {
|
|
90
|
+
const ips = header.split(",").map((ip) => ip.trim()).filter(Boolean);
|
|
91
|
+
if (ips.length === 0) return void 0;
|
|
92
|
+
const clientIndex = Math.max(0, ips.length - trustedProxyCount);
|
|
93
|
+
return ips[clientIndex] || void 0;
|
|
94
|
+
}
|
|
95
|
+
function detectClientIp(req, options = {}) {
|
|
96
|
+
const { platform = "auto", trustedProxyCount = 1 } = options;
|
|
97
|
+
const r = req;
|
|
98
|
+
const resolvedPlatform = platform === "auto" ? getCachedPlatform() : platform;
|
|
99
|
+
if (resolvedPlatform !== "generic" && resolvedPlatform in PLATFORM_HEADERS) {
|
|
100
|
+
const headerName = PLATFORM_HEADERS[resolvedPlatform];
|
|
101
|
+
if (headerName) {
|
|
102
|
+
if (resolvedPlatform === "aws-alb") {
|
|
103
|
+
const xff2 = getHeader(r, "x-forwarded-for");
|
|
104
|
+
if (xff2) {
|
|
105
|
+
const ip = parseForwardedFor(xff2, trustedProxyCount);
|
|
106
|
+
if (ip) return sanitizeIp(ip);
|
|
107
|
+
}
|
|
108
|
+
} else {
|
|
109
|
+
const ip = getHeader(r, headerName);
|
|
110
|
+
if (ip) return sanitizeIp(ip);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (r.ip) return sanitizeIp(r.ip);
|
|
115
|
+
const xff = getHeader(r, "x-forwarded-for");
|
|
116
|
+
if (xff) {
|
|
117
|
+
const ip = parseForwardedFor(xff, trustedProxyCount);
|
|
118
|
+
if (ip) return sanitizeIp(ip);
|
|
119
|
+
}
|
|
120
|
+
const realIp = getHeader(r, "x-real-ip");
|
|
121
|
+
if (realIp) return sanitizeIp(realIp);
|
|
122
|
+
const socketIp = r.socket?.remoteAddress ?? r.connection?.remoteAddress;
|
|
123
|
+
if (socketIp) return sanitizeIp(socketIp);
|
|
124
|
+
return "unknown";
|
|
125
|
+
}
|
|
126
|
+
function isPrivateIp(ip) {
|
|
127
|
+
const normalized = ip.startsWith("::ffff:") ? ip.slice(7) : ip;
|
|
128
|
+
if (/^127\./.test(normalized)) return true;
|
|
129
|
+
if (/^10\./.test(normalized)) return true;
|
|
130
|
+
if (/^172\.(1[6-9]|2\d|3[01])\./.test(normalized)) return true;
|
|
131
|
+
if (/^192\.168\./.test(normalized)) return true;
|
|
132
|
+
if (/^169\.254\./.test(normalized)) return true;
|
|
133
|
+
if (/^0\./.test(normalized)) return true;
|
|
134
|
+
if (ip === "::1") return true;
|
|
135
|
+
if (/^fe80:/i.test(ip)) return true;
|
|
136
|
+
if (/^fc00:/i.test(ip)) return true;
|
|
137
|
+
if (/^fd/i.test(ip)) return true;
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
function getHeader2(req, name) {
|
|
141
|
+
const val = req.headers[name];
|
|
142
|
+
if (Array.isArray(val)) return val[0] ?? "";
|
|
143
|
+
return val ?? "";
|
|
144
|
+
}
|
|
145
|
+
function fingerprint(req, options = {}) {
|
|
146
|
+
const {
|
|
147
|
+
ip = true,
|
|
148
|
+
userAgent = true,
|
|
149
|
+
accept = true,
|
|
150
|
+
acceptLanguage = true,
|
|
151
|
+
acceptEncoding = true,
|
|
152
|
+
custom = [],
|
|
153
|
+
ipOptions
|
|
154
|
+
} = options;
|
|
155
|
+
const components = [];
|
|
156
|
+
if (ip) {
|
|
157
|
+
components.push(`ip:${detectClientIp(req, ipOptions)}`);
|
|
158
|
+
}
|
|
159
|
+
if (userAgent) {
|
|
160
|
+
components.push(`ua:${getHeader2(req, "user-agent")}`);
|
|
161
|
+
}
|
|
162
|
+
if (accept) {
|
|
163
|
+
components.push(`accept:${getHeader2(req, "accept")}`);
|
|
164
|
+
}
|
|
165
|
+
if (acceptLanguage) {
|
|
166
|
+
components.push(`lang:${getHeader2(req, "accept-language")}`);
|
|
167
|
+
}
|
|
168
|
+
if (acceptEncoding) {
|
|
169
|
+
components.push(`enc:${getHeader2(req, "accept-encoding")}`);
|
|
170
|
+
}
|
|
171
|
+
for (const c of custom) {
|
|
172
|
+
if (c !== null && c !== void 0) components.push(`custom:${c}`);
|
|
173
|
+
}
|
|
174
|
+
components.sort();
|
|
175
|
+
const hash = createHash("sha256");
|
|
176
|
+
hash.update(components.join("|"));
|
|
177
|
+
return hash.digest("hex");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export { detectClientIp, fingerprint, formatDuration, isPrivateIp, parseDuration };
|
|
181
|
+
//# sourceMappingURL=index.mjs.map
|
|
182
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/utils/duration.ts","../../src/utils/ip.ts","../../src/utils/fingerprint.ts"],"names":["xff","getHeader"],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * @module @arcis/node/utils/duration\n * Parse human-readable duration strings into milliseconds.\n *\n * Supports: ms, s, m, h, d\n *\n * @example\n * parseDuration('5m') // 300000\n * parseDuration('2h') // 7200000\n * parseDuration(60000) // 60000 (passthrough)\n * parseDuration('500ms') // 500\n */\n\n/** Maximum duration: ~49.7 days (uint32 max in ms) */\nconst MAX_DURATION_MS = 4_294_967_295;\n\nconst DURATION_REGEX = /^(\\d+(?:\\.\\d+)?)\\s*(ms|s|m|h|d)$/i;\n\nconst UNIT_TO_MS: Record<string, number> = {\n ms: 1,\n s: 1_000,\n m: 60_000,\n h: 3_600_000,\n d: 86_400_000,\n};\n\n/**\n * Parse a duration string or number into milliseconds.\n *\n * @param value - Duration string (e.g. \"5m\", \"2h\", \"30s\") or number (ms)\n * @returns Duration in milliseconds\n * @throws {Error} If the value is not a valid duration\n *\n * @example\n * parseDuration('15m') // 900000\n * parseDuration('1d') // 86400000\n * parseDuration('500ms') // 500\n * parseDuration(60000) // 60000\n */\nexport function parseDuration(value: string | number): number {\n if (typeof value === 'number') {\n if (!Number.isFinite(value) || value < 0) {\n throw new Error(`Invalid duration: ${value}. Must be a non-negative finite number.`);\n }\n return Math.min(Math.floor(value), MAX_DURATION_MS);\n }\n\n if (typeof value !== 'string' || value.trim() === '') {\n throw new Error(`Invalid duration: \"${value}\". Expected a duration string (e.g. \"5m\", \"2h\") or number.`);\n }\n\n const match = value.trim().match(DURATION_REGEX);\n if (!match) {\n throw new Error(\n `Invalid duration: \"${value}\". Expected format: <number><unit> where unit is ms, s, m, h, or d.`\n );\n }\n\n const amount = parseFloat(match[1]);\n const unit = match[2].toLowerCase();\n const ms = Math.floor(amount * UNIT_TO_MS[unit]);\n\n if (ms < 0 || ms > MAX_DURATION_MS) {\n throw new Error(`Duration \"${value}\" exceeds maximum allowed (${MAX_DURATION_MS}ms / ~49.7 days).`);\n }\n\n return ms;\n}\n\n/**\n * Format milliseconds into a human-readable duration string.\n *\n * @param ms - Duration in milliseconds\n * @returns Human-readable string (e.g. \"5m\", \"2h 30m\")\n */\nexport function formatDuration(ms: number): string {\n if (!Number.isFinite(ms) || ms < 0) return '0ms';\n\n if (ms < 1000) return `${ms}ms`;\n\n const days = Math.floor(ms / 86_400_000);\n const hours = Math.floor((ms % 86_400_000) / 3_600_000);\n const minutes = Math.floor((ms % 3_600_000) / 60_000);\n const seconds = Math.floor((ms % 60_000) / 1_000);\n\n const parts: string[] = [];\n if (days > 0) parts.push(`${days}d`);\n if (hours > 0) parts.push(`${hours}h`);\n if (minutes > 0) parts.push(`${minutes}m`);\n if (seconds > 0) parts.push(`${seconds}s`);\n\n return parts.join(' ') || '0ms';\n}\n","/**\n * @module @arcis/node/utils/ip\n * Platform-aware client IP detection.\n *\n * Prevents IP spoofing by reading platform-specific headers\n * instead of blindly trusting X-Forwarded-For.\n *\n * @example\n * // Auto-detect platform from environment\n * const ip = detectClientIp(req);\n *\n * // Explicit platform\n * const ip = detectClientIp(req, { platform: 'cloudflare' });\n */\n\nimport type { IncomingMessage } from 'http';\n\nexport type Platform =\n | 'auto'\n | 'cloudflare'\n | 'vercel'\n | 'flyio'\n | 'render'\n | 'firebase'\n | 'aws-alb'\n | 'generic';\n\nexport interface DetectIpOptions {\n /** Platform to use for header selection. Default: 'auto' */\n platform?: Platform;\n /** Number of trusted proxies (for X-Forwarded-For parsing). Default: 1 */\n trustedProxyCount?: number;\n}\n\ninterface RequestLike {\n headers: Record<string, string | string[] | undefined>;\n socket?: { remoteAddress?: string };\n connection?: { remoteAddress?: string };\n ip?: string;\n}\n\n/**\n * Platform-specific header configurations.\n * Each platform sets a trusted header that cannot be spoofed by the client.\n */\nconst PLATFORM_HEADERS: Record<Exclude<Platform, 'auto' | 'generic'>, string> = {\n cloudflare: 'cf-connecting-ip',\n vercel: 'x-real-ip',\n flyio: 'fly-client-ip',\n render: 'x-render-client-ip',\n firebase: 'x-appengine-user-ip',\n 'aws-alb': 'x-forwarded-for',\n};\n\n/**\n * Auto-detect the platform from environment variables.\n */\nfunction detectPlatform(): Platform {\n const env = typeof process !== 'undefined' ? process.env : {};\n\n if (env.CF_PAGES || env.CF_WORKERS) return 'cloudflare';\n if (env.VERCEL) return 'vercel';\n if (env.FLY_APP_NAME) return 'flyio';\n if (env.RENDER) return 'render';\n if (env.FIREBASE_CONFIG || env.GCLOUD_PROJECT) return 'firebase';\n if (env.AWS_EXECUTION_ENV || env.AWS_LAMBDA_FUNCTION_NAME) return 'aws-alb';\n\n return 'generic';\n}\n\n// Cache the detected platform — it won't change during process lifetime\nlet _cachedPlatform: Platform | null = null;\n\nfunction getCachedPlatform(): Platform {\n if (_cachedPlatform === null) {\n _cachedPlatform = detectPlatform();\n }\n return _cachedPlatform;\n}\n\n/** Max IP string length (IPv6 max = 45 chars) */\nconst MAX_IP_LENGTH = 45;\n\n/**\n * Sanitize an IP string: trim, truncate, strip control characters.\n * Prevents unbounded strings from being used as map keys.\n */\nfunction sanitizeIp(ip: string): string {\n const trimmed = ip.trim();\n if (trimmed.length > MAX_IP_LENGTH) return trimmed.slice(0, MAX_IP_LENGTH);\n return trimmed;\n}\n\n/**\n * Get a header value from the request, handling string arrays.\n */\nfunction getHeader(req: RequestLike, name: string): string | undefined {\n const val = req.headers[name];\n if (Array.isArray(val)) return val[0];\n return val;\n}\n\n/**\n * Parse the rightmost trusted IP from X-Forwarded-For.\n * Reading from the right prevents client spoofing — the rightmost entry\n * is the one added by the closest trusted proxy.\n */\nfunction parseForwardedFor(header: string, trustedProxyCount: number): string | undefined {\n const ips = header.split(',').map(ip => ip.trim()).filter(Boolean);\n if (ips.length === 0) return undefined;\n\n // The client IP is at position (length - trustedProxyCount)\n const clientIndex = Math.max(0, ips.length - trustedProxyCount);\n return ips[clientIndex] || undefined;\n}\n\n/**\n * Detect the real client IP address from a request.\n *\n * Uses platform-specific headers when available to prevent IP spoofing.\n * Falls back to X-Forwarded-For (parsed from the right) and then\n * the socket remote address.\n *\n * @param req - HTTP request object (Express, raw http, etc.)\n * @param options - Detection options\n * @returns Client IP address, or 'unknown' if unresolvable\n *\n * @example\n * // Auto-detect platform\n * app.use((req, res, next) => {\n * const clientIp = detectClientIp(req);\n * console.log('Client IP:', clientIp);\n * next();\n * });\n *\n * @example\n * // Behind Cloudflare\n * const ip = detectClientIp(req, { platform: 'cloudflare' });\n *\n * @example\n * // Behind 2 proxies (e.g. CDN + load balancer)\n * const ip = detectClientIp(req, { trustedProxyCount: 2 });\n */\nexport function detectClientIp(\n req: RequestLike | IncomingMessage,\n options: DetectIpOptions = {}\n): string {\n const { platform = 'auto', trustedProxyCount = 1 } = options;\n const r = req as RequestLike;\n\n const resolvedPlatform = platform === 'auto' ? getCachedPlatform() : platform;\n\n // 1. Try platform-specific header (most trusted)\n if (resolvedPlatform !== 'generic' && resolvedPlatform in PLATFORM_HEADERS) {\n const headerName = PLATFORM_HEADERS[resolvedPlatform as keyof typeof PLATFORM_HEADERS];\n if (headerName) {\n if (resolvedPlatform === 'aws-alb') {\n // AWS ALB: parse X-Forwarded-For from the right\n const xff = getHeader(r, 'x-forwarded-for');\n if (xff) {\n const ip = parseForwardedFor(xff, trustedProxyCount);\n if (ip) return sanitizeIp(ip);\n }\n } else {\n const ip = getHeader(r, headerName);\n if (ip) return sanitizeIp(ip);\n }\n }\n }\n\n // 2. Try Express req.ip (respects trust proxy setting)\n if (r.ip) return sanitizeIp(r.ip);\n\n // 3. Try X-Forwarded-For (parsed from the right for safety)\n const xff = getHeader(r, 'x-forwarded-for');\n if (xff) {\n const ip = parseForwardedFor(xff, trustedProxyCount);\n if (ip) return sanitizeIp(ip);\n }\n\n // 4. Try X-Real-IP\n const realIp = getHeader(r, 'x-real-ip');\n if (realIp) return sanitizeIp(realIp);\n\n // 5. Socket remote address\n const socketIp = r.socket?.remoteAddress ?? r.connection?.remoteAddress;\n if (socketIp) return sanitizeIp(socketIp);\n\n return 'unknown';\n}\n\n/**\n * Check if an IP address is a private/internal address.\n *\n * Detects: loopback, private ranges (RFC 1918), link-local, IPv6 equivalents.\n */\nexport function isPrivateIp(ip: string): boolean {\n // Strip IPv4-mapped IPv6 prefix (::ffff:127.0.0.1 -> 127.0.0.1)\n const normalized = ip.startsWith('::ffff:') ? ip.slice(7) : ip;\n\n // IPv4 private ranges\n if (/^127\\./.test(normalized)) return true; // Loopback\n if (/^10\\./.test(normalized)) return true; // Class A private\n if (/^172\\.(1[6-9]|2\\d|3[01])\\./.test(normalized)) return true; // Class B private\n if (/^192\\.168\\./.test(normalized)) return true; // Class C private\n if (/^169\\.254\\./.test(normalized)) return true; // Link-local\n if (/^0\\./.test(normalized)) return true; // Current network\n\n // IPv6\n if (ip === '::1') return true; // Loopback\n if (/^fe80:/i.test(ip)) return true; // Link-local\n if (/^fc00:/i.test(ip)) return true; // Unique local\n if (/^fd/i.test(ip)) return true; // Unique local\n\n return false;\n}\n\n/** Reset cached platform (for testing). */\nexport function _resetPlatformCache(): void {\n _cachedPlatform = null;\n}\n","/**\n * @module @arcis/node/utils/fingerprint\n * Deterministic request fingerprinting via SHA-256.\n *\n * Generates a stable hash from request characteristics for\n * rate limiting keys, abuse detection, and analytics.\n *\n * @example\n * const fp = await fingerprint(req);\n * // \"a3f2b8c1d4e5...\"\n */\n\nimport { createHash } from 'crypto';\nimport { detectClientIp } from './ip';\nimport type { DetectIpOptions } from './ip';\n\nexport interface FingerprintOptions {\n /** Include IP address in fingerprint. Default: true */\n ip?: boolean;\n /** Include User-Agent header. Default: true */\n userAgent?: boolean;\n /** Include Accept header. Default: true */\n accept?: boolean;\n /** Include Accept-Language header. Default: true */\n acceptLanguage?: boolean;\n /** Include Accept-Encoding header. Default: true */\n acceptEncoding?: boolean;\n /** Additional custom components to include */\n custom?: string[];\n /** IP detection options */\n ipOptions?: DetectIpOptions;\n}\n\ninterface RequestLike {\n headers: Record<string, string | string[] | undefined>;\n socket?: { remoteAddress?: string };\n connection?: { remoteAddress?: string };\n ip?: string;\n}\n\nfunction getHeader(req: RequestLike, name: string): string {\n const val = req.headers[name];\n if (Array.isArray(val)) return val[0] ?? '';\n return val ?? '';\n}\n\n/**\n * Generate a deterministic fingerprint for a request.\n *\n * Creates a SHA-256 hash from configurable request components.\n * The fingerprint is stable across requests from the same client\n * (same IP, browser, language settings).\n *\n * @param req - HTTP request object\n * @param options - Fingerprint configuration\n * @returns Hex-encoded SHA-256 hash (64 characters)\n *\n * @example\n * // Default fingerprint (IP + UA + Accept headers)\n * const fp = fingerprint(req);\n *\n * @example\n * // IP-only fingerprint (for simple rate limiting)\n * const fp = fingerprint(req, { userAgent: false, accept: false, acceptLanguage: false, acceptEncoding: false });\n *\n * @example\n * // With custom components\n * const fp = fingerprint(req, { custom: [req.body?.userId] });\n */\nexport function fingerprint(req: RequestLike, options: FingerprintOptions = {}): string {\n const {\n ip = true,\n userAgent = true,\n accept = true,\n acceptLanguage = true,\n acceptEncoding = true,\n custom = [],\n ipOptions,\n } = options;\n\n const components: string[] = [];\n\n if (ip) {\n components.push(`ip:${detectClientIp(req, ipOptions)}`);\n }\n if (userAgent) {\n components.push(`ua:${getHeader(req, 'user-agent')}`);\n }\n if (accept) {\n components.push(`accept:${getHeader(req, 'accept')}`);\n }\n if (acceptLanguage) {\n components.push(`lang:${getHeader(req, 'accept-language')}`);\n }\n if (acceptEncoding) {\n components.push(`enc:${getHeader(req, 'accept-encoding')}`);\n }\n\n for (const c of custom) {\n if (c !== null && c !== undefined) components.push(`custom:${c}`);\n }\n\n // Sort for deterministic ordering\n components.sort();\n\n const hash = createHash('sha256');\n hash.update(components.join('|'));\n return hash.digest('hex');\n}\n"]}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @arcis/node/utils/ip
|
|
3
|
+
* Platform-aware client IP detection.
|
|
4
|
+
*
|
|
5
|
+
* Prevents IP spoofing by reading platform-specific headers
|
|
6
|
+
* instead of blindly trusting X-Forwarded-For.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* // Auto-detect platform from environment
|
|
10
|
+
* const ip = detectClientIp(req);
|
|
11
|
+
*
|
|
12
|
+
* // Explicit platform
|
|
13
|
+
* const ip = detectClientIp(req, { platform: 'cloudflare' });
|
|
14
|
+
*/
|
|
15
|
+
import type { IncomingMessage } from 'http';
|
|
16
|
+
export type Platform = 'auto' | 'cloudflare' | 'vercel' | 'flyio' | 'render' | 'firebase' | 'aws-alb' | 'generic';
|
|
17
|
+
export interface DetectIpOptions {
|
|
18
|
+
/** Platform to use for header selection. Default: 'auto' */
|
|
19
|
+
platform?: Platform;
|
|
20
|
+
/** Number of trusted proxies (for X-Forwarded-For parsing). Default: 1 */
|
|
21
|
+
trustedProxyCount?: number;
|
|
22
|
+
}
|
|
23
|
+
interface RequestLike {
|
|
24
|
+
headers: Record<string, string | string[] | undefined>;
|
|
25
|
+
socket?: {
|
|
26
|
+
remoteAddress?: string;
|
|
27
|
+
};
|
|
28
|
+
connection?: {
|
|
29
|
+
remoteAddress?: string;
|
|
30
|
+
};
|
|
31
|
+
ip?: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Detect the real client IP address from a request.
|
|
35
|
+
*
|
|
36
|
+
* Uses platform-specific headers when available to prevent IP spoofing.
|
|
37
|
+
* Falls back to X-Forwarded-For (parsed from the right) and then
|
|
38
|
+
* the socket remote address.
|
|
39
|
+
*
|
|
40
|
+
* @param req - HTTP request object (Express, raw http, etc.)
|
|
41
|
+
* @param options - Detection options
|
|
42
|
+
* @returns Client IP address, or 'unknown' if unresolvable
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* // Auto-detect platform
|
|
46
|
+
* app.use((req, res, next) => {
|
|
47
|
+
* const clientIp = detectClientIp(req);
|
|
48
|
+
* console.log('Client IP:', clientIp);
|
|
49
|
+
* next();
|
|
50
|
+
* });
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* // Behind Cloudflare
|
|
54
|
+
* const ip = detectClientIp(req, { platform: 'cloudflare' });
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* // Behind 2 proxies (e.g. CDN + load balancer)
|
|
58
|
+
* const ip = detectClientIp(req, { trustedProxyCount: 2 });
|
|
59
|
+
*/
|
|
60
|
+
export declare function detectClientIp(req: RequestLike | IncomingMessage, options?: DetectIpOptions): string;
|
|
61
|
+
/**
|
|
62
|
+
* Check if an IP address is a private/internal address.
|
|
63
|
+
*
|
|
64
|
+
* Detects: loopback, private ranges (RFC 1918), link-local, IPv6 equivalents.
|
|
65
|
+
*/
|
|
66
|
+
export declare function isPrivateIp(ip: string): boolean;
|
|
67
|
+
/** Reset cached platform (for testing). */
|
|
68
|
+
export declare function _resetPlatformCache(): void;
|
|
69
|
+
export {};
|
|
70
|
+
//# sourceMappingURL=ip.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ip.d.ts","sourceRoot":"","sources":["../../src/utils/ip.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,MAAM,CAAC;AAE5C,MAAM,MAAM,QAAQ,GAChB,MAAM,GACN,YAAY,GACZ,QAAQ,GACR,OAAO,GACP,QAAQ,GACR,UAAU,GACV,SAAS,GACT,SAAS,CAAC;AAEd,MAAM,WAAW,eAAe;IAC9B,4DAA4D;IAC5D,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,0EAA0E;IAC1E,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,UAAU,WAAW;IACnB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,CAAC;IACvD,MAAM,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACpC,UAAU,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACxC,EAAE,CAAC,EAAE,MAAM,CAAC;CACb;AA6ED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,cAAc,CAC5B,GAAG,EAAE,WAAW,GAAG,eAAe,EAClC,OAAO,GAAE,eAAoB,GAC5B,MAAM,CA2CR;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAmB/C;AAED,2CAA2C;AAC3C,wBAAgB,mBAAmB,IAAI,IAAI,CAE1C"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @arcis/node/validation/email
|
|
3
|
+
* Advanced email validation with disposable detection and typo suggestions.
|
|
4
|
+
*
|
|
5
|
+
* Three levels of validation:
|
|
6
|
+
* 1. Syntax — RFC-compliant format checking
|
|
7
|
+
* 2. Domain intelligence — disposable/free provider detection, typo correction
|
|
8
|
+
* 3. MX verification — DNS MX record lookup (async, optional)
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* const result = validateEmail('user@tempmail.com');
|
|
12
|
+
* // { valid: false, reason: 'disposable' }
|
|
13
|
+
*
|
|
14
|
+
* const result = validateEmail('user@gmial.com');
|
|
15
|
+
* // { valid: true, reason: 'typo', suggestion: 'user@gmail.com' }
|
|
16
|
+
*/
|
|
17
|
+
export interface EmailValidationOptions {
|
|
18
|
+
/** Check for disposable email providers. Default: true */
|
|
19
|
+
checkDisposable?: boolean;
|
|
20
|
+
/** Suggest corrections for typos. Default: true */
|
|
21
|
+
suggestTypoFix?: boolean;
|
|
22
|
+
/** Verify MX records via DNS. Default: false */
|
|
23
|
+
checkMx?: boolean;
|
|
24
|
+
/** Additional blocked domains */
|
|
25
|
+
blockedDomains?: string[];
|
|
26
|
+
/** Additional allowed domains (bypasses disposable check) */
|
|
27
|
+
allowedDomains?: string[];
|
|
28
|
+
}
|
|
29
|
+
export interface EmailValidationResult {
|
|
30
|
+
/** Whether the email is valid */
|
|
31
|
+
valid: boolean;
|
|
32
|
+
/** Reason for the result */
|
|
33
|
+
reason: 'valid' | 'invalid_syntax' | 'disposable' | 'no_mx' | 'blocked' | 'typo';
|
|
34
|
+
/** Suggested correction if a typo was detected */
|
|
35
|
+
suggestion: string | null;
|
|
36
|
+
/** Whether the domain is a free email provider */
|
|
37
|
+
isFree: boolean;
|
|
38
|
+
/** Whether the domain is a disposable email provider */
|
|
39
|
+
isDisposable: boolean;
|
|
40
|
+
/** The normalized email address */
|
|
41
|
+
normalized: string;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Validate an email address with syntax checking, disposable detection,
|
|
45
|
+
* and typo suggestions.
|
|
46
|
+
*
|
|
47
|
+
* @param email - Email address to validate
|
|
48
|
+
* @param options - Validation options
|
|
49
|
+
* @returns Validation result
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* validateEmail('user@gmail.com')
|
|
53
|
+
* // { valid: true, reason: 'valid', isFree: true }
|
|
54
|
+
*
|
|
55
|
+
* validateEmail('user@tempmail.com')
|
|
56
|
+
* // { valid: false, reason: 'disposable' }
|
|
57
|
+
*
|
|
58
|
+
* validateEmail('user@gmial.com')
|
|
59
|
+
* // { valid: true, reason: 'typo', suggestion: 'user@gmail.com' }
|
|
60
|
+
*/
|
|
61
|
+
export declare function validateEmail(email: string, options?: EmailValidationOptions): EmailValidationResult;
|
|
62
|
+
/**
|
|
63
|
+
* Verify that the email domain has MX records (can receive email).
|
|
64
|
+
*
|
|
65
|
+
* This performs a DNS lookup and requires network access.
|
|
66
|
+
* Use for registration flows where you need high confidence.
|
|
67
|
+
*
|
|
68
|
+
* @param email - Email address to verify
|
|
69
|
+
* @returns True if the domain has MX records
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* if (await verifyEmailMx('user@example.com')) {
|
|
73
|
+
* // Domain can receive email
|
|
74
|
+
* }
|
|
75
|
+
*/
|
|
76
|
+
export declare function verifyEmailMx(email: string): Promise<boolean>;
|
|
77
|
+
/**
|
|
78
|
+
* Quick check if an email address has valid syntax.
|
|
79
|
+
* Faster than validateEmail() — just syntax, no domain intelligence.
|
|
80
|
+
*/
|
|
81
|
+
export declare function isValidEmailSyntax(email: string): boolean;
|
|
82
|
+
//# sourceMappingURL=email.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"email.d.ts","sourceRoot":"","sources":["../../src/validation/email.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAIH,MAAM,WAAW,sBAAsB;IACrC,0DAA0D;IAC1D,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,mDAAmD;IACnD,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,gDAAgD;IAChD,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,iCAAiC;IACjC,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,6DAA6D;IAC7D,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,qBAAqB;IACpC,iCAAiC;IACjC,KAAK,EAAE,OAAO,CAAC;IACf,4BAA4B;IAC5B,MAAM,EAAE,OAAO,GAAG,gBAAgB,GAAG,YAAY,GAAG,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC;IACjF,kDAAkD;IAClD,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,kDAAkD;IAClD,MAAM,EAAE,OAAO,CAAC;IAChB,wDAAwD;IACxD,YAAY,EAAE,OAAO,CAAC;IACtB,mCAAmC;IACnC,UAAU,EAAE,MAAM,CAAC;CACpB;AAkHD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,sBAA2B,GACnC,qBAAqB,CAqGvB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAanE;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAWzD"}
|