Files
dataroom/goja/bundle.js
zooqueen 7ef03a2bdd the bundle becomes a module cloud can pin, not a file it copies
cloud carried a hand-vendored byte-copy of goja/bundle.js because this repo is
a TS app with no Go module, so nothing checked what it embedded. The copy had
already drifted: cloud's carried the link deny-list and this one did not.

A std-lib-only embed module — the same shape github.com/hanzoai/sign has — makes
the bundle a dependency cloud requires at a version and records in go.sum, so
the three folds (sign, captable, dataroom) are all pinned the same way and there
is no second copy to drift.

The deny-list the copy was ahead by lands here with it: an address on a link's
deny list is refused even when the allow list would admit it, checked first so
deny cannot be bypassed.
2026-08-14 21:12:45 -07:00

401 lines
19 KiB
JavaScript

// Hanzo Dataroom — goja bundle (read-WRITE, on clients/goja).
//
// SELF-CONTAINED, NO ESM, NO node: imports. The complete dataroom business
// logic (documents, data rooms, shareable links with access controls, viewers,
// per-page view analytics), authored to run verbatim inside the dop251/goja
// engine embedded in hanzoai/cloud (HIP-0106, task #101). It is the ESM-free
// port of the Papermark/Next.js API handlers this fold replaces — the Prisma
// data model becomes Base/SQLite tables (see the leaf's schema.go), the handlers
// become the route table below. No Postgres, no Next.js.
//
// Host contract (clients/goja injects these per dispatch; each dispatch runs
// inside ONE per-tenant SQLite transaction that commits iff status < 400):
// globalThis.__db.query(sql, args) -> [ {col: val, ...}, ... ]
// globalThis.__db.exec(sql, args) -> { changes, lastId }
// globalThis.__newId() -> collision-resistant id (crypto/rand)
// globalThis.__now() -> unix milliseconds
// globalThis.__bcrypt.hash(pw) -> bcrypt hash (leaf HostFn, vetted Go)
// globalThis.__bcrypt.verify(pw,h) -> bool
// globalThis.handle({ route, params, query, orgId, body }) -> { status, body }
//
// Document BYTES live in object storage (the cloud VFS/S3 seam) and are handled
// by the Go leaf; this bundle only ever stores/reads the opaque storage KEY.
//
// This file is the bundle itself, not a copy of one. hanzoai/cloud reaches it by
// importing this repo's Go module (embed.go -> Bundle()) at a pinned version, so
// what cloud runs is these bytes, checked by go.sum. Edit it here.
(function () {
'use strict';
// === tiny helpers ==========================================================
function q(sql, args) { return globalThis.__db.query(sql, args || []); }
function e(sql, args) { return globalThis.__db.exec(sql, args || []); }
function id(prefix) { return (prefix || '') + globalThis.__newId(); }
function now() { return globalThis.__now(); }
function one(rows) { return rows && rows.length ? rows[0] : null; }
function jsonList(v) {
if (v == null) return [];
try { var a = JSON.parse(v); return Array.isArray(a) ? a : []; }
catch (_) { return []; }
}
function truthy(v) { return v === true || v === 1 || v === '1'; }
function asInt(v, dflt) {
if (v == null || v === '') return dflt == null ? null : dflt;
var n = parseInt(v, 10);
return isNaN(n) ? (dflt == null ? null : dflt) : n;
}
// err builds a route result carrying a non-200 status via __status. A >=400
// status also rolls back the dispatch transaction (NewBase), so a rejected
// request leaves the tenant DB untouched.
function err(status, message) { return { __status: status, error: message }; }
// === shapers (row -> API object) ===========================================
function documentOut(r) {
if (!r) return null;
return {
id: r.id, name: r.name, fileKey: r.file_key, contentType: r.content_type,
type: r.type, numPages: r.num_pages, fileSize: r.file_size,
createdAt: r.created_at, updatedAt: r.updated_at,
};
}
function dataroomOut(r) {
if (!r) return null;
return {
id: r.id, pId: r.p_id, name: r.name, description: r.description,
createdAt: r.created_at, updatedAt: r.updated_at,
};
}
function linkOut(r) {
if (!r) return null;
// password_hash is NEVER returned — only whether one is set.
return {
id: r.id, linkType: r.link_type, dataroomId: r.dataroom_id, documentId: r.document_id,
name: r.name, emailProtected: truthy(r.email_protected), hasPassword: !!r.password_hash,
allowList: jsonList(r.allow_list), denyList: jsonList(r.deny_list), allowDownload: truthy(r.allow_download),
expiresAt: r.expires_at, isArchived: truthy(r.is_archived),
createdAt: r.created_at, updatedAt: r.updated_at,
};
}
// emailInList reports whether email matches ANY entry. ONE matching predicate,
// shared by the allow and deny gates (DRY). An entry may be a full email
// ("[email protected]"), a "@domain.com" suffix, or a bare "domain.com".
function emailInList(email, listArr) {
if (!email || !listArr || !listArr.length) return false;
var lc = String(email).toLowerCase().trim();
var at = lc.lastIndexOf('@');
var domain = at >= 0 ? lc.slice(at + 1) : '';
for (var i = 0; i < listArr.length; i++) {
var entry = String(listArr[i]).toLowerCase().trim();
if (!entry) continue;
if (entry.indexOf('@') === 0) { if (domain === entry.slice(1)) return true; }
else if (entry.indexOf('@') > 0) { if (lc === entry) return true; }
else if (domain === entry) return true;
}
return false;
}
// emailAllowed reports whether email passes the link allow list. An EMPTY list
// allows anyone (email gate only); a non-empty list admits only matches.
function emailAllowed(email, allowList) {
if (!allowList || !allowList.length) return true;
return emailInList(email, allowList);
}
// emailDenied reports whether email is on the link deny list. An EMPTY list
// denies no one; a non-empty list rejects any match. Deny is checked BEFORE
// allow in view.authenticate, so deny WINS over allow (an address on both the
// allow and deny lists is refused).
function emailDenied(email, denyList) {
if (!denyList || !denyList.length) return false;
return emailInList(email, denyList);
}
function linkExpired(r) {
return r.expires_at != null && Number(r.expires_at) > 0 && now() > Number(r.expires_at);
}
// === route handlers ========================================================
// Admin routes are org-scoped by the per-tenant DB NewBase selects; the Go
// leaf refuses any request without a validated principal before dispatching.
// Viewer routes run under the org resolved from the public link id.
var routes = {
// ---- documents ----------------------------------------------------------
// POST: the Go leaf has already stored the bytes on the object-storage seam
// and passes the resulting fileKey; this records the metadata row.
'documents.create': function (ctx) {
var b = ctx.body || {};
if (!b.name || !b.fileKey) return err(400, 'name and fileKey required');
var docId = id('doc_');
var t = now();
e('INSERT INTO document (id,name,file_key,content_type,type,num_pages,file_size,created_at,updated_at) ' +
'VALUES (?,?,?,?,?,?,?,?,?)',
[docId, String(b.name), String(b.fileKey), b.contentType || null, b.type || null,
asInt(b.numPages), asInt(b.fileSize), t, t]);
return { document: documentOut(one(q('SELECT * FROM document WHERE id=?', [docId]))) };
},
'documents.list': function () {
return { documents: q('SELECT * FROM document ORDER BY created_at DESC').map(documentOut) };
},
'documents.get': function (ctx) {
var r = one(q('SELECT * FROM document WHERE id=?', [ctx.params.id]));
if (!r) return err(404, 'document not found');
return { document: documentOut(r) };
},
// documents.file returns the storage key so the Go leaf can stream the bytes.
'documents.file': function (ctx) {
var r = one(q('SELECT * FROM document WHERE id=?', [ctx.params.id]));
if (!r) return err(404, 'document not found');
return { fileKey: r.file_key, contentType: r.content_type, name: r.name };
},
// ---- datarooms ----------------------------------------------------------
'datarooms.create': function (ctx) {
var b = ctx.body || {};
if (!b.name) return err(400, 'name required');
var roomId = id('room_');
var pId = 'dr_' + globalThis.__newId().slice(1, 13);
var t = now();
e('INSERT INTO dataroom (id,p_id,name,description,created_at,updated_at) VALUES (?,?,?,?,?,?)',
[roomId, pId, String(b.name), b.description || null, t, t]);
return { dataroom: dataroomOut(one(q('SELECT * FROM dataroom WHERE id=?', [roomId]))) };
},
'datarooms.list': function () {
return { datarooms: q('SELECT * FROM dataroom ORDER BY created_at DESC').map(dataroomOut) };
},
'datarooms.get': function (ctx) {
var r = one(q('SELECT * FROM dataroom WHERE id=?', [ctx.params.id]));
if (!r) return err(404, 'dataroom not found');
var docs = q(
'SELECT dd.id AS dd_id, dd.order_index, doc.* FROM dataroom_document dd ' +
'JOIN document doc ON doc.id = dd.document_id WHERE dd.dataroom_id=? ' +
'ORDER BY dd.order_index IS NULL, dd.order_index, dd.created_at', [r.id]);
var out = dataroomOut(r);
out.documents = docs.map(function (d) {
var doc = documentOut(d);
doc.dataroomDocumentId = d.dd_id;
doc.orderIndex = d.order_index;
return doc;
});
return { dataroom: out };
},
'datarooms.addDocument': function (ctx) {
var b = ctx.body || {};
var roomId = ctx.params.id;
var docId = b.documentId;
if (!docId) return err(400, 'documentId required');
if (!one(q('SELECT id FROM dataroom WHERE id=?', [roomId]))) return err(404, 'dataroom not found');
if (!one(q('SELECT id FROM document WHERE id=?', [docId]))) return err(404, 'document not found');
if (one(q('SELECT id FROM dataroom_document WHERE dataroom_id=? AND document_id=?', [roomId, docId]))) {
return err(409, 'document already in dataroom');
}
var ddId = id('dd_');
e('INSERT INTO dataroom_document (id,dataroom_id,document_id,order_index,created_at) VALUES (?,?,?,?,?)',
[ddId, roomId, docId, asInt(b.orderIndex), now()]);
return { dataroomDocumentId: ddId, dataroomId: roomId, documentId: docId };
},
// ---- links --------------------------------------------------------------
'links.create': function (ctx) {
var b = ctx.body || {};
var roomId = b.dataroomId || null;
var docId = b.documentId || null;
if (!roomId && !docId) return err(400, 'dataroomId or documentId required');
if (roomId && !one(q('SELECT id FROM dataroom WHERE id=?', [roomId]))) return err(404, 'dataroom not found');
if (docId && !one(q('SELECT id FROM document WHERE id=?', [docId]))) return err(404, 'document not found');
var pwHash = null;
if (b.password) pwHash = globalThis.__bcrypt.hash(String(b.password));
var allowList = Array.isArray(b.allowList) ? b.allowList : [];
var denyList = Array.isArray(b.denyList) ? b.denyList : [];
var linkId = id('link_');
var t = now();
var linkType = docId && !roomId ? 'DOCUMENT_LINK' : 'DATAROOM_LINK';
e('INSERT INTO link (id,link_type,dataroom_id,document_id,name,password_hash,email_protected,' +
'allow_list,deny_list,allow_download,expires_at,is_archived,created_at,updated_at) ' +
'VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)',
[linkId, linkType, roomId, docId, b.name || null, pwHash,
b.emailProtected === false ? 0 : 1, JSON.stringify(allowList), JSON.stringify(denyList),
b.allowDownload ? 1 : 0, asInt(b.expiresAt), 0, t, t]);
return { link: linkOut(one(q('SELECT * FROM link WHERE id=?', [linkId]))) };
},
'links.list': function () {
return { links: q('SELECT * FROM link WHERE is_archived=0 ORDER BY created_at DESC').map(linkOut) };
},
// ---- viewer surface (public) -------------------------------------------
// view.link returns the pre-auth metadata a visitor sees (what gates apply).
'view.link': function (ctx) {
var r = one(q('SELECT * FROM link WHERE id=?', [ctx.params.linkId]));
if (!r || truthy(r.is_archived)) return err(404, 'link not found');
var out = {
id: r.id, name: r.name, linkType: r.link_type,
emailProtected: truthy(r.email_protected), hasPassword: !!r.password_hash,
allowDownload: truthy(r.allow_download), expired: linkExpired(r),
};
if (r.dataroom_id) {
var room = one(q('SELECT * FROM dataroom WHERE id=?', [r.dataroom_id]));
if (room) out.dataroom = { id: room.id, pId: room.p_id, name: room.name, description: room.description };
}
if (r.document_id) {
var doc = one(q('SELECT * FROM document WHERE id=?', [r.document_id]));
if (doc) out.document = { id: doc.id, name: doc.name, numPages: doc.num_pages };
}
return { link: out };
},
// view.authenticate enforces the access controls and, on success, records a
// Viewer + a View (the analytics session) and returns the viewable payload.
'view.authenticate': function (ctx) {
var b = ctx.body || {};
var r = one(q('SELECT * FROM link WHERE id=?', [ctx.params.linkId]));
if (!r || truthy(r.is_archived)) return err(404, 'link not found');
if (linkExpired(r)) return err(403, 'link expired');
var email = b.email ? String(b.email).toLowerCase().trim() : '';
if (truthy(r.email_protected) && !email) return err(401, 'email required');
// Deny wins over allow: an address on the deny list is refused even if the
// allow list would admit it. Checked FIRST so deny cannot be bypassed.
if (emailDenied(email, jsonList(r.deny_list))) return err(403, 'email denied');
if (!emailAllowed(email, jsonList(r.allow_list))) return err(403, 'email not allowed');
if (r.password_hash) {
if (!b.password || !globalThis.__bcrypt.verify(String(b.password), r.password_hash)) {
return err(401, 'invalid password');
}
}
var viewerId = null;
if (email) {
var existing = one(q('SELECT * FROM viewer WHERE email=?', [email]));
if (existing) { viewerId = existing.id; }
else {
viewerId = id('viewer_');
var tv = now();
e('INSERT INTO viewer (id,email,verified,created_at,updated_at) VALUES (?,?,?,?,?)',
[viewerId, email, 0, tv, tv]);
}
}
var viewId = id('view_');
var viewType = r.dataroom_id ? 'DATAROOM_VIEW' : 'DOCUMENT_VIEW';
e('INSERT INTO view (id,link_id,dataroom_id,document_id,viewer_id,viewer_email,view_type,verified,viewed_at) ' +
'VALUES (?,?,?,?,?,?,?,?,?)',
[viewId, r.id, r.dataroom_id, r.document_id, viewerId, email || null, viewType, 0, now()]);
var payload = { viewId: viewId, viewerId: viewerId, allowDownload: truthy(r.allow_download) };
if (r.dataroom_id) {
var docs = q(
'SELECT dd.id AS dd_id, doc.* FROM dataroom_document dd ' +
'JOIN document doc ON doc.id = dd.document_id WHERE dd.dataroom_id=? ' +
'ORDER BY dd.order_index IS NULL, dd.order_index, dd.created_at', [r.dataroom_id]);
payload.documents = docs.map(function (d) {
return { id: d.id, dataroomDocumentId: d.dd_id, name: d.name, numPages: d.num_pages };
});
} else if (r.document_id) {
var single = one(q('SELECT * FROM document WHERE id=?', [r.document_id]));
if (single) payload.document = { id: single.id, name: single.name, numPages: single.num_pages };
}
return payload;
},
// view.recordPage records ONE per-page analytics event (the page-by-page
// tracking) against an existing View.
'view.recordPage': function (ctx) {
var b = ctx.body || {};
var linkId = ctx.params.linkId;
var v = one(q('SELECT * FROM view WHERE id=? AND link_id=?', [b.viewId, linkId]));
if (!v) return err(404, 'view not found');
if (b.pageNumber == null) return err(400, 'pageNumber required');
var pvId = id('pv_');
e('INSERT INTO page_view (id,view_id,link_id,document_id,dataroom_id,page_number,version_number,duration,viewed_at) ' +
'VALUES (?,?,?,?,?,?,?,?,?)',
[pvId, v.id, linkId, b.documentId || v.document_id, v.dataroom_id,
asInt(b.pageNumber, 0), asInt(b.versionNumber), asInt(b.duration, 0), now()]);
return { ok: true, id: pvId };
},
// view.file authorises a viewer download and returns the storage key.
'view.file': function (ctx) {
var linkId = ctx.params.linkId;
var docId = ctx.params.documentId;
var viewId = ctx.query.viewId;
var r = one(q('SELECT * FROM link WHERE id=?', [linkId]));
if (!r || truthy(r.is_archived)) return err(404, 'link not found');
if (!one(q('SELECT id FROM view WHERE id=? AND link_id=?', [viewId, linkId]))) return err(403, 'no active view');
if (truthy(ctx.query.download) && !truthy(r.allow_download)) return err(403, 'download disabled');
// The document must be reachable through this link (dataroom membership or
// the link's own document) — a viewer cannot address an unrelated doc.
if (r.dataroom_id) {
if (!one(q('SELECT id FROM dataroom_document WHERE dataroom_id=? AND document_id=?', [r.dataroom_id, docId]))) {
return err(404, 'document not in dataroom');
}
} else if (r.document_id !== docId) {
return err(404, 'document not on link');
}
var doc = one(q('SELECT * FROM document WHERE id=?', [docId]));
if (!doc) return err(404, 'document not found');
return { fileKey: doc.file_key, contentType: doc.content_type, name: doc.name };
},
// ---- analytics ----------------------------------------------------------
'analytics.link': function (ctx) {
var linkId = ctx.params.linkId;
if (!one(q('SELECT id FROM link WHERE id=?', [linkId]))) return err(404, 'link not found');
return linkAnalytics(linkId);
},
'analytics.dataroom': function (ctx) {
var roomId = ctx.params.dataroomId;
if (!one(q('SELECT id FROM dataroom WHERE id=?', [roomId]))) return err(404, 'dataroom not found');
var links = q('SELECT id FROM link WHERE dataroom_id=?', [roomId]);
var perLink = links.map(function (l) { return linkAnalytics(l.id); });
var totalViews = 0, totalPageViews = 0;
perLink.forEach(function (a) { totalViews += a.totalViews; totalPageViews += a.totalPageViews; });
return { dataroomId: roomId, totalViews: totalViews, totalPageViews: totalPageViews, links: perLink };
},
};
function linkAnalytics(linkId) {
var totalViews = Number((one(q('SELECT COUNT(*) AS n FROM view WHERE link_id=?', [linkId])) || {}).n || 0);
var totalPageViews = Number((one(q('SELECT COUNT(*) AS n FROM page_view WHERE link_id=?', [linkId])) || {}).n || 0);
var pages = q(
'SELECT page_number, COUNT(*) AS views, SUM(duration) AS total_duration ' +
'FROM page_view WHERE link_id=? GROUP BY page_number ORDER BY page_number', [linkId]);
return {
linkId: linkId, totalViews: totalViews, totalPageViews: totalPageViews,
pages: pages.map(function (p) {
var v = Number(p.views || 0), d = Number(p.total_duration || 0);
return { pageNumber: Number(p.page_number), views: v, totalDuration: d, avgDuration: v ? Math.round(d / v) : 0 };
}),
};
}
// === dispatch entry (host calls this per request) ==========================
globalThis.handle = function (req) {
req = req || {};
var ctx = {
params: req.params || {},
query: req.query || {},
orgId: req.orgId || '',
body: req.body || null,
};
var fn = routes[req.route];
if (!fn) return { status: 404, body: { error: 'unknown dataroom route: ' + req.route } };
try {
var out = fn(ctx);
if (out && out.__status) {
var status = out.__status;
delete out.__status;
return { status: status, body: out };
}
return { status: 200, body: out };
} catch (ex) {
return { status: 500, body: { error: String(ex && ex.message || ex) } };
}
};
})();