diff --git a/libs/client/package.json b/libs/client/package.json index 4b430b8..2488dca 100644 --- a/libs/client/package.json +++ b/libs/client/package.json @@ -1,7 +1,7 @@ { "name": "@fal-ai/client", "description": "The fal.ai client for JavaScript and TypeScript", - "version": "1.9.1", + "version": "1.9.2", "license": "MIT", "repository": { "type": "git", diff --git a/libs/client/src/auth.ts b/libs/client/src/auth.ts index 0e32225..559073d 100644 --- a/libs/client/src/auth.ts +++ b/libs/client/src/auth.ts @@ -4,7 +4,7 @@ import { parseEndpointId } from "./utils"; /** * A function that provides a temporary authentication token. - * @param app - The app/endpoint identifier + * @param app - The app/endpoint identifier, including the path (e.g. "fal-ai/myapp/realtime") * @returns A promise that resolves to the token string */ export type TokenProvider = (app: string) => Promise; diff --git a/libs/client/src/realtime.spec.ts b/libs/client/src/realtime.spec.ts index 77fe07b..41ba265 100644 --- a/libs/client/src/realtime.spec.ts +++ b/libs/client/src/realtime.spec.ts @@ -333,7 +333,7 @@ describe("createRealtimeClient", () => { await Promise.resolve(); expect(customTokenProvider).toHaveBeenCalledTimes(1); - expect(customTokenProvider).toHaveBeenCalledWith("123-myapp"); + expect(customTokenProvider).toHaveBeenCalledWith("123/myapp/realtime"); expect(getTemporaryAuthToken).not.toHaveBeenCalled(); expect(WebSocketMock).toHaveBeenCalledTimes(1); @@ -359,7 +359,7 @@ describe("createRealtimeClient", () => { await Promise.resolve(); await Promise.resolve(); - expect(customTokenProvider).toHaveBeenCalledWith("456-otherapp"); + expect(customTokenProvider).toHaveBeenCalledWith("456/otherapp/realtime"); expect(getTemporaryAuthToken).not.toHaveBeenCalled(); }); diff --git a/libs/client/src/realtime.ts b/libs/client/src/realtime.ts index c9bc365..7168905 100644 --- a/libs/client/src/realtime.ts +++ b/libs/client/src/realtime.ts @@ -20,7 +20,12 @@ import { import { RequiredConfig } from "./config"; import { ApiError } from "./response"; import { isBrowser } from "./runtime"; -import { ensureEndpointIdFormat, isReact, throttle } from "./utils"; +import { + ensureEndpointIdFormat, + isReact, + resolveEndpointPath, + throttle, +} from "./utils"; // Define the context interface Context { @@ -312,8 +317,8 @@ function buildRealtimeUrl( queryParams.set("max_buffering", maxBuffering.toFixed(0)); } const appId = ensureEndpointIdFormat(app); - const normalizedPath = path ? `/${path.replace(/^\/+/, "")}` : "/realtime"; - return `wss://fal.run/${appId}${normalizedPath}?${queryParams.toString()}`; + const resolvedPath = resolveEndpointPath(app, path, "/realtime") ?? ""; + return `wss://fal.run/${appId}${resolvedPath}?${queryParams.toString()}`; } const DEFAULT_THROTTLE_INTERVAL = 128; @@ -567,8 +572,11 @@ export function createRealtimeClient({ ) { send({ type: "initiateAuth" }); // Use custom tokenProvider if provided, otherwise use default + const appId = ensureEndpointIdFormat(app); + const resolvedPath = + resolveEndpointPath(app, path, "/realtime") ?? ""; const fetchToken = tokenProvider - ? () => tokenProvider(app) + ? () => tokenProvider(`${appId}${resolvedPath}`) : () => { console.warn( "[fal.realtime] Using the default token provider is deprecated. " + diff --git a/libs/client/src/streaming.ts b/libs/client/src/streaming.ts index fad1482..5532ebe 100644 --- a/libs/client/src/streaming.ts +++ b/libs/client/src/streaming.ts @@ -5,6 +5,7 @@ import { buildUrl, dispatchRequest } from "./request"; import { ApiError, defaultResponseHandler } from "./response"; import { type StorageClient } from "./storage"; import { EndpointType, InputType, OutputType } from "./types/client"; +import { ensureEndpointIdFormat, resolveEndpointPath } from "./utils"; export type StreamingConnectionMode = "client" | "server"; @@ -116,7 +117,7 @@ export class FalStream { this.url = options.url ?? buildUrl(endpointId, { - path: "/stream", + path: resolveEndpointPath(endpointId, undefined, "/stream"), query: options.queryParams, }); this.options = options; @@ -166,8 +167,11 @@ export class FalStream { if (connectionMode === "client") { // if we are in the browser, we need to get a temporary token // to authenticate the request + const appId = ensureEndpointIdFormat(endpointId); + const resolvedPath = + resolveEndpointPath(endpointId, undefined, "/stream") ?? ""; const fetchToken = tokenProvider - ? () => tokenProvider(endpointId) + ? () => tokenProvider(`${appId}${resolvedPath}`) : () => { console.warn( "[fal.stream] Using the default token provider is deprecated. " + diff --git a/libs/client/src/utils.ts b/libs/client/src/utils.ts index 0da3880..c0d67f1 100644 --- a/libs/client/src/utils.ts +++ b/libs/client/src/utils.ts @@ -41,6 +41,30 @@ export function parseEndpointId(id: string): EndpointId { }; } +/** + * Resolves the endpoint path, normalizing it and applying a default. + * If no explicit path is provided and the app already ends with the + * default path, returns undefined to avoid duplication. + * + * @param app - The app/endpoint identifier + * @param path - An explicitly provided path (always used if present) + * @param defaultPath - The default path to use (e.g. "/realtime") + * @returns The resolved path, or undefined if the app already includes it + */ +export function resolveEndpointPath( + app: string, + path: string | undefined, + defaultPath: string, +): string | undefined { + if (path) { + return `/${path.replace(/^\/+/, "")}`; + } + if (app.endsWith(defaultPath)) { + return undefined; + } + return defaultPath; +} + export function isValidUrl(url: string) { try { const { host } = new URL(url);