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>
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:
- Persistence —
globalThis.__db(per-tenant Hanzo Base/SQLite, one transaction per dispatch), NOT Prisma. - PDF + PKI —
globalThis.__pdf(real x509/PKCS#7 signing + pdfcpu rendering), injected byclients/signvia gojabaseConfig.HostFns, NOTpdf-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/signprovides thesign.dbschema and the__pdfhost-functions, maps/v1/sign/*HTTP routes to bundle route names, and callshandle()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 |