dedpaste
Version:
CLI pastebin application using Cloudflare Workers and R2
1,287 lines (1,095 loc) • 79.5 kB
JavaScript
#!/usr/bin/env node
import { program } from 'commander';
import fetch from 'node-fetch';
import { lookup } from 'mime-types';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { createRequire } from 'module';
import { promises as fsPromises } from 'fs';
import { homedir, tmpdir } from 'os';
import inquirer from 'inquirer';
// Import clipboardy with error handling
let clipboard;
try {
clipboard = await import('clipboardy');
// Clipboard module loaded successfully
} catch (error) {
console.error(`Failed to load clipboardy: ${error.message}`);
// Fallback implementation if clipboard fails to load
clipboard = {
writeSync: (text) => {
console.error('Clipboard access is not available. URL could not be copied.');
console.log(`Manual copy: ${text}`);
}
};
}
// Import our core modules
import {
generateKeyPair,
addFriendKey,
listKeys,
getKey,
removeKey,
updateLastUsed,
addPgpKey,
addKeybaseKey
} from './keyManager.js';
import {
encryptContent,
decryptContent
} from './encryptionUtils.js';
import {
interactiveKeyManagement,
interactiveListKeys,
interactiveAddFriend,
interactiveRemoveKey,
interactiveExportKey,
interactiveSend
} from './interactiveMode.js';
// Import PGP and Keybase utilities
import {
fetchPgpKey,
importPgpKey,
addPgpKeyFromServer
} from './pgpUtils.js';
import {
fetchKeybaseUser,
fetchKeybasePgpKey,
verifyKeybaseProofs,
addKeybaseKey as fetchAndAddKeybaseKey
} from './keybaseUtils.js';
const require = createRequire(import.meta.url);
const packageJson = require('../package.json');
// Default API URL - can be changed via environment variable
const API_URL = process.env.DEDPASTE_API_URL || 'https://paste.d3d.dev';
program
.name('dedpaste')
.description('CLI client for DedPaste, a simple pastebin service')
.version(packageJson.version)
.addHelpText('before', `
DedPaste - Secure pastebin with end-to-end encryption
USAGE:
$ dedpaste Create a paste from stdin
$ dedpaste < file.txt Create a paste from a file
$ dedpaste --file path/to/file Create a paste from a specific file
$ dedpaste --temp Create a one-time paste (deleted after viewing)
$ dedpaste --encrypt Create an encrypted paste
$ dedpaste keys Manage encryption keys
$ dedpaste keys:enhanced Manage keys with enhanced interactive UI
$ dedpaste send Create and send an encrypted paste
$ dedpaste get <url-or-id> Retrieve and display a paste
$ dedpaste completion Generate shell completion scripts
EXAMPLES:
$ echo "Hello, world!" | dedpaste
$ dedpaste --file secret.txt --temp --encrypt
$ dedpaste keys --gen-key
$ dedpaste keys:enhanced # Use the enhanced UI for key management
$ dedpaste send --encrypt --for alice --temp
$ dedpaste get https://paste.d3d.dev/AbCdEfGh
$ dedpaste completion --bash > ~/.dedpaste-completion.bash
`);
// Add a command to manage keys
// Add a special direct command for enhanced mode
program
.command('keys:enhanced')
.description('Manage encryption keys in enhanced interactive TUI mode')
.action(async () => {
try {
const { spawn } = await import('child_process');
const { fileURLToPath } = await import('url');
const path = await import('path');
// Get the path to the current module directory
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const enhancedLauncherPath = path.join(__dirname, 'enhancedModeLauncher.js');
console.log('Starting enhanced mode...');
// Spawn the launcher as a separate process
const enhancedProcess = spawn('node', [enhancedLauncherPath], {
stdio: 'inherit',
env: process.env
});
// Handle process events
process.on('SIGINT', () => {
console.log('\nTerminating enhanced mode...');
enhancedProcess.kill('SIGTERM');
// Allow clean exit
setTimeout(() => process.exit(0), 300);
});
// Wait for the process to complete with a timeout
await new Promise((resolve, reject) => {
// Set a timeout to prevent hanging
const timeout = setTimeout(() => {
console.log('\nEnhanced mode is taking too long. Terminating...');
enhancedProcess.kill('SIGTERM');
reject(new Error('Enhanced mode timed out'));
}, 60000); // 60 second timeout
enhancedProcess.on('exit', (code) => {
clearTimeout(timeout);
if (code === 0) {
resolve();
} else {
reject(new Error(`Enhanced mode exited with code ${code}`));
}
});
enhancedProcess.on('error', (err) => {
clearTimeout(timeout);
reject(err);
});
});
} catch (error) {
console.error(`Error running enhanced mode: ${error.message}`);
process.exit(1);
}
});
program
.command('keys')
.description('Manage encryption keys for secure communication')
.option('--interactive', 'Use interactive menu-driven mode for key management')
.option('--enhanced', 'Use enhanced interactive TUI mode with color and advanced features')
.option('--list', 'List all your keys and friends\' keys with fingerprints')
.option('--add-friend <name>', 'Add a friend\'s public key (requires --key-file)')
.option('--key-file <path>', 'Path to key file for import/export operations')
.option('--export', 'Export your public key to share with friends')
.option('--remove <name>', 'Remove a friend\'s key from your keyring')
.option('--gen-key', 'Generate a new RSA key pair for encryption')
.option('--my-key', 'Output your public key to the console for sharing')
.option('--diagnostics', 'Run diagnostics on your key configuration and show a report')
.option('--search <query>', 'Search for keys by name, email, or fingerprint')
.option('--details <id>', 'Show detailed information about a specific key')
.option('--backup <dir>', 'Backup all keys to the specified directory')
// PGP options
.option('--pgp-key <email-or-id>', 'Fetch and add a PGP key from a keyserver')
.option('--pgp-name <name>', 'Custom name for the PGP key (optional)')
.option('--import-pgp-key <path>', 'Import a PGP private key for encryption/decryption')
.option('--pgp-passphrase <phrase>', 'Passphrase for PGP private key')
.option('--native-pgp', 'Use native PGP encryption instead of converting to PEM')
.option('--from-gpg <key-id>', 'Import a key from your GPG keyring')
.option('--gpg-import <path>', 'Import a key to your GPG keyring')
// Keybase options
.option('--keybase <username>', 'Fetch and add a Keybase user\'s PGP key')
.option('--keybase-name <name>', 'Custom name for the Keybase user\'s key (optional)')
.option('--no-verify', 'Skip verification of Keybase proofs')
// Debugging and logging options
.option('--verbose', 'Enable verbose logging (same as --log-level debug)')
.option('--debug', 'Enable debug mode with extensive logging (same as --log-level trace)')
.option('--log-level <level>', 'Set logging level (error, warn, info, debug, trace)')
.option('--log-file <path>', 'Log to the specified file')
.option('--no-log-file', 'Disable logging to file')
.addHelpText('after', `
Examples:
$ dedpaste keys --gen-key # Generate a new key pair
$ dedpaste keys --list # List all your keys
$ dedpaste keys --add-friend alice --key-file alice_public.pem # Add a friend's key
$ dedpaste keys --my-key # Display your public key
$ dedpaste keys --interactive # Use interactive mode
$ dedpaste keys:enhanced # Use enhanced UI (recommended)
PGP Integration:
$ dedpaste keys --pgp-key user@example.com # Add a PGP key from keyservers
$ dedpaste keys --pgp-key 0x1234ABCD # Add a PGP key using key ID
$ dedpaste keys --pgp-key user@example.com --pgp-name alice # Add with custom name
Keybase Integration:
$ dedpaste keys --keybase username # Add a Keybase user's key
$ dedpaste keys --keybase username --keybase-name bob # Add with custom name
$ dedpaste keys --keybase username --no-verify # Skip verification of proofs
Key Storage:
- Your keys are stored in ~/.dedpaste/keys/
- Friend keys are stored in ~/.dedpaste/friends/
- PGP keys are stored in ~/.dedpaste/pgp/
- Keybase keys are stored in ~/.dedpaste/keybase/
- Key database is at ~/.dedpaste/keydb.json
`)
.action(async (options) => {
try {
// Initialize logger based on options
let logLevel = 'info';
if (options.verbose) logLevel = 'debug';
if (options.debug) logLevel = 'trace';
if (options.logLevel) logLevel = options.logLevel;
const logOptions = {
level: logLevel,
logToFile: options.logFile !== false,
logFile: options.logFile || 'dedpaste.log'
};
// Import and initialize logger
const { initialize: initLogger } = await import('./logger.js');
const logger = await initLogger(logOptions);
// Log the start of execution
logger.info('Starting key management operation', { options });
// Enhanced interactive mode takes precedence but we need to ensure it doesn't hang
if (options.enhanced) {
logger.debug('Enhanced mode requested - using non-blocking startup', { options });
console.log('Starting enhanced mode...');
console.log('Debug info: enhanced flag =', options.enhanced);
// Create a completely isolated process for enhanced mode
// This avoids all potential circular dependencies and module loading issues
try {
// Use child_process to launch the enhanced mode in a separate process
// This guarantees that any hanging in module initialization won't affect the main CLI
const childProcess = await import('child_process');
const { spawn } = childProcess;
// Get the current module directory where our launcher is located
const cliDir = path.dirname(fileURLToPath(import.meta.url));
const launcherPath = path.join(cliDir, 'enhancedModeLauncher.js');
// Verify that the launcher exists
if (!fs.existsSync(launcherPath)) {
logger.error('Enhanced mode launcher not found', { path: launcherPath });
throw new Error(`Enhanced mode launcher not found at: ${launcherPath}`);
}
// Spawn the launcher as a separate process with the right working directory
const enhancedProcess = spawn('node', [launcherPath, '--debug'], {
stdio: 'inherit',
env: process.env,
cwd: cliDir // Use CLI directory as working directory
});
// Process termination handling
process.on('SIGINT', () => {
console.log('\nTerminating enhanced mode...');
enhancedProcess.kill('SIGTERM');
// Allow clean exit
setTimeout(() => process.exit(0), 300);
});
// Wait for the process to complete with a timeout
await new Promise((resolve, reject) => {
// Set a timeout to prevent hanging
const timeout = setTimeout(() => {
console.log('\nEnhanced mode is taking too long. Terminating...');
enhancedProcess.kill('SIGTERM');
reject(new Error('Enhanced mode timed out'));
}, 30000); // 30 second timeout
enhancedProcess.on('exit', (code) => {
clearTimeout(timeout);
if (code === 0) {
resolve();
} else {
reject(new Error(`Enhanced mode exited with code ${code}`));
}
});
enhancedProcess.on('error', (err) => {
clearTimeout(timeout);
reject(err);
});
});
// No temp file to clean up since we're using a dedicated launcher file
} catch (enhancedError) {
logger.error('Enhanced mode failed', { error: enhancedError.message, stack: enhancedError.stack });
console.error(`Error running enhanced mode: ${enhancedError.message}`);
console.error('Stack trace:', enhancedError.stack);
console.log('Falling back to standard interactive mode...');
// Fall back to regular interactive mode
const result = await interactiveKeyManagement();
if (result.message) {
console.log(result.message);
}
}
return;
}
// Regular interactive mode
if (options.interactive) {
logger.debug('Entering interactive mode');
const result = await interactiveKeyManagement();
if (result.message) {
console.log(result.message);
}
return;
}
// Run diagnostics
if (options.diagnostics) {
logger.debug('Running key diagnostics');
const { runKeyDiagnostics, formatDiagnosticsReport } = await import('./keyDiagnostics.js');
console.log('Running key system diagnostics... Please wait...');
const results = await runKeyDiagnostics();
const report = formatDiagnosticsReport(results);
console.log(report);
if (results.status !== 'ok') {
logger.warn('Diagnostics found issues', {
errors: results.errors.length,
warnings: results.warnings.length
});
// Suggest fixing issues
console.log('\nTo fix issues automatically, use:');
console.log('dedpaste keys --enhanced');
console.log('Then select "Run diagnostics" and follow the prompts.');
}
return;
}
// Search for keys
if (options.search) {
logger.debug('Searching for keys', { query: options.search });
const { searchKeys } = await import('./unifiedKeyManager.js');
const keys = await searchKeys(options.search, { includeGpg: true });
if (keys.length === 0) {
console.log(`No keys found matching "${options.search}"`);
return;
}
console.log(`\nFound ${keys.length} keys matching "${options.search}":\n`);
for (const key of keys) {
console.log(`- ${key.name} (${key.type.toUpperCase()})`);
if (key.email) {
console.log(` Email: ${key.email}`);
}
if (key.username) {
console.log(` Username: ${key.username}`);
}
console.log(` Fingerprint: ${key.fingerprint}`);
console.log(` Source: ${key.source}`);
if (key.created) {
console.log(` Created: ${new Date(key.created).toLocaleString()}`);
}
console.log('');
}
return;
}
// Show detailed key information
if (options.details) {
logger.debug('Showing key details', { keyId: options.details });
const { getKeyById, readKeyContent } = await import('./unifiedKeyManager.js');
const keyInfo = await getKeyById(options.details, { includeGpg: true });
if (!keyInfo) {
console.error(`Key "${options.details}" not found`);
process.exit(1);
}
console.log(`\nKey details for "${keyInfo.name}":\n`);
console.log(`- ID: ${keyInfo.id}`);
console.log(`- Name: ${keyInfo.name}`);
console.log(`- Type: ${keyInfo.type.toUpperCase()}`);
console.log(`- Source: ${keyInfo.source}`);
console.log(`- Fingerprint: ${keyInfo.fingerprint}`);
if (keyInfo.email) {
console.log(`- Email: ${keyInfo.email}`);
}
if (keyInfo.username) {
console.log(`- Username: ${keyInfo.username}`);
}
if (keyInfo.created) {
console.log(`- Created: ${new Date(keyInfo.created).toLocaleString()}`);
}
if (keyInfo.lastUsed) {
console.log(`- Last Used: ${new Date(keyInfo.lastUsed).toLocaleString()}`);
}
if (keyInfo.path) {
if (typeof keyInfo.path === 'object') {
console.log(`- Public Key Path: ${keyInfo.path.public}`);
console.log(`- Private Key Path: ${keyInfo.path.private}`);
} else {
console.log(`- Key Path: ${keyInfo.path}`);
}
}
// Ask if user wants to see key content
if (keyInfo.source === 'self' || keyInfo.source === 'friend' ||
keyInfo.source === 'pgp' || keyInfo.source === 'keybase') {
const { default: inquirer } = await import('inquirer');
const { showContent } = await inquirer.prompt([{
type: 'confirm',
name: 'showContent',
message: 'Show key content?',
default: false
}]);
if (showContent) {
// For self keys, ask if they want to see private key
let showPrivate = false;
if (keyInfo.source === 'self') {
const { viewPrivate } = await inquirer.prompt([{
type: 'confirm',
name: 'viewPrivate',
message: 'Show private key? (This is sensitive information)',
default: false
}]);
showPrivate = viewPrivate;
}
// Read the key content
const keyContent = await readKeyContent(keyInfo, { private: showPrivate });
if (keyContent) {
console.log('\n--- Key Content ---\n');
console.log(keyContent);
console.log('\n-------------------\n');
} else {
console.error('Could not read key content');
}
}
}
return;
}
// Backup keys
if (options.backup) {
logger.debug('Backing up keys', { directory: options.backup });
const backupDir = options.backup;
// Create backup directory if it doesn't exist
await fsPromises.mkdir(backupDir, { recursive: true });
// Get timestamp for backup files
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
// Get all keys
const db = await loadKeyDatabase();
let backupCount = 0;
// Backup self key if it exists
if (db.keys.self) {
const privateKeyDest = path.join(backupDir, `self-private-${timestamp}.pem`);
const publicKeyDest = path.join(backupDir, `self-public-${timestamp}.pem`);
await fsPromises.copyFile(db.keys.self.private, privateKeyDest);
await fsPromises.copyFile(db.keys.self.public, publicKeyDest);
backupCount += 2;
console.log(`Backed up self key to ${privateKeyDest} and ${publicKeyDest}`);
}
// Backup friend keys
for (const [name, info] of Object.entries(db.keys.friends)) {
const friendKeyDest = path.join(backupDir, `friend-${name}-${timestamp}.pem`);
await fsPromises.copyFile(info.public, friendKeyDest);
backupCount++;
console.log(`Backed up friend key "${name}" to ${friendKeyDest}`);
}
// Backup PGP keys
for (const [name, info] of Object.entries(db.keys.pgp || {})) {
const pgpKeyDest = path.join(backupDir, `pgp-${name}-${timestamp}.asc`);
await fsPromises.copyFile(info.path, pgpKeyDest);
backupCount++;
console.log(`Backed up PGP key "${name}" to ${pgpKeyDest}`);
}
// Backup Keybase keys
for (const [name, info] of Object.entries(db.keys.keybase || {})) {
const keybaseKeyDest = path.join(backupDir, `keybase-${name}-${timestamp}.asc`);
await fsPromises.copyFile(info.path, keybaseKeyDest);
backupCount++;
console.log(`Backed up Keybase key "${name}" to ${keybaseKeyDest}`);
}
// Backup key database
const dbDest = path.join(backupDir, `keydb-${timestamp}.json`);
await fsPromises.copyFile(path.join(homedir(), '.dedpaste', 'keydb.json'), dbDest);
console.log(`Backed up key database to ${dbDest}`);
console.log(`\nBackup complete: ${backupCount} keys backed up to ${backupDir}`);
return;
}
// Import a key from GPG keyring
if (options.fromGpg) {
logger.debug('Importing key from GPG keyring', { keyId: options.fromGpg });
const { importKey } = await import('./unifiedKeyManager.js');
const name = options.pgpName || options.fromGpg;
console.log(`Importing key ${options.fromGpg} from GPG keyring...`);
const result = await importKey({
source: 'gpg-keyring',
keyId: options.fromGpg,
name: name
});
if (result.success) {
console.log(`\n✓ Imported key from GPG keyring: ${result.name}`);
console.log(` - Type: ${result.type}`);
console.log(` - Fingerprint: ${result.fingerprint}`);
if (result.email) console.log(` - Email: ${result.email}`);
console.log(` - Stored at: ${result.path}`);
} else {
console.error(`Error importing key from GPG keyring: ${result.error}`);
process.exit(1);
}
return;
}
// Import a key to GPG keyring
if (options.gpgImport) {
logger.debug('Importing key to GPG keyring', { file: options.gpgImport });
const { importKey } = await import('./unifiedKeyManager.js');
console.log(`Importing key from ${options.gpgImport} to GPG keyring...`);
const result = await importKey({
source: 'gpg-import',
file: options.gpgImport
});
if (result.success) {
console.log(`\n✓ Key imported to GPG keyring successfully`);
console.log(` - Key ID: ${result.keyId}`);
console.log('\nOutput from GPG:');
console.log(result.output);
} else {
console.error(`Error importing key to GPG keyring: ${result.error}`);
process.exit(1);
}
return;
}
// List keys
if (options.list) {
logger.debug('Listing all keys');
await interactiveListKeys();
return;
}
// Add a friend's key
if (options.addFriend) {
if (!options.keyFile) {
console.error('Error: --key-file is required when adding a friend');
process.exit(1);
}
logger.debug('Adding friend key', { name: options.addFriend, file: options.keyFile });
try {
const keyContent = await fsPromises.readFile(options.keyFile, 'utf8');
const keyPath = await addFriendKey(options.addFriend, keyContent);
console.log(`Added ${options.addFriend}'s public key at ${keyPath}`);
} catch (error) {
logger.error('Failed to add friend key', { error: error.message });
console.error(`Error adding friend's key: ${error.message}`);
process.exit(1);
}
return;
}
// Export public key
if (options.export) {
logger.debug('Exporting public key');
const result = await interactiveExportKey();
if (result.message) {
console.log(result.message);
}
return;
}
// Remove a key
if (options.remove) {
logger.debug('Removing key', { name: options.remove });
try {
// Try to remove from any collection
const success = await removeKey('any', options.remove);
if (success) {
console.log(`Removed ${options.remove}'s key successfully`);
} else {
console.error(`Key "${options.remove}" not found`);
process.exit(1);
}
} catch (error) {
logger.error('Failed to remove key', { error: error.message });
console.error(`Error removing key: ${error.message}`);
process.exit(1);
}
return;
}
// Fetch and add a PGP key from keyservers
if (options.pgpKey) {
logger.debug('Fetching PGP key', { identifier: options.pgpKey });
try {
console.log(`Fetching PGP key for "${options.pgpKey}" from keyservers...`);
const name = options.pgpName || options.pgpKey;
const result = await addPgpKeyFromServer(options.pgpKey, name);
console.log(`
✓ Added PGP key:
- Name: ${result.name}
- Email: ${result.email || 'Not specified'}
- Key ID: ${result.keyId}
- Stored at: ${result.path}
`);
} catch (error) {
logger.error('Failed to fetch PGP key', { error: error.message });
console.error(`Error fetching PGP key: ${error.message}`);
process.exit(1);
}
return;
}
// Import a PGP private key
if (options.importPgpKey) {
// Check if passphrase was provided
if (!options.pgpPassphrase) {
console.error('Error: --pgp-passphrase is required when importing a PGP private key');
process.exit(1);
}
logger.debug('Importing PGP private key', { file: options.importPgpKey });
try {
// Read the PGP private key file
const pgpPrivateKeyContent = await fsPromises.readFile(options.importPgpKey, 'utf8');
// Import the PGP private key
console.log(`Importing PGP private key from ${options.importPgpKey}...`);
const result = await importPgpPrivateKey(pgpPrivateKeyContent, options.pgpPassphrase);
// Save the private and public key to the PGP key directory
const { PGP_KEY_DIR } = await ensureDirectories();
const privateKeyPath = path.join(PGP_KEY_DIR, `private.pem`);
const publicKeyPath = path.join(PGP_KEY_DIR, `public.pem`);
// Write the keys to files
await fsPromises.writeFile(privateKeyPath, result.privateKey);
await fsPromises.writeFile(publicKeyPath, result.publicKey);
// Update the key database
const db = await loadKeyDatabase();
db.keys.pgp.self = {
private: privateKeyPath,
public: publicKeyPath,
original: options.importPgpKey,
fingerprint: result.keyId,
name: result.name,
email: result.email,
created: new Date().toISOString()
};
await saveKeyDatabase(db);
console.log(`
✓ Imported PGP private key:
- Name: ${result.name}
- Email: ${result.email || 'Not specified'}
- Key ID: ${result.keyId}
- Converted private key: ${privateKeyPath}
- Converted public key: ${publicKeyPath}
- Original PGP key: ${options.importPgpKey}
`);
} catch (error) {
logger.error('Failed to import PGP private key', { error: error.message });
console.error(`Error importing PGP private key: ${error.message}`);
process.exit(1);
}
return;
}
// Fetch and add a Keybase user's key
if (options.keybase) {
logger.debug('Fetching Keybase key', { username: options.keybase });
try {
console.log(`Fetching Keybase key for user "${options.keybase}"...`);
if (options.verify) {
console.log('Verifying user proofs on Keybase...');
}
const name = options.keybaseName || `keybase:${options.keybase}`;
const result = await fetchAndAddKeybaseKey(options.keybase, name, options.verify);
console.log(`
✓ Added Keybase key:
- Name: ${result.name}
- Keybase username: ${result.username}
- Email: ${result.email || 'Not specified'}
- Key ID: ${result.keyId}
- Stored at: ${result.path}
`);
} catch (error) {
logger.error('Failed to fetch Keybase key', { error: error.message });
console.error(`Error fetching Keybase key: ${error.message}`);
process.exit(1);
}
return;
}
// Generate a new key pair
if (options.genKey) {
logger.debug('Generating new key pair');
const { privateKeyPath, publicKeyPath } = await generateKeyPair();
console.log(`
✓ Generated new key pair:
- Private key: ${privateKeyPath}
- Public key: ${publicKeyPath}
`);
return;
}
// Output public key to console
if (options.myKey) {
logger.debug('Showing self public key');
const selfKey = await getKey('self');
if (!selfKey) {
console.error('No personal key found. Generate one with --gen-key first.');
process.exit(1);
}
try {
const publicKeyContent = await fsPromises.readFile(selfKey.public, 'utf8');
console.log('\nYour public key:');
console.log('----------------');
console.log(publicKeyContent);
console.log('----------------');
console.log('\nShare this key with your friends so they can send you encrypted pastes.');
} catch (error) {
logger.error('Failed to read public key', { error: error.message });
console.error(`Error reading public key: ${error.message}`);
process.exit(1);
}
return;
}
// If no specific action is provided, show help
program.commands.find(cmd => cmd.name() === 'keys').help();
} catch (error) {
console.error('Error:', error.message);
process.exit(1);
}
});
// Add a command to send a paste
program
.command('send')
.description('Create and send an encrypted paste to friends')
.option('-t, --temp', 'Create a one-time paste that is deleted after being viewed')
.option('--type <content-type>', 'Specify the content type of the paste (e.g., application/json)')
.option('-f, --file <path>', 'Upload a file from the specified path instead of stdin')
.option('-o, --output', 'Print only the URL (without any additional text, useful for scripts)')
.option('-e, --encrypt', 'Encrypt the content before uploading (requires key setup)')
.option('--for <friend>', 'Encrypt for a specific friend (requires adding their key first)')
.option('--list-friends', 'List available friends you can encrypt messages for')
.option('--key-file <path>', 'Path to public key for encryption (alternative to stored keys)')
.option('--gen-key', 'Generate a new key pair for encryption if you don\'t have one')
.option('--interactive', 'Use interactive mode with guided prompts for message creation')
.option('--enhanced', 'Use enhanced interactive mode with advanced key selection features')
.option('--debug', 'Debug mode: show encrypted content without uploading')
.option('-c, --copy', 'Copy the URL to clipboard automatically')
// PGP options
.option('--pgp', 'Use PGP encryption instead of hybrid RSA/AES')
.option('--pgp-key-file <path>', 'Use a specific PGP public key file for encryption')
.option('--pgp-armor', 'Output ASCII-armored PGP instead of binary format')
.addHelpText('after', `
Examples:
$ echo "Secret message" | dedpaste send --encrypt # Encrypt for yourself (RSA/AES)
$ echo "For Alice only" | dedpaste send --encrypt --for alice # Encrypt for a friend
$ dedpaste send --file secret.txt --encrypt --temp # Encrypt a file as one-time paste
$ dedpaste send --interactive --encrypt # Interactive encrypted message
$ dedpaste send --enhanced --encrypt # Enhanced interactive mode with full key support
$ dedpaste send --list-friends # List available recipients
PGP Options:
$ dedpaste send --encrypt --for alice@example.com --pgp # Encrypt for PGP key (REQUIRED: specify recipient)
$ dedpaste send --encrypt --for alice --pgp --pgp-key-file friend.asc # Use specific PGP key file
$ dedpaste send --enhanced --encrypt --pgp # Use enhanced mode with GPG keyring support
IMPORTANT: PGP encryption always requires specifying a recipient with --for
Encryption:
- Standard encryption uses RSA for key exchange and AES-256-GCM for content
- PGP encryption (--pgp) uses OpenPGP standard compatible with GnuPG/GPG
- Each paste uses a different key for forward secrecy
- Encrypted pastes include metadata about sender and recipient
- Use --debug to test encryption without uploading
`)
.action(async (options) => {
// Ensure temp flag is properly set by checking command line arguments
if (!options.temp && (process.argv.includes('--temp') || process.argv.includes('-t'))) {
options.temp = true;
}
try {
// List friends if requested
if (options.listFriends) {
const db = await listKeys();
const friendNames = Object.keys(db.keys.friends);
if (friendNames.length === 0) {
console.log('No friend keys found. Add one with "dedpaste keys add-friend"');
return;
}
console.log('\nAvailable friends:');
for (const name of friendNames) {
const friend = db.keys.friends[name];
const lastUsed = new Date(friend.last_used).toLocaleString();
console.log(` - ${name} (last used: ${lastUsed})`);
}
return;
}
let content;
let contentType;
let recipientName = options.for;
// Interactive mode
if (options.interactive) {
// Check if we should use the enhanced interactive mode
if (options.enhanced) {
console.log('Using enhanced interactive mode for sending...');
const { enhancedInteractiveSend } = await import('./enhancedInteractiveMode.js');
const result = await enhancedInteractiveSend();
if (!result.success) {
console.error(`Error: ${result.message}`);
process.exit(1);
}
content = result.content;
recipientName = result.recipient;
options.temp = result.temp;
options.pgp = result.pgp; // Use PGP flag from enhanced mode
contentType = 'text/plain';
} else {
// Use standard interactive mode
const result = await interactiveSend();
content = result.content;
recipientName = result.recipient;
options.temp = result.temp;
contentType = 'text/plain';
}
} else {
// Fix for file option parsing - ensure it's correctly recognized
if (!options.file && (process.argv.includes('--file') || process.argv.includes('-f'))) {
// If Commander didn't parse it correctly, get the file path manually
const fileArgIndex = Math.max(
process.argv.indexOf('--file'),
process.argv.indexOf('-f')
);
if (fileArgIndex !== -1 && fileArgIndex < process.argv.length - 1) {
options.file = process.argv[fileArgIndex + 1];
}
}
// Additional PGP flag check for send command
if (process.argv.includes('--pgp') && !options.pgp) {
options.pgp = true;
}
// Determine if we're reading from a file or stdin
if (options.file) {
// Read from the specified file
if (!fs.existsSync(options.file)) {
console.error(`Error: File '${options.file}' does not exist`);
process.exit(1);
}
content = fs.readFileSync(options.file);
// Try to detect the content type from the file extension
contentType = options.type || lookup(options.file) || 'text/plain';
} else {
// Read from stdin
const stdinBuffer = [];
for await (const chunk of process.stdin) {
stdinBuffer.push(chunk);
}
if (stdinBuffer.length === 0) {
console.error('Error: No input provided. Pipe content to dedpaste or use --file option.');
process.exit(1);
}
content = Buffer.concat(stdinBuffer);
contentType = options.type || 'text/plain';
}
}
// Parse command line arguments and options
// Check if encryption is requested (manually check for the flag)
const shouldEncrypt = process.argv.includes('--encrypt') || process.argv.includes('-e');
// Ensure file option is recognized correctly - check both parsed options and raw arguments
if (!options.file && (process.argv.includes('--file') || process.argv.includes('-f'))) {
// If Commander didn't parse it correctly, get the file path manually
const fileArgIndex = Math.max(
process.argv.indexOf('--file'),
process.argv.indexOf('-f')
);
if (fileArgIndex !== -1 && fileArgIndex < process.argv.length - 1) {
options.file = process.argv[fileArgIndex + 1];
}
}
// Handle encryption if requested
if (shouldEncrypt) {
// Generate new keys if requested
if (options.genKey) {
console.log('Generating new key pair');
const { publicKeyPath, privateKeyPath } = await generateKeyPair();
console.log(`
✓ Generated new key pair:
- Private key: ${privateKeyPath}
- Public key: ${publicKeyPath}
`);
}
// Encrypt the content
try {
// Check if PGP mode is requested
const usePgp = options.pgp;
// Validate encryption parameters
// For PGP encryption, always require a recipient
if (usePgp && !recipientName) {
console.error(`Error: PGP encryption requires specifying a recipient with --for`);
console.error(`Please use: dedpaste send --encrypt --pgp --for <recipient_name>`);
process.exit(1);
}
// If PGP key file is provided, read it and use it directly
if (options.pgpKeyFile) {
if (!recipientName) {
console.error(`Error: When using --pgp-key-file, you must specify a recipient with --for`);
process.exit(1);
}
const pgpKeyContent = await fsPromises.readFile(options.pgpKeyFile, 'utf8');
content = await createPgpEncryptedMessage(content, pgpKeyContent, recipientName);
} else {
// Use the standard encryption flow with PGP option
content = await encryptContent(content, recipientName, usePgp);
}
// Log PGP mode if used
if (usePgp || options.pgpKeyFile) {
console.log('Using PGP encryption');
}
// Set content type to application/json for encrypted content
contentType = 'application/json';
} catch (error) {
console.error(`Encryption failed: ${error.message}`);
// Provide helpful suggestions for common errors
if (error.message.includes('PGP key detected') && !options.pgp) {
console.log('\nSuggestion: This key appears to be a PGP key. Try adding the --pgp flag:');
console.log(`dedpaste send --encrypt --for ${recipientName} --pgp`);
} else if (error.message.includes('requires specifying a recipient')) {
console.log('\nFor PGP encryption, always specify a recipient:');
console.log(`dedpaste send --encrypt --pgp --for <recipient_name>`);
}
process.exit(1);
}
} else {
console.log('No encryption requested');
}
// Determine the endpoint based on whether it's a temporary paste and encrypted
let endpoint;
if (shouldEncrypt) {
endpoint = options.temp ? '/e/temp' : '/e/upload';
} else {
endpoint = options.temp ? '/temp' : '/upload';
}
// In debug mode, just show the encrypted content
if (options.debug && shouldEncrypt) {
console.log('Debug mode: Showing encrypted content without uploading');
console.log('Encrypted content (JSON):');
console.log(content.toString());
return;
}
// Make the API request
try {
const response = await fetch(`${API_URL}${endpoint}`, {
method: 'POST',
headers: {
'Content-Type': contentType,
'User-Agent': `dedpaste-cli/${packageJson.version}`
},
body: content
});
if (!response.ok) {
console.error(`Error: ${response.status} ${response.statusText}`);
const errorText = await response.text();
console.error(errorText);
process.exit(1);
}
const url = await response.text();
// Copy to clipboard if requested
if (options.copy) {
try {
const cleanUrl = url.trim();
if (clipboard.default) {
clipboard.default.writeSync(cleanUrl);
} else {
clipboard.writeSync(cleanUrl);
}
} catch (error) {
console.error(`Unable to copy to clipboard: ${error.message}`);
}
}
// Output the result
if (options.output) {
console.log(url.trim());
} else {
let encryptionMessage = '';
if (shouldEncrypt) {
if (recipientName) {
encryptionMessage = `🔒 This paste is encrypted for ${recipientName} and can only be decrypted with their private key\n`;
} else {
encryptionMessage = '🔒 This paste is encrypted and can only be decrypted with your private key\n';
}
}
console.log(`
✓ Paste created successfully!
${options.temp ? '⚠️ This is a one-time paste that will be deleted after first view\n' : ''}
${encryptionMessage}
${options.copy ? '📋 URL copied to clipboard: ' : '📋 '} ${url.trim()}
`);
}
} catch (error) {
console.error(`Network error: ${error.message}`);
console.error('If you just want to test encryption without uploading, use the --debug flag');
process.exit(1);
}
} catch (error) {
console.error('Error:', error.message);
process.exit(1);
}
});
// Update the 'get' command to use the new decryption function
program
.command('get')
.description('Retrieve and decrypt a paste by URL or ID')
.argument('<url-or-id>', 'URL or ID of the paste to retrieve (e.g., https://paste.d3d.dev/AbCdEfGh or just AbCdEfGh)')
.option('--key-file <path>', 'Path to private key for decryption (if not using default key)')
.option('--pgp-key-file <path>', 'Path to PGP private key for decryption')
.option('--pgp-passphrase <passphrase>', 'Passphrase for PGP private key')
.option('--use-gpg-keyring', 'Try to decrypt PGP messages using the system GPG keyring', true)
.option('--no-gpg-keyring', 'Disable automatic GPG keyring decryption')
.option('--show-metadata', 'Show detailed metadata about the encrypted message')
.option('--interactive', 'Use interactive mode for decryption (will prompt for passphrase)')
.addHelpText('after', `
Examples:
$ dedpaste get https://paste.d3d.dev/AbCdEfGh # Get a regular paste by URL
$ dedpaste get AbCdEfGh # Get a regular paste by ID
$ dedpaste get https://paste.d3d.dev/e/AbCdEfGh # Get and decrypt an encrypted paste
$ dedpaste get e/AbCdEfGh # Get and decrypt an encrypted paste by ID
$ dedpaste get e/AbCdEfGh --interactive # Use interactive mode with password prompt
$ dedpaste get e/AbCdEfGh --show-metadata # Show detailed metadata about the paste
PGP Decryption:
$ dedpaste get e/AbCdEfGh --pgp-key-file my.pgp # Decrypt with PGP private key
$ dedpaste get e/AbCdEfGh --pgp-key-file my.pgp --pgp-passphrase "secret"
$ dedpaste get e/AbCdEfGh # Automatically try GPG keyring for PGP content
$ dedpaste get e/AbCdEfGh --no-gpg-keyring # Disable automatic GPG keyring usage
$ dedpaste get e/AbCdEfGh --interactive # Interactive mode with passphrase prompt
URL Format:
- Regular pastes: https://paste.d3d.dev/{id}
- Encrypted pastes: https://paste.d3d.dev/e/{id}
- The CLI automatically detects encrypted pastes and attempts decryption
Decryption:
- Encrypted pastes are automatically decrypted if you have the correct private key
- PGP encrypted pastes will first try the system GPG keyring (if available)
- If GPG keyring fails, a PGP private key and passphrase will be required
- Metadata about sender and creation time is displayed when available
- One-time pastes are deleted from the server after viewing
GPG Keyring Integration:
- By default, dedpaste will attempt to use your GPG keyring for PGP-encrypted pastes
- This allows decryption without explicitly providing a private key file
- Your system's gpg command is used to perform the decryption
- To disable this feature, use the --no-gpg-keyring flag
`)
.action(async (urlOrId, options) => {
try {
// Extract ID and check if it's an encrypted paste
let id = urlOrId;
let isEncrypted = false;
// Parse the URL or ID
if (urlOrId.startsWith('http')) {
// It's a URL
const url = new URL(urlOrId);
const path = url.pathname;
// Check if it's an encrypted paste
const encryptedMatch = path.match(/^\/e\/([a-zA-Z0-9]{8})$/);
if (encryptedMatch) {
id = encryptedMatch[1];
isEncrypted = true;
} else {
const regularMatch = path.match(/^\/([a-zA-Z0-9]{8})$/);
if (regularMatch) {
id = regularMatch[1];
} else {
console.error('Invalid paste URL format');
process.exit(1);
}
}
} else if (urlOrId.startsWith('e/') && urlOrId.length === 10) {
// It's an encrypted ID in the format "e/AbCdEfGh"
id = urlOrId.substring(2);
isEncrypted = true;
} else if (id.length === 8) {
// It's just a regular ID
isEncrypted = false;
} else {
console.error('Invalid paste ID format');
console.error('Valid formats: https://paste.d3d.dev/AbCdEfGh, AbCdEfGh, https://paste.d3d.dev/e/AbCdEfGh, e/AbCdEfGh');
process.exit(1);
}
// Determine the URL to fetch
const fetchUrl = isEncrypted
? `${API_URL}/e/${id}`
: `${API_URL}/${id}`;
console.log(`Fetching paste from ${fetchUrl}...`);
// Fetch the paste
const response = await fetch(fetchUrl);
if (!response.ok) {
console.error(`Error: ${response.status} ${response.statusText}`);
const errorText = await response.text();
console.error(errorText);
// Provide more helpful error messages
if (response.status === 404) {
console.error('The paste was not found. It may have been deleted or expired.');
} else if (response.status === 429) {
console.error('Too many requests. Please try again later.');
}
process.exit(1);
}
// Get the content
const content = await response.arrayBuffer();
const contentBuffer = Buffer.from(content);
// If it's encrypted, decrypt it
if (isEncrypted) {
console.log('🔒 This paste is encrypted');
try {
let result;
// Determine whether to use GPG keyring
const useGpgKeyring = options.useGpgKeyring !== false;
// Check if we're in interactive mode
if (options.interactive) {
console.log('Using interactive mode for decryption...');
// Import the inquirer module
const inquirer = await import('inquirer');
// Check if PGP key file is provided or should be prompted
let pgpKeyFile = options.pgpKeyFile;
let passphrase = options.pgpPassphrase;
// If no key file is provided but we're in interactive mode, ask if user wants to use one
if (!pgpKeyFile) {
const { usePgpKey } = await inquirer.default.prompt([{
type: 'confirm',
name: 'usePgpKey',
message: 'Do you want to use a PGP private key file for decryption?',
default: false
}]);
if (usePgpKey) {
const { keyFile } = await inquirer.default.prompt([{
type: 'input',
name: 'keyFile',
message: 'Enter the path to your PGP private key file:',
validate: (input) => {
if (!input) return 'Path cannot be empty';
if (!fs.existsSync(input)) return 'File does not exist';
return true;