diff --git a/theseus/index.js b/theseus/index.js index 4fa090c..759d5f1 100644 --- a/theseus/index.js +++ b/theseus/index.js @@ -328,7 +328,9 @@ module.exports = { const safe = String(name || "pithos-credentials.pithoskey").replace(/[\\/:*?"<>|]+/g, "_").slice(0, 120); const r = await dialog.showSaveDialog(BrowserWindow.getFocusedWindow() || undefined, { defaultPath: path.join(app.getPath("downloads"), safe), - filters: [{ name: "Pithos credentials", extensions: ["pithoskey"] }], + filters: safe.endsWith(".json") + ? [{ name: "JSON", extensions: ["json"] }] + : [{ name: "Pithos credentials", extensions: ["pithoskey"] }], }); if (r.canceled || !r.filePath) return { saved: false }; fs.writeFileSync(r.filePath, String(text || ""), { mode: 0o600 }); diff --git a/ui/app.js b/ui/app.js index b3bee57..fde8338 100644 --- a/ui/app.js +++ b/ui/app.js @@ -1093,13 +1093,47 @@ AWS_ENDPOINT_URL=${endpoint}`; fresh && h('p', { class: 'small muted', style: 'margin:0' }, 'Treat the secret like a password. You can view it again here later.'), secretRow('Access key ID', k.accessKeyId), secretRow('Secret key', k.secretKey), - h('div', {}, h('button', { class: 'btn small', type: 'button', onclick: () => saveCredentialsDialog(k) }, 'Save credentials…')), + h('div', { class: 'row' }, + h('button', { class: 'btn small', type: 'button', onclick: () => saveCredentialsDialog(k) }, 'Save credentials…'), + h('button', { class: 'btn small', type: 'button', onclick: () => exportJsonDialog(k) }, 'Export JSON…')), h('h3', { style: 'margin-top:8px' }, 'Client config'), tabBtns, pre, h('div', {}, h('button', { class: 'btn small', type: 'button', onclick: () => copy(pre.textContent) }, 'Copy snippet'))), [{ label: 'Done', kind: 'primary', submit: true, result: true }]); } +// ---------------------------------------------------------------- plain JSON export + +// The shape scripts and agents read: one key, one drive. It is not encrypted, +// so the dialog says where it should and should not go. +async function exportJsonDialog(k) { + const endpoint = state.status?.config.apiAddress || isHosted() ? s3Endpoint() : 'http://127.0.0.1:8000'; + let drives = []; + try { drives = (await api('GET', `/api/s3/${encodeURIComponent(k.user)}/buckets`)).map((b) => b.name); } catch {} + const sel = h('select', {}, drives.length + ? drives.map((n) => h('option', { value: n }, n)) + : h('option', { value: '' }, '(no drives yet)')); + const json = () => JSON.stringify({ endpoint, accessKey: k.accessKeyId, secretKey: k.secretKey, bucket: sel.value }, null, 2) + '\n'; + const masked = () => json().replace(k.secretKey, '•'.repeat(12)); + const pre = h('pre', { class: 'snippet' }, masked()); + sel.addEventListener('change', () => { pre.textContent = masked(); }); + await modal('Export JSON', h('div', { class: 'stack' }, + h('p', { class: 'small muted', style: 'margin:0' }, `A plain JSON file with the endpoint, this key and one drive of ${k.user}, for scripts, backup tools and AI agents. The key can reach every drive of ${k.user}; the bucket field only says which one to use.`), + drives.length > 0 && h('label', { class: 'field' }, h('span', {}, 'Drive'), sel), + pre, + h('div', { class: 'banner warn' }, h('span', {}, 'The secret is not encrypted in this file. Keep it out of git, chats and shared folders, and give an agent its own S3 user so you can revoke it alone. For a protected copy use Save credentials….')), + isHosted() && h('p', { class: 'small muted', style: 'margin:0' }, 'Drives on Silent Mode are encrypted by Pithos on your computer, so other tools see encrypted files except in folders you share.')), + [{ label: 'Cancel', result: null }, + { label: 'Copy', value: async () => { await copy(json(), 'JSON copied'); return false; } }, + { label: 'Save file', kind: 'primary', submit: true, value: async () => { + try { + const name = `pithos-${k.user}${sel.value ? '-' + sel.value : ''}.json`; + if (await saveFile(name, json())) toast('Saved ' + name); + return true; + } catch (e) { fail(e); return false; } + } }]); +} + // ---------------------------------------------------------------- saved credentials (encrypted file) // A key pair saved to a file the user keeps. Password files use PBKDF2-SHA256