refactor(gateway): unify v3 auth payload builders and vectors

This commit is contained in:
Peter Steinberger
2026-02-26 15:08:40 +01:00
parent 8315c58675
commit 081b1aa1ed
10 changed files with 313 additions and 168 deletions

View File

@@ -0,0 +1,55 @@
import Foundation
public enum GatewayDeviceAuthPayload {
public static func buildV3(
deviceId: String,
clientId: String,
clientMode: String,
role: String,
scopes: [String],
signedAtMs: Int,
token: String?,
nonce: String,
platform: String?,
deviceFamily: String?) -> String
{
let scopeString = scopes.joined(separator: ",")
let authToken = token ?? ""
let normalizedPlatform = normalizeMetadataField(platform)
let normalizedDeviceFamily = normalizeMetadataField(deviceFamily)
return [
"v3",
deviceId,
clientId,
clientMode,
role,
scopeString,
String(signedAtMs),
authToken,
nonce,
normalizedPlatform,
normalizedDeviceFamily,
].joined(separator: "|")
}
static func normalizeMetadataField(_ value: String?) -> String {
guard let value else { return "" }
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
if trimmed.isEmpty {
return ""
}
// Keep cross-runtime normalization deterministic (TS/Swift/Kotlin):
// lowercase ASCII A-Z only for auth payload metadata fields.
var output = String()
output.reserveCapacity(trimmed.count)
for scalar in trimmed.unicodeScalars {
let codePoint = scalar.value
if codePoint >= 65, codePoint <= 90, let lowered = UnicodeScalar(codePoint + 32) {
output.unicodeScalars.append(lowered)
} else {
output.unicodeScalars.append(scalar)
}
}
return output
}
}

View File

@@ -399,7 +399,7 @@ public actor GatewayChannelActor {
let signedAtMs = Int(Date().timeIntervalSince1970 * 1000)
let connectNonce = try await self.waitForConnectChallenge()
if includeDeviceIdentity, let identity {
let payload = buildDeviceAuthPayloadV3(
let payload = GatewayDeviceAuthPayload.buildV3(
deviceId: identity.deviceId,
clientId: clientId,
clientMode: clientMode,
@@ -443,44 +443,6 @@ public actor GatewayChannelActor {
}
}
private func buildDeviceAuthPayloadV3(
deviceId: String,
clientId: String,
clientMode: String,
role: String,
scopes: [String],
signedAtMs: Int,
token: String?,
nonce: String,
platform: String?,
deviceFamily: String?) -> String
{
let scopeString = scopes.joined(separator: ",")
let authToken = token ?? ""
let normalizedPlatform = normalizeMetadataField(platform)
let normalizedDeviceFamily = normalizeMetadataField(deviceFamily)
return [
"v3",
deviceId,
clientId,
clientMode,
role,
scopeString,
String(signedAtMs),
authToken,
nonce,
normalizedPlatform,
normalizedDeviceFamily,
].joined(separator: "|")
}
private func normalizeMetadataField(_ value: String?) -> String {
guard let value else { return "" }
return value
.trimmingCharacters(in: .whitespacesAndNewlines)
.lowercased(with: Locale(identifier: "en_US_POSIX"))
}
private func handleConnectResponse(
_ res: ResponseFrame,
identity: DeviceIdentity?,

View File

@@ -0,0 +1,30 @@
import Testing
@testable import OpenClawKit
@Suite("DeviceAuthPayload")
struct DeviceAuthPayloadTests {
@Test("builds canonical v3 payload vector")
func buildsCanonicalV3PayloadVector() {
let payload = GatewayDeviceAuthPayload.buildV3(
deviceId: "dev-1",
clientId: "openclaw-macos",
clientMode: "ui",
role: "operator",
scopes: ["operator.admin", "operator.read"],
signedAtMs: 1_700_000_000_000,
token: "tok-123",
nonce: "nonce-abc",
platform: " IOS ",
deviceFamily: " iPhone ")
#expect(
payload
== "v3|dev-1|openclaw-macos|ui|operator|operator.admin,operator.read|1700000000000|tok-123|nonce-abc|ios|iphone")
}
@Test("normalizes metadata with ASCII-only lowercase")
func normalizesMetadataWithAsciiLowercase() {
#expect(GatewayDeviceAuthPayload.normalizeMetadataField(" İOS ") == "İos")
#expect(GatewayDeviceAuthPayload.normalizeMetadataField(" MAC ") == "mac")
#expect(GatewayDeviceAuthPayload.normalizeMetadataField(nil) == "")
}
}