import webpush from "web-push"; import type { PushProvider, PushSendResult, SendPushParams } from "./push-provider"; /** * Standards-based Web Push (RFC 8030 + VAPID) via the `web-push` package — * works against any browser's push service (Chrome/FCM, Firefox, Safari) * without a vendor-specific SDK. Nothing here is Brevo-specific; push and * email are unrelated delivery channels with unrelated providers. */ export interface WebPushProviderConfig { publicKey: string; privateKey: string; subject: string; } export function loadWebPushProviderConfigFromEnv(): WebPushProviderConfig { const publicKey = process.env.VAPID_PUBLIC_KEY; const privateKey = process.env.VAPID_PRIVATE_KEY; const subject = process.env.VAPID_SUBJECT; if (!publicKey || !privateKey || !subject) { throw new Error("Web Push is not configured — VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY, and VAPID_SUBJECT must all be set. See .env.example."); } return { publicKey, privateKey, subject }; } interface WebPushError { statusCode?: number; message?: string; } export class WebPushProvider implements PushProvider { readonly name = "web-push"; constructor(config: WebPushProviderConfig) { webpush.setVapidDetails(config.subject, config.publicKey, config.privateKey); } async sendPush(params: SendPushParams): Promise { try { await webpush.sendNotification( { endpoint: params.subscription.endpoint, keys: { p256dh: params.subscription.p256dhKey, auth: params.subscription.authKey }, }, JSON.stringify({ title: params.title, body: params.body, url: params.deepLinkPath }), ); return { status: "sent" }; } catch (error) { const webPushError = error as WebPushError; // 404/410: the push service itself says this subscription no longer // exists (browser data cleared, permission revoked, uninstalled) — // never retryable, and the caller should revoke the row. if (webPushError.statusCode === 404 || webPushError.statusCode === 410) { return { status: "failed", retryable: false, subscriptionInvalid: true, reason: "Subscription is no longer valid." }; } if (typeof webPushError.statusCode === "number" && webPushError.statusCode >= 500) { return { status: "failed", retryable: true, reason: webPushError.message ?? "Push service error." }; } return { status: "failed", retryable: false, reason: webPushError.message ?? "Unknown push error." }; } } }