fix(auth): SDK device/login-events response wrappers + correct API paths

- Swift + Kotlin SDKs: listDevices() now unwraps { devices: [...] }
- Swift + Kotlin SDKs: getLoginHistory() now unwraps { events: [...] }
- Swift + Kotlin SDKs: revokeDevice() uses fingerprint param (not doc ID)
- Swift + Kotlin SDKs: revokeAllDevices() uses POST /revoke-all (not DELETE)
- Swift + Kotlin SDKs: getLoginHistory() path /login-events (not /login-events/me)
- Swift + Kotlin SDKs: Device model updated to match backend response fields
- All 53 auth tests passing
This commit is contained in:
saravanakumardb1 2026-03-12 15:42:54 -07:00
parent b0e1a54481
commit b8f22be677
2 changed files with 74 additions and 25 deletions

View File

@ -85,12 +85,28 @@ class BLAuthClient(
@Serializable
data class Device(
val id: String,
val name: String,
val platform: String,
val fingerprint: String,
val trustLevel: String,
val deviceInfo: DeviceInfo? = null,
val lastIp: String? = null,
val lastLocation: String? = null,
val trustExpiresAt: String? = null,
val lastLoginAt: String,
val createdAt: String,
val lastSeenAt: String,
val isTrusted: Boolean,
) {
/** Convenience: display name from device info. */
val name: String get() = deviceInfo?.model ?: deviceInfo?.platform ?: fingerprint.take(8)
/** Convenience: platform string. */
val platform: String get() = deviceInfo?.platform ?: "unknown"
}
@Serializable
data class DeviceInfo(
val userAgent: String? = null,
val platform: String? = null,
val model: String? = null,
val os: String? = null,
)
@Serializable
@ -518,10 +534,13 @@ class BLAuthClient(
// ── Devices (SmartAuth v2) ───────────────────────────────
@Serializable
private data class DevicesResponse(val devices: List<Device>)
/** List devices for current user. */
suspend fun listDevices(): List<Device> {
val response = client.request("GET", "/api/auth/devices")
return client.json.decodeFromString<List<Device>>(response)
return client.json.decodeFromString<DevicesResponse>(response).devices
}
/** Trust the current device (promotes to trusted, skips MFA for 90 days). */
@ -529,14 +548,14 @@ class BLAuthClient(
client.request("POST", "/api/auth/devices/trust")
}
/** Revoke trust on a specific device. */
suspend fun revokeDevice(deviceId: String) {
client.request("DELETE", "/api/auth/devices/$deviceId")
/** Revoke trust on a specific device by fingerprint. */
suspend fun revokeDevice(fingerprint: String) {
client.request("DELETE", "/api/auth/devices/$fingerprint")
}
/** Revoke all devices (requires step-up). */
/** Revoke all device trust. */
suspend fun revokeAllDevices() {
client.request("DELETE", "/api/auth/devices")
client.request("POST", "/api/auth/devices/revoke-all")
}
// ── Step-Up Auth (SmartAuth v2) ──────────────────────────
@ -550,10 +569,13 @@ class BLAuthClient(
// ── Login History (SmartAuth v2) ─────────────────────────
@Serializable
private data class EventsResponse(val events: List<LoginEvent>)
/** Get login events for the current user. */
suspend fun getLoginHistory(limit: Int = 20): List<LoginEvent> {
val response = client.request("GET", "/api/auth/login-events/me?limit=$limit")
return client.json.decodeFromString<List<LoginEvent>>(response)
val response = client.request("GET", "/api/auth/login-events?limit=$limit")
return client.json.decodeFromString<EventsResponse>(response).events
}
// ── TOTP Secret Retrieval (Phase 5C) ─────────────────────

View File

@ -93,12 +93,35 @@ public struct BLPasskey: Codable, Sendable {
/// Trusted/remembered device.
public struct BLDevice: Codable, Sendable {
public let id: String
public let name: String
public let platform: String
public let fingerprint: String
public let trustLevel: String
public let deviceInfo: DeviceInfo?
public let lastIp: String?
public let lastLocation: String?
public let trustExpiresAt: String?
public let lastLoginAt: String
public let createdAt: String
public let lastSeenAt: String
public let isTrusted: Bool
public struct DeviceInfo: Codable, Sendable {
public let userAgent: String?
public let platform: String?
public let model: String?
public let os: String?
}
/// Convenience: display name derived from device info.
public var name: String {
deviceInfo?.model ?? deviceInfo?.platform ?? fingerprint.prefix(8).description
}
/// Convenience: platform string.
public var platform: String {
deviceInfo?.platform ?? "unknown"
}
/// Stable identifier for SwiftUI ForEach.
public var id: String { fingerprint }
}
/// Login event for security log.
@ -505,7 +528,9 @@ public final class BLAuthClient {
/// List devices for current user.
public func listDevices() async throws -> [BLDevice] {
return try await client.request(path: "/api/auth/devices", responseType: [BLDevice].self)
struct DevicesResponse: Codable { let devices: [BLDevice] }
let result = try await client.request(path: "/api/auth/devices", responseType: DevicesResponse.self)
return result.devices
}
/// Trust the current device (promotes to trusted, skips MFA for 90 days).
@ -513,14 +538,14 @@ public final class BLAuthClient {
_ = try await client.rawRequest(path: "/api/auth/devices/trust", method: "POST")
}
/// Revoke trust on a specific device.
public func revokeDevice(deviceId: String) async throws {
_ = try await client.rawRequest(path: "/api/auth/devices/\(deviceId)", method: "DELETE")
/// Revoke trust on a specific device by fingerprint.
public func revokeDevice(fingerprint: String) async throws {
_ = try await client.rawRequest(path: "/api/auth/devices/\(fingerprint)", method: "DELETE")
}
/// Revoke all devices (requires step-up).
/// Revoke all device trust.
public func revokeAllDevices() async throws {
_ = try await client.rawRequest(path: "/api/auth/devices", method: "DELETE")
_ = try await client.rawRequest(path: "/api/auth/devices/revoke-all", method: "POST")
}
// MARK: - Step-Up Auth (SmartAuth v2)
@ -542,10 +567,12 @@ public final class BLAuthClient {
/// Get login events for the current user.
public func getLoginHistory(limit: Int = 20) async throws -> [BLLoginEvent] {
return try await client.request(
path: "/api/auth/login-events/me?limit=\(limit)",
responseType: [BLLoginEvent].self
struct EventsResponse: Codable { let events: [BLLoginEvent] }
let result = try await client.request(
path: "/api/auth/login-events?limit=\(limit)",
responseType: EventsResponse.self
)
return result.events
}
// MARK: - TOTP Secret Retrieval (Phase 5C)