diff --git a/.planning/notes/2026-10-18-webpush-go.md b/.planning/notes/2026-10-18-webpush-go.md new file mode 100644 index 00000000..b2a888ce --- /dev/null +++ b/.planning/notes/2026-10-18-webpush-go.md @@ -0,0 +1,30 @@ +# Decision: webpush-go for Herald Web Push + +**Date:** 2026-10-18 +**Status:** Accepted +**Context:** Phase 19.1 plan 04 (`sm-herald-plugin`), D-09 / D-10 / D-12, 19.1-RESEARCH.md Pitfall 7 and Pitfall 8 + +## Decision + +`sm-herald-plugin` depends on `github.com/SherClockHolmes/webpush-go` at **v1.4.0** (2025-01-02, MIT, Go 1.13) for VAPID (RFC 8292) and aes128gcm (RFC 8291) Web Push. Confirmed this session: `go list -m -versions github.com/SherClockHolmes/webpush-go` lists v1.4.0 as current. + +- The frozen PHP plugin (`golem15/herald` @ a749f83) sends through Minishlink WebPush. The Go port needs the same record encryption, VAPID JWT, TTL and urgency — not a hand-rolled AES-GCM / ES256 stack (HKDF, subscription-key decode, RFC 8291 padding). +- The library is MIT, maintained, and already shares `github.com/golang-jwt/jwt/v5` with the framework (`go.mod` pins jwt/v5 v5.3.1). Transitive: jwt/v5 and `golang.org/x/crypto`. +- `Options.HTTPClient` is the `Do(*http.Request) (*http.Response, error)` interface. `fetchguard.Client.Do` matches it (`modules/fetchguard/client.go`), so `*fetchguard.Client` is passed as `Options.HTTPClient` directly. Pitfall 8: no adapter type, no `http.Client{Transport: …}` wrapper. +- Send uses `fetchguard.NewClient` with `PublicOnlyMode` (HTTPS only; private and reserved IPs refused at dial time). Subscribe validates the same policy. There is no `EndpointGuard` type. +- PHP defaults carry over: TTL **2419200** (4 weeks) and urgency **normal**, both overridable per send. The worker prunes rows on HTTP **404** and **410**. +- VAPID keys are config keys `golem15.herald.vapid.{subject,public_key,private_key}` (PHP env names `VAPID_SUBJECT` / `VAPID_PUBLIC_KEY` / `VAPID_PRIVATE_KEY`). This note does not generate keys. + +The in-tree Web Push module (`modules/flare`) is **not** used as the sender. It already implements RFC 8291/8292, but its config keys are `push.*`, its command is `websockets:generate-vapid-keys`, and it takes `*http.Client` rather than the `HTTPClient` Do interface. D-09 locks webpush-go so Herald keeps PHP's `golem15.herald.vapid.*` keys and `herald:vapid-generate` command name. + +## Related dependencies + +- `fetchguard` (in-tree) is the SSRF boundary: PublicOnly at subscribe and send. Tests inject `fetchguard.WithTransport` — no extra Doer type. +- `github.com/golang-jwt/jwt/v5` is already a framework dependency; webpush-go's VAPID JWT uses the same module. Herald does not add a second JWT library. +- `modules/flare` stays the framework Web Push package for other callers. Herald never imports it. + +## Consequences + +- `sm-herald-plugin/go.mod` requires `github.com/SherClockHolmes/webpush-go v1.4.0` only **after** this note exists. The importer is the plugin sender (`classes/sender.go`); `Options.HTTPClient` is the PublicOnly `*fetchguard.Client`. +- `summer herald:vapid-generate` calls `webpush.GenerateVAPIDKeys` and prints `VAPID_PUBLIC_KEY=` / `VAPID_PRIVATE_KEY=` lines. The private key is never written to slog, payloads, or README examples. +- Swapping the library later is a local sender change (D-09 reversibility). A later bump still needs a note confirming `HTTPClient` stays the Do interface so fetchguard continues to pass through without an adapter.