mirror of
https://github.com/hanzoai/trust.git
synced 2026-10-30 14:54:58 +00:00
151 lines
6.7 KiB
JavaScript
151 lines
6.7 KiB
JavaScript
// Builds the self-contained, ESM-free goja bundle from the TypeScript source and
|
|
// the inventory.
|
|
//
|
|
// Order matters and is the point: VALIDATE, then inline, then bundle. A control
|
|
// with a bad status, a mapping to a clause no framework declares, or a sentence
|
|
// that claims a certification stops the build — there is no artifact to ship and
|
|
// therefore no number to misread. See check.mjs for the rules.
|
|
//
|
|
// Output: goja/bundle.js — an IIFE targeting ES2015, platform "neutral" so
|
|
// esbuild injects NO node/browser globals and no `node:` builtins. The only
|
|
// globals it touches are the two the Go host injects (__now, __audit) and
|
|
// standard JS (JSON/Math/RegExp/Date.UTC).
|
|
//
|
|
// Run: `node goja/build.mjs` (esbuild is the sole devDependency)
|
|
// The built bundle.js is committed so hanzoai/cloud can go:embed it (embed.go),
|
|
// exactly how hanzoai/plans and hanzoai/captable ship theirs.
|
|
|
|
import { build as esbuild } from "esbuild";
|
|
import { readFileSync } from "node:fs";
|
|
import { fileURLToPath } from "node:url";
|
|
import { dirname, join } from "node:path";
|
|
import { assertValid } from "./check.mjs";
|
|
|
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
const root = join(here, "..");
|
|
|
|
const read = (name) => JSON.parse(readFileSync(join(root, name), "utf8"));
|
|
|
|
// controls.json is a LIST of controls, optionally wrapped in an envelope that
|
|
// carries a "$comment" beside it. Both spellings are read here, once, so
|
|
// everything downstream — the validator, the bundle, the tests — sees a list.
|
|
// Handing an envelope to code that indexes an array is the exact failure this
|
|
// repo exists to refuse: every count comes back zero and nothing says why.
|
|
export const list = (controls) => (Array.isArray(controls) ? controls : controls.controls);
|
|
|
|
// The inventory has been written two ways: `enforced`/`verified`/`maps`-as-edges,
|
|
// and `source`/`check`/`evidence`/`maps`-as-lists. They carry the same facts.
|
|
// normalize reads either and emits the first, ONCE, at the build edge — so the
|
|
// validator, the bundle and the tests below it all see one shape and there is no
|
|
// second spelling anywhere past this line.
|
|
export function normalize(input) {
|
|
return list(input).map((c) => {
|
|
const enforced = c.enforced || c.source || [];
|
|
let verified = c.verified;
|
|
if (!verified) {
|
|
verified = [];
|
|
if (c.check) verified.push({ method: "read", at: enforced, detail: c.check });
|
|
if (c.evidence && c.evidence.length) {
|
|
verified.push({ method: "audit", actions: c.evidence });
|
|
}
|
|
}
|
|
let maps = c.maps;
|
|
if (maps && !Array.isArray(maps)) {
|
|
// A clause list carries no per-edge strength, so the control's own status
|
|
// sets it: an automated control answers a clause it names in full, a
|
|
// partial one only in part. Nothing is invented, and nothing is inflated.
|
|
const strength = c.status === "automated" ? "full" : "partial";
|
|
const flat = [];
|
|
for (const fw of Object.keys(maps)) {
|
|
for (const id of maps[fw]) flat.push({ clause: `${fw}:${id}`, strength });
|
|
}
|
|
maps = flat;
|
|
}
|
|
const out = { id: c.id };
|
|
// normalize builds the control field by field rather than spreading, so a
|
|
// field it does not name is DROPPED — silently, and only visible on the
|
|
// page, where the group a control belongs to had simply gone missing.
|
|
if (c.category) out.category = c.category;
|
|
if (c.title) out.title = c.title;
|
|
out.claim = c.claim;
|
|
out.mechanism = c.mechanism;
|
|
out.status = c.status;
|
|
if (c.note) out.note = c.note;
|
|
out.enforced = enforced;
|
|
out.verified = verified;
|
|
out.maps = maps || [];
|
|
return out;
|
|
});
|
|
}
|
|
|
|
// version comes from embed.go, so the Go const and the bundle can never disagree
|
|
// about which inventory is running.
|
|
export function version() {
|
|
const go = readFileSync(join(root, "embed.go"), "utf8");
|
|
const m = /const Version = "([^"]+)"/.exec(go);
|
|
if (!m) throw new Error("trust: embed.go has no Version const");
|
|
return m[1];
|
|
}
|
|
|
|
// build returns the bundle source.
|
|
//
|
|
// write false rebuild in memory — how the staleness test compares the
|
|
// committed artifact against a fresh one.
|
|
// controls/frameworks
|
|
// build against data other than the repo's. The tests use
|
|
// this to fold synthetic inventories through the SAME code
|
|
// that ships, rather than through a second implementation.
|
|
//
|
|
// Validation runs either way. A synthetic inventory that would not be allowed
|
|
// in the repo is not allowed in a test either.
|
|
export async function build({ write = true, controls, frameworks, subprocessors } = {}) {
|
|
controls = normalize(controls || read("controls.json"));
|
|
frameworks = frameworks || read("frameworks.json");
|
|
// The envelope again — `{ $comment, subprocessors }` — read once, here, so
|
|
// everything downstream sees a list.
|
|
const parties = subprocessors || read("subprocessors.json");
|
|
subprocessors = Array.isArray(parties) ? parties : parties.subprocessors;
|
|
assertValid(controls, frameworks, subprocessors);
|
|
|
|
const out = await esbuild({
|
|
entryPoints: [join(here, "src/index.ts")],
|
|
bundle: true,
|
|
format: "iife",
|
|
target: "es2015",
|
|
platform: "neutral",
|
|
legalComments: "none",
|
|
// The bundle is a TRACKED artifact, so it is read by the same copy gate every
|
|
// hand-written surface is, and that gate is LINE-SCOPED. Printed as one line,
|
|
// 174 clause titles and three notes all share it with every framework name,
|
|
// so ISO A.5.30 "ICT readiness for business continuity" reads as a hedge
|
|
// beside "SOC 2". Wrapping restores the scoping the gate assumes, and makes
|
|
// a 64 KB inventory diffable in git, which is the repo's whole premise.
|
|
lineLimit: 100,
|
|
define: {
|
|
// The file is an envelope — "$comment" beside the list — so the LIST is
|
|
// what gets inlined. Handing the envelope to a bundle that indexes an
|
|
// array is the exact failure this repo exists to refuse: every count
|
|
// would come back zero and nothing would say why.
|
|
__CONTROLS__: JSON.stringify(controls),
|
|
__FRAMEWORKS__: JSON.stringify(frameworks),
|
|
__SUBPROCESSORS__: JSON.stringify(subprocessors),
|
|
__VERSION__: JSON.stringify(version()),
|
|
},
|
|
banner: {
|
|
js:
|
|
"// @hanzo/trust — goja bundle (GENERATED by goja/build.mjs; edit src/*.ts + controls.json).\n" +
|
|
"// Self-contained, ESM-free; runs in dop251/goja inside hanzoai/cloud (HIP-0106).",
|
|
},
|
|
outfile: write ? join(here, "bundle.js") : undefined,
|
|
write: write,
|
|
});
|
|
|
|
return write ? null : out.outputFiles[0].text;
|
|
}
|
|
|
|
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
const controls = normalize(read("controls.json"));
|
|
await build();
|
|
console.log(`built goja/bundle.js — ${controls.length} controls, version ${version()}`);
|
|
}
|