import { Injectable } from '@nestjs/common'; import webpush from 'web-push'; import type { NotificationPayload, NotificationPort, PushSub } from './notification.port'; type SendFn = typeof webpush.sendNotification; export interface Vapid { publicKey: string; privateKey: string; subject: string; } /** Web Push (VAPID) delivery. A dead subscription (404/410) → 'gone' so the caller prunes it. */ @Injectable() export class WebPushDelivery implements NotificationPort { private readonly vapid?: Vapid; constructor( vapid?: Vapid, private readonly send: SendFn = webpush.sendNotification, ) { // Store VAPID; pass it per-send (not global setVapidDetails) so construction never // validates keys — keeps the adapter unit-testable with a mocked transport. this.vapid = vapid?.publicKey ? vapid : undefined; } async deliver(sub: PushSub, payload: NotificationPayload): Promise<'sent' | 'gone' | 'failed'> { if (!this.vapid) return 'failed'; // push disabled (no VAPID keys) try { await this.send( { endpoint: sub.endpoint, keys: { p256dh: sub.p256dh, auth: sub.auth } }, JSON.stringify(payload), { vapidDetails: this.vapid }, ); return 'sent'; } catch (e) { const code = (e as { statusCode?: number }).statusCode; return code === 404 || code === 410 ? 'gone' : 'failed'; } } }