55 lines
2.1 KiB
TypeScript
55 lines
2.1 KiB
TypeScript
/**
|
|
* 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);
|
|
});
|
|
};
|