63 lines
2.5 KiB
TypeScript
63 lines
2.5 KiB
TypeScript
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<PushSendResult> {
|
|
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." };
|
|
}
|
|
}
|
|
}
|