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 70bace54357d4b57b674108f
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
// Resume's public HTTP API: no key and no account to create a resume. It takes JSON Resume
// (https://jsonresume.org/schema) and hands back a link where the resume shows and its PDF downloads.
// A resume's id is what lets someone read it, exactly like its link; changing it takes the owner key
// handed back at creation (x-owner-key header). Every route answers JSON; GET / describes the routes.
// Writes go through the same policy rules as the web app, as this route's public principal.
import { SITE_URL } from "./lib/config";
import { cleanResume, colorOf, fromJsonResume, toJsonResume, toText, randomId, ResumeError, LISTS, MAX_DATA, type Resume } from "./lib/resume";
const NOW = { operation: "time", value: "now" };
const ID_RE = /^[A-Za-z0-9]{8,32}$/;
const cors = {
"access-control-allow-origin": "*",
"access-control-allow-methods": "GET, POST, PATCH, 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", ...cors } });
const fail = (status: number, error: string) => json({ error }, status);
class HttpError extends Error { constructor(public status: number, message: string) { super(message); } }
type Obj = Record<string, unknown>;
const isObj = (v: unknown): v is Obj => Boolean(v) && typeof v === "object" && !Array.isArray(v);
async function body(req: Request): Promise<Obj> {
const text = await req.text();
try {
const parsed = JSON.parse(text);
if (isObj(parsed)) return parsed;
} catch { /* fall through */ }
throw new HttpError(400, 'Send a JSON object body, like {"basics": {"name": "Sam Rivera"}, "work": [...]}.');
}
/** The stored resume, checked: it needs a name and must fit. */
function stored(r: Resume): string {
if (!r.basics.name) throw new HttpError(400, 'basics.name is required, like {"basics": {"name": "Sam Rivera"}}.');
const data = JSON.stringify(cleanResume(r));
if (data.length > MAX_DATA) throw new HttpError(400, `This resume is too long: keep it under ${MAX_DATA.toLocaleString("en-US")} characters of JSON.`);
return data;
}
/** PATCH input on top of the stored resume: basics and meta merge field by field, lists replace. */
function merge(base: Resume, input: Obj): Resume {
const current = toJsonResume(base) as Obj;
const next: Obj = { ...current };
for (const k of LISTS) if (k in input) next[k] = input[k];
for (const k of ["basics", "meta"]) {
if (!(k in input)) continue;
if (!isObj(input[k])) throw new HttpError(400, `${k} must be an object.`);
next[k] = { ...(isObj(current[k]) ? current[k] : {}), ...input[k] };
}
for (const k of ["template", "paper", "color"]) if (k in input) next[k] = input[k];
return fromJsonResume(next, base.settings.paper);
}
function view(resumeId: string, doc: { updatedAt?: number }, r: Resume) {
return {
resumeId,
url: `${SITE_URL}/r/${resumeId}`,
updatedAt: doc.updatedAt ? new Date(doc.updatedAt * 1000).toISOString() : null,
template: r.settings.template,
paper: r.settings.paper,
color: colorOf(r.settings),
resume: toJsonResume(r),
text: toText(r),
pdf: `Open ${SITE_URL}/r/${resumeId} and press Download PDF.`,
};
}
/** Joins the resume as this route (knowing the id is the permission), then reads it. */
async function openResume(ctx: any, resumeId: string) {
if (!ID_RE.test(resumeId)) throw new HttpError(404, "No resume with that id.");
const me = ctx.user.id;
if (!(await ctx.bounded.get(`resumes/${resumeId}/joins/${me}`))) {
try {
await ctx.bounded.set(`resumes/${resumeId}/joins/${me}`, { at: NOW });
} catch {
throw new HttpError(404, "No resume with that id.");
}
}
const doc = await ctx.bounded.get(`resumes/${resumeId}`);
if (!doc) throw new HttpError(404, "No resume with that id.");
try {
// Owners can also write through the web SDK, so the stored resume is read back through the checked path.
const r = JSON.parse(doc.data) as Resume;
return { doc, resume: fromJsonResume(toJsonResume(r), r.settings.paper) };
} catch {
throw new HttpError(500, "This resume could not be read.");
}
}
/** Checks the owner key before anything else, then makes this route an owner if it is not one yet. */
async function asOwner(ctx: any, req: Request, resumeId: string) {
if (!ID_RE.test(resumeId)) throw new HttpError(404, "No resume with that id.");
const key = String(req.headers.get("x-owner-key") || "").trim();
if (!key) throw new HttpError(401, "Send the resume's owner key in the x-owner-key header.");
const keyDoc = await ctx.bounded.get(`resumekeys/${resumeId}`);
if (!keyDoc?.key) throw new HttpError(404, "No resume with that id.");
if (keyDoc.key !== key) throw new HttpError(403, "That owner key does not match this resume.");
const opened = await openResume(ctx, resumeId);
const me = ctx.user.id;
if (!(await ctx.bounded.get(`resumes/${resumeId}/owners/${me}`))) {
await ctx.bounded.setMany([
{ path: `resumeclaims/${resumeId}/c/${me}`, document: { key } },
{ path: `resumes/${resumeId}/owners/${me}`, document: { at: NOW } },
]);
}
return opened;
}
const routes = {
name: "Resume by OpenApps",
about: "Free resumes with a free PDF download. No key and no account. Send JSON Resume (https://jsonresume.org/schema) and get back a link where the resume shows and its PDF downloads. A resume's id lets anyone read it, like its link; changes need the owner key returned at creation, sent as the x-owner-key header.",
docs: `${SITE_URL}/llms.txt`,
openapi: `${SITE_URL}/openapi.json`,
routes: [
"POST /resumes {basics: {name, label?, email?, phone?, url?, location?, summary?, profiles?: [{network?, url}]}, work?, education?, skills?, projects?, volunteer?, awards?, certificates?, publications?, languages?, interests?, template?: classic|modern|compact, paper?: Letter|A4, color?: ink|cobalt|navy|sage|brick|plum} -> resumeId, url, adminUrl, ownerKey",
"GET /resumes/:id -> the resume as JSON Resume, plus its plain text, template, paper and color",
"PATCH /resumes/:id (x-owner-key) {any field of POST}: basics merges field by field, a list replaces the whole list",
],
};
export default async function api(req: Request, ctx: any) {
if (req.method === "OPTIONS") return new Response(null, { status: 204, headers: cors });
const parts = new URL(req.url).pathname.split("/").filter(Boolean);
// Accept both /resumes/... and /api/resumes/... 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] !== "resumes" || parts.length > 2) return fail(404, "Unknown route. GET / lists the routes.");
const me = ctx.user.id;
if (parts.length === 1 && req.method === "POST") {
const input = await body(req);
if (!isObj(input.basics)) throw new HttpError(400, 'basics is required, like {"basics": {"name": "Sam Rivera"}}.');
const resume = fromJsonResume(input);
const data = stored(resume);
const resumeId = randomId(20);
const ownerKey = randomId(24);
await ctx.bounded.setMany([
{ path: `resumes/${resumeId}`, document: { data, createdBy: me, createdAt: NOW, updatedAt: NOW } },
{ path: `resumes/${resumeId}/joins/${me}`, document: { at: NOW } },
{ path: `resumes/${resumeId}/owners/${me}`, document: { at: NOW } },
{ path: `resumekeys/${resumeId}`, document: { key: ownerKey } },
]);
return json({
resumeId,
url: `${SITE_URL}/r/${resumeId}`,
adminUrl: `${SITE_URL}/r/${resumeId}/admin#k=${ownerKey}`,
ownerKey,
template: resume.settings.template,
paper: resume.settings.paper,
color: colorOf(resume.settings),
share: "Send url to the person: they (and anyone they send it to) can see the resume and download the PDF. Give adminUrl only to the person the resume is for: it opens the editor, so they can change it from any device.",
}, 201);
}
const resumeId = parts[1];
if (parts.length === 2 && req.method === "GET") {
const { doc, resume } = await openResume(ctx, resumeId);
return json(view(resumeId, doc, resume));
}
if (parts.length === 2 && req.method === "PATCH") {
const input = await body(req);
const { resume: base } = await asOwner(ctx, req, resumeId);
const resume = merge(base, input);
await ctx.bounded.set(`resumes/${resumeId}`, { data: stored(resume), updatedAt: NOW });
const doc = await ctx.bounded.get(`resumes/${resumeId}`);
return json(view(resumeId, doc || {}, resume));
}
return fail(404, "Unknown route. GET / lists the routes.");
} catch (err) {
if (err instanceof HttpError) return fail(err.status, err.message);
if (err instanceof ResumeError) return fail(400, err.message);
const message = err instanceof Error ? err.message : String(err);
return fail(500, `Something went wrong: ${message}`);
}
}
Change history
Quality pass: several resumes, cover letters, phone-readable shared pages
Bounded source sync
Bounded source sync
Bounded source sync