Each checkpoint is a few KB of JSON plus a tiny sidecar; even at 90
entries on a config with hundreds of endpoints the on-disk footprint
is negligible (worst case ~20 MB). With daily auto-checkpoints plus
on-save snapshots, 30 entries could fill in a couple weeks of
moderate use; 90 gives a comfortable ~3-month window.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two follow-ups to the previous Config Checkpoints commit:
1. Bring back the post-install "Launch Webhook Server" checkbox in the
installer. The previous attempt failed because Inno Setup's
postinstall flag launches via CreateProcess after Setup exits,
bypassing the GUI's requireAdministrator manifest. Adding the
shellexec flag switches to ShellExecute, which DOES honor the
manifest and triggers a clean UAC prompt - so the post-install
GUI launch works as expected.
2. Each checkpoint now carries a description, stored in a sidecar
.meta.json file next to the snapshot. Defaults:
- Auto-on-save: "Before save"
- Midnight scheduler: "Nightly auto-checkpoint"
- Manual: opens a small dialog so the user can type a meaningful
description (defaults to "Manual checkpoint" if blank)
The dialog and pruning both clean up sidecars alongside snapshots.
The Config Checkpoints grid grows a Description column between
When and Size.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three fixes:
1. Config Checkpoints submenu replaced with a proper dialog. Lists
checkpoints with timestamp/size/filename, has a "Take Checkpoint
Now" button, and a "Roll Back" button that becomes enabled when a
row is selected. The previous click-a-menu-entry-immediate-restore
flow was too easy to fire by accident.
2. New CheckpointScheduler BackgroundService creates a checkpoint at
midnight every day. Combined with the existing auto-on-save
snapshots, this guarantees a daily rollback point even if the
config wasn't edited that day. A new "create-checkpoint" admin op
plus AdminPipeServer.CreateCheckpoint helper does the actual file
copy; both manual (via the dialog) and the scheduler use it.
3. Installer: drop the post-install "Launch Webhook Server" wizard
step. It tried to launch the GUI un-elevated, which fails because
the GUI's manifest is requireAdministrator. The Start Menu shortcut
handles elevation correctly, so the user can launch from there.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Phase 3: app icon (multi-resolution ICO + master PNG)
scripts/generate-icons.ps1 renders the icon programmatically with
System.Drawing - rounded teal square (#0E7C66) with a stylized white
hook glyph - at 16/24/32/48/64/128/256 px and assembles a proper
multi-resolution Microsoft ICO. The PNG and ICO outputs land in
resources/. The script is the source of truth; re-run after editing
the design.
GUI csproj uses ApplicationIcon for the EXE icon and embeds the .ico
+ .png as Resources so MainWindow and AboutDialog can use them via
WPF's resource URI scheme.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Phase 5: tray icon with minimize-to-tray and context menu
GUI csproj enables UseWindowsForms (NotifyIcon lives in WinForms even
in .NET 8). New Services/TrayIcon.cs wraps NotifyIcon with a context
menu (Open / Restart service / Exit) and the embedded webhook-server
icon. MainWindow creates the TrayIcon, hides itself on minimize and
restores on tray double-click.
Adds GlobalUsings.cs to alias the WPF defaults for types that exist
in both WPF and WinForms (Application, MessageBox, TextBox, Binding,
etc.) so existing code keeps compiling without per-file changes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Phase 6+7: Inno Setup installer + GitHub Actions release pipeline
installer/webhook-server.iss is an Inno Setup 6 script that:
- Installs to %ProgramFiles%\WebhookServer
- Creates Start Menu folder + GUI shortcut (and optional desktop icon)
- Runs install-service.ps1 post-install to register the Windows Service
- Runs uninstall-service.ps1 pre-uninstall to remove it
- Bundles the webhook-server icon for the installer / uninstaller
scripts/build-installer.ps1 is the local build helper: publishes both
projects, finds ISCC.exe (PATH or standard install path), compiles the
installer with the version pulled from Directory.Build.props, drops the
output in dist/.
.github/workflows/ci.yml runs build + test on every push/PR to main.
.github/workflows/release.yml triggers on v* tags (or manual dispatch),
runs tests, installs Inno Setup via choco, builds the installer, and
attaches the .exe to a GitHub Release. Pre-1.0 versions are flagged
prerelease automatically.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Phase 4: backups + import/export config
ConfigStore.SaveAsync now snapshots the previous config to
%ProgramData%\WebhookServer\backups\config-<timestamp>.json before
overwriting, retaining the last 30. Failures are silent so a
backup-write hiccup never blocks an actual save.
Three new admin pipe ops:
- list-backups: returns newest 50 entries with timestamps and sizes
- restore-backup: takes a fileName, refuses path-traversal chars,
loads the named backup over the live config (which itself triggers
a fresh backup of the current state via the SaveAsync hook)
- import-config: replaces the current config with a GUI-supplied
ServerConfig, merging encrypted secrets where the GUI didn't supply
new plaintext
GUI File menu items are wired:
- Import config: file picker -> ImportConfigAsync
- Export config: SaveFileDialog writes the current config as JSON
- Backups: dynamic submenu auto-refreshed when opened, listing
backups with timestamp + size; click to confirm-and-restore
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ServerConfig grows two fields:
- BindAddresses: list of IPs Kestrel binds to (empty = all interfaces,
current behavior). Listening only on a subset is useful when the host
has multiple NICs and the webhook should not be reachable on all of
them.
- DisplayHost: the hostname/IP the GUI splices into the URL column and
Copy URL button. Cosmetic; doesn't affect what the server accepts.
Server Settings dialog gains a "Network" section: a checkbox for "all
interfaces" plus per-NIC checkboxes auto-detected via NetworkInterface.
GetAllNetworkInterfaces, and an editable ComboBox for the display host
pre-populated with detected IPs and the machine name.
Listener restart fires on BindAddresses change but not on DisplayHost
change (cosmetic).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Setting lpDesktop on STARTUPINFO forces the child to open that desktop;
the LogonUser-derived token in SpecificUser mode usually cannot, since
winsta0\default's DACL only grants the currently-logged-in user. The
result was STATUS_DLL_INIT_FAILED (exit 0xC0000142) with empty stdio.
Only InteractiveUser mode needs the explicit interactive desktop -
that whole point of the mode is to land in the user's session. For
SpecificUser, leaving lpDesktop null lets the child inherit our
service desktop, which works for headless batch tasks (AD reads, file
ops, etc.).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CreateProcessWithLogonW (which ProcessStartInfo.UserName/Password uses
under the hood) refuses to run when the caller is LocalSystem - which
is exactly the scenario every hook hits, since the service runs as
SYSTEM by default. The hook just got "Access is denied" with no useful
context.
Switch SpecificUser to the same LogonUser + DuplicateTokenEx +
CreateProcessAsUser path that InteractiveUser already uses. The
launcher tries LOGON32_LOGON_INTERACTIVE first, falling back to
LOGON32_LOGON_BATCH for accounts without interactive-logon rights
(typical for service-only users). Domain "." is normalized to the
machine name so ".\justin" works.
The launcher's two public entry points - LaunchAsActiveConsoleUser
and LaunchAsSpecificUser - share the same LaunchWithToken core, so
stdio capture and environment-block construction stay identical.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Native per-endpoint identity instead of the schtasks bridge:
- Service (default) keeps the existing path - hooks inherit the service
account (SYSTEM by default, or whatever you installed under).
- SpecificUser binds ProcessStartInfo.UserName / Password / Domain so
the hook runs in a batch logon session as the named account. Useful
for AD-write hooks that should NOT run as SYSTEM.
- InteractiveUser uses WTSQueryUserToken(WTSGetActiveConsoleSessionId)
+ DuplicateTokenEx + CreateProcessAsUser to drop the child into the
logged-in user's session with their environment block. This is the
real fix for "calc.exe should pop up on my desktop" - no Task
Scheduler bridge required. Stdio is captured via inheritable
anonymous pipes so the hook still returns stdout/stderr to the
caller normally.
Implementation:
- New RunAsMode enum + RunAsConfig model on EndpointConfig
- ConfigStore round-trips RunAs.Password through DPAPI alongside
bearer/HMAC/PFX secrets
- AdminPipeServer's secret-merge logic preserves the encrypted blob
when the GUI saves an endpoint without re-typing the password
- New WebhookServer.Core.Execution.Native namespace with NativeMethods
(P/Invoke) and InteractiveProcessLauncher (token-based launcher)
- ProcessExecutor branches on RunAs.Mode; the Service/SpecificUser
paths share .NET's Process; InteractiveUser uses the launcher
- GUI editor gets a "Run as" section: dropdown + conditional
username/password/load-profile fields under SpecificUser
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PowerShell's -Command treats trailing argv entries as part of the
command-line text rather than as $args, so a hook with an inline
command and an arg template raised a parser error. Wrap inline commands
in a scriptblock with @args splat, and pipe $input into the block so
{{body.*}} arg templates AND stdin JSON both reach the script. Verified
end-to-end against ping, bearer (good/bad/missing), HMAC (good/bad/missing),
IP allowlist deny, async 202, and stdin+template combined.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>