import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
import chalk from 'chalk';
import ora from 'ora';
import fs from 'fs-extra';
import path from 'path';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
import { createRequire } from 'module';
import { setupPgpKeys } from './lib/pgp.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const require = createRequire(import.meta.url);
let proofmode;
async function initializeProofMode() {
try {
const wasmModule = require('../../pkg-node/proofmode.js');
proofmode = wasmModule;
if (!proofmode.checkFiles || !proofmode.generate_proof_wasm) {
throw new Error('Required WASM functions not found');
}
return true;
} catch (error) {
console.error(chalk.red('Failed to load ProofMode WASM module'));
console.error(chalk.red('Error:', error.message));
console.error(chalk.red('Please ensure @guardianproject/proofmode is properly built'));
process.exit(1);
}
}
async function readFileAsUint8Array(filePath) {
const buffer = await fs.readFile(filePath);
return new Uint8Array(buffer);
}
async function collectFiles(files = [], dirs = []) {
const allFiles = [...files];
for (const dir of dirs) {
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
if (entry.isFile()) {
allFiles.push(path.join(dir, entry.name));
}
}
}
return allFiles;
}
function createProgressCallback(spinner) {
return (messageType, message) => {
switch (messageType) {
case 'Progress':
spinner.text = message;
break;
case 'Info':
spinner.info(chalk.blue(message));
spinner.start();
break;
case 'Success':
spinner.succeed(chalk.green(message));
spinner.start();
break;
case 'Warning':
spinner.warn(chalk.yellow(message));
spinner.start();
break;
case 'Error':
spinner.fail(chalk.red(message));
spinner.start();
break;
default:
console.log(`${messageType}: ${message}`);
}
};
}
async function checkCommand(argv) {
const spinner = ora('Starting verification...').start();
const callback = createProgressCallback(spinner);
try {
let result;
if (argv.url && argv.url.length > 0) {
spinner.text = 'Checking URLs...';
result = await proofmode.checkURLs(argv.url, callback);
} else if (argv.cid && argv.cid.length > 0) {
spinner.text = 'Checking CIDs...';
result = await proofmode.checkCIDs(argv.cid, callback);
} else {
const files = await collectFiles(argv.file, argv.dir);
if (files.length === 0) {
spinner.fail(chalk.red('No files specified'));
process.exit(1);
}
spinner.text = 'Checking files...';
result = await proofmode.checkFiles(files, callback);
}
spinner.succeed(chalk.green('Verification complete'));
if (argv.outputFile) {
await fs.writeJSON(argv.outputFile, result, { spaces: 2 });
console.log(chalk.green(`Results written to ${argv.outputFile}`));
} else {
console.log(JSON.stringify(result, null, 2));
}
} catch (error) {
spinner.fail(chalk.red(`Verification failed: ${error.message}`));
process.exit(1);
}
}
async function generateCommand(argv) {
const spinner = ora('Generating proofs...').start();
try {
const files = await collectFiles(argv.file, argv.dir);
if (files.length === 0) {
spinner.fail(chalk.red('No files specified'));
process.exit(1);
}
const storageDir = argv.storage || './proofmode';
await fs.ensureDir(storageDir);
const pgp = await setupPgpKeys(storageDir, argv.email, argv.passphrase);
let hasError = false;
for (const filePath of files) {
try {
spinner.text = `Processing ${path.basename(filePath)}...`;
const mediaData = await readFileAsUint8Array(filePath);
const metadata = {
description: argv.description || null,
location: argv.location || null,
event_type: argv.eventType || null,
tags: argv.tags || null
};
const callbacks = {
getLocationData: async () => {
return null;
},
getDeviceData: async () => {
return {
manufacturer: process.platform,
model: 'CLI',
os_version: process.version,
device_id: null
};
},
getNetworkData: async () => {
return {
network_type: 'unknown',
wifi_ssid: null,
cell_info: null
};
},
saveProofData: async (hash, proofData) => {
const proofDir = path.join(storageDir, hash.substring(0, 2), hash.substring(2, 4));
await fs.ensureDir(proofDir);
const jsonPath = path.join(proofDir, `${hash}.proof.json`);
await fs.writeJSON(jsonPath, JSON.parse(proofData), { spaces: 2 });
return jsonPath;
},
getPgpKey: async () => {
return await pgp.loadKey();
},
signWithPgp: async (data) => {
return await pgp.signData(data, argv.passphrase);
}
};
const result = proofmode.generate_proof_wasm(
mediaData,
JSON.stringify(metadata),
callbacks
);
const hash = proofmode.get_file_hash(mediaData);
spinner.succeed(chalk.green(`Generated proof for ${path.basename(filePath)}: ${hash}`));
} catch (error) {
spinner.fail(chalk.red(`Failed to generate proof for ${filePath}: ${error.message}`));
hasError = true;
}
}
if (hasError) {
process.exit(1);
} else {
spinner.succeed(chalk.green('All proofs generated successfully'));
}
} catch (error) {
spinner.fail(chalk.red(`Generation failed: ${error.message}`));
process.exit(1);
}
}
async function signCommand(argv) {
console.error(chalk.yellow('Sign functionality not yet implemented'));
console.error(chalk.gray(`CSR: ${argv.csr}, Output: ${argv.output}`));
process.exit(1);
}
const cli = yargs(hideBin(process.argv))
.scriptName('proofmode')
.usage('$0 <command> [options]')
.command(
'check',
'Verify media files and their associated proofs',
(yargs) => {
return yargs
.option('file', {
alias: 'f',
type: 'array',
description: 'File paths to check'
})
.option('dir', {
alias: 'd',
type: 'array',
description: 'Directory paths to check'
})
.option('url', {
alias: 'u',
type: 'array',
description: 'URLs to check'
})
.option('cid', {
alias: 'c',
type: 'array',
description: 'IPFS CIDs to check'
})
.option('output-file', {
alias: 'o',
type: 'string',
description: 'Output file for results'
})
.check((argv) => {
if (!argv.file && !argv.dir && !argv.url && !argv.cid) {
throw new Error('At least one of --file, --dir, --url, or --cid must be specified');
}
return true;
});
},
checkCommand
)
.command(
'generate',
'Generate cryptographic proof bundles for media files',
(yargs) => {
return yargs
.option('file', {
alias: 'f',
type: 'array',
description: 'File paths to generate proofs for'
})
.option('dir', {
alias: 'd',
type: 'array',
description: 'Directory paths to generate proofs for'
})
.option('storage', {
alias: 's',
type: 'string',
description: 'Storage directory for proof data',
default: './proofmode'
})
.option('email', {
alias: 'e',
type: 'string',
description: 'Email for PGP key generation',
default: 'user@example.com'
})
.option('passphrase', {
alias: 'p',
type: 'string',
description: 'Passphrase for PGP key',
default: 'default_passphrase'
})
.option('description', {
type: 'string',
description: 'Description metadata for the proof'
})
.option('location', {
type: 'string',
description: 'Location metadata for the proof'
})
.option('event-type', {
type: 'string',
description: 'Event type metadata for the proof'
})
.option('tags', {
type: 'string',
description: 'Tags metadata for the proof'
})
.check((argv) => {
if (!argv.file && !argv.dir) {
throw new Error('At least one of --file or --dir must be specified');
}
return true;
});
},
generateCommand
)
.command(
'sign',
'Certificate signing functionality (not yet implemented)',
(yargs) => {
return yargs
.option('csr', {
alias: 'c',
type: 'string',
description: 'Certificate signing request path'
})
.option('output', {
alias: 'o',
type: 'string',
description: 'Output certificate path'
});
},
signCommand
)
.demandCommand(1, 'You must specify a command')
.recommendCommands()
.strict()
.help()
.alias('help', 'h')
.version()
.alias('version', 'v')
.wrap(process.stdout.columns || 80)
.epilogue('For more information, visit https://proofmode.org');
async function main() {
const initialized = await initializeProofMode();
if (!initialized) {
process.exit(1);
}
cli.parse();
}
main().catch(error => {
console.error(chalk.red('Fatal error:', error.message));
process.exit(1);
});