dbsc-toolkit 2.9.7 → 2.9.8

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 +74 -79
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -5,24 +5,24 @@
5
5
  [![License](https://img.shields.io/npm/l/dbsc-toolkit.svg)](./LICENSE)
6
6
  [![Node](https://img.shields.io/node/v/dbsc-toolkit.svg)](https://nodejs.org)
7
7
 
8
- > **Stop stolen session cookies from being replayed on another device.**
8
+ > Stop stolen session cookies from being replayed on another device.
9
9
  >
10
- > `dbsc-toolkit` implements W3C [Device Bound Session Credentials](https://www.w3.org/TR/dbsc/) on the server, and ships a Web Crypto polyfill so the same protection works on browsers that don't speak native DBSC yet. Native DBSC on Chromium 146+ (TPM / Secure Enclave); the polyfill on every other browser. **One server, every browser, no user prompts.**
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
11
  >
12
- > Works with **Express · Fastify · Hono · Next.js · Redis · PostgreSQL**.
12
+ > Adapters: Express, Fastify, Hono, Next.js. Storage: in-memory, Redis, Postgres.
13
13
 
14
14
  ```ts
15
- // In your login route — one line to bind the session:
15
+ // Bind the session inside your existing login route:
16
16
  app.post("/login", async (req, res) => {
17
17
  await dbsc.bind(res, sessionId, { userId });
18
18
  res.json({ ok: true });
19
19
  });
20
20
 
21
- // On any sensitive route — one guard:
21
+ // Guard sensitive routes:
22
22
  app.post("/payment", express.raw({ type: "*/*" }), requireProof(), paymentHandler);
23
23
  ```
24
24
 
25
- That's it. The rest — key generation, protocol routes, cookie management, polyfill — is handled by the library.
25
+ Key generation, the protocol routes, cookie management, and the polyfill all live inside the library.
26
26
 
27
27
  ## Contents
28
28
 
@@ -32,7 +32,7 @@ That's it. The rest — key generation, protocol routes, cookie management, poly
32
32
  - [Why dbsc-toolkit](#why-dbsc-toolkit)
33
33
  - [Comparison](#comparison)
34
34
  - [Why not JWT?](#why-not-just-use-jwt)
35
- - [How it works](#how-it-works-the-30-second-version)
35
+ - [How it works](#how-it-works)
36
36
  - [Who is this for?](#who-is-this-for)
37
37
  - [Adding to an existing app](#adding-to-an-existing-app)
38
38
  - [Protect your routes](#protect-your-routes)
@@ -43,18 +43,20 @@ That's it. The rest — key generation, protocol routes, cookie management, poly
43
43
 
44
44
  ## Why dbsc-toolkit
45
45
 
46
- Native DBSC ships on Chrome 146+ for Windows users with a TPM. That's about a third of internet users. Every other DBSC implementation gives the rest of your users nothing — they remain on plain bearer cookies.
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
47
 
48
- This library is among the first DBSC implementations to provide a Web Crypto fallback for browsers that don't support native DBSC. Same wire-level protection against cookie theft, same `requireProof()` guard server-side, key stored non-extractably in IndexedDB instead of a TPM. No biometric prompt, no user interaction, no checkbox.
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.
49
49
 
50
- - ✅ **Native W3C DBSC** on Chromium 146+ (TPM on Windows, Secure Enclave on macOS)
51
- - ✅ **Web Crypto polyfill** on Firefox, Safari, mobile, older Chromium — same protection, IndexedDB key
52
- - ✅ **Express, Fastify, Hono, Next.js** adapters — all four ship in one package
53
- - ✅ **Memory, Redis, Postgres** storage — pick one, swap with one line
54
- - ✅ **`requireProof()`** per-request signature guard — works on every browser, body-hash defended against MITM
55
- - ✅ **Replay cache** (v2.8+) — rejects captured-proof replay outside the timestamp window
56
- - ✅ **Multi-subdomain** (v2.9+) — `cookieScope: "site"` for `app.example.com` + `api.example.com`
57
- - ✅ **Apache 2.0** — read every line before you ship it
50
+ What you actually get:
51
+
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.
58
60
 
59
61
  ## Install
60
62
 
@@ -62,16 +64,16 @@ This library is among the first DBSC implementations to provide a Web Crypto fal
62
64
  npm install dbsc-toolkit
63
65
  ```
64
66
 
65
- Optional peer deps — install only the framework + storage you actually use:
67
+ Optional peer dependencies — install only what you actually use:
66
68
 
67
69
  ```sh
68
- npm install express ioredis # Express + Redis
70
+ npm install express ioredis # Express + Redis
69
71
  npm install fastify @fastify/cookie pg # Fastify + Postgres
70
72
  ```
71
73
 
72
74
  ## Quick start
73
75
 
74
- Copy-paste runnable. `createDbsc()` takes your config once; `install()` mounts everything — the protocol routes, the bound-route JSON parser, the `/dbsc-client` SDK, and `trust proxy`. No `cookie-parser`, no manual static mount.
76
+ `createDbsc()` takes your config once. `install()` mounts the protocol routes, the bound-route JSON parser, the `/dbsc-client` SDK, and sets `trust proxy`.
75
77
 
76
78
  ```ts
77
79
  import express from "express";
@@ -83,7 +85,7 @@ const app = express();
83
85
  app.use(express.json()); // for your own routes' JSON bodies
84
86
 
85
87
  const dbsc = createDbsc({ storage: new MemoryStorage() }); // swap for Redis/Postgres in prod
86
- dbsc.install(app); // mounts /dbsc/*, /dbsc-bound/*, the SDK, trust proxy
88
+ dbsc.install(app);
87
89
 
88
90
  app.post("/login", async (req, res) => {
89
91
  await dbsc.bind(res, randomUUID(), { userId: req.body.username });
@@ -94,7 +96,7 @@ app.get("/me", (_req, res) => res.json(res.locals.dbsc));
94
96
  app.listen(3000);
95
97
  ```
96
98
 
97
- In your HTML, load the polyfill once so Firefox / Safari / older Chromium reach `tier: "bound"`:
99
+ Load the polyfill in your HTML so Firefox / Safari / older Chromium can reach `tier: "bound"`:
98
100
 
99
101
  ```html
100
102
  <script type="module">
@@ -103,16 +105,16 @@ In your HTML, load the polyfill once so Firefox / Safari / older Chromium reach
103
105
  </script>
104
106
  ```
105
107
 
106
- Without the script tag those browsers stay on `tier: "none"`. Native Chromium 146+ does not need the script — it negotiates the protocol on its own from the headers `dbsc()` sets.
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.
107
109
 
108
110
  ### Common failure modes
109
111
 
110
- - **`tier` always reads `"none"` on Chromium 146+?** Running on plain HTTP (DBSC needs HTTPS), or `dbsc.install(app)` was never called. `install()` already sets `trust proxy` and parses cookies, so the old "middleware order" class of bug is gone.
111
- - **Chrome loops registration?** Storage was wiped — switch off `MemoryStorage` to Redis or Postgres before deploying anywhere that ever restarts.
112
- - **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).
113
- - **Firefox / Safari still on `"none"`?** Forgot the `<script type="module">` tag above. `install()` serves the SDK at `/dbsc-client` for you; you still load it on the page.
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.
114
116
 
115
- Full walk-through: [docs/getting-started.md](./docs/getting-started.md).
117
+ Walk-through: [docs/getting-started.md](./docs/getting-started.md).
116
118
 
117
119
  ## Live demo
118
120
 
@@ -134,43 +136,43 @@ Sign up, log in, click **Check session**. Chromium 146+ lands on `tier: "dbsc"`
134
136
  | Multi-subdomain binding | ✅ (loose) | ✅ (loose) | ❌ (`__Host-` only) | ✅ (v2.9 `cookieScope: "site"`) |
135
137
  | Server runtime | any | any | n/a (browser-side) | Node.js ≥ 20 |
136
138
 
137
- The native-DBSC column refers to a server that uses the spec without a polyfill — what every other DBSC implementation currently offers. The polyfill key lives in IndexedDB (`extractable: false`) rather than a TPM, so it's a notch weaker against malware reading the browser profile, but defeats every remote-theft scenario.
139
+ 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.
138
140
 
139
141
  ## "Why not just use JWT?"
140
142
 
141
- JWTs are still bearer tokens. Whoever has the bytes can replay them. Steal a JWT from a log line, an XSS, a leaked proxy header, a misconfigured intermediary — paste it into another machine and the server treats it as the legitimate user, exactly like a stolen cookie.
143
+ 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.
142
144
 
143
- DBSC pins the session to a cryptographic key that lives on the user's device (TPM on Windows, Secure Enclave on macOS, or — under the polyfill — a non-extractable IndexedDB key on every other browser). The token alone is no longer sufficient: the browser has to prove possession of the key on every refresh, and on every guarded request via `requireProof()`. The attacker's device has no key, so the replay fails and the session demotes within one cycle.
145
+ It complements your existing auth — passwords, MFA, session cookies, JWTs — rather than replacing any of them. The gap it closes is *replay after issue*.
144
146
 
145
- DBSC complements your existing auth (passwords, MFA, JWT/session cookies) — it does not replace them. It closes the *replay-after-issue* gap that bearer tokens cannot.
146
-
147
- ## How it works (the 30-second version)
147
+ ## How it works
148
148
 
149
149
  ```
150
- Login → Key Registration → Cookie Issued → Challenge → Signature Verification → Session Refresh
150
+ Login → Key Registration → Cookie Issued → Challenge → Signature Verification → Refresh
151
151
  ```
152
152
 
153
- On login the server responds with a `Secure-Session-Registration` header. The browser generates an ECDSA keypair in the TPM (Chrome) or IndexedDB (Firefox/Safari/polyfill), POSTs the public key, and gets back 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 `requireProof()` 403s every guarded request immediately.
153
+ 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.
154
154
 
155
155
  Full wire-format walk-through: [HOW-IT-WORKS.md](./HOW-IT-WORKS.md).
156
156
 
157
157
  ## Who is this for?
158
158
 
159
- **Use `dbsc-toolkit` if:**
159
+ Use `dbsc-toolkit` if:
160
+
160
161
  - You run a Node.js backend.
161
162
  - Your users authenticate with session cookies (or JWT bearer tokens).
162
163
  - Cookie / token theft is in your threat model — XSS, infostealer malware, leaked logs, compromised proxies.
163
- - You want one library that covers Chrome **and** Firefox / Safari today, not "eventually."
164
+ - You want coverage on Chrome and Firefox / Safari now, not eventually.
165
+
166
+ Not the right fit if:
164
167
 
165
- **Not the right fit if:**
166
168
  - You don't issue any persistent client credential (every request re-authenticates from scratch).
167
169
  - You can't deploy HTTPS — DBSC cookies require `Secure`, and the spec rejects insecure origins.
168
- - Your backend is Python / Go / Rust / Java / .NET — no port of this library exists yet; you'd implement against the [W3C spec](https://www.w3.org/TR/dbsc/) directly.
169
- - 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.)
170
+ - 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.
171
+ - 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.
170
172
 
171
173
  ## Adding to an existing app
172
174
 
173
- You don't rewrite login, you don't migrate the session store. DBSC sits alongside your existing session cookie and binds to the same session id. The whole integration for an Express app:
175
+ 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.
174
176
 
175
177
  ```ts
176
178
  import { createDbsc, requireProof } from "dbsc-toolkit/express";
@@ -202,40 +204,38 @@ app.post("/logout", async (req, res) => {
202
204
  });
203
205
  ```
204
206
 
205
- `install()` handles `trust proxy`, cookie parsing, the bound-route JSON parser, and the `/dbsc-client` static mount — you don't wire those. 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.
207
+ `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.
206
208
 
207
- **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).
209
+ **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).
208
210
 
209
- **App split across subdomains** (`app.example.com` + `api.example.com`)? Default `__Host-` cookies are origin-locked — keep DBSC on one origin (proxy `/dbsc/*` and `/dbsc-bound/*` through the UI host) for the strongest setting. If a same-origin layout is genuinely not 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 if either is wrong, so 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).
211
+ **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).
210
212
 
211
- **The full step-by-step** — every option and its default, the `autoBind` transparent-rollout variant, the per-route policy table, the migration timeline — is in [docs/integrating-existing-auth.md](./docs/integrating-existing-auth.md).
213
+ Full walk-through with `autoBind`, per-route policy, and the migration timeline: [docs/integrating-existing-auth.md](./docs/integrating-existing-auth.md).
212
214
 
213
215
  ## Protect your routes
214
216
 
215
- > **First time here?** [docs/usage.md](./docs/usage.md) walks the setup and the guard in order, with concrete code. Five-minute read.
216
-
217
- 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 **one guard, `requireProof()`**, to each route that matters.
217
+ 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.
218
218
 
219
219
  | Your route does… | Use this guard | What it stops |
220
220
  |---|---|---|
221
221
  | Public / read-only (feed, search, public profile) | Nothing | n/a — no auth gate at all |
222
- | Anything authenticated (post, comment, upvote, settings, payment, admin) | `requireProof()` (server) + `wrapFetch()` (client) | A stolen cookie cannot be replayed from another device, cannot ride along during the freshness window, and an MITM cannot substitute a POST body |
222
+ | 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 |
223
223
 
224
- There is deliberately **no tier-level argument**. A `dbsc`-only gate would lock out every Firefox/Safari user; a `bound`-only check (tier without a per-request proof) is not actually secure. `requireProof()` is the one honest answer — it requires a bound device + a per-request proof and works on every browser. As of v2.7 Chromium sessions also register a polyfill key alongside the TPM key, so the per-request signature is enforced on every tier (not just Firefox/Safari). `wrapFetch` signs the request body by default in v2.8+; a POST guarded route mounts `express.raw()` in front. Apps with many guarded routes can use `installFetchInterceptor({ pathPrefixes: ["/api/secure/"] })` to wrap once at boot instead of per-call.
224
+ `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.
225
225
 
226
- For apps with a stricter threat model (active MITM, 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).
226
+ 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).
227
227
 
228
- The full threat boundary, the per-framework wiring (Fastify / Hono / Next.js), and the migration timeline for an existing app are in [docs/integrating-existing-auth.md](./docs/integrating-existing-auth.md) and [docs/per-request-signing.md](./docs/per-request-signing.md).
228
+ 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).
229
229
 
230
230
  ## Production checklist
231
231
 
232
- - [ ] HTTPS enabled — DBSC requires `Secure` cookies; plain HTTP locks every user to `tier: "none"`
233
- - [ ] Redis or PostgreSQL storage — `MemoryStorage` is lost on restart; bound browsers loop registration
234
- - [ ] `requireProof()` on every authenticated route — read-only public routes need no guard
235
- - [ ] `wrapFetch({ signBody: true })` on the client side for those routes (already the default in v2.8+)
236
- - [ ] Replay cache enabled — `replayCache: new RedisReplayCache(redis)` in `createDbsc({...})`
237
- - [ ] `trust proxy` already set — `install()` sets it, but verify if you have your own app config overriding it
238
- - [ ] Polyfill script loaded — the `<script type="module">` tag; without it Firefox/Safari stay on `tier: "none"`
232
+ - [ ] HTTPS enabled — DBSC requires `Secure` cookies; plain HTTP locks every user to `tier: "none"`.
233
+ - [ ] Redis or PostgreSQL storage — `MemoryStorage` is lost on restart; bound browsers loop registration.
234
+ - [ ] `requireProof()` on every authenticated route — read-only public routes need no guard.
235
+ - [ ] `wrapFetch({ signBody: true })` on the client side for those routes (default since v2.8).
236
+ - [ ] Replay cache enabled when relevant — `replayCache: new RedisReplayCache(redis)` in `createDbsc({...})`.
237
+ - [ ] `trust proxy` set — `install()` sets it, but verify nothing in your own app config overrides it.
238
+ - [ ] Polyfill script loaded — the `<script type="module">` tag; without it Firefox/Safari stay on `tier: "none"`.
239
239
 
240
240
  ## Subpath imports
241
241
 
@@ -249,11 +249,11 @@ The full threat boundary, the per-framework wiring (Fastify / Hono / Next.js), a
249
249
  | `dbsc-toolkit/client` | Browser SDK with `initBoundDbsc()` for the polyfill |
250
250
  | `dbsc-toolkit/storage/{memory,redis,postgres}` | Storage adapters |
251
251
 
252
- 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).
252
+ 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).
253
253
 
254
254
  ## Protection tiers
255
255
 
256
- `tier` is the **state** of a session — what kind of binding the browser achieved. It is *not* a knob you gate on directly:
256
+ `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.
257
257
 
258
258
  | Tier | Mechanism | Protects against |
259
259
  |------|-----------|------------------|
@@ -261,37 +261,32 @@ Tree-shaking eliminates anything you don't import. Using Koa, Hapi, raw `http`,
261
261
  | `bound` | Web Crypto polyfill, non-extractable ECDSA P-256 key in IndexedDB | Cookie theft. Does not defeat infostealer malware on the user's machine. |
262
262
  | `none` | No active / fresh binding | Nothing the cookie itself doesn't already do |
263
263
 
264
- **How to gate routes — the whole decision tree:**
265
-
266
- - **Public / read-only route?** → no guard.
267
- - **Anything authenticated?** → `requireProof()`. That's it.
268
-
269
- There is no third option, and **never gate a route on `tier === "dbsc"`** — that locks out every Firefox and Safari user (they can only reach `tier: "bound"`), and `requireProof()` already gives those browsers the same per-request guarantee via a signed proof. `requireProof()` is the one guard; it works on every browser. Read `res.locals.dbsc.tier` only if you want to *display* binding state in the UI — not to build a gate. Full guidance in [docs/security/best-practices.md](./docs/security/best-practices.md).
264
+ 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).
270
265
 
271
266
  ## Security model
272
267
 
273
- `dbsc-toolkit` protects against:
268
+ What this protects against:
274
269
 
275
- - Stolen session cookies replayed from another device
276
- - Stolen bearer tokens (same category — bearer == portable)
277
- - Cookie exfiltration via XSS, network capture, log leakage, or proxy leakage
270
+ - Stolen session cookies replayed from another device.
271
+ - Stolen bearer tokens (same category — bearer == portable).
272
+ - Cookie exfiltration via XSS, network capture, log leakage, or proxy leakage.
278
273
 
279
- The Web Crypto polyfill (`tier: "bound"`) does **not** protect against:
274
+ What the Web Crypto polyfill (`tier: "bound"`) does not protect against:
280
275
 
281
- - Malware already running on the user's machine with access to the browser profile
282
- - Browser or OS compromise
276
+ - Malware running on the user's machine with access to the browser profile.
277
+ - Browser or OS compromise.
283
278
 
284
- Native DBSC (`tier: "dbsc"`) provides stronger protection against local malware because the private key lives inside the TPM or Secure Enclave — software cannot extract it even with admin access. The polyfill key is non-extractable to JavaScript, but the encrypted blob lives on disk and a sufficiently privileged attacker can reach it.
279
+ 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.
285
280
 
286
- Neither tier is a substitute for HTTPS, input validation, strong passwords, or MFA.
281
+ None of this replaces HTTPS, input validation, strong passwords, or MFA.
287
282
 
288
283
  ## Going deeper
289
284
 
290
285
  - **Concepts and protocol:** [HOW-IT-WORKS.md](./HOW-IT-WORKS.md)
291
286
  - **Bound polyfill wire protocol:** [docs/bound-polyfill.md](./docs/bound-polyfill.md)
292
- - **Per-request signing (the v2.7 dual-key story + the v2.8 replay cache):** [docs/per-request-signing.md](./docs/per-request-signing.md)
287
+ - **Per-request signing (v2.7 dual-key + v2.8 replay cache):** [docs/per-request-signing.md](./docs/per-request-signing.md)
293
288
  - **API reference:** [docs/api-reference.md](./docs/api-reference.md)
294
- - **Adapters (Express / Fastify / Hono / Next.js + write your own):** [docs/adapters.md](./docs/adapters.md)
289
+ - **Adapters (Express / Fastify / Hono / Next.js + writing your own):** [docs/adapters.md](./docs/adapters.md)
295
290
  - **Storage (memory / Redis / Postgres):** [docs/storage.md](./docs/storage.md)
296
291
  - **Telemetry hooks:** [docs/telemetry.md](./docs/telemetry.md)
297
292
  - **Deployment (Render / Fly / Vercel / Cloudflare / nginx):** [docs/deployment.md](./docs/deployment.md)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dbsc-toolkit",
3
- "version": "2.9.7",
3
+ "version": "2.9.8",
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",