-
Notifications
You must be signed in to change notification settings - Fork 1.5k
PM-8353 MacOS passkey provider #13963
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #13963 +/- ##
==========================================
+ Coverage 41.23% 41.27% +0.03%
==========================================
Files 3543 3546 +3
Lines 101963 102291 +328
Branches 15295 15338 +43
==========================================
+ Hits 42048 42224 +176
- Misses 58152 58300 +148
- Partials 1763 1767 +4 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
|
New Issues (7)Checkmarx found the following issues in this Pull Request
|
* React to IPC disconnects * Minor cleanup * Update apps/desktop/package.json Co-authored-by: Daniel García <[email protected]> * Relaxed ordering --------- Co-authored-by: Daniel García <[email protected]>
* Passkey stuff Co-authored-by: Anders Åberg <[email protected]> * Ugly hacks * Work On Modal State Management * Applying modalStyles * modal * Improved hide/show * fixed promise * File name * fix prettier * Protecting against null API's and undefined data * Only show fake popup to devs * cleanup mock code * rename minmimal-app to modal-app * Added comment * Added comment * removed old comment * Avoided changing minimum size * Add small comment * Rename component * adress feedback * Fixed uppercase file * Fixed build * Added codeowners * added void * commentary * feat: reset setting on app start * Moved reset to be in main / process launch * Add comment to create window * Added a little bit of styling * Use Messaging service to loadUrl * Enable passkeysautofill * Add logging * halfbaked * Integration working * And now it works without extra delay * Clean up * add note about messaging * lb * removed console.logs * Cleanup and adress review feedback * This hides the swift UI * add modal components * update modal with correct ciphers and functionality * add create screen * pick credential, draft * Remove logger * a whole lot of wiring * not working * Improved wiring * Cancel after 90s * Introduced observable * update cipher handling * update to use matchesUri * Launching bitwarden if its not running * Passing position from native to electron * Rename inModalMode to modalMode * remove tap * revert spaces * added back isDev * cleaned up a bit * Cleanup swift file * tweaked logging * clean up * Update apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift Co-authored-by: Andreas Coroiu <[email protected]> * Update apps/desktop/src/platform/main/autofill/native-autofill.main.ts Co-authored-by: Andreas Coroiu <[email protected]> * Update apps/desktop/src/platform/services/desktop-settings.service.ts Co-authored-by: Andreas Coroiu <[email protected]> * adress position feedback * Update apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift Co-authored-by: Andreas Coroiu <[email protected]> * Removed extra logging * Adjusted error logging * Use .error to log errors * remove dead code * Update desktop-autofill.service.ts * use parseCredentialId instead of guidToRawFormat * Update apps/desktop/src/autofill/services/desktop-autofill.service.ts Co-authored-by: Andreas Coroiu <[email protected]> * Change windowXy to a Record instead of [number,number] * Update apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts Co-authored-by: Andreas Coroiu <[email protected]> * Remove unsued dep and comment * changed timeout to be spec recommended maxium, 10 minutes, for now. * Correctly assume UP * Removed extra cancelRequest in deinint * Add timeout and UV to confirmChoseCipher UV is performed by UI, not the service * Improved docs regarding undefined cipherId * cleanup: UP is no longer undefined * Run completeError if ipc messages conversion failed * don't throw, instead return undefined * Disabled passkey provider * Throw error if no activeUserId was found * removed comment * Fixed lint * removed unsued service * reset entitlement formatting * Update entitlements.mas.plist * Fix build issues * Fix import issues * Update route names to use `fido2` * Fix being unable to select a passkey * Fix linting issues * Followup to fix merge issues and other comments * Update `userHandle` value * Add error handling for missing session or other errors * Remove unused route * Fix linting issues * Simplify updateCredential method * Followup to remove comments and timeouts and handle errors * Address lint issue by using `takeUntilDestroyed` * PR Followup for typescript and vault concerns * Add try block for cipher creation * Make userId manditory for cipher service --------- Co-authored-by: Justin Baur <[email protected]> Co-authored-by: Anders Åberg <[email protected]> Co-authored-by: Anders Åberg <[email protected]> Co-authored-by: Colton Hurst <[email protected]> Co-authored-by: Andreas Coroiu <[email protected]> Co-authored-by: Evan Bassler <[email protected]> Co-authored-by: Andreas Coroiu <[email protected]>
* Implemented a SendNativeStatus command This allows reporting status or asking the electron app to do something. * fmt * Update apps/desktop/src/autofill/services/desktop-autofill.service.ts Co-authored-by: Daniel García <[email protected]> * clean up * Don't add empty callbacks * Removed comment --------- Co-authored-by: Daniel García <[email protected]>
Handle unlocktimeout
* works * Add mapping * remove the build script * cleanup
* Add support for padding in base64url decode * whitespace * whitespace
* Passkey stuff Co-authored-by: Anders Åberg <[email protected]> * Ugly hacks * Work On Modal State Management * Applying modalStyles * modal * Improved hide/show * fixed promise * File name * fix prettier * Protecting against null API's and undefined data * Only show fake popup to devs * cleanup mock code * rename minmimal-app to modal-app * Added comment * Added comment * removed old comment * Avoided changing minimum size * Add small comment * Rename component * adress feedback * Fixed uppercase file * Fixed build * Added codeowners * added void * commentary * feat: reset setting on app start * Moved reset to be in main / process launch * Add comment to create window * Added a little bit of styling * Use Messaging service to loadUrl * Enable passkeysautofill * Add logging * halfbaked * Integration working * And now it works without extra delay * Clean up * add note about messaging * lb * removed console.logs * Cleanup and adress review feedback * This hides the swift UI * add modal components * update modal with correct ciphers and functionality * add create screen * pick credential, draft * Remove logger * a whole lot of wiring * not working * Improved wiring * Cancel after 90s * Introduced observable * update cipher handling * update to use matchesUri * Launching bitwarden if its not running * Passing position from native to electron * Rename inModalMode to modalMode * remove tap * revert spaces * added back isDev * cleaned up a bit * Cleanup swift file * tweaked logging * clean up * Update apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift Co-authored-by: Andreas Coroiu <[email protected]> * Update apps/desktop/src/platform/main/autofill/native-autofill.main.ts Co-authored-by: Andreas Coroiu <[email protected]> * Update apps/desktop/src/platform/services/desktop-settings.service.ts Co-authored-by: Andreas Coroiu <[email protected]> * adress position feedback * Update apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift Co-authored-by: Andreas Coroiu <[email protected]> * Removed extra logging * Adjusted error logging * Use .error to log errors * remove dead code * Update desktop-autofill.service.ts * use parseCredentialId instead of guidToRawFormat * Update apps/desktop/src/autofill/services/desktop-autofill.service.ts Co-authored-by: Andreas Coroiu <[email protected]> * Change windowXy to a Record instead of [number,number] * Update apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts Co-authored-by: Andreas Coroiu <[email protected]> * Remove unsued dep and comment * changed timeout to be spec recommended maxium, 10 minutes, for now. * Correctly assume UP * Removed extra cancelRequest in deinint * Add timeout and UV to confirmChoseCipher UV is performed by UI, not the service * Improved docs regarding undefined cipherId * cleanup: UP is no longer undefined * Run completeError if ipc messages conversion failed * don't throw, instead return undefined * Disabled passkey provider * Throw error if no activeUserId was found * removed comment * Fixed lint * removed unsued service * reset entitlement formatting * Update entitlements.mas.plist * Fix build issues * Fix import issues * Update route names to use `fido2` * Fix being unable to select a passkey * Fix linting issues * Added support for handling a locked vault * Followup to fix merge issues and other comments * Update `userHandle` value * Add error handling for missing session or other errors * Remove unused route * Fix linting issues * Simplify updateCredential method * Add master password reprompt on passkey create * Followup to remove comments and timeouts and handle errors * Address lint issue by using `takeUntilDestroyed` * Add MP prompt to cipher selection * Change how timeout is handled * Include `of` from rxjs * Hide blue header for passkey popouts (#14095) * Hide blue header for passkey popouts * Fix issue with test * Fix ngOnDestroy complaint * Import OnDestroy correctly * Only require master password if item requires it --------- Co-authored-by: Justin Baur <[email protected]> Co-authored-by: Anders Åberg <[email protected]> Co-authored-by: Anders Åberg <[email protected]> Co-authored-by: Colton Hurst <[email protected]> Co-authored-by: Andreas Coroiu <[email protected]> Co-authored-by: Evan Bassler <[email protected]> Co-authored-by: Andreas Coroiu <[email protected]>
This changes the behaviour to react to logoff, but not to account locks. It also adds better error handling on the native side.
* Enables autofill extension building * Try use macos-14 * add --break-system-packages for macos14 * revert using build-native * try add rustup target add x86_64-apple-darwin * add more rustup target add x86_64-apple-darwin * try to force sdk version * Show SDK versions * USE KVC for excludedCredentials * added xcodebuild deugging * Revert "try to force sdk version" This reverts commit d94f255. * Use macos-15 * undo merge * remove macos-15 from cli * remove macos-15 from browser --------- Co-authored-by: Anders Åberg <[email protected]>
* Delay IPC server start * Better ipc handling * Rename ready() to listenerReady() --------- Co-authored-by: Daniel García <[email protected]>
apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts
Show resolved
Hide resolved
* Show items for url that don't have passkey * Show existing login items in the UI * Filter available cipher results (#14399) * Filter available cipher results * Fix linting issues * Update logic for eligible ciphers * Remove unused method to check matching username * PM-20608 update styling for excludedCredentials (#14444) * PM-20608 update styling for excludedCredentials * Have flow correctly move to creation for excluded cipher * Remove duplicate confirmNeCredential call * Revert fido2-authenticator changes and move the excluded check * Create a separate component for excluded cipher view * Display traffic light MacOS buttons when the vault is locked (#14673) * Remove unneccessary filter for excludedCiphers * Remove dead code from the excluded ciphers work * Remove excludedCipher checks from fido2 create and vault * Remove excludedCipher remnants from vault and simplify create cipher logic * Move cipherHasNoOtherPasskeys to shared fido2-utils * Remove all containsExcludedCipher references * Use `bufferToString` to convert `userHandle` --------- Co-authored-by: Jeffrey Holland <[email protected]> Co-authored-by: Jeffrey Holland <[email protected]>
| </bit-section> | ||
|
|
||
| <bit-section class="tw-bg-background-alt tw-p-4 tw-flex tw-flex-col"> | ||
| <div *ngIf="(ciphers$ | async)?.length === 0; else hasCiphers"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
❌ Angular Control Flow: This PR should use the modern @if / @else syntax instead of *ngIf with ng-template.
| <div *ngIf="(ciphers$ | async)?.length === 0; else hasCiphers"> | |
| <div @if ((ciphers$ | async)?.length === 0) { |
This applies to the entire template structure. The modern syntax improves readability and is the recommended pattern for new code. Since this is a new component being added, it should follow current best practices from the start.
| </div> | ||
| </div> | ||
| <ng-template #hasCiphers> | ||
| <bit-item *ngFor="let c of ciphers$ | async" class=""> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
❌ Angular Control Flow: Use @for instead of *ngFor for iteration.
| <bit-item *ngFor="let c of ciphers$ | async" class=""> | |
| <bit-item @for (c of ciphers$ | async; track c.id) { |
The @for syntax is the modern Angular pattern and should be used in new components. Note the required track expression for performance optimization.
|
|
||
| #[uniffi::export] | ||
| impl MacOSProviderClient { | ||
| // FIXME: Remove unwraps! They panic and terminate the whole application. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FIXME comment correctly identifies that unwraps should be removed. These can cause panics and crash the extension.
The comment acknowledges this is a known issue. Consider creating a follow-up ticket to address this before the feature exits beta/MVP status, as production crashes from panics would significantly impact user experience.
| return MacOsProviderClient.connect() | ||
| }() | ||
| self.client = newClient | ||
| return newClient! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
newClient! will crash if the connection loop completes without establishing a connection.
guard let client = newClient else {
logger.error("[autofill-extension] Failed to create client after retries")
throw NSError(domain: "com.bitwarden.desktop", code: 1, userInfo: nil)
}
self.client = client
return clientThis prevents crashes and provides better error messaging to users.
| changeDetection: ChangeDetectionStrategy.OnPush, | ||
| }) | ||
| export class Fido2CreateComponent implements OnInit, OnDestroy { | ||
| session?: DesktopFido2UserInterfaceSession = null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
session is typed as DesktopFido2UserInterfaceSession | undefined but initialized to null.
| session?: DesktopFido2UserInterfaceSession = null; | |
| session?: DesktopFido2UserInterfaceSession; |
Use either undefined (via ?) or null, but not both. The ? syntax is preferred and matches how it's checked elsewhere in the code.
| changeDetection: ChangeDetectionStrategy.OnPush, | ||
| }) | ||
| export class Fido2VaultComponent implements OnInit, OnDestroy { | ||
| session?: DesktopFido2UserInterfaceSession = null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
? for optional instead of = null.
| session?: DesktopFido2UserInterfaceSession = null; | |
| session?: DesktopFido2UserInterfaceSession; |
|
|
||
| .tw-app-region-no-drag { | ||
| -webkit-app-region: no-drag; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💭 Documentation needed: These custom utility classes lack documentation about their purpose and when to use them.
Consider adding a comment block:
/**
* macOS window dragging utilities
* .tw-app-region-drag - Makes the element draggable for window movement (macOS titlebar)
* .tw-app-region-no-drag - Prevents dragging on interactive elements within draggable areas
*/This helps future developers understand the purpose of these non-standard Tailwind classes.
| "fileSavedToDevice": { | ||
| "message": "File saved to device. Manage from your device downloads." | ||
| }, | ||
| "importantNotice": { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💭 Localization process: Since this adds new user-facing strings, confirm these have been or will be submitted for translation via Crowdin.
Per the earlier comment thread, design approval was obtained. Just verifying the i18n process is also complete or planned.
|
|
||
| function normalizePosition(position: { x: number; y: number }): { x: number; y: number } { | ||
| // Add 100 pixels to the x-coordinate to offset the native OS dialog positioning. | ||
| const xPostionOffset = 100; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
xPostionOffset should be xPositionOffset
| changeDetection: ChangeDetectionStrategy.OnPush, | ||
| }) | ||
| export class Fido2CreateComponent implements OnInit, OnDestroy { | ||
| session?: DesktopFido2UserInterfaceSession = null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💭 TypeScript inconsistency: The type annotation DesktopFido2UserInterfaceSession | undefined suggests it could be undefined, but it's initialized to null. Consider using null in the type annotation instead for consistency: DesktopFido2UserInterfaceSession | null
| changeDetection: ChangeDetectionStrategy.OnPush, | ||
| }) | ||
| export class Fido2VaultComponent implements OnInit, OnDestroy { | ||
| session?: DesktopFido2UserInterfaceSession = null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💭 Same as in fido2-create.component.ts: initialized to null but typed as undefined. Consider using consistent null/undefined semantics.
|
|
||
| #[uniffi::export] | ||
| impl MacOSProviderClient { | ||
| // FIXME: Remove unwraps! They panic and terminate the whole application. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.expect() with descriptive messages for better debugging. The current comment acknowledges this but the issue persists.
| }), | ||
| ), | ||
| ); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
❌ Observable anti-pattern: Using map with an async function doesn't wait for the promise to complete. The observable will emit the unresolved Promise, not the result.
Fix: Change map to switchMap:
async updateCredential(cipher: CipherView): Promise<void> {
this.logService.info("updateCredential");
await firstValueFrom(
this.accountService.activeAccount$.pipe(
switchMap(async (a) => {
if (a) {
const encCipher = await this.cipherService.encrypt(cipher, a.id);
await this.cipherService.updateWithServer(encCipher);
}
}),
),
);
}This ensures the async operations complete before the observable completes.
| return MacOsProviderClient.connect() | ||
| }() | ||
| self.client = newClient | ||
| return newClient! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
newClient! could crash if all connection attempts fail. Consider handling this case gracefully:
guard let client = newClient else {
logger.error("[autofill-extension] Failed to establish connection after all retries")
// Handle failure appropriately - perhaps throw an error or return a default/stub client
fatalError("Unable to connect to Bitwarden Desktop")
}
self.client = client
return clientOr make the return type optional and handle nil at call sites.
| } | ||
|
|
||
| const userHandle = Fido2Utils.bufferToString( | ||
| new Uint8Array(lastRegistrationRequest.userHandle), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lastRegistrationRequest.userHandle before conversion to prevent potential crashes:
const userHandle = lastRegistrationRequest.userHandle
? Fido2Utils.bufferToString(new Uint8Array(lastRegistrationRequest.userHandle))
: null;
if (!userHandle) {
this.logService.error("Invalid userHandle in registration request");
return;
}|
|
||
| function normalizePosition(position: { x: number; y: number }): { x: number; y: number } { | ||
| // Add 100 pixels to the x-coordinate to offset the native OS dialog positioning. | ||
| const xPostionOffset = 100; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎨 Minor improvement: Typo in variable name and could be extracted to class-level constant:
// At class level:
private readonly POSITION_X_OFFSET = 100;
// In function:
return {
x: Math.round(position.x + this.POSITION_X_OFFSET),
y: Math.round(position.y),
};Note: "xPostionOffset" → "POSITION_X_OFFSET"
| this.registrationRequest = request; | ||
|
|
||
| this.logService.debug("listenPasskeyRegistration", clientId, sequenceNumber, request); | ||
| this.logService.debug("listenPasskeyRegistration2", this.convertRegistrationRequest(request)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
❌ Security concern: This debug log outputs the full converted registration request which may contain sensitive user data (userHandle, userName, etc.).
While this doesn't log decrypted vault data directly, it does log user identification information that should not appear in production logs.
Recommendation: Either:
- Remove the second debug log entirely (the first on line 212 already logs the basic info)
- Gate this behind a development-only check
- Create a sanitized version that logs only non-sensitive fields (rpId, algorithms count, etc.)
// Option 1: Remove entirely
this.logService.debug("listenPasskeyRegistration", clientId, sequenceNumber, request);
// Option 2: Development only
if (isDevelopment()) {
this.logService.debug("listenPasskeyRegistration2", this.convertRegistrationRequest(request));
}
// Option 3: Sanitized logging
this.logService.debug("listenPasskeyRegistration2", {
rpId: request.rpId,
algorithmCount: request.supportedAlgorithms.length,
hasExcludedCredentials: request.excludedCredentials.length > 0
});| return; | ||
| } | ||
|
|
||
| this.logService.debug("listenPasskeyAssertion", clientId, sequenceNumber, request); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
❌ Similar concern: This logs the full assertion request which includes credential identifiers and user context.
Recommendation: Consider reducing to essential info only:
this.logService.debug("listenPasskeyAssertion", {
clientId,
sequenceNumber,
rpId: request.rpId,
credentialCount: request.allowedCredentials?.length ?? 0
});| // Create a new client instance for each retry | ||
| newClient = MacOsProviderClient.connect() | ||
| try? await Task.sleep(nanoseconds: UInt64(100 * attempt + (delayMs * 1_000_000))) // Convert ms to nanoseconds | ||
| let connectionStatus = newClient!.getConnectionStatus() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
newClient could be nil, causing a crash here.
Recommendation: Use safer optional handling:
guard let client = newClient else {
logger.error("[autofill-extension] Failed to establish client after \(maxRetries) attempts")
// Could throw an error or return a failed client state
fatalError("Unable to establish IPC connection to Bitwarden Desktop")
}
let connectionStatus = client.getConnectionStatus()This makes the failure mode explicit rather than risking an unexpected crash.
| return MacOsProviderClient.connect() | ||
| }() | ||
| self.client = newClient | ||
| return newClient! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
guard let client = self.client, let newClient = newClient else {
logger.error("[autofill-extension] Client initialization failed")
fatalError("Unable to establish IPC connection to Bitwarden Desktop")
}
return client ?? newClient| combineLatest([this.cipherService.cipherListViews$(activeUserId), this.cipherIds$ || of(null)]) | ||
| .pipe( | ||
| map(([ciphers, cipherIds]) => { | ||
| // Filter out deleted ciphers | ||
| const activeCiphers = ciphers.filter((cipher) => !cipher.deletedDate); | ||
|
|
||
| // If specific IDs provided, filter by them | ||
| if (cipherIds?.length > 0) { | ||
| return activeCiphers.filter((cipher) => cipherIds.includes(cipher.id as string)); | ||
| } | ||
|
|
||
| return activeCiphers; | ||
| }), | ||
| takeUntil(this.destroy$), | ||
| ) | ||
| .subscribe({ | ||
| next: (ciphers) => this.ciphersSubject.next(ciphers as CipherView[]), | ||
| error: (error: unknown) => this.logService.error("Failed to load ciphers", error), | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💭 Question about subscription pattern: This implementation uses explicit subscribe with takeUntil(this.destroy$) which is valid, but I'm curious about the pattern choice.
Given ADR-0003's preference for the async pipe pattern, was there a specific reason not to use:
// In template:
<div *ngFor="let cipher of ciphers$ | async">This would:
- Handle subscription cleanup automatically
- Be more consistent with the Observable Data Services pattern
- Eliminate the need for manual BehaviorSubject management
Is the manual subscription needed for error handling or some other requirement I'm missing? Just want to understand the design decision.
|
|




🎟️ Tracking
https://bitwarden.atlassian.net/browse/PM-8353
📔 Objective
This PR serves as the long running feature branch for passkey provider support.
MacOS Passkey MVP Proposal
Explainer for other teams
Testing Setup
Passkey Workflow Videos
📸 Screenshots
⏰ Reminders before review
🦮 Reviewer guidelines
:+1:) or similar for great changes:memo:) or ℹ️ (:information_source:) for notes or general info:question:) for questions:thinking:) or 💭 (:thought_balloon:) for more open inquiry that's not quite a confirmed issue and could potentially benefit from discussion:art:) for suggestions / improvements:x:) or:warning:) for more significant problems or concerns needing attention:seedling:) or ♻️ (:recycle:) for future improvements or indications of technical debt:pick:) for minor or nitpick changes