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

unshared-frontend-sdk

v3.0.0

Published

Browser SDK for Unshared Labs — routes fingerprint events through the customer's own backend

Readme

unshared-frontend-sdk

Browser SDK for Unshared Labs — collects a device fingerprint and delivers it in one of two modes:

  • Proxy mode: events go through your own backend (baseUrl). Your secret API key never touches the browser.
  • Direct mode: events go straight to the Unshared Labs platform, authenticated by a publishable key (upk_…). No customer backend required — the tag-manager-style integration (à la Adobe Launch / BlueConic). The publishable key is public by design and only grants event submission.

Client-specific script-tag builds can package UNSHARED_PUBLISHABLE_API_KEY into the UMD bundle. When the key is packaged, new UnsharedBrowser({}) runs in direct mode. Use baseUrl: "" for same-origin proxy mode.

// Direct mode — no backend integration needed
const client = new UnsharedBrowser({ publishableKey: 'upk_your_key' });
await client.init({
  userId: currentUser.id,
  emailAddress: currentUser.email,
  isPaidSubscriber: currentUser.hasSubscription, // false → nothing is ever collected or sent
});

// Direct mode also exposes the read/verification API:
const { data } = await client.checkUser();
if (data?.is_user_flagged) {
  await client.triggerEmailVerification();        // emails a 6-digit code
  const result = await client.verify(codeFromUser); // -> { verified: true }
}

Direct-mode reads are protected server-side: the publishable key only answers for the identity+device the browser presents, origins are allowlisted, and every endpoint is rate-limited per IP (plus per-email cooldowns and code-attempt budgets).


Install

npm install unshared-frontend-sdk

The bundled fallback uses exact public npm dependency [email protected] (Node >=18.20 for installation/tooling). No vendored tarball is required. S3-first loading remains the default. Version 1.5.2 filters components only in the library's own sendFingerprint reporter; this SDK preserves complete raw collection results and applies its existing payload-size policy instead.

Or via CDN (no build step) — see CDN / UMD usage below for the complete example.


Quick Start

import { UnsharedBrowser } from 'unshared-frontend-sdk';

// Same-origin proxy mode (frontend served by the same backend)
const client = new UnsharedBrowser({ baseUrl: '' });

// Cross-origin (separate domains): set baseUrl via env var
// const client = new UnsharedBrowser({ baseUrl: process.env.BACKEND_URL });

// On page load — fire and forget
const fingerprint = await client.collect();
client.submitFingerprintEvent(fingerprint, { userId: currentUser?.id });

That's it. The SDK handles timeouts and errors without throwing. Read-only calls may retry; Trigger and Verify never retry.

Using this SDK alongside the Node unsharedBoundToUser middleware (Tier 1)? The middleware also auto-injects an inline fingerprint script into your HTML. That is expected — the two submitters share a client-side dedup guard (window.__unshared) and emit one event per (user, route). You do not need to turn injection off. See Fingerprint Collection: what loads what, who submits when.


Configuration

new UnsharedBrowser({
  baseUrl:    '',      // same-origin proxy mode
  maxRetries: 3,       // optional, default: 3
  timeout:    30_000,  // optional, default: 30s per attempt
});

BrowserConfig

| Option | Type | Default | Description | |--------|------|---------|-------------| | baseUrl | string | undefined | Proxy mode: base URL of your backend. Use "" when your frontend and backend share the same domain. Omitting both baseUrl and publishableKey defaults to same-origin proxy mode (unless a key was baked into a client-specific build — see below). | | publishableKey | string | — | Direct mode: publishable key (upk_…) issued by Unshared Labs. When set, events bypass your backend and go straight to the platform. | | apiUrl | string | https://api.unshared.ai | Direct mode: Unshared Labs platform origin. | | maxRetries | number | 3 | Retry budget for eligible operations | | timeout | number | 30000 | Per-attempt timeout in milliseconds | | includePathPrefix | string[] | Unset (all paths) | Literal location.pathname prefixes for lifecycle event submission; [] submits on none. This is browser-side filtering, not server enforcement. | | skipPaths | string[] | Unset | Literal pathname prefixes excluded from lifecycle submission; wins over includes. Does not prevent explicit init() from establishing identity/collecting, or disable low-level collection methods. | | sessionId | () => string \| undefined | Cookie, then UUID | Resolver runs at construction; a defined value wins over __unshared_sid/generated UUID and is persisted to the correlation cookie. Not an auth-session TTL setting. | | deviceId | () => string \| undefined | Legacy stored device, then collected stable hash | Resolver runs at construction and init; a nonempty value overrides the device identifier, not the canonical collected hashes or separate permanent ID. | | enableInterstitial | boolean | false | Auto-render the interstitial modal when the user is flagged (via the unshared:flagged event); works in direct or proxy mode. See Interstitial modal. | | interstitialFlowType | string | email_verification | Flow type requested for the auto-shown interstitial. |

Packaged publishable key

For a client-specific script-tag build, package the publishable key into the UMD bundle so the client does not pass it in page code. When baseUrl and publishableKey are omitted, the SDK resolves:

  1. Explicit publishableKey.
  2. Build-time UNSHARED_PUBLISHABLE_API_KEY baked in by scripts/build.js (client-specific builds only, e.g. the S3 flow).
  3. Otherwise no key — the SDK stays in same-origin proxy mode. There is no hardcoded fallback; builds made without the env var (including every npm release) never carry a usable key.

Build a client-specific distribution with:

UNSHARED_PUBLISHABLE_API_KEY=upk_test_or_client_key npm --prefix sdks/javascript/browser run build

Passing baseUrl, including baseUrl: "", suppresses the packaged-key fallback and keeps proxy mode unless an explicit nonempty publishableKey is supplied.

An explicit nonempty publishableKey takes precedence even when baseUrl is also passed; baseUrl suppresses only the packaged-key fallback. Use HTTPS origins (HTTP is accepted only for loopback development), and never put a secret usk_ key in browser config. maxRetries applies only to eligible reads, not ingestion, Trigger or Verify.

The build applies targeted obfuscation before minification to the packaged publishable key and the default API URL so they are not present as obvious raw string literals in dist/index.umd.js. This is only a friction layer against casual source inspection; the browser still reconstructs those values at runtime, and server-side origin allowlists/rate limits remain the real security controls.

The browser build also runs a markdown confidentiality guard before producing dist files. It removes obvious generated metadata sections and fails if markdown contains confidential metadata markers. Mentions of known key names print warnings by default because those names are sometimes documented intentionally.

Local overrides:

npm --prefix sdks/javascript/browser run build -- --allow-key-mentions
npm --prefix sdks/javascript/browser run build -- --allow-confidential-docs

Use the confidential-docs override only for local investigation; do not commit markdown containing generated system metadata or real key values.


Methods

Identity and Lifecycle

Call init({ userId, emailAddress, isPaidSubscriber }) after authenticated identity is available, onRouteChange() on SPA navigation, and client.destroy() on logout or teardown. isPaidSubscriber: false suppresses collection/submission for that user. destroy() clears cached collection/login state and detaches listeners; it does not erase permanent device/hash cookies or change your application's auth TTL.

Init and MPA collection reuse a paired raw/wire fingerprint cache: the complete raw JSON is in per-tab sessionStorage __unshared_fp_raw, and the normalized wire view is in __unshared_fp. SPA events reuse this pair rather than recollecting; overlapping init/MPA collection shares one pending collection per SDK instance. Old wire-only caches are recollected. In-memory fallback keeps the current instance usable when storage is blocked, but cannot persist across reloads.

Eligible init/route submission activity refreshes readable correlation cookies to rolling 400 days (Max-Age=34560000) before deduplication, even if no new event is sent: __unshared_stable_hash, __unshared_full_hash, __unshared_device_id, legacy stable alias __unshared_fp_id, and __unshared_sid. The permanent __unshared_device_id=upid_<uuid-v4> is independent of mutable hashes, backed by localStorage; an existing valid localStorage ID takes precedence over a cookie when restoring it. Cookies use Path=/, SameSite=Lax and Secure on HTTPS. Browser privacy controls, storage blocking and user deletion can shorten retention.

With the protection proxy, the server also writes the legacy HttpOnly __unshared_fingerprint_id: it preserves the first full hash, while canonical __unshared_full_hash tracks the current collection. Only a server response can renew that HttpOnly alias. These correlation lifetimes do not extend authentication: the SDK UID/email server cookies retain their existing 365-day TTL and the verification challenge remains ten minutes. The per-tab collection cache is not a 400-day fingerprint snapshot.

Lifecycle submissions share window.__unshared.lastKey with the inline middleware script and mirror the last (user, pathname + query) in sessionStorage. Consecutive duplicates are suppressed, including reloads in the tab; A-to-B-to-A can submit again. This is not guaranteed delivery: no persistent replay queue is maintained, old queue entries are discarded, and failed ingestion is not retried.

collect()

Collect a browser fingerprint. Returns a FingerprintWireFormat object ready to pass to submitFingerprintEvent.

const fingerprint = await client.collect();

collectRaw()

Collect the fingerprint library's complete JSON object without reducing it to the compatibility wire type.

const rawFingerprint = await client.collectRaw();

submitFingerprintEvent(fingerprint, opts?)

Send the fingerprint to your backend's /__unshared/submit-fp route. Fire-and-forget — you don't need to await this unless you want delivery confirmation.

// Fire and forget (recommended)
client.submitFingerprintEvent(fingerprint, {
  userId:    currentUser?.id,  // optional
  eventType: 'page_view',      // optional
});

// Awaited (if you need delivery confirmation)
const result = await client.submitFingerprintEvent(fingerprint);
if (!result.success) {
  console.warn('Delivery failed:', result.error?.message);
}

Options:

| Option | Type | Description | |--------|------|-------------| | userId | string | ID of the logged-in user | | eventType | string | Event label (e.g. "page_view", "login") |


submitFingerprint(payload)

Submit any JSON object without reshaping or filtering unknown fields. Direct mode encrypts the complete object with the publishable key; proxy mode keeps the browser-to-customer hop plaintext so middleware can add trusted context before encrypting the final request to Unshared.

await client.submitFingerprint({
  fingerprint: await client.collectRaw(),
  context: {
    identity: { user_id: currentUser?.id },
    event: { type: location.pathname + location.search },
  },
});

The payload must be a non-null JSON object; arrays and primitives fail locally with VALIDATION_ERROR. Direct requests larger than 1 MiB fail locally with REQUEST_TOO_LARGE. Direct ingestion succeeds only on a strict 202 v3 acknowledgement.

Proxy submissions have a fixed 100 KiB (102,400 serialized UTF-8 bytes) limit, compatible with an upstream default express.json() without configuration or middleware reordering. Structured submissions send the full snapshot if it fits; otherwise they empty local/session storage lists while retaining all cookies, then omit context.browser_storage if still too large. The complete raw fingerprint, including future fields, and core identity/device/session/SDK/event metadata are never trimmed. Oversized core payloads return REQUEST_TOO_LARGE without sending; the inline submitter logs that code. Low-level submitFingerprint(payload) never reduces arbitrary caller objects: it sends them exactly or rejects above 100 KiB. Direct mode keeps full snapshots and its existing 1 MiB logical / 1.5 MiB encrypted limits. Duplicate fingerprint caches and legacy queues are excluded from snapshots.


Direct-mode API (requires publishableKey)

All four methods default to the identity captured by init() (email + stable fingerprint hash as deviceId); pass { email, deviceId } to override. In proxy mode they return error.code === 'DIRECT_MODE_REQUIRED' — proxy-mode apps get these flows from their own backend middleware.

| Method | Retries | Returns | |--------|---------|---------| | checkUser(opts?) | bounded availability failures only | { is_user_flagged, decision_id, decision_code } | | triggerEmailVerification(opts?) | never (each call emails a code) | { verification_id, next_allowed_at, retry_after_seconds }; the SDK retains the challenge for verify(...) | | verify(code, opts?) | never (attempts are budgeted) | Exact verified/not-verified result; UUIDv4 challenges and 6-digit OTPs are validated locally | | verifyEmail({ verification_id, code }) | never | Verification for an explicit challenge ID; reconciles flagged state when retained identity is available | | emailVerificationStatus(opts?) | yes | { can_send: boolean, next_allowed_at } |

V3 methods require exact success/error envelopes and endpoint-specific status/result shapes. After verified: true, the SDK performs one no-retry checkUser reconciliation; sticky flagged state is cleared only by a strict available unflagged decision, never by Verify alone or CHECK_UNAVAILABLE.


Interstitial modal

Instead of wiring your own remediation UI, the SDK can render a server-driven interstitial modal (e.g. an email-OTP verification flow) when a user is flagged. The flow is authored in the Unshared dashboard and stored per company; the SDK fetches the published definition from GET /v2/browser/interstitial-flow, renders it in a Shadow-DOM-isolated modal, and routes the flow's actions back through triggerEmailVerification() / verify() above — so all verification logic stays server-side. Direct mode uses publishableKey; proxy mode is described below.

Proxy mode is supported too. When the SDK is constructed with baseUrl (no publishableKey) and your backend runs the Node middleware, showInterstitial() fetches the flow through GET {baseUrl}/__unshared/interstitial-flow and runs the modal's actions through the middleware's /__unshared/verify-trigger and /__unshared/verify routes. The secret key never leaves your server, and the user's identity is resolved server-side; the browser sends only the OTP. Configure the middleware's trusted emailAddress resolver (preferred over the __unshared_email cookie fallback).

Publish the requested web flow for your company in the dashboard before enabling the modal; a draft/missing flow cannot render. A modal or blur alone is not access control: use server-side gate/403 enforcement for protected content. Verification rate limits and attempt budgets are existing platform controls, not new browser functionality; additional custom gates/rate limits belong to your app or gateway.

showInterstitial(opts?)

Fetch and render the published flow on demand. No-op if a modal is already open.

const sdk = new UnsharedBrowser({ publishableKey: 'upk_…' });
await sdk.init({ userId, emailAddress });

// e.g. from your own flagged handler:
await sdk.showInterstitial();                       // default flow_type, web variant
await sdk.showInterstitial({ flowType: 'email_verification', container: myEl });

| Option | Type | Default | Description | |--------|------|---------|-------------| | flowType | string | constructor's interstitialFlowType or email_verification | Which published flow to fetch | | container | HTMLElement | document.body | Where to mount the modal |

Returns { success, data: { shown } }; rendering errors are swallowed (never-throw). shown is false when a modal was already open.

Auto-show

Set enableInterstitial: true to render the modal automatically the first time the unshared:flagged event fires (dispatched by the proxy-mode injected script, or by your own code). If you detect flagging via createFetchInterceptor / createAxiosInterceptor instead, call sdk.showInterstitial() from your onFlagged handler.

const sdk = new UnsharedBrowser({ publishableKey: 'upk_…', enableInterstitial: true });
await sdk.init({ userId, emailAddress });
// On the next `unshared:flagged` event, the modal renders itself.

destroy() removes any open modal and detaches the listener.


Backend requirement

The SDK sends fingerprints to {baseUrl}/__unshared/submit-fp. Your backend must handle this route. If you're using unshared-clientjs-sdk, the unsharedBoundToUser middleware sets this up automatically:

// On your Express backend
import express from 'express';
import { UnsharedClient } from 'unshared-clientjs-sdk';
import { unsharedBoundToUser } from 'unshared-clientjs-sdk';

const app = express();
const client = new UnsharedClient({ apiKey: process.env.UNSHARED_API_KEY });

app.use(express.json());
// Mount your authentication/session middleware before the SDK.
const middleware = unsharedBoundToUser(client, {
  userId: (req) => req.session?.user?.id,
  emailAddress: (req) => req.session?.user?.email,
});
app.use(middleware);
// Call middleware.destroy() on server shutdown/test teardown/before re-mounting.

Node includePathPrefix scopes ordinary checks/enforcement/events, not identity bootstrap or eligible HTML injection. For example, include /api/article/ to gate article data while /api/me/list remains outside enforcement; the surrounding HTML can still bootstrap fingerprinting. Node skipPaths truly bypasses the SDK for matching application requests. These server options are separate from browser lifecycle path filters. See the complete protection config.

The Node middleware serves both /__unshared/fp.js (browser SDK) and /__unshared/fingerprint.js (agent); unavailable bundles return 503 with Cache-Control: no-store. The Web Standard handler instead requires a manually supplied fingerprintSdkBundle for fp.js and does not automatically serve fingerprint.js or interstitial-flow; wire those separately when needed.

For the injected script, middleware debug: true exposes PII-free reason codes and optional HTTP status in window.__unshared.lastDecision and [Unshared] debug logs (for example MISSING_UID, DEDUP_SKIPPED, ASSET_LOAD_ERROR, SUBMIT_SUCCESS). Middleware disableBotFilter: true also reaches the inline submitter for E2E tests. Neither is a BrowserConfig option; the standalone SDK's bot filter is unchanged.


Error handling

All methods follow a never-throw contract. Errors are always returned as a result object — the SDK will not throw or reject under any circumstances.

const result = await client.submitFingerprintEvent(fingerprint);

// result.success === false means delivery failed
// result.error.code === 'DELIVERY_FAILED'

Read-only direct calls retry eligible network and 5xx failures. Fingerprint ingestion, Trigger, and Verify are never retried because an ambiguous request may already have committed. Failed ingestion is not queued for replay.


CDN / UMD usage

The UMD bundle exposes a window.UnsharedBrowser namespace object. Destructure the class from it first. If the bundle was built with UNSHARED_PUBLISHABLE_API_KEY, the client does not pass a key:

<script src="https://unpkg.com/[email protected]/dist/index.umd.js"></script>
<script>
  const { UnsharedBrowser } = window.UnsharedBrowser;

  // Direct mode using the publishable key packaged into the UMD build.
  const client = new UnsharedBrowser({});

  client.init({
    userId: currentUser.id,
    emailAddress: currentUser.email,
    isPaidSubscriber: currentUser.hasSubscription,
  });
</script>

For proxy mode instead, pass baseUrl explicitly.


Real-browser acceptance coverage

For Boston Globe style script-tag integration, split client-side browser tests into host-site flows and SDK-owned delivery behavior.

| Area | Cases | Implementation status | |------|-------|-----------------------| | Signup | Successful signup initiation; duplicate email; missing or invalid password rules. | Host-site auth behavior. Test in the client's browser suite; no SDK code required beyond confirming the script tag remains loaded and does not block the flow. | | Identity verification | Correct 6-digit code; wrong code; timeout/expired code. | Host-site or Unshared direct verification UI behavior, depending on who owns the screen. SDK exposes triggerEmailVerification, verify, and emailVerificationStatus; page-level form states must be tested in the client's browser suite. | | Signup completion | Success page/message; failure page/message with reason. | Host-site auth behavior. Test in the client's browser suite. | | Login | Valid login; wrong email; wrong password; unregistered email; missing password; missing email. | Host-site auth behavior. Test in the client's browser suite while verifying the SDK initializes only after a real identified user is available. | | Password reset | Successful reset initiation. | Host-site auth behavior. Test in the client's browser suite. | | Password reset completion failures | Wrong current password; mismatched new-password entries; password-rule failures; missing required values. | Host-site auth behavior. Test in the client's browser suite. | | Page navigation | Full page loads with script tag; link clicks; forward/back; refresh with same script tag injected. | SDK supports script-tag load, init, MPA DOMContentLoaded, and SPA route-change submission. Real-browser tests should assert the UMD bundle loads and the client page still renders. | | Event firing | Event fires where applicable after user identity is present. | Implemented by init, onRouteChange, MPA listener, and direct/proxy submit endpoints. Covered by unit tests; should also be verified in real browser against mocked or test backend responses. | | Retry | Read-only checks may retry; ingestion, Trigger, and Verify make one attempt and are never queued. | Implemented and unit-tested. |