feat(ios): 中继通道 P4/P5——端点抽象、信封、传输、配对与分层错误
- InstanceEndpoint 取代 Instance.origin(旧数据 init(from:) 读成 .direct, 写盘只输出 endpoint;dedupKey 直通用 origin、中继用 relay:<nodeID>) - RelayWire:Swift 侧重算 contracts/test-vectors.json(CanonicalJSON 手写解析器, 因为 Foundation 的解码器会抹掉'拼写',而 intent 摘要定义的正是拼写) - NodeTransport 抽象 + DirectTransport / RelayTransport / NodeTransportRouter; DirectTransport 是薄适配,直通路径行为不变 - RelayTransport:信封构造、签名、加密、验签、解密;响应验签失败即丢弃, 四类以上失败各有独立 UpstreamError 分支与用户文案 - DeviceIdentity(沙盒文件 0600,配对时由节点分配 deviceID)、RelayPairing、配对 sheet - 卡片通道徽标 + freshness 副标题;危险写 intent 在发送瞬间生成 - scripts/export-app-routes.swift 生成 vendored 路由表,AppRoutesContractTests 反向守卫 - 测试 78→92:向量互算、迁移(字面量旧 JSON)、配对四类失败、路由器、 传输往返与各类拒绝路径
This commit is contained in:
@@ -29,7 +29,7 @@ public struct FileInstanceRepository: InstanceRepository {
|
||||
!search.isEmpty {
|
||||
instances = instances.filter {
|
||||
$0.name.localizedCaseInsensitiveContains(search)
|
||||
|| $0.origin.absoluteString.localizedCaseInsensitiveContains(search)
|
||||
|| $0.endpoint.displayAddress.localizedCaseInsensitiveContains(search)
|
||||
}
|
||||
}
|
||||
if let tags = query.tags, !tags.isEmpty {
|
||||
@@ -48,15 +48,15 @@ public struct FileInstanceRepository: InstanceRepository {
|
||||
guard !input.name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
|
||||
throw RepositoryError.invalidOrigin
|
||||
}
|
||||
let origin = try Self.normalizedOrigin(input.origin)
|
||||
guard !store.instances.contains(where: { $0.origin == origin }) else {
|
||||
let endpoint = try Self.normalized(input.endpoint)
|
||||
guard !store.instances.contains(where: { $0.endpoint.dedupKey == endpoint.dedupKey }) else {
|
||||
throw RepositoryError.duplicateOrigin
|
||||
}
|
||||
let now = Date()
|
||||
let instance = Instance(
|
||||
id: UUID(),
|
||||
name: input.name.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
origin: origin,
|
||||
endpoint: endpoint,
|
||||
tags: Self.normalizedTags(input.tags),
|
||||
credentialConfigured: !(input.password ?? "").isEmpty,
|
||||
createdAt: now,
|
||||
@@ -82,12 +82,14 @@ public struct FileInstanceRepository: InstanceRepository {
|
||||
guard !normalized.isEmpty else { throw RepositoryError.invalidOrigin }
|
||||
instance.name = normalized
|
||||
}
|
||||
if let origin = patch.origin {
|
||||
let normalized = try Self.normalizedOrigin(origin)
|
||||
guard !store.instances.contains(where: { $0.id != id && $0.origin == normalized }) else {
|
||||
if let endpoint = patch.endpoint {
|
||||
let normalized = try Self.normalized(endpoint)
|
||||
guard !store.instances.contains(where: {
|
||||
$0.id != id && $0.endpoint.dedupKey == normalized.dedupKey
|
||||
}) else {
|
||||
throw RepositoryError.duplicateOrigin
|
||||
}
|
||||
instance.origin = normalized
|
||||
instance.endpoint = normalized
|
||||
}
|
||||
if let tags = patch.tags {
|
||||
instance.tags = Self.normalizedTags(tags)
|
||||
@@ -150,6 +152,39 @@ public struct FileInstanceRepository: InstanceRepository {
|
||||
}
|
||||
}
|
||||
|
||||
private static func normalized(_ endpoint: InstanceEndpoint) throws -> InstanceEndpoint {
|
||||
switch endpoint {
|
||||
case .direct(let url):
|
||||
return .direct(try normalizedOrigin(url))
|
||||
case .relay(let relay):
|
||||
// A relay endpoint is validated on its own terms: https, a parseable node id, and the
|
||||
// exact key sizes the envelope format requires. A half-configured relay entry that
|
||||
// reached the transport would fail as an unreadable response rather than as "this
|
||||
// pairing is incomplete".
|
||||
guard relay.relayBaseURL.scheme == "https",
|
||||
relay.relayBaseURL.host?.isEmpty == false,
|
||||
relay.relayBaseURL.path.isEmpty || relay.relayBaseURL.path == "/"
|
||||
else {
|
||||
throw RepositoryError.invalidOrigin
|
||||
}
|
||||
guard relay.nodeID.hasPrefix("nd_"), relay.nodeID.count >= 11 else {
|
||||
throw RepositoryError.invalidOrigin
|
||||
}
|
||||
guard relay.agentSigningKey.count == RelayWire.keyBytes,
|
||||
relay.agentKxKey.count == RelayWire.keyBytes,
|
||||
relay.deviceToken.count >= 16,
|
||||
relay.deviceID.hasPrefix("ap_")
|
||||
else {
|
||||
throw RepositoryError.invalidOrigin
|
||||
}
|
||||
var normalized = relay
|
||||
normalized.relayBaseURL = URL(string: relay.relayBaseURL.absoluteString.trimmingCharacters(
|
||||
in: CharacterSet(charactersIn: "/")
|
||||
)) ?? relay.relayBaseURL
|
||||
return .relay(normalized)
|
||||
}
|
||||
}
|
||||
|
||||
private static func normalizedOrigin(_ url: URL) throws -> URL {
|
||||
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
|
||||
components.scheme == "http" || components.scheme == "https",
|
||||
|
||||
Reference in New Issue
Block a user