bio-free.openapps.xyz

Source

This app’s code and revision history are public. Download a copy or explore the files below.

Public
The app’s code and revision history are public.
Owned app · rev 3e98d5e10bac
Run it yourself
Take the whole tree and run it on your own machine. The download needs no account. Cloning needs a sign-in.
Download .zip
bounded clone 4ce232ee856d7de1ba5f2b6d
A copy you download is not bounded. The guarantees this app runs under - declared egress, proven invariants, writes that fail closed - live in the runtime serving this page, not in the files. What you get is auditable source you can read and run, with no runtime guarantees attached.
← All files
functions/lib/bio.ts
// What a Bio page may hold, shared by the web app and the public API.
import { SITE_URL } from "./config";

export const MAX_NAME = 60;
export const MAX_BIO = 160;
export const MAX_LINKS = 50;
export const MAX_TITLE = 80;
export const MAX_URL = 2000;
export const MAX_AVATAR = 60000;

export const THEMES = ["paper", "ink", "cobalt", "sage", "night"] as const;
export type Theme = (typeof THEMES)[number];
export const BUTTON_STYLES = ["filled", "outline"] as const;
export type ButtonStyle = (typeof BUTTON_STYLES)[number];

export type SocialKind = "website" | "email" | "x" | "instagram" | "tiktok" | "youtube" | "linkedin" | "github" | "threads" | "bluesky"
  | "facebook" | "whatsapp" | "telegram" | "spotify" | "twitch";
export type Socials = Partial<Record<SocialKind, string>>;
/** A link on the page, or a header (a title between links, with an empty url). Hidden ones stay in the list but do not show. */
export interface Link { id: string; title: string; url: string; header?: true; hidden?: true }

/** A page as stored: links and socials are JSON strings. */
export interface PageDoc {
  name: string; bio: string; avatar: string; theme: string; buttonStyle: string;
  links: string; socials: string; showCredit: boolean; views: number; createdBy: string; createdAt: number;
}

/** A page as shown, edited and returned by the API. */
export interface Page {
  name: string; bio: string; avatar: string; theme: Theme; buttonStyle: ButtonStyle;
  links: Link[]; socials: Socials; showCredit: boolean;
}

type Checked<T> = T | { error: string };

// Paths the site uses itself. Handles cannot hold a dot, so files like llms.txt are already safe.
// Keep in sync with the inline script at the top of index.html.
export const RESERVED = ["api", "new", "admin", "about", "help", "assets", "art", "fonts", "edit", "login", "signup",
  "settings", "privacy", "terms", "support", "app", "apps", "www", "bio", "openapps", "static", "dashboard"];
const HANDLE_RE = /^[a-z0-9][a-z0-9-]{1,28}[a-z0-9]$/;
const LINK_ID_RE = /^[A-Za-z0-9]{6,16}$/;
const EMAIL_RE = /^[^\s@<>"]+@[^\s@<>"]+\.[^\s@<>"]+$/;
const AVATAR_RE = /^data:image\/(png|jpeg|webp);base64,[A-Za-z0-9+/]+={0,2}$/;
const KEY_ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";

interface SocialDef { kind: SocialKind; label: string; placeholder: string; hosts?: string[]; base?: string; re?: RegExp }
/** The social icons a page can show, in the order they appear. */
export const SOCIALS: SocialDef[] = [
  { kind: "website", label: "Website", placeholder: "yoursite.com" },
  { kind: "email", label: "Email", placeholder: "you@example.com" },
  { kind: "x", label: "X", placeholder: "@yourname", hosts: ["x.com", "twitter.com"], base: "https://x.com/", re: /^[A-Za-z0-9_]{1,15}$/ },
  { kind: "instagram", label: "Instagram", placeholder: "@yourname", hosts: ["instagram.com"], base: "https://www.instagram.com/", re: /^[A-Za-z0-9._]{1,30}$/ },
  { kind: "tiktok", label: "TikTok", placeholder: "@yourname", hosts: ["tiktok.com"], base: "https://www.tiktok.com/@", re: /^[A-Za-z0-9._]{2,24}$/ },
  { kind: "youtube", label: "YouTube", placeholder: "@yourchannel", hosts: ["youtube.com", "youtu.be"], base: "https://www.youtube.com/@", re: /^[A-Za-z0-9._-]{3,30}$/ },
  { kind: "linkedin", label: "LinkedIn", placeholder: "yourname", hosts: ["linkedin.com"], base: "https://www.linkedin.com/in/", re: /^[A-Za-z0-9-]{3,100}$/ },
  { kind: "github", label: "GitHub", placeholder: "yourname", hosts: ["github.com"], base: "https://github.com/", re: /^[A-Za-z0-9-]{1,39}$/ },
  { kind: "threads", label: "Threads", placeholder: "@yourname", hosts: ["threads.com", "threads.net"], base: "https://www.threads.com/@", re: /^[A-Za-z0-9._]{1,30}$/ },
  { kind: "bluesky", label: "Bluesky", placeholder: "you.bsky.social", hosts: ["bsky.app"], base: "https://bsky.app/profile/", re: /^[A-Za-z0-9.-]{3,253}$/ },
  { kind: "facebook", label: "Facebook", placeholder: "yourname", hosts: ["facebook.com", "fb.com"], base: "https://www.facebook.com/", re: /^[A-Za-z0-9.]{5,50}$/ },
  { kind: "whatsapp", label: "WhatsApp", placeholder: "+1 555 123 4567", hosts: ["wa.me", "whatsapp.com"], base: "https://wa.me/", re: /^\d{7,15}$/ },
  { kind: "telegram", label: "Telegram", placeholder: "@yourname", hosts: ["t.me", "telegram.me"], base: "https://t.me/", re: /^[A-Za-z0-9_]{5,32}$/ },
  { kind: "spotify", label: "Spotify", placeholder: "open.spotify.com/artist/...", hosts: ["open.spotify.com", "spotify.com"] },
  { kind: "twitch", label: "Twitch", placeholder: "yourname", hosts: ["twitch.tv"], base: "https://www.twitch.tv/", re: /^[A-Za-z0-9_]{4,25}$/ },
];

function random(alphabet: string, length: number): string {
  const out: string[] = [];
  const limit = 256 - (256 % alphabet.length);
  while (out.length < length) {
    for (const b of crypto.getRandomValues(new Uint8Array(length * 2))) {
      if (b < limit && out.length < length) out.push(alphabet[b % alphabet.length]);
    }
  }
  return out.join("");
}

export const randomKey = () => random(KEY_ALPHABET, 24);
export const randomLinkId = () => random(KEY_ALPHABET, 8);
export const pageUrl = (handle: string) => `${SITE_URL}/${handle}`;
export const editUrl = (handle: string, key: string) => `${SITE_URL}/e/${handle}#k=${key}`;
export const hostOf = (url: string) => { try { return new URL(url).hostname.replace(/^www\./, ""); } catch { return url; } };
const oneLine = (raw: unknown) => String(raw ?? "").replace(/\s+/g, " ").trim();

export const newPage = (name: string): Page => ({
  name, bio: "", avatar: "", theme: "paper", buttonStyle: "filled", links: [], socials: {}, showCredit: true,
});

/** Checks a page address. "@Maya" becomes "maya". */
export function checkHandle(raw: unknown): Checked<{ handle: string }> {
  const handle = String(raw ?? "").trim().replace(/^[@/]+|\/+$/g, "").toLowerCase();
  if (!handle) return { error: "Pick the name for your page address." };
  if (!HANDLE_RE.test(handle)) return { error: "Use 3 to 30 lowercase letters, numbers or dashes, starting and ending with a letter or number." };
  if (RESERVED.includes(handle)) return { error: `"${handle}" is reserved. Try another.` };
  return { handle };
}

/** Normalizes a web address, or says what is wrong with it. "example.com/x" becomes "https://example.com/x". */
export function checkUrl(raw: unknown): Checked<{ url: string }> {
  let text = String(raw ?? "").trim();
  if (!text) return { error: "Add the web address." };
  const scheme = text.match(/^([a-z][a-z0-9+.-]*):/i);
  if (!scheme || (!/^[a-z][a-z0-9+.-]*:\/\//i.test(text) && scheme[1].includes("."))) text = `https://${text}`;
  let u: URL;
  try {
    u = new URL(text);
  } catch {
    return { error: "That does not look like a web address." };
  }
  if (u.protocol !== "http:" && u.protocol !== "https:") return { error: "Only web addresses (http or https) work here." };
  if (!u.hostname.includes(".")) return { error: "That does not look like a web address." };
  if (u.href.length > MAX_URL) return { error: `Web addresses can be up to ${MAX_URL.toLocaleString("en-US")} characters long.` };
  return { url: u.href };
}

export function checkName(raw: unknown): Checked<{ name: string }> {
  const name = oneLine(raw);
  if (!name) return { error: "Add a name for the page." };
  if (name.length > MAX_NAME) return { error: `Names can be up to ${MAX_NAME} characters.` };
  return { name };
}

export function checkBio(raw: unknown): Checked<{ bio: string }> {
  const bio = oneLine(raw);
  if (bio.length > MAX_BIO) return { error: `The bio can be up to ${MAX_BIO} characters.` };
  return { bio };
}

export const isAvatar = (s: unknown): s is string => typeof s === "string" && s.length <= MAX_AVATAR && AVATAR_RE.test(s);

export function checkAvatar(raw: unknown): Checked<{ avatar: string }> {
  if (raw == null || raw === "") return { avatar: "" };
  if (!isAvatar(raw)) return { error: `avatar must be a PNG, JPEG or WebP data URL up to ${MAX_AVATAR.toLocaleString("en-US")} characters (a 256 by 256 picture fits).` };
  return { avatar: raw };
}

/** Checks the list of links and headers. Ids are kept when sent back, or matched by address, so click counts survive edits. */
export function checkLinks(raw: unknown, current: Link[] = []): Checked<{ links: Link[] }> {
  if (raw == null) return { links: [] };
  if (!Array.isArray(raw)) return { error: "links must be a list like [{\"title\": \"My shop\", \"url\": \"https://...\"}]." };
  if (raw.length > MAX_LINKS) return { error: `A page can have up to ${MAX_LINKS} links and headers.` };
  const links: Link[] = [];
  const seen = new Set<string>();
  for (const [i, item] of raw.entries()) {
    const entry = (item && typeof item === "object" ? item : { url: item }) as Record<string, unknown>;
    for (const flag of ["header", "hidden"]) {
      if (entry[flag] !== undefined && typeof entry[flag] !== "boolean") return { error: `Link ${i + 1}: ${flag} must be true or false.` };
    }
    const hidden = entry.hidden === true ? { hidden: true as const } : {};
    let title = oneLine(entry.title);
    let url = "";
    if (entry.header !== true) {
      const checked = checkUrl(entry.url);
      if ("error" in checked) return { error: `Link ${i + 1}: ${checked.error}` };
      url = checked.url;
      title = title || hostOf(url);
    } else if (!title) return { error: `Link ${i + 1}: a header needs a title.` };
    if (title.length > MAX_TITLE) return { error: `Link ${i + 1}: titles can be up to ${MAX_TITLE} characters.` };
    let id = typeof entry.id === "string" && LINK_ID_RE.test(entry.id) ? entry.id : current.find((l) => url && l.url === url && !seen.has(l.id))?.id;
    if (!id || seen.has(id)) id = randomLinkId();
    seen.add(id);
    links.push(url ? { id, title, url, ...hidden } : { id, title, url, header: true, ...hidden });
  }
  return { links };
}

/** Checks one social: a full address on that site, or just the name ("@maya" becomes "https://x.com/maya"). */
export function checkSocial(kind: SocialKind, raw: unknown): Checked<{ value: string }> {
  const def = SOCIALS.find((s) => s.kind === kind)!;
  const text = String(raw ?? "").trim();
  if (kind === "website") {
    const c = checkUrl(text);
    return "error" in c ? c : { value: c.url };
  }
  if (kind === "email") {
    const email = text.replace(/^mailto:/i, "");
    return EMAIL_RE.test(email) && email.length <= 254 ? { value: email } : { error: "That does not look like an email address." };
  }
  const hosts = def.hosts!;
  const bare = text.toLowerCase().replace(/^www\.|^m\./, "");
  if (/^https?:\/\//i.test(text) || hosts.some((h) => bare.startsWith(`${h}/`))) {
    const c = checkUrl(text);
    if ("error" in c) return c;
    const host = new URL(c.url).hostname.toLowerCase();
    if (!hosts.some((h) => host === h || host.endsWith(`.${h}`))) return { error: `Paste your ${def.label} address${def.base ? ", or just the name" : ""}.` };
    return { value: c.url.replace(/^http:/i, "https:") };
  }
  if (!def.base) return { error: `Paste your ${def.label} address.` };
  let name = kind === "whatsapp" ? text.replace(/[\s().+-]/g, "") : text.replace(/^@/, "");
  if (kind === "bluesky" && !name.includes(".")) name += ".bsky.social";
  if (!def.re!.test(name)) return { error: kind === "whatsapp" ? "Type the number with its country code, like +1 555 123 4567." : `That ${def.label} name does not look right.` };
  return { value: def.base + name };
}

export function checkSocials(raw: unknown): Checked<{ socials: Socials }> {
  if (raw == null) return { socials: {} };
  if (typeof raw !== "object" || Array.isArray(raw)) return { error: "socials must be an object like {\"instagram\": \"@maya\"}." };
  const socials: Socials = {};
  for (const [kind, value] of Object.entries(raw)) {
    const def = SOCIALS.find((s) => s.kind === kind);
    if (!def) return { error: `Unknown social "${kind}". Use ${SOCIALS.map((s) => s.kind).join(", ")}.` };
    if (value == null || String(value).trim() === "") continue;
    const c = checkSocial(def.kind, value);
    if ("error" in c) return { error: `${def.label}: ${c.error}` };
    socials[def.kind] = c.value;
  }
  return { socials };
}

/** Applies the fields present in input (as the API receives them) on top of base. */
export function applyInput(input: Record<string, unknown>, base: Page): Checked<{ page: Page }> {
  const page = { ...base };
  if (input.name !== undefined) {
    const c = checkName(input.name);
    if ("error" in c) return c;
    page.name = c.name;
  }
  if (input.bio !== undefined) {
    const c = checkBio(input.bio);
    if ("error" in c) return c;
    page.bio = c.bio;
  }
  if (input.avatar !== undefined) {
    const c = checkAvatar(input.avatar);
    if ("error" in c) return c;
    page.avatar = c.avatar;
  }
  if (input.theme !== undefined) {
    if (!THEMES.includes(input.theme as Theme)) return { error: `theme must be one of ${THEMES.join(", ")}.` };
    page.theme = input.theme as Theme;
  }
  if (input.buttonStyle !== undefined) {
    if (!BUTTON_STYLES.includes(input.buttonStyle as ButtonStyle)) return { error: `buttonStyle must be ${BUTTON_STYLES.join(" or ")}.` };
    page.buttonStyle = input.buttonStyle as ButtonStyle;
  }
  if (input.links !== undefined) {
    const c = checkLinks(input.links, base.links);
    if ("error" in c) return c;
    page.links = c.links;
  }
  if (input.socials !== undefined) {
    const c = checkSocials(input.socials);
    if ("error" in c) return c;
    page.socials = c.socials;
  }
  if (input.showCredit !== undefined) {
    if (typeof input.showCredit !== "boolean") return { error: "showCredit must be true or false." };
    page.showCredit = input.showCredit;
  }
  return { page };
}

/** The stored fields for a page (everything an owner may change). */
export const toDoc = (p: Page) => ({
  name: p.name, bio: p.bio, avatar: p.avatar, theme: p.theme, buttonStyle: p.buttonStyle,
  links: JSON.stringify(p.links), socials: JSON.stringify(p.socials), showCredit: p.showCredit,
});

function parseJson(text: unknown): unknown {
  try { return JSON.parse(String(text)); } catch { return null; }
}

/** Reads a stored page defensively: anything malformed is dropped rather than shown. */
export function fromDoc(doc: PageDoc): Page {
  const rawLinks = parseJson(doc.links);
  const links: Link[] = [];
  for (const l of Array.isArray(rawLinks) ? rawLinks : []) {
    if (!l || typeof l.id !== "string" || typeof l.title !== "string") continue;
    const hidden = l.hidden === true ? { hidden: true as const } : {};
    if (l.header === true) links.push({ id: l.id, title: l.title, url: "", header: true, ...hidden });
    else if (typeof l.url === "string" && /^https?:\/\//i.test(l.url)) links.push({ id: l.id, title: l.title, url: l.url, ...hidden });
  }
  const rawSocials = parseJson(doc.socials);
  const socials: Socials = {};
  if (rawSocials && typeof rawSocials === "object") {
    for (const { kind } of SOCIALS) {
      const v = (rawSocials as Record<string, unknown>)[kind];
      if (typeof v === "string" && (kind === "email" ? EMAIL_RE.test(v) : /^https?:\/\//i.test(v))) socials[kind] = v;
    }
  }
  return {
    name: String(doc.name ?? ""),
    bio: String(doc.bio ?? ""),
    avatar: isAvatar(doc.avatar) ? doc.avatar : "",
    theme: THEMES.includes(doc.theme as Theme) ? (doc.theme as Theme) : "paper",
    buttonStyle: BUTTON_STYLES.includes(doc.buttonStyle as ButtonStyle) ? (doc.buttonStyle as ButtonStyle) : "filled",
    links,
    socials,
    showCredit: doc.showCredit !== false,
  };
}

Change history

Quality pass: reorder, hide links, headers, QR share, 15 social icons, API docs
3e98d5e10b · 5 hours ago
Bounded source sync
c1194348e3 · 1 day ago
Bounded source sync
5b1f3fab35 · 1 day ago
Bounded source sync
21e45889ea · 1 day ago
Bounded source sync
8e21b0e5b7 · 1 day ago
Bounded source sync
0598bcc9b2 · 1 day ago
Bounded source sync
0ea5668865 · 1 day ago