@portable/web-push
Functions
WebPush.encrypt
Encrypts a payload for a subscription with the aes128gcm content encoding (RFC 8188, RFC 8291),
and returns the request body: an 86-byte header holding the salt, record size and server public
key, then one AES-GCM record. Use it to send with your own transport. toRequest and send
call it for you.
async function encrypt(
options: encrypt.Options,
): Promise<Uint8Array<ArrayBuffer>>;| Option | Type | Default | Description |
|---|---|---|---|
subscription | Pick<Subscription, "keys"> | The subscription. The payload is encrypted with its keys. | |
payload | string | Uint8Array<ArrayBufferLike> | The message. | |
salt | Uint8Array<ArrayBufferLike> | 16 random bytes | The 16-byte salt. Use a fresh random salt for every message. |
keyPair | CryptoKeyPair | a new key pair from crypto.subtle.generateKey | The application server's ECDH P-256 key pair. Use a fresh pair for every message. |
signal | AbortSignal | Cancels the encryption. It rejects with signal.reason. |
Throws
InvalidInputErrorWhen the subscription has no keys,p256dhisn't a P-256 public key, orauthisn't 16 bytes.MaxBytesErrorWhen the payload is longer thanmaxPayloadBytes.RangeErrorWhensaltisn't 16 bytes.
import * as WebPush from '@portable/web-push';
const body = await WebPush.encrypt({
subscription,
payload: JSON.stringify({ title: 'Hi' }),
});
// send with the headers `Content-Encoding: aes128gcm`, `TTL` and a VAPID `Authorization`WebPush.send
Encrypts a payload, signs the request with VAPID, and sends it to the subscription's push
service. Leave out payload to send a push with no data.
async function send(options: send.Options): Promise<send.ReturnType>;| Option | Type | Default | Description |
|---|---|---|---|
subscription | Subscription | The browser's push subscription, from PushSubscription.toJSON(). | |
payload | string | Uint8Array<ArrayBufferLike> | The message to encrypt. Leave it out to send a push with no data. | |
vapid | Vapid | The VAPID subject and key pair that sign the request. | |
ttl | number | 2419200 (four weeks) | How long the push service keeps the message for an offline browser, in seconds. 0 means deliver now or drop it. Push services may cap it. |
urgency | Urgency | Lets the push service hold back low-priority messages to save battery. toRequest adds no Urgency header by default. | |
topic | string | Replaces an undelivered message with the same topic, so an offline browser gets only the latest. Up to 32 base64url characters. | |
now | any | new Date() | The time the VAPID token's expiry is counted from, as a Date or a Temporal.Instant. |
signal | AbortSignal | Cancels the encryption and, through the Request, the fetch. | |
fetch | ((request: Request) => Promise<Response>) | globalThis.fetch | Sends the request. Pass one to add retries, logging or a proxy. |
Throws
ResponseErrorWhen the push service answers with a status other than 2xx, 404 or 410.InvalidInputErrorWhen the endpoint isn't anhttps:URL, or a payload is given and the subscription's keys are missing or malformed.MaxBytesErrorWhen the payload is longer thanmaxPayloadBytes.RangeErrorWhen an option is malformed.TypeErrorWhenfetchrejects.
import * as WebPush from '@portable/web-push';
const result = await WebPush.send({
subscription,
payload: JSON.stringify({ title: 'New message' }),
vapid: { subject: 'mailto:ops@example.com', publicKey, privateKey },
ttl: 3600,
urgency: 'high',
});
if (result.kind === 'gone') {
await deleteSubscription(subscription.endpoint);
}WebPush.toRequest
Builds the push request for a subscription: a POST to its endpoint with the encrypted payload
and the TTL, Content-Encoding, Authorization, Urgency and Topic headers. Use it to send
with your own fetch or queue. send calls it for you.
async function toRequest(options: toRequest.Options): Promise<Request>;| Option | Type | Default | Description |
|---|---|---|---|
subscription | Subscription | The browser's push subscription, from PushSubscription.toJSON(). | |
payload | string | Uint8Array<ArrayBufferLike> | The message to encrypt. Leave it out to send a push with no data. | |
vapid | Vapid | The VAPID subject and key pair that sign the request. | |
ttl | number | 2419200 (four weeks) | How long the push service keeps the message for an offline browser, in seconds. 0 means deliver now or drop it. Push services may cap it. |
urgency | Urgency | Lets the push service hold back low-priority messages to save battery. toRequest adds no Urgency header by default. | |
topic | string | Replaces an undelivered message with the same topic, so an offline browser gets only the latest. Up to 32 base64url characters. | |
now | any | new Date() | The time the VAPID token's expiry is counted from, as a Date or a Temporal.Instant. |
signal | AbortSignal | Cancels the encryption and, through the Request, the fetch. |
Throws
InvalidInputErrorWhen the endpoint isn't anhttps:URL, or a payload is given and the subscription's keys are missing or malformed.MaxBytesErrorWhen the payload is longer thanmaxPayloadBytes.RangeErrorWhenvapid,urgencyortopicis malformed,ttlisn't a non-negative integer, ornowis an invalid time.
import * as WebPush from '@portable/web-push';
const request = await WebPush.toRequest({
subscription,
payload: 'hello',
vapid,
});
const response = await fetch(request);WebPush.generateVapidKeys
Generates a VAPID key pair. Generate it once, store the private key as a secret, and give the
public key to browsers as applicationServerKey when they subscribe.
async function generateVapidKeys(
options: generateVapidKeys.Options = {},
): Promise<VapidKeys>;| Option | Type | Default | Description |
|---|---|---|---|
signal | AbortSignal | Cancels the key generation. It rejects with signal.reason. |
import * as WebPush from '@portable/web-push';
const { publicKey, privateKey } = await WebPush.generateVapidKeys();Constants
WebPush.maxPayloadBytes
The largest payload in bytes. Push services must accept a 4096-byte encrypted body (RFC 8291 §4).
const maxPayloadBytes: 3993;import * as WebPush from '@portable/web-push';
const payload = new TextEncoder().encode(JSON.stringify(notification));
if (payload.byteLength > WebPush.maxPayloadBytes) {
throw new Error('notification too large');
}Errors
WebPush.InvalidInputError
Thrown when a subscription can't be used: its endpoint isn't an https: URL, p256dh isn't a P-256
public key, or auth isn't 16 bytes. Subscriptions come from browsers, so delete one that
fails this way.
class InvalidInputError extends Errors.InvalidInputError {
readonly kind: string;
}import * as WebPush from '@portable/web-push';
try {
await WebPush.send({ subscription, payload: 'hello', vapid });
} catch (error) {
if (error instanceof WebPush.InvalidInputError) {
await deleteSubscription(subscription);
} else {
throw error;
}
}WebPush.MaxBytesError
Thrown when a payload is longer than maxPayloadBytes, before anything is encrypted or sent.
The subscription is still valid, so send a shorter payload, such as an id the service worker
uses to fetch the rest.
class MaxBytesError extends Errors.MaxBytesError {
readonly kind: string;
}import * as WebPush from '@portable/web-push';
try {
await WebPush.send({ subscription, payload, vapid });
} catch (error) {
if (error instanceof WebPush.MaxBytesError) {
await WebPush.send({
subscription,
payload: JSON.stringify({ id }),
vapid,
});
} else {
throw error;
}
}WebPush.ResponseError
Thrown by send when the push service answers with a status other than 2xx, 404 or 410, such
as 400 for a bad VAPID token, 413 for a payload that is too large, or 429 when rate limited.
class ResponseError extends Errors.ResponseError {
constructor(
options: Errors.ResponseError.Options & {
body: string;
},
);
readonly kind: string;
readonly body: string;
}import * as WebPush from '@portable/web-push';
try {
await WebPush.send({ subscription, payload: 'hello', vapid });
} catch (error) {
if (error instanceof WebPush.ResponseError && error.status === 429) {
retryAfter(error.response.headers.get('retry-after'));
} else {
throw error;
}
}Types
WebPush.Keys
The browser's public key and authentication secret for payload encryption.
type Keys = {
p256dh: string | Uint8Array;
auth: string | Uint8Array;
};WebPush.Subscription
A push subscription as PushSubscription.toJSON() returns it in the browser. Keys are base64url
or base64 strings, or their bytes.
type Subscription = {
endpoint: string;
expirationTime?: number | null | undefined;
keys?: Keys | undefined;
};WebPush.Urgency
How soon the push service should deliver the message (RFC 8030 §5.3).
type Urgency = | | | ;WebPush.Vapid
How the application server identifies itself to push services (RFC 8292).
type Vapid = VapidKeys & {
subject: string;
};WebPush.VapidKeys
A VAPID key pair as base64url strings, in the format the web-push package uses.
type VapidKeys = {
publicKey: string;
privateKey: string;
};