Document the push providers in the README and the audit catalogue

README covers the provider model, the two providers and their transports, the
admin configuration surface at /admin/services/push, the delivery loop and the
updated file map. events.md records that push.subscribe and push.update now
carry the provider in their metadata, with endpoint_host set only for
endpoint-based providers.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
retoor 2026-07-31 22:50:54 +02:00
parent 7674dac628
commit a78b656ef9
2 changed files with 48 additions and 7 deletions

View File

@ -15,6 +15,12 @@ make test-headed # same tests in visible browser
Open `http://localhost:10500`.
PDF export (DeepSearch reports, via weasyprint) needs the Pango text stack installed at system level. The production image installs it; on a development host install it once:
```bash
sudo apt-get install -y libglib2.0-0 libpango-1.0-0 libpangoft2-1.0-0 libharfbuzz0b libfontconfig1 fonts-dejavu-core
```
## Stack
| Layer | Technology |
@ -40,7 +46,7 @@ devplacepy/
avatar.py # Multiavatar generation, URL builder
utils/ # Password hashing, session mgmt, time_ago, notification hook (package)
models.py # Pydantic schemas
push.py # Web push crypto, VAPID keys, encrypt/send/register
push/ # Push delivery: provider protocol, Web Push, APNs, registrations
routers/ # One file per domain (auth, feed, posts, push, ...)
templates/ # Jinja2 HTML templates
static/css/ # Page-specific CSS files
@ -811,9 +817,37 @@ calling itself.
## Push notifications & PWA
Authenticated users can receive native web push notifications, and the site is an
Authenticated users can receive native push notifications, and the site is an
installable Progressive Web App. Push uses only standard libraries (`cryptography`,
`PyJWT`, `httpx`) against the Web Push Protocol - no third-party push wrapper.
`PyJWT`, `httpx`) against the Web Push Protocol and the Apple Push Notification service -
no third-party push wrapper.
### Providers
Delivery is split into providers behind one protocol (`devplacepy/push/providers/`). A user
receives a notification through every provider they hold a live subscription for.
| Provider | Registration | Transport |
|----------|--------------|-----------|
| `webpush` | `PushSubscription` from the browser `PushManager` (endpoint + `p256dh`/`auth` keys) | Web Push Protocol, VAPID signed, `aesgcm` encrypted payload |
| `apns` | Hexadecimal device token | `POST https://api.push.apple.com/3/device/{token}` over HTTP/2, ES256 provider token |
`POST /push.json` accepts a registration for any active provider; a body without a
`provider` field is a `webpush` body, so browsers need no change. `GET /push.json` returns
the VAPID public key plus the providers currently accepting registrations. A provider that
is disabled or not fully configured accepts no registrations and is skipped during
delivery, so an unconfigured provider is inert rather than an error.
Every provider setting is edited at **`/admin/services/push`**: per provider an `Enabled`
toggle, the VAPID subject for `webpush`, and team id, key id, `.p8` auth key (stored as a
masked secret), topic and environment (production or sandbox) for `apns`. The same page
holds the shared delivery timeout and the retention window after which dead subscriptions
are removed. Push delivery does not depend on that service running; stopping it only stops
the pruning sweep.
Adding a third provider is one file plus one registry entry: the registration route, the
delivery loop, the admin page, the audit record and the metrics are all written against the
provider protocol.
### Events
@ -836,9 +870,11 @@ in real time, bridged onto the in-process pub/sub bus by a lock-owner relay:
`create_notification` schedules delivery as a fire-and-forget async task, so a dead
subscription or push-service error never blocks the triggering request. Delivery
(`push.notify_user`) iterates a user's subscriptions, encrypts the payload
(legacy `aesgcm` content encoding), and POSTs to each endpoint; subscriptions that
return `404`/`410` are soft-deleted.
(`push.notify_user`) reads a user's subscriptions once, groups them by provider, builds
each provider's payload once, and sends over a single shared HTTP client. A subscription
the push service reports as gone (`404`/`410` for Web Push, `410` or an `Unregistered`
class reason for APNs) is soft-deleted; any other failure is logged and the subscription is
kept.
A notification is also **marked read automatically when you open the page that shows its
content** - viewing a post clears its comment, reply, upvote and mention notifications;
@ -895,7 +931,10 @@ offline. Installation requires a secure origin (HTTPS, or `localhost` for develo
| File | Role |
|------|------|
| `devplacepy/push.py` | VAPID keys, payload encryption, send, register |
| `devplacepy/push/providers/` | Provider protocol, Web Push (VAPID keys, payload encryption), APNs |
| `devplacepy/push/store.py` | `push_registration` reads and writes |
| `devplacepy/push/delivery.py` | `notify_user` - group by provider, deliver, reap dead subscriptions |
| `devplacepy/services/push/service.py` | Provider configuration at `/admin/services/push`, retention sweep, metrics |
| `devplacepy/routers/push.py` | `/push.json`, `/service-worker.js`, `/manifest.json` |
| `static/js/PushManager.js` | Service-worker registration + subscribe + opt-in UI |
| `static/service-worker.js` | Receives push, shows notification, offline fallback |

View File

@ -408,6 +408,8 @@ Every state-changing action in DevPlace records one append-only row through `dev
| `push.subscribe` | `routers/push.py` |
| `push.update` | `routers/push.py` |
Both carry `metadata.provider` (`webpush`, `apns`, ...) plus `created`; `metadata.endpoint_host` is set for endpoint-based providers and `null` for token-based ones.
## Gamification (`reward`)
| Event key | Recorded in |