diff --git a/libs/client/package.json b/libs/client/package.json index 306350c..27a99c4 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.1.1", + "version": "1.2.0-alpha.2", "license": "MIT", "repository": { "type": "git", diff --git a/libs/client/src/storage.ts b/libs/client/src/storage.ts index adb1981..015b768 100644 --- a/libs/client/src/storage.ts +++ b/libs/client/src/storage.ts @@ -28,51 +28,59 @@ export interface StorageClient { transformInput: (input: Record) => Promise>; } -type InitiateUploadResult = { - file_url: string; - upload_url: string; +type CdnAuthToken = { + base_url: string; + expires_at: string; + token: string; + token_type: string; }; -type InitiateUploadData = { - file_name: string; - content_type: string | null; -}; +function isExpired(token: CdnAuthToken): boolean { + return new Date().getTime() >= new Date(token.expires_at).getTime(); +} -/** - * Get the file extension from the content type. This is used to generate - * a file name if the file name is not provided. - * - * @param contentType the content type of the file. - * @returns the file extension or `bin` if the content type is not recognized. - */ -function getExtensionFromContentType(contentType: string): string { - const [_, fileType] = contentType.split("/"); - return fileType.split(/[-;]/)[0] ?? "bin"; +interface TokenManager { + token: CdnAuthToken | null; + + fetchToken(config: RequiredConfig): Promise; + + getToken(config: RequiredConfig): Promise; } -/** - * Initiate the upload of a file to the server. This returns the URL to upload - * the file to and the URL of the file once it is uploaded. - * - * @param file the file to upload - * @returns the URL to upload the file to and the URL of the file once it is uploaded. - */ -async function initiateUpload( - file: Blob, - config: RequiredConfig, -): Promise { - const contentType = file.type || "application/octet-stream"; - const filename = - file.name || `${Date.now()}.${getExtensionFromContentType(contentType)}`; - return await dispatchRequest({ +type UploadCdnResponse = { + access_url: string; +}; + +const tokenManager: TokenManager = { + token: null, + async getToken(config: RequiredConfig) { + if (!tokenManager.token || isExpired(tokenManager.token)) { + tokenManager.token = await tokenManager.fetchToken(config); + } + return tokenManager.token; + }, + async fetchToken(config: RequiredConfig): Promise { + return dispatchRequest({ + method: "POST", + targetUrl: `${getRestApiUrl()}/storage/auth/token?storage_type=fal-cdn-v3`, + config: config, + input: {}, + }); + }, +}; + +async function uploadFile(file: Blob, config: RequiredConfig) { + const token = await tokenManager.getToken(config); + const response = await fetch(`${token.base_url}/files/upload`, { method: "POST", - targetUrl: `${getRestApiUrl()}/storage/upload/initiate`, - input: { - content_type: contentType, - file_name: filename, + headers: { + Authorization: `${token.token_type} ${token.token}`, + "Content-Type": file.type || "application/octet-stream", }, - config, + body: file, }); + const result: UploadCdnResponse = await response.json(); + return result.access_url; } // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -87,20 +95,7 @@ export function createStorageClient({ }: StorageClientDependencies): StorageClient { const ref: StorageClient = { upload: async (file: Blob) => { - const { fetch, responseHandler } = config; - const { upload_url: uploadUrl, file_url: url } = await initiateUpload( - file, - config, - ); - const response = await fetch(uploadUrl, { - method: "PUT", - body: file, - headers: { - "Content-Type": file.type || "application/octet-stream", - }, - }); - await responseHandler(response); - return url; + return await uploadFile(file, config); }, // eslint-disable-next-line @typescript-eslint/no-explicit-any