mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
4caa52844d
> [!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)
3782 lines
101 KiB
TypeScript
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();
|