Support stored device-token node startup
Allow WindowsNodeClient to bootstrap from an already-paired device token when the original gateway/bootstrap token is no longer present in settings. Add a side-effect-free DeviceIdentity helper for reading a persisted device token without generating a new identity file. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
24dfd6aebe
commit
5be08f4bc8
@ -24,6 +24,43 @@ public class DeviceIdentity
|
||||
public string DeviceId => _deviceId ?? throw new InvalidOperationException("Device not initialized");
|
||||
public string PublicKeyBase64Url => _publicKey != null ? Base64UrlEncode(_publicKey.Export(KeyBlobFormat.RawPublicKey)) : throw new InvalidOperationException("Device not initialized");
|
||||
public string? DeviceToken => _deviceToken;
|
||||
|
||||
public static string? TryReadStoredDeviceToken(string dataPath, IOpenClawLogger? logger = null)
|
||||
{
|
||||
var keyPath = Path.Combine(dataPath, "device-key-ed25519.json");
|
||||
if (!File.Exists(keyPath))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(File.ReadAllText(keyPath));
|
||||
if (doc.RootElement.TryGetProperty(nameof(DeviceKeyData.DeviceToken), out var deviceToken) &&
|
||||
deviceToken.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
var value = deviceToken.GetString();
|
||||
return string.IsNullOrWhiteSpace(value) ? null : value;
|
||||
}
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
logger?.Warn($"Failed to read stored device token: {ex.Message}");
|
||||
}
|
||||
catch (UnauthorizedAccessException ex)
|
||||
{
|
||||
logger?.Warn($"Failed to read stored device token: {ex.Message}");
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
logger?.Warn($"Failed to read stored device token: {ex.Message}");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static bool HasStoredDeviceToken(string dataPath, IOpenClawLogger? logger = null) =>
|
||||
!string.IsNullOrWhiteSpace(TryReadStoredDeviceToken(dataPath, logger));
|
||||
|
||||
public DeviceIdentity(string dataPath, IOpenClawLogger? logger = null)
|
||||
{
|
||||
|
||||
@ -74,7 +74,7 @@ public class WindowsNodeClient : WebSocketClientBase
|
||||
protected override string ClientRole => "node";
|
||||
|
||||
public WindowsNodeClient(string gatewayUrl, string token, string dataPath, IOpenClawLogger? logger = null, string? bootstrapToken = null)
|
||||
: base(gatewayUrl, ResolveRequiredCredential(token, bootstrapToken), logger)
|
||||
: base(gatewayUrl, ResolveRequiredCredential(token, bootstrapToken, dataPath), logger)
|
||||
{
|
||||
_gatewayToken = NormalizeOptionalCredential(token);
|
||||
_bootstrapToken = NormalizeOptionalCredential(bootstrapToken);
|
||||
@ -98,7 +98,7 @@ public class WindowsNodeClient : WebSocketClientBase
|
||||
return string.IsNullOrWhiteSpace(credential) ? string.Empty : credential;
|
||||
}
|
||||
|
||||
private static string ResolveRequiredCredential(string? token, string? bootstrapToken)
|
||||
private static string ResolveRequiredCredential(string? token, string? bootstrapToken, string dataPath)
|
||||
{
|
||||
var gatewayToken = NormalizeOptionalCredential(token);
|
||||
if (!string.IsNullOrEmpty(gatewayToken))
|
||||
@ -112,6 +112,12 @@ public class WindowsNodeClient : WebSocketClientBase
|
||||
return bootstrap;
|
||||
}
|
||||
|
||||
var storedDeviceToken = DeviceIdentity.TryReadStoredDeviceToken(dataPath);
|
||||
if (!string.IsNullOrEmpty(storedDeviceToken))
|
||||
{
|
||||
return storedDeviceToken;
|
||||
}
|
||||
|
||||
throw new ArgumentException("Token or bootstrap token is required.", nameof(token));
|
||||
}
|
||||
|
||||
|
||||
@ -41,6 +41,31 @@ public class WindowsNodeClientTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_AllowsStoredDeviceTokenWithoutGatewayOrBootstrapToken()
|
||||
{
|
||||
var dataPath = Path.Combine(Path.GetTempPath(), $"openclaw-node-test-{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(dataPath);
|
||||
|
||||
try
|
||||
{
|
||||
var identity = new DeviceIdentity(dataPath);
|
||||
identity.Initialize();
|
||||
identity.StoreDeviceToken("stored-device-token");
|
||||
|
||||
using var client = new WindowsNodeClient("ws://localhost:18789", "", dataPath);
|
||||
|
||||
Assert.True(client.IsPaired);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (Directory.Exists(dataPath))
|
||||
{
|
||||
Directory.Delete(dataPath, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Regression test: when hello-ok includes auth.deviceToken, PairingStatusChanged must
|
||||
/// fire exactly once — not twice (once from the token block and again from the DeviceToken
|
||||
|
||||
Loading…
Reference in New Issue
Block a user