audit, advisories and doctors/setup work
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
using StellaOps.Determinism;
|
||||
|
||||
namespace StellaOps.Provcache.Events;
|
||||
|
||||
/// <summary>
|
||||
@@ -91,6 +93,8 @@ public sealed record FeedEpochAdvancedEvent
|
||||
/// <param name="correlationId">Correlation ID for tracing.</param>
|
||||
/// <param name="eventId">Optional event ID (defaults to new GUID).</param>
|
||||
/// <param name="timestamp">Optional timestamp (defaults to current UTC time).</param>
|
||||
/// <param name="guidProvider">Optional GUID provider for deterministic IDs.</param>
|
||||
/// <param name="timeProvider">Optional time provider for deterministic timestamps.</param>
|
||||
public static FeedEpochAdvancedEvent Create(
|
||||
string feedId,
|
||||
string previousEpoch,
|
||||
@@ -102,12 +106,17 @@ public sealed record FeedEpochAdvancedEvent
|
||||
string? tenantId = null,
|
||||
string? correlationId = null,
|
||||
Guid? eventId = null,
|
||||
DateTimeOffset? timestamp = null)
|
||||
DateTimeOffset? timestamp = null,
|
||||
IGuidProvider? guidProvider = null,
|
||||
TimeProvider? timeProvider = null)
|
||||
{
|
||||
var guidSource = guidProvider ?? SystemGuidProvider.Instance;
|
||||
var timeSource = timeProvider ?? TimeProvider.System;
|
||||
|
||||
return new FeedEpochAdvancedEvent
|
||||
{
|
||||
EventId = eventId ?? Guid.NewGuid(),
|
||||
Timestamp = timestamp ?? DateTimeOffset.UtcNow,
|
||||
EventId = eventId ?? guidSource.NewGuid(),
|
||||
Timestamp = timestamp ?? timeSource.GetUtcNow(),
|
||||
FeedId = feedId,
|
||||
PreviousEpoch = previousEpoch,
|
||||
NewEpoch = newEpoch,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using StellaOps.Determinism;
|
||||
|
||||
namespace StellaOps.Provcache.Events;
|
||||
|
||||
/// <summary>
|
||||
@@ -80,6 +82,8 @@ public sealed record SignerRevokedEvent
|
||||
/// <param name="correlationId">Correlation ID for tracing.</param>
|
||||
/// <param name="eventId">Optional event ID (defaults to new GUID).</param>
|
||||
/// <param name="timestamp">Optional timestamp (defaults to current UTC time).</param>
|
||||
/// <param name="guidProvider">Optional GUID provider for deterministic IDs.</param>
|
||||
/// <param name="timeProvider">Optional time provider for deterministic timestamps.</param>
|
||||
public static SignerRevokedEvent Create(
|
||||
Guid anchorId,
|
||||
string keyId,
|
||||
@@ -89,12 +93,17 @@ public sealed record SignerRevokedEvent
|
||||
string? actor = null,
|
||||
string? correlationId = null,
|
||||
Guid? eventId = null,
|
||||
DateTimeOffset? timestamp = null)
|
||||
DateTimeOffset? timestamp = null,
|
||||
IGuidProvider? guidProvider = null,
|
||||
TimeProvider? timeProvider = null)
|
||||
{
|
||||
var guidSource = guidProvider ?? SystemGuidProvider.Instance;
|
||||
var timeSource = timeProvider ?? TimeProvider.System;
|
||||
|
||||
return new SignerRevokedEvent
|
||||
{
|
||||
EventId = eventId ?? Guid.NewGuid(),
|
||||
Timestamp = timestamp ?? DateTimeOffset.UtcNow,
|
||||
EventId = eventId ?? guidSource.NewGuid(),
|
||||
Timestamp = timestamp ?? timeSource.GetUtcNow(),
|
||||
AnchorId = anchorId,
|
||||
KeyId = keyId,
|
||||
SignerHash = signerHash,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Canonical.Json;
|
||||
using StellaOps.Cryptography;
|
||||
using StellaOps.Determinism;
|
||||
using StellaOps.Provenance.Attestation;
|
||||
|
||||
@@ -15,6 +17,8 @@ public sealed class MinimalProofExporter : IMinimalProofExporter
|
||||
private readonly IProvcacheService _provcacheService;
|
||||
private readonly IEvidenceChunkRepository _chunkRepository;
|
||||
private readonly ISigner? _signer;
|
||||
private readonly ICryptoHmac? _cryptoHmac;
|
||||
private readonly IKeyProvider? _keyProvider;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly IGuidProvider _guidProvider;
|
||||
private readonly ILogger<MinimalProofExporter> _logger;
|
||||
@@ -32,11 +36,15 @@ public sealed class MinimalProofExporter : IMinimalProofExporter
|
||||
ISigner? signer = null,
|
||||
TimeProvider? timeProvider = null,
|
||||
IGuidProvider? guidProvider = null,
|
||||
ILogger<MinimalProofExporter>? logger = null)
|
||||
ILogger<MinimalProofExporter>? logger = null,
|
||||
ICryptoHmac? cryptoHmac = null,
|
||||
IKeyProvider? keyProvider = null)
|
||||
{
|
||||
_provcacheService = provcacheService ?? throw new ArgumentNullException(nameof(provcacheService));
|
||||
_chunkRepository = chunkRepository ?? throw new ArgumentNullException(nameof(chunkRepository));
|
||||
_signer = signer;
|
||||
_cryptoHmac = cryptoHmac;
|
||||
_keyProvider = keyProvider;
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
_guidProvider = guidProvider ?? SystemGuidProvider.Instance;
|
||||
_logger = logger ?? Microsoft.Extensions.Logging.Abstractions.NullLogger<MinimalProofExporter>.Instance;
|
||||
@@ -114,7 +122,7 @@ public sealed class MinimalProofExporter : IMinimalProofExporter
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var bundle = await ExportAsync(veriKey, options, cancellationToken);
|
||||
return JsonSerializer.SerializeToUtf8Bytes(bundle, s_jsonOptions);
|
||||
return CanonJson.Canonicalize(bundle, s_jsonOptions);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -127,7 +135,8 @@ public sealed class MinimalProofExporter : IMinimalProofExporter
|
||||
ArgumentNullException.ThrowIfNull(outputStream);
|
||||
|
||||
var bundle = await ExportAsync(veriKey, options, cancellationToken);
|
||||
await JsonSerializer.SerializeAsync(outputStream, bundle, s_jsonOptions, cancellationToken);
|
||||
var payload = CanonJson.Canonicalize(bundle, s_jsonOptions);
|
||||
await outputStream.WriteAsync(payload, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -384,19 +393,20 @@ public sealed class MinimalProofExporter : IMinimalProofExporter
|
||||
|
||||
// Serialize bundle without signature for signing
|
||||
var bundleWithoutSig = bundle with { Signature = null };
|
||||
var payload = JsonSerializer.SerializeToUtf8Bytes(bundleWithoutSig, s_jsonOptions);
|
||||
var payload = CanonJson.Canonicalize(bundleWithoutSig, s_jsonOptions);
|
||||
|
||||
var signRequest = new SignRequest(
|
||||
Payload: payload,
|
||||
ContentType: "application/vnd.stellaops.proof-bundle+json");
|
||||
|
||||
var signResult = await _signer.SignAsync(signRequest, cancellationToken);
|
||||
var algorithm = _cryptoHmac?.GetAlgorithmForPurpose(HmacPurpose.Signing) ?? "HMAC-SHA256";
|
||||
|
||||
return bundle with
|
||||
{
|
||||
Signature = new BundleSignature
|
||||
{
|
||||
Algorithm = "HMAC-SHA256", // Could be made configurable
|
||||
Algorithm = algorithm,
|
||||
KeyId = signResult.KeyId,
|
||||
SignatureBytes = Convert.ToBase64String(signResult.Signature),
|
||||
SignedAt = signResult.SignedAt
|
||||
@@ -436,11 +446,44 @@ public sealed class MinimalProofExporter : IMinimalProofExporter
|
||||
|
||||
private bool VerifySignature(MinimalProofBundle bundle)
|
||||
{
|
||||
// For now, we don't have signature verification implemented
|
||||
// This would require the signer's public key or certificate
|
||||
// Return true as a placeholder - signature presence is enough for MVP
|
||||
_logger.LogWarning("Signature verification not fully implemented - assuming valid");
|
||||
return bundle.Signature is not null;
|
||||
if (bundle.Signature is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_cryptoHmac is null || _keyProvider is null)
|
||||
{
|
||||
_logger.LogWarning("Signature verification skipped: no HMAC verifier or key configured");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!string.Equals(bundle.Signature.KeyId, _keyProvider.KeyId, StringComparison.Ordinal))
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Signature key mismatch: expected {Expected}, got {Actual}",
|
||||
_keyProvider.KeyId,
|
||||
bundle.Signature.KeyId);
|
||||
return false;
|
||||
}
|
||||
|
||||
var expectedAlgorithm = _cryptoHmac.GetAlgorithmForPurpose(HmacPurpose.Signing);
|
||||
if (!string.Equals(bundle.Signature.Algorithm, expectedAlgorithm, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Signature algorithm mismatch: expected {Expected}, got {Actual}",
|
||||
expectedAlgorithm,
|
||||
bundle.Signature.Algorithm);
|
||||
return false;
|
||||
}
|
||||
|
||||
var bundleWithoutSig = bundle with { Signature = null };
|
||||
var payload = CanonJson.Canonicalize(bundleWithoutSig, s_jsonOptions);
|
||||
|
||||
return _cryptoHmac.VerifyHmacBase64ForPurpose(
|
||||
_keyProvider.KeyMaterial,
|
||||
payload,
|
||||
bundle.Signature.SignatureBytes,
|
||||
HmacPurpose.Signing);
|
||||
}
|
||||
|
||||
private static long CalculateChunkDataSize(ChunkManifest manifest, int chunkCount)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Http.Json;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.Json;
|
||||
@@ -11,10 +12,21 @@ namespace StellaOps.Provcache;
|
||||
/// </summary>
|
||||
public sealed class HttpChunkFetcher : ILazyEvidenceFetcher, IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Named client for use with IHttpClientFactory.
|
||||
/// </summary>
|
||||
public const string HttpClientName = "provcache-lazy-fetch";
|
||||
|
||||
private static readonly string[] DefaultSchemes = ["https", "http"];
|
||||
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly bool _ownsClient;
|
||||
private readonly ILogger<HttpChunkFetcher> _logger;
|
||||
private readonly JsonSerializerOptions _jsonOptions;
|
||||
private readonly LazyFetchHttpOptions _options;
|
||||
private readonly IReadOnlyList<string> _allowedHosts;
|
||||
private readonly IReadOnlySet<string> _allowedSchemes;
|
||||
private readonly bool _allowAllHosts;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string FetcherType => "http";
|
||||
@@ -23,9 +35,15 @@ public sealed class HttpChunkFetcher : ILazyEvidenceFetcher, IDisposable
|
||||
/// Creates an HTTP chunk fetcher with the specified base URL.
|
||||
/// </summary>
|
||||
/// <param name="baseUrl">The base URL of the Stella API.</param>
|
||||
/// <param name="httpClientFactory">HTTP client factory.</param>
|
||||
/// <param name="logger">Logger instance.</param>
|
||||
public HttpChunkFetcher(Uri baseUrl, ILogger<HttpChunkFetcher> logger)
|
||||
: this(CreateClient(baseUrl), ownsClient: true, logger)
|
||||
/// <param name="options">Lazy fetch HTTP options.</param>
|
||||
public HttpChunkFetcher(
|
||||
Uri baseUrl,
|
||||
IHttpClientFactory httpClientFactory,
|
||||
ILogger<HttpChunkFetcher> logger,
|
||||
LazyFetchHttpOptions? options = null)
|
||||
: this(CreateClient(httpClientFactory, baseUrl), ownsClient: false, logger, options)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -35,25 +53,149 @@ public sealed class HttpChunkFetcher : ILazyEvidenceFetcher, IDisposable
|
||||
/// <param name="httpClient">The HTTP client to use.</param>
|
||||
/// <param name="ownsClient">Whether this fetcher owns the client lifecycle.</param>
|
||||
/// <param name="logger">Logger instance.</param>
|
||||
public HttpChunkFetcher(HttpClient httpClient, bool ownsClient, ILogger<HttpChunkFetcher> logger)
|
||||
/// <param name="options">Lazy fetch HTTP options.</param>
|
||||
public HttpChunkFetcher(
|
||||
HttpClient httpClient,
|
||||
bool ownsClient,
|
||||
ILogger<HttpChunkFetcher> logger,
|
||||
LazyFetchHttpOptions? options = null)
|
||||
{
|
||||
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
|
||||
_ownsClient = ownsClient;
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_options = options ?? new LazyFetchHttpOptions();
|
||||
_jsonOptions = new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
|
||||
var baseAddress = _httpClient.BaseAddress
|
||||
?? throw new InvalidOperationException("HttpChunkFetcher requires a BaseAddress on the HTTP client.");
|
||||
|
||||
_allowedSchemes = NormalizeSchemes(_options.AllowedSchemes);
|
||||
_allowedHosts = NormalizeHosts(_options.AllowedHosts, baseAddress.Host, out _allowAllHosts);
|
||||
|
||||
ValidateBaseAddress(baseAddress);
|
||||
ApplyClientConfiguration();
|
||||
}
|
||||
|
||||
private static HttpClient CreateClient(Uri baseUrl)
|
||||
private static HttpClient CreateClient(IHttpClientFactory httpClientFactory, Uri baseUrl)
|
||||
{
|
||||
var client = new HttpClient { BaseAddress = baseUrl };
|
||||
client.DefaultRequestHeaders.Add("Accept", "application/json");
|
||||
ArgumentNullException.ThrowIfNull(httpClientFactory);
|
||||
ArgumentNullException.ThrowIfNull(baseUrl);
|
||||
|
||||
var client = httpClientFactory.CreateClient(HttpClientName);
|
||||
client.BaseAddress = baseUrl;
|
||||
return client;
|
||||
}
|
||||
|
||||
private void ApplyClientConfiguration()
|
||||
{
|
||||
var timeout = _options.Timeout;
|
||||
if (timeout <= TimeSpan.Zero || timeout == Timeout.InfiniteTimeSpan)
|
||||
{
|
||||
throw new InvalidOperationException("Lazy fetch HTTP timeout must be a positive, non-infinite duration.");
|
||||
}
|
||||
|
||||
if (_httpClient.Timeout == Timeout.InfiniteTimeSpan || _httpClient.Timeout > timeout)
|
||||
{
|
||||
_httpClient.Timeout = timeout;
|
||||
}
|
||||
|
||||
if (!_httpClient.DefaultRequestHeaders.Accept.Any(header =>
|
||||
string.Equals(header.MediaType, "application/json", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
_httpClient.DefaultRequestHeaders.Accept.Add(
|
||||
new MediaTypeWithQualityHeaderValue("application/json"));
|
||||
}
|
||||
}
|
||||
|
||||
private void ValidateBaseAddress(Uri baseAddress)
|
||||
{
|
||||
if (!baseAddress.IsAbsoluteUri)
|
||||
{
|
||||
throw new InvalidOperationException("Lazy fetch base URL must be absolute.");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(baseAddress.UserInfo))
|
||||
{
|
||||
throw new InvalidOperationException("Lazy fetch base URL must not include user info.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(baseAddress.Host))
|
||||
{
|
||||
throw new InvalidOperationException("Lazy fetch base URL must include a host.");
|
||||
}
|
||||
|
||||
if (!_allowedSchemes.Contains(baseAddress.Scheme))
|
||||
{
|
||||
throw new InvalidOperationException($"Lazy fetch base URL scheme '{baseAddress.Scheme}' is not allowed.");
|
||||
}
|
||||
|
||||
if (!_allowAllHosts && !IsHostAllowed(baseAddress.Host, _allowedHosts))
|
||||
{
|
||||
throw new InvalidOperationException($"Lazy fetch base URL host '{baseAddress.Host}' is not allowlisted.");
|
||||
}
|
||||
}
|
||||
|
||||
private static IReadOnlySet<string> NormalizeSchemes(IList<string> schemes)
|
||||
{
|
||||
var normalized = schemes
|
||||
.Where(s => !string.IsNullOrWhiteSpace(s))
|
||||
.Select(s => s.Trim())
|
||||
.ToArray();
|
||||
|
||||
if (normalized.Length == 0)
|
||||
{
|
||||
normalized = DefaultSchemes;
|
||||
}
|
||||
|
||||
return new HashSet<string>(normalized, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<string> NormalizeHosts(
|
||||
IList<string> hosts,
|
||||
string baseHost,
|
||||
out bool allowAllHosts)
|
||||
{
|
||||
var normalized = hosts
|
||||
.Where(h => !string.IsNullOrWhiteSpace(h))
|
||||
.Select(h => h.Trim())
|
||||
.ToList();
|
||||
|
||||
allowAllHosts = normalized.Any(h => string.Equals(h, "*", StringComparison.Ordinal));
|
||||
|
||||
if (!allowAllHosts && normalized.Count == 0)
|
||||
{
|
||||
normalized.Add(baseHost);
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private static bool IsHostAllowed(string host, IReadOnlyList<string> allowedHosts)
|
||||
{
|
||||
foreach (var allowed in allowedHosts)
|
||||
{
|
||||
if (string.Equals(allowed, host, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (allowed.StartsWith("*.", StringComparison.Ordinal))
|
||||
{
|
||||
var suffix = allowed[1..];
|
||||
if (host.EndsWith(suffix, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<FetchedChunk?> FetchChunkAsync(
|
||||
string proofRoot,
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace StellaOps.Provcache;
|
||||
|
||||
/// <summary>
|
||||
/// Options for HTTP lazy evidence fetching.
|
||||
/// </summary>
|
||||
public sealed class LazyFetchHttpOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Configuration section name under Provcache.
|
||||
/// </summary>
|
||||
public const string SectionName = "LazyFetchHttp";
|
||||
|
||||
/// <summary>
|
||||
/// HTTP timeout for fetch requests.
|
||||
/// Default: 10 seconds.
|
||||
/// </summary>
|
||||
[Range(typeof(TimeSpan), "00:00:01", "00:05:00", ErrorMessage = "Timeout must be between 1 second and 5 minutes")]
|
||||
public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(10);
|
||||
|
||||
/// <summary>
|
||||
/// Allowlisted hostnames for HTTP fetches.
|
||||
/// Supports exact match and "*.example.com" suffix entries.
|
||||
/// </summary>
|
||||
public IList<string> AllowedHosts { get; } = new List<string>();
|
||||
|
||||
/// <summary>
|
||||
/// Allowlisted schemes for HTTP fetches.
|
||||
/// When empty, defaults to http and https.
|
||||
/// </summary>
|
||||
public IList<string> AllowedSchemes { get; } = new List<string>();
|
||||
}
|
||||
@@ -5,8 +5,10 @@
|
||||
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using StellaOps.Canonical.Json;
|
||||
|
||||
namespace StellaOps.Provcache.Oci;
|
||||
|
||||
@@ -16,11 +18,12 @@ namespace StellaOps.Provcache.Oci;
|
||||
/// </summary>
|
||||
public sealed class ProvcacheOciAttestationBuilder : IProvcacheOciAttestationBuilder
|
||||
{
|
||||
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
|
||||
private static readonly JsonSerializerOptions CanonicalOptions = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
WriteIndented = false // Deterministic output
|
||||
WriteIndented = false,
|
||||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
||||
};
|
||||
|
||||
private readonly TimeProvider _timeProvider;
|
||||
@@ -53,8 +56,8 @@ public sealed class ProvcacheOciAttestationBuilder : IProvcacheOciAttestationBui
|
||||
};
|
||||
|
||||
// Serialize to canonical JSON (deterministic)
|
||||
var statementJson = JsonSerializer.Serialize(statement, SerializerOptions);
|
||||
var statementBytes = Encoding.UTF8.GetBytes(statementJson);
|
||||
var statementBytes = CanonJson.Canonicalize(statement, CanonicalOptions);
|
||||
var statementJson = Encoding.UTF8.GetString(statementBytes);
|
||||
|
||||
// Build OCI annotations
|
||||
var annotations = BuildAnnotations(request, predicate);
|
||||
@@ -275,7 +278,7 @@ public sealed class ProvcacheOciAttestationBuilder : IProvcacheOciAttestationBui
|
||||
["stellaops.provcache.verikey"] = predicate.VeriKey,
|
||||
["stellaops.provcache.verdict-hash"] = predicate.VerdictHash,
|
||||
["stellaops.provcache.proof-root"] = predicate.ProofRoot,
|
||||
["stellaops.provcache.trust-score"] = predicate.TrustScore.ToString(),
|
||||
["stellaops.provcache.trust-score"] = predicate.TrustScore.ToString(CultureInfo.InvariantCulture),
|
||||
["stellaops.provcache.expires-at"] = predicate.ExpiresAt
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Globalization;
|
||||
|
||||
namespace StellaOps.Provcache;
|
||||
|
||||
@@ -144,6 +145,6 @@ public sealed class ProvcacheOptions
|
||||
{
|
||||
var bucketTicks = TimeWindowBucket.Ticks;
|
||||
var epoch = timestamp.UtcTicks / bucketTicks * bucketTicks;
|
||||
return new DateTimeOffset(epoch, TimeSpan.Zero).ToString("yyyy-MM-ddTHH:mm:ssZ");
|
||||
return new DateTimeOffset(epoch, TimeSpan.Zero).ToString("yyyy-MM-ddTHH:mm:ssZ", CultureInfo.InvariantCulture);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace StellaOps.Provcache;
|
||||
|
||||
@@ -21,8 +22,18 @@ public static class ProvcacheServiceCollectionExtensions
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
ArgumentNullException.ThrowIfNull(configuration);
|
||||
|
||||
var section = configuration.GetSection(ProvcacheOptions.SectionName);
|
||||
|
||||
// Register options
|
||||
services.Configure<ProvcacheOptions>(configuration.GetSection(ProvcacheOptions.SectionName));
|
||||
services.AddOptions<ProvcacheOptions>()
|
||||
.Bind(section)
|
||||
.ValidateDataAnnotations()
|
||||
.ValidateOnStart();
|
||||
|
||||
services.AddOptions<LazyFetchHttpOptions>()
|
||||
.Bind(section.GetSection(LazyFetchHttpOptions.SectionName))
|
||||
.ValidateDataAnnotations()
|
||||
.ValidateOnStart();
|
||||
|
||||
// Register core services
|
||||
services.AddSingleton<IProvcacheService, ProvcacheService>();
|
||||
@@ -31,6 +42,7 @@ public static class ProvcacheServiceCollectionExtensions
|
||||
services.AddSingleton<WriteBehindQueue>();
|
||||
services.AddSingleton<IWriteBehindQueue>(sp => sp.GetRequiredService<WriteBehindQueue>());
|
||||
services.AddHostedService(sp => sp.GetRequiredService<WriteBehindQueue>());
|
||||
services.AddHttpClient(HttpChunkFetcher.HttpClientName);
|
||||
|
||||
return services;
|
||||
}
|
||||
@@ -49,7 +61,14 @@ public static class ProvcacheServiceCollectionExtensions
|
||||
ArgumentNullException.ThrowIfNull(configure);
|
||||
|
||||
// Register options
|
||||
services.Configure(configure);
|
||||
services.AddOptions<ProvcacheOptions>()
|
||||
.Configure(configure)
|
||||
.ValidateDataAnnotations()
|
||||
.ValidateOnStart();
|
||||
|
||||
services.AddOptions<LazyFetchHttpOptions>()
|
||||
.ValidateDataAnnotations()
|
||||
.ValidateOnStart();
|
||||
|
||||
// Register core services
|
||||
services.AddSingleton<IProvcacheService, ProvcacheService>();
|
||||
@@ -58,6 +77,7 @@ public static class ProvcacheServiceCollectionExtensions
|
||||
services.AddSingleton<WriteBehindQueue>();
|
||||
services.AddSingleton<IWriteBehindQueue>(sp => sp.GetRequiredService<WriteBehindQueue>());
|
||||
services.AddHostedService(sp => sp.GetRequiredService<WriteBehindQueue>());
|
||||
services.AddHttpClient(HttpChunkFetcher.HttpClientName);
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" />
|
||||
|
||||
@@ -7,4 +7,4 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
|
||||
| --- | --- | --- |
|
||||
| AUDIT-0101-M | DONE | Revalidated 2026-01-08; maintainability audit for Provcache core. |
|
||||
| AUDIT-0101-T | DONE | Revalidated 2026-01-08; test coverage audit for Provcache core. |
|
||||
| AUDIT-0101-A | TODO | Pending approval (revalidated 2026-01-08). |
|
||||
| AUDIT-0101-A | DONE | Applied 2026-01-13; hotlist fixes and tests added. |
|
||||
|
||||
@@ -133,7 +133,7 @@ public sealed class WriteBehindQueue : BackgroundService, IWriteBehindQueue
|
||||
}
|
||||
|
||||
// Drain remaining items on shutdown
|
||||
await DrainAsync(CancellationToken.None).ConfigureAwait(false);
|
||||
await DrainAsync(stoppingToken).ConfigureAwait(false);
|
||||
|
||||
_logger.LogInformation("Write-behind queue stopped");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user