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

@endstate-sdk/reader

v0.2.0

Published

Read Endstate chips with NFC readers in the browser - TapTrack Tappy (Web Serial / WebUSB) and Web NFC behind one interface.

Readme

@endstate-sdk/reader

Read Endstate chips from your own application. One interface across supported NFC readers - a TapTrack Tappy plugged into a computer, or a phone's built-in NFC - with zero runtime dependencies.

npm install @endstate-sdk/reader

Quick start

import { pickReader } from "@endstate-sdk/reader";

const reader = pickReader(); // best available reader, or null
if (!reader) throw new Error("No supported NFC reader in this browser.");
await reader.connect(); // picker on the first visit, silent after; call from a click
await reader.start({
  onTap: ({ url, chipId, e, c }) => {
    if (!chipId || !e) return; // tag carried some other URL
    // chipId/e/c are the values the chip endpoints take - send chipId
    // as chip_id, with your unit_id, and the pairing body is complete.
  },
  onError: (message) => console.warn(message),
});

A tap yields an identified chip:

| Field | Shape | Meaning | | --- | --- | --- | | url | string | Full URL read from the tag | | chipId | ^[0-9A-F]{10}$ | The chip's id - the API's chip_id | | e | ^[0-9A-F]{32}$ | Single-use encrypted tap payload | | c | ^[0-9A-F]{16}$ | Tap verification code, when the tag provides one - forward it with e |

chipId and e are present when the tag carries an Endstate chip URL (both the /verify/{chipId}?e= and /u/{chipId} forms), and absent otherwise; c rides along whenever the tag includes it. parseChipUrl(url) and identifyTap(tap) are exported for URLs and taps you obtained some other way.

Pairing chips: the two-legged flow

Reader hardware runs in the browser (Web Serial / WebUSB / Web NFC). Endstate API keys (end_sk_…) are server-side only and must never reach the browser. So pairing is always two legs:

// Operator UI (browser): tap → hand the identified chip to YOUR backend
onTap: ({ chipId, e, c }) =>
  fetch("/api/pair", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ chipId, e, c, unitId }),
  });

// Your backend (holds end_sk_): create the unit, then pair the chip
// POST /v1/units  { collection_id, external_id?, name? }  → unit.id
// POST /v1/chips  { unit_id, chip_id: chipId, e, c }

Readers and environments

pickReader() picks the best supported driver:

  • Phone NFC (WebNfcReader) - Android Chrome. No extra hardware.
  • Tappy over Web Serial (WebSerialTappyReader) - Chrome/Edge desktop. Preferred for the Tappy: rides the OS serial driver, works on Windows, macOS, and Linux.
  • Tappy over WebUSB (RawTappyReader) - fallback. On Windows the OS serial driver owns the device, so WebUSB open() is denied - Web Serial is used there instead.

Connecting once, not every time

The browser remembers which reader an origin may use, so picking a device is a one-time authorization, not part of connecting. @endstate-sdk/reader treats it that way:

// Silent. Reconnects to a reader this site is already authorized to use.
// No prompt, no user gesture, safe to call on page load.
await reader.connect({ prompt: false });

// The pairing step. Always opens the browser device chooser, so it must run
// in a click handler.
await reader.requestDevice?.();

// Plain connect() does both: silent if possible, chooser if this site has no
// reader yet. Call it from a click so the fallback is allowed to prompt.
await reader.connect();

stopScanning() stops delivering taps and leaves the device connected, so the next scan starts instantly and without a prompt - that is what a component teardown wants. disconnect() is the explicit release, for when another tab or a desktop app needs the reader. stop() still means "stop and release" as it always has; it is now a deprecated alias for disconnect().

Drive the UI off the connection state rather than guessing:

type NfcConnectionState =
  | "disconnected"
  | "authorization-required" // no reader paired yet: show "Connect reader"
  | "connecting"
  | "connected"
  | "error";

reader.onConnectionStateChange?.((state) => render(state));

Only authorization-required needs a user gesture. Every other state either resolves itself (a replugged reader reconnects on its own while the browser still holds the grant) or needs a different fix, so a "Connect reader" button gated on that one state disappears once a reader is paired. It comes back if the browser drops the grant: a different origin, an incognito window, or a reader that reports no serial number after an unplug or a browser restart.

pickReader() is memoized per document: the reader owns an open device connection that has to outlive a component remount or an SPA navigation. Use it as your single entry point. One document holds one reader per device - if you construct a driver yourself (new WebSerialTappyReader()), keep one instance alive at a time. Two instances that resolve the same reader share one byte stream and one command channel, so the last to connect takes the taps and either one's disconnect() releases the device for both.

Phone NFC permissions

Web NFC permission is per origin. If you move your operator flow to a new domain, every operator is prompted again on the new origin. A denied permission never re-prompts: the operator must re-enable NFC via the address-bar lock icon → Permissions. Chrome also refuses to show the prompt while another app draws a screen overlay. Taps surface these states through onError with operator-actionable messages.

Subpath exports

  • @endstate-sdk/reader - readers + chip URL parsing (browser)
  • @endstate-sdk/reader/react - useNfcReader hook (requires React ≥ 19, optional peer dependency)
  • @endstate-sdk/reader/ndef - NDEF URI parsing (pure, runs anywhere)
  • @endstate-sdk/reader/tcmp - TapTrack TCMP protocol helpers (pure, runs anywhere)

React

import { useNfcReader } from "@endstate-sdk/reader/react";

const {
  isScanning,
  error,
  needsAuthorization,
  startScanning,
  stopScanning,
  requestDevice,
} = useNfcReader({
  onTap: ({ chipId, e, c }) => {
    /* … */
  },
  // Scan as soon as an already-authorized reader connects, with no click.
  // Without this, a return visit connects silently and then sits idle.
  autoStartWhenGranted: true,
});

// Pairing: only while the browser has not been given a reader yet.
{
  needsAuthorization && <button onClick={requestDevice}>Connect reader</button>;
}
// Scanning: for a reader that is connected but stopped.
{
  !needsAuthorization &&
    (isScanning ? (
      <button onClick={stopScanning}>Stop</button>
    ) : (
      <button onClick={startScanning}>Start scanning</button>
    ));
}

The hook owns the reader lifecycle: connect/start sessions, per-tag debounce, permission surfacing, and an optional mock mode for development. On mount it reconnects silently to an already-authorized reader (autoConnect, default true), so remounts, SPA navigation, and refreshes show no prompt.

For AI agents

This package ships an AGENTS.md (in the install root, next to this README) with the integration rules that prevent real mistakes - secret-key handling, the single-use e contract, and the browser/hardware support matrix. Endstate docs are agent-readable at https://docs.endstate.io/llms.txt, and every docs page is searchable through the MCP endpoint at https://docs.endstate.io/mcp.