webpushkit

package
v0.57.0 Latest Latest
Warning

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

Go to latest
Published: Aug 12, 2026 License: Apache-2.0 Imports: 24 Imported by: 0

README

webpushkit

Транспорт доставки Web Push: VAPID-аутентификация (RFC 8292), aes128gcm-шифрование payload (RFC 8291 поверх RFC 8188) и POST на endpoint push-сервиса (RFC 8030). Kit — только типы и отправка; хранение подписок и планирование уведомлений — зона потребителя (notification-домен).

Сценарий: SPA-вкладка закрыта, но пользователю нужно доставить пуш (смена фазы помодоро, дедлайн). Браузер держит подписку в своём push-сервисе; сервис POST'ит ей зашифрованное сообщение, service worker будит и показывает нотификацию.

Импорт

import "git.stit.tech/stit-core/golibinfra/webpushkit"

Версия модуля — см. ../README.md (§ Версионирование).

Генерация VAPID-ключей

VAPID-пара (P-256/ES256) выдаётся один раз на сервис. Public-ключ также кладётся в браузерный PushManager.subscribe({ applicationServerKey }).

pub, priv, err := webpushkit.GenerateVAPIDKeys()
// pub  → WEBPUSH_VAPID_PUBLIC_KEY  (+ applicationServerKey фронта)
// priv → WEBPUSH_VAPID_PRIVATE_KEY (секрет)

Оба значения — base64url без паддинга: public — uncompressed point (65 байт), private — raw scalar (32 байта).

Быстрый старт (fx)

Module provide'ит *Sender. Config не provide'ится (как в cachekit/oidckit) — caller добавляет webpushkit.NewConfig. *http.Client опционален: без него — http.DefaultClient; передашь свой (httpclient/otelhttp) — Sender возьмёт его и вы получите транспортные RED-метрики.

fx.New(
    fx.Provide(webpushkit.NewConfig), // WEBPUSH_*
    webpushkit.Module,                // *Sender
)

Без fx:

cfg, _ := webpushkit.NewConfig()
sender, err := webpushkit.NewSender(cfg, nil) // nil → http.DefaultClient

Отправка

Подписку (endpoint + keys.p256dh + keys.auth) потребитель хранит у себя и передаёт в Send как есть:

err := sender.Send(ctx, webpushkit.Subscription{
    Endpoint: sub.Endpoint,
    P256dh:   sub.P256dh,
    Auth:     sub.Auth,
}, payload,
    webpushkit.WithTTL(10*time.Minute),
    webpushkit.WithUrgency(webpushkit.UrgencyHigh),
    webpushkit.WithTopic("pomodoro"), // новый пуш с тем же топиком вытесняет прежний недоставленный
)

switch {
case err == nil:
    // доставлено push-сервису
case errors.Is(err, webpushkit.ErrSubscriptionGone):
    store.Delete(ctx, sub.ID) // 404/410: подписка мертва — удаляем
case errors.Is(err, webpushkit.ErrPayloadTooLarge):
    // payload > ~3993 байт plaintext
default:
    var pe *webpushkit.PushError // прочий не-2xx: pe.StatusCode / pe.RetryAfter (напр. 429)
    _ = errors.As(err, &pe)
}

payload — произвольные байты (обычно JSON, который читает service worker). Потолок plaintext — ~3993 байта: столько, сколько влезает в гарантированные push-сервисами 4096 байт после aes128gcm-накладных.

Конфигурация

Имена env финализируются при первом потребителе (envconfig-теги — часть API kit'а).

Env Поле Default Назначение
WEBPUSH_VAPID_PUBLIC_KEY Config.VAPIDPublicKey VAPID public (uncompressed P-256, base64url); опционален — при наличии сверяется с private
WEBPUSH_VAPID_PRIVATE_KEY Config.VAPIDPrivateKey VAPID private (raw ES256 scalar, base64url); секрет
WEBPUSH_SUBJECT Config.Subject sub VAPID-JWT: mailto: или https:// контакт отправителя
WEBPUSH_DEFAULT_TTL Config.DefaultTTL 24h TTL по умолчанию (переопределяется WithTTL)

API

  • GenerateVAPIDKeys() (publicB64, privateB64 string, err error).
  • NewConfig() (Config, error); Config{VAPIDPublicKey, VAPIDPrivateKey, Subject, DefaultTTL}.
  • NewSender(cfg Config, client *http.Client) (*Sender, error) (client nil → default); (*Sender).Send(ctx, Subscription, payload []byte, ...SendOption) error.
  • Subscription{Endpoint, P256dh, Auth} — как отдаёт браузерный PushManager.
  • SendOption: WithTTL(time.Duration), WithUrgency(Urgency), WithTopic(string); Urgency ∈ {UrgencyVeryLow, UrgencyLow, UrgencyNormal, UrgencyHigh}.
  • Sentinel'ы: ErrConfig, ErrSubscription, ErrSubscriptionGone, ErrPayloadTooLarge, ErrPush; *PushError{StatusCode, RetryAfter, Body} (Is ErrPush).
  • Module (fx) — provide *Sender.

Семантика ErrSubscriptionGone

Push-сервис ответил 404 или 410 → подписка недействительна (пользователь отозвал разрешение, сменил устройство, чистка на стороне сервиса). Потребитель обязан удалить такую подписку из своего хранилища и больше на неё не слать — иначе накопятся мёртвые endpoint'ы. Прочие не-2xx (*PushError) — временные: решение про retry/backoff (в т.ч. по RetryAfter при 429) за потребителем.

Хранение подписок — у потребителя

Kit намеренно не хранит подписки: их модель, привязка к пользователю/устройству, TTL и чистка мёртвых — доменная логика (notification-домен). Kit получает Subscription на вход и возвращает результат доставки.

Observability

Собственных метрик kit не эмитит: HTTP-сигналы (RED, latency) даёт *http.Client, если передать обёртку httpclient/otelhttp в NewSender. Ошибки доставки возвращаются вызывающему для доменного учёта (метрика доставленных/умерших пушей — на стороне потребителя).

Инварианты

  • Эфемерная ECDH-пара сервера приложения и record salt — новые на каждое сообщение (RFC 8291): переиспользование компрометирует шифрование.
  • aes128gcm кладёт dh/salt в тело; в заголовках — только Content-Encoding: aes128gcm, Authorization: vapid t=…, k=…, TTL (+ опц. Urgency/Topic). Legacy-схема aesgcm с Crypto-Key/Encryption не поддерживается.
  • VAPID public, если задан в конфиге, обязан соответствовать private — иначе ErrConfig (k= в Authorization и applicationServerKey браузера должны быть одним ключом).
  • Subject — только mailto:/https: (требование RFC 8292 к sub).

Documentation

Overview

Package webpushkit — транспорт доставки Web Push: VAPID-аутентификация (RFC 8292) и aes128gcm-шифрование payload (RFC 8291 поверх RFC 8188), POST на endpoint push-сервиса (RFC 8030). Kit — только типы и отправка; хранение подписок и планирование — зона потребителя (notification-домен).

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrConfig — VAPID-конфиг невалиден (нет/битый приватный ключ, subject не
	// mailto:/https:, public не соответствует private).
	ErrConfig = errors.New("webpushkit: invalid config")

	// ErrSubscription — подписка синтаксически невалидна (битый p256dh/auth,
	// неверная длина). В отличие от ErrSubscriptionGone это ошибка входных данных,
	// а не сигнал «удали подписку».
	ErrSubscription = errors.New("webpushkit: invalid subscription")

	// ErrSubscriptionGone — push-сервис вернул 404/410: подписка больше не
	// действительна, потребитель должен удалить её из своего хранилища.
	ErrSubscriptionGone = errors.New("webpushkit: subscription gone")

	// ErrPayloadTooLarge — plaintext превышает потолок, при котором зашифрованное
	// тело укладывается в гарантированные push-сервисами 4096 байт (либо сам
	// push-сервис ответил 413).
	ErrPayloadTooLarge = errors.New("webpushkit: payload too large")

	// ErrPush — прочий не-2xx ответ push-сервиса; конкретный статус несёт *PushError.
	ErrPush = errors.New("webpushkit: push service error")
)
View Source
var Module = fx.Module(
	"webpushkit",
	fx.Provide(newSender),
)

Module provide'ит *Sender. Config не provide'ится (asymmetry note как в cachekit/oidckit) — caller добавляет `fx.Provide(webpushkit.NewConfig)`. *http.Client опционален: без него — http.DefaultClient; передашь свой (httpclient/otelhttp) — Sender возьмёт его.

Functions

func GenerateVAPIDKeys

func GenerateVAPIDKeys() (publicB64, privateB64 string, err error)

GenerateVAPIDKeys генерирует VAPID-пару P-256/ES256: public — uncompressed point (65 байт), private — raw scalar (32 байта), оба base64url без паддинга. Пригодится инфре для первичной выдачи WEBPUSH_VAPID_*; public также кладётся в браузерный PushManager.subscribe как applicationServerKey.

Types

type Config

type Config struct {
	VAPIDPublicKey  string        `envconfig:"WEBPUSH_VAPID_PUBLIC_KEY"`
	VAPIDPrivateKey string        `envconfig:"WEBPUSH_VAPID_PRIVATE_KEY"`
	Subject         string        `envconfig:"WEBPUSH_SUBJECT"`
	DefaultTTL      time.Duration `envconfig:"WEBPUSH_DEFAULT_TTL"       default:"24h"`
}

Config — параметры VAPID-идентичности отправителя. Ключи — raw ES256 (P-256) в base64url, как их отдаёт генератор VAPID-пары (см. GenerateVAPIDKeys); тот же public-ключ браузер передаёт в PushManager.subscribe как applicationServerKey.

Имена env финализируются при первом потребителе (нужен, notification-домен) — envconfig-теги здесь часть публичного API kit'а.

func NewConfig

func NewConfig() (Config, error)

type PushError

type PushError struct {
	StatusCode int
	RetryAfter string
	Body       string
}

PushError — не-2xx ответ push-сервиса, не подпадающий под gone/too-large. Оборачивает ErrPush (errors.Is(err, ErrPush)); StatusCode/RetryAfter позволяют потребителю решить про backoff (например, 429 + Retry-After).

func (*PushError) Error

func (e *PushError) Error() string

func (*PushError) Unwrap

func (e *PushError) Unwrap() error

type SendOption

type SendOption func(*sendOptions)

SendOption переопределяет параметры одной доставки.

func WithTTL

func WithTTL(ttl time.Duration) SendOption

WithTTL задаёт TTL сообщения (сколько push-сервис держит его для оффлайн-клиента).

func WithTopic

func WithTopic(topic string) SendOption

WithTopic задаёт Topic: новое сообщение с тем же топиком вытесняет прежнее недоставленное (полезно, чтобы не копить устаревшие уведомления).

func WithUrgency

func WithUrgency(u Urgency) SendOption

WithUrgency задаёт приоритет доставки (заголовок Urgency).

type Sender

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

Sender отправляет Web Push сообщения от лица одной VAPID-идентичности. Потокобезопасен: не хранит состояния между вызовами Send.

func NewSender

func NewSender(cfg Config, client *http.Client) (*Sender, error)

NewSender готовит отправителя из VAPID-конфига. client опционален: nil → http.DefaultClient (потребитель может передать httpclient/otelhttp-обёртку, чтобы получить транспортные RED-метрики). Невалидный конфиг → ErrConfig.

func (*Sender) Send

func (s *Sender) Send(ctx context.Context, sub Subscription, payload []byte, opts ...SendOption) error

Send шифрует payload под ключи подписки и POST'ит его на endpoint. Возвращает: nil при 2xx; ErrSubscriptionGone при 404/410 (потребитель удаляет подписку); ErrPayloadTooLarge; *PushError (Is ErrPush) при прочих не-2xx.

type Subscription

type Subscription struct {
	Endpoint string // URL push-сервиса, куда POST'ится зашифрованное сообщение
	P256dh   string // публичный ключ user agent'а (uncompressed P-256, base64url)
	Auth     string // authentication secret подписки (16 байт, base64url)
}

Subscription — то, что отдаёт браузерный PushManager.subscribe (поля endpoint + keys.p256dh + keys.auth). Kit подписки не хранит — это зона потребителя (notification-домен); сюда они приходят как есть.

type Urgency

type Urgency string

Urgency — приоритет доставки (RFC 8030 §5.3): push-сервис может придержать low-urgency сообщения, экономя батарею устройства.

const (
	UrgencyVeryLow Urgency = "very-low"
	UrgencyLow     Urgency = "low"
	UrgencyNormal  Urgency = "normal"
	UrgencyHigh    Urgency = "high"
)

Jump to

Keyboard shortcuts

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