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.
src/lib/actions.ts
// Starting and copying documents, and reading their status for the "Your documents" list.
// The landing page loads this on first use, so its first paint carries no data client or PDF library.
import { get, setMany, setFile, serverTimestamp } from "@bounded-sh/client";
import { me, explain } from "./bounded";
import { rememberDoc } from "./mydocs";
import { parseJson, randomId, sha256Hex, MAX_TEXT, type Doc, type Field, type Signer } from "../../functions/lib/doc";
const MAX_BYTES = 10 * 1024 * 1024;
interface Draft { title: string; sourceKind: "pdf" | "text"; fileHash: string; pageCount: number; text?: string; signers?: Signer[]; fields?: Field[]; message?: string }
/** Creates a draft document (with its owner record and admin key) in one batch, and remembers it on this device. */
async function createDraft(d: Draft) {
const uid = await me();
const id = randomId(20);
const key = randomId(28);
await setMany([
{ path: `docs/${id}`, document: {
title: d.title, sourceKind: d.sourceKind, text: d.text ?? "", fileHash: d.fileHash, pageCount: d.pageCount,
signers: JSON.stringify(d.signers ?? []), fields: JSON.stringify(d.fields ?? []), status: "draft", message: d.message ?? "",
createdBy: uid, createdAt: serverTimestamp(), sentAt: 0,
} },
{ path: `docs/${id}/owners/${uid}`, document: { at: serverTimestamp() } },
{ path: `dockeys/${id}`, document: { key } },
{ path: `docs/${id}/events/${randomId(12)}`, document: { type: "created", signerId: "", at: serverTimestamp(), by: uid } },
]);
rememberDoc({ id, key, title: d.title, at: Date.now() });
return { id, uid };
}
async function uploadOriginal(id: string, uid: string, bytes: Uint8Array) {
const res = await setFile(`docfiles/${id}/f/original`, new File([bytes], "original.pdf", { type: "application/pdf" }), { metadata: { owner: uid } });
if (res?.status !== "uploaded") throw new Error("The upload did not finish. Check your connection and try again.");
}
/** Checks a chosen file, creates a draft from it and uploads it. Returns the new document's id. */
export async function startFromPdf(file: File, onStep: (label: string) => void): Promise<string> {
if (file.type !== "application/pdf" && !file.name.toLowerCase().endsWith(".pdf")) throw new Error("Choose a PDF file. For a Word document, save it as a PDF first, or paste its text.");
if (file.size > MAX_BYTES) throw new Error("That PDF is over 10 MB. Try a smaller or compressed copy.");
onStep("Reading the PDF");
const bytes = new Uint8Array(await file.arrayBuffer());
let pages = 0;
try {
const { PDFDocument } = await import("pdf-lib");
pages = (await PDFDocument.load(bytes)).getPageCount();
} catch (e) {
throw new Error(/encrypt/i.test(String(e)) ? "That PDF is password protected. Remove the password and try again." : "That file does not open as a PDF.");
}
onStep("Uploading");
try {
const title = file.name.replace(/\.pdf$/i, "").replace(/[_-]+/g, " ").trim().slice(0, 200) || "Document";
const { id, uid } = await createDraft({ title, sourceKind: "pdf", fileHash: await sha256Hex(bytes), pageCount: pages });
await uploadOriginal(id, uid, bytes);
return id;
} catch (e) {
throw new Error(explain(e));
}
}
/** Creates a draft from text. Returns the new document's id. */
export async function startFromText(title: string, text: string): Promise<string> {
const t = text.replace(/\r\n/g, "\n").trim();
if (t.length > MAX_TEXT) throw new Error("That text is too long. Upload it as a PDF instead.");
try {
return (await createDraft({ title: title.trim().slice(0, 200), sourceKind: "text", fileHash: await sha256Hex(t), pageCount: 0, text: t })).id;
} catch (e) {
throw new Error(explain(e));
}
}
/** A new draft with the same pages, signers, fields and message, ready to edit and send again. */
export async function copyDocument(doc: Doc, bytes: Uint8Array | null): Promise<string> {
try {
const signers = parseJson<Signer[]>(doc.signers, []);
const fields = doc.sourceKind === "pdf" ? parseJson<Field[]>(doc.fields, []) : [];
const { id, uid } = await createDraft({
title: doc.title, sourceKind: doc.sourceKind, fileHash: doc.fileHash, pageCount: doc.pageCount || 0,
text: doc.text || "", signers, fields, message: doc.message || "",
});
if (doc.sourceKind === "pdf") {
if (!bytes) throw new Error("The document's file has not loaded yet. Try again in a moment.");
await uploadOriginal(id, uid, bytes);
}
return id;
} catch (e) {
throw new Error(explain(e));
}
}
export type ListStatus = { label: string; tone: "draft" | "waiting" | "done" | "voided" } | null;
/** One document's status for the list, or null when this browser can no longer read it. */
export async function docStatus(id: string): Promise<ListStatus> {
await me();
const doc = (await get(`docs/${id}`)) as Doc | null;
if (!doc) return null;
if (doc.status === "draft") return { label: "Draft", tone: "draft" };
if (doc.status === "voided") return { label: "Voided", tone: "voided" };
const signers = parseJson<Signer[]>(doc.signers, []);
const rows = ((await get(`docs/${id}/signatures`, { limit: 50 })) as { data?: { _id?: string; id?: string }[] })?.data ?? [];
const signed = new Set(rows.map((r) => r.id ?? String(r._id ?? "").split("/").pop()));
const done = signers.filter((s) => signed.has(s.id)).length;
return done >= signers.length && signers.length > 0 ? { label: "Completed", tone: "done" } : { label: `Signed ${done} of ${signers.length}`, tone: "waiting" };
}
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