import APIClient import ClientTLS 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 { // C-iOS-2 (MEDIUM no-relaunch fix) · Resolve the installed device client // identity LAZILY from the keychain on each connect/challenge, injected // into BOTH transports + the probe as a provider. This way a certificate // imported from the "设备证书" screen takes effect on the NEXT connection // without relaunching — a snapshot captured here would stay stale. A // missing cert is the normal pre-install state (→ nil); genuine faults // are logged, not fatal (`loadedIdentityOrNil`). mTLS challenges only // fire for tunnel hosts, so a `nil` result is inert for local hosts. let identityStore = KeychainClientIdentityStore() let identityProvider: @Sendable () -> ClientIdentity? = { identityStore.loadedIdentityOrNil() } let http = URLSessionHTTPTransport(identityProvider: identityProvider) let termTransport = URLSessionTermTransport(identityProvider: identityProvider) 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) } } }