using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; using VaultSharp; using VaultSharp.V1.AuthMethods.Token; namespace GoodBooks.PAY.PAYBLL.Vault { /// /// Reads secrets from HashiCorp Vault KV v2. Caches values in IMemoryCache with a /// 5-minute sliding TTL to avoid hammering Vault on every gateway call. /// /// NEVER log the secret value — only the path. /// The mount point is always "secret"; the path is the mount-relative key stored in /// MPAYGATEWAYCONFIG.APIKEYIDVAULTKEY / APISECRETVAULTKEY / WEBHOOKSECRETVAULTKEY. /// /// public sealed class VaultService : IVaultService { private readonly IVaultClient _vault; private readonly IMemoryCache _cache; private readonly ILogger _logger; private static readonly TimeSpan _ttl = TimeSpan.FromMinutes(5); public VaultService(IVaultClient vault, IMemoryCache cache, ILogger logger) { _vault = vault; _cache = cache; _logger = logger; } /// public async Task GetSecretAsync(string vaultKeyPath, CancellationToken ct = default) { if (_cache.TryGetValue(vaultKeyPath, out string? cached) && cached is not null) return cached; _logger.LogDebug("Vault cache miss for path {VaultPath} — fetching from Vault", vaultKeyPath); // KV v2: path format is "secret/data/{vaultKeyPath}" // ReadSecretAsync handles the /data/ segment internally when mountPoint is provided. var secret = await _vault.V1.Secrets.KeyValue.V2 .ReadSecretAsync(path: vaultKeyPath, mountPoint: "secret") .ConfigureAwait(false); // KV v2 returns a dictionary of key→value pairs; take the first (and typically only) value. var value = secret?.Data?.Data?.Values.FirstOrDefault()?.ToString() ?? throw new InvalidOperationException( $"Vault secret at path '{vaultKeyPath}' returned empty or null data."); var options = new MemoryCacheEntryOptions().SetSlidingExpiration(_ttl); _cache.Set(vaultKeyPath, value, options); return value; } } }