proofmode 0.9.0

Capture, share, and preserve verifiable photos and videos
Documentation
#!/usr/bin/env node

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);

// Import ProofMode WASM module
let proofmode;
async function initializeProofMode() {
  try {
    // Import the WASM module (CommonJS for Node.js target)
    const wasmModule = require('../../pkg-node/proofmode.js');
    
    // Set the module reference
    proofmode = wasmModule;
    
    // Verify required functions are available
    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);
  }
}

// Helper function to read file as Uint8Array
async function readFileAsUint8Array(filePath) {
  const buffer = await fs.readFile(filePath);
  return new Uint8Array(buffer);
}

// Helper function to collect files from directories
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;
}

// Progress callback handler
function createProgressCallback(spinner) {
  return (messageType, message) => {
    // Update spinner text based on message type
    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}`);
    }
  };
}

// Check command
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'));
    
    // Output results
    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);
  }
}

// Generate command
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);
    
    // Setup PGP keys
    const pgp = await setupPgpKeys(storageDir, argv.email, argv.passphrase);
    
    let hasError = false;
    
    for (const filePath of files) {
      try {
        spinner.text = `Processing ${path.basename(filePath)}...`;
        
        // Read file data
        const mediaData = await readFileAsUint8Array(filePath);
        
        // Create metadata
        const metadata = {
          description: argv.description || null,
          location: argv.location || null,
          event_type: argv.eventType || null,
          tags: argv.tags || null
        };
        
        // Create callbacks object
        const callbacks = {
          getLocationData: async () => {
            // In CLI mode, we can't get real location data
            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);
          }
        };
        
        // Generate proof
        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);
  }
}

// Sign command
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);
}

// Main CLI setup
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');

// Initialize ProofMode before parsing arguments
async function main() {
  const initialized = await initializeProofMode();
  if (!initialized) {
    process.exit(1);
  }
  
  // Parse and execute
  cli.parse();
}

main().catch(error => {
  console.error(chalk.red('Fatal error:', error.message));
  process.exit(1);
});