function toBase64Url(array) { return btoa([...array].map(c => String.fromCharCode(c)).join('')).replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "") } class VAPID { #publicKey; #privateKey; constructor(publicKey, privateKey) { this.#publicKey = publicKey; this.#privateKey = privateKey; } get publicKey() { return this.#publicKey; } async #jws(audience) { // https://datatracker.ietf.org/doc/html/rfc7515#section-3.1 // BASE64URL(UTF8(JWS Protected Header)) || '.' || // BASE64URL(JWS Payload) || '.' || // BASE64URL(JWS Signature) // https://datatracker.ietf.org/doc/html/rfc8292#section-2 // ECDSA on the NIST P-256 curve [FIPS186], which is identified as "ES256" [RFC7518]. const rawHeader = { typ: "JWT", alg: "ES256" }; const header = toBase64Url(new TextEncoder().encode(JSON.stringify(rawHeader))); // https://datatracker.ietf.org/doc/html/rfc8292#section-2 const rawPayload = { // An "aud" (Audience) claim in the token MUST include the Unicode // serialization of the origin (Section 6.1 of [RFC6454]) of the push // resource URL. aud: audience, // An "exp" (Expiry) claim MUST be included with the time after which // the token expires. exp: parseInt(new Date().getTime() / 1000) + 24 * 60 * 60, // seconds, 24hr // The "sub" claim SHOULD include a contact URI for the application server as either a // "mailto:" (email) [RFC6068] or an "https:" [RFC2818] URI. sub: "mailto:webpush@example.com", }; const payload = toBase64Url(new TextEncoder().encode(JSON.stringify(rawPayload))); const input = `${header}.${payload}`; // https://datatracker.ietf.org/doc/html/rfc7518#section-3.1 // ES256 | ECDSA using P-256 and SHA-256 const rawSignature = await crypto.subtle.sign({ name: "ECDSA", namedCurve: "P-256", hash: { name: "SHA-256" }, }, this.#privateKey, new TextEncoder().encode(input)); const signature = toBase64Url(new Uint8Array(rawSignature)); return `${input}.${signature}`; } async generateAuthHeader(audience) { // https://datatracker.ietf.org/doc/html/rfc8292#section-3.1 // The "t" parameter of the "vapid" authentication scheme carries a JWT // as described in Section 2. const t = await this.#jws(audience); // https://datatracker.ietf.org/doc/html/rfc8292#section-3.2 // The "k" parameter includes an ECDSA public key [FIPS186] in // uncompressed form [X9.62] that is encoded using base64url encoding // [RFC7515]. const k = toBase64Url(this.#publicKey) return `vapid t=${t},k=${k}`; } }; export async function createVapid() { // https://datatracker.ietf.org/doc/html/rfc8292#section-2 // The signature MUST use ECDSA on the NIST P-256 curve [FIPS186] const keys = await crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-256" }, true, ["sign"]); const publicKey = new Uint8Array(await crypto.subtle.exportKey("raw", keys.publicKey)); const privateKey = keys.privateKey; return new VAPID(publicKey, privateKey); };