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