84 lines · 3.1 KB
import { z } from "zod";
import { isReservedName } from "./reserved-names";

/** Account name shape only; `accountName` also refuses reserved names. */
export const accountNameFormat = z
  .string()
  .trim()
  .min(1)
  .max(39)
  .regex(/^[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?$/, "Use letters, numbers and single hyphens");

export const accountName = accountNameFormat.refine((name) => !isReservedName(name), "That name is reserved");

export const repoName = z
  .string()
  .trim()
  .min(1)
  .max(100)
  .regex(/^[A-Za-z0-9._-]+$/, "Use letters, numbers, '.', '-' and '_'")
  .refine((name) => !name.endsWith(".git") && name !== "." && name !== "..", "Invalid repository name");

export const repoRef = z.object({ owner: z.string().min(1), repo: z.string().min(1) });

export const password = z.string().min(8, "Use at least 8 characters").max(200);

const DISPLAY_NAME_FORBIDDEN = /[<>\u0000-\u001f\u007f]/g;

/**
 * Display names end up in git author/committer idents and `Co-authored-by` trailers, where `<`, `>` and
 * newlines would forge a mailbox or start a new header line. Strip them and collapse whitespace; an empty
 * result becomes `null` so callers fall back to the login.
 */
export function cleanDisplayName(value: string | null | undefined): string | null {
  if (value === null || value === undefined) return null;
  const cleaned = value.replace(DISPLAY_NAME_FORBIDDEN, "").replace(/\s+/g, " ").trim();
  return cleaned.length > 0 ? cleaned : null;
}

export const displayName = z.string().max(100).nullable().transform(cleanDisplayName);