Conversation
WalkthroughIntroduces WebAuthn/passkeys support by adding native electron-webauthn addon integration with IPC handlers for credential operations, preload-level API bridges for intercepting credentials API calls, FIDO2 data mapping utilities, build configuration updates, and Electron security hardening settings. Changes
Sequence Diagram(s)sequenceDiagram
participant Web as Web App<br/>(Renderer)
participant Preload as Preload<br/>(Bridge)
participant Main as Main Process<br/>(IPC)
participant Addon as Native Addon<br/>(electron-webauthn)
Web->>Preload: navigator.credentials.create(options)
Preload->>Preload: Intercept & validate options
Preload->>Preload: Map to CreateCredentialParams
Preload->>Main: IPC: webauthn:create(params)
Main->>Main: Validate options & derive origin
Main->>Main: Resolve window & extract frame info
Main->>Addon: createCredential(origin, topFrameOrigin,<br/>isPublicSuffix, handle)
Addon->>Addon: Process credential creation
Addon-->>Main: CreateCredentialResult
Main->>Main: Check for errors
Main-->>Preload: Result or error
Preload->>Preload: Map to PublicKeyCredential
Preload->>Preload: Translate errors to DOMException
Preload-->>Web: Promise<Credential | null>
sequenceDiagram
participant Web as Web App<br/>(Renderer)
participant Preload as Preload<br/>(Bridge)
participant Main as Main Process<br/>(IPC)
participant Addon as Native Addon<br/>(electron-webauthn)
Web->>Preload: navigator.credentials.get(options)
Preload->>Preload: Intercept & validate options
Preload->>Preload: Map to AssertCredentialParams
Preload->>Main: IPC: webauthn:get(params)
Main->>Main: Block conditional mediation
Main->>Main: Validate & derive origin
Main->>Addon: getCredential(origin, topFrameOrigin,<br/>isPublicSuffix, handle)
Addon->>Addon: Process assertion
Addon-->>Main: AssertCredentialResult
Main-->>Preload: Result or error
Preload->>Preload: Map to PublicKeyCredential
Preload->>Preload: Construct response objects
Preload-->>Web: Promise<Credential | null>
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Build artifacts for all platforms are ready! 🚀Download the artifacts for: One-line installer (Unstable):bunx flow-debug-build --open 20861479992(execution 20861479992 / attempt 1) |
…ten flag + cross origin iframe support
e137019 to
d24fdc9
Compare
There was a problem hiding this comment.
Actionable comments posted: 17
🤖 Fix all issues with AI agents
In @package.json:
- Around line 112-116: Add explicit documentation for the two entries in the
trustedDependencies list: update the package.json by adding a top-level
"trustedDependenciesNotes" (or similar descriptive field) explaining that
"electron-winstaller" is a transitive dependency used for Windows installer
generation via electron-builder and "objc-js" is a transitive macOS native
dependency required by electron-webauthn; mention that their versions are locked
in bun.lock for reproducibility and security. Ensure the notes reference the
trustedDependencies array and the exact dependency names ("electron-winstaller",
"objc-js") so reviewers can find the rationale without changing the dependency
list itself.
- Line 78: Replace the deprecated externalizeDepsPlugin usage in
electron.vite.config.ts: remove any imports and plugin entries referencing
externalizeDepsPlugin and instead add build.externalizeDeps configuration to
both the main and preload build configs; for the main build set
build.externalizeDeps.exclude to ["electron-context-menu","hono"] and for the
preload build set build.externalizeDeps.exclude to
["electron-chrome-extensions"], keeping all other build settings intact (note
electron-vite v5 enables externalization by default so only these override
arrays are required).
In @src/main/controllers/tabs-controller/tab.ts:
- Around line 98-104: The comment misstates behavior: explain that
nodeIntegration: false disables Node in the main renderer,
nodeIntegrationInSubFrames: true only enables Node in subframes/iframes (not
preload scripts), and contextIsolation: true isolates renderer globals so
preload scripts (enabled via the webPreferences.preload option) are used to
expose safe APIs; confirm whether Node in iframes is intentional and either set
nodeIntegrationInSubFrames to false or keep it and document the reason, and if
you intend to use a preload script, enable the preload option (uncomment/set
webPreferences.preload) and update the comment to accurately describe each
setting (nodeIntegration, nodeIntegrationInSubFrames, contextIsolation,
preload).
In @src/main/ipc/webauthn/index.ts:
- Around line 143-155: The getCredential branch currently awaits
webauthn.getCredential and either returns result.error or result.data but does
not wrap thrown exceptions; update the handler to mirror the create path by
surrounding the await call in a try/catch, catch any thrown error from
webauthn.getCredential(publicKeyOptions, { currentOrigin, topFrameOrigin,
isPublicSuffix, nativeWindowHandle: win.getNativeWindowHandle() }), map/shape
the caught error into the same AssertCredentialErrorCodes | null structure used
by the create handler, and return that shaped error instead of letting the
exception propagate.
- Around line 94-156: The get handler for "webauthn:get" can call
webauthn.getCredential with publicKeyOptions undefined; add an early return when
options.publicKey is missing (same guard as in webauthn:create) to avoid passing
undefined to webauthn.getCredential. Specifically, inside the ipcMain.handle for
"webauthn:get", check if options.publicKey (publicKeyOptions) is falsy and
return null (or the same sentinel used by create) before proceeding to call
webauthn.getCredential, so the addon always receives a defined publicKeyOptions.
- Around line 32-92: The IPC handler for "webauthn:create" currently calls
webauthn.createCredential(...) without a try/catch, so thrown exceptions can
escape the handler; wrap the call to webauthn.createCredential(publicKeyOptions,
{...}) in a try/catch and convert any thrown error into one of the declared
return values (a CreateCredentialErrorCodes value or null) before returning to
the caller, while keeping the existing handling for result.success === false and
returning result.data on success; ensure the catch covers both synchronous
throws and rejected promises and returns a sensible mapped error code (or null)
consistently.
- Around line 11-30: The TypeScript DOM types (BufferSource,
CredentialCreationOptions, CredentialRequestOptions) used by
bufferSourceToBuffer and related WebAuthn code aren’t available in the
main-process tsconfig, so the build will fail; fix by either adding the DOM lib
to the node compiler options (add "lib": ["DOM","ES2022"] to compilerOptions in
tsconfig.node.json) or by creating/using local type declarations (e.g., a shared
types file that exports BufferSource and the WebAuthn interfaces and import
those into src/main/ipc/webauthn/index.ts) and update imports; ensure whichever
approach you pick is applied consistently for all WebAuthn-related symbols
(BufferSource, CredentialCreationOptions, CredentialRequestOptions) referenced
in the file.
In @src/main/ipc/webauthn/module.ts:
- Around line 1-15: The getWebauthnAddon function can race on concurrent calls
and propagate import errors; introduce a module-level Promise cache (e.g.,
webauthnModulePromise: Promise<WebauthnModule | null> | null) alongside the
existing webauthnModule, populate that promise on the first import attempt, and
await it in subsequent callers to prevent duplicate imports; wrap the dynamic
import("electron-webauthn") in try/catch inside that promise, set webauthnModule
on success and return it, and on failure clear the promise cache and return null
(do not throw) so IPC callers receive null instead of an uncaught error.
In @src/main/ipc/webauthn/psl-check.ts:
- Around line 1-9: The isPublicSuffix function currently treats any input with
r.domain === null as a public suffix, which misclassifies invalid hostnames like
"localhost"; update the check to use the parse result's publicSuffix property:
call parseTld(d, { allowPrivateDomains: false }) as before but return true only
when r.publicSuffix !== null && r.publicSuffix === d (i.e., the parsed
publicSuffix exactly matches the input), leaving the function name
isPublicSuffix and the use of parseTld unchanged.
In @src/preload/index.ts:
- Around line 248-253: The implementation of
PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable calls
patchedCredentials.isAvailable() twice causing extra IPC; modify the function to
call patchedCredentials.isAvailable() once, store the boolean result in a local
variable, and then return that stored result (falling back to calling
oldIsUserVerifyingPlatformAuthenticatorAvailable() only if patched result is
falsy) so you avoid the duplicate IPC round-trip; locate the method by name
PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable and the helper
patchedCredentials.isAvailable()/oldIsUserVerifyingPlatformAuthenticatorAvailable
to apply this change.
In @src/preload/webauthn/fido2-utils.ts:
- Around line 97-105: bufferToString duplicates the base64url conversion logic;
change it to call the existing fromB64toUrlB64 helper instead of performing
manual replace operations. Specifically, after obtaining b64 from
fromBufferToB64, pass it into Fido2Utils.fromB64toUrlB64 (and handle the
null/error case consistently with existing behavior) so bufferToString returns
the normalized base64url string using the shared implementation used by
fromB64toUrlB64.
- Around line 97-105: bufferToString currently casts uint8Array.buffer to
ArrayBuffer which breaks if the backing store is a SharedArrayBuffer; update
bufferToString (and use Fido2Utils.bufferSourceToUint8Array) to detect if
uint8Array.buffer instanceof SharedArrayBuffer and, in that case, create a new
ArrayBuffer by slicing/copying the Uint8Array (e.g., using uint8Array.slice()
and taking its .buffer) before passing it to Fido2Utils.fromBufferToB64,
otherwise continue using uint8Array.buffer directly; ensure the resulting
variable is an ArrayBuffer for fromBufferToB64.
In @src/preload/webauthn/helpers.ts:
- Around line 42-46: rpId.includes(":") wrongly blocks IPv6 and isLocalhostLike
uses isIpLiteral(host) which makes all IPs treated as localhost; instead, ensure
IPv6 rpIds are handled by checking isIpLiteral(host) (or a dedicated
isIPv6/isLoopback helper) before rejecting on colon, and restrict
isLocalhostLike to true only for literal loopback addresses or the hostname
"localhost" (e.g., replace isIpLiteral(host) with an isLoopbackAddress(host)
check). Update the secureEnough expression to use that isLoopbackAddress(host)
with allowInsecureLocalhost && scheme === "http:" so only loopback allows http,
and remove or reorder the rpId.includes(":") guard so IPv6 rpIds pass into the
IP-origin branch; apply the same changes to the other similar block referenced
(lines ~56-64).
- Around line 76-91: Skip PSL rejection for localhost and IP literals by
checking rpId before calling isPublicSuffix: if rpId === "localhost" or it's an
IP literal (IPv4 or IPv6) then treat as safe and do not return the public-suffix
error. Update the branch that calls isPublicSuffix(rpId) to first detect IPs
(use an existing utility like net.isIP or add a small IP check helper) and skip
the check for those cases; also adjust the fallback branch so it does not reject
localhost or IP literals when doing the single-label check; keep the same return
shapes (ok:false, rpId, reason) for other failures and ensure references to
isPublicSuffix and rpId are used to locate the changes.
In @src/shared/types/fido2-types.ts:
- Around line 220-229: Update the JSDoc above the CreateCredentialErrorCodes
type to accurately describe it as error codes for creating a credential (not
asserting); replace the incorrect "Error codes for asserting a credential"
comment with something like "Error codes for creating a credential" and ensure
the AssertCredentialErrorCodes comment remains describing asserting a credential
to avoid the copy-paste mismatch between CreateCredentialErrorCodes and
AssertCredentialErrorCodes.
- Around line 257-262: The FallbackRequestedError class should set the error
name for clearer identification; update the constructor of
FallbackRequestedError to assign this.name = "FallbackRequestedError" (and
optionally call Error.captureStackTrace(this, FallbackRequestedError) for proper
stack traces in V8) while preserving the existing readonly fallbackRequested
flag and super("FallbackRequested") call.
📜 Review details
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock
📒 Files selected for processing (15)
.gitignorebuild/entitlements.mac.plistbuild/profile.provisionprofileelectron-builder.tspackage.jsonsrc/main/controllers/tabs-controller/tab.tssrc/main/ipc/index.tssrc/main/ipc/webauthn/index.tssrc/main/ipc/webauthn/module.tssrc/main/ipc/webauthn/psl-check.tssrc/preload/index.tssrc/preload/webauthn/fido2-utils.tssrc/preload/webauthn/helpers.tssrc/preload/webauthn/webauthn-utils.tssrc/shared/types/fido2-types.ts
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{js,jsx,ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc)
**/*.{js,jsx,ts,tsx}: Usebun <file>instead ofnode <file>orts-node <file>
Bun automatically loads .env, so don't use dotenv.
UseBun.serve()for HTTP servers and WebSockets instead ofexpress.
Usebun:sqlitefor SQLite instead ofbetter-sqlite3.
UseBun.redisfor Redis instead ofioredis.
UseBun.sqlfor Postgres instead ofpgorpostgres.js.
Use built-inWebSocketinstead ofws.
PreferBun.fileovernode:fs's readFile/writeFile.
UseBun.$for shell commands instead of execa.
Files:
src/main/ipc/webauthn/module.tssrc/main/ipc/webauthn/index.tssrc/main/ipc/index.tssrc/preload/webauthn/helpers.tssrc/preload/index.tssrc/main/controllers/tabs-controller/tab.tssrc/shared/types/fido2-types.tssrc/main/ipc/webauthn/psl-check.tssrc/preload/webauthn/webauthn-utils.tssrc/preload/webauthn/fido2-utils.tselectron-builder.ts
**/*.{html,ts,css}
📄 CodeRabbit inference engine (.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc)
Use
bun build <file.html|file.ts|file.css>instead ofwebpackoresbuild
Files:
src/main/ipc/webauthn/module.tssrc/main/ipc/webauthn/index.tssrc/main/ipc/index.tssrc/preload/webauthn/helpers.tssrc/preload/index.tssrc/main/controllers/tabs-controller/tab.tssrc/shared/types/fido2-types.tssrc/main/ipc/webauthn/psl-check.tssrc/preload/webauthn/webauthn-utils.tssrc/preload/webauthn/fido2-utils.tselectron-builder.ts
package.json
📄 CodeRabbit inference engine (.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc)
package.json: Usebun installinstead ofnpm installoryarn installorpnpm install
Usebun run <script>instead ofnpm run <script>oryarn run <script>orpnpm run <script>
Files:
package.json
🧠 Learnings (1)
📚 Learning: 2025-07-24T08:49:07.217Z
Learnt from: CR
Repo: MultiboxLabs/flow-browser PR: 0
File: .cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc:0-0
Timestamp: 2025-07-24T08:49:07.217Z
Learning: Applies to **/*.{js,jsx,ts,tsx} : Use `bun:sqlite` for SQLite instead of `better-sqlite3`.
Applied to files:
package.json
🧬 Code graph analysis (4)
src/main/ipc/webauthn/index.ts (2)
src/shared/types/fido2-types.ts (4)
CreateCredentialResult(137-160)CreateCredentialErrorCodes(223-229)AssertCredentialResult(200-218)AssertCredentialErrorCodes(234-239)src/main/ipc/webauthn/module.ts (1)
getWebauthnAddon(5-15)
src/preload/webauthn/helpers.ts (1)
src/main/ipc/webauthn/psl-check.ts (1)
isPublicSuffix(5-9)
src/preload/index.ts (2)
src/shared/types/fido2-types.ts (4)
CreateCredentialResult(137-160)AssertCredentialResult(200-218)AssertCredentialErrorCodes(234-239)CreateCredentialErrorCodes(223-229)src/preload/webauthn/webauthn-utils.ts (1)
WebauthnUtils(9-270)
src/preload/webauthn/fido2-utils.ts (1)
src/shared/types/fido2-types.ts (2)
CreateCredentialResult(137-160)AssertCredentialResult(200-218)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Cursor Bugbot
🔇 Additional comments (14)
.gitignore (1)
7-10: LGTM!The added gitignore entries are appropriate. The yalc-related entries (.yalc, yalc.lock) handle local package development artifacts, and test.sh excludes development test scripts from version control.
build/entitlements.mac.plist (2)
7-8: LGTM!The hardened runtime entitlement is required for macOS notarization and distribution.
35-36: LGTM!The web-browser public-key-credential entitlement correctly enables WebAuthn/Passkeys support on macOS, allowing the browser to access platform authenticators.
electron-builder.ts (1)
95-95: Provisioning profile file is already present in the repository.The provisioning profile at
build/profile.provisionprofileexists and is committed to the repository. The file is a valid macOS provisioning profile with the correct entitlements, including the web-browser.public-key-credential capability. No action is needed.src/main/ipc/index.ts (1)
26-28: Side-effect IPC registration import looks fine.Assuming
@/ipc/webauthnonly registers handlers once (module cache) and returnsNotSupportedErroroff macOS, this is an appropriate place to wire it in.package.json (1)
43-43: No action needed—electron-webauthnis already properly guarded.The package supports Windows and Linux with prebuilt binaries. The codebase already has a runtime check (
process.platform !== "darwin") that gracefully returnsnullon non-macOS platforms, and all callers handle thenullcase by returning error codes orfalse. Cross-platform installs and builds will succeed without issues.Likely an incorrect or invalid review comment.
src/preload/index.ts (2)
150-163: Cached availability check may become stale.The
shouldUseMacOSWebauthnAddonfunction caches the availability result indefinitely. If the underlying WebAuthn service availability changes during the page lifecycle (e.g., system settings change, hardware events), the cached value won't reflect the new state.Is this intentional? If platform authenticator availability can change during runtime, consider either removing the cache or implementing cache invalidation. If it's guaranteed to be stable for the page lifetime, this is acceptable.
101-104: Good security practice: temporary exposure with cleanup.The implementation correctly exposes
electronCredentialstemporarily viacontextBridge.exposeInMainWorld, uses it immediately inexecuteInMainWorld, and then deletes it to prevent external access. This is a solid pattern for bridging preload and main world contexts securely.Also applies to: 144-144, 266-271
src/shared/types/fido2-types.ts (1)
1-47: Well-documented abstract service and types.The
Fido2ClientServiceabstract class with JSDoc references to the W3C WebAuthn specification provides a clean abstraction layer. The genericParentWindowReferenceparameter allows flexibility for different window reference implementations.src/preload/webauthn/fido2-utils.ts (1)
159-175: Good: Proper base64url padding handling.The
fromUrlB64ToB64method correctly handles all padding cases including the invalid case (length % 4 == 1), which throws an appropriate error.src/preload/webauthn/webauthn-utils.ts (4)
93-109: Good: AuthenticatorAttestationResponse implementation matches spec.The implementation correctly provides getter methods (
getAuthenticatorData,getPublicKey,getPublicKeyAlgorithm,getTransports) that match the WebAuthn specification. Creating new buffers on each call is consistent with browser behavior.
222-269: Consistent credential assertion result mapping.The
mapCredentialAssertResultimplementation follows the same patterns asmapCredentialRegistrationResult, properly handling optional extensions and setting prototype chains forinstanceofcompatibility.
72-78: Good defensive handling for spec-deviant sites.The code correctly handles the KeycloakJS edge case where
algmay be passed as a string. Converting toNumber()and filteringNaNvalues is a pragmatic approach. The comment documenting this deviation is helpful.
10-15: Remove unused mapping functions or clarify their intended use.
mapCredentialCreationOptionsandmapCredentialRequestOptionsare defined but never used anywhere in the codebase. The preload script passes options directly toipcRenderer.invokewithout mapping (lines 112, 124-126), and the main process IPC handlers insrc/main/ipc/webauthn/index.tsreceive and use these options as-is without transformation (lines 36-84, 98-155). These functions should either be removed if they are dead code, or their intended use case should be documented.Likely an incorrect or invalid review comment.
|
@greptile |
There was a problem hiding this comment.
Greptile Overview
Greptile Summary
Implements macOS passkeys (WebAuthn) support using the electron-webauthn library, enabling biometric authentication for web applications.
Key Changes
- Added
com.apple.developer.web-browser.public-key-credentialentitlement to enable passkeys on macOS - Implemented WebAuthn IPC handlers with proper security validation (origin checks, frame validation, public suffix checks)
- Patched
navigator.credentialsAPI in preload to intercept and route WebAuthn calls through electron-webauthn when available - Hardened web security by explicitly setting
nodeIntegration: false,nodeIntegrationInSubFrames: true, andcontextIsolation: truein tab WebContents - Added fallback to Chromium's native WebAuthn implementation when electron-webauthn is unavailable
Issues Found
src/preload/webauthn/helpers.tsis never imported or used in the codebaseelectron-viteupgraded from v3 to v5 (major version bump) - build configuration compatibility should be verified
Confidence Score: 4/5
- Safe to merge with minor cleanup recommended
- Implementation is solid with proper security validation, frame origin checks, and public suffix list checking. The unused helpers.ts file and electron-vite major version upgrade are minor issues that should be addressed but don't block merging.
- Pay attention to
src/preload/webauthn/helpers.ts(unused file) and verifyelectron-vitev5 compatibility
Important Files Changed
File Analysis
| Filename | Score | Overview |
|---|---|---|
| src/preload/webauthn/helpers.ts | 3/5 | New file containing isRpIdAllowed helper function, but it's never imported or used anywhere in the codebase |
| build/entitlements.mac.plist | 5/5 | Added passkeys entitlement com.apple.developer.web-browser.public-key-credential and hardened runtime settings |
| src/main/ipc/webauthn/index.ts | 5/5 | Implements IPC handlers for WebAuthn create/get operations with proper origin and frame validation |
| src/preload/index.ts | 4/5 | Patches navigator.credentials API to intercept WebAuthn calls and route them through electron-webauthn when available |
| src/main/controllers/tabs-controller/tab.ts | 5/5 | Hardened web security by explicitly setting nodeIntegration: false, nodeIntegrationInSubFrames: true, and contextIsolation: true |
| package.json | 4/5 | Added electron-webauthn and tldts dependencies, upgraded electron-vite to v5.0.0 |
Sequence Diagram
sequenceDiagram
participant Web as Web Page
participant Preload as Preload Script
participant Main as Main Process
participant Addon as electron-webauthn
participant macOS as macOS WebAuthn
Note over Web,macOS: WebAuthn Registration Flow
Web->>Preload: navigator.credentials.create(options)
Preload->>Preload: Check if electron-webauthn available
alt electron-webauthn available
Preload->>Main: IPC: webauthn:create
Main->>Main: Validate senderFrame & topFrame
Main->>Main: Extract origin & check security
Main->>Addon: createCredential(publicKeyOptions)
Addon->>macOS: Request passkey creation
macOS-->>Addon: Credential data
Addon-->>Main: CreateCredentialResult
Main-->>Preload: Serialized result
Preload->>Preload: Map to PublicKeyCredential
Preload-->>Web: PublicKeyCredential
else electron-webauthn not available
Preload->>Preload: Fall back to native
Preload-->>Web: Use Chromium's WebAuthn
end
Note over Web,macOS: WebAuthn Authentication Flow
Web->>Preload: navigator.credentials.get(options)
Preload->>Preload: Check if electron-webauthn available
alt electron-webauthn available
Preload->>Main: IPC: webauthn:get
Main->>Main: Validate senderFrame & topFrame
Main->>Main: Extract origin & check security
Main->>Addon: getCredential(publicKeyOptions)
Addon->>macOS: Request passkey assertion
macOS-->>Addon: Assertion data
Addon-->>Main: AssertCredentialResult
Main-->>Preload: Serialized result
Preload->>Preload: Map to PublicKeyCredential
Preload-->>Web: PublicKeyCredential
else electron-webauthn not available
Preload->>Preload: Fall back to native
Preload-->>Web: Use Chromium's WebAuthn
end
There was a problem hiding this comment.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/main/controllers/tabs-controller/tab.ts (1)
82-108: Critical:nodeIntegrationInSubFrames: trueenables Node APIs in subframes—a documented security risk.This flag grants Node/renderer APIs to any subframe, increasing XSS/compromise impact. CVE-2022-29247 demonstrated renderer code can leverage this to access
ipcRendererin new renderer processes, leading to privilege escalation if IPC handlers lack propersenderFramevalidation. Electron's security guidance explicitly recommends defaulting this tofalseand enabling only for fully trusted, local content.The existing comment is misleading—
nodeIntegrationInSubFramesdoes not "enable preload scripts in iframes"; it specifically enables Node integration in subframes. Preload scripts work independently and are unaffected by this flag. Since the preload is currently commented out (line 107), there is no evident need for Node in subframes.Set
nodeIntegrationInSubFrames: falseunless there is an explicit, documented requirement for Node APIs in trusted subframes. If that requirement exists, document it clearly and validate all IPC handlers withsenderFramechecks to prevent privilege escalation.Proposed fix
nodeIntegration: false, - nodeIntegrationInSubFrames: true, + nodeIntegrationInSubFrames: false, contextIsolation: true
🤖 Fix all issues with AI agents
In @src/main/ipc/webauthn/index.ts:
- Line 47: The handler reads options.publicKey into publicKeyOptions and may
pass it undefined to the native addon; add a guard that verifies
options.publicKey is defined before calling the addon (use the same pattern used
in the webauthn:get handler) and return or throw a clear error/log when it's
missing, and when present pass the validated publicKeyOptions to the addon;
update both the create/register flow (the variable publicKeyOptions used for the
addon call around the current addon invocation) and the webauthn:get handler
branch to perform the same existence check and early exit so the addon never
receives undefined.
In @src/preload/index.ts:
- Around line 113-123: Update the type annotations for the IPC responses to
match the actual values returned: change the type of the serialized variable in
the create method (the result of ipcRenderer.invoke("webauthn:create", options))
from CreateCredentialResult | null to CreateCredentialResult |
CreateCredentialErrorCodes | null, and do the same for the get() handler’s
serialized variable (ipcRenderer.invoke("webauthn:get", ...)); keep the existing
runtime checks unchanged so error-code strings still pass through to
tinyPasskeysScript.
- Around line 256-263: The override of
PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable redundantly
calls patchedCredentials.isAvailable() twice; change it to call
patchedCredentials.isAvailable() once, store the boolean result in a local
variable (e.g., available) and use that value for both the conditional check and
the return, preserving the fallback to
oldIsUserVerifyingPlatformAuthenticatorAvailable() when unavailable.
In @src/shared/types/fido2-types.ts:
- Around line 49-132: CreateCredentialParams marks timeout as optional but
AssertCredentialParams currently requires timeout, which is inconsistent with
the WebAuthn spec; change the AssertCredentialParams interface's timeout
property to be optional (timeout?: number) to match CreateCredentialParams (or
if intentional, add a comment on AssertCredentialParams explaining the
defaulting behavior). Update any callers of AssertCredentialParams to handle an
undefined timeout or to supply an explicit default where necessary.
📜 Review details
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (6)
electron.vite.config.tssrc/main/controllers/tabs-controller/tab.tssrc/main/ipc/webauthn/index.tssrc/main/ipc/webauthn/module.tssrc/preload/index.tssrc/shared/types/fido2-types.ts
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{js,jsx,ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc)
**/*.{js,jsx,ts,tsx}: Usebun <file>instead ofnode <file>orts-node <file>
Bun automatically loads .env, so don't use dotenv.
UseBun.serve()for HTTP servers and WebSockets instead ofexpress.
Usebun:sqlitefor SQLite instead ofbetter-sqlite3.
UseBun.redisfor Redis instead ofioredis.
UseBun.sqlfor Postgres instead ofpgorpostgres.js.
Use built-inWebSocketinstead ofws.
PreferBun.fileovernode:fs's readFile/writeFile.
UseBun.$for shell commands instead of execa.
Files:
src/main/controllers/tabs-controller/tab.tssrc/main/ipc/webauthn/module.tssrc/main/ipc/webauthn/index.tssrc/preload/index.tssrc/shared/types/fido2-types.tselectron.vite.config.ts
**/*.{html,ts,css}
📄 CodeRabbit inference engine (.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc)
Use
bun build <file.html|file.ts|file.css>instead ofwebpackoresbuild
Files:
src/main/controllers/tabs-controller/tab.tssrc/main/ipc/webauthn/module.tssrc/main/ipc/webauthn/index.tssrc/preload/index.tssrc/shared/types/fido2-types.tselectron.vite.config.ts
🧠 Learnings (1)
📚 Learning: 2025-04-18T23:20:02.623Z
Learnt from: iamEvanYT
Repo: MultiboxLabs/flow-browser PR: 24
File: electron/browser/tabs/tab.ts:309-310
Timestamp: 2025-04-18T23:20:02.623Z
Learning: In the Flow Browser codebase, `URL.parse` is preferred over `new URL()` for URL parsing because it works without errors in the Electron runtime environment, even though TypeScript's type definitions require `ts-ignore` comments.
Applied to files:
src/main/ipc/webauthn/index.ts
🧬 Code graph analysis (1)
src/main/ipc/webauthn/index.ts (2)
src/shared/types/fido2-types.ts (4)
CreateCredentialResult(137-160)CreateCredentialErrorCodes(223-229)AssertCredentialResult(200-218)AssertCredentialErrorCodes(234-239)src/main/ipc/webauthn/module.ts (1)
getWebauthnAddon(6-38)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
- GitHub Check: build (macos-latest)
- GitHub Check: build (ubuntu-24.04-arm)
- GitHub Check: build (windows-latest)
- GitHub Check: build (macos-15-intel)
- GitHub Check: build (ubuntu-latest)
- GitHub Check: Cursor Bugbot
- GitHub Check: format
- GitHub Check: main-checks
🔇 Additional comments (8)
src/main/ipc/webauthn/module.ts (1)
1-38: Well-structured lazy loading pattern with proper caching.The implementation correctly handles:
- Platform-specific loading (macOS only)
- Module caching to avoid repeated imports
- In-flight deduplication to prevent concurrent import races
- Retry-on-failure by clearing the promise cache
One minor observation: the
finallyblock clearswebauthnModulePromiseeven on success. This is fine becausewebauthnModuleis set before thefinallyruns, so subsequent calls hit the early return on line 13. The pattern is sound.src/main/ipc/webauthn/index.ts (3)
91-153: Consistent structure withwebauthn:createhandler.The
webauthn:gethandler follows the same pattern aswebauthn:create, which is good for maintainability. The conditional mediation check (lines 109-111) correctly returnsNotSupportedErroras documented.Same concern applies:
publicKeyOptionson line 106 could beundefined.
155-161: Simple availability check.The
webauthn:is-availablehandler is straightforward. Consider simplifying to:return !!await getWebauthnAddon();This is purely stylistic; the current implementation is also correct.
11-30: Remove unused exportbufferSourceToBufferor document its intended useThis utility function is exported but not referenced anywhere in the codebase. If it's part of a public API for external consumers, add a comment explaining this. Otherwise, remove the export to reduce dead code.
src/preload/index.ts (1)
101-286: Well-structured passkeys patching implementation.The overall approach is sound:
- Feature detection before patching
- Proper fallback to native implementations
- Clean error-to-DOMException mapping that mirrors Chromium behavior
- Cleanup of exposed bridge after patching
The use of
contextBridge.executeInMainWorldto inject the patching script is the correct approach for this type of runtime modification.src/shared/types/fido2-types.ts (2)
1-262: Comprehensive and well-documented FIDO2/WebAuthn types.The type definitions are thorough:
- Clear JSDoc documentation with spec references
- Base64 encoding requirements are consistently noted
- Extension types (PRF, largeBlob, credProps) are properly modeled
- Error code unions align with WebAuthn error handling
16-47: Abstract class appears unused in this PR.
Fido2ClientServiceis an abstract class that defines the WebAuthn client interface, but the IPC handlers in this PR don't extend it—they directly use theelectron-webauthnaddon. This class may be intended for future extensibility or external implementations.electron.vite.config.ts (1)
34-50: Migration tobuild.externalizeDepsis correct and aligns with electron-vite v5 best practices.The configuration properly excludes
electron-context-menuandhonofor the main process, andelectron-chrome-extensionsfor the preload, forcing these packages to be bundled rather than externalized. This approach is the recommended pattern in electron-vite v5, where the deprecatedexternalizeDepsPluginhas been replaced with the build configuration option.
| return null; | ||
| } | ||
|
|
||
| const publicKeyOptions = options.publicKey; |
There was a problem hiding this comment.
Potential undefined publicKeyOptions passed to addon.
options.publicKey may be undefined if the caller provides CredentialCreationOptions without a publicKey property. The addon call on line 76 would then receive undefined, which may cause unexpected behavior depending on the addon's implementation.
Proposed guard for publicKeyOptions
const publicKeyOptions = options.publicKey;
+ if (!publicKeyOptions) {
+ return null;
+ }
const senderFrame = event.senderFrame;Apply the same pattern in the webauthn:get handler around line 106.
Also applies to: 76-81
🤖 Prompt for AI Agents
In @src/main/ipc/webauthn/index.ts at line 47, The handler reads
options.publicKey into publicKeyOptions and may pass it undefined to the native
addon; add a guard that verifies options.publicKey is defined before calling the
addon (use the same pattern used in the webauthn:get handler) and return or
throw a clear error/log when it's missing, and when present pass the validated
publicKeyOptions to the addon; update both the create/register flow (the
variable publicKeyOptions used for the addon call around the current addon
invocation) and the webauthn:get handler branch to perform the same existence
check and early exit so the addon never receives undefined.
| create: async (options) => { | ||
| const serialized: CreateCredentialResult | null = await ipcRenderer.invoke("webauthn:create", options); | ||
|
|
||
| if (!serialized) return null; | ||
| if (typeof serialized === "string") { | ||
| return serialized; | ||
| } | ||
|
|
||
| const publicKeyCredential = WebauthnUtils.mapCredentialRegistrationResult(serialized); | ||
| return publicKeyCredential; | ||
| }, |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Type annotation mismatch with actual IPC response.
The type annotation on line 114 shows CreateCredentialResult | null, but the IPC handler returns CreateCredentialResult | CreateCredentialErrorCodes | null. The error code strings pass through and are handled in tinyPasskeysScript, so the logic works, but the types are misleading.
Suggested type correction
create: async (options) => {
- const serialized: CreateCredentialResult | null = await ipcRenderer.invoke("webauthn:create", options);
+ const serialized: CreateCredentialResult | CreateCredentialErrorCodes | null = await ipcRenderer.invoke("webauthn:create", options);The same applies to get() on line 126.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| create: async (options) => { | |
| const serialized: CreateCredentialResult | null = await ipcRenderer.invoke("webauthn:create", options); | |
| if (!serialized) return null; | |
| if (typeof serialized === "string") { | |
| return serialized; | |
| } | |
| const publicKeyCredential = WebauthnUtils.mapCredentialRegistrationResult(serialized); | |
| return publicKeyCredential; | |
| }, | |
| create: async (options) => { | |
| const serialized: CreateCredentialResult | CreateCredentialErrorCodes | null = await ipcRenderer.invoke("webauthn:create", options); | |
| if (!serialized) return null; | |
| if (typeof serialized === "string") { | |
| return serialized; | |
| } | |
| const publicKeyCredential = WebauthnUtils.mapCredentialRegistrationResult(serialized); | |
| return publicKeyCredential; | |
| }, |
🤖 Prompt for AI Agents
In @src/preload/index.ts around lines 113 - 123, Update the type annotations for
the IPC responses to match the actual values returned: change the type of the
serialized variable in the create method (the result of
ipcRenderer.invoke("webauthn:create", options)) from CreateCredentialResult |
null to CreateCredentialResult | CreateCredentialErrorCodes | null, and do the
same for the get() handler’s serialized variable
(ipcRenderer.invoke("webauthn:get", ...)); keep the existing runtime checks
unchanged so error-code strings still pass through to tinyPasskeysScript.
| credentials.create = async (options) => { | ||
| if (options && (await shouldUseMacOSWebauthnAddon())) { | ||
| if (options.publicKey) { | ||
| const result = await patchedCredentials.create(options); | ||
|
|
||
| // Cannot throw errors in patchedCredentials, so we need to handle the errors here. | ||
| const errorCode = result as unknown as CreateCredentialErrorCodes; | ||
| if (errorCode === "NotAllowedError") { | ||
| // Mirror Chromium's error message. | ||
| throw new DOMException( | ||
| "The operation either timed out or was not allowed. See: https://www.w3.org/TR/webauthn-2/#sctn-privacy-considerations-client.", | ||
| "NotAllowedError" | ||
| ); | ||
| } else if (errorCode === "SecurityError") { | ||
| throw new DOMException("The calling domain is not a valid domain.", "SecurityError"); | ||
| } else if (errorCode === "TypeError") { | ||
| throw new DOMException("Failed to parse arguments.", "TypeError"); | ||
| } else if (errorCode === "AbortError") { | ||
| throw new DOMException("The operation was aborted.", "AbortError"); | ||
| } else if (errorCode === "NotSupportedError") { | ||
| throw new DOMException("The user agent does not support this operation.", "NotSupportedError"); | ||
| } else if (errorCode === "InvalidStateError") { | ||
| throw new DOMException( | ||
| "The user attempted to register an authenticator that contains one of the credentials already registered with the relying party.", | ||
| "InvalidStateError" | ||
| ); | ||
| } | ||
|
|
||
| return result; | ||
| } | ||
| } | ||
|
|
||
| return await oldCredentialsCreate(options); | ||
| }; |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Error handling works but has a subtle flow issue.
The error-to-DOMException mapping (lines 183-203) correctly translates error codes. However, after throwing on an error code, the code still has return result; on line 205. This is unreachable after a throw, but if an unknown error code is returned (not in the union), it would return the string directly to the caller instead of a PublicKeyCredential.
Since the error codes are defined in a union type, this shouldn't happen in practice, but adding a fallback throw for unexpected strings would be more defensive.
Defensive handling for unexpected error codes
} else if (errorCode === "InvalidStateError") {
throw new DOMException(
"The user attempted to register an authenticator that contains one of the credentials already registered with the relying party.",
"InvalidStateError"
);
+ } else if (typeof result === "string") {
+ // Unexpected error code - throw generic error
+ throw new DOMException("An unknown error occurred.", "UnknownError");
}
return result;Apply similar logic in the get() handler around line 241.
| const oldIsUserVerifyingPlatformAuthenticatorAvailable = | ||
| PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable.bind(PublicKeyCredential); | ||
| PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable = async () => { | ||
| if (await patchedCredentials.isAvailable()) { | ||
| return await patchedCredentials.isAvailable(); | ||
| } | ||
| return await oldIsUserVerifyingPlatformAuthenticatorAvailable(); | ||
| }; |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Redundant isAvailable() call in isUserVerifyingPlatformAuthenticatorAvailable.
Lines 259-260 call patchedCredentials.isAvailable() twice in succession. The first call checks if the addon is available, and then it returns the result of calling it again. This works but is inefficient.
Simplify to single call
PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable = async () => {
- if (await patchedCredentials.isAvailable()) {
- return await patchedCredentials.isAvailable();
- }
+ const addonAvailable = await patchedCredentials.isAvailable();
+ if (addonAvailable) {
+ return true; // macOS platform authenticator is available via addon
+ }
return await oldIsUserVerifyingPlatformAuthenticatorAvailable();
};🤖 Prompt for AI Agents
In @src/preload/index.ts around lines 256 - 263, The override of
PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable redundantly
calls patchedCredentials.isAvailable() twice; change it to call
patchedCredentials.isAvailable() once, store the boolean result in a local
variable (e.g., available) and use that value for both the conditional check and
the return, preserving the fallback to
oldIsUserVerifyingPlatformAuthenticatorAvailable() when unavailable.
| /** | ||
| * Parameters for creating a new credential. | ||
| */ | ||
| export interface CreateCredentialParams { | ||
| /** The Relaying Parties origin, see: https://html.spec.whatwg.org/multipage/browsers.html#concept-origin */ | ||
| origin: string; | ||
| /** | ||
| * A value which is true if and only if the caller’s environment settings object is same-origin with its ancestors. | ||
| * It is false if caller is cross-origin. | ||
| * */ | ||
| sameOriginWithAncestors: boolean; | ||
| /** The Relying Party's preference for attestation conveyance */ | ||
| attestation?: "direct" | "enterprise" | "indirect" | "none"; | ||
| /** The Relying Party's requirements of the authenticator used in the creation of the credential. */ | ||
| authenticatorSelection?: { | ||
| // authenticatorAttachment?: AuthenticatorAttachment; // not used | ||
| requireResidentKey?: boolean; | ||
| residentKey?: "discouraged" | "preferred" | "required"; | ||
| userVerification?: UserVerification; | ||
| }; | ||
| /** Challenge intended to be used for generating the newly created credential's attestation object. */ | ||
| challenge: string; // b64 encoded | ||
| /** | ||
| * This member is intended for use by Relying Parties that wish to limit the creation of multiple credentials for | ||
| * the same account on a single authenticator. The client is requested to return an error if the new credential would | ||
| * be created on an authenticator that also contains one of the credentials enumerated in this parameter. | ||
| * */ | ||
| excludeCredentials?: { | ||
| id: string; // b64 encoded | ||
| transports?: ("ble" | "hybrid" | "internal" | "nfc" | "usb")[]; | ||
| type: "public-key"; | ||
| }[]; | ||
| /** | ||
| * This member contains additional parameters requesting additional processing by the client and authenticator. | ||
| **/ | ||
| extensions?: { | ||
| appid?: string; // Not supported | ||
| appidExclude?: string; // Not supported | ||
| uvm?: boolean; // Not supported | ||
| credProps?: boolean; | ||
| prf?: { | ||
| eval?: { | ||
| first: string; // b64 encoded | ||
| second?: string; // b64 encoded | ||
| }; | ||
| evalByCredential?: Record< | ||
| string, | ||
| { | ||
| first: string; // b64 encoded | ||
| second?: string; // b64 encoded | ||
| } | ||
| >; | ||
| }; | ||
| largeBlob?: { | ||
| support?: "required" | "preferred"; | ||
| read?: boolean; | ||
| write?: string; // b64 encoded | ||
| }; | ||
| }; | ||
| /** | ||
| * This member contains information about the desired properties of the credential to be created. | ||
| * The sequence is ordered from most preferred to least preferred. | ||
| * The client makes a best-effort to create the most preferred credential that it can. | ||
| */ | ||
| pubKeyCredParams: PublicKeyCredentialParam[]; | ||
| /** Data about the Relying Party responsible for the request. */ | ||
| rp: { | ||
| id?: string; | ||
| name: string; | ||
| }; | ||
| /** Data about the user account for which the Relying Party is requesting attestation. */ | ||
| user: { | ||
| id: string; // b64 encoded | ||
| displayName: string; | ||
| name: string; | ||
| }; | ||
| /** Forwarded to user interface */ | ||
| fallbackSupported: boolean; | ||
| /** | ||
| * This member specifies a time, in milliseconds, that the caller is willing to wait for the call to complete. | ||
| * This is treated as a hint, and MAY be overridden by the client. | ||
| **/ | ||
| timeout?: number; | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Inconsistent timeout optionality between params interfaces.
In CreateCredentialParams, timeout is optional (line 131: timeout?: number), but in AssertCredentialParams, timeout is required (line 171: timeout: number).
Per the WebAuthn spec, timeout is optional for both operations. If this inconsistency is intentional (e.g., the implementation always provides a default for assertions), document the reason; otherwise, consider aligning them.
Make timeout optional in AssertCredentialParams
rpId: string;
origin: string;
challenge: string;
userVerification?: UserVerification;
- timeout: number;
+ timeout?: number;
sameOriginWithAncestors: boolean;Also applies to: 162-195
🤖 Prompt for AI Agents
In @src/shared/types/fido2-types.ts around lines 49 - 132,
CreateCredentialParams marks timeout as optional but AssertCredentialParams
currently requires timeout, which is inconsistent with the WebAuthn spec; change
the AssertCredentialParams interface's timeout property to be optional
(timeout?: number) to match CreateCredentialParams (or if intentional, add a
comment on AssertCredentialParams explaining the defaulting behavior). Update
any callers of AssertCredentialParams to handle an undefined timeout or to
supply an explicit default where necessary.
Note
Enables macOS passkeys and tightens app security via entitlement updates.
com.apple.developer.web-browser.public-key-credentialtobuild/entitlements.mac.plistto support WebAuthn/passkeyscom.apple.security.hardened-runtime.gitignoreto ignore.env,.yalc,yalc.lock, andtest.shWritten by Cursor Bugbot for commit 3a5058d. This will update automatically on new commits. Configure here.
Summary by CodeRabbit
Release Notes
New Features
Security
Chores
✏️ Tip: You can customize this high-level summary in your review settings.