🌐 한국어
LidGuard.Notifications is a small ASP.NET Core Razor Pages server that receives LidGuard pre-suspend and post-session-end webhooks and forwards them to subscribed browsers through Web Push. It stores browser subscriptions, webhook events, and delivery results in SQLite.
The server is intentionally not a pure WebAssembly app. LidGuard needs an HTTP endpoint for incoming webhooks, and Web Push requires a private VAPID key that must stay on the server.
Browser Web Push requires a secure context. Use HTTPS in production. During development, browsers also allow service workers and push APIs on localhost.
Do not expose this server publicly without HTTPS, a strong AccessToken, and a separate strong WebhookSecret.
Generate AccessToken and WebhookSecret as two separate random values. The AccessToken is used to sign in to the browser dashboard. The WebhookSecret is embedded in the LidGuard webhook URL, so use a URL-safe value.
Run the command for your OS twice, once for AccessToken and once for WebhookSecret:
# Windows PowerShell
$bytes = New-Object byte[] 32
[System.Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($bytes)
[Convert]::ToBase64String($bytes).TrimEnd('=').Replace('+', '-').Replace('/', '_')# Linux
openssl rand -base64 32 | tr '+/' '-_' | tr -d '='# macOS
openssl rand -base64 32 | tr '+/' '-_' | tr -d '='Keep both values outside Git. Do not reuse the same value for both settings.
Generate VAPID keys from a CLI instead of creating a temporary .NET project. The web-push npm package provides a cross-platform generator and does not add any dependency to this repository.
Install or prepare npx for your OS first:
- Windows: install Node.js LTS from the official installer or run
winget install OpenJS.NodeJS.LTS. - macOS: install Node.js LTS from the official installer or run
brew install node. - Linux: install Node.js and npm from your distribution packages, such as
sudo apt install nodejs npmon Debian/Ubuntu.
Then run the same command on every OS:
npx --yes web-push generate-vapid-keysPut the generated public key in VapidPublicKey and the generated private key in VapidPrivateKey.
Never put the private key in client JavaScript, a service worker, a browser-visible file, or Git-tracked configuration. For development, use dotnet user-secrets. For production, use environment variables or the hosting provider's secret store.
From the repository root:
dotnet user-secrets set "LidGuardNotifications:AccessToken" "<personal-access-token>" --project LidGuard.Notifications
dotnet user-secrets set "LidGuardNotifications:WebhookSecret" "<webhook-secret>" --project LidGuard.Notifications
dotnet user-secrets set "LidGuardNotifications:VapidPublicKey" "<vapid-public-key>" --project LidGuard.Notifications
dotnet user-secrets set "LidGuardNotifications:VapidPrivateKey" "<vapid-private-key>" --project LidGuard.Notifications
dotnet user-secrets set "LidGuardNotifications:VapidSubject" "mailto:you@example.com" --project LidGuard.Notifications
dotnet user-secrets set "LidGuardNotifications:PublicBaseUrl" "https://localhost:5001" --project LidGuard.Notifications
dotnet user-secrets set "LidGuardNotifications:UserInterfaceCulture" "auto" --project LidGuard.NotificationsAccessToken and WebhookSecret must be different values.
DatabasePath is optional. When omitted, the server uses %LOCALAPPDATA%\LidGuard\Notifications\notifications.sqlite on Windows.
Dashboard access uses a short 15-minute authentication cookie. When Keep me signed in is checked, the server also stores a hashed refresh token in SQLite and keeps the refresh cookie valid for 14 days. Each authenticated page request rotates the refresh token and renews the short authentication cookie.
UserInterfaceCulture is optional and defaults to auto. It accepts en, ko, or any CultureInfo-resolvable culture name. The LIDGUARD_UI_CULTURE environment variable overrides it.
Use double underscores for nested configuration:
$env:LidGuardNotifications__AccessToken = "<personal-access-token>"
$env:LidGuardNotifications__WebhookSecret = "<webhook-secret>"
$env:LidGuardNotifications__VapidPublicKey = "<vapid-public-key>"
$env:LidGuardNotifications__VapidPrivateKey = "<vapid-private-key>"
$env:LidGuardNotifications__VapidSubject = "mailto:you@example.com"
$env:LidGuardNotifications__PublicBaseUrl = "https://notify.example.com"
$env:LidGuardNotifications__DatabasePath = "C:\Data\LidGuard\notifications.sqlite"
$env:LidGuardNotifications__UserInterfaceCulture = "auto"
$env:LIDGUARD_UI_CULTURE = "ko"dotnet run --project LidGuard.NotificationsOpen the displayed localhost URL, sign in with AccessToken, keep or clear Keep me signed in, then subscribe the browser from the dashboard.
- Open the dashboard over HTTPS or
localhost. - Sign in with
AccessToken. - Select
Subscribe browser. - Allow browser notifications.
- Use
Unsubscribeto remove the current browser subscription.
Subscriptions are upserted by endpoint. A new browser subscription reactivates the stored row and updates the endpoint keys.
Configure LidGuard with the server URL and webhook secret:
lidguard settings --pre-suspend-webhook-url https://host/api/webhooks/lidguard/{webhookSecret}
lidguard settings --post-session-end-webhook-url https://host/api/webhooks/lidguard/{webhookSecret}
lidguard settings --closed-lid-stop-follow-up-webhook-url https://host/api/webhooks/lidguard/{webhookSecret}Replace {webhookSecret} with the configured WebhookSecret. The webhook secret is for LidGuard only; it must not be the same value as the browser login AccessToken.
Use curl or PowerShell after at least one browser is subscribed:
curl.exe -X POST "https://host/api/webhooks/lidguard/{webhookSecret}" -H "Content-Type: application/json" -d "{\"eventType\":\"PreSuspend\",\"reason\":\"SoftLocked\",\"userInterfaceCulture\":\"ko\",\"softLockedSessionCount\":2}"
curl.exe -X POST "https://host/api/webhooks/lidguard/{webhookSecret}" -H "Content-Type: application/json" -d "{\"eventType\":\"PreSuspend\",\"reason\":\"Completed\",\"userInterfaceCulture\":\"ko\",\"provider\":\"Codex\",\"sessionIdentifier\":\"abc123\",\"startedAtUtc\":\"2026-05-02T00:00:00Z\",\"lastActivityAtUtc\":\"2026-05-02T00:03:00Z\",\"endedAtUtc\":\"2026-05-02T00:04:00Z\",\"endReason\":\"SessionEnd\",\"activeSessionCount\":0,\"inputPromptPreview\":\"Summarize the latest changes\",\"lastAssistantMessage\":\"Updated the webhook payload and notification UI before the configured suspend flow.\"}"
curl.exe -X POST "https://host/api/webhooks/lidguard/{webhookSecret}" -H "Content-Type: application/json" -d "{\"eventType\":\"PostSessionEnd\",\"reason\":\"SessionEnded\",\"userInterfaceCulture\":\"en\",\"provider\":\"Codex\",\"sessionIdentifier\":\"abc123\",\"startedAtUtc\":\"2026-05-02T00:00:00Z\",\"lastActivityAtUtc\":\"2026-05-02T00:03:00Z\",\"endedAtUtc\":\"2026-05-02T00:04:00Z\",\"endReason\":\"SessionEnd\",\"activeSessionCount\":0,\"inputPromptPreview\":\"Summarize the latest changes\",\"lastAssistantMessage\":\"Updated the webhook payload and notification UI. The event list shows a short preview, and clicking the row details shows the full assistant message.\"}"
curl.exe -X POST "https://host/api/webhooks/lidguard/{webhookSecret}" -H "Content-Type: application/json" -d "{\"eventType\":\"StopFollowUp\",\"reason\":\"AwaitingReply\",\"userInterfaceCulture\":\"ko\",\"provider\":\"Codex\",\"sessionIdentifier\":\"abc123\",\"startedAtUtc\":\"2026-05-02T00:00:00Z\",\"lastActivityAtUtc\":\"2026-05-02T00:03:00Z\",\"endedAtUtc\":\"2026-05-02T00:04:00Z\",\"endReason\":\"Stop\",\"replyWaitSeconds\":10,\"replyDeadlineUtc\":\"2026-05-02T00:04:10Z\",\"lastAssistantMessage\":\"Need your reply before I suspend.\"}"The webhook endpoint records the event and immediately returns 202 Accepted. For StopFollowUp, it also returns a JSON body with followUpRequestIdentifier, replyPollUrl, and expiresAtUtc. The background service then sends notifications and records delivery results.
Authenticated dashboards can reply to a pending follow-up through /events or POST /api/follow-ups/{publicIdentifier}/reply with JSON like { "reply": "Keep going and update the changelog too.", "waitForConsumption": true }. They can extend the reply wait without refreshing the page through POST /api/follow-ups/{publicIdentifier}/extend with { "extendMinutes": 1 }; the value must be 1-5 minutes, and the server caps the deadline at the initial reply deadline plus 5 minutes so it cannot outlive the provider hook timeout. They can also cancel the wait through /events or POST /api/follow-ups/{publicIdentifier}/cancel; the next poll returns Canceled, so LidGuard immediately continues the normal stop and suspend flow. Reply, extend, and cancel return JSON with succeeded, status, message, deadlineAtUtc, maximumDeadlineAtUtc, providerHookTimeoutRemainingSeconds, and reply collection fields. LidGuard polls GET /api/follow-ups/{publicIdentifier}/poll/{pollToken} without authentication, so protect the poll token like a temporary secret. Poll responses include the current expiresAtUtc, and updated LidGuard clients use it to keep polling after a dashboard extension. The server stores only the poll token hash in SQLite.
Managed provider hook timeouts are calculated as the post-stop delay plus the initial reply window plus 5 minutes. After changing this feature or updating from an older LidGuard build, re-run hook install/status for the providers you use so their configured timeout is refreshed.
- Serve the app over HTTPS.
- Store
AccessToken,WebhookSecret, andVapidPrivateKeyoutside Git. - Keep
AccessTokenandWebhookSecretdifferent. - Configure
VapidSubjectas a contactablemailto:or HTTPS URL. - Back up the SQLite database if notification history matters.
- Restrict inbound access to the dashboard when possible.
- Monitor the
WebhookEventsandNotificationDeliveriestables or the/eventspage for delivery failures.
- If the browser cannot subscribe, confirm the page is served over HTTPS or
localhost. - If the browser never prompts for notification permission, check that notifications are allowed for the site in browser settings.
- If
Subscribe browserfails with a VAPID error, regenerate keys and make sure the public key in server configuration matches the private key. - If LidGuard webhooks return
404, confirm the URL contains the exactWebhookSecret. - If events appear but notifications do not, open
/eventsand check for permanent or transient delivery failures. - Permanent Web Push failures with HTTP
404or410deactivate the subscription. Subscribe the browser again.