Spaces:
Running
Running
| import fs from 'node:fs'; | |
| import path from 'node:path'; | |
| import webpush from 'web-push'; | |
| import { STATE_DIR } from './config.js'; | |
| // Web Push plumbing: one VAPID keypair per install (persisted on the bucket so | |
| // subscriptions survive rebuilds), plus the list of subscribed devices. | |
| // Notifications are AGENT-initiated only — sent when the operator explicitly | |
| // asked an agent to notify them (documented in the generated environment skill). | |
| const VAPID_FILE = path.join(STATE_DIR, 'vapid.json'); | |
| const SUBS_FILE = path.join(STATE_DIR, 'push-subscriptions.json'); | |
| let keys = null; | |
| let subs = []; // [{ subscription, ua, createdAt }] | |
| function persistSubs() { | |
| const tmp = `${SUBS_FILE}.tmp`; | |
| fs.writeFileSync(tmp, JSON.stringify(subs, null, 2)); | |
| fs.renameSync(tmp, SUBS_FILE); | |
| } | |
| export function initPush() { | |
| try { keys = JSON.parse(fs.readFileSync(VAPID_FILE, 'utf8')); } catch {} | |
| if (!keys || !keys.publicKey || !keys.privateKey) { | |
| keys = webpush.generateVAPIDKeys(); | |
| try { fs.writeFileSync(VAPID_FILE, JSON.stringify(keys, null, 2)); } catch {} | |
| } | |
| // VAPID subject: a contact URL for push services. The Space page is apt. | |
| const subject = process.env.SPACE_ID ? `https://huggingface.co/spaces/${process.env.SPACE_ID}` : 'mailto:agent-manager@localhost.invalid'; | |
| webpush.setVapidDetails(subject, keys.publicKey, keys.privateKey); | |
| try { | |
| subs = JSON.parse(fs.readFileSync(SUBS_FILE, 'utf8')); | |
| if (!Array.isArray(subs)) subs = []; | |
| } catch { subs = []; } | |
| } | |
| export function publicKey() { return keys.publicKey; } | |
| export function deviceCount() { return subs.length; } | |
| export function addSubscription(subscription, ua) { | |
| if (!subscription || typeof subscription.endpoint !== 'string') return false; | |
| subs = subs.filter((s) => s.subscription.endpoint !== subscription.endpoint); | |
| subs.push({ subscription, ua: String(ua || '').slice(0, 160), createdAt: new Date().toISOString() }); | |
| persistSubs(); | |
| return true; | |
| } | |
| export function removeSubscription(endpoint) { | |
| const before = subs.length; | |
| subs = subs.filter((s) => s.subscription.endpoint !== endpoint); | |
| if (subs.length !== before) persistSubs(); | |
| return before !== subs.length; | |
| } | |
| /** Push {title, body, url} to every subscribed device; prunes dead endpoints. */ | |
| export async function sendToAll({ title, body, url }) { | |
| const payload = JSON.stringify({ title, body, url }); | |
| let sent = 0; | |
| let failed = 0; | |
| const dead = new Set(); | |
| await Promise.all(subs.map(async (s) => { | |
| try { | |
| await webpush.sendNotification(s.subscription, payload, { TTL: 3600 }); | |
| sent++; | |
| } catch (e) { | |
| failed++; | |
| // 404/410 = the subscription is gone (app uninstalled, permission revoked) | |
| if (e && (e.statusCode === 404 || e.statusCode === 410)) dead.add(s.subscription.endpoint); | |
| } | |
| })); | |
| if (dead.size) { | |
| subs = subs.filter((s) => !dead.has(s.subscription.endpoint)); | |
| persistSubs(); | |
| } | |
| return { sent, failed, devices: subs.length }; | |
| } | |