Files
hapi/ios/Packages/HapiKit/Sources/HapiClient/Endpoints/MachineEndpoints.swift
T
weishu 1f0ec6184d feat(ios): HapiClient API transport + auth (A-M1b)
APIClient (final class, Sendable) with typed endpoints for the M2/M3 REST
surface behind an HTTPPerforming seam; actor AuthManager with single-flight
JWT refresh (POST /api/auth), proactive refresh 10 min before exp, terminal
authFailed state, and 401 -> refresh -> retry-once wiring per
docs/api/client-contract/auth.md; Keychain credential store (per-hub records
under run.hapi.companion) with CredentialStoring seam + in-memory double;
HubRegistry (ordered hubs + active hub in injectable UserDefaults, origin
normalization); payload-only JWT decoding; APIError {status, code, body}
parsing per errors.md; minimal multipart builder for M4c dictation; 256 MB
URLCache session for generated images.

Request/response wire models (SendMessageRequest, PermissionApproveRequest,
SpawnRequest/SpawnResponse, MessagesQuery, envelopes, RPC-wrapped shapes)
join HapiProtocol/Models mirroring shared/src/apiTypes.ts.

swift-testing coverage: JWT decode (padding/garbage/hostile exp), auth
single-flight (8 concurrent callers -> one exchange), 401 retry-once and
terminal paths, APIError body parsing, endpoint request construction
byte-checks (cursor queries, deliveryMode body, answers formats, explicit
null model reset) via a recording performer, hub URL normalization,
multipart bytes.
2026-08-17 15:20:03 +08:00

67 lines
2.4 KiB
Swift

import Foundation
import HapiProtocol
/// Machines and spawning (`docs/api/client-contract/rest.md`).
extension APIClient {
/// `GET /api/machines` — online machines in the caller's namespace.
public func listMachines() async throws -> [Machine] {
let response: MachinesResponse = try await request(.get, "/api/machines")
return response.machines
}
/// `PATCH /api/machines/:id` — rename (≤ 64 chars; empty string clears
/// the custom name back to the hostname).
public func renameMachine(id: String, displayName: String) async throws {
struct RenameMachineRequest: Encodable {
let displayName: String
}
try await requestVoid(
.patch,
"/api/machines/\(encodePathComponent(id))",
body: RenameMachineRequest(displayName: displayName)
)
}
/// `POST /api/machines/:id/spawn`. The response is discriminated on
/// `type`, not HTTP status — a failed spawn is still HTTP 200 with
/// `.error`.
public func spawnSession(machineId: String, _ spawn: SpawnRequest) async throws -> SpawnResponse {
try await request(
.post,
"/api/machines/\(encodePathComponent(machineId))/spawn",
body: spawn
)
}
/// `POST /api/machines/:id/list-directory` (RPC envelope — check
/// `success`).
public func listMachineDirectory(
machineId: String,
path: String,
includeHidden: Bool = false
) async throws -> MachineListDirectoryResponse {
struct ListDirectoryRequest: Encodable {
let path: String
let includeHidden: Bool
}
return try await request(
.post,
"/api/machines/\(encodePathComponent(machineId))/list-directory",
body: ListDirectoryRequest(path: path, includeHidden: includeHidden)
)
}
/// `POST /api/machines/:id/paths/exists` (≤ 1000 paths).
public func machinePathsExist(machineId: String, paths: [String]) async throws -> [String: Bool] {
struct PathsExistsRequest: Encodable {
let paths: [String]
}
let response: MachinePathsExistsResponse = try await request(
.post,
"/api/machines/\(encodePathComponent(machineId))/paths/exists",
body: PathsExistsRequest(paths: paths)
)
return response.exists
}
}