npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@anomira/browser-sdk

v0.1.2

Published

Anomira browser-side fingerprinting, bot detection, and form guard SDK

Readme

@anomira/browser-sdk

Browser-side device fingerprinting, bot detection, and form behavioural analysis for Anomira.

Works alongside @anomira/node-sdk on your server. The browser SDK collects signals the server cannot see — canvas rendering, WebGL GPU strings, automation flags, and form interaction timing — and passes them to your backend via a single request header. No separate API calls, no external network requests, no cookies.


How it fits into the stack

Browser                          Your Server                    Anomira
────────                         ───────────                    ───────
@anomira/browser-sdk             @anomira/node-sdk              ingest
  ↓ collects signals               ↓ reads X-Anomira-FP          ↓ combines
  ↓ builds token                   ↓ adds to event meta           ↓ server + browser
  → X-Anomira-FP header ────────→  → sends to ingest ──────────→  scores together

The server SDK middleware reads the header automatically — you do not need to change anything on your backend after upgrading to the latest @anomira/node-sdk.


Installation

npm install @anomira/browser-sdk

Or via CDN for server-rendered apps (Django, Laravel, Rails, plain HTML):

<!-- Always latest (not recommended for production — use a pinned version) -->
<script src="https://cdn.jsdelivr.net/npm/@anomira/browser-sdk/dist/index.global.js"></script>

<!-- Pinned to a specific version (recommended) -->
<script src="https://cdn.jsdelivr.net/npm/@anomira/[email protected]/dist/index.global.js"></script>

The CDN URL is served by jsDelivr and becomes active automatically once the package is published to npm. No separate CDN setup is required.


Quick start

One line (auto-attach mode)

import { AnomaliraBrowser } from "@anomira/browser-sdk";

AnomaliraBrowser.init().attach();

Call this once on page load. After this, every fetch() and XMLHttpRequest to your own origin automatically carries the X-Anomira-FP header. No further changes needed.


Framework guides

Next.js (App Router)

Create a client component that initialises the SDK once on page load:

// app/providers.tsx
"use client";
import { useEffect } from "react";
import { AnomaliraBrowser } from "@anomira/browser-sdk";

export function AnomalyProvider({ children }: { children: React.ReactNode }) {
  useEffect(() => {
    AnomaliraBrowser.init().attach();
  }, []);
  return <>{children}</>;
}

Add it to your root layout:

// app/layout.tsx
import { AnomalyProvider } from "./providers";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <AnomalyProvider>{children}</AnomalyProvider>
      </body>
    </html>
  );
}

Works with Next.js rewrites and separate backends. The SDK sets a first-party session cookie (anomira_fp) after fingerprinting. The cookie is automatically included in every request by the browser — no header injection required, no proxy configuration needed.

Next.js (Pages Router)

// pages/_app.tsx
import type { AppProps } from "next/app";
import { useEffect } from "react";
import { AnomaliraBrowser } from "@anomira/browser-sdk";

export default function App({ Component, pageProps }: AppProps) {
  useEffect(() => {
    AnomaliraBrowser.init().attach();
  }, []);
  return <Component {...pageProps} />;
}

React (Vite / CRA)

// src/main.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import { AnomaliraBrowser } from "@anomira/browser-sdk";
import App from "./App";

// Initialise before React mounts so fingerprinting runs in parallel
AnomaliraBrowser.init().attach();

ReactDOM.createRoot(document.getElementById("root")!).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

Vue 3

// src/main.ts
import { createApp } from "vue";
import { AnomaliraBrowser } from "@anomira/browser-sdk";
import App from "./App.vue";

AnomaliraBrowser.init().attach();
createApp(App).mount("#app");

Plain HTML / CDN

<!DOCTYPE html>
<html>
  <head>...</head>
  <body>
    <!-- your content -->

    <script src="https://cdn.jsdelivr.net/npm/@anomira/[email protected]/dist/index.global.js"></script>
    <script>
      AnomaliraBrowserSDK.AnomaliraBrowser.init().attach();
    </script>
  </body>
</html>

Identity linkage — identify()

Call identify() after login succeeds to link the current browser session to the authenticated user. Once called, every subsequent request carries both the device fingerprint and the userId in the browser token.

This is the connection between browser-level signals (bot score, fingerprint, form timing) and your server-side user profiles. Without it, Anomira sees "fingerprint A7B3 made 10 failed logins" — with it, it sees "user [email protected] using fingerprint A7B3 made 10 failed logins from a new device."

// After login succeeds
const res  = await fetch("/api/auth/login", { method: "POST", ... });
const data = await res.json();

if (res.ok) {
  // Link this browser session to the authenticated user
  AnomaliraBrowser.identify({ userId: data.user.id });
}
// Or in your auth context — runs on every page load if user is already logged in
useEffect(() => {
  if (user?.id) {
    AnomaliraBrowser.identify({ userId: user.id });
  }
}, [user?.id]);

The server SDK uses this as a fallback userId on routes where your auth middleware hasn't populated req.user yet — including the login route itself, where the user is not authenticated at the time of the request.


Login form integration (recommended)

Auto-attach mode covers all requests, but for login and registration forms you should also call recordSubmit() so the SDK can measure accurate time-to-submit — a strong signal for detecting automated credential stuffing.

// components/LoginForm.tsx
import { AnomaliraBrowser } from "@anomira/browser-sdk";

export function LoginForm() {
  async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault();

    // Record submit time before any async work
    AnomaliraBrowser.init().recordSubmit();

    const form = new FormData(e.currentTarget);
    await fetch("/api/auth/login", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        email:    form.get("email"),
        password: form.get("password"),
      }),
      // X-Anomira-FP is injected automatically by attach()
    });
  }

  return (
    <form onSubmit={handleSubmit}>
      <input type="email"    name="email"    />
      <input type="password" name="password" />
      <button type="submit">Log in</button>
    </form>
  );
}

Manual mode

If you prefer explicit control over when and where the header is sent:

import { AnomaliraBrowser, HEADER_NAME } from "@anomira/browser-sdk";

const sdk = AnomaliraBrowser.init();

// Wait for async fingerprinting (canvas + audio) to complete
await sdk.ready();

// Add the header manually on specific requests
const response = await fetch("/api/auth/login", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    [HEADER_NAME]: sdk.getToken(true), // true = include form interaction signals
  },
  body: JSON.stringify({ email, password }),
});

API reference

AnomaliraBrowser.init()

Returns the singleton instance, creating it if it does not exist. Starts fingerprinting immediately in the background. Safe to call multiple times — always returns the same instance.

const sdk = AnomaliraBrowser.init();

sdk.attach(options?)

Patches window.fetch and XMLHttpRequest to inject X-Anomira-FP on every same-origin request. Returns this for chaining.

sdk.attach({
  sameOriginOnly:       true,  // default: true — only inject on same-origin requests
  includeFormOnSubmit:  true,  // default: true — include form signals on auth endpoints
});

sdk.ready()

Returns a Promise<void> that resolves when async fingerprinting (canvas rendering, audio processing) is complete. Calling getToken() before ready() resolves will produce a token with an empty fingerprint.

await sdk.ready();

sdk.getToken(includeForm?)

Returns a base64-encoded payload string for the X-Anomira-FP header.

const token = sdk.getToken();       // without form signals
const token = sdk.getToken(true);   // with form interaction signals (paste, timing)

sdk.recordSubmit()

Records the exact timestamp of a form submission. Call this in your submit handler before any await statements for accurate time-to-submit measurement.

form.addEventListener("submit", () => {
  sdk.recordSubmit();
  // ... rest of submit handler
});

sdk.botScore

Current bot confidence score (0–100). Available after await sdk.ready(). A score of 0 means the SDK has no evidence of automation; 95+ means a definitive automation flag was detected.

await sdk.ready();
console.log(sdk.botScore);     // e.g. 0 (human) or 95 (webdriver detected)
console.log(sdk.botSignals);   // e.g. ["webdriver", "no_plugins"]
console.log(sdk.fingerprint);  // e.g. "a7b3c2d1e4f5..."

HEADER_NAME

The header name constant: "X-Anomira-FP". Use this if you need to set the header manually.

import { HEADER_NAME } from "@anomira/browser-sdk";
// HEADER_NAME === "X-Anomira-FP"

What signals are collected

Device fingerprint

| Signal | Source | Stability | |--------|--------|-----------| | Canvas rendering | GPU + driver + font renderer | High — survives incognito, restarts | | WebGL renderer string | GPU vendor (e.g. "NVIDIA GeForce RTX 3070") | Very high | | Audio processing | DAC + OS audio driver | High | | Screen dimensions + pixel ratio | Hardware | Medium | | User agent + language | Browser | Medium (changes on updates) |

Bot detection signals

| Signal | What it catches | Score | |--------|----------------|-------| | navigator.webdriver | Selenium, Puppeteer, Playwright | 95 | | PhantomJS globals (_phantom, callPhantom) | PhantomJS | 95 | | __nightmare global | Nightmare.js | 95 | | domAutomation / domAutomationController | Chrome DevTools automation | 95 | | __pw_manual | Playwright stealth | 95 | | Missing window.chrome in Chromium UA | Headless Chrome | 75 | | Zero plugins in Chromium | Headless Chrome | 65 | | Empty navigator.languages | Misconfigured automation | 70 | | Zero-dimension screen (0x0) | Headless with no display | 85 | | Default headless screen (800x600) | Unmodified headless | 55 | | Mobile UA without touch support | UA spoofing | 50 |

Form signals (login forms)

| Signal | What it catches | |--------|----------------| | pasted | Password field paste event — nearly all credential stuffing bots paste | | ttf | Time from page load to first field interaction (bots act in < 200ms) | | tts | Time from first interaction to submission (bots submit in < 500ms) |


How the server uses these signals

You do not need to read the header yourself. After upgrading to the latest @anomira/node-sdk, the Express and Fastify middleware automatically:

  1. Reads X-Anomira-FP from the incoming request
  2. Decodes the payload and adds _bfp, _bbot, _bsigs, _bpaste, _bttf, _btts to the event meta
  3. Takes the higher of the server-side HTTP fingerprint score and the browser-side bot score as the effective score
  4. Flags paste events on login endpoints as credential stuffing candidates

The Anomira dashboard will show browser-confirmed signals alongside server-side signals, improving detection accuracy — particularly for headless Chrome bots that can spoof HTTP headers but cannot easily fake canvas rendering or hide navigator.webdriver.


Verifying the integration

Open your browser DevTools → Network tab → click any API request to your backend → Request Headers. You should see:

X-Anomira-FP: eyJ2IjoxLCJmcCI6ImE3YjNjMmQxZTRmNS4uLiIsImJvdCI6MCwic2lncyI6W10s...

You can decode it in the browser console to inspect the payload:

JSON.parse(atob(/* paste the header value here */))
// {v: 1, fp: "a7b3c2d1...", bot: 0, sigs: [], tz: "Africa/Lagos", ...}

Accuracy and limitations

Client-side-only fingerprinting achieves approximately 40–60% device uniqueness (source: FingerprintJS research). Anomira combines the browser fingerprint with server-side signals (TLS JA3/JA4, H2 SETTINGS frames, IP reputation, geo-velocity) to reach significantly higher combined accuracy.

What the browser SDK catches well:

  • Puppeteer and Playwright without stealth plugins (~95% of automated attacks)
  • Selenium WebDriver
  • PhantomJS / Nightmare.js
  • Headless Chrome with default configuration
  • Credential stuffing bots that paste credentials

What it does not catch:

  • Puppeteer with stealth plugins and correct chrome globals
  • Real-device bot farms (humans solving CAPTCHAs, real devices)
  • Attacks that do not go through the browser (direct API calls bypass the header entirely)

For direct API attacks (no browser), the server-side SDK's IP reputation, rate limiting, and behavioural baseline detection remains the primary defence.


Privacy and compliance

The browser SDK collects device characteristics for fraud prevention and security purposes. This qualifies as legitimate interest under:

  • GDPR Article 6(1)(f) — no consent banner required for security-purpose fingerprinting
  • NDPA 2023 — security processing is permitted without explicit consent; disclosure is required

What you must do: Update your privacy policy to disclose that device attributes are collected for security and fraud prevention. A single sentence is sufficient:

"We collect technical device attributes (browser rendering characteristics, hardware identifiers) for the purpose of fraud detection and account security."

No cookie banner or explicit consent toggle is required for security-purpose fingerprinting.

What the SDK does NOT do:

  • Cross-site tracking
  • Advertising or analytics profiling
  • Persistent storage (no cookies, no localStorage)
  • External network requests (all data goes to your own server)

Troubleshooting

Header not appearing in requests

Make sure attach() is called before any requests are made, and that you are not using a service worker that intercepts fetch before the SDK can patch it.

Bot score is 0 but the attacker is automated

The SDK only detects automation artifacts that bots leave in the browser environment. Sophisticated bots using stealth plugins can clear most of these signals. The server-side SDK's rate limiting and IP reputation will still catch most of these.

TypeScript errors about deviceMemory

navigator.deviceMemory is not in the standard TypeScript DOM types. The SDK handles this internally. If you are accessing sdk.fingerprint directly, cast navigator to unknown first.

SDK not initialising in SSR environments

The browser SDK must only run in the browser. In Next.js, always call AnomaliraBrowser.init() inside a useEffect hook or in a "use client" component. Calling it during server-side rendering will throw because document and window are not available.


Changelog

0.1.0

  • Initial release
  • Canvas, WebGL, and audio device fingerprinting
  • Three-tier bot detection (webdriver, headless Chrome, automation globals)
  • Form guard: paste detection and interaction timing
  • Auto-attach mode for fetch and XMLHttpRequest
  • Express and Fastify server SDK integration via X-Anomira-FP header