bun-ws-router
Version:
Lightweight client/server WebSocket router for Bun with type-safe Zod/Valibot validation.
103 lines (91 loc) • 2.68 kB
text/typescript
// SPDX-FileCopyrightText: 2025-present Kriasoft
// SPDX-License-Identifier: MIT
/**
* Authentication token attachment for WebSocket connections.
* See @specs/client.md#protocol-merging for protocol handling.
*/
export interface AuthConfig {
getToken?: () =>
| string
| null
| undefined
| Promise<string | null | undefined>;
attach?: "query" | "protocol";
queryParam?: string;
protocolPrefix?: string;
protocolPosition?: "append" | "prepend";
}
/**
* Validates protocolPrefix doesn't contain spaces or commas (RFC 6455).
* Throws TypeError if invalid.
*/
export function validateProtocolPrefix(prefix: string): void {
if (!/^[^\s,]+$/.test(prefix)) {
throw new TypeError(
`Invalid protocolPrefix: "${prefix}" (must not contain spaces/commas)`,
);
}
}
/**
* Attaches auth token to URL as query parameter.
*/
export function attachTokenToUrl(
url: string | URL,
token: string,
queryParam: string,
): URL {
const urlObj = typeof url === "string" ? new URL(url) : new URL(url.href);
urlObj.searchParams.set(queryParam, token);
return urlObj;
}
/**
* Merges user protocols with auth token protocol.
* Returns combined protocols array with deduplication.
*/
export function mergeProtocols(
userProtocols: string | string[] | undefined,
token: string | null | undefined,
protocolPrefix: string,
protocolPosition: "append" | "prepend",
): string[] | undefined {
// Normalize user protocols to array
const normalized: string[] =
userProtocols === undefined
? []
: Array.isArray(userProtocols)
? userProtocols
: [userProtocols];
// Generate token protocol if token exists
const tokenProtocol =
token !== null && token !== undefined ? `${protocolPrefix}${token}` : null;
// Combine based on position
const combined =
tokenProtocol === null
? normalized
: protocolPosition === "prepend"
? [tokenProtocol, ...normalized]
: [...normalized, tokenProtocol];
// Deduplicate preserving first occurrence
const seen = new Set<string>();
const deduplicated = combined.filter((p) => {
if (seen.has(p)) return false;
seen.add(p);
return true;
});
// Filter out empty strings (prevent malformed header)
const filtered = deduplicated.filter((p) => p !== "");
return filtered.length > 0 ? filtered : undefined;
}
/**
* Retrieves auth token (handles sync/async getToken).
*/
export async function getAuthToken(
getToken?: () =>
| string
| null
| undefined
| Promise<string | null | undefined>,
): Promise<string | null | undefined> {
if (!getToken) return undefined;
return await getToken();
}