dbsc-toolkit 1.4.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 +104 -47
  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 +5 -5
  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 +5 -1
  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 +137 -7
  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,19 +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
- Chrome 147+ supports DBSC natively. This library handles the server side. Verified end-to-end against Chrome 147 on Windows.
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) |
16
+
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.
13
22
 
14
23
  ## Live demo
15
24
 
16
25
  Try it: <https://dbsc-toolkit.onrender.com/>
17
26
 
18
- Open in Chrome 147+, click **Login**, then **Check session** — `tier` reads `"dbsc"` once the TPM 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.
19
34
 
20
- > 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.
21
38
 
22
39
  ## Install
23
40
 
@@ -68,6 +85,17 @@ app.listen(3000);
68
85
 
69
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.
70
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
+
71
99
  A full demo with `/me`, `/logout`, and `/clear-cookies` is in [examples/express/src/server.js](./examples/express/src/server.js).
72
100
 
73
101
  ## Adding DBSC to an existing app
@@ -83,12 +111,12 @@ Full integration story, per-route policy table, and rollout timeline in [docs/in
83
111
 
84
112
  | Import | What it is |
85
113
  |--------|------------|
86
- | `dbsc-toolkit` | Core types, crypto, protocol functions |
114
+ | `dbsc-toolkit` | Core types, crypto, protocol functions (native DBSC + bound polyfill) |
87
115
  | `dbsc-toolkit/express` | Express middleware |
88
116
  | `dbsc-toolkit/fastify` | Fastify plugin |
89
117
  | `dbsc-toolkit/hono` | Hono middleware |
90
118
  | `dbsc-toolkit/nextjs` | Next.js App Router middleware + handlers |
91
- | `dbsc-toolkit/client` | Browser SDK for fallback paths |
119
+ | `dbsc-toolkit/client` | Browser SDK — `initBoundDbsc()` for the polyfill |
92
120
  | `dbsc-toolkit/storage/memory` | In-memory storage (dev/test) |
93
121
  | `dbsc-toolkit/storage/redis` | Redis storage |
94
122
  | `dbsc-toolkit/storage/postgres` | Postgres storage |
@@ -97,49 +125,55 @@ Tree-shaking eliminates anything you don't import.
97
125
 
98
126
  ## How a verified flow looks
99
127
 
128
+ On Chromium 145+:
129
+
100
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`).
101
- 2. Chrome immediately POSTs to `/dbsc/registration` with `Secure-Session-Response: <jws>`. The JWS carries the device public key signed by the matching private key (TPM).
102
- 3. Middleware verifies the JWS, stores the public key bound to the session, sets `__Host-dbsc-session` cookie, returns DBSC session config JSON.
103
- 4. From now on, every refresh cycle (default 10 min) Chrome signs a fresh challenge with the TPM key. A copied cookie cannot pass refresh — the attacker has no key.
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).
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.
134
+
135
+ On Firefox / Safari / older Chromium (with the `initBoundDbsc()` client SDK loaded on the page):
104
136
 
105
- `tier` on `res.locals.dbsc` reads `"dbsc"` once registration completes.
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).
106
143
 
107
144
  ## Using the tier to actually defend
108
145
 
109
- 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"`:
110
149
 
111
150
  ```ts
112
- app.get("/payment", (req, res) => {
113
- if (res.locals.dbsc.tier !== "dbsc") {
114
- 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" });
115
154
  }
116
- // safe to process payment
155
+ // safe — request is bound to a hardware-resident or browser-resident key
117
156
  });
118
157
  ```
119
158
 
120
- 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 TPM proof, tier drops to `"none"` (or stays at the lower fallback tier) and your gate refuses the request.
121
-
122
- Suggested handling per tier in a real application:
123
-
124
- - `tier === "dbsc"`: full access. Payments, account changes, anything sensitive.
125
- - `tier === "webauthn"`: most access. Hardware-bound via platform authenticator.
126
- - `tier === "hmac"`: read-only or low-risk actions. The binding is best-effort.
127
- - `tier === "none"`: treat as unauthenticated. Force re-login, revoke the session, log a `session_stolen` candidate, depending on context.
128
-
129
- 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"`:
130
160
 
131
161
  ```ts
132
- function requireDbsc(req, res, next) {
162
+ app.post("/payment", (req, res) => {
133
163
  if (res.locals.dbsc.tier !== "dbsc") {
134
- return res.status(401).json({ error: "re-authenticate" });
164
+ return res.status(403).json({ error: "hardware-bound session required" });
135
165
  }
136
- next();
137
- }
138
-
139
- app.post("/payment", requireDbsc, handler);
140
- app.post("/account/email", requireDbsc, handler);
166
+ });
141
167
  ```
142
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
+
143
177
  See [docs/security/best-practices.md](./docs/security/best-practices.md) for the full tier-policy guidance.
144
178
 
145
179
  ## Local testing
@@ -182,16 +216,13 @@ const result = await handleRefresh({
182
216
 
183
217
  ## Protection tiers
184
218
 
185
- The library negotiates the strongest available binding per session:
186
-
187
- | Tier | Mechanism | Protection |
188
- |------|-----------|------------|
189
- | `dbsc` | Hardware-backed key, Chrome 147+ | Hardware binding exfiltrated cookie is useless |
190
- | `webauthn` | Platform authenticator | Hardware binding via platform authenticator |
191
- | `hmac` | HMAC + browser signals | Best-effort context binding, not hardware |
192
- | `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 |
193
224
 
194
- The tier is available at `res.locals.dbsc.tier` (Express), `c.get("dbscTier")` (Hono), `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"`.
195
226
 
196
227
  ## Storage
197
228
 
@@ -235,7 +266,7 @@ app.use(dbsc({
235
266
  }));
236
267
  ```
237
268
 
238
- Event types: `registration`, `refresh`, `verification_failure`, `session_stolen`, `fallback_tier`.
269
+ Event types: `registration`, `refresh`, `verification_failure`, `session_stolen`, `tier_change`.
239
270
 
240
271
  ## Skipped sessions
241
272
 
@@ -259,7 +290,7 @@ The quota is scoped per `(browser install, origin)`, not per origin globally. A
259
290
 
260
291
  ## Header naming
261
292
 
262
- The W3C draft renamed the headers from `Sec-Session-*` to `Secure-Session-*`. Chrome 147 acts on the new names. The middleware reads both and writes both for compatibility. If you build response headers manually, send both:
293
+ The W3C draft renamed the headers from `Sec-Session-*` to `Secure-Session-*`. Chromium 145+ acts on the new names. The middleware reads both and writes both for compatibility. If you build response headers manually, send both:
263
294
 
264
295
  ```ts
265
296
  res.setHeader("Secure-Session-Registration", header);
@@ -270,16 +301,42 @@ res.setHeader("Sec-Session-Registration", header);
270
301
 
271
302
  Defense-in-depth layer. Does not replace TLS, secure cookie flags, MFA, or server hardening.
272
303
 
273
- 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.
274
305
 
275
- 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.
276
307
 
277
308
  ## Project status
278
309
 
279
310
  - Single package on npm: `dbsc-toolkit`
280
- - Verified end-to-end on Chrome 147 / Windows / TPM 2.0
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
313
+ - Verified end-to-end on Chrome 147 / Windows / TPM 2.0 (other Chromium browsers and platforms should work but not independently verified)
281
314
  - No third-party security audit yet
282
315
 
316
+ ## Production readiness
317
+
318
+ Honest table — what you're getting and where the rough edges are.
319
+
320
+ | Area | Status | Confidence |
321
+ |------|--------|-----------|
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 |
324
+ | Express adapter | Stable | High — used in the live demo, exercised on Render |
325
+ | Fastify / Hono / Next.js adapters | Stable | Medium — unit tests pass, share core code with Express, not battle-tested in production |
326
+ | `MemoryStorage` | Dev / test only | N/A — explicitly non-production |
327
+ | `RedisStorage` | Stable | Medium — atomic challenge consume via Lua, tested locally |
328
+ | `PostgresStorage` | Stable | Medium — migrations included, tested locally |
329
+ | Security audit | None | — |
330
+ | W3C spec stability | Draft, library tracks Chromium's implementation | Spec may evolve; expect occasional wire-format adjustments |
331
+
332
+ **Should you use this in production?** Yes, with three conditions:
333
+
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.
337
+
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).
339
+
283
340
  ## License
284
341
 
285
342
  Apache 2.0
@@ -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"}