Skip to content

Commit 7973bc2

Browse files
azooz2003-bitclaude
andcommitted
Merge origin/main into feat-remote-workspace
Re-merge to clear CONFLICTING after main advanced today. Main brought in: iOS sign-out local-first/offline-safe revocation (#5776), iOS workspace list groups/unread-dots/last-activity previews + shared Unread filter (#5726), Focus/DND-honoring fallback notification sound (#5651), and the budget refreshes #6018/#6020/#6025. #6020 extracted NotificationSoundSettings out of TerminalNotificationStore.swift into a new Sources/NotificationSoundSettings.swift; #6025 deduped the TerminalNotificationStore budget entry. Conflict resolution: - Sources/TerminalNotificationStore.swift: main redesigned this file by extracting the NotificationSoundSettings enum. Took main's version (the extraction wins). The slice's two edits here (import CmuxFoundation; swap ProcessPipeReader.readDataToEndOfFileOrEmpty(from:) for the new FileHandle.readDataToEndOfFileOrEmpty() extension) both targeted the enum body that main moved, so they no longer belong in this file. - Sources/NotificationSoundSettings.swift (main's new extracted file): RE-LIFT. The slice removed the app-target ProcessPipeReader type in favor of a CmuxFoundation FileHandle extension, so main's extracted file would not compile as-is. Re-lifted the slice's migration here: added import CmuxFoundation and switched the errorPipe read to errorPipe.fileHandleForReading.readDataToEndOfFileOrEmpty(). - .github/swift-file-length-budget.tsv: two conflict regions (slice vs main ordering, plus #6025's TerminalNotificationStore dedup). Regenerated the whole budget to merged-tree actual counts via --write-budget; check passes. - cmux.xcodeproj/project.pbxproj: auto-merged, re-normalized; check-pbxproj passes, NotificationSoundSettings.swift wired, no ID collisions. Slice packages (CmuxCore/CmuxRemoteDaemon/CmuxRemoteSession/CmuxRemoteWorkspace) and the modified CmuxFoundation kept intact; ghostty pointer matches origin/main; no CmuxControlSocket phantom inherited. Gates: ensure-ghosttykit OK, swift_file_length_budget exit 0, lint-ios-package-conventions exit 0, all slice packages + CmuxFoundation swift build OK, app xcodebuild Debug BUILD SUCCEEDED. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 parents 8495a18 + 1e3b103 commit 7973bc2

75 files changed

Lines changed: 6244 additions & 1453 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/swift-file-length-budget.tsv

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55
19266 Sources/ContentView.swift
66
18118 Sources/AppDelegate.swift
77
16674 Sources/GhosttyTerminalView.swift
8-
14787 Sources/TerminalController.swift
8+
14635 Sources/TerminalController.swift
99
13568 Sources/Panels/BrowserPanel.swift
10-
12405 Sources/Workspace.swift
10+
12410 Sources/Workspace.swift
1111
12044 cmuxTests/AppDelegateShortcutRoutingTests.swift
1212
10020 Sources/TabManager.swift
1313
9345 cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift
@@ -21,7 +21,7 @@
2121
6071 Sources/TextBoxInput.swift
2222
5482 cmuxTests/BrowserConfigTests.swift
2323
5462 Sources/cmuxApp.swift
24-
4827 Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
24+
4801 Packages/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
2525
4460 Sources/Panels/FilePreviewPanel.swift
2626
4400 cmuxTests/BrowserPanelTests.swift
2727
4227 Sources/BrowserWindowPortal.swift
@@ -38,17 +38,17 @@
3838
2565 Sources/Panels/CmuxWebView.swift
3939
2545 cmuxTests/WorkspaceManualUnreadTests.swift
4040
2544 cmuxTests/CommandPaletteSearchEngineTests.swift
41-
2528 Sources/TerminalNotificationStore.swift
4241
2516 Sources/KeyboardShortcutSettings.swift
43-
2340 Sources/Mobile/MobileHostService.swift
4442
2327 cmuxTests/CJKIMEInputTests.swift
43+
2322 Sources/Mobile/MobileHostService.swift
4544
2314 Sources/FileExplorerView.swift
4645
2260 Sources/TerminalWindowPortal.swift
4746
2199 Sources/SessionPersistence.swift
4847
2123 cmuxTests/ShortcutAndCommandPaletteTests.swift
4948
2117 cmuxTests/CmuxConfigTests.swift
5049
2030 Sources/KeyboardShortcutSettingsFileStore.swift
5150
1949 Sources/Panels/BrowserWebAuthnSupport.swift
51+
1941 Sources/TerminalNotificationStore.swift
5252
1860 cmuxTests/NotificationAndMenuBarTests.swift
5353
1794 Sources/SessionIndexStore.swift
5454
1751 Sources/WindowDragHandleView.swift
@@ -70,7 +70,7 @@
7070
1362 Sources/CMUXInstalledExtensionSidebarHostView.swift
7171
1313 cmuxTests/MobileHostAuthorizationTests.swift
7272
1285 cmuxUITests/SidebarHelpMenuUITests.swift
73-
1239 Sources/Feed/FeedCoordinator.swift
73+
1255 Sources/Feed/FeedCoordinator.swift
7474
1205 Packages/CmuxMobileTerminal/Sources/CmuxMobileTerminal/TerminalInputTextView.swift
7575
1197 cmuxTests/CodexAppServerSessionTests.swift
7676
1156 cmuxTests/SidebarOrderingTests.swift
@@ -109,12 +109,12 @@
109109
768 Sources/MainWindowFocusController.swift
110110
762 Packages/CmuxMobileTransport/Sources/CmuxMobileTransport/CmxNetworkByteTransport.swift
111111
760 Packages/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/AgentLaunchSanitizerTests.swift
112-
757 Packages/CmuxAuthRuntime/Sources/CmuxAuthRuntime/Coordinator/AuthCoordinator.swift
113112
756 Sources/Panels/AgentSessionWebRendererCoordinator.swift
114113
752 cmuxUITests/CloseWorkspaceCmdDUITests.swift
115114
751 Sources/TerminalController+ControlWorkspaceContext.swift
116115
739 Sources/App/MenuBarExtraController.swift
117116
738 Packages/CMUXProjectModel/Sources/CMUXProjectModel/XcodeProjectAdapter.swift
117+
738 Packages/CmuxAuthRuntime/Sources/CmuxAuthRuntime/Coordinator/AuthCoordinator.swift
118118
716 Sources/TaskManagerSnapshot.swift
119119
714 Sources/AppleScriptSupport.swift
120120
710 Sources/TerminalSSHSessionDetector.swift
@@ -126,6 +126,7 @@
126126
697 cmuxTests/TerminalNotificationClearAllTests.swift
127127
696 cmuxTests/UpdatePillReleaseVisibilityTests.swift
128128
693 Sources/Panels/BrowserPopupWindowController.swift
129+
691 Sources/NotificationSoundSettings.swift
129130
691 cmuxTests/TaskManagerResourcesTests.swift
130131
683 Packages/CmuxSwiftRender/Sources/CmuxSwiftRender/SwiftViewInterpreter.swift
131132
683 Sources/Panels/CodexAppServerSession.swift

Packages/CmuxAuthRuntime/Sources/CmuxAuthRuntime/BrowserSignIn/HostBrowserSignInFlow.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,21 @@ public final class HostBrowserSignInFlow {
345345
try await coordinator.completeExternalSignIn()
346346
} catch {
347347
log.log("auth.callback completion failed: \(error)")
348+
// No flow-side seed clear here, deliberately. When a sign-out
349+
// raced the validation round trip, the seeds were already in the
350+
// store when the coordinator's local-first clear ran (they are
351+
// seeded before `completeExternalSignIn`), so the coordinator's
352+
// clear owns wiping them. Clearing here instead RACES that
353+
// sign-out: the flow bumps `signOutGeneration` before the
354+
// coordinator captures the teardown credentials with raw store
355+
// reads, so a clear from this catch can empty the store inside
356+
// the capture window and silently strip the best-effort server
357+
// teardown (push unregister, session revocation) of its
358+
// credentials. A coordinator-level cancellation without a
359+
// sign-out (a concurrent publish) must not clear either: in
360+
// production the published session is typically authenticated by
361+
// these very tokens (same shared store), and clearing them would
362+
// strand it.
348363
return false
349364
}
350365
log.log("auth.callback.coordinator.complete.end attempt=\(attemptID.map(String.init) ?? "external") signedIn=\(coordinator.isAuthenticated)")

Packages/CmuxAuthRuntime/Sources/CmuxAuthRuntime/Client/AuthClient.swift

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,45 @@ public protocol AuthClient: Sendable {
6060
/// - anchor: The presentation-anchor provider for the auth UI.
6161
func signInWithOAuth(provider: String, anchor: any AuthPresentationAnchoring) async throws
6262

63-
/// Clear the persisted Stack session (tokens) for the current device.
64-
func signOut() async throws
63+
/// The access token exactly as stored, with no freshness check and no
64+
/// network refresh (unlike ``accessToken()``, which may mint a new token
65+
/// over the network when the stored one looks stale).
66+
///
67+
/// For capturing the credentials a best-effort server-side teardown needs
68+
/// before ``clearLocalSession()`` destroys them; never blocks on
69+
/// connectivity.
70+
func storedAccessToken() async -> String?
71+
72+
/// Clear the locally persisted session (tokens) without any network call.
73+
/// The device is signed out once this returns, regardless of connectivity.
74+
func clearLocalSession() async
75+
76+
/// Clear the locally persisted session only while the stored refresh
77+
/// token still equals `refreshToken`: an atomic compare-and-clear at the
78+
/// token store. For stale-session cleanup that can race a fresh sign-in's
79+
/// store write; a store that changed owners after the cleanup decision is
80+
/// left alone. User-intent sign-out keeps using the unconditional
81+
/// ``clearLocalSession()``.
82+
func clearLocalSession(ifRefreshTokenMatches refreshToken: String) async
83+
84+
/// Revoke the server-side session the captured token pair authenticates,
85+
/// without touching local token storage (the local session is typically
86+
/// already cleared by ``clearLocalSession()`` when this runs).
87+
///
88+
/// Best-effort by contract: callers bound it with a deadline and log
89+
/// failures rather than letting revocation gate sign-out.
90+
func revokeSession(accessToken: String?, refreshToken: String?) async throws
91+
92+
/// A likely-valid access token for an explicit captured token pair,
93+
/// resolved through an ephemeral store that never touches local token
94+
/// storage: the captured access token when still fresh, else one freshly
95+
/// minted from the captured refresh token.
96+
///
97+
/// For the sign-out teardown: the raw capture can hold an expired access
98+
/// token (or none on a refresh-only store), and the cmux API
99+
/// authenticates with the Bearer + refresh header pair, so the bounded
100+
/// best-effort teardown needs a usable access token even though the local
101+
/// session is already cleared. Best-effort: returns `nil` when no token
102+
/// could be resolved (offline, dead server).
103+
func freshAccessToken(accessToken: String?, refreshToken: String) async -> String?
65104
}

Packages/CmuxAuthRuntime/Sources/CmuxAuthRuntime/Client/StackAuthClient.swift

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,39 @@ public struct StackAuthClient: AuthClient {
8181
try await stack.signInWithOAuth(provider: provider, presentationContextProvider: anchor)
8282
}
8383

84-
public func signOut() async throws {
85-
try await stack.signOut()
84+
/// The access token exactly as stored: a raw read, no freshness check,
85+
/// no network refresh. See ``AuthClient/storedAccessToken()``.
86+
public func storedAccessToken() async -> String? {
87+
await stack.getStoredAccessToken()
88+
}
89+
90+
/// Clear the locally persisted Stack session unconditionally, with no
91+
/// network call. See ``AuthClient/clearLocalSession()``.
92+
public func clearLocalSession() async {
93+
await stack.clearStoredTokens()
94+
}
95+
96+
/// Compare-and-clear the locally persisted Stack session, atomic at the
97+
/// SDK token store. See
98+
/// ``AuthClient/clearLocalSession(ifRefreshTokenMatches:)``.
99+
public func clearLocalSession(ifRefreshTokenMatches refreshToken: String) async {
100+
await stack.clearStoredTokens(ifRefreshTokenEquals: refreshToken)
101+
}
102+
103+
/// Revoke the server-side session the captured token pair authenticates,
104+
/// through an ephemeral token store that never writes to the app's real
105+
/// keychain. See ``AuthClient/revokeSession(accessToken:refreshToken:)``.
106+
public func revokeSession(accessToken: String?, refreshToken: String?) async throws {
107+
// Nothing to authenticate the DELETE with; skip the round trip.
108+
guard accessToken != nil || refreshToken != nil else { return }
109+
try await stack.revokeSession(accessToken: accessToken, refreshToken: refreshToken)
110+
}
111+
112+
/// A likely-valid access token for an explicit captured pair, resolved
113+
/// without touching local token storage. See
114+
/// ``AuthClient/freshAccessToken(accessToken:refreshToken:)``.
115+
public func freshAccessToken(accessToken: String?, refreshToken: String) async -> String? {
116+
await stack.likelyValidAccessToken(accessToken: accessToken, refreshToken: refreshToken)
86117
}
87118

88119
private static func mapped(_ user: CurrentUser) async -> CMUXAuthUser {

0 commit comments

Comments
 (0)