67 lines
2.2 KiB
TypeScript
67 lines
2.2 KiB
TypeScript
import webpush from "web-push";
|
|
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
import { resolve } from "path";
|
|
import { ensureDataDir } from "../paths";
|
|
import { logger } from "../logger";
|
|
|
|
let vapidKeys: { publicKey: string; privateKey: string } | null = null;
|
|
|
|
function getVapidPath(): string {
|
|
return resolve(ensureDataDir(), "vapid-keys.json");
|
|
}
|
|
|
|
export function loadOrGenerateVapidKeys(): { publicKey: string; privateKey: string } {
|
|
if (vapidKeys) return vapidKeys;
|
|
|
|
const vapidPath = getVapidPath();
|
|
|
|
if (existsSync(vapidPath)) {
|
|
try {
|
|
const raw = readFileSync(vapidPath, "utf-8");
|
|
const parsed = JSON.parse(raw);
|
|
if (parsed.publicKey && parsed.privateKey) {
|
|
vapidKeys = { publicKey: parsed.publicKey, privateKey: parsed.privateKey };
|
|
logger.info("[web-push] loaded VAPID keys from " + vapidPath);
|
|
return vapidKeys;
|
|
}
|
|
} catch (err) {
|
|
logger.warn("[web-push] failed to read vapid-keys.json, regenerating", err);
|
|
}
|
|
}
|
|
|
|
const generated = webpush.generateVAPIDKeys();
|
|
vapidKeys = { publicKey: generated.publicKey, privateKey: generated.privateKey };
|
|
|
|
try {
|
|
writeFileSync(vapidPath, JSON.stringify(vapidKeys, null, 2));
|
|
logger.info("[web-push] generated and saved VAPID keys to " + vapidPath);
|
|
} catch (err) {
|
|
logger.warn("[web-push] failed to persist VAPID keys", err);
|
|
}
|
|
|
|
return vapidKeys;
|
|
}
|
|
|
|
export function getVapidPublicKey(): string {
|
|
return loadOrGenerateVapidKeys().publicKey;
|
|
}
|
|
|
|
export async function sendWebPush(
|
|
subscription: { endpoint: string; keys: { p256dh: string; auth: string } },
|
|
payload: string,
|
|
): Promise<{ success: boolean; gone?: boolean; error?: string }> {
|
|
const keys = loadOrGenerateVapidKeys();
|
|
webpush.setVapidDetails("mailto:overlord@localhost", keys.publicKey, keys.privateKey);
|
|
|
|
try {
|
|
await webpush.sendNotification(subscription, payload, { TTL: 60 * 60 });
|
|
return { success: true };
|
|
} catch (err: any) {
|
|
const statusCode = err?.statusCode;
|
|
if (statusCode === 404 || statusCode === 410) {
|
|
return { success: false, gone: true, error: `subscription gone (${statusCode})` };
|
|
}
|
|
return { success: false, error: err?.message || String(err) };
|
|
}
|
|
}
|