Skip to content
LogoLogo

@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>>;
OptionTypeDefaultDescription
subscriptionPick<Subscription, "keys">The subscription. The payload is encrypted with its keys.
payloadstring | Uint8Array<ArrayBufferLike>The message.
saltUint8Array<ArrayBufferLike>16 random bytesThe 16-byte salt. Use a fresh random salt for every message.
keyPairCryptoKeyPaira new key pair from crypto.subtle.generateKeyThe application server's ECDH P-256 key pair. Use a fresh pair for every message.
signalAbortSignalCancels the encryption. It rejects with signal.reason.

Throws

  • InvalidInputError When the subscription has no keys, p256dh isn't a P-256 public key, or auth isn't 16 bytes.
  • MaxBytesError When the payload is longer than maxPayloadBytes.
  • RangeError When salt isn'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>;
OptionTypeDefaultDescription
subscriptionSubscriptionThe browser's push subscription, from PushSubscription.toJSON().
payloadstring | Uint8Array<ArrayBufferLike>The message to encrypt. Leave it out to send a push with no data.
vapidVapidThe VAPID subject and key pair that sign the request.
ttlnumber2419200 (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.
urgencyUrgencyLets the push service hold back low-priority messages to save battery. toRequest adds no Urgency header by default.
topicstringReplaces an undelivered message with the same topic, so an offline browser gets only the latest. Up to 32 base64url characters.
nowanynew Date()The time the VAPID token's expiry is counted from, as a Date or a Temporal.Instant.
signalAbortSignalCancels the encryption and, through the Request, the fetch.
fetch((request: Request) => Promise<Response>)globalThis.fetchSends the request. Pass one to add retries, logging or a proxy.

Throws

  • ResponseError When the push service answers with a status other than 2xx, 404 or 410.
  • InvalidInputError When the endpoint isn't an https: URL, or a payload is given and the subscription's keys are missing or malformed.
  • MaxBytesError When the payload is longer than maxPayloadBytes.
  • RangeError When an option is malformed.
  • TypeError When fetch rejects.
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>;
OptionTypeDefaultDescription
subscriptionSubscriptionThe browser's push subscription, from PushSubscription.toJSON().
payloadstring | Uint8Array<ArrayBufferLike>The message to encrypt. Leave it out to send a push with no data.
vapidVapidThe VAPID subject and key pair that sign the request.
ttlnumber2419200 (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.
urgencyUrgencyLets the push service hold back low-priority messages to save battery. toRequest adds no Urgency header by default.
topicstringReplaces an undelivered message with the same topic, so an offline browser gets only the latest. Up to 32 base64url characters.
nowanynew Date()The time the VAPID token's expiry is counted from, as a Date or a Temporal.Instant.
signalAbortSignalCancels the encryption and, through the Request, the fetch.

Throws

  • InvalidInputError When the endpoint isn't an https: URL, or a payload is given and the subscription's keys are missing or malformed.
  • MaxBytesError When the payload is longer than maxPayloadBytes.
  • RangeError When vapid, urgency or topic is malformed, ttl isn't a non-negative integer, or now is 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>;
OptionTypeDefaultDescription
signalAbortSignalCancels 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;
};