Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions Docs/TestPlan.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,31 @@ repeat prompting for this rollout, “Set as Default” must register both HTTP
and a link opened from another app must arrive in a new Lumen Browser tab. No prompt
should appear when Lumen Browser already handles both schemes.

### Sidebar Space-Paging Performance

Profile a Release app bundle launched with `./script/build_and_run.sh` on a
120 Hz ProMotion display, with the app active, AC power connected, and Low
Power Mode disabled. Use at least eight differently themed spaces containing
large tab lists, folders, favorites, loading tabs, and long URLs.

- Record at least 100 forward, reverse, cancelled, and interrupted horizontal
swipes with Instruments Animation Hitches, SwiftUI, and Time Profiler.
- Include Activity-to-space transitions and the pull-to-create interaction
beyond the final space.
- Confirm the live webpage preview still changes at the directional threshold,
while the selected space commits only after the pager settles.
- At least 99% of offered 120 Hz gesture frames must complete within 8.33 ms,
with no consecutive app-caused misses and no display-rate SwiftUI
body/layout invalidation from pager geometry samples.
- Check light and dark appearance, pinned and floating sidebars, Reduce Motion,
Reduce Transparency, exact theme endpoints, and the live glass/tint/noise
interpolation.

Debug builds expose the `Sidebar Space Paging`, `Sidebar Pager Live Render`,
`Sidebar Preview Handoff`, and `Sidebar Pager Frame Summary` points of
interest for local Instruments traces. These measurements are not persisted
or transmitted.

## Required Future Tests

- UI tests for creating spaces, folders, profiles, opening tabs, switching tabs, restoring sessions, and split view.
Expand Down
3 changes: 2 additions & 1 deletion Docs/ThreatModel.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@
- Pop-up/new-window requests and WebKit media-capture permission callbacks are routed through store state before any grant; unsupported permission kinds are denied with an explicit message.
- Autoplay is configured to require a user gesture by default.
- Password form submissions can create a native save prompt for HTTPS origins and loopback HTTP development origins. Accepted credentials are scoped to the current persistent profile and stored in the local macOS Keychain with this-device-only accessibility; private profiles do not prompt or persist passwords.
- Profile isolation covers website sessions and Bare Browser-managed Keychain credentials. macOS/WebKit Password AutoFill suggestions are device-wide system behavior and are not claimed as profile-isolated; Bare Browser does not use fragile webpage-specific suppression to hide them.
- Profile isolation covers website sessions and Lumen Browser-managed Keychain credentials. macOS/WebKit Password AutoFill suggestions are device-wide system behavior and are not claimed as profile-isolated; Lumen Browser does not use fragile webpage-specific suppression to hide them.
- WebAuthn interception is installed in the page content world only when the running app lacks Apple's signed browser public-key-credential entitlement. It wraps only `navigator.credentials.create` and `get` calls carrying `publicKey` options, forwards other Credential Management requests unchanged, sends no relying-party or page data to native code, and rejects the unsupported request with `NotSupportedError` after showing the native passkey-unavailable message. Entitled builds do not install the wrapper.
- The command bar returns open-tab matches only from the active profile. The intentional all-profile Activity view labels profile ownership and selects a matching space before navigation.
- App Sandbox entitlement file includes only sandbox and outbound network client entitlement.
- A small `WKContentRuleList` blocks common tracker/ad endpoints without request interception hacks.
Expand Down
1 change: 1 addition & 0 deletions Docs/WebKitLimitations.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Bare Browser uses `WKWebView`, not Safari's full browser process or Chromium. Kn
## Current Known Limitations

- Some Safari browser features are not public `WKWebView` APIs.
- Passkeys and security keys for arbitrary relying-party domains require Apple's managed `com.apple.developer.web-browser.public-key-credential` entitlement. When that signed entitlement is absent, Lumen Browser rejects webpage WebAuthn `create` and `get` requests and displays a native message directing the user to another sign-in method. Entitled builds leave WebAuthn handling to WebKit.
- Chrome extension compatibility is not available and should not be claimed.
- Safari Web Extensions require a dedicated extension architecture and are not part of this scaffold.
- Per-profile isolation depends on WebKit support for identified website data stores. Persistent profiles use `WKWebsiteDataStore(forIdentifier:)`; each private profile reuses one `.nonPersistent()` store until that private profile closes.
Expand Down
88 changes: 80 additions & 8 deletions Sources/MeridianCore/Models/BrowserContentPresentationState.swift
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import AppKit

enum BrowserContentPagerPreviewTarget: Equatable, Sendable {
case activity
case space
case tab(TabID)
case startPage(SpaceID)
}

@MainActor
public final class BrowserContentPresentationState: ObservableObject {
@Published public private(set) var previewTabID: TabID?
@Published public private(set) var previewStartPageSpaceID: SpaceID?
@Published private(set) var pagerPreviewTarget: BrowserContentPagerPreviewTarget?
@Published public private(set) var activeContentTabID: TabID?
@Published public private(set) var snapshotHandoffIdentity: WebContentSessionIdentity?
private var snapshotHandoffID: UUID?
Expand All @@ -15,6 +21,28 @@ public final class BrowserContentPresentationState: ObservableObject {
snapshotHandoffIdentity?.tabID
}

public var previewTabID: TabID? {
guard case .tab(let tabID) = pagerPreviewTarget else {
return nil
}
return tabID
}

public var previewStartPageSpaceID: SpaceID? {
guard case .startPage(let spaceID) = pagerPreviewTarget else {
return nil
}
return spaceID
}

// nil follows the committed selection; true/false follows the live pager target.
var activityPagePreviewOverride: Bool? {
guard let pagerPreviewTarget else {
return nil
}
return pagerPreviewTarget == .activity
}

public init(snapshotHandoffExpirationNanoseconds: UInt64 = 1_200_000_000) {
self.snapshotHandoffExpirationNanoseconds = snapshotHandoffExpirationNanoseconds
}
Expand All @@ -23,20 +51,38 @@ public final class BrowserContentPresentationState: ObservableObject {
snapshotHandoffExpirationTask?.cancel()
}

public func setPreviewTabID(_ tabID: TabID?) {
guard previewTabID != tabID else {
func setPagerPreviewTarget(_ target: BrowserContentPagerPreviewTarget?) {
guard pagerPreviewTarget != target else {
return
}
pagerPreviewTarget = target
}

previewTabID = tabID
public func setPreviewTabID(_ tabID: TabID?) {
if let tabID {
setPagerPreviewTarget(.tab(tabID))
} else if case .tab = pagerPreviewTarget {
setPagerPreviewTarget(nil)
}
}

public func setPreviewStartPageSpaceID(_ spaceID: SpaceID?) {
guard previewStartPageSpaceID != spaceID else {
return
if let spaceID {
setPagerPreviewTarget(.startPage(spaceID))
} else if case .startPage = pagerPreviewTarget {
setPagerPreviewTarget(nil)
}
}

previewStartPageSpaceID = spaceID
func setActivityPagePreviewOverride(_ isPresented: Bool?) {
switch isPresented {
case true:
setPagerPreviewTarget(.activity)
case false:
setPagerPreviewTarget(.space)
case nil:
setPagerPreviewTarget(nil)
}
}

public func setActiveContentTabID(_ tabID: TabID?) {
Expand Down Expand Up @@ -142,6 +188,32 @@ public final class BrowserContentPresentationState: ObservableObject {
}
}

struct BrowserActivityPagePresentation {
static func isPresented(
isSelected: Bool,
previewOverride: Bool?
) -> Bool {
previewOverride ?? isSelected
}
}

struct BrowserContentPreviewPlaceholder {
static func shouldShow(
for previewTab: BrowserTab?,
selectedTabID: TabID?,
snapshotIsAvailable: Bool
) -> Bool {
guard let previewTab,
previewTab.id != selectedTabID,
previewTab.content.isWeb,
previewTab.url != nil else {
return false
}

return !snapshotIsAvailable
}
}

struct BrowserSpaceFocusedTabResolver {
static func focusedTabID(
for space: BrowserSpace,
Expand Down
43 changes: 26 additions & 17 deletions Sources/MeridianCore/Security/BrowserPasskeyAccessController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,24 @@ private let browserPasskeyLogger = Logger(
category: "Passkeys"
)

public enum BrowserPasskeyCapability {
public static let browserEntitlement =
"com.apple.developer.web-browser.public-key-credential"

public static let hasSignedBrowserEntitlement: Bool = {
guard let task = SecTaskCreateFromSelf(nil),
let value = SecTaskCopyValueForEntitlement(
task,
browserEntitlement as CFString,
nil
) else {
return false
}

return value as? Bool == true
}()
}

public enum BrowserPasskeyAuthorizationState: Equatable, Sendable {
case authorized
case denied
Expand Down Expand Up @@ -37,12 +55,18 @@ public enum BrowserPasskeyAccessPolicy {
&& state == .notDetermined
&& !didRequestThisLaunch
}

public static func shouldInstallUnavailableInterception(
isBrowserEntitled: Bool
) -> Bool {
!isBrowserEntitled
}
}

@MainActor
public final class BrowserPasskeyAccessController: ObservableObject {
public static let browserEntitlement =
"com.apple.developer.web-browser.public-key-credential"
BrowserPasskeyCapability.browserEntitlement

@Published public private(set) var authorizationState: BrowserPasskeyAuthorizationState
public let isBrowserEntitled: Bool
Expand All @@ -56,9 +80,7 @@ public final class BrowserPasskeyAccessController: ObservableObject {
self.authorizationState = BrowserPasskeyAuthorizationState(
credentialManager.authorizationStateForPlatformCredentials
)
self.isBrowserEntitled = Self.signedEntitlementIsEnabled(
Self.browserEntitlement
)
self.isBrowserEntitled = BrowserPasskeyCapability.hasSignedBrowserEntitlement
}

public func requestAuthorizationIfNeeded() {
Expand Down Expand Up @@ -86,17 +108,4 @@ public final class BrowserPasskeyAccessController: ObservableObject {
}
}
}

private static func signedEntitlementIsEnabled(_ entitlement: String) -> Bool {
guard let task = SecTaskCreateFromSelf(nil),
let value = SecTaskCopyValueForEntitlement(
task,
entitlement as CFString,
nil
) else {
return false
}

return value as? Bool == true
}
}
2 changes: 1 addition & 1 deletion Sources/MeridianCore/Stores/BrowserStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -828,7 +828,7 @@ public final class BrowserStore: ObservableObject {
selectedTabID = selectedSpace?.selectedTabID
}
refreshActivePageSecurityStatus()
persistSession()
schedulePersistSession()
return true
}

Expand Down
Loading