paste-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 01fdb2b1f66e
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 6eca480d6447d873e2e5eed9
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/paste.ts
// What a paste may be, shared by the web app and the public API: ids, keys, sizes, expiry and languages.
import { SITE_URL, API_BASE } from "./config";

/** Most a paste may hold, in UTF-8 bytes. The API's request body cap (128 KiB) is lower. */
export const MAX_BYTES = 512 * 1024;
export const MAX_TITLE = 200;

// Ids skip look-alikes (0 O o, 1 l I) so they survive being read aloud or retyped. 56^12 ids cannot be guessed.
const ID_ALPHABET = "23456789abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ";
const KEY_ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
export const ID_RE = /^[A-Za-z0-9]{12}$/;

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 randomId = () => random(ID_ALPHABET, 12);
export const randomKey = () => random(KEY_ALPHABET, 24);
export const pasteUrl = (id: string) => `${SITE_URL}/${id}`;
export const rawUrl = (id: string) => `${API_BASE}/pastes/${id}/raw`;
export const adminUrl = (id: string, key: string) => `${SITE_URL}/${id}/admin#k=${key}`;

export const byteLength = (text: string) => new TextEncoder().encode(text).length;

export function formatBytes(n: number): string {
  if (n < 1024) return `${n} bytes`;
  return `${(n / 1024).toFixed(n < 10 * 1024 ? 1 : 0)} KB`;
}

/** Expiry choices, the same as Pastebin's main ones. ttl is seconds after creation; 0 never expires. */
export const EXPIRES: Record<string, number> = { never: 0, "10m": 600, "1h": 3600, "1d": 86400, "1w": 604800, "1mo": 2592000, "1y": 31536000 };
export const MIN_TTL = 60;
export const MAX_TTL = 31536000;
const EXPIRES_HELP = "expires must be never, 10m, 1h, 1d, 1w, 1mo, 1y, or a number of seconds from 60 to 31536000.";

/** Reads an API "expires" value: one of the EXPIRES names, or a number of seconds from 60 to a year. */
export function parseExpires(raw: unknown): { ttl: number } | { error: string } {
  if (raw == null || raw === "") return { ttl: 0 };
  if (typeof raw === "string" && Object.prototype.hasOwnProperty.call(EXPIRES, raw)) return { ttl: EXPIRES[raw] };
  const n = typeof raw === "number" ? raw : /^\d+$/.test(String(raw)) ? Number(raw) : NaN;
  if (Number.isInteger(n) && (n === 0 || (n >= MIN_TTL && n <= MAX_TTL))) return { ttl: n };
  return { error: EXPIRES_HELP };
}

/** A paste locked with a password holds this prefix, then base64(salt + iv + ciphertext). A paste whose key is in the link holds plain base64. */
export const PASSWORD_PREFIX = "pw1.";
export const isPasswordLocked = (p: { encrypted: boolean; content: string }) => p.encrypted && p.content.startsWith(PASSWORD_PREFIX);

export interface Language { id: string; name: string; ext: string }

/** Languages a paste can be marked as. ids match highlight.js names, except html (highlight.js calls it xml). */
export const LANGUAGES: Language[] = [
  { id: "text", name: "Plain text", ext: "txt" },
  { id: "markdown", name: "Markdown", ext: "md" },
  { id: "javascript", name: "JavaScript", ext: "js" },
  { id: "typescript", name: "TypeScript", ext: "ts" },
  { id: "python", name: "Python", ext: "py" },
  { id: "json", name: "JSON", ext: "json" },
  { id: "html", name: "HTML", ext: "html" },
  { id: "css", name: "CSS", ext: "css" },
  { id: "bash", name: "Shell", ext: "sh" },
  { id: "sql", name: "SQL", ext: "sql" },
  { id: "yaml", name: "YAML", ext: "yaml" },
  { id: "xml", name: "XML", ext: "xml" },
  { id: "diff", name: "Diff", ext: "diff" },
  { id: "go", name: "Go", ext: "go" },
  { id: "rust", name: "Rust", ext: "rs" },
  { id: "java", name: "Java", ext: "java" },
  { id: "kotlin", name: "Kotlin", ext: "kt" },
  { id: "swift", name: "Swift", ext: "swift" },
  { id: "c", name: "C", ext: "c" },
  { id: "cpp", name: "C++", ext: "cpp" },
  { id: "csharp", name: "C#", ext: "cs" },
  { id: "php", name: "PHP", ext: "php" },
  { id: "ruby", name: "Ruby", ext: "rb" },
];
const BY_ID = new Map(LANGUAGES.map((l) => [l.id, l]));
export const languageOf = (id: string) => BY_ID.get(id) ?? LANGUAGES[0];
export const isLanguage = (id: unknown): id is string => typeof id === "string" && BY_ID.has(id);

/** A best guess at a paste's language from its first lines, for the common ones. Anything unsure is plain text. */
export function detectLanguage(text: string): string {
  const s = text.slice(0, 20000).replace(/^/, "");
  const t = s.trimStart();
  if (!t) return "text";
  const has = (re: RegExp) => re.test(s);
  if (/^[[{]/.test(t)) {
    try { JSON.parse(text); return "json"; } catch { /* not JSON */ }
  }
  if (/^#!.*\b(ba|z|da)?sh\b/.test(t)) return "bash";
  if (/^#!.*python/.test(t)) return "python";
  if (/^#!.*\b(node|deno|bun)\b/.test(t)) return "javascript";
  if (/^<\?php/.test(t)) return "php";
  if (/^<\?xml/.test(t)) return "xml";
  if (/^(<!doctype html|<html[\s>])/i.test(t) || (has(/<\/(div|p|span|body|head|a|ul|li|section|button)>/i) && /^</.test(t))) return "html";
  if (/^diff --git /m.test(s) || (has(/^@@ -\d+(,\d+)? \+\d+(,\d+)? @@/m) && has(/^(\+\+\+|---) /m))) return "diff";
  // A fenced code block means Markdown, even when the code inside looks like another language.
  if (has(/^```/m)) return "markdown";
  if (has(/^package \w+\s*$/m) && has(/^func /m)) return "go";
  if (has(/^\s*(pub )?fn \w+.*\{/m) && has(/\b(let mut|impl|use std::|println!|-> )/)) return "rust";
  if (has(/^\s*#include\s*[<"]/m)) return has(/\b(std::|cout|template\s*<|namespace \w+|class \w+)/) ? "cpp" : "c";
  if (has(/^\s*using System[;.]/m) || has(/\bConsole\.Write(Line)?\(/)) return "csharp";
  if (has(/\bpublic (static )?(final )?(class|void|interface) /) || has(/\bSystem\.out\.print/)) return "java";
  if (has(/^\s*(fun main\(|data class |val \w+ = |fun \w+\(.*\)\s*(:\s*\w+\s*)?[{=])/m) && !has(/;\s*$/m)) return "kotlin";
  if (has(/^\s*import (SwiftUI|Foundation|UIKit)\s*$/m) || has(/\bfunc \w+\(.*\)\s*->\s*\w+/)) return "swift";
  if (has(/^\s*(def \w+\(.*\)\s*(->.*)?:|class \w+(\(.*\))?:|from [\w.]+ import |import \w+(\.\w+)*\s*$|if __name__ == )/m) && !has(/[{};]\s*$/m)) return "python";
  if (has(/^\s*(interface \w+|type \w+ = |enum \w+ \{|import type )/m) || has(/\b(const|let|function)\b.*:\s*(string|number|boolean|unknown|any)\b/)) return "typescript";
  if (has(/^\s*(const|let|var|function|import|export|async function|class)\b.*([=({;]|from\s)/m) || has(/\b(console\.log|document\.|require\(|=>)/)) return "javascript";
  if (has(/^\s*(select\s.+\sfrom|insert into|update \w+ set|delete from|create (table|index|view)|alter table|drop table|with \w+ as \()/im)) return "sql";
  if (has(/^\s*(def |class |module |require ['"]|puts |end\s*$)/m) && has(/^\s*end\s*$/m)) return "ruby";
  if (has(/^\s*[.#]?[a-z][\w-]*(\s*[,>+~]\s*[.#]?[\w-]+)*\s*\{\s*$/im) && has(/^\s*[a-z-]+\s*:\s*[^;]+;\s*$/m)) return "css";
  if (has(/\[[^\]]+\]\(https?:\/\/[^)]+\)/)) return "markdown";
  // An env file (KEY=value lines) reads best as shell. Checked before headings, since its "# comments" look like them.
  if ((s.match(/^[A-Za-z_][A-Za-z0-9_]*=\S/gm)?.length ?? 0) >= 2 && !has(/[{};]\s*$/m)) return "bash";
  // "# Heading" alone could be a comment, so Markdown also needs a list, bold text or a sentence.
  if (has(/^#{1,6} \S/m) && (has(/^\s*([-*]|\d+\.) \S/m) || has(/\*\*[^*]+\*\*/) || has(/^[A-Z][^\n]{30,}[.!?]$/m))) return "markdown";
  if (has(/^\s*[-*] \S/m) && has(/\*\*[^*]+\*\*/)) return "markdown";
  if (/^---\s*$/.test(t.split("\n")[0]) || ((s.match(/^[\w-]+:(\s.*)?$/gm)?.length ?? 0) >= 3 && !has(/[{};]\s*$/m))) return "yaml";
  if (has(/^\s*(\$ |sudo |apt(-get)? |brew |npm |npx |pnpm |yarn |pip |cd |echo |export |curl |git |docker |kubectl )/m)) return "bash";
  return "text";
}

/** Checks a paste's text. */
export function checkContent(raw: unknown): { content: string } | { error: string } {
  if (typeof raw !== "string" || !raw.trim()) return { error: "Paste some text first." };
  const bytes = byteLength(raw);
  if (bytes > MAX_BYTES) return { error: `A paste can hold up to 512 KB. This one is ${Math.ceil(bytes / 1024)} KB.` };
  return { content: raw };
}

/** Checks a title: one line, up to 200 characters. Empty is fine. */
export function checkTitle(raw: unknown): { title: string } | { error: string } {
  const title = String(raw ?? "").replace(/\s+/g, " ").trim();
  if (title.length > MAX_TITLE) return { error: `Titles can be up to ${MAX_TITLE} characters.` };
  return { title };
}

/** When a paste stops being readable, in seconds, or null if never. */
export const expiresAt = (createdAt: number, ttl: number) => (ttl ? createdAt + ttl : null);

Change history

Quality pass: password lock, year expiry, line links, text/plain API
01fdb2b1f6 · 4 hours ago
Bounded source sync
fe5788c2d2 · 1 day ago
Bounded source sync
ac6f7dbc95 · 1 day ago
Bounded source sync
0a45926293 · 1 day ago