import Foundation /// Holds session state for an authenticated MCMS connection. /// /// Cookie handling: `URLSession` stores the `sessionid` and `csrftoken` cookies /// automatically and replays them. Our job is to (a) read the CSRF cookie value /// so we can echo it in the `X-CSRFToken` header on writes (Django-style), and /// (b) clear cookies on logout. @MainActor final class SessionStore { private(set) var isAuthenticated = false /// The cookie storage backing the session. Injected so a proxy deployment /// can use an isolated storage if needed. let cookieStorage: HTTPCookieStorage private let config: APIConfiguration init(config: APIConfiguration, cookieStorage: HTTPCookieStorage = .shared) { self.config = config self.cookieStorage = cookieStorage } func markAuthenticated() { isAuthenticated = true } /// Current CSRF token (the value of the CSRF cookie), to be sent as the /// `X-CSRFToken` header on mutating requests. Returns nil before login. func csrfToken() -> String? { guard let cookies = cookieStorage.cookies(for: config.baseURL) else { return nil } return cookies.first { Self.matches($0.name, config.csrfCookieName) }?.value } /// Whether a session cookie is currently present. var hasSessionCookie: Bool { guard let cookies = cookieStorage.cookies(for: config.baseURL) else { return false } return cookies.contains { Self.matches($0.name, config.sessionCookieName) } } /// Modern MCMS 6.2 builds emit `__Host-csrftoken` / `__Host-sessionid`; /// older ones use the bare names. Accept either form (also `__Secure-`). static func matches(_ cookieName: String, _ base: String) -> Bool { cookieName == base || cookieName == "__Host-" + base || cookieName == "__Secure-" + base } /// Drop all cookies for the configured host (call after logout / on reset). func clear() { if let cookies = cookieStorage.cookies(for: config.baseURL) { cookies.forEach { cookieStorage.deleteCookie($0) } } isAuthenticated = false } }