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
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
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