Skip to content

feat(webpush): add Web Push (VAPID) provider - #30

Merged
Wikid82 merged 14 commits into
mainfrom
development
Sep 14, 2026
Merged

Wikid82 merged 14 commits into
mainfrom
development

Conversation

@Wikid82

@Wikid82 Wikid82 commented Sep 14, 2026

Copy link
Copy Markdown
Owner

Summary

  • Adds providers/webpush, a new notify.Sender for direct browser Web Push delivery (RFC 8291 payload encryption + RFC 8292 VAPID auth) — no third-party relay involved, stdlib-only crypto (crypto/ecdh, crypto/hkdf, crypto/ecdsa, crypto/aes/crypto/cipher). Closes Add provider: Web Push (Push API / VAPID) #14.
  • Ships GenerateVAPIDKeyPair() for setup ergonomics, and a documented Config.TTL == 0 → 4-week default policy (a deliberate, permanent simplification vs. RFC 8030's literal zero-TTL "attempt-only" semantics — flagged explicitly in the spec and doc comments).
  • Small supporting changes: providers/internal/regconfig.IntField helper, and an additive-only transport.sanitizeOutboundHeaders allowlist addition (Content-Encoding/TTL/Urgency/Topic) needed for Web Push requests.
  • Full plan → implementation → review → QA pipeline: docs/plans/current_spec.md (design), two rounds of implementation review, and docs/reports/qa_report.md (final QA/security pass — 2 non-blocking findings, both fixed).

Test plan

  • go build ./...
  • go vet ./... / staticcheck ./... clean
  • go test ./... — all packages pass
  • bash scripts/test-coverage.sh — 94.6% repo-wide (floor 85%)
  • RFC 8291 Appendix A fixed-vector test — exact-byte match against the RFC's published example
  • grep -r "Wikid82/charon" --include=*.go . — zero hits
  • providers/all/all_test.go's wantProviderCount bumped 8→9, passing

Wikid82 and others added 14 commits September 14, 2026 16:58
Ports Charon's orchestration model (.claude/agents/planning, supervisor,
go-dev, qa-security, devops, docs-writer) scaled to this repo's surface —
no frontend-dev/playwright-dev since there's no frontend or E2E here.
CLAUDE.md's Orchestration Model section replaces the prior "no multi-agent
pipeline" stance.

Also installs lefthook (scaled down from Charon's lefthook.yml: file
hygiene, shellcheck, actionlint, go vet, staticcheck) so the Definition of
Done's lint requirements are enforced automatically on every commit
instead of relying on remembering to run them by hand.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JdcrJhvqk42UL3b1mEKFMs
Records the approved technical spec for providers/webpush (RFC 8291/8292
Web Push) so the design that drove the implementation is tracked in repo
history alongside it, matching Charon's docs/plans/*.md convention.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012zpLzMxyw465hYA4WjoQVW
Webpush's Config.TTL needs an int-typed config field; regconfig only had
StringField/StringSliceField before this. Accepts int/int64/float64 to
cover both native Go construction and a generic JSON-style decode.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012zpLzMxyw465hYA4WjoQVW
…aders

Web Push requires these four RFC 8030/8188 headers on every request, none
of which were in the outbound header allowlist. Additive only — every
existing provider's tests still pass unchanged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012zpLzMxyw465hYA4WjoQVW
Implements the Web Push message encryption content-coding: ECDH between a
fresh ephemeral sender keypair and the subscriber's p256dh key, two-step
HKDF-SHA256 derivation of the content-encryption key/nonce, and RFC 8188
single-record aes128gcm framing. encryptAES128GCMWithKeys takes the
ephemeral keypair/salt as parameters (rather than generating them) so
tests can force RFC 8291 Appendix A's exact published example and assert
byte-for-byte equality against its ciphertext — the highest-value
correctness gate in this feature, since a round-trip-only test can't
catch a bug that's symmetric on both the encrypt and decrypt sides.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012zpLzMxyw465hYA4WjoQVW
buildVAPIDHeader signs an ES256 JWT (raw r||s signature, not ASN.1 DER)
and formats it as the RFC 8292 "vapid t=<jwt>, k=<key>" Authorization
header value; it takes the three VAPID strings directly rather than a
Config, since Config isn't defined until a later commit. GenerateVAPIDKeyPair
is a small convenience for hosts that don't already have an externally
generated keypair, using the non-deprecated ecdsa.ParseRawPrivateKey/
ParseUncompressedPublicKey/Bytes APIs to avoid crypto/elliptic's
deprecated low-level Marshal/ScalarBaseMult surface.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012zpLzMxyw465hYA4WjoQVW
Wires the RFC 8291 encryption and RFC 8292 VAPID signing pieces from the
previous two commits into a full notify.Sender: fail-fast validation of
all six required Config fields plus a VAPID keypair-consistency check
(catches a mismatched public/private key pair locally instead of an
opaque remote 401), Urgency/Topic validation, template rendering,
encryption, and dispatch via transport.Wrapper with the aes128gcm/TTL/
Authorization headers set. Unlike ntfy/pushover, the rendered payload is
not required to carry a "message" field — the payload shape is entirely
up to the receiving service worker, which this module has no visibility
into.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012zpLzMxyw465hYA4WjoQVW
Registers the webpush Factory under "webpush" (mirroring every other
provider's register.go convention) and adds it to providers/all's blank
imports so it's available via notify.New without an explicit per-provider
import. Bumps providers/all's wantProviderCount safety-net test.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012zpLzMxyw465hYA4WjoQVW
Adds a providers/webpush row to README's provider table, updates the
"Project status" paragraph to explain why webpush is a deliberate,
maintainer-approved exception to the "no new providers without an
explicit ask" policy rather than a violation of it, adds a webpush.New
construction example (with the one-Client-per-subscriber fan-out pattern)
to docs/INTEGRATION.md, and flags webpush.Config's app-wide/per-recipient
field split as a documented precedent in ARCHITECTURE.md §3.2.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012zpLzMxyw465hYA4WjoQVW
… add QA report

QA/Security final gate for issue #14 (Web Push). Closes real (non-fault-injection)
coverage gaps: invalid-base64 (not merely wrong-length) p256dh/auth in
encryptAES128GCMWithKeys, and malformed VAPID public/private keys reached
through the real Client.Send path rather than only the lower-level
buildVAPIDHeader seam. providers/webpush package coverage 87.7% -> 89.6%.

Full gate results, SSRF/retry/crypto review, and two non-blocking findings
(raw Endpoint echoed into a vapid.go error on malformed-URL input; the
INTEGRATION.md example logs the raw Endpoint on send failure) are in
docs/reports/qa_report.md, with concrete remediations for both.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012zpLzMxyw465hYA4WjoQVW
buildVAPIDHeader wrapped net/url.Parse's error with %w, which embeds the
full raw input — leaking a malformed Endpoint's bearer-token-equivalent
path segment (e.g. FCM) into Send's returned error. Return a generic
message instead, matching transport/wrapper.go's buildSafeRequestURL
convention for the same class of failure. Fixes QA report Finding 1
(MEDIUM, docs/reports/qa_report.md).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012zpLzMxyw465hYA4WjoQVW
…example

Fixes QA report Finding 2 (LOW): docs/INTEGRATION.md's webpush fan-out
example logged sub.Endpoint verbatim on every Send failure, which for
push services like FCM commonly embeds a bearer-token-equivalent path
segment; it now logs only the endpoint's host via neturl.Parse. Also
fixes stale provider-count/list omissions of webpush found during the
final docs pass (README.md intro, ARCHITECTURE.md's providers/* list
and §3.8, docs/INTEGRATION.md's "eight providers" bullets) so all three
docs correctly reflect nine built-in providers including webpush.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012zpLzMxyw465hYA4WjoQVW
Integrates the 0.2.3 release-please automation (version bump + changelog) that landed upstream while the webpush provider work was in progress locally.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012zpLzMxyw465hYA4WjoQVW
@Wikid82
Wikid82 merged commit 9411a45 into main Sep 14, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add provider: Web Push (Push API / VAPID)

1 participant