From cb622aa5af8f2a766b8cce685c65032fa4ee2ca0 Mon Sep 17 00:00:00 2001 From: Local Dev Date: Sun, 4 Oct 2026 22:34:55 +0200 Subject: [PATCH] Pithos: export a key as JSON for scripts, backup tools and agents MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit People setting up backups or AI agents want one small file with the endpoint, key, secret and drive, in the shape scripts read. Export JSON… in the key dialog writes exactly that, with a drive picker since a key reaches all of its user's drives. The secret stays masked in the preview, and the dialog says the file is unencrypted and suggests one S3 user per agent. The Theseus save dialog now offers .json for it instead of .pithoskey. --- theseus/index.js | 4 +++- ui/app.js | 36 +++++++++++++++++++++++++++++++++++- 2 files changed, 38 insertions(+), 2 deletions(-) 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