feat(notify): Web Push plumbing end-to-end — VAPID key file, subscriptions, sender, service-worker handlers, settings enrollment #98
Labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Cloonar/coding-lab#98
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What to build
The complete Web Push (VAPID, RFC 8292) pipe with no trigger logic: the tracer bullet that proves HTTPS, push-gateway egress, and iOS PWA enrollment before any notification source exists. Standards-based Web Push only — no Apple/Google developer registration; the server authenticates to the gateways (web.push.apple.com, fcm.googleapis.com, Mozilla autopush) with a lab-generated VAPID keypair.
internal/vault/masterkey.go+cmd/lab/main.gowiring):--vapid-key-file/LAB_VAPID_KEY_FILE, default<state-dir>/vapid.key, generated on first start when absent (atomic exclusive write, 0600), refuses lax permissions on load, never overwrites an existing file.push_subscriptionstable (migration): endpoint (unique), p256dh/auth keys, created-at, a user-agent-derived device label.pushhandler renders title/body/tag from the JSON payload;notificationclickfocuses an existing client or opens the payload's route. The existing rule stands:/api,/agent, probes never intercepted.Acceptance criteria
notificationclickto the payload route;/apiand/agentremain un-intercepted (extend the existing PWA smoke)Blocked by
None - can start immediately
Agent Brief
Category: enhancement
Summary: Build the complete standards-based Web Push (VAPID, RFC 8292) pipeline — key lifecycle, subscription storage, async sender, service-worker handlers, settings enrollment — with no trigger logic. This is the tracer bullet for the notification stack (#99, #100, #101 build on it); the settings page's Send test button is the only notification source in this slice.
Current behavior:
lab has no push capability at all:
install/activate/fetch. It has nopushornotificationclickhandlers — these are net-new. It does already enforce a network-only rule that never intercepts/api/*,/agent/*,/healthz,/readyz, or/metrics; that exact rule must survive unchanged.requireAuth-style wrapper, with CSRF protection on mutations.Desired behavior:
--vapid-key-fileflag /LAB_VAPID_KEY_FILEenv var, defaulting to<state-dir>/vapid.key, holding a lab-generated VAPID keypair with the master-key file semantics described above. The server authenticates to the public push gateways (web.push.apple.com, fcm.googleapis.com, Mozilla autopush) with this key — no Apple/Google developer registration anywhere.push_subscriptionstable keyed by unique endpoint, storing the p256dh and auth keys, a created-at timestamp, and a device label derived from the enrolling user agent.pushhandler renders title/body/tag from the JSON payload; anotificationclickhandler focuses an existing client on the payload's route or opens a new one. The network-only rule for/api,/agent, and probe paths stays intact.Key interfaces:
push_subscriptionsmigration added to both dialect trees with the same version number.requireAuth+ CSRF pattern.Acceptance criteria:
notificationclickroutes to the payload's route;/api,/agent, and probe paths remain un-intercepted (new SW smoke coverage — none exists today)Out of scope:
Land-PR audit — PR #102 (
afk/98) → VERDICT: PASSVerification signal relied on: Forgejo Actions CI, both checks green (not re-run):
ci / native— success (8m39s)ci-nix / flake-check— success (15m30s), so the nix Go suite, golangci-lint, web vitest/eslint/prettier, and nixos-module eval all passed.Mergeability: clean merge into
main, no conflicts.Conventions: Conventional Commits title (
feat(notify): …);Closes #98present; AFK branchafk/98.Diff review (correctness against intent):
internal/push/key.go— VAPID key file mirrors the vault master-key contract: refuses group/other-readable perms, never overwrites (exclusivelink(2), race → exactly one winner), rejects hex-valid-but-out-of-range scalars viaecdh.P256().NewPrivateKey.internal/fsx— the vault crash-safe write helpers extracted verbatim (byte-identical temp+fsync+rename/link+dir-fsync); vault + materialize call sites updated; behavior-preserving.internal/push/sender.go— fire-and-forget on a trackedWaitGroup, hard panic-recover per send, background contexts (survive the request), 30s timeout, 404/410 endpoint reaping, endpoint logged host-only (path is a bearer capability).internal/httpapi/push.go—requireAuth+ tree-wide CSRF, nil-push-safe route mounting, endpoint http(s)/length validation, server-derived device label (not client-trusted), keys never echoed back.internal/store/pushsubscriptions.go+ migration 0007 — endpoint-UNIQUE upsert preserving id/created_at, idempotent by-endpoint delete for the reap race; sqlite/postgres parity.web/public/sw.js—pushalways shows a notification even on malformed payload (avoids the iOS Safari silent-push subscription-revocation trap); empty-tag guard;notificationclickfocus-or-open deep link.No blockers; no changes requested. Awaiting the maintainer's free-text merge go-ahead.