feat: support single fal_key env var (#10)

* feat: support single fal_key env var

* fix: default config and tests
This commit is contained in:
Daniel Rochetti 2023-09-24 18:21:18 -03:00 committed by GitHub
parent 9a0497da30
commit f075fd914f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 62 additions and 59 deletions

View File

@ -1,3 +1,3 @@
// @snippet:start(client.proxy.nextjs) // @snippet:start("client.proxy.nextjs")
export { config, handler as default } from '@fal-ai/serverless-nextjs'; export { config, handler as default } from '@fal-ai/serverless-nextjs';
// @snippet:end // @snippet:end

View File

@ -33,10 +33,11 @@ function Error(props) {
); );
} }
const DEFAULT_PROMPT = "a city landscape of a cyberpunk metropolis, raining, purple, pink and teal neon lights, highly detailed, uhd"; const DEFAULT_PROMPT =
'a city landscape of a cyberpunk metropolis, raining, purple, pink and teal neon lights, highly detailed, uhd';
export function Index() { export function Index() {
// @snippet:start(client.ui.state) // @snippet:start("client.ui.state")
// Input state // Input state
const [prompt, setPrompt] = useState<string>(DEFAULT_PROMPT); const [prompt, setPrompt] = useState<string>(DEFAULT_PROMPT);
// Result state // Result state
@ -64,7 +65,7 @@ export function Index() {
const handleOnClick = async (e) => { const handleOnClick = async (e) => {
e.preventDefault(); e.preventDefault();
reset(); reset();
// @snippet:start(client.queue.subscribe) // @snippet:start("client.queue.subscribe")
setLoading(true); setLoading(true);
const start = Date.now(); const start = Date.now();
try { try {
@ -74,10 +75,13 @@ export function Index() {
model_name: 'stabilityai/stable-diffusion-xl-base-1.0', model_name: 'stabilityai/stable-diffusion-xl-base-1.0',
image_size: 'square_hd', image_size: 'square_hd',
}, },
onQueueUpdate(status) { onQueueUpdate(update) {
setElapsedTime(Date.now() - start); setElapsedTime(Date.now() - start);
if (status.status === 'IN_PROGRESS') { if (
setLogs(status.logs.map((log) => log.message)); update.status === 'IN_PROGRESS' ||
update.status === 'COMPLETED'
) {
setLogs(update.logs.map((log) => log.message));
} }
}, },
}); });
@ -91,8 +95,8 @@ export function Index() {
// @snippet:end // @snippet:end
}; };
return ( return (
<div className="min-h-screen dark:bg-gray-800 dark:text-gray-50 bg-gray-100 text-gray-800"> <div className="min-h-screen dark:bg-gray-900 bg-gray-100">
<main className="flex flex-col items-center justify-center w-full flex-1 px-20 py-10 space-y-8"> <main className="container dark:text-gray-50 text-gray-900 flex flex-col items-center justify-center w-full flex-1 py-10 space-y-8">
<h1 className="text-4xl font-bold mb-8"> <h1 className="text-4xl font-bold mb-8">
Hello <code className="font-light text-pink-600">fal</code> Hello <code className="font-light text-pink-600">fal</code>
</h1> </h1>
@ -134,7 +138,7 @@ export function Index() {
<p className="text-sm text-current/80"> <p className="text-sm text-current/80">
{`Elapsed Time (seconds): ${(elapsedTime / 1000).toFixed(2)}`} {`Elapsed Time (seconds): ${(elapsedTime / 1000).toFixed(2)}`}
</p> </p>
<pre className="text-sm bg-black/80 text-white/80 font-mono h-60 rounded whitespace-pre overflow-auto w-full"> <pre className="text-sm bg-black/70 text-white/80 font-mono h-60 rounded whitespace-pre overflow-auto w-full">
{result {result
? JSON.stringify(result, null, 2) ? JSON.stringify(result, null, 2)
: '// result pending...'} : '// result pending...'}
@ -143,7 +147,7 @@ export function Index() {
<div className="space-y-2"> <div className="space-y-2">
<h3 className="text-xl font-light">Logs</h3> <h3 className="text-xl font-light">Logs</h3>
<pre className="text-sm bg-black/80 text-white/80 font-mono h-60 rounded whitespace-pre overflow-auto w-full"> <pre className="text-sm bg-black/70 text-white/80 font-mono h-60 rounded whitespace-pre overflow-auto w-full">
{logs.filter(Boolean).join('\n')} {logs.filter(Boolean).join('\n')}
</pre> </pre>
</div> </div>

View File

@ -4,10 +4,7 @@ describe('The config test suite', () => {
it('should set the config variables accordingly', () => { it('should set the config variables accordingly', () => {
const newConfig = { const newConfig = {
host: 'some-other-host', host: 'some-other-host',
credentials: { credentials: 'key-id:key-secret',
keyId: 'key-id',
keySecret: 'key-secret',
},
}; };
config(newConfig); config(newConfig);
const currentConfig = getConfig(); const currentConfig = getConfig();

View File

@ -2,15 +2,10 @@ import type { RequestMiddleware } from './middleware';
import type { ResponseHandler } from './response'; import type { ResponseHandler } from './response';
import { defaultResponseHandler } from './response'; import { defaultResponseHandler } from './response';
export type Credentials = { export type CredentialsResolver = () => string | undefined;
keyId: string;
keySecret: string;
};
export type CredentialsResolver = () => Credentials;
export type Config = { export type Config = {
credentials?: Credentials | CredentialsResolver; credentials?: undefined | string | CredentialsResolver;
host?: string; host?: string;
requestMiddleware?: RequestMiddleware; requestMiddleware?: RequestMiddleware;
responseHandler?: ResponseHandler<any>; responseHandler?: ResponseHandler<any>;
@ -28,29 +23,22 @@ function hasEnvVariables(): boolean {
return ( return (
process && process &&
process.env && process.env &&
typeof process.env.FAL_KEY_ID !== 'undefined' && (typeof process.env.FAL_KEY !== 'undefined' ||
typeof process.env.FAL_KEY_SECRET !== 'undefined' (typeof process.env.FAL_KEY_ID !== 'undefined' &&
typeof process.env.FAL_KEY_SECRET !== 'undefined'))
); );
} }
export const credentialsFromEnv: CredentialsResolver = () => { export const credentialsFromEnv: CredentialsResolver = () => {
if (!hasEnvVariables()) { if (!hasEnvVariables()) {
return { return undefined;
keyId: '',
keySecret: '',
};
}
if (typeof window !== 'undefined') {
console.warn(
"The fal credentials are exposed in the browser's environment. " +
"That's not recommended for production use cases."
);
} }
return { if (typeof process.env.FAL_KEY !== 'undefined') {
keyId: process.env.FAL_KEY_ID || '', return process.env.FAL_KEY;
keySecret: process.env.FAL_KEY_SECRET || '', }
};
return `${process.env.FAL_KEY_ID}:${process.env.FAL_KEY_SECRET}`;
}; };
/** /**
@ -93,6 +81,7 @@ export function config(config: Config) {
export function getConfig(): RequiredConfig { export function getConfig(): RequiredConfig {
if (!configuration) { if (!configuration) {
console.info('Using default configuration for the fal client'); console.info('Using default configuration for the fal client');
return { ...DEFAULT_CONFIG } as RequiredConfig;
} }
return configuration; return configuration;
} }

View File

@ -1,15 +1,7 @@
import { randomUUID } from 'crypto'; import { randomUUID } from 'crypto';
import { config, getConfig } from './config'; import { getConfig } from './config';
import { buildUrl } from './function'; import { buildUrl } from './function';
config({
host: 'gateway.alpha.fal.ai',
credentials: {
keyId: 'a91ff3ca-71bc-4c8c-b400-859f6cbe804d',
keySecret: '0123456789abcdfeghijklmnopqrstuv',
},
});
describe('The function test suite', () => { describe('The function test suite', () => {
it('should build the URL with a function UUIDv4', () => { it('should build the URL with a function UUIDv4', () => {
const id = randomUUID(); const id = randomUUID();

View File

@ -70,17 +70,28 @@ export async function run<Input, Output>(
id: string, id: string,
options: RunOptions<Input> = {} options: RunOptions<Input> = {}
): Promise<Output> { ): Promise<Output> {
const { credentials, requestMiddleware, responseHandler } = getConfig(); const {
credentials: credentialsValue,
requestMiddleware,
responseHandler,
} = getConfig();
const method = (options.method ?? 'post').toLowerCase(); const method = (options.method ?? 'post').toLowerCase();
const userAgent = isBrowser() ? {} : { 'User-Agent': getUserAgent() }; const userAgent = isBrowser() ? {} : { 'User-Agent': getUserAgent() };
const { keyId, keySecret } = const credentials =
typeof credentials === 'function' ? credentials() : credentials; typeof credentialsValue === 'function'
? credentialsValue()
: credentialsValue;
const { url, headers } = await requestMiddleware({ const { url, headers } = await requestMiddleware({
url: buildUrl(id, options), url: buildUrl(id, options),
}); });
const authHeader = const authHeader = credentials ? { Authorization: `Key ${credentials}` } : {};
keyId && keySecret ? { Authorization: `Key ${keyId}:${keySecret}` } : {}; if (typeof window !== 'undefined' && credentials) {
console.warn(
"The fal credentials are exposed in the browser's environment. " +
"That's not recommended for production use cases."
);
}
const requestHeaders = { const requestHeaders = {
...authHeader, ...authHeader,
Accept: 'application/json', Accept: 'application/json',

View File

@ -1,5 +1,4 @@
export { config, getConfig } from './config'; export { config, getConfig } from './config';
export type { Credentials } from './config';
export { queue, run } from './function'; export { queue, run } from './function';
export { withMiddleware } from './middleware'; export { withMiddleware } from './middleware';
export type { RequestMiddleware } from './middleware'; export type { RequestMiddleware } from './middleware';

View File

@ -1,6 +1,7 @@
import type { NextApiHandler, NextApiRequest, PageConfig } from 'next'; import type { NextApiHandler, NextApiRequest, PageConfig } from 'next';
import { TARGET_URL_HEADER } from './config'; import { TARGET_URL_HEADER } from './config';
const FAL_KEY = process.env.FAL_KEY || process.env.NEXT_PUBLIC_FAL_KEY;
const FAL_KEY_ID = process.env.FAL_KEY_ID || process.env.NEXT_PUBLIC_FAL_KEY_ID; const FAL_KEY_ID = process.env.FAL_KEY_ID || process.env.NEXT_PUBLIC_FAL_KEY_ID;
const FAL_KEY_SECRET = const FAL_KEY_SECRET =
process.env.FAL_KEY_SECRET || process.env.NEXT_PUBLIC_FAL_KEY_SECRET; process.env.FAL_KEY_SECRET || process.env.NEXT_PUBLIC_FAL_KEY_SECRET;
@ -28,10 +29,19 @@ function getHeader(request: NextApiRequest, key: string): string | undefined {
function cleanUpHeaders(request: NextApiRequest) { function cleanUpHeaders(request: NextApiRequest) {
delete request.headers['origin']; delete request.headers['origin'];
delete request.headers['referer']; delete request.headers['referer'];
// delete request.headers['transfer-encoding'];
delete request.headers[TARGET_URL_HEADER]; delete request.headers[TARGET_URL_HEADER];
} }
function getFalKey(): string | undefined {
if (FAL_KEY) {
return FAL_KEY;
}
if (FAL_KEY_ID && FAL_KEY_SECRET) {
return `${FAL_KEY_ID}:${FAL_KEY_SECRET}`;
}
return undefined;
}
/** /**
* A Next request handler that proxies the request to the fal-serverless * A Next request handler that proxies the request to the fal-serverless
* endpoint. This is useful so client-side calls to the fal-serverless endpoint * endpoint. This is useful so client-side calls to the fal-serverless endpoint
@ -55,15 +65,16 @@ export const handler: NextApiHandler = async (request, response) => {
cleanUpHeaders(request); cleanUpHeaders(request);
const authHeader = const falKey = getFalKey();
FAL_KEY_ID && FAL_KEY_SECRET if (!falKey) {
? { authorization: `Key ${FAL_KEY_ID}:${FAL_KEY_SECRET}` } response.status(401).send('Missing fal.ai credentials');
: {}; return;
}
const res = await fetch(targetUrl, { const res = await fetch(targetUrl, {
method: request.method, method: request.method,
headers: { headers: {
...authHeader, authorization: `Key ${falKey}`,
accept: 'application/json', accept: 'application/json',
'content-type': 'application/json', 'content-type': 'application/json',
'x-fal-client-proxy': '@fal-ai/serverless-nextjs', 'x-fal-client-proxy': '@fal-ai/serverless-nextjs',