import APIClient import Foundation import HostRegistry import SessionCore import WireProtocol /// T-iOS-15 · Production dependency graph (composition root). One immutable /// value assembled at launch; every screen/VM receives its dependencies from /// here — no ad-hoc URLSession/keychain access anywhere else in the App layer. /// /// Assembly security audit (task 安全注, verified at this single point): /// - All `G` (state-changing) HTTP goes through `APIClient` (kill via /// SessionListViewModel's client, probe's kill round-trip inside /// `runPairingProbe`); `URLSessionHTTPTransport` adds no headers of its own. /// - Origin is derived solely by `HostEndpoint` (WS: URLSessionTermTransport; /// HTTP: APIClient's route builder) — nothing here hand-assembles one. /// - Secrets: hosts in `KeychainHostStore` /// (AfterFirstUnlockThisDeviceOnly, hosted keychain test asserts it); /// UserDefaults carries only the non-secret per-host lastSessionId. /// - No debug ATS overrides exist (project.yml declares NO /// NSAllowsArbitraryLoads in any configuration — only the five §5.2 CIDR /// exceptions), and no isSecureTextEntry-style screenshot hacks are used; /// `UIScreen.isCaptured` detection is SKIPPED per plan (accepted residual /// risk, local trust model). struct AppEnvironment: Sendable { let hostStore: any HostStore let lastSessionStore: any LastSessionStore let http: any HTTPTransport let termTransport: any TermTransport /// Injected into `PairingViewModel` — production is `runPairingProbe` /// over the real transports (two-step: RO GET, then WS attach + guarded /// kill; only runs after the user's explicit confirm, T-iOS-12). let probe: PairingViewModel.Probe /// T-iOS-23 · unread last-seen watermarks (non-secret; UserDefaults). /// `var` + default so the memberwise init stays source-compatible for /// pre-P1 call sites while tests can inject an in-memory fake. var unreadStore: any UnreadWatermarkStore = UserDefaultsUnreadWatermarkStore() static func production() -> AppEnvironment { let http = URLSessionHTTPTransport() let termTransport = URLSessionTermTransport() return AppEnvironment( hostStore: KeychainHostStore(), lastSessionStore: UserDefaultsLastSessionStore(), http: http, termTransport: termTransport, probe: { endpoint in await runPairingProbe(endpoint: endpoint, http: http, ws: termTransport) } ) } /// Away-digest source for a session engine: wraps `APIClient.events` per /// host (the engine never holds an HTTP client — plan §3.2). func makeEventsSource(endpoint: HostEndpoint) -> @Sendable (UUID) async throws -> [TimelineEvent] { let client = APIClient(endpoint: endpoint, http: http) return { id in try await client.events(id: id) } } }