Skip to content

Latest commit

 

History

History
164 lines (112 loc) · 11 KB

File metadata and controls

164 lines (112 loc) · 11 KB

LidGuard.Notifications

🌐 한국어

Overview

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.

HTTPS And Localhost Requirements

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.

AccessToken And WebhookSecret Generation

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.

VAPID Key Generation

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 npm on Debian/Ubuntu.

Then run the same command on every OS:

npx --yes web-push generate-vapid-keys

Put 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.

User-Secrets Configuration

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.Notifications

AccessToken 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.

Environment Variable Configuration

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"

Local Run

dotnet run --project LidGuard.Notifications

Open the displayed localhost URL, sign in with AccessToken, keep or clear Keep me signed in, then subscribe the browser from the dashboard.

Browser Subscribe And Unsubscribe

  1. Open the dashboard over HTTPS or localhost.
  2. Sign in with AccessToken.
  3. Select Subscribe browser.
  4. Allow browser notifications.
  5. Use Unsubscribe to remove the current browser subscription.

Subscriptions are upserted by endpoint. A new browser subscription reactivates the stored row and updates the endpoint keys.

LidGuard Webhook URL

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.

Test Webhook Call

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.

Operations Checklist

  • Serve the app over HTTPS.
  • Store AccessToken, WebhookSecret, and VapidPrivateKey outside Git.
  • Keep AccessToken and WebhookSecret different.
  • Configure VapidSubject as a contactable mailto: or HTTPS URL.
  • Back up the SQLite database if notification history matters.
  • Restrict inbound access to the dashboard when possible.
  • Monitor the WebhookEvents and NotificationDeliveries tables or the /events page for delivery failures.

Troubleshooting

  • 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 browser fails 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 exact WebhookSecret.
  • If events appear but notifications do not, open /events and check for permanent or transient delivery failures.
  • Permanent Web Push failures with HTTP 404 or 410 deactivate the subscription. Subscribe the browser again.