openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
249 lines (248 loc) • 11.1 kB
JavaScript
import { l as normalizeOptionalString, p as normalizeStringifiedOptionalString } from "./string-coerce-CIXf7egm.js";
import { t as ErrorCodes } from "./gateway-error-details-w0nAGBBp.js";
import { as as validateWebPushTestParams, is as validateWebPushSubscribeParams, ns as validateWebPushPreferencesGetParams, os as validateWebPushUnsubscribeParams, rr as validatePushTestParams, rs as validateWebPushPreferencesSetParams, ss as validateWebPushVapidPublicKeyParams } from "./src-BiL5aQto.js";
import { d as errorShape } from "./error-codes-Bo8q2D1o.js";
import { C as getUserPreferences, c as resolveUserProfileId, w as setUserPreferences } from "./user-profiles-4AB7AmiH.js";
import { r as respondUnavailableOnThrow } from "./nodes.helpers-7uACMTmw.js";
import { t as assertValidParams } from "./validation-pzrlzFvo.js";
import { A as resolveEffectiveWebPushPreferences, D as normalizeWebPushDevicePreferences, T as WEB_PUSH_USER_PREFERENCES_KEY, b as setWebPushSubscriptionPreferences, k as normalizeWebPushNotificationPreferences, l as findBoundWebPushSubscriptionByEndpoint, r as WebPushSubscriptionBindingError } from "./push-web-store-DllT5THN.js";
import { a as resolveVapidKeys, i as registerWebPushSubscription, n as clearBoundWebPushSubscription, t as broadcastWebPush } from "./push-web-Q11I0BtA.js";
import { c as resolveApnsAuthConfigFromEnv, s as shouldClearStoredApnsRegistration, t as sendApnsAlert } from "./push-apns-n6D4554g.js";
import { c as loadApnsRegistration, g as resolveApnsRelayConfigFromEnv, i as clearApnsRegistrationIfCurrent, u as normalizeApnsEnvironment } from "./push-apns-store-cDZzCfro.js";
//#region src/gateway/server-methods/push.ts
function hasValidWebPushQuietHoursTimeZone(preferences) {
const timeZone = preferences.quietHours?.timeZone;
if (!timeZone) return true;
try {
new Intl.DateTimeFormat("en-US", { timeZone }).format(0);
return true;
} catch {
return false;
}
}
function authorizeWebPushSubscription(endpoint, { client, respond }) {
const deviceId = normalizeOptionalString(client?.connect.device?.id);
const subscription = findBoundWebPushSubscriptionByEndpoint({ endpoint });
if (!deviceId || !subscription || subscription.deviceId !== deviceId) {
respond(false, void 0, errorShape(ErrorCodes.FORBIDDEN, "subscription is not bound to this device"));
return;
}
const currentProfileId = client?.authenticatedUserProfile?.profileId ? resolveUserProfileId(client.authenticatedUserProfile.profileId) : void 0;
const subscriptionProfileId = subscription.userProfileId ? resolveUserProfileId(subscription.userProfileId) : void 0;
if (subscription.userProfileId && !subscriptionProfileId || client?.authenticatedUserProfile?.profileId && !currentProfileId || (subscriptionProfileId ?? null) !== (currentProfileId ?? null)) {
respond(false, void 0, errorShape(ErrorCodes.FORBIDDEN, "subscription is not bound to this user"));
return;
}
return {
subscription,
currentProfileId
};
}
const pushHandlers = {
"push.test": async ({ params, respond, context }) => {
if (!assertValidParams(params, validatePushTestParams, "push.test", respond)) return;
const nodeId = normalizeStringifiedOptionalString(params.nodeId) ?? "";
if (!nodeId) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "nodeId required"));
return;
}
const title = normalizeOptionalString(params.title) ?? "OpenClaw";
const body = normalizeOptionalString(params.body) ?? `Push test for node ${nodeId}`;
await respondUnavailableOnThrow(respond, async () => {
const registration = await loadApnsRegistration(nodeId);
if (!registration) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, `node ${nodeId} has no APNs registration (connect iOS node first)`));
return;
}
const overrideEnvironment = normalizeApnsEnvironment(params.environment);
const result = registration.transport === "direct" ? await (async () => {
const auth = await resolveApnsAuthConfigFromEnv(process.env);
if (!auth.ok) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, auth.error));
return null;
}
return await sendApnsAlert({
registration: {
...registration,
environment: overrideEnvironment ?? registration.environment
},
nodeId,
title,
body,
auth: auth.value
});
})() : await (async () => {
const relay = resolveApnsRelayConfigFromEnv(process.env, context.getRuntimeConfig().gateway, { registrationRelayOrigin: registration.relayOrigin });
if (!relay.ok) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, relay.error));
return null;
}
return await sendApnsAlert({
registration,
nodeId,
title,
body,
relayConfig: relay.value
});
})();
if (!result) return;
if (shouldClearStoredApnsRegistration({
registration,
result,
overrideEnvironment
})) await clearApnsRegistrationIfCurrent({
nodeId,
registration
});
respond(true, result, void 0);
});
},
"push.web.vapidPublicKey": async ({ params, respond }) => {
if (!assertValidParams(params, validateWebPushVapidPublicKeyParams, "push.web.vapidPublicKey", respond)) return;
await respondUnavailableOnThrow(respond, async () => {
respond(true, { vapidPublicKey: (await resolveVapidKeys()).publicKey }, void 0);
});
},
"push.web.subscribe": async ({ params, respond, client, context }) => {
if (!assertValidParams(params, validateWebPushSubscribeParams, "push.web.subscribe", respond)) return;
const deviceId = normalizeOptionalString(client?.connect.device?.id);
if (!deviceId) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "paired browser device identity required"));
return;
}
const userProfileId = normalizeOptionalString(client?.authenticatedUserProfile?.profileId);
if (context.getRuntimeConfig().gateway?.roles && !userProfileId) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "Web Push requires an authenticated user profile when Gateway roles are enabled"));
return;
}
await respondUnavailableOnThrow(respond, async () => {
try {
respond(true, { subscriptionId: (await registerWebPushSubscription({
endpoint: params.endpoint,
keys: params.keys,
binding: {
deviceId,
userProfileId: userProfileId ?? null
}
})).subscriptionId }, void 0);
} catch (error) {
if (!(error instanceof WebPushSubscriptionBindingError)) throw error;
respond(false, void 0, errorShape(ErrorCodes.FORBIDDEN, error.message));
}
});
},
"push.web.unsubscribe": async ({ params, respond, client }) => {
if (!assertValidParams(params, validateWebPushUnsubscribeParams, "push.web.unsubscribe", respond)) return;
await respondUnavailableOnThrow(respond, async () => {
const authorized = authorizeWebPushSubscription(params.endpoint, {
client,
respond
});
if (!authorized) return;
const { subscription } = authorized;
const removed = await clearBoundWebPushSubscription({
endpoint: params.endpoint,
expectedDeviceId: subscription.deviceId,
expectedUserProfileId: subscription.userProfileId
});
if (!removed) {
respond(false, void 0, errorShape(ErrorCodes.FORBIDDEN, "subscription binding changed"));
return;
}
respond(true, { removed }, void 0);
});
},
"push.web.preferences.get": async ({ params, respond, client }) => {
if (!assertValidParams(params, validateWebPushPreferencesGetParams, "push.web.preferences.get", respond)) return;
const authorized = authorizeWebPushSubscription(params.endpoint, {
client,
respond
});
if (!authorized) return;
const { subscription, currentProfileId } = authorized;
const storedUser = currentProfileId ? getUserPreferences(currentProfileId, [WEB_PUSH_USER_PREFERENCES_KEY])[WEB_PUSH_USER_PREFERENCES_KEY] : void 0;
const user = normalizeWebPushNotificationPreferences(storedUser);
respond(true, {
durableIdentity: Boolean(currentProfileId),
user,
device: subscription.devicePreferences,
effective: resolveEffectiveWebPushPreferences({
user,
device: subscription.devicePreferences
})
}, void 0);
},
"push.web.preferences.set": async ({ params, respond, client, context }) => {
if (!assertValidParams(params, validateWebPushPreferencesSetParams, "push.web.preferences.set", respond)) return;
const authorized = authorizeWebPushSubscription(params.endpoint, {
client,
respond
});
if (!authorized) return;
const { subscription, currentProfileId } = authorized;
if (!hasValidWebPushQuietHoursTimeZone(params.preferences)) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "invalid notification quiet-hours time zone"));
return;
}
if (params.scope === "user") {
if (!currentProfileId) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "user defaults require a durable authenticated profile"));
return;
}
const preferences = normalizeWebPushNotificationPreferences(params.preferences);
if (!setUserPreferences(currentProfileId, { ["notifications.web.v1"]: preferences }).ok) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "could not save notification preferences"));
return;
}
respond(true, {
scope: "user",
preferences
}, void 0);
const connIds = context.getClientConnIds?.((connectedClient) => {
const connectedProfileId = connectedClient.authenticatedUserProfile?.profileId;
return Boolean(connectedProfileId && resolveUserProfileId(connectedProfileId) === currentProfileId);
});
if (connIds?.size) context.broadcastToConnIds("users.prefs.changed", {
profileId: currentProfileId,
keys: [WEB_PUSH_USER_PREFERENCES_KEY]
}, connIds);
return;
}
const preferences = normalizeWebPushDevicePreferences(params.preferences);
if (!setWebPushSubscriptionPreferences({
endpoint: params.endpoint,
preferences,
expectedDeviceId: subscription.deviceId,
expectedUserProfileId: subscription.userProfileId
})) {
respond(false, void 0, errorShape(ErrorCodes.FORBIDDEN, "subscription binding changed"));
return;
}
respond(true, {
scope: "device",
preferences
}, void 0);
},
"push.web.test": async ({ params, respond }) => {
if (!assertValidParams(params, validateWebPushTestParams, "push.web.test", respond)) return;
const title = normalizeOptionalString(params.title) ?? "OpenClaw";
const body = normalizeOptionalString(params.body) ?? "Web push test notification";
await respondUnavailableOnThrow(respond, async () => {
const results = await broadcastWebPush({
title,
body
});
if (results.length === 0) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "no web push subscriptions registered"));
return;
}
if (!results.some((result) => result.ok)) {
respond(false, void 0, errorShape(ErrorCodes.UNAVAILABLE, "all web push deliveries failed", { details: { results } }));
return;
}
respond(true, { results }, void 0);
});
}
};
//#endregion
export { pushHandlers };