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

@uengage.io/browser-sdk

v2.1.0

Published

Browser bundles for the uEngage platform APIs. One IIFE per service for pages with no build step (served from cdn.platform.uengage.io), plus ESM/CJS entry points for bundled apps. Token minting, refresh-before-expiry and transport degradation are internal

Readme

@uengage.io/browser-sdk

Browser bundles for the uEngage platform APIs, for pages that have no build step — a PHP/CI4 template, a legacy admin screen, a white-label storefront. One <script> tag, one createClient call, done.

Bundles are served from https://cdn.platform.uengage.io and also published to npm for apps that do bundle.

| Bundle | Global | What it does | | ---------- | ------------------ | ------------------------------------------- | | realtime | Uengage.realtime | Live order updates from services/realtime |

Restricted realtime channels

Use tokenUrl to obtain a channel-restricted token from your backend, then call client.subscribe('delivery-updates', ...). The backend must authorize the user and mint the token with the platform SDK's auth.mintRealtimeToken helper. An ordinary service token cannot substitute for the restricted grant on named channels.

Names are case-sensitive identifiers, not paths or wildcards. Tokens permit exactly one name, are valid for 60–900 seconds (300 by default), and cannot publish or call other platform APIs. Minting does not create a publisher. See the complete integration and authorization guide.

Quick start

<script
  src="https://cdn.platform.uengage.io/browser-sdk/1.0.0/realtime.min.js"
  integrity="sha384-…"
  crossorigin="anonymous"
></script>
<script>
  var rt = Uengage.realtime.createClient({
    env: 'prod',
    token: grant.token, // minted by your backend, already in the page
    transport: 'auto',
  });

  var sub = rt.subscribe('/orders/' + orderId, {
    onUpdate: function (event, meta) {
      // meta.origin is 'live' or 'snapshot'; the data is the same either way.
      render(event.data);
    },
    onError: function (err) {
      console.warn(err.code, err.message);
    },
  });

  // sub.unsubscribe();  rt.close();
</script>

The integrity value above is a placeholder — the real hash is only known once the release build runs, and the publish job prints it into its GitHub Actions summary. Copy it from there; a stale hash blocks the script outright.

Pin the exact version and keep the integrity hash. The floating /browser-sdk/v1/ alias exists for emergency rollout, but a floating URL cannot be used with integrity, and SRI is what stops a compromised bucket from injecting script into every embedding site. The release job prints the hash for each version into its GitHub Actions summary.

There is no bundle.init() and no polling loop to write. Token handling, refresh, reconnect, catch-up after a dropped connection and de-duplication all happen inside subscribe.

Options

| Option | Default | Notes | | --------------------- | -------- | ----------------------------------------------------------------------- | | env | — | 'dev' \| 'uat' \| 'prod'. Resolves API and AppSync endpoints. | | token | — | The token your page already holds. The usual choice. | | getToken | — | Supplies the token, for a screen that outlives one. See below. | | tokenUrl | — | Same-origin GET returning { token, expiresIn }; the SDK fetches it. | | serviceId | — | OAuth2 client id. With serviceSecret. | | serviceSecret | — | OAuth2 client secret. Present in the page — read the security note. | | transport | 'auto' | 'auto' \| 'live' \| 'poll' \| 'proxy' | | snapshotUrl | — | Required by 'proxy'. Same-origin route on your backend. | | pollIntervalMs | 5000 | Poll cadence when polling. | | degradeAfter | 3 | Consecutive dead sockets before 'auto' falls back to polling. | | apiBase | — | Overrides env, for a preview stack. | | endpoints | — | Explicit AppSync endpoints, for a stack not in the built-in table. | | maxReconnectDelayMs | 30000 | Backoff ceiling for socket reconnects. |

Pass exactly one of token, getToken, tokenUrl, or serviceId+serviceSecret — transport: 'proxy' takes none of them. Supplying two is refused rather than resolved by a precedence rule.

token is the normal shape: your backend authorizes the user, mints a scoped token, and the page receives it before it builds a client. Reach for getToken only when a screen outlives one token — it is asked whenever the client needs a fresh one, roughly once per token lifetime and always before the current one expires, so returning a newer value is all a long-lived screen has to do. It is not called on every subscribe; the refresh scheduler caches in between.

An expired token fails once, permanently, naming getToken as the fix — a literal cannot change, so retrying it would only stall. A getToken that throws is retried, like tokenUrl; an app that knows the session is gone opts into the permanent latch by throwing a UengageBrowserError itself.

subscribe(channel, { onUpdate, onError }) returns { channel, unsubscribe() }. onUpdate receives the platform event envelope and { channel, origin }.

Reading once, on demand

getLatest(channel) is the "refresh button": one read of the channel's latest value, resolving to null when nothing is cached.

document.querySelector('#refresh').addEventListener('click', async function () {
  try {
    var event = await rt.getLatest('/orders/' + orderId);
    if (event) render(event.data);
    else showNoUpdatesYet();
  } catch (err) {
    showError(err.message); // err.permanent tells you whether to offer a retry
  }
});

It opens no socket and starts no polling, so a page that only ever calls getLatest holds no connection at all. It also does not fire onUpdate on a subscription to the same channel — you already have the value in hand, and delivering it twice would be a surprise.

null is also the answer for a channel the caller cannot see. The service returns the same 404 for "nothing cached" and "not yours", deliberately, so sequential order ids cannot be walked to discover which ones exist.

In 'proxy' mode it reads your snapshotUrl and no platform credential is involved.

Errors

onError receives a UengageBrowserError with a stable code and a permanent flag. permanent means retrying cannot help — surface it rather than hiding it behind a spinner.

| code | permanent | Meaning | | -------------------- | ----------- | --------------------------------------------------------------------------------------------------------------------- | | invalid_options | yes | Thrown synchronously from createClient. | | auth_failed | yes | The credential was rejected, or getToken returned a non-string. | | session_expired | yes | The session is gone: a 401/403 from tokenUrl, an expired token, or a UengageBrowserError your getToken threw. | | auth_unavailable | no | Network or 5xx while minting, including a getToken that threw. | | snapshot_failed | no | A snapshot read failed. | | forbidden | yes | The channel was rejected by the namespace policy. | | transport_degraded | maybe | Live updates unavailable; the client fell back to polling. |

Transports

| Mode | Behaviour | | --------- | ---------------------------------------------------------------------------------------------------------------- | | 'auto' | WebSocket, falling back to polling after degradeAfter dead sockets and recovering automatically. Use this. | | 'live' | WebSocket only. For debugging. | | 'poll' | Poll the platform snapshot route only. | | 'proxy' | Poll a route on your backend. No platform credential in the page at all. |

'auto' exists because corporate proxies and some mobile carriers block wss: outright, and that is only discoverable by trying. The fallback reads the same last-value cache the socket would have delivered from, so the data is identical and only the latency changes.

How it stays authenticated

Platform tokens live about 15 minutes. AppSync validates the JWT when the connection opens and on every subscribe frame, and closes the socket once the token behind it lapses — AppSync Events has no way to re-authenticate a live connection, so a reconnect roughly every 15 minutes is unavoidable. The client makes it invisible:

  • The token is cached in memory and never handed out with less than 60 seconds of life left.
  • Concurrent callers share one in-flight mint, so eight subscriptions produce one token rather than eight.
  • A background timer refreshes 120 seconds before expiry, so the forced reconnect finds a token already in hand instead of paying a round-trip inside its backoff window.
  • On reconnect the SDK resubscribes every channel and re-reads each snapshot; ULID ordering drops anything already delivered, so nothing renders twice and nothing is missed.
  • A hidden tab stops refreshing entirely, and catches up when it comes back.

Bad credentials are latched, not retried. A wrong serviceId fails identically every time, and retrying from every page view would turn a copy-paste mistake into sustained load on the auth service.

Security: the secret is public

Prefer token — your backend authorizes the user and mints a scoped token, and nothing secret reaches the page at all. The rest of this section is about what you avoid by doing so.

A serviceSecret passed to createClient is in the page and readable by anyone who opens devtools or fetches the bundle. It also mints tokens whose subject is service:*, and services/realtime skips its tenant-ownership check for service principals — so an extracted credential can read orders across tenants, and can mint fresh tokens for as long as the registry entry lives.

If that is not acceptable for a given surface, both alternatives keep the same page code:

  • tokenUrl — your backend mints against its own session and returns { token, expiresIn }. A stolen response is worth only the minutes left on it.
  • transport: 'proxy' — nothing platform-related reaches the page. Your backend authorizes from its own session and calls the platform server-side, which is also the only way to enforce per-customer ownership today.

See examples/ci4/ for both, wired up with packages/platform-sdk-php.

If you do embed a secret

  1. Register a dedicated client per embedding site, with the narrowest allowedScopes that works. Scopes do not gate the realtime routes, but the same JWT is accepted by wallet, business, zones and audit — narrow scopes are what stop a leaked storefront credential from reaching those.
  2. Never reuse a credential across environments.
  3. Rate-limit and alarm on /auth/business/oauth/token: every page view mints a token, and there is no cross-page cache by design.

Rotation runbook

Rotating is a multi-site deploy, not a single action. Know that before you need it:

  1. Register a replacement client and confirm it works against uat.
  2. Cut a new browser-sdk version if anything in the bundle changed.
  3. Update the credential in every embedding site and deploy each one.
  4. Only once every site is confirmed on the new credential, disable the old registry entry — deleting it first takes every un-migrated site down.

npm

pnpm add @uengage.io/browser-sdk
import { createClient } from '@uengage.io/browser-sdk/realtime';

Apps that already bundle can equally use @uengage.io/platform-sdk/realtime directly; this package adds the token lifecycle and transport fallback on top.

Publishing

| Target | Trigger | Goes to npm | Overwrite | | ------------- | ------------------------------------------------------------- | ----------- | -------------------------------------------------------- | | prod | tag browser-sdk-v<version> | yes | never — versions are immutable | | uat / any env | Publish browser-sdk workflow, Run workflow → pick the env | no | allowed, so a branch can be re-published while iterating |

The dispatch path exists to put a build in front of someone on UAT before it is tagged. It never publishes to npm — a tag is the only way onto npm. Both paths assume the target environment's existing AWS_DEPLOY_ROLE_ARN, so publishing needs no IAM of its own.

Overwriting on a non-prod CDN invalidates any SRI hash taken from an earlier publish of that same version, so re-copy the hash from the job summary after each dispatch.

Adding a bundle for another service

  1. src/bundles/<service>/index.ts, exporting createClient.
  2. An entry in bundles.config.json.
  3. Tests under test/bundles/<service>/.
  4. An exports entry in package.json, and tag browser-sdk-v<next>.

No new package, workflow, CI job, IAM role, CDN behaviour or DNS record — the distribution serves /browser-sdk/** and the release job walks the manifest. Everything in src/core/ (auth, refresh, HTTP retry, errors) is already shared.

All bundles ship under one version. Published paths are immutable and never deleted, so a version bump nobody needed costs nobody anything.

Local

pnpm --filter @uengage.io/browser-sdk test
pnpm --filter @uengage.io/browser-sdk build
pnpm --filter @uengage.io/browser-sdk check:browser-safe   # asserts what ships
pnpm --filter @uengage.io/browser-sdk sri                  # sizes + <script> tags

realtime-demo/ at the repo root is a working harness against uat: pnpm check verifies the environment (OIDC discovery, the snapshot route, credentials) and pnpm dev serves a page you can point at a locally built bundle.