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

@sv.sphoro/device-intelligence-web-sdk

v3.2.0

Published

Browser device intelligence SDK

Readme

@sv.sphoro/device-intelligence-web-sdk

Browser signal collection for the Sphoro device intelligence platform.

~63 KB minified as a standalone <script> bundle, no runtime dependencies.

Install

npm install @sv.sphoro/device-intelligence-web-sdk

Or drop in the standalone build, which needs no bundler. Serve dist/device-score.global.js from your own asset host:

<script src="/assets/device-score.global.js"></script>
<script>
  const client = DeviceScore.createClient({
    endpoint: 'https://api.score.sphoro.com',
    publishableKey: 'pk_live_a1b2c3',
  });
</script>

Usage

import { createClient } from '@sv.sphoro/device-intelligence-web-sdk';

const client = createClient({
  endpoint: 'https://api.score.sphoro.com',
  publishableKey: 'pk_live_a1b2c3',
  requestTimeoutMs: 5000,
});

const result = await client.identify();
// {
//   deviceId: '6c2f9b1e-…',
//   confidence: 0.9871,
//   riskScore: 12,
//   matchedExistingDevice: true,
//   changedAttributes: ['browser.majorVersion'],
//   fingerprintVersion: 1,
//   requestDurationMs: 41
// }

Collect without sending

To post from your own backend, or to inspect what would be sent:

const envelope = await client.collect();
// or, with no client at all:
import { collect } from '@sv.sphoro/device-intelligence-web-sdk';
const envelope = await collect();

Compute a fingerprint locally

For offline scoring, tests, or an on-premise deployment that never calls the hosted API:

import { collect, computeFingerprint } from '@sv.sphoro/device-intelligence-web-sdk';

const { signals } = await collect();
const { canonicalHash, stableHash, confidence } = computeFingerprint(signals);

Options

createClient({
  endpoint: string;            // API origin — no path, the SDK appends /v1/collect
  publishableKey: string;      // pk_… — safe in page source
  requestTimeoutMs?: number;   // default 5000
  collect?: CollectOptions;    // applied to every identify() call
});

endpoint is an origin, not a URL. The SDK appends /v1/collect itself, so pass https://api.score.sphoro.com, not https://api.score.sphoro.com/v1/collect. Trailing slashes are stripped.

| Environment | endpoint | |---|---| | Production | https://api.score.sphoro.com | | UAT | https://api-uat.score.sphoro.com | | Local development | http://localhost:8080 |

collect.sphoro.com and collect-uat.sphoro.com still resolve for existing integrations. New ones should use the api. names.

Registering your origins

A publishable key is bound to an origin allowlist held against the key itself, so each site integrates without any change to the API deployment. Register the origin of the page running the SDK, not the API's, on the client's page in the portal:

https://app.example.com     exact origin — no path, no trailing slash
*.example.com               that domain's subdomains

A key called from an origin outside its list is rejected with 403 origin_not_allowed. An empty list means any origin, which is convenient in development and worth closing before launch — anyone who reads the key out of your page source can otherwise submit under it.

Origins must match exactly, including scheme and port: https://example.com does not cover https://www.example.com, and http://localhost:5173 does not cover http://localhost:3000.

collect({
  timeoutMs?: number;          // outer deadline, default 2000
  context?: Record<string, string>;  // forwarded to the API
  skip?: CollectorName[];      // drop specific collectors
  sensors?: boolean;           // inertial calibration, default true
});

skip trades match quality for latency. fonts is the most expensive collector by a clear margin; dropping it removes roughly 0.14 of the similarity weight, so do it only on paths where the latency genuinely matters.

sensors samples the accelerometer and gyroscope to recover this unit's factory calibration. It was opt-in through 3.1.x, when it cost a flat 700ms; it now stops as soon as it has enough and gives up within 250ms on any device whose motion sensor stays silent, which is every desktop and every iOS visit. Set it false to skip the sampling entirely, and see the note on identical handsets below for what that costs.

What it collects

Eighteen collectors: navigator, screen, canvas, webgl, audio, fonts, css, storage, webgpu, media, network, permissions, sensors, battery, features, intl, timing, tamper.

The tamper collector never contributes to the fingerprint. It reports observations about whether the environment is honest — automation globals, patched natives, canvas instability, User-Agent contradictions — which only the server-side risk engine consumes.

No permission prompts. Every API used requires no user grant. enumerateDevices is read for counts only, since labels need permission. On iOS, DeviceMotionEvent needs a grant that must come from a user gesture — this SDK never asks for it, so sensors simply returns nothing there.

Nothing is written. No cookie, no localStorage value, no IndexedDB record — not even transiently. Storage APIs are probed for availability by reading: touching localStorage.length throws in exactly the configurations a write attempt was there to detect, so the write is unnecessary. Earlier versions set and removed a namespaced probe key, which left nothing behind but still appeared in a devtools recording and still counted as storage access under ePrivacy. The cost of the change is that these fields now report availability rather than usability: a localStorage that reads fine and throws on write — a full quota — is reported as available.

Two signals describe the unit rather than the model. sensors recovers gyroscope zero-rate offset and accelerometer gain, trimmed per part at the factory. battery reads charge and charging state, which is not a fingerprint — it is excluded from the attribute registry, since a value that moves every few minutes would fork a device on every visit — and is used server-side only to test whether one battery could have been in two observed states. Everything else in the list above is a property of the model: two units of one phone, on one OS and browser build, agree on all of it byte for byte. battery is Chromium-only; Firefox removed the API and Safari never shipped it.

Design notes

Collection always returns. It usually sits in front of a login or a checkout. A partial fingerprint is worth a great deal; a hung promise is worth less than nothing. Every collector is individually time-boxed, every failure is captured rather than thrown, and the whole run has an outer deadline on top.

Failures are reported, not hidden. A collector that times out leaves an entry in envelope.errors, which discounts confidence server-side. Silently returning nulls would make a blocked browser look identical to one that simply lacks the API.

Canvas and audio are each rendered twice. Identical renders that produce different bytes prove the browser is injecting per-call noise. Without that check, a randomising browser mints a brand new device on every page load and nothing downstream can tell.

The SDK does not sign requests. A secret shipped to a browser is readable by anyone who opens the network tab. It authenticates with a publishable key bound to an origin allowlist, and the server treats everything it sends as untrusted.

identify() throws on failure. It does not return a default. Treating an unreachable fraud service as "low risk" is a policy decision that belongs in your code:

try {
  const result = await client.identify();
  if (result.riskScore >= 75) return blockCheckout();
} catch (error) {
  // Your call: fail open, fail closed, or degrade.
  logger.warn({ error }, 'device-score unavailable');
}

Browser support

Chromium, Firefox, Safari and their mobile equivalents, current and previous two majors. Older browsers work with fewer signals — every collector degrades individually rather than failing the run.

The SDK requires fetch, Promise and AbortController. It does not require a secure context, though crypto.getRandomValues and several collectors are unavailable without one, which lowers confidence.