From 6d1ece3156c3ee7ce3fe1c19aabeb45b34d6436a Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Sat, 22 Aug 2026 12:29:32 +0200 Subject: feat: node:start auto-provisions config and unlock key MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the wizard calls node:start with {hubUrl, username}, the handler writes a minimal node.toml and a random unlock.key if they don't exist. The daemon then creates the keystore on first start using the unlock file — fully non-interactive. Existing configs are left untouched: if node.toml or unlock.key already exist, provisioning is skipped and the node starts as before. Co-Authored-By: Claude Opus 4.6 --- packages/meshbay-client/src/main.js | 35 +++++++++++++++++++++- packages/meshbay-client/src/preload.js | 2 +- packages/meshbay-hub/src/meshbay_hub/static/app.js | 6 ++-- .../meshbay-hub/src/meshbay_hub/static/platform.js | 4 +-- 4 files changed, 40 insertions(+), 7 deletions(-) diff --git a/packages/meshbay-client/src/main.js b/packages/meshbay-client/src/main.js index 22e0162..8dcc2ae 100644 --- a/packages/meshbay-client/src/main.js +++ b/packages/meshbay-client/src/main.js @@ -744,7 +744,36 @@ function registerBridge() { } catch { return null; } } - ipcMain.handle('node:start', async () => { + function provisionNode(hubUrl, username) { + const configDir = path.join(os.homedir(), '.config', 'meshbay'); + fs.mkdirSync(configDir, { recursive: true }); + + const configFile = nodeConfigPath(); + if (!fs.existsSync(configFile)) { + const toml = [ + '[hub]', + `url = "${hubUrl}"`, + `username = "${username}"`, + '', + '[node]', + 'quic_port = 19010', + 'ui_port = 18000', + '', + '[keystore]', + `unlock_file = "${path.join(configDir, 'unlock.key')}"`, + '', + ].join('\n'); + fs.writeFileSync(configFile, toml, { mode: 0o600 }); + } + + const unlockFile = path.join(configDir, 'unlock.key'); + if (!fs.existsSync(unlockFile)) { + const key = crypto.randomBytes(32).toString('base64url'); + fs.writeFileSync(unlockFile, key + '\n', { mode: 0o600 }); + } + } + + ipcMain.handle('node:start', async (_e, opts) => { const already = await probeNode(); if (already) return { started: true, ...already }; @@ -752,6 +781,10 @@ function registerBridge() { throw new Error('Automatic node start is only supported on Linux'); } + if (opts && opts.hubUrl && opts.username) { + provisionNode(opts.hubUrl, opts.username); + } + const deadline = Date.now() + 15000; const configFile = nodeConfigPath(); let launched = false; diff --git a/packages/meshbay-client/src/preload.js b/packages/meshbay-client/src/preload.js index c8553a5..f240995 100644 --- a/packages/meshbay-client/src/preload.js +++ b/packages/meshbay-client/src/preload.js @@ -87,7 +87,7 @@ contextBridge.exposeInMainWorld('meshbay', { node: { detect: () => ipcRenderer.invoke('node:detect'), installed: () => ipcRenderer.invoke('node:installed'), - start: () => ipcRenderer.invoke('node:start'), + start: (opts) => ipcRenderer.invoke('node:start', opts), call: (method, path, body) => ipcRenderer.invoke('node:call', method, path, body), pairingCode: () => ipcRenderer.invoke('node:pairing-code'), setPairingCode: (code) => ipcRenderer.invoke('node:set-pairing-code', code), diff --git a/packages/meshbay-hub/src/meshbay_hub/static/app.js b/packages/meshbay-hub/src/meshbay_hub/static/app.js index 4b5ccd3..595674d 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/app.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/app.js @@ -1149,7 +1149,7 @@ function CreateGroupFormSimple({ token, onCreated }) { // ── Create Group Wizard (Electron-only) ───────────────────────────────────── -function CreateGroupWizard({ token, onCreated }) { +function CreateGroupWizard({ token, username, onCreated }) { const [step, setStep] = useState(0); // 0=node check, 1=details, 2=setup, 3=done const [nodeStatus, setNodeStatus] = useState(null); // null=loading, object=result const [nodeStarting, setNodeStarting] = useState(false); @@ -1201,7 +1201,7 @@ function CreateGroupWizard({ token, onCreated }) { setNodeStarting(true); setError(''); try { - const result = await platform.node.start(); + const result = await platform.node.start({ hubUrl: HUB, username }); await linkNodeKey(result.pk_node_ed25519); setNodeStatus({ detected: true, ...result }); setNodeStarting(false); @@ -6441,7 +6441,7 @@ function App() { page = html`<${ExplorePage} token=${user.token} myGroupIds=${groups.map(g => g.id)} />`; } else if (route === '/create-group') { - page = html`<${CreateGroupPage} token=${user.token} + page = html`<${CreateGroupPage} token=${user.token} username=${user.username} onCreated=${() => { hubFetch('/v1/groups/mine', { token: user.token }) .then(data => setGroups(data.groups || [])) diff --git a/packages/meshbay-hub/src/meshbay_hub/static/platform.js b/packages/meshbay-hub/src/meshbay_hub/static/platform.js index eeffd49..a8e17bf 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/platform.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/platform.js @@ -227,9 +227,9 @@ export const node = { async installed() { return bridge && bridge.node ? bridge.node.installed() : { installed: false }; }, - async start() { + async start(opts) { if (!bridge || !bridge.node) throw new Error('Node bridge not available'); - return bridge.node.start(); + return bridge.node.start(opts); }, async call(method, path, body) { if (!bridge || !bridge.node) throw new Error('Node bridge not available'); -- cgit v1.2.3