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