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:
chick
2026-09-16 18:57:41 +08:00
parent a6867bc9cb
commit 3f6090081f
37 changed files with 4129 additions and 199 deletions
@@ -21,15 +21,23 @@ public struct InstanceSessionService: Sendable {
self.client = client
}
/// Logs in directly and stores the session cookie.
///
/// Only meaningful in direct mode: a relay instance has no origin to dial and no cookie to keep,
/// because the node's agent holds that session itself. Passing one here returns the explicit
/// unauthenticated state rather than dialing something arbitrary.
public func login(_ instance: Instance, password: String) async throws -> SessionMetadata {
guard let origin = instance.endpoint.directOrigin else {
return SessionMetadata(instanceID: instance.id, authenticated: false, checkedAt: Date())
}
do {
let response = try await client.send(
.authLogin(password: password),
baseURL: instance.origin,
baseURL: origin,
cookie: nil
)
if let cookie = Self.sessionCookie(response.setCookie) {
await store.set(instance.id, origin: instance.origin, cookie: cookie)
await store.set(instance.id, origin: origin, cookie: cookie)
return SessionMetadata(instanceID: instance.id, authenticated: true, checkedAt: Date())
}
} catch {
@@ -40,15 +48,19 @@ public struct InstanceSessionService: Sendable {
}
public func logout(_ instance: Instance) async throws -> SessionMetadata {
if let cookie = await store.cookie(for: instance.id, origin: instance.origin) {
_ = try? await client.send(.authLogout, baseURL: instance.origin, cookie: cookie)
guard let origin = instance.endpoint.directOrigin else {
return SessionMetadata(instanceID: instance.id, authenticated: false, checkedAt: Date())
}
if let cookie = await store.cookie(for: instance.id, origin: origin) {
_ = try? await client.send(.authLogout, baseURL: origin, cookie: cookie)
}
await store.clear(instance.id)
return SessionMetadata(instanceID: instance.id, authenticated: false, checkedAt: Date())
}
public func cookie(for instance: Instance) async -> String? {
await store.cookie(for: instance.id, origin: instance.origin)
guard let origin = instance.endpoint.directOrigin else { return nil }
return await store.cookie(for: instance.id, origin: origin)
}
static func sessionCookie(_ header: String?) -> String? {