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:
2026-09-01 11:28:07 -07:00
co-authored by Claude Opus 5
parent 39002336c0
commit 5d51c5bf77
101 changed files with 3629 additions and 1354 deletions
@@ -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(