Pithos: export a key as JSON for scripts, backup tools and agents

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.
This commit is contained in:
Local Dev 2026-10-04 22:34:55 +02:00
parent 11d37aa660
commit cb622aa5af
2 changed files with 38 additions and 2 deletions

View file

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

View file

@ -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