feat(ios): add native app foundation and unsigned ipa build

This commit is contained in:
chick
2026-09-09 00:05:51 +08:00
parent fda3e11f1e
commit 54eb8dcb86
29 changed files with 1680 additions and 0 deletions
@@ -0,0 +1,20 @@
import Foundation
public actor InstanceSessionStore {
private var cookies: [UUID: (origin: URL, cookie: String)] = [:]
public init() {}
public func cookie(for id: UUID, origin: URL) -> String? {
guard let value = cookies[id], value.origin == origin else { return nil }
return value.cookie
}
public func set(_ id: UUID, origin: URL, cookie: String) {
cookies[id] = (origin, cookie)
}
public func clear(_ id: UUID) {
cookies.removeValue(forKey: id)
}
}
@@ -0,0 +1,71 @@
import Foundation
public struct UpstreamResponse: Equatable, Sendable {
public let status: Int
public let data: Data
public let setCookie: String?
public init(status: Int, data: Data, setCookie: String? = nil) {
self.status = status
self.data = data
self.setCookie = setCookie
}
}
public struct SimAdminClient: Sendable {
private let session: URLSession
private let maximumBytes: Int
public init(session: URLSession = .shared, maximumBytes: Int = 262_144) {
self.session = session
self.maximumBytes = maximumBytes
}
public func send(
_ route: UpstreamRoute,
baseURL: URL,
cookie: String?
) async throws -> UpstreamResponse {
guard baseURL.scheme == "http" || baseURL.scheme == "https",
baseURL.path.isEmpty || baseURL.path == "/"
else {
throw UpstreamError.invalidRequest
}
guard let url = URL(string: baseURL.absoluteString + route.path) else {
throw UpstreamError.invalidRequest
}
var request = URLRequest(url: url, timeoutInterval: 30)
request.httpMethod = route.method
request.httpBody = route.body
request.setValue("application/json", forHTTPHeaderField: "accept")
if route.body != nil {
request.setValue("application/json", forHTTPHeaderField: "content-type")
}
if let cookie {
request.setValue(cookie, forHTTPHeaderField: "cookie")
}
do {
let (data, response) = try await session.data(for: request)
guard let http = response as? HTTPURLResponse, data.count <= maximumBytes else {
throw UpstreamError.invalidResponse
}
switch http.statusCode {
case 200...299:
break
case 401: throw UpstreamError.unauthorized
case 403: throw UpstreamError.forbidden
case 404, 405: throw UpstreamError.notSupported
default: throw UpstreamError.transportFailed
}
let setCookie = http.allHeaderFields["Set-Cookie"] as? String
return UpstreamResponse(status: http.statusCode, data: data, setCookie: setCookie)
} catch let error as UpstreamError {
throw error
} catch let error as URLError where error.code == .timedOut {
throw UpstreamError.timeout
} catch {
throw UpstreamError.transportFailed
}
}
}
@@ -0,0 +1,70 @@
import Foundation
public enum UpstreamRoute: Equatable, Sendable {
case health
case device
case sim
case network
case signalStrength
case connectivity
case smsList(limit: Int, offset: Int, direction: String?)
case smsSend(phoneNumber: String, content: String)
case smsBatchDelete(ids: [Int])
case authLogin(password: String)
case authLogout
public var method: String {
switch self {
case .health, .device, .sim, .network, .signalStrength, .connectivity, .smsList:
return "GET"
case .smsSend, .smsBatchDelete, .authLogin, .authLogout:
return "POST"
}
}
public var path: String {
switch self {
case .health: return "/api/health"
case .device: return "/api/device"
case .sim: return "/api/sim"
case .network: return "/api/network"
case .signalStrength: return "/api/network/signal-strength"
case .connectivity: return "/api/connectivity"
case .smsList(let limit, let offset, let direction):
var items = ["limit=\(limit)", "offset=\(offset)"]
if let direction {
items.append("direction=\(direction)")
}
return "/api/sms/list?\(items.joined(separator: "&"))"
case .smsSend: return "/api/sms/send"
case .smsBatchDelete: return "/api/sms/batch-delete"
case .authLogin: return "/api/auth/login"
case .authLogout: return "/api/auth/logout"
}
}
public var body: Data? {
switch self {
case .smsSend(let phoneNumber, let content):
return try? JSONSerialization.data(
withJSONObject: ["phone_number": phoneNumber, "content": content]
)
case .smsBatchDelete(let ids):
return try? JSONSerialization.data(withJSONObject: ["ids": ids])
case .authLogin(let password):
return try? JSONSerialization.data(withJSONObject: ["password": password])
default:
return nil
}
}
}
public enum UpstreamError: Error, Equatable {
case invalidRequest
case unauthorized
case forbidden
case notSupported
case timeout
case transportFailed
case invalidResponse
}