@fal-ai/server-proxy
Version:
The fal.ai server proxy adapter for JavaScript and TypeScript Web frameworks
175 lines • 7.5 kB
JavaScript
;
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