#!/usr/bin/env node // land-work's read of the bus (row 116, #1610). The checks themselves are in // packages/bus/src/landing.mjs, which the broker's agent landings share // (row 137, #1631); every query there is a SELECT. // // node scripts/land-work/read.mjs // // It opens /bus/bus.sqlite with readOnly: true and checks the // schema the way the Store does before reading anything. Output is one // "keyvalue" line per fact land-work needs, or one // "refusereasondetail" line and exit 2. A missing or unsafe // database or business file exits 3. // // Schema version and digest follow the Store exports; a schema bump has one authority. import { DatabaseSync } from 'node:sqlite'; import { readFileSync, lstatSync } from 'node:fs'; import { join } from 'node:path'; import { pathToFileURL } from 'node:url'; import { createHash } from 'node:crypto'; import { schemaDigest, SCHEMA_SHA256, SCHEMA_VERSION } from '../../packages/bus/src/store.mjs'; import { LandingRefusal, readLanding as shared } from '../../packages/bus/src/landing.mjs'; const refuse = (reason, detail) => { throw new LandingRefusal(reason, detail); }; export function openBus(dataRoot) { const path = join(dataRoot, 'bus', 'bus.sqlite'); let s; try { s = lstatSync(path); } catch { throw Object.assign(new Error(`no bus database at ${path}`), { exit: 3 }); } if (s.isSymbolicLink() || !s.isFile() || s.uid !== process.getuid()) throw Object.assign(new Error(`unsafe bus database ${path}`), { exit: 3 }); const db = new DatabaseSync(path, { readOnly: true, timeout: 5000 }); try { checkSchema(db); } catch (e) { db.close(); throw e; } return db; } // The Store's own open check (store.mjs), against a reference built from the // same schema.sql the import has already verified against SCHEMA_SHA256. export function checkSchema(db) { const ddl = readFileSync(new URL('../../packages/bus/schema.sql', import.meta.url), 'utf8'); if (createHash('sha256').update(ddl).digest('hex') !== SCHEMA_SHA256) refuse('bus-schema', 'packages/bus/schema.sql does not match SCHEMA_SHA256'); const reference = new DatabaseSync(':memory:'); reference.exec(ddl); const expected = schemaDigest(reference); reference.close(); const meta = (key) => db.prepare('SELECT value FROM meta WHERE key = ?').get(key)?.value; let digest; try { digest = schemaDigest(db); } catch (e) { refuse('bus-schema', `cannot read the schema: ${e.message}`); } if (digest !== expected) refuse('bus-schema', `schema digest ${digest.slice(0, 12)}, the store expects ${expected.slice(0, 12)}`); if (meta('schema_version') !== SCHEMA_VERSION || meta('schema_digest') !== expected) refuse('bus-schema', `meta says version ${meta('schema_version')}, digest ${String(meta('schema_digest')).slice(0, 12)}`); } // land-work lands only on refactor (#1610): the base it was given must be // refactor, and the raise must target it. export function readLanding(db, business, id, base) { if (base !== 'refactor') refuse('target-branch', `the base is ${base}, land-work lands only on refactor`); const q = { get: (sql, ...p) => db.prepare(sql).get(...p), all: (sql, ...p) => db.prepare(sql).all(...p) }; return shared(q, business, id, { into: 'refactor' }); } function main([dataRoot, businessFile, id, base]) { const out = (k, v) => process.stdout.write(`${k}\t${String(v ?? '').replace(/[\t\n\r]/g, ' ')}\n`); let business; try { business = JSON.parse(readFileSync(businessFile, 'utf8')); } catch (e) { process.stderr.write(`land-work: cannot read the business file ${businessFile}: ${e.message}\n`); return 3; } if (typeof business?.id !== 'string' || typeof business?.human !== 'string' || !business.human) { process.stderr.write(`land-work: ${businessFile} names no business id and human\n`); return 3; } let db; try { db = openBus(dataRoot); for (const [k, v] of Object.entries(readLanding(db, business, id, base))) out(k, v); return 0; } catch (e) { if (e instanceof LandingRefusal) { out('refuse', `${e.reason}\t${e.message}`); return 2; } process.stderr.write(`land-work: ${e.message}\n`); return e.exit ?? 1; } finally { db?.close(); } } if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) process.exitCode = main(process.argv.slice(2));