/** * @file Coder is starting to import the Coder API file into more and more * external projects, as a "pseudo-SDK". We are not at a stage where we are * ready to commit to maintaining a public SDK, but we need equivalent * functionality in other places. * * Message somebody from Team Blueberry if you need more context, but so far, * these projects are importing the file: * * - The Coder VS Code extension * @see {@link https://github.com/coder/vscode-coder} * - The Coder Backstage plugin * @see {@link https://github.com/coder/backstage-plugins} * * It is important that this file not do any aliased imports, or else the other * consumers could break (particularly for platforms that limit how much you can * touch their configuration files, like Backstage). Relative imports are still * safe, though. * * For example, `utils/delay` must be imported using `../utils/delay` instead. */ import globalAxios, { type AxiosInstance, isAxiosError } from "axios"; import type dayjs from "dayjs"; import userAgentParser from "ua-parser-js"; import { OneWayWebSocket } from "../utils/OneWayWebSocket"; import { delay } from "../utils/delay"; import type { DynamicParametersRequest, PostWorkspaceUsageRequest, } from "./typesGenerated"; import * as TypesGen from "./typesGenerated"; const getMissingParameters = ( oldBuildParameters: TypesGen.WorkspaceBuildParameter[], newBuildParameters: TypesGen.WorkspaceBuildParameter[], templateParameters: TypesGen.TemplateVersionParameter[], ) => { const missingParameters: TypesGen.TemplateVersionParameter[] = []; const requiredParameters: TypesGen.TemplateVersionParameter[] = []; for (const p of templateParameters) { // It is mutable and required. Mutable values can be changed after so we // don't need to ask them if they are not required. const isMutableAndRequired = p.mutable && p.required; // Is immutable, so we can check if it is its first time on the build const isImmutable = !p.mutable; if (isMutableAndRequired || isImmutable) { requiredParameters.push(p); } } for (const parameter of requiredParameters) { // Check if there is a new value let buildParameter = newBuildParameters.find( (p) => p.name === parameter.name, ); // If not, get the old one if (!buildParameter) { buildParameter = oldBuildParameters.find( (p) => p.name === parameter.name, ); } // If there is a value from the new or old one, it is not missed if (buildParameter) { continue; } missingParameters.push(parameter); } // Check if parameter "options" changed and we can't use old build parameters. for (const templateParameter of templateParameters) { if (templateParameter.options.length === 0) { continue; } // For multi-select, extra steps are necessary to JSON parse the value. if (templateParameter.form_type === "multi-select") { continue; } let buildParameter = newBuildParameters.find( (p) => p.name === templateParameter.name, ); // If not, get the old one if (!buildParameter) { buildParameter = oldBuildParameters.find( (p) => p.name === templateParameter.name, ); } if (!buildParameter) { continue; } const matchingOption = templateParameter.options.find( (option) => option.value === buildParameter?.value, ); if (!matchingOption) { missingParameters.push(templateParameter); } } return missingParameters; }; /** * @param agentId * @returns {OneWayWebSocket} A OneWayWebSocket that emits Server-Sent Events. */ export const watchAgentMetadata = ( agentId: string, ): OneWayWebSocket => { return new OneWayWebSocket({ apiRoute: `/api/v2/workspaceagents/${agentId}/watch-metadata-ws`, }); }; /** * @returns {OneWayWebSocket} A OneWayWebSocket that emits Server-Sent Events. */ export const watchWorkspace = ( workspaceId: string, ): OneWayWebSocket => { return new OneWayWebSocket({ apiRoute: `/api/v2/workspaces/${workspaceId}/watch-ws`, }); }; type WatchInboxNotificationsParams = Readonly<{ read_status?: "read" | "unread" | "all"; }>; export function watchInboxNotifications( params?: WatchInboxNotificationsParams, ): OneWayWebSocket { return new OneWayWebSocket({ apiRoute: "/api/v2/notifications/inbox/watch", searchParams: params, }); } export const getURLWithSearchParams = ( basePath: string, options?: SearchParamOptions, ): string => { if (!options) { return basePath; } const searchParams = new URLSearchParams(); for (const [key, value] of Object.entries(options)) { if (value !== undefined && value !== "") { searchParams.append(key, value.toString()); } } const searchString = searchParams.toString(); return searchString ? `${basePath}?${searchString}` : basePath; }; // withDefaultFeatures sets all unspecified features to not_entitled and // disabled. export const withDefaultFeatures = ( fs: Partial, ): TypesGen.Entitlements["features"] => { for (const feature of TypesGen.FeatureNames) { // Skip fields that are already filled. if (fs[feature] !== undefined) { continue; } fs[feature] = { enabled: false, entitlement: "not_entitled", }; } return fs as TypesGen.Entitlements["features"]; }; type WatchBuildLogsByTemplateVersionIdOptions = { after?: number; onMessage: (log: TypesGen.ProvisionerJobLog) => void; onDone?: () => void; onError: (error: Error) => void; }; export const watchBuildLogsByTemplateVersionId = ( versionId: string, { onMessage, onDone, onError, after, }: WatchBuildLogsByTemplateVersionIdOptions, ) => { const searchParams = new URLSearchParams({ follow: "true" }); if (after !== undefined) { searchParams.append("after", after.toString()); } const socket = createWebSocket( `/api/v2/templateversions/${versionId}/logs`, searchParams, ); socket.addEventListener("message", (event) => onMessage(JSON.parse(event.data) as TypesGen.ProvisionerJobLog), ); socket.addEventListener("error", () => { onError(new Error("Connection for logs failed.")); socket.close(); }); socket.addEventListener("close", () => { // When the socket closes, logs have finished streaming! onDone?.(); }); return socket; }; export const watchWorkspaceAgentLogs = ( agentId: string, params?: WatchWorkspaceAgentLogsParams, ) => { const searchParams = new URLSearchParams({ follow: "true", after: params?.after?.toString() ?? "", }); /** * WebSocket compression in Safari (confirmed in 16.5) is broken when * the server sends large messages. The following error is seen: * WebSocket connection to 'wss://...' failed: The operation couldn't be completed. */ if (userAgentParser(navigator.userAgent).browser.name === "Safari") { searchParams.set("no_compression", ""); } return new OneWayWebSocket({ apiRoute: `/api/v2/workspaceagents/${agentId}/logs`, searchParams, }); }; type WatchWorkspaceAgentLogsParams = { after?: number; }; type WatchBuildLogsByBuildIdOptions = { after?: number; onMessage: (log: TypesGen.ProvisionerJobLog) => void; onDone?: () => void; onError?: (error: Error) => void; }; export const watchBuildLogsByBuildId = ( buildId: string, { onMessage, onDone, onError, after }: WatchBuildLogsByBuildIdOptions, ) => { const searchParams = new URLSearchParams({ follow: "true" }); if (after !== undefined) { searchParams.append("after", after.toString()); } const socket = createWebSocket( `/api/v2/workspacebuilds/${buildId}/logs`, searchParams, ); socket.addEventListener("message", (event) => onMessage(JSON.parse(event.data) as TypesGen.ProvisionerJobLog), ); socket.addEventListener("error", () => { if (socket.readyState === socket.CLOSED) { return; } onError?.(new Error("Connection for logs failed.")); socket.close(); }); socket.addEventListener("close", () => { // When the socket closes, logs have finished streaming! onDone?.(); }); return socket; }; // This is the base header that is used for several requests. This is defined as // a readonly value, but only copies of it should be passed into the API calls, // because Axios is able to mutate the headers const BASE_CONTENT_TYPE_JSON = { "Content-Type": "application/json", } as const satisfies HeadersInit; export type GetTemplatesOptions = Readonly<{ readonly deprecated?: boolean; }>; export type GetTemplatesQuery = Readonly<{ readonly q: string; }>; function normalizeGetTemplatesOptions( options: GetTemplatesOptions | GetTemplatesQuery = {}, ): Record { if ("q" in options) { return options; } const params: Record = {}; if (options.deprecated !== undefined) { params.deprecated = String(options.deprecated); } return params; } type SearchParamOptions = TypesGen.Pagination & { q?: string; }; type RestartWorkspaceParameters = Readonly<{ workspace: TypesGen.Workspace; buildParameters?: TypesGen.WorkspaceBuildParameter[]; }>; export type DeleteWorkspaceOptions = Pick< TypesGen.CreateWorkspaceBuildRequest, "log_level" | "orphan" >; export type DeploymentConfig = Readonly<{ config: TypesGen.DeploymentValues; options: TypesGen.SerpentOption[]; }>; type Claims = { license_expires: number; account_type?: string; account_id?: string; trial: boolean; all_features: boolean; // feature_set is omitted on legacy licenses feature_set?: string; version: number; features: Record; require_telemetry?: boolean; }; export type GetLicensesResponse = Omit & { claims: Claims; expires_at: string; }; export type InsightsParams = { start_time: string; end_time: string; template_ids: string; }; export type InsightsTemplateParams = InsightsParams & { interval: "day" | "week"; }; export class MissingBuildParameters extends Error { parameters: TypesGen.TemplateVersionParameter[] = []; versionId: string; constructor( parameters: TypesGen.TemplateVersionParameter[], versionId: string, ) { super("Missing build parameters."); this.parameters = parameters; this.versionId = versionId; } } export type GetProvisionerJobsParams = { status?: string; limit?: number; // IDs separated by comma ids?: string; }; export type GetProvisionerDaemonsParams = { // IDs separated by comma ids?: string; // Stringified JSON Object tags?: string; limit?: number; }; /** * This is the container for all API methods. It's split off to make it more * clear where API methods should go, but it is eventually merged into the Api * class with a more flat hierarchy * * All public methods should be defined as arrow functions to ensure that they * can be passed around the React UI without losing their `this` context. * * This is one of the few cases where you have to worry about the difference * between traditional methods and arrow function properties. Arrow functions * disable JS's dynamic scope, and force all `this` references to resolve via * lexical scope. */ class ApiMethods { constructor(protected readonly axios: AxiosInstance) {} login = async ( email: string, password: string, ): Promise => { const payload = JSON.stringify({ email, password }); const response = await this.axios.post( "/api/v2/users/login", payload, { headers: { ...BASE_CONTENT_TYPE_JSON } }, ); return response.data; }; convertToOAUTH = async (request: TypesGen.ConvertLoginRequest) => { const response = await this.axios.post( "/api/v2/users/me/convert-login", request, ); return response.data; }; logout = async (): Promise => { return this.axios.post("/api/v2/users/logout"); }; getAuthenticatedUser = async () => { const response = await this.axios.get("/api/v2/users/me"); return response.data; }; getUserParameters = async (templateID: string) => { const response = await this.axios.get( `/api/v2/users/me/autofill-parameters?template_id=${templateID}`, ); return response.data; }; getAuthMethods = async (): Promise => { const response = await this.axios.get( "/api/v2/users/authmethods", ); return response.data; }; getUserLoginType = async (): Promise => { const response = await this.axios.get( "/api/v2/users/me/login-type", ); return response.data; }; checkAuthorization = async ( params: TypesGen.AuthorizationRequest, ) => { const response = await this.axios.post( "/api/v2/authcheck", params, ); return response.data; }; getApiKey = async (): Promise => { const response = await this.axios.post( "/api/v2/users/me/keys", ); return response.data; }; getTokens = async ( params: TypesGen.TokensFilter, ): Promise => { const response = await this.axios.get( "/api/v2/users/me/keys/tokens", { params }, ); return response.data; }; deleteToken = async (keyId: string): Promise => { await this.axios.delete(`/api/v2/users/me/keys/${keyId}`); }; createToken = async ( params: TypesGen.CreateTokenRequest, ): Promise => { const response = await this.axios.post( "/api/v2/users/me/keys/tokens", params, ); return response.data; }; getTokenConfig = async (): Promise => { const response = await this.axios.get( "/api/v2/users/me/keys/tokens/tokenconfig", ); return response.data; }; getUsers = async ( options: TypesGen.UsersRequest, signal?: AbortSignal, ): Promise => { const url = getURLWithSearchParams("/api/v2/users", options); const response = await this.axios.get( url.toString(), { signal }, ); return response.data; }; createOrganization = async (params: TypesGen.CreateOrganizationRequest) => { const response = await this.axios.post( "/api/v2/organizations", params, ); return response.data; }; /** * @param organization Can be the organization's ID or name */ updateOrganization = async ( organization: string, params: TypesGen.UpdateOrganizationRequest, ) => { const response = await this.axios.patch( `/api/v2/organizations/${organization}`, params, ); return response.data; }; /** * @param organization Can be the organization's ID or name */ deleteOrganization = async (organization: string) => { await this.axios.delete( `/api/v2/organizations/${organization}`, ); }; /** * @param organization Can be the organization's ID or name */ getOrganization = async ( organization: string, ): Promise => { const response = await this.axios.get( `/api/v2/organizations/${organization}`, ); return response.data; }; /** * @param organization Can be the organization's ID or name */ getOrganizationMembers = async (organization: string) => { const response = await this.axios.get< TypesGen.OrganizationMemberWithUserData[] >(`/api/v2/organizations/${organization}/members`); return response.data; }; /** * @param organization Can be the organization's ID or name * @param options Pagination options */ getOrganizationPaginatedMembers = async ( organization: string, options?: TypesGen.Pagination, ) => { const url = getURLWithSearchParams( `/api/v2/organizations/${organization}/paginated-members`, options, ); const response = await this.axios.get(url); return response.data; }; /** * @param organization Can be the organization's ID or name */ getOrganizationRoles = async (organization: string) => { const response = await this.axios.get( `/api/v2/organizations/${organization}/members/roles`, ); return response.data; }; /** * @param organization Can be the organization's ID or name */ updateOrganizationMemberRoles = async ( organization: string, userId: string, roles: TypesGen.SlimRole["name"][], ): Promise => { const response = await this.axios.put( `/api/v2/organizations/${organization}/members/${userId}/roles`, { roles }, ); return response.data; }; /** * @param organization Can be the organization's ID or name */ createOrganizationRole = async ( organization: string, role: TypesGen.Role, ): Promise => { const response = await this.axios.post( `/api/v2/organizations/${organization}/members/roles`, role, ); return response.data; }; /** * @param organization Can be the organization's ID or name */ updateOrganizationRole = async ( organization: string, role: TypesGen.Role, ): Promise => { const response = await this.axios.put( `/api/v2/organizations/${organization}/members/roles`, role, ); return response.data; }; /** * @param organization Can be the organization's ID or name */ deleteOrganizationRole = async (organization: string, roleName: string) => { await this.axios.delete( `/api/v2/organizations/${organization}/members/roles/${roleName}`, ); }; /** * @param organization Can be the organization's ID or name */ addOrganizationMember = async (organization: string, userId: string) => { const response = await this.axios.post( `/api/v2/organizations/${organization}/members/${userId}`, ); return response.data; }; /** * @param organization Can be the organization's ID or name */ removeOrganizationMember = async (organization: string, userId: string) => { await this.axios.delete( `/api/v2/organizations/${organization}/members/${userId}`, ); }; getOrganizations = async (): Promise => { const response = await this.axios.get( "/api/v2/organizations", ); return response.data; }; getMyOrganizations = async (): Promise => { const response = await this.axios.get( "/api/v2/users/me/organizations", ); return response.data; }; getProvisionerDaemonsByOrganization = async ( organization: string, params?: GetProvisionerDaemonsParams, ): Promise => { const response = await this.axios.get( `/api/v2/organizations/${organization}/provisionerdaemons`, { params }, ); return response.data; }; /** * @param organization Can be the organization's ID or name */ getProvisionerDaemonGroupsByOrganization = async ( organization: string, ): Promise => { const response = await this.axios.get( `/api/v2/organizations/${organization}/provisionerkeys/daemons`, ); return response.data; }; getOrganizationIdpSyncSettings = async (): Promise => { const response = await this.axios.get( "/api/v2/settings/idpsync/organization", ); return response.data; }; patchOrganizationIdpSyncSettings = async ( data: TypesGen.OrganizationSyncSettings, ) => { const response = await this.axios.patch( "/api/v2/settings/idpsync/organization", data, ); return response.data; }; /** * @param data * @param organization Can be the organization's ID or name */ patchGroupIdpSyncSettings = async ( data: TypesGen.GroupSyncSettings, organization: string, ) => { const response = await this.axios.patch( `/api/v2/organizations/${organization}/settings/idpsync/groups`, data, ); return response.data; }; /** * @param data * @param organization Can be the organization's ID or name */ patchRoleIdpSyncSettings = async ( data: TypesGen.RoleSyncSettings, organization: string, ) => { const response = await this.axios.patch( `/api/v2/organizations/${organization}/settings/idpsync/roles`, data, ); return response.data; }; /** * @param organization Can be the organization's ID or name */ getGroupIdpSyncSettingsByOrganization = async ( organization: string, ): Promise => { const response = await this.axios.get( `/api/v2/organizations/${organization}/settings/idpsync/groups`, ); return response.data; }; /** * @param organization Can be the organization's ID or name */ getRoleIdpSyncSettingsByOrganization = async ( organization: string, ): Promise => { const response = await this.axios.get( `/api/v2/organizations/${organization}/settings/idpsync/roles`, ); return response.data; }; getDeploymentIdpSyncFieldValues = async ( field: string, ): Promise => { const params = new URLSearchParams(); params.set("claimField", field); const response = await this.axios.get( `/api/v2/settings/idpsync/field-values?${params}`, ); return response.data; }; getDeploymentLLMs = async (): Promise => { const response = await this.axios.get( "/api/v2/deployment/llms", ); return response.data; }; getOrganizationIdpSyncClaimFieldValues = async ( organization: string, field: string, ) => { const params = new URLSearchParams(); params.set("claimField", field); const response = await this.axios.get( `/api/v2/organizations/${organization}/settings/idpsync/field-values?${params}`, ); return response.data; }; getTemplate = async (templateId: string): Promise => { const response = await this.axios.get( `/api/v2/templates/${templateId}`, ); return response.data; }; getTemplates = async ( options?: GetTemplatesOptions | GetTemplatesQuery, ): Promise => { const params = normalizeGetTemplatesOptions(options); const response = await this.axios.get( "/api/v2/templates", { params }, ); return response.data; }; /** * @param organization Can be the organization's ID or name */ getTemplatesByOrganization = async ( organization: string, options?: GetTemplatesOptions, ): Promise => { const params = normalizeGetTemplatesOptions(options); const response = await this.axios.get( `/api/v2/organizations/${organization}/templates`, { params }, ); return response.data; }; /** * @param organization Can be the organization's ID or name */ getTemplateByName = async ( organization: string, name: string, ): Promise => { const response = await this.axios.get( `/api/v2/organizations/${organization}/templates/${name}`, ); return response.data; }; getTemplateVersion = async ( versionId: string, ): Promise => { const response = await this.axios.get( `/api/v2/templateversions/${versionId}`, ); return response.data; }; getTemplateVersionResources = async ( versionId: string, ): Promise => { const response = await this.axios.get( `/api/v2/templateversions/${versionId}/resources`, ); return response.data; }; getTemplateVersionVariables = async ( versionId: string, ): Promise => { // Defined as separate variable to avoid wonky Prettier formatting because // the type definition is so long type VerArray = TypesGen.TemplateVersionVariable[]; const response = await this.axios.get( `/api/v2/templateversions/${versionId}/variables`, ); return response.data; }; getTemplateVersions = async ( templateId: string, ): Promise => { const response = await this.axios.get( `/api/v2/templates/${templateId}/versions`, ); return response.data; }; /** * @param organization Can be the organization's ID or name */ getTemplateVersionByName = async ( organization: string, templateName: string, versionName: string, ): Promise => { const response = await this.axios.get( `/api/v2/organizations/${organization}/templates/${templateName}/versions/${versionName}`, ); return response.data; }; /** * @param organization Can be the organization's ID or name */ getPreviousTemplateVersionByName = async ( organization: string, templateName: string, versionName: string, ) => { try { const response = await this.axios.get( `/api/v2/organizations/${organization}/templates/${templateName}/versions/${versionName}/previous`, ); return response.data; } catch (error) { // When there is no previous version, like the first version of a // template, the API returns 404 so in this case we can safely return // undefined const is404 = isAxiosError(error) && error.response && error.response.status === 404; if (is404) { return undefined; } throw error; } }; /** * @param organization Can be the organization's ID or name */ createTemplateVersion = async ( organization: string, data: TypesGen.CreateTemplateVersionRequest, ): Promise => { const response = await this.axios.post( `/api/v2/organizations/${organization}/templateversions`, data, ); return response.data; }; getTemplateVersionExternalAuth = async ( versionId: string, ): Promise => { const response = await this.axios.get( `/api/v2/templateversions/${versionId}/external-auth`, ); return response.data; }; getTemplateVersionDynamicParameters = async ( versionId: string, data: TypesGen.DynamicParametersRequest, ): Promise => { const response = await this.axios.post( `/api/v2/templateversions/${versionId}/dynamic-parameters/evaluate`, data, ); return response.data; }; getTemplateVersionRichParameters = async ( versionId: string, ): Promise => { const response = await this.axios.get( `/api/v2/templateversions/${versionId}/rich-parameters`, ); return response.data; }; templateVersionDynamicParameters = ( versionId: string, { onMessage, onError, onClose, }: { onMessage: (response: TypesGen.DynamicParametersResponse) => void; onError: (error: Error) => void; onClose: () => void; }, ): WebSocket => { const socket = createWebSocket( `/api/v2/templateversions/${versionId}/dynamic-parameters`, ); socket.addEventListener("message", (event) => onMessage(JSON.parse(event.data) as TypesGen.DynamicParametersResponse), ); socket.addEventListener("error", () => { onError(new Error("Connection for dynamic parameters failed.")); socket.close(); }); socket.addEventListener("close", () => { onClose(); }); return socket; }; /** * @param organization Can be the organization's ID or name */ createTemplate = async ( organization: string, data: TypesGen.CreateTemplateRequest, ): Promise => { const response = await this.axios.post( `/api/v2/organizations/${organization}/templates`, data, ); return response.data; }; updateActiveTemplateVersion = async ( templateId: string, data: TypesGen.UpdateActiveTemplateVersion, ) => { const response = await this.axios.patch( `/api/v2/templates/${templateId}/versions`, data, ); return response.data; }; patchTemplateVersion = async ( templateVersionId: string, data: TypesGen.PatchTemplateVersionRequest, ) => { const response = await this.axios.patch( `/api/v2/templateversions/${templateVersionId}`, data, ); return response.data; }; archiveTemplateVersion = async (templateVersionId: string) => { const response = await this.axios.post( `/api/v2/templateversions/${templateVersionId}/archive`, ); return response.data; }; unarchiveTemplateVersion = async (templateVersionId: string) => { const response = await this.axios.post( `/api/v2/templateversions/${templateVersionId}/unarchive`, ); return response.data; }; /** * Downloads a template version as a tar or zip archive * @param fileId The file ID from the template version's job * @param format Optional format: "zip" for zip archive, empty/undefined for tar * @returns Promise that resolves to a Blob containing the archive */ downloadTemplateVersion = async ( fileId: string, format?: "zip", ): Promise => { const params = new URLSearchParams(); if (format) { params.set("format", format); } const response = await this.axios.get( `/api/v2/files/${fileId}?${params.toString()}`, { responseType: "blob", }, ); return response.data; }; updateTemplateMeta = async ( templateId: string, data: TypesGen.UpdateTemplateMeta, ): Promise => { const response = await this.axios.patch( `/api/v2/templates/${templateId}`, data, ); // On 304 response there is no data payload. if (response.status === 304) { return null; } return response.data; }; deleteTemplate = async (templateId: string): Promise => { const response = await this.axios.delete( `/api/v2/templates/${templateId}`, ); return response.data; }; getWorkspace = async ( workspaceId: string, params?: TypesGen.WorkspaceOptions, ): Promise => { const response = await this.axios.get( `/api/v2/workspaces/${workspaceId}`, { params }, ); return response.data; }; getWorkspaces = async ( options: TypesGen.WorkspacesRequest, ): Promise => { const url = getURLWithSearchParams("/api/v2/workspaces", options); const response = await this.axios.get(url); return response.data; }; getWorkspaceByOwnerAndName = async ( username: string, workspaceName: string, params?: TypesGen.WorkspaceOptions, ): Promise => { const response = await this.axios.get( `/api/v2/users/${username}/workspace/${workspaceName}`, { params }, ); return response.data; }; getWorkspaceBuildByNumber = async ( username: string, workspaceName: string, buildNumber: number, ): Promise => { const response = await this.axios.get( `/api/v2/users/${username}/workspace/${workspaceName}/builds/${buildNumber}`, ); return response.data; }; waitForBuild = (build: TypesGen.WorkspaceBuild) => { return new Promise((res, reject) => { void (async () => { let latestJobInfo: TypesGen.ProvisionerJob | undefined = undefined; while ( !["succeeded", "canceled"].some((status) => latestJobInfo?.status.includes(status), ) ) { const { job } = await this.getWorkspaceBuildByNumber( build.workspace_owner_name, build.workspace_name, build.build_number, ); latestJobInfo = job; if (latestJobInfo.status === "failed") { return reject(latestJobInfo); } await delay(1000); } return res(latestJobInfo); })(); }); }; postWorkspaceBuild = async ( workspaceId: string, data: TypesGen.CreateWorkspaceBuildRequest, ): Promise => { const response = await this.axios.post( `/api/v2/workspaces/${workspaceId}/builds`, data, ); return response.data; }; getTemplateVersionPresets = async ( templateVersionId: string, ): Promise => { const response = await this.axios.get( `/api/v2/templateversions/${templateVersionId}/presets`, ); return response.data; }; startWorkspace = ( workspaceId: string, templateVersionId: string, logLevel?: TypesGen.ProvisionerLogLevel, buildParameters?: TypesGen.WorkspaceBuildParameter[], ) => { return this.postWorkspaceBuild(workspaceId, { transition: "start", template_version_id: templateVersionId, log_level: logLevel, rich_parameter_values: buildParameters, }); }; stopWorkspace = ( workspaceId: string, logLevel?: TypesGen.ProvisionerLogLevel, ) => { return this.postWorkspaceBuild(workspaceId, { transition: "stop", log_level: logLevel, }); }; deleteWorkspace = (workspaceId: string, options?: DeleteWorkspaceOptions) => { return this.postWorkspaceBuild(workspaceId, { transition: "delete", ...options, }); }; cancelWorkspaceBuild = async ( workspaceBuildId: TypesGen.WorkspaceBuild["id"], ): Promise => { const response = await this.axios.patch( `/api/v2/workspacebuilds/${workspaceBuildId}/cancel`, ); return response.data; }; updateWorkspaceDormancy = async ( workspaceId: string, dormant: boolean, ): Promise => { const data: TypesGen.UpdateWorkspaceDormancy = { dormant }; const response = await this.axios.put( `/api/v2/workspaces/${workspaceId}/dormant`, data, ); return response.data; }; updateWorkspaceAutomaticUpdates = async ( workspaceId: string, automaticUpdates: TypesGen.AutomaticUpdates, ): Promise => { const req: TypesGen.UpdateWorkspaceAutomaticUpdatesRequest = { automatic_updates: automaticUpdates, }; const response = await this.axios.put( `/api/v2/workspaces/${workspaceId}/autoupdates`, req, ); return response.data; }; restartWorkspace = async ({ workspace, buildParameters, }: RestartWorkspaceParameters): Promise => { const stopBuild = await this.stopWorkspace(workspace.id); const awaitedStopBuild = await this.waitForBuild(stopBuild); // If the restart is canceled halfway through, make sure we bail if (awaitedStopBuild?.status === "canceled") { return; } const startBuild = await this.startWorkspace( workspace.id, workspace.latest_build.template_version_id, undefined, buildParameters, ); await this.waitForBuild(startBuild); }; cancelTemplateVersionBuild = async ( templateVersionId: string, ): Promise => { const response = await this.axios.patch( `/api/v2/templateversions/${templateVersionId}/cancel`, ); return response.data; }; cancelTemplateVersionDryRun = async ( templateVersionId: string, jobId: string, ): Promise => { const response = await this.axios.patch( `/api/v2/templateversions/${templateVersionId}/dry-run/${jobId}/cancel`, ); return response.data; }; createUser = async ( user: TypesGen.CreateUserRequestWithOrgs, ): Promise => { const response = await this.axios.post( "/api/v2/users", user, ); return response.data; }; createWorkspace = async ( userId: string, workspace: TypesGen.CreateWorkspaceRequest, ): Promise => { const response = await this.axios.post( `/api/v2/users/${userId}/workspaces`, workspace, ); return response.data; }; patchWorkspace = async ( workspaceId: string, data: TypesGen.UpdateWorkspaceRequest, ): Promise => { await this.axios.patch(`/api/v2/workspaces/${workspaceId}`, data); }; getBuildInfo = async (): Promise => { const response = await this.axios.get("/api/v2/buildinfo"); return response.data; }; getUpdateCheck = async (): Promise => { const response = await this.axios.get("/api/v2/updatecheck"); return response.data; }; putWorkspaceAutostart = async ( workspaceID: string, autostart: TypesGen.UpdateWorkspaceAutostartRequest, ): Promise => { const payload = JSON.stringify(autostart); await this.axios.put( `/api/v2/workspaces/${workspaceID}/autostart`, payload, { headers: { ...BASE_CONTENT_TYPE_JSON } }, ); }; putWorkspaceAutostop = async ( workspaceID: string, ttl: TypesGen.UpdateWorkspaceTTLRequest, ): Promise => { const payload = JSON.stringify(ttl); await this.axios.put(`/api/v2/workspaces/${workspaceID}/ttl`, payload, { headers: { ...BASE_CONTENT_TYPE_JSON }, }); }; updateProfile = async ( userId: string, data: TypesGen.UpdateUserProfileRequest, ): Promise => { const response = await this.axios.put( `/api/v2/users/${userId}/profile`, data, ); return response.data; }; getAppearanceSettings = async (): Promise => { const response = await this.axios.get("/api/v2/users/me/appearance"); return response.data; }; updateAppearanceSettings = async ( data: TypesGen.UpdateUserAppearanceSettingsRequest, ): Promise => { const response = await this.axios.put("/api/v2/users/me/appearance", data); return response.data; }; getUserQuietHoursSchedule = async ( userId: TypesGen.User["id"], ): Promise => { const response = await this.axios.get( `/api/v2/users/${userId}/quiet-hours`, ); return response.data; }; updateUserQuietHoursSchedule = async ( userId: TypesGen.User["id"], data: TypesGen.UpdateUserQuietHoursScheduleRequest, ): Promise => { const response = await this.axios.put( `/api/v2/users/${userId}/quiet-hours`, data, ); return response.data; }; activateUser = async ( userId: TypesGen.User["id"], ): Promise => { const response = await this.axios.put( `/api/v2/users/${userId}/status/activate`, ); return response.data; }; suspendUser = async (userId: TypesGen.User["id"]): Promise => { const response = await this.axios.put( `/api/v2/users/${userId}/status/suspend`, ); return response.data; }; deleteUser = async (userId: TypesGen.User["id"]): Promise => { await this.axios.delete(`/api/v2/users/${userId}`); }; // API definition: // https://github.com/coder/coder/blob/db665e7261f3c24a272ccec48233a3e276878239/coderd/users.go#L33-L53 hasFirstUser = async (): Promise => { try { // If it is success, it is true await this.axios.get("/api/v2/users/first"); return true; } catch (error) { // If it returns a 404, it is false if (isAxiosError(error) && error.response?.status === 404) { return false; } throw error; } }; createFirstUser = async ( req: TypesGen.CreateFirstUserRequest, ): Promise => { const response = await this.axios.post("/api/v2/users/first", req); return response.data; }; updateUserPassword = async ( userId: TypesGen.User["id"], updatePassword: TypesGen.UpdateUserPasswordRequest, ): Promise => { await this.axios.put(`/api/v2/users/${userId}/password`, updatePassword); }; validateUserPassword = async ( password: string, ): Promise => { const response = await this.axios.post("/api/v2/users/validate-password", { password, }); return response.data; }; getRoles = async (): Promise> => { const response = await this.axios.get( "/api/v2/users/roles", ); return response.data; }; updateUserRoles = async ( roles: TypesGen.SlimRole["name"][], userId: TypesGen.User["id"], ): Promise => { const response = await this.axios.put( `/api/v2/users/${userId}/roles`, { roles }, ); return response.data; }; getUserSSHKey = async (userId = "me"): Promise => { const response = await this.axios.get( `/api/v2/users/${userId}/gitsshkey`, ); return response.data; }; regenerateUserSSHKey = async (userId = "me"): Promise => { const response = await this.axios.put( `/api/v2/users/${userId}/gitsshkey`, ); return response.data; }; getWorkspaceBuilds = async ( workspaceId: string, req?: TypesGen.WorkspaceBuildsRequest, ) => { const response = await this.axios.get( getURLWithSearchParams(`/api/v2/workspaces/${workspaceId}/builds`, req), ); return response.data; }; getWorkspaceBuildLogs = async ( buildId: string, ): Promise => { const response = await this.axios.get( `/api/v2/workspacebuilds/${buildId}/logs`, ); return response.data; }; getWorkspaceAgentLogs = async ( agentID: string, ): Promise => { const response = await this.axios.get( `/api/v2/workspaceagents/${agentID}/logs`, ); return response.data; }; putWorkspaceExtension = async ( workspaceId: string, newDeadline: dayjs.Dayjs, ): Promise => { await this.axios.put(`/api/v2/workspaces/${workspaceId}/extend`, { deadline: newDeadline, }); }; refreshEntitlements = async (): Promise => { await this.axios.post("/api/v2/licenses/refresh-entitlements"); }; getEntitlements = async (): Promise => { try { const response = await this.axios.get( "/api/v2/entitlements", ); return response.data; } catch (ex) { if (isAxiosError(ex) && ex.response?.status === 404) { return { errors: [], features: withDefaultFeatures({}), has_license: false, require_telemetry: false, trial: false, warnings: [], refreshed_at: "", }; } throw ex; } }; getExperiments = async (): Promise => { try { const response = await this.axios.get( "/api/v2/experiments", ); return response.data; } catch (error) { if (isAxiosError(error) && error.response?.status === 404) { return []; } throw error; } }; getAvailableExperiments = async (): Promise => { try { const response = await this.axios.get("/api/v2/experiments/available"); return response.data; } catch (error) { if (isAxiosError(error) && error.response?.status === 404) { return { safe: [] }; } throw error; } }; getExternalAuthProvider = async ( provider: string, ): Promise => { const res = await this.axios.get(`/api/v2/external-auth/${provider}`); return res.data; }; getExternalAuthDevice = async ( provider: string, ): Promise => { const resp = await this.axios.get( `/api/v2/external-auth/${provider}/device`, ); return resp.data; }; exchangeExternalAuthDevice = async ( provider: string, req: TypesGen.ExternalAuthDeviceExchange, ): Promise => { const resp = await this.axios.post( `/api/v2/external-auth/${provider}/device`, req, ); return resp.data; }; getUserExternalAuthProviders = async (): Promise => { const resp = await this.axios.get("/api/v2/external-auth"); return resp.data; }; unlinkExternalAuthProvider = async (provider: string): Promise => { const resp = await this.axios.delete(`/api/v2/external-auth/${provider}`); return resp.data; }; getOAuth2GitHubDeviceFlowCallback = async ( code: string, state: string, ): Promise => { const resp = await this.axios.get( `/api/v2/users/oauth2/github/callback?code=${code}&state=${state}`, ); // sanity check if ( typeof resp.data !== "object" || typeof resp.data.redirect_url !== "string" ) { console.error("Invalid response from OAuth2 GitHub callback", resp); throw new Error("Invalid response from OAuth2 GitHub callback"); } return resp.data; }; getOAuth2GitHubDevice = async (): Promise => { const resp = await this.axios.get("/api/v2/users/oauth2/github/device"); return resp.data; }; getOAuth2ProviderApps = async ( filter?: TypesGen.OAuth2ProviderAppFilter, ): Promise => { const params = filter?.user_id ? new URLSearchParams({ user_id: filter.user_id }).toString() : ""; const resp = await this.axios.get(`/api/v2/oauth2-provider/apps?${params}`); return resp.data; }; getOAuth2ProviderApp = async ( id: string, ): Promise => { const resp = await this.axios.get(`/api/v2/oauth2-provider/apps/${id}`); return resp.data; }; postOAuth2ProviderApp = async ( data: TypesGen.PostOAuth2ProviderAppRequest, ): Promise => { const response = await this.axios.post( "/api/v2/oauth2-provider/apps", data, ); return response.data; }; putOAuth2ProviderApp = async ( id: string, data: TypesGen.PutOAuth2ProviderAppRequest, ): Promise => { const response = await this.axios.put( `/api/v2/oauth2-provider/apps/${id}`, data, ); return response.data; }; deleteOAuth2ProviderApp = async (id: string): Promise => { await this.axios.delete(`/api/v2/oauth2-provider/apps/${id}`); }; getOAuth2ProviderAppSecrets = async ( id: string, ): Promise => { const resp = await this.axios.get( `/api/v2/oauth2-provider/apps/${id}/secrets`, ); return resp.data; }; postOAuth2ProviderAppSecret = async ( id: string, ): Promise => { const resp = await this.axios.post( `/api/v2/oauth2-provider/apps/${id}/secrets`, ); return resp.data; }; deleteOAuth2ProviderAppSecret = async ( appId: string, secretId: string, ): Promise => { await this.axios.delete( `/api/v2/oauth2-provider/apps/${appId}/secrets/${secretId}`, ); }; revokeOAuth2ProviderApp = async (appId: string): Promise => { await this.axios.delete(`/oauth2/tokens?client_id=${appId}`); }; getAuditLogs = async ( options: TypesGen.AuditLogsRequest, ): Promise => { const url = getURLWithSearchParams("/api/v2/audit", options); const response = await this.axios.get(url); return response.data; }; getTemplateDAUs = async ( templateId: string, ): Promise => { const response = await this.axios.get( `/api/v2/templates/${templateId}/daus`, ); return response.data; }; getDeploymentDAUs = async ( // Default to user's local timezone. // As /api/v2/insights/daus only accepts whole-number values for tz_offset // we truncate the tz offset down to the closest hour. offset = Math.trunc(new Date().getTimezoneOffset() / 60), ): Promise => { const response = await this.axios.get( `/api/v2/insights/daus?tz_offset=${offset}`, ); return response.data; }; getTemplateACLAvailable = async ( templateId: string, options: TypesGen.UsersRequest, ): Promise => { const url = getURLWithSearchParams( `/api/v2/templates/${templateId}/acl/available`, options, ).toString(); const response = await this.axios.get(url); return response.data; }; getTemplateACL = async ( templateId: string, ): Promise => { const response = await this.axios.get( `/api/v2/templates/${templateId}/acl`, ); return response.data; }; updateTemplateACL = async ( templateId: string, data: TypesGen.UpdateTemplateACL, ): Promise<{ message: string }> => { const response = await this.axios.patch( `/api/v2/templates/${templateId}/acl`, data, ); return response.data; }; getApplicationsHost = async (): Promise => { const response = await this.axios.get("/api/v2/applications/host"); return response.data; }; getGroups = async ( options: { userId?: string } = {}, ): Promise => { const params: Record = {}; if (options.userId !== undefined) { params.has_member = options.userId; } const response = await this.axios.get("/api/v2/groups", { params }); return response.data; }; /** * @param organization Can be the organization's ID or name */ getGroupsByOrganization = async ( organization: string, ): Promise => { const response = await this.axios.get( `/api/v2/organizations/${organization}/groups`, ); return response.data; }; /** * @param organization Can be the organization's ID or name */ createGroup = async ( organization: string, data: TypesGen.CreateGroupRequest, ): Promise => { const response = await this.axios.post( `/api/v2/organizations/${organization}/groups`, data, ); return response.data; }; /** * @param organization Can be the organization's ID or name */ getGroup = async ( organization: string, groupName: string, ): Promise => { const response = await this.axios.get( `/api/v2/organizations/${organization}/groups/${groupName}`, ); return response.data; }; patchGroup = async ( groupId: string, data: TypesGen.PatchGroupRequest, ): Promise => { const response = await this.axios.patch(`/api/v2/groups/${groupId}`, data); return response.data; }; addMember = async (groupId: string, userId: string) => { return this.patchGroup(groupId, { name: "", add_users: [userId], remove_users: [], display_name: null, avatar_url: null, quota_allowance: null, }); }; removeMember = async (groupId: string, userId: string) => { return this.patchGroup(groupId, { name: "", add_users: [], remove_users: [userId], display_name: null, avatar_url: null, quota_allowance: null, }); }; deleteGroup = async (groupId: string): Promise => { await this.axios.delete(`/api/v2/groups/${groupId}`); }; getWorkspaceQuota = async ( organizationName: string, username: string, ): Promise => { const response = await this.axios.get( `/api/v2/organizations/${encodeURIComponent(organizationName)}/members/${encodeURIComponent(username)}/workspace-quota`, ); return response.data; }; getAgentListeningPorts = async ( agentID: string, ): Promise => { const response = await this.axios.get( `/api/v2/workspaceagents/${agentID}/listening-ports`, ); return response.data; }; getWorkspaceAgentSharedPorts = async ( workspaceID: string, ): Promise => { const response = await this.axios.get( `/api/v2/workspaces/${workspaceID}/port-share`, ); return response.data; }; upsertWorkspaceAgentSharedPort = async ( workspaceID: string, req: TypesGen.UpsertWorkspaceAgentPortShareRequest, ): Promise => { const response = await this.axios.post( `/api/v2/workspaces/${workspaceID}/port-share`, req, ); return response.data; }; deleteWorkspaceAgentSharedPort = async ( workspaceID: string, req: TypesGen.DeleteWorkspaceAgentPortShareRequest, ): Promise => { const response = await this.axios.delete( `/api/v2/workspaces/${workspaceID}/port-share`, { data: req }, ); return response.data; }; // getDeploymentSSHConfig is used by the VSCode-Extension. getDeploymentSSHConfig = async (): Promise => { const response = await this.axios.get("/api/v2/deployment/ssh"); return response.data; }; getDeploymentConfig = async (): Promise => { const response = await this.axios.get("/api/v2/deployment/config"); return response.data; }; getDeploymentStats = async (): Promise => { const response = await this.axios.get("/api/v2/deployment/stats"); return response.data; }; getReplicas = async (): Promise => { const response = await this.axios.get("/api/v2/replicas"); return response.data; }; getFile = async (fileId: string): Promise => { const response = await this.axios.get( `/api/v2/files/${fileId}`, { responseType: "arraybuffer" }, ); return response.data; }; getWorkspaceProxyRegions = async (): Promise< TypesGen.RegionsResponse > => { const response = await this.axios.get>( "/api/v2/regions", ); return response.data; }; getWorkspaceProxies = async (): Promise< TypesGen.RegionsResponse > => { const response = await this.axios.get< TypesGen.RegionsResponse >("/api/v2/workspaceproxies"); return response.data; }; createWorkspaceProxy = async ( b: TypesGen.CreateWorkspaceProxyRequest, ): Promise => { const response = await this.axios.post("/api/v2/workspaceproxies", b); return response.data; }; getAppearance = async (): Promise => { try { const response = await this.axios.get("/api/v2/appearance"); return response.data || {}; } catch (ex) { if (isAxiosError(ex) && ex.response?.status === 404) { return { application_name: "", docs_url: "", logo_url: "", announcement_banners: [], service_banner: { enabled: false, }, }; } throw ex; } }; updateAppearance = async ( b: TypesGen.AppearanceConfig, ): Promise => { const response = await this.axios.put("/api/v2/appearance", b); return response.data; }; /** * @param organization Can be the organization's ID or name */ getTemplateExamples = async (): Promise => { const response = await this.axios.get("/api/v2/templates/examples"); return response.data; }; uploadFile = async (file: File): Promise => { const response = await this.axios.post("/api/v2/files", file, { headers: { "Content-Type": file.type }, }); return response.data; }; getTemplateVersionLogs = async ( versionId: string, ): Promise => { const response = await this.axios.get( `/api/v2/templateversions/${versionId}/logs`, ); return response.data; }; updateWorkspaceVersion = async ( workspace: TypesGen.Workspace, ): Promise => { const template = await this.getTemplate(workspace.template_id); return this.startWorkspace(workspace.id, template.active_version_id); }; getWorkspaceBuildParameters = async ( workspaceBuildId: TypesGen.WorkspaceBuild["id"], ): Promise => { const response = await this.axios.get( `/api/v2/workspacebuilds/${workspaceBuildId}/parameters`, ); return response.data; }; getLicenses = async (): Promise => { const response = await this.axios.get("/api/v2/licenses"); return response.data; }; createLicense = async ( data: TypesGen.AddLicenseRequest, ): Promise => { const response = await this.axios.post("/api/v2/licenses", data); return response.data; }; removeLicense = async (licenseId: number): Promise => { await this.axios.delete(`/api/v2/licenses/${licenseId}`); }; getDynamicParameters = async ( templateVersionId: string, ownerId: string, oldBuildParameters: TypesGen.WorkspaceBuildParameter[], ) => { const request: DynamicParametersRequest = { id: 1, owner_id: ownerId, inputs: Object.fromEntries( new Map(oldBuildParameters.map((param) => [param.name, param.value])), ), }; const dynamicParametersResponse = await this.getTemplateVersionDynamicParameters( templateVersionId, request, ); return dynamicParametersResponse.parameters.map((p) => ({ ...p, description_plaintext: p.description || "", default_value: p.default_value?.valid ? p.default_value.value : "", options: p.options ? p.options.map((opt) => ({ ...opt, value: opt.value?.valid ? opt.value.value : "", })) : [], })); }; /** Steps to change the workspace version * - Get the latest template to access the latest active version * - Get the current build parameters * - Get the template parameters * - Update the build parameters and check if there are missed parameters for * the new version * - If there are missing parameters raise an error * - Create a build with the version and updated build parameters */ changeWorkspaceVersion = async ( workspace: TypesGen.Workspace, templateVersionId: string, newBuildParameters: TypesGen.WorkspaceBuildParameter[] = [], isDynamicParametersEnabled = false, ): Promise => { const currentBuildParameters = await this.getWorkspaceBuildParameters( workspace.latest_build.id, ); let templateParameters: TypesGen.TemplateVersionParameter[] = []; if (isDynamicParametersEnabled) { templateParameters = await this.getDynamicParameters( templateVersionId, workspace.owner_id, currentBuildParameters, ); } else { templateParameters = await this.getTemplateVersionRichParameters(templateVersionId); } const missingParameters = getMissingParameters( currentBuildParameters, newBuildParameters, templateParameters, ); if (missingParameters.length > 0) { throw new MissingBuildParameters(missingParameters, templateVersionId); } return this.postWorkspaceBuild(workspace.id, { transition: "start", template_version_id: templateVersionId, rich_parameter_values: newBuildParameters, }); }; /** Steps to update the workspace * - Get the latest template to access the latest active version * - Get the current build parameters * - Get the template parameters * - Update the build parameters and check if there are missed parameters for * the newest version * - If there are missing parameters raise an error * - Create a build with the latest version and updated build parameters */ updateWorkspace = async ( workspace: TypesGen.Workspace, newBuildParameters: TypesGen.WorkspaceBuildParameter[] = [], isDynamicParametersEnabled = false, ): Promise => { const [template, oldBuildParameters] = await Promise.all([ this.getTemplate(workspace.template_id), this.getWorkspaceBuildParameters(workspace.latest_build.id), ]); const activeVersionId = template.active_version_id; let templateParameters: TypesGen.TemplateVersionParameter[] = []; if (isDynamicParametersEnabled) { templateParameters = await this.getDynamicParameters( activeVersionId, workspace.owner_id, oldBuildParameters, ); } else { templateParameters = await this.getTemplateVersionRichParameters(activeVersionId); } const missingParameters = getMissingParameters( oldBuildParameters, newBuildParameters, templateParameters, ); if (missingParameters.length > 0) { throw new MissingBuildParameters(missingParameters, activeVersionId); } return this.postWorkspaceBuild(workspace.id, { transition: "start", template_version_id: activeVersionId, rich_parameter_values: newBuildParameters, }); }; getWorkspaceResolveAutostart = async ( workspaceId: string, ): Promise => { const response = await this.axios.get( `/api/v2/workspaces/${workspaceId}/resolve-autostart`, ); return response.data; }; issueReconnectingPTYSignedToken = async ( params: TypesGen.IssueReconnectingPTYSignedTokenRequest, ): Promise => { const response = await this.axios.post( "/api/v2/applications/reconnecting-pty-signed-token", params, ); return response.data; }; getWorkspaceParameters = async (workspace: TypesGen.Workspace) => { const latestBuild = workspace.latest_build; const [templateVersionRichParameters, buildParameters] = await Promise.all([ this.getTemplateVersionRichParameters(latestBuild.template_version_id), this.getWorkspaceBuildParameters(latestBuild.id), ]); return { templateVersionRichParameters, buildParameters, }; }; getInsightsUserLatency = async ( filters: InsightsParams, ): Promise => { const params = new URLSearchParams(filters); const response = await this.axios.get( `/api/v2/insights/user-latency?${params}`, ); return response.data; }; getInsightsUserActivity = async ( filters: InsightsParams, ): Promise => { const params = new URLSearchParams(filters); const response = await this.axios.get( `/api/v2/insights/user-activity?${params}`, ); return response.data; }; getInsightsUserStatusCounts = async ( offset = Math.trunc(new Date().getTimezoneOffset() / 60), ): Promise => { const searchParams = new URLSearchParams({ tz_offset: offset.toString(), }); const response = await this.axios.get( `/api/v2/insights/user-status-counts?${searchParams}`, ); return response.data; }; getInsightsTemplate = async ( params: InsightsTemplateParams, ): Promise => { const searchParams = new URLSearchParams(params); const response = await this.axios.get( `/api/v2/insights/templates?${searchParams}`, ); return response.data; }; getHealth = async (force = false) => { const params = new URLSearchParams({ force: force.toString() }); const response = await this.axios.get( `/api/v2/debug/health?${params}`, ); return response.data; }; getHealthSettings = async (): Promise => { const res = await this.axios.get( "/api/v2/debug/health/settings", ); return res.data; }; updateHealthSettings = async (data: TypesGen.UpdateHealthSettings) => { const response = await this.axios.put( "/api/v2/debug/health/settings", data, ); return response.data; }; putFavoriteWorkspace = async (workspaceID: string) => { await this.axios.put(`/api/v2/workspaces/${workspaceID}/favorite`); }; deleteFavoriteWorkspace = async (workspaceID: string) => { await this.axios.delete(`/api/v2/workspaces/${workspaceID}/favorite`); }; postWorkspaceUsage = async ( workspaceID: string, options: PostWorkspaceUsageRequest, ) => { const response = await this.axios.post( `/api/v2/workspaces/${workspaceID}/usage`, options, ); return response.data; }; getUserNotificationPreferences = async (userId: string) => { const res = await this.axios.get( `/api/v2/users/${userId}/notifications/preferences`, ); return res.data ?? []; }; putUserNotificationPreferences = async ( userId: string, req: TypesGen.UpdateUserNotificationPreferences, ) => { const res = await this.axios.put( `/api/v2/users/${userId}/notifications/preferences`, req, ); return res.data; }; getSystemNotificationTemplates = async () => { const res = await this.axios.get( "/api/v2/notifications/templates/system", ); return res.data; }; getNotificationDispatchMethods = async () => { const res = await this.axios.get( "/api/v2/notifications/dispatch-methods", ); return res.data; }; updateNotificationTemplateMethod = async ( templateId: string, req: TypesGen.UpdateNotificationTemplateMethod, ) => { const res = await this.axios.put( `/api/v2/notifications/templates/${templateId}/method`, req, ); return res.data; }; postTestNotification = async () => { await this.axios.post("/api/v2/notifications/test"); }; createWebPushSubscription = async ( userId: string, req: TypesGen.WebpushSubscription, ) => { await this.axios.post( `/api/v2/users/${userId}/webpush/subscription`, req, ); }; deleteWebPushSubscription = async ( userId: string, req: TypesGen.DeleteWebpushSubscription, ) => { await this.axios.delete( `/api/v2/users/${userId}/webpush/subscription`, { data: req, }, ); }; requestOneTimePassword = async ( req: TypesGen.RequestOneTimePasscodeRequest, ) => { await this.axios.post("/api/v2/users/otp/request", req); }; changePasswordWithOTP = async ( req: TypesGen.ChangePasswordWithOneTimePasscodeRequest, ) => { await this.axios.post("/api/v2/users/otp/change-password", req); }; workspaceBuildTimings = async (workspaceBuildId: string) => { const res = await this.axios.get( `/api/v2/workspacebuilds/${workspaceBuildId}/timings`, ); return res.data; }; getProvisionerJobs = async ( orgId: string, params: GetProvisionerJobsParams = {}, ) => { const res = await this.axios.get( `/api/v2/organizations/${orgId}/provisionerjobs`, { params }, ); return res.data; }; cancelProvisionerJob = async (job: TypesGen.ProvisionerJob) => { switch (job.type) { case "workspace_build": if (!job.input.workspace_build_id) { throw new Error("Workspace build ID is required to cancel this job"); } return this.cancelWorkspaceBuild(job.input.workspace_build_id); case "template_version_import": if (!job.input.template_version_id) { throw new Error("Template version ID is required to cancel this job"); } return this.cancelTemplateVersionBuild(job.input.template_version_id); case "template_version_dry_run": if (!job.input.template_version_id) { throw new Error("Template version ID is required to cancel this job"); } return this.cancelTemplateVersionDryRun( job.input.template_version_id, job.id, ); } }; getAgentContainers = async (agentId: string, labels?: string[]) => { const params = new URLSearchParams( labels?.map((label) => ["label", label]), ); const res = await this.axios.get( `/api/v2/workspaceagents/${agentId}/containers?${params.toString()}`, ); return res.data; }; getInboxNotifications = async (startingBeforeId?: string) => { const params = new URLSearchParams(); if (startingBeforeId) { params.append("starting_before", startingBeforeId); } const res = await this.axios.get( `/api/v2/notifications/inbox?${params.toString()}`, ); return res.data; }; updateInboxNotificationReadStatus = async ( notificationId: string, req: TypesGen.UpdateInboxNotificationReadStatusRequest, ) => { const res = await this.axios.put( `/api/v2/notifications/inbox/${notificationId}/read-status`, req, ); return res.data; }; markAllInboxNotificationsAsRead = async () => { await this.axios.put("/api/v2/notifications/inbox/mark-all-as-read"); }; createChat = async () => { const res = await this.axios.post("/api/v2/chats"); return res.data; }; getChats = async () => { const res = await this.axios.get("/api/v2/chats"); return res.data; }; getChatMessages = async (chatId: string) => { const res = await this.axios.get( `/api/v2/chats/${chatId}/messages`, ); return res.data; }; } // This is a hard coded CSRF token/cookie pair for local development. In prod, // the GoLang webserver generates a random cookie with a new token for each // document request. For local development, we don't use the Go webserver for // static files, so this is the 'hack' to make local development work with // remote apis. The CSRF cookie for this token is "JXm9hOUdZctWt0ZZGAy9xiS/gxMKYOThdxjjMnMUyn4=" const csrfToken = "KNKvagCBEHZK7ihe2t7fj6VeJ0UyTDco1yVUJE8N06oNqxLu5Zx1vRxZbgfC0mJJgeGkVjgs08mgPbcWPBkZ1A=="; // Always attach CSRF token to all requests. In puppeteer the document is // undefined. In those cases, just do nothing. const tokenMetadataElement = typeof document !== "undefined" ? document.head.querySelector('meta[property="csrf-token"]') : null; function getConfiguredAxiosInstance(): AxiosInstance { const instance = globalAxios.create(); // Adds 304 for the default axios validateStatus function // https://github.com/axios/axios#handling-errors Check status here // https://httpstatusdogs.com/ instance.defaults.validateStatus = (status) => { return (status >= 200 && status < 300) || status === 304; }; const metadataIsAvailable = tokenMetadataElement !== null && tokenMetadataElement.getAttribute("content") !== null; if (metadataIsAvailable) { if (process.env.NODE_ENV === "development") { // Development mode uses a hard-coded CSRF token instance.defaults.headers.common["X-CSRF-TOKEN"] = csrfToken; instance.defaults.headers.common["X-CSRF-TOKEN"] = csrfToken; tokenMetadataElement.setAttribute("content", csrfToken); } else { instance.defaults.headers.common["X-CSRF-TOKEN"] = tokenMetadataElement.getAttribute("content") ?? ""; } } else { // Do not write error logs if we are in a FE unit test. if (process.env.JEST_WORKER_ID === undefined) { console.error("CSRF token not found"); } } return instance; } /** * Utility function to help create a WebSocket connection with Coder's API. */ function createWebSocket( path: string, params: URLSearchParams = new URLSearchParams(), ) { const protocol = location.protocol === "https:" ? "wss:" : "ws:"; const socket = new WebSocket( `${protocol}//${location.host}${path}?${params}`, ); socket.binaryType = "blob"; return socket; } // Other non-API methods defined here to make it a little easier to find them. interface ClientApi extends ApiMethods { getCsrfToken: () => string; setSessionToken: (token: string) => void; setHost: (host: string | undefined) => void; getAxiosInstance: () => AxiosInstance; } class Api extends ApiMethods implements ClientApi { constructor() { const scopedAxiosInstance = getConfiguredAxiosInstance(); super(scopedAxiosInstance); } // As with ApiMethods, all public methods should be defined with arrow // function syntax to ensure they can be passed around the React UI without // losing/detaching their `this` context! getCsrfToken = (): string => { return csrfToken; }; setSessionToken = (token: string): void => { this.axios.defaults.headers.common["Coder-Session-Token"] = token; }; setHost = (host: string | undefined): void => { this.axios.defaults.baseURL = host; }; getAxiosInstance = (): AxiosInstance => { return this.axios; }; } export const API = new Api();