Path: blob/main/components/gitpod-protocol/src/protocol.ts
2498 views
/**1* Copyright (c) 2020 Gitpod GmbH. All rights reserved.2* Licensed under the GNU Affero General Public License (AGPL).3* See License.AGPL.txt in the project root for license information.4*/56import { WorkspaceInstance, PortVisibility, PortProtocol, WorkspaceInstanceMetrics } from "./workspace-instance";7import { RoleOrPermission } from "./permission";8import { Project } from "./teams-projects-protocol";9import { createHash } from "crypto";10import { WorkspaceRegion } from "./workspace-cluster";1112export interface UserInfo {13name?: string;14}1516export interface User {17/** The user id */18id: string;1920/** The ID of the Organization this user is owned by. If undefined, the user is owned by the installation */21organizationId?: string;2223/** The timestamp when the user entry was created */24creationDate: string;2526avatarUrl?: string;2728name?: string;2930/** Optional for backwards compatibility */31fullName?: string;3233identities: Identity[];3435/**36* Whether the user has been blocked to use our service, because of TOS violation for example.37* Optional for backwards compatibility.38*/39blocked?: boolean;4041/** A map of random settings that alter the behaviour of Gitpod on a per-user basis */42featureFlags?: UserFeatureSettings;4344/** The permissions and/or roles the user has */45rolesOrPermissions?: RoleOrPermission[];4647/** Whether the user is logical deleted. This flag is respected by all queries in UserDB */48markedDeleted?: boolean;4950additionalData?: AdditionalUserData;5152// Identifies an explicit team or user ID to which all the user's workspace usage should be attributed to (e.g. for billing purposes)53usageAttributionId?: string;5455// The last time this user got verified somehow. The user is not verified if this is empty.56lastVerificationTime?: string;5758// The phone number used for the last phone verification.59verificationPhoneNumber?: string;6061// The FGA relationships version of this user62fgaRelationshipsVersion?: number;63}6465export namespace User {66export function is(data: any): data is User {67return data && data.hasOwnProperty("id") && data.hasOwnProperty("identities");68}69export function getIdentity(user: User, authProviderId: string): Identity | undefined {70return user.identities.find((id) => id.authProviderId === authProviderId);71}7273export function isOrganizationOwned(user: User) {74return !!user.organizationId;75}76}7778export interface WorkspaceTimeoutSetting {79// user globol workspace timeout80workspaceTimeout: string;81// control whether to enable the closed timeout of a workspace, i.e. close web ide, disconnect ssh connection82disabledClosedTimeout: boolean;83}8485export interface AdditionalUserData extends Partial<WorkspaceTimeoutSetting> {86/** @deprecated unused */87platforms?: UserPlatform[];88emailNotificationSettings?: EmailNotificationSettings;89featurePreview?: boolean;90ideSettings?: IDESettings;91// key is the name of the news, string the iso date when it was seen92whatsNewSeen?: { [key: string]: string };93// key is the name of the OAuth client i.e. local app, string the iso date when it was approved94// TODO(rl): provide a management UX to allow rescinding of approval95oauthClientsApproved?: { [key: string]: string };96// to remember GH Orgs the user installed/updated the GH App for97/** @deprecated unused */98knownGitHubOrgs?: string[];99// Git clone URL pointing to the user's dotfile repo100dotfileRepo?: string;101// preferred workspace classes102workspaceClasses?: WorkspaceClasses;103// additional user profile data104profile?: ProfileDetails;105/** @deprecated */106shouldSeeMigrationMessage?: boolean;107// remembered workspace auto start options108workspaceAutostartOptions?: WorkspaceAutostartOption[];109}110111export interface WorkspaceAutostartOption {112cloneURL: string;113organizationId: string;114workspaceClass?: string;115ideSettings?: IDESettings;116region?: WorkspaceRegion;117}118119export namespace AdditionalUserData {120export function set(user: User, partialData: Partial<AdditionalUserData>): User {121if (!user.additionalData) {122user.additionalData = {123...partialData,124};125} else {126user.additionalData = {127...user.additionalData,128...partialData,129};130}131return user;132}133}134135// The format in which we store User Profiles in136export interface ProfileDetails {137// when was the last time the user updated their profile information or has been nudged to do so.138lastUpdatedDetailsNudge?: string;139// when was the last time the user has accepted our privacy policy140acceptedPrivacyPolicyDate?: string;141// the user's company name142companyName?: string;143// the user's email144emailAddress?: string;145// type of role user has in their job146jobRole?: string;147// freeform entry for job role user works in (when jobRole is "other")148jobRoleOther?: string;149// Reasons user is exploring Gitpod when they signed up150explorationReasons?: string[];151// what user hopes to accomplish when they signed up152signupGoals?: string[];153// freeform entry for signup goals (when signupGoals is "other")154signupGoalsOther?: string;155// Set after a user completes the onboarding flow156onboardedTimestamp?: string;157// Onboarding question about a user's company size158companySize?: string;159// key-value pairs of dialogs in the UI which should only appear once. The value usually is a timestamp of the last dismissal160coachmarksDismissals?: { [key: string]: string };161}162163export interface EmailNotificationSettings {164allowsChangelogMail?: boolean;165allowsDevXMail?: boolean;166allowsOnboardingMail?: boolean;167}168169export type IDESettings = {170settingVersion?: string;171defaultIde?: string;172useLatestVersion?: boolean;173preferToolbox?: boolean;174// DEPRECATED: Use defaultIde after `settingVersion: 2.0`, no more specialify desktop or browser.175useDesktopIde?: boolean;176// DEPRECATED: Same with useDesktopIde.177defaultDesktopIde?: string;178};179180export interface WorkspaceClasses {181regular?: string;182/**183* @deprecated see Project.settings.prebuilds.workspaceClass184*/185prebuild?: string;186}187188export interface UserPlatform {189uid: string;190userAgent: string;191browser: string;192os: string;193lastUsed: string;194firstUsed: string;195/**196* Since when does the user have the browser extension installe don this device.197*/198browserExtensionInstalledSince?: string;199200/**201* Since when does the user not have the browser extension installed anymore (but previously had).202*/203browserExtensionUninstalledSince?: string;204}205206export interface UserFeatureSettings {207/**208* Permanent feature flags are added to each and every workspace instance209* this user starts.210*/211permanentWSFeatureFlags?: NamedWorkspaceFeatureFlag[];212}213214export type BillingTier = "paid" | "free";215216/**217* The values of this type MUST MATCH enum values in WorkspaceFeatureFlag from ws-manager/client/core_pb.d.ts218* If they don't we'll break things during workspace startup.219*/220export const WorkspaceFeatureFlags = {221full_workspace_backup: undefined,222workspace_class_limiting: undefined,223workspace_connection_limiting: undefined,224workspace_psi: undefined,225ssh_ca: undefined,226};227export type NamedWorkspaceFeatureFlag = keyof typeof WorkspaceFeatureFlags;228export namespace NamedWorkspaceFeatureFlag {229export const WORKSPACE_PERSISTED_FEATTURE_FLAGS: NamedWorkspaceFeatureFlag[] = ["full_workspace_backup"];230export function isWorkspacePersisted(ff: NamedWorkspaceFeatureFlag): boolean {231return WORKSPACE_PERSISTED_FEATTURE_FLAGS.includes(ff);232}233}234235export type EnvVar = UserEnvVar | ProjectEnvVarWithValue | OrgEnvVarWithValue | EnvVarWithValue;236237export interface EnvVarWithValue {238name: string;239value: string;240}241242export interface ProjectEnvVarWithValue extends EnvVarWithValue {243id?: string;244/** If a project-scoped env var is "censored", it is only visible in Prebuilds */245censored: boolean;246}247248export interface ProjectEnvVar extends Omit<ProjectEnvVarWithValue, "value"> {249id: string;250projectId: string;251}252253export interface OrgEnvVarWithValue extends EnvVarWithValue {254id?: string;255}256257export interface OrgEnvVar extends Omit<OrgEnvVarWithValue, "value"> {258id: string;259orgId: string;260}261262export interface UserEnvVarValue extends EnvVarWithValue {263id?: string;264repositoryPattern: string; // DEPRECATED: Use ProjectEnvVar instead of repositoryPattern - https://github.com/gitpod-com/gitpod/issues/5322265}266export interface UserEnvVar extends UserEnvVarValue {267id: string;268userId: string;269deleted?: boolean;270}271272export namespace EnvVar {273export const GITPOD_IMAGE_AUTH_ENV_VAR_NAME = "GITPOD_IMAGE_AUTH";274/**275* - GITPOD_IMAGE_AUTH is documented https://www.gitpod.io/docs/configure/workspaces/workspace-image#use-a-private-docker-image276*/277export const WhiteListFromReserved = [GITPOD_IMAGE_AUTH_ENV_VAR_NAME];278279export function is(data: any): data is EnvVar {280return data.hasOwnProperty("name") && data.hasOwnProperty("value");281}282283/**284* Extracts the "host:credentials" pairs from the GITPOD_IMAGE_AUTH environment variable.285* @param envVars286* @returns A map of host to credentials287*/288export function getGitpodImageAuth(envVars: EnvVarWithValue[]): Map<string, string> {289const res = new Map<string, string>();290const imageAuth = envVars.find((e) => e.name === EnvVar.GITPOD_IMAGE_AUTH_ENV_VAR_NAME);291if (!imageAuth) {292return res;293}294295const parse = (parts: string[]): { host: string; auth: string } | undefined => {296if (parts.some((e) => e === "")) {297return undefined;298}299if (parts.length === 2) {300return { host: parts[0], auth: parts[1] };301} else if (parts.length === 3) {302return { host: `${parts[0]}:${parts[1]}`, auth: parts[2] };303}304return undefined;305};306307(imageAuth.value || "")308.split(",")309.map((e) => e.split(":").map((part) => part.trim()))310.forEach((parts) => {311const parsed = parse(parts);312if (parsed) {313res.set(parsed.host, parsed.auth);314}315});316317return res;318}319}320321export namespace UserEnvVar {322export const DELIMITER = "/";323export const WILDCARD_ASTERISK = "*";324// This wildcard is only allowed on the last segment, and matches arbitrary segments to the right325export const WILDCARD_DOUBLE_ASTERISK = "**";326const WILDCARD_SHARP = "#"; // TODO(gpl) Where does this come from? Bc we have/had patterns as part of URLs somewhere, maybe...?327const MIN_PATTERN_SEGMENTS = 2;328329function isWildcard(c: string): boolean {330return c === WILDCARD_ASTERISK || c === WILDCARD_SHARP;331}332333export function is(data: any): data is UserEnvVar {334return (335EnvVar.is(data) &&336data.hasOwnProperty("id") &&337data.hasOwnProperty("userId") &&338data.hasOwnProperty("repositoryPattern")339);340}341342/**343* @param variable344* @returns Either a string containing an error message or undefined.345*/346export function validate(variable: UserEnvVarValue): string | undefined {347const name = variable.name;348const pattern = variable.repositoryPattern;349if (!EnvVar.WhiteListFromReserved.includes(name) && name.startsWith("GITPOD_")) {350return "Name with prefix 'GITPOD_' is reserved.";351}352if (name.trim() === "") {353return "Name must not be empty.";354}355if (name.length > 255) {356return "Name too long. Maximum name length is 255 characters.";357}358if (!/^[a-zA-Z_]+[a-zA-Z0-9_]*$/.test(name)) {359return "Name must match /^[a-zA-Z_]+[a-zA-Z0-9_]*$/.";360}361if (variable.value.trim() === "") {362return "Value must not be empty.";363}364if (variable.value.length > 32767) {365return "Value too long. Maximum value length is 32767 characters.";366}367if (pattern.trim() === "") {368return "Scope must not be empty.";369}370const split = splitRepositoryPattern(pattern);371if (split.length < MIN_PATTERN_SEGMENTS) {372return "A scope must use the form 'organization/repo'.";373}374for (const name of split) {375if (name !== "*") {376if (!/^[a-zA-Z0-9_\-.\*]+$/.test(name)) {377return "Invalid scope segment. Only ASCII characters, numbers, -, _, . or * are allowed.";378}379}380}381return undefined;382}383384export function normalizeRepoPattern(pattern: string) {385return pattern.toLocaleLowerCase();386}387388function splitRepositoryPattern(pattern: string): string[] {389return pattern.split(DELIMITER);390}391392function joinRepositoryPattern(...parts: string[]): string {393return parts.join(DELIMITER);394}395396/**397* Matches the given EnvVar pattern against the fully qualified name of the repository:398* - GitHub: "repo/owner"399* - GitLab: "some/nested/repo" (up to MAX_PATTERN_SEGMENTS deep)400* @param pattern401* @param repoFqn402* @returns True if the pattern matches that fully qualified name403*/404export function matchEnvVarPattern(pattern: string, repoFqn: string): boolean {405const fqnSegments = splitRepositoryPattern(repoFqn);406const patternSegments = splitRepositoryPattern(pattern);407if (patternSegments.length < MIN_PATTERN_SEGMENTS) {408// Technically not a problem, but we should not allow this for safety reasons.409// And because we hisotrically never allowed this (GitHub would always require at least "*/*") we are just keeping old constraints.410// Note: Most importantly this guards against arbitrary wildcard matches411return false;412}413414function isWildcardMatch(patternSegment: string, isLastSegment: boolean): boolean {415if (isWildcard(patternSegment)) {416return true;417}418return isLastSegment && patternSegment === WILDCARD_DOUBLE_ASTERISK;419}420let i = 0;421for (; i < patternSegments.length; i++) {422if (i >= fqnSegments.length) {423return false;424}425const fqnSegment = fqnSegments[i];426const patternSegment = patternSegments[i];427const isLastSegment = patternSegments.length === i + 1;428if (!isWildcardMatch(patternSegment, isLastSegment) && patternSegment !== fqnSegment.toLocaleLowerCase()) {429return false;430}431}432if (fqnSegments.length > i) {433// Special case: "**" as last segment matches arbitrary # of segments to the right434if (patternSegments[i - 1] === WILDCARD_DOUBLE_ASTERISK) {435return true;436}437return false;438}439return true;440}441442// Matches the following patterns for "some/nested/repo", ordered by highest score:443// - exact: some/nested/repo ()444// - partial:445// - some/nested/*446// - some/*447// - generic:448// - */*/*449// - */*450// Does NOT match:451// - */repo (number of parts does not match)452// cmp. test cases in env-var-service.spec.ts453export function filter<T extends UserEnvVarValue>(vars: T[], owner: string, repo: string): T[] {454const matchedEnvVars = vars.filter((e) =>455matchEnvVarPattern(e.repositoryPattern, joinRepositoryPattern(owner, repo)),456);457const resmap = new Map<string, T[]>();458matchedEnvVars.forEach((e) => {459const l = resmap.get(e.name) || [];460l.push(e);461resmap.set(e.name, l);462});463464// In cases of multiple matches per env var: find the best match465// Best match is the most specific pattern, where the left-most segment is most significant466function scoreMatchedEnvVar(e: T): number {467function valueSegment(segment: string): number {468switch (segment) {469case WILDCARD_ASTERISK:470return 2;471case WILDCARD_SHARP:472return 2;473case WILDCARD_DOUBLE_ASTERISK:474return 1;475default:476return 3;477}478}479const patternSegments = splitRepositoryPattern(e.repositoryPattern);480let result = 0;481for (const segment of patternSegments) {482// We can assume the pattern matches from context, so we just need to check whether it's a wildcard or not483const val = valueSegment(segment);484result = result * 10 + val;485}486return result;487}488const result = [];489for (const name of resmap.keys()) {490const candidates = resmap.get(name);491if (!candidates) {492// not sure how this can happen, but so be it493continue;494}495496if (candidates.length == 1) {497result.push(candidates[0]);498continue;499}500501let bestCandidate = candidates[0];502let bestScore = scoreMatchedEnvVar(bestCandidate);503for (const e of candidates.slice(1)) {504const score = scoreMatchedEnvVar(e);505if (score > bestScore) {506bestScore = score;507bestCandidate = e;508}509}510result.push(bestCandidate!);511}512513return result;514}515}516517export interface SSHPublicKeyValue {518name: string;519key: string;520}521export interface UserSSHPublicKey extends SSHPublicKeyValue {522id: string;523key: string;524userId: string;525fingerprint: string;526creationTime: string;527lastUsedTime?: string;528}529530export type UserSSHPublicKeyValue = Omit<UserSSHPublicKey, "userId">;531532export namespace SSHPublicKeyValue {533export function validate(value: SSHPublicKeyValue): string | undefined {534if (value.name.length === 0) {535return "Title must not be empty.";536}537if (value.name.length > 255) {538return "Title too long. Maximum value length is 255 characters.";539}540if (value.key.length === 0) {541return "Key must not be empty.";542}543try {544getData(value);545} catch (e) {546return "Key is invalid. You must supply a key in OpenSSH public key format.";547}548return;549}550551export function getData(value: SSHPublicKeyValue) {552// Begins with 'ssh-rsa', 'ecdsa-sha2-nistp256', 'ecdsa-sha2-nistp384', 'ecdsa-sha2-nistp521', 'ssh-ed25519', '[email protected]', or '[email protected]'.553const regex =554/^(?<type>ssh-rsa|ecdsa-sha2-nistp256|ecdsa-sha2-nistp384|ecdsa-sha2-nistp521|ssh-ed25519|sk-ecdsa-sha2-nistp256@openssh\.com|sk-ssh-ed25519@openssh\.com) (?<key>.*?)( (?<email>.*?))?$/;555const resultGroup = regex.exec(value.key.trim());556if (!resultGroup) {557throw new Error("Key is invalid.");558}559return {560type: resultGroup.groups?.["type"] as string,561key: resultGroup.groups?.["key"] as string,562email: resultGroup.groups?.["email"] || undefined,563};564}565566export function getFingerprint(value: SSHPublicKeyValue) {567const data = getData(value);568const buf = Buffer.from(data.key, "base64");569// gitlab style570// const hash = createHash("md5").update(buf).digest("hex");571// github style572const hash = createHash("sha256").update(buf).digest("base64");573return hash;574}575576export const MAXIMUM_KEY_LENGTH = 5;577}578579export interface GitpodToken {580/** Hash value (SHA256) of the token (primary key). */581tokenHash: string;582583/** Human readable name of the token */584name?: string;585586/** Token kind */587type: GitpodTokenType;588589/** The user the token belongs to. */590userId: string;591592/** Scopes (e.g. limition to read-only) */593scopes: string[];594595/** Created timestamp */596created: string;597}598599export enum GitpodTokenType {600API_AUTH_TOKEN = 0,601MACHINE_AUTH_TOKEN = 1,602}603604export interface OneTimeSecret {605id: string;606607value: string;608609expirationTime: string;610611deleted: boolean;612}613614export interface WorkspaceInstanceUser {615name?: string;616avatarUrl?: string;617instanceId: string;618userId: string;619lastSeen: string;620}621622export interface Identity {623authProviderId: string;624authId: string;625authName: string;626primaryEmail?: string;627/** This is a flag that triggers the HARD DELETION of this entity */628deleted?: boolean;629// The last time this entry was touched during a signin. It's only set for SSO identities.630lastSigninTime?: string;631632// @deprecated as no longer in use since '19633readonly?: boolean;634}635636export type IdentityLookup = Pick<Identity, "authProviderId" | "authId">;637638export namespace Identity {639export function is(data: any): data is Identity {640return (641data.hasOwnProperty("authProviderId") && data.hasOwnProperty("authId") && data.hasOwnProperty("authName")642);643}644export function equals(id1: IdentityLookup, id2: IdentityLookup) {645return id1.authProviderId === id2.authProviderId && id1.authId === id2.authId;646}647}648649export interface Token {650value: string;651scopes: string[];652updateDate?: string;653expiryDate?: string;654reservedUntilDate?: string;655idToken?: string;656refreshToken?: string;657username?: string;658}659export interface TokenEntry {660uid: string;661authProviderId: string;662authId: string;663token: Token;664expiryDate?: string;665reservedUntilDate?: string;666refreshable?: boolean;667}668669export interface EmailDomainFilterEntry {670domain: string;671negative: boolean;672}673674export type AppInstallationPlatform = "github";675export type AppInstallationState = "claimed.user" | "claimed.platform" | "installed" | "uninstalled";676export interface AppInstallation {677platform: AppInstallationPlatform;678installationID: string;679ownerUserID?: string;680platformUserID?: string;681state: AppInstallationState;682creationTime: string;683lastUpdateTime: string;684}685686export interface PendingGithubEvent {687id: string;688githubUserId: string;689creationDate: Date;690type: string;691event: string;692deleted: boolean;693}694695export interface Snapshot {696id: string;697creationTime: string;698availableTime?: string;699originalWorkspaceId: string;700bucketId: string;701state: SnapshotState;702message?: string;703}704705export type SnapshotState = "pending" | "available" | "error";706707export interface Workspace {708id: string;709creationTime: string;710organizationId: string;711contextURL: string;712description: string;713ownerId: string;714projectId?: string;715context: WorkspaceContext;716config: WorkspaceConfig;717718/**719* The source where to get the workspace base image from. This source is resolved720* during workspace creation. Once a base image has been built the information in here721* is superseded by baseImageNameResolved.722*/723imageSource?: WorkspaceImageSource;724725/**726* The resolved, fix name of the workspace image. We only use this727* to access the logs during an image build.728*/729imageNameResolved?: string;730731/**732* The resolved/built fixed named of the base image. This field is only set if the workspace733* already has its base image built.734*/735baseImageNameResolved?: string;736737shareable?: boolean;738pinned?: boolean;739740// workspace is hard-deleted on the database and about to be collected by periodic deleter741readonly deleted?: boolean;742743/**744* Mark as deleted (user-facing). The actual deletion of the workspace content is executed745* with a (configurable) delay746*/747softDeleted?: WorkspaceSoftDeletion;748749/**750* Marks the time when the workspace was marked as softDeleted. The actual deletion of the751* workspace content happens after a configurable period752*/753softDeletedTime?: string;754755/**756* Marks the time when the workspace content has been deleted.757*/758contentDeletedTime?: string;759760/**761* The time when the workspace is eligible for soft deletion. This is the time when the workspace762* is marked as softDeleted earliest.763*/764deletionEligibilityTime?: string;765766type: WorkspaceType;767768basedOnPrebuildId?: string;769770basedOnSnapshotId?: string;771}772773export type WorkspaceSoftDeletion = "user" | "gc";774775export type WorkspaceType = "regular" | "prebuild" | "imagebuild";776777export namespace Workspace {778export function getPullRequestNumber(ws: Workspace): number | undefined {779if (PullRequestContext.is(ws.context)) {780return ws.context.nr;781}782return undefined;783}784785export function getIssueNumber(ws: Workspace): number | undefined {786if (IssueContext.is(ws.context)) {787return ws.context.nr;788}789return undefined;790}791792export function getBranchName(ws: Workspace): string | undefined {793if (CommitContext.is(ws.context)) {794return ws.context.ref;795}796return undefined;797}798799export function getCommit(ws: Workspace): string | undefined {800if (CommitContext.is(ws.context)) {801return ws.context.revision && ws.context.revision.substr(0, 8);802}803return undefined;804}805806const NAME_PREFIX = "named:";807export function fromWorkspaceName(name?: Workspace["description"]): string | undefined {808if (name?.startsWith(NAME_PREFIX)) {809return name.slice(NAME_PREFIX.length);810}811return undefined;812}813export function toWorkspaceName(name?: Workspace["description"]): string {814if (!name || name?.trim().length === 0) {815return "no-name";816}817return `${NAME_PREFIX}${name}`;818}819}820821export interface GuessGitTokenScopesParams {822host: string;823repoUrl: string;824gitCommand: string;825}826827export interface GuessedGitTokenScopes {828message?: string;829scopes?: string[];830}831832export interface VSCodeConfig {833extensions?: string[];834}835836export interface JetBrainsConfig {837intellij?: JetBrainsProductConfig;838goland?: JetBrainsProductConfig;839pycharm?: JetBrainsProductConfig;840phpstorm?: JetBrainsProductConfig;841rubymine?: JetBrainsProductConfig;842webstorm?: JetBrainsProductConfig;843rider?: JetBrainsProductConfig;844clion?: JetBrainsProductConfig;845rustrover?: JetBrainsProductConfig;846}847export interface JetBrainsProductConfig {848prebuilds?: JetBrainsPrebuilds;849vmoptions?: string;850}851export interface JetBrainsPrebuilds {852version?: "stable" | "latest" | "both";853}854855export interface RepositoryCloneInformation {856url: string;857checkoutLocation?: string;858}859860export interface CoreDumpConfig {861enabled?: boolean;862softLimit?: number;863hardLimit?: number;864}865866export interface WorkspaceConfig {867mainConfiguration?: string;868additionalRepositories?: RepositoryCloneInformation[];869image?: ImageConfig;870ports?: PortConfig[];871tasks?: TaskConfig[];872checkoutLocation?: string;873workspaceLocation?: string;874gitConfig?: { [config: string]: string };875github?: GithubAppConfig;876vscode?: VSCodeConfig;877jetbrains?: JetBrainsConfig;878coreDump?: CoreDumpConfig;879ideCredentials?: string;880env?: { [env: string]: any };881882/** deprecated. Enabled by default **/883experimentalNetwork?: boolean;884885/**886* Where the config object originates from.887*888* repo - from the repository889* derived - computed based on analyzing the repository890* additional-content - config comes from additional content, usually provided through the project's configuration891* default - our static catch-all default config892*/893_origin?: "repo" | "derived" | "additional-content" | "default";894895/**896* Set of automatically infered feature flags. That's not something the user can set, but897* that is set by gitpod at workspace creation time.898*/899_featureFlags?: NamedWorkspaceFeatureFlag[];900}901902export interface GithubAppConfig {903prebuilds?: GithubAppPrebuildConfig;904}905export interface GithubAppPrebuildConfig {906master?: boolean;907branches?: boolean;908pullRequests?: boolean;909pullRequestsFromForks?: boolean;910addCheck?: boolean | "prevent-merge-on-error";911addBadge?: boolean;912addLabel?: boolean | string;913addComment?: boolean;914}915export namespace GithubAppPrebuildConfig {916export function is(obj: boolean | GithubAppPrebuildConfig): obj is GithubAppPrebuildConfig {917return !(typeof obj === "boolean");918}919}920921export type WorkspaceImageSource = WorkspaceImageSourceDocker | WorkspaceImageSourceReference;922export interface WorkspaceImageSourceDocker {923dockerFilePath: string;924dockerFileHash: string;925dockerFileSource?: Commit;926}927export namespace WorkspaceImageSourceDocker {928export function is(obj: object): obj is WorkspaceImageSourceDocker {929return "dockerFileHash" in obj && "dockerFilePath" in obj;930}931}932export interface WorkspaceImageSourceReference {933/** The resolved, fix base image reference */934baseImageResolved: string;935}936export namespace WorkspaceImageSourceReference {937export function is(obj: object): obj is WorkspaceImageSourceReference {938return "baseImageResolved" in obj;939}940}941942export type PrebuiltWorkspaceState =943// the prebuild is queued and may start at anytime944| "queued"945// the workspace prebuild is currently running (i.e. there's a workspace pod deployed)946| "building"947// the prebuild was aborted948| "aborted"949// the prebuild timed out950| "timeout"951// the prebuild has finished (even if a headless task failed) and a snapshot is available952| "available"953// the prebuild (headless workspace) failed due to some system error954| "failed";955956export interface PrebuiltWorkspace {957id: string;958cloneURL: string;959branch?: string;960projectId?: string;961commit: string;962buildWorkspaceId: string;963creationTime: string;964state: PrebuiltWorkspaceState;965statusVersion: number;966error?: string;967snapshot?: string;968}969970export type PrebuiltWorkspaceWithWorkspace = PrebuiltWorkspace & { workspace: Workspace };971972export namespace PrebuiltWorkspace {973export function isDone(pws: PrebuiltWorkspace) {974return (975pws.state === "available" || pws.state === "timeout" || pws.state === "aborted" || pws.state === "failed"976);977}978979export function isAvailable(pws: PrebuiltWorkspace) {980return pws.state === "available" && !!pws.snapshot;981}982983export function buildDidSucceed(pws: PrebuiltWorkspace) {984return pws.state === "available" && !pws.error;985}986}987988export interface PrebuiltWorkspaceUpdatable {989id: string;990prebuiltWorkspaceId: string;991owner: string;992repo: string;993isResolved: boolean;994installationId: string;995/**996* the commitSHA of the commit that triggered the prebuild997*/998commitSHA?: string;999issue?: string;1000contextUrl?: string;1001}10021003export type PortOnOpen = "open-browser" | "open-preview" | "notify" | "ignore" | "ignore-completely";10041005export interface PortConfig {1006port: number;1007onOpen?: PortOnOpen;1008visibility?: PortVisibility;1009protocol?: PortProtocol;1010description?: string;1011name?: string;1012}1013export namespace PortConfig {1014export function is(config: any): config is PortConfig {1015return config && "port" in config && typeof config.port === "number";1016}1017}10181019export interface PortRangeConfig {1020port: string;1021onOpen?: PortOnOpen;1022}1023export namespace PortRangeConfig {1024export function is(config: any): config is PortRangeConfig {1025return config && "port" in config && (typeof config.port === "string" || config.port instanceof String);1026}1027}10281029export interface TaskConfig {1030name?: string;1031before?: string;1032init?: string;1033prebuild?: string;1034command?: string;1035env?: { [env: string]: any };1036openIn?: "bottom" | "main" | "left" | "right";1037openMode?: "split-top" | "split-left" | "split-right" | "split-bottom" | "tab-before" | "tab-after";1038}10391040export namespace TaskConfig {1041export function is(config: any): config is TaskConfig {1042return config && ("command" in config || "init" in config || "before" in config);1043}1044}10451046export namespace WorkspaceImageBuild {1047export type Phase = "BaseImage" | "GitpodLayer" | "Error" | "Done";1048export interface StateInfo {1049phase: Phase;1050currentStep?: number;1051maxSteps?: number;1052}1053export interface LogContent {1054data: number[]; // encode with "Array.from(UInt8Array)"", decode with "new UInt8Array(data)"1055}1056export type LogCallback = (info: StateInfo, content: LogContent | undefined) => void;1057export namespace LogLine {1058export const DELIMITER = "\r\n";1059export const DELIMITER_REGEX = /\r?\n/;1060}1061}10621063export type ImageConfig = ImageConfigString | ImageConfigFile;1064export type ImageConfigString = string;1065export namespace ImageConfigString {1066export function is(config: ImageConfig | undefined): config is ImageConfigString {1067return typeof config === "string";1068}1069}1070export interface ImageConfigFile {1071// Path to the Dockerfile relative to repository root1072file: string;1073// Path to the docker build context relative to repository root1074context?: string;1075}1076export namespace ImageConfigFile {1077export function is(config: ImageConfig | undefined): config is ImageConfigFile {1078return typeof config === "object" && "file" in config;1079}1080}1081export interface ExternalImageConfigFile extends ImageConfigFile {1082externalSource: Commit;1083}1084export namespace ExternalImageConfigFile {1085export function is(config: any | undefined): config is ExternalImageConfigFile {1086return typeof config === "object" && "file" in config && "externalSource" in config;1087}1088}10891090export interface WorkspaceContext {1091title: string;1092ref?: string;1093/** This contains the URL portion of the contextURL (which might contain other modifiers as well). It's optional because it's not set for older workspaces. */1094normalizedContextURL?: string;1095forceCreateNewWorkspace?: boolean;1096forceImageBuild?: boolean;1097// The context can have non-blocking warnings that should be displayed to the user.1098warnings?: string[];1099}11001101export namespace WorkspaceContext {1102export function is(context: any): context is WorkspaceContext {1103return context && "title" in context;1104}1105}11061107export interface WithSnapshot {1108snapshotBucketId: string;1109}1110export namespace WithSnapshot {1111export function is(context: any): context is WithSnapshot {1112return context && "snapshotBucketId" in context;1113}1114}11151116export interface WithPrebuild extends WithSnapshot {1117prebuildWorkspaceId: string;1118wasPrebuilt: true;1119}1120export namespace WithPrebuild {1121export function is(context: any): context is WithPrebuild {1122return context && WithSnapshot.is(context) && "prebuildWorkspaceId" in context && "wasPrebuilt" in context;1123}1124}11251126/**1127* WithDefaultConfig contexts disable the download of the gitpod.yml from the repository1128* and fall back to the built-in configuration.1129*/1130export interface WithDefaultConfig {1131withDefaultConfig: true;1132}11331134export namespace WithDefaultConfig {1135export function is(context: any): context is WithDefaultConfig {1136return context && "withDefaultConfig" in context && context.withDefaultConfig;1137}11381139export function mark(ctx: WorkspaceContext): WorkspaceContext & WithDefaultConfig {1140return {1141...ctx,1142withDefaultConfig: true,1143};1144}1145}11461147export interface SnapshotContext extends WorkspaceContext, WithSnapshot {1148snapshotId: string;1149}11501151export namespace SnapshotContext {1152export function is(context: any): context is SnapshotContext {1153return context && WithSnapshot.is(context) && "snapshotId" in context;1154}1155}11561157export interface WithCommitHistory {1158commitHistory?: string[];1159additionalRepositoryCommitHistories?: {1160cloneUrl: string;1161commitHistory: string[];1162}[];1163}11641165export interface StartPrebuildContext extends WorkspaceContext, WithCommitHistory {1166actual: WorkspaceContext;1167project: Project;1168branch?: string;1169}11701171export namespace StartPrebuildContext {1172export function is(context: any): context is StartPrebuildContext {1173return context && "actual" in context;1174}1175}11761177export interface PrebuiltWorkspaceContext extends WorkspaceContext {1178originalContext: WorkspaceContext;1179prebuiltWorkspace: PrebuiltWorkspace;1180snapshotBucketId?: string;1181}11821183export namespace PrebuiltWorkspaceContext {1184export function is(context: any): context is PrebuiltWorkspaceContext {1185return context && "originalContext" in context && "prebuiltWorkspace" in context;1186}1187}11881189export interface WithReferrerContext extends WorkspaceContext {1190referrer: string;1191referrerIde?: string;1192}11931194export namespace WithReferrerContext {1195export function is(context: any): context is WithReferrerContext {1196return context && "referrer" in context;1197}1198}11991200export interface WithEnvvarsContext extends WorkspaceContext {1201envvars: EnvVarWithValue[];1202}12031204export namespace WithEnvvarsContext {1205export function is(context: any): context is WithEnvvarsContext {1206return context && "envvars" in context;1207}1208}12091210export type RefType = "branch" | "tag" | "revision";1211export namespace RefType {1212export const getRefType = (commit: Commit): RefType => {1213if (!commit.ref) {1214return "revision";1215}1216// This fallback is meant to handle the cases where (for historic reasons) ref is present but refType is missing1217return commit.refType || "branch";1218};1219}12201221export interface Commit {1222repository: Repository;1223revision: string;12241225// Might contain either a branch or a tag (determined by refType)1226ref?: string;12271228// refType is only set if ref is present (and not for old workspaces, before this feature was added)1229refType?: RefType;1230}12311232export interface AdditionalContentContext extends WorkspaceContext {1233/**1234* utf-8 encoded contents that will be copied on top of the workspace's filesystem1235*/1236additionalFiles: { [filePath: string]: string };1237}12381239export namespace AdditionalContentContext {1240export function is(ctx: any): ctx is AdditionalContentContext {1241return "additionalFiles" in ctx;1242}12431244export function hasDockerConfig(ctx: any, config: WorkspaceConfig): boolean {1245return is(ctx) && ImageConfigFile.is(config.image) && !!ctx.additionalFiles[config.image.file];1246}1247}12481249export interface OpenPrebuildContext extends WorkspaceContext {1250openPrebuildID: string;1251}12521253export namespace OpenPrebuildContext {1254export function is(ctx: any): ctx is OpenPrebuildContext {1255return "openPrebuildID" in ctx;1256}1257}12581259export interface CommitContext extends WorkspaceContext, GitCheckoutInfo {1260/** @deprecated Moved to .repository.cloneUrl, left here for backwards-compatibility for old workspace contextes in the DB */1261cloneUrl?: string;12621263/**1264* The clone and checkout information for additional repositories in case of multi-repo projects.1265*/1266additionalRepositoryCheckoutInfo?: GitCheckoutInfo[];1267}12681269export namespace CommitContext {1270/**1271* Creates a hash for all the commits of the CommitContext and all sub-repo commit infos.1272* The hash is max 255 chars long.1273* @param commitContext1274* @returns hash for commitcontext1275*/1276export function computeHash(commitContext: CommitContext): string {1277// for single commits we use the revision to be backward compatible.1278if (1279!commitContext.additionalRepositoryCheckoutInfo ||1280commitContext.additionalRepositoryCheckoutInfo.length === 01281) {1282return commitContext.revision;1283}1284const hasher = createHash("sha256");1285hasher.update(commitContext.revision);1286for (const info of commitContext.additionalRepositoryCheckoutInfo) {1287hasher.update(info.revision);1288}1289return hasher.digest("hex");1290}12911292export function isDefaultBranch(commitContext: CommitContext): boolean {1293return commitContext.ref === commitContext.repository.defaultBranch;1294}1295}12961297export interface GitCheckoutInfo extends Commit {1298checkoutLocation?: string;1299upstreamRemoteURI?: string;1300localBranch?: string;1301}13021303export namespace CommitContext {1304export function is(commit: any): commit is CommitContext {1305return WorkspaceContext.is(commit) && "repository" in commit && "revision" in commit;1306}1307}13081309export interface PullRequestContext extends CommitContext {1310nr: number;1311ref: string;1312base: {1313repository: Repository;1314ref: string;1315};1316}13171318export namespace PullRequestContext {1319export function is(ctx: any): ctx is PullRequestContext {1320return CommitContext.is(ctx) && "nr" in ctx && "ref" in ctx && "base" in ctx;1321}1322}13231324export interface IssueContext extends CommitContext {1325nr: number;1326ref: string;1327localBranch: string;1328}13291330export namespace IssueContext {1331export function is(ctx: any): ctx is IssueContext {1332return CommitContext.is(ctx) && "nr" in ctx && "ref" in ctx && "localBranch" in ctx;1333}1334}13351336export interface NavigatorContext extends CommitContext {1337path: string;1338isFile: boolean;1339}13401341export namespace NavigatorContext {1342export function is(ctx: any): ctx is NavigatorContext {1343return CommitContext.is(ctx) && "path" in ctx && "isFile" in ctx;1344}1345}13461347export interface Repository {1348host: string;1349owner: string;1350name: string;1351cloneUrl: string;1352/* Optional kind to differentiate between repositories of orgs/groups/projects and personal repos. */1353repoKind?: string;1354description?: string;1355avatarUrl?: string;1356webUrl?: string;1357defaultBranch?: string;1358/** Optional for backwards compatibility */1359private?: boolean;1360fork?: {1361// The direct parent of this fork1362parent: Repository;1363};1364/**1365* Optional date when the repository was last pushed to.1366*/1367pushedAt?: string;1368/**1369* Optional display name (e.g. for Bitbucket)1370*/1371displayName?: string;1372}13731374export interface RepositoryInfo {1375url: string;1376name: string;1377}13781379export interface Branch {1380name: string;1381commit: CommitInfo;1382htmlUrl: string;1383}13841385export interface CommitInfo {1386author: string;1387sha: string;1388commitMessage: string;1389authorAvatarUrl?: string;1390authorDate?: string;1391}13921393export interface WorkspaceInstancePortsChangedEvent {1394type: "PortsChanged";1395instanceID: string;1396portsOpened: number[];1397portsClosed: number[];1398}13991400export namespace WorkspaceInstancePortsChangedEvent {1401export function is(data: any): data is WorkspaceInstancePortsChangedEvent {1402return data && data.type == "PortsChanged";1403}1404}14051406export interface WorkspaceSession {1407workspace: Workspace;1408instance: WorkspaceInstance;1409metrics?: WorkspaceInstanceMetrics;1410}1411export interface WorkspaceInfo {1412workspace: Workspace;1413latestInstance?: WorkspaceInstance;1414}14151416export namespace WorkspaceInfo {1417export function lastActiveISODate(info: WorkspaceInfo): string {1418return info.latestInstance?.creationTime || info.workspace.creationTime;1419}1420}14211422export type RunningWorkspaceInfo = WorkspaceInfo & { latestInstance: WorkspaceInstance };14231424export interface WorkspaceCreationResult {1425createdWorkspaceId?: string;1426workspaceURL?: string;1427existingWorkspaces?: WorkspaceInfo[];1428}14291430export namespace WorkspaceCreationResult {1431export function is(data: any): data is WorkspaceCreationResult {1432return (1433data &&1434("createdWorkspaceId" in data ||1435"existingWorkspaces" in data ||1436"runningWorkspacePrebuild" in data ||1437"runningPrebuildWorkspaceID" in data)1438);1439}1440}14411442export interface AuthProviderInfo {1443readonly authProviderId: string;1444readonly authProviderType: string;1445readonly host: string;1446readonly ownerId?: string;1447readonly organizationId?: string;1448readonly verified: boolean;1449readonly hiddenOnDashboard?: boolean;1450readonly disallowLogin?: boolean;1451readonly icon?: string;1452readonly description?: string;14531454readonly settingsUrl?: string;1455readonly scopes?: string[];1456readonly requirements?: {1457readonly default: string[];1458readonly publicRepo: string[];1459readonly privateRepo: string[];1460};1461}14621463export interface AuthProviderEntry {1464readonly id: string;1465readonly type: AuthProviderEntry.Type;1466readonly host: string;1467readonly ownerId: string;1468readonly organizationId?: string;14691470readonly status: AuthProviderEntry.Status;14711472readonly oauth: OAuth2Config;1473/** A random string that is to change whenever oauth changes (enforced on DB level) */1474readonly oauthRevision?: string;1475}14761477export interface OAuth2Config {1478readonly clientId: string;1479readonly clientSecret: string;1480readonly callBackUrl: string;1481readonly authorizationUrl: string;1482readonly tokenUrl: string;1483readonly scope?: string;1484readonly scopeSeparator?: string;14851486readonly settingsUrl?: string;1487readonly authorizationParams?: { [key: string]: string };1488}14891490export namespace AuthProviderEntry {1491export type Type = "GitHub" | "GitLab" | "Bitbucket" | "BitbucketServer" | "AzureDevOps" | string;1492export type Status = "pending" | "verified";14931494/**1495* Some auth providers require additional configuration like Azure DevOps.1496*/1497export interface OAuth2CustomConfig {1498/**1499* The URL to the authorize endpoint of the provider.1500*/1501authorizationUrl?: string;1502/**1503* The URL to the oauth token endpoint of the provider.1504*/1505tokenUrl?: string;1506}1507export type NewEntry = Pick<AuthProviderEntry, "ownerId" | "host" | "type"> & {1508clientId?: string;1509clientSecret?: string;1510} & OAuth2CustomConfig;1511export type UpdateEntry = Pick<AuthProviderEntry, "id" | "ownerId"> & {1512clientId?: string;1513clientSecret?: string;1514} & OAuth2CustomConfig;1515export type NewOrgEntry = NewEntry & {1516organizationId: string;1517};1518export type UpdateOrgEntry = Pick<AuthProviderEntry, "id"> & {1519clientId?: string;1520clientSecret?: string;1521organizationId: string;1522} & OAuth2CustomConfig;1523export type UpdateOAuth2Config = Pick<OAuth2Config, "clientId" | "clientSecret"> & OAuth2CustomConfig;1524export function redact(entry: AuthProviderEntry): AuthProviderEntry {1525return {1526...entry,1527oauth: {1528...entry.oauth,1529clientSecret: "redacted",1530},1531};1532}1533}15341535export interface Configuration {1536readonly isDedicatedInstallation: boolean;1537}15381539export interface StripeConfig {1540individualUsagePriceIds: { [currency: string]: string };1541teamUsagePriceIds: { [currency: string]: string };1542}15431544export interface LinkedInProfile {1545id: string;1546firstName: string;1547lastName: string;1548profilePicture: string;1549emailAddress: string;1550}15511552export type SuggestedRepository = {1553url: string;1554projectId?: string;1555projectName?: string;1556repositoryName?: string;1557};155815591560