defmodule WebPush do @moduledoc """ Web Push sender: encrypts a JSON payload with `aes128gcm` (`WebPush.Encryption`), signs a VAPID JWT (`WebPush.Vapid`), and POSTs the binary body to the subscription's push service endpoint via Finch. This is a raw library: it starts no processes. The host application must supervise a Finch pool and tell WebPush its name: # in your application.ex children = [ {Finch, name: MyApp.WebPushFinch} ] # in config config :web_push, finch: MyApp.WebPushFinch The pool name can also be passed per call via the `:finch` option. ## Return values * `:ok`: push service accepted (HTTP 200/201/202/204) * `{:error, :gone}`: subscription no longer valid (404/410); the caller should delete the stored subscription * `{:error, term}`: transport failure or unexpected status """ require Logger alias WebPush.{Encryption, Subscription, Vapid} @default_ttl 86_400 @type send_result :: :ok | {:error, :gone} | {:error, term()} @doc """ Sends `payload` (JSON-encoded) to `subscription`. ## Options * `:ttl`: seconds the push service may hold the message (default 86400) * `:urgency`: `"very-low" | "low" | "normal" | "high"` (default `"normal"`) * `:topic`: URL-safe string ≤ 32 chars; lets the push service collapse pending messages with the same topic into the latest one * `:finch`: Finch pool name, overrides `config :web_push, :finch` """ @spec send(Subscription.t(), map(), keyword()) :: send_result() def send(%Subscription{} = sub, payload, opts \\ []) when is_map(payload) do body = JSON.encode!(payload) encrypted = Encryption.encrypt(body, sub.p256dh, sub.auth) headers = build_headers(sub.endpoint, opts) req = Finch.build(:post, sub.endpoint, headers, encrypted) case Finch.request(req, finch_name(opts)) do {:ok, %{status: status}} when status in [200, 201, 202, 204] -> :ok {:ok, %{status: status}} when status in [404, 410] -> Logger.info("[web_push] subscription gone (#{status})") {:error, :gone} {:ok, %{status: status, body: body}} -> Logger.warning("[web_push] push failed #{status}: #{inspect(body)}") {:error, {:http, status}} {:error, reason} -> Logger.warning("[web_push] transport failure: #{inspect(reason)}") {:error, reason} end end defp build_headers(endpoint, opts) do ttl = Keyword.get(opts, :ttl, @default_ttl) urgency = Keyword.get(opts, :urgency, "normal") topic = Keyword.get(opts, :topic) base = [ {"authorization", Vapid.authorization_header(endpoint)}, {"content-type", "application/octet-stream"}, {"content-encoding", "aes128gcm"}, {"ttl", Integer.to_string(ttl)}, {"urgency", urgency} ] if topic, do: [{"topic", topic} | base], else: base end defp finch_name(opts) do Keyword.get(opts, :finch) || Application.get_env(:web_push, :finch) || raise """ No Finch pool configured for WebPush. Supervise one in your application and set: config :web_push, finch: MyApp.WebPushFinch or pass the `:finch` option to WebPush.send/3. """ end end