openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
153 lines (152 loc) • 6.54 kB
JavaScript
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 };