Skip to content

Instantly share code, notes, and snippets.

@AldeRoberge
Created July 4, 2025 13:27
Show Gist options
  • Save AldeRoberge/c7239eaebed4b4c843f254ec5d70378c to your computer and use it in GitHub Desktop.
Save AldeRoberge/c7239eaebed4b4c843f254ec5d70378c to your computer and use it in GitHub Desktop.
Secrets easy management, uses Ardalis.SmartEnum, FluentValidation, Levenshtein Distance, to provide help in getting secrets.
using Ardalis.SmartEnum;
using FluentResults;
namespace ADG.Server.Shared.Secrets;
public class Secret : SmartEnum<Secret, string>
{
public static readonly Secret OpenAI_ApiKey = new(nameof(OpenAI_ApiKey), "OPEN_AI_API_KEY");
public static readonly Secret ElevenLabs_ApiKey = new(nameof(ElevenLabs_ApiKey), "ELEVEN_LABS_API_KEY");
protected internal string Key { get; }
protected internal new string Value => base.Value;
private Secret(string name, string key) : base(name, key)
{
Key = key;
}
}
public static class SecretValidator
{
/// <summary>
/// Ensure that all required secrets are set in the environment variables.
/// </summary>
public static void ValidateAllSecretsOrThrow()
{
var result = ValidateAllSecrets();
if (result.IsFailed)
{
throw new InvalidOperationException(result.Errors.First().Message);
}
Console.WriteLine("All secrets are validated successfully.");
}
/// <summary>
/// Validate all required secrets and return a Result indicating success or failure.
/// </summary>
private static Result ValidateAllSecrets()
{
// Check if all required secrets are set in the environment variables (and not null or empty)
var missingSecrets = Secret.List
.Where(secret => string.IsNullOrEmpty(Environment.GetEnvironmentVariable(secret.Key)))
.ToList();
// If no secrets are missing, return success
if (missingSecrets.Count == 0)
return Result.Ok();
// Collect actual environment variable keys
var actualEnvKeys = Environment.GetEnvironmentVariables()
.Keys
.Cast<string>()
.ToList();
// Provide helpful suggestion for mistyped keys
var suggestions = missingSecrets
.Select(missing =>
{
var closestMatch = actualEnvKeys
.OrderBy(envKey => envKey.LevenshteinDistance(missing.Key))
.FirstOrDefault();
return closestMatch?.LevenshteinDistance(missing.Key) <= 3
? $"It looks like you typed '{closestMatch}' (wrong) instead of '{missing.Key}' (correct) in your environment variables, please correct it."
: null;
})
.Where(s => s != null)
.ToList();
// If no suggestions, just return the missing secrets
var helpText = suggestions.Any()
? Environment.NewLine + string.Join(Environment.NewLine, suggestions) + Environment.NewLine
: string.Empty;
// Return a failure result with the missing secrets and suggestions
return Result.Fail(new Error(
$"{helpText}Missing secrets: {string.Join(", ", missingSecrets.Select(x => $"'{x.Value}'"))}. " +
"Some secrets are missing from the environment variables. " +
$"Please add them and restart the application."
));
}
}
// LevenshteinDistance
public static class StringExtensions
{
public static int LevenshteinDistance(this string source, string target)
{
if (string.IsNullOrEmpty(source)) return target?.Length ?? 0;
if (string.IsNullOrEmpty(target)) return source.Length;
var lengthSource = source.Length;
var lengthTarget = target.Length;
var distance = new int[lengthSource + 1, lengthTarget + 1];
for (var i = 0; i <= lengthSource; i++)
distance[i, 0] = i;
for (var j = 0; j <= lengthTarget; j++)
distance[0, j] = j;
for (var i = 1; i <= lengthSource; i++)
{
for (var j = 1; j <= lengthTarget; j++)
{
var cost = source[i - 1] == target[j - 1] ? 0 : 1;
distance[i, j] = Math.Min(
Math.Min(distance[i - 1, j] + 1, distance[i, j - 1] + 1),
distance[i - 1, j - 1] + cost);
}
}
return distance[lengthSource, lengthTarget];
}
}
public static class SecretExtensions
{
public static Secret GetSecret(this string secretName)
{
return Secret.List.FirstOrDefault(s => s.Name.Equals(secretName, StringComparison.OrdinalIgnoreCase))
?? throw new ArgumentException($"Secret '{secretName}' not found.");
}
/// <summary>
/// Get the actual value of the secret from the environment variables.
/// </summary>
public static string GetValue(this Secret secret)
{
return Environment.GetEnvironmentVariable(secret.Key) ?? throw new InvalidOperationException($"Environment variable '{secret.Key}' is not set.");
}
}
@AldeRoberge
Copy link
Author

You can validate that all secrets are present on start like this :

SecretValidator.ValidateAllSecretsOrThrow();

You can then consume the secrets like this :

_elevenLabsClient ??= new ElevenLabsClient(Secret.ElevenLabs_ApiKey.GetValue());

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment