From 280d843aebc205edea04083e8d3101cf1d6a013d Mon Sep 17 00:00:00 2001 From: Heimdall Date: Thu, 12 Nov 2026 13:46:24 +0000 Subject: [PATCH] AppSecretFile: pin the deprecation at Warning level, say trailing newlines Soul pass on 25b9cc16: the warning level was unpinned (a Trace demotion passed) and the docs said one newline where the code trims all. Co-Authored-By: Claude Sonnet 5.5 --- docs/deploy-main-to-live.md | 2 +- docs/heimdall-app-secret-file.md | 2 +- .../HeimdallAppSecretFileTests.cs | 6 +++--- .../Support/CapturingLoggerProvider.cs | 20 ++++++++++++++++++- 4 files changed, 24 insertions(+), 6 deletions(-) diff --git a/docs/deploy-main-to-live.md b/docs/deploy-main-to-live.md index c0f0e14..9e50e98 100644 --- a/docs/deploy-main-to-live.md +++ b/docs/deploy-main-to-live.md @@ -55,7 +55,7 @@ permits, windows and `MaxTrackedKeys`. New since the live release, and not read by the validator: - **`Heimdall__AppSecretFile`**: the preferred way to give Bifrost the app secret below. A path to a file holding it (one - trailing newline is trimmed), for example `%d/heimdall-app-secret` under systemd `LoadCredential=`, so the secret never + trailing newlines are trimmed), for example `%d/heimdall-app-secret` under systemd `LoadCredential=`, so the secret never rides in the process environment. When set, the file wins over `Heimdall__AppSecret`; a missing, unreadable or empty file stops startup in every environment, with a message that names the option and path and never the value. Ops steps: `docs/heimdall-app-secret-file.md`. diff --git a/docs/heimdall-app-secret-file.md b/docs/heimdall-app-secret-file.md index 063dd7d..2f8c283 100644 --- a/docs/heimdall-app-secret-file.md +++ b/docs/heimdall-app-secret-file.md @@ -4,7 +4,7 @@ `x-heimdall-app-secret` header. Set through the environment it rides in the process environment. `Heimdall:AppSecretFile` (`Heimdall__AppSecretFile`) names a file holding it instead. -- File set: Bifrost reads it once at startup and one trailing newline is trimmed. The file wins over `Heimdall__AppSecret`. +- File set: Bifrost reads it once at startup and trailing newlines are trimmed. The file wins over `Heimdall__AppSecret`. - File set but missing, unreadable, a directory, or empty: startup fails in every environment. The message names `Heimdall:AppSecretFile` and the path, never the secret. - Only `Heimdall__AppSecret` set: still works; startup logs a deprecation warning (without the value). diff --git a/tests/Bifrost.Web.Tests/HeimdallAppSecretFileTests.cs b/tests/Bifrost.Web.Tests/HeimdallAppSecretFileTests.cs index 5e3bca6..1094bb5 100644 --- a/tests/Bifrost.Web.Tests/HeimdallAppSecretFileTests.cs +++ b/tests/Bifrost.Web.Tests/HeimdallAppSecretFileTests.cs @@ -45,7 +45,7 @@ public sealed class HeimdallAppSecretFileTests : IDisposable [InlineData("")] [InlineData("\n")] [InlineData("\r\n")] - public void The_file_is_the_secret_with_only_the_trailing_newline_trimmed(string ending) + public void The_file_is_the_secret_with_only_trailing_newlines_trimmed(string ending) { var options = Resolve(appSecretFile: Write(FileSecret + ending)); @@ -67,7 +67,7 @@ public sealed class HeimdallAppSecretFileTests : IDisposable var options = Resolve(EnvSecret, Write(FileSecret + "\n")); Assert.Equal(FileSecret, options.AppSecret); - Assert.Contains(_logs.Lines, line => line.Contains("deprecated")); + Assert.Contains(_logs.WarningLines, line => line.Contains("deprecated")); AssertNoSecretLogged(); } @@ -110,7 +110,7 @@ public sealed class HeimdallAppSecretFileTests : IDisposable var options = Resolve(EnvSecret); Assert.Equal(EnvSecret, options.AppSecret); - Assert.Single(_logs.Lines, line => line.Contains("deprecated") && line.Contains("Heimdall:AppSecretFile")); + Assert.Single(_logs.WarningLines, line => line.Contains("deprecated") && line.Contains("Heimdall:AppSecretFile")); AssertNoSecretLogged(); } diff --git a/tests/Bifrost.Web.Tests/Support/CapturingLoggerProvider.cs b/tests/Bifrost.Web.Tests/Support/CapturingLoggerProvider.cs index ea7558b..a641e7c 100644 --- a/tests/Bifrost.Web.Tests/Support/CapturingLoggerProvider.cs +++ b/tests/Bifrost.Web.Tests/Support/CapturingLoggerProvider.cs @@ -5,6 +5,19 @@ namespace Bifrost.Web.Tests.Support; public sealed class CapturingLoggerProvider : ILoggerProvider { private readonly List _lines = []; + private readonly List _warnings = []; + + /// Lines logged at Warning or above, to pin that an operator-facing notice reaches the journal. + public IReadOnlyList WarningLines + { + get + { + lock (_lines) + { + return _warnings.ToArray(); + } + } + } public IReadOnlyList Lines { @@ -38,7 +51,12 @@ public sealed class CapturingLoggerProvider : ILoggerProvider { lock (owner._lines) { - owner._lines.Add(formatter(state, exception) + (exception is null ? string.Empty : "\n" + exception)); + var line = formatter(state, exception) + (exception is null ? string.Empty : "\n" + exception); + owner._lines.Add(line); + if (logLevel >= LogLevel.Warning) + { + owner._warnings.Add(line); + } } } }