crossmate

A collaborative crossword app for iOS
Log | Files | Refs | LICENSE

commit 513adfa1c71a2344e7d09a278fc0449fb2cf91d9
parent a912de458b584f23297fa919b410c41a7e34505b
Author: Michael Camilleri <[email protected]>
Date:   Thu,  4 Jun 2026 07:49:00 +0900

Add logging for cookie errors

Diffstat:
MCrossmate/Services/AppServices.swift | 4+++-
MCrossmate/Services/KeychainHelper.swift | 8++++++--
MCrossmate/Services/NYTAuthService.swift | 36++++++++++++++++++++++++++++++++++--
3 files changed, 43 insertions(+), 5 deletions(-)

diff --git a/Crossmate/Services/AppServices.swift b/Crossmate/Services/AppServices.swift @@ -179,8 +179,10 @@ final class AppServices { let syncEngine = SyncEngine(container: self.ckContainer, persistence: persistence) self.syncEngine = syncEngine self.syncMonitor = SyncMonitor(log: eventLog) - self.nytAuth = NYTAuthService() self.driveMonitor = DriveMonitor() + self.nytAuth = NYTAuthService(log: { message in + eventLog.note(message) + }) self.nytFetcher = NYTPuzzleFetcher { NYTAuthService.currentCookie() } self.inputMonitor = InputMonitor() let identity = AuthorIdentity() diff --git a/Crossmate/Services/KeychainHelper.swift b/Crossmate/Services/KeychainHelper.swift @@ -24,6 +24,10 @@ enum KeychainHelper { } static func load(key: String) -> Data? { + loadWithStatus(key: key).data + } + + static func loadWithStatus(key: String) -> (data: Data?, status: OSStatus) { let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, @@ -34,8 +38,8 @@ enum KeychainHelper { var result: AnyObject? let status = SecItemCopyMatching(query as CFDictionary, &result) - guard status == errSecSuccess else { return nil } - return result as? Data + guard status == errSecSuccess else { return (nil, status) } + return (result as? Data, status) } static func delete(key: String) { diff --git a/Crossmate/Services/NYTAuthService.swift b/Crossmate/Services/NYTAuthService.swift @@ -1,4 +1,5 @@ import Foundation +import Security import UIKit import WebKit @@ -8,10 +9,15 @@ final class NYTAuthService { private(set) var signedInEmail: String? private(set) var isLoading = false var errorMessage: String? + private let log: ((String) -> Void)? nonisolated private static let emailKey = "nyt-email" nonisolated private static let cookieKey = "nyt-cookie" + init(log: ((String) -> Void)? = nil) { + self.log = log + } + /// Thread-safe read of the stored NYT cookie for use from non-MainActor contexts. nonisolated static func currentCookie() -> String? { guard let data = KeychainHelper.load(key: cookieKey) else { return nil } @@ -21,14 +27,17 @@ final class NYTAuthService { static let loginURL = URL(string: "https://myaccount.nytimes.com/auth/login?response_type=cookie&client_id=games&redirect_uri=https%3A%2F%2Fwww.nytimes.com%2Fcrosswords")! func loadStoredSession() { - guard KeychainHelper.load(key: Self.cookieKey) != nil else { + let cookieResult = KeychainHelper.loadWithStatus(key: Self.cookieKey) + logCookieKeychainLoad(result: cookieResult) + guard cookieResult.data != nil else { isSignedIn = false signedInEmail = nil return } isSignedIn = true - if let emailData = KeychainHelper.load(key: Self.emailKey), + let emailResult = KeychainHelper.loadWithStatus(key: Self.emailKey) + if let emailData = emailResult.data, let email = String(data: emailData, encoding: .utf8), !email.isEmpty { signedInEmail = email @@ -95,6 +104,29 @@ final class NYTAuthService { // MARK: - Private + private func logCookieKeychainLoad( + result: (data: Data?, status: OSStatus) + ) { + log?( + "NYT session cookie keychain load: status=\(Self.keychainStatusDescription(result.status)) " + + "hasData=\(result.data != nil)" + ) + } + + nonisolated private static func keychainStatusDescription(_ status: OSStatus) -> String { + if status == errSecSuccess { + return "success(0)" + } + if status == errSecItemNotFound { + return "itemNotFound(\(status))" + } + let message = SecCopyErrorMessageString(status, nil) as String? + if let message, !message.isEmpty { + return "\(message)(\(status))" + } + return "OSStatus(\(status))" + } + private func fetchUserEmail(cookies: [HTTPCookie]) async throws -> String? { let cookieHeader = Self.cookieHeader(for: cookies) guard let configuration = try await fetchAccountGraphQLConfiguration(