- 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)、配对四类失败、路由器、 传输往返与各类拒绝路径
75 lines
3.0 KiB
Swift
75 lines
3.0 KiB
Swift
import Foundation
|
|
|
|
public struct SessionMetadata: Equatable, Sendable {
|
|
public let instanceID: UUID
|
|
public let authenticated: Bool
|
|
public let checkedAt: Date
|
|
|
|
public init(instanceID: UUID, authenticated: Bool, checkedAt: Date) {
|
|
self.instanceID = instanceID
|
|
self.authenticated = authenticated
|
|
self.checkedAt = checkedAt
|
|
}
|
|
}
|
|
|
|
public struct InstanceSessionService: Sendable {
|
|
private let store: InstanceSessionStore
|
|
private let client: any SimAdminClientProtocol
|
|
|
|
public init(store: InstanceSessionStore, client: any SimAdminClientProtocol) {
|
|
self.store = store
|
|
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: origin,
|
|
cookie: nil
|
|
)
|
|
if let cookie = Self.sessionCookie(response.setCookie) {
|
|
await store.set(instance.id, origin: origin, cookie: cookie)
|
|
return SessionMetadata(instanceID: instance.id, authenticated: true, checkedAt: Date())
|
|
}
|
|
} catch {
|
|
// Rejected credentials fall through to the same explicit unauthenticated state.
|
|
}
|
|
await store.clear(instance.id)
|
|
return SessionMetadata(instanceID: instance.id, authenticated: false, checkedAt: Date())
|
|
}
|
|
|
|
public func logout(_ instance: Instance) async throws -> SessionMetadata {
|
|
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? {
|
|
guard let origin = instance.endpoint.directOrigin else { return nil }
|
|
return await store.cookie(for: instance.id, origin: origin)
|
|
}
|
|
|
|
static func sessionCookie(_ header: String?) -> String? {
|
|
guard let header,
|
|
let range = header.range(of: "simadmin_session=([^;\\s,]+)", options: .regularExpression)
|
|
else {
|
|
return nil
|
|
}
|
|
return String(header[range])
|
|
}
|
|
}
|