Files
coder/site/src/api/api.ts
T
Jake Howell 4caa52844d chore!: remove api.ts unnecessary calls (#22168)
> [!WARNING]  
> The change of the status code from `404` to `204` could break peoples
code downstream. Adding this as a breaking change incase.

Theres a whole ton of noise around failed requests, these are all
unrelated to the actual thing that is broken at hand (and are
confusing).

* Change `/api/v2/organizations/.../templates/.../versions/.../previous`
to return `204` instead of `404` (actually makes more sense because the
content doesn't exist, but the route is found.
* Remove unnecessary calls to `/api/v2/users/me/appearance` when the
user isn't logged in.
* Remove unnecessary calls to `/api/v2/deployment/stats` when the
deployment stats aren't allowed to be seen.
* Various changes to `workspace-sharing` so we don't make unnecessary
calls.

Whats left:

* `/api/v2/users/me` still `401`s on the login page. This persists as
when the user is logged in but tries to reach the sign-in page they
should be redirected to the app, not sign in again.
* `monaco-editor` is still upset... we theoretically could inject an
environment that can serve workers... but eh.

#### Old

```sh
% pnpm playwright:test -g "create workspace with default and required parameters"

> coder-v2@ playwright:test /home/coder/coder/site
> playwright test --config=e2e/playwright.config.ts -g 'create workspace with default and required parameters'

...

Running 2 tests using 1 worker

  ✓  1 …e/setup/addUsersAndLicense.spec.ts:7:5 › setup deployment (8.2s)
     2 ….ts:79:5 › create workspace with default and required parameters
[console][error] Failed to load resource: the server responded with a status of 401 (Unauthorized)
[console][error] Failed to load resource: the server responded with a status of 401 (Unauthorized)
[response] url=http://localhost:3111/api/v2/users/me/appearance status=401 body={"message":"You are signed out or your session has expired. Please sign in again to continue.","detail":"Cookie \"coder_session_token\" or query parameter must be provided."}
[response] url=http://localhost:3111/api/v2/users/me status=401 body={"message":"You are signed out or your session has expired. Please sign in again to continue.","detail":"Cookie \"coder_session_token\" or query parameter must be provided."}
[console][error] Failed to load resource: the server responded with a status of 403 (Forbidden)
[response] url=http://localhost:3111/api/v2/deployment/stats status=403 body={"message":"Forbidden.","detail":"You don't have permission to view this content. If you believe this is a mistake, please contact your administrator or try signing in with different credentials."}
[console][error] Failed to load resource: the server responded with a status of 403 (Forbidden)
[response] url=http://localhost:3111/api/v2/deployment/stats status=403 body={"message":"Forbidden.","detail":"You don't have permission to view this content. If you believe this is a mistake, please contact your administrator or try signing in with different credentials."}
[console][error] Failed to load resource: the server responded with a status of 404 (Not Found)
[response] url=http://localhost:3111/api/v2/organizations//provisionerdaemons status=404 body={"message":"Resource not found or you do not have access to this resource"}
[console][error] Failed to load resource: the server responded with a status of 404 (Not Found)
[response] url=http://localhost:3111/api/v2/organizations/default/templates/a4e8096d/versions/agreeable_glenn33/previous status=404 body={"message":"No previous template version found for \"agreeable_glenn33\"."}
[console][warning] Could not create web worker(s). Falling back to loading web worker code in main thread, which might cause UI freezes. Please see https://github.com/microsoft/monaco-editor#faq
[console][warning] You must define a function MonacoEnvironment.getWorkerUrl or MonacoEnvironment.getWorker
[console][error] Failed to load resource: the server responded with a status of 401 (Unauthorized)
[console][error] Failed to load resource: the server responded with a status of 401 (Unauthorized)
[response] url=http://localhost:3111/api/v2/users/me/appearance status=401 body={"message":"You are signed out or your session has expired. Please sign in again to continue.","detail":"Cookie \"coder_session_token\" or query parameter must be provided."}
[response] url=http://localhost:3111/api/v2/users/me status=401 body={"message":"You are signed out or your session has expired. Please sign in again to continue.","detail":"Cookie \"coder_session_token\" or query parameter must be provided."}
[console][error] Failed to load resource: the server responded with a status of 403 (Forbidden)
[response] url=http://localhost:3111/api/v2/deployment/stats status=403 body={"message":"Forbidden.","detail":"You don't have permission to view this content. If you believe this is a mistake, please contact your administrator or try signing in with different credentials."}
  ✓  2 …5 › create workspace with default and required parameters (7.0s)atus of 403 (Forbidden)
[response] url=http://localhost:3111/api/v2/deployment/stats status=403 body={"message":"Forbidden.","detail":"You don't have permission to view this content. If you believe this is a mistake, please contact your administrator or try signing in with different credentials."}
[console][error] Failed to load resource: the server responded with a status of 403 (Forbidden)
[response] url=http://localhost:3111/api/v2/deployment/stats status=403 body={"message":"Forbidden.","detail":"You don't have permission to view this content. If you believe this is a mistake, please contact your administrator or try signing in with different credentials."}

  2 passed (56.1s)
```

`23 LOL` (Lines of logs)

#### New

```sh
% pnpm playwright:test -g "create workspace with default and required parameters"

> coder-v2@ playwright:test /home/coder/coder/site
> playwright test --config=e2e/playwright.config.ts -g 'create workspace with default and required parameters'

...

Running 2 tests using 1 worker

  ✓  1 …e/setup/addUsersAndLicense.spec.ts:7:5 › setup deployment (8.7s)
     2 ….ts:79:5 › create workspace with default and required parameters
[console][error] Failed to load resource: the server responded with a status of 401 (Unauthorized)
[console][error] Failed to load resource: the server responded with a status of 401 (Unauthorized)
[response] url=http://localhost:3111/api/v2/users/me/appearance status=401 body={"message":"You are signed out or your session has expired. Please sign in again to continue.","detail":"Cookie \"coder_session_token\" or query parameter must be provided."}
[response] url=http://localhost:3111/api/v2/users/me status=401 body={"message":"You are signed out or your session has expired. Please sign in again to continue.","detail":"Cookie \"coder_session_token\" or query parameter must be provided."}
[console][warning] Could not create web worker(s). Falling back to loading web worker code in main thread, which might cause UI freezes. Please see https://github.com/microsoft/monaco-editor#faq
[console][warning] You must define a function MonacoEnvironment.getWorkerUrl or MonacoEnvironment.getWorker
  ✓  2 …5 › create workspace with default and required parameters (7.1s)atus of 401 (Unauthorized)
[console][error] Failed to load resource: the server responded with a status of 401 (Unauthorized)
[response] url=http://localhost:3111/api/v2/users/me/appearance status=401 body={"message":"You are signed out or your session has expired. Please sign in again to continue.","detail":"Cookie \"coder_session_token\" or query parameter must be provided."}
[response] url=http://localhost:3111/api/v2/users/me status=401 body={"message":"You are signed out or your session has expired. Please sign in again to continue.","detail":"Cookie \"coder_session_token\" or query parameter must be provided."}

  2 passed (32.0s)
```

`9 LOL` (Lines of logs)
2026-04-23 06:20:35 +10:00

3782 lines
101 KiB
TypeScript

/**
* @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 {
DeleteExternalAuthByIDResponse,
DynamicParametersRequest,
PostWorkspaceUsageRequest,
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<TypesGen.ServerSentEvent> => {
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<TypesGen.ServerSentEvent> => {
return new OneWayWebSocket({
apiRoute: `/api/v2/workspaces/${workspaceId}/watch-ws`,
});
};
export const watchChat = (
chatId: string,
afterMessageId?: number,
): OneWayWebSocketApi<TypesGen.ChatStreamEvent[]> => {
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<TypesGen.ChatWatchEvent> => {
const searchParams: Record<string, string> = {};
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<TypesGen.WorkspaceAgentListContainersResponse> => {
return new OneWayWebSocket({
apiRoute: `/api/v2/workspaceagents/${agentId}/containers/watch`,
});
};
type WatchInboxNotificationsParams = Readonly<{
read_status?: "read" | "unread" | "all";
}>;
export function watchInboxNotifications(
params?: WatchInboxNotificationsParams,
): OneWayWebSocket<TypesGen.GetInboxNotificationResponse> {
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"]>,
): 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<TypesGen.WorkspaceAgentLog[]>({
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<string, string> {
if ("q" in options) {
return options;
}
const params: Record<string, string> = {};
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 chatProviderConfigsPath = "/api/experimental/chats/providers";
const chatModelConfigsPath = "/api/experimental/chats/model-configs";
const userChatProviderConfigsPath =
"/api/experimental/chats/user-provider-configs";
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<string, number>;
require_telemetry?: boolean;
};
export type GetLicensesResponse = Omit<TypesGen.License, "claims"> & {
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<TypesGen.LoginWithPasswordResponse> => {
const payload = JSON.stringify({ email, password });
const response = await this.axios.post<TypesGen.LoginWithPasswordResponse>(
"/api/v2/users/login",
payload,
{ headers: { ...BASE_CONTENT_TYPE_JSON } },
);
return response.data;
};
convertToOAUTH = async (request: TypesGen.ConvertLoginRequest) => {
const response = await this.axios.post<TypesGen.OAuthConversionResponse>(
"/api/v2/users/me/convert-login",
request,
);
return response.data;
};
logout = async (): Promise<void> => {
return this.axios.post("/api/v2/users/logout");
};
getAuthenticatedUser = async () => {
const response = await this.axios.get<TypesGen.User>("/api/v2/users/me");
return response.data;
};
getUser = async (usernameOrId: string) => {
const response = await this.axios.get<TypesGen.User>(
`/api/v2/users/${encodeURIComponent(usernameOrId)}`,
);
return response.data;
};
getUserParameters = async (templateID: string) => {
const response = await this.axios.get<TypesGen.UserParameter[]>(
`/api/v2/users/me/autofill-parameters?template_id=${templateID}`,
);
return response.data;
};
getAuthMethods = async (): Promise<TypesGen.AuthMethods> => {
const response = await this.axios.get<TypesGen.AuthMethods>(
"/api/v2/users/authmethods",
);
return response.data;
};
getUserLoginType = async (): Promise<TypesGen.UserLoginType> => {
const response = await this.axios.get<TypesGen.UserLoginType>(
"/api/v2/users/me/login-type",
);
return response.data;
};
checkAuthorization = async <TResponse extends TypesGen.AuthorizationResponse>(
params: TypesGen.AuthorizationRequest,
) => {
const response = await this.axios.post<TResponse>(
"/api/v2/authcheck",
params,
);
return response.data;
};
getApiKey = async (): Promise<TypesGen.GenerateAPIKeyResponse> => {
const response = await this.axios.post<TypesGen.GenerateAPIKeyResponse>(
"/api/v2/users/me/keys",
);
return response.data;
};
getTokens = async (
params: TypesGen.TokensFilter,
): Promise<TypesGen.APIKeyWithOwner[]> => {
const response = await this.axios.get<TypesGen.APIKeyWithOwner[]>(
"/api/v2/users/me/keys/tokens",
{ params },
);
return response.data;
};
deleteToken = async (keyId: string): Promise<void> => {
await this.axios.delete(`/api/v2/users/me/keys/${keyId}`);
};
createToken = async (
params: TypesGen.CreateTokenRequest,
): Promise<TypesGen.GenerateAPIKeyResponse> => {
const response = await this.axios.post(
"/api/v2/users/me/keys/tokens",
params,
);
return response.data;
};
getTokenConfig = async (): Promise<TypesGen.TokenConfig> => {
const response = await this.axios.get(
"/api/v2/users/me/keys/tokens/tokenconfig",
);
return response.data;
};
getUsers = async (
options: TypesGen.UsersRequest,
signal?: AbortSignal,
): Promise<TypesGen.GetUsersResponse> => {
const url = getURLWithSearchParams("/api/v2/users", options);
const response = await this.axios.get<TypesGen.GetUsersResponse>(
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<TypesGen.MinimalUser[]> => {
const url = getURLWithSearchParams(
`/api/v2/organizations/${organizationId}/members/me/workspaces/available-users`,
options,
);
const response = await this.axios.get<TypesGen.MinimalUser[]>(
url.toString(),
{ signal },
);
return response.data;
};
createOrganization = async (params: TypesGen.CreateOrganizationRequest) => {
const response = await this.axios.post<TypesGen.Organization>(
"/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<TypesGen.Organization>(
`/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<TypesGen.Organization>(
`/api/v2/organizations/${organization}`,
);
};
/**
* @param organization Can be the organization's ID or name
*/
getOrganization = async (
organization: string,
): Promise<TypesGen.Organization> => {
const response = await this.axios.get<TypesGen.Organization>(
`/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<TypesGen.PaginatedMembersResponse>(url);
return response.data;
};
/**
* @param organization Can be the organization's ID or name
*/
getOrganizationRoles = async (organization: string) => {
const response = await this.axios.get<TypesGen.AssignableRoles[]>(
`/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<TypesGen.User> => {
const response = await this.axios.put<TypesGen.User>(
`/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<TypesGen.Role> => {
const response = await this.axios.post<TypesGen.Role>(
`/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<TypesGen.Role> => {
const response = await this.axios.put<TypesGen.Role>(
`/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<TypesGen.OrganizationMember>(
`/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<TypesGen.Organization[]> => {
const response = await this.axios.get<TypesGen.Organization[]>(
"/api/v2/organizations",
);
return response.data;
};
getMyOrganizations = async (): Promise<TypesGen.Organization[]> => {
const response = await this.axios.get<TypesGen.Organization[]>(
"/api/v2/users/me/organizations",
);
return response.data;
};
/**
* @param organization Can be the organization's ID or name
*/
getWorkspaceSharingSettings = async (
organization: string,
): Promise<TypesGen.WorkspaceSharingSettings> => {
const response = await this.axios.get<TypesGen.WorkspaceSharingSettings>(
`/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<TypesGen.WorkspaceSharingSettings> => {
const response = await this.axios.patch<TypesGen.WorkspaceSharingSettings>(
`/api/v2/organizations/${organization}/settings/workspace-sharing`,
data,
);
return response.data;
};
getProvisionerDaemonsByOrganization = async (
organization: string,
params?: GetProvisionerDaemonsParams,
): Promise<TypesGen.ProvisionerDaemon[]> => {
const response = await this.axios.get<TypesGen.ProvisionerDaemon[]>(
`/api/v2/organizations/${organization}/provisionerdaemons`,
{ params },
);
return response.data;
};
/**
* @param organization Can be the organization's ID or name
*/
getProvisionerDaemonGroupsByOrganization = async (
organization: string,
): Promise<TypesGen.ProvisionerKeyDaemons[]> => {
const response = await this.axios.get<TypesGen.ProvisionerKeyDaemons[]>(
`/api/v2/organizations/${organization}/provisionerkeys/daemons`,
);
return response.data;
};
getOrganizationIdpSyncSettings =
async (): Promise<TypesGen.OrganizationSyncSettings> => {
const response = await this.axios.get<TypesGen.OrganizationSyncSettings>(
"/api/v2/settings/idpsync/organization",
);
return response.data;
};
patchOrganizationIdpSyncSettings = async (
data: TypesGen.OrganizationSyncSettings,
) => {
const response = await this.axios.patch<TypesGen.Response>(
"/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<TypesGen.Response>(
`/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<TypesGen.Response>(
`/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<TypesGen.GroupSyncSettings> => {
const response = await this.axios.get<TypesGen.GroupSyncSettings>(
`/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<TypesGen.RoleSyncSettings> => {
const response = await this.axios.get<TypesGen.RoleSyncSettings>(
`/api/v2/organizations/${organization}/settings/idpsync/roles`,
);
return response.data;
};
getDeploymentIdpSyncFieldValues = async (
field: string,
): Promise<readonly string[]> => {
const params = new URLSearchParams();
params.set("claimField", field);
const response = await this.axios.get<readonly string[]>(
`/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<readonly string[]>(
`/api/v2/organizations/${organization}/settings/idpsync/field-values?${params}`,
);
return response.data;
};
getTemplate = async (templateId: string): Promise<TypesGen.Template> => {
const response = await this.axios.get<TypesGen.Template>(
`/api/v2/templates/${templateId}`,
);
return response.data;
};
getTemplates = async (
options?: GetTemplatesOptions | GetTemplatesQuery,
): Promise<TypesGen.Template[]> => {
const params = normalizeGetTemplatesOptions(options);
const response = await this.axios.get<TypesGen.Template[]>(
"/api/v2/templates",
{ params },
);
return response.data;
};
/**
* @param organization Can be the organization's ID or name
*/
getTemplatesByOrganization = async (
organization: string,
options?: GetTemplatesOptions,
): Promise<TypesGen.Template[]> => {
const params = normalizeGetTemplatesOptions(options);
const response = await this.axios.get<TypesGen.Template[]>(
`/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<TypesGen.Template> => {
const response = await this.axios.get<TypesGen.Template>(
`/api/v2/organizations/${organization}/templates/${name}`,
);
return response.data;
};
getTemplateVersion = async (
versionId: string,
): Promise<TypesGen.TemplateVersion> => {
const response = await this.axios.get<TypesGen.TemplateVersion>(
`/api/v2/templateversions/${versionId}`,
);
return response.data;
};
getTemplateVersionResources = async (
versionId: string,
): Promise<TypesGen.WorkspaceResource[]> => {
const response = await this.axios.get<TypesGen.WorkspaceResource[]>(
`/api/v2/templateversions/${versionId}/resources`,
);
return response.data;
};
getTemplateVersionVariables = async (
versionId: string,
): Promise<TypesGen.TemplateVersionVariable[]> => {
// 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<VerArray>(
`/api/v2/templateversions/${versionId}/variables`,
);
return response.data;
};
getTemplateVersions = async (
templateId: string,
): Promise<TypesGen.TemplateVersion[]> => {
const response = await this.axios.get<TypesGen.TemplateVersion[]>(
`/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<TypesGen.TemplateVersion> => {
const response = await this.axios.get<TypesGen.TemplateVersion>(
`/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<TypesGen.TemplateVersion>(
`/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<TypesGen.TemplateVersion> => {
const response = await this.axios.post<TypesGen.TemplateVersion>(
`/api/v2/organizations/${organization}/templateversions`,
data,
);
return response.data;
};
getTemplateVersionExternalAuth = async (
versionId: string,
): Promise<TypesGen.TemplateVersionExternalAuth[]> => {
const response = await this.axios.get(
`/api/v2/templateversions/${versionId}/external-auth`,
);
return response.data;
};
getTemplateVersionDynamicParameters = async (
versionId: string,
data: TypesGen.DynamicParametersRequest,
): Promise<TypesGen.DynamicParametersResponse> => {
const response = await this.axios.post(
`/api/v2/templateversions/${versionId}/dynamic-parameters/evaluate`,
data,
);
return response.data;
};
getTemplateVersionRichParameters = async (
versionId: string,
): Promise<TypesGen.TemplateVersionParameter[]> => {
const response = await this.axios.get(
`/api/v2/templateversions/${versionId}/rich-parameters`,
);
return response.data;
};
templateVersionDynamicParameters = (
versionId: string,
userId: string,
{
onMessage,
onError,
onClose,
}: {
onMessage: (response: TypesGen.DynamicParametersResponse) => void;
onError: (error: Error) => void;
onClose: () => void;
},
): WebSocket => {
const socket = createWebSocket(
`/api/v2/templateversions/${versionId}/dynamic-parameters`,
new URLSearchParams({ user_id: userId }),
);
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<TypesGen.Template> => {
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<TypesGen.Response>(
`/api/v2/templates/${templateId}/versions`,
data,
);
return response.data;
};
patchTemplateVersion = async (
templateVersionId: string,
data: TypesGen.PatchTemplateVersionRequest,
) => {
const response = await this.axios.patch<TypesGen.TemplateVersion>(
`/api/v2/templateversions/${templateVersionId}`,
data,
);
return response.data;
};
archiveTemplateVersion = async (templateVersionId: string) => {
const response = await this.axios.post<TypesGen.TemplateVersion>(
`/api/v2/templateversions/${templateVersionId}/archive`,
);
return response.data;
};
unarchiveTemplateVersion = async (templateVersionId: string) => {
const response = await this.axios.post<TypesGen.TemplateVersion>(
`/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<Blob> => {
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<TypesGen.Template | null> => {
const response = await this.axios.patch<TypesGen.Template>(
`/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<TypesGen.Template> => {
const response = await this.axios.delete<TypesGen.Template>(
`/api/v2/templates/${templateId}`,
);
return response.data;
};
invalidateTemplatePresets = async (
templateId: string,
): Promise<TypesGen.InvalidatePresetsResponse> => {
const response = await this.axios.post<TypesGen.InvalidatePresetsResponse>(
`/api/v2/templates/${templateId}/prebuilds/invalidate`,
);
return response.data;
};
getWorkspace = async (
workspaceId: string,
params?: TypesGen.WorkspaceOptions,
): Promise<TypesGen.Workspace> => {
const response = await this.axios.get<TypesGen.Workspace>(
`/api/v2/workspaces/${workspaceId}`,
{ params },
);
return response.data;
};
getWorkspaces = async (
req: TypesGen.WorkspacesRequest,
): Promise<TypesGen.WorkspacesResponse> => {
const url = getURLWithSearchParams("/api/v2/workspaces", req);
const response = await this.axios.get<TypesGen.WorkspacesResponse>(url);
return response.data;
};
getWorkspaceByOwnerAndName = async (
username: string,
workspaceName: string,
params?: TypesGen.WorkspaceOptions,
): Promise<TypesGen.Workspace> => {
const response = await this.axios.get<TypesGen.Workspace>(
`/api/v2/users/${username}/workspace/${workspaceName}`,
{ params },
);
return response.data;
};
getWorkspaceBuildByNumber = async (
username: string,
workspaceName: string,
buildNumber: number,
): Promise<TypesGen.WorkspaceBuild> => {
const response = await this.axios.get<TypesGen.WorkspaceBuild>(
`/api/v2/users/${username}/workspace/${workspaceName}/builds/${buildNumber}`,
);
return response.data;
};
waitForBuild = (build: TypesGen.WorkspaceBuild) => {
return new Promise<TypesGen.ProvisionerJob | undefined>((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<TypesGen.WorkspaceBuild> => {
const response = await this.axios.post(
`/api/v2/workspaces/${workspaceId}/builds`,
data,
);
return response.data;
};
getTemplateVersionPresets = async (
templateVersionId: string,
): Promise<TypesGen.Preset[] | null> => {
const response = await this.axios.get<TypesGen.Preset[]>(
`/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<TypesGen.Response> => {
const response = await this.axios.patch(
`/api/v2/workspacebuilds/${workspaceBuildId}/cancel`,
null,
{ params },
);
return response.data;
};
updateWorkspaceDormancy = async (
workspaceId: string,
dormant: boolean,
): Promise<TypesGen.Workspace> => {
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<void> => {
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<void> => {
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<TypesGen.WorkspaceBuild> => {
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<TypesGen.Response> => {
const response = await this.axios.patch(
`/api/v2/templateversions/${templateVersionId}/cancel`,
);
return response.data;
};
cancelTemplateVersionDryRun = async (
templateVersionId: string,
jobId: string,
): Promise<TypesGen.Response> => {
const response = await this.axios.patch(
`/api/v2/templateversions/${templateVersionId}/dry-run/${jobId}/cancel`,
);
return response.data;
};
createUser = async (
user: TypesGen.CreateUserRequestWithOrgs,
): Promise<TypesGen.User> => {
const response = await this.axios.post<TypesGen.User>(
"/api/v2/users",
user,
);
return response.data;
};
createWorkspace = async (
userId: string,
workspace: TypesGen.CreateWorkspaceRequest,
): Promise<TypesGen.Workspace> => {
const response = await this.axios.post<TypesGen.Workspace>(
`/api/v2/users/${userId}/workspaces`,
workspace,
);
return response.data;
};
patchWorkspace = async (
workspaceId: string,
data: TypesGen.UpdateWorkspaceRequest,
): Promise<void> => {
await this.axios.patch(`/api/v2/workspaces/${workspaceId}`, data);
};
getBuildInfo = async (): Promise<TypesGen.BuildInfoResponse> => {
const response = await this.axios.get("/api/v2/buildinfo");
return response.data;
};
getUpdateCheck = async (): Promise<TypesGen.UpdateCheckResponse> => {
const response = await this.axios.get("/api/v2/updatecheck");
return response.data;
};
putWorkspaceAutostart = async (
workspaceID: string,
autostart: TypesGen.UpdateWorkspaceAutostartRequest,
): Promise<void> => {
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<void> => {
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<TypesGen.User> => {
const response = await this.axios.put(
`/api/v2/users/${userId}/profile`,
data,
);
return response.data;
};
getAppearanceSettings =
async (): Promise<TypesGen.UserAppearanceSettings> => {
const response = await this.axios.get("/api/v2/users/me/appearance");
return response.data;
};
updateAppearanceSettings = async (
data: TypesGen.UpdateUserAppearanceSettingsRequest,
): Promise<TypesGen.UserAppearanceSettings> => {
const response = await this.axios.put("/api/v2/users/me/appearance", data);
return response.data;
};
getUserPreferenceSettings =
async (): Promise<TypesGen.UserPreferenceSettings> => {
const response = await this.axios.get("/api/v2/users/me/preferences");
return response.data;
};
updateUserPreferenceSettings = async (
req: TypesGen.UpdateUserPreferenceSettingsRequest,
): Promise<TypesGen.UserPreferenceSettings> => {
const response = await this.axios.put("/api/v2/users/me/preferences", req);
return response.data;
};
getUserQuietHoursSchedule = async (
userId: TypesGen.User["id"],
): Promise<TypesGen.UserQuietHoursScheduleResponse> => {
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<TypesGen.UserQuietHoursScheduleResponse> => {
const response = await this.axios.put(
`/api/v2/users/${userId}/quiet-hours`,
data,
);
return response.data;
};
activateUser = async (
userId: TypesGen.User["id"],
): Promise<TypesGen.User> => {
const response = await this.axios.put<TypesGen.User>(
`/api/v2/users/${userId}/status/activate`,
);
return response.data;
};
suspendUser = async (userId: TypesGen.User["id"]): Promise<TypesGen.User> => {
const response = await this.axios.put<TypesGen.User>(
`/api/v2/users/${userId}/status/suspend`,
);
return response.data;
};
deleteUser = async (userId: TypesGen.User["id"]): Promise<void> => {
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<boolean> => {
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<TypesGen.CreateFirstUserResponse> => {
const response = await this.axios.post("/api/v2/users/first", req);
return response.data;
};
updateUserPassword = async (
userId: TypesGen.User["id"],
updatePassword: TypesGen.UpdateUserPasswordRequest,
): Promise<void> => {
await this.axios.put(`/api/v2/users/${userId}/password`, updatePassword);
};
validateUserPassword = async (
password: string,
): Promise<TypesGen.ValidateUserPasswordResponse> => {
const response = await this.axios.post("/api/v2/users/validate-password", {
password,
});
return response.data;
};
getRoles = async (): Promise<Array<TypesGen.AssignableRoles>> => {
const response = await this.axios.get<TypesGen.AssignableRoles[]>(
"/api/v2/users/roles",
);
return response.data;
};
updateUserRoles = async (
roles: TypesGen.SlimRole["name"][],
userId: TypesGen.User["id"],
): Promise<TypesGen.User> => {
const response = await this.axios.put<TypesGen.User>(
`/api/v2/users/${userId}/roles`,
{ roles },
);
return response.data;
};
getUserSSHKey = async (userId = "me"): Promise<TypesGen.GitSSHKey> => {
const response = await this.axios.get<TypesGen.GitSSHKey>(
`/api/v2/users/${userId}/gitsshkey`,
);
return response.data;
};
regenerateUserSSHKey = async (userId = "me"): Promise<TypesGen.GitSSHKey> => {
const response = await this.axios.put<TypesGen.GitSSHKey>(
`/api/v2/users/${userId}/gitsshkey`,
);
return response.data;
};
getWorkspaceBuilds = async (
workspaceId: string,
req?: TypesGen.WorkspaceBuildsRequest,
) => {
const response = await this.axios.get<TypesGen.WorkspaceBuild[]>(
getURLWithSearchParams(`/api/v2/workspaces/${workspaceId}/builds`, req),
);
return response.data;
};
getWorkspaceBuildLogs = async (
buildId: string,
): Promise<TypesGen.ProvisionerJobLog[]> => {
const response = await this.axios.get<TypesGen.ProvisionerJobLog[]>(
`/api/v2/workspacebuilds/${buildId}/logs`,
);
return response.data;
};
getWorkspaceAgentLogs = async (
agentID: string,
): Promise<TypesGen.WorkspaceAgentLog[]> => {
const response = await this.axios.get<TypesGen.WorkspaceAgentLog[]>(
`/api/v2/workspaceagents/${agentID}/logs`,
);
return response.data;
};
putWorkspaceExtension = async (
workspaceId: string,
newDeadline: dayjs.Dayjs,
): Promise<void> => {
await this.axios.put(`/api/v2/workspaces/${workspaceId}/extend`, {
deadline: newDeadline,
});
};
refreshEntitlements = async (): Promise<void> => {
await this.axios.post("/api/v2/licenses/refresh-entitlements");
};
getEntitlements = async (): Promise<TypesGen.Entitlements> => {
try {
const response = await this.axios.get<TypesGen.Entitlements>(
"/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<TypesGen.Experiment[]> => {
try {
const response = await this.axios.get<TypesGen.Experiment[]>(
"/api/v2/experiments",
);
return response.data;
} catch (error) {
if (isAxiosError(error) && error.response?.status === 404) {
return [];
}
throw error;
}
};
getAvailableExperiments =
async (): Promise<TypesGen.AvailableExperiments> => {
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<TypesGen.ExternalAuth> => {
const res = await this.axios.get(`/api/v2/external-auth/${provider}`);
return res.data;
};
getExternalAuthDevice = async (
provider: string,
): Promise<TypesGen.ExternalAuthDevice> => {
const resp = await this.axios.get(
`/api/v2/external-auth/${provider}/device`,
);
return resp.data;
};
exchangeExternalAuthDevice = async (
provider: string,
req: TypesGen.ExternalAuthDeviceExchange,
): Promise<void> => {
const resp = await this.axios.post(
`/api/v2/external-auth/${provider}/device`,
req,
);
return resp.data;
};
getUserExternalAuthProviders =
async (): Promise<TypesGen.ListUserExternalAuthResponse> => {
const resp = await this.axios.get("/api/v2/external-auth");
return resp.data;
};
unlinkExternalAuthProvider = async (
provider: string,
): Promise<DeleteExternalAuthByIDResponse> => {
const resp = await this.axios.delete(`/api/v2/external-auth/${provider}`);
return resp.data;
};
getOAuth2GitHubDeviceFlowCallback = async (
code: string,
state: string,
): Promise<TypesGen.OAuth2DeviceFlowCallbackResponse> => {
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<TypesGen.ExternalAuthDevice> => {
const resp = await this.axios.get("/api/v2/users/oauth2/github/device");
return resp.data;
};
getOAuth2ProviderApps = async (
filter?: TypesGen.OAuth2ProviderAppFilter,
): Promise<TypesGen.OAuth2ProviderApp[]> => {
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<TypesGen.OAuth2ProviderApp> => {
const resp = await this.axios.get(`/api/v2/oauth2-provider/apps/${id}`);
return resp.data;
};
postOAuth2ProviderApp = async (
data: TypesGen.PostOAuth2ProviderAppRequest,
): Promise<TypesGen.OAuth2ProviderApp> => {
const response = await this.axios.post(
"/api/v2/oauth2-provider/apps",
data,
);
return response.data;
};
putOAuth2ProviderApp = async (
id: string,
data: TypesGen.PutOAuth2ProviderAppRequest,
): Promise<TypesGen.OAuth2ProviderApp> => {
const response = await this.axios.put(
`/api/v2/oauth2-provider/apps/${id}`,
data,
);
return response.data;
};
deleteOAuth2ProviderApp = async (id: string): Promise<void> => {
await this.axios.delete(`/api/v2/oauth2-provider/apps/${id}`);
};
getOAuth2ProviderAppSecrets = async (
id: string,
): Promise<TypesGen.OAuth2ProviderAppSecret[]> => {
const resp = await this.axios.get(
`/api/v2/oauth2-provider/apps/${id}/secrets`,
);
return resp.data;
};
postOAuth2ProviderAppSecret = async (
id: string,
): Promise<TypesGen.OAuth2ProviderAppSecretFull> => {
const resp = await this.axios.post(
`/api/v2/oauth2-provider/apps/${id}/secrets`,
);
return resp.data;
};
deleteOAuth2ProviderAppSecret = async (
appId: string,
secretId: string,
): Promise<void> => {
await this.axios.delete(
`/api/v2/oauth2-provider/apps/${appId}/secrets/${secretId}`,
);
};
revokeOAuth2ProviderApp = async (appId: string): Promise<void> => {
await this.axios.delete(`/oauth2/tokens?client_id=${appId}`);
};
getAuditLogs = async (
options: TypesGen.AuditLogsRequest,
): Promise<TypesGen.AuditLogResponse> => {
const url = getURLWithSearchParams("/api/v2/audit", options);
const response = await this.axios.get(url);
return response.data;
};
getConnectionLogs = async (
options: TypesGen.ConnectionLogsRequest,
): Promise<TypesGen.ConnectionLogResponse> => {
const url = getURLWithSearchParams("/api/v2/connectionlog", options);
const response = await this.axios.get(url);
return response.data;
};
getTemplateDAUs = async (
templateId: string,
): Promise<TypesGen.DAUsResponse> => {
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<TypesGen.DAUsResponse> => {
const response = await this.axios.get(
`/api/v2/insights/daus?tz_offset=${offset}`,
);
return response.data;
};
getTemplateACLAvailable = async (
templateId: string,
options: TypesGen.UsersRequest,
): Promise<TypesGen.ACLAvailable> => {
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<TypesGen.TemplateACL> => {
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<TypesGen.WorkspaceACL> => {
const response = await this.axios.get(
`/api/v2/workspaces/${workspaceId}/acl`,
);
return response.data;
};
updateWorkspaceACL = async (
workspaceId: string,
data: TypesGen.UpdateWorkspaceACL,
): Promise<void> => {
await this.axios.patch(`/api/v2/workspaces/${workspaceId}/acl`, data);
};
getApplicationsHost = async (): Promise<TypesGen.AppHostResponse> => {
const response = await this.axios.get("/api/v2/applications/host");
return response.data;
};
getGroups = async (
options: { userId?: string } = {},
): Promise<TypesGen.Group[]> => {
const params: Record<string, string> = {};
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<TypesGen.Group[]> => {
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<TypesGen.Group> => {
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<TypesGen.Group> => {
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<TypesGen.GroupMembersResponse> => {
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<TypesGen.Group> => {
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<void> => {
await this.axios.delete(`/api/v2/groups/${groupId}`);
};
getWorkspaceQuota = async (
organizationName: string,
username: string,
): Promise<TypesGen.WorkspaceQuota> => {
const response = await this.axios.get(
`/api/v2/organizations/${encodeURIComponent(organizationName)}/members/${encodeURIComponent(username)}/workspace-quota`,
);
return response.data;
};
getAgentListeningPorts = async (
agentID: string,
): Promise<TypesGen.WorkspaceAgentListeningPortsResponse> => {
const response = await this.axios.get(
`/api/v2/workspaceagents/${agentID}/listening-ports`,
);
return response.data;
};
getWorkspaceAgentSharedPorts = async (
workspaceID: string,
): Promise<TypesGen.WorkspaceAgentPortShares> => {
const response = await this.axios.get(
`/api/v2/workspaces/${workspaceID}/port-share`,
);
return response.data;
};
getWorkspaceAgentCredentials = async (
workspaceID: string,
agentName: string,
): Promise<TypesGen.ExternalAgentCredentials> => {
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<TypesGen.WorkspaceAgentPortShares> => {
const response = await this.axios.post(
`/api/v2/workspaces/${workspaceID}/port-share`,
req,
);
return response.data;
};
deleteWorkspaceAgentSharedPort = async (
workspaceID: string,
req: TypesGen.DeleteWorkspaceAgentPortShareRequest,
): Promise<TypesGen.WorkspaceAgentPortShares> => {
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<TypesGen.SSHConfigResponse> => {
const response = await this.axios.get("/api/v2/deployment/ssh");
return response.data;
};
getDeploymentConfig = async (): Promise<DeploymentConfig> => {
const response = await this.axios.get("/api/v2/deployment/config");
return response.data;
};
getDeploymentStats = async (): Promise<TypesGen.DeploymentStats> => {
const response = await this.axios.get("/api/v2/deployment/stats");
return response.data;
};
getReplicas = async (): Promise<TypesGen.Replica[]> => {
const response = await this.axios.get("/api/v2/replicas");
return response.data;
};
getFile = async (fileId: string): Promise<ArrayBuffer> => {
const response = await this.axios.get<ArrayBuffer>(
`/api/v2/files/${fileId}`,
{ responseType: "arraybuffer" },
);
return response.data;
};
getWorkspaceProxyRegions = async (): Promise<
TypesGen.RegionsResponse<TypesGen.Region>
> => {
const response =
await this.axios.get<TypesGen.RegionsResponse<TypesGen.Region>>(
"/api/v2/regions",
);
return response.data;
};
getWorkspaceProxies = async (): Promise<
TypesGen.RegionsResponse<TypesGen.WorkspaceProxy>
> => {
const response = await this.axios.get<
TypesGen.RegionsResponse<TypesGen.WorkspaceProxy>
>("/api/v2/workspaceproxies");
return response.data;
};
createWorkspaceProxy = async (
b: TypesGen.CreateWorkspaceProxyRequest,
): Promise<TypesGen.UpdateWorkspaceProxyResponse> => {
const response = await this.axios.post("/api/v2/workspaceproxies", b);
return response.data;
};
getAppearance = async (): Promise<TypesGen.AppearanceConfig> => {
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<TypesGen.AppearanceConfig> => {
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<TypesGen.TemplateExample[]> => {
const response = await this.axios.get("/api/v2/templates/examples");
return response.data;
};
uploadFile = async (file: File): Promise<TypesGen.UploadResponse> => {
const response = await this.axios.post("/api/v2/files", file, {
headers: { "Content-Type": file.type },
});
return response.data;
};
getTemplateVersionLogs = async (
versionId: string,
): Promise<TypesGen.ProvisionerJobLog[]> => {
const response = await this.axios.get<TypesGen.ProvisionerJobLog[]>(
`/api/v2/templateversions/${versionId}/logs`,
);
return response.data;
};
updateWorkspaceVersion = async (
workspace: TypesGen.Workspace,
): Promise<TypesGen.WorkspaceBuild> => {
const template = await this.getTemplate(workspace.template_id);
return this.startWorkspace(workspace.id, template.active_version_id);
};
getWorkspaceBuildParameters = async (
workspaceBuildId: TypesGen.WorkspaceBuild["id"],
): Promise<TypesGen.WorkspaceBuildParameter[]> => {
const response = await this.axios.get<TypesGen.WorkspaceBuildParameter[]>(
`/api/v2/workspacebuilds/${workspaceBuildId}/parameters`,
);
return response.data;
};
getLicenses = async (): Promise<GetLicensesResponse[]> => {
const response = await this.axios.get("/api/v2/licenses");
return response.data;
};
createLicense = async (
data: TypesGen.AddLicenseRequest,
): Promise<TypesGen.AddLicenseRequest> => {
const response = await this.axios.post("/api/v2/licenses", data);
return response.data;
};
removeLicense = async (licenseId: number): Promise<void> => {
await this.axios.delete(`/api/v2/licenses/${licenseId}`);
};
getDynamicParameters = async (
templateVersionId: string,
ownerId: string,
oldBuildParameters: TypesGen.WorkspaceBuildParameter[],
) => {
const request: DynamicParametersRequest = {
id: 1,
owner_id: ownerId,
inputs: Object.fromEntries(
new Map(oldBuildParameters.map((param) => [param.name, param.value])),
),
};
const dynamicParametersResponse =
await this.getTemplateVersionDynamicParameters(
templateVersionId,
request,
);
return dynamicParametersResponse.parameters.map((p) => ({
...p,
description_plaintext: p.description || "",
default_value: p.default_value?.valid ? p.default_value.value : "",
options: p.options
? p.options.map((opt) => ({
...opt,
value: opt.value?.valid ? opt.value.value : "",
}))
: [],
}));
};
/** Steps to change the workspace version
* - Get the latest template to access the latest active version
* - Get the current build parameters
* - Get the template parameters
* - Update the build parameters and check if there are missed parameters for
* the new version
* - If there are missing parameters raise an error
* - Create a build with the version and updated build parameters
*/
changeWorkspaceVersion = async (
workspace: TypesGen.Workspace,
templateVersionId: string,
newBuildParameters: TypesGen.WorkspaceBuildParameter[] = [],
isDynamicParametersEnabled = false,
): Promise<TypesGen.WorkspaceBuild> => {
const currentBuildParameters = await this.getWorkspaceBuildParameters(
workspace.latest_build.id,
);
let templateParameters: TypesGen.TemplateVersionParameter[] = [];
if (isDynamicParametersEnabled) {
templateParameters = await this.getDynamicParameters(
templateVersionId,
workspace.owner_id,
currentBuildParameters,
);
} else {
templateParameters =
await this.getTemplateVersionRichParameters(templateVersionId);
}
const missingParameters = getMissingParameters(
currentBuildParameters,
newBuildParameters,
templateParameters,
);
if (missingParameters.length > 0) {
throw new MissingBuildParameters(missingParameters, templateVersionId);
}
return this.postWorkspaceBuild(workspace.id, {
transition: "start",
template_version_id: templateVersionId,
rich_parameter_values: newBuildParameters,
});
};
/** Steps to update the workspace
* - Get the latest template to access the latest active version
* - Get the current build parameters
* - Get the template parameters
* - Update the build parameters and check if there are missed parameters for
* the newest version
* - If there are missing parameters raise an error
* - Stop the workspace with the current template version 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<TypesGen.WorkspaceBuild> => {
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);
}
}
// Stop the workspace if it is already running.
if (workspace.latest_build.status === "running") {
const stopBuild = await this.stopWorkspace(workspace.id);
const awaitedStopBuild = await this.waitForBuild(stopBuild);
// If the stop is canceled halfway through, we bail.
// This is the same behaviour as restartWorkspace.
if (awaitedStopBuild?.status === "canceled") {
return Promise.reject(
new Error("Workspace stop was canceled, not proceeding with update."),
);
}
}
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<TypesGen.ResolveAutostartResponse> => {
const response = await this.axios.get(
`/api/v2/workspaces/${workspaceId}/resolve-autostart`,
);
return response.data;
};
issueReconnectingPTYSignedToken = async (
params: TypesGen.IssueReconnectingPTYSignedTokenRequest,
): Promise<TypesGen.IssueReconnectingPTYSignedTokenResponse> => {
const response = await this.axios.post(
"/api/v2/applications/reconnecting-pty-signed-token",
params,
);
return response.data;
};
getInsightsUserLatency = async (
filters: InsightsParams,
): Promise<TypesGen.UserLatencyInsightsResponse> => {
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<TypesGen.UserActivityInsightsResponse> => {
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<TypesGen.GetUserStatusCountsResponse> => {
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<TypesGen.TemplateInsightsResponse> => {
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<TypesGen.HealthcheckReport>(
`/api/v2/debug/health?${params}`,
);
return response.data;
};
getHealthSettings = async (): Promise<TypesGen.HealthSettings> => {
const res = await this.axios.get<TypesGen.HealthSettings>(
"/api/v2/debug/health/settings",
);
return res.data;
};
updateHealthSettings = async (data: TypesGen.UpdateHealthSettings) => {
const response = await this.axios.put<TypesGen.HealthSettings>(
"/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<TypesGen.NotificationPreference[] | null>(
`/api/v2/users/${userId}/notifications/preferences`,
);
return res.data ?? [];
};
putUserNotificationPreferences = async (
userId: string,
req: TypesGen.UpdateUserNotificationPreferences,
) => {
const res = await this.axios.put<TypesGen.NotificationPreference[]>(
`/api/v2/users/${userId}/notifications/preferences`,
req,
);
return res.data;
};
getSystemNotificationTemplates = async () => {
const res = await this.axios.get<TypesGen.NotificationTemplate[]>(
"/api/v2/notifications/templates/system",
);
return res.data;
};
getCustomNotificationTemplates = async () => {
const res = await this.axios.get<TypesGen.NotificationTemplate[]>(
"/api/v2/notifications/templates/custom",
);
return res.data;
};
getNotificationDispatchMethods = async () => {
const res = await this.axios.get<TypesGen.NotificationMethodsResponse>(
"/api/v2/notifications/dispatch-methods",
);
return res.data;
};
updateNotificationTemplateMethod = async (
templateId: string,
req: TypesGen.UpdateNotificationTemplateMethod,
) => {
const res = await this.axios.put<void>(
`/api/v2/notifications/templates/${templateId}/method`,
req,
);
return res.data;
};
postTestNotification = async () => {
await this.axios.post<void>("/api/v2/notifications/test");
};
createWebPushSubscription = async (
userId: string,
req: TypesGen.WebpushSubscription,
) => {
await this.axios.post<void>(
`/api/v2/users/${userId}/webpush/subscription`,
req,
);
};
deleteWebPushSubscription = async (
userId: string,
req: TypesGen.DeleteWebpushSubscription,
) => {
await this.axios.delete<void>(
`/api/v2/users/${userId}/webpush/subscription`,
{
data: req,
},
);
};
requestOneTimePassword = async (
req: TypesGen.RequestOneTimePasscodeRequest,
) => {
await this.axios.post<void>("/api/v2/users/otp/request", req);
};
changePasswordWithOTP = async (
req: TypesGen.ChangePasswordWithOneTimePasscodeRequest,
) => {
await this.axios.post<void>("/api/v2/users/otp/change-password", req);
};
workspaceBuildTimings = async (workspaceBuildId: string) => {
const res = await this.axios.get<TypesGen.WorkspaceBuildTimings>(
`/api/v2/workspacebuilds/${workspaceBuildId}/timings`,
);
return res.data;
};
getProvisionerJobs = async (
orgId: string,
params: GetProvisionerJobsParams = {},
) => {
const res = await this.axios.get<TypesGen.ProvisionerJob[]>(
`/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<TypesGen.Response>(
`/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<TypesGen.WorkspaceAgentListContainersResponse>(
`/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<TypesGen.ListInboxNotificationsResponse>(
`/api/v2/notifications/inbox?${params.toString()}`,
);
return res.data;
};
updateInboxNotificationReadStatus = async (
notificationId: string,
req: TypesGen.UpdateInboxNotificationReadStatusRequest,
) => {
const res =
await this.axios.put<TypesGen.UpdateInboxNotificationReadStatusResponse>(
`/api/v2/notifications/inbox/${notificationId}/read-status`,
req,
);
return res.data;
};
markAllInboxNotificationsAsRead = async () => {
await this.axios.put<void>("/api/v2/notifications/inbox/mark-all-as-read");
};
createTask = async (
user: string,
req: TypesGen.CreateTaskRequest,
): Promise<TypesGen.Task> => {
const response = await this.axios.post<TypesGen.Task>(
`/api/v2/tasks/${user}`,
req,
);
return response.data;
};
getTasks = async (
filter: TypesGen.TasksFilter,
): Promise<readonly TypesGen.Task[]> => {
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<TypesGen.TasksListResponse>(
"/api/v2/tasks",
{
params: {
q: query.join(", "),
},
},
);
return res.data.tasks;
};
getTask = async (user: string, id: string): Promise<TypesGen.Task> => {
const response = await this.axios.get<TypesGen.Task>(
`/api/v2/tasks/${user}/${id}`,
);
return response.data;
};
deleteTask = async (user: string, id: string): Promise<void> => {
await this.axios.delete(`/api/v2/tasks/${user}/${id}`);
};
updateTaskInput = async (
user: string,
id: string,
input: string,
): Promise<void> => {
await this.axios.patch(`/api/v2/tasks/${user}/${id}/input`, {
input,
} satisfies TypesGen.UpdateTaskInputRequest);
};
getTaskLogs = async (
user: string,
id: string,
): Promise<TypesGen.TaskLogsResponse> => {
const response = await this.axios.get<TypesGen.TaskLogsResponse>(
`/api/v2/tasks/${user}/${id}/logs`,
);
return response.data;
};
pauseTask = async (
user: string,
id: string,
): Promise<TypesGen.PauseTaskResponse> => {
const response = await this.axios.post<TypesGen.PauseTaskResponse>(
`/api/v2/tasks/${user}/${id}/pause`,
);
return response.data;
};
resumeTask = async (
user: string,
id: string,
): Promise<TypesGen.ResumeTaskResponse> => {
const response = await this.axios.post<TypesGen.ResumeTaskResponse>(
`/api/v2/tasks/${user}/${id}/resume`,
);
return response.data;
};
sendTaskInput = async (
user: string,
id: string,
input: string,
): Promise<void> => {
await this.axios.post(`/api/v2/tasks/${user}/${id}/send`, {
input,
} satisfies TypesGen.TaskSendRequest);
};
createTaskFeedback = async (
_taskId: string,
_req: CreateTaskFeedbackRequest,
) => {
return new Promise<void>((res) => {
setTimeout(() => res(), 500);
});
};
getAIBridgeInterceptions = async (options: SearchParamOptions) => {
const url = getURLWithSearchParams(
"/api/v2/aibridge/interceptions",
options,
);
const response =
await this.axios.get<TypesGen.AIBridgeListInterceptionsResponse>(url);
return response.data;
};
getAIBridgeSessionList = async (options: SearchParamOptions) => {
const url = getURLWithSearchParams("/api/v2/aibridge/sessions", options);
const response =
await this.axios.get<TypesGen.AIBridgeListSessionsResponse>(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<TypesGen.AIBridgeSessionThreadsResponse>(url);
return response.data;
};
getAIBridgeModels = async (options: SearchParamOptions) => {
const url = getURLWithSearchParams("/api/v2/aibridge/models", options);
const response = await this.axios.get<string[]>(url);
return response.data;
};
getAIBridgeClients = async (options: SearchParamOptions) => {
const url = getURLWithSearchParams("/api/v2/aibridge/clients", options);
const response = await this.axios.get<string[]>(url);
return response.data;
};
}
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<Record<string, string>> => {
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<TypesGen.UploadChatFileResponse> => {
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<string> => {
const response = await this.axios.get(
`/api/experimental/chats/files/${fileId}`,
{ responseType: "text" },
);
return response.data as string;
};
// Chat API methods
getChats = async (req?: {
after_id?: string;
limit?: number;
offset?: number;
q?: string;
}): Promise<TypesGen.Chat[]> => {
const response = await this.axios.get<TypesGen.Chat[]>(
getURLWithSearchParams("/api/experimental/chats", req),
);
return response.data;
};
getChat = async (chatId: string): Promise<TypesGen.Chat> => {
const response = await this.axios.get<TypesGen.Chat>(
`/api/experimental/chats/${chatId}`,
);
return response.data;
};
getChatMessages = async (
chatId: string,
opts?: { before_id?: number; limit?: number },
): Promise<TypesGen.ChatMessagesResponse> => {
const params = new URLSearchParams();
if (opts?.before_id) {
params.set("before_id", opts.before_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<TypesGen.ChatMessagesResponse>(url);
return response.data;
};
createChat = async (
req: TypesGen.CreateChatRequest,
): Promise<TypesGen.Chat> => {
const response = await this.axios.post<TypesGen.Chat>(
"/api/experimental/chats",
req,
);
return response.data;
};
updateChat = async (
chatId: string,
req: UpdateChatRequestWithClearablePlanMode,
): Promise<void> => {
await this.axios.patch(`/api/experimental/chats/${chatId}`, req);
};
regenerateChatTitle = async (chatId: string): Promise<TypesGen.Chat> => {
const response = await this.axios.post<TypesGen.Chat>(
`/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<TypesGen.CreateChatMessageResponse> => {
const response = await this.axios.post<TypesGen.CreateChatMessageResponse>(
`/api/experimental/chats/${chatId}/messages`,
req,
);
return response.data;
};
editChatMessage = async (
chatId: string,
messageId: number,
req: TypesGen.EditChatMessageRequest,
): Promise<TypesGen.EditChatMessageResponse> => {
const response = await this.axios.patch<TypesGen.EditChatMessageResponse>(
`/api/experimental/chats/${chatId}/messages/${messageId}`,
req,
);
return response.data;
};
interruptChat = async (chatId: string): Promise<TypesGen.Chat> => {
const response = await this.axios.post<TypesGen.Chat>(
`/api/experimental/chats/${chatId}/interrupt`,
);
return response.data;
};
deleteChatQueuedMessage = async (
chatId: string,
queuedMessageId: number,
): Promise<void> => {
await this.axios.delete(
`/api/experimental/chats/${chatId}/queue/${queuedMessageId}`,
);
};
promoteChatQueuedMessage = async (
chatId: string,
queuedMessageId: number,
): Promise<TypesGen.ChatMessage> => {
const response = await this.axios.post<TypesGen.ChatMessage>(
`/api/experimental/chats/${chatId}/queue/${queuedMessageId}/promote`,
);
return response.data;
};
getChatDiffContents = async (
chatId: string,
): Promise<TypesGen.ChatDiffContents> => {
const response = await this.axios.get<TypesGen.ChatDiffContents>(
`/api/experimental/chats/${chatId}/diff`,
);
return response.data;
};
getChatModels = async (): Promise<TypesGen.ChatModelsResponse> => {
const response = await this.axios.get<TypesGen.ChatModelsResponse>(
"/api/experimental/chats/models",
);
return response.data;
};
getChatSystemPrompt =
async (): Promise<TypesGen.ChatSystemPromptResponse> => {
const response = await this.axios.get<TypesGen.ChatSystemPromptResponse>(
"/api/experimental/chats/config/system-prompt",
);
return response.data;
};
updateChatSystemPrompt = async (
req: TypesGen.UpdateChatSystemPromptRequest,
): Promise<void> => {
await this.axios.put("/api/experimental/chats/config/system-prompt", req);
};
getChatPlanModeInstructions =
async (): Promise<TypesGen.ChatPlanModeInstructionsResponse> => {
const response =
await this.axios.get<TypesGen.ChatPlanModeInstructionsResponse>(
"/api/experimental/chats/config/plan-mode-instructions",
);
return response.data;
};
updateChatPlanModeInstructions = async (
req: TypesGen.UpdateChatPlanModeInstructionsRequest,
): Promise<void> => {
await this.axios.put(
"/api/experimental/chats/config/plan-mode-instructions",
req,
);
};
getChatExploreModelOverride =
async (): Promise<TypesGen.ChatExploreModelOverrideResponse> => {
const response =
await this.axios.get<TypesGen.ChatExploreModelOverrideResponse>(
"/api/experimental/chats/config/explore-model-override",
);
return response.data;
};
updateChatExploreModelOverride = async (
req: TypesGen.UpdateChatExploreModelOverrideRequest,
): Promise<void> => {
await this.axios.put(
"/api/experimental/chats/config/explore-model-override",
req,
);
};
getChatDebugLogging =
async (): Promise<TypesGen.ChatDebugLoggingAdminSettings> => {
const response =
await this.axios.get<TypesGen.ChatDebugLoggingAdminSettings>(
"/api/experimental/chats/config/debug-logging",
);
return response.data;
};
updateChatDebugLogging = async (
req: TypesGen.UpdateChatDebugLoggingAllowUsersRequest,
): Promise<void> => {
await this.axios.put("/api/experimental/chats/config/debug-logging", req);
};
getUserChatDebugLogging =
async (): Promise<TypesGen.UserChatDebugLoggingSettings> => {
const response =
await this.axios.get<TypesGen.UserChatDebugLoggingSettings>(
"/api/experimental/chats/config/user-debug-logging",
);
return response.data;
};
updateUserChatDebugLogging = async (
req: TypesGen.UpdateUserChatDebugLoggingRequest,
): Promise<void> => {
await this.axios.put(
"/api/experimental/chats/config/user-debug-logging",
req,
);
};
getChatDebugRuns = async (
chatId: string,
): Promise<TypesGen.ChatDebugRunSummary[]> => {
const response = await this.axios.get<TypesGen.ChatDebugRunSummary[]>(
`/api/experimental/chats/${chatId}/debug/runs`,
);
return response.data;
};
getChatDebugRun = async (
chatId: string,
runId: string,
): Promise<TypesGen.ChatDebugRun> => {
const response = await this.axios.get<TypesGen.ChatDebugRun>(
`/api/experimental/chats/${chatId}/debug/runs/${runId}`,
);
return response.data;
};
getChatDesktopEnabled =
async (): Promise<TypesGen.ChatDesktopEnabledResponse> => {
const response =
await this.axios.get<TypesGen.ChatDesktopEnabledResponse>(
"/api/experimental/chats/config/desktop-enabled",
);
return response.data;
};
updateChatDesktopEnabled = async (
req: TypesGen.UpdateChatDesktopEnabledRequest,
): Promise<void> => {
await this.axios.put("/api/experimental/chats/config/desktop-enabled", req);
};
getChatWorkspaceTTL =
async (): Promise<TypesGen.ChatWorkspaceTTLResponse> => {
const response = await this.axios.get<TypesGen.ChatWorkspaceTTLResponse>(
"/api/experimental/chats/config/workspace-ttl",
);
return response.data;
};
getChatTemplateAllowlist =
async (): Promise<TypesGen.ChatTemplateAllowlist> => {
const response = await this.axios.get<TypesGen.ChatTemplateAllowlist>(
"/api/experimental/chats/config/template-allowlist",
);
return response.data;
};
updateChatWorkspaceTTL = async (
req: TypesGen.UpdateChatWorkspaceTTLRequest,
): Promise<void> => {
await this.axios.put("/api/experimental/chats/config/workspace-ttl", req);
};
getChatRetentionDays =
async (): Promise<TypesGen.ChatRetentionDaysResponse> => {
const response = await this.axios.get<TypesGen.ChatRetentionDaysResponse>(
"/api/experimental/chats/config/retention-days",
);
return response.data;
};
updateChatRetentionDays = async (
req: TypesGen.UpdateChatRetentionDaysRequest,
): Promise<void> => {
await this.axios.put("/api/experimental/chats/config/retention-days", req);
};
updateChatTemplateAllowlist = async (
req: TypesGen.ChatTemplateAllowlist,
): Promise<void> => {
await this.axios.put(
"/api/experimental/chats/config/template-allowlist",
req,
);
};
getUserChatCustomPrompt =
async (): Promise<TypesGen.UserChatCustomPrompt> => {
const response = await this.axios.get<TypesGen.UserChatCustomPrompt>(
"/api/experimental/chats/config/user-prompt",
);
return response.data;
};
updateUserChatCustomPrompt = async (
req: TypesGen.UserChatCustomPrompt,
): Promise<TypesGen.UserChatCustomPrompt> => {
const response = await this.axios.put<TypesGen.UserChatCustomPrompt>(
"/api/experimental/chats/config/user-prompt",
req,
);
return response.data;
};
getUserChatCompactionThresholds =
async (): Promise<TypesGen.UserChatCompactionThresholds> => {
const response =
await this.axios.get<TypesGen.UserChatCompactionThresholds>(
"/api/experimental/chats/config/user-compaction-thresholds",
);
return response.data;
};
updateUserChatCompactionThreshold = async (
modelConfigId: string,
req: TypesGen.UpdateUserChatCompactionThresholdRequest,
): Promise<TypesGen.UserChatCompactionThreshold> => {
const response = await this.axios.put<TypesGen.UserChatCompactionThreshold>(
`/api/experimental/chats/config/user-compaction-thresholds/${encodeURIComponent(modelConfigId)}`,
req,
);
return response.data;
};
deleteUserChatCompactionThreshold = async (
modelConfigId: string,
): Promise<void> => {
await this.axios.delete(
`/api/experimental/chats/config/user-compaction-thresholds/${encodeURIComponent(modelConfigId)}`,
);
};
getChatProviderConfigs = async (): Promise<TypesGen.ChatProviderConfig[]> => {
const response = await this.axios.get<TypesGen.ChatProviderConfig[]>(
chatProviderConfigsPath,
);
return response.data;
};
createChatProviderConfig = async (
req: TypesGen.CreateChatProviderConfigRequest,
): Promise<TypesGen.ChatProviderConfig> => {
const response = await this.axios.post<TypesGen.ChatProviderConfig>(
chatProviderConfigsPath,
req,
);
return response.data;
};
updateChatProviderConfig = async (
providerConfigId: string,
req: TypesGen.UpdateChatProviderConfigRequest,
): Promise<TypesGen.ChatProviderConfig> => {
const response = await this.axios.patch<TypesGen.ChatProviderConfig>(
`${chatProviderConfigsPath}/${encodeURIComponent(providerConfigId)}`,
req,
);
return response.data;
};
deleteChatProviderConfig = async (
providerConfigId: string,
): Promise<void> => {
await this.axios.delete(
`${chatProviderConfigsPath}/${encodeURIComponent(providerConfigId)}`,
);
};
getChatModelConfigs = async (): Promise<TypesGen.ChatModelConfig[]> => {
const response =
await this.axios.get<TypesGen.ChatModelConfig[]>(chatModelConfigsPath);
return response.data;
};
createChatModelConfig = async (
req: TypesGen.CreateChatModelConfigRequest,
): Promise<TypesGen.ChatModelConfig> => {
const response = await this.axios.post<TypesGen.ChatModelConfig>(
chatModelConfigsPath,
req,
);
return response.data;
};
updateChatModelConfig = async (
modelConfigId: string,
req: TypesGen.UpdateChatModelConfigRequest,
): Promise<TypesGen.ChatModelConfig> => {
const response = await this.axios.patch<TypesGen.ChatModelConfig>(
`${chatModelConfigsPath}/${encodeURIComponent(modelConfigId)}`,
req,
);
return response.data;
};
deleteChatModelConfig = async (modelConfigId: string): Promise<void> => {
await this.axios.delete(
`${chatModelConfigsPath}/${encodeURIComponent(modelConfigId)}`,
);
};
getUserChatProviderConfigs = async (): Promise<
TypesGen.UserChatProviderConfig[]
> => {
const response = await this.axios.get<TypesGen.UserChatProviderConfig[]>(
userChatProviderConfigsPath,
);
return response.data;
};
upsertUserChatProviderKey = async (
providerConfigId: string,
req: TypesGen.CreateUserChatProviderKeyRequest,
): Promise<TypesGen.UserChatProviderConfig> => {
const response = await this.axios.put<TypesGen.UserChatProviderConfig>(
`${userChatProviderConfigsPath}/${encodeURIComponent(providerConfigId)}`,
req,
);
return response.data;
};
deleteUserChatProviderKey = async (
providerConfigId: string,
): Promise<void> => {
await this.axios.delete(
`${userChatProviderConfigsPath}/${encodeURIComponent(providerConfigId)}`,
);
};
getMCPServerConfigs = async (): Promise<TypesGen.MCPServerConfig[]> => {
const response =
await this.axios.get<TypesGen.MCPServerConfig[]>(mcpServerConfigsPath);
return response.data;
};
createMCPServerConfig = async (
req: TypesGen.CreateMCPServerConfigRequest,
): Promise<TypesGen.MCPServerConfig> => {
const response = await this.axios.post<TypesGen.MCPServerConfig>(
mcpServerConfigsPath,
req,
);
return response.data;
};
updateMCPServerConfig = async (
id: string,
req: TypesGen.UpdateMCPServerConfigRequest,
): Promise<TypesGen.MCPServerConfig> => {
const response = await this.axios.patch<TypesGen.MCPServerConfig>(
`${mcpServerConfigsPath}/${encodeURIComponent(id)}`,
req,
);
return response.data;
};
deleteMCPServerConfig = async (id: string): Promise<void> => {
await this.axios.delete(
`${mcpServerConfigsPath}/${encodeURIComponent(id)}`,
);
};
getChatCostSummary = async (
user = "me",
params?: ChatCostDateParams,
): Promise<TypesGen.ChatCostSummary> => {
const url = getURLWithSearchParams(
`/api/experimental/chats/cost/${encodeURIComponent(user)}/summary`,
params,
);
const response = await this.axios.get<TypesGen.ChatCostSummary>(url);
return response.data;
};
getChatCostUsers = async (
params?: ChatCostUsersParams,
): Promise<TypesGen.ChatCostUsersResponse> => {
const url = getURLWithSearchParams(
"/api/experimental/chats/cost/users",
params,
);
const response = await this.axios.get<TypesGen.ChatCostUsersResponse>(url);
return response.data;
};
getPRInsights = async (params?: {
start_date?: string;
end_date?: string;
}): Promise<TypesGen.PRInsightsResponse> => {
const url = getURLWithSearchParams(
"/api/experimental/chats/insights/pull-requests",
params,
);
const response = await this.axios.get<TypesGen.PRInsightsResponse>(url);
return response.data;
};
getChatUsageLimitConfig =
async (): Promise<TypesGen.ChatUsageLimitConfigResponse> => {
const response =
await this.axios.get<TypesGen.ChatUsageLimitConfigResponse>(
"/api/experimental/chats/usage-limits",
);
return response.data;
};
getChatUsageLimitStatus =
async (): Promise<TypesGen.ChatUsageLimitStatus> => {
const response = await this.axios.get<TypesGen.ChatUsageLimitStatus>(
"/api/experimental/chats/usage-limits/status",
);
return response.data;
};
updateChatUsageLimitConfig = async (
req: TypesGen.ChatUsageLimitConfig,
): Promise<TypesGen.ChatUsageLimitConfig> => {
const response = await this.axios.put<TypesGen.ChatUsageLimitConfig>(
"/api/experimental/chats/usage-limits",
req,
);
return response.data;
};
upsertChatUsageLimitOverride = async (
userID: string,
req: TypesGen.UpsertChatUsageLimitOverrideRequest,
): Promise<TypesGen.ChatUsageLimitOverride> => {
const response = await this.axios.put<TypesGen.ChatUsageLimitOverride>(
`/api/experimental/chats/usage-limits/overrides/${encodeURIComponent(userID)}`,
req,
);
return response.data;
};
deleteChatUsageLimitOverride = async (userID: string): Promise<void> => {
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<TypesGen.ChatUsageLimitGroupOverride> => {
const response = await this.axios.put<TypesGen.ChatUsageLimitGroupOverride>(
`/api/experimental/chats/usage-limits/group-overrides/${encodeURIComponent(groupID)}`,
req,
);
return response.data;
};
deleteChatUsageLimitGroupOverride = async (
groupID: string,
): Promise<void> => {
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();