UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

153 lines (152 loc) 6.54 kB
import "./src-vebZIeLe.js"; import { t as expectDefined } from "./expect-CyE8FADM.js"; import { l as normalizeOptionalString } from "./string-coerce-CIXf7egm.js"; import { r as createLazyRuntimeModule } from "./lazy-runtime-CgCh8H_K.js"; import { w as resolveStateDir } from "./paths-D2sRr1a_.js"; import { t as pathMayExistSync } from "./path-existence-CzRtGGnO.js"; import { _ as listWebPushSubscriptions, a as deleteBoundWebPushSubscription, d as insertVapidKeyPairIfAbsent, f as isValidWebPushEndpoint, i as createWebPushVapidKeyPair, p as isValidWebPushKey, s as deleteWebPushSubscriptionIfCurrent, u as hashWebPushEndpoint, x as upsertWebPushSubscription, y as readPersistedVapidKeyPair } from "./push-web-store-DllT5THN.js"; import path from "node:path"; import { randomUUID } from "node:crypto"; //#region src/infra/push-web.ts const LEGACY_WEB_PUSH_PATHS = ["push/web-push-subscriptions.json", "push/vapid-keys.json"]; const loadWebPushRuntime = createLazyRuntimeModule(() => import("web-push").then((mod) => mod.default ?? mod)); function assertLegacyWebPushMigrationComplete(baseDir) { const stateDir = baseDir ?? resolveStateDir(); if (LEGACY_WEB_PUSH_PATHS.find((relativePath) => { const sourcePath = path.join(stateDir, relativePath); return pathMayExistSync(sourcePath) || pathMayExistSync(`${sourcePath}.doctor-importing`); })) throw new Error(`legacy Web Push state requires migration; run \`openclaw doctor --fix\` before using Web Push`); } async function resolveVapidKeys(baseDir) { assertLegacyWebPushMigrationComplete(baseDir); const envPublic = resolveVapidPublicKeyFromEnv(); const envPrivate = resolveVapidPrivateKeyFromEnv(); if (envPublic && envPrivate) return { publicKey: envPublic, privateKey: envPrivate, subject: resolveVapidSubjectFromEnv() }; const existing = readPersistedVapidKeyPair(baseDir); if (existing) return { ...existing, subject: resolveVapidSubjectFromEnv() }; const keys = (await loadWebPushRuntime()).generateVAPIDKeys(); return { ...insertVapidKeyPairIfAbsent({ candidate: createWebPushVapidKeyPair(keys.publicKey, keys.privateKey, resolveVapidSubjectFromEnv()), nowMs: Date.now(), stateDir: baseDir }), subject: resolveVapidSubjectFromEnv() }; } function resolveVapidSubjectFromEnv() { return normalizeOptionalString(process.env.OPENCLAW_VAPID_SUBJECT) ?? "https://openclaw.ai"; } function resolveVapidPublicKeyFromEnv() { return normalizeOptionalString(process.env.OPENCLAW_VAPID_PUBLIC_KEY); } function resolveVapidPrivateKeyFromEnv() { return normalizeOptionalString(process.env.OPENCLAW_VAPID_PRIVATE_KEY); } async function registerWebPushSubscription(params) { const { endpoint, keys, baseDir } = params; if (!isValidWebPushEndpoint(endpoint)) throw new Error("invalid push subscription endpoint: must be an HTTPS URL under 2048 chars"); if (!isValidWebPushKey(keys.p256dh) || !isValidWebPushKey(keys.auth)) throw new Error("invalid push subscription keys: must be non-empty strings under 512 chars"); assertLegacyWebPushMigrationComplete(baseDir); return upsertWebPushSubscription({ endpointHash: hashWebPushEndpoint(endpoint), endpoint, keys: { p256dh: keys.p256dh, auth: keys.auth }, binding: params.binding, candidateSubscriptionId: randomUUID(), nowMs: Date.now(), stateDir: baseDir }); } async function clearBoundWebPushSubscription(params) { assertLegacyWebPushMigrationComplete(params.baseDir); return deleteBoundWebPushSubscription({ ...params, endpointHash: hashWebPushEndpoint(params.endpoint), stateDir: params.baseDir }); } function applyVapidDetails(webPush, keys) { webPush.setVapidDetails(keys.subject, keys.publicKey, keys.privateKey); } async function sendPreparedWebPushNotification(webPush, subscription, payload, deliveryOptions) { const pushSubscription = { endpoint: subscription.endpoint, keys: { p256dh: subscription.keys.p256dh, auth: subscription.keys.auth } }; try { const result = await webPush.sendNotification(pushSubscription, JSON.stringify(payload), deliveryOptions); return { ok: true, subscriptionId: subscription.subscriptionId, statusCode: result.statusCode }; } catch (err) { const statusCode = typeof err === "object" && err !== null && "statusCode" in err ? err.statusCode : void 0; const message = typeof err === "object" && err !== null && "message" in err ? err.message : "unknown error"; return { ok: false, subscriptionId: subscription.subscriptionId, statusCode, error: message }; } } async function sendPreparedWebPushNotifications(params) { const { subscriptions, webPush } = params; if (subscriptions.length === 0) return []; const mapped = (await Promise.allSettled(subscriptions.map((subscription) => sendPreparedWebPushNotification(webPush, subscription, params.payload, params.deliveryOptions)))).map((r, i) => r.status === "fulfilled" ? r.value : { ok: false, subscriptionId: expectDefined(subscriptions[i], "subscriptions entry at i").subscriptionId, error: r.reason instanceof Error ? r.reason.message : "unknown error" }); const expiredSubscriptions = mapped.map((result, i) => ({ result, sub: subscriptions[i] })).filter(({ result }) => !result.ok && (result.statusCode === 410 || result.statusCode === 404)).map(({ sub }) => expectDefined(sub, "push web sub")); for (const subscription of expiredSubscriptions) try { assertLegacyWebPushMigrationComplete(params.baseDir); deleteWebPushSubscriptionIfCurrent({ endpointHash: hashWebPushEndpoint(subscription.endpoint), subscription, stateDir: params.baseDir }); } catch {} return mapped; } /** Prepares transport state so callers can perform final authorization immediately before send. */ async function prepareWebPushNotificationSender(baseDir) { assertLegacyWebPushMigrationComplete(baseDir); const vapidKeys = await resolveVapidKeys(baseDir); const webPush = await loadWebPushRuntime(); applyVapidDetails(webPush, vapidKeys); return (params) => sendPreparedWebPushNotifications({ ...params, webPush, baseDir }); } async function broadcastWebPush(payload, baseDir) { assertLegacyWebPushMigrationComplete(baseDir); const subscriptions = listWebPushSubscriptions(baseDir); if (subscriptions.length === 0) return []; return await (await prepareWebPushNotificationSender(baseDir))({ subscriptions, payload }); } //#endregion export { resolveVapidKeys as a, registerWebPushSubscription as i, clearBoundWebPushSubscription as n, prepareWebPushNotificationSender as r, broadcastWebPush as t };