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