Files
git.stella-ops.org/src/Policy/StellaOps.Policy.Registry/PolicyRegistryClient.cs
StellaOps Bot 4042fc2184
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
release-manifest-verify / verify (push) Has been cancelled
Add unit tests for PackRunAttestation and SealedInstallEnforcer
- Implement comprehensive tests for PackRunAttestationService, covering attestation generation, verification, and event emission.
- Add tests for SealedInstallEnforcer to validate sealed install requirements and enforcement logic.
- Introduce a MonacoLoaderService stub for testing purposes to prevent Monaco workers/styles from loading during Karma runs.
2025-12-06 22:25:30 +02:00

635 lines
28 KiB
C#

using System.Net.Http.Json;
using System.Text.Json;
using Microsoft.Extensions.Options;
using StellaOps.Policy.Registry.Contracts;
namespace StellaOps.Policy.Registry;
/// <summary>
/// HTTP client implementation for Policy Registry API.
/// </summary>
public sealed class PolicyRegistryClient : IPolicyRegistryClient
{
private readonly HttpClient _httpClient;
private readonly JsonSerializerOptions _jsonOptions;
public PolicyRegistryClient(HttpClient httpClient, IOptions<PolicyRegistryClientOptions>? options = null)
{
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
_jsonOptions = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
PropertyNameCaseInsensitive = true
};
if (options?.Value?.BaseUrl is not null && _httpClient.BaseAddress is null)
{
_httpClient.BaseAddress = new Uri(options.Value.BaseUrl);
}
}
private static void AddTenantHeader(HttpRequestMessage request, Guid tenantId)
{
request.Headers.Add("X-Tenant-Id", tenantId.ToString());
}
private static string BuildQueryString(PaginationParams? pagination, params (string name, string? value)[] additional)
{
var parts = new List<string>();
if (pagination is not null)
{
if (pagination.PageSize != 20)
{
parts.Add($"page_size={pagination.PageSize}");
}
if (!string.IsNullOrWhiteSpace(pagination.PageToken))
{
parts.Add($"page_token={Uri.EscapeDataString(pagination.PageToken)}");
}
}
foreach (var (name, value) in additional)
{
if (!string.IsNullOrWhiteSpace(value))
{
parts.Add($"{name}={Uri.EscapeDataString(value)}");
}
}
return parts.Count > 0 ? "?" + string.Join("&", parts) : string.Empty;
}
// ============================================================
// VERIFICATION POLICY OPERATIONS
// ============================================================
public async Task<VerificationPolicyList> ListVerificationPoliciesAsync(
Guid tenantId,
PaginationParams? pagination = null,
CancellationToken cancellationToken = default)
{
var query = BuildQueryString(pagination);
using var request = new HttpRequestMessage(HttpMethod.Get, $"/api/v1/policy/verification-policies{query}");
AddTenantHeader(request, tenantId);
var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<VerificationPolicyList>(_jsonOptions, cancellationToken).ConfigureAwait(false)
?? throw new InvalidOperationException("Failed to deserialize response");
}
public async Task<VerificationPolicy> CreateVerificationPolicyAsync(
Guid tenantId,
CreateVerificationPolicyRequest request,
CancellationToken cancellationToken = default)
{
using var httpRequest = new HttpRequestMessage(HttpMethod.Post, "/api/v1/policy/verification-policies");
AddTenantHeader(httpRequest, tenantId);
httpRequest.Content = JsonContent.Create(request, options: _jsonOptions);
var response = await _httpClient.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<VerificationPolicy>(_jsonOptions, cancellationToken).ConfigureAwait(false)
?? throw new InvalidOperationException("Failed to deserialize response");
}
public async Task<VerificationPolicy> GetVerificationPolicyAsync(
Guid tenantId,
string policyId,
CancellationToken cancellationToken = default)
{
using var request = new HttpRequestMessage(HttpMethod.Get, $"/api/v1/policy/verification-policies/{Uri.EscapeDataString(policyId)}");
AddTenantHeader(request, tenantId);
var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<VerificationPolicy>(_jsonOptions, cancellationToken).ConfigureAwait(false)
?? throw new InvalidOperationException("Failed to deserialize response");
}
public async Task<VerificationPolicy> UpdateVerificationPolicyAsync(
Guid tenantId,
string policyId,
UpdateVerificationPolicyRequest request,
CancellationToken cancellationToken = default)
{
using var httpRequest = new HttpRequestMessage(HttpMethod.Put, $"/api/v1/policy/verification-policies/{Uri.EscapeDataString(policyId)}");
AddTenantHeader(httpRequest, tenantId);
httpRequest.Content = JsonContent.Create(request, options: _jsonOptions);
var response = await _httpClient.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<VerificationPolicy>(_jsonOptions, cancellationToken).ConfigureAwait(false)
?? throw new InvalidOperationException("Failed to deserialize response");
}
public async Task DeleteVerificationPolicyAsync(
Guid tenantId,
string policyId,
CancellationToken cancellationToken = default)
{
using var request = new HttpRequestMessage(HttpMethod.Delete, $"/api/v1/policy/verification-policies/{Uri.EscapeDataString(policyId)}");
AddTenantHeader(request, tenantId);
var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
}
// ============================================================
// POLICY PACK OPERATIONS
// ============================================================
public async Task<PolicyPackList> ListPolicyPacksAsync(
Guid tenantId,
PolicyPackStatus? status = null,
PaginationParams? pagination = null,
CancellationToken cancellationToken = default)
{
var query = BuildQueryString(pagination, ("status", status?.ToString().ToLowerInvariant()));
using var request = new HttpRequestMessage(HttpMethod.Get, $"/api/v1/policy/packs{query}");
AddTenantHeader(request, tenantId);
var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<PolicyPackList>(_jsonOptions, cancellationToken).ConfigureAwait(false)
?? throw new InvalidOperationException("Failed to deserialize response");
}
public async Task<PolicyPack> CreatePolicyPackAsync(
Guid tenantId,
CreatePolicyPackRequest request,
CancellationToken cancellationToken = default)
{
using var httpRequest = new HttpRequestMessage(HttpMethod.Post, "/api/v1/policy/packs");
AddTenantHeader(httpRequest, tenantId);
httpRequest.Content = JsonContent.Create(request, options: _jsonOptions);
var response = await _httpClient.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<PolicyPack>(_jsonOptions, cancellationToken).ConfigureAwait(false)
?? throw new InvalidOperationException("Failed to deserialize response");
}
public async Task<PolicyPack> GetPolicyPackAsync(
Guid tenantId,
Guid packId,
CancellationToken cancellationToken = default)
{
using var request = new HttpRequestMessage(HttpMethod.Get, $"/api/v1/policy/packs/{packId}");
AddTenantHeader(request, tenantId);
var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<PolicyPack>(_jsonOptions, cancellationToken).ConfigureAwait(false)
?? throw new InvalidOperationException("Failed to deserialize response");
}
public async Task<PolicyPack> UpdatePolicyPackAsync(
Guid tenantId,
Guid packId,
UpdatePolicyPackRequest request,
CancellationToken cancellationToken = default)
{
using var httpRequest = new HttpRequestMessage(HttpMethod.Put, $"/api/v1/policy/packs/{packId}");
AddTenantHeader(httpRequest, tenantId);
httpRequest.Content = JsonContent.Create(request, options: _jsonOptions);
var response = await _httpClient.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<PolicyPack>(_jsonOptions, cancellationToken).ConfigureAwait(false)
?? throw new InvalidOperationException("Failed to deserialize response");
}
public async Task DeletePolicyPackAsync(
Guid tenantId,
Guid packId,
CancellationToken cancellationToken = default)
{
using var request = new HttpRequestMessage(HttpMethod.Delete, $"/api/v1/policy/packs/{packId}");
AddTenantHeader(request, tenantId);
var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
}
public async Task<CompilationResult> CompilePolicyPackAsync(
Guid tenantId,
Guid packId,
CancellationToken cancellationToken = default)
{
using var request = new HttpRequestMessage(HttpMethod.Post, $"/api/v1/policy/packs/{packId}/compile");
AddTenantHeader(request, tenantId);
var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
// Note: 422 also returns CompilationResult, so we read regardless of status
return await response.Content.ReadFromJsonAsync<CompilationResult>(_jsonOptions, cancellationToken).ConfigureAwait(false)
?? throw new InvalidOperationException("Failed to deserialize response");
}
public async Task<SimulationResult> SimulatePolicyPackAsync(
Guid tenantId,
Guid packId,
SimulationRequest request,
CancellationToken cancellationToken = default)
{
using var httpRequest = new HttpRequestMessage(HttpMethod.Post, $"/api/v1/policy/packs/{packId}/simulate");
AddTenantHeader(httpRequest, tenantId);
httpRequest.Content = JsonContent.Create(request, options: _jsonOptions);
var response = await _httpClient.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<SimulationResult>(_jsonOptions, cancellationToken).ConfigureAwait(false)
?? throw new InvalidOperationException("Failed to deserialize response");
}
public async Task<PolicyPack> PublishPolicyPackAsync(
Guid tenantId,
Guid packId,
PublishRequest? request = null,
CancellationToken cancellationToken = default)
{
using var httpRequest = new HttpRequestMessage(HttpMethod.Post, $"/api/v1/policy/packs/{packId}/publish");
AddTenantHeader(httpRequest, tenantId);
if (request is not null)
{
httpRequest.Content = JsonContent.Create(request, options: _jsonOptions);
}
var response = await _httpClient.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<PolicyPack>(_jsonOptions, cancellationToken).ConfigureAwait(false)
?? throw new InvalidOperationException("Failed to deserialize response");
}
public async Task<PolicyPack> PromotePolicyPackAsync(
Guid tenantId,
Guid packId,
PromoteRequest? request = null,
CancellationToken cancellationToken = default)
{
using var httpRequest = new HttpRequestMessage(HttpMethod.Post, $"/api/v1/policy/packs/{packId}/promote");
AddTenantHeader(httpRequest, tenantId);
if (request is not null)
{
httpRequest.Content = JsonContent.Create(request, options: _jsonOptions);
}
var response = await _httpClient.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<PolicyPack>(_jsonOptions, cancellationToken).ConfigureAwait(false)
?? throw new InvalidOperationException("Failed to deserialize response");
}
// ============================================================
// SNAPSHOT OPERATIONS
// ============================================================
public async Task<SnapshotList> ListSnapshotsAsync(
Guid tenantId,
PaginationParams? pagination = null,
CancellationToken cancellationToken = default)
{
var query = BuildQueryString(pagination);
using var request = new HttpRequestMessage(HttpMethod.Get, $"/api/v1/policy/snapshots{query}");
AddTenantHeader(request, tenantId);
var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<SnapshotList>(_jsonOptions, cancellationToken).ConfigureAwait(false)
?? throw new InvalidOperationException("Failed to deserialize response");
}
public async Task<Snapshot> CreateSnapshotAsync(
Guid tenantId,
CreateSnapshotRequest request,
CancellationToken cancellationToken = default)
{
using var httpRequest = new HttpRequestMessage(HttpMethod.Post, "/api/v1/policy/snapshots");
AddTenantHeader(httpRequest, tenantId);
httpRequest.Content = JsonContent.Create(request, options: _jsonOptions);
var response = await _httpClient.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<Snapshot>(_jsonOptions, cancellationToken).ConfigureAwait(false)
?? throw new InvalidOperationException("Failed to deserialize response");
}
public async Task<Snapshot> GetSnapshotAsync(
Guid tenantId,
Guid snapshotId,
CancellationToken cancellationToken = default)
{
using var request = new HttpRequestMessage(HttpMethod.Get, $"/api/v1/policy/snapshots/{snapshotId}");
AddTenantHeader(request, tenantId);
var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<Snapshot>(_jsonOptions, cancellationToken).ConfigureAwait(false)
?? throw new InvalidOperationException("Failed to deserialize response");
}
public async Task DeleteSnapshotAsync(
Guid tenantId,
Guid snapshotId,
CancellationToken cancellationToken = default)
{
using var request = new HttpRequestMessage(HttpMethod.Delete, $"/api/v1/policy/snapshots/{snapshotId}");
AddTenantHeader(request, tenantId);
var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
}
public async Task<Snapshot> GetSnapshotByDigestAsync(
Guid tenantId,
string digest,
CancellationToken cancellationToken = default)
{
using var request = new HttpRequestMessage(HttpMethod.Get, $"/api/v1/policy/snapshots/by-digest/{Uri.EscapeDataString(digest)}");
AddTenantHeader(request, tenantId);
var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<Snapshot>(_jsonOptions, cancellationToken).ConfigureAwait(false)
?? throw new InvalidOperationException("Failed to deserialize response");
}
// ============================================================
// VIOLATION OPERATIONS
// ============================================================
public async Task<ViolationList> ListViolationsAsync(
Guid tenantId,
Severity? severity = null,
PaginationParams? pagination = null,
CancellationToken cancellationToken = default)
{
var query = BuildQueryString(pagination, ("severity", severity?.ToString().ToLowerInvariant()));
using var request = new HttpRequestMessage(HttpMethod.Get, $"/api/v1/policy/violations{query}");
AddTenantHeader(request, tenantId);
var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<ViolationList>(_jsonOptions, cancellationToken).ConfigureAwait(false)
?? throw new InvalidOperationException("Failed to deserialize response");
}
public async Task<Violation> AppendViolationAsync(
Guid tenantId,
CreateViolationRequest request,
CancellationToken cancellationToken = default)
{
using var httpRequest = new HttpRequestMessage(HttpMethod.Post, "/api/v1/policy/violations");
AddTenantHeader(httpRequest, tenantId);
httpRequest.Content = JsonContent.Create(request, options: _jsonOptions);
var response = await _httpClient.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<Violation>(_jsonOptions, cancellationToken).ConfigureAwait(false)
?? throw new InvalidOperationException("Failed to deserialize response");
}
public async Task<ViolationBatchResult> AppendViolationBatchAsync(
Guid tenantId,
ViolationBatchRequest request,
CancellationToken cancellationToken = default)
{
using var httpRequest = new HttpRequestMessage(HttpMethod.Post, "/api/v1/policy/violations/batch");
AddTenantHeader(httpRequest, tenantId);
httpRequest.Content = JsonContent.Create(request, options: _jsonOptions);
var response = await _httpClient.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<ViolationBatchResult>(_jsonOptions, cancellationToken).ConfigureAwait(false)
?? throw new InvalidOperationException("Failed to deserialize response");
}
public async Task<Violation> GetViolationAsync(
Guid tenantId,
Guid violationId,
CancellationToken cancellationToken = default)
{
using var request = new HttpRequestMessage(HttpMethod.Get, $"/api/v1/policy/violations/{violationId}");
AddTenantHeader(request, tenantId);
var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<Violation>(_jsonOptions, cancellationToken).ConfigureAwait(false)
?? throw new InvalidOperationException("Failed to deserialize response");
}
// ============================================================
// OVERRIDE OPERATIONS
// ============================================================
public async Task<Override> CreateOverrideAsync(
Guid tenantId,
CreateOverrideRequest request,
CancellationToken cancellationToken = default)
{
using var httpRequest = new HttpRequestMessage(HttpMethod.Post, "/api/v1/policy/overrides");
AddTenantHeader(httpRequest, tenantId);
httpRequest.Content = JsonContent.Create(request, options: _jsonOptions);
var response = await _httpClient.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<Override>(_jsonOptions, cancellationToken).ConfigureAwait(false)
?? throw new InvalidOperationException("Failed to deserialize response");
}
public async Task<Override> GetOverrideAsync(
Guid tenantId,
Guid overrideId,
CancellationToken cancellationToken = default)
{
using var request = new HttpRequestMessage(HttpMethod.Get, $"/api/v1/policy/overrides/{overrideId}");
AddTenantHeader(request, tenantId);
var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<Override>(_jsonOptions, cancellationToken).ConfigureAwait(false)
?? throw new InvalidOperationException("Failed to deserialize response");
}
public async Task DeleteOverrideAsync(
Guid tenantId,
Guid overrideId,
CancellationToken cancellationToken = default)
{
using var request = new HttpRequestMessage(HttpMethod.Delete, $"/api/v1/policy/overrides/{overrideId}");
AddTenantHeader(request, tenantId);
var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
}
public async Task<Override> ApproveOverrideAsync(
Guid tenantId,
Guid overrideId,
ApproveOverrideRequest? request = null,
CancellationToken cancellationToken = default)
{
using var httpRequest = new HttpRequestMessage(HttpMethod.Post, $"/api/v1/policy/overrides/{overrideId}:approve");
AddTenantHeader(httpRequest, tenantId);
if (request is not null)
{
httpRequest.Content = JsonContent.Create(request, options: _jsonOptions);
}
var response = await _httpClient.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<Override>(_jsonOptions, cancellationToken).ConfigureAwait(false)
?? throw new InvalidOperationException("Failed to deserialize response");
}
public async Task<Override> DisableOverrideAsync(
Guid tenantId,
Guid overrideId,
CancellationToken cancellationToken = default)
{
using var request = new HttpRequestMessage(HttpMethod.Post, $"/api/v1/policy/overrides/{overrideId}:disable");
AddTenantHeader(request, tenantId);
var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<Override>(_jsonOptions, cancellationToken).ConfigureAwait(false)
?? throw new InvalidOperationException("Failed to deserialize response");
}
// ============================================================
// SEALED MODE OPERATIONS
// ============================================================
public async Task<SealedModeStatus> GetSealedModeStatusAsync(
Guid tenantId,
CancellationToken cancellationToken = default)
{
using var request = new HttpRequestMessage(HttpMethod.Get, "/api/v1/policy/sealed-mode/status");
AddTenantHeader(request, tenantId);
var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<SealedModeStatus>(_jsonOptions, cancellationToken).ConfigureAwait(false)
?? throw new InvalidOperationException("Failed to deserialize response");
}
public async Task<SealedModeStatus> SealAsync(
Guid tenantId,
SealRequest? request = null,
CancellationToken cancellationToken = default)
{
using var httpRequest = new HttpRequestMessage(HttpMethod.Post, "/api/v1/policy/sealed-mode/seal");
AddTenantHeader(httpRequest, tenantId);
if (request is not null)
{
httpRequest.Content = JsonContent.Create(request, options: _jsonOptions);
}
var response = await _httpClient.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<SealedModeStatus>(_jsonOptions, cancellationToken).ConfigureAwait(false)
?? throw new InvalidOperationException("Failed to deserialize response");
}
public async Task<SealedModeStatus> UnsealAsync(
Guid tenantId,
UnsealRequest request,
CancellationToken cancellationToken = default)
{
using var httpRequest = new HttpRequestMessage(HttpMethod.Post, "/api/v1/policy/sealed-mode/unseal");
AddTenantHeader(httpRequest, tenantId);
httpRequest.Content = JsonContent.Create(request, options: _jsonOptions);
var response = await _httpClient.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<SealedModeStatus>(_jsonOptions, cancellationToken).ConfigureAwait(false)
?? throw new InvalidOperationException("Failed to deserialize response");
}
public async Task<BundleVerificationResult> VerifyBundleAsync(
Guid tenantId,
VerifyBundleRequest request,
CancellationToken cancellationToken = default)
{
using var httpRequest = new HttpRequestMessage(HttpMethod.Post, "/api/v1/policy/sealed-mode/verify");
AddTenantHeader(httpRequest, tenantId);
httpRequest.Content = JsonContent.Create(request, options: _jsonOptions);
var response = await _httpClient.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<BundleVerificationResult>(_jsonOptions, cancellationToken).ConfigureAwait(false)
?? throw new InvalidOperationException("Failed to deserialize response");
}
// ============================================================
// STALENESS OPERATIONS
// ============================================================
public async Task<StalenessStatus> GetStalenessStatusAsync(
Guid tenantId,
CancellationToken cancellationToken = default)
{
using var request = new HttpRequestMessage(HttpMethod.Get, "/api/v1/policy/staleness/status");
AddTenantHeader(request, tenantId);
var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<StalenessStatus>(_jsonOptions, cancellationToken).ConfigureAwait(false)
?? throw new InvalidOperationException("Failed to deserialize response");
}
public async Task<StalenessEvaluation> EvaluateStalenessAsync(
Guid tenantId,
EvaluateStalenessRequest request,
CancellationToken cancellationToken = default)
{
using var httpRequest = new HttpRequestMessage(HttpMethod.Post, "/api/v1/policy/staleness/evaluate");
AddTenantHeader(httpRequest, tenantId);
httpRequest.Content = JsonContent.Create(request, options: _jsonOptions);
var response = await _httpClient.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<StalenessEvaluation>(_jsonOptions, cancellationToken).ConfigureAwait(false)
?? throw new InvalidOperationException("Failed to deserialize response");
}
}
/// <summary>
/// Configuration options for Policy Registry client.
/// </summary>
public sealed class PolicyRegistryClientOptions
{
public string? BaseUrl { get; set; }
}