import HostRegistry /// User-facing pairing copy (plan §3.4 taxonomy → actionable wording; §5.2 /// Local-Network guidance including the iOS 18 restart caveat). /// /// Extracted from `PairingViewModel.swift` by C1: the VM grew the access-token /// and host-management flows, and copy is presentation, not state machine /// (plan §4 file-size discipline — many small focused files). /// /// SECURITY (§5.3): no function here ever takes or echoes a token value. The /// shape-rejection copy is derived from `AccessTokenError`, which deliberately /// carries a length at most — never the rejected secret. enum PairingCopy { static let scanRejected = "二维码不是 http(s) 地址,无法配对。请扫描 web 终端工具栏「Connect a device」弹出的二维码。" static let manualRejected = "无法解析这个地址。请输入完整 URL,例如 http://192.168.1.5:3000" static let storeFailed = "主机已通过验证,但保存到本机失败,请重试。" static let localNetworkDenied = "无法访问本地网络——「本地网络」权限可能被拒绝。请到 设置 → 隐私与安全性 → 本地网络 打开 WebTerm 的开关" + "(iOS 18 存在需要重启手机才生效的已知问题)。" static let notWebTerminal = "对方在响应 HTTP,但不是 web-terminal——端口对吗?" static let tlsFailure = "TLS 连接失败:证书无效或不受信任。" static let timeout = "连接超时。请确认主机在线、与手机在同一网络后重试。" /// C-iOS-3 · Tunnel host reached without a device certificate installed. static let deviceCertRequired = "请先安装本设备证书:到 设置 →「设备证书」导入 .p12 后,再连接该隧道主机。" /// C-iOS-3 · nginx rejected the presented client certificate (invalid / /// revoked). Surfaced in place of the mis-classified "server cert invalid". static let clientCertRejected = "本设备证书无效或已吊销,请重新导入。" // MARK: - Access token (C1 · ios-completion §1.1) /// 401 from `POST /auth` — the token itself is wrong. Never echoes it. static let tokenInvalid = "访问令牌不正确。请到主机上核对 WEBTERM_TOKEN 的值后重新输入。" /// 429 — the server allows 10 `/auth` attempts per minute per IP. static let tokenRateLimited = "尝试次数过多(主机限制每分钟 10 次),请稍后再试。" /// 204 WITHOUT `Set-Cookie`: this host never enabled auth, so there is /// nothing to authenticate against and nothing gets saved. The pairing /// failure therefore has another cause — almost always `ALLOWED_ORIGINS`. static let tokenNotRequired = "该主机没有启用访问令牌(服务器未设置 WEBTERM_TOKEN),令牌不会被保存。" + "配对失败的原因更可能是主机的 ALLOWED_ORIGINS 不含本 App 拨号的地址。" /// Defensive: a shape violation that slipped past the field validation. static let tokenShapeUnknown = "访问令牌格式不符合要求(16–512 位,且只能包含 A-Z a-z 0-9 . _ ~ + / = -)。" static let hostsLoadFailed = "无法读取已配对的主机列表(钥匙串读取失败)。" static let hostRemoveFailed = "移除主机失败(钥匙串写入失败),请重试。" /// Turn the typed `AccessTokenError` into field-level copy. The length cases /// report the length only — that is not secret, and it is exactly what tells /// the user whether they pasted a truncated value. static func tokenShapeRejected(_ error: AccessTokenError) -> String { switch error { case .tooShort(let length): return "访问令牌太短(\(length) 位,至少 \(AccessToken.minLength) 位)。" case .tooLong(let length): return "访问令牌太长(\(length) 位,最多 \(AccessToken.maxLength) 位)。" case .invalidCharacters: return "访问令牌含有不允许的字符(只能是 A-Z a-z 0-9 . _ ~ + / = -)。" case .unknownHost: return hostsLoadFailed } } static func hostUnreachable(_ underlying: String) -> String { "无法连接主机:\(underlying)" } /// §3.4 wording for the ATS cleartext block. static func atsBlocked(host: String) -> String { "明文 HTTP 被 ATS 拦截——\(host) 所在 IP 段不在 App 例外列表内," + "请改用 https / tailscale serve,或反馈该网段。" } }