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

tether-sessions

v0.1.0

Published

Device-bound sessions: a stolen session token is useless without the device's non-extractable key. DPoP-style proof-of-possession as a drop-in middleware for any backend.

Readme

Tether — device-bound sessions

A stolen session token is useless without the device's key.

A bearer token is "whoever holds it, wins" — so stealing the cookie (infostealer, XSS, leaked log, MITM) hands the attacker your session, straight past MFA. Tether binds each token to a non-extractable device key. Every request carries a fresh proof-of-possession (PoP) signed by that key. Steal the token and, without the private key that never left the real device, it's dead on any other machine.

This is the DPoP (RFC 9449) family. Google is shipping the close cousin DBSC in Chrome. Tether's niche: a drop-in for stacks outside Google — ~5 lines on the client, one middleware on the server, any backend, self-hosted.

Status: working prototype. Core, browser client, Express middleware, Redis store, multi-device + rotation, and a nonce challenge — all tested. Zero runtime dependencies. Not yet published to npm.

Integration in ~5 lines

Browser (client) — the key is generated extractable:false and lives in IndexedDB; JS can ask it to sign but can never read it out:

import { Device } from 'tether-sessions/client';

const device = await Device.loadOrCreate();
await fetch('/login', { method: 'POST', body: JSON.stringify({ publicJwk: device.publicJwk }) });

// on every request:
const proof = await device.createProof({ method: 'GET', url, token });
fetch(url, { headers: { Authorization: `Bearer ${token}`, PoP: proof } });

Server (Express):

const tether = require('tether-sessions/express')();

app.post('/login', async (req, res) => {
  // ...authenticate the user your own way (password, OAuth)...
  const token = await tether.issueSession(res, req.body.publicJwk); // binds token to key
  res.json({ token });
});

app.get('/me', tether.protect(), (req, res) => res.json({ ok: true })); // now proof-gated

That's it. Every request to a protect()ed route now needs a valid, fresh proof.

What verify() catches

| # | Attack | Rejected by | |---|--------|-------------| | 1 | Token replayed with no proof | missing proof-of-possession | | 2 | Proof signed with the attacker's own key | thumbprint mismatch (RFC 7638) | | 3 | A captured proof replayed verbatim | jti replay cache | | 4 | A /me proof reused against /transfer | htm / htu binding | | 5 | A proof minted for a different token | ath (token hash) binding | | 6 | An expired / pre-stamped proof | iat window + optional server nonce |

Every one of these still assumes the token was already stolen — and every time it's useless without the private key.

Pieces

| Module | What | |--------|------| | tether-sessions (index) | Device, Server, MemoryStore, RedisStore, Accounts, express | | tether-sessions/client | Browser WebCrypto client (ES module), non-extractable key in IndexedDB | | tether-sessions/express | protect() middleware + issueSession() login helper | | tether-sessions/store | MemoryStore (default, TTL) and RedisStore (multi-node, atomic replay via SET NX EX) | | tether-sessions/accounts | Multiple devices per user + key rotation | | browser-demo.html | Self-contained demo: runs all six steal scenarios in your browser |

Multi-node

const { Server, RedisStore } = require('tether-sessions');
const server = new Server({ store: new RedisStore(redisClient), nonceSecret: process.env.TETHER_SECRET });

Token bindings get the session TTL; the replay cache uses SET NX EX (atomic, no cross-process race). Share nonceSecret across nodes so any node can validate a nonce another issued.

Multiple devices & key rotation

const { Accounts } = require('tether-sessions');
const accounts = new Accounts(server);
await accounts.addDevice('user:42', laptopJwk);
await accounts.addDevice('user:42', phoneJwk);
const t2 = await accounts.rotateKey('user:42', oldToken, newLaptopJwk);
await accounts.revokeAll('user:42'); // log out everywhere

Optional server nonce (anti pre-stamping)

const server = new Server({ requireNonce: true });

On a nonce-less proof the middleware answers 401 with a Tether-Nonce header; the client re-signs including that nonce and retries. Nonces are stateless (HMAC over a timestamp), reusable within their TTL, and unforgeable without the secret.

Performance

The whole pitch depends on the per-request cost being negligible. On Node v24 (Ed25519, MemoryStore):

                p50       p99       (microseconds)
createProof     45        143
verify         185        308

verify p99 ≈ 0.31 ms — well under the 1 ms target — ≈5,000 verifies/sec/core. Run it yourself: npm run bench.

Algorithm

  • Browser: ECDSA P-256 (ES256). Universal WebCrypto support across every non-Chrome / self-hosted target — the whole point of Tether. (Ed25519 in WebCrypto only landed recently and unevenly.)
  • Native / Node devices: Ed25519 (EdDSA).
  • The server accepts both, and pins the algorithm to the key type, so a proof can't claim ES256 over an Ed25519 key.

Try it live (end-to-end)

A real HTTP server (server.js, zero deps) protecting /me and /transfer, with requireNonce:true, serving a browser client from the same origin:

npm start                    # -> http://localhost:8787

Open it and hit Run full end-to-end flow. You'll watch, over real fetch:

POST /login            -> token bound to the device key
GET  /me   (no nonce)  -> 401 missing nonce  + Tether-Nonce header
GET  /me   (nonce)     -> 200                 (client auto-retried)
GET  /me   (no proof)  -> 401 missing proof-of-possession   [stolen token alone]
GET  /me   (wrong key) -> 401 proof key != bound device key  [attacker's key]

Run the tests

npm test          # demo + express + store + advanced + live server (63 checks)
npm run bench     # per-request overhead
# standalone browser demo (no server): open browser-demo.html via any static host
python -m http.server 8722   # then open http://localhost:8722/browser-demo.html

Requires Node 18+ (Ed25519 in crypto). Verified on Node v22 and v24.

License

MIT