Files
Commilitia-Drop/ios/CDrop/Sources/Auth/BrokerLogin.swift
T
admin 512d7563ee 原生 macOS 引擎 + Broker 登录接通(Phase 3a)
- 引擎 / 鉴权跨平台 in-place(iOS 路径 #if 守卫、逐字不变,仅加 macOS/AppKit 分支):EngineController(import 守卫 + 剪贴板 UIPasteboard→NSPasteboard 上下行两处 + device_type 平台条件)、EngineWebView(UIViewRepresentable→typealias 统一后分 NSViewRepresentable)、Theme(动态色 Color(nsColor:dynamicProvider) 同 RGB)、DeviceNameStore(Host.current().localizedName)、PushRegistry(NSApplication.registerForRemoteNotifications)、Auth/BrokerLogin(presentationAnchor→NSApplication.keyWindow)、Auth/AuthManager(死 import UIKit 守卫)
- BackgroundTaskManager:整文件 #if os(iOS)(BGContinuedProcessingTask 为 API_UNAVAILABLE(macos)、编译级拒绝)+ macOS 同签名 no-op stub,使 EngineController.syncBackgroundTask 调用点无须改动
- device_type 修正(非机械):Mac 原生客户端 boot 注入 device_type=macos(与桌面 Wails 客户端一致,后端 / 各端已识别),不再硬编 ios——否则其他端设备列表图标错、且触发其 isWakeable 的 contains("ios") 误判本 Mac 可推送唤醒
- CommilitiaDropMac target 纳入「引擎 + 登录」最小集:Shared + Sources/Engine + Sources/Auth(除 iOS 专属 LoginView)+ RecordsStore / PushRegistry / LocalNetworkPermission / DeviceNameStore / BackgroundTaskManager + Resources/i18n;依赖 WebRTC;Info.plist 加 cdrop:// 回调 scheme(ASWebAuthenticationSession OAuth 回调)+ 本地网络(NSLocalNetworkUsageDescription / NSBonjourServices _cdrop._tcp)
- Mac 壳接引擎:CommilitiaDropMacApp(@State auth + engine,MacAppRoot 登录门 + 会话激活分桶记录)、MacLoginView(Broker 账户登录,免相机)、MacRootView(背景挂离屏引擎 WebView 保活 + 设备真实 presence 列表 + 设置账号/登出/引擎日志)、MacMenuContent(账号 + 在线设备数)。传输/文件/消息详情暂占位,随 Phase 3b 复用 iOS 视图
- 验证:just mac-build(CommilitiaDropMac.app)+ iOS 模拟器(CommilitiaDrop.app)均 BUILD SUCCEEDED。运行期登录 / 引擎联通待真机自测(Broker OAuth 需账号交互)
2026-07-07 22:22:30 +08:00

120 lines
4.6 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import AuthenticationServices
import CryptoKit
import Foundation
#if canImport(UIKit)
import UIKit
#elseif canImport(AppKit)
import AppKit
#endif
// Broker device-authorization + PKCE desktop/platform/oauth.goiOS
// loopback ASWebAuthenticationSession + scheme cdrop://auth-callback
// 1. GET {broker}/device/authorize?app=cdrop&redirect_uri=cdrop://auth-callback&state&code_challenge&S256
// 2. broker SSO cdrop://auth-callback?code=..&state=..
// 3. POST {broker}/device/token {code, code_verifier, redirect_uri} bootstrap
// 4. POST {api}/api/auth/device-sessionBearer bootstrap cdrop / device_id
//
// broker cdrop redirect_uri cdrop://auth-callback loopback
enum BrokerLoginError: Error
{
case incompleteConfig
case cannotStart
case noCallback
case stateMismatch
case missingCode
case tokenFailed(Int)
case deviceSessionFailed(Int)
case badResponse
}
// ASWebAuthenticationSession async + session
// presentationContextProvider weak session AuthManager
@MainActor
final class BrokerAuthFlow: NSObject, ASWebAuthenticationPresentationContextProviding
{
private var session: ASWebAuthenticationSession?
// URLcdrop://auth-callback?code=..&state=.. /
func run(url: URL, callbackScheme: String) async throws -> URL
{
try await withCheckedThrowingContinuation
{ cont in
let s = ASWebAuthenticationSession(url: url, callbackURLScheme: callbackScheme)
{ [weak self] callback, error in
self?.session = nil
if let error { cont.resume(throwing: error) }
else if let callback { cont.resume(returning: callback) }
else { cont.resume(throwing: BrokerLoginError.noCallback) }
}
s.presentationContextProvider = self
// Safari broker SSO SSO
s.prefersEphemeralWebBrowserSession = false
self.session = s
if !s.start() { cont.resume(throwing: BrokerLoginError.cannotStart) }
}
}
func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor
{
#if os(iOS)
let scenes = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }
return scenes.flatMap { $0.windows }.first { $0.isKeyWindow }
?? scenes.first?.windows.first
?? ASPresentationAnchor()
#else
return NSApplication.shared.keyWindow ?? NSApplication.shared.windows.first ?? ASPresentationAnchor()
#endif
}
}
// device_idbroker meta / UserDefaults
// device_idbroker R2 UPSERT
// web localStorage cdrop.device_id
enum DeviceIDStore
{
private static let key = "cdrop.device_id"
static var value: String
{
get { UserDefaults.standard.string(forKey: key) ?? "" }
set { UserDefaults.standard.set(newValue, forKey: key) }
}
}
// PKCE / randStringn base64url n PKCE unreserved
enum PKCE
{
static func randomToken(_ n: Int) -> String
{
var bytes = [UInt8](repeating: 0, count: n)
_ = SecRandomCopyBytes(kSecRandomDefault, n, &bytes)
return String(Data(bytes).base64URLEncoded().prefix(n))
}
static func challenge(for verifier: String) -> String
{
let digest = SHA256.hash(data: Data(verifier.utf8))
return Data(digest).base64URLEncoded()
}
}
extension Data
{
// base64url- / _ + / PKCE challenge /
func base64URLEncoded() -> String
{
base64EncodedString()
.replacingOccurrences(of: "+", with: "-")
.replacingOccurrences(of: "/", with: "_")
.replacingOccurrences(of: "=", with: "")
}
}
extension String
{
func trimmingTrailingSlash() -> String
{
hasSuffix("/") ? String(dropLast()) : self
}
}