199 lines · 9.2 KB
import { z } from "zod";
import { peekAuthToken, storeAuthToken } from "../auth-tokens";
import { randomToken } from "../crypto";
import { badRequest, forbidden, HttpError, unauthorized, paymentRequired } from "../errors";
import { publicUrl } from "../events/emit";
import { authenticatedRemote, mintRepoToken, revokeRepoToken } from "../git/artifacts";
import { limitWrite } from "../limits";
import { hasPermission, loadRepoById, repoPermission } from "../repos";
import { loadActiveUserById, toViewer } from "../auth";
import { containerLimits, containersEnabled, containerStub } from "./container-config";
import { CONTAINERS_PAID_ONLY, containerUsage, EDITOR_METER } from "./container-meter";
import { remainingQuota } from "../metering/quota";
import { json } from "./http";
import { previewResponse } from "./preview-headers";
import { editorOriginFor } from "./origin";
import { editorRoomStub } from "./room-client";
import { canWrite, EDITOR_TOKEN_PREFIX, resolveEditorToken } from "./session";
import type { EditorSession } from "./session";
import { EDITOR_WS_PROTOCOL } from "~/lib/editor-protocol";

const PREVIEW_PREFIX = "gitfor_pvw";
/** Short: the ticket is a bearer in the URL, so a leaked link must go stale before it can be reused. */
const PREVIEW_TTL_SECONDS = 5 * 60;
const UNLIMITED_ATTEMPTS = Number.MAX_SAFE_INTEGER;
const HTTP_TOO_MANY = 429;
const MAX_PORT = 65535;
const SECONDS_PER_MINUTE = 60;
const SECONDS_PER_HOUR = 3600;
/** Covers the boot and a large initial clone; the token is revoked as soon as start returns. */
const CLONE_TOKEN_TTL_SECONDS = 30 * 60;
/** Starting for less than this isn't worth a boot; the user is told the month's quota is used up. */
const MIN_BUDGET_SECONDS = 60;

function requireContainerAccess(session: EditorSession): void {
  if (!containersEnabled()) throw forbidden("Containers aren't enabled on this forge");
  if (!canWrite(session)) throw forbidden("Containers need write access to a branch");
}

/** Plan gate for anything that boots or talks to a container; status stays readable so the UI can explain. */
async function requirePlanAllows(session: EditorSession): Promise<void> {