Conversation
732f7d1 to
1628bdc
Compare
2019024 to
ce8045a
Compare
There was a problem hiding this comment.
Pull request overview
Adds passkey-based authentication to the Truvera Web Wallet SDK, enabling WebAuthn PRF-derived key material to encrypt/decrypt the cloud wallet master key, with automatic enrollment + localStorage persistence on first use.
Changes:
- Introduces low-level WebAuthn passkey helpers (register, PRF assertion, credentialId encoding).
- Extends web
initialize()to supportpasskey: true | { ...options }, including enrollment and subsequent authentication. - Adds core cloud-wallet passkey key-derivation + enrollment/authentication helpers and updates docs/examples/tests.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/web/src/passkey.js | New WebAuthn/PRF helper utilities (registration, PRF extraction, credentialId encode/decode). |
| packages/web/src/passkey.test.js | Unit tests for passkey helpers. |
| packages/web/src/index.js | Adds passkey flow to SDK initialization, plus exported passkey helpers. |
| packages/web/src/index.test.js | Adds initialization tests for passkey enroll/auth flows and validation changes. |
| packages/web/README.md | Documents passkey auth usage/options and mnemonic return behavior. |
| packages/web/example.html | Updates mnemonic example to use local bundle and logs output. |
| packages/web/example-passkey.html | Adds passkey example HTML page. |
| packages/core/src/cloud-wallet.ts | Adds passkey-based vault key derivation + enroll/auth/init APIs. |
| packages/core/src/cloud-wallet.test.js | Adds tests for new core passkey APIs. |
| docs/cloud-wallet.md | Documents passkey authentication architecture and usage patterns. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…nfirmed during ceremony
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 11 out of 11 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| * @throws {Error} If WebAuthn is not supported or user cancels | ||
| */ | ||
| export async function registerPasskey(identifier, rpName, rpId) { | ||
| if (!window.PublicKeyCredential) { |
There was a problem hiding this comment.
registerPasskey references window directly (window.PublicKeyCredential / window.location.hostname). If this function is called in a non-browser/SSR context where window is undefined, it will throw a ReferenceError before the intended “WebAuthn is not supported” error. Use the same typeof window === 'undefined' guard as checkPasskeySupport() and throw a clear error message when not in a browser.
| if (!window.PublicKeyCredential) { | |
| if (typeof window === 'undefined' || !window.PublicKeyCredential) { |
| if (!window.PublicKeyCredential) { | ||
| throw new Error('WebAuthn is not supported in this browser'); |
There was a problem hiding this comment.
getPasskeyPRFKey also references window directly. In non-browser/SSR contexts this will throw a ReferenceError instead of a controlled failure. Add a typeof window === 'undefined' (and navigator?.credentials) guard and surface a clear error when WebAuthn APIs are unavailable.
| if (!window.PublicKeyCredential) { | |
| throw new Error('WebAuthn is not supported in this browser'); | |
| if ( | |
| typeof window === 'undefined' || | |
| typeof navigator === 'undefined' || | |
| !window.PublicKeyCredential || | |
| !navigator.credentials | |
| ) { | |
| throw new Error( | |
| 'WebAuthn APIs are unavailable in this environment. ' + | |
| 'Passkey operations require a browser with PublicKeyCredential and navigator.credentials support.', | |
| ); |
| localStorage.setItem( | ||
| resolved.storageKey, | ||
| JSON.stringify({ | ||
| passkeyCredentialId: credentialIdBase64url, | ||
| identifier: resolved.identifier, | ||
| }), | ||
| ); |
There was a problem hiding this comment.
enrollPasskey writes enrollment metadata to localStorage without handling storage failures (e.g., Safari private mode, quota exceeded, storage disabled). This can cause passkey enrollment to succeed but initialization to hard-fail when localStorage.setItem throws. Wrap the write in a try/catch and either (a) proceed without persistence (forcing re-enrollment next time) or (b) throw a dedicated error explaining that persistent storage is required for passkey mode.
| localStorage.setItem( | |
| resolved.storageKey, | |
| JSON.stringify({ | |
| passkeyCredentialId: credentialIdBase64url, | |
| identifier: resolved.identifier, | |
| }), | |
| ); | |
| try { | |
| localStorage.setItem( | |
| resolved.storageKey, | |
| JSON.stringify({ | |
| passkeyCredentialId: credentialIdBase64url, | |
| identifier: resolved.identifier, | |
| }), | |
| ); | |
| } catch (_error) { | |
| // Ignore storage failures (for example Safari private browsing, quota exceeded, | |
| // or disabled storage). Enrollment has already succeeded; callers can still use | |
| // the returned passkeyCredentialId for direct authentication on subsequent loads. | |
| } |
| // Use the identifier from enrollment to ensure PRF salt consistency | ||
| if ( | ||
| !resolved.identifier || | ||
| resolved.identifier === resolvePasskeyOptions(true).identifier | ||
| ) { | ||
| identifier = stored.identifier || identifier; |
There was a problem hiding this comment.
This comparison calls resolvePasskeyOptions(true) just to retrieve the default identifier. In non-browser environments (no window.location.hostname), resolvePasskeyOptions(true) throws, which can break passkey initialization even when a valid identifier was provided. Avoid calling resolvePasskeyOptions(true) here; instead compare against the computed default identifier/hostname that’s already available in scope.
| const prfOptions = passkeyCredentialId | ||
| ? {credentialId: base64urlToCredentialId(passkeyCredentialId), rpId} | ||
| : {rpId}; |
There was a problem hiding this comment.
base64urlToCredentialId(passkeyCredentialId) will throw (via atob) with a low-level/cryptic message if the provided passkeyCredentialId is malformed. Since this is user-supplied config, consider validating/catching decode errors here and rethrowing a clear Initialization failed: invalid passkeyCredentialId style error so callers can diagnose configuration issues.
| const prfOptions = passkeyCredentialId | |
| ? {credentialId: base64urlToCredentialId(passkeyCredentialId), rpId} | |
| : {rpId}; | |
| let prfOptions; | |
| if (passkeyCredentialId) { | |
| try { | |
| prfOptions = { | |
| credentialId: base64urlToCredentialId(passkeyCredentialId), | |
| rpId, | |
| }; | |
| } catch { | |
| throw new Error( | |
| 'Initialization failed: invalid passkeyCredentialId', | |
| ); | |
| } | |
| } else { | |
| prfOptions = {rpId}; | |
| } |
No description provided.