Service worker push handlers and client subscribe (Feature 19) #156

3 Commits
Author SHA1 Message Date
dev_agent 6963eb9b20 Merge remote-tracking branch 'origin/feature/bbd91ead-1e2b-48fb-b577-fa0a3faf5842-notifications-web-push-and-google-chat' into task/push-handlers_fb6dd817-2224-4800-9b7d-8499e856e171
CI / ci (push) Successful in 5m19s
CI / ci (pull_request) Successful in 5m28s
2026-10-09 10:11:18 -04:00
dev_agentandClaude Opus 5.5 cafea18651 Refuse protocol-relative notification paths and require an observed tap
CI / ci (push) Failing after 4m32s
CI / ci (pull_request) Failing after 4m28s
PUSH-01: appPath now rejects a path that resolves to a leading // after
the URL parser handles dot segments (also %2e), backslashes and stripped
tabs and newlines, and planClick throws if the final URL is off origin.
Regression tests drive openNotification with the attack forms.

PUSH-02: subscribePush no longer asks for notification permission where
navigator.userActivation is unavailable; it throws a typed
PermissionGestureError ('unobservable', or 'inactive' outside a tap).
The test for that path now overrides userActivation directly: the old one
passed undefined into a defaulted parameter and so never exercised it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 09:19:28 -04:00
dev_agentandClaude Opus 5.5 067e85bd54 Add service worker push handlers and client push subscribe
CI / ci (push) Failing after 7m33s
CI / ci (pull_request) Failing after 4m31s
The worker's push listener shows the payload's title, body and tag with
the app icon and badge, carrying the same-origin url as its data; a
payload that is not a MealQuest notice (no payload, not JSON, no title,
wrong field types, an off-origin url) shows a generic MealQuest
notification and logs why, never throwing. notificationclick focuses
an open MealQuest window and navigates it to the url, or opens a new
window there. Both listeners delegate to pure functions in
web/src/push/notification.ts.

web/src/push/subscribe.ts adds subscribePush, which checks support,
asks for permission only during a tap (and before any await), subscribes
with the server's VAPID key, replacing a subscription made with an
older key, and POSTs exactly {endpoint, keys, user_agent?}; and
unsubscribePush, which DELETEs on the server and then unsubscribes the
browser, answering not-subscribed when there is nothing to remove.
usePush binds both to the app's API client.

Maestro task fb6dd817-2224-4800-9b7d-8499e856e171 (Feature 19).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 08:48:00 -04:00