import { fetchWebPushConfig, registerWebPushSubscription, unregisterWebPushSubscription, type WebPushSubscriptionPayload, } from './notificationApi'; import { getOrCreateClientId } from './clientIdentity'; import { getSavedNotificationDeviceId } from './notificationIdentity'; const WEB_PUSH_METADATA_STORAGE_KEY = 'work-server.web-push.registration-meta.v1'; type WebPushRegistrationMetadata = { deviceId: string; clientId: string; userAgent: string; appOrigin: string; appDomain: string; enabled: boolean; updatedAt: string; }; function getCurrentAppOrigin() { if (typeof window === 'undefined') { return ''; } return window.location.origin; } function getCurrentAppDomain() { if (typeof window === 'undefined') { return ''; } return window.location.hostname; } function buildWebPushRegistrationMetadata(enabled: boolean): WebPushRegistrationMetadata { return { deviceId: getSavedNotificationDeviceId().trim(), clientId: getOrCreateClientId().trim(), userAgent: typeof navigator !== 'undefined' ? navigator.userAgent : '', appOrigin: getCurrentAppOrigin().trim(), appDomain: getCurrentAppDomain().trim(), enabled, updatedAt: new Date().toISOString(), }; } function storeWebPushRegistrationMetadata(metadata: WebPushRegistrationMetadata | null) { if (typeof window === 'undefined') { return; } try { if (metadata) { window.localStorage.setItem(WEB_PUSH_METADATA_STORAGE_KEY, JSON.stringify(metadata)); return; } window.localStorage.removeItem(WEB_PUSH_METADATA_STORAGE_KEY); } catch { // Ignore storage failures in restricted runtimes. } } async function syncWebPushRegistrationMetadataWithServiceWorker( registration: ServiceWorkerRegistration | null, metadata: WebPushRegistrationMetadata | null, ) { if (!registration) { return; } const target = registration.active ?? registration.waiting ?? registration.installing ?? (typeof navigator !== 'undefined' && 'serviceWorker' in navigator ? navigator.serviceWorker.controller : null); if (!target) { return; } target.postMessage({ type: metadata ? 'WEB_PUSH_SYNC_METADATA' : 'WEB_PUSH_CLEAR_METADATA', payload: metadata, }); } export function urlBase64ToUint8Array(base64String: string) { const padding = '='.repeat((4 - (base64String.length % 4)) % 4); const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/'); const rawData = window.atob(base64); const outputArray = new Uint8Array(rawData.length); for (let index = 0; index < rawData.length; index += 1) { outputArray[index] = rawData.charCodeAt(index); } return outputArray; } function isSamePushApplicationServerKey(leftKey: ArrayBuffer | null, rightKey: Uint8Array) { if (!leftKey) { return false; } const leftBytes = new Uint8Array(leftKey); if (leftBytes.byteLength !== rightKey.byteLength) { return false; } for (let index = 0; index < leftBytes.byteLength; index += 1) { if (leftBytes[index] !== rightKey[index]) { return false; } } return true; } export function serializePushSubscription(subscription: PushSubscription): WebPushSubscriptionPayload { const json = subscription.toJSON(); return { endpoint: subscription.endpoint, expirationTime: subscription.expirationTime, keys: { p256dh: json.keys?.p256dh ?? '', auth: json.keys?.auth ?? '', }, }; } export async function ensureWebPushSubscriptionRegistered( registration: ServiceWorkerRegistration, options?: { deviceId?: string }, ) { const config = await fetchWebPushConfig(); if (!config.enabled || !config.publicKey) { throw new Error('서버 Web Push 설정이 비어 있습니다.'); } const expectedApplicationServerKey = urlBase64ToUint8Array(config.publicKey); let subscription = await registration.pushManager.getSubscription(); if ( subscription && !isSamePushApplicationServerKey(subscription.options.applicationServerKey, expectedApplicationServerKey) ) { await unregisterWebPushSubscription(subscription.endpoint).catch(() => undefined); await subscription.unsubscribe().catch(() => undefined); subscription = null; } if (!subscription) { subscription = await registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: expectedApplicationServerKey, }); } await registerWebPushSubscription(serializePushSubscription(subscription), options?.deviceId ?? getSavedNotificationDeviceId()); const metadata = buildWebPushRegistrationMetadata(true); storeWebPushRegistrationMetadata(metadata); await syncWebPushRegistrationMetadataWithServiceWorker(registration, metadata); return subscription; } export async function syncExistingWebPushSubscriptionRegistration( registration: ServiceWorkerRegistration, options?: { deviceId?: string }, ) { const config = await fetchWebPushConfig(); if (!config.enabled || !config.publicKey || typeof Notification === 'undefined' || Notification.permission !== 'granted') { return null; } const subscription = await registration.pushManager.getSubscription(); if (!subscription) { return null; } await registerWebPushSubscription(serializePushSubscription(subscription), options?.deviceId ?? getSavedNotificationDeviceId()); const metadata = buildWebPushRegistrationMetadata(true); storeWebPushRegistrationMetadata(metadata); await syncWebPushRegistrationMetadataWithServiceWorker(registration, metadata); return subscription; } export async function clearWebPushSubscriptionRegistration(registration: ServiceWorkerRegistration) { const subscription = await registration.pushManager.getSubscription(); if (subscription) { await unregisterWebPushSubscription(subscription.endpoint); await subscription.unsubscribe(); } storeWebPushRegistrationMetadata(null); await syncWebPushRegistrationMetadataWithServiceWorker(registration, null); }