Created
July 4, 2025 13:27
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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."); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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());