dbsc-toolkit 2.9.9 → 2.9.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +124 -256
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,62 +1,60 @@
1
- # DBSC Toolkit
1
+ <p align="center">
2
+ <img src="./assets/banner.png" width="880" alt="DBSC Toolkit">
3
+ </p>
2
4
 
3
- [![npm](https://img.shields.io/npm/v/dbsc-toolkit.svg)](https://www.npmjs.com/package/dbsc-toolkit)
4
- [![CI](https://github.com/SulimanAbdulrazzaq/dbsc-toolkit/actions/workflows/ci.yml/badge.svg)](https://github.com/SulimanAbdulrazzaq/dbsc-toolkit/actions/workflows/ci.yml)
5
- [![License](https://img.shields.io/npm/l/dbsc-toolkit.svg)](./LICENSE)
6
- [![Node](https://img.shields.io/node/v/dbsc-toolkit.svg)](https://nodejs.org)
5
+ <h1 align="center">DBSC Toolkit</h1>
7
6
 
8
- > Stop stolen session cookies from being replayed on another device.
9
- >
10
- > `dbsc-toolkit` is a Node.js implementation of W3C [Device Bound Session Credentials](https://www.w3.org/TR/dbsc/) with a Web Crypto polyfill for browsers that don't speak the protocol natively yet. Native DBSC on Chromium 146+ (TPM on Windows, Secure Enclave on macOS); the polyfill covers Firefox, Safari, mobile, and older Chromium.
11
- >
12
- > Adapters: Express, Fastify, Hono, Next.js. Storage: in-memory, Redis, Postgres.
7
+ <p align="center">
8
+ <strong>Stop stolen session cookies from being replayed on another device.</strong><br>
9
+ W3C Device Bound Session Credentials for Node.js — with a Web Crypto polyfill so every browser is covered, not just Chrome.
10
+ </p>
13
11
 
14
- ```ts
15
- // Bind the session inside your existing login route:
16
- app.post("/login", async (req, res) => {
17
- await dbsc.bind(res, sessionId, { userId });
18
- res.json({ ok: true });
19
- });
12
+ <p align="center">
13
+ <a href="https://www.npmjs.com/package/dbsc-toolkit"><img src="https://img.shields.io/npm/v/dbsc-toolkit.svg" alt="npm"></a>
14
+ <a href="https://www.npmjs.com/package/dbsc-toolkit"><img src="https://img.shields.io/npm/dm/dbsc-toolkit.svg" alt="downloads"></a>
15
+ <a href="https://github.com/SulimanAbdulrazzaq/dbsc-toolkit/actions/workflows/ci.yml"><img src="https://github.com/SulimanAbdulrazzaq/dbsc-toolkit/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
16
+ <img src="https://img.shields.io/badge/types-TypeScript-blue.svg" alt="TypeScript">
17
+ <a href="./LICENSE"><img src="https://img.shields.io/npm/l/dbsc-toolkit.svg" alt="License"></a>
18
+ <a href="https://nodejs.org"><img src="https://img.shields.io/node/v/dbsc-toolkit.svg" alt="Node"></a>
19
+ </p>
20
20
 
21
- // Guard sensitive routes:
22
- app.post("/payment", express.raw({ type: "*/*" }), requireProof(), paymentHandler);
23
- ```
21
+ ## The problem
24
22
 
25
- Key generation, the protocol routes, cookie management, and the polyfill all live inside the library.
23
+ An attacker steals a session cookie — XSS, infostealer malware, a leaked log, a compromised proxy. The moment they have it, they paste it into their own browser and they're you. `HttpOnly` didn't matter. `Secure` didn't matter. Rotating the token only shortens the window; it doesn't close it.
26
24
 
27
- ## Contents
25
+ ## The solution
26
+
27
+ DBSC binds the session to a cryptographic key generated on the user's device at login. The cookie is still stealable — but every refresh, and every guarded request, needs a signature from that key. An attacker on another machine has nothing to sign with. The replay 403s.
28
+
29
+ ```
30
+ Browser Server
31
+ ─────── ──────
32
+ sign in ─────────────────────────────▶ issue session + Secure-Session-Registration
33
+ generate keypair (TPM / IndexedDB)
34
+ register public key ──────────────────▶ store key, bind to session
35
+ ◀────────────────── bound session cookie
36
+ ...later, on a guarded request:
37
+ sign challenge with private key ──────▶ verify signature → 200
38
+ stolen cookie, no key ──────▶ 403 ✋
39
+ ```
28
40
 
29
- - [Install](#install)
30
- - [Quick start](#quick-start)
31
- - [Live demo](#live-demo)
32
- - [Why dbsc-toolkit](#why-dbsc-toolkit)
33
- - [Comparison](#comparison)
34
- - [Why not JWT?](#why-not-just-use-jwt)
35
- - [How it works](#how-it-works)
36
- - [Who is this for?](#who-is-this-for)
37
- - [Adding to an existing app](#adding-to-an-existing-app)
38
- - [Protect your routes](#protect-your-routes)
39
- - [Production checklist](#production-checklist)
40
- - [API](#subpath-imports)
41
- - [Security model](#security-model)
42
- - [Going deeper](#going-deeper)
41
+ <p align="center">
42
+ <img src="./assets/demo.gif" width="720" alt="Login → bound → stolen-cookie replay → 403">
43
+ </p>
43
44
 
44
- ## Why dbsc-toolkit
45
+ ### What a stolen cookie gets the attacker
45
46
 
46
- Native DBSC ships on Chrome 146+ for Windows and macOS users with a hardware key store. That's roughly a third of the desktop traffic most apps see. Most other DBSC implementations focus on native browser support only, which leaves the rest of your users on plain bearer cookies.
47
+ <p align="center">
48
+ <img src="./assets/what-stealer-sees.png" width="760" alt="Attacker replays the stolen cookie from another machine and the request is rejected">
49
+ </p>
47
50
 
48
- This library ships a Web Crypto polyfill alongside native DBSC, so non-Chromium browsers get the same wire-level protection against cookie theft and the same `requireProof()` guard on the server. The polyfill key is stored non-extractably in IndexedDB rather than a TPM — there is no biometric prompt or any user interaction.
51
+ The cookie copies fine. The key doesn't — it never left the user's device. So the replayed request fails the proof check.
49
52
 
50
- What you actually get:
53
+ ## Live demo
51
54
 
52
- - Native W3C DBSC on Chromium 146+ (TPM on Windows, Secure Enclave on macOS).
53
- - A Web Crypto polyfill that brings the same protection to Firefox, Safari, mobile and older Chromium. Non-extractable IndexedDB key.
54
- - Adapters for Express, Fastify, Hono and Next.js — all four in one package.
55
- - Storage adapters for memory, Redis and Postgres. Swap by changing one import.
56
- - `requireProof()` — a per-request signature guard that works on every browser and defends against MITM body substitution via the signed body hash.
57
- - Replay cache (since v2.8) that rejects a captured-proof replay inside the timestamp window. Opt-in; you wire `RedisReplayCache` when you want it.
58
- - Multi-subdomain binding (since v2.9) via `cookieScope: "site"` for apps split across `app.example.com` + `api.example.com`.
59
- - Apache 2.0.
55
+ **<https://dbsc-toolkit.onrender.com/>** — sign in, then hit the **"simulate stolen cookie"** button. It fires a bare request with the bound cookie and no proof, and comes back `403 PROOF_MISSING`. That's the whole library in one button.
56
+
57
+ Chromium 145+ lands on `tier: "dbsc"` (TPM-backed); Firefox/Safari land on `tier: "bound"` (Web Crypto polyfill). Same guard either way.
60
58
 
61
59
  ## Install
62
60
 
@@ -64,7 +62,7 @@ What you actually get:
64
62
  npm install dbsc-toolkit
65
63
  ```
66
64
 
67
- Optional peer dependencies — install only what you actually use:
65
+ Framework and storage drivers are optional peer deps — install only what you use:
68
66
 
69
67
  ```sh
70
68
  npm install express ioredis # Express + Redis
@@ -73,8 +71,6 @@ npm install fastify @fastify/cookie pg # Fastify + Postgres
73
71
 
74
72
  ## Quick start
75
73
 
76
- `createDbsc()` takes your config once. `install()` mounts the protocol routes, the bound-route JSON parser, the `/dbsc-client` SDK, and sets `trust proxy`.
77
-
78
74
  ```ts
79
75
  import express from "express";
80
76
  import { randomUUID } from "node:crypto";
@@ -82,21 +78,20 @@ import { createDbsc } from "dbsc-toolkit/express";
82
78
  import { MemoryStorage } from "dbsc-toolkit/storage/memory";
83
79
 
84
80
  const app = express();
85
- app.use(express.json()); // for your own routes' JSON bodies
81
+ app.use(express.json());
86
82
 
87
- const dbsc = createDbsc({ storage: new MemoryStorage() }); // swap for Redis/Postgres in prod
88
- dbsc.install(app);
83
+ const dbsc = createDbsc({ storage: new MemoryStorage() }); // Redis/Postgres in prod
84
+ dbsc.install(app); // mounts protocol routes + SDK
89
85
 
90
86
  app.post("/login", async (req, res) => {
91
- await dbsc.bind(res, randomUUID(), { userId: req.body.username });
87
+ await dbsc.bind(res, randomUUID(), { userId: req.body.username }); // the one new line
92
88
  res.json({ ok: true });
93
89
  });
94
90
 
95
- app.get("/me", (_req, res) => res.json(res.locals.dbsc));
96
- app.listen(3000);
91
+ app.post("/payment", express.raw({ type: "*/*" }), dbsc.requireProof(), payHandler);
97
92
  ```
98
93
 
99
- Load the polyfill in your HTML so Firefox / Safari / older Chromium can reach `tier: "bound"`:
94
+ Load the polyfill in your HTML so non-Chromium browsers reach `tier: "bound"`:
100
95
 
101
96
  ```html
102
97
  <script type="module">
@@ -105,249 +100,122 @@ Load the polyfill in your HTML so Firefox / Safari / older Chromium can reach `t
105
100
  </script>
106
101
  ```
107
102
 
108
- Without the script those browsers stay on `tier: "none"`. Chromium 146+ doesn't need it — it negotiates the protocol on its own from the headers `dbsc()` sets.
109
-
110
- ### Common failure modes
111
-
112
- - **`tier` always reads `"none"` on Chromium 146+** — running on plain HTTP (DBSC needs HTTPS), or `dbsc.install(app)` was never called. `install()` sets `trust proxy` and parses cookies, so the old "middleware order" class of bug is gone.
113
- - **Chrome loops registration** — storage got wiped. Move off `MemoryStorage` to Redis or Postgres before deploying anywhere that restarts. I learned this the hard way on Render free tier.
114
- - **Tier flips back to `"none"` right after login** — the race between `/login` returning and the browser running `POST /dbsc/registration`. Poll `/me` for ~1 s after login, or await the bound-SDK outcome promise. The demo wires both — see [examples/express/src/server.js](./examples/express/src/server.js).
115
- - **Firefox / Safari still on `"none"`** — the `<script type="module">` tag is missing. `install()` serves the SDK at `/dbsc-client`; you still load it on the page.
116
-
117
- Walk-through: [docs/getting-started.md](./docs/getting-started.md).
103
+ Full walk-through, failure modes, and migration timeline: **[docs/getting-started.md](./docs/getting-started.md)** and **[docs/integrating-existing-auth.md](./docs/integrating-existing-auth.md)**.
118
104
 
119
105
  ### Not on Express?
120
106
 
121
- `createDbsc()` is the Express convenience layer. The protocol itself — native DBSC, the polyfill, per-request proof verification — lives in a framework-agnostic core that takes plain data and a `StorageAdapter`, nothing else. The Express, Fastify, Hono, and Next.js adapters are thin wrappers over it. If you're on a different runtime (or wiring DBSC into your own framework's session layer), import the core handlers directly:
107
+ The protocol lives in a framework-agnostic core — plain functions plus a `StorageAdapter`, no HTTP-layer assumptions. The Express/Fastify/Hono/Next.js adapters are thin wrappers. Build on the core directly for any runtime:
122
108
 
123
109
  ```ts
124
110
  import {
125
- handleRegistration, // POST /dbsc/registration
126
- handleRefresh, // POST /dbsc/refresh
127
- handleBoundRegistration, handleBoundRefresh, // the polyfill routes
128
- verifyBoundProof, // your requireProof() equivalent
129
- issueChallenge,
130
- buildRegistrationHeader,
131
- type StorageAdapter, // the only interface you implement
111
+ handleRegistration, handleRefresh,
112
+ handleBoundRegistration, handleBoundRefresh,
113
+ verifyBoundProof, // your requireProof() equivalent
114
+ issueChallenge, buildRegistrationHeader,
115
+ type StorageAdapter, // the only interface you implement
132
116
  } from "dbsc-toolkit";
133
117
  ```
134
118
 
135
- These are the same functions every shipped adapter calls. A complete raw-`http` integration built on them — both protocol surfaces, every header and cookie detail — is in [docs/adapters.md](./docs/adapters.md#writing-your-own-adapter).
136
-
137
- ## Live demos
138
-
139
- | URL | Stack | Source |
140
- |---|---|---|
141
- | <https://dbsc-toolkit.onrender.com/> | Express + raw `dbsc-toolkit` | [examples/express/](./examples/express/) |
142
- | <https://dbsc-better-auth-demo.onrender.com/> | Hono + Better Auth + `@dbsc-toolkit/better-auth` | [examples/better-auth/](./examples/better-auth/) |
143
-
144
- Sign up, log in, click **Check session**. Chromium 145+ lands on `tier: "dbsc"` within a second; Firefox/Safari land on `tier: "bound"` within ~3 seconds. Open DevTools Network and watch the binding flow.
145
-
146
- ## Comparison
147
-
148
- | | Plain cookies | JWT (bearer) | Native DBSC (Chrome 146+) | **dbsc-toolkit** |
149
- |----------------------------|:-------------:|:------------:|:-------------------------:|:----------------:|
150
- | Stops cookie / token replay from another device | ❌ | ❌ | ✅ | ✅ |
151
- | Works on Chrome / Edge / Brave | ✅ | ✅ | ✅ (Windows + TPM) | ✅ |
152
- | Works on Firefox | ✅ | ✅ | ❌ | ✅ (polyfill) |
153
- | Works on Safari | ✅ | ✅ | ❌ | ✅ (polyfill) |
154
- | Works on mobile / no-TPM | ✅ | ✅ | ❌ | ✅ (polyfill) |
155
- | Per-request body-hash proof against MITM | ❌ | ❌ | ❌ (TPM key isn't reachable from JS) | ✅ |
156
- | Captured-proof replay defense | n/a | ❌ | n/a | ✅ (v2.8 replay cache) |
157
- | Multi-subdomain binding | ✅ (loose) | ✅ (loose) | ❌ (`__Host-` only) | ✅ (v2.9 `cookieScope: "site"`) |
158
- | Server runtime | any | any | n/a (browser-side) | Node.js ≥ 20 |
159
-
160
- The native-DBSC column above describes a server that uses the spec without any polyfill. The polyfill key in this library lives in IndexedDB (`extractable: false`) rather than a TPM — a notch weaker against malware reading the browser profile, but it stops every remote-theft scenario the same way native DBSC does.
161
-
162
- ## "Why not just use JWT?"
163
-
164
- JWTs are bearer tokens. If stolen, they can be replayed from another device. DBSC adds proof-of-possession: the browser has to prove on every refresh (and on every `requireProof()`-guarded request) that it still has the private key bound at login. An attacker who got the token alone has nothing to sign with.
165
-
166
- It complements your existing auth — passwords, MFA, session cookies, JWTs — rather than replacing any of them. The gap it closes is *replay after issue*.
167
-
168
- ## How it works
169
-
170
- ```
171
- Login → Key Registration → Cookie Issued → Challenge → Signature Verification → Refresh
172
- ```
173
-
174
- On login the server responds with a `Secure-Session-Registration` header. The browser generates an ECDSA keypair in the TPM (native DBSC) or IndexedDB (polyfill), POSTs the public key, and gets a short-lived session cookie. Every ~10 minutes the browser re-signs a fresh server challenge to renew the cookie. A copy of the cookie pasted into another device has no key — the next refresh fails, and any route guarded by `requireProof()` returns 403 immediately.
175
-
176
- Full wire-format walk-through: [HOW-IT-WORKS.md](./HOW-IT-WORKS.md).
177
-
178
- ## Who is this for?
119
+ Complete raw-`http` example: [docs/adapters.md](./docs/adapters.md#writing-your-own-adapter).
179
120
 
180
- Use `dbsc-toolkit` if:
121
+ ## How it compares
181
122
 
182
- - You run a Node.js backend.
183
- - Your users authenticate with session cookies (or JWT bearer tokens).
184
- - Cookie / token theft is in your threat model — XSS, infostealer malware, leaked logs, compromised proxies.
185
- - You want coverage on Chrome and Firefox / Safari now, not eventually.
123
+ | | Plain cookies | JWT (bearer) | Native DBSC (Chrome 146+) | **dbsc-toolkit** |
124
+ |---|:---:|:---:|:---:|:---:|
125
+ | Replay-resistant (stolen cookie from another device) | ❌ | ❌ | ✅ | ✅ |
126
+ | Works on Chrome / Edge / Brave | ✅ | ✅ | ✅ (TPM) | ✅ |
127
+ | Works on Firefox / Safari / mobile / no-TPM | ✅ | ✅ | ❌ | ✅ (polyfill) |
128
+ | Per-request body-hash proof vs MITM | ❌ | ❌ | ❌ | ✅ |
129
+ | Captured-proof replay defense | n/a | ❌ | n/a | ✅ (replay cache) |
130
+ | Multi-subdomain binding | loose | loose | ❌ | ✅ (`cookieScope: "site"`) |
131
+ | Better Auth integration | n/a | n/a | n/a | ✅ (plugin) |
186
132
 
187
- Not the right fit if:
133
+ DBSC complements your existing auth (passwords, MFA, sessions, JWTs) — it closes *replay after issue*, the gap none of them cover. More: [docs/security/threat-model.md](./docs/security/threat-model.md).
188
134
 
189
- - You don't issue any persistent client credential (every request re-authenticates from scratch).
190
- - You can't deploy HTTPS — DBSC cookies require `Secure`, and the spec rejects insecure origins.
191
- - Your backend is Python / Go / Rust / Java / .NET — no port exists yet; you'd implement against the [W3C spec](https://www.w3.org/TR/dbsc/) directly.
192
- - You need DBSC on iOS / mobile Chrome / ChromeOS *with native key storage* — those platforms don't ship native DBSC. The polyfill still works there; the key just lives in IndexedDB instead of secure hardware.
135
+ ## Security at a glance
193
136
 
194
- ## Adding to an existing app
137
+ Defended:
195
138
 
196
- You don't rewrite login and you don't migrate the session store. DBSC sits alongside your existing session cookie and binds to the same session id.
139
+ - ✅ Cookie theft replayed from another device
140
+ - ✅ Stolen bearer tokens (same category)
141
+ - ✅ XSS reading `document.cookie`
142
+ - ✅ Network capture / TLS-stripping proxy
143
+ - ✅ Server log leakage
144
+ - ✅ MITM body substitution (signed body hash)
197
145
 
198
- ```ts
199
- import { createDbsc, requireProof } from "dbsc-toolkit/express";
200
- import { RedisStorage } from "dbsc-toolkit/storage/redis";
201
- import Redis from "ioredis";
146
+ Not defended (be honest about the boundary):
202
147
 
203
- // 1. configure the kit once — storage is the only required option
204
- const dbsc = createDbsc({ storage: new RedisStorage(new Redis(process.env.REDIS_URL)) });
148
+ - ⚠️ On-device malware reading the browser profile — only the `dbsc` tier (TPM) defeats this; the `bound` polyfill key is on disk
149
+ - ⚠️ Browser/OS compromise, rogue extension with `subtle.sign` access
205
150
 
206
- // 2. install once — mounts the protocol routes, trust proxy, the SDK
207
- dbsc.install(app);
151
+ Full STRIDE analysis: [docs/security/threat-model.md](./docs/security/threat-model.md) · best practices: [docs/security/best-practices.md](./docs/security/best-practices.md).
208
152
 
209
- // 3. one line in your existing /login, after the password check
210
- app.post("/login", async (req, res) => {
211
- const user = await yourPasswordCheck(req.body); // unchanged
212
- const sid = await issueYourOwnSession(user.id); // unchanged
213
- await dbsc.bind(res, sid, { userId: user.id }); // <- the new line
214
- res.json({ ok: true });
215
- });
216
-
217
- // 4. guard sensitive routes — one call. POST routes deliver raw bytes.
218
- app.post("/payment", express.raw({ type: "*/*" }), requireProof(), paymentHandler);
219
-
220
- // 5. one line in /logout
221
- app.post("/logout", async (req, res) => {
222
- await res.locals.dbsc.revoke(); // <- the new line
223
- await yourSessionStore.delete(req.cookies.sid); // unchanged
224
- res.json({ ok: true });
225
- });
226
- ```
227
-
228
- `install()` handles `trust proxy`, cookie parsing, the bound-route JSON parser, and the `/dbsc-client` static mount. You still need `express.json()` for your *own* routes' bodies. `sessionId` is whatever id your session store already issues — DBSC binds to it, no second id-space.
153
+ ## Protection tiers
229
154
 
230
- **No server-side session id?** (NextAuth JWT mode, iron-session, Lucia stateless) Call `dbsc.bind(res, { userId })` with no id — the kit derives a stable one and manages a per-device cookie so each browser of the same user binds independently. Per-system recipes: [docs/integration-recipes.md](./docs/integration-recipes.md).
155
+ Every session carries a `tier`. You don't gate on it directly — `requireProof()` does the enforcement — but it tells you how the binding was achieved on a given browser.
231
156
 
232
- **App split across subdomains?** (`app.example.com` + `api.example.com`) Default `__Host-` cookies are origin-locked — proxy `/dbsc/*` and `/dbsc-bound/*` through one origin if you can; that's the strongest setting. If a same-origin layout isn't workable, v2.9.0+ exposes `cookieScope: "site"` + `cookieDomain: "example.com"` on `createDbsc({...})`, switching the binding cookies to `__Secure-` with a `Domain` attribute. The validator throws at construction time if either is wrong — misconfiguration is loud, not silent. Trade-off and concrete recipe: [docs/integration-recipes.md#multi-subdomain-apps-cookiescope-site](./docs/integration-recipes.md#multi-subdomain-apps-cookiescope-site).
157
+ <p align="center">
158
+ <img src="./assets/tier.png" width="760" alt="dbsc (TPM-backed), bound (Web Crypto polyfill), and none">
159
+ </p>
233
160
 
234
- Full walk-through with `autoBind`, per-route policy, and the migration timeline: [docs/integrating-existing-auth.md](./docs/integrating-existing-auth.md).
161
+ `dbsc` is hardware-backed (TPM / Secure Enclave). `bound` is the Web Crypto polyfill (non-extractable IndexedDB key). `none` is an unbound or stale session. Detail: [HOW-IT-WORKS.md](./HOW-IT-WORKS.md).
235
162
 
236
- ### Using Better Auth?
163
+ ## Ecosystem
237
164
 
238
- Install [@dbsc-toolkit/better-auth](./packages/better-auth/) — sessions from every sign-in method (email, OAuth, magic link, passkey) get bound automatically:
165
+ | Package | What it is |
166
+ |---|---|
167
+ | `dbsc-toolkit` | Core + Express/Fastify/Hono/Next.js adapters, memory/Redis/Postgres storage |
168
+ | `dbsc-toolkit/client` | Browser SDK + Web Crypto polyfill |
169
+ | [`@dbsc-toolkit/better-auth`](./packages/better-auth/) | First-class [Better Auth](https://better-auth.com) plugin — binds every sign-in method automatically |
239
170
 
240
171
  ```ts
241
- // auth.ts
242
- import { betterAuth } from "better-auth"
243
- import { dbsc } from "@dbsc-toolkit/better-auth"
244
-
245
- export const auth = betterAuth({
246
- plugins: [dbsc()],
247
- })
172
+ // Better Auth, two lines:
173
+ import { dbsc } from "@dbsc-toolkit/better-auth" // auth.ts → plugins: [dbsc()]
174
+ import { dbscExpress } from "@dbsc-toolkit/better-auth/express" // server.ts → dbscExpress(auth).install(app)
248
175
  ```
249
176
 
250
- ```ts
251
- // server.ts
252
- import { dbscExpress } from "@dbsc-toolkit/better-auth/express"
253
-
254
- const dbsc = dbscExpress(auth)
255
- dbsc.install(app) // mounts /dbsc/*, /dbsc-bound/*, /dbsc-client/*
256
- app.all("/api/auth/*splat", toNodeHandler(auth))
257
- app.get("/profile", dbsc.requireProof(), profileHandler)
258
- ```
259
-
260
- ```html
261
- <script src="/dbsc-client/init.js" type="module"></script>
262
- ```
263
-
264
- Live demo: <https://dbsc-better-auth-demo.onrender.com/>. Full docs in the [package README](./packages/better-auth/README.md).
265
-
266
- ## Protect your routes
267
-
268
- After `createDbsc().install()`, every request through the middleware has a `tier` field on the request context. The library does not auto-protect anything — you add `requireProof()` to each route that matters.
177
+ ## Roadmap
269
178
 
270
- | Your route does… | Use this guard | What it stops |
271
- |---|---|---|
272
- | Public / read-only (feed, search, public profile) | Nothing | n/a — no auth gate at all |
273
- | Anything authenticated (post, comment, upvote, settings, payment, admin) | `requireProof()` (server) + `wrapFetch()` (client) | A stolen cookie can't be replayed from another device, can't ride along during the freshness window, and an MITM can't substitute a POST body |
179
+ - [x] Express, Fastify, Hono, Next.js adapters
180
+ - [x] Memory, Redis, PostgreSQL storage
181
+ - [x] Web Crypto polyfill (Firefox / Safari / mobile / older Chromium)
182
+ - [x] Per-request proof + body signing + replay cache
183
+ - [x] Multi-subdomain binding (`cookieScope: "site"`)
184
+ - [x] Better Auth plugin
185
+ - [ ] Koa / NestJS adapters
186
+ - [ ] Bun / Deno native paths
187
+ - [ ] Third-party security audit
188
+ - [ ] WebAuthn step-up integration
274
189
 
275
- `requireProof()` requires a bound device plus a per-request proof, and works on every browser. As of v2.7, Chromium sessions co-register a polyfill key alongside the TPM key so the per-request signature is enforced uniformly across tiers. `wrapFetch` signs the request body by default since v2.8; POST routes mount `express.raw()` in front. Apps with many guarded routes can swap to `installFetchInterceptor({ pathPrefixes: ["/api/secure/"] })` and wrap once at boot.
190
+ ## Used by
276
191
 
277
- For apps facing active MITM or log-spillage exposure, v2.8 also adds an optional **replay cache** that rejects a second arrival of the same proof bytes — pass `replayCache: new RedisReplayCache(redis)` to `createDbsc`. See [docs/per-request-signing.md](./docs/per-request-signing.md).
278
-
279
- Full threat boundary, per-framework wiring (Fastify / Hono / Next.js), and the migration timeline: [docs/integrating-existing-auth.md](./docs/integrating-existing-auth.md) and [docs/per-request-signing.md](./docs/per-request-signing.md).
280
-
281
- ## Production checklist
282
-
283
- - [ ] HTTPS enabled — DBSC requires `Secure` cookies; plain HTTP locks every user to `tier: "none"`.
284
- - [ ] Redis or PostgreSQL storage — `MemoryStorage` is lost on restart; bound browsers loop registration.
285
- - [ ] `requireProof()` on every authenticated route — read-only public routes need no guard.
286
- - [ ] `wrapFetch({ signBody: true })` on the client side for those routes (default since v2.8).
287
- - [ ] Replay cache enabled when relevant — `replayCache: new RedisReplayCache(redis)` in `createDbsc({...})`.
288
- - [ ] `trust proxy` set — `install()` sets it, but verify nothing in your own app config overrides it.
289
- - [ ] Polyfill script loaded — the `<script type="module">` tag; without it Firefox/Safari stay on `tier: "none"`.
192
+ Using DBSC Toolkit in production? Open a PR and add your project here.
290
193
 
291
194
  ## Subpath imports
292
195
 
293
196
  | Import | What it is |
294
- |--------|------------|
295
- | `dbsc-toolkit` | Core types, crypto, protocol functions |
296
- | `dbsc-toolkit/express` | Express middleware |
297
- | `dbsc-toolkit/fastify` | Fastify plugin |
298
- | `dbsc-toolkit/hono` | Hono middleware |
299
- | `dbsc-toolkit/nextjs` | Next.js App Router middleware + handlers |
300
- | `dbsc-toolkit/client` | Browser SDK with `initBoundDbsc()` for the polyfill |
197
+ |---|---|
198
+ | `dbsc-toolkit` | Core: types, crypto, protocol handlers, framework-agnostic |
199
+ | `dbsc-toolkit/express` · `/fastify` · `/hono` · `/nextjs` | Framework adapters |
200
+ | `dbsc-toolkit/client` | Browser SDK + polyfill |
301
201
  | `dbsc-toolkit/storage/{memory,redis,postgres}` | Storage adapters |
302
202
 
303
- Tree-shaking eliminates anything you don't import. Using Koa, Hapi, raw `http`, Bun, or Deno? Call core directly — see [docs/adapters.md](./docs/adapters.md).
304
-
305
- ## Protection tiers
306
-
307
- `tier` is the state of a session — what kind of binding the browser achieved. Don't gate on it directly; gate with `requireProof()` instead and read `tier` only for display.
308
-
309
- | Tier | Mechanism | Protects against |
310
- |------|-----------|------------------|
311
- | `dbsc` | Native W3C DBSC, key in TPM (Windows) or Secure Enclave (macOS) | Cookie theft (XSS, network, logs, paste-to-other-browser) **and** infostealer malware reading the browser profile |
312
- | `bound` | Web Crypto polyfill, non-extractable ECDSA P-256 key in IndexedDB | Cookie theft. Does not defeat infostealer malware on the user's machine. |
313
- | `none` | No active / fresh binding | Nothing the cookie itself doesn't already do |
314
-
315
- Why not gate routes on `tier === "dbsc"`? Because every Firefox and Safari user can only reach `tier: "bound"`, and `requireProof()` already gives those browsers the same per-request guarantee via a signed proof. The one exception is routes whose threat model specifically includes on-device infostealer malware — there, hardware-backed key isolation actually matters, and you accept the trade-off of locking non-Chromium browsers out. More in [docs/security/best-practices.md](./docs/security/best-practices.md).
316
-
317
- ## Security model
318
-
319
- What this protects against:
320
-
321
- - Stolen session cookies replayed from another device.
322
- - Stolen bearer tokens (same category — bearer == portable).
323
- - Cookie exfiltration via XSS, network capture, log leakage, or proxy leakage.
324
-
325
- What the Web Crypto polyfill (`tier: "bound"`) does not protect against:
326
-
327
- - Malware running on the user's machine with access to the browser profile.
328
- - Browser or OS compromise.
329
-
330
- Native DBSC (`tier: "dbsc"`) is stronger against local malware because the private key lives inside the TPM or Secure Enclave — software can't extract it even with admin access. The polyfill key is non-extractable from JavaScript, but the encrypted blob lives on disk and a privileged on-device attacker can reach it.
331
-
332
- None of this replaces HTTPS, input validation, strong passwords, or MFA.
333
-
334
- ## Going deeper
203
+ ## Docs
335
204
 
336
- - **Concepts and protocol:** [HOW-IT-WORKS.md](./HOW-IT-WORKS.md)
205
+ - **Concepts & protocol:** [HOW-IT-WORKS.md](./HOW-IT-WORKS.md)
206
+ - **Getting started:** [docs/getting-started.md](./docs/getting-started.md)
207
+ - **Add to an existing app:** [docs/integrating-existing-auth.md](./docs/integrating-existing-auth.md)
208
+ - **Per-request signing & replay cache:** [docs/per-request-signing.md](./docs/per-request-signing.md)
337
209
  - **Bound polyfill wire protocol:** [docs/bound-polyfill.md](./docs/bound-polyfill.md)
338
- - **Per-request signing (v2.7 dual-key + v2.8 replay cache):** [docs/per-request-signing.md](./docs/per-request-signing.md)
339
210
  - **API reference:** [docs/api-reference.md](./docs/api-reference.md)
340
- - **Adapters (Express / Fastify / Hono / Next.js + writing your own):** [docs/adapters.md](./docs/adapters.md)
341
- - **Storage (memory / Redis / Postgres):** [docs/storage.md](./docs/storage.md)
342
- - **Telemetry hooks:** [docs/telemetry.md](./docs/telemetry.md)
343
- - **Deployment (Render / Fly / Vercel / Cloudflare / nginx):** [docs/deployment.md](./docs/deployment.md)
344
- - **Security best practices:** [docs/security/best-practices.md](./docs/security/best-practices.md)
345
- - **Threat model:** [docs/security/threat-model.md](./docs/security/threat-model.md)
211
+ - **Adapters (+ build your own):** [docs/adapters.md](./docs/adapters.md)
212
+ - **Storage:** [docs/storage.md](./docs/storage.md) · **Deployment:** [docs/deployment.md](./docs/deployment.md)
213
+ - **Security:** [threat model](./docs/security/threat-model.md) · [best practices](./docs/security/best-practices.md)
346
214
  - **Troubleshooting:** [docs/troubleshooting.md](./docs/troubleshooting.md)
347
215
 
348
216
  ## Status
349
217
 
350
- Verified end-to-end on Chrome 147 / Windows / TPM 2.0. Native DBSC requires Chromium 146+ on Windows or macOS Apple Silicon. The bound polyfill works on every browser with Web Crypto + IndexedDB (Firefox, Safari, mobile browsers, older Chromium). No third-party security audit yet. Production-readiness table and adoption guidance: [HOW-IT-WORKS.md#production-readiness](./HOW-IT-WORKS.md#production-readiness).
218
+ Verified end-to-end on Chrome 147 / Windows / TPM 2.0. Native DBSC requires Chromium 146+ on Windows or Apple Silicon macOS; the polyfill covers every browser with Web Crypto + IndexedDB. No third-party security audit yet — see [HOW-IT-WORKS.md#production-readiness](./HOW-IT-WORKS.md#production-readiness).
351
219
 
352
220
  ## License
353
221
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dbsc-toolkit",
3
- "version": "2.9.9",
3
+ "version": "2.9.10",
4
4
  "description": "Server-side Device Bound Session Credentials (DBSC) for Node.js plus a Web Crypto polyfill for Firefox, Safari, and older Chromium. Cookie-theft protection on every modern browser.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",