Stop stolen session cookies from being replayed.
dbsc-toolkit is the server-side Device Bound Session Credentials (DBSC) library for Node.js. The cookie still copies. The device key it needs to refresh does not. A session lifted to another machine dies on the next request.
Only the device that registered the key gets through.
The problem
A session cookie trusts whoever holds it
That single property drives most account takeovers. Infostealer malware lifts the cookie off disk, the attacker pastes it into their own browser, and they are you. No password, no MFA prompt. DBSC binds the session to a key that never leaves the device, so a copied cookie has nothing to sign with.
// 1. at login, the browser mints a keypair in the TPM
// and registers the public half. the private key
// never leaves the chip.
// 2. every refresh and every guarded request needs a
// fresh signature from that key.
// 3. a replayed cookie from another device cannot sign,
// so the server demotes it:
tier: "dbsc" // real device, verified
tier: "none" // replay rejected, 403
The protocol
How the binding holds
The server is reactive. It sets one header after login and answers two endpoints; the browser drives the rest. You never write the protocol routes.
Key born in hardware
At login the browser creates an EC P-256 keypair in the TPM or Secure Enclave and registers the public half. The private key never leaves the chip.
Proof every cycle
The bound cookie is short-lived. Before each renewal the browser signs a fresh server challenge. A device without the key cannot refresh.
One call per route
requireProof() demands a per-request signature on sensitive routes, closing the gap between refreshes. Same guard on every browser.
Coverage
Every browser, one server
Native DBSC is Chromium-only: Windows from Chrome 145 (TPM), macOS from Chrome 147 (Secure Enclave). The Web Crypto polyfill covers everything else, so you read one tier and don't branch per browser. Run native-only with bound: false.
Hardware key in the TPM or Secure Enclave. Chromium on Windows (Chrome 145+) or macOS (Chrome 147+).
Non-extractable IndexedDB key. Firefox, Safari, older Chromium.
Unbound or stale session. The bare cookie, like any normal app.
Adapters
DBSC for any Node.js framework
Every adapter is a thin shell over the same framework-agnostic core. Pick yours, or wire the core into anything when no adapter exists.
DBSC for Express
dbsc-toolkit/expressThe reference adapter. install() mounts the protocol routes; bind() starts a session.
const dbsc = createDbsc({ storage });
dbsc.install(app);
await dbsc.bind(res, sessionId, { userId });
DBSC for Fastify
dbsc-toolkit/fastifyA plugin that registers @fastify/cookie and the protocol routes for you.
const dbsc = createDbsc({ storage });
await dbsc.install(app);
await dbsc.bind(reply, sessionId, { userId });
DBSC for Hono
dbsc-toolkit/honoWeb-standard middleware. Runs on Node, Bun, Deno and Cloudflare Workers.
const dbsc = createDbsc({ storage });
dbsc.install(app);
await dbsc.bind(c, sessionId, { userId });
DBSC for Next.js
dbsc-toolkit/nextjsApp Router. Export the middleware, read the session inside route handlers.
// middleware.ts
export default dbsc.middleware();
// route: const s = await dbsc.getSession(req);
DBSC for NestJS
dbsc-toolkit/nestjsA module mounts the protocol; DbscGuard protects routes; DbscService binds.
imports: [DbscModule.forRoot({ storage })]
// @UseGuards(DbscGuard) on a route
// this.dbsc.bind(res, sessionId, { userId })
DBSC for Koa
dbsc-toolkit/koaMiddleware over Koa's raw request and response objects.
const dbsc = createDbsc({ storage });
dbsc.install(app);
await dbsc.bind(ctx, sessionId, { userId });
DBSC for SvelteKit
dbsc-toolkit/sveltekitA handle hook answers the routes; bind and guard inside actions.
// hooks.server.ts
export const handle = dbscHandle({ storage });
// await requireProof()(event) in a +server
DBSC for Electron
dbsc-toolkit/electronThe Electron main process plays server for your renderer. A bridge mounts the protocol on a custom scheme via protocol.handle; the renderer reaches the bound tier with the client SDK (native dbsc needs an https origin and hardware key store Electron rarely has).
const dbsc = createElectronDbsc({ storage });
const handleDbsc = dbsc.protocolHandler();
protocol.handle("app", (request) => handleDbsc(request));
Any other server, via raw node:http or the core
dbsc-toolkit/nodeNo adapter for your framework? The generic node:http handler works with any server that exposes the Node request and response objects, and the framework-agnostic core wires into anything.
const handle = dbsc.handler();
createServer(async (req, res) => {
if (await handle(req, res)) return; // protocol route answered
// ... your routes; read getDbscSession(req)
});
Install
Install and bind in minutes
One dependency, optional framework and storage peers. Memory storage to start; Redis or Postgres for production.
// guard a sensitive route. a replayed cookie
// from another device is rejected on arrival.
app.post("/payment", dbsc.requireProof(), (req, res) => {
res.json({ ok: true, tier: res.locals.dbsc.tier });
});
Documentation
Learn the protocol
Full documentation on this site, plus deep dives written from shipping it against real hardware.
Getting started
Zero to a bound session on Express in under five minutes, with the failure modes called out.
Open guideHow DBSC works
The whole protocol end to end, framework-free. Registration, refresh, the tier model, and what it protects.
Read the explainerThe threat boundary
What DBSC stops and what it does not. An honest read you can hand to a security review.
See the limitsSpecification
A spec, not just a library
The wire protocol is written up as a language-neutral specification with round-trip test vectors. Node is the reference implementation; the spec is what lets a conforming DBSC server be built in any language.
Header grammar, JWS and JSON shapes, the storage and cookie contracts, and error codes. Read the spec
Known inputs and expected outputs, so any implementation self-checks without a browser. Test vectors
Tested against Chrome 147 on real TPM 2.0 hardware, including the refresh cycle. How it works