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.
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.
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
Bounded source sync
Bounded source sync
Bounded source sync