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
+77
View File
@@ -0,0 +1,77 @@
<#
.SYNOPSIS
Installs and starts the WebhookServer Windows Service.
.DESCRIPTION
Creates the service via sc.exe pointed at the published WebhookServer.Service.exe,
sets it to start automatically, and starts it. Re-running the script with the same
BinaryPath updates the binary path of an existing service.
.PARAMETER BinaryPath
Full path to WebhookServer.Service.exe. Defaults to .\publish\WebhookServer.Service.exe
relative to the script.
.PARAMETER ServiceAccount
Account to run the service under. Defaults to LocalSystem.
For Active-Directory-aware hooks pass a domain user (DOMAIN\user) or a gMSA
(DOMAIN\svc-name$ — note the trailing $). Domain users require -Password.
Never pass LocalService — it has no network identity and cannot reach a DC.
.PARAMETER Password
Password for a domain-user account. Not required for LocalSystem, NetworkService,
LocalService, or gMSA accounts.
.EXAMPLE
.\install-service.ps1 -BinaryPath C:\WebhookServer\WebhookServer.Service.exe
.EXAMPLE
.\install-service.ps1 -BinaryPath C:\WebhookServer\WebhookServer.Service.exe `
-ServiceAccount 'CONTOSO\svc-webhookserver$'
#>
[CmdletBinding()]
param(
[string]$BinaryPath = (Join-Path $PSScriptRoot '..\publish\WebhookServer.Service.exe'),
[string]$ServiceName = 'WebhookServer',
[string]$DisplayName = 'Webhook Server',
[string]$ServiceAccount = 'LocalSystem',
[string]$Password
)
$ErrorActionPreference = 'Stop'
if ($ServiceAccount -ieq 'LocalService') {
throw 'LocalService has no network identity and cannot talk to a domain controller. Use LocalSystem, a domain user, or a gMSA instead.'
}
$BinaryPath = (Resolve-Path -LiteralPath $BinaryPath).Path
if (-not (Test-Path -LiteralPath $BinaryPath)) {
throw "Binary not found: $BinaryPath"
}
# Build sc.exe argv. Note: sc.exe is fussy about spaces — keep "key= value" format.
$obj = $ServiceAccount
$existing = sc.exe query $ServiceName 2>$null
if ($existing) {
Write-Host "Service '$ServiceName' already exists; updating binPath and account."
sc.exe config $ServiceName binPath= "`"$BinaryPath`"" obj= $obj $(if ($Password) { "password= $Password" }) | Out-Null
} else {
$args = @(
'create', $ServiceName,
"binPath=", "`"$BinaryPath`"",
"DisplayName=", "`"$DisplayName`"",
"start=", "auto",
"obj=", $obj
)
if ($Password) { $args += @('password=', $Password) }
sc.exe @args | Out-Null
}
# Configure failure recovery: restart the service on first/second failure, reset count after a day.
sc.exe failure $ServiceName reset= 86400 actions= restart/5000/restart/5000/restart/5000 | Out-Null
Write-Host "Starting service '$ServiceName'..."
sc.exe start $ServiceName | Out-Null
Start-Sleep -Seconds 1
sc.exe query $ServiceName
+38
View File
@@ -0,0 +1,38 @@
<#
.SYNOPSIS
Stops and removes the WebhookServer Windows Service.
.DESCRIPTION
Leaves C:\ProgramData\WebhookServer (config + logs) untouched. Pass -PurgeData
to remove that directory as well.
#>
[CmdletBinding()]
param(
[string]$ServiceName = 'WebhookServer',
[switch]$PurgeData
)
$ErrorActionPreference = 'Stop'
$existing = sc.exe query $ServiceName 2>$null
if (-not $existing) {
Write-Host "Service '$ServiceName' is not installed."
return
}
Write-Host "Stopping service '$ServiceName'..."
sc.exe stop $ServiceName 2>$null | Out-Null
Start-Sleep -Seconds 2
Write-Host "Deleting service '$ServiceName'..."
sc.exe delete $ServiceName | Out-Null
if ($PurgeData) {
$dataRoot = Join-Path $env:ProgramData 'WebhookServer'
if (Test-Path -LiteralPath $dataRoot) {
Write-Host "Removing $dataRoot"
Remove-Item -LiteralPath $dataRoot -Recurse -Force
}
}
Write-Host 'Done.'