Add Authority Advisory AI and API Lifecycle Configuration

- Introduced AuthorityAdvisoryAiOptions and related classes for managing advisory AI configurations, including remote inference options and tenant-specific settings.
- Added AuthorityApiLifecycleOptions to control API lifecycle settings, including legacy OAuth endpoint configurations.
- Implemented validation and normalization methods for both advisory AI and API lifecycle options to ensure proper configuration.
- Created AuthorityNotificationsOptions and its related classes for managing notification settings, including ack tokens, webhooks, and escalation options.
- Developed IssuerDirectoryClient and related models for interacting with the issuer directory service, including caching mechanisms and HTTP client configurations.
- Added support for dependency injection through ServiceCollectionExtensions for the Issuer Directory Client.
- Updated project file to include necessary package references for the new Issuer Directory Client library.
This commit is contained in:
master
2025-11-02 13:40:38 +02:00
parent 66cb6c4b8a
commit f98cea3bcf
516 changed files with 68157 additions and 24754 deletions

View File

@@ -0,0 +1,120 @@
using System;
using System.Globalization;
using System.Net.Http;
using System.Net.Http.Json;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace StellaOps.IssuerDirectory.Client;
internal sealed class IssuerDirectoryClient : IIssuerDirectoryClient
{
private readonly HttpClient _httpClient;
private readonly IMemoryCache _cache;
private readonly IssuerDirectoryClientOptions _options;
private readonly ILogger<IssuerDirectoryClient> _logger;
public IssuerDirectoryClient(
HttpClient httpClient,
IMemoryCache cache,
IOptions<IssuerDirectoryClientOptions> options,
ILogger<IssuerDirectoryClient> logger)
{
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
_cache = cache ?? throw new ArgumentNullException(nameof(cache));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
ArgumentNullException.ThrowIfNull(options);
_options = options.Value;
_options.Validate();
}
public async ValueTask<IReadOnlyList<IssuerKeyModel>> GetIssuerKeysAsync(
string tenantId,
string issuerId,
bool includeGlobal,
CancellationToken cancellationToken)
{
ArgumentException.ThrowIfNullOrWhiteSpace(tenantId);
ArgumentException.ThrowIfNullOrWhiteSpace(issuerId);
var cacheKey = CacheKey("keys", tenantId, issuerId, includeGlobal.ToString(CultureInfo.InvariantCulture));
if (_cache.TryGetValue(cacheKey, out IReadOnlyList<IssuerKeyModel>? cached) && cached is not null)
{
return cached;
}
var requestUri = $"issuer-directory/issuers/{Uri.EscapeDataString(issuerId)}/keys?includeGlobal={includeGlobal.ToString().ToLowerInvariant()}";
using var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
request.Headers.TryAddWithoutValidation(_options.TenantHeader, tenantId);
using var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
{
_logger.LogWarning(
"Issuer Directory key lookup failed for {IssuerId} (tenant={TenantId}) {StatusCode}",
issuerId,
tenantId,
response.StatusCode);
response.EnsureSuccessStatusCode();
}
var payload = await response.Content.ReadFromJsonAsync<List<IssuerKeyModel>>(cancellationToken: cancellationToken)
.ConfigureAwait(false);
IReadOnlyList<IssuerKeyModel> result = payload?.ToArray() ?? Array.Empty<IssuerKeyModel>();
_cache.Set(cacheKey, result, _options.Cache.Keys);
return result;
}
public async ValueTask<IssuerTrustResponseModel> GetIssuerTrustAsync(
string tenantId,
string issuerId,
bool includeGlobal,
CancellationToken cancellationToken)
{
ArgumentException.ThrowIfNullOrWhiteSpace(tenantId);
ArgumentException.ThrowIfNullOrWhiteSpace(issuerId);
var cacheKey = CacheKey("trust", tenantId, issuerId, includeGlobal.ToString(CultureInfo.InvariantCulture));
if (_cache.TryGetValue(cacheKey, out IssuerTrustResponseModel? cached) && cached is not null)
{
return cached;
}
var requestUri = $"issuer-directory/issuers/{Uri.EscapeDataString(issuerId)}/trust?includeGlobal={includeGlobal.ToString().ToLowerInvariant()}";
using var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
request.Headers.TryAddWithoutValidation(_options.TenantHeader, tenantId);
using var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
{
_logger.LogWarning(
"Issuer Directory trust lookup failed for {IssuerId} (tenant={TenantId}) {StatusCode}",
issuerId,
tenantId,
response.StatusCode);
response.EnsureSuccessStatusCode();
}
var payload = await response.Content.ReadFromJsonAsync<IssuerTrustResponseModel>(cancellationToken: cancellationToken)
.ConfigureAwait(false) ?? new IssuerTrustResponseModel(null, null, 0m);
_cache.Set(cacheKey, payload, _options.Cache.Trust);
return payload;
}
private static string CacheKey(string prefix, params string[] parts)
{
if (parts is null || parts.Length == 0)
{
return prefix;
}
var segments = new string[1 + parts.Length];
segments[0] = prefix;
Array.Copy(parts, 0, segments, 1, parts.Length);
return string.Join('|', segments);
}
}