@anomira/node-sdk 0.2.3 → 0.2.5

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/README.md CHANGED
@@ -168,41 +168,70 @@ Once registered, the middleware records **every request** with zero additional c
168
168
 
169
169
  Use these when the middleware can't infer the event on its own — e.g., application-level failures.
170
170
 
171
+ ### Getting the real client IP
172
+
173
+ > **Important — do not use `req.ip` for the `ip` field.**
174
+ >
175
+ > Behind a reverse proxy (Nginx, AWS ALB, Cloudflare — which every production app uses), `req.ip` in Express without `trust proxy` configured returns `127.0.0.1` — the proxy's address, not the user's. This breaks geo-detection, IP blocking, and attack correlation.
176
+ >
177
+ > Always use `anomira.getClientIp(req)` instead. It reads `X-Forwarded-For`, `CF-Connecting-IP`, `X-Real-IP`, and other forwarded headers in the correct priority order automatically.
178
+
179
+ ```ts
180
+ // ✅ Correct — works behind Nginx, Cloudflare, AWS ALB, any reverse proxy
181
+ const ip = anomira.getClientIp(req);
182
+
183
+ // ❌ Wrong behind a proxy — returns 127.0.0.1 unless trust proxy is configured
184
+ const ip = req.ip;
185
+ ```
186
+
187
+ If you use `anomira.express()` or `anomira.fastify()` middleware and call `track()` **inside a request handler**, you can omit the `ip` field entirely — the SDK reads it from the active request context automatically.
188
+
189
+ ```ts
190
+ // ✅ Also correct — IP is resolved from middleware context, no need to pass it
191
+ anomira.track("auth.otp.failed", { userId: req.body.phone });
192
+ ```
193
+
194
+ ### Examples
195
+
171
196
  ```ts
172
197
  // Credential stuffing / failed login attempt
173
198
  await anomira.trackLogin({
174
- ip: req.ip,
199
+ ip: anomira.getClientIp(req),
175
200
  userId: req.body.email, // email or user ID
176
201
  success: false, // false = failed attempt
177
202
  });
178
203
 
179
204
  // Successful login (enables geo-velocity tracking)
180
205
  await anomira.trackLogin({
181
- ip: req.ip,
182
- userId: user.id,
206
+ ip: anomira.getClientIp(req),
207
+ userId: user.id,
183
208
  success: true,
184
209
  });
185
210
 
186
211
  // Failed OTP — otp_flood detection
187
212
  anomira.track("auth.otp.failed", {
188
- ip: req.ip,
213
+ ip: anomira.getClientIp(req),
189
214
  userId: req.body.phone,
190
215
  meta: { endpoint: "/api/verify-otp" },
191
216
  });
192
217
 
193
218
  // SIM swap detection — call after any phone-based auth
194
- anomira.trackPhoneAuth({ ip: req.ip, userId: user.id, phone: user.phone });
219
+ anomira.trackPhoneAuth({
220
+ ip: anomira.getClientIp(req),
221
+ userId: user.id,
222
+ phone: user.phone,
223
+ });
195
224
 
196
225
  // Account takeover signal — e.g., password changed from new IP
197
226
  anomira.track("auth.account.takeover", {
198
- ip: req.ip,
227
+ ip: anomira.getClientIp(req),
199
228
  userId: user.id,
200
229
  meta: { reason: "password_changed_new_ip" },
201
230
  });
202
231
 
203
232
  // Webhook replay — call when you detect a replayed webhook signature
204
233
  anomira.track("webhook.replay.detected", {
205
- ip: req.ip,
234
+ ip: anomira.getClientIp(req),
206
235
  meta: { webhookId: req.headers["x-webhook-id"] },
207
236
  });
208
237
  ```
@@ -230,8 +259,10 @@ The SDK syncs your dashboard's blocked IPs and firewall rules every 60 seconds.
230
259
 
231
260
  ```ts
232
261
  app.use((req, res, next) => {
262
+ const ip = anomira.getClientIp(req); // always use this, not req.ip
263
+
233
264
  // Check if IP is manually blocked in the dashboard
234
- if (anomira.isBlocked(req.ip)) {
265
+ if (anomira.isBlocked(ip)) {
235
266
  return res.status(403).json({ error: "Forbidden" });
236
267
  }
237
268
 
@@ -241,7 +272,7 @@ app.use((req, res, next) => {
241
272
  method: req.method,
242
273
  body: req.body,
243
274
  headers: req.headers,
244
- ip: req.ip,
275
+ ip,
245
276
  });
246
277
 
247
278
  if (match) {
@@ -379,8 +410,8 @@ app.addHook("onClose", async () => {
379
410
  3. Confirm the middleware is registered **before** your routes and **after** body parsers.
380
411
  4. Make sure you're not blocking outbound HTTPS traffic to `api.anomira.io`.
381
412
 
382
- **TypeScript errors on `req.ip`?**
383
- Express types sometimes don't include `ip` directly. Use `(req as express.Request).ip ?? req.socket.remoteAddress ?? "0.0.0.0"`.
413
+ **TypeScript errors on IP extraction?**
414
+ Use `anomira.getClientIp(req)` it handles all proxy headers and TypeScript types correctly. Do not use `req.ip` or `req.socket.remoteAddress` directly in production; both return the proxy address behind Nginx.
384
415
 
385
416
  **`flush()` taking too long on shutdown?**
386
417
  The flush waits for in-flight HTTP requests to complete. If your process needs to exit fast, you can add a timeout:
package/dist/index.cjs CHANGED
@@ -404,7 +404,13 @@ var EventName = {
404
404
  SQL_ERROR: "db.sql.error",
405
405
  // Firewall (emitted when a custom request filtering rule fires)
406
406
  FIREWALL_BLOCK: "http.firewall.block",
407
- FIREWALL_FLAG: "http.firewall.flag"
407
+ FIREWALL_FLAG: "http.firewall.flag",
408
+ FIREWALL_RATE_LIMIT: "http.firewall.rate_limit",
409
+ FIREWALL_REDIRECT_HONEYPOT: "http.firewall.redirect_honeypot",
410
+ FIREWALL_TAG: "http.firewall.tag",
411
+ FIREWALL_MONITOR: "http.firewall.monitor",
412
+ FIREWALL_CHALLENGE: "http.firewall.challenge",
413
+ FIREWALL_TEMP_BLOCK: "http.firewall.temporary_block"
408
414
  };
409
415
 
410
416
  // src/agent-detection.ts
@@ -521,6 +527,9 @@ var KNOWN_BOT_UA = [
521
527
  [/^Ruby$/i, "Ruby"]
522
528
  ];
523
529
  var AXIOS_ACCEPT = "application/json, text/plain, */*";
530
+ function isBrowserUa(ua) {
531
+ return /Mozilla\/5\.0/.test(ua) && /(?:Chrome|Firefox|Safari|OPR|Edg(?:e|HTML)?|Trident)\/[\d.]+/.test(ua);
532
+ }
524
533
  function computeBrowserFingerprint(headers, ua) {
525
534
  const signals = [];
526
535
  let score = 0;
@@ -537,10 +546,15 @@ function computeBrowserFingerprint(headers, ua) {
537
546
  }
538
547
  const accept = str(headers["accept"]);
539
548
  if (!isDefiniteBot && accept === AXIOS_ACCEPT) {
540
- isDefiniteBot = true;
541
- knownClient = "axios";
542
- score = 100;
543
- signals.push("known_accept:axios");
549
+ if (!isBrowserUa(ua)) {
550
+ isDefiniteBot = true;
551
+ knownClient = "axios";
552
+ score = 100;
553
+ signals.push("known_accept:axios");
554
+ } else {
555
+ score += 15;
556
+ signals.push("browser_axios_accept");
557
+ }
544
558
  }
545
559
  if (isDefiniteBot) return { score, signals, isDefiniteBot, knownClient };
546
560
  const hasSecFetchSite = "sec-fetch-site" in headers;
@@ -1147,6 +1161,108 @@ var DEFAULT_INGEST_URL = "https://ingest.anomira.io/v1/events";
1147
1161
  var DEFAULT_BATCH_SIZE = 100;
1148
1162
  var DEFAULT_FLUSH_MS = 5e3;
1149
1163
  var DEFAULT_MAX_RETRIES = 3;
1164
+ var SENSITIVE_FIELDS = {
1165
+ identity: [
1166
+ "first_name",
1167
+ "last_name",
1168
+ "full_name",
1169
+ "name",
1170
+ "bvn",
1171
+ "nin",
1172
+ "ssn",
1173
+ "national_id",
1174
+ "tin",
1175
+ "passport",
1176
+ "passport_number",
1177
+ "drivers_license",
1178
+ "license_number",
1179
+ "voter_id",
1180
+ "dob",
1181
+ "date_of_birth",
1182
+ "birth_date",
1183
+ "birthdate",
1184
+ "gender",
1185
+ "marital_status",
1186
+ "nationality"
1187
+ ],
1188
+ contact: [
1189
+ "email",
1190
+ "phone",
1191
+ "phone_number",
1192
+ "mobile",
1193
+ "mobile_number",
1194
+ "address",
1195
+ "street",
1196
+ "city",
1197
+ "state",
1198
+ "postal_code",
1199
+ "zip"
1200
+ ],
1201
+ financial: [
1202
+ "card_number",
1203
+ "credit_card",
1204
+ "debit_card",
1205
+ "cvv",
1206
+ "account_number",
1207
+ "bank_account",
1208
+ "routing_number",
1209
+ "sort_code",
1210
+ "iban",
1211
+ "swift",
1212
+ "wallet_id"
1213
+ ],
1214
+ authentication: [
1215
+ "password",
1216
+ "pin",
1217
+ "otp",
1218
+ "secret",
1219
+ "private_key",
1220
+ "api_key",
1221
+ "access_token",
1222
+ "refresh_token",
1223
+ "jwt",
1224
+ "bearer_token",
1225
+ "security_answer",
1226
+ "mother_maiden_name"
1227
+ ],
1228
+ biometric: [
1229
+ "fingerprint",
1230
+ "face_id",
1231
+ "iris",
1232
+ "voiceprint",
1233
+ "biometric"
1234
+ ],
1235
+ health: [
1236
+ "medical_record",
1237
+ "diagnosis",
1238
+ "blood_group",
1239
+ "insurance_id"
1240
+ ],
1241
+ fintech: [
1242
+ "kyc_id",
1243
+ "customer_id",
1244
+ "beneficiary_account",
1245
+ "transaction_pin",
1246
+ "wallet_balance",
1247
+ "bank_verification_number",
1248
+ "virtual_account",
1249
+ "monnify_account",
1250
+ "paystack_customer",
1251
+ "flutterwave_customer"
1252
+ ]
1253
+ };
1254
+ var FIELD_CATEGORY_MAP = /* @__PURE__ */ new Map();
1255
+ for (const [cat, fields] of Object.entries(SENSITIVE_FIELDS)) {
1256
+ for (const f of fields) FIELD_CATEGORY_MAP.set(f, cat);
1257
+ }
1258
+ var VALUE_PATTERNS = [
1259
+ { name: "email", category: "contact", re: /^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$/ },
1260
+ { name: "bvn_nin", category: "identity", re: /^\d{11}$/ },
1261
+ { name: "card_number", category: "financial", re: /^\d{13,19}$/ },
1262
+ { name: "phone_ng", category: "contact", re: /^(?:\+234|0)[789][01]\d{8}$/ },
1263
+ { name: "jwt", category: "authentication", re: /^eyJ[A-Za-z0-9_\-]{10,}\.[A-Za-z0-9_\-]{10,}\./ },
1264
+ { name: "iban", category: "financial", re: /^[A-Z]{2}\d{2}[A-Z0-9]{4,30}$/ }
1265
+ ];
1150
1266
  var AnomiraClient = class {
1151
1267
  constructor(config) {
1152
1268
  this.logBuffer = [];
@@ -1479,13 +1595,35 @@ var AnomiraClient = class {
1479
1595
  }
1480
1596
  }
1481
1597
  // ─── Public API ────────────────────────────────────────────────────────────
1598
+ /**
1599
+ * Extract the real client IP from a request object, reading forwarded headers
1600
+ * in the correct priority order (Cloudflare → XFF → X-Real-IP → socket).
1601
+ *
1602
+ * Use this instead of `req.ip` when tracking events manually. `req.ip` in
1603
+ * Express without `trust proxy` set returns the proxy's address (127.0.0.1
1604
+ * behind Nginx), which breaks geo-detection and IP blocking.
1605
+ *
1606
+ * ```ts
1607
+ * // ✅ Correct — reads X-Forwarded-For / CF-Connecting-IP automatically
1608
+ * sentinel.track(EventName.OTP_FAILED, {
1609
+ * ip: sentinel.getClientIp(req),
1610
+ * userId: req.body.phone,
1611
+ * meta: { endpoint: "/api/verify-otp" },
1612
+ * });
1613
+ *
1614
+ * // ❌ Wrong behind a proxy — req.ip is 127.0.0.1 without trust proxy configured
1615
+ * sentinel.track(EventName.OTP_FAILED, { ip: req.ip, ... });
1616
+ * ```
1617
+ */
1618
+ getClientIp(req) {
1619
+ return this.config.getIp(req);
1620
+ }
1482
1621
  /**
1483
1622
  * Track a custom security event.
1484
1623
  *
1485
1624
  * ```ts
1486
- * // Track a failed OTP attempt
1487
1625
  * sentinel.track(EventName.OTP_FAILED, {
1488
- * ip: req.ip,
1626
+ * ip: sentinel.getClientIp(req), // use getClientIp, not req.ip
1489
1627
  * userId: req.body.phone,
1490
1628
  * meta: { endpoint: "/api/verify-otp", attempts: 3 },
1491
1629
  * });
@@ -1558,6 +1696,11 @@ var AnomiraClient = class {
1558
1696
  if (this.disabled) return;
1559
1697
  const ctx = requestContext.getStore();
1560
1698
  const resolvedIp = data.ip && data.ip !== "0.0.0.0" && data.ip !== "" ? data.ip : ctx?.ip ?? "0.0.0.0";
1699
+ if (this.config.debug && data.ip && isPrivateIp2(data.ip)) {
1700
+ this._origWarn(
1701
+ `[Anomira] Warning: track() received a private/loopback IP "${data.ip}" for event "${eventName}". Behind a reverse proxy, req.ip is the proxy's address \u2014 use sentinel.getClientIp(req) instead.`
1702
+ );
1703
+ }
1561
1704
  const resolvedMeta = ctx?.endpoint && !data.meta?.["endpoint"] ? { endpoint: ctx.endpoint, method: ctx.method, ...data.meta } : data.meta;
1562
1705
  const event = {
1563
1706
  name: eventName,
@@ -1798,6 +1941,9 @@ function normalizeIp(raw) {
1798
1941
  if (raw.startsWith("::ffff:")) return raw.slice(7);
1799
1942
  return raw;
1800
1943
  }
1944
+ function isPrivateIp2(ip) {
1945
+ return ip === "127.0.0.1" || ip === "::1" || ip === "0.0.0.0" || ip.startsWith("10.") || ip.startsWith("192.168.") || /^172\.(1[6-9]|2\d|3[01])\./.test(ip);
1946
+ }
1801
1947
  function defaultGetIp(req) {
1802
1948
  const r = req;
1803
1949
  const fwd = r["headers"];
@@ -1898,6 +2044,27 @@ function createExpressMiddleware(client) {
1898
2044
  const fingerprint = computeBrowserFingerprint(headers ?? {}, ua);
1899
2045
  const h2settings = extractHttp2Settings(req);
1900
2046
  const upstreamTls = extractUpstreamTls(headers ?? {});
2047
+ const cookieHeader = headers?.["cookie"];
2048
+ const cookieToken = cookieHeader ? cookieHeader.split(";").map((c) => c.trim()).find((c) => c.startsWith("anomira_fp="))?.slice("anomira_fp=".length) ?? "" : "";
2049
+ const browserFpRaw = cookieToken || headers?.["x-anomira-fp"] || "";
2050
+ let browserFp = null;
2051
+ if (browserFpRaw) {
2052
+ try {
2053
+ const raw = JSON.parse(Buffer.from(browserFpRaw, "base64").toString("utf8"));
2054
+ if (typeof raw["v"] === "number" && typeof raw["fp"] === "string") {
2055
+ browserFp = {
2056
+ fp: raw["fp"],
2057
+ bot: raw["bot"] ?? 0,
2058
+ sigs: (raw["sigs"] ?? []).join(","),
2059
+ uid: typeof raw["uid"] === "string" && raw["uid"] ? raw["uid"] : void 0,
2060
+ pst: raw["frm"]?.pst,
2061
+ ttf: raw["frm"]?.ttf,
2062
+ tts: raw["frm"]?.tts
2063
+ };
2064
+ }
2065
+ } catch {
2066
+ }
2067
+ }
1901
2068
  if (client["canaryTokenCache"] && client["canaryTokenCache"].size > 0) {
1902
2069
  const authHeader = (typeof headers?.["authorization"] === "string" ? headers["authorization"] : "") ?? "";
1903
2070
  if (authHeader.startsWith("Bearer ")) {
@@ -1992,11 +2159,39 @@ function createExpressMiddleware(client) {
1992
2159
  userId,
1993
2160
  meta: { url, method, ruleId: fwMatch.rule.id, attackType: fwMatch.rule.attackType }
1994
2161
  });
1995
- if (fwMatch.rule.action === "block") {
1996
- const res_ = res;
1997
- if (typeof res_["status"] === "function") res_["status"](403);
1998
- else res_["statusCode"] = 403;
1999
- if (typeof res_["end"] === "function") res_["end"]('{"error":"Blocked by firewall rule"}');
2162
+ const res_ = res;
2163
+ const setStatus = (code) => {
2164
+ if (typeof res_["status"] === "function") res_["status"](code);
2165
+ else res_["statusCode"] = code;
2166
+ };
2167
+ const sendBody = (body) => {
2168
+ if (typeof res_["end"] === "function") res_["end"](body);
2169
+ };
2170
+ const setHeader = (k, v) => {
2171
+ if (typeof res_["setHeader"] === "function") res_["setHeader"](k, v);
2172
+ };
2173
+ const action = fwMatch.rule.action;
2174
+ if (action === "block" || action === "temporary_block") {
2175
+ setStatus(403);
2176
+ sendBody('{"error":"Blocked by firewall rule"}');
2177
+ return;
2178
+ }
2179
+ if (action === "rate_limit") {
2180
+ setStatus(429);
2181
+ setHeader("Retry-After", "60");
2182
+ sendBody('{"error":"Rate limit exceeded","retryAfter":60}');
2183
+ return;
2184
+ }
2185
+ if (action === "redirect_honeypot") {
2186
+ const target = fwMatch.rule.redirectTarget ?? "/.env";
2187
+ setStatus(302);
2188
+ setHeader("Location", target);
2189
+ sendBody("");
2190
+ return;
2191
+ }
2192
+ if (action === "challenge") {
2193
+ setStatus(403);
2194
+ sendBody('{"error":"Request challenge required"}');
2000
2195
  return;
2001
2196
  }
2002
2197
  }
@@ -2010,7 +2205,7 @@ function createExpressMiddleware(client) {
2010
2205
  }
2011
2206
  }
2012
2207
  const onFinish = () => {
2013
- const lateUserId = client.config.getUserId(req);
2208
+ const lateUserId = client.config.getUserId(req) || browserFp?.uid || "";
2014
2209
  if (!userIdWarnFired && userIdCheckCount < 20) {
2015
2210
  userIdCheckCount++;
2016
2211
  if (!lateUserId) userIdMissCount++;
@@ -2025,6 +2220,30 @@ function createExpressMiddleware(client) {
2025
2220
  const latencyMs = Date.now() - startMs;
2026
2221
  const getHeader = res["getHeader"];
2027
2222
  const bytes = typeof getHeader === "function" ? parseInt(getHeader.call(res, "content-length") ?? "0", 10) || 0 : 0;
2223
+ const rawBody = req["body"];
2224
+ const piiFields = [];
2225
+ const piiPatterns = [];
2226
+ const piiCatSet = /* @__PURE__ */ new Set();
2227
+ if (rawBody != null && typeof rawBody === "object" && !Array.isArray(rawBody)) {
2228
+ const body = rawBody;
2229
+ for (const key of Object.keys(body)) {
2230
+ const cat = FIELD_CATEGORY_MAP.get(key.toLowerCase());
2231
+ if (cat) {
2232
+ piiFields.push(key.toLowerCase());
2233
+ piiCatSet.add(cat);
2234
+ }
2235
+ }
2236
+ for (const val of Object.values(body)) {
2237
+ if (typeof val !== "string" || val.length > 200) continue;
2238
+ for (const { name, category, re } of VALUE_PATTERNS) {
2239
+ if (!piiPatterns.includes(name) && re.test(val)) {
2240
+ piiPatterns.push(name);
2241
+ piiCatSet.add(category);
2242
+ }
2243
+ }
2244
+ }
2245
+ }
2246
+ const piiCategories = piiCatSet.size > 0 ? [...piiCatSet] : void 0;
2028
2247
  client.track(EventName.REQUEST, {
2029
2248
  ip,
2030
2249
  userId: lateUserId,
@@ -2035,9 +2254,20 @@ function createExpressMiddleware(client) {
2035
2254
  latencyMs,
2036
2255
  userAgent: ua,
2037
2256
  bytes,
2257
+ ...piiFields.length > 0 ? { piiFields } : {},
2258
+ ...piiPatterns.length > 0 ? { piiPatterns } : {},
2259
+ ...piiCategories ? { piiCategories } : {},
2038
2260
  _fp: fingerprint.score,
2039
2261
  _fpSig: fingerprint.signals.join(","),
2040
2262
  ...fingerprint.knownClient ? { _fpClient: fingerprint.knownClient } : {},
2263
+ ...browserFp ? {
2264
+ _bfp: browserFp.fp,
2265
+ _bbot: browserFp.bot,
2266
+ _bsigs: browserFp.sigs,
2267
+ ...browserFp.pst !== void 0 ? { _bpaste: browserFp.pst } : {},
2268
+ ...browserFp.ttf !== void 0 && browserFp.ttf >= 0 ? { _bttf: browserFp.ttf } : {},
2269
+ ...browserFp.tts !== void 0 && browserFp.tts >= 0 ? { _btts: browserFp.tts } : {}
2270
+ } : {},
2041
2271
  ...h2settings ? {
2042
2272
  _h2Window: h2settings.initialWindowSize,
2043
2273
  _h2HdrTable: h2settings.headerTableSize,
@@ -2165,14 +2395,32 @@ function createFastifyPlugin(client) {
2165
2395
  userId,
2166
2396
  meta: { url, method, ruleId: fwMatch.rule.id, attackType: fwMatch.rule.attackType }
2167
2397
  });
2168
- if (fwMatch.rule.action === "block") {
2169
- const rep = reply;
2170
- if (typeof rep["code"] === "function") {
2171
- const chained = rep["code"](403);
2172
- if (chained && typeof chained["send"] === "function") {
2173
- chained["send"]({ error: "Blocked by firewall rule" });
2174
- }
2175
- }
2398
+ const rep = reply;
2399
+ const replyCode = (code) => typeof rep["code"] === "function" ? rep["code"](code) : rep;
2400
+ const replyHeader = (k, v) => {
2401
+ if (typeof rep["header"] === "function") rep["header"](k, v);
2402
+ };
2403
+ const replySend = (chained, body) => {
2404
+ if (chained && typeof chained["send"] === "function") chained["send"](body);
2405
+ };
2406
+ const action = fwMatch.rule.action;
2407
+ if (action === "block" || action === "temporary_block") {
2408
+ replySend(replyCode(403), { error: "Blocked by firewall rule" });
2409
+ return;
2410
+ }
2411
+ if (action === "rate_limit") {
2412
+ replyHeader("Retry-After", "60");
2413
+ replySend(replyCode(429), { error: "Rate limit exceeded", retryAfter: 60 });
2414
+ return;
2415
+ }
2416
+ if (action === "redirect_honeypot") {
2417
+ const target = fwMatch.rule.redirectTarget ?? "/.env";
2418
+ replyHeader("Location", target);
2419
+ replySend(replyCode(302), "");
2420
+ return;
2421
+ }
2422
+ if (action === "challenge") {
2423
+ replySend(replyCode(403), { error: "Request challenge required" });
2176
2424
  return;
2177
2425
  }
2178
2426
  }
@@ -2187,11 +2435,11 @@ function createFastifyPlugin(client) {
2187
2435
  const ip = client.config.getIp(req);
2188
2436
  const url = req["url"] ?? "/";
2189
2437
  const method = req["method"]?.toUpperCase() ?? "GET";
2190
- const userId = client.config.getUserId(req);
2438
+ const serverUserId = client.config.getUserId(req);
2191
2439
  const status = reply["statusCode"] ?? 0;
2192
2440
  if (!userIdWarnFired && userIdCheckCount < 20) {
2193
2441
  userIdCheckCount++;
2194
- if (!userId) userIdMissCount++;
2442
+ if (!serverUserId) userIdMissCount++;
2195
2443
  if (userIdCheckCount === 20 && userIdMissCount >= 18) {
2196
2444
  userIdWarnFired = true;
2197
2445
  console.warn(
@@ -2206,6 +2454,28 @@ function createFastifyPlugin(client) {
2206
2454
  const fingerprint = computeBrowserFingerprint(headers ?? {}, ua);
2207
2455
  const h2settings = extractHttp2Settings(req);
2208
2456
  const upstreamTls = extractUpstreamTls(headers ?? {});
2457
+ const cookieHdrF = headers?.["cookie"];
2458
+ const cookieTokF = cookieHdrF ? cookieHdrF.split(";").map((c) => c.trim()).find((c) => c.startsWith("anomira_fp="))?.slice("anomira_fp=".length) ?? "" : "";
2459
+ const bfpRawF = cookieTokF || headers?.["x-anomira-fp"] || "";
2460
+ let browserFpF = null;
2461
+ if (bfpRawF) {
2462
+ try {
2463
+ const raw = JSON.parse(Buffer.from(bfpRawF, "base64").toString("utf8"));
2464
+ if (typeof raw["v"] === "number" && typeof raw["fp"] === "string") {
2465
+ browserFpF = {
2466
+ fp: raw["fp"],
2467
+ bot: raw["bot"] ?? 0,
2468
+ sigs: (raw["sigs"] ?? []).join(","),
2469
+ uid: typeof raw["uid"] === "string" && raw["uid"] ? raw["uid"] : void 0,
2470
+ pst: raw["frm"]?.pst,
2471
+ ttf: raw["frm"]?.ttf,
2472
+ tts: raw["frm"]?.tts
2473
+ };
2474
+ }
2475
+ } catch {
2476
+ }
2477
+ }
2478
+ const userId = serverUserId || browserFpF?.uid || "";
2209
2479
  client.track(EventName.REQUEST, {
2210
2480
  ip,
2211
2481
  userId,
@@ -2219,6 +2489,14 @@ function createFastifyPlugin(client) {
2219
2489
  _fp: fingerprint.score,
2220
2490
  _fpSig: fingerprint.signals.join(","),
2221
2491
  ...fingerprint.knownClient ? { _fpClient: fingerprint.knownClient } : {},
2492
+ ...browserFpF ? {
2493
+ _bfp: browserFpF.fp,
2494
+ _bbot: browserFpF.bot,
2495
+ _bsigs: browserFpF.sigs,
2496
+ ...browserFpF.pst !== void 0 ? { _bpaste: browserFpF.pst } : {},
2497
+ ...browserFpF.ttf !== void 0 && browserFpF.ttf >= 0 ? { _bttf: browserFpF.ttf } : {},
2498
+ ...browserFpF.tts !== void 0 && browserFpF.tts >= 0 ? { _btts: browserFpF.tts } : {}
2499
+ } : {},
2222
2500
  ...h2settings ? {
2223
2501
  _h2Window: h2settings.initialWindowSize,
2224
2502
  _h2HdrTable: h2settings.headerTableSize,