blob: 4cde24798a69b21dd831bac70a7078f5dce046b0 [file]
#!/usr/bin/env node
// Acquire + verify + extract the pinned cua-driver compatibility release for
// bundling into Maka.app. The source patch remains published in hqhq1025/cua
// and proposed upstream; Maka only consumes an immutable, provenance-carrying
// release artifact. Mirrors scripts/prepare-officecli.mjs: single-source version pin in
// apps/desktop/bundled-tools.json, checksum verified fail-closed, extracted to a
// pinned repo path (resources/bin/cua-driver), idempotent via a marker file.
//
// cua-driver ships ONE darwin-universal tarball (arm64 + x64), so unlike
// OfficeCLI there is no per-arch asset. This tool is macOS-only (the Tier-2
// coordinate-injection backend); on other platforms this is a no-op.
//
// Dev usage: `npm run prepare:cua-driver`. The extracted binary is spawned as a
// DIRECT child by cua-driver-backend.ts. This script verifies that the release
// artifact has a valid code signature, but it does not claim Developer ID,
// notarization, Gatekeeper, or final Maka.app nested-signature readiness.
import { execFile } from 'node:child_process';
import { createHash, randomUUID } from 'node:crypto';
import { constants } from 'node:fs';
import {
access,
chmod,
mkdir,
open,
readFile,
rename,
rm,
stat,
writeFile,
} from 'node:fs/promises';
import { mkdtemp } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { promisify } from 'node:util';
const execFileAsync = promisify(execFile);
const scriptDir = dirname(fileURLToPath(import.meta.url));
const repoRoot = resolve(scriptDir, '..');
const manifestPath = join(repoRoot, 'apps', 'desktop', 'bundled-tools.json');
const binDir = join(repoRoot, 'apps', 'desktop', 'resources', 'bin');
const licenseDir = join(repoRoot, 'apps', 'desktop', 'resources', 'licenses', 'cua-driver');
const DEFAULT_FETCH_TIMEOUT_MS = 300_000;
const SHA256_PATTERN = /^[a-f0-9]{64}$/;
const MACH_O_MAGICS = new Set([
'feedface',
'feedfacf',
'cefaedfe',
'cffaedfe',
'cafebabe',
'bebafeca',
'cafebabf',
'bfbafeca',
]);
const manifest = JSON.parse(await readFile(manifestPath, 'utf8'));
const cua = manifest.cuaDriver;
const FETCH_TIMEOUT_MS = readPositiveIntEnv(
'MAKA_CUA_DRIVER_FETCH_TIMEOUT_MS',
DEFAULT_FETCH_TIMEOUT_MS,
);
export function cuaDriverSupported(platform = process.platform) {
return platform === 'darwin';
}
export function cuaDriverDownloadUrl(tag, asset) {
return `https://github.com/${cua.repo}/releases/download/${tag}/${asset}`;
}
export function sha256(data) {
return createHash('sha256').update(Buffer.from(data)).digest('hex');
}
export function assertPinnedCuaDriverChecksums(entry) {
for (const field of ['archiveSha256', 'binarySha256', 'licenseSha256', 'sourceSha256']) {
if (!SHA256_PATTERN.test(entry?.[field] ?? '')) {
throw new Error(
`bundled-tools.json cuaDriver.${field} must be a pinned lowercase 64-character SHA-256 digest ` +
`(received ${JSON.stringify(entry?.[field])}).`,
);
}
}
if (entry.archiveSha256 === entry.binarySha256) {
throw new Error(
'bundled-tools.json must pin the cua-driver archive and extracted binary separately.',
);
}
if (Object.prototype.hasOwnProperty.call(entry, 'sha256')) {
throw new Error(
'bundled-tools.json cuaDriver.sha256 is ambiguous; use archiveSha256 and binarySha256.',
);
}
if (
typeof entry?.expectedVersion !== 'string' ||
typeof entry?.expectedProtocolVersion !== 'string' ||
typeof entry?.sourceCommit !== 'string' ||
typeof entry?.upstreamCommit !== 'string' ||
typeof entry?.upstreamMergeCommit !== 'string' ||
typeof entry?.cargoLockSha256 !== 'string' ||
!Array.isArray(entry?.architectures) ||
entry.architectures.length === 0 ||
!['archiveSizeBytes', 'binarySizeBytes', 'licenseSizeBytes', 'sourceSizeBytes'].every(
(field) => Number.isSafeInteger(entry?.[field]) && entry[field] > 0,
)
) {
throw new Error(
'bundled-tools.json cuaDriver must pin version, protocol, source commits, Cargo.lock, architectures, and exact file sizes.',
);
}
}
function destinationPath() {
return join(binDir, cua.binaryName);
}
function markerPath() {
return join(binDir, '.cua-driver.json');
}
function expectedMarker() {
return {
version: cua.version,
expectedVersion: cua.expectedVersion,
expectedProtocolVersion: cua.expectedProtocolVersion,
sourceCommit: cua.sourceCommit,
upstreamCommit: cua.upstreamCommit,
upstreamMergeCommit: cua.upstreamMergeCommit,
archiveSizeBytes: cua.archiveSizeBytes,
binarySizeBytes: cua.binarySizeBytes,
licenseSizeBytes: cua.licenseSizeBytes,
sourceSizeBytes: cua.sourceSizeBytes,
archiveSha256: cua.archiveSha256,
binarySha256: cua.binarySha256,
licenseSha256: cua.licenseSha256,
sourceSha256: cua.sourceSha256,
};
}
function markerMatches(marker) {
const expected = expectedMarker();
return (
marker?.version === expected.version &&
marker?.expectedVersion === expected.expectedVersion &&
marker?.expectedProtocolVersion === expected.expectedProtocolVersion &&
marker?.sourceCommit === expected.sourceCommit &&
marker?.upstreamCommit === expected.upstreamCommit &&
marker?.upstreamMergeCommit === expected.upstreamMergeCommit &&
marker?.archiveSizeBytes === expected.archiveSizeBytes &&
marker?.binarySizeBytes === expected.binarySizeBytes &&
marker?.licenseSizeBytes === expected.licenseSizeBytes &&
marker?.sourceSizeBytes === expected.sourceSizeBytes &&
marker?.archiveSha256 === expected.archiveSha256 &&
marker?.binarySha256 === expected.binarySha256 &&
marker?.licenseSha256 === expected.licenseSha256 &&
marker?.sourceSha256 === expected.sourceSha256
);
}
function readPositiveIntEnv(name, fallback) {
const raw = process.env[name];
if (!raw) return fallback;
const parsed = Number(raw);
if (!Number.isSafeInteger(parsed) || parsed <= 0) {
throw new Error(`${name} must be a positive integer timeout in milliseconds`);
}
return parsed;
}
function isTimeoutError(error) {
return Boolean(
error &&
typeof error === 'object' &&
(error.name === 'AbortError' || error.name === 'TimeoutError'),
);
}
function timeoutError(url) {
return new Error(`Timed out downloading ${url} after ${FETCH_TIMEOUT_MS}ms`);
}
export async function downloadFileWithSha256(
url,
destination,
{ maxBytes, fetchImpl = fetch, timeoutMs = FETCH_TIMEOUT_MS } = {},
) {
if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0) {
throw new Error('cua-driver download maxBytes must be a positive integer');
}
let response;
try {
response = await fetchImpl(url, {
redirect: 'follow',
signal: AbortSignal.timeout(timeoutMs),
});
} catch (error) {
if (isTimeoutError(error)) throw timeoutError(url);
throw error;
}
if (!response.ok) throw new Error(`Failed to download ${url}: HTTP ${response.status}`);
const declaredLength = Number(response.headers.get('content-length'));
if (Number.isFinite(declaredLength) && declaredLength > maxBytes) {
throw new Error(
`Refusing oversized cua-driver download: ${declaredLength} bytes exceeds ${maxBytes}`,
);
}
if (!response.body) {
throw new Error(`Failed to download ${url}: response body missing`);
}
const hash = createHash('sha256');
let bytes = 0;
let completed = false;
const file = await open(destination, 'wx', 0o600);
try {
for await (const rawChunk of response.body) {
const chunk = Buffer.from(rawChunk);
bytes += chunk.byteLength;
if (bytes > maxBytes) {
throw new Error(
`Refusing oversized cua-driver download: received more than ${maxBytes} bytes`,
);
}
hash.update(chunk);
await file.write(chunk);
}
completed = true;
return { bytes, sha256: hash.digest('hex') };
} catch (error) {
if (isTimeoutError(error)) throw timeoutError(url);
throw error;
} finally {
await file.close();
if (!completed) await rm(destination, { force: true });
}
}
// Idempotency: skip the network round-trip when the pinned version + checksum
// already match the on-disk marker AND the binary is present + executable.
async function alreadyPrepared() {
try {
await access(destinationPath(), constants.X_OK);
const marker = JSON.parse(await readFile(markerPath(), 'utf8'));
if (!markerMatches(marker)) return false;
const [binaryInfo, licenseInfo, sourceInfo] = await Promise.all([
stat(destinationPath()),
stat(join(licenseDir, 'LICENSE.md')),
stat(join(licenseDir, 'SOURCE.json')),
]);
if (
binaryInfo.size !== cua.binarySizeBytes ||
licenseInfo.size !== cua.licenseSizeBytes ||
sourceInfo.size !== cua.sourceSizeBytes
)
return false;
// Re-hash the actual binary so a corrupted/swapped file with an intact marker
// is not silently trusted — on drift, fall through to re-download/re-verify.
const actualBinarySha256 = sha256(await readFile(destinationPath()));
const actualLicenseSha256 = sha256(await readFile(join(licenseDir, 'LICENSE.md')));
const actualSourceSha256 = sha256(await readFile(join(licenseDir, 'SOURCE.json')));
return (
actualBinarySha256 === cua.binarySha256 &&
actualLicenseSha256 === cua.licenseSha256 &&
actualSourceSha256 === cua.sourceSha256
);
} catch {
return false;
}
}
export function assertMachOHeader(bytes) {
const header = Buffer.from(bytes).subarray(0, 4);
const magic = header.toString('hex');
if (header.byteLength !== 4 || !MACH_O_MAGICS.has(magic)) {
throw new Error(`cua-driver is not a recognized Mach-O binary (magic ${magic || 'missing'})`);
}
}
export async function verifyBinaryMetadata(binaryPath, entry = cua, runCommand = execFileAsync) {
const info = await stat(binaryPath);
if (!info.isFile() || info.size !== entry.binarySizeBytes) {
throw new Error(
`Unexpected cua-driver size: expected ${entry.binarySizeBytes}, got ${info.size}`,
);
}
const handle = await open(binaryPath, 'r');
try {
const header = Buffer.alloc(4);
const { bytesRead } = await handle.read(header, 0, header.length, 0);
assertMachOHeader(header.subarray(0, bytesRead));
} finally {
await handle.close();
}
await runCommand('lipo', [binaryPath, '-verify_arch', ...entry.architectures]);
await runCommand('codesign', ['--verify', '--strict', '--verbose=2', binaryPath]);
const signature = await runCommand('codesign', ['--display', '--verbose=4', binaryPath]);
const signatureDetails = `${signature.stdout ?? ''}\n${signature.stderr ?? ''}`;
if (entry.signature === 'adhoc' && !/Signature=adhoc/.test(signatureDetails)) {
throw new Error('cua-driver signature mismatch: expected an ad hoc code signature');
}
}
export async function verifyBinaryVersion(binaryPath, entry = cua, runCommand = execFileAsync) {
const { stdout } = await runCommand(binaryPath, ['--version']);
if (stdout.trim() !== `cua-driver ${entry.expectedVersion}`) {
throw new Error(
`Unexpected cua-driver version: expected ${entry.expectedVersion}, got ${JSON.stringify(stdout.trim())}`,
);
}
}
export function assertSafeTarEntries(entries) {
for (const rawEntry of entries) {
const entry = rawEntry.trim();
if (!entry) continue;
if (entry.startsWith('/') || entry.split('/').includes('..') || entry.includes('\\')) {
throw new Error(`Unsafe cua-driver archive entry: ${JSON.stringify(entry)}`);
}
}
}
export function assertSafeTarListing(lines) {
for (const line of lines) {
if (!line.trim()) continue;
const type = line[0];
if (type !== '-' && type !== 'd') {
throw new Error(`Unsupported cua-driver archive entry type: ${JSON.stringify(line)}`);
}
}
}
export function assertSourceProvenance(source, entry = cua) {
const expected = {
repository: entry.repo,
upstreamTag: entry.upstreamTag,
upstreamCommit: entry.upstreamCommit,
sourceCommit: entry.sourceCommit,
patchPullRequest: entry.patchPullRequest,
cargoLockSha256: entry.cargoLockSha256,
signature: entry.signature,
};
for (const [field, value] of Object.entries(expected)) {
if (source?.[field] !== value) {
throw new Error(
`cua-driver SOURCE.json ${field} mismatch: expected ${JSON.stringify(value)}, got ${JSON.stringify(source?.[field])}`,
);
}
}
if (
!Array.isArray(source?.architectures) ||
source.architectures.length !== entry.architectures.length ||
!entry.architectures.every((arch) => source.architectures.includes(arch))
) {
throw new Error('cua-driver SOURCE.json architectures do not match bundled-tools.json.');
}
}
export async function prepareCuaDriver(targetPlatform = process.platform) {
if (!cuaDriverSupported(targetPlatform)) {
return { skipped: true, reason: `cua-driver is macOS-only; skipping ${targetPlatform}` };
}
assertPinnedCuaDriverChecksums(cua);
if (await alreadyPrepared()) {
return {
skipped: true,
reason: 'up-to-date',
destination: destinationPath(),
version: cua.version,
};
}
// Extract the tarball to a temp dir, then copy out the single `cua-driver`
// Mach-O. Tarball internal layout is not assumed — we locate the binary.
const workDir = await mkdtemp(join(tmpdir(), 'maka-cua-driver-'));
try {
const tarPath = join(workDir, cua.asset);
const url = cuaDriverDownloadUrl(cua.tag, cua.asset);
const downloaded = await downloadFileWithSha256(url, tarPath, {
maxBytes: cua.archiveSizeBytes,
});
if (downloaded.bytes !== cua.archiveSizeBytes) {
throw new Error(
`Size mismatch for ${cua.asset}: expected ${cua.archiveSizeBytes}, got ${downloaded.bytes}`,
);
}
if (downloaded.sha256 !== cua.archiveSha256) {
throw new Error(
`Checksum mismatch for ${cua.asset}: expected ${cua.archiveSha256}, got ${downloaded.sha256}`,
);
}
const listed = await execFileAsync('tar', ['-tzf', tarPath]);
assertSafeTarEntries(listed.stdout.split('\n'));
const verboseListing = await execFileAsync('tar', ['-tvzf', tarPath]);
assertSafeTarListing(verboseListing.stdout.split('\n'));
await execFileAsync('tar', ['-xzf', tarPath, '-C', workDir]);
const { stdout } = await execFileAsync('find', [
workDir,
'-name',
cua.binaryName,
'-type',
'f',
]);
const found = stdout
.split('\n')
.map((l) => l.trim())
.filter(Boolean);
if (found.length !== 1) {
throw new Error(
`Extracted archive ${cua.asset} must contain exactly one '${cua.binaryName}' binary (found ${found.length})`,
);
}
const binaryBytes = await readFile(found[0]);
if (binaryBytes.byteLength !== cua.binarySizeBytes) {
throw new Error(
`Size mismatch for extracted ${cua.binaryName}: expected ${cua.binarySizeBytes}, got ${binaryBytes.byteLength}`,
);
}
const actualBinarySha256 = sha256(binaryBytes);
if (actualBinarySha256 !== cua.binarySha256) {
throw new Error(
`Checksum mismatch for extracted ${cua.binaryName}: expected ${cua.binarySha256}, got ${actualBinarySha256}`,
);
}
await verifyBinaryMetadata(found[0]);
const licensePaths = await execFileAsync('find', [
workDir,
'-name',
'LICENSE.md',
'-type',
'f',
]);
const sourcePaths = await execFileAsync('find', [
workDir,
'-name',
'SOURCE.json',
'-type',
'f',
]);
const licenses = licensePaths.stdout
.split('\n')
.map((line) => line.trim())
.filter(Boolean);
const sources = sourcePaths.stdout
.split('\n')
.map((line) => line.trim())
.filter(Boolean);
if (licenses.length !== 1 || sources.length !== 1) {
throw new Error(
`Extracted archive ${cua.asset} must contain exactly one LICENSE.md and SOURCE.json`,
);
}
const licenseBytes = await readFile(licenses[0]);
const sourceBytes = await readFile(sources[0]);
if (licenseBytes.byteLength !== cua.licenseSizeBytes) {
throw new Error(
`Size mismatch for extracted cua-driver LICENSE.md: expected ${cua.licenseSizeBytes}, got ${licenseBytes.byteLength}`,
);
}
if (sourceBytes.byteLength !== cua.sourceSizeBytes) {
throw new Error(
`Size mismatch for extracted cua-driver SOURCE.json: expected ${cua.sourceSizeBytes}, got ${sourceBytes.byteLength}`,
);
}
if (sha256(licenseBytes) !== cua.licenseSha256) {
throw new Error(`Checksum mismatch for extracted cua-driver LICENSE.md`);
}
if (sha256(sourceBytes) !== cua.sourceSha256) {
throw new Error(`Checksum mismatch for extracted cua-driver SOURCE.json`);
}
assertSourceProvenance(JSON.parse(sourceBytes.toString('utf8')));
await verifyBinaryVersion(found[0]);
await mkdir(binDir, { recursive: true });
await mkdir(licenseDir, { recursive: true });
const destination = destinationPath();
const marker = markerPath();
const installId = randomUUID();
const stagedBinary = `${destination}.${installId}.tmp`;
const stagedMarker = `${marker}.${installId}.tmp`;
try {
await writeFile(stagedBinary, binaryBytes);
await chmod(stagedBinary, 0o755);
// Best-effort: clear the download quarantine xattr so the dev Electron process
// can spawn it without a Gatekeeper prompt. Non-fatal if xattr is absent.
try {
await execFileAsync('xattr', ['-d', 'com.apple.quarantine', stagedBinary]);
} catch {
/* no quarantine attr — fine */
}
await writeFile(stagedMarker, `${JSON.stringify(expectedMarker(), null, 2)}\n`);
await rename(stagedBinary, destination);
await rename(stagedMarker, marker);
await writeFile(join(licenseDir, 'LICENSE.md'), licenseBytes);
await writeFile(join(licenseDir, 'SOURCE.json'), sourceBytes);
} finally {
await rm(stagedBinary, { force: true });
await rm(stagedMarker, { force: true });
}
return {
skipped: false,
destination,
version: cua.version,
signatureMode: cua.signature,
releaseSigningReady: false,
};
} finally {
await rm(workDir, { recursive: true, force: true });
}
}
if (process.argv[1] === fileURLToPath(import.meta.url)) {
const result = await prepareCuaDriver();
if (result.skipped) {
process.stdout.write(`cua-driver: ${result.reason}\n`);
} else {
process.stdout.write(`Prepared cua-driver ${result.version}: ${result.destination}\n`);
}
}