resume-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 3f16b2375723
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.
← All files
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
3f16b23757 · 5 hours ago
Bounded source sync
b7eb5b26b5 · 1 day ago
Bounded source sync
adae97536e · 1 day ago
Bounded source sync
3b0cf36b39 · 1 day ago