Initial WebhookServer implementation

Add the .NET 8 solution scaffolded against PLAN.md. Three projects share
WebhookServer.Core (models, auth, execution, storage, IPC, callbacks)
and WebhookServer.Service hosts an embedded Kestrel listener plus the
named-pipe admin server. WebhookServer.Gui is a thin MVVM client over
the pipe. Includes 25 unit tests covering HMAC verification, bearer
auth, IP allowlist parsing, arg-template rendering, DPAPI round-trip,
and the encrypt-on-save config store.

Install/uninstall PowerShell scripts default to LocalSystem and accept
a domain user or gMSA via -ServiceAccount.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-07 22:04:52 -04:00
parent 2f61b342af
commit 8ecfe84540
62 changed files with 3721 additions and 0 deletions
@@ -0,0 +1,52 @@
using System.Runtime.InteropServices;
using WebhookServer.Core.Models;
using WebhookServer.Core.Storage;
using Xunit;
namespace WebhookServer.Core.Tests;
public class ConfigStoreTests
{
[Fact]
public async Task Save_then_load_preserves_endpoints_and_encrypts_secrets()
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return;
var path = Path.Combine(Path.GetTempPath(), $"webhook-test-{Guid.NewGuid():N}.json");
try
{
var store = new ConfigStore(path);
var cfg = new ServerConfig
{
HttpPort = 9000,
Endpoints =
{
new EndpointConfig
{
Slug = "deploy",
AuthMode = AuthMode.Bearer,
Bearer = new BearerOptions { Secret = ProtectedString.FromPlaintext("topsecret") },
},
},
};
await store.SaveAsync(cfg);
// Persisted config must not contain plaintext.
var rawJson = await File.ReadAllTextAsync(path);
Assert.DoesNotContain("topsecret", rawJson);
Assert.Contains("encrypted", rawJson);
var reloaded = await store.LoadAsync();
ConfigStore.DecryptSecrets(reloaded);
var ep = Assert.Single(reloaded.Endpoints);
Assert.Equal("deploy", ep.Slug);
Assert.Equal("topsecret", ep.Bearer!.Secret.Plaintext);
}
finally
{
if (File.Exists(path)) File.Delete(path);
}
}
}