import Foundation
/// Device login (`ird login`) in three phases a native app can drive as state
/// transitions: start (show the code, open the verification URL), wait for the user's
/// approval, then complete with the confirmation code shown in the browser. The
/// resulting access token is an account session: store it with a `CredentialStore`.
extension AuthAPI {
/// Opens the flow. Show `userCode` and open `verificationUriComplete`.
public func startDeviceLogin() async throws -> DeviceStartResponse {
try await deviceStart()
}
/// Polls until the user approves. A 400 (`authorization-pending`) and a 429
/// (`slow-down`) mean "not yet" and let the backoff grow; anything else is rethrown.
/// `interval` defaults to the server's, `timeout` to the flow's `expires_in`.
public func waitForDeviceApproval(_ start: DeviceStartResponse, interval: Duration? = nil, timeout: Duration? = nil) async throws {
let interval = interval ?? .seconds(max(1, start.interval))
let options = PollOptions(interval: interval, maxInterval: interval * 4,
timeout: timeout ?? .seconds(start.expiresIn),
message: "Timed out waiting for the device login to be approved")
_ = try await pollUntil(options, clock: client.configuration.clock,
step: {
do {
return try await devicePoll(body: DevicePollBody(deviceCode: start.deviceCode)).approved
} catch let e as IrdError where e.status == 400 || e.status == 429 {
return false
}
},
done: { $0 })
}
/// Exchanges the confirmation code the browser showed for the session tokens.
public func completeDeviceLogin(_ start: DeviceStartResponse, confirmationCode: String) async throws -> DeviceCompleteResponse {
try await deviceComplete(body: DeviceCompleteBody(deviceCode: start.deviceCode, confirmationCode: confirmationCode))
}
/// The composed flow: `prompt` shows the code and URL, `confirmationCode` asks for the
/// code once approved.
public func deviceLogin(prompt: @Sendable (DeviceStartResponse) async -> Void,
confirmationCode: @Sendable (DeviceStartResponse) async throws -> String,
interval: Duration? = nil, timeout: Duration? = nil) async throws -> DeviceCompleteResponse {
let start = try await startDeviceLogin()
await prompt(start)
try await waitForDeviceApproval(start, interval: interval, timeout: timeout)
return try await completeDeviceLogin(start, confirmationCode: try await confirmationCode(start))
}
}