using System.Text.Json; using Microsoft.Extensions.Logging; using StellaOps.Cryptography; namespace StellaOps.Policy.Snapshots; /// /// Service for managing knowledge snapshots. /// public sealed class SnapshotService : ISnapshotService { private readonly ISnapshotIdGenerator _idGenerator; private readonly ICryptoSigner? _signer; private readonly ISnapshotStore _store; private readonly ILogger _logger; public SnapshotService( ISnapshotIdGenerator idGenerator, ISnapshotStore store, ILogger logger, ICryptoSigner? signer = null) { _idGenerator = idGenerator ?? throw new ArgumentNullException(nameof(idGenerator)); _store = store ?? throw new ArgumentNullException(nameof(store)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _signer = signer; } /// /// Creates and persists a new snapshot. /// public async Task CreateSnapshotAsync( SnapshotBuilder builder, CancellationToken ct = default) { ArgumentNullException.ThrowIfNull(builder); var manifest = builder.Build(); // Validate ID before storing if (!_idGenerator.ValidateId(manifest)) throw new InvalidOperationException("Snapshot ID validation failed"); await _store.SaveAsync(manifest, ct).ConfigureAwait(false); _logger.LogInformation("Created snapshot {SnapshotId}", manifest.SnapshotId); return manifest; } /// /// Seals a snapshot with a DSSE signature. /// public async Task SealSnapshotAsync( KnowledgeSnapshotManifest manifest, CancellationToken ct = default) { ArgumentNullException.ThrowIfNull(manifest); if (_signer is null) throw new InvalidOperationException("No signer configured for sealing snapshots"); var payload = JsonSerializer.SerializeToUtf8Bytes(manifest with { Signature = null }, SnapshotSerializerOptions.Canonical); var signatureBytes = await _signer.SignAsync(payload, ct).ConfigureAwait(false); var signature = Convert.ToBase64String(signatureBytes); var sealedManifest = manifest with { Signature = signature }; await _store.SaveAsync(sealedManifest, ct).ConfigureAwait(false); _logger.LogInformation("Sealed snapshot {SnapshotId}", manifest.SnapshotId); return sealedManifest; } /// /// Verifies a snapshot's integrity and signature. /// public async Task VerifySnapshotAsync( KnowledgeSnapshotManifest manifest, CancellationToken ct = default) { ArgumentNullException.ThrowIfNull(manifest); // Verify content-addressed ID if (!_idGenerator.ValidateId(manifest)) { return SnapshotVerificationResult.Fail("Snapshot ID does not match content"); } // Verify signature if present if (manifest.Signature is not null) { if (_signer is null) { return SnapshotVerificationResult.Fail("No signer configured for signature verification"); } var payload = JsonSerializer.SerializeToUtf8Bytes(manifest with { Signature = null }, SnapshotSerializerOptions.Canonical); var signatureBytes = Convert.FromBase64String(manifest.Signature); var sigValid = await _signer.VerifyAsync(payload, signatureBytes, ct).ConfigureAwait(false); if (!sigValid) { return SnapshotVerificationResult.Fail("Signature verification failed"); } } return SnapshotVerificationResult.Success(); } /// /// Retrieves a snapshot by ID. /// public async Task GetSnapshotAsync( string snapshotId, CancellationToken ct = default) { if (string.IsNullOrWhiteSpace(snapshotId)) return null; return await _store.GetAsync(snapshotId, ct).ConfigureAwait(false); } /// /// Lists all snapshots in the store. /// public async Task> ListSnapshotsAsync( int skip = 0, int take = 100, CancellationToken ct = default) { return await _store.ListAsync(skip, take, ct).ConfigureAwait(false); } } /// /// Result of snapshot verification. /// public sealed record SnapshotVerificationResult(bool IsValid, string? Error) { public static SnapshotVerificationResult Success() => new(true, null); public static SnapshotVerificationResult Fail(string error) => new(false, error); } /// /// Interface for snapshot management operations. /// public interface ISnapshotService { /// /// Creates and persists a new snapshot. /// Task CreateSnapshotAsync(SnapshotBuilder builder, CancellationToken ct = default); /// /// Seals a snapshot with a DSSE signature. /// Task SealSnapshotAsync(KnowledgeSnapshotManifest manifest, CancellationToken ct = default); /// /// Verifies a snapshot's integrity and signature. /// Task VerifySnapshotAsync(KnowledgeSnapshotManifest manifest, CancellationToken ct = default); /// /// Retrieves a snapshot by ID. /// Task GetSnapshotAsync(string snapshotId, CancellationToken ct = default); /// /// Lists all snapshots in the store. /// Task> ListSnapshotsAsync(int skip = 0, int take = 100, CancellationToken ct = default); } /// /// Interface for snapshot persistence. /// public interface ISnapshotStore { /// /// Saves a snapshot manifest. /// Task SaveAsync(KnowledgeSnapshotManifest manifest, CancellationToken ct = default); /// /// Retrieves a snapshot manifest by ID. /// Task GetAsync(string snapshotId, CancellationToken ct = default); /// /// Lists snapshot manifests. /// Task> ListAsync(int skip = 0, int take = 100, CancellationToken ct = default); /// /// Deletes a snapshot manifest by ID. /// Task DeleteAsync(string snapshotId, CancellationToken ct = default); /// /// Gets bundled content by path. /// Task GetBundledContentAsync(string bundlePath, CancellationToken ct = default); /// /// Gets content by digest. /// Task GetByDigestAsync(string digest, CancellationToken ct = default); } /// /// In-memory implementation of for testing. /// public sealed class InMemorySnapshotStore : ISnapshotStore { private readonly Dictionary _snapshots = new(); private readonly object _lock = new(); public Task SaveAsync(KnowledgeSnapshotManifest manifest, CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); lock (_lock) { _snapshots[manifest.SnapshotId] = manifest; } return Task.CompletedTask; } public Task GetAsync(string snapshotId, CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); lock (_lock) { return Task.FromResult(_snapshots.TryGetValue(snapshotId, out var manifest) ? manifest : null); } } public Task> ListAsync(int skip = 0, int take = 100, CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); lock (_lock) { var result = _snapshots.Values .OrderByDescending(s => s.CreatedAt) .Skip(skip) .Take(take) .ToList(); return Task.FromResult>(result); } } public Task DeleteAsync(string snapshotId, CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); lock (_lock) { return Task.FromResult(_snapshots.Remove(snapshotId)); } } public Task GetBundledContentAsync(string bundlePath, CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); // In-memory implementation doesn't support bundled content return Task.FromResult(null); } public Task GetByDigestAsync(string digest, CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); // In-memory implementation doesn't support digest-based lookup return Task.FromResult(null); } /// /// Clears all snapshots from the store (for testing only). /// public void Clear() { lock (_lock) { _snapshots.Clear(); } } }