Files
HiveGrid-Wireframes/03 Quellen und Referenzen/hivegrid-cloud/apps/web/app/modules/chat/client/chatTokenProvider.ts
T

84 lines
2.8 KiB
TypeScript

import type { ChatTokenProvider } from "@hivegrid/realtime";
export type ChatGatewayRouting = {
readonly protocol: "http" | "https";
readonly apiHost: string;
};
export type ChatTokenFetch = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
export type CreateChatTokenProviderInput = {
readonly tenantSlug: string;
readonly csrfToken: string;
readonly fetchImpl?: ChatTokenFetch;
};
type ChatTokenPayload = {
readonly ok: true;
readonly data: {
readonly token: string;
};
};
/** The gateway lives on the configured API host and exposes one fixed path. */
export function buildChatWebSocketUrl(routing: ChatGatewayRouting): string {
const url = new URL(`${routing.protocol}://${routing.apiHost}`);
if (url.username || url.password || url.pathname !== "/" || url.search || url.hash) {
throw new Error("Chat API routing must contain a host without credentials or a path.");
}
url.protocol = routing.protocol === "https" ? "wss:" : "ws:";
url.pathname = "/ws/chat";
return url.toString();
}
/** Loads a short-lived token over the same-origin session boundary. */
export function createChatTokenProvider(input: CreateChatTokenProviderInput): ChatTokenProvider {
const tenantSlug = requireNonEmpty(input.tenantSlug, "tenant slug");
const csrfToken = requireNonEmpty(input.csrfToken, "CSRF token");
const fetchImpl = input.fetchImpl ?? globalThis.fetch;
if (typeof fetchImpl !== "function") {
throw new Error("Fetch is not available; provide a fetch implementation.");
}
const tokenPath = `/${encodeURIComponent(tenantSlug)}/chat/token`;
return async () => {
const response = await fetchImpl(tokenPath, {
method: "POST",
headers: {
accept: "application/json",
"x-csrf-token": csrfToken,
},
credentials: "same-origin",
cache: "no-store",
redirect: "error",
});
if (!response.ok) throw new Error("Chat gateway token could not be loaded.");
let payload: unknown;
try {
payload = await response.json();
} catch {
throw new Error("Chat gateway token response was invalid.");
}
if (!isChatTokenPayload(payload)) {
throw new Error("Chat gateway token response was invalid.");
}
return payload.data.token;
};
}
function isChatTokenPayload(value: unknown): value is ChatTokenPayload {
if (!isRecord(value) || value.ok !== true || !isRecord(value.data)) return false;
const token = value.data.token;
return typeof token === "string" && token.length > 0 && token === token.trim();
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function requireNonEmpty(value: string, label: string): string {
if (value.length === 0 || value !== value.trim()) throw new Error(`Chat ${label} is invalid.`);
return value;
}