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>
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/v5with the framework (go.modpins jwt/v5 v5.3.1). Transitive: jwt/v5 andgolang.org/x/crypto. Options.HTTPClientis theDo(*http.Request) (*http.Response, error)interface.fetchguard.Client.Domatches it (modules/fetchguard/client.go), so*fetchguard.Clientis passed asOptions.HTTPClientdirectly. Pitfall 8: no adapter type, nohttp.Client{Transport: …}wrapper.- Send uses
fetchguard.NewClientwithPublicOnlyMode(HTTPS only; private and reserved IPs refused at dial time). Subscribe validates the same policy. There is noEndpointGuardtype. - 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 namesVAPID_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 injectfetchguard.WithTransport— no extra Doer type.github.com/golang-jwt/jwt/v5is already a framework dependency; webpush-go's VAPID JWT uses the same module. Herald does not add a second JWT library.modules/flarestays the framework Web Push package for other callers. Herald never imports it.
Consequences
sm-herald-plugin/go.modrequiresgithub.com/SherClockHolmes/webpush-go v1.4.0only after this note exists. The importer is the plugin sender (classes/sender.go);Options.HTTPClientis the PublicOnly*fetchguard.Client.summer herald:vapid-generatecallswebpush.GenerateVAPIDKeysand printsVAPID_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
HTTPClientstays the Do interface so fetchguard continues to pass through without an adapter.