T-iOS-15: AppEnvironment production DI (real Keychain/probe/transports), EventFanOut (engine.events → TerminalVM + GateVM + activity bridge), scenePhase lifecycle (.background→close, suspend→rebuild+reopen reclaims size), privacy shade on != .active, spike screen deleted, cold-start routing Walkthrough proxies: hosted live-server smoke over the production DI graph (probe→store→ attach→echo→close) + real-Keychain kSecAttrAccessible assertion (closes T-iOS-7 deferral) Deviation closed: TerminalViewModel.lastSentDims (valid-only) + alive-engine .active → notifyForegrounded(dims:) (orchestrator fix on behalf of T-iOS-11 owner) Env finding: repo under ~/Documents → TCC blocks sim-spawned node; SimServerHarness dual-mode (self-bootstrap / WEBTERM_SERVER_URL via simctl launchctl setenv) Verified: 214 pkg + 76 app + 10 integration tests green; verify agent 8/8 PASS
57 lines
2.6 KiB
Swift
57 lines
2.6 KiB
Swift
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
|
|
|
|
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) }
|
|
}
|
|
}
|