dbsc-toolkit 1.5.0 → 2.0.0

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 (72) hide show
  1. package/README.md +80 -54
  2. package/dist/client/index.d.ts +10 -3
  3. package/dist/client/index.d.ts.map +1 -1
  4. package/dist/client/index.js +126 -3
  5. package/dist/client/index.js.map +1 -1
  6. package/dist/client/keystore.d.ts +8 -0
  7. package/dist/client/keystore.d.ts.map +1 -0
  8. package/dist/client/keystore.js +44 -0
  9. package/dist/client/keystore.js.map +1 -0
  10. package/dist/core/bound/index.d.ts +6 -0
  11. package/dist/core/bound/index.d.ts.map +1 -0
  12. package/dist/core/bound/index.js +4 -0
  13. package/dist/core/bound/index.js.map +1 -0
  14. package/dist/core/bound/refresh.d.ts +9 -0
  15. package/dist/core/bound/refresh.d.ts.map +1 -0
  16. package/dist/core/bound/refresh.js +52 -0
  17. package/dist/core/bound/refresh.js.map +1 -0
  18. package/dist/core/bound/registration.d.ts +12 -0
  19. package/dist/core/bound/registration.d.ts.map +1 -0
  20. package/dist/core/bound/registration.js +52 -0
  21. package/dist/core/bound/registration.js.map +1 -0
  22. package/dist/core/bound/verify.d.ts +2 -0
  23. package/dist/core/bound/verify.d.ts.map +1 -0
  24. package/dist/core/bound/verify.js +23 -0
  25. package/dist/core/bound/verify.js.map +1 -0
  26. package/dist/core/index.d.ts +3 -4
  27. package/dist/core/index.d.ts.map +1 -1
  28. package/dist/core/index.js +2 -3
  29. package/dist/core/index.js.map +1 -1
  30. package/dist/core/types.d.ts +4 -4
  31. package/dist/core/types.d.ts.map +1 -1
  32. package/dist/express/index.d.ts +4 -0
  33. package/dist/express/index.d.ts.map +1 -1
  34. package/dist/express/index.js +191 -3
  35. package/dist/express/index.js.map +1 -1
  36. package/dist/fastify/index.d.ts +4 -0
  37. package/dist/fastify/index.d.ts.map +1 -1
  38. package/dist/fastify/index.js +125 -3
  39. package/dist/fastify/index.js.map +1 -1
  40. package/dist/hono/index.d.ts +4 -6
  41. package/dist/hono/index.d.ts.map +1 -1
  42. package/dist/hono/index.js +118 -6
  43. package/dist/hono/index.js.map +1 -1
  44. package/dist/nextjs/index.d.ts +4 -0
  45. package/dist/nextjs/index.d.ts.map +1 -1
  46. package/dist/nextjs/index.js +133 -3
  47. package/dist/nextjs/index.js.map +1 -1
  48. package/package.json +2 -4
  49. package/dist/client/detect.d.ts +0 -3
  50. package/dist/client/detect.d.ts.map +0 -1
  51. package/dist/client/detect.js +0 -20
  52. package/dist/client/detect.js.map +0 -1
  53. package/dist/client/signals.d.ts +0 -9
  54. package/dist/client/signals.d.ts.map +0 -1
  55. package/dist/client/signals.js +0 -13
  56. package/dist/client/signals.js.map +0 -1
  57. package/dist/client/webauthn.d.ts +0 -3
  58. package/dist/client/webauthn.d.ts.map +0 -1
  59. package/dist/client/webauthn.js +0 -8
  60. package/dist/client/webauthn.js.map +0 -1
  61. package/dist/core/fallback/hmac.d.ts +0 -9
  62. package/dist/core/fallback/hmac.d.ts.map +0 -1
  63. package/dist/core/fallback/hmac.js +0 -37
  64. package/dist/core/fallback/hmac.js.map +0 -1
  65. package/dist/core/fallback/negotiate.d.ts +0 -9
  66. package/dist/core/fallback/negotiate.d.ts.map +0 -1
  67. package/dist/core/fallback/negotiate.js +0 -22
  68. package/dist/core/fallback/negotiate.js.map +0 -1
  69. package/dist/core/fallback/webauthn.d.ts +0 -10
  70. package/dist/core/fallback/webauthn.d.ts.map +0 -1
  71. package/dist/core/fallback/webauthn.js +0 -41
  72. package/dist/core/fallback/webauthn.js.map +0 -1
package/README.md CHANGED
@@ -5,21 +5,36 @@
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
- Server-side implementation of [Device Bound Session Credentials](https://w3c.github.io/webappsec-dbsc/) (DBSC) for Node.js.
8
+ Server-side implementation of [Device Bound Session Credentials](https://w3c.github.io/webappsec-dbsc/) (DBSC) for Node.js, with a silent Web Crypto polyfill for browsers that don't ship DBSC natively.
9
9
 
10
- DBSC is a W3C draft that binds session cookies to a hardware-resident private key inside the browser. A stolen cookie is useless without that key — which never leaves the user's device.
10
+ DBSC binds session cookies to a hardware-resident private key inside the browser. A stolen cookie is useless without that key — which never leaves the user's device. Chromium 145+ does this natively. Firefox, Safari, and older Chromium browsers get the same cryptographic refresh-signing protection via a Web Crypto polyfill that activates automatically after login.
11
11
 
12
- Chromium 145+ supports DBSC natively that includes Chrome, Edge, Brave, Opera, Arc, Vivaldi, and any other Chromium-based browser. Works across Windows (TPM 2.0), macOS Apple Silicon (Secure Enclave on M1/M2/M3/M4+), and Android (Keystore). This library handles the server side. Verified end-to-end against Chrome 147 on Windows with a real TPM 2.0.
12
+ | Browser | Tier | Key location |
13
+ |---------|------|--------------|
14
+ | Chromium 145+ (Chrome, Edge, Brave, Opera, Arc, Vivaldi) | `dbsc` | TPM / Secure Enclave / Android Keystore |
15
+ | Firefox, Safari, older Chromium | `bound` | Browser keystore (non-extractable Web Crypto key) |
13
16
 
14
- **New here?** Read [HOW-IT-WORKS.md](./HOW-IT-WORKS.md) first. It walks through the threat model, the on-the-wire protocol, where the library fits in your app, and tier semantics in ~15 minutes. Skip it only if you already know DBSC.
17
+ Both tiers defeat the entire "stolen cookie replayed from another device" class of attack. `dbsc` additionally defeats infostealer malware reading the browser profile directory the `bound` polyfill key is software-bound.
18
+
19
+ Verified end-to-end against Chrome 147 on Windows with a real TPM 2.0.
20
+
21
+ **New here?** Read [HOW-IT-WORKS.md](./HOW-IT-WORKS.md) first.
15
22
 
16
23
  ## Live demo
17
24
 
18
25
  Try it: <https://dbsc-toolkit.onrender.com/>
19
26
 
20
- Open in any Chromium-based browser version 145+ (Chrome, Edge, Brave, Opera), click **Login**, then **Check session** — `tier` reads `"dbsc"` once the hardware-backed key is bound. The demo uses a 60-second bound-cookie TTL so refresh kicks in fast — watch DevTools Network for the automatic `POST /dbsc/refresh` after the cookie expires. Use **Clear cookies** to reset and replay the flow. Source in [examples/express/](./examples/express/).
27
+ Sign up, log in, then click **Check session**:
28
+ - On Chromium 145+ you land on `tier: "dbsc"` within a second of login.
29
+ - On Firefox/Safari/older Chromium you land on `tier: "bound"` within ~3 seconds (the polyfill activates silently).
30
+
31
+ The demo uses a 60-second bound-cookie TTL so refresh fires fast — watch DevTools Network for `POST /dbsc/refresh` (native) or `POST /dbsc-bound/refresh` (polyfill) after the cookie expires. Source in [examples/express/](./examples/express/).
32
+
33
+ The demo runs on `RedisStorage` (Upstash) by default, so sessions survive deploys, cold starts, and laptop reboots. Locally without `REDIS_URL`, it falls back to `MemoryStorage` — fine for one terminal session, wiped on every restart.
21
34
 
22
- > Heads up: the demo runs on in-memory storage. Render restarts wipe sessions, so if "Check session" returns `not authenticated` after a while, the instance probably restarted — click **Login** again.
35
+ Two protected routes show the gating pattern: `/profile` requires `tier === "dbsc"` specifically (TPM-only flows); `/profile-soft` accepts either `"dbsc"` or `"bound"`.
36
+
37
+ > **Hitting `not authenticated` after a few login/logout cycles?** That's Chrome's DBSC quota — the browser's anti-abuse throttle. The demo surfaces it explicitly now (red banner + reason text in the response). To recover: `chrome://settings/clearBrowserData` → Last hour → Cookies and site data → clear, or open an Incognito window. The quota is per `(browser install, origin)`, so production users (who log in once and stay logged in) essentially never trip it.
23
38
 
24
39
  ## Install
25
40
 
@@ -70,6 +85,17 @@ app.listen(3000);
70
85
 
71
86
  `app.use(dbsc(...))` mounts `POST /dbsc/registration` and `POST /dbsc/refresh` automatically — Chrome drives both, your code never sees them. `bindSession()` is the one-liner you add to your login route: it writes the session row, issues a challenge, builds the registration header (both legacy + new names), and sets the two short-lived cookies Chrome needs to complete binding.
72
87
 
88
+ Call `bindSession()` after you have verified the user's credentials — in the login route, or in a signup route that auto-logs the user in. Calling it in a bare signup that does not establish an authenticated session is not useful: there is no session to bind yet.
89
+
90
+ ### The registration race after login
91
+
92
+ Chrome posts to `/dbsc/registration` *after* the login response returns. The handshake includes TPM key generation, JWS signing, and a network round-trip, so it commonly takes 300 ms to 1.5 s — sometimes longer on a cold device. If the page immediately requests `/me` or `/payment` and gates on `tier === "dbsc"`, the check can land before registration completes and report `tier: "none"` even on a fully supported browser. Two ways to absorb this on the client:
93
+
94
+ - **Status indicator with short polling.** After a successful login, poll a low-cost endpoint (`/me` works) every 500 ms for up to ~8 s — long enough to cover both native DBSC and the bound-polyfill activation window. Stop when `tier !== "none"`; show a small "Session bound" badge. The live demo uses this pattern — see `pollDbscReady` in [examples/express/src/server.js](./examples/express/src/server.js).
95
+ - **One-shot auto-retry on the first call after login.** If a tier-gated request returns `tier: "none"` within the first few seconds of a fresh login, wait ~1 s and retry once. Cheap, invisible to the user, and avoids the false demotion entirely.
96
+
97
+ For server-driven flows (a payment route called from a server redirect immediately after login), either pattern works. The race only matters when the very first authenticated request is also a tier check; routine browsing past the first second is unaffected.
98
+
73
99
  A full demo with `/me`, `/logout`, and `/clear-cookies` is in [examples/express/src/server.js](./examples/express/src/server.js).
74
100
 
75
101
  ## Adding DBSC to an existing app
@@ -85,12 +111,12 @@ Full integration story, per-route policy table, and rollout timeline in [docs/in
85
111
 
86
112
  | Import | What it is |
87
113
  |--------|------------|
88
- | `dbsc-toolkit` | Core types, crypto, protocol functions |
114
+ | `dbsc-toolkit` | Core types, crypto, protocol functions (native DBSC + bound polyfill) |
89
115
  | `dbsc-toolkit/express` | Express middleware |
90
116
  | `dbsc-toolkit/fastify` | Fastify plugin |
91
117
  | `dbsc-toolkit/hono` | Hono middleware |
92
118
  | `dbsc-toolkit/nextjs` | Next.js App Router middleware + handlers |
93
- | `dbsc-toolkit/client` | Browser SDK for fallback paths |
119
+ | `dbsc-toolkit/client` | Browser SDK — `initBoundDbsc()` for the polyfill |
94
120
  | `dbsc-toolkit/storage/memory` | In-memory storage (dev/test) |
95
121
  | `dbsc-toolkit/storage/redis` | Redis storage |
96
122
  | `dbsc-toolkit/storage/postgres` | Postgres storage |
@@ -99,51 +125,55 @@ Tree-shaking eliminates anything you don't import.
99
125
 
100
126
  ## How a verified flow looks
101
127
 
128
+ On Chromium 145+:
129
+
102
130
  1. User hits `POST /login`. Server creates a session, issues a challenge, sets `Secure-Session-Registration` response header and two short-lived cookies (`__Host-dbsc-reg`, `__Host-dbsc-challenge`).
103
131
  2. The browser immediately POSTs to `/dbsc/registration` with `Secure-Session-Response: <jws>`. The JWS carries the device public key signed by the matching private key (held in the platform's hardware key store).
104
- 3. Middleware verifies the JWS, stores the public key bound to the session, sets `__Host-dbsc-session` cookie, returns DBSC session config JSON.
105
- 4. From now on, every refresh cycle (default 10 min) the browser signs a fresh challenge with the hardware-resident key. A copied cookie cannot pass refresh — the attacker has no key.
132
+ 3. Middleware verifies the JWS, stores the public key bound to the session, sets `__Host-dbsc-session` cookie. `tier` is now `"dbsc"`.
133
+ 4. Every refresh cycle (default 10 min) the browser signs a fresh challenge with the hardware-resident key. A copied cookie cannot pass refresh — the attacker has no key.
106
134
 
107
- `tier` on `res.locals.dbsc` reads `"dbsc"` once registration completes.
135
+ On Firefox / Safari / older Chromium (with the `initBoundDbsc()` client SDK loaded on the page):
108
136
 
109
- For a complete walk-through of the protocol with every header value and timing detail, see [HOW-IT-WORKS.md](./HOW-IT-WORKS.md).
137
+ 1. Same `/login` response the registration headers are sent, but the browser ignores them.
138
+ 2. After a 3-second probe (waiting in case native DBSC is just slow), the client SDK generates a non-extractable ECDSA P-256 keypair via Web Crypto, exports the public key, and POSTs to `/dbsc-bound/registration` with the signed challenge.
139
+ 3. Middleware verifies the signature against the JWK, stores the public key, sets `__Host-dbsc-session`. `tier` is now `"bound"`.
140
+ 4. Every refresh cycle the SDK calls `/dbsc-bound/refresh` with a fresh signature. A copied cookie alone has no key in IndexedDB on the attacker's machine, so refresh fails.
141
+
142
+ For the complete protocol walk-through with every header value and timing detail, see [HOW-IT-WORKS.md](./HOW-IT-WORKS.md). The bound polyfill protocol is documented in [docs/bound-polyfill.md](./docs/bound-polyfill.md).
110
143
 
111
144
  ## Using the tier to actually defend
112
145
 
113
- Setting up the middleware does not protect anything on its own. The library does the negotiation and gives you a tier; **enforcing it is your responsibility**. The pattern:
146
+ Setting up the middleware does not protect anything on its own. The library exposes a tier; **enforcing it is your responsibility**.
147
+
148
+ Most routes should gate on `tier !== "none"`:
114
149
 
115
150
  ```ts
116
- app.get("/payment", (req, res) => {
117
- if (res.locals.dbsc.tier !== "dbsc") {
118
- return res.status(403).json({ error: "hardware-bound session required" });
151
+ app.get("/dashboard", (req, res) => {
152
+ if (res.locals.dbsc.tier === "none") {
153
+ return res.status(403).json({ error: "session not bound" });
119
154
  }
120
- // safe to process payment
155
+ // safe — request is bound to a hardware-resident or browser-resident key
121
156
  });
122
157
  ```
123
158
 
124
- If you skip the tier check, a stolen cookie still works. The cookie reaches your server, the session record exists, your code happily proceeds — DBSC bought you nothing. The whole point is the demotion: when a cookie is replayed without the hardware-signed proof, tier drops to `"none"` (or stays at the lower fallback tier) and your gate refuses the request.
125
-
126
- Suggested handling per tier in a real application:
127
-
128
- - `tier === "dbsc"`: full access. Payments, account changes, anything sensitive.
129
- - `tier === "webauthn"`: most access. Hardware-bound via platform authenticator.
130
- - `tier === "hmac"`: read-only or low-risk actions. The binding is best-effort.
131
- - `tier === "none"`: treat as unauthenticated. Force re-login, revoke the session, log a `session_stolen` candidate, depending on context.
132
-
133
- Putting this in a single middleware keeps it consistent:
159
+ For genuinely sensitive routes where you want the TPM-backed guarantee specifically (defeats infostealer malware, not just remote cookie theft), gate on `"dbsc"`:
134
160
 
135
161
  ```ts
136
- function requireDbsc(req, res, next) {
162
+ app.post("/payment", (req, res) => {
137
163
  if (res.locals.dbsc.tier !== "dbsc") {
138
- return res.status(401).json({ error: "re-authenticate" });
164
+ return res.status(403).json({ error: "hardware-bound session required" });
139
165
  }
140
- next();
141
- }
142
-
143
- app.post("/payment", requireDbsc, handler);
144
- app.post("/account/email", requireDbsc, handler);
166
+ });
145
167
  ```
146
168
 
169
+ If you skip the tier check entirely, a stolen cookie works just like before — the library bought you nothing. The whole point is the demotion: when a cookie is replayed without a valid refresh signature, tier drops to `"none"` and your gate refuses the request.
170
+
171
+ Tier handling at a glance:
172
+
173
+ - `tier === "dbsc"`: hardware-bound via TPM / Secure Enclave / Android Keystore. Full access.
174
+ - `tier === "bound"`: software-bound via Web Crypto + IndexedDB. Defeats XSS, network capture, and cookie paste-to-other-machine. Does not defeat infostealer malware on the user's device.
175
+ - `tier === "none"`: treat as unauthenticated for any route you care about. Force re-login, log a `session_stolen` candidate, depending on context.
176
+
147
177
  See [docs/security/best-practices.md](./docs/security/best-practices.md) for the full tier-policy guidance.
148
178
 
149
179
  ## Local testing
@@ -186,16 +216,13 @@ const result = await handleRefresh({
186
216
 
187
217
  ## Protection tiers
188
218
 
189
- The library negotiates the strongest available binding per session:
190
-
191
- | Tier | Mechanism | Protection |
192
- |------|-----------|------------|
193
- | `dbsc` | Hardware-backed key, Chromium 145+ | Hardware binding exfiltrated cookie is useless |
194
- | `webauthn` | Platform authenticator | Hardware binding via platform authenticator |
195
- | `hmac` | HMAC + browser signals | Best-effort context binding, not hardware |
196
- | `none` | Standard cookie | No additional binding |
219
+ | Tier | Mechanism | Protects against |
220
+ |------|-----------|------------------|
221
+ | `dbsc` | Native W3C DBSC, key in TPM / Secure Enclave / Android Keystore | Cookie theft (XSS, network, logs, paste-to-other-browser) **and** infostealer malware reading the browser profile |
222
+ | `bound` | Web Crypto polyfill, non-extractable ECDSA P-256 key in IndexedDB | Cookie theft. Does not defeat infostealer malware on the user's machine. |
223
+ | `none` | Plain cookie | Nothing the cookie itself doesn't already do |
197
224
 
198
- The tier is available at `res.locals.dbsc.tier` (Express), `c.get("dbsc").tier` (Hono — legacy `c.get("dbscTier")` still works in 1.x, removed in 2.0.0), `req.dbsc.tier` (Fastify), and via `getDbscSession()` (Next.js). Use it to apply different authorization policies per tier — for example, block payment flows when `tier !== "dbsc"`.
225
+ The tier is available at `res.locals.dbsc.tier` (Express), `req.dbsc.tier` (Fastify), `c.get("dbsc").tier` (Hono), and via `getDbscSession()` (Next.js). Use it to apply per-route policy — for example, block payment flows when `tier !== "dbsc"` and accept any binding for everything else with `tier !== "none"`.
199
226
 
200
227
  ## Storage
201
228
 
@@ -239,7 +266,7 @@ app.use(dbsc({
239
266
  }));
240
267
  ```
241
268
 
242
- Event types: `registration`, `refresh`, `verification_failure`, `session_stolen`, `fallback_tier`.
269
+ Event types: `registration`, `refresh`, `verification_failure`, `session_stolen`, `tier_change`.
243
270
 
244
271
  ## Skipped sessions
245
272
 
@@ -274,14 +301,15 @@ res.setHeader("Sec-Session-Registration", header);
274
301
 
275
302
  Defense-in-depth layer. Does not replace TLS, secure cookie flags, MFA, or server hardening.
276
303
 
277
- HMAC tier is not hardware binding. It provides better-than-nothing context binding for browsers without DBSC or WebAuthn. Operators should communicate the protection tier to users and restrict sensitive operations when `tier === "hmac"` or `tier === "none"`.
304
+ The `bound` polyfill defeats remote cookie theft but is not hardware-bound the key lives in IndexedDB on the user's disk and can be read by infostealer malware with filesystem access on the user's machine. For routes that must defeat that threat, gate on `tier === "dbsc"` specifically. For everything else, `tier !== "none"` is the right gate.
278
305
 
279
- See [SECURITY.md](./SECURITY.md) for reporting vulnerabilities.
306
+ See [SECURITY.md](./SECURITY.md) for reporting vulnerabilities and [docs/security/threat-model.md](./docs/security/threat-model.md) for the per-tier STRIDE breakdown.
280
307
 
281
308
  ## Project status
282
309
 
283
310
  - Single package on npm: `dbsc-toolkit`
284
- - Support floor: Chromium 145+ (Chrome, Edge, Brave, Opera, Arc, etc.) on Windows / macOS Apple Silicon / Android
311
+ - Native DBSC: Chromium 145+ on Windows (TPM 2.0) / macOS Apple Silicon (Secure Enclave) / Android (Keystore)
312
+ - Bound polyfill: every browser with Web Crypto + IndexedDB — Firefox, Safari, older Chromium
285
313
  - Verified end-to-end on Chrome 147 / Windows / TPM 2.0 (other Chromium browsers and platforms should work but not independently verified)
286
314
  - No third-party security audit yet
287
315
 
@@ -292,24 +320,22 @@ Honest table — what you're getting and where the rough edges are.
292
320
  | Area | Status | Confidence |
293
321
  |------|--------|-----------|
294
322
  | Core protocol (registration + refresh + verification) | Stable | High — verified against real Chrome 147 + TPM 2.0 |
323
+ | Bound polyfill (`/dbsc-bound/*` + client SDK) | New in v2.0.0 | Medium — unit-tested; cross-browser verification on the live demo |
295
324
  | Express adapter | Stable | High — used in the live demo, exercised on Render |
296
325
  | Fastify / Hono / Next.js adapters | Stable | Medium — unit tests pass, share core code with Express, not battle-tested in production |
297
326
  | `MemoryStorage` | Dev / test only | N/A — explicitly non-production |
298
327
  | `RedisStorage` | Stable | Medium — atomic challenge consume via Lua, tested locally |
299
328
  | `PostgresStorage` | Stable | Medium — migrations included, tested locally |
300
- | Fallback tiers (WebAuthn, HMAC) | Implemented, lightly tested | Low — protocol shape correct, real-world step-up flows TBD |
301
329
  | Security audit | None | — |
302
330
  | W3C spec stability | Draft, library tracks Chromium's implementation | Spec may evolve; expect occasional wire-format adjustments |
303
331
 
304
332
  **Should you use this in production?** Yes, with three conditions:
305
333
 
306
- 1. **Use Redis or Postgres storage**, not memory. Memory storage on a server that ever restarts (Render free tier, serverless, autoscaling) produces a broken loop where browsers hold cookies that no longer match any stored key.
307
- 2. **Treat it as defense-in-depth**, never the only auth layer. Your existing session cookie, password, MFA, rate limiting — all still required. DBSC raises the floor on session-replay attacks; it doesn't replace anything else.
308
- 3. **Pin a version.** The W3C spec is still draft and the library tracks Chromium's implementation. Wire-format changes are unlikely but possible. Pin `dbsc-toolkit@~1.5.0` (patch updates only) and read the changelog before bumping.
309
-
310
- Rough readiness estimate: **~85%** for the Express + Redis path (core protocol + main adapter + production storage all solid). **~70%** for Fastify / Hono / Next.js (same core, less production mileage). **~60%** for the fallback tier flows (WebAuthn/HMAC code is there, real-world step-up UX is on the user).
334
+ 1. **Use Redis or Postgres storage**, not memory. Memory storage on a server that ever restarts produces a broken loop where browsers hold cookies that no longer match any stored key.
335
+ 2. **Treat it as defense-in-depth**, never the only auth layer. Your existing session cookie, password, MFA, rate limiting — all still required. This library raises the floor on session-replay attacks; it doesn't replace anything else.
336
+ 3. **Pin a version.** Pin `dbsc-toolkit@~2.0.0` (patch updates only) and read the changelog before bumping. v2 dropped the HMAC and WebAuthn tiers — see CHANGELOG for the migration path.
311
337
 
312
- The realistic adoption pattern: ship it as the second layer behind your existing auth on Chromium-supporting routes. Don't lock non-Chromium users out. Gate genuinely high-value actions (payments, password change, admin) on `tier === "dbsc"`; leave everything else permissive. That's the recipe a Reddit-style site would use — see [docs/integrating-existing-auth.md](./docs/integrating-existing-auth.md).
338
+ The realistic adoption pattern: ship it as the second layer behind your existing auth. The bound polyfill means you don't have to lock non-Chromium users out. Gate genuinely high-value actions (payments, password change, admin) on `tier === "dbsc"`; gate everything else on `tier !== "none"`. See [docs/integrating-existing-auth.md](./docs/integrating-existing-auth.md).
313
339
 
314
340
  ## License
315
341
 
@@ -1,4 +1,11 @@
1
- export { detectClientTier, type ClientTier } from "./detect.js";
2
- export { registerWebAuthn, authenticateWebAuthn } from "./webauthn.js";
3
- export { collectClientSignals, type ClientSignals } from "./signals.js";
1
+ export interface InitBoundDbscOptions {
2
+ statePath?: string;
3
+ challengePath?: string;
4
+ registrationPath?: string;
5
+ refreshPath?: string;
6
+ nativeProbeWindowMs?: number;
7
+ refreshMarginMs?: number;
8
+ }
9
+ export declare function initBoundDbsc(options?: InitBoundDbscOptions): Promise<void>;
10
+ export declare function stopBoundDbsc(): void;
4
11
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AAChE,OAAO,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AACvE,OAAO,EAAE,oBAAoB,EAAE,KAAK,aAAa,EAAE,MAAM,cAAc,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,oBAAoB;IACnC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AA0CD,wBAAsB,aAAa,CAAC,OAAO,GAAE,oBAAyB,GAAG,OAAO,CAAC,IAAI,CAAC,CA4CrF;AAED,wBAAgB,aAAa,IAAI,IAAI,CAKpC"}
@@ -1,4 +1,127 @@
1
- export { detectClientTier } from "./detect.js";
2
- export { registerWebAuthn, authenticateWebAuthn } from "./webauthn.js";
3
- export { collectClientSignals } from "./signals.js";
1
+ import { clearKeyRecord, getKeyRecord, setKeyRecord } from "./keystore.js";
2
+ const DEFAULTS = {
3
+ statePath: "/dbsc-bound/state",
4
+ challengePath: "/dbsc-bound/challenge",
5
+ registrationPath: "/dbsc-bound/registration",
6
+ refreshPath: "/dbsc-bound/refresh",
7
+ nativeProbeWindowMs: 3000,
8
+ refreshMarginMs: 5000,
9
+ };
10
+ let refreshTimer = null;
11
+ export async function initBoundDbsc(options = {}) {
12
+ if (typeof window === "undefined" || typeof indexedDB === "undefined")
13
+ return;
14
+ const cfg = {
15
+ statePath: options.statePath ?? DEFAULTS.statePath,
16
+ challengePath: options.challengePath ?? DEFAULTS.challengePath,
17
+ registrationPath: options.registrationPath ?? DEFAULTS.registrationPath,
18
+ refreshPath: options.refreshPath ?? DEFAULTS.refreshPath,
19
+ nativeProbeWindowMs: options.nativeProbeWindowMs ?? DEFAULTS.nativeProbeWindowMs,
20
+ refreshMarginMs: options.refreshMarginMs ?? DEFAULTS.refreshMarginMs,
21
+ };
22
+ const state = await fetchState(cfg.statePath);
23
+ if (state.phase === "unbound") {
24
+ await clearKeyRecord().catch(() => { });
25
+ return;
26
+ }
27
+ if (state.phase === "bound") {
28
+ if (state.tier === "dbsc")
29
+ return;
30
+ const rec = await getKeyRecord().catch(() => null);
31
+ if (!rec || rec.sessionId !== state.sessionId) {
32
+ await clearKeyRecord().catch(() => { });
33
+ const fresh = await fetchState(cfg.statePath);
34
+ if (fresh.phase === "needs-registration") {
35
+ await runRegistration(fresh.sessionId, fresh.challenge, cfg);
36
+ scheduleRefresh(cfg, state.refreshIntervalMs);
37
+ }
38
+ return;
39
+ }
40
+ scheduleRefresh(cfg, state.refreshIntervalMs);
41
+ return;
42
+ }
43
+ await sleep(cfg.nativeProbeWindowMs);
44
+ const recheck = await fetchState(cfg.statePath);
45
+ if (recheck.phase === "bound" && recheck.tier === "dbsc")
46
+ return;
47
+ if (recheck.phase !== "needs-registration")
48
+ return;
49
+ await runRegistration(recheck.sessionId, recheck.challenge, cfg);
50
+ const final = await fetchState(cfg.statePath);
51
+ if (final.phase === "bound")
52
+ scheduleRefresh(cfg, final.refreshIntervalMs);
53
+ }
54
+ export function stopBoundDbsc() {
55
+ if (refreshTimer !== null) {
56
+ clearTimeout(refreshTimer);
57
+ refreshTimer = null;
58
+ }
59
+ }
60
+ async function fetchState(path) {
61
+ const r = await fetch(path, { credentials: "include" });
62
+ return (await r.json());
63
+ }
64
+ async function runRegistration(sessionId, challenge, cfg) {
65
+ await clearKeyRecord().catch(() => { });
66
+ const keyPair = await crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-256" }, false, ["sign", "verify"]);
67
+ const publicKey = await crypto.subtle.exportKey("jwk", keyPair.publicKey);
68
+ const signature = await signMessage(keyPair.privateKey, challenge);
69
+ const res = await fetch(cfg.registrationPath, {
70
+ method: "POST",
71
+ credentials: "include",
72
+ headers: { "Content-Type": "application/json" },
73
+ body: JSON.stringify({ publicKey, signature, challenge }),
74
+ });
75
+ if (!res.ok) {
76
+ throw new Error(`bound registration failed: ${res.status}`);
77
+ }
78
+ await setKeyRecord({ sessionId, keyPair });
79
+ }
80
+ async function runRefresh(cfg) {
81
+ const rec = await getKeyRecord().catch(() => null);
82
+ if (!rec)
83
+ return false;
84
+ const cRes = await fetch(cfg.challengePath, { credentials: "include" });
85
+ if (!cRes.ok)
86
+ return false;
87
+ const { challenge } = (await cRes.json());
88
+ const timestamp = Date.now();
89
+ const message = `${challenge}.${timestamp}`;
90
+ const signature = await signMessage(rec.keyPair.privateKey, message);
91
+ const res = await fetch(cfg.refreshPath, {
92
+ method: "POST",
93
+ credentials: "include",
94
+ headers: { "Content-Type": "application/json" },
95
+ body: JSON.stringify({ challenge, signature, timestamp }),
96
+ });
97
+ return res.ok;
98
+ }
99
+ function scheduleRefresh(cfg, intervalMs) {
100
+ if (refreshTimer !== null)
101
+ clearTimeout(refreshTimer);
102
+ const wait = Math.max(1000, intervalMs - cfg.refreshMarginMs);
103
+ refreshTimer = setTimeout(async () => {
104
+ const ok = await runRefresh(cfg).catch(() => false);
105
+ if (ok) {
106
+ scheduleRefresh(cfg, intervalMs);
107
+ }
108
+ else {
109
+ refreshTimer = null;
110
+ }
111
+ }, wait);
112
+ }
113
+ async function signMessage(privateKey, message) {
114
+ const data = new TextEncoder().encode(message);
115
+ const sig = await crypto.subtle.sign({ name: "ECDSA", hash: "SHA-256" }, privateKey, data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength));
116
+ return base64urlEncode(new Uint8Array(sig));
117
+ }
118
+ function base64urlEncode(bytes) {
119
+ let s = "";
120
+ for (let i = 0; i < bytes.length; i++)
121
+ s += String.fromCharCode(bytes[i]);
122
+ return btoa(s).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
123
+ }
124
+ function sleep(ms) {
125
+ return new Promise((r) => setTimeout(r, ms));
126
+ }
4
127
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAmB,MAAM,aAAa,CAAC;AAChE,OAAO,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AACvE,OAAO,EAAE,oBAAoB,EAAsB,MAAM,cAAc,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ export interface KeyRecord {
2
+ sessionId: string;
3
+ keyPair: CryptoKeyPair;
4
+ }
5
+ export declare function getKeyRecord(): Promise<KeyRecord | null>;
6
+ export declare function setKeyRecord(rec: KeyRecord): Promise<void>;
7
+ export declare function clearKeyRecord(): Promise<void>;
8
+ //# sourceMappingURL=keystore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keystore.d.ts","sourceRoot":"","sources":["../../src/client/keystore.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,SAAS;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,aAAa,CAAC;CACxB;AAgBD,wBAAsB,YAAY,IAAI,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAQ9D;AAED,wBAAsB,YAAY,CAAC,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAQhE;AAED,wBAAsB,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC,CAQpD"}
@@ -0,0 +1,44 @@
1
+ const DB_NAME = "dbsc-toolkit";
2
+ const STORE_NAME = "bound";
3
+ const KEY_RECORD_KEY = "key-record";
4
+ function openDb() {
5
+ return new Promise((resolve, reject) => {
6
+ const req = indexedDB.open(DB_NAME, 1);
7
+ req.onupgradeneeded = () => {
8
+ const db = req.result;
9
+ if (!db.objectStoreNames.contains(STORE_NAME)) {
10
+ db.createObjectStore(STORE_NAME);
11
+ }
12
+ };
13
+ req.onsuccess = () => resolve(req.result);
14
+ req.onerror = () => reject(req.error);
15
+ });
16
+ }
17
+ export async function getKeyRecord() {
18
+ const db = await openDb();
19
+ return new Promise((resolve, reject) => {
20
+ const tx = db.transaction(STORE_NAME, "readonly");
21
+ const req = tx.objectStore(STORE_NAME).get(KEY_RECORD_KEY);
22
+ req.onsuccess = () => resolve(req.result ?? null);
23
+ req.onerror = () => reject(req.error);
24
+ });
25
+ }
26
+ export async function setKeyRecord(rec) {
27
+ const db = await openDb();
28
+ return new Promise((resolve, reject) => {
29
+ const tx = db.transaction(STORE_NAME, "readwrite");
30
+ tx.objectStore(STORE_NAME).put(rec, KEY_RECORD_KEY);
31
+ tx.oncomplete = () => resolve();
32
+ tx.onerror = () => reject(tx.error);
33
+ });
34
+ }
35
+ export async function clearKeyRecord() {
36
+ const db = await openDb();
37
+ return new Promise((resolve, reject) => {
38
+ const tx = db.transaction(STORE_NAME, "readwrite");
39
+ tx.objectStore(STORE_NAME).delete(KEY_RECORD_KEY);
40
+ tx.oncomplete = () => resolve();
41
+ tx.onerror = () => reject(tx.error);
42
+ });
43
+ }
44
+ //# sourceMappingURL=keystore.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keystore.js","sourceRoot":"","sources":["../../src/client/keystore.ts"],"names":[],"mappings":"AAAA,MAAM,OAAO,GAAG,cAAc,CAAC;AAC/B,MAAM,UAAU,GAAG,OAAO,CAAC;AAC3B,MAAM,cAAc,GAAG,YAAY,CAAC;AAOpC,SAAS,MAAM;IACb,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,MAAM,GAAG,GAAG,SAAS,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;QACvC,GAAG,CAAC,eAAe,GAAG,GAAG,EAAE;YACzB,MAAM,EAAE,GAAG,GAAG,CAAC,MAAM,CAAC;YACtB,IAAI,CAAC,EAAE,CAAC,gBAAgB,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC;gBAC9C,EAAE,CAAC,iBAAiB,CAAC,UAAU,CAAC,CAAC;YACnC,CAAC;QACH,CAAC,CAAC;QACF,GAAG,CAAC,SAAS,GAAG,GAAG,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QAC1C,GAAG,CAAC,OAAO,GAAG,GAAG,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IACxC,CAAC,CAAC,CAAC;AACL,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,YAAY;IAChC,MAAM,EAAE,GAAG,MAAM,MAAM,EAAE,CAAC;IAC1B,OAAO,IAAI,OAAO,CAAmB,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACvD,MAAM,EAAE,GAAG,EAAE,CAAC,WAAW,CAAC,UAAU,EAAE,UAAU,CAAC,CAAC;QAClD,MAAM,GAAG,GAAG,EAAE,CAAC,WAAW,CAAC,UAAU,CAAC,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;QAC3D,GAAG,CAAC,SAAS,GAAG,GAAG,EAAE,CAAC,OAAO,CAAE,GAAG,CAAC,MAAgC,IAAI,IAAI,CAAC,CAAC;QAC7E,GAAG,CAAC,OAAO,GAAG,GAAG,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IACxC,CAAC,CAAC,CAAC;AACL,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,GAAc;IAC/C,MAAM,EAAE,GAAG,MAAM,MAAM,EAAE,CAAC;IAC1B,OAAO,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QAC3C,MAAM,EAAE,GAAG,EAAE,CAAC,WAAW,CAAC,UAAU,EAAE,WAAW,CAAC,CAAC;QACnD,EAAE,CAAC,WAAW,CAAC,UAAU,CAAC,CAAC,GAAG,CAAC,GAAG,EAAE,cAAc,CAAC,CAAC;QACpD,EAAE,CAAC,UAAU,GAAG,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC;QAChC,EAAE,CAAC,OAAO,GAAG,GAAG,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC;IACtC,CAAC,CAAC,CAAC;AACL,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,cAAc;IAClC,MAAM,EAAE,GAAG,MAAM,MAAM,EAAE,CAAC;IAC1B,OAAO,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QAC3C,MAAM,EAAE,GAAG,EAAE,CAAC,WAAW,CAAC,UAAU,EAAE,WAAW,CAAC,CAAC;QACnD,EAAE,CAAC,WAAW,CAAC,UAAU,CAAC,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC;QAClD,EAAE,CAAC,UAAU,GAAG,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC;QAChC,EAAE,CAAC,OAAO,GAAG,GAAG,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC;IACtC,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,6 @@
1
+ export { handleBoundRegistration } from "./registration.js";
2
+ export type { BoundRegistrationRequest, BoundRegistrationResult } from "./registration.js";
3
+ export { handleBoundRefresh } from "./refresh.js";
4
+ export type { BoundRefreshRequest } from "./refresh.js";
5
+ export { verifyP256Signature } from "./verify.js";
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/bound/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,MAAM,mBAAmB,CAAC;AAC5D,YAAY,EAAE,wBAAwB,EAAE,uBAAuB,EAAE,MAAM,mBAAmB,CAAC;AAC3F,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAClD,YAAY,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACxD,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC"}
@@ -0,0 +1,4 @@
1
+ export { handleBoundRegistration } from "./registration.js";
2
+ export { handleBoundRefresh } from "./refresh.js";
3
+ export { verifyP256Signature } from "./verify.js";
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/core/bound/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,MAAM,mBAAmB,CAAC;AAE5D,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAElD,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC"}
@@ -0,0 +1,9 @@
1
+ import type { RefreshProof, StorageAdapter } from "../types.js";
2
+ export interface BoundRefreshRequest {
3
+ sessionId: string;
4
+ signature: string;
5
+ expectedJti: string;
6
+ timestamp: number;
7
+ }
8
+ export declare function handleBoundRefresh(req: BoundRefreshRequest, storage: StorageAdapter): Promise<RefreshProof>;
9
+ //# sourceMappingURL=refresh.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"refresh.d.ts","sourceRoot":"","sources":["../../../src/core/bound/refresh.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAKhE,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,wBAAsB,kBAAkB,CACtC,GAAG,EAAE,mBAAmB,EACxB,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,YAAY,CAAC,CAyDvB"}
@@ -0,0 +1,52 @@
1
+ import { DbscProtocolError, DbscVerificationError, ErrorCodes } from "../errors.js";
2
+ import { verifyP256Signature } from "./verify.js";
3
+ const TIMESTAMP_WINDOW_MS = 60_000;
4
+ export async function handleBoundRefresh(req, storage) {
5
+ if (!req.signature) {
6
+ throw new DbscProtocolError(ErrorCodes.MISSING_RESPONSE_HEADER, "signature is required for bound refresh");
7
+ }
8
+ const skew = Math.abs(Date.now() - req.timestamp);
9
+ if (skew > TIMESTAMP_WINDOW_MS) {
10
+ throw new DbscVerificationError(ErrorCodes.SIGNATURE_INVALID, "timestamp outside acceptable window");
11
+ }
12
+ const key = await storage.getBoundKey(req.sessionId);
13
+ if (!key) {
14
+ throw new DbscVerificationError(ErrorCodes.KEY_NOT_FOUND, "no bound key for session");
15
+ }
16
+ const challenge = await storage.getChallenge(req.expectedJti);
17
+ if (!challenge) {
18
+ throw new DbscVerificationError(ErrorCodes.CHALLENGE_NOT_FOUND, "challenge not found");
19
+ }
20
+ if (challenge.consumed) {
21
+ throw new DbscVerificationError(ErrorCodes.CHALLENGE_CONSUMED, "challenge already consumed");
22
+ }
23
+ if (Date.now() > challenge.expiresAt) {
24
+ throw new DbscVerificationError(ErrorCodes.CHALLENGE_EXPIRED, "challenge expired");
25
+ }
26
+ if (challenge.sessionId !== req.sessionId) {
27
+ throw new DbscVerificationError(ErrorCodes.JTI_MISMATCH, "challenge does not belong to this session");
28
+ }
29
+ const message = `${req.expectedJti}.${req.timestamp}`;
30
+ const ok = await verifyP256Signature(key.jwk, req.signature, message);
31
+ if (!ok) {
32
+ const session = await storage.getSession(req.sessionId);
33
+ if (session) {
34
+ await storage.setSession({ ...session, tier: "none" });
35
+ }
36
+ throw new DbscVerificationError(ErrorCodes.SIGNATURE_INVALID, "signature does not verify");
37
+ }
38
+ const consumed = await storage.consumeChallenge(req.expectedJti);
39
+ if (!consumed) {
40
+ throw new DbscVerificationError(ErrorCodes.CHALLENGE_CONSUMED, "challenge already consumed");
41
+ }
42
+ const session = await storage.getSession(req.sessionId);
43
+ if (session) {
44
+ await storage.setSession({ ...session, tier: "bound", lastRefreshAt: Date.now() });
45
+ }
46
+ return {
47
+ sessionId: req.sessionId,
48
+ jti: req.expectedJti,
49
+ verified: true,
50
+ };
51
+ }
52
+ //# sourceMappingURL=refresh.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"refresh.js","sourceRoot":"","sources":["../../../src/core/bound/refresh.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ import type { BoundKey, StorageAdapter } from "../types.js";
2
+ export interface BoundRegistrationRequest {
3
+ sessionId: string;
4
+ publicKey: JsonWebKey;
5
+ signature: string;
6
+ expectedJti: string;
7
+ }
8
+ export interface BoundRegistrationResult {
9
+ boundKey: BoundKey;
10
+ }
11
+ export declare function handleBoundRegistration(req: BoundRegistrationRequest, storage: StorageAdapter): Promise<BoundRegistrationResult>;
12
+ //# sourceMappingURL=registration.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registration.d.ts","sourceRoot":"","sources":["../../../src/core/bound/registration.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAG5D,MAAM,WAAW,wBAAwB;IACvC,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,UAAU,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,QAAQ,CAAC;CACpB;AAED,wBAAsB,uBAAuB,CAC3C,GAAG,EAAE,wBAAwB,EAC7B,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,uBAAuB,CAAC,CAiElC"}