using System.Collections.Concurrent; namespace MoShiro.Backend.Application; /// 节点的一个属主:它报上来的 passkey 凭证 id,以及面板上对应的 passkey 名称和账户(对不上任何已注册的 passkey 时为空)。 public sealed record NodeOwner(string CredentialId, string? PasskeyName, string? Username); /// /// 节点的属主:Agent 和 Probe 握手时报上来的、安装时固定在节点本地的 passkey 凭证 id。 /// 属主的强制在节点本地(没有属主的节点不运行,见 OwnerGuard),面板这边只是顺着凭证 id 反查出它绑定在哪个账户的哪把 passkey 上—— /// 以后给节点更多的接口和操作(extension 会话、加密内容)时,据此判断谁有资格用。上报的只是自述,真正的授权由节点自己对 passkey 签名的校验把关。 /// 在内存里:节点每次连上来都会重新报。 /// public sealed class NodeOwnerService { private const int MaxOwners = 16; private const int MaxCredentialIdLength = 256; private readonly ConcurrentDictionary _reported = new(StringComparer.Ordinal); private readonly IAuthStore _auth; public NodeOwnerService(IAuthStore auth) { _auth = auth; } public void Report(string nodeId, IReadOnlyList? credentialIds) { string[] clean = credentialIds is null ? [] : [.. credentialIds.Where(id => id.Length is > 0 and <= MaxCredentialIdLength).Distinct(StringComparer.Ordinal).Take(MaxOwners)]; if (clean.Length == 0) { _reported.TryRemove(nodeId, out _); } else { _reported[nodeId] = clean; } } public void Forget(string nodeId) => _reported.TryRemove(nodeId, out _); /// 每个节点的属主(只含报过的节点)。 public async ValueTask>> ListAsync(CancellationToken ct) { var result = new Dictionary>(StringComparer.Ordinal); var users = new Dictionary(StringComparer.Ordinal); foreach ((string nodeId, string[] ids) in _reported) { var owners = new List(); foreach (string id in ids) { PasskeyRecord? passkey = await _auth.FindPasskeyAsync(id, ct); string? username = null; if (passkey is not null && !users.TryGetValue(passkey.UserId, out username)) { username = (await _auth.GetUserAsync(passkey.UserId, ct))?.Username; users[passkey.UserId] = username; } owners.Add(new NodeOwner(id, passkey?.Name, username)); } result[nodeId] = owners; } return result; } }