feat: add DPoP core storage layer (ENG-4782)#201
Conversation
- Add `dpop` v2.1.1 runtime dependency and `fake-indexeddb` devDependency to packages/core/package.json - SDKConfig: add optional `useDpop` and `dpopTokenStorage` fields - UrlHelper: add `getAuthorizeUrl(state?, dpopJkt?, codeChallenge?)` targeting FusionAuth /oauth2/authorize directly; update UrlHelperTypes to include response_type, code_challenge, code_challenge_method, dpop_jkt - DPoPStorage: IndexedDB abstraction for ES256 CryptoKeyPair persistence (db: fusionauth-sdk:dpop, store: keypair, keyed by clientId) - DPoPTokenStore: localStorage/memory token storage for DPoP-bound tokens (key: fusionauth-sdk:tokens:<clientId>); includes getAccessToken() and isExpired getter - packages/core/src/DPoP/index.ts re-exports both classes - 54 tests passing (21 DPoPTokenStore, 6 DPoPStorage, 16 UrlHelper, 7 SDKCore, 4 CookieHelpers)
There was a problem hiding this comment.
Pull request overview
This PR introduces the core building blocks needed to support DPoP mode in @fusionauth-sdk/core, adding a persistent keypair store (IndexedDB), a token store (localStorage/memory), and an authorize URL helper for DPoP + PKCE flows.
Changes:
- Added DPoP keypair persistence via IndexedDB (
DPoPStorage) and corresponding tests. - Added a DPoP token storage abstraction (
DPoPTokenStore) with localStorage/memory backends and tests. - Extended
UrlHelperto generate a direct/oauth2/authorizeURL for DPoP/PKCE, plus type/test updates and public exports.
Reviewed changes
Copilot reviewed 12 out of 13 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/core/src/UrlHelper/UrlHelperTypes.ts | Expands query param typing to include DPoP + PKCE authorize parameters. |
| packages/core/src/UrlHelper/UrlHelper.ts | Adds getAuthorizeUrl for direct FusionAuth /oauth2/authorize URL generation. |
| packages/core/src/UrlHelper/UrlHelper.test.ts | Adds coverage for getAuthorizeUrl query string construction. |
| packages/core/src/SDKConfig/SDKConfig.ts | Introduces DPoP opt-in configuration (useDpop, dpopTokenStorage). |
| packages/core/src/index.ts | Exposes the new DPoP module from the package entrypoint. |
| packages/core/src/DPoP/index.ts | Barrel exports for DPoP storage/token store APIs. |
| packages/core/src/DPoP/DPoPTokenStore.ts | Implements token persistence abstraction for DPoP mode. |
| packages/core/src/DPoP/DPoPTokenStore.test.ts | Adds tests for localStorage and memory token persistence behavior. |
| packages/core/src/DPoP/DPoPStorage.ts | Implements IndexedDB-backed persistence for DPoP keypairs. |
| packages/core/src/DPoP/DPoPStorage.test.ts | Adds IndexedDB persistence tests using fake-indexeddb. |
| packages/core/package.json | Adds dpop runtime dependency and fake-indexeddb dev dependency. |
| AGENTS.md | Adds repo agent guidance (workspace layout, testing notes, and conventions). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Remove 'as any' cast in catch block — reject() accepts unknown directly - Add tests for indexedDB unavailable (SSR/non-browser): all three public methods (getKeyPair, setKeyPair, clearKeyPair) reject with a descriptive error - Add test for indexedDB.open() throwing synchronously (e.g. security policy block)
All three DPoPStorage methods (getKeyPair, setKeyPair, clearKeyPair) now resolve on tx.oncomplete and reject on tx.onerror / tx.onabort. Previously, resolving on req.onsuccess meant the caller was told 'success' before the transaction had fully committed — a transaction abort occurring after the request succeeded (e.g. quota exceeded) would go undetected. Applies the same fix consistently to all three methods, including getKeyPair (readonly, lower risk, but now consistent) and setKeyPair (readwrite, same durability concern as clearKeyPair). Adds a test that aborts a clearKeyPair transaction synchronously inside the request onsuccess handler and verifies the promise rejects and the key pair is still present in IndexedDB.
mrudatsprint
left a comment
There was a problem hiding this comment.
self-review completed
lyleschemmerling
left a comment
There was a problem hiding this comment.
The use of the indexedDB looks good. I am still not sold that this DPoP library is worth adding the dependency. Have you looked at what it would take to do without it?
The functionality to be implemented if we didn't use the library:
I believe this can be implemented in 1-2 days. Would we want to perform cross-validation testing against the panva/dpop library as a sanity check that we're building the thumbprint and proof correctly? Also, integration testing against FusionAuth will give us the best validation. Thus, end to end tests would be key. The RFC has an example to validate against. |
…g object - Export DEFAULT_DPOP_DB_NAME, DEFAULT_DPOP_DB_VERSION, DEFAULT_DPOP_STORE_NAME as named constants (no hardcoded magic strings anywhere in the codebase) - Add DPoPStorageConfig interface with clientId (required) and optional dbName, dbVersion, storeName fields — each defaults to the exported constant - Refactor DPoPStorage constructor from positional (clientId: string) to config object, matching the UrlHelperConfig convention in this monorepo - openDb() and all three public methods now reference instance fields (this.dbName, this.dbVersion, this.storeName) instead of module constants - Add tests: defaults apply when no config overrides provided; custom dbName and storeName land data in the right database; two instances with different dbNames but the same clientId do not share keys; dbVersion downgrade produces a clean rejection (VersionError) - Update AGENTS.md: note the config-object constructor convention and the IndexedDB dbVersion must-only-increase constraint
f5b1143
into
parent/dpop-in-the-javascript-sdk
Issue:
Description:
Implement the storage for the Cryptographic Key Pair (DPoPStorage) and the storage for the tokens (DPoPTokenStore)