import { createServerFn } from "@tanstack/react-start";
import { z } from "zod";
import { requireViewer, viewerMiddleware } from "./middleware";
import type { Viewer } from "~/lib/types";
import { forbidden } from "~/server/errors";
import { limitWrite } from "~/server/limits";
import { billingAccountFor } from "~/server/metering/access";
import { canAdministerAccount, requireRepoAccess } from "~/server/repos";
import { AI_ENDPOINT_KINDS, deleteEndpoint, saveEndpoint } from "~/server/ai/endpoint";
import type { AiScope } from "~/server/ai/endpoint";
/** Where an endpoint is saved: a repository (repo admins) or an account (the user, org owners, site admins). */
const target = z.discriminatedUnion("scope", [
z.object({ scope: z.literal("repo"), owner: z.string().min(1).max(100), repo: z.string().min(1).max(100) }),
z.object({ scope: z.literal("account"), org: z.string().min(1).max(100).nullable() }),
]);
async function resolveTarget(
input: z.infer<typeof target>,
viewer: Viewer,
): Promise<{ scope: AiScope; ownerId: string }> {
if (input.scope === "repo") {
const { repo } = await requireRepoAccess(input.owner, input.repo, viewer, "admin");
// The endpoint spends the owner's money and receives repository content, so the owner decides, not any repo admin.
if (!(await canAdministerAccount(repo.owner_id, viewer))) {
throw forbidden("Only the repository owner can change its AI endpoint");
}
return { scope: "repo", ownerId: repo.id };
}
const account = await billingAccountFor(viewer, input.org);
if (!account.canEdit || !(await canAdministerAccount(account.accountId, viewer))) {
throw forbidden("Only the account owner can change its AI endpoint");
}
return { scope: "account", ownerId: account.accountId };
}
/** Saves an endpoint. The key is sealed and never returned: the response carries only the summary. */
export const saveAiEndpoint = createServerFn({ method: "POST" })
.middleware([viewerMiddleware])
.validator(