chore: snapshot uncommitted local work before rebasing onto cloud-agent
Safety commit so the 94 uncommitted files in the working tree (in-flight Account/Mac/Tip work plus the feature-preview recording tooling) survive the move onto cursor/cloud-agent-1787824905651-vxuwc. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -80,7 +80,7 @@ public actor AccountAPIClient {
|
||||
private struct RefreshOperation {
|
||||
let id: UUID
|
||||
let failedAccessToken: String
|
||||
let task: Task<AccountSession, Error>
|
||||
let task: Task<AccountTokenSession, Error>
|
||||
}
|
||||
|
||||
private let baseURL: URL
|
||||
@@ -90,7 +90,7 @@ public actor AccountAPIClient {
|
||||
private let encoder: JSONEncoder
|
||||
private let decoder: JSONDecoder
|
||||
|
||||
private var cachedSession: AccountSession?
|
||||
private var cachedSession: AccountTokenSession?
|
||||
private var didLoadSession = false
|
||||
private var refreshOperation: RefreshOperation?
|
||||
private var invalidationContinuations: [
|
||||
@@ -113,19 +113,19 @@ public actor AccountAPIClient {
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func signInWithApple(_ request: AppleSignInRequest) async throws -> AccountSession {
|
||||
public func signInWithApple(_ request: AppleSignInRequest) async throws -> AccountTokenSession {
|
||||
let data = try await perform(
|
||||
endpoint: .appleSignIn,
|
||||
body: try encode(request),
|
||||
requiresSession: false
|
||||
)
|
||||
let session = try decode(APIDataEnvelope<AccountSession>.self, from: data).data
|
||||
let session = try decode(APIDataEnvelope<AccountTokenSession>.self, from: data).data
|
||||
try await replaceSession(with: session)
|
||||
try? await sessionVault.clearRefreshTransaction()
|
||||
return session
|
||||
}
|
||||
|
||||
public func currentSession() async throws -> AccountSession? {
|
||||
public func currentSession() async throws -> AccountTokenSession? {
|
||||
try await loadSessionIfNeeded()
|
||||
}
|
||||
|
||||
@@ -386,7 +386,7 @@ public actor AccountAPIClient {
|
||||
}
|
||||
}
|
||||
|
||||
private func sessionForRequest() async throws -> AccountSession {
|
||||
private func sessionForRequest() async throws -> AccountTokenSession {
|
||||
guard let session = try await loadSessionIfNeeded() else {
|
||||
throw AccountAPIError.sessionUnavailable
|
||||
}
|
||||
@@ -403,7 +403,7 @@ public actor AccountAPIClient {
|
||||
return session
|
||||
}
|
||||
|
||||
private func refreshSession(afterUnauthorizedAccessToken failedToken: String) async throws -> AccountSession {
|
||||
private func refreshSession(afterUnauthorizedAccessToken failedToken: String) async throws -> AccountTokenSession {
|
||||
// Refresh always re-reads Keychain so another client instance cannot
|
||||
// rotate a stale in-memory session with a new operation identifier.
|
||||
guard let current = try await reloadSessionFromVault() else {
|
||||
@@ -438,7 +438,7 @@ public actor AccountAPIClient {
|
||||
return try await finishRefresh(operation)
|
||||
}
|
||||
|
||||
private func finishRefresh(_ operation: RefreshOperation) async throws -> AccountSession {
|
||||
private func finishRefresh(_ operation: RefreshOperation) async throws -> AccountTokenSession {
|
||||
do {
|
||||
let replacement = try await operation.task.value
|
||||
guard let current = try await reloadSessionFromVault() else {
|
||||
@@ -478,7 +478,7 @@ public actor AccountAPIClient {
|
||||
private func requestRefresh(
|
||||
using refreshToken: String,
|
||||
operationId: UUID
|
||||
) async throws -> AccountSession {
|
||||
) async throws -> AccountTokenSession {
|
||||
let request = try makeRequest(
|
||||
endpoint: .refresh,
|
||||
body: try encode(
|
||||
@@ -491,7 +491,7 @@ public actor AccountAPIClient {
|
||||
)
|
||||
let response = try await send(request)
|
||||
let data = try validatedData(response)
|
||||
return try decode(APIDataEnvelope<AccountSession>.self, from: data).data
|
||||
return try decode(APIDataEnvelope<AccountTokenSession>.self, from: data).data
|
||||
}
|
||||
|
||||
private func shouldClearSession(afterRefreshError error: Error) -> Bool {
|
||||
@@ -504,7 +504,7 @@ public actor AccountAPIClient {
|
||||
}
|
||||
}
|
||||
|
||||
private func loadSessionIfNeeded() async throws -> AccountSession? {
|
||||
private func loadSessionIfNeeded() async throws -> AccountTokenSession? {
|
||||
if didLoadSession {
|
||||
return cachedSession
|
||||
}
|
||||
@@ -517,7 +517,7 @@ public actor AccountAPIClient {
|
||||
}
|
||||
}
|
||||
|
||||
private func reloadSessionFromVault() async throws -> AccountSession? {
|
||||
private func reloadSessionFromVault() async throws -> AccountTokenSession? {
|
||||
do {
|
||||
let session = try await sessionVault.loadSession()
|
||||
cachedSession = session
|
||||
@@ -528,7 +528,7 @@ public actor AccountAPIClient {
|
||||
}
|
||||
}
|
||||
|
||||
private func replaceSession(with session: AccountSession) async throws {
|
||||
private func replaceSession(with session: AccountTokenSession) async throws {
|
||||
do {
|
||||
try await sessionVault.saveSession(session)
|
||||
cachedSession = session
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct AccountSession: Codable, Equatable, Sendable {
|
||||
public struct AccountTokenSession: Codable, Equatable, Sendable {
|
||||
public let accountId: UUID
|
||||
public let tokenType: String
|
||||
public let accessToken: String
|
||||
@@ -229,8 +229,8 @@ public struct AccountRefreshTransaction: Codable, Equatable, Sendable {
|
||||
}
|
||||
|
||||
public protocol AccountSessionVault: Sendable {
|
||||
func loadSession() async throws -> AccountSession?
|
||||
func saveSession(_ session: AccountSession) async throws
|
||||
func loadSession() async throws -> AccountTokenSession?
|
||||
func saveSession(_ session: AccountTokenSession) async throws
|
||||
func clearSession() async throws
|
||||
func beginRefreshTransaction(
|
||||
refreshTokenDigest: String
|
||||
|
||||
@@ -10,6 +10,10 @@ import Security
|
||||
public struct HostPrivateAccountKeychainDescriptor: Equatable, Sendable {
|
||||
public static let defaultService = "com.osgkeyboard.ios.account"
|
||||
public static let hostBundleIdentifier = "com.osgkeyboard.ios"
|
||||
/// The macOS menu-bar app has no extension to share with, so it owns its
|
||||
/// own private group. Listed here so the guard below still rejects the
|
||||
/// App-Group-reachable `com.osgkeyboard.shared` group on both platforms.
|
||||
public static let macHostBundleIdentifier = "com.osgkeyboard.mac"
|
||||
|
||||
public let service: String
|
||||
public let accessGroup: String
|
||||
@@ -20,8 +24,10 @@ public struct HostPrivateAccountKeychainDescriptor: Equatable, Sendable {
|
||||
) throws {
|
||||
let normalizedService = service.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let normalizedAccessGroup = accessGroup.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let isHostPrivate = [Self.hostBundleIdentifier, Self.macHostBundleIdentifier]
|
||||
.contains { normalizedAccessGroup.hasSuffix(".\($0)") }
|
||||
guard !normalizedService.isEmpty,
|
||||
normalizedAccessGroup.hasSuffix(".\(Self.hostBundleIdentifier)"),
|
||||
isHostPrivate,
|
||||
!normalizedAccessGroup.hasSuffix(".com.osgkeyboard.shared") else {
|
||||
throw AccountAPIError.secureStorage
|
||||
}
|
||||
@@ -67,9 +73,9 @@ public actor HostPrivateAccountKeychain:
|
||||
self.decoder = JSONDecoder()
|
||||
}
|
||||
|
||||
public func loadSession() async throws -> AccountSession? {
|
||||
public func loadSession() async throws -> AccountTokenSession? {
|
||||
do {
|
||||
let session = try read(AccountSession.self, account: Account.session)
|
||||
let session = try read(AccountTokenSession.self, account: Account.session)
|
||||
Self.logger.info(
|
||||
"session keychain restore status=\(session == nil ? "not-found" : "found", privacy: .public)"
|
||||
)
|
||||
@@ -80,7 +86,7 @@ public actor HostPrivateAccountKeychain:
|
||||
}
|
||||
}
|
||||
|
||||
public func saveSession(_ session: AccountSession) async throws {
|
||||
public func saveSession(_ session: AccountTokenSession) async throws {
|
||||
try write(session, account: Account.session)
|
||||
}
|
||||
|
||||
@@ -205,12 +211,20 @@ public actor HostPrivateAccountKeychain:
|
||||
}
|
||||
|
||||
private func baseQuery(account: String) -> [String: Any] {
|
||||
[
|
||||
var query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrService as String: descriptor.service,
|
||||
kSecAttrAccount as String: account,
|
||||
kSecAttrAccessGroup as String: descriptor.accessGroup,
|
||||
kSecAttrSynchronizable as String: kCFBooleanFalse!
|
||||
]
|
||||
#if os(macOS)
|
||||
// macOS defaults to the legacy file-based keychain, which ignores
|
||||
// `kSecAttrAccessGroup` and rejects `kSecAttrAccessible`. Opt into the
|
||||
// data-protection keychain so entitlement-scoped access groups and the
|
||||
// AfterFirstUnlockThisDeviceOnly protection behave as they do on iOS.
|
||||
query[kSecUseDataProtectionKeychain as String] = true
|
||||
#endif
|
||||
return query
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// OSGKeyboard · HostSupport
|
||||
//
|
||||
// Provisions anonymous, App-Attest-bound OOBE credentials without creating or
|
||||
// mutating an AccountSession.
|
||||
// mutating an AccountTokenSession.
|
||||
|
||||
import Foundation
|
||||
#if canImport(OSGKeyboardShared)
|
||||
|
||||
@@ -150,7 +150,7 @@ public actor AccountSignInCoordinator {
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func signIn() async throws -> AccountSession {
|
||||
public func signIn() async throws -> AccountTokenSession {
|
||||
let nonce = try nonceGenerator.makeNonce()
|
||||
let credential = try await appleAuthorization.authorize(nonceSHA256: nonce.sha256Hex)
|
||||
let evidence = try await integrity.evidenceForAppleSignIn(
|
||||
|
||||
Reference in New Issue
Block a user