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

@signap/signals-web

v2.2.0

Published

Browser fingerprinting agent for Signap

Readme

@signap/signals-web

TypeScript SDK that collects browser fingerprint signals and identifies a visitor in a single round-trip, returning the resolved visitor id + confidence in the same response. Apache-2.0.

Bundle target: <60KB gzipped (tsup + esbuild, ESM+CJS dual output)

Sync-inline identification (v2)

agent.identify() returns the resolved visitor in a single round-trip — no polling. The Signap endpoint identifies the visitor inline before responding.

import { SignalsAgent } from "@signap/signals-web";

const sig = await SignalsAgent.load({
  apiKey: "pk_live_...",
  endpoint: "https://your-ingest-host.example", // required — no baked default host
});

const result = await sig.identify({ linkedId: "user_abc", tag: "checkout" });
// { requestId, ingestedAt, region, visitorId, confidence, identifiedAt }

Endpoint (required)

endpoint is required — the published SDK ships no baked default host, so load() throws INVALID_OPTIONS when neither an explicit endpoint nor a resolver is given. Resolution precedence: an explicit endpoint string → an endpoint callback. (region selects a baked host, but the region→host map ships empty in this alpha publish, so region alone does not resolve.)

// (a) explicit string (highest precedence) — the common case.
const sig = await SignalsAgent.load({ apiKey: "pk_live_...", endpoint: "https://ingest.signap.example" });

// (b) a callback you resolve however you like (sync or async).
const sig2 = await SignalsAgent.load({ apiKey: "pk_live_...", endpoint: async () => (await myConfig()).ingestEndpoint });

There is no runtime bootstrap fetch — resolution is fully client-static, so the first identify() posts straight to the resolved host. Sealed mode's key endpoint follows the same rule (explicit sealedKeyEndpoint string → resolver; also required, no baked default).

The resolved URL is visible in the browser Network tab — that's fine: the protection is your key's rate-limit + origin policy, not endpoint secrecy.

Custom attributes (linkedId / tag / extra)

identify() takes optional, customer-controlled attributes that are stored on the event and surface in the operator dashboard. Use them to answer "which site / page / campaign did this event come from?" — especially when one API key is embedded across several surfaces:

await sig.identify({
  linkedId: "user_12345",                 // your stable user id (≤ 256 chars)
  tag: "shop.example.com/checkout",       // free-form label (≤ 1024 chars)
  extra: { plan: "pro", experiment: "B" } // up to 16 key/value pairs
});

Where they show up in the dashboard:

| Attribute | Dashboard surface | |---|---| | tag | Events → "Source" column (chip, next to the auto-captured origin) + filterable via tag:<value> | | linkedId | Event detail → "Custom" tab | | extra | Event detail → "Custom" tab (JSON) |

The browser origin (which domain the SDK runs on) is captured automatically — no code needed — and also appears in the "Source" column (filter origin:<url>). Use tag when you need finer granularity than the domain (page, section, campaign).

Reserved extra keys

Some server-side integrations read a specific extra key by name. These are plain extra entries — no special SDK API, just a convention on the key:

| Key | Used by | Value format | |---|---|---| | shopify_order_gid | Shopify Order-Risk push — the Signap Order-Risk integration reads custom.extra["shopify_order_gid"] off the enriched event and pushes a risk assessment to Shopify. Omit it and it simply no-ops. | Shopify order GID, gid://shopify/Order/{numeric-id} |

// On a Shopify checkout/order page, tag the identify call with the order GID:
await sig.identify({
  extra: { shopify_order_gid: "gid://shopify/Order/450789469" },
});

Error taxonomy

| SdkError.code | When | |---|---| | INVALID_OPTIONS | bad apiKey / endpoint / timeoutMs | | NETWORK_ERROR | fetch + XHR both failed | | TIMEOUT | per-request timeoutMs exceeded | | HTTP_ERROR | non-2xx (status on .status) — includes the 503 the server surfaces when the inline Identify hop is unavailable | | INVALID_RESPONSE | non-JSON or missing required fields | | ABORTED | external AbortSignal triggered |

Migrating from v1.x

v1 was the async-ack contract (identify() returned an ack, lookup() polled). v2 returns the resolved visitor directly:

// v1
const ack = await sig.identify();
const visitor = await sig.lookupWithPolling(ack.requestId);

// v2
const visitor = await sig.identify();

lookup(), lookupWithPolling(), lookupEndpoint, LOOKUP_NOT_READY, LOOKUP_TIMEOUT, and IdentifyAck are removed. See CHANGELOG.md for the full breaking list.

Sources collected (MVP)

  • Canvas (2D render → SHA-256 hash)
  • WebGL (vendor, renderer, supported extensions, parameters hash)
  • AudioContext oscillator (44.1 kHz triangle wave → hash)
  • navigator.{userAgent, languages, hardwareConcurrency, deviceMemory}
  • screen.{width, height, colorDepth, pixelRatio}
  • Intl.DateTimeFormat().resolvedOptions().timeZone
  • Touch support
  • Network Information (effectiveType, rtt, downlink) — graceful if absent

Wire format

  • Identify: POST {endpoint}/v1/identify — request: a proto3-JSON FingerprintPayload; response: { requestId, ingestedAt, region, visitorId, confidence, identifiedAt }.

Browser support

Tested floors: Chromium/Edge 90+, Firefox 90+, Safari 14.1+ (iOS 14.5+). Full matrix, runtime features used, and known cross-browser quirks: docs/BROWSER_COMPAT.md.

Build & test

pnpm install
pnpm build     # tsup → dist/
pnpm test      # vitest
pnpm e2e       # playwright (Chrome+Firefox+Safari+Brave)
pnpm size      # bundle size check (fail if >60KB gz)

Distribution

  • npm: @signap/signals-web
  • CDN: a pinned-major …/signals/v2/agent.min.js or a floating …/signals/latest/agent.min.js (short cache). The CDN base URL is provided with your Signap account.

Wrappers

  • React: @signap/signals-react (2.0.0+ tracks SDK v2 sync-inline flow)
  • Next.js: @signap/signals-nextjs (SSR-safe)
  • Vue: @signap/signals-vue (2.0.0+; <SignalsProvider> + useVisitorData() + a Vue plugin)
  • Nuxt: @signap/signals-nuxtjs (SSR-safe)

Security

  • No eval, new Function, setTimeout(string, ...)
  • API key only via header (never query string, never localStorage)
  • CSP nonce-friendly

Timezone

  • clientTs in payload = new Date().toISOString() (always UTC Z-suffix)
  • Never parse user-input timestamps with new Date(string) — use dayjs.utc()

License

Apache-2.0 — see LICENSE.