/** * @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 { delay } from "../utils/delay"; import { OneWayWebSocket, type OneWayWebSocketApi, } from "../utils/OneWayWebSocket"; import { type FieldError, isApiError } from "./errors"; import type { AdvisorConfig, DeleteExternalAuthByIDResponse, DynamicParametersRequest, PostWorkspaceUsageRequest, UpdateAdvisorConfigRequest, UsersRequest, } 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; }; /** * Originally from codersdk/client.go. * The below declaration is required to stop Knip from complaining. * @public */ export const SessionTokenCookie = "coder_session_token"; /** * @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`, }); }; export const watchChat = ( chatId: string, afterMessageId?: number, ): OneWayWebSocketApi => { const params = new URLSearchParams(); if (afterMessageId !== undefined && afterMessageId > 0) { params.set("after_id", afterMessageId.toString()); } const token = API.getSessionToken(); if (token) { params.set(SessionTokenCookie, token); } const query = params.toString(); const route = `/api/experimental/chats/${chatId}/stream${query ? `?${query}` : ""}`; return new OneWayWebSocket({ apiRoute: route, }); }; export const watchChats = (): OneWayWebSocket => { const searchParams: Record = {}; const token = API.getSessionToken(); if (token) { searchParams[SessionTokenCookie] = token; } return new OneWayWebSocket({ apiRoute: "/api/experimental/chats/watch", searchParams, }); }; export const watchChatGit = (chatId: string): WebSocket => { return createWebSocket(`/api/experimental/chats/${chatId}/stream/git`); }; export const watchChatDesktop = (chatId: string): WebSocket => { const socket = createWebSocket( `/api/experimental/chats/${chatId}/stream/desktop`, ); // RFB is a binary protocol — noVNC expects arraybuffer, not blob. socket.binaryType = "arraybuffer"; return socket; }; export const watchAgentContainers = ( agentId: string, ): OneWayWebSocket => { return new OneWayWebSocket({ apiRoute: `/api/v2/workspaceagents/${agentId}/containers/watch`, }); }; 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?: object, ): 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[]; }>; const aiProviderConfigsPath = "/api/v2/ai/providers"; const chatModelConfigsPath = "/api/experimental/chats/model-configs"; const userSkillsPath = (user: string) => `/api/experimental/users/${encodeURIComponent(user)}/skills`; const userSkillPath = (user: string, name: string) => `${userSkillsPath(user)}/${encodeURIComponent(name)}`; const userAIProviderKeysPath = (user = "me") => `/api/experimental/users/${encodeURIComponent(user)}/ai-provider-keys`; const mcpServerConfigsPath = "/api/experimental/mcp/servers"; type ChatCostDateParams = { start_date?: string; end_date?: string; }; type ChatCostUsersParams = ChatCostDateParams & { username?: string; limit?: number; offset?: number; }; type Claims = { license_expires: number; // nbf is a standard JWT claim for "not before" - the license valid from date nbf?: number; account_type?: string; account_id?: string; trial: boolean; all_features: boolean; // feature_set is omitted on legacy licenses feature_set?: string; addons?: 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 class ParameterValidationError extends Error { constructor( public readonly versionId: string, public readonly validations: FieldError[], ) { super("Parameters are not valid for new template version"); } } 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; // Include offline provisioner daemons? offline?: boolean; }; /** * 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 { experimental: ExperimentalApiMethods; constructor(protected readonly axios: AxiosInstance) { this.experimental = new ExperimentalApiMethods(this.axios); } 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; }; getUser = async (usernameOrId: string) => { const response = await this.axios.get( `/api/v2/users/${encodeURIComponent(usernameOrId)}`, ); 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; }; /** * Get users for workspace owner selection. Requires * permission to create workspaces for other users in the * organization. Returns minimal user data (no email, roles, * etc.). */ getWorkspaceAvailableUsers = async ( organizationId: string, options: TypesGen.UsersRequest, signal?: AbortSignal, ): Promise => { const url = getURLWithSearchParams( `/api/v2/organizations/${organizationId}/members/me/workspaces/available-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.UsersRequest, ) => { 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; }; /** * @param organization Can be the organization's ID or name */ getWorkspaceSharingSettings = async ( organization: string, ): Promise => { const response = await this.axios.get( `/api/v2/organizations/${organization}/settings/workspace-sharing`, ); return response.data; }; /** * @param organization Can be the organization's ID or name */ patchWorkspaceSharingSettings = async ( organization: string, data: TypesGen.UpdateWorkspaceSharingSettingsRequest, ): Promise => { const response = await this.axios.patch( `/api/v2/organizations/${organization}/settings/workspace-sharing`, data, ); 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; }; 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, ) => { const response = await this.axios.get( `/api/v2/organizations/${organization}/templates/${templateName}/versions/${versionName}/previous`, ); // The API returns 204 No Content when there is no previous version // (e.g. the first version of a template). if (response.status === 204) { return undefined; } return response.data; }; /** * @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, userId: string, { onOpen, onMessage, onError, onClose, }: { onOpen?: () => void; onMessage: (response: TypesGen.DynamicParametersResponse) => void; onError: (error: Error) => void; onClose: () => void; }, ): WebSocket => { const socket = createWebSocket( `/api/v2/templateversions/${versionId}/dynamic-parameters`, new URLSearchParams({ user_id: userId }), ); socket.addEventListener("open", () => { onOpen?.(); }); 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; }; invalidateTemplatePresets = async ( templateId: string, ): Promise => { const response = await this.axios.post( `/api/v2/templates/${templateId}/prebuilds/invalidate`, ); 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 ( req: TypesGen.WorkspacesRequest, ): Promise => { const url = getURLWithSearchParams("/api/v2/workspaces", req); 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; 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, reason: "dashboard", }); }; 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"], params?: TypesGen.CancelWorkspaceBuildParams, ): Promise => { const response = await this.axios.patch( `/api/v2/workspacebuilds/${workspaceBuildId}/cancel`, null, { params }, ); 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); }; /** * Starts a workspace, but if the last build was a failed start, * stops it first to give it a clean slate and the best chance * of success. */ retryWorkspace = async ( workspace: TypesGen.Workspace, templateVersionId: string, logLevel?: TypesGen.ProvisionerLogLevel, buildParameters?: TypesGen.WorkspaceBuildParameter[], ): Promise => { if ( workspace.latest_build.status === "failed" && workspace.latest_build.transition === "start" ) { const stopBuild = await this.stopWorkspace(workspace.id, logLevel); const awaitedStop = await this.waitForBuild(stopBuild); if (awaitedStop?.status === "canceled") { throw new Error("Cleanup stop was canceled"); } } return this.startWorkspace( workspace.id, templateVersionId, logLevel, buildParameters, ); }; 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; }; getUserPreferenceSettings = async (): Promise => { const response = await this.axios.get("/api/v2/users/me/preferences"); return response.data; }; updateUserPreferenceSettings = async ( req: TypesGen.UpdateUserPreferenceSettingsRequest, ): Promise => { const response = await this.axios.put("/api/v2/users/me/preferences", req); 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; }; getUserSecrets = async (userId: string): Promise => { const response = await this.axios.get( `/api/v2/users/${encodeURIComponent(userId)}/secrets`, ); return response.data; }; getUserSecret = async ( userId: string, name: string, ): Promise => { const response = await this.axios.get( `/api/v2/users/${encodeURIComponent(userId)}/secrets/${encodeURIComponent(name)}`, ); return response.data; }; createUserSecret = async ( userId: string, request: TypesGen.CreateUserSecretRequest, ): Promise => { const response = await this.axios.post( `/api/v2/users/${encodeURIComponent(userId)}/secrets`, request, ); return response.data; }; updateUserSecret = async ( userId: string, name: string, request: TypesGen.UpdateUserSecretRequest, ): Promise => { const response = await this.axios.patch( `/api/v2/users/${encodeURIComponent(userId)}/secrets/${encodeURIComponent(name)}`, request, ); return response.data; }; deleteUserSecret = async (userId: string, name: string): Promise => { await this.axios.delete( `/api/v2/users/${encodeURIComponent(userId)}/secrets/${encodeURIComponent(name)}`, ); }; 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; }; getConnectionLogs = async ( options: TypesGen.ConnectionLogsRequest, ): Promise => { const url = getURLWithSearchParams("/api/v2/connectionlog", 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; }; getWorkspaceACL = async ( workspaceId: string, ): Promise => { const response = await this.axios.get( `/api/v2/workspaces/${workspaceId}/acl`, ); return response.data; }; updateWorkspaceACL = async ( workspaceId: string, data: TypesGen.UpdateWorkspaceACL, ): Promise => { await this.axios.patch(`/api/v2/workspaces/${workspaceId}/acl`, 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, req: TypesGen.GroupRequest, signal?: AbortSignal, ): Promise => { const url = getURLWithSearchParams( `/api/v2/organizations/${organization}/groups/${groupName}`, req, ); const response = await this.axios.get(url, { signal }); return response.data; }; getGroupMembers = async ( organization: string, groupName: string, filter?: UsersRequest, signal?: AbortSignal, ): Promise => { const url = getURLWithSearchParams( `/api/v2/organizations/${organization}/groups/${groupName}/members`, filter, ); const response = await this.axios.get(url.toString(), { signal }); 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; }; addMembers = async (groupId: string, userIds: string[]) => { return this.patchGroup(groupId, { name: "", add_users: userIds, remove_users: [], display_name: null, avatar_url: null, quota_allowance: null, }); }; 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; }; getWorkspaceAgentCredentials = async ( workspaceID: string, agentName: string, ): Promise => { const response = await this.axios.get( `/api/v2/workspaces/${workspaceID}/external-agent/${agentName}/credentials`, ); 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 : "", })) : [], })); }; /** * Stops a workspace if it is currently running and waits for the stop * to complete. Throws if the stop build is canceled. */ private stopWorkspaceIfRunning = async ( workspace: TypesGen.Workspace, ): Promise => { // Workspace is already in a state where it's "stopped". if (workspace.latest_build.status !== "running") return; const stopBuild = await this.stopWorkspace(workspace.id); const awaitedStopBuild = await this.waitForBuild(stopBuild); if (awaitedStopBuild?.status === "canceled") { throw new Error("Workspace stop was canceled."); } }; /** 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 * - Stop the workspace if it is already running * - 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); } await this.stopWorkspaceIfRunning(workspace); 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 * - Stop the workspace if it is already running * - 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; if (!isDynamicParametersEnabled) { // Dynamic templates rely on the backend to fully validate parameters. // Legacy templates do not, so do an additional check for any missing params. const templateParameters = await this.getTemplateVersionRichParameters(activeVersionId); const missingParameters = getMissingParameters( oldBuildParameters, newBuildParameters, templateParameters, ); if (missingParameters.length > 0) { throw new MissingBuildParameters(missingParameters, activeVersionId); } } await this.stopWorkspaceIfRunning(workspace); try { return await this.postWorkspaceBuild(workspace.id, { transition: "start", template_version_id: activeVersionId, rich_parameter_values: newBuildParameters, }); } catch (error) { // If the build failed because of a parameter validation error, then we // throw a special sentinel error that can be caught by the caller. if ( isDynamicParametersEnabled && isApiError(error) && error.response.status === 400 && error.response.data.validations && error.response.data.validations.length > 0 ) { throw new ParameterValidationError( activeVersionId, error.response.data.validations, ); } throw error; } }; 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; }; 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; }; // Intl.DateTimeFormat().resolvedOptions().timeZone returns an IANA timezone // name (e.g. "America/New_York") per ECMA-402. Go's time.LoadLocation and // PostgreSQL's timezone() both accept IANA names, so these are compatible. getInsightsUserStatusCounts = async ( timezone = Intl.DateTimeFormat().resolvedOptions().timeZone, ): Promise => { const searchParams = new URLSearchParams({ timezone, }); 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; }; getCustomNotificationTemplates = async () => { const res = await this.axios.get( "/api/v2/notifications/templates/custom", ); 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, ); } }; deleteDevContainer = async ({ parentAgentId, devcontainerId, }: { parentAgentId: string; devcontainerId: string; }) => { await this.axios.delete( `/api/v2/workspaceagents/${parentAgentId}/containers/devcontainers/${devcontainerId}`, ); }; recreateDevContainer = async ({ parentAgentId, devcontainerId, }: { parentAgentId: string; devcontainerId: string; }) => { const response = await this.axios.post( `/api/v2/workspaceagents/${parentAgentId}/containers/devcontainers/${devcontainerId}/recreate`, ); return response.data; }; 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"); }; createTask = async ( user: string, req: TypesGen.CreateTaskRequest, ): Promise => { const response = await this.axios.post( `/api/v2/tasks/${user}`, req, ); return response.data; }; getTasks = async ( filter: TypesGen.TasksFilter, ): Promise => { const query: string[] = []; if (filter.owner) { query.push(`owner:${filter.owner}`); } if (filter.status) { query.push(`status:${filter.status}`); } const res = await this.axios.get( "/api/v2/tasks", { params: { q: query.join(", "), }, }, ); return res.data.tasks; }; getTask = async (user: string, id: string): Promise => { const response = await this.axios.get( `/api/v2/tasks/${user}/${id}`, ); return response.data; }; deleteTask = async (user: string, id: string): Promise => { await this.axios.delete(`/api/v2/tasks/${user}/${id}`); }; updateTaskInput = async ( user: string, id: string, input: string, ): Promise => { await this.axios.patch(`/api/v2/tasks/${user}/${id}/input`, { input, } satisfies TypesGen.UpdateTaskInputRequest); }; getTaskLogs = async ( user: string, id: string, ): Promise => { const response = await this.axios.get( `/api/v2/tasks/${user}/${id}/logs`, ); return response.data; }; pauseTask = async ( user: string, id: string, ): Promise => { const response = await this.axios.post( `/api/v2/tasks/${user}/${id}/pause`, ); return response.data; }; resumeTask = async ( user: string, id: string, ): Promise => { const response = await this.axios.post( `/api/v2/tasks/${user}/${id}/resume`, ); return response.data; }; sendTaskInput = async ( user: string, id: string, input: string, ): Promise => { await this.axios.post(`/api/v2/tasks/${user}/${id}/send`, { input, } satisfies TypesGen.TaskSendRequest); }; createTaskFeedback = async ( _taskId: string, _req: CreateTaskFeedbackRequest, ) => { return new Promise((res) => { setTimeout(() => res(), 500); }); }; getAIBridgeModels = async (options: SearchParamOptions) => { const url = getURLWithSearchParams("/api/v2/aibridge/models", options); const response = await this.axios.get(url); return response.data; }; getAIBridgeClients = async (options: SearchParamOptions) => { const url = getURLWithSearchParams("/api/v2/aibridge/clients", options); const response = await this.axios.get(url); return response.data; }; getAIBridgeSessionList = async (options: SearchParamOptions) => { const url = getURLWithSearchParams("/api/v2/aibridge/sessions", options); const response = await this.axios.get(url); return response.data; }; getAIBridgeSessionThreads = async ( sessionId: string, options?: { after_id?: string; before_id?: string; limit?: number }, ) => { const url = getURLWithSearchParams( `/api/v2/aibridge/sessions/${sessionId}`, options, ); const response = await this.axios.get(url); return response.data; }; getAIProviders = async (): Promise => { const response = await this.axios.get( "/api/v2/ai/providers", ); return response.data; }; getAIProvider = async (idOrName: string): Promise => { const response = await this.axios.get( `/api/v2/ai/providers/${encodeURIComponent(idOrName)}`, ); return response.data; }; createAIProvider = async ( req: TypesGen.CreateAIProviderRequest, ): Promise => { const response = await this.axios.post( "/api/v2/ai/providers", req, ); return response.data; }; updateAIProvider = async ( idOrName: string, req: TypesGen.UpdateAIProviderRequest, ): Promise => { const response = await this.axios.patch( `/api/v2/ai/providers/${encodeURIComponent(idOrName)}`, req, ); return response.data; }; deleteAIProvider = async (idOrName: string): Promise => { await this.axios.delete( `/api/v2/ai/providers/${encodeURIComponent(idOrName)}`, ); }; getAIGatewayKeys = async (): Promise => { const response = await this.axios.get( "/api/v2/aibridge/keys", ); return response.data; }; createAIGatewayKey = async ( req: TypesGen.CreateAIGatewayKeyRequest, ): Promise => { const response = await this.axios.post( "/api/v2/aibridge/keys", req, ); return response.data; }; deleteAIGatewayKey = async (id: string): Promise => { await this.axios.delete(`/api/v2/aibridge/keys/${encodeURIComponent(id)}`); }; } export type TaskFeedbackRating = "good" | "okay" | "bad"; export type CreateTaskFeedbackRequest = { rate: TaskFeedbackRating; comment?: string; }; export type ChatPlanModeOrClear = TypesGen.ChatPlanMode | ""; export type CreateChatMessageRequestWithClearablePlanMode = Omit< TypesGen.CreateChatMessageRequest, "plan_mode" > & { readonly plan_mode?: ChatPlanModeOrClear; }; type UpdateChatRequestWithClearablePlanMode = Omit< TypesGen.UpdateChatRequest, "plan_mode" > & { readonly plan_mode?: ChatPlanModeOrClear; }; // Experimental API methods call endpoints under the /api/experimental/ prefix. // These endpoints are not stable and may change or be removed at any time. // // All methods must be defined with arrow function syntax. See the docstring // above the ApiMethods class for a full explanation. class ExperimentalApiMethods { constructor(protected readonly axios: AxiosInstance) {} getChatsByWorkspace = async ( workspaceIds: readonly string[], ): Promise> => { const res = await this.axios.get("/api/experimental/chats/by-workspace", { params: { workspace_ids: workspaceIds.join(",") }, }); return res.data; }; uploadChatFile = async ( file: File, organizationId: string, ): Promise => { const response = await this.axios.post( `/api/experimental/chats/files?organization=${organizationId}`, file, { headers: { "Content-Type": file.type || "application/octet-stream", // Use RFC 5987 encoding for the filename to support // non-ASCII characters. Placing the raw name directly in // the header causes XMLHttpRequest to throw because HTTP // headers only allow ISO-8859-1 code points. "Content-Disposition": `attachment; filename="file"; filename*=UTF-8''${encodeURIComponent(file.name)}`, }, }, ); return response.data; }; getChatFileText = async (fileId: string): Promise => { const response = await this.axios.get( `/api/experimental/chats/files/${fileId}`, { responseType: "text" }, ); return response.data as string; }; // Chat API methods getChatACL = async (chatId: string): Promise => { const response = await this.axios.get( `/api/experimental/chats/${chatId}/acl`, ); return response.data; }; updateChatACL = async ( chatId: string, req: TypesGen.UpdateChatACL, ): Promise => { await this.axios.patch(`/api/experimental/chats/${chatId}/acl`, req); }; getChats = async (req?: { after_id?: string; limit?: number; offset?: number; q?: string; }): Promise => { const response = await this.axios.get( getURLWithSearchParams("/api/experimental/chats", req), ); return response.data; }; getChat = async (chatId: string): Promise => { const response = await this.axios.get( `/api/experimental/chats/${chatId}`, ); return response.data; }; getChatMessages = async ( chatId: string, opts?: { before_id?: number; after_id?: number; limit?: number }, ): Promise => { const params = new URLSearchParams(); if (opts?.before_id) { params.set("before_id", opts.before_id.toString()); } if (opts?.after_id) { params.set("after_id", opts.after_id.toString()); } if (opts?.limit) { params.set("limit", opts.limit.toString()); } const query = params.toString(); const url = `/api/experimental/chats/${chatId}/messages${query ? `?${query}` : ""}`; const response = await this.axios.get(url); return response.data; }; /** * Lists the user-authored prompts in a chat, newest first. * Powers the composer's up/down arrow prompt-history cycle. */ getChatPrompts = async ( chatId: string, opts?: { limit?: number }, ): Promise => { const url = getURLWithSearchParams( `/api/experimental/chats/${chatId}/prompts`, opts, ); const response = await this.axios.get(url); return response.data; }; createChat = async ( req: TypesGen.CreateChatRequest, ): Promise => { const response = await this.axios.post( "/api/experimental/chats", req, ); return response.data; }; updateChat = async ( chatId: string, req: UpdateChatRequestWithClearablePlanMode, ): Promise => { await this.axios.patch(`/api/experimental/chats/${chatId}`, req); }; regenerateChatTitle = async (chatId: string): Promise => { const response = await this.axios.post( `/api/experimental/chats/${chatId}/title/regenerate`, ); return response.data; }; proposeChatTitle = async (chatId: string): Promise<{ title: string }> => { const response = await this.axios.post<{ title: string }>( `/api/experimental/chats/${chatId}/title/propose`, ); return response.data; }; createChatMessage = async ( chatId: string, req: CreateChatMessageRequestWithClearablePlanMode, ): Promise => { const response = await this.axios.post( `/api/experimental/chats/${chatId}/messages`, req, ); return response.data; }; editChatMessage = async ( chatId: string, messageId: number, req: TypesGen.EditChatMessageRequest, ): Promise => { const response = await this.axios.patch( `/api/experimental/chats/${chatId}/messages/${messageId}`, req, ); return response.data; }; interruptChat = async (chatId: string): Promise => { const response = await this.axios.post( `/api/experimental/chats/${chatId}/interrupt`, ); return response.data; }; deleteChatQueuedMessage = async ( chatId: string, queuedMessageId: number, ): Promise => { await this.axios.delete( `/api/experimental/chats/${chatId}/queue/${queuedMessageId}`, ); }; promoteChatQueuedMessage = async ( chatId: string, queuedMessageId: number, ): Promise => { await this.axios.post( `/api/experimental/chats/${chatId}/queue/${queuedMessageId}/promote`, ); }; getChatDiffContents = async ( chatId: string, ): Promise => { const response = await this.axios.get( `/api/experimental/chats/${chatId}/diff`, ); return response.data; }; getChatModels = async (): Promise => { const response = await this.axios.get( "/api/experimental/chats/models", ); return response.data; }; listAIProviders = async (): Promise => { const response = await this.axios.get( aiProviderConfigsPath, ); return response.data; }; createAIProvider = async ( req: TypesGen.CreateAIProviderRequest, ): Promise => { const response = await this.axios.post( aiProviderConfigsPath, req, ); return response.data; }; updateAIProvider = async ( providerId: string, req: TypesGen.UpdateAIProviderRequest, ): Promise => { const response = await this.axios.patch( `${aiProviderConfigsPath}/${providerId}`, req, ); return response.data; }; deleteAIProvider = async (providerId: string): Promise => { await this.axios.delete(`${aiProviderConfigsPath}/${providerId}`); }; getUserAIProviderKeyConfigs = async ( user = "me", ): Promise => { const response = await this.axios.get( userAIProviderKeysPath(user), ); return response.data; }; upsertUserAIProviderKey = async ( providerId: string, req: TypesGen.CreateUserAIProviderKeyRequest, user = "me", ): Promise => { const response = await this.axios.put( `${userAIProviderKeysPath(user)}/${providerId}`, req, ); return response.data; }; deleteUserAIProviderKey = async ( providerId: string, user = "me", ): Promise => { await this.axios.delete(`${userAIProviderKeysPath(user)}/${providerId}`); }; getChatSystemPrompt = async (): Promise => { const response = await this.axios.get( "/api/experimental/chats/config/system-prompt", ); return response.data; }; updateChatSystemPrompt = async ( req: TypesGen.UpdateChatSystemPromptRequest, ): Promise => { await this.axios.put("/api/experimental/chats/config/system-prompt", req); }; getChatPlanModeInstructions = async (): Promise => { const response = await this.axios.get( "/api/experimental/chats/config/plan-mode-instructions", ); return response.data; }; updateChatPlanModeInstructions = async ( req: TypesGen.UpdateChatPlanModeInstructionsRequest, ): Promise => { await this.axios.put( "/api/experimental/chats/config/plan-mode-instructions", req, ); }; getChatModelOverride = async ( context: TypesGen.ChatModelOverrideContext, ): Promise => { const response = await this.axios.get( `/api/experimental/chats/config/model-override/${encodeURIComponent(context)}`, ); return response.data; }; updateChatModelOverride = async ( context: TypesGen.ChatModelOverrideContext, req: TypesGen.UpdateChatModelOverrideRequest, ): Promise => { await this.axios.put( `/api/experimental/chats/config/model-override/${encodeURIComponent(context)}`, req, ); }; getChatPersonalModelOverridesAdminSettings = async (): Promise => { const response = await this.axios.get( "/api/experimental/chats/config/personal-model-overrides", ); return response.data; }; updateChatPersonalModelOverridesAdminSettings = async ( req: TypesGen.UpdateChatPersonalModelOverridesAdminSettingsRequest, ): Promise => { await this.axios.put( "/api/experimental/chats/config/personal-model-overrides", req, ); }; getChatDebugLogging = async (): Promise => { const response = await this.axios.get( "/api/experimental/chats/config/debug-logging", ); return response.data; }; updateChatDebugLogging = async ( req: TypesGen.UpdateChatDebugLoggingAllowUsersRequest, ): Promise => { await this.axios.put("/api/experimental/chats/config/debug-logging", req); }; getUserChatDebugLogging = async (): Promise => { const response = await this.axios.get( "/api/experimental/chats/config/user-debug-logging", ); return response.data; }; updateUserChatDebugLogging = async ( req: TypesGen.UpdateUserChatDebugLoggingRequest, ): Promise => { await this.axios.put( "/api/experimental/chats/config/user-debug-logging", req, ); }; getUserChatPersonalModelOverrides = async (): Promise => { const response = await this.axios.get( "/api/experimental/chats/config/user-personal-model-overrides", ); return response.data; }; updateUserChatPersonalModelOverride = async ( context: TypesGen.ChatPersonalModelOverrideContext, req: TypesGen.UpdateUserChatPersonalModelOverrideRequest, ): Promise => { await this.axios.put( `/api/experimental/chats/config/user-personal-model-overrides/${encodeURIComponent(context)}`, req, ); }; getChatDebugRuns = async ( chatId: string, ): Promise => { const response = await this.axios.get( `/api/experimental/chats/${chatId}/debug/runs`, ); return response.data; }; getChatDebugRun = async ( chatId: string, runId: string, ): Promise => { const response = await this.axios.get( `/api/experimental/chats/${chatId}/debug/runs/${runId}`, ); return response.data; }; getChatDesktopEnabled = async (): Promise => { const response = await this.axios.get( "/api/experimental/chats/config/desktop-enabled", ); return response.data; }; updateChatDesktopEnabled = async ( req: TypesGen.UpdateChatDesktopEnabledRequest, ): Promise => { await this.axios.put("/api/experimental/chats/config/desktop-enabled", req); }; getChatAdvisorConfig = async (): Promise => { const response = await this.axios.get( "/api/experimental/chats/config/advisor", ); return response.data; }; updateChatAdvisorConfig = async ( req: UpdateAdvisorConfigRequest, ): Promise => { await this.axios.put("/api/experimental/chats/config/advisor", req); }; getChatComputerUseProvider = async (): Promise => { const response = await this.axios.get( "/api/experimental/chats/config/computer-use-provider", ); return response.data; }; updateChatComputerUseProvider = async ( req: TypesGen.UpdateChatComputerUseProviderRequest, ): Promise => { await this.axios.put( "/api/experimental/chats/config/computer-use-provider", req, ); }; getChatWorkspaceTTL = async (): Promise => { const response = await this.axios.get( "/api/experimental/chats/config/workspace-ttl", ); return response.data; }; getChatTemplateAllowlist = async (): Promise => { const response = await this.axios.get( "/api/experimental/chats/config/template-allowlist", ); return response.data; }; updateChatWorkspaceTTL = async ( req: TypesGen.UpdateChatWorkspaceTTLRequest, ): Promise => { await this.axios.put("/api/experimental/chats/config/workspace-ttl", req); }; getChatRetentionDays = async (): Promise => { const response = await this.axios.get( "/api/experimental/chats/config/retention-days", ); return response.data; }; updateChatRetentionDays = async ( req: TypesGen.UpdateChatRetentionDaysRequest, ): Promise => { await this.axios.put("/api/experimental/chats/config/retention-days", req); }; getChatDebugRetentionDays = async (): Promise => { const response = await this.axios.get( "/api/experimental/chats/config/debug-retention-days", ); return response.data; }; updateChatDebugRetentionDays = async ( req: TypesGen.UpdateChatDebugRetentionDaysRequest, ): Promise => { await this.axios.put( "/api/experimental/chats/config/debug-retention-days", req, ); }; getChatAutoArchiveDays = async (): Promise => { const response = await this.axios.get( "/api/experimental/chats/config/auto-archive-days", ); return response.data; }; updateChatAutoArchiveDays = async ( req: TypesGen.UpdateChatAutoArchiveDaysRequest, ): Promise => { await this.axios.put( "/api/experimental/chats/config/auto-archive-days", req, ); }; updateChatTemplateAllowlist = async ( req: TypesGen.ChatTemplateAllowlist, ): Promise => { await this.axios.put( "/api/experimental/chats/config/template-allowlist", req, ); }; getUserChatCustomPrompt = async (): Promise => { const response = await this.axios.get( "/api/experimental/chats/config/user-prompt", ); return response.data; }; updateUserChatCustomPrompt = async ( req: TypesGen.UserChatCustomPrompt, ): Promise => { const response = await this.axios.put( "/api/experimental/chats/config/user-prompt", req, ); return response.data; }; createUserSkill = async ( user: string, req: TypesGen.CreateUserSkillRequest, ): Promise => { const response = await this.axios.post( userSkillsPath(user), req, ); return response.data; }; getUserSkills = async ( user: string, ): Promise => { const response = await this.axios.get( userSkillsPath(user), ); return response.data; }; getUserSkillByName = async ( user: string, name: string, ): Promise => { const response = await this.axios.get( userSkillPath(user, name), ); return response.data; }; updateUserSkill = async ( user: string, name: string, req: TypesGen.UpdateUserSkillRequest, ): Promise => { const response = await this.axios.patch( userSkillPath(user, name), req, ); return response.data; }; deleteUserSkill = async (user: string, name: string): Promise => { await this.axios.delete(userSkillPath(user, name)); }; getUserChatCompactionThresholds = async (): Promise => { const response = await this.axios.get( "/api/experimental/chats/config/user-compaction-thresholds", ); return response.data; }; updateUserChatCompactionThreshold = async ( modelConfigId: string, req: TypesGen.UpdateUserChatCompactionThresholdRequest, ): Promise => { const response = await this.axios.put( `/api/experimental/chats/config/user-compaction-thresholds/${encodeURIComponent(modelConfigId)}`, req, ); return response.data; }; deleteUserChatCompactionThreshold = async ( modelConfigId: string, ): Promise => { await this.axios.delete( `/api/experimental/chats/config/user-compaction-thresholds/${encodeURIComponent(modelConfigId)}`, ); }; getChatModelConfigs = async (): Promise => { const response = await this.axios.get(chatModelConfigsPath); return response.data; }; createChatModelConfig = async ( req: TypesGen.CreateChatModelConfigRequest, ): Promise => { const response = await this.axios.post( chatModelConfigsPath, req, ); return response.data; }; updateChatModelConfig = async ( modelConfigId: string, req: TypesGen.UpdateChatModelConfigRequest, ): Promise => { const response = await this.axios.patch( `${chatModelConfigsPath}/${encodeURIComponent(modelConfigId)}`, req, ); return response.data; }; deleteChatModelConfig = async (modelConfigId: string): Promise => { await this.axios.delete( `${chatModelConfigsPath}/${encodeURIComponent(modelConfigId)}`, ); }; getMCPServerConfigs = async (): Promise => { const response = await this.axios.get(mcpServerConfigsPath); return response.data; }; createMCPServerConfig = async ( req: TypesGen.CreateMCPServerConfigRequest, ): Promise => { const response = await this.axios.post( mcpServerConfigsPath, req, ); return response.data; }; updateMCPServerConfig = async ( id: string, req: TypesGen.UpdateMCPServerConfigRequest, ): Promise => { const response = await this.axios.patch( `${mcpServerConfigsPath}/${encodeURIComponent(id)}`, req, ); return response.data; }; deleteMCPServerConfig = async (id: string): Promise => { await this.axios.delete( `${mcpServerConfigsPath}/${encodeURIComponent(id)}`, ); }; getChatCostSummary = async ( user = "me", params?: ChatCostDateParams, ): Promise => { const url = getURLWithSearchParams( `/api/experimental/chats/cost/${encodeURIComponent(user)}/summary`, params, ); const response = await this.axios.get(url); return response.data; }; getChatCostUsers = async ( params?: ChatCostUsersParams, ): Promise => { const url = getURLWithSearchParams( "/api/experimental/chats/cost/users", params, ); const response = await this.axios.get(url); return response.data; }; getPRInsights = async (params?: { start_date?: string; end_date?: string; }): Promise => { const url = getURLWithSearchParams( "/api/experimental/chats/insights/pull-requests", params, ); const response = await this.axios.get(url); return response.data; }; getChatUsageLimitConfig = async (): Promise => { const response = await this.axios.get( "/api/experimental/chats/usage-limits", ); return response.data; }; getChatUsageLimitStatus = async (): Promise => { const response = await this.axios.get( "/api/experimental/chats/usage-limits/status", ); return response.data; }; updateChatUsageLimitConfig = async ( req: TypesGen.ChatUsageLimitConfig, ): Promise => { const response = await this.axios.put( "/api/experimental/chats/usage-limits", req, ); return response.data; }; upsertChatUsageLimitOverride = async ( userID: string, req: TypesGen.UpsertChatUsageLimitOverrideRequest, ): Promise => { const response = await this.axios.put( `/api/experimental/chats/usage-limits/overrides/${encodeURIComponent(userID)}`, req, ); return response.data; }; deleteChatUsageLimitOverride = async (userID: string): Promise => { const response = await this.axios.delete( `/api/experimental/chats/usage-limits/overrides/${encodeURIComponent(userID)}`, ); return response.data; }; upsertChatUsageLimitGroupOverride = async ( groupID: string, req: TypesGen.UpsertChatUsageLimitGroupOverrideRequest, ): Promise => { const response = await this.axios.put( `/api/experimental/chats/usage-limits/group-overrides/${encodeURIComponent(groupID)}`, req, ); return response.data; }; deleteChatUsageLimitGroupOverride = async ( groupID: string, ): Promise => { const response = await this.axios.delete( `/api/experimental/chats/usage-limits/group-overrides/${encodeURIComponent(groupID)}`, ); return response.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; 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 or if there is no document (e.g., Electron) if ( typeof document !== "undefined" && !process.env.JEST_WORKER_ID && !process.env.VITEST ) { 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(), ) { // When running in an embedded context (e.g. VS Code webview), // the session token is set via the API header but browsers // cannot attach custom headers to WebSocket connections. // Pass it as a query parameter instead. const token = API.getSessionToken(); if (token) { params.set(SessionTokenCookie, token); } 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; getSessionToken: () => string | undefined; setHost: (host: string | undefined) => void; getAxiosInstance: () => AxiosInstance; } /** @public Exported for use by external consumers (e.g., VS Code extension). */ export 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; }; getSessionToken = (): string | undefined => { return this.axios.defaults.headers.common["Coder-Session-Token"] as | string | undefined; }; setHost = (host: string | undefined): void => { this.axios.defaults.baseURL = host; }; getAxiosInstance = (): AxiosInstance => { return this.axios; }; } export const API = new Api();