import * as fs from "fs"; import { COMPONENT_EVENT_HEADER, ComponentEvent, ForbiddenError, getBucketeerUrlFromSuperblocksUrl, getContentType, LocalGitRepoState, NotFoundError, SuperblocksResourceType, BadRequestError, unreachable, ValidateGitSetupRequestBody, } from "@superblocksteam/util"; import axios, { AxiosError, AxiosRequestConfig } from "axios"; import FormData from "form-data"; import { isEqual, isEmpty } from "lodash"; import { BranchNotCheckedOutError, CommitAlreadyExistsError, ValidateGitSetupError, } from "./errors"; import { signingEnabled } from "./flag"; import { connectToISocketRPCServer, StdISocketRPCClient } from "./socket"; import { AgentType, Api, ApiWithPb, Page, RemoteCommitDto, UserMeDto, ViewMode, DeploymentDto, } from "./types"; import { getAgentUrl } from "./utils"; const BASE_BUCKETEER_URL = "api"; const BASE_SERVER_PUBLIC_API_URL_V1 = "api/v1/public"; const BASE_SERVER_PUBLIC_API_URL_v2 = "api/v2/public"; const BASE_SERVER_API_URL_V2 = "api/v2"; const BASE_SERVER_API_URL_V3 = "api/v3"; const SUPERBLOCKS_MAX_FILE_SIZE_MB = 30; const CLI_VERSION_HEADER = "x-superblocks-cli-version"; const SUPERBLOCKS_URL_HEADER = "x-superblocks-url"; export interface UploadFile { name: string; filename: string; ContentType: string; } export interface ApplicationWrapper { application: Record; page: Page; apis: ApiWithPb[]; } export interface MultiPageApplicationWrapper { application: Record; pages: Page[]; apis: Record[]; } export type PushMultiPageApplicationWithCommitConfig = MultiPageApplicationWrapper & { commitId?: string; commitMessage?: string; gitState: LocalGitRepoState; skipCommit: boolean; }; export interface ApiWrapper { apiPb: Api; name?: string; //@deprecated this attribute is used for getting a name for backends(jobs and workflows) that were never migrated to the new API version. } export type PushApiWithCommitConfig = { apiPb: Record; commitId?: string; commitMessage?: string; gitState: LocalGitRepoState; skipCommit: boolean; }; export type Branches = { branches: Branch[]; }; export type Branch = { name: string; isDefault: boolean; }; type ResponseWithMeta = { responseMeta: unknown; data: T }; export interface CommitDto { commitMessage: string; committer: { name?: string; email: string; }; commitId: string; commitDate: number; branch?: string; autosave?: boolean; tag: string; externalCommitId?: string | null; externalCommitDate?: number | null; } export interface GetCommitsResponseBody { autosaves: CommitDto[]; commits: CommitDto[]; } enum ResourceType { APPLICATION = "APPLICATION", BACKEND = "BACKEND", } export async function fetchApplication({ cliVersion, applicationId, branch, token, superblocksBaseUrl, viewMode, commitId, skipSigningVerification = false, injectedHeaders = {}, }: { cliVersion: string; applicationId: string; branch?: string; token: string; superblocksBaseUrl: string; viewMode: ViewMode; commitId?: string; skipSigningVerification?: boolean; injectedHeaders: Record; }): Promise { if (commitId && viewMode !== "export-commit") { throw new Error( `If commitId ${commitId} is provided, viewMode cannot be ${viewMode}`, ); } try { const serverURL = branch ? new URL( `${BASE_SERVER_PUBLIC_API_URL_v2}/applications/${applicationId}/branches/${encodeURIComponent( branch, )}`, superblocksBaseUrl, ) : new URL( `${BASE_SERVER_PUBLIC_API_URL_v2}/applications/${applicationId}`, superblocksBaseUrl, ); serverURL.search = new URLSearchParams({ viewMode, ...(commitId ? { commitId } : {}), }).toString(); const socket = !skipSigningVerification ? await createSocketConnectionIfNeeded( cliVersion, token, superblocksBaseUrl, ) : undefined; if (socket) { try { const resp = await socket.call.v2.public.application.get({ applicationId, viewMode, branchName: branch, commitId, }); return resp.data; } finally { socket.close(); } } const config: AxiosRequestConfig = { method: "get", url: serverURL.toString(), headers: { Authorization: "Bearer " + token, [CLI_VERSION_HEADER]: cliVersion, ...injectedHeaders, }, }; const serverResponse = await axios>(config); const data = serverResponse?.data?.data; return data; } catch (e) { if (axios.isAxiosError(e) && e.response?.status === 404) { throw new NotFoundError(`Application ${applicationId} was not found`); } throw new Error( `Could not fetch application: ${ typeof e === "object" && e && "message" in e ? e.message : e }`, ); } } export async function fetchApplicationBranches({ cliVersion, applicationId, token, superblocksBaseUrl, injectedHeaders = {}, }: { cliVersion: string; applicationId: string; token: string; superblocksBaseUrl: string; injectedHeaders: Record; }): Promise { const serverURL = new URL( `public/applications/${applicationId}/branches`, superblocksBaseUrl, ); let serverResponse; try { const config: AxiosRequestConfig = { method: "get", url: serverURL.toString(), headers: { Authorization: "Bearer " + token, [CLI_VERSION_HEADER]: cliVersion, ...injectedHeaders, }, }; serverResponse = await axios>(config); } catch (e: any) { if (axios.isAxiosError(e) && e.response?.status === 404) { throw new NotFoundError(`Application ${applicationId} was not found`); } throw new Error(`Could not fetch application branches: ${e.message}`); } return serverResponse?.data?.data; } export async function fetchApplicationWithComponents({ cliVersion, applicationId, branch, token, superblocksBaseUrl, viewMode, commitId, skipSigningVerification = false, injectedHeaders = {}, }: { cliVersion: string; applicationId: string; branch: string; token: string; superblocksBaseUrl: string; viewMode: ViewMode; commitId?: string; skipSigningVerification?: boolean; injectedHeaders: Record; }): Promise< | (MultiPageApplicationWrapper & { componentFiles: any; }) | undefined > { const applicationWrapper = await fetchApplication({ cliVersion, applicationId, branch, token, superblocksBaseUrl, viewMode, commitId, skipSigningVerification, injectedHeaders, }); if (isEmpty(applicationWrapper)) { return; } // if there are no custom components, just return here without trying to initialize them using bucketeer if (isEmpty(applicationWrapper.application?.settings?.registeredComponents)) { return { ...applicationWrapper, componentFiles: null, }; } superblocksBaseUrl = superblocksBaseUrl.replace(/\/$/, ""); const bucketeerBaseUrl = getBucketeerUrlFromSuperblocksUrl(superblocksBaseUrl); // fetch files from bucketeer const branchPath = branch ? `/branches/${encodeURIComponent(branch)}` : ""; const componentFileURL = new URL( `${BASE_BUCKETEER_URL}/v1/components/${applicationId}${branchPath}?commit=${commitId}&viewMode=${viewMode}&multiPage=true`, bucketeerBaseUrl, ).toString(); try { const config: AxiosRequestConfig = { method: "get", url: componentFileURL, headers: { Authorization: "Bearer " + token, [SUPERBLOCKS_URL_HEADER]: superblocksBaseUrl, ...injectedHeaders, }, }; const bucketeerApp = (await axios(config)) .data as MultiPageApplicationWrapper & { componentFiles: any; }; if ( !isEqual( applicationWrapper.application.settings, bucketeerApp.application.settings, ) ) { throw new Error( "Application settings fetched from bucketeer do not match the settings fetched from the server", ); } return bucketeerApp; } catch (e) { if (axios.isAxiosError(e) && e.response?.status === 404) { throw new NotFoundError(`Application ${applicationId} was not found`); } throw new Error("Could not fetch application"); } } export async function fetchApplications( cliVersion: string, token: string, superblocksBaseUrl: string, injectedHeaders: Record = {}, ) { try { const config: AxiosRequestConfig = { method: "get", url: new URL( `${BASE_SERVER_PUBLIC_API_URL_V1}/applications`, superblocksBaseUrl, ).toString(), headers: { Authorization: "Bearer " + token, [CLI_VERSION_HEADER]: cliVersion, ...injectedHeaders, }, }; const response = await axios(config); return response.data.data.applications; } catch (e: any) { let message: string; if (e instanceof AxiosError) { message = (e.response?.data?.responseMeta?.message as string) ?? JSON.stringify(e.response?.data) ?? e.response?.statusText ?? e?.message; } else { message = `${e?.message ? e?.message : e}`; } throw new Error(`Could not fetch applications: ${message}`); } } export async function fetchApi( cliVersion: string, apiId: string, token: string, superblocksBaseUrl: string, viewMode: ViewMode, branch?: string, commitId?: string, skipSigningVerification = false, ) { try { const serverURL = branch ? new URL( `${BASE_SERVER_PUBLIC_API_URL_V1}/apis/${apiId}/branches/${encodeURIComponent( branch, )}`, superblocksBaseUrl, ) : new URL( `${BASE_SERVER_PUBLIC_API_URL_V1}/apis/${apiId}`, superblocksBaseUrl, ); serverURL.search = new URLSearchParams({ viewMode, ...(commitId ? { commitId } : {}), }).toString(); const socket = !skipSigningVerification ? await createSocketConnectionIfNeeded( cliVersion, token, superblocksBaseUrl, ) : undefined; if (socket) { try { const resp = await socket.call.v1.public.api.get({ apiId, viewMode, branchName: branch, }); return resp.data; } finally { socket.close(); } } else { const config: AxiosRequestConfig = { method: "get", url: new URL(serverURL, superblocksBaseUrl).toString(), headers: { Authorization: "Bearer " + token, [CLI_VERSION_HEADER]: cliVersion, }, }; const response = await axios(config); return response.data.data; } } catch (e) { if (axios.isAxiosError(e) && e.response?.status === 404) { throw new NotFoundError(`Api ${apiId} was not found`); } throw new Error( `Could not fetch api: ${ typeof e === "object" && e && "message" in e ? e.message : e }`, ); } } export async function fetchApis( cliVersion: string, token: string, superblocksBaseUrl: string, ) { try { const config: AxiosRequestConfig = { method: "get", url: new URL( `${BASE_SERVER_PUBLIC_API_URL_V1}/apis`, superblocksBaseUrl, ).toString(), headers: { Authorization: "Bearer " + token, [CLI_VERSION_HEADER]: cliVersion, }, }; const response = await axios(config); return response.data.data.apis; } catch { throw new Error("Could not fetch apis"); } } export async function validateGitSetup( cliVersion: string, resourceId: string, resourceType: SuperblocksResourceType, event: ComponentEvent, localGitRepoState: LocalGitRepoState, token: string, superblocksBaseUrl: string, injectedHeaders?: Record, ) { try { const requestBody: ValidateGitSetupRequestBody = { event, gitState: localGitRepoState, }; let path: string; switch (resourceType) { case ResourceType.APPLICATION: { path = `/application/${resourceId}/validate-git-setup`; break; } case ResourceType.BACKEND: { path = `/api/${resourceId}/validate-git-setup`; break; } default: unreachable(resourceType as never); } const config: AxiosRequestConfig = { method: "post", url: new URL( `${BASE_SERVER_PUBLIC_API_URL_V1}/${path}`, superblocksBaseUrl, ).toString(), headers: { Authorization: "Bearer " + token, [CLI_VERSION_HEADER]: cliVersion, ...injectedHeaders, }, data: requestBody, }; const response = await axios(config); const { validationResult } = response.data.data; if (validationResult.errors.length > 0) { throw new Error(validationResult.errors.join("\n")); } return { branchName: (validationResult.branchName ?? null) as string | null, }; } catch (e: any) { let message: string; if (e instanceof AxiosError) { message = (e.response?.data?.responseMeta?.message as string) ?? JSON.stringify(e.response?.data) ?? e.response?.statusText ?? e?.message; } else { message = `${e?.message ? e.message : e}`; } // TODO(@taha-au @gpoulios-sb) Replace this check with a more robust one that uses error codes once // the backend starts returning them as part of the response body if ( message.includes("Please initialize git locally") || message.includes("Superblocks does not have a reference to") || message.includes("Branching is not enabled") ) { throw new ValidateGitSetupError(message); } else { throw new Error(message); } } } export async function registerComponents( cliVersion: string, applicationId: string, componentConfigs: Record, token: string, superblocksBaseUrl: string, branch: string | null, injectedHeaders: Record, ) { try { const branchPath = branch ? `/branches/${encodeURIComponent(branch)}` : ""; const socket = await createSocketConnectionIfNeeded( cliVersion, token, superblocksBaseUrl, ); if (socket) { const resp = await socket.call.v1.public.application.component.register({ applicationId, branchName: branch || "", cliVersion: cliVersion, componentEvent: injectedHeaders[COMPONENT_EVENT_HEADER], components: componentConfigs, }); socket.close(); return resp.data; } else { const config: AxiosRequestConfig = { method: "put", url: new URL( `${BASE_SERVER_PUBLIC_API_URL_V1}/application/${applicationId}${branchPath}/components`, superblocksBaseUrl, ).toString(), headers: { Authorization: "Bearer " + token, [CLI_VERSION_HEADER]: cliVersion, ...injectedHeaders, }, data: { components: componentConfigs }, }; const response = await axios(config); return response.data; } } catch (e: any) { if (e instanceof AxiosError) { const message: string = (e.response?.data?.responseMeta?.message as string) ?? JSON.stringify(e.response?.data) ?? e.response?.statusText; throw new Error( `Could not register application components ${ message ? "\n" + message : "" }`, ); } throw new Error( `Could not register application components ${ e.message ? "\n" + (e as Error).message : "" }`, ); } } export async function uploadComponents({ cliVersion, applicationId, componentConfigs, files, token, superblocksBaseUrl, branch, }: { cliVersion: string; applicationId: string; componentConfigs: Record; files: string[]; token: string; superblocksBaseUrl: string; branch: string | null; }) { superblocksBaseUrl = superblocksBaseUrl.replace(/\/$/, ""); const bucketeerBaseUrl = getBucketeerUrlFromSuperblocksUrl(superblocksBaseUrl); // parse files to source and bundled files based on path const uploadFiles: UploadFile[] = files.map((file, ind) => { let contentType: string = getContentType(file); if (contentType.length === 0) { contentType = "application/octet-stream"; // default case when we don't know the encoding } return { name: `file${ind + 1}`, // win32 => posix filepath. Bucketeer does not accept windows style filepaths filename: file.replace(/\\/g, "/"), ContentType: contentType, } as UploadFile; }); const buildFiles: UploadFile[] = uploadFiles.filter((uploadFile) => uploadFile.filename.startsWith("dist/"), ); const srcFiles: UploadFile[] = uploadFiles.filter( (uploadFile) => !uploadFile.filename.startsWith("dist/"), ); const formatJSON = JSON.stringify({ applicationId, branch, registeredComponents: componentConfigs, srcFiles, buildFiles, }); const postURL = new URL( `${BASE_BUCKETEER_URL}/v2/components/upload`, bucketeerBaseUrl, ).toString(); let totalFileSizeMB = 0; const formData = new FormData(); formData.append("format", formatJSON); uploadFiles.forEach((uploadFile) => { const uploadFileStats = fs.statSync(uploadFile.filename); totalFileSizeMB += uploadFileStats.size / (1024 * 1024); formData.append(uploadFile.name, fs.createReadStream(uploadFile.filename)); }); if (totalFileSizeMB > SUPERBLOCKS_MAX_FILE_SIZE_MB) { throw new Error(`upload size exceeded maximum allowed size. Current Size: ${totalFileSizeMB.toFixed(2)} MB Max Allowed Size: ${SUPERBLOCKS_MAX_FILE_SIZE_MB.toFixed(0)} MB You can reduce your component bundle size by uploading static assets to a separate service.`); } const formHeaders = formData.getHeaders(); try { const config: AxiosRequestConfig = { headers: { Authorization: "Bearer " + token, [CLI_VERSION_HEADER]: cliVersion, [COMPONENT_EVENT_HEADER]: ComponentEvent.UPLOAD, [SUPERBLOCKS_URL_HEADER]: superblocksBaseUrl, ...formHeaders, }, }; const uploadResponse = await axios.post(postURL, formData, config); const initialSocket = await createSocketConnectionIfNeeded( cliVersion, token, superblocksBaseUrl, ); const socket = initialSocket ?? (await connectToISocketRPCServer({ agentUrl: undefined, superblocksBaseUrl, token, })); try { await socket.call.v1.public.application.component.update({ applicationId, branchName: branch ?? undefined, srcFiles: srcFiles.map((file) => file.filename), buildFiles: buildFiles.map((file) => file.filename), registeredComponents: componentConfigs, cliVersion, componentBaseUrl: uploadResponse.data.componentBaseUrl, signingRequired: !isEmpty(initialSocket), }); } finally { socket.close(); } } catch (e: any) { if (e instanceof AxiosError && e.response?.status === 413) { throw new Error( `Could not upload application components, file size limit exceeded`, ); } else { let message: string; if (e instanceof AxiosError) { message = (e.response?.data?.responseMeta?.message || e.response?.data || e.response?.statusText) as string; } else { message = e.message as string; } if (typeof message !== "string") { message = JSON.stringify(message, null, 2); } throw new Error(`Could not upload application components\n${message}`); } } } export async function fetchCurrentUser( cliVersion: string, token: string, superblocksBaseUrl: string, ): Promise { try { const config: AxiosRequestConfig = { method: "get", url: new URL( `${BASE_SERVER_PUBLIC_API_URL_V1}/users/me`, superblocksBaseUrl, ).toString(), headers: { Authorization: "Bearer " + token, [CLI_VERSION_HEADER]: cliVersion, [COMPONENT_EVENT_HEADER]: ComponentEvent.LOGIN, }, }; const response = await axios(config); return response.data.data as UserMeDto; } catch (e: any) { let message: string; if (e instanceof AxiosError) { message = (e.response?.data?.responseMeta?.message as string) ?? JSON.stringify(e.response?.data) ?? e.response?.statusText ?? e?.message; } else { message = `${e?.message ? e?.message : e}`; } throw new Error(`Could not fetch current user: ${message}`); } } export const createSocketConnectionIfNeeded = async ( cliVersion: string, token: string, superblocksBaseUrl: string, ): Promise => { const userMe = await fetchCurrentUser(cliVersion, token, superblocksBaseUrl); const organization = userMe.organizations[0]; if ( organization.agentType == AgentType.ONPREMISE && signingEnabled(userMe.flagBootstrap) ) { const profile = process.env.SUPERBLOCKS_PROFILE; const agentUrl = await getAgentUrl( userMe.agents, organization.agentType, profile, ); return await connectToISocketRPCServer({ agentUrl, superblocksBaseUrl, token, }); } }; export async function pushApplication({ cliVersion, applicationId, token, superblocksBaseUrl, applicationConfig, branch, injectedHeaders = {}, }: { cliVersion: string; applicationId: string; token: string; superblocksBaseUrl: string; branch: string; injectedHeaders: Record; applicationConfig: PushMultiPageApplicationWithCommitConfig; }): Promise { const handleHttpError = (status: number) => { if (status === 405) { throw new BranchNotCheckedOutError(`Branch ${branch} is not checked out`); } else if (status === 409) { throw new CommitAlreadyExistsError( `Commit ${applicationConfig.commitId} already exists`, ); } }; try { const socket = await createSocketConnectionIfNeeded( cliVersion, token, superblocksBaseUrl, ); if (socket) { const resp = await socket.call.v2.public.application.pushCommit({ applicationId, apis: applicationConfig.apis, application: applicationConfig.application, branchName: branch, commitId: applicationConfig.commitId, commitMessage: applicationConfig.commitMessage, pages: ( applicationConfig as unknown as PushMultiPageApplicationWithCommitConfig ).pages, gitState: applicationConfig.gitState, skipCommit: applicationConfig.skipCommit, }); socket.close(); handleHttpError(resp.responseMeta.status); if (resp.responseMeta.status !== 200) { // Get the raw error message from the server and throw it. The outer try-catch block will wrap it in a nicer error message const message: string = (resp?.responseMeta?.message as string) ?? JSON.stringify(resp?.data); throw new Error(message); } return resp.data; } else { const serverURL = new URL( `${BASE_SERVER_PUBLIC_API_URL_v2}/applications/${applicationId}/branches/${encodeURIComponent( branch, )}/push`, superblocksBaseUrl, ); const config: AxiosRequestConfig = { method: "post", url: serverURL.toString(), headers: { Authorization: "Bearer " + token, [CLI_VERSION_HEADER]: cliVersion, ...injectedHeaders, }, data: applicationConfig, }; try { const serverResponse = await axios>( config, ); return serverResponse?.data?.data; } catch (e) { if (!axios.isAxiosError(e)) { throw e; } handleHttpError( e.response?.data?.responseMeta?.status ?? e.response?.status, ); const message = (e.response?.data?.responseMeta?.message as string) ?? JSON.stringify(e.response?.data) ?? e.response?.statusText; throw new Error(message); } } } catch (e) { if ( e instanceof BranchNotCheckedOutError || e instanceof CommitAlreadyExistsError ) { // Rethrow these exceptions because they will be handled higher up the call stack throw e; } throw new Error( `Could not push application ${ (e as Error).message ? "\n" + (e as Error).message : "" }`, ); } } export async function pushApi({ cliVersion, apiId, token, superblocksBaseUrl, apiConfig, branch, injectedHeaders = {}, }: { cliVersion: string; apiId: string; token: string; superblocksBaseUrl: string; apiConfig: PushApiWithCommitConfig; branch: string; injectedHeaders: Record; }): Promise<{ commitId: string } | { updated: Date } | undefined> { const handleHttpError = (status: number) => { if (status === 405) { throw new BranchNotCheckedOutError(`Branch ${branch} is not checked out`); } else if (status === 409) { throw new CommitAlreadyExistsError( `Commit ${apiConfig.commitId} already exists`, ); } }; const serverURL = new URL( `${BASE_SERVER_PUBLIC_API_URL_V1}/apis/${apiId}/branches/${encodeURIComponent( branch, )}/push`, superblocksBaseUrl, ); try { const socket = await createSocketConnectionIfNeeded( cliVersion, token, superblocksBaseUrl, ); if (socket) { const resp = await socket.call.v1.public.api.pushCommit({ apiId, apiPb: apiConfig.apiPb, branchName: branch, commitId: apiConfig.commitId, commitMessage: apiConfig.commitMessage, gitState: apiConfig.gitState, skipCommit: apiConfig.skipCommit, }); socket.close(); handleHttpError(resp.responseMeta.status); if (resp.responseMeta.status !== 200) { // Get the raw error message from the server and throw it. The outer try-catch block will wrap it in a nicer error message const message: string = (resp?.responseMeta?.message as string) ?? JSON.stringify(resp?.data); throw new Error(message); } return resp.data; } else { const config: AxiosRequestConfig = { method: "post", url: serverURL.toString(), headers: { Authorization: "Bearer " + token, [CLI_VERSION_HEADER]: cliVersion, ...injectedHeaders, }, data: apiConfig, }; try { const serverResponse = await axios< ResponseWithMeta<{ commitId: string } | { updated: Date }> >(config); return serverResponse?.data?.data; } catch (e) { if (!axios.isAxiosError(e)) { throw e; } handleHttpError(e.response?.data?.responseMeta?.status); const message = (e.response?.data?.responseMeta?.message as string) ?? JSON.stringify(e.response?.data) ?? e.response?.statusText; throw new Error(message); } } } catch (e) { if ( e instanceof BranchNotCheckedOutError || e instanceof CommitAlreadyExistsError ) { // Rethrow these exceptions because they will be handled higher up the call stack throw e; } throw new Error( `Could not push api ${ (e as Error).message ? "\n" + (e as Error).message : "" }`, ); } } export async function fetchApplicationCommits({ cliVersion, applicationId, branch, token, superblocksBaseUrl, injectedHeaders = {}, limit, offset, }: { cliVersion: string; applicationId: string; branch?: string; token: string; superblocksBaseUrl: string; injectedHeaders: Record; limit?: number; offset?: number; }): Promise { try { const serverURL = branch ? new URL( `${BASE_SERVER_API_URL_V2}/applications/${applicationId}/branches/${encodeURIComponent( branch, )}/commits`, superblocksBaseUrl, ) : new URL( `${BASE_SERVER_API_URL_V2}/applications/${applicationId}/commits`, superblocksBaseUrl, ); serverURL.search = new URLSearchParams({ commitType: "commit", ...(limit ? { limit: limit.toString() } : {}), ...(offset ? { offset: offset.toString() } : {}), }).toString(); const config: AxiosRequestConfig = { method: "get", url: serverURL.toString(), headers: { Authorization: "Bearer " + token, [CLI_VERSION_HEADER]: cliVersion, ...injectedHeaders, }, }; const serverResponse = await axios>(config); return serverResponse?.data?.data; } catch (e) { if (axios.isAxiosError(e) && e.response?.status === 404) { throw new NotFoundError(`Application ${applicationId} was not found`); } throw new Error("Could not fetch application"); } } export async function fetchApiCommits({ cliVersion, applicationId, branch, token, superblocksBaseUrl, injectedHeaders = {}, limit, offset, }: { cliVersion: string; applicationId: string; branch?: string; token: string; superblocksBaseUrl: string; injectedHeaders: Record; limit?: number; offset?: number; }): Promise { try { const serverURL = branch ? new URL( `${BASE_SERVER_API_URL_V3}/apis/${applicationId}/branches/${encodeURIComponent( branch, )}/commits`, superblocksBaseUrl, ) : new URL( `${BASE_SERVER_API_URL_V3}/apis/${applicationId}/commits`, superblocksBaseUrl, ); serverURL.search = new URLSearchParams({ commitType: "commit", ...(limit ? { limit: limit.toString() } : {}), ...(offset ? { offset: offset.toString() } : {}), }).toString(); const config: AxiosRequestConfig = { method: "get", url: serverURL.toString(), headers: { Authorization: "Bearer " + token, [CLI_VERSION_HEADER]: cliVersion, ...injectedHeaders, }, }; const serverResponse = await axios>(config); return serverResponse?.data?.data; } catch (e) { if (axios.isAxiosError(e) && e.response?.status === 404) { throw new NotFoundError(`Application ${applicationId} was not found`); } throw new Error("Could not fetch application"); } } export async function deployApplication( cliVersion: string, applicationId: string, token: string, superblocksBaseUrl: string, commitId?: string, ): Promise { return deployResource( cliVersion, ResourceType.APPLICATION, applicationId, token, superblocksBaseUrl, commitId, ); } export async function deployApi( cliVersion: string, apiId: string, token: string, superblocksBaseUrl: string, commitId?: string, ): Promise { return deployResource( cliVersion, ResourceType.BACKEND, apiId, token, superblocksBaseUrl, commitId, ); } async function deployResource( cliVersion: string, resourceType: ResourceType, resourceId: string, token: string, superblocksBaseUrl: string, commitId?: string, ): Promise { let apiBaseUrl; try { if (resourceType === ResourceType.BACKEND) { apiBaseUrl = `${BASE_SERVER_PUBLIC_API_URL_V1}/api/${resourceId}/deploy`; } else if (resourceType === ResourceType.APPLICATION) { apiBaseUrl = `${BASE_SERVER_PUBLIC_API_URL_V1}/application/${resourceId}/deploy`; } else { throw new Error(`Unsupported resource type: ${resourceType}`); } const config: AxiosRequestConfig = { method: "post", url: new URL(apiBaseUrl, superblocksBaseUrl).toString(), headers: { Authorization: "Bearer " + token, [CLI_VERSION_HEADER]: cliVersion, }, data: { commitId: commitId ?? "HEAD", }, }; const serverResponse = await axios(config); return serverResponse?.data?.data; } catch (e: any) { let message: string; if (axios.isAxiosError(e)) { if (e.response?.status === 404) { throw new NotFoundError(`${e.response?.data?.responseMeta?.message}`); } else if (e.response?.status === 403) { throw new ForbiddenError(`${e.response?.data?.responseMeta?.message}`); } else if (e.response?.status === 400) { throw new BadRequestError(`${e.response?.data?.responseMeta?.message}`); } else { message = (e.response?.data?.responseMeta?.message as string) ?? JSON.stringify(e.response?.data) ?? e.response?.statusText; throw new Error(`${message}`); } } throw new Error(`${e.message}`); } }