Skip to content

Commit ddac90c

Browse files
committed
feat(browser): add native WebAuthn/passkey bridge for browser panes
Ports the WKWebView-side WebAuthn/FIDO2 bridge from upstream cmux commit e14710e (the working, non-reverted implementation — an earlier same-day attempt was reverted upstream for breaking nightly). BrowserWebAuthnCoordinator overrides navigator.credentials.create/get in the page world and runs the ceremony natively via AuthenticationServices, falling back to WebKit's own handling when native isn't available. Wired into the coordinator lifecycle for both BrowserPanel (profile switches, content-process-termination replacement, workspace-context resets, close/deinit) and BrowserPopupWindowController (window.open() popups). Renames cmux-era JS bridge tokens to programa per the rebrand lockstep rule (handler name, injected install-guard global). Adds NSBluetoothAlwaysUsageDescription (hybrid/phone-as-security-key transport uses Bluetooth) and the com.apple.developer.web-browser.public-key-credential entitlement — see the PR body for a signing/notarization caveat on the latter.
1 parent 9fd818e commit ddac90c

8 files changed

Lines changed: 2024 additions & 0 deletions

File tree

GhosttyTabs.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
A5001403 /* TerminalPanelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001413 /* TerminalPanelView.swift */; };
8080
A5001404 /* BrowserPanelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001414 /* BrowserPanelView.swift */; };
8181
A5007420 /* BrowserPopupWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5007421 /* BrowserPopupWindowController.swift */; };
82+
NRBW0001 /* BrowserWebAuthnSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = NRBW0002 /* BrowserWebAuthnSupport.swift */; };
8283
A5001420 /* MarkdownPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001418 /* MarkdownPanel.swift */; };
8384
A5001421 /* MarkdownPanelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001419 /* MarkdownPanelView.swift */; };
8485
MDRD00000000000000000001 /* MarkdownDocumentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = MDRD00000000000000000002 /* MarkdownDocumentView.swift */; };
@@ -361,6 +362,7 @@
361362
A5001413 /* TerminalPanelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/TerminalPanelView.swift; sourceTree = "<group>"; };
362363
A5001414 /* BrowserPanelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/BrowserPanelView.swift; sourceTree = "<group>"; };
363364
A5007421 /* BrowserPopupWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/BrowserPopupWindowController.swift; sourceTree = "<group>"; };
365+
NRBW0002 /* BrowserWebAuthnSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/BrowserWebAuthnSupport.swift; sourceTree = "<group>"; };
364366
A5001415 /* PanelContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/PanelContentView.swift; sourceTree = "<group>"; };
365367
A5001418 /* MarkdownPanel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/MarkdownPanel.swift; sourceTree = "<group>"; };
366368
A5001419 /* MarkdownPanelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/MarkdownPanelView.swift; sourceTree = "<group>"; };
@@ -726,6 +728,7 @@
726728
A5001413 /* TerminalPanelView.swift */,
727729
A5001414 /* BrowserPanelView.swift */,
728730
A5007421 /* BrowserPopupWindowController.swift */,
731+
NRBW0002 /* BrowserWebAuthnSupport.swift */,
729732
A5001418 /* MarkdownPanel.swift */,
730733
A5001419 /* MarkdownPanelView.swift */,
731734
MDRD00000000000000000002 /* MarkdownDocumentView.swift */,
@@ -1110,6 +1113,7 @@
11101113
A5001403 /* TerminalPanelView.swift in Sources */,
11111114
A5001404 /* BrowserPanelView.swift in Sources */,
11121115
A5007420 /* BrowserPopupWindowController.swift in Sources */,
1116+
NRBW0001 /* BrowserWebAuthnSupport.swift in Sources */,
11131117
A5001420 /* MarkdownPanel.swift in Sources */,
11141118
A5001421 /* MarkdownPanelView.swift in Sources */,
11151119
MDRD00000000000000000001 /* MarkdownDocumentView.swift in Sources */,

Resources/Info.plist

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@
4747
<string>A program running within Programa would like to use your microphone.</string>
4848
<key>NSCameraUsageDescription</key>
4949
<string>A program running within Programa would like to use your camera.</string>
50+
<key>NSBluetoothAlwaysUsageDescription</key>
51+
<string>A program running within Programa would like to use Bluetooth to discover passkeys and security keys.</string>
5052
<key>NSPrincipalClass</key>
5153
<string>NSApplication</string>
5254
<key>NSAppleScriptEnabled</key>

Resources/InfoPlist.xcstrings

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,23 @@
11
{
22
"sourceLanguage": "en",
33
"strings": {
4+
"NSBluetoothAlwaysUsageDescription": {
5+
"extractionState": "manual",
6+
"localizations": {
7+
"en": {
8+
"stringUnit": {
9+
"state": "translated",
10+
"value": "A program running within Programa would like to use Bluetooth to discover passkeys and security keys."
11+
}
12+
},
13+
"ja": {
14+
"stringUnit": {
15+
"state": "translated",
16+
"value": "Programa内で実行中のプログラムが、パスキーとセキュリティキーを検出するためにBluetoothの使用を求めています。"
17+
}
18+
}
19+
}
20+
},
421
"NSCameraUsageDescription": {
522
"extractionState": "manual",
623
"localizations": {

Sources/Panels/BrowserPanel.swift

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1169,6 +1169,7 @@ final class BrowserPanel: Panel, ObservableObject {
11691169
private var navigationDelegate: BrowserNavigationDelegate?
11701170
private var uiDelegate: BrowserUIDelegate?
11711171
private var downloadDelegate: BrowserDownloadDelegate?
1172+
private var webAuthnCoordinator: BrowserWebAuthnCoordinator?
11721173
private var webViewObservers: [NSKeyValueObservation] = []
11731174
private var activeDownloadCount: Int = 0
11741175

@@ -1486,6 +1487,17 @@ final class BrowserPanel: Panel, ObservableObject {
14861487
forMainFrameOnly: true
14871488
)
14881489
)
1490+
// WebAuthn can originate from same-origin child frames. The native
1491+
// bridge rejects first-time authorization requests from cross-origin
1492+
// subframes before touching the shared browser authorization state.
1493+
configuration.userContentController.addUserScript(
1494+
WKUserScript(
1495+
source: BrowserWebAuthnBridgeContract.scriptSource,
1496+
injectionTime: .atDocumentStart,
1497+
forMainFrameOnly: false,
1498+
in: .page
1499+
)
1500+
)
14891501
}
14901502

14911503
private func bindWebView(_ webView: ProgramaWebView) {
@@ -1504,6 +1516,9 @@ final class BrowserPanel: Panel, ObservableObject {
15041516
webView.uiDelegate = uiDelegate
15051517
setupObservers(for: webView)
15061518
setupReactGrabMessageHandler(for: webView)
1519+
let webAuthnCoordinator = BrowserWebAuthnCoordinator()
1520+
webAuthnCoordinator.install(on: webView)
1521+
self.webAuthnCoordinator = webAuthnCoordinator
15071522
setupIMECompositionTracking(for: webView)
15081523
}
15091524

@@ -1800,6 +1815,8 @@ final class BrowserPanel: Panel, ObservableObject {
18001815
faviconTask = nil
18011816
faviconRefreshGeneration &+= 1
18021817
BrowserWindowPortalRegistry.detach(webView: previousWebView)
1818+
webAuthnCoordinator?.uninstall(from: previousWebView)
1819+
webAuthnCoordinator = nil
18031820
previousWebView.stopLoading()
18041821
previousWebView.navigationDelegate = nil
18051822
previousWebView.uiDelegate = nil
@@ -2155,6 +2172,8 @@ final class BrowserPanel: Panel, ObservableObject {
21552172
faviconTask = nil
21562173
faviconRefreshGeneration &+= 1
21572174
BrowserWindowPortalRegistry.detach(webView: oldWebView)
2175+
webAuthnCoordinator?.uninstall(from: oldWebView)
2176+
webAuthnCoordinator = nil
21582177
oldWebView.stopLoading()
21592178
oldWebView.navigationDelegate = nil
21602179
oldWebView.uiDelegate = nil
@@ -2296,6 +2315,8 @@ final class BrowserPanel: Panel, ObservableObject {
22962315
popup.closePopup()
22972316
}
22982317

2318+
webAuthnCoordinator?.uninstall(from: webView)
2319+
webAuthnCoordinator = nil
22992320
webView.stopLoading()
23002321
webView.navigationDelegate = nil
23012322
webView.uiDelegate = nil
@@ -2907,8 +2928,11 @@ final class BrowserPanel: Panel, ObservableObject {
29072928
}
29082929
webViewObservers.removeAll()
29092930
webViewCancellables.removeAll()
2931+
let webAuthnCoordinator = webAuthnCoordinator
2932+
self.webAuthnCoordinator = nil
29102933
let webView = webView
29112934
Task { @MainActor in
2935+
webAuthnCoordinator?.uninstall(from: webView)
29122936
BrowserWindowPortalRegistry.detach(webView: webView)
29132937
}
29142938
}
@@ -2999,6 +3023,8 @@ extension BrowserPanel {
29993023
webViewObservers.removeAll()
30003024
webViewCancellables.removeAll()
30013025
BrowserWindowPortalRegistry.detach(webView: oldWebView)
3026+
webAuthnCoordinator?.uninstall(from: oldWebView)
3027+
webAuthnCoordinator = nil
30023028
oldWebView.stopLoading()
30033029
oldWebView.navigationDelegate = nil
30043030
oldWebView.uiDelegate = nil

Sources/Panels/BrowserPopupWindowController.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ final class BrowserPopupWindowController: NSObject, NSWindowDelegate {
7878
private let popupUIDelegate: PopupUIDelegate
7979
private let popupNavigationDelegate: PopupNavigationDelegate
8080
private let downloadDelegate: BrowserDownloadDelegate
81+
private let webAuthnCoordinator: BrowserWebAuthnCoordinator
8182

8283
private static var associatedObjectKey: UInt8 = 0
8384
private var hasShownPanel = false
@@ -111,6 +112,7 @@ final class BrowserPopupWindowController: NSObject, NSWindowDelegate {
111112
webView.customUserAgent = BrowserUserAgentSettings.safariUserAgent
112113
BrowserThemeSettings.apply(openerPanel?.currentBrowserThemeMode ?? BrowserThemeSettings.mode(), to: webView)
113114
self.webView = webView
115+
self.webAuthnCoordinator = BrowserWebAuthnCoordinator()
114116

115117
// --- Window sizing from WKWindowFeatures ---
116118
let defaultWidth: CGFloat = 800
@@ -203,6 +205,7 @@ final class BrowserPopupWindowController: NSObject, NSWindowDelegate {
203205
navDel.downloadDelegate = dlDel
204206
webView.uiDelegate = uiDel
205207
webView.navigationDelegate = navDel
208+
webAuthnCoordinator.install(on: webView)
206209

207210
// Context menu "Open Link in New Tab" → open in opener's workspace,
208211
// not as a nested popup. Falls back to system browser if opener is gone.
@@ -299,6 +302,7 @@ final class BrowserPopupWindowController: NSObject, NSWindowDelegate {
299302
urlObservation = nil
300303

301304
// Tear down web view
305+
webAuthnCoordinator.uninstall(from: webView)
302306
webView.stopLoading()
303307
webView.navigationDelegate = nil
304308
webView.uiDelegate = nil

0 commit comments

Comments
 (0)