UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

322 lines (321 loc) • 14.7 kB
import { l as normalizeOptionalString, p as normalizeStringifiedOptionalString } from "./string-coerce-6kR-86XC.mjs"; import { $s as validateWebPushUnsubscribeParams, Qs as validateWebPushTestParams, Tr as validatePushTestParams, Xs as validateWebPushPreferencesSetParams, Ys as validateWebPushPreferencesGetParams, Zs as validateWebPushSubscribeParams, ec as validateWebPushVapidPublicKeyParams } from "./src-D2gvuSyP.mjs"; import { t as ErrorCodes } from "./gateway-error-details-DIefe7Zy.mjs"; import { f as errorShape } from "./error-codes-DI20vDEB.mjs"; import { c as resolveUserProfileId } from "./user-profiles-DeU3iL6Y.mjs"; import { n as authorizeOperatorScopesForMethod } from "./method-scopes-C-MKLkWf.mjs"; import { a as readGatewayRequestMutationAuthority } from "./session-mutation-guards-C7E56jUW.mjs"; import { c as normalizeApnsEnvironment, g as resolveApnsRelayConfigFromEnv, n as clearApnsRegistrationIfCurrent, o as loadApnsRegistration } from "./push-apns-store-rb4I57UR.mjs"; import { _ as resolveEffectiveWebPushPreferences, f as WEB_PUSH_USER_PREFERENCES_KEY, g as normalizeWebPushNotificationPreferences, m as normalizeWebPushDevicePreferences, r as WebPushSubscriptionBindingError } from "./push-web-store.records-Bg3ARQhT.mjs"; import { t as assertValidParams } from "./validation-B7TobU6-.mjs"; import { n as parseGatewayRole, t as isRoleAuthorizedForMethod } from "./role-policy-CtUzNkwE.mjs"; import { a as resolveVapidKeys, d as setWebPushSubscriptionPreferences, f as withBoundWebPushSubscriptionByEndpoint, i as registerWebPushSubscription, n as clearBoundWebPushSubscription, t as broadcastWebPush } from "./push-web-BsxZM3qE.mjs"; import { a as setUserPreferences, r as getUserPreferences } from "./user-preferences-BKrhAahm.mjs"; import { c as resolveApnsAuthConfigFromEnv, s as shouldClearStoredApnsRegistration, t as sendApnsAlert } from "./push-apns-CeEG94DB.mjs"; import { n as respondUnavailableOnThrow } from "./response-DiopswxY.mjs"; //#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 createWebPushRequestGuard(options) { const { client, req } = options; const authority = readGatewayRequestMutationAuthority(options); const method = req.method; const deviceId = normalizeOptionalString(client?.connect.device?.id); const profileReference = client?.authenticatedUserProfile?.profileId; const assertPolicyCurrent = () => { const role = parseGatewayRole(client?.connect.role ?? "operator"); if (!role || !isRoleAuthorizedForMethod(role, method) || !authorizeOperatorScopesForMethod(method, client?.connect.scopes ?? []).allowed || normalizeOptionalString(client?.connect.device?.id) !== deviceId) throw new Error("Web Push requester authority changed"); }; const assertCurrent = () => { authority.assertCurrent(); assertPolicyCurrent(); const currentReference = client?.authenticatedUserProfile?.profileId; const currentProfileId = currentReference ? resolveUserProfileId(currentReference) : void 0; const originalProfileId = profileReference ? resolveUserProfileId(profileReference) : void 0; if (currentReference && !currentProfileId || profileReference && !originalProfileId || currentProfileId !== originalProfileId) throw new Error("Web Push requester profile changed"); return currentProfileId; }; return { assertCurrent, prepareMutation: () => { assertCurrent(); if (authority.family === "native-compatibility") return { family: "native-compatibility", assertCurrent }; const currentReference = client?.authenticatedUserProfile?.profileId; return { family: "worker", profiles: { original: profileReference ?? null, current: currentReference ?? null }, assertCurrent: () => { authority.assertWorkerCurrent(); assertPolicyCurrent(); if (client?.authenticatedUserProfile?.profileId !== currentReference) throw new Error("Web Push requester profile changed"); }, assertProfiles: (facts) => { authority.expectedProfileBinding?.assertMatchesResolvedProfile(facts.profileId ?? void 0); if (!facts.bindingCurrent) throw new Error("Web Push requester profile changed"); } }; } }; } function withAuthorizedWebPushSubscription(endpoint, options, start) { const { client, respond } = options; const requester = createWebPushRequestGuard(options); const deviceId = normalizeOptionalString(client?.connect.device?.id); return withBoundWebPushSubscriptionByEndpoint({ endpoint }, (subscription) => { 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; } const assertCurrent = () => { const profileId = requester.assertCurrent(); const currentSubscriptionProfileId = subscription.userProfileId ? resolveUserProfileId(subscription.userProfileId) : void 0; if (subscription.userProfileId && !currentSubscriptionProfileId || currentSubscriptionProfileId !== profileId) throw new Error("Web Push subscription owner changed"); return profileId; }; assertCurrent(); return { start: () => start({ subscription, assertCurrent, prepareMutation: () => { const guard = requester.prepareMutation(); return guard.family === "native-compatibility" ? { ...guard, assertCurrent } : guard; } }) }; }); } 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 (options) => { const { params, respond, client, context } = options; 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 { const requester = createWebPushRequestGuard(options); const subscription = await registerWebPushSubscription({ endpoint: params.endpoint, keys: params.keys, binding: { deviceId, userProfileId: userProfileId ?? null }, guard: requester.prepareMutation() }); respond(true, { subscriptionId: subscription.subscriptionId }, void 0); } catch (error) { if (!(error instanceof WebPushSubscriptionBindingError)) throw error; respond(false, void 0, errorShape(ErrorCodes.FORBIDDEN, error.message)); } }); }, "push.web.unsubscribe": async (options) => { const { params, respond } = options; if (!assertValidParams(params, validateWebPushUnsubscribeParams, "push.web.unsubscribe", respond)) return; await respondUnavailableOnThrow(respond, async () => { await withAuthorizedWebPushSubscription(params.endpoint, options, (authorized) => { const { subscription } = authorized; return clearBoundWebPushSubscription({ endpoint: params.endpoint, expectedDeviceId: subscription.deviceId, expectedUserProfileId: subscription.userProfileId, guard: authorized.prepareMutation() }).then((removed) => { if (!removed) { respond(false, void 0, errorShape(ErrorCodes.FORBIDDEN, "subscription binding changed")); return; } respond(true, { removed }, void 0); }); }); }); }, "push.web.preferences.get": async (options) => { const { params, respond } = options; if (!assertValidParams(params, validateWebPushPreferencesGetParams, "push.web.preferences.get", respond)) return; await withAuthorizedWebPushSubscription(params.endpoint, options, (authorized) => { const { subscription } = authorized; const currentProfileId = authorized.assertCurrent(); 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 (options) => { const { params, respond, context } = options; if (!assertValidParams(params, validateWebPushPreferencesSetParams, "push.web.preferences.set", respond)) return; await withAuthorizedWebPushSubscription(params.endpoint, options, (authorized) => { const { subscription } = authorized; const currentProfileId = authorized.assertCurrent(); 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); return setWebPushSubscriptionPreferences({ endpoint: params.endpoint, preferences, expectedDeviceId: subscription.deviceId, expectedUserProfileId: subscription.userProfileId, guard: authorized.prepareMutation() }).then((updated) => { if (!updated) { 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 };