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/api.ts
// Paste's public HTTP API: no account. Anyone can make a paste and anyone with its id can read it;
// the owner key that comes back is what lets you edit or delete it. Every route answers JSON except
// /raw; GET / describes the routes. Writes go through the same policy rules as the web app.
import { SITE_URL } from "./lib/config";
import {
ID_RE, randomId, randomKey, pasteUrl, rawUrl, adminUrl, byteLength, expiresAt,
checkContent, checkTitle, parseExpires, detectLanguage, isLanguage, isPasswordLocked,
} from "./lib/paste";
const NOW = { operation: "time", value: "now" };
const GONE = "This paste does not exist. It may have expired, or been deleted after it was read.";
const cors = {
"access-control-allow-origin": "*",
"access-control-allow-methods": "GET, POST, PATCH, DELETE, OPTIONS",
"access-control-allow-headers": "content-type, x-owner-key",
};
const json = (body: unknown, status = 200) =>
new Response(JSON.stringify(body, null, 2), { status, headers: { "content-type": "application/json; charset=utf-8", "cache-control": "no-store", ...cors } });
const fail = (status: number, error: string) => json({ error }, status);
class HttpError extends Error { constructor(public status: number, message: string) { super(message); } }
async function body(req: Request): Promise<Record<string, any>> {
try {
const parsed = await req.json();
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
} catch { /* fall through */ }
throw new HttpError(400, "Send a JSON object body, or the text itself with content-type: text/plain.");
}
/** What POST /pastes was sent: a JSON object, or (with content-type text/plain) the text itself with options in the query string. */
async function createInput(req: Request, url: URL): Promise<Record<string, any>> {
if (!/^text\/plain\b/i.test(req.headers.get("content-type") || "")) return body(req);
const q = url.searchParams;
const flag = q.get("burn");
return {
content: await req.text(),
title: q.get("title"),
language: q.get("language"),
expires: q.get("expires"),
burn: flag == null ? undefined : !["0", "false", "no"].includes(flag.toLowerCase()),
};
}
interface Paste { content: string; title: string; language: string; ttl: number; burn: boolean; encrypted: boolean; opened?: boolean; createdAt: number }
/** Reads a paste. The rules refuse an expired paste, so it reads as gone. */
async function load(ctx: any, id: string): Promise<Paste> {
let paste: Paste | null = null;
if (ID_RE.test(id)) {
try {
paste = await ctx.bounded.get(`pastes/${id}`);
} catch (err) {
if (!/denied|403|forbidden|not allowed/i.test(String((err as Error)?.message ?? err))) throw err;
}
}
if (!paste || paste.opened) throw new HttpError(404, GONE);
return paste;
}
/** A burn paste is marked opened, which the rules allow only once, so of two simultaneous readers only one gets it. Then it is deleted. */
async function burn(ctx: any, id: string) {
try {
await ctx.bounded.set(`pastes/${id}`, { opened: true });
} catch {
throw new HttpError(404, GONE);
}
await ctx.bounded.delete(`pastes/${id}`);
}
/** Every API caller shares one identity, so owner actions always check the key first. */
async function ownerKey(ctx: any, req: Request, id: string): Promise<string> {
const key = req.headers.get("x-owner-key")?.trim() || "";
if (!key) throw new HttpError(401, "Send the paste's owner key in the x-owner-key header.");
const stored = await ctx.bounded.get(`pastekeys/${id}`);
if (!stored || stored.key !== key) throw new HttpError(403, "That owner key does not match this paste.");
// Become an owner with the key (once), so the rules let this route change the paste.
const me = ctx.user.id;
if (!(await ctx.bounded.get(`pastes/${id}/owners/${me}`))) {
await ctx.bounded.setMany([
{ path: `pasteclaims/${id}/c/${me}`, document: { key } },
{ path: `pastes/${id}/owners/${me}`, document: { at: NOW } },
]);
}
return key;
}
function describe(id: string, p: Paste, withContent: boolean) {
const exp = expiresAt(p.createdAt, p.ttl);
return {
id,
url: pasteUrl(id),
rawUrl: rawUrl(id),
title: p.title,
language: p.language,
size: byteLength(p.content),
encrypted: p.encrypted,
password: isPasswordLocked(p),
burn: p.burn,
createdAt: new Date(p.createdAt * 1000).toISOString(),
expiresAt: exp ? new Date(exp * 1000).toISOString() : null,
...(withContent ? { content: p.content } : {}),
};
}
const routes = {
name: "Paste by OpenApps",
about: "Share text, code and logs by link. Free, no account, no key. Pastes are unlisted: only people with the link can read them.",
docs: `${SITE_URL}/llms.txt`,
openapi: `${SITE_URL}/openapi.json`,
routes: [
"POST /pastes {content, title?, language?, expires?, burn?} -> the link, the raw link, a private admin link and the owner key",
"POST /pastes?title=&expires=&burn= with content-type: text/plain and the text as the body -> the same, for sending a file as it is",
"GET /pastes/:id -> the paste as JSON (a burn-after-reading paste is deleted by this read; add ?peek=1 to see its details without reading it)",
"GET /pastes/:id/raw -> the paste as plain text (also deletes a burn-after-reading paste)",
"PATCH /pastes/:id with header x-owner-key {content?, title?, language?} -> the updated paste",
"DELETE /pastes/:id with header x-owner-key -> deletes the paste",
],
limits: "content up to 512 KB in the web app; this API takes request bodies up to 128 KiB. expires: never, 10m, 1h, 1d, 1w, 1mo, 1y, or seconds from 60 to 31536000.",
};
export default async function api(req: Request, ctx: any) {
if (req.method === "OPTIONS") return new Response(null, { status: 204, headers: cors });
const url = new URL(req.url);
const parts = url.pathname.split("/").filter(Boolean);
// Accept both /pastes/... and /api/pastes/... so a copied path works either way.
if (parts[0] === "api") parts.shift();
try {
if (!parts.length && req.method === "GET") return json(routes);
if (parts[0] !== "pastes") return fail(404, "Unknown route. GET / lists the routes.");
if (parts.length === 1) {
if (req.method !== "POST") return fail(405, "Use POST /pastes to make a paste. Nobody can list pastes.");
const input = await createInput(req, url);
const content = checkContent(input.content);
if ("error" in content) return fail(400, content.error);
const title = checkTitle(input.title);
if ("error" in title) return fail(400, title.error);
const lang = input.language == null || input.language === "" || input.language === "auto" ? detectLanguage(content.content) : input.language;
if (!isLanguage(lang)) return fail(400, `Unknown language "${lang}". GET ${SITE_URL}/llms.txt lists them, or leave it out to detect it.`);
const expires = parseExpires(input.expires);
if ("error" in expires) return fail(400, expires.error);
if (input.burn != null && typeof input.burn !== "boolean") return fail(400, "burn must be true or false.");
const me = ctx.user.id;
const id = randomId();
const key = randomKey();
const paste = { content: content.content, title: title.title, language: lang, ttl: expires.ttl, burn: input.burn === true, encrypted: false };
await ctx.bounded.setMany([
{ path: `pastes/${id}`, document: { ...paste, opened: false, createdBy: me, createdAt: NOW } },
{ path: `pastes/${id}/owners/${me}`, document: { at: NOW } },
{ path: `pastekeys/${id}`, document: { key } },
]);
const made = describe(id, { ...paste, createdAt: Math.floor(Date.now() / 1000) }, false);
return json({ ...made, adminUrl: adminUrl(id, key), ownerKey: key }, 201);
}
const id = parts[1];
if (!id || parts.length > 3) return fail(404, "Unknown route. GET / lists the routes.");
if (parts.length === 3) {
if (parts[2] !== "raw") return fail(404, "Unknown route. GET / lists the routes.");
if (req.method !== "GET") return fail(405, "Use GET on /pastes/<id>/raw.");
const paste = await load(ctx, id);
if (paste.encrypted) return fail(409, "This paste is encrypted in the browser. Open its full link in a browser to read it.");
if (paste.burn) await burn(ctx, id);
return new Response(paste.content, { headers: { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store", ...cors } });
}
if (req.method === "GET") {
const paste = await load(ctx, id);
if (paste.burn) {
if (url.searchParams.has("peek")) return json(describe(id, paste, false));
await burn(ctx, id);
return json({ ...describe(id, paste, true), deleted: true });
}
return json(describe(id, paste, true));
}
if (req.method === "PATCH") {
const paste = await load(ctx, id);
await ownerKey(ctx, req, id);
const input = await body(req);
const patch: Partial<Paste> = {};
if (input.content != null) {
if (paste.encrypted) return fail(400, "This paste is encrypted. Edit it from its admin link in a browser.");
const c = checkContent(input.content);
if ("error" in c) return fail(400, c.error);
patch.content = c.content;
}
if (input.title != null) {
const t = checkTitle(input.title);
if ("error" in t) return fail(400, t.error);
patch.title = t.title;
}
if (input.language != null) {
if (!isLanguage(input.language)) return fail(400, `Unknown language "${input.language}".`);
patch.language = input.language;
}
if (!Object.keys(patch).length) return fail(400, "Send content, title, language, or any of them.");
await ctx.bounded.set(`pastes/${id}`, patch);
return json(describe(id, { ...paste, ...patch }, false));
}
if (req.method === "DELETE") {
await load(ctx, id);
await ownerKey(ctx, req, id);
await ctx.bounded.delete(`pastes/${id}`);
return json({ id, deleted: true });
}
return fail(405, "Use GET, PATCH or DELETE on /pastes/<id>.");
} catch (err) {
if (err instanceof HttpError) return fail(err.status, err.message);
const message = err instanceof Error ? err.message : String(err);
return fail(500, `Something went wrong: ${message}`);
}
}
Change history
Quality pass: password lock, year expiry, line links, text/plain API
Bounded source sync
Bounded source sync
Bounded source sync