Feedemy.KeyManagement
3.3.0
dotnet add package Feedemy.KeyManagement --version 3.3.0
NuGet\Install-Package Feedemy.KeyManagement -Version 3.3.0
<PackageReference Include="Feedemy.KeyManagement" Version="3.3.0" />
<PackageVersion Include="Feedemy.KeyManagement" Version="3.3.0" />
<PackageReference Include="Feedemy.KeyManagement" />
paket add Feedemy.KeyManagement --version 3.3.0
#r "nuget: Feedemy.KeyManagement, 3.3.0"
#:package Feedemy.KeyManagement@3.3.0
#addin nuget:?package=Feedemy.KeyManagement&version=3.3.0
#tool nuget:?package=Feedemy.KeyManagement&version=3.3.0
Feedemy.KeyManagement
Enterprise-grade key management library for .NET applications.
Features
- Automatic Key Rotation - Background service with configurable intervals
- Versioned Encryption - Auto-decryption with version detection
- Asymmetric Keys - RSA (2048/3072/4096) and ECDSA (P-256/P-384/P-521)
- Multi-Platform Storage - Windows DPAPI, Linux Keyring, Azure Key Vault
- Distributed Caching - Redis with pub/sub invalidation
- Database Persistence - SQL Server, PostgreSQL, and SQLite
- Health Monitoring - ASP.NET Core health checks
- Fallback Storage - Multi-provider redundancy
- Audit Trail - Complete compliance logging
- Roslyn Analyzers - 48 compile-time security rules
Installation
dotnet add package Feedemy.KeyManagement
# Persistence (choose one)
dotnet add package Feedemy.KeyManagement.Providers.EntityFramework # SQL Server
dotnet add package Feedemy.KeyManagement.Providers.Npgsql # PostgreSQL
dotnet add package Feedemy.KeyManagement.Providers.Sqlite # SQLite (dev/testing)
# Optional
dotnet add package Feedemy.KeyManagement.Analyzers
Quick Start
Development Setup
using Feedemy.KeyManagement.Extensions;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddKeyManagement(options =>
{
options.EnableAutoRotation = true;
options.RotationCheckInterval = TimeSpan.FromHours(6);
options.DefaultRotationDays = 90;
});
var app = builder.Build();
app.Run();
Production Setup
builder.Services.AddKeyManagement(options =>
{
options.EnableAutoRotation = true;
options.RotationCheckInterval = TimeSpan.FromHours(6);
// Auto-initialization
options.Initialization.EnableAutoInitialization = true;
options.Initialization.ExternalKeysJsonPath = "keys.json";
})
.WithAzureKeyStorage("https://your-vault.vault.azure.net/")
.WithRedisCache("localhost:6379")
.WithSqlServerMetadata("Server=localhost;Database=KeyManagement;...");
builder.Services.AddHealthChecks()
.AddKeyManagementHealthCheck();
Basic Usage
public class MyService
{
private readonly IKeyManagementService _keyService;
private readonly IKeyManagementAdminService _adminService;
// Create a key
public async Task CreateKeyAsync()
{
await _adminService.CreateKeyAsync(new CreateKeyRequest
{
KeyName = "MyEncryptionKey",
AutoGenerate = true,
KeySize = 32,
RotationIntervalDays = 90,
CreatedBy = "Admin"
});
}
// Retrieve a key (cached - sub-millisecond)
public async Task<byte[]> GetKeyAsync()
{
return await _keyService.RetrieveKeyAsync("MyEncryptionKey");
}
// Check health
public async Task<KeyHealthStatus> GetHealthAsync()
{
var health = await _keyService.GetKeyHealthAsync("MyEncryptionKey");
return health.Status;
}
}
Key Initialization
Define keys in keys.json for auto-generation on first run:
{
"Keys": [
{
"KeyName": "EncryptionKey",
"KeyType": "Symmetric",
"RotationIntervalDays": 90,
"Category": "MasterKey"
},
{
"KeyName": "JwtSigningKey",
"KeyType": "RSA",
"KeySize": 2048,
"RotationIntervalDays": 180,
"Category": "Signing"
}
]
}
Asymmetric Keys
// Sign data
var signature = await _asymmetricOps.SignAsync(
"JwtSigningKey",
data,
HashAlgorithmName.SHA256);
// Verify signature
var isValid = await _asymmetricOps.VerifyAsync(
"JwtSigningKey",
data,
signature,
HashAlgorithmName.SHA256);
// Get public key for external use
var publicKeyPem = await _asymmetricOps.GetPublicKeyPemAsync("JwtSigningKey");
Performance
| Operation | Mean | Notes |
|---|---|---|
| RetrieveKey (cached) | < 1 μs | L1 cache hit |
| RetrieveKey (cache miss) | ~12 ms | Database + storage |
| Sign (RSA-2048) | ~1.2 ms | |
| Verify (RSA-2048) | ~0.15 ms | |
| Sign (ECDSA P-256) | ~0.35 ms | |
| Verify (ECDSA P-256) | ~0.12 ms |
Packages
| Package | Description |
|---|---|
Feedemy.KeyManagement |
Core library |
Feedemy.KeyManagement.Providers.EntityFramework |
SQL Server persistence |
Feedemy.KeyManagement.Providers.Npgsql |
PostgreSQL persistence |
Feedemy.KeyManagement.Providers.Sqlite |
SQLite persistence (dev/testing) |
Feedemy.KeyManagement.Analyzers |
Roslyn analyzers |
Documentation
Detailed documentation available in docs/partial/:
README_CORE.md- API referenceREADME_CONFIG.md- Configuration optionsREADME_STORAGE.md- Storage providersREADME_PERSISTENCE.md- Database setupREADME_CACHE.md- Caching architectureREADME_INIT.md- Initialization guideREADME_BACKGROUND.md- Background servicesREADME_EXTENSIONS.md- DI setup
License
This project is dual-licensed:
- MIT License - Free for all use cases including commercial
- Commercial License - For enterprise support, SLA, and priority features
| Use Case | License | Cost |
|---|---|---|
| Personal projects | MIT | Free |
| Open source projects | MIT | Free |
| Commercial products | MIT | Free |
| Enterprise support & SLA | Commercial | Paid |
Commercial inquiries: licensing@feedemy.com
Support
Copyright (c) 2025 Feedemy
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Azure.Identity (>= 1.21.0)
- Azure.Security.KeyVault.Secrets (>= 4.11.0)
- Microsoft.Data.SqlClient (>= 7.0.1)
- Microsoft.Data.Sqlite (>= 10.0.9)
- Microsoft.EntityFrameworkCore.Abstractions (>= 10.0.9)
- Microsoft.Extensions.Caching.Abstractions (>= 10.0.9)
- Microsoft.Extensions.Caching.Memory (>= 10.0.9)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.9)
- Microsoft.Extensions.Diagnostics.HealthChecks (>= 10.0.9)
- Microsoft.Extensions.Hosting (>= 10.0.9)
- Microsoft.Extensions.Hosting.Abstractions (>= 10.0.9)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.9)
- Microsoft.Extensions.Options (>= 10.0.9)
- OpenTelemetry.Api (>= 1.16.0)
- Polly (>= 8.7.0)
- StackExchange.Redis (>= 2.13.17)
NuGet packages (3)
Showing the top 3 NuGet packages that depend on Feedemy.KeyManagement:
| Package | Downloads |
|---|---|
|
Feedemy.KeyManagement.Providers.EntityFramework
Entity Framework Core persistence provider for Feedemy.KeyManagement. Provides SQL Server storage for key metadata, versions, and audit logs with migrations support. Fully tested with 56/56 integration tests passing. |
|
|
Feedemy.KeyManagement.Providers.Npgsql
PostgreSQL (Npgsql) persistence provider for Feedemy.KeyManagement. Provides PostgreSQL storage for key metadata, versions, and audit logs with migrations support. Platform-independent alternative to SQL Server. |
|
|
Feedemy.KeyManagement.Providers.Sqlite
SQLite persistence provider for Feedemy.KeyManagement. Provides lightweight, file-based storage for key metadata, versions, and audit logs. Ideal for development, testing, and single-server deployments. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated | |
|---|---|---|---|
| 3.3.0 | 72 | 6/11/2026 | |
| 3.2.1 | 185 | 4/24/2026 | |
| 3.2.0 | 121 | 4/24/2026 | |
| 3.1.0 | 176 | 3/9/2026 | |
| 3.0.6 | 134 | 3/9/2026 | |
| 3.0.5 | 133 | 3/9/2026 | |
| 3.0.4 | 126 | 3/9/2026 | |
| 3.0.3 | 138 | 3/9/2026 | |
| 3.0.2 | 134 | 3/9/2026 | |
| 3.0.1 | 136 | 2/27/2026 | |
| 2.5.10 | 286 | 1/21/2026 | |
| 2.5.9 | 178 | 1/10/2026 | |
| 2.5.8 | 127 | 1/10/2026 | |
| 2.5.7 | 123 | 1/9/2026 | |
| 2.5.6 | 124 | 1/9/2026 | |
| 2.5.5 | 150 | 1/6/2026 | |
| 2.5.4 | 156 | 1/4/2026 | |
| 2.5.3 | 262 | 1/3/2026 | |
| 2.5.2 | 1,441 | 12/1/2025 | |
| 2.5.1 | 1,068 | 12/1/2025 |
v3.3.0 — Storage hardening, write atomicity, cache coordination & dependency refresh
CRITICAL FIXES (audit-driven hardening pass):
- ATOMIC WRITE PATHS: Create/Update/Rotate/Rollback database phases (and their
compensating-rollback blocks) now run in an ambient TransactionScope — a
mid-write crash can no longer leave the database half-written (orphaned
version rows / stale CurrentVersion). Fully atomic on PostgreSQL and
SQL Server (local transaction, no MSDTC); no-op on SQLite (does not
auto-enlist; behavior unchanged there). EF transient-failure retry is
preserved via an ambient-tx-aware execution strategy.
- ALL WRITES SHARE ONE PER-KEY DISTRIBUTED LOCK: Rollback and
Deactivate/Reactivate now serialize on the same key:write:lock as
Update/Rotation (new ROLLBACK_IN_PROGRESS / WRITE_IN_PROGRESS error codes) —
a rollback racing a rotation can no longer corrupt CurrentVersion.
- ROTATION-DUE INDEX (V1.2.0 migration, auto-applied): partial index for the
background rotation scan — measured 30x faster at 100k keys (25.7ms -> 0.85ms).
- PER-PROCESS PUB/SUB IDENTITY: same-hostname instances (containers) no longer
drop each other's cache invalidations (self-skip now uses MachineName:guid).
- REDIS ROUND-TRIPS CUT: version-range invalidation = one multi-key DEL
(was one per version); 5 publishes -> 1 on complete-invalidation, 3 -> 1 on
metadata-invalidation; removed a guaranteed-no-op DEL per received message.
NOTE: ICacheProvider gained RemoveManyAsync and ICacheInvalidationService
gained InvalidateByPatternSilentAsync (breaking only for custom
implementations of these interfaces).
- GetPendingRotations no longer silently capped at 1000 keys (full pagination).
Security/reliability review hardening. BEHAVIOR CHANGES (review before upgrade):
- FAIL-FAST STARTUP: the storage factory no longer silently falls back to
ephemeral InMemory storage when the requested secure provider cannot be
created (Azure URL missing / Azure unavailable / platform mismatch / no
platform provider). It now throws KeyStorageException so the app fails to
start instead of unknowingly losing keys on restart. For dev/test, opt into
InMemory explicitly via builder.WithInMemoryKeyStorage().
- AZURE ERRORS NO LONGER SWALLOWED: AzureKeyVaultStorageProvider
Retrieve/Delete/Exists/List now propagate real failures as KeyStorageException
(a genuine 404 is still treated as "not found"). In fallback mode this
correctly triggers failover; in Azure-only mode outages surface instead of
masquerading as empty results.
- HEALTH CHECK SURFACES PRIMARY OUTAGE: the ASP.NET health check now reports
Degraded when the primary provider (e.g. Azure) is unreachable but the local
fallback is still serving keys, and Unhealthy only when BOTH are down. Uses a
live per-provider probe instead of the init-time IsAvailable flag.
- IgnoreFallbackWriteFailures DEFAULT FLIPPED true -> false: a fallback write
failure now surfaces even when the primary write succeeded (a key absent from
the local fallback would be unavailable during an Azure outage). Set it back
to true to restore the old log-only behavior.
- LinuxStorageOptions are now honored by the storage factory (previously the
factory constructed the Linux provider without passing configured options).
Existing Linux keys are migrated automatically: derivation/salt changes are
re-encrypted lazily on first read, and a custom KeyringPath triggers a
one-time guarded copy from the default ~/.feedemy-keys directory.
- LINUX ATOMIC WRITES: key files and the per-installation salt are written via
temp-file + 0600 + atomic rename, eliminating the partial-write / brief
world-readable window.
- /etc/machine-id key derivation now falls back to the hostname when machine-id
exists but is empty (seen on some minimal/cloud-init images).
- Build hygiene: root .dockerignore excludes credential JSON files
(azure-credentials.json / initial-keys.json / *-credentials.json) from build
context (the committed FAKE BenchNode fixture is explicitly re-included).
CACHE COORDINATION & DISTRIBUTED INVALIDATION:
- Cache is now centralized behind IKeyCacheCoordinator (metadata/version);
decrypted key content stays L1-only (never routed through the coordinator).
- DEPLOY NOTE: Redis L2 metadata is now stored as a HASH (data+ver) for
version-guarded writes. A pre-3.3.0 STRING entry is treated as a cache miss
(pcall-guarded, degrades to a repo read, self-heals on next write) — no crash;
expect a brief one-time repo-read bump during a mixed-version rollout.
- Distributed pub/sub invalidation REPAIRED (was effectively non-functional):
listener registration, private-L1 wiring, and shared-cache leak all fixed.
- Asymmetric public-key PEM now served from the distributed-invalidated metadata
cache (previously a per-instance dictionary that pub/sub never cleared, so a
rotation on one node left others serving a stale PEM up to the 15-min TTL).
Public API and the read-path "no DB write" guarantee are unchanged.
OTHER FIXES:
- Asymmetric rollback now updates the cached public key PEM to the rolled-back
version (GetPublicKeyPemAsync could otherwise return the newer key after a rollback).
- Deactivated asymmetric keys now throw InvalidOperationException ("deactivated")
instead of the same KeyNotFoundException as a missing key.
- MasterKeyProvider propagates OperationCanceledException (previously wrapped as
InvalidOperationException).
- DatabaseMigrator: Npgsql transaction-reflection AmbiguousMatchException fixed;
SQLite current-version detection is now semantic (not AppliedAt-timestamp ordered);
the V1.1.0 "drop StorageProvider" migration is a no-op on SQLite.
DEPENDENCIES (all upgraded to latest; all solution packages aligned to 3.3.0):
- OpenTelemetry.Api 1.15.0 → 1.16.0 — resolves advisory GHSA-g94r-2vxg-569j.
- Microsoft.Data.SqlClient 6.1.4 → 7.0.1 (MAJOR — review transitive impact).
- Microsoft.Extensions.* / EntityFrameworkCore.* 10.0.3 → 10.0.9; Azure.Identity
1.18 → 1.21; StackExchange.Redis 2.11.8 → 2.13.17; Polly 8.6.5 → 8.7.0.
Upgrade: review the fail-fast and IgnoreFallbackWriteFailures changes. If you
relied on implicit InMemory fallback in any environment, configure storage
explicitly. Existing Linux keys are migrated automatically (no manual step).
Reading Azure credentials directly from /run/secrets without local persistence
is tracked separately.
-----
v3.2.1 — Hotfix: Private L1 cache (do not configure shared IMemoryCache)
v3.2.0 called services.AddMemoryCache(opts => { opts.SizeLimit = ... })
which reconfigured the SHARED DI-wide IMemoryCache singleton. Any host
application that also uses IMemoryCache for its own purposes was forced
to call SetSize(...) on every Set() call (otherwise InvalidOperationException).
This was a real breaking change that slipped through "zero breaking" testing.
Fix in 3.2.1:
- Introduced FeedemyKeyManagementMemoryCache (sealed, inherits MemoryCache).
Library now owns its own private cache instance registered in DI by its
concrete type.
- MemoryCacheProvider and KeyCacheService are registered via factory methods
that inject the private cache, NOT the shared IMemoryCache.
- services.AddMemoryCache(...) is no longer called by the library; the host
application's shared IMemoryCache registration (if any) is left untouched.
Upgrade from 3.2.0: drop-in. If you applied a PostConfigure<MemoryCacheOptions>
workaround to null out SizeLimit, you can remove it.
-----
v3.2.0 — Bench-Driven Reliability & Scalability Release
Produced by a 90-scenario benchmark harness (bench/ + BenchNode) and 3 h
of sustained endurance load. 31 findings documented, 21 fixed in 10 PRs,
zero breaking changes.
NEW APIS (additive):
- SecretFileKeyStorageProvider: raw-key storage for Docker Swarm / K8s
secret mounts. Fixes the multi-node /etc/machine-id derivation trap
(finding #3). Fluent: WithSecretFileKeyStorage("/run/secrets").
- LinuxStorageOptions: KeyringPath, SaltPath, MachineKeySource (MachineId|
Hostname|Environment|File), EnvironmentVariableName, FilePath.
- WithLinuxKeyStorage(Action<LinuxStorageOptions>?) overload;
parameterless overload still resolves to default.
- ExternalKeyPairDto: public record for external asymmetric key import
(finding #17 — schema was undocumented).
- Paged repository APIs on all three repositories:
GetPagedAsync(skip,take), GetDueForRotationPagedAsync(threshold),
GetActiveCountAsync, GetByKeyNamePagedAsync (audit),
GetActiveVersionCountsAsync (batched GROUP BY — kills N+1).
- PagedResult<T> model.
- KeyManagementOptions.AutoBootstrapMasterKey (default true): startup
now creates the master EncryptionKey if missing; consumers no longer
have to manually seed before first asymmetric create.
- KeyManagementOptions.Cache.L1MaxSize (default 100_000 units) +
L1CompactionPercentage (default 0.25): bounded L1 IMemoryCache.
- 2 new telemetry histograms:
keymanagement.cache.invalidation.publish.duration (ms)
keymanagement.cache.invalidation.receive.duration (ms)
- ErrorCodes.MASTER_KEY_MISSING.
CRITICAL FIXES:
- #28 MasterKeyProvider cache stampede: per-version SemaphoreSlim +
atomic InvalidateCache + deferred ZeroMemory. Closes the 1 h-endurance
race that silently returned null for ~2767 reads per run.
- #29 Unbounded IMemoryCache: SizeLimit + SetSize across all five entry
types. Working set dropped from 6.1 GB to bounded after sustained load.
- #11 Admin API O(N) scan: pagination + SQL-level filtering throughout.
SystemHealthAsync goes from 20 k SQL queries (N+1) to 2 (single
GROUP BY). At 5 k keys, p99 drops from 2-4 s to <100 ms.
- #1 Asymmetric silent master key dependency: auto-bootstrap hosted
service + clean Result.Fail(MASTER_KEY_MISSING) precondition.
Fresh deployments no longer crash on first RSA/ECDSA create.
- #3 Multi-node /etc/machine-id trap: addressed via SecretFileKeyStorage
(primary recommendation) or LinuxStorageOptions.MachineKeySource
override.
- #4 PostgreSQL schema init race: DatabaseMigrator now acquires
pg_advisory_xact_lock("KMMG") on the migration transaction. Three
replicas converge cleanly instead of racing on CREATE TABLE.
- #31 Rotation-retrieve race (regression surfaced by #28 fix):
RetrieveKeyAsync falls back to version-1 when the new version is
committed to DB but not yet written to storage. Closes the
"DB-ahead-of-storage" window (~6.4 % gap under concurrent retrieve +
rotate) transparently.
CONTRACT & BEHAVIOR FIXES:
- #16 RSA oversized plaintext: ArgumentException → CryptographicException
(honors IAsymmetricKeyOperations XML doc).
- #25 Public key re-fetch write amplification: dedicated 15-min L1
cache replaces per-read metadata.UpdateAsync (eliminated
DbUpdateConcurrencyException under concurrent reads).
- #23 GetECDsaAsync raw accessor: ImportPkcs8PrivateKey errors wrapped
with InvalidOperationException + meaningful context.
- #27 Fresh asymmetric GetCurrentVersionAsync: bypass metadata cache
(L2 round-trip was corrupting large PublicKeyPem fields).
- #9 WithRedisCache implicitly enables EnableCacheInvalidationListener
(opt-out still possible by explicit override).
- #12 EncryptionTotal Prometheus counter: now emits on both symmetric
and asymmetric success paths (was a dead counter).
NEW DOCS:
- docs/claude/telemetry.md — OTel ↔ Prometheus naming reference, meter
inventory, PromQL + Grafana snippets.
- docs/claude/security-model.md — threat matrix, per-provider guarantees
(cold disk, container escape, host root, memory dump, network), and
mitigation roadmap (Vault Transit, TPM sealed key, PKCS#11 softHSM).
- Expanded XML docs: LinuxKeyringStorageProvider misnomer warning,
CreateKeyAsync concurrent-duplicate contract, RotationCheckInterval
validation message, CreateKeyRequest.ExternalAsymmetricKeyPair schema
(with JSON example).
UPGRADE:
Drop-in: all public APIs are additive; no signature changes. Consumers
that relied on the dead EncryptionTotal counter will now see non-zero
values. Consumers configuring Redis via WithRedisCache() will have
the cache invalidation listener enabled automatically — set
EnableCacheInvalidationListener=false explicitly to preserve the
pre-3.2 behavior.
VERIFICATION:
- 54+ unit tests pass (solution-wide 0 errors).
- Bench 69/69 scenarios green, 0 violations (was 11+ violations in
pre-fix runs). S34 master-key-race endurance 0 bad (was 2767).
- See docs/claude/bench-findings-verified-2026-04-24.md for the full
verification matrix.
----
v3.0.0 - Major Security, Performance & Stability Release
SECURITY (CRITICAL):
- Fixed SQL injection in PostgreSQL DatabaseMigrator (parameterized queries via NpgsqlParameter)
- Fixed plaintext Azure credential storage on Linux (AES-256-GCM encryption with machine-derived key)
- AES-GCM AAD bypass mitigation (AllowLegacyNoAadDecryption option, default false)
- DPAPI application-specific entropy ("Feedemy.KeyManagement.v1") with legacy auto-migration
- Linux per-installation random salt with .salt.bak backup/recovery chain
- Linux key re-encryption safety: write-read-verify with FixedTimeEquals, .migrating transactional backup
- Linux RetrieveKeyAsync TOCTOU fix (File.Exists moved inside lock)
- Credential JSON secure deletion after import (random-overwrite + File.Delete)
- CryptographicOperations.ZeroMemory throughout all sensitive byte array paths (12+ locations)
- IDisposable for WindowsDpapiStorageProvider, FallbackStorageProvider, MasterKeyProvider
PERFORMANCE:
- MasterKeyProvider in-memory cache with 5-min TTL (eliminates 2 IO round-trips per decryption)
- MasterKeyProvider.InvalidateCache() called after master key rotation for cache coherence
- Source-generated logging ([LoggerMessage]) for retrieval hot path
- TagList zero-allocation telemetry, Span-based IsEncryptedFormat detection
- Prefix-based cache pattern removal fast path (bypasses regex for simple patterns)
- Health check response caching (30s TTL with SemaphoreSlim double-check)
- RandomNumberGenerator.Fill / SHA256.HashData static APIs (no instance allocation)
- RegexOptions.None instead of Compiled (prevents assembly leak)
- Volatile.Read for FallbackStorageProvider._consecutiveFailures (prevents torn reads)
- Atomic RetrievalLockEntry.Dispose via Interlocked.CompareExchange
- Static eviction callback in KeyCacheService (eliminates closure allocation)
FIXED:
- Startup crash risks shifted behind DI build (ConfigurationValidationHostedService)
- Background services opt-in by default (EnableAutoRotation, EnableCacheInvalidationListener, EnableFallbackSync)
- CacheInvalidationListener resiliency with bounded retry and exponential backoff
- Double pub/sub removed from InvalidateAllVersionsAsync
- Startup ordering fixed for ServicesStartConcurrently=true
- UseAzureDefaults production safety: metadata provider validation enforced
- Rollback log level: successful rollback now logs Warning instead of Critical
- ConfigurationEnumMappings default cases now throw ArgumentOutOfRangeException
- Fire-and-forget defensive copy in FallbackStorageProvider.SyncToFallbackAsync
BREAKING CHANGES:
- OperationResult<T> now immutable (init-only properties), Warnings is IReadOnlyList<string>
- IKeyMetadataRepository has new required members (ExistsIncludingInactiveAsync, GetByKeyNameAsync)
- IMasterKeyProvider has new InvalidateCache() method (custom implementations must add it)
- MasterKeyProvider now implements IDisposable
- Background services opt-in by default
- UseAzureDefaults() requires persistent metadata provider
- GetByNameAsync/ExistsAsync targets active records by default
- See CHANGELOG.md and docs/MIGRATION_GUIDE.md for full migration guide