stabilizaiton work - projects rework for maintenanceability and ui livening
This commit is contained in:
@@ -1,559 +0,0 @@
|
||||
namespace StellaOps.Provcache.Api;
|
||||
|
||||
/// <summary>
|
||||
/// Response model for GET /v1/provcache/{veriKey}.
|
||||
/// </summary>
|
||||
public sealed class ProvcacheGetResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// The VeriKey that was looked up.
|
||||
/// </summary>
|
||||
public required string VeriKey { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The cache entry if found.
|
||||
/// </summary>
|
||||
public ProvcacheEntry? Entry { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The source of the cache hit (valkey, postgres, etc.).
|
||||
/// </summary>
|
||||
public string? Source { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Time taken for the lookup in milliseconds.
|
||||
/// </summary>
|
||||
public double ElapsedMs { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Status: "hit", "miss", "bypassed", "expired".
|
||||
/// </summary>
|
||||
public required string Status { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request model for POST /v1/provcache.
|
||||
/// </summary>
|
||||
public sealed class ProvcacheCreateRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// The cache entry to store.
|
||||
/// </summary>
|
||||
public ProvcacheEntry? Entry { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Response model for POST /v1/provcache.
|
||||
/// </summary>
|
||||
public sealed class ProvcacheCreateResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// The VeriKey that was stored.
|
||||
/// </summary>
|
||||
public required string VeriKey { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the store operation succeeded.
|
||||
/// </summary>
|
||||
public required bool Success { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When the entry expires.
|
||||
/// </summary>
|
||||
public DateTimeOffset ExpiresAt { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request model for POST /v1/provcache/invalidate.
|
||||
/// </summary>
|
||||
public sealed class ProvcacheInvalidateRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// The invalidation type. If null, defaults to exact VeriKey match.
|
||||
/// </summary>
|
||||
public InvalidationType? Type { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The value to match for invalidation.
|
||||
/// For VeriKey type: exact VeriKey.
|
||||
/// For PolicyHash type: policy hash to match.
|
||||
/// For Pattern type: glob pattern.
|
||||
/// </summary>
|
||||
public required string Value { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Reason for invalidation (for audit log).
|
||||
/// </summary>
|
||||
public string? Reason { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Actor performing the invalidation (for audit log).
|
||||
/// </summary>
|
||||
public string? Actor { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Response model for POST /v1/provcache/invalidate.
|
||||
/// </summary>
|
||||
public sealed class ProvcacheInvalidateResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Number of entries affected by the invalidation.
|
||||
/// </summary>
|
||||
public long EntriesAffected { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The invalidation type that was used.
|
||||
/// </summary>
|
||||
public required string Type { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The value that was matched.
|
||||
/// </summary>
|
||||
public required string Value { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Reason for invalidation if provided.
|
||||
/// </summary>
|
||||
public string? Reason { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Response model for GET /v1/provcache/metrics.
|
||||
/// </summary>
|
||||
public sealed class ProvcacheMetricsResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Total number of cache requests.
|
||||
/// </summary>
|
||||
public long TotalRequests { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Total number of cache hits.
|
||||
/// </summary>
|
||||
public long TotalHits { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Total number of cache misses.
|
||||
/// </summary>
|
||||
public long TotalMisses { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Total number of invalidations performed.
|
||||
/// </summary>
|
||||
public long TotalInvalidations { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Cache hit rate (0.0 - 1.0).
|
||||
/// </summary>
|
||||
public double HitRate { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Current number of entries in the cache.
|
||||
/// </summary>
|
||||
public long CurrentEntryCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Average lookup latency in milliseconds.
|
||||
/// </summary>
|
||||
public double AvgLatencyMs { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 99th percentile lookup latency in milliseconds.
|
||||
/// </summary>
|
||||
public double P99LatencyMs { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the Valkey cache layer is healthy.
|
||||
/// </summary>
|
||||
public bool ValkeyCacheHealthy { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the Postgres repository layer is healthy.
|
||||
/// </summary>
|
||||
public bool PostgresRepositoryHealthy { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When these metrics were collected.
|
||||
/// </summary>
|
||||
public DateTimeOffset CollectedAt { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Additional invalidation type for direct VeriKey invalidation via API.
|
||||
/// </summary>
|
||||
internal static class InvalidationTypeExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Direct VeriKey invalidation type constant.
|
||||
/// </summary>
|
||||
public const string VeriKey = "VeriKey";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Response model for GET /v1/proofs/{proofRoot}.
|
||||
/// </summary>
|
||||
public sealed class ProofEvidenceResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// The proof root (Merkle root).
|
||||
/// </summary>
|
||||
public required string ProofRoot { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Total number of chunks available.
|
||||
/// </summary>
|
||||
public required int TotalChunks { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Total size of all evidence in bytes.
|
||||
/// </summary>
|
||||
public required long TotalSize { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The chunks in this page.
|
||||
/// </summary>
|
||||
public required IReadOnlyList<ProofChunkResponse> Chunks { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Pagination cursor for next page (null if last page).
|
||||
/// </summary>
|
||||
public string? NextCursor { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether there are more chunks available.
|
||||
/// </summary>
|
||||
public bool HasMore { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Response model for a single proof chunk.
|
||||
/// </summary>
|
||||
public sealed class ProofChunkResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique chunk identifier.
|
||||
/// </summary>
|
||||
public required Guid ChunkId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Zero-based chunk index.
|
||||
/// </summary>
|
||||
public required int Index { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// SHA256 hash for verification.
|
||||
/// </summary>
|
||||
public required string Hash { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Size in bytes.
|
||||
/// </summary>
|
||||
public required int Size { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Content type.
|
||||
/// </summary>
|
||||
public required string ContentType { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Base64-encoded chunk data (included only when includeData=true).
|
||||
/// </summary>
|
||||
public string? Data { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Response model for GET /v1/proofs/{proofRoot}/manifest.
|
||||
/// </summary>
|
||||
public sealed class ProofManifestResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// The proof root (Merkle root).
|
||||
/// </summary>
|
||||
public required string ProofRoot { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Total number of chunks.
|
||||
/// </summary>
|
||||
public required int TotalChunks { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Total size of all evidence in bytes.
|
||||
/// </summary>
|
||||
public required long TotalSize { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Ordered list of chunk metadata (without data).
|
||||
/// </summary>
|
||||
public required IReadOnlyList<ChunkMetadataResponse> Chunks { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When the manifest was generated.
|
||||
/// </summary>
|
||||
public required DateTimeOffset GeneratedAt { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Response model for chunk metadata (without data).
|
||||
/// </summary>
|
||||
public sealed class ChunkMetadataResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Chunk identifier.
|
||||
/// </summary>
|
||||
public required Guid ChunkId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Zero-based index.
|
||||
/// </summary>
|
||||
public required int Index { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// SHA256 hash for verification.
|
||||
/// </summary>
|
||||
public required string Hash { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Size in bytes.
|
||||
/// </summary>
|
||||
public required int Size { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Content type.
|
||||
/// </summary>
|
||||
public required string ContentType { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Response model for POST /v1/proofs/{proofRoot}/verify.
|
||||
/// </summary>
|
||||
public sealed class ProofVerificationResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// The proof root that was verified.
|
||||
/// </summary>
|
||||
public required string ProofRoot { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the Merkle tree is valid.
|
||||
/// </summary>
|
||||
public required bool IsValid { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Details about each chunk's verification.
|
||||
/// </summary>
|
||||
public IReadOnlyList<ChunkVerificationResult>? ChunkResults { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Error message if verification failed.
|
||||
/// </summary>
|
||||
public string? Error { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of verifying a single chunk.
|
||||
/// </summary>
|
||||
public sealed class ChunkVerificationResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Chunk index.
|
||||
/// </summary>
|
||||
public required int Index { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the chunk hash is valid.
|
||||
/// </summary>
|
||||
public required bool IsValid { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Expected hash from manifest.
|
||||
/// </summary>
|
||||
public required string ExpectedHash { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Computed hash from chunk data.
|
||||
/// </summary>
|
||||
public string? ComputedHash { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Response model for GET /v1/provcache/{veriKey}/manifest.
|
||||
/// </summary>
|
||||
public sealed class InputManifestResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// The VeriKey this manifest describes.
|
||||
/// </summary>
|
||||
public required string VeriKey { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Information about the source artifact (container image, binary, etc.).
|
||||
/// </summary>
|
||||
public required SourceArtifactInfo SourceArtifact { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Information about the SBOM used in the decision.
|
||||
/// </summary>
|
||||
public required SbomInfoDto Sbom { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Information about VEX statements contributing to the decision.
|
||||
/// </summary>
|
||||
public required VexInfoDto Vex { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Information about the policy used in evaluation.
|
||||
/// </summary>
|
||||
public required PolicyInfoDto Policy { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Information about signers/attestors.
|
||||
/// </summary>
|
||||
public required SignerInfoDto Signers { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Time window information for cache validity.
|
||||
/// </summary>
|
||||
public required TimeWindowInfoDto TimeWindow { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When the manifest was generated.
|
||||
/// </summary>
|
||||
public required DateTimeOffset GeneratedAt { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SBOM information in API response.
|
||||
/// </summary>
|
||||
public sealed class SbomInfoDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Canonical hash of the SBOM content.
|
||||
/// </summary>
|
||||
public required string Hash { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// SBOM format (spdx-2.3, cyclonedx-1.6, etc.).
|
||||
/// </summary>
|
||||
public string? Format { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of packages in the SBOM.
|
||||
/// </summary>
|
||||
public int? PackageCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Completeness percentage (0-100).
|
||||
/// </summary>
|
||||
public int? CompletenessScore { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// VEX information in API response.
|
||||
/// </summary>
|
||||
public sealed class VexInfoDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Hash of the sorted VEX statement set.
|
||||
/// </summary>
|
||||
public required string HashSetHash { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of VEX statements contributing to this decision.
|
||||
/// </summary>
|
||||
public int StatementCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Sources of VEX statements (vendor names, OpenVEX IDs, etc.).
|
||||
/// </summary>
|
||||
public IReadOnlyList<string>? Sources { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Policy information in API response.
|
||||
/// </summary>
|
||||
public sealed class PolicyInfoDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Canonical hash of the policy bundle.
|
||||
/// </summary>
|
||||
public required string Hash { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Policy pack identifier.
|
||||
/// </summary>
|
||||
public string? PackId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Policy version number.
|
||||
/// </summary>
|
||||
public int? Version { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Human-readable policy name.
|
||||
/// </summary>
|
||||
public string? Name { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Signer information in API response.
|
||||
/// </summary>
|
||||
public sealed class SignerInfoDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Hash of the sorted signer set.
|
||||
/// </summary>
|
||||
public required string SetHash { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of signers in the set.
|
||||
/// </summary>
|
||||
public int SignerCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Signer certificate information.
|
||||
/// </summary>
|
||||
public IReadOnlyList<SignerCertificateDto>? Certificates { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Signer certificate information in API response.
|
||||
/// </summary>
|
||||
public sealed class SignerCertificateDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Subject of the certificate (e.g., CN=...).
|
||||
/// </summary>
|
||||
public string? Subject { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Certificate issuer.
|
||||
/// </summary>
|
||||
public string? Issuer { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When the certificate expires.
|
||||
/// </summary>
|
||||
public DateTimeOffset? ExpiresAt { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Time window information in API response.
|
||||
/// </summary>
|
||||
public sealed class TimeWindowInfoDto
|
||||
{
|
||||
/// <summary>
|
||||
/// The time window bucket identifier.
|
||||
/// </summary>
|
||||
public required string Bucket { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Start of the time window (UTC).
|
||||
/// </summary>
|
||||
public DateTimeOffset? StartsAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// End of the time window (UTC).
|
||||
/// </summary>
|
||||
public DateTimeOffset? EndsAt { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
namespace StellaOps.Provcache.Api;
|
||||
|
||||
/// <summary>
|
||||
/// Response model for GET /v1/provcache/{veriKey}/manifest.
|
||||
/// </summary>
|
||||
public sealed class InputManifestResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// The VeriKey this manifest describes.
|
||||
/// </summary>
|
||||
public required string VeriKey { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Information about the source artifact (container image, binary, etc.).
|
||||
/// </summary>
|
||||
public required SourceArtifactInfo SourceArtifact { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Information about the SBOM used in the decision.
|
||||
/// </summary>
|
||||
public required SbomInfoDto Sbom { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Information about VEX statements contributing to the decision.
|
||||
/// </summary>
|
||||
public required VexInfoDto Vex { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Information about the policy used in evaluation.
|
||||
/// </summary>
|
||||
public required PolicyInfoDto Policy { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Information about signers/attestors.
|
||||
/// </summary>
|
||||
public required SignerInfoDto Signers { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Time window information for cache validity.
|
||||
/// </summary>
|
||||
public required TimeWindowInfoDto TimeWindow { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When the manifest was generated.
|
||||
/// </summary>
|
||||
public required DateTimeOffset GeneratedAt { get; init; }
|
||||
}
|
||||
27
src/__Libraries/StellaOps.Provcache.Api/PolicyInfoDto.cs
Normal file
27
src/__Libraries/StellaOps.Provcache.Api/PolicyInfoDto.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
namespace StellaOps.Provcache.Api;
|
||||
|
||||
/// <summary>
|
||||
/// Policy information in API response.
|
||||
/// </summary>
|
||||
public sealed class PolicyInfoDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Canonical hash of the policy bundle.
|
||||
/// </summary>
|
||||
public required string Hash { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Policy pack identifier.
|
||||
/// </summary>
|
||||
public string? PackId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Policy version number.
|
||||
/// </summary>
|
||||
public int? Version { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Human-readable policy name.
|
||||
/// </summary>
|
||||
public string? Name { get; init; }
|
||||
}
|
||||
13
src/__Libraries/StellaOps.Provcache.Api/ProblemDetails.cs
Normal file
13
src/__Libraries/StellaOps.Provcache.Api/ProblemDetails.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace StellaOps.Provcache.Api;
|
||||
|
||||
/// <summary>
|
||||
/// Placeholder for problem details when ASP.NET Core's ProblemDetails isn't available.
|
||||
/// </summary>
|
||||
internal sealed class ProblemDetails
|
||||
{
|
||||
public string? Type { get; set; }
|
||||
public string? Title { get; set; }
|
||||
public int? Status { get; set; }
|
||||
public string? Detail { get; set; }
|
||||
public string? Instance { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
namespace StellaOps.Provcache.Api;
|
||||
|
||||
/// <summary>
|
||||
/// Response model for GET /v1/proofs/{proofRoot}.
|
||||
/// </summary>
|
||||
public sealed class ProofEvidenceResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// The proof root (Merkle root).
|
||||
/// </summary>
|
||||
public required string ProofRoot { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Total number of chunks available.
|
||||
/// </summary>
|
||||
public required int TotalChunks { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Total size of all evidence in bytes.
|
||||
/// </summary>
|
||||
public required long TotalSize { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The chunks in this page.
|
||||
/// </summary>
|
||||
public required IReadOnlyList<ProofChunkResponse> Chunks { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Pagination cursor for next page (null if last page).
|
||||
/// </summary>
|
||||
public string? NextCursor { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether there are more chunks available.
|
||||
/// </summary>
|
||||
public bool HasMore { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Response model for a single proof chunk.
|
||||
/// </summary>
|
||||
public sealed class ProofChunkResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique chunk identifier.
|
||||
/// </summary>
|
||||
public required Guid ChunkId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Zero-based chunk index.
|
||||
/// </summary>
|
||||
public required int Index { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// SHA256 hash for verification.
|
||||
/// </summary>
|
||||
public required string Hash { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Size in bytes.
|
||||
/// </summary>
|
||||
public required int Size { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Content type.
|
||||
/// </summary>
|
||||
public required string ContentType { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Base64-encoded chunk data (included only when includeData=true).
|
||||
/// </summary>
|
||||
public string? Data { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
namespace StellaOps.Provcache.Api;
|
||||
|
||||
/// <summary>
|
||||
/// Response model for GET /v1/proofs/{proofRoot}/manifest.
|
||||
/// </summary>
|
||||
public sealed class ProofManifestResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// The proof root (Merkle root).
|
||||
/// </summary>
|
||||
public required string ProofRoot { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Total number of chunks.
|
||||
/// </summary>
|
||||
public required int TotalChunks { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Total size of all evidence in bytes.
|
||||
/// </summary>
|
||||
public required long TotalSize { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Ordered list of chunk metadata (without data).
|
||||
/// </summary>
|
||||
public required IReadOnlyList<ChunkMetadataResponse> Chunks { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When the manifest was generated.
|
||||
/// </summary>
|
||||
public required DateTimeOffset GeneratedAt { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Response model for chunk metadata (without data).
|
||||
/// </summary>
|
||||
public sealed class ChunkMetadataResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Chunk identifier.
|
||||
/// </summary>
|
||||
public required Guid ChunkId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Zero-based index.
|
||||
/// </summary>
|
||||
public required int Index { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// SHA256 hash for verification.
|
||||
/// </summary>
|
||||
public required string Hash { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Size in bytes.
|
||||
/// </summary>
|
||||
public required int Size { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Content type.
|
||||
/// </summary>
|
||||
public required string ContentType { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
namespace StellaOps.Provcache.Api;
|
||||
|
||||
/// <summary>
|
||||
/// Response model for POST /v1/proofs/{proofRoot}/verify.
|
||||
/// </summary>
|
||||
public sealed class ProofVerificationResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// The proof root that was verified.
|
||||
/// </summary>
|
||||
public required string ProofRoot { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the Merkle tree is valid.
|
||||
/// </summary>
|
||||
public required bool IsValid { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Details about each chunk's verification.
|
||||
/// </summary>
|
||||
public IReadOnlyList<ChunkVerificationResult>? ChunkResults { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Error message if verification failed.
|
||||
/// </summary>
|
||||
public string? Error { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of verifying a single chunk.
|
||||
/// </summary>
|
||||
public sealed class ChunkVerificationResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Chunk index.
|
||||
/// </summary>
|
||||
public required int Index { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the chunk hash is valid.
|
||||
/// </summary>
|
||||
public required bool IsValid { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Expected hash from manifest.
|
||||
/// </summary>
|
||||
public required string ExpectedHash { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Computed hash from chunk data.
|
||||
/// </summary>
|
||||
public string? ComputedHash { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace StellaOps.Provcache.Api;
|
||||
|
||||
/// <summary>
|
||||
/// Marker class for logging in Proofs API endpoints.
|
||||
/// </summary>
|
||||
public sealed class ProofsApiEndpoints;
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace StellaOps.Provcache.Api;
|
||||
|
||||
/// <summary>
|
||||
/// Marker class for logging in Provcache API endpoints.
|
||||
/// </summary>
|
||||
public sealed class ProvcacheApiEndpoints;
|
||||
@@ -0,0 +1,33 @@
|
||||
namespace StellaOps.Provcache.Api;
|
||||
|
||||
/// <summary>
|
||||
/// Request model for POST /v1/provcache.
|
||||
/// </summary>
|
||||
public sealed class ProvcacheCreateRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// The cache entry to store.
|
||||
/// </summary>
|
||||
public ProvcacheEntry? Entry { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Response model for POST /v1/provcache.
|
||||
/// </summary>
|
||||
public sealed class ProvcacheCreateResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// The VeriKey that was stored.
|
||||
/// </summary>
|
||||
public required string VeriKey { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the store operation succeeded.
|
||||
/// </summary>
|
||||
public required bool Success { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When the entry expires.
|
||||
/// </summary>
|
||||
public DateTimeOffset ExpiresAt { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace StellaOps.Provcache.Api;
|
||||
|
||||
public static partial class ProvcacheEndpointExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// POST /v1/provcache
|
||||
/// </summary>
|
||||
private static async Task<IResult> CreateOrUpdateAsync(
|
||||
ProvcacheCreateRequest request,
|
||||
IProvcacheService provcacheService,
|
||||
ILogger<ProvcacheApiEndpoints> logger,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
logger.LogDebug("POST /v1/provcache for VeriKey {VeriKey}", request.Entry?.VeriKey);
|
||||
|
||||
if (request.Entry is null)
|
||||
{
|
||||
return Results.Problem(
|
||||
detail: "Request body must contain a valid entry",
|
||||
statusCode: StatusCodes.Status400BadRequest,
|
||||
title: "Invalid request");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var success = await provcacheService.SetAsync(request.Entry, cancellationToken);
|
||||
|
||||
if (!success)
|
||||
{
|
||||
return Results.Problem(
|
||||
detail: "Failed to store cache entry",
|
||||
statusCode: StatusCodes.Status500InternalServerError,
|
||||
title: "Cache write failed");
|
||||
}
|
||||
|
||||
return Results.Created($"/v1/provcache/{request.Entry.VeriKey}", new ProvcacheCreateResponse
|
||||
{
|
||||
VeriKey = request.Entry.VeriKey,
|
||||
Success = true,
|
||||
ExpiresAt = request.Entry.ExpiresAt
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error storing cache entry for VeriKey {VeriKey}", request.Entry?.VeriKey);
|
||||
return InternalError("Cache write failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace StellaOps.Provcache.Api;
|
||||
|
||||
public static partial class ProvcacheEndpointExtensions
|
||||
{
|
||||
private static IResult BadRequest(string detail, string title)
|
||||
{
|
||||
return Results.Problem(
|
||||
detail: detail,
|
||||
statusCode: StatusCodes.Status400BadRequest,
|
||||
title: title);
|
||||
}
|
||||
|
||||
private static IResult InternalError(string title)
|
||||
{
|
||||
return Results.Problem(
|
||||
detail: "An unexpected error occurred while processing the request.",
|
||||
statusCode: StatusCodes.Status500InternalServerError,
|
||||
title: title);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace StellaOps.Provcache.Api;
|
||||
|
||||
public static partial class ProvcacheEndpointExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// GET /v1/provcache/{veriKey}
|
||||
/// </summary>
|
||||
private static async Task<IResult> GetByVeriKeyAsync(
|
||||
string veriKey,
|
||||
bool? bypassCache,
|
||||
IProvcacheService provcacheService,
|
||||
ILogger<ProvcacheApiEndpoints> logger,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
logger.LogDebug("GET /v1/provcache/{VeriKey}", veriKey);
|
||||
|
||||
try
|
||||
{
|
||||
var result = await provcacheService.GetAsync(veriKey, bypassCache ?? false, cancellationToken);
|
||||
|
||||
return result.Status switch
|
||||
{
|
||||
ProvcacheResultStatus.CacheHit => Results.Ok(new ProvcacheGetResponse
|
||||
{
|
||||
VeriKey = result.Entry!.VeriKey,
|
||||
Entry = result.Entry,
|
||||
Source = result.Source,
|
||||
ElapsedMs = result.ElapsedMs,
|
||||
Status = "hit"
|
||||
}),
|
||||
ProvcacheResultStatus.Bypassed => Results.Ok(new ProvcacheGetResponse
|
||||
{
|
||||
VeriKey = veriKey,
|
||||
Entry = null,
|
||||
Source = null,
|
||||
ElapsedMs = result.ElapsedMs,
|
||||
Status = "bypassed"
|
||||
}),
|
||||
ProvcacheResultStatus.Expired => Results.StatusCode(StatusCodes.Status410Gone),
|
||||
_ => Results.NoContent()
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error getting cache entry for VeriKey {VeriKey}", veriKey);
|
||||
return InternalError("Cache lookup failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Globalization;
|
||||
|
||||
namespace StellaOps.Provcache.Api;
|
||||
|
||||
public static partial class ProvcacheEndpointExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// GET /v1/provcache/proofs/{proofRoot}
|
||||
/// </summary>
|
||||
private static async Task<IResult> GetEvidenceChunksAsync(
|
||||
string proofRoot,
|
||||
int? offset,
|
||||
int? limit,
|
||||
bool? includeData,
|
||||
[FromServices] IEvidenceChunkRepository chunkRepository,
|
||||
ILogger<ProofsApiEndpoints> logger,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
logger.LogDebug("GET /v1/provcache/proofs/{ProofRoot} offset={Offset} limit={Limit}", proofRoot, offset, limit);
|
||||
|
||||
try
|
||||
{
|
||||
if (offset is < 0)
|
||||
{
|
||||
return BadRequest("Offset must be zero or greater.", "Invalid pagination");
|
||||
}
|
||||
|
||||
if (limit is <= 0)
|
||||
{
|
||||
return BadRequest("Limit must be greater than zero.", "Invalid pagination");
|
||||
}
|
||||
|
||||
var startIndex = offset ?? 0;
|
||||
var pageSize = Math.Min(limit ?? DefaultPageSize, MaxPageSize);
|
||||
|
||||
// Get manifest for total count
|
||||
var manifest = await chunkRepository.GetManifestAsync(proofRoot, cancellationToken);
|
||||
if (manifest is null)
|
||||
{
|
||||
return Results.NotFound();
|
||||
}
|
||||
|
||||
if (startIndex >= manifest.TotalChunks)
|
||||
{
|
||||
return Results.Ok(new ProofEvidenceResponse
|
||||
{
|
||||
ProofRoot = proofRoot,
|
||||
TotalChunks = manifest.TotalChunks,
|
||||
TotalSize = manifest.TotalSize,
|
||||
Chunks = [],
|
||||
NextCursor = null,
|
||||
HasMore = false
|
||||
});
|
||||
}
|
||||
|
||||
// Get chunk range
|
||||
var chunks = await chunkRepository.GetChunkRangeAsync(proofRoot, startIndex, pageSize, cancellationToken);
|
||||
|
||||
var chunkResponses = chunks
|
||||
.OrderBy(c => c.ChunkIndex)
|
||||
.Select(c => new ProofChunkResponse
|
||||
{
|
||||
ChunkId = c.ChunkId,
|
||||
Index = c.ChunkIndex,
|
||||
Hash = c.ChunkHash,
|
||||
Size = c.BlobSize,
|
||||
ContentType = c.ContentType,
|
||||
Data = includeData == true ? Convert.ToBase64String(c.Blob) : null
|
||||
}).ToList();
|
||||
|
||||
var hasMore = startIndex + chunks.Count < manifest.TotalChunks;
|
||||
var nextCursor = hasMore
|
||||
? (startIndex + pageSize).ToString(CultureInfo.InvariantCulture)
|
||||
: null;
|
||||
|
||||
return Results.Ok(new ProofEvidenceResponse
|
||||
{
|
||||
ProofRoot = proofRoot,
|
||||
TotalChunks = manifest.TotalChunks,
|
||||
TotalSize = manifest.TotalSize,
|
||||
Chunks = chunkResponses,
|
||||
NextCursor = nextCursor,
|
||||
HasMore = hasMore
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error getting evidence chunks for proof root {ProofRoot}", proofRoot);
|
||||
return InternalError("Evidence retrieval failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace StellaOps.Provcache.Api;
|
||||
|
||||
public static partial class ProvcacheEndpointExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// GET /v1/provcache/{veriKey}/manifest
|
||||
/// </summary>
|
||||
private static async Task<IResult> GetInputManifestAsync(
|
||||
string veriKey,
|
||||
IProvcacheService provcacheService,
|
||||
TimeProvider timeProvider,
|
||||
ILogger<ProvcacheApiEndpoints> logger,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
logger.LogDebug("GET /v1/provcache/{VeriKey}/manifest", veriKey);
|
||||
|
||||
try
|
||||
{
|
||||
// First get the entry to verify it exists
|
||||
var result = await provcacheService.GetAsync(veriKey, bypassCache: false, cancellationToken);
|
||||
|
||||
if (result.Status == ProvcacheResultStatus.CacheMiss)
|
||||
{
|
||||
return Results.NotFound(new ProblemDetails
|
||||
{
|
||||
Title = "Entry not found",
|
||||
Detail = $"No cache entry found for VeriKey: {veriKey}",
|
||||
Status = StatusCodes.Status404NotFound
|
||||
});
|
||||
}
|
||||
|
||||
if (result.Status == ProvcacheResultStatus.Expired)
|
||||
{
|
||||
return Results.NotFound(new ProblemDetails
|
||||
{
|
||||
Title = "Entry expired",
|
||||
Detail = $"Cache entry for VeriKey '{veriKey}' has expired",
|
||||
Status = StatusCodes.Status404NotFound
|
||||
});
|
||||
}
|
||||
|
||||
var entry = result.Entry;
|
||||
if (entry is null)
|
||||
{
|
||||
return Results.NotFound(new ProblemDetails
|
||||
{
|
||||
Title = "Entry not found",
|
||||
Detail = $"No cache entry found for VeriKey: {veriKey}",
|
||||
Status = StatusCodes.Status404NotFound
|
||||
});
|
||||
}
|
||||
|
||||
// Build the input manifest from the entry metadata
|
||||
// In a full implementation, we'd resolve these hashes to more detailed metadata
|
||||
// from their respective stores (SBOM store, VEX store, policy registry, etc.)
|
||||
var manifest = BuildInputManifest(entry, timeProvider);
|
||||
|
||||
return Results.Ok(manifest);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error getting input manifest for VeriKey {VeriKey}", veriKey);
|
||||
return InternalError("Manifest retrieval failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace StellaOps.Provcache.Api;
|
||||
|
||||
public static partial class ProvcacheEndpointExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// GET /v1/provcache/metrics
|
||||
/// </summary>
|
||||
private static async Task<IResult> GetMetricsAsync(
|
||||
IProvcacheService provcacheService,
|
||||
ILogger<ProvcacheApiEndpoints> logger,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
logger.LogDebug("GET /v1/provcache/metrics");
|
||||
|
||||
try
|
||||
{
|
||||
var metrics = await provcacheService.GetMetricsAsync(cancellationToken);
|
||||
|
||||
var hitRate = metrics.TotalRequests > 0
|
||||
? (double)metrics.TotalHits / metrics.TotalRequests
|
||||
: 0;
|
||||
|
||||
return Results.Ok(new ProvcacheMetricsResponse
|
||||
{
|
||||
TotalRequests = metrics.TotalRequests,
|
||||
TotalHits = metrics.TotalHits,
|
||||
TotalMisses = metrics.TotalMisses,
|
||||
TotalInvalidations = metrics.TotalInvalidations,
|
||||
HitRate = hitRate,
|
||||
CurrentEntryCount = metrics.CurrentEntryCount,
|
||||
AvgLatencyMs = metrics.AvgLatencyMs,
|
||||
P99LatencyMs = metrics.P99LatencyMs,
|
||||
ValkeyCacheHealthy = metrics.ValkeyCacheHealthy,
|
||||
PostgresRepositoryHealthy = metrics.PostgresRepositoryHealthy,
|
||||
CollectedAt = metrics.CollectedAt
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error getting cache metrics");
|
||||
return InternalError("Metrics retrieval failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace StellaOps.Provcache.Api;
|
||||
|
||||
public static partial class ProvcacheEndpointExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// GET /v1/provcache/proofs/{proofRoot}/manifest
|
||||
/// </summary>
|
||||
private static async Task<IResult> GetProofManifestAsync(
|
||||
string proofRoot,
|
||||
[FromServices] IEvidenceChunkRepository chunkRepository,
|
||||
ILogger<ProofsApiEndpoints> logger,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
logger.LogDebug("GET /v1/provcache/proofs/{ProofRoot}/manifest", proofRoot);
|
||||
|
||||
try
|
||||
{
|
||||
var manifest = await chunkRepository.GetManifestAsync(proofRoot, cancellationToken);
|
||||
if (manifest is null)
|
||||
{
|
||||
return Results.NotFound();
|
||||
}
|
||||
|
||||
var chunkMetadata = manifest.Chunks
|
||||
.OrderBy(c => c.Index)
|
||||
.Select(c => new ChunkMetadataResponse
|
||||
{
|
||||
ChunkId = c.ChunkId,
|
||||
Index = c.Index,
|
||||
Hash = c.Hash,
|
||||
Size = c.Size,
|
||||
ContentType = c.ContentType
|
||||
}).ToList();
|
||||
|
||||
return Results.Ok(new ProofManifestResponse
|
||||
{
|
||||
ProofRoot = proofRoot,
|
||||
TotalChunks = manifest.TotalChunks,
|
||||
TotalSize = manifest.TotalSize,
|
||||
Chunks = chunkMetadata,
|
||||
GeneratedAt = manifest.GeneratedAt
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error getting manifest for proof root {ProofRoot}", proofRoot);
|
||||
return InternalError("Manifest retrieval failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace StellaOps.Provcache.Api;
|
||||
|
||||
public static partial class ProvcacheEndpointExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// GET /v1/provcache/proofs/{proofRoot}/chunks/{chunkIndex}
|
||||
/// </summary>
|
||||
private static async Task<IResult> GetSingleChunkAsync(
|
||||
string proofRoot,
|
||||
int chunkIndex,
|
||||
[FromServices] IEvidenceChunkRepository chunkRepository,
|
||||
ILogger<ProofsApiEndpoints> logger,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
logger.LogDebug("GET /v1/provcache/proofs/{ProofRoot}/chunks/{ChunkIndex}", proofRoot, chunkIndex);
|
||||
|
||||
try
|
||||
{
|
||||
var chunk = await chunkRepository.GetChunkAsync(proofRoot, chunkIndex, cancellationToken);
|
||||
if (chunk is null)
|
||||
{
|
||||
return Results.NotFound();
|
||||
}
|
||||
|
||||
return Results.Ok(new ProofChunkResponse
|
||||
{
|
||||
ChunkId = chunk.ChunkId,
|
||||
Index = chunk.ChunkIndex,
|
||||
Hash = chunk.ChunkHash,
|
||||
Size = chunk.BlobSize,
|
||||
ContentType = chunk.ContentType,
|
||||
Data = Convert.ToBase64String(chunk.Blob)
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error getting chunk {ChunkIndex} for proof root {ProofRoot}", chunkIndex, proofRoot);
|
||||
return InternalError("Chunk retrieval failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
namespace StellaOps.Provcache.Api;
|
||||
|
||||
public static partial class ProvcacheEndpointExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Builds an InputManifestResponse from a ProvcacheEntry.
|
||||
/// </summary>
|
||||
private static InputManifestResponse BuildInputManifest(ProvcacheEntry entry, TimeProvider timeProvider)
|
||||
{
|
||||
// Build input manifest from the entry and its embedded DecisionDigest
|
||||
// The DecisionDigest contains the VeriKey components as hashes
|
||||
var placeholderHash = BuildPlaceholderHash(entry.VeriKey);
|
||||
|
||||
return new InputManifestResponse
|
||||
{
|
||||
VeriKey = entry.VeriKey,
|
||||
SourceArtifact = new SourceArtifactInfo
|
||||
{
|
||||
// VeriKey includes source hash as first component
|
||||
Digest = entry.VeriKey,
|
||||
},
|
||||
Sbom = new SbomInfoDto
|
||||
{
|
||||
// SBOM hash is embedded in VeriKey computation
|
||||
// In a full implementation, we'd resolve this from the SBOM store
|
||||
Hash = placeholderHash, // Placeholder - actual hash would come from VeriKey decomposition
|
||||
},
|
||||
Vex = new VexInfoDto
|
||||
{
|
||||
// VEX hash set is embedded in VeriKey computation
|
||||
HashSetHash = placeholderHash, // Placeholder
|
||||
StatementCount = 0, // Would be resolved from VEX store
|
||||
},
|
||||
Policy = new PolicyInfoDto
|
||||
{
|
||||
Hash = entry.PolicyHash,
|
||||
},
|
||||
Signers = new SignerInfoDto
|
||||
{
|
||||
SetHash = entry.SignerSetHash,
|
||||
SignerCount = 0, // Would be resolved from signer registry
|
||||
},
|
||||
TimeWindow = new TimeWindowInfoDto
|
||||
{
|
||||
Bucket = entry.FeedEpoch,
|
||||
StartsAt = entry.CreatedAt,
|
||||
EndsAt = entry.ExpiresAt,
|
||||
},
|
||||
GeneratedAt = timeProvider.GetUtcNow(),
|
||||
};
|
||||
}
|
||||
|
||||
private static string BuildPlaceholderHash(string veriKey)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(veriKey))
|
||||
{
|
||||
return "sha256:unknown";
|
||||
}
|
||||
|
||||
var trimmed = veriKey;
|
||||
if (trimmed.StartsWith("sha256:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
trimmed = trimmed["sha256:".Length..];
|
||||
}
|
||||
|
||||
if (trimmed.Length < 32)
|
||||
{
|
||||
return "sha256:unknown";
|
||||
}
|
||||
|
||||
return $"sha256:{trimmed[..32]}...";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace StellaOps.Provcache.Api;
|
||||
|
||||
public static partial class ProvcacheEndpointExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// POST /v1/provcache/invalidate
|
||||
/// </summary>
|
||||
private static async Task<IResult> InvalidateAsync(
|
||||
ProvcacheInvalidateRequest request,
|
||||
IProvcacheService provcacheService,
|
||||
ILogger<ProvcacheApiEndpoints> logger,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
logger.LogDebug("POST /v1/provcache/invalidate type={Type} value={Value}", request.Type, request.Value);
|
||||
|
||||
try
|
||||
{
|
||||
// If single VeriKey invalidation (Type is null = single VeriKey mode)
|
||||
if (request.Type is null)
|
||||
{
|
||||
var success = await provcacheService.InvalidateAsync(request.Value, request.Reason, cancellationToken);
|
||||
return Results.Ok(new ProvcacheInvalidateResponse
|
||||
{
|
||||
EntriesAffected = success ? 1 : 0,
|
||||
Type = "verikey",
|
||||
Value = request.Value,
|
||||
Reason = request.Reason
|
||||
});
|
||||
}
|
||||
|
||||
// Bulk invalidation
|
||||
var invalidationRequest = new InvalidationRequest
|
||||
{
|
||||
Type = request.Type ?? InvalidationType.Pattern,
|
||||
Value = request.Value,
|
||||
Reason = request.Reason,
|
||||
Actor = request.Actor
|
||||
};
|
||||
|
||||
var result = await provcacheService.InvalidateByAsync(invalidationRequest, cancellationToken);
|
||||
return Results.Ok(new ProvcacheInvalidateResponse
|
||||
{
|
||||
EntriesAffected = result.EntriesAffected,
|
||||
Type = request.Type?.ToString() ?? "pattern",
|
||||
Value = request.Value,
|
||||
Reason = request.Reason
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error invalidating cache entries");
|
||||
return InternalError("Cache invalidation failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
#pragma warning disable ASPDEPR002 // WithOpenApi is deprecated - will migrate to new OpenAPI approach
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace StellaOps.Provcache.Api;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for mapping Provcache API endpoints.
|
||||
/// </summary>
|
||||
public static partial class ProvcacheEndpointExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Maps Provcache API endpoints to the specified route builder.
|
||||
/// </summary>
|
||||
/// <param name="endpoints">The endpoint route builder.</param>
|
||||
/// <param name="prefix">The route prefix (default: "/v1/provcache").</param>
|
||||
/// <returns>A route group builder for further customization.</returns>
|
||||
public static RouteGroupBuilder MapProvcacheEndpoints(
|
||||
this IEndpointRouteBuilder endpoints,
|
||||
string prefix = "/v1/provcache")
|
||||
{
|
||||
var group = endpoints.MapGroup(prefix)
|
||||
.WithTags("Provcache")
|
||||
.WithOpenApi();
|
||||
|
||||
MapCoreEndpoints(group);
|
||||
|
||||
var proofsGroup = endpoints.MapGroup($"{prefix}/proofs")
|
||||
.WithTags("Provcache Evidence")
|
||||
.WithOpenApi();
|
||||
|
||||
MapProofEndpoints(proofsGroup);
|
||||
|
||||
return group;
|
||||
}
|
||||
}
|
||||
#pragma warning restore ASPDEPR002
|
||||
@@ -0,0 +1,59 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
|
||||
namespace StellaOps.Provcache.Api;
|
||||
|
||||
public static partial class ProvcacheEndpointExtensions
|
||||
{
|
||||
private static void MapCoreEndpoints(RouteGroupBuilder group)
|
||||
{
|
||||
// GET /v1/provcache/{veriKey}
|
||||
group.MapGet("/{veriKey}", GetByVeriKeyAsync)
|
||||
.WithName("GetProvcacheEntry")
|
||||
.WithSummary("Get a cached decision by VeriKey")
|
||||
.WithDescription("Retrieves a cached evaluation decision by its VeriKey. Returns 200 if found, 204 if not cached, 410 if expired.")
|
||||
.Produces<ProvcacheGetResponse>(StatusCodes.Status200OK)
|
||||
.Produces(StatusCodes.Status204NoContent)
|
||||
.Produces(StatusCodes.Status410Gone)
|
||||
.Produces<ProblemDetails>(StatusCodes.Status500InternalServerError);
|
||||
|
||||
// POST /v1/provcache
|
||||
group.MapPost("/", CreateOrUpdateAsync)
|
||||
.WithName("CreateOrUpdateProvcacheEntry")
|
||||
.WithSummary("Store a decision in the cache (idempotent)")
|
||||
.WithDescription("Stores or updates a cached evaluation decision. This operation is idempotent - storing the same VeriKey multiple times is safe.")
|
||||
.Accepts<ProvcacheCreateRequest>("application/json")
|
||||
.Produces<ProvcacheCreateResponse>(StatusCodes.Status201Created)
|
||||
.Produces<ProvcacheCreateResponse>(StatusCodes.Status200OK)
|
||||
.Produces<ProblemDetails>(StatusCodes.Status400BadRequest)
|
||||
.Produces<ProblemDetails>(StatusCodes.Status500InternalServerError);
|
||||
|
||||
// POST /v1/provcache/invalidate
|
||||
group.MapPost("/invalidate", InvalidateAsync)
|
||||
.WithName("InvalidateProvcacheEntries")
|
||||
.WithSummary("Invalidate cache entries by key or pattern")
|
||||
.WithDescription("Invalidates one or more cache entries. Can invalidate by exact VeriKey, policy hash, signer set hash, feed epoch, or pattern.")
|
||||
.Accepts<ProvcacheInvalidateRequest>("application/json")
|
||||
.Produces<ProvcacheInvalidateResponse>(StatusCodes.Status200OK)
|
||||
.Produces<ProblemDetails>(StatusCodes.Status400BadRequest)
|
||||
.Produces<ProblemDetails>(StatusCodes.Status500InternalServerError);
|
||||
|
||||
// GET /v1/provcache/metrics
|
||||
group.MapGet("/metrics", GetMetricsAsync)
|
||||
.WithName("GetProvcacheMetrics")
|
||||
.WithSummary("Get cache performance metrics")
|
||||
.WithDescription("Returns current cache metrics including hit rate, miss rate, latency percentiles, and entry counts.")
|
||||
.Produces<ProvcacheMetricsResponse>(StatusCodes.Status200OK)
|
||||
.Produces<ProblemDetails>(StatusCodes.Status500InternalServerError);
|
||||
|
||||
// GET /v1/provcache/{veriKey}/manifest
|
||||
group.MapGet("/{veriKey}/manifest", GetInputManifestAsync)
|
||||
.WithName("GetInputManifest")
|
||||
.WithSummary("Get input manifest for VeriKey components")
|
||||
.WithDescription("Returns detailed information about the inputs (SBOM, VEX, policy, signers) that formed a cached decision. Use for transparency and debugging.")
|
||||
.Produces<InputManifestResponse>(StatusCodes.Status200OK)
|
||||
.Produces(StatusCodes.Status404NotFound)
|
||||
.Produces<ProblemDetails>(StatusCodes.Status500InternalServerError);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
|
||||
namespace StellaOps.Provcache.Api;
|
||||
|
||||
public static partial class ProvcacheEndpointExtensions
|
||||
{
|
||||
private static void MapProofEndpoints(RouteGroupBuilder proofsGroup)
|
||||
{
|
||||
// GET /v1/provcache/proofs/{proofRoot}
|
||||
proofsGroup.MapGet("/{proofRoot}", GetEvidenceChunksAsync)
|
||||
.WithName("GetProofEvidence")
|
||||
.WithSummary("Get evidence chunks by proof root")
|
||||
.WithDescription("Retrieves evidence chunks for a proof root with pagination support. Use cursor parameter for subsequent pages.")
|
||||
.Produces<ProofEvidenceResponse>(StatusCodes.Status200OK)
|
||||
.Produces(StatusCodes.Status404NotFound)
|
||||
.Produces<ProblemDetails>(StatusCodes.Status500InternalServerError);
|
||||
|
||||
// GET /v1/provcache/proofs/{proofRoot}/manifest
|
||||
proofsGroup.MapGet("/{proofRoot}/manifest", GetProofManifestAsync)
|
||||
.WithName("GetProofManifest")
|
||||
.WithSummary("Get chunk manifest (metadata without data)")
|
||||
.WithDescription("Retrieves the chunk manifest for lazy evidence fetching. Contains hashes and sizes but no blob data.")
|
||||
.Produces<ProofManifestResponse>(StatusCodes.Status200OK)
|
||||
.Produces(StatusCodes.Status404NotFound)
|
||||
.Produces<ProblemDetails>(StatusCodes.Status500InternalServerError);
|
||||
|
||||
// GET /v1/provcache/proofs/{proofRoot}/chunks/{chunkIndex}
|
||||
proofsGroup.MapGet("/{proofRoot}/chunks/{chunkIndex:int}", GetSingleChunkAsync)
|
||||
.WithName("GetProofChunk")
|
||||
.WithSummary("Get a single chunk by index")
|
||||
.WithDescription("Retrieves a specific chunk by its index within the proof.")
|
||||
.Produces<ProofChunkResponse>(StatusCodes.Status200OK)
|
||||
.Produces(StatusCodes.Status404NotFound)
|
||||
.Produces<ProblemDetails>(StatusCodes.Status500InternalServerError);
|
||||
|
||||
// POST /v1/provcache/proofs/{proofRoot}/verify
|
||||
proofsGroup.MapPost("/{proofRoot}/verify", VerifyProofAsync)
|
||||
.WithName("VerifyProof")
|
||||
.WithSummary("Verify Merkle tree integrity")
|
||||
.WithDescription("Verifies all chunk hashes and the Merkle tree for the proof root.")
|
||||
.Produces<ProofVerificationResponse>(StatusCodes.Status200OK)
|
||||
.Produces(StatusCodes.Status404NotFound)
|
||||
.Produces<ProblemDetails>(StatusCodes.Status500InternalServerError);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace StellaOps.Provcache.Api;
|
||||
|
||||
public static partial class ProvcacheEndpointExtensions
|
||||
{
|
||||
private const int DefaultPageSize = 10;
|
||||
private const int MaxPageSize = 100;
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace StellaOps.Provcache.Api;
|
||||
|
||||
public static partial class ProvcacheEndpointExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// POST /v1/provcache/proofs/{proofRoot}/verify
|
||||
/// </summary>
|
||||
private static async Task<IResult> VerifyProofAsync(
|
||||
string proofRoot,
|
||||
[FromServices] IEvidenceChunkRepository chunkRepository,
|
||||
[FromServices] IEvidenceChunker chunker,
|
||||
ILogger<ProofsApiEndpoints> logger,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
logger.LogDebug("POST /v1/provcache/proofs/{ProofRoot}/verify", proofRoot);
|
||||
|
||||
try
|
||||
{
|
||||
var chunks = await chunkRepository.GetChunksAsync(proofRoot, cancellationToken);
|
||||
if (chunks.Count == 0)
|
||||
{
|
||||
return Results.NotFound();
|
||||
}
|
||||
|
||||
var orderedChunks = chunks.OrderBy(c => c.ChunkIndex).ToList();
|
||||
var chunkResults = new List<ChunkVerificationResult>();
|
||||
var allValid = true;
|
||||
|
||||
foreach (var chunk in orderedChunks)
|
||||
{
|
||||
var isValid = chunker.VerifyChunk(chunk);
|
||||
var computedHash = isValid ? chunk.ChunkHash : ComputeChunkHash(chunk.Blob);
|
||||
|
||||
chunkResults.Add(new ChunkVerificationResult
|
||||
{
|
||||
Index = chunk.ChunkIndex,
|
||||
IsValid = isValid,
|
||||
ExpectedHash = chunk.ChunkHash,
|
||||
ComputedHash = isValid ? null : computedHash
|
||||
});
|
||||
|
||||
if (!isValid)
|
||||
{
|
||||
allValid = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Verify Merkle root
|
||||
var chunkHashes = orderedChunks.Select(c => c.ChunkHash).ToList();
|
||||
var computedRoot = chunker.ComputeMerkleRoot(chunkHashes);
|
||||
var rootMatches = string.Equals(computedRoot, proofRoot, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
return Results.Ok(new ProofVerificationResponse
|
||||
{
|
||||
ProofRoot = proofRoot,
|
||||
IsValid = allValid && rootMatches,
|
||||
ChunkResults = chunkResults,
|
||||
Error = !rootMatches ? $"Merkle root mismatch. Expected: {proofRoot}, Computed: {computedRoot}" : null
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error verifying proof root {ProofRoot}", proofRoot);
|
||||
return InternalError("Proof verification failed");
|
||||
}
|
||||
}
|
||||
|
||||
private static string ComputeChunkHash(byte[] data)
|
||||
{
|
||||
var hash = System.Security.Cryptography.SHA256.HashData(data);
|
||||
return $"sha256:{Convert.ToHexStringLower(hash)}";
|
||||
}
|
||||
}
|
||||
@@ -1,719 +0,0 @@
|
||||
#pragma warning disable ASPDEPR002 // WithOpenApi is deprecated - will migrate to new OpenAPI approach
|
||||
|
||||
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Globalization;
|
||||
|
||||
namespace StellaOps.Provcache.Api;
|
||||
|
||||
/// <summary>
|
||||
/// Marker class for logging in Provcache API endpoints.
|
||||
/// </summary>
|
||||
public sealed class ProvcacheApiEndpoints;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for mapping Provcache API endpoints.
|
||||
/// </summary>
|
||||
public static partial class ProvcacheEndpointExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Maps Provcache API endpoints to the specified route builder.
|
||||
/// </summary>
|
||||
/// <param name="endpoints">The endpoint route builder.</param>
|
||||
/// <param name="prefix">The route prefix (default: "/v1/provcache").</param>
|
||||
/// <returns>A route group builder for further customization.</returns>
|
||||
public static RouteGroupBuilder MapProvcacheEndpoints(
|
||||
this IEndpointRouteBuilder endpoints,
|
||||
string prefix = "/v1/provcache")
|
||||
{
|
||||
var group = endpoints.MapGroup(prefix)
|
||||
.WithTags("Provcache")
|
||||
.WithOpenApi();
|
||||
|
||||
// GET /v1/provcache/{veriKey}
|
||||
group.MapGet("/{veriKey}", GetByVeriKey)
|
||||
.WithName("GetProvcacheEntry")
|
||||
.WithSummary("Get a cached decision by VeriKey")
|
||||
.WithDescription("Retrieves a cached evaluation decision by its VeriKey. Returns 200 if found, 204 if not cached, 410 if expired.")
|
||||
.Produces<ProvcacheGetResponse>(StatusCodes.Status200OK)
|
||||
.Produces(StatusCodes.Status204NoContent)
|
||||
.Produces(StatusCodes.Status410Gone)
|
||||
.Produces<ProblemDetails>(StatusCodes.Status500InternalServerError);
|
||||
|
||||
// POST /v1/provcache
|
||||
group.MapPost("/", CreateOrUpdate)
|
||||
.WithName("CreateOrUpdateProvcacheEntry")
|
||||
.WithSummary("Store a decision in the cache (idempotent)")
|
||||
.WithDescription("Stores or updates a cached evaluation decision. This operation is idempotent - storing the same VeriKey multiple times is safe.")
|
||||
.Accepts<ProvcacheCreateRequest>("application/json")
|
||||
.Produces<ProvcacheCreateResponse>(StatusCodes.Status201Created)
|
||||
.Produces<ProvcacheCreateResponse>(StatusCodes.Status200OK)
|
||||
.Produces<ProblemDetails>(StatusCodes.Status400BadRequest)
|
||||
.Produces<ProblemDetails>(StatusCodes.Status500InternalServerError);
|
||||
|
||||
// POST /v1/provcache/invalidate
|
||||
group.MapPost("/invalidate", Invalidate)
|
||||
.WithName("InvalidateProvcacheEntries")
|
||||
.WithSummary("Invalidate cache entries by key or pattern")
|
||||
.WithDescription("Invalidates one or more cache entries. Can invalidate by exact VeriKey, policy hash, signer set hash, feed epoch, or pattern.")
|
||||
.Accepts<ProvcacheInvalidateRequest>("application/json")
|
||||
.Produces<ProvcacheInvalidateResponse>(StatusCodes.Status200OK)
|
||||
.Produces<ProblemDetails>(StatusCodes.Status400BadRequest)
|
||||
.Produces<ProblemDetails>(StatusCodes.Status500InternalServerError);
|
||||
|
||||
// GET /v1/provcache/metrics
|
||||
group.MapGet("/metrics", GetMetrics)
|
||||
.WithName("GetProvcacheMetrics")
|
||||
.WithSummary("Get cache performance metrics")
|
||||
.WithDescription("Returns current cache metrics including hit rate, miss rate, latency percentiles, and entry counts.")
|
||||
.Produces<ProvcacheMetricsResponse>(StatusCodes.Status200OK)
|
||||
.Produces<ProblemDetails>(StatusCodes.Status500InternalServerError);
|
||||
|
||||
// GET /v1/provcache/{veriKey}/manifest
|
||||
group.MapGet("/{veriKey}/manifest", GetInputManifest)
|
||||
.WithName("GetInputManifest")
|
||||
.WithSummary("Get input manifest for VeriKey components")
|
||||
.WithDescription("Returns detailed information about the inputs (SBOM, VEX, policy, signers) that formed a cached decision. Use for transparency and debugging.")
|
||||
.Produces<InputManifestResponse>(StatusCodes.Status200OK)
|
||||
.Produces(StatusCodes.Status404NotFound)
|
||||
.Produces<ProblemDetails>(StatusCodes.Status500InternalServerError);
|
||||
|
||||
// Map evidence paging endpoints under /proofs
|
||||
var proofsGroup = endpoints.MapGroup($"{prefix}/proofs")
|
||||
.WithTags("Provcache Evidence")
|
||||
.WithOpenApi();
|
||||
|
||||
// GET /v1/provcache/proofs/{proofRoot}
|
||||
proofsGroup.MapGet("/{proofRoot}", GetEvidenceChunks)
|
||||
.WithName("GetProofEvidence")
|
||||
.WithSummary("Get evidence chunks by proof root")
|
||||
.WithDescription("Retrieves evidence chunks for a proof root with pagination support. Use cursor parameter for subsequent pages.")
|
||||
.Produces<ProofEvidenceResponse>(StatusCodes.Status200OK)
|
||||
.Produces(StatusCodes.Status404NotFound)
|
||||
.Produces<ProblemDetails>(StatusCodes.Status500InternalServerError);
|
||||
|
||||
// GET /v1/provcache/proofs/{proofRoot}/manifest
|
||||
proofsGroup.MapGet("/{proofRoot}/manifest", GetProofManifest)
|
||||
.WithName("GetProofManifest")
|
||||
.WithSummary("Get chunk manifest (metadata without data)")
|
||||
.WithDescription("Retrieves the chunk manifest for lazy evidence fetching. Contains hashes and sizes but no blob data.")
|
||||
.Produces<ProofManifestResponse>(StatusCodes.Status200OK)
|
||||
.Produces(StatusCodes.Status404NotFound)
|
||||
.Produces<ProblemDetails>(StatusCodes.Status500InternalServerError);
|
||||
|
||||
// GET /v1/provcache/proofs/{proofRoot}/chunks/{chunkIndex}
|
||||
proofsGroup.MapGet("/{proofRoot}/chunks/{chunkIndex:int}", GetSingleChunk)
|
||||
.WithName("GetProofChunk")
|
||||
.WithSummary("Get a single chunk by index")
|
||||
.WithDescription("Retrieves a specific chunk by its index within the proof.")
|
||||
.Produces<ProofChunkResponse>(StatusCodes.Status200OK)
|
||||
.Produces(StatusCodes.Status404NotFound)
|
||||
.Produces<ProblemDetails>(StatusCodes.Status500InternalServerError);
|
||||
|
||||
// POST /v1/provcache/proofs/{proofRoot}/verify
|
||||
proofsGroup.MapPost("/{proofRoot}/verify", VerifyProof)
|
||||
.WithName("VerifyProof")
|
||||
.WithSummary("Verify Merkle tree integrity")
|
||||
.WithDescription("Verifies all chunk hashes and the Merkle tree for the proof root.")
|
||||
.Produces<ProofVerificationResponse>(StatusCodes.Status200OK)
|
||||
.Produces(StatusCodes.Status404NotFound)
|
||||
.Produces<ProblemDetails>(StatusCodes.Status500InternalServerError);
|
||||
|
||||
return group;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// GET /v1/provcache/{veriKey}
|
||||
/// </summary>
|
||||
private static async Task<IResult> GetByVeriKey(
|
||||
string veriKey,
|
||||
bool? bypassCache,
|
||||
IProvcacheService provcacheService,
|
||||
ILogger<ProvcacheApiEndpoints> logger,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
logger.LogDebug("GET /v1/provcache/{VeriKey}", veriKey);
|
||||
|
||||
try
|
||||
{
|
||||
var result = await provcacheService.GetAsync(veriKey, bypassCache ?? false, cancellationToken);
|
||||
|
||||
return result.Status switch
|
||||
{
|
||||
ProvcacheResultStatus.CacheHit => Results.Ok(new ProvcacheGetResponse
|
||||
{
|
||||
VeriKey = result.Entry!.VeriKey,
|
||||
Entry = result.Entry,
|
||||
Source = result.Source,
|
||||
ElapsedMs = result.ElapsedMs,
|
||||
Status = "hit"
|
||||
}),
|
||||
ProvcacheResultStatus.Bypassed => Results.Ok(new ProvcacheGetResponse
|
||||
{
|
||||
VeriKey = veriKey,
|
||||
Entry = null,
|
||||
Source = null,
|
||||
ElapsedMs = result.ElapsedMs,
|
||||
Status = "bypassed"
|
||||
}),
|
||||
ProvcacheResultStatus.Expired => Results.StatusCode(StatusCodes.Status410Gone),
|
||||
_ => Results.NoContent()
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error getting cache entry for VeriKey {VeriKey}", veriKey);
|
||||
return InternalError("Cache lookup failed");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// POST /v1/provcache
|
||||
/// </summary>
|
||||
private static async Task<IResult> CreateOrUpdate(
|
||||
ProvcacheCreateRequest request,
|
||||
IProvcacheService provcacheService,
|
||||
ILogger<ProvcacheApiEndpoints> logger,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
logger.LogDebug("POST /v1/provcache for VeriKey {VeriKey}", request.Entry?.VeriKey);
|
||||
|
||||
if (request.Entry is null)
|
||||
{
|
||||
return Results.Problem(
|
||||
detail: "Request body must contain a valid entry",
|
||||
statusCode: StatusCodes.Status400BadRequest,
|
||||
title: "Invalid request");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var success = await provcacheService.SetAsync(request.Entry, cancellationToken);
|
||||
|
||||
if (!success)
|
||||
{
|
||||
return Results.Problem(
|
||||
detail: "Failed to store cache entry",
|
||||
statusCode: StatusCodes.Status500InternalServerError,
|
||||
title: "Cache write failed");
|
||||
}
|
||||
|
||||
return Results.Created($"/v1/provcache/{request.Entry.VeriKey}", new ProvcacheCreateResponse
|
||||
{
|
||||
VeriKey = request.Entry.VeriKey,
|
||||
Success = true,
|
||||
ExpiresAt = request.Entry.ExpiresAt
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error storing cache entry for VeriKey {VeriKey}", request.Entry?.VeriKey);
|
||||
return InternalError("Cache write failed");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// POST /v1/provcache/invalidate
|
||||
/// </summary>
|
||||
private static async Task<IResult> Invalidate(
|
||||
ProvcacheInvalidateRequest request,
|
||||
IProvcacheService provcacheService,
|
||||
ILogger<ProvcacheApiEndpoints> logger,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
logger.LogDebug("POST /v1/provcache/invalidate type={Type} value={Value}", request.Type, request.Value);
|
||||
|
||||
try
|
||||
{
|
||||
// If single VeriKey invalidation (Type is null = single VeriKey mode)
|
||||
if (request.Type is null)
|
||||
{
|
||||
var success = await provcacheService.InvalidateAsync(request.Value, request.Reason, cancellationToken);
|
||||
return Results.Ok(new ProvcacheInvalidateResponse
|
||||
{
|
||||
EntriesAffected = success ? 1 : 0,
|
||||
Type = "verikey",
|
||||
Value = request.Value,
|
||||
Reason = request.Reason
|
||||
});
|
||||
}
|
||||
|
||||
// Bulk invalidation
|
||||
var invalidationRequest = new InvalidationRequest
|
||||
{
|
||||
Type = request.Type ?? InvalidationType.Pattern,
|
||||
Value = request.Value,
|
||||
Reason = request.Reason,
|
||||
Actor = request.Actor
|
||||
};
|
||||
|
||||
var result = await provcacheService.InvalidateByAsync(invalidationRequest, cancellationToken);
|
||||
|
||||
return Results.Ok(new ProvcacheInvalidateResponse
|
||||
{
|
||||
EntriesAffected = result.EntriesAffected,
|
||||
Type = request.Type?.ToString() ?? "pattern",
|
||||
Value = request.Value,
|
||||
Reason = request.Reason
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error invalidating cache entries");
|
||||
return InternalError("Cache invalidation failed");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// GET /v1/provcache/metrics
|
||||
/// </summary>
|
||||
private static async Task<IResult> GetMetrics(
|
||||
IProvcacheService provcacheService,
|
||||
ILogger<ProvcacheApiEndpoints> logger,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
logger.LogDebug("GET /v1/provcache/metrics");
|
||||
|
||||
try
|
||||
{
|
||||
var metrics = await provcacheService.GetMetricsAsync(cancellationToken);
|
||||
|
||||
var hitRate = metrics.TotalRequests > 0
|
||||
? (double)metrics.TotalHits / metrics.TotalRequests
|
||||
: 0;
|
||||
|
||||
return Results.Ok(new ProvcacheMetricsResponse
|
||||
{
|
||||
TotalRequests = metrics.TotalRequests,
|
||||
TotalHits = metrics.TotalHits,
|
||||
TotalMisses = metrics.TotalMisses,
|
||||
TotalInvalidations = metrics.TotalInvalidations,
|
||||
HitRate = hitRate,
|
||||
CurrentEntryCount = metrics.CurrentEntryCount,
|
||||
AvgLatencyMs = metrics.AvgLatencyMs,
|
||||
P99LatencyMs = metrics.P99LatencyMs,
|
||||
ValkeyCacheHealthy = metrics.ValkeyCacheHealthy,
|
||||
PostgresRepositoryHealthy = metrics.PostgresRepositoryHealthy,
|
||||
CollectedAt = metrics.CollectedAt
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error getting cache metrics");
|
||||
return InternalError("Metrics retrieval failed");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// GET /v1/provcache/{veriKey}/manifest
|
||||
/// </summary>
|
||||
private static async Task<IResult> GetInputManifest(
|
||||
string veriKey,
|
||||
IProvcacheService provcacheService,
|
||||
TimeProvider timeProvider,
|
||||
ILogger<ProvcacheApiEndpoints> logger,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
logger.LogDebug("GET /v1/provcache/{VeriKey}/manifest", veriKey);
|
||||
|
||||
try
|
||||
{
|
||||
// First get the entry to verify it exists
|
||||
var result = await provcacheService.GetAsync(veriKey, bypassCache: false, cancellationToken);
|
||||
|
||||
if (result.Status == ProvcacheResultStatus.CacheMiss)
|
||||
{
|
||||
return Results.NotFound(new ProblemDetails
|
||||
{
|
||||
Title = "Entry not found",
|
||||
Detail = $"No cache entry found for VeriKey: {veriKey}",
|
||||
Status = StatusCodes.Status404NotFound
|
||||
});
|
||||
}
|
||||
|
||||
if (result.Status == ProvcacheResultStatus.Expired)
|
||||
{
|
||||
return Results.NotFound(new ProblemDetails
|
||||
{
|
||||
Title = "Entry expired",
|
||||
Detail = $"Cache entry for VeriKey '{veriKey}' has expired",
|
||||
Status = StatusCodes.Status404NotFound
|
||||
});
|
||||
}
|
||||
|
||||
var entry = result.Entry;
|
||||
if (entry is null)
|
||||
{
|
||||
return Results.NotFound(new ProblemDetails
|
||||
{
|
||||
Title = "Entry not found",
|
||||
Detail = $"No cache entry found for VeriKey: {veriKey}",
|
||||
Status = StatusCodes.Status404NotFound
|
||||
});
|
||||
}
|
||||
|
||||
// Build the input manifest from the entry metadata
|
||||
// In a full implementation, we'd resolve these hashes to more detailed metadata
|
||||
// from their respective stores (SBOM store, VEX store, policy registry, etc.)
|
||||
var manifest = BuildInputManifest(entry, timeProvider);
|
||||
|
||||
return Results.Ok(manifest);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error getting input manifest for VeriKey {VeriKey}", veriKey);
|
||||
return InternalError("Manifest retrieval failed");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds an InputManifestResponse from a ProvcacheEntry.
|
||||
/// </summary>
|
||||
private static InputManifestResponse BuildInputManifest(ProvcacheEntry entry, TimeProvider timeProvider)
|
||||
{
|
||||
// Build input manifest from the entry and its embedded DecisionDigest
|
||||
// The DecisionDigest contains the VeriKey components as hashes
|
||||
var placeholderHash = BuildPlaceholderHash(entry.VeriKey);
|
||||
|
||||
return new InputManifestResponse
|
||||
{
|
||||
VeriKey = entry.VeriKey,
|
||||
SourceArtifact = new SourceArtifactInfo
|
||||
{
|
||||
// VeriKey includes source hash as first component
|
||||
Digest = entry.VeriKey,
|
||||
},
|
||||
Sbom = new SbomInfoDto
|
||||
{
|
||||
// SBOM hash is embedded in VeriKey computation
|
||||
// In a full implementation, we'd resolve this from the SBOM store
|
||||
Hash = placeholderHash, // Placeholder - actual hash would come from VeriKey decomposition
|
||||
},
|
||||
Vex = new VexInfoDto
|
||||
{
|
||||
// VEX hash set is embedded in VeriKey computation
|
||||
HashSetHash = placeholderHash, // Placeholder
|
||||
StatementCount = 0, // Would be resolved from VEX store
|
||||
},
|
||||
Policy = new PolicyInfoDto
|
||||
{
|
||||
Hash = entry.PolicyHash,
|
||||
},
|
||||
Signers = new SignerInfoDto
|
||||
{
|
||||
SetHash = entry.SignerSetHash,
|
||||
SignerCount = 0, // Would be resolved from signer registry
|
||||
},
|
||||
TimeWindow = new TimeWindowInfoDto
|
||||
{
|
||||
Bucket = entry.FeedEpoch,
|
||||
StartsAt = entry.CreatedAt,
|
||||
EndsAt = entry.ExpiresAt,
|
||||
},
|
||||
GeneratedAt = timeProvider.GetUtcNow(),
|
||||
};
|
||||
}
|
||||
|
||||
private static string BuildPlaceholderHash(string veriKey)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(veriKey))
|
||||
{
|
||||
return "sha256:unknown";
|
||||
}
|
||||
|
||||
var trimmed = veriKey;
|
||||
if (trimmed.StartsWith("sha256:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
trimmed = trimmed["sha256:".Length..];
|
||||
}
|
||||
|
||||
if (trimmed.Length < 32)
|
||||
{
|
||||
return "sha256:unknown";
|
||||
}
|
||||
|
||||
return $"sha256:{trimmed[..32]}...";
|
||||
}
|
||||
|
||||
private static IResult BadRequest(string detail, string title)
|
||||
{
|
||||
return Results.Problem(
|
||||
detail: detail,
|
||||
statusCode: StatusCodes.Status400BadRequest,
|
||||
title: title);
|
||||
}
|
||||
|
||||
private static IResult InternalError(string title)
|
||||
{
|
||||
return Results.Problem(
|
||||
detail: "An unexpected error occurred while processing the request.",
|
||||
statusCode: StatusCodes.Status500InternalServerError,
|
||||
title: title);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Placeholder for problem details when ASP.NET Core's ProblemDetails isn't available.
|
||||
/// </summary>
|
||||
internal sealed class ProblemDetails
|
||||
{
|
||||
public string? Type { get; set; }
|
||||
public string? Title { get; set; }
|
||||
public int? Status { get; set; }
|
||||
public string? Detail { get; set; }
|
||||
public string? Instance { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marker class for logging in Proofs API endpoints.
|
||||
/// </summary>
|
||||
public sealed class ProofsApiEndpoints;
|
||||
|
||||
partial class ProvcacheEndpointExtensions
|
||||
{
|
||||
private const int DefaultPageSize = 10;
|
||||
private const int MaxPageSize = 100;
|
||||
|
||||
/// <summary>
|
||||
/// GET /v1/provcache/proofs/{proofRoot}
|
||||
/// </summary>
|
||||
private static async Task<IResult> GetEvidenceChunks(
|
||||
string proofRoot,
|
||||
int? offset,
|
||||
int? limit,
|
||||
bool? includeData,
|
||||
[FromServices] IEvidenceChunkRepository chunkRepository,
|
||||
ILogger<ProofsApiEndpoints> logger,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
logger.LogDebug("GET /v1/provcache/proofs/{ProofRoot} offset={Offset} limit={Limit}", proofRoot, offset, limit);
|
||||
|
||||
try
|
||||
{
|
||||
if (offset is < 0)
|
||||
{
|
||||
return BadRequest("Offset must be zero or greater.", "Invalid pagination");
|
||||
}
|
||||
|
||||
if (limit is <= 0)
|
||||
{
|
||||
return BadRequest("Limit must be greater than zero.", "Invalid pagination");
|
||||
}
|
||||
|
||||
var startIndex = offset ?? 0;
|
||||
var pageSize = Math.Min(limit ?? DefaultPageSize, MaxPageSize);
|
||||
|
||||
// Get manifest for total count
|
||||
var manifest = await chunkRepository.GetManifestAsync(proofRoot, cancellationToken);
|
||||
if (manifest is null)
|
||||
{
|
||||
return Results.NotFound();
|
||||
}
|
||||
|
||||
if (startIndex >= manifest.TotalChunks)
|
||||
{
|
||||
return Results.Ok(new ProofEvidenceResponse
|
||||
{
|
||||
ProofRoot = proofRoot,
|
||||
TotalChunks = manifest.TotalChunks,
|
||||
TotalSize = manifest.TotalSize,
|
||||
Chunks = [],
|
||||
NextCursor = null,
|
||||
HasMore = false
|
||||
});
|
||||
}
|
||||
|
||||
// Get chunk range
|
||||
var chunks = await chunkRepository.GetChunkRangeAsync(proofRoot, startIndex, pageSize, cancellationToken);
|
||||
|
||||
var chunkResponses = chunks
|
||||
.OrderBy(c => c.ChunkIndex)
|
||||
.Select(c => new ProofChunkResponse
|
||||
{
|
||||
ChunkId = c.ChunkId,
|
||||
Index = c.ChunkIndex,
|
||||
Hash = c.ChunkHash,
|
||||
Size = c.BlobSize,
|
||||
ContentType = c.ContentType,
|
||||
Data = includeData == true ? Convert.ToBase64String(c.Blob) : null
|
||||
}).ToList();
|
||||
|
||||
var hasMore = startIndex + chunks.Count < manifest.TotalChunks;
|
||||
var nextCursor = hasMore
|
||||
? (startIndex + pageSize).ToString(CultureInfo.InvariantCulture)
|
||||
: null;
|
||||
|
||||
return Results.Ok(new ProofEvidenceResponse
|
||||
{
|
||||
ProofRoot = proofRoot,
|
||||
TotalChunks = manifest.TotalChunks,
|
||||
TotalSize = manifest.TotalSize,
|
||||
Chunks = chunkResponses,
|
||||
NextCursor = nextCursor,
|
||||
HasMore = hasMore
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error getting evidence chunks for proof root {ProofRoot}", proofRoot);
|
||||
return InternalError("Evidence retrieval failed");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// GET /v1/provcache/proofs/{proofRoot}/manifest
|
||||
/// </summary>
|
||||
private static async Task<IResult> GetProofManifest(
|
||||
string proofRoot,
|
||||
[FromServices] IEvidenceChunkRepository chunkRepository,
|
||||
ILogger<ProofsApiEndpoints> logger,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
logger.LogDebug("GET /v1/provcache/proofs/{ProofRoot}/manifest", proofRoot);
|
||||
|
||||
try
|
||||
{
|
||||
var manifest = await chunkRepository.GetManifestAsync(proofRoot, cancellationToken);
|
||||
if (manifest is null)
|
||||
{
|
||||
return Results.NotFound();
|
||||
}
|
||||
|
||||
var chunkMetadata = manifest.Chunks
|
||||
.OrderBy(c => c.Index)
|
||||
.Select(c => new ChunkMetadataResponse
|
||||
{
|
||||
ChunkId = c.ChunkId,
|
||||
Index = c.Index,
|
||||
Hash = c.Hash,
|
||||
Size = c.Size,
|
||||
ContentType = c.ContentType
|
||||
}).ToList();
|
||||
|
||||
return Results.Ok(new ProofManifestResponse
|
||||
{
|
||||
ProofRoot = proofRoot,
|
||||
TotalChunks = manifest.TotalChunks,
|
||||
TotalSize = manifest.TotalSize,
|
||||
Chunks = chunkMetadata,
|
||||
GeneratedAt = manifest.GeneratedAt
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error getting manifest for proof root {ProofRoot}", proofRoot);
|
||||
return InternalError("Manifest retrieval failed");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// GET /v1/provcache/proofs/{proofRoot}/chunks/{chunkIndex}
|
||||
/// </summary>
|
||||
private static async Task<IResult> GetSingleChunk(
|
||||
string proofRoot,
|
||||
int chunkIndex,
|
||||
[FromServices] IEvidenceChunkRepository chunkRepository,
|
||||
ILogger<ProofsApiEndpoints> logger,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
logger.LogDebug("GET /v1/provcache/proofs/{ProofRoot}/chunks/{ChunkIndex}", proofRoot, chunkIndex);
|
||||
|
||||
try
|
||||
{
|
||||
var chunk = await chunkRepository.GetChunkAsync(proofRoot, chunkIndex, cancellationToken);
|
||||
if (chunk is null)
|
||||
{
|
||||
return Results.NotFound();
|
||||
}
|
||||
|
||||
return Results.Ok(new ProofChunkResponse
|
||||
{
|
||||
ChunkId = chunk.ChunkId,
|
||||
Index = chunk.ChunkIndex,
|
||||
Hash = chunk.ChunkHash,
|
||||
Size = chunk.BlobSize,
|
||||
ContentType = chunk.ContentType,
|
||||
Data = Convert.ToBase64String(chunk.Blob)
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error getting chunk {ChunkIndex} for proof root {ProofRoot}", chunkIndex, proofRoot);
|
||||
return InternalError("Chunk retrieval failed");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// POST /v1/provcache/proofs/{proofRoot}/verify
|
||||
/// </summary>
|
||||
private static async Task<IResult> VerifyProof(
|
||||
string proofRoot,
|
||||
[FromServices] IEvidenceChunkRepository chunkRepository,
|
||||
[FromServices] IEvidenceChunker chunker,
|
||||
ILogger<ProofsApiEndpoints> logger,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
logger.LogDebug("POST /v1/provcache/proofs/{ProofRoot}/verify", proofRoot);
|
||||
|
||||
try
|
||||
{
|
||||
var chunks = await chunkRepository.GetChunksAsync(proofRoot, cancellationToken);
|
||||
if (chunks.Count == 0)
|
||||
{
|
||||
return Results.NotFound();
|
||||
}
|
||||
|
||||
var orderedChunks = chunks.OrderBy(c => c.ChunkIndex).ToList();
|
||||
var chunkResults = new List<ChunkVerificationResult>();
|
||||
var allValid = true;
|
||||
|
||||
foreach (var chunk in orderedChunks)
|
||||
{
|
||||
var isValid = chunker.VerifyChunk(chunk);
|
||||
var computedHash = isValid ? chunk.ChunkHash : ComputeChunkHash(chunk.Blob);
|
||||
|
||||
chunkResults.Add(new ChunkVerificationResult
|
||||
{
|
||||
Index = chunk.ChunkIndex,
|
||||
IsValid = isValid,
|
||||
ExpectedHash = chunk.ChunkHash,
|
||||
ComputedHash = isValid ? null : computedHash
|
||||
});
|
||||
|
||||
if (!isValid)
|
||||
{
|
||||
allValid = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Verify Merkle root
|
||||
var chunkHashes = orderedChunks.Select(c => c.ChunkHash).ToList();
|
||||
var computedRoot = chunker.ComputeMerkleRoot(chunkHashes);
|
||||
var rootMatches = string.Equals(computedRoot, proofRoot, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
return Results.Ok(new ProofVerificationResponse
|
||||
{
|
||||
ProofRoot = proofRoot,
|
||||
IsValid = allValid && rootMatches,
|
||||
ChunkResults = chunkResults,
|
||||
Error = !rootMatches ? $"Merkle root mismatch. Expected: {proofRoot}, Computed: {computedRoot}" : null
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error verifying proof root {ProofRoot}", proofRoot);
|
||||
return InternalError("Proof verification failed");
|
||||
}
|
||||
}
|
||||
|
||||
private static string ComputeChunkHash(byte[] data)
|
||||
{
|
||||
var hash = System.Security.Cryptography.SHA256.HashData(data);
|
||||
return $"sha256:{Convert.ToHexStringLower(hash)}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
namespace StellaOps.Provcache.Api;
|
||||
|
||||
/// <summary>
|
||||
/// Response model for GET /v1/provcache/{veriKey}.
|
||||
/// </summary>
|
||||
public sealed class ProvcacheGetResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// The VeriKey that was looked up.
|
||||
/// </summary>
|
||||
public required string VeriKey { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The cache entry if found.
|
||||
/// </summary>
|
||||
public ProvcacheEntry? Entry { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The source of the cache hit (valkey, postgres, etc.).
|
||||
/// </summary>
|
||||
public string? Source { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Time taken for the lookup in milliseconds.
|
||||
/// </summary>
|
||||
public double ElapsedMs { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Status: "hit", "miss", "bypassed", "expired".
|
||||
/// </summary>
|
||||
public required string Status { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
namespace StellaOps.Provcache.Api;
|
||||
|
||||
/// <summary>
|
||||
/// Request model for POST /v1/provcache/invalidate.
|
||||
/// </summary>
|
||||
public sealed class ProvcacheInvalidateRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// The invalidation type. If null, defaults to exact VeriKey match.
|
||||
/// </summary>
|
||||
public InvalidationType? Type { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The value to match for invalidation.
|
||||
/// For VeriKey type: exact VeriKey.
|
||||
/// For PolicyHash type: policy hash to match.
|
||||
/// For Pattern type: glob pattern.
|
||||
/// </summary>
|
||||
public required string Value { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Reason for invalidation (for audit log).
|
||||
/// </summary>
|
||||
public string? Reason { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Actor performing the invalidation (for audit log).
|
||||
/// </summary>
|
||||
public string? Actor { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Response model for POST /v1/provcache/invalidate.
|
||||
/// </summary>
|
||||
public sealed class ProvcacheInvalidateResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Number of entries affected by the invalidation.
|
||||
/// </summary>
|
||||
public long EntriesAffected { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The invalidation type that was used.
|
||||
/// </summary>
|
||||
public required string Type { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The value that was matched.
|
||||
/// </summary>
|
||||
public required string Value { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Reason for invalidation if provided.
|
||||
/// </summary>
|
||||
public string? Reason { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Additional invalidation type for direct VeriKey invalidation via API.
|
||||
/// </summary>
|
||||
internal static class InvalidationTypeExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Direct VeriKey invalidation type constant.
|
||||
/// </summary>
|
||||
public const string VeriKey = "VeriKey";
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
namespace StellaOps.Provcache.Api;
|
||||
|
||||
/// <summary>
|
||||
/// Response model for GET /v1/provcache/metrics.
|
||||
/// </summary>
|
||||
public sealed class ProvcacheMetricsResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Total number of cache requests.
|
||||
/// </summary>
|
||||
public long TotalRequests { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Total number of cache hits.
|
||||
/// </summary>
|
||||
public long TotalHits { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Total number of cache misses.
|
||||
/// </summary>
|
||||
public long TotalMisses { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Total number of invalidations performed.
|
||||
/// </summary>
|
||||
public long TotalInvalidations { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Cache hit rate (0.0 - 1.0).
|
||||
/// </summary>
|
||||
public double HitRate { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Current number of entries in the cache.
|
||||
/// </summary>
|
||||
public long CurrentEntryCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Average lookup latency in milliseconds.
|
||||
/// </summary>
|
||||
public double AvgLatencyMs { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 99th percentile lookup latency in milliseconds.
|
||||
/// </summary>
|
||||
public double P99LatencyMs { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the Valkey cache layer is healthy.
|
||||
/// </summary>
|
||||
public bool ValkeyCacheHealthy { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the Postgres repository layer is healthy.
|
||||
/// </summary>
|
||||
public bool PostgresRepositoryHealthy { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When these metrics were collected.
|
||||
/// </summary>
|
||||
public DateTimeOffset CollectedAt { get; init; }
|
||||
}
|
||||
27
src/__Libraries/StellaOps.Provcache.Api/SbomInfoDto.cs
Normal file
27
src/__Libraries/StellaOps.Provcache.Api/SbomInfoDto.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
namespace StellaOps.Provcache.Api;
|
||||
|
||||
/// <summary>
|
||||
/// SBOM information in API response.
|
||||
/// </summary>
|
||||
public sealed class SbomInfoDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Canonical hash of the SBOM content.
|
||||
/// </summary>
|
||||
public required string Hash { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// SBOM format (spdx-2.3, cyclonedx-1.6, etc.).
|
||||
/// </summary>
|
||||
public string? Format { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of packages in the SBOM.
|
||||
/// </summary>
|
||||
public int? PackageCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Completeness percentage (0-100).
|
||||
/// </summary>
|
||||
public int? CompletenessScore { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
namespace StellaOps.Provcache.Api;
|
||||
|
||||
/// <summary>
|
||||
/// Signer certificate information in API response.
|
||||
/// </summary>
|
||||
public sealed class SignerCertificateDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Subject of the certificate (e.g., CN=...).
|
||||
/// </summary>
|
||||
public string? Subject { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Certificate issuer.
|
||||
/// </summary>
|
||||
public string? Issuer { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When the certificate expires.
|
||||
/// </summary>
|
||||
public DateTimeOffset? ExpiresAt { get; init; }
|
||||
}
|
||||
22
src/__Libraries/StellaOps.Provcache.Api/SignerInfoDto.cs
Normal file
22
src/__Libraries/StellaOps.Provcache.Api/SignerInfoDto.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
namespace StellaOps.Provcache.Api;
|
||||
|
||||
/// <summary>
|
||||
/// Signer information in API response.
|
||||
/// </summary>
|
||||
public sealed class SignerInfoDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Hash of the sorted signer set.
|
||||
/// </summary>
|
||||
public required string SetHash { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of signers in the set.
|
||||
/// </summary>
|
||||
public int SignerCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Signer certificate information.
|
||||
/// </summary>
|
||||
public IReadOnlyList<SignerCertificateDto>? Certificates { get; init; }
|
||||
}
|
||||
@@ -9,3 +9,4 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
|
||||
| AUDIT-0098-T | DONE | Revalidated 2026-01-08; test coverage audit for Provcache.Api. |
|
||||
| AUDIT-0098-A | DONE | Applied 2026-01-13 (error redaction, ordering/pagination, placeholder guard, tests). |
|
||||
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
|
||||
| REMED-08 | DONE | Split API DTOs and endpoint handlers into <= 100 line files; added pagination cap, manifest mapping, and error redaction tests; `dotnet test src/__Libraries/__Tests/StellaOps.Provcache.Tests/StellaOps.Provcache.Tests.csproj` passed 2026-02-03. |
|
||||
|
||||
22
src/__Libraries/StellaOps.Provcache.Api/TimeWindowInfoDto.cs
Normal file
22
src/__Libraries/StellaOps.Provcache.Api/TimeWindowInfoDto.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
namespace StellaOps.Provcache.Api;
|
||||
|
||||
/// <summary>
|
||||
/// Time window information in API response.
|
||||
/// </summary>
|
||||
public sealed class TimeWindowInfoDto
|
||||
{
|
||||
/// <summary>
|
||||
/// The time window bucket identifier.
|
||||
/// </summary>
|
||||
public required string Bucket { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Start of the time window (UTC).
|
||||
/// </summary>
|
||||
public DateTimeOffset? StartsAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// End of the time window (UTC).
|
||||
/// </summary>
|
||||
public DateTimeOffset? EndsAt { get; init; }
|
||||
}
|
||||
22
src/__Libraries/StellaOps.Provcache.Api/VexInfoDto.cs
Normal file
22
src/__Libraries/StellaOps.Provcache.Api/VexInfoDto.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
namespace StellaOps.Provcache.Api;
|
||||
|
||||
/// <summary>
|
||||
/// VEX information in API response.
|
||||
/// </summary>
|
||||
public sealed class VexInfoDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Hash of the sorted VEX statement set.
|
||||
/// </summary>
|
||||
public required string HashSetHash { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of VEX statements contributing to this decision.
|
||||
/// </summary>
|
||||
public int StatementCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Sources of VEX statements (vendor names, OpenVEX IDs, etc.).
|
||||
/// </summary>
|
||||
public IReadOnlyList<string>? Sources { get; init; }
|
||||
}
|
||||
Reference in New Issue
Block a user