sign-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 528326727848
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 1da91c033c738ab74538d13d
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
// Sign's public HTTP API: no account, no API key. An assistant creates a document from text and
// gets back one private signing link per signer, plus an admin link and owner key for the person
// it works for. Writes go through the same policy rules as the web app, as this route's public principal.
import { SITE_URL } from "./lib/config";
import { MAX_SIGNERS, MAX_TEXT, parseJson, randomId, sha256Hex, type Doc, type Signer, type Signature, type SignEvent } from "./lib/doc";

const NOW = { operation: "time", value: "now" };
const ID_RE = /^[A-Za-z0-9]{8,32}$/;
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

const cors = {
  "access-control-allow-origin": "*",
  "access-control-allow-methods": "GET, POST, OPTIONS",
  "access-control-allow-headers": "content-type, x-owner-key, idempotency-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); } }

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.");
}

const leaf = (r: any) => r.id ?? String(r._id ?? r.pathId ?? "").split("/").pop();
async function listAll(ctx: any, path: string): Promise<any[]> {
  const rows: any[] = [];
  let cursor: string | undefined;
  for (let page = 0; page < 10; page++) {
    const res = await ctx.bounded.get(path, { limit: 200, ...(cursor ? { cursor } : {}) });
    for (const r of res?.data || []) rows.push({ ...r, id: leaf(r) });
    cursor = res?.nextCursor;
    if (!cursor) break;
  }
  return rows;
}

/** Checks the owner key for a document, then makes sure this route can act as an owner. */
async function ownDoc(ctx: any, req: Request, docId: string): Promise<Doc> {
  if (!ID_RE.test(docId)) throw new HttpError(404, "No document with that id.");
  const url = new URL(req.url);
  const key = req.headers.get("x-owner-key") || url.searchParams.get("key") || "";
  if (!key) throw new HttpError(401, "Send the owner key in the x-owner-key header.");
  const stored = await ctx.bounded.get(`dockeys/${docId}`);
  if (!stored) throw new HttpError(404, "No document with that id.");
  if (stored.key !== key) throw new HttpError(403, "That owner key does not match this document.");
  const me = ctx.user.id;
  if (!(await ctx.bounded.get(`docs/${docId}/owners/${me}`))) {
    await ctx.bounded.setMany([
      { path: `docclaims/${docId}/c/${me}`, document: { key } },
      { path: `docs/${docId}/owners/${me}`, document: { at: NOW } },
    ]);
  }
  const doc = await ctx.bounded.get(`docs/${docId}`);
  if (!doc) throw new HttpError(404, "No document with that id.");
  return doc as Doc;
}

const signUrl = (docId: string, signerId: string, token: string) => `${SITE_URL}/s/${docId}/${signerId}#t=${token}`;
const adminUrl = (docId: string, key: string) => `${SITE_URL}/d/${docId}#k=${key}`;

async function status(ctx: any, docId: string, doc: Doc, key: string) {
  const signers = parseJson<Signer[]>(doc.signers, []);
  const signatures = (await listAll(ctx, `docs/${docId}/signatures`)) as (Signature & { id: string })[];
  const events = (await listAll(ctx, `docs/${docId}/events`)) as (SignEvent & { id: string })[];
  const signedBy = new Map(signatures.map((s) => [s.id, s.signedAt]));
  const tokens = await Promise.all(signers.map((s) => ctx.bounded.get(`signertokens/${docId}/s/${s.id}`)));
  const viewed = (id: string) => events.filter((e) => e.type === "viewed" && e.signerId === id).sort((a, b) => a.at - b.at)[0]?.at;
  const iso = (sec?: number) => (sec ? new Date(sec * 1000).toISOString() : null);
  const complete = doc.status === "sent" && signers.length > 0 && signers.every((s) => signedBy.has(s.id));
  return {
    documentId: docId,
    title: doc.title,
    status: doc.status === "voided" ? "voided" : complete ? "completed" : doc.status,
    adminUrl: adminUrl(docId, key),
    fingerprint: `sha256:${doc.fileHash}`,
    signers: signers.map((s, i) => ({
      name: s.name,
      email: s.email || null,
      signUrl: tokens[i]?.token ? signUrl(docId, s.id, tokens[i].token) : null,
      viewedAt: iso(viewed(s.id)),
      signedAt: iso(signedBy.get(s.id)),
    })),
    note: complete
      ? "Everyone has signed. Anyone with the admin link or a signing link can download the signed PDF with its completion certificate."
      : "Send each signer their own signUrl. The signed PDF is ready once everyone has signed.",
  };
}

const routes = {
  name: "Sign by OpenApps",
  about: "Free e-signatures. No account and no API key. Create a document from text, get one private signing link per signer.",
  docs: `${SITE_URL}/llms.txt`,
  routes: [
    "POST /documents {title, text, signers: [{name, email?}], message?} -> signing links, admin link and owner key",
    "GET /documents/:id (x-owner-key) -> status, who viewed and signed, signing links",
    "POST /documents/:id/void (x-owner-key) -> stop collecting signatures",
  ],
  pdf: "To send an existing PDF, open the web app: the API takes text documents only.",
};

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);
  if (parts[0] === "api") parts.shift();
  try {
    if (!parts.length && req.method === "GET") return json(routes);
    if (parts[0] !== "documents") 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);
      const title = String(input.title ?? "").trim();
      const text = String(input.text ?? "").replace(/\r\n/g, "\n").trim();
      const message = String(input.message ?? "").trim();
      if (!title || title.length > 200) return fail(400, "title is required (up to 200 characters).");
      if (text.length < 20) return fail(400, "text is required: the full document to sign, as plain text or simple Markdown.");
      if (text.length > MAX_TEXT) return fail(400, `text can be up to ${MAX_TEXT.toLocaleString("en-US")} characters.`);
      if (message.length > 2000) return fail(400, "message can be up to 2,000 characters.");
      const rawSigners = Array.isArray(input.signers) ? input.signers : [];
      if (!rawSigners.length) return fail(400, "signers is required: [{\"name\": \"Sam Lee\", \"email\": \"sam@example.com\"}].");
      if (rawSigners.length > MAX_SIGNERS) return fail(400, `A document can have up to ${MAX_SIGNERS} signers.`);
      const signers: Signer[] = [];
      for (const s of rawSigners) {
        const name = String(s?.name ?? "").trim();
        const email = String(s?.email ?? "").trim();
        if (!name || name.length > 80) return fail(400, "Each signer needs a name (up to 80 characters).");
        if (email && (!EMAIL_RE.test(email) || email.length > 200)) return fail(400, `${email} is not a valid email address.`);
        signers.push({ id: randomId(10), name, ...(email ? { email } : {}) });
      }
      const docId = randomId(20);
      const key = randomId(28);
      const tokens = signers.map(() => randomId(28));
      const evt = (type: string) => ({ path: `docs/${docId}/events/${randomId(12)}`, document: { type, signerId: "", at: NOW, by: me } });
      await ctx.bounded.setMany([
        { path: `docs/${docId}`, document: {
          title, sourceKind: "text", text, fileHash: await sha256Hex(text), pageCount: 0,
          signers: JSON.stringify(signers), fields: "[]", status: "sent", message, createdBy: me, createdAt: NOW, sentAt: NOW,
        } },
        { path: `docs/${docId}/owners/${me}`, document: { at: NOW } },
        { path: `dockeys/${docId}`, document: { key } },
        ...signers.map((s, i) => ({ path: `signertokens/${docId}/s/${s.id}`, document: { token: tokens[i] } })),
        evt("created"),
        evt("sent"),
      ]);
      return json({
        documentId: docId,
        title,
        status: "sent",
        adminUrl: adminUrl(docId, key),
        ownerKey: key,
        signers: signers.map((s, i) => ({ name: s.name, email: s.email || null, signUrl: signUrl(docId, s.id, tokens[i]) })),
        next: "Send each signer their own signUrl (it is private to them). Give the person you work for the adminUrl: it shows who has signed and the signed PDF. Check status with GET /documents/:id and the ownerKey.",
      }, 201);
    }

    const docId = parts[1];
    if (parts.length === 2 && req.method === "GET") {
      const doc = await ownDoc(ctx, req, docId);
      const key = (await ctx.bounded.get(`dockeys/${docId}`)).key;
      return json(await status(ctx, docId, doc, key));
    }

    if (parts.length === 3 && parts[2] === "void" && req.method === "POST") {
      const doc = await ownDoc(ctx, req, docId);
      if (doc.status === "voided") return fail(409, "This document is already voided.");
      if (doc.status !== "sent") return fail(409, "Only a sent document can be voided.");
      await ctx.bounded.setMany([
        { path: `docs/${docId}`, document: { status: "voided" } },
        { path: `docs/${docId}/events/${randomId(12)}`, document: { type: "voided", signerId: "", at: NOW, by: me } },
      ]);
      const key = (await ctx.bounded.get(`dockeys/${docId}`)).key;
      return json(await status(ctx, docId, { ...doc, status: "voided" }, key));
    }

    return fail(404, "Unknown route. GET / lists the routes.");
  } catch (err) {
    if (err instanceof HttpError) return fail(err.status, err.message);
    return fail(500, `Something went wrong: ${err instanceof Error ? err.message : String(err)}`);
  }
}

Change history

Quality pass: phone signing, edit signers, copy to send again
5283267278 · 5 hours ago
Bounded source sync
15f0fa2e27 · 1 day ago
Bounded source sync
d3aa1ca65b · 1 day ago
Bounded source sync
8e2ca0b773 · 1 day ago
Bounded source sync
5cfa84715c · 1 day ago
Bounded source sync
c849e7b286 · 1 day ago
Bounded source sync
68c1cabe03 · 1 day ago
Bounded source sync
62ae8944f0 · 1 day ago
Bounded source sync
7ed54e7381 · 1 day ago