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