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:
parent
9a0497da30
commit
f075fd914f
@ -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
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
@ -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';
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user