Files
summercms/.planning/notes/2026-10-18-webpush-go.md
Jakub Zych 56b4a72a6a docs(19.1-04): record webpush-go v1.4.0 for Herald before import
Pin github.com/SherClockHolmes/webpush-go v1.4.0 (MIT, jwt/v5 share,
Options.HTTPClient = *fetchguard.Client Do). The in-tree flare module is
the rejected alternative (push.* keys, websockets:generate-vapid-keys).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-18 13:38:06 +02:00

3.2 KiB

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.

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