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/lib/doc.ts
// Sign's document model, shared by the web app and the public API.
export type FieldType = "signature" | "initials" | "date" | "name" | "text" | "checkbox";
export type DocStatus = "draft" | "sent" | "voided";
export type SourceKind = "pdf" | "text";
export interface Signer { id: string; name: string; email?: string }
/** A field on a page. Positions are fractions of the page width and height, from the top left. */
export interface Field {
id: string;
signerId: string;
type: FieldType;
page: number; // 0-based
x: number;
y: number;
w: number;
h: number;
label?: string;
required?: boolean;
}
export interface Doc {
title: string;
sourceKind: SourceKind;
text?: string | null;
fileHash: string;
pageCount?: number | null;
signers: string; // JSON Signer[]
fields: string; // JSON Field[] (text documents compute theirs from the text)
status: DocStatus;
message?: string | null;
createdBy: string;
createdAt: number;
sentAt?: number | null;
}
export interface Signature {
values: string; // JSON { fieldId: string } (signatures and initials are PNG data URLs)
consent: boolean;
signedAt: number;
by: string;
ua?: string | null;
}
export interface SignEvent { type: "created" | "sent" | "viewed" | "signed" | "voided"; signerId?: string | null; at: number; by: string }
export const FIELD_LABEL: Record<FieldType, string> = {
signature: "Signature",
initials: "Initials",
date: "Date signed",
name: "Full name",
text: "Text",
checkbox: "Checkbox",
};
/** Default size of a new field, as a fraction of a Letter-shaped page. */
export const FIELD_SIZE: Record<FieldType, { w: number; h: number }> = {
signature: { w: 0.3, h: 0.06 },
initials: { w: 0.11, h: 0.05 },
date: { w: 0.2, h: 0.032 },
name: { w: 0.28, h: 0.032 },
text: { w: 0.3, h: 0.032 },
checkbox: { w: 0.028, h: 0.022 },
};
/** One color per signer, in order: muted, distinct, readable on paper. */
export const SIGNER_COLORS = ["#2e5ec4", "#a8442c", "#4f7a5c", "#8a5cb8", "#b8892c", "#2f8a8a", "#c0577f", "#5b6472"];
export const MAX_SIGNERS = 10;
export const MAX_TEXT = 100_000;
export function parseJson<T>(raw: string | null | undefined, fallback: T): T {
try {
const v = JSON.parse(raw || "");
return v ?? fallback;
} catch {
return fallback;
}
}
const ALPHABET = "abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789";
/** A random id from an unambiguous alphabet; 20 characters is about 116 bits. */
export function randomId(length = 20): string {
const bytes = crypto.getRandomValues(new Uint8Array(length));
let out = "";
for (const b of bytes) out += ALPHABET[b % ALPHABET.length];
return out;
}
export async function sha256Hex(data: ArrayBuffer | Uint8Array | string): Promise<string> {
const bytes = typeof data === "string" ? new TextEncoder().encode(data) : data instanceof Uint8Array ? data : new Uint8Array(data);
const digest = await crypto.subtle.digest("SHA-256", bytes);
return Array.from(new Uint8Array(digest), (b) => b.toString(16).padStart(2, "0")).join("");
}
/** Field ids for a text document's signature block, the same everywhere they are computed. */
export const textFieldIds = (signerId: string) => ({ signature: `sig-${signerId}`, name: `name-${signerId}`, date: `date-${signerId}` });
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