T-iOS-22: DeepLinkRouter (full-field whitelist, cold-start stash, route(from:) for push-tap reuse), 21 tests T-iOS-24: TimelineSheet mirroring web render() order; disabled→empty-state; reuses AwayDigest onExpand T-iOS-25: QuickReply chips (built-ins mirror quick-reply.ts via KeyByteMap; visible iff live gate && !readOnly) T-iOS-27: read-only DiffScreen + App-layer DiffFetcher (RO no-Origin; APIClient fold-in noted for T-iOS-38 owner) T-iOS-26: Projects list/detail — grouping byte-identical to web group keys (prefs-shared collapse state), prefs-clobber defenses (no blind PUT on empty base; adopt server echo), claude\r bootstrap T-iOS-23: UnreadLedger + TitleSanitizer (SessionCore, +15 tests), lastOutputAt decode, list-boundary re-sanitize T-iOS-29: new-in-cwd (untrusted cwd, no bootstrap re-injection) + exited-session reopen; fixes stale-controller SwiftTerm view bug via .id(controller.id) T-iOS-28: offscreen SwiftTerm thumbnail pipeline (LRU 32, concurrency gate 2, 256KiB cap, grid clamp) T-iOS-21: PushRegistrar (WEBTERM_GATE category: Allow=.authenticationRequired, no .foreground) + NotificationActionHandler (whitelisted payload, token never persisted, bg-task-wrapped POST, 403 fallback) CRITICAL fix (verify-found boot crash): @Sendable literals on UN completion closures — MainActor-inherited closures trapped Swift 6 executor check on UN's background queue; boot re-verified (no new crash reports, permission prompt reached, privacy shade correctly covering during system alert) Verified: 261 pkg + 247 app + 10 integration tests green; 7/7 semantics checks; Owns audit clean
61 lines
2.9 KiB
Swift
61 lines
2.9 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
|
|
/// 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) }
|
|
}
|
|
}
|