Fix UI & Rotate Salt

This commit is contained in:
2025-09-19 22:33:40 +02:00
parent 1755b5cb54
commit 612e7c88a2
7 changed files with 92 additions and 35 deletions

View File

@@ -258,7 +258,7 @@ public class NearbyDiscoveryService : IHostedService, IMediatorSubscriber
if (entries.Count != _lastLocalCount) if (entries.Count != _lastLocalCount)
{ {
_lastLocalCount = entries.Count; _lastLocalCount = entries.Count;
_logger.LogInformation("Nearby: {count} players detected locally", _lastLocalCount); _logger.LogTrace("Nearby: {count} players detected locally", _lastLocalCount);
} }
// Try query server if config and endpoints are present // Try query server if config and endpoints are present
@@ -291,7 +291,7 @@ public class NearbyDiscoveryService : IHostedService, IMediatorSubscriber
return $"{e.Name}({e.WorldId})->{shortH}"; return $"{e.Name}({e.WorldId})->{shortH}";
}); });
var saltShort = saltHex.Length > 8 ? saltHex[..8] : saltHex; var saltShort = saltHex.Length > 8 ? saltHex[..8] : saltHex;
_logger.LogInformation("Nearby snapshot: {count} entries; salt={saltShort}…; samples=[{samples}]", _logger.LogTrace("Nearby snapshot: {count} entries; salt={saltShort}…; samples=[{samples}]",
entries.Count, saltShort, string.Join(", ", sample)); entries.Count, saltShort, string.Join(", ", sample));
} }
} }
@@ -310,7 +310,7 @@ public class NearbyDiscoveryService : IHostedService, IMediatorSubscriber
ushort meWorld = 0; ushort meWorld = 0;
if (me is Dalamud.Game.ClientState.Objects.SubKinds.IPlayerCharacter mePc) if (me is Dalamud.Game.ClientState.Objects.SubKinds.IPlayerCharacter mePc)
meWorld = (ushort)mePc.HomeWorld.RowId; meWorld = (ushort)mePc.HomeWorld.RowId;
_logger.LogInformation("Nearby self ident: {name} ({world})", displayName, meWorld); _logger.LogTrace("Nearby self ident: {name} ({world})", displayName, meWorld);
selfHash = (saltHex + displayName + meWorld.ToString()).GetHash256(); selfHash = (saltHex + displayName + meWorld.ToString()).GetHash256();
} }
} }
@@ -380,14 +380,31 @@ public class NearbyDiscoveryService : IHostedService, IMediatorSubscriber
} }
} }
} }
_logger.LogInformation("Nearby: server returned {count} matches", allMatches.Count); if (allMatches.Count > 0)
{
_logger.LogInformation("Nearby: server returned {count} matches", allMatches.Count);
}
else
{
_logger.LogTrace("Nearby: server returned {count} matches", allMatches.Count);
}
// Log change in number of Umbra matches // Log change in number of Umbra matches
int matchCount = entries.Count(e => e.IsMatch); int matchCount = entries.Count(e => e.IsMatch);
if (matchCount != _lastMatchCount) if (matchCount != _lastMatchCount)
{ {
_lastMatchCount = matchCount; _lastMatchCount = matchCount;
_logger.LogDebug("Nearby: {count} Umbra users nearby", matchCount); if (matchCount > 0)
{
var matchSamples = entries.Where(e => e.IsMatch).Take(5)
.Select(e => string.IsNullOrEmpty(e.DisplayName) ? e.Name : e.DisplayName!);
_logger.LogInformation("Nearby: {count} Umbra users nearby [{samples}]",
matchCount, string.Join(", ", matchSamples));
}
else
{
_logger.LogTrace("Nearby: {count} Umbra users nearby", matchCount);
}
} }
} }
} }

View File

@@ -12,6 +12,7 @@ using MareSynchronos.PlayerData.Pairs;
using MareSynchronos.Services; using MareSynchronos.Services;
using MareSynchronos.Services.Mediator; using MareSynchronos.Services.Mediator;
using MareSynchronos.Services.ServerConfiguration; using MareSynchronos.Services.ServerConfiguration;
using MareSynchronos.Services.AutoDetect;
using MareSynchronos.UI.Components; using MareSynchronos.UI.Components;
using MareSynchronos.UI.Handlers; using MareSynchronos.UI.Handlers;
using MareSynchronos.WebAPI; using MareSynchronos.WebAPI;
@@ -44,7 +45,8 @@ public class CompactUi : WindowMediatorSubscriberBase
private readonly ServerConfigurationManager _serverManager; private readonly ServerConfigurationManager _serverManager;
private readonly Stopwatch _timeout = new(); private readonly Stopwatch _timeout = new();
private readonly CharaDataManager _charaDataManager; private readonly CharaDataManager _charaDataManager;
private readonly Services.AutoDetect.NearbyPendingService _nearbyPending; private readonly NearbyPendingService _nearbyPending;
private readonly AutoDetectRequestService _autoDetectRequestService;
private readonly UidDisplayHandler _uidDisplayHandler; private readonly UidDisplayHandler _uidDisplayHandler;
private readonly UiSharedService _uiSharedService; private readonly UiSharedService _uiSharedService;
private bool _buttonState; private bool _buttonState;
@@ -63,7 +65,8 @@ public class CompactUi : WindowMediatorSubscriberBase
public CompactUi(ILogger<CompactUi> logger, UiSharedService uiShared, MareConfigService configService, ApiController apiController, PairManager pairManager, ChatService chatService, public CompactUi(ILogger<CompactUi> logger, UiSharedService uiShared, MareConfigService configService, ApiController apiController, PairManager pairManager, ChatService chatService,
ServerConfigurationManager serverManager, MareMediator mediator, FileUploadManager fileTransferManager, UidDisplayHandler uidDisplayHandler, CharaDataManager charaDataManager, ServerConfigurationManager serverManager, MareMediator mediator, FileUploadManager fileTransferManager, UidDisplayHandler uidDisplayHandler, CharaDataManager charaDataManager,
Services.AutoDetect.NearbyPendingService nearbyPendingService, NearbyPendingService nearbyPendingService,
AutoDetectRequestService autoDetectRequestService,
PerformanceCollectorService performanceCollectorService) PerformanceCollectorService performanceCollectorService)
: base(logger, mediator, "###UmbraSyncMainUI", performanceCollectorService) : base(logger, mediator, "###UmbraSyncMainUI", performanceCollectorService)
{ {
@@ -76,6 +79,7 @@ public class CompactUi : WindowMediatorSubscriberBase
_uidDisplayHandler = uidDisplayHandler; _uidDisplayHandler = uidDisplayHandler;
_charaDataManager = charaDataManager; _charaDataManager = charaDataManager;
_nearbyPending = nearbyPendingService; _nearbyPending = nearbyPendingService;
_autoDetectRequestService = autoDetectRequestService;
var tagHandler = new TagHandler(_serverManager); var tagHandler = new TagHandler(_serverManager);
_groupPanel = new(this, uiShared, _pairManager, chatService, uidDisplayHandler, _configService, _serverManager, _charaDataManager); _groupPanel = new(this, uiShared, _pairManager, chatService, uidDisplayHandler, _configService, _serverManager, _charaDataManager);
@@ -428,20 +432,31 @@ public class CompactUi : WindowMediatorSubscriberBase
isPaired = _pairManager.DirectPairs.Any(p => string.Equals(p.UserData.AliasOrUID, key, StringComparison.OrdinalIgnoreCase)); isPaired = _pairManager.DirectPairs.Any(p => string.Equals(p.UserData.AliasOrUID, key, StringComparison.OrdinalIgnoreCase));
} }
var statusText = isPaired ? "✔ Paired" : (e.AcceptPairRequests ? " Invite" : "⛔ Requests disabled"); var statusButtonSize = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.UserPlus);
var statusSize = ImGui.CalcTextSize(statusText); ImGui.SetCursorPosX(right - statusButtonSize.X);
ImGui.SetCursorPosX(right - statusSize.X);
if (isPaired || !e.AcceptPairRequests) if (isPaired)
{ {
ImGui.TextUnformatted(statusText); _uiSharedService.IconText(FontAwesomeIcon.Check, ImGuiColors.ParsedGreen);
UiSharedService.AttachToolTip("Déjà apparié sur Umbra");
}
else if (!e.AcceptPairRequests)
{
_uiSharedService.IconText(FontAwesomeIcon.Ban, ImGuiColors.DalamudGrey3);
UiSharedService.AttachToolTip("Les demandes sont désactivées pour ce joueur");
}
else if (!string.IsNullOrEmpty(e.Token))
{
if (_uiSharedService.IconButton(FontAwesomeIcon.UserPlus))
{
_ = _autoDetectRequestService.SendRequestAsync(e.Token!);
}
UiSharedService.AttachToolTip("Envoyer une invitation Umbra");
} }
else else
{ {
if (_uiSharedService.IconTextButton(FontAwesomeIcon.UserPlus, "Invite", statusSize.X)) _uiSharedService.IconText(FontAwesomeIcon.QuestionCircle, ImGuiColors.DalamudGrey3);
{ UiSharedService.AttachToolTip("Impossible d'inviter ce joueur");
Mediator.Publish(new UiToggleMessage(typeof(AutoDetectUi)));
}
} }
} }
} }

View File

@@ -16,6 +16,7 @@ namespace MareSynchronos.UI.Components;
public class DrawUserPair : DrawPairBase public class DrawUserPair : DrawPairBase
{ {
private static readonly Vector4 Violet = new(0.63f, 0.25f, 1f, 1f);
protected readonly MareMediator _mediator; protected readonly MareMediator _mediator;
private readonly SelectGroupForPairUi _selectGroupForPairUi; private readonly SelectGroupForPairUi _selectGroupForPairUi;
private readonly CharaDataManager _charaDataManager; private readonly CharaDataManager _charaDataManager;
@@ -39,12 +40,11 @@ public class DrawUserPair : DrawPairBase
protected override void DrawLeftSide(float textPosY, float originalY) protected override void DrawLeftSide(float textPosY, float originalY)
{ {
var online = _pair.IsOnline; var online = _pair.IsOnline;
var violet = new Vector4(0.69f, 0.27f, 0.93f, 1f);
var offlineGrey = ImGuiColors.DalamudGrey3; var offlineGrey = ImGuiColors.DalamudGrey3;
ImGui.SetCursorPosY(textPosY); ImGui.SetCursorPosY(textPosY);
ImGui.PushFont(UiBuilder.IconFont); ImGui.PushFont(UiBuilder.IconFont);
UiSharedService.ColorText(FontAwesomeIcon.Moon.ToIconString(), online ? violet : offlineGrey); UiSharedService.ColorText(FontAwesomeIcon.Moon.ToIconString(), online ? Violet : offlineGrey);
ImGui.PopFont(); ImGui.PopFont();
UiSharedService.AttachToolTip(online UiSharedService.AttachToolTip(online
? "User is online" ? "User is online"
@@ -72,7 +72,7 @@ public class DrawUserPair : DrawPairBase
ImGui.SameLine(); ImGui.SameLine();
ImGui.SetCursorPosY(textPosY); ImGui.SetCursorPosY(textPosY);
ImGui.PushFont(UiBuilder.IconFont); ImGui.PushFont(UiBuilder.IconFont);
UiSharedService.ColorText(FontAwesomeIcon.Eye.ToIconString(), ImGuiColors.ParsedGreen); UiSharedService.ColorText(FontAwesomeIcon.Eye.ToIconString(), Violet);
if (ImGui.IsItemClicked()) if (ImGui.IsItemClicked())
{ {
_mediator.Publish(new TargetPairMessage(_pair)); _mediator.Publish(new TargetPairMessage(_pair));

View File

@@ -52,6 +52,7 @@ internal sealed class GroupPanel
private bool _showModalChangePassword; private bool _showModalChangePassword;
private bool _showModalCreateGroup; private bool _showModalCreateGroup;
private bool _showModalEnterPassword; private bool _showModalEnterPassword;
private string _newSyncShellAlias = string.Empty;
private string _syncShellPassword = string.Empty; private string _syncShellPassword = string.Empty;
private string _syncShellToJoin = string.Empty; private string _syncShellToJoin = string.Empty;
@@ -82,7 +83,7 @@ internal sealed class GroupPanel
{ {
var buttonSize = _uiShared.GetIconButtonSize(FontAwesomeIcon.Plus); var buttonSize = _uiShared.GetIconButtonSize(FontAwesomeIcon.Plus);
ImGui.SetNextItemWidth(UiSharedService.GetWindowContentRegionWidth() - ImGui.GetWindowContentRegionMin().X - buttonSize.X); ImGui.SetNextItemWidth(UiSharedService.GetWindowContentRegionWidth() - ImGui.GetWindowContentRegionMin().X - buttonSize.X);
ImGui.InputTextWithHint("##syncshellid", "Syncshell GID/Alias (leave empty to create)", ref _syncShellToJoin, 20); ImGui.InputTextWithHint("##syncshellid", "Syncshell GID/Alias (leave empty to create)", ref _syncShellToJoin, 50);
ImGui.SameLine(ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth() - buttonSize.X); ImGui.SameLine(ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth() - buttonSize.X);
bool userCanJoinMoreGroups = _pairManager.GroupPairs.Count < ApiController.ServerInfo.MaxGroupsJoinedByUser; bool userCanJoinMoreGroups = _pairManager.GroupPairs.Count < ApiController.ServerInfo.MaxGroupsJoinedByUser;
@@ -108,6 +109,7 @@ internal sealed class GroupPanel
{ {
_lastCreatedGroup = null; _lastCreatedGroup = null;
_errorGroupCreate = false; _errorGroupCreate = false;
_newSyncShellAlias = string.Empty;
_showModalCreateGroup = true; _showModalCreateGroup = true;
ImGui.OpenPopup("Create Syncshell"); ImGui.OpenPopup("Create Syncshell");
} }
@@ -150,13 +152,21 @@ internal sealed class GroupPanel
if (ImGui.BeginPopupModal("Create Syncshell", ref _showModalCreateGroup, UiSharedService.PopupWindowFlags)) if (ImGui.BeginPopupModal("Create Syncshell", ref _showModalCreateGroup, UiSharedService.PopupWindowFlags))
{ {
UiSharedService.TextWrapped("Press the button below to create a new Syncshell."); UiSharedService.TextWrapped("Donnez un nom à votre Syncshell (optionnel) puis créez-la. Le préfixe 'UMB-' reste inchangé.");
ImGui.SetNextItemWidth(-1);
ImGui.InputTextWithHint("##syncshellalias", "Nom du Syncshell", ref _newSyncShellAlias, 50);
UiSharedService.TextWrapped("Appuyez sur le bouton ci-dessous pour créer une nouvelle Syncshell.");
ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale); ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale);
if (ImGui.Button("Create Syncshell")) if (ImGui.Button("Create Syncshell"))
{ {
try try
{ {
_lastCreatedGroup = ApiController.GroupCreate().Result; var aliasInput = string.IsNullOrWhiteSpace(_newSyncShellAlias) ? null : _newSyncShellAlias.Trim();
_lastCreatedGroup = ApiController.GroupCreate(aliasInput).Result;
if (_lastCreatedGroup != null)
{
_newSyncShellAlias = string.Empty;
}
} }
catch catch
{ {
@@ -169,6 +179,10 @@ internal sealed class GroupPanel
{ {
ImGui.Separator(); ImGui.Separator();
_errorGroupCreate = false; _errorGroupCreate = false;
if (!string.IsNullOrWhiteSpace(_lastCreatedGroup.Group.Alias))
{
ImGui.TextUnformatted("Syncshell Name: " + _lastCreatedGroup.Group.Alias);
}
ImGui.TextUnformatted("Syncshell ID: " + _lastCreatedGroup.Group.GID); ImGui.TextUnformatted("Syncshell ID: " + _lastCreatedGroup.Group.GID);
ImGui.AlignTextToFramePadding(); ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("Syncshell Password: " + _lastCreatedGroup.Password); ImGui.TextUnformatted("Syncshell Password: " + _lastCreatedGroup.Password);

View File

@@ -29,24 +29,25 @@ public class DiscoveryApiClient
{ {
var token = await _tokenProvider.GetOrUpdateToken(ct).ConfigureAwait(false); var token = await _tokenProvider.GetOrUpdateToken(ct).ConfigureAwait(false);
if (string.IsNullOrEmpty(token)) return []; if (string.IsNullOrEmpty(token)) return [];
var distinctHashes = hashes.Distinct(StringComparer.Ordinal).ToArray();
using var req = new HttpRequestMessage(HttpMethod.Post, endpoint); using var req = new HttpRequestMessage(HttpMethod.Post, endpoint);
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
var body = JsonSerializer.Serialize(new var body = JsonSerializer.Serialize(new
{ {
hashes = hashes.Distinct(StringComparer.Ordinal).ToArray(), hashes = distinctHashes,
salt = _configProvider.SaltB64 salt = _configProvider.SaltB64
}); });
req.Content = new StringContent(body, Encoding.UTF8, "application/json"); req.Content = new StringContent(body, Encoding.UTF8, "application/json");
var resp = await _httpClient.SendAsync(req, ct).ConfigureAwait(false); var resp = await _httpClient.SendAsync(req, ct).ConfigureAwait(false);
if (resp.StatusCode == System.Net.HttpStatusCode.Unauthorized) if (resp.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{ {
var token2 = await _tokenProvider.GetOrUpdateToken(ct).ConfigureAwait(false); var token2 = await _tokenProvider.ForceRefreshToken(ct).ConfigureAwait(false);
if (string.IsNullOrEmpty(token2)) return []; if (string.IsNullOrEmpty(token2)) return [];
using var req2 = new HttpRequestMessage(HttpMethod.Post, endpoint); using var req2 = new HttpRequestMessage(HttpMethod.Post, endpoint);
req2.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token2); req2.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token2);
var body2 = JsonSerializer.Serialize(new var body2 = JsonSerializer.Serialize(new
{ {
hashes = hashes.Distinct(StringComparer.Ordinal).ToArray(), hashes = distinctHashes,
salt = _configProvider.SaltB64 salt = _configProvider.SaltB64
}); });
req2.Content = new StringContent(body2, Encoding.UTF8, "application/json"); req2.Content = new StringContent(body2, Encoding.UTF8, "application/json");
@@ -77,7 +78,7 @@ public class DiscoveryApiClient
var resp = await _httpClient.SendAsync(req, ct).ConfigureAwait(false); var resp = await _httpClient.SendAsync(req, ct).ConfigureAwait(false);
if (resp.StatusCode == System.Net.HttpStatusCode.Unauthorized) if (resp.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{ {
var jwt2 = await _tokenProvider.GetOrUpdateToken(ct).ConfigureAwait(false); var jwt2 = await _tokenProvider.ForceRefreshToken(ct).ConfigureAwait(false);
if (string.IsNullOrEmpty(jwt2)) return false; if (string.IsNullOrEmpty(jwt2)) return false;
using var req2 = new HttpRequestMessage(HttpMethod.Post, endpoint); using var req2 = new HttpRequestMessage(HttpMethod.Post, endpoint);
req2.Headers.Authorization = new AuthenticationHeaderValue("Bearer", jwt2); req2.Headers.Authorization = new AuthenticationHeaderValue("Bearer", jwt2);
@@ -121,7 +122,7 @@ public class DiscoveryApiClient
var resp = await _httpClient.SendAsync(req, ct).ConfigureAwait(false); var resp = await _httpClient.SendAsync(req, ct).ConfigureAwait(false);
if (resp.StatusCode == System.Net.HttpStatusCode.Unauthorized) if (resp.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{ {
var jwt2 = await _tokenProvider.GetOrUpdateToken(ct).ConfigureAwait(false); var jwt2 = await _tokenProvider.ForceRefreshToken(ct).ConfigureAwait(false);
if (string.IsNullOrEmpty(jwt2)) return false; if (string.IsNullOrEmpty(jwt2)) return false;
using var req2 = new HttpRequestMessage(HttpMethod.Post, endpoint); using var req2 = new HttpRequestMessage(HttpMethod.Post, endpoint);
req2.Headers.Authorization = new AuthenticationHeaderValue("Bearer", jwt2); req2.Headers.Authorization = new AuthenticationHeaderValue("Bearer", jwt2);
@@ -152,7 +153,7 @@ public class DiscoveryApiClient
var resp = await _httpClient.SendAsync(req, ct).ConfigureAwait(false); var resp = await _httpClient.SendAsync(req, ct).ConfigureAwait(false);
if (resp.StatusCode == System.Net.HttpStatusCode.Unauthorized) if (resp.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{ {
var jwt2 = await _tokenProvider.GetOrUpdateToken(ct).ConfigureAwait(false); var jwt2 = await _tokenProvider.ForceRefreshToken(ct).ConfigureAwait(false);
if (string.IsNullOrEmpty(jwt2)) return false; if (string.IsNullOrEmpty(jwt2)) return false;
using var req2 = new HttpRequestMessage(HttpMethod.Post, endpoint); using var req2 = new HttpRequestMessage(HttpMethod.Post, endpoint);
req2.Headers.Authorization = new AuthenticationHeaderValue("Bearer", jwt2); req2.Headers.Authorization = new AuthenticationHeaderValue("Bearer", jwt2);
@@ -179,7 +180,7 @@ public class DiscoveryApiClient
var resp = await _httpClient.SendAsync(req, ct).ConfigureAwait(false); var resp = await _httpClient.SendAsync(req, ct).ConfigureAwait(false);
if (resp.StatusCode == System.Net.HttpStatusCode.Unauthorized) if (resp.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{ {
var jwt2 = await _tokenProvider.GetOrUpdateToken(ct).ConfigureAwait(false); var jwt2 = await _tokenProvider.ForceRefreshToken(ct).ConfigureAwait(false);
if (string.IsNullOrEmpty(jwt2)) return; if (string.IsNullOrEmpty(jwt2)) return;
using var req2 = new HttpRequestMessage(HttpMethod.Post, endpoint); using var req2 = new HttpRequestMessage(HttpMethod.Post, endpoint);
req2.Headers.Authorization = new AuthenticationHeaderValue("Bearer", jwt2); req2.Headers.Authorization = new AuthenticationHeaderValue("Bearer", jwt2);

View File

@@ -49,10 +49,10 @@ public partial class ApiController
await _mareHub!.SendAsync(nameof(GroupClear), group).ConfigureAwait(false); await _mareHub!.SendAsync(nameof(GroupClear), group).ConfigureAwait(false);
} }
public async Task<GroupPasswordDto> GroupCreate() public async Task<GroupPasswordDto> GroupCreate(string? alias = null)
{ {
CheckConnection(); CheckConnection();
return await _mareHub!.InvokeAsync<GroupPasswordDto>(nameof(GroupCreate)).ConfigureAwait(false); return await _mareHub!.InvokeAsync<GroupPasswordDto>(nameof(GroupCreate), string.IsNullOrWhiteSpace(alias) ? null : alias.Trim()).ConfigureAwait(false);
} }
public async Task<List<string>> GroupCreateTempInvite(GroupDto group, int amount) public async Task<List<string>> GroupCreateTempInvite(GroupDto group, int amount)

View File

@@ -172,6 +172,16 @@ public sealed class TokenProvider : IDisposable, IMediatorSubscriber
return await GetNewToken(jwtIdentifier, ct).ConfigureAwait(false); return await GetNewToken(jwtIdentifier, ct).ConfigureAwait(false);
} }
public async Task<string?> ForceRefreshToken(CancellationToken ct)
{
JwtIdentifier? jwtIdentifier = await GetIdentifier().ConfigureAwait(false);
if (jwtIdentifier == null) return null;
_tokenCache.TryRemove(jwtIdentifier, out _);
_logger.LogTrace("ForceRefresh: Getting new token");
return await GetNewToken(jwtIdentifier, ct).ConfigureAwait(false);
}
public string? GetStapledWellKnown(string apiUrl) public string? GetStapledWellKnown(string apiUrl)
{ {
_wellKnownCache.TryGetValue(apiUrl, out var wellKnown); _wellKnownCache.TryGetValue(apiUrl, out var wellKnown);