Bifrost/src/Bifrost.Web/Configuration/HeimdallOptions.cs
Heimdall 95d2e8d262 Read the Heimdall app secret from Heimdall__AppSecretFile; env value deprecated
The file wins, is read once when the options are built, and a set-but-missing,
unreadable or empty file stops startup in every environment without naming the
secret. Heimdall__AppSecret still works and logs a deprecation warning.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-12 13:24:59 +00:00

79 lines
3 KiB
C#

namespace Bifrost.Web.Configuration;
public sealed class HeimdallOptions
{
public const string SectionName = "Heimdall";
public string BaseUrl { get; init; } = "https://heimdall.gamecult.org";
public string AppSlug { get; init; } = "bifrost";
public string DiscordGuildId { get; init; } = string.Empty;
public string[] DiscordAllowedRoleIds { get; init; } = [];
public string PatreonTierTitle { get; init; } = "Inner Sanctum";
/// <summary>Deprecated: rides in the process environment. Use <see cref="AppSecretFile"/>; the file wins when both are set.</summary>
public string AppSecret { get; set; } = string.Empty;
/// <summary>Path of a file holding the app secret (systemd <c>LoadCredential</c> style: <c>%d/heimdall-app-secret</c>).</summary>
public string AppSecretFile { get; init; } = string.Empty;
public string PatronSupportIntakeSecret { get; init; } = string.Empty;
public bool EnablePatronSupportIntake { get; init; } = true;
public bool IsConfigured =>
!string.IsNullOrWhiteSpace(BaseUrl) &&
!string.IsNullOrWhiteSpace(AppSlug);
public bool IsDiscordConfigured =>
IsConfigured &&
!string.IsNullOrWhiteSpace(AppSecret) &&
!string.IsNullOrWhiteSpace(DiscordGuildId) &&
DiscordAllowedRoleIds.Length > 0;
public bool IsPatreonConfigured =>
IsConfigured &&
!string.IsNullOrWhiteSpace(AppSecret) &&
!string.IsNullOrWhiteSpace(PatreonTierTitle);
public bool IsPatronSupportIntakeConfigured =>
IsConfigured &&
EnablePatronSupportIntake &&
!string.IsNullOrWhiteSpace(PatronSupportIntakeSecret);
/// <summary>
/// Resolves <see cref="AppSecret"/> once, when the options are built. A set <see cref="AppSecretFile"/> must be a
/// readable, non-empty file, or startup fails; the message names the option and path, never a secret.
/// </summary>
public static void ResolveAppSecret(HeimdallOptions options, ILogger logger)
{
if (!string.IsNullOrWhiteSpace(options.AppSecret))
{
logger.LogWarning("Heimdall:AppSecret is deprecated: it rides in the process environment. Use Heimdall:AppSecretFile, which wins when both are set.");
}
if (!string.IsNullOrWhiteSpace(options.AppSecretFile))
{
string secret;
try
{
secret = File.ReadAllText(options.AppSecretFile).TrimEnd('\r', '\n');
}
catch (Exception error) when (error is IOException or UnauthorizedAccessException or ArgumentException or NotSupportedException)
{
throw new InvalidOperationException(
$"Heimdall:AppSecretFile must name a readable file; '{options.AppSecretFile}' could not be read ({error.GetType().Name}).");
}
if (string.IsNullOrWhiteSpace(secret))
{
throw new InvalidOperationException($"Heimdall:AppSecretFile '{options.AppSecretFile}' is empty.");
}
options.AppSecret = secret;
}
}
}