Skip to content

Commit adfbe26

Browse files
committed
Merge upstream/main (11 commits): iOS dismiss-sync, sidebar perf, founder welcome
PRs included: - iOS notifications cross-device dismiss-sync + authoritative unread count - iOS workspace row actions (manaflow-ai#6022) - Sidebar perf: kill LazyVStack layout livelock (manaflow-ai#6019 + manaflow-ai#6024) - iOS sign-out local-first offline-safe revocation - iOS workspace list groups, unread dots, last-activity previews - Refresh notification fallback sound DnD - Email Founder's Edition customers welcome via Resend webhook - Misc swift-file-length-budget refresh + extract NotificationSoundSettings - cloud-testflight --marketing-version override Fork-side adjustments: - Drop fork's mobileAllowsWorkspaceAction static helper from TerminalController: upstream put it in Sources/TerminalController+MobileNotificationSync.swift - Restore fork's renameTopLevelLayoutTabContaining and closeTopLevelLayoutTabContaining methods on Workspace (TerminalController v2WorkspaceTopTabRename/Close call sites depended on them) - pbxproj keep-both for upstream new files + fork P41 entries - Budget tsv merged via max-per-path script
2 parents 807783d + 0b82dfe commit adfbe26

110 files changed

Lines changed: 9888 additions & 1915 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: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@
6161
19265 Sources/ContentView.swift
6262
924 Sources/DockPanelView.swift
6363
1365 Sources/Feed/FeedButtonStyleDebugWindowController.swift
64-
1239 Sources/Feed/FeedCoordinator.swift
64+
1255 Sources/Feed/FeedCoordinator.swift
6565
3937 Sources/Feed/FeedPanelView.swift
6666
574 Sources/Feed/FeedTextEditorDebugWindowController.swift
6767
680 Sources/FileExplorerSearchController.swift
@@ -74,6 +74,7 @@
7474
2039 Sources/KeyboardShortcutSettingsFileStore.swift
7575
768 Sources/MainWindowFocusController.swift
7676
2340 Sources/Mobile/MobileHostService.swift
77+
690 Sources/NotificationSoundSettings.swift
7778
681 Sources/Panels/AgentSessionProcessStore.swift
7879
756 Sources/Panels/AgentSessionWebRendererCoordinator.swift
7980
554 Sources/Panels/BrowserAutomation.swift
@@ -115,7 +116,7 @@
115116
1144 Sources/VaultAgentProcessScanner.swift
116117
1751 Sources/WindowDragHandleView.swift
117118
546 Sources/Windowing/WindowGlassEffect.swift
118-
19985 Sources/Workspace.swift
119+
19990 Sources/Workspace.swift
119120
878 Sources/WorkspaceContentView.swift
120121
627 Sources/WorkspaceRemoteConfiguration.swift
121122
5462 Sources/cmuxApp.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)