Help us shape the future of Go! Take the 2026 Go Developer Survey

webpushpush

package
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 1, 2026 License: MIT Imports: 19 Imported by: 0

README

Web Push Driver

arkoren.dev/driver/webpush/webpushpush implements push.Driver for encrypted, standards-based browser notifications. It submits one visible notification at a time without automatic retries. Your application owns subscription storage, permissions, and service-worker display.

Install the module from your application's module directory:

go get arkoren.dev/driver/webpush

First load a persistent P-256 private key using the VAPID key guide. With that key.Signing named identity, an operation context ctx, and an authorized browser subscription named subscription, construct and reuse the driver and sender:

import (
	"arkoren.dev/core/service/push"
	"arkoren.dev/driver/webpush/webpushpush"
)

driver, err := webpushpush.New(identity, "mailto:push@example.com")
sender := push.New(driver)
message := push.NewMessage(subscription, "Order shipped")

_, err = sender.Send(ctx, message)

A nil error confirms provider acceptance, not device delivery or display. The returned Receipt.ID is empty rather than a sensitive provider URL. Construction does no network I/O and requires no lifecycle hooks. Do not generate a new key on every server startup: subscriptions remain bound to the key used when registering. Keep the constructor's pointer rather than copying its synchronized cache. Pointer formatting emits a fixed redaction marker without traversing owned keys or authorizations. This does not sanitize arbitrary caller objects or explicitly unwrapped native error causes, which can contain confidential transport data.

The driver enforces its approved HTTPS endpoint policy and public-address dialing, rejects redirects, and bounds requests and response processing. It sends a flat {title, body, data} JSON payload; the browser's service worker must understand that shape. Native FCM/APNs token delivery is not implemented by this driver.

See the browser integration guide, driver setup and limits, and API reference.

Documentation

Overview

Package webpushpush sends encrypted browser notifications using Web Push, RFC 8291 encryption and RFC 8292 VAPID authentication. It implements arkoren.dev/core/service/push.Driver without lifecycle I/O or retries.

Construct a driver with the application's persistent key.Signing identity and contact URI, then pass it to push.New:

identity, err := key.LoadSigning("vapid-private.pem")
if err != nil {
	return err
}
driver, err := webpushpush.New(identity, "mailto:push@example.com")
if err != nil {
	return err
}
_ = push.New(driver)

The plaintext is flat JSON: {"title":"...","body":"...","data":{...}}. Empty body and data are omitted. Encoded plaintext may contain at most 3993 bytes, including JSON syntax and escaping, for a 4096-byte encrypted record. TTL is rounded down to whole seconds; zero explicitly forbids storage. Only HTTP 201 confirms acceptance, not display or delivery. Receipt IDs are empty: capability URLs in provider headers are neither returned nor followed. Unexpected responses never trigger a retry.

Defaults permit Google's fcm.googleapis.com, Mozilla's updates.push.services.mozilla.com, and subdomains of push.apple.com. AllowedOrigins replaces these defaults with exact HTTPS origins. All connections still require public DNS addresses, verified TLS and port 443. Environment proxies, redirects, connection reuse and transparent retries are disabled. An operation has a finite ten-second default timeout.

VAPID authorizations are reused per origin until five minutes before their twelve-hour expiration, including concurrent sends. At most 256 origins with unexpired authorizations are retained; additional origins fail closed. Reuse a long-lived driver rather than constructing one for each send. Authentication state is not shared across processes or driver instances. Keep constructor pointers instead of copying synchronized drivers. Formatting the pointer emits a fixed redaction marker for owned credentials without traversing caller objects. Error text is bounded, but explicitly unwrapped native causes retain identity and may contain confidential transport data. Applications own subscription authorization, persistence, service workers, visible notification display and reconciliation of uncertain outcomes.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Builder

type Builder struct {
	// contains filtered or unexported fields
}

Builder holds construction-only settings. New detaches its owned slices, so retaining a draft cannot change a published driver.

Example:

_, err := webpushpush.New(identity, "mailto:push@example.com", func(connection *webpushpush.Builder) {
	connection.Timeout(5 * time.Second)
})
_ = err

func (*Builder) AllowedOrigins

func (connection *Builder) AllowedOrigins(origins ...string) *Builder

AllowedOrigins replaces the curated provider policy with exact HTTPS origins, without paths, queries or wildcards. Empty input denies all. This is trusted deployment configuration, never subscription input. Addresses resolved at connection time must still be public.

Example:

_, err := webpushpush.New(identity, "mailto:push@example.com", func(connection *webpushpush.Builder) {
	connection.AllowedOrigins("https://fcm.googleapis.com")
})
_ = err

func (*Builder) Timeout

func (connection *Builder) Timeout(timeout time.Duration) *Builder

Timeout bounds the complete send. Zero restores ten seconds; negative values fail New. A shorter caller context deadline always wins.

Example:

_, err := webpushpush.New(identity, "mailto:push@example.com", func(connection *webpushpush.Builder) {
	connection.Timeout(3 * time.Second)
})
_ = err

type BuilderFunc

type BuilderFunc = func(*Builder)

BuilderFunc edits one fresh construction draft, synchronously and in order. Nil callbacks panic; empty callback slices leave defaults unchanged.

Example:

var policy webpushpush.BuilderFunc = func(connection *webpushpush.Builder) {
	connection.Timeout(time.Second)
}
_, err := webpushpush.New(identity, "mailto:push@example.com", policy)
_ = err

type Driver

type Driver struct {
	// contains filtered or unexported fields
}

Driver sends one encrypted request per Send and never retries a request. Its private credentials and settings are detached from construction inputs; concurrent calls share a synchronized per-origin authorization cache. Keep New's pointer: the mutex must not be copied. Pointer formatting emits a fixed redaction marker without traversing owned credentials or caller objects.

Example:

driver, err := webpushpush.New(identity, "mailto:push@example.com")
if err != nil {
	return err
}
_, err = driver.Send(ctx, push.NewMessage(subscription, "Ready"), push.SendConfig{})
return err

func New

func New(identity key.Signing, contact string, configure ...BuilderFunc) (*Driver, error)

New validates configuration and snapshots key material without network I/O. identity must be a validated key.Signing value; contact must be a mailto address or an HTTPS URI. Invalid settings return a redacted error. Callbacks run once in order even for invalid required inputs. The returned driver is safe for concurrent use and requires no shutdown operation.

Example:

identity, err := key.LoadSigning("vapid-private.pem")
if err != nil {
	return err
}
driver, err := webpushpush.New(identity, "mailto:push@example.com")
if err != nil {
	return err
}
_ = driver
Example

ExampleNew constructs a ready sender without network I/O. Production applications load a persistent key matching their browser subscriptions.

package main

import (
	"fmt"
	"time"

	"arkoren.dev/core/service/push"
	"arkoren.dev/driver/webpush/webpushpush"
	"arkoren.dev/foundation/key"
)

func main() {
	identity, err := key.GenerateSigning()

	if err != nil {
		fmt.Println("key generation failed")

		return
	}

	driver, err := webpushpush.New(identity, "mailto:push@example.com", func(connection *webpushpush.Builder) {
		connection.Timeout(5 * time.Second)
	})

	if err != nil {
		fmt.Println("configuration failed")

		return
	}

	sender := push.New(driver)
	fmt.Println(sender != nil)
}
Output:
true

func (*Driver) Format

func (driver *Driver) Format(state fmt.State, _ rune)

Format redacts the owned signing scalar and cached authorizations without traversing cache state, transports or caller objects. Keep the constructor's pointer: Driver contains a mutex and must not be copied.

Example:

text := fmt.Sprintf("%#v", driver)
_ = text // [REDACTED Web Push driver]

func (*Driver) Send

func (driver *Driver) Send(ctx context.Context, message push.Message, config push.SendConfig) (push.Receipt, error)

Send validates the message, endpoint policy, actual encoded size and TTL before dispatch. Only value push.Subscription recipients are supported. TTL is truncated to seconds and must not exceed 2147483647 seconds. Failures return a zero receipt. A 201 response returns an empty receipt ID even if optional headers are absent or cleanup fails. Transport errors conservatively wrap push.ErrOutcomeUnknown and preserve native/context error identity. Top-level diagnostics never contain endpoint URLs, keys or payload text.

Example:

message := push.NewMessage(subscription, "Your export is ready")
_, err := driver.Send(ctx, message, push.SendConfig{TTL: time.Hour})
if errors.Is(err, push.ErrNotFound) {
	invalidateSubmittedVersion()
}
return err

type ProviderError

type ProviderError struct {
	// StatusCode is the authoritative HTTP response code, not a delivery result.
	// Example: if rejected.StatusCode == 410 { invalidateSubmittedVersion() }
	StatusCode int
	// RetryAfter is the nonnegative provider delay, or zero when absent,
	// invalid, in the past or too large for time.Duration. No retry is made.
	// Example: nextAttemptAt := time.Now().Add(rejected.RetryAfter)
	RetryAfter time.Duration
	// contains filtered or unexported fields
}

ProviderError records a rejected provider response without retaining its body, endpoint, headers or capability URL. Errors.As exposes safe metadata; errors.Is follows its portable push classification through Unwrap.

Example:

var rejected *webpushpush.ProviderError
if errors.As(err, &rejected) {
	log.Printf("push rejected: status=%d retry_after=%s", rejected.StatusCode, rejected.RetryAfter)
}

func (*ProviderError) Error

func (rejected *ProviderError) Error() string

Error produces safe diagnostic text containing only the numeric status. It never includes provider-controlled body text or endpoint capabilities.

Example:

var rejected *webpushpush.ProviderError
if errors.As(err, &rejected) { log.Print(rejected.Error()) }

func (*ProviderError) Unwrap

func (rejected *ProviderError) Unwrap() error

Unwrap returns the portable push classification for errors.Is checks, or nil when no contract sentinel describes the rejection. Authentication failures, redirects and other unclassified rejections remain typed provider errors; they do not mean the recipient is unsupported or its subscription expired.

Example:

if errors.Is(err, push.ErrNotFound) { invalidateSubmittedVersion() }

Directories

Path Synopsis
internal
network
Package network owns endpoint authority policy and pinned public-address dialing.
Package network owns endpoint authority policy and pinned public-address dialing.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL