/** * Realtime channel authorization. * * Our Soketi channels are `private-`/`presence-` prefixed, so Soketi refuses a * subscription without a signature. Only the BFF holds the app secret, so the * client asks it to sign each subscription at * POST /api/v1/broadcasting/auth — which first checks the session cookie * identifies someone allowed on that channel. * * We supply a `customHandler` rather than pusher-js's built-in `authEndpoint` * because that built-in uses XMLHttpRequest without setting * `withCredentials`, so it would never send our httpOnly session cookie * cross-origin (prabhupadasays.org → api.prabhupadasays.org) and every * subscription would 401. A plain fetch with `credentials: "include"` matches * how the rest of lib/ talks to the BFF. * * The endpoint answers with a BARE {auth, channel_data} body rather than the * usual {success, data} envelope, because pusher-js reads `auth` off the top * level of the response. */ import type { ChannelAuthorizationHandler } from "pusher-js"; const BFF_BASE = process.env.NEXT_PUBLIC_BFF_URL || "http://localhost:8080"; /** * Signs private/presence subscriptions. Shared by every realtime consumer, so * new channel families need no client-side change — only a rule on the BFF. */ export const channelAuthorizer: ChannelAuthorizationHandler = ( { socketId, channelName }, callback, ) => { fetch(`${BFF_BASE}/api/v1/broadcasting/auth`, { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ socket_id: socketId, channel_name: channelName }), }) .then(async (res) => { if (!res.ok) { // 403 is a normal outcome (not a participant / not your channel), so // carry the status rather than presenting it as a network fault. throw new Error( `channel authorization failed for ${channelName}: ${res.status}`, ); } return (await res.json()) as { auth: string; channel_data?: string }; }) .then((data) => callback(null, data)) .catch((err: unknown) => { callback(err instanceof Error ? err : new Error(String(err)), null); }); };