276 lines · 11.5 KB
/** Pure helpers for site host labels, cookies and outbound checks; no Worker bindings so they unit-test under bun. */

import { isNonPublicIpLiteral } from "../net/resolve-guard";

const LABEL_MAX = 63;
const HASH_SUFFIX = 7;
const LABEL = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
const encoder = new TextEncoder();

/** DNS-safe slug with no `--`, so `--` can separate the branch, repo and handle parts of a label. */
export function slug(value: string): string {
  return value
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, "-")
    .replace(/-+/g, "-")
    .replace(/^-|-$/g, "");
}

function fnv(value: string): string {
  let hash = 0x811c9dc5;
  for (const char of value) {
    hash ^= char.codePointAt(0) ?? 0;
    hash = Math.imul(hash, 0x01000193) >>> 0;
  }
  return hash.toString(36).padStart(HASH_SUFFIX, "0").slice(-HASH_SUFFIX);
}

/** Truncates an over-long leading part and appends a hash of the full value so distinct inputs stay distinct. */
function fit(head: string, tail: string, full: string): string {
  const joined = tail ? `${head}--${tail}` : head;
  if (joined.length <= LABEL_MAX) return joined;
  const room = LABEL_MAX - (tail ? tail.length + 2 : 0) - HASH_SUFFIX - 1;
  // An over-long tail keeps three parts under the reserved `scratch` handle, which no real site label uses.
  if (room < 1) return `${fnv(full)}--${fnv(tail)}--scratch`;
  const cut = head.slice(0, room).replace(/-+$/, "");
  return `${cut}-${fnv(full)}${tail ? `--${tail}` : ""}`;
}

export function isUserSiteRepo(handle: string, repo: string, domain: string | null): boolean {
  const lower = repo.toLowerCase();