feat(ios): P1-A — server touch-points (lastOutputAt, APNs sender+token endpoint) + APIClient P1 contract

T-iOS-37: LiveSessionInfo.lastOutputAt additive-optional field + manager.list() mapping (+4 tests; web consumers unaffected)
T-iOS-20: src/push/apns.ts — env-gated (all-or-disabled, no crash, zero key-material logging), hand-rolled ES256 JWT
(node:crypto ieee-p1363, zero new deps), NEEDS-INPUT/DONE payloads with structural minimization, token store per
subscription-store conventions, combineNotifyServices parallel to web-push, POST/DELETE /push/apns-token per frozen
wire shape (G-guard, 5/min/IP, 8kb); 65 tests incl. dual-channel e2e vs local fake APNs
T-iOS-38: APIClient builders — apns-token (client-side hex mirror, pre-network reject), projects/detail (lossy decode,
single-point percent-encoding), prefs (unknown-key byte-exact round-trip preservation), public four-tier HostNetworkTier
Coordination: webterminal:// CFBundleURLTypes pre-registered in project.yml for T-iOS-22
Verified: root 1470 tests + tsc clean; APIClient 76 tests, 95.26% coverage; wire-shape cross-check zero mismatches
This commit is contained in:
Yaojia Wang
2026-07-05 13:34:01 +02:00
parent aa956fcbb4
commit 4871e8ac3d
20 changed files with 3319 additions and 48 deletions

View File

@@ -0,0 +1,83 @@
import Foundation
import Testing
import WireProtocol
import APIClient
/// T-iOS-38 · host plan §5.4 API
/// loopback / privateLAN / tailscale / publicW3 PairingViewModel
/// `isPrivateOrLocalHost` internal
/// VM VM
/// ** @testable** API public
struct HostClassificationTests {
@Test("loopback 层:localhost/127.0.0.0\u{2044}8/IPv6 ::1(裸与带括号)")
func loopbackTierMatchesLocalTargets() {
// Arrange
let hosts = ["localhost", "LOCALHOST", "127.0.0.1", "127.8.8.8", "::1", "[::1]"]
// Act + Assert
for host in hosts {
#expect(HostClassifier.classify(host: host) == .loopback, "\(host) 应为 loopback")
}
}
@Test("privateLAN 层:RFC1918 三段 + link-local 169.254\u{2044}16 + mDNS .local")
func privateLANTierMatchesRFC1918AndLinkLocal() {
// Arrange
let hosts = [
"10.0.0.5", "192.168.0.9", "172.16.0.1", "172.31.255.255",
"169.254.1.1", "mac-mini.local", "Mac-Mini.LOCAL",
]
// Act + Assert
for host in hosts {
#expect(HostClassifier.classify(host: host) == .privateLAN, "\(host) 应为 privateLAN")
}
}
@Test("tailscale 层:CGNAT 100.64.0.0\u{2044}10 与 MagicDNS *.ts.net")
func tailscaleTierMatchesCGNATAndMagicDNS() {
// Arrange
let hosts = [
"100.64.0.1", "100.100.1.1", "100.127.255.255",
"mac.tailnet.ts.net", "foo.TS.NET",
]
// Act + Assert
for host in hosts {
#expect(HostClassifier.classify(host: host) == .tailscale, "\(host) 应为 tailscale")
}
}
@Test("public 层:公网 IP/域名/越界 CIDR/畸形 IPv4 一律最强警告层")
func publicTierIsTheFailSafeDefault() {
// Arrange :172.32 172.16/12;100.128 100.63 100.64/10
let hosts = [
"8.8.8.8", "203.0.113.7", "example.com", "tsnet.example.com",
"172.32.0.1", "100.128.0.1", "100.63.255.255", "256.1.1.1", "1.2.3", "",
]
// Act + Assert public(fail-safe:,plan §5.4)
for host in hosts {
#expect(HostClassifier.classify(host: host) == .public, "\(host) 应为 public")
}
}
@Test("classify(endpoint:) 便捷入口与 host 字符串版一致(VM 只有 HostEndpoint 可用)")
func endpointConvenienceMatchesHostClassification() throws {
// Arrange
let vectors: [(url: String, tier: HostNetworkTier)] = [
("http://127.0.0.1:3000", .loopback),
("http://192.168.1.5:3000", .privateLAN),
("http://100.100.1.1:3000", .tailscale),
("https://mac.tailnet.ts.net", .tailscale),
("https://example.com", .public),
]
// Act + Assert
for vector in vectors {
let url = try #require(URL(string: vector.url))
let endpoint = try #require(HostEndpoint(baseURL: url))
#expect(HostClassifier.classify(endpoint: endpoint) == vector.tier, "\(vector.url)")
}
}
}