Merge nucleic/warm-north-vole-pf37 into dev
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace NucleicProtocol.Interop;
|
||||
|
||||
public sealed record ProtocolClientConfiguration
|
||||
{
|
||||
[JsonPropertyName("identityDir")]
|
||||
public required string IdentityDirectory { get; init; }
|
||||
|
||||
[JsonPropertyName("hostKeyPins")]
|
||||
public IReadOnlyDictionary<string, string>? HostKeyPins { get; init; }
|
||||
|
||||
[JsonPropertyName("deviceID")]
|
||||
public string? DeviceId { get; init; }
|
||||
|
||||
[JsonPropertyName("deviceLabel")]
|
||||
public string DeviceLabel { get; init; } = "Nucleic for Windows";
|
||||
|
||||
[JsonPropertyName("scopeClaim")]
|
||||
public string ScopeClaim { get; init; } = "control";
|
||||
|
||||
[JsonPropertyName("channel")]
|
||||
public string? Channel { get; init; }
|
||||
}
|
||||
|
||||
/// The owner-only `run/hostd.json` written by nucleic-hostd.
|
||||
public sealed record HostdRendezvous
|
||||
{
|
||||
[JsonPropertyName("pid")]
|
||||
public required int ProcessId { get; init; }
|
||||
|
||||
[JsonPropertyName("host")]
|
||||
public string Host { get; init; } = "127.0.0.1";
|
||||
|
||||
[JsonPropertyName("port")]
|
||||
public required int Port { get; init; }
|
||||
|
||||
[JsonPropertyName("localPSK")]
|
||||
public required string LocalPsk { get; init; }
|
||||
|
||||
/// Selects the DLL's persisted pin on renderer relaunch. A fresh identity has no matching
|
||||
/// pin and falls back to the one-time local PSK.
|
||||
[JsonPropertyName("hostID")]
|
||||
public required string HostId { get; init; }
|
||||
|
||||
public void Validate()
|
||||
{
|
||||
if (ProcessId <= 0) throw new JsonException("rendezvous pid must be positive");
|
||||
if (Host is not ("127.0.0.1" or "::1" or "localhost"))
|
||||
throw new JsonException("renderer rendezvous must be loopback-only");
|
||||
if (Port is <= 0 or > 65_535) throw new JsonException("rendezvous port is invalid");
|
||||
if (string.IsNullOrWhiteSpace(HostId)) throw new JsonException("rendezvous hostID is missing");
|
||||
byte[] secret;
|
||||
try { secret = Convert.FromBase64String(LocalPsk); }
|
||||
catch (FormatException error) { throw new JsonException("rendezvous localPSK is invalid", error); }
|
||||
if (secret.Length != 32) throw new JsonException("rendezvous localPSK must be 32 bytes");
|
||||
}
|
||||
}
|
||||
|
||||
/// A forward-compatible HostMsg view. The tagged enum remains owned by Swift; C# reads the
|
||||
/// discriminator and keeps the complete JSON value so a newer field or message never gets lost.
|
||||
public sealed record ProtocolMessage(string Tag, JsonElement Root, string Json)
|
||||
{
|
||||
public static ProtocolMessage Parse(string json)
|
||||
{
|
||||
using var document = JsonDocument.Parse(json);
|
||||
if (document.RootElement.ValueKind != JsonValueKind.Object ||
|
||||
!document.RootElement.TryGetProperty("t", out var tagValue) ||
|
||||
tagValue.ValueKind != JsonValueKind.String)
|
||||
{
|
||||
throw new JsonException("protocol message has no string 't' discriminator");
|
||||
}
|
||||
return new ProtocolMessage(tagValue.GetString()!, document.RootElement.Clone(), json);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record ProtocolState(
|
||||
string State, string? Message, string? Transport, string? Peer,
|
||||
string? HostId, JsonElement? Welcome, string Json)
|
||||
{
|
||||
public static ProtocolState Parse(string json)
|
||||
{
|
||||
using var document = JsonDocument.Parse(json);
|
||||
var root = document.RootElement;
|
||||
if (!root.TryGetProperty("state", out var state) || state.ValueKind != JsonValueKind.String)
|
||||
throw new JsonException("protocol state callback has no state");
|
||||
return new ProtocolState(
|
||||
state.GetString()!, OptionalString(root, "message"),
|
||||
OptionalString(root, "transport"), OptionalString(root, "peer"),
|
||||
OptionalString(root, "hostID"),
|
||||
root.TryGetProperty("welcome", out var welcome) ? welcome.Clone() : null,
|
||||
json);
|
||||
}
|
||||
|
||||
private static string? OptionalString(JsonElement root, string name) =>
|
||||
root.TryGetProperty(name, out var value) && value.ValueKind == JsonValueKind.String
|
||||
? value.GetString() : null;
|
||||
}
|
||||
|
||||
public sealed record ProjectionDiff(JsonElement Root, string Json)
|
||||
{
|
||||
public static ProjectionDiff Parse(string json)
|
||||
{
|
||||
using var document = JsonDocument.Parse(json);
|
||||
var root = document.RootElement;
|
||||
if (root.ValueKind != JsonValueKind.Object ||
|
||||
!root.TryGetProperty("version", out var version) || version.GetInt32() != 1 ||
|
||||
!root.TryGetProperty("splices", out var splices) ||
|
||||
splices.ValueKind != JsonValueKind.Array)
|
||||
{
|
||||
throw new JsonException("invalid transcript projection result");
|
||||
}
|
||||
return new ProjectionDiff(root.Clone(), json);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed record TagOnlyIntent([property: JsonPropertyName("t")] string Tag);
|
||||
|
||||
[JsonSourceGenerationOptions(
|
||||
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
[JsonSerializable(typeof(ProtocolClientConfiguration))]
|
||||
[JsonSerializable(typeof(HostdRendezvous))]
|
||||
[JsonSerializable(typeof(TagOnlyIntent))]
|
||||
internal partial class ProtocolJsonContext : JsonSerializerContext;
|
||||
|
||||
public static class ProtocolJson
|
||||
{
|
||||
public static HostdRendezvous DeserializeRendezvous(string json)
|
||||
{
|
||||
var value = JsonSerializer.Deserialize(json, ProtocolJsonContext.Default.HostdRendezvous)
|
||||
?? throw new JsonException("empty hostd rendezvous");
|
||||
value.Validate();
|
||||
return value;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user