#!/usr/bin/env node /** * Vaultwarden CLI Tool (交互版) * - 交互式输入密码 * - 支持查看密码 * - OAuth 自动认证 */ const fs = require('fs'); const path = require('path'); const https = require('https'); const readline = require('readline'); // Configuration const CONFIG_PATH = process.env.VAULTWARDEN_CONFIG_PATH || path.join(process.env.HOME || process.env.USERPROFILE, '.config', 'vaultwarden', 'config.json'); const ENV = { url: process.env.VAULTWARDEN_URL || 'https://bit.180356.xyz', clientId: process.env.VAULTWARDEN_CLIENT_ID || '', clientSecret: process.env.VAULTWARDEN_CLIENT_SECRET || '' }; // Load config file let config = { url: ENV.url, client_id: '', client_secret: '' }; if (fs.existsSync(CONFIG_PATH)) { try { config = { ...config, ...JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8')) }; } catch (e) { console.error('Error reading config:', e.message); } } const VAULT_URL = ENV.url || config.url; const CLIENT_ID = ENV.clientId || config.client_id; const CLIENT_SECRET = ENV.clientSecret || config.client_secret; // 交互式输入 const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); function ask(question) { return new Promise(resolve => { rl.question(question, answer => resolve(answer)); }); } class VaultwardenAPI { constructor() { this.token = null; this.tokenExpiry = null; this.deviceId = 'openclaw-cli-' + require('crypto').randomUUID(); this.deviceType = '14'; this.deviceName = 'OpenClaw CLI'; } async getToken() { if (this.token && this.tokenExpiry && Date.now() < this.tokenExpiry - 300000) { return this.token; } const params = new URLSearchParams({ grant_type: 'client_credentials', scope: 'api', client_id: CLIENT_ID, client_secret: CLIENT_SECRET, device_identifier: this.deviceId, device_type: this.deviceType, device_name: this.deviceName }); const response = await this.request( '/identity/connect/token', 'POST', params.toString(), false, 'application/x-www-form-urlencoded' ); if (!response.access_token) { throw new Error('认证失败'); } this.token = response.access_token; this.tokenExpiry = Date.now() + ((response.expires_in || 7200) - 300) * 1000; return this.token; } async request(endpoint, method = 'GET', data = null, useAuth = true, contentType = 'application/json') { const url = new URL(VAULT_URL + endpoint); return new Promise((resolve, reject) => { const options = { hostname: url.hostname, port: url.port || 443, path: url.pathname + url.search, method: method, headers: { 'Content-Type': contentType, 'Accept': 'application/json' }, timeout: 30000 }; if (useAuth && this.token) { options.headers['Authorization'] = `Bearer ${this.token}`; } const req = https.request(options, (res) => { let body = ''; res.on('data', chunk => body += chunk); res.on('end', () => { try { const json = body ? JSON.parse(body) : {}; if (res.statusCode >= 200 && res.statusCode < 300) { resolve(json); } else { reject(new Error(`HTTP ${res.statusCode}: ${json.message || body}`)); } } catch (e) { reject(e); } }); }); req.on('error', reject); req.on('timeout', () => { req.destroy(); reject(new Error('超时')); }); if (data) req.write(data); req.end(); }); } async listItems() { await this.getToken(); const response = await this.request('/api/ciphers'); return response.data || []; } async getItem(id) { await this.getToken(); return await this.request(`/api/ciphers/${id}`); } async getItemByName(name) { const items = await this.listItems(); return items.find(item => item.name?.toLowerCase() === name.toLowerCase()); } async createItem(name, password, username = '', notes = '') { await this.getToken(); const itemData = { name: name, notes: notes, type: 1, favorite: false, login: { username: username, password: password, totp: null }, fields: [], collectionIds: [] }; return await this.request('/api/ciphers', 'POST', JSON.stringify(itemData)); } async deleteItem(id) { await this.getToken(); return await this.request(`/api/ciphers/${id}?permanent=true`, 'DELETE'); } } async function main() { const args = process.argv.slice(2); const command = args[0] || 'list'; if (!CLIENT_ID || !CLIENT_SECRET) { console.error('❌ 未配置! 请设置环境变量或配置文件'); console.log('\n💡 运行: npm run setup'); process.exit(1); } const api = new VaultwardenAPI(); try { switch (command) { case 'save': case 'add': await handleSave(args.slice(1), api); break; case 'list': await handleList(api); break; case 'get': await handleGet(args.slice(1), api); break; case 'search': await handleSearch(args.slice(1), api); break; case 'delete': await handleDelete(args.slice(1), api); break; case 'setup': await handleSetup(); break; case 'help': default: showHelp(); break; } } catch (error) { console.error('\n❌ 错误:', error.message); process.exit(1); } finally { rl.close(); } } async function handleSave(args, api) { console.log('\n📝 保存新项目\n'); const name = await ask('名称: '); if (!name) { console.error('❌ 名称不能为空'); return; } const username = await ask('用户名 (可选): '); const password = await ask('密码: '); if (!password) { console.error('❌ 密码不能为空'); return; } const notes = await ask('备注 (可选): '); await api.createItem(name, password, username, notes); console.log(`\n✅ 已保存: ${name}\n`); } async function handleList(api) { const items = await api.listItems(); console.log(`\n📋 项目列表 (${items.length})\n`); items.forEach(item => { const name = item.name || 'Unknown'; const id = item.id?.slice(0, 8) || 'N/A'; console.log(` • ${name} [${id}]`); }); console.log(''); } async function handleGet(args, api) { const name = args[0]; if (!name) { console.error('\n用法: get <名称>\n'); return; } const item = await api.getItemByName(name); if (!item) { console.error(`\n❌ 未找到: ${name}\n`); return; } console.log(`\n📄 ${item.name}\n`); console.log(` ID: ${item.id}`); console.log(` 用户名: ${item.login?.username || '无'}`); console.log(` 密码: ${item.login?.password || '无'}`); console.log(` 备注: ${item.notes || '无'}`); console.log(''); } async function handleSearch(args, api) { const query = args[0]; if (!query) { console.error('\n用法: search <关键词>\n'); return; } const items = await api.listItems(); const results = items.filter(item => item.name?.toLowerCase().includes(query.toLowerCase()) || item.login?.username?.toLowerCase().includes(query.toLowerCase()) || item.notes?.toLowerCase().includes(query.toLowerCase()) ); console.log(`\n🔍 搜索 "${query}" (${results.length} 个结果)\n`); results.forEach(item => { console.log(` • ${item.name}`); }); console.log(''); } async function handleDelete(args, api) { const name = args[0]; if (!name) { console.error('\n用法: delete <名称>\n'); return; } const item = await api.getItemByName(name); if (!item) { console.error(`\n❌ 未找到: ${name}\n`); return; } const answer = await ask(`🗑️ 删除 "${item.name}"? (y/N): `); if (answer.toLowerCase() !== 'y' && answer.toLowerCase() !== 'yes') { console.log('已取消\n'); return; } await api.deleteItem(item.id); console.log(`\n✅ 已删除: ${item.name}\n`); } async function handleSetup() { console.log('\n🔐 Vaultwarden 配置\n'); const url = await ask('服务器地址 (默认 https://bit.180356.xyz): '); const clientId = await ask('Client ID: '); const clientSecret = await ask('Client Secret: '); const configData = { url: url || 'https://bit.180356.xyz', client_id: clientId, client_secret: clientSecret }; fs.mkdirSync(path.dirname(CONFIG_PATH), { recursive: true }); fs.writeFileSync(CONFIG_PATH, JSON.stringify(configData, null, 2)); console.log('\n✅ 配置已保存!\n'); } function showHelp() { console.log(` 🔐 Vaultwarden CLI (交互版) 用法: save 保存新项目 (交互式) list 列出所有项目 get <名称> 获取项目详情 search <关键词> 搜索项目 delete <名称> 删除项目 setup 配置 help 显示帮助 示例: save # 交互式保存 list # 列出所有 get GitHub # 获取 GitHub search GitHub # 搜索 delete GitHub # 删除 注意: 密码会在命令行中显示 `); } module.exports = {}; if (require.main === module) { main(); }