UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

249 lines (248 loc) 11.1 kB
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 };