Files
315a3fe6a5 fix(sign/goja): enforce SEQUENTIAL signing order + route PDFs through the blob seam (RED M7/M8) (#9)
M7 — SEQUENTIAL was declared but never enforced: documentsSend marked all
recipients SENT and signField/signComplete let anyone sign in any order. Add
assertTurn(doc,r): in SEQUENTIAL mode a recipient may only act once every
signer/approver ahead of them (listRecipients order: signing_order ASC,
created_at ASC) has SIGNED. Gated in signField and signComplete. PARALLEL
unchanged.

M8 — PDF bytes no longer live inline in the per-tenant SQLite (document_data.data
held 32 MiB base64, bloating the tenant DB and copied on every read). They now go
through the gojabase __blob object-storage seam (same VFS/S3 plane dataroom uses):
- documentsCreate: __blob.put(original) → store the blob KEY (type 'BLOB_KEY').
- maybeSeal: read original via __blob.get, seal, __blob.put(sealed) to a new key,
  point document_data.data at it (initial_data keeps the original key).
- signView/documentsDownload: __blob.get(dd.data) to return pdfBase64.
Keys are tenant-scoped by the host; the bundle handles base64 only.

Consumed by hanzoai/cloud clients/sign (RED consolidation review #117 M7/M8);
gojabase gains a Config.Blob seam + __blob global, sign leaf passes deps.VFS.

Claude-Session: https://claude.ai/code/session_016yg7GPhYdWCh9vpp4HEwLZ

Co-authored-by: hanzo-dev <dev@hanzo.ai>
2026-07-10 14:49:50 -07:00
..

goja/ — Hanzo Sign inside the unified cloud binary

bundle.js is a self-contained, ESM-free port of the Hanzo Sign (Documenso fork) server-side e-signature domain, authored so it runs verbatim inside the dop251/goja JavaScript engine embedded in hanzoai/cloud per HIP-0106 (task #100). It replaces the Next.js/Remix + tRPC + Prisma/Postgres runtime for the core signing flow with TS-on-goja + Hanzo Base/SQLite.

Why a bundle?

goja supports ES2020 but not ES-module import/export and not node: builtins, Prisma, or pdf-lib. So the tRPC handlers cannot be loaded directly. Instead, the exact domain flow — create → recipients → fields → send → sign → complete → seal → audit — is ported here as one CommonJS IIFE and runs in goja. It runs on the reusable clients/gojabase read-write-Base host (the same binding the captable pilot established) — the two things goja cannot do are injected by that host:

  • PersistenceglobalThis.__db (per-tenant Hanzo Base/SQLite, one transaction per dispatch), NOT Prisma.
  • PDF + PKIglobalThis.__pdf (real x509/PKCS#7 signing + pdfcpu rendering), injected by clients/sign via gojabase Config.HostFns, NOT pdf-lib/node:crypto.

Host contract

globalThis.__db.query(sql, args) -> rows[]                 // gojabase, per-tenant Base/SQLite
globalThis.__db.exec(sql, args)  -> { changes, lastId }    // (one txn per dispatch; commits iff status<400)
globalThis.__newId()             -> crypto-random id
globalThis.__now()               -> unix milliseconds
globalThis.__pdf.stamp(pdfBase64, stampsJSON) -> pdfBase64        // render field values (pdfcpu)
globalThis.__pdf.sign(pdfBase64)              -> signedPdfBase64  // x509/PKCS#7 seal (digitorus/pdfsign)
globalThis.handle({ route, params, query, orgId, body }) -> { status, body }

clients/gojabase provides __db/__newId/__now + the per-tenant SQLite file

  • the request transaction; clients/sign provides the sign.db schema and the __pdf host-functions, maps /v1/sign/* HTTP routes to bundle route names, and calls handle() per request.

Tenancy

The host binds __db to the caller's tenant DB before calling handle — from the validated principal for owner routes (/v1/sign/documents/*), or from the :org path segment for recipient token routes (/v1/sign/o/:org/sign/:token). The bundle never chooses a tenant; isolation is a host property.

Route names

Route name HTTP
documents.create POST /v1/sign/documents
documents.list GET /v1/sign/documents
documents.get GET /v1/sign/documents/:id
recipients.add POST /v1/sign/documents/:id/recipients
fields.add POST /v1/sign/documents/:id/fields
documents.send POST /v1/sign/documents/:id/send
documents.download GET /v1/sign/documents/:id/download
documents.audit GET /v1/sign/documents/:id/audit
sign.view GET /v1/sign/o/:org/sign/:token
sign.field POST /v1/sign/o/:org/sign/:token/fields/:fieldId
sign.complete POST /v1/sign/o/:org/sign/:token/complete
sign.reject POST /v1/sign/o/:org/sign/:token/reject