webpush
Send Web Push notifications to browsers with VAPID keys, and build and verify the signed tokens that push services require.
import "std:webpush"Web Push lets a server show a notification in a user's browser even when your site is closed. The browser gives your page a subscription (an address plus keys), you send it to your server, and std:webpush encrypts a message for it and delivers it through the browser vendor's push service.
Sending needs a real subscription from a real browser, so send is shown but not run. Everything around it, the keys and the signed tokens, runs fine here.
Make your server's keys#
A push service wants to know which server is sending. You prove it with a VAPID key pair. generateKeys() makes one: keep privateKey secret, and give publicKey to the page so it can subscribe:
import "std:webpush"
auto keys = webpush.generateKeys()
log keys.privateKey.startsWith("-----BEGIN")
log keys.publicKey.len
log webpush.derivePublicKey(keys.privateKey).unwrap() == keys.publicKeyThe public key is derived from the private one, so the private key is all you have to keep.
Keep keys between runs#
Making new keys on every start would break existing subscriptions. ensureKeys(path) loads the key stored at path, or creates and saves one the first time:
import "std:webpush"
import "std:fs"
fs.remove("vapid.pem", {force: true}).unwrap()
auto first = webpush.ensureKeys("vapid.pem").unwrap()
auto second = webpush.ensureKeys("vapid.pem").unwrap()
log fs.exists("vapid.pem")
log first.publicKey == second.publicKey
fs.remove("vapid.pem", {force: true}).unwrap()Run it twice: the second run loads what the first saved. Treat that file as a secret.
Sign and verify a token#
Each push request carries a short-lived token (an ES256 JSON Web Token) for the push service's address. signJWT(audience, privateKey, options) makes one. A subject contact address is required, so the service can reach you if something goes wrong:
import "std:webpush"
auto keys = webpush.generateKeys()
string token = webpush.signJWT(
"https://push.example.com",
keys.privateKey,
{subject: "mailto:[email protected]"}
).unwrap()
log token.split(".").len
log webpush.verifyJWT(token, keys.publicKey)
log webpush.verifyJWT(token, webpush.generateKeys().publicKey)send does this for you. You only call signJWT yourself when you talk to a push service by other means.
Check a subscription#
The browser's subscription has an endpoint URL and two keys, p256dh and auth. verifySubscription says whether they are the right shape, so you can refuse junk at the door before you store it:
import "std:webpush"
log webpush.verifySubscription("http://push.example.com/abc", "key", "secret")
log webpush.verifySubscription("not a url", "", "")Try an https endpoint with real keys from a browser to see true. A plain http endpoint is always refused.
Send a notification#
With a subscription from a browser, send(subscription, payload, options) encrypts the payload (up to 3993 bytes) and delivers it. An object payload goes as JSON, which your service worker reads in its push event:
import "std:webpush"
import "std:env"
auto subscription = {
endpoint: "https://fcm.googleapis.com/fcm/send/abc123",
keys: {p256dh: "BNc...", auth: "tBH..."}
}
auto outcome = webpush.send(subscription, {title: "New message", body: "Ada says hi"}, {
privateKey: env.get("VAPID_PRIVATE_KEY"),
subject: "mailto:[email protected]",
ttl: 3600000
})
switch outcome (
case ok(_)
log "delivered to the push service"
case err(e)
log "failed:" e.kind
)A NotFoundError usually means the user unsubscribed, so delete that subscription from your store. ttl is how long the push service should hold the message when the device is offline, in milliseconds.
Why does send need your private VAPID key?
Fresh keys, same identity
Make a key pair, then print whether derivePublicKey of the private key matches the public one, and whether a token signed with it verifies.
true true
API reference#
12 members, generated from the std:webpush source of OSL 9.3.0.
Functions
webpush.derivePublicKey(string privateKey) result<string, error>derivePublicKey returns the base64url VAPID public key that belongs to a PEM-encoded P-256 private key. Fails with ParseError when privateKey isn't one.
webpush.ensureKeys(string path) result<{privateKey: string, publicKey: string}, error>ensureKeys returns the VAPID key pair whose private key is stored at path, creating the key and saving it there (readable only by the owner) when the file doesn't exist yet. Use it to keep the same keys across restarts, since browsers' subscriptions are tied to the public key. Fails with ParseError when the file isn't a PEM-encoded P-256 private key (it is never overwritten), and PermissionError or IOError when it can't be read or written.
webpush.ensureVAPIDKeys(any configPath, any privateKeyPath) objectDeprecated. Deprecated: use webpush.ensureKeys(path) instead, which keeps everything in the private key file and never overwrites a file it can't read.
ensureVAPIDKeys(configPath, privateKeyPath) loads the keys saved by an earlier call, or generates new ones, writes the private key and a {public_key, claims_email} config (claims_email is the package author's address, as before), and returns the config, or {error} on failure. Malformed files are replaced.
webpush.generateKeys() {privateKey: string, publicKey: string}generateKeys creates a new VAPID key pair: privateKey is a PEM-encoded P-256 key to keep secret on the server, and publicKey is the base64url key to give browsers as applicationServerKey when they subscribe.
webpush.generateVAPIDKeys() objectDeprecated. Deprecated: use webpush.generateKeys() instead, which returns {privateKey, publicKey}.
generateVAPIDKeys returns {private_key, public_key}, or {error} when no key could be generated.
webpush.send(object subscription, any payload, object? options) result<unit, error>send encrypts payload (at most 3993 bytes) and delivers it to a browser subscription. Options: privateKey and subject (required), ttl (milliseconds, default 86400000), urgency, topic, timeout and allowPrivate. The endpoint comes from the browser, so send only goes where a push service can be: an https URL whose host resolves to a public address, without following redirects. allowPrivate (default false) also allows loopback, private and link-local addresses, for a push service on your own network such as a self-hosted UnifiedPush server; it lets any client make the server post to those addresses. Fails with NotFoundError, ValueError (also for an endpoint that isn't https or resolves to an address that isn't allowed), PermissionError, NetworkError, TimeoutError or RuntimeError.
webpush.sendWebPush(any endpoint, any p256dh, any auth, any ...rest) objectDeprecated. Deprecated: use webpush.send(subscription, payload, {privateKey, subject, ttl}) instead, which takes ttl in milliseconds and fails with a typed error.
sendWebPush(endpoint, p256dh, auth, data, vapidPrivateKey, vapidClaimsEmail, ttl) sends one message with ttl in seconds and returns {success, status, error}.
webpush.signJWT(string audience, string privateKey, object? options) result<string, error>signJWT returns a VAPID token (an ES256 JSON Web Token) for audience, the origin of a push service such as "https://fcm.googleapis.com". send makes these itself; signJWT is for code that talks to a push service directly. Options: subject (required, a "mailto:" or "https:" contact the push service can reach you at) and expires (milliseconds the token stays valid, default 43200000, at most 86400000). Fails with ParseError when privateKey isn't a PEM-encoded P-256 private key. Throws ValueError for a missing subject or an expires out of range.
webpush.signVapidJWT(any audience, any expiresIn, any privateKeyPEM, any ...claimsEmail) stringDeprecated. Deprecated: use webpush.signJWT(audience, privateKey, {subject, expires}) instead, which takes expires in milliseconds.
signVapidJWT(audience, expiresIn, privateKeyPEM, claimsEmail) signs a VAPID token valid for expiresIn seconds (default and maximum 86400), or returns "" when the key is invalid.
webpush.verifyJWT(string token, string publicKey) booleanverifyJWT reports whether token is a VAPID token signed by the private key that belongs to publicKey (base64url) and has not expired.
webpush.verifySubscription(string endpoint, string p256dh, string auth) booleanverifySubscription reports whether endpoint is an https URL, p256dh a base64url P-256 public key and auth a base64url 16-byte secret: the parts of a browser's PushSubscription that send needs.
webpush.verifyVapidJWT(any token, any publicKey) booleanDeprecated. Deprecated: use webpush.verifyJWT(token, publicKey) instead, which also rejects expired tokens.
verifyVapidJWT reports whether token's signature matches publicKey, without checking its expiry.