UNPKG

@fal-ai/server-proxy

Version:

The fal.ai server proxy adapter for JavaScript and TypeScript Web frameworks

175 lines 7.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.resolveApiKeyFromEnv = exports.responsePassthrough = exports.DEFAULT_PROXY_ROUTE = exports.TARGET_URL_HEADER = exports.resolveProxyConfig = exports.DEFAULT_ALLOWED_URL_PATTERNS = exports.createUrlMatcher = void 0; exports.isAllowedUrl = isAllowedUrl; exports.getEndpoint = getEndpoint; exports.isAllowedEndpoint = isAllowedEndpoint; exports.handleRequest = handleRequest; exports.fromHeaders = fromHeaders; const config_1 = require("./config"); const utils_1 = require("./utils"); var config_2 = require("./config"); Object.defineProperty(exports, "createUrlMatcher", { enumerable: true, get: function () { return config_2.createUrlMatcher; } }); Object.defineProperty(exports, "DEFAULT_ALLOWED_URL_PATTERNS", { enumerable: true, get: function () { return config_2.DEFAULT_ALLOWED_URL_PATTERNS; } }); Object.defineProperty(exports, "resolveProxyConfig", { enumerable: true, get: function () { return config_2.resolveProxyConfig; } }); exports.TARGET_URL_HEADER = "x-fal-target-url"; exports.DEFAULT_PROXY_ROUTE = "/api/fal/proxy"; const FAL_KEY = process.env.FAL_KEY; const FAL_KEY_ID = process.env.FAL_KEY_ID; const FAL_KEY_SECRET = process.env.FAL_KEY_SECRET; // Default matcher using the default allowed URL patterns const defaultUrlMatcher = (0, config_1.createUrlMatcher)(config_1.DEFAULT_ALLOWED_URL_PATTERNS); /** * Checks if a URL matches any of the allowed URL patterns. * * @param url the URL to check (without scheme, e.g., "fal.run/path"). * @param patterns the allowed URL patterns (glob-style). If not provided, uses default patterns. * @returns whether the URL is allowed. */ function isAllowedUrl(url, patterns) { if (patterns) { return (0, config_1.createUrlMatcher)(patterns)(url); } return defaultUrlMatcher(url); } /** * Extracts the URL without the scheme for validation purposes. * @param targetUrl the full URL including scheme. * @returns the URL without the scheme (host + path + query). */ function getUrlWithoutScheme(targetUrl) { const url = new URL(targetUrl); return `${url.host}${url.pathname}${url.search}`; } /** * Checks if the URL is on the fal.ai domain or any of its subdomains. * @param targetUrl the full URL including scheme. * @returns true if the URL is on *.fal.ai domain. */ function isFalAiDomain(targetUrl) { const url = new URL(targetUrl); return url.host === "fal.ai" || url.host.endsWith(".fal.ai"); } /** * Extracts the endpoint from a URL (path without leading slash). * @param targetUrl the full URL including scheme. * @returns the endpoint (path without leading slash). */ function getEndpoint(targetUrl) { const url = new URL(targetUrl); // Remove leading slash from pathname return url.pathname.replace(/^\//, ""); } /** * Checks if an endpoint matches any of the allowed endpoint patterns. * * @param endpoint the endpoint to check (path without leading slash). * @param patterns the allowed endpoint patterns (glob-style). * @returns whether the endpoint is allowed. */ function isAllowedEndpoint(endpoint, patterns) { // Empty array means all endpoints are allowed (backwards compatibility) if (patterns.length === 0) { return true; } return (0, config_1.createUrlMatcher)(patterns)(endpoint); } function getFalKey() { if (FAL_KEY) { return FAL_KEY; } if (FAL_KEY_ID && FAL_KEY_SECRET) { return `${FAL_KEY_ID}:${FAL_KEY_SECRET}`; } return undefined; } const EXCLUDED_HEADERS = ["content-length", "content-encoding"]; /** * A request handler that proxies the request to the fal API * endpoint. This is useful so client-side calls to the fal endpoint * can be made without CORS issues and the correct credentials can be added * effortlessly. * * @param behavior the request proxy behavior. * @param config the proxy configuration. Can be a partial config (will be resolved internally) * or a pre-resolved config from `resolveProxyConfig` to avoid per-request warnings. * @returns Promise<any> the promise that will be resolved once the request is done. */ async function handleRequest(behavior, config = {}) { const targetUrl = (0, utils_1.singleHeaderValue)(behavior.getHeader(exports.TARGET_URL_HEADER)); if (!targetUrl) { return behavior.respondWith(400, "Invalid request"); } // Check if config is already resolved (has all required fields with non-undefined values) const isResolved = config.isAuthenticated !== undefined && config.resolveFalAuth !== undefined && config.allowUnauthorizedRequests !== undefined && config.allowedUrlPatterns !== undefined; const resolvedConfig = isResolved ? config : (0, config_1.applyProxyConfig)(config); const urlToValidate = getUrlWithoutScheme(targetUrl); if (!isAllowedUrl(urlToValidate, resolvedConfig.allowedUrlPatterns)) { return behavior.respondWith(400, "Invalid request"); } // Check allowed endpoints for POST requests only, skip for *.fal.ai domains if (behavior.method?.toUpperCase() === "POST" && !isFalAiDomain(targetUrl)) { const endpoint = getEndpoint(targetUrl); if (!isAllowedEndpoint(endpoint, resolvedConfig.allowedEndpoints ?? [])) { return behavior.respondWith(400, "Invalid request"); } } const isAuthenticated = (await resolvedConfig.isAuthenticated?.(behavior)) ?? false; if (!isAuthenticated && !resolvedConfig.allowUnauthorizedRequests) { return behavior.respondWith(401, "Unauthorized"); } const authorization = (await resolvedConfig.resolveFalAuth?.(behavior)) ?? (await behavior.resolveApiKey?.()); if (!authorization) { return behavior.respondWith(401, "Unauthorized"); } // pass over headers prefixed with x-fal-* const headers = {}; Object.keys(behavior.getHeaders()).forEach((key) => { if (key.toLowerCase().startsWith("x-fal-")) { headers[key.toLowerCase()] = behavior.getHeader(key); } }); const proxyUserAgent = `@fal-ai/server-proxy/${behavior.id}`; const userAgent = (0, utils_1.singleHeaderValue)(behavior.getHeader("user-agent")); const res = await fetch(targetUrl, { method: behavior.method, headers: { ...headers, authorization, accept: "application/json", "content-type": "application/json", "user-agent": userAgent, "x-fal-client-proxy": proxyUserAgent, }, body: behavior.method?.toUpperCase() === "GET" ? undefined : await behavior.getRequestBody(), }); // copy headers from fal to the proxied response res.headers.forEach((value, key) => { if (!EXCLUDED_HEADERS.includes(key.toLowerCase())) { behavior.sendHeader(key, value); } }); return behavior.sendResponse(res); } function fromHeaders(headers) { // TODO once Header.entries() is available, use that instead // Object.fromEntries(headers.entries()); const result = {}; headers.forEach((value, key) => { result[key] = value; }); return result; } const responsePassthrough = (res) => Promise.resolve(res); exports.responsePassthrough = responsePassthrough; const resolveApiKeyFromEnv = () => Promise.resolve(getFalKey()); exports.resolveApiKeyFromEnv = resolveApiKeyFromEnv; //# sourceMappingURL=index.js.map