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,16 @@
namespace StellaOps.IssuerDirectory.Client;
public interface IIssuerDirectoryClient
{
ValueTask<IReadOnlyList<IssuerKeyModel>> GetIssuerKeysAsync(
string tenantId,
string issuerId,
bool includeGlobal,
CancellationToken cancellationToken);
ValueTask<IssuerTrustResponseModel> GetIssuerTrustAsync(
string tenantId,
string issuerId,
bool includeGlobal,
CancellationToken cancellationToken);
}

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);
}
}

View File

@@ -0,0 +1,44 @@
namespace StellaOps.IssuerDirectory.Client;
public sealed class IssuerDirectoryClientOptions
{
public const string SectionName = "IssuerDirectory:Client";
public Uri? BaseAddress { get; set; }
public TimeSpan HttpTimeout { get; set; } = TimeSpan.FromSeconds(10);
public string TenantHeader { get; set; } = "X-StellaOps-Tenant";
public IssuerDirectoryCacheOptions Cache { get; set; } = new();
internal void Validate()
{
if (BaseAddress is null)
{
throw new InvalidOperationException("IssuerDirectory client base address must be configured.");
}
if (!BaseAddress.IsAbsoluteUri)
{
throw new InvalidOperationException("IssuerDirectory client base address must be absolute.");
}
if (HttpTimeout <= TimeSpan.Zero)
{
throw new InvalidOperationException("IssuerDirectory client timeout must be positive.");
}
if (string.IsNullOrWhiteSpace(TenantHeader))
{
throw new InvalidOperationException("IssuerDirectory tenant header must be configured.");
}
}
}
public sealed class IssuerDirectoryCacheOptions
{
public TimeSpan Keys { get; set; } = TimeSpan.FromMinutes(5);
public TimeSpan Trust { get; set; } = TimeSpan.FromMinutes(5);
}

View File

@@ -0,0 +1,30 @@
using System.Text.Json.Serialization;
namespace StellaOps.IssuerDirectory.Client;
public sealed record IssuerKeyModel(
[property: JsonPropertyName("id")] string Id,
[property: JsonPropertyName("issuerId")] string IssuerId,
[property: JsonPropertyName("tenantId")] string TenantId,
[property: JsonPropertyName("type")] string Type,
[property: JsonPropertyName("status")] string Status,
[property: JsonPropertyName("materialFormat")] string MaterialFormat,
[property: JsonPropertyName("materialValue")] string MaterialValue,
[property: JsonPropertyName("fingerprint")] string Fingerprint,
[property: JsonPropertyName("expiresAtUtc")] DateTimeOffset? ExpiresAtUtc,
[property: JsonPropertyName("retiredAtUtc")] DateTimeOffset? RetiredAtUtc,
[property: JsonPropertyName("revokedAtUtc")] DateTimeOffset? RevokedAtUtc,
[property: JsonPropertyName("replacesKeyId")] string? ReplacesKeyId);
public sealed record IssuerTrustOverrideModel(
[property: JsonPropertyName("weight")] decimal Weight,
[property: JsonPropertyName("reason")] string? Reason,
[property: JsonPropertyName("updatedAtUtc")] DateTimeOffset UpdatedAtUtc,
[property: JsonPropertyName("updatedBy")] string UpdatedBy,
[property: JsonPropertyName("createdAtUtc")] DateTimeOffset CreatedAtUtc,
[property: JsonPropertyName("createdBy")] string CreatedBy);
public sealed record IssuerTrustResponseModel(
[property: JsonPropertyName("tenantOverride")] IssuerTrustOverrideModel? TenantOverride,
[property: JsonPropertyName("globalOverride")] IssuerTrustOverrideModel? GlobalOverride,
[property: JsonPropertyName("effectiveWeight")] decimal EffectiveWeight);

View File

@@ -0,0 +1,57 @@
using System;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
namespace StellaOps.IssuerDirectory.Client;
public static class IssuerDirectoryClientServiceCollectionExtensions
{
public static IServiceCollection AddIssuerDirectoryClient(
this IServiceCollection services,
IConfiguration configuration,
Action<IssuerDirectoryClientOptions>? configure = null)
{
ArgumentNullException.ThrowIfNull(configuration);
return services.AddIssuerDirectoryClient(configuration.GetSection(IssuerDirectoryClientOptions.SectionName), configure);
}
public static IServiceCollection AddIssuerDirectoryClient(
this IServiceCollection services,
IConfigurationSection configurationSection,
Action<IssuerDirectoryClientOptions>? configure = null)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(configurationSection);
services.AddMemoryCache();
services.AddOptions<IssuerDirectoryClientOptions>()
.Bind(configurationSection)
.PostConfigure(options => configure?.Invoke(options))
.Validate(options =>
{
try
{
options.Validate();
return true;
}
catch
{
return false;
}
})
.ValidateOnStart();
services.AddHttpClient<IIssuerDirectoryClient, IssuerDirectoryClient>((provider, client) =>
{
var opts = provider.GetRequiredService<IOptions<IssuerDirectoryClientOptions>>().Value;
opts.Validate();
client.BaseAddress = opts.BaseAddress;
client.Timeout = opts.HttpTimeout;
});
return services;
}
}

View File

@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>preview</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="10.0.0-rc.2.25502.107" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.0-rc.2.25502.107" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.0-rc.2.25502.107" />
</ItemGroup>
</Project>