From 4230c35728371deb299991c5300ee3205b8b072b Mon Sep 17 00:00:00 2001 From: Marceli Pawlinski Date: Mon, 5 Oct 2026 15:47:35 +0100 Subject: [PATCH 1/9] Stop restoring passwords from localStorage --- src/main.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main.js b/src/main.js index c3b99ac..78dfb47 100644 --- a/src/main.js +++ b/src/main.js @@ -11,11 +11,13 @@ class App { async init() { try { - // Check for saved settings + // Restore non-sensitive settings only. Passwords are deliberately kept + // in memory and must be re-entered after a reload. + localStorage.removeItem('opencode-password'); // purge legacy plaintext storage const savedConfig = { url: localStorage.getItem('opencode-url'), username: localStorage.getItem('opencode-username') || 'opencode', - password: localStorage.getItem('opencode-password') || '' + password: '' }; // Restore theme From 9518b966fa788b6c15f589da16bf4439fb8930ac Mon Sep 17 00:00:00 2001 From: Marceli Pawlinski Date: Mon, 5 Oct 2026 15:47:38 +0100 Subject: [PATCH 2/9] Keep OpenCode password in memory only --- src/ui.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/ui.js b/src/ui.js index 6be1935..39a06c5 100644 --- a/src/ui.js +++ b/src/ui.js @@ -768,17 +768,18 @@ class OpenCodeUI { this.api.username = username; this.api.password = password; - // Save to localStorage + // Persist only non-sensitive settings. The password remains in memory + // for this page lifetime and is removed from legacy localStorage. localStorage.setItem('opencode-url', url); localStorage.setItem('opencode-username', username); - localStorage.setItem('opencode-password', password); + localStorage.removeItem('opencode-password'); // Apply theme document.documentElement.dataset.theme = darkMode ? 'dark' : 'light'; localStorage.setItem('opencode-theme', darkMode ? 'dark' : 'light'); this.hideModal(); - this.showToast('Settings saved', 'success'); + this.showToast('Settings saved. Password will be forgotten when this page reloads.', 'success'); // Reconnect this.loadSessions(); From 3a59b980752cda42bdca699f438a5bfe3936abcc Mon Sep 17 00:00:00 2001 From: Marceli Pawlinski Date: Mon, 5 Oct 2026 15:47:41 +0100 Subject: [PATCH 3/9] Remove persistent credential storage --- src/auth.js | 40 ++++++++++------------------------------ 1 file changed, 10 insertions(+), 30 deletions(-) diff --git a/src/auth.js b/src/auth.js index 8800b24..5a582b9 100644 --- a/src/auth.js +++ b/src/auth.js @@ -5,43 +5,37 @@ class Auth { constructor() { - this.storageKey = 'opencode-auth'; + this.credentials = null; + // Remove credentials persisted by older versions of the app. + localStorage.removeItem('opencode-auth'); } /** * Store credentials */ saveCredentials(username, password) { - const credentials = { + this.credentials = { username, - password: this.encode(password), + password, timestamp: new Date().toISOString() }; - localStorage.setItem(this.storageKey, JSON.stringify(credentials)); } /** * Get stored credentials */ getCredentials() { - try { - const data = JSON.parse(localStorage.getItem(this.storageKey)); - if (!data) return null; - return { - username: data.username, - password: this.decode(data.password), - timestamp: data.timestamp - }; - } catch (e) { - return null; - } + if (!this.credentials) return null; + return { ...this.credentials }; } /** * Clear credentials */ clearCredentials() { - localStorage.removeItem(this.storageKey); + this.credentials = null; + localStorage.removeItem('opencode-auth'); + localStorage.removeItem('opencode-password'); } /** @@ -51,20 +45,6 @@ class Auth { return this.getCredentials() !== null; } - /** - * Encode password (obfuscation, not encryption) - */ - encode(text) { - return btoa(encodeURIComponent(text)); - } - - /** - * Decode password - */ - decode(encoded) { - return decodeURIComponent(atob(encoded)); - } - /** * Verify server connection with credentials */ From e6c39a4fcda871f7df741333c55c8dfefd22ebfb Mon Sep 17 00:00:00 2001 From: Marceli Pawlinski Date: Mon, 5 Oct 2026 15:47:44 +0100 Subject: [PATCH 4/9] Default OpenCode server to loopback --- src/server.js | 28 +++++----------------------- 1 file changed, 5 insertions(+), 23 deletions(-) diff --git a/src/server.js b/src/server.js index a3f1a9e..95e4d7d 100644 --- a/src/server.js +++ b/src/server.js @@ -12,7 +12,7 @@ class OpenCodeServer { constructor(options = {}) { this.options = { port: options.port || 4096, - hostname: options.hostname || '0.0.0.0', + hostname: options.hostname || process.env.OPENCODE_HOSTNAME || '127.0.0.1', password: options.password || process.env.OPENCODE_SERVER_PASSWORD, username: options.username || process.env.OPENCODE_SERVER_USERNAME || 'opencode', cors: options.cors || [], @@ -79,8 +79,10 @@ class OpenCodeServer { const installed = await this.checkOpenCodeInstalled(); if (!installed) { - console.log('OpenCode not installed. Installing...'); - await this.installOpenCode(); + throw new Error( + 'OpenCode is not installed. Install it explicitly, verify the installer you use, ' + + 'then run this command again. Automatic curl-to-shell installation is disabled.' + ); } console.log('šŸš€ Starting OpenCode web server...'); @@ -132,26 +134,6 @@ class OpenCodeServer { }; } - /** - * Install OpenCode - */ - installOpenCode() { - return new Promise((resolve, reject) => { - const install = spawn('bash', ['-c', 'curl -fsSL https://opencode.ai/install | bash'], { - stdio: 'inherit', - shell: false - }); - - install.on('exit', (code) => { - if (code === 0) { - console.log('OpenCode installed successfully'); - resolve(); - } else { - reject(new Error('Failed to install OpenCode')); - } - }); - }); - } /** * Wait for server to be ready From f3cff3c277a97895aa2204e9023eb9079ed54232 Mon Sep 17 00:00:00 2001 From: Marceli Pawlinski Date: Mon, 5 Oct 2026 15:48:56 +0100 Subject: [PATCH 5/9] Do not print server passwords --- src/server-manager.js | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/server-manager.js b/src/server-manager.js index e2b182c..cc78f71 100644 --- a/src/server-manager.js +++ b/src/server-manager.js @@ -10,6 +10,7 @@ const command = process.argv[2] || 'start'; const port = parseInt(process.env.OPENCODE_PORT || process.argv[3] || '4096', 10); const password = process.env.OPENCODE_SERVER_PASSWORD; const username = process.env.OPENCODE_SERVER_USERNAME || 'opencode'; +const hostname = process.env.OPENCODE_HOSTNAME || '127.0.0.1'; if (!password) { console.error('āŒ OPENCODE_SERVER_PASSWORD environment variable is required'); @@ -19,6 +20,7 @@ if (!password) { const server = new OpenCodeServer({ port, + hostname, password, username, cors: (process.env.OPENCODE_CORS || '').split(',').filter(Boolean) @@ -28,11 +30,15 @@ async function main() { switch (command) { case 'start': await server.start(); - console.log('\nšŸ“± Access OpenCode mobile UI at:'); - console.log(` http://localhost:${port}`); - console.log(`\nšŸ” Credentials:`); - console.log(` Username: ${username}`); - console.log(` Password: ${password}`); + const displayHost = ['0.0.0.0', '::'].includes(hostname) ? 'localhost' : hostname; + console.log('\nšŸ“± OpenCode server:'); + console.log(` http://${displayHost}:${port}`); + console.log(`\nšŸ” Username: ${username}`); + + console.log(' Password: configured via OPENCODE_SERVER_PASSWORD (not displayed)'); + if (!['127.0.0.1', '::1', 'localhost'].includes(hostname)) { + console.warn('āš ļø OpenCode is bound beyond loopback. Only do this behind an authenticated/private network boundary.'); + } console.log('\nPress Ctrl+C to stop\n'); // Keep process alive From 5de954b6be7505f012f3222a907b9ceab92e6ae7 Mon Sep 17 00:00:00 2001 From: Marceli Pawlinski Date: Mon, 5 Oct 2026 15:49:15 +0100 Subject: [PATCH 6/9] Use loopback bind by default --- .env.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env.example b/.env.example index e110c5a..a270d7d 100644 --- a/.env.example +++ b/.env.example @@ -3,7 +3,7 @@ # OpenCode Server OPENCODE_PORT=4096 -OPENCODE_HOSTNAME=0.0.0.0 +OPENCODE_HOSTNAME=127.0.0.1 OPENCODE_SERVER_USERNAME=opencode # REQUIRED - Change this to a strong password! OPENCODE_SERVER_PASSWORD=your-strong-password-here From 107f70fc5e202336f67625f95e15dd201df8339c Mon Sep 17 00:00:00 2001 From: Marceli Pawlinski Date: Mon, 5 Oct 2026 15:49:18 +0100 Subject: [PATCH 7/9] Document credential and bind hardening --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 67caa44..567c65e 100644 --- a/README.md +++ b/README.md @@ -119,7 +119,7 @@ npm start | Variable | Description | Default | |----------|-------------|---------| | `OPENCODE_PORT` | OpenCode server port | `4096` | -| `OPENCODE_HOSTNAME` | Server bind address | `0.0.0.0` | +| `OPENCODE_HOSTNAME` | Server bind address | `127.0.0.1` | | `OPENCODE_SERVER_USERNAME` | Auth username | `opencode` | | `OPENCODE_SERVER_PASSWORD` | Auth password (**required**) | - | | `OPENCODE_CORS` | Allowed CORS origins (comma-separated) | - | @@ -135,7 +135,8 @@ Add these in **Settings → Secrets and variables → Codespaces**: - Always set a strong `OPENCODE_SERVER_PASSWORD` - Use HTTPS (automatically provided by Codespaces) - Keep CORS origins restricted -- Clear credentials when not in use +- Passwords are kept in memory only and are not persisted to `localStorage`; reloads require re-entry +- Keep the default `127.0.0.1` bind unless you intentionally place the server behind a private/authenticated network boundary ## Testing From ee042e19c7e4032143e512acec7d12c6aec7cca1 Mon Sep 17 00:00:00 2001 From: Marceli Pawlinski Date: Mon, 5 Oct 2026 15:49:21 +0100 Subject: [PATCH 8/9] Avoid shelling out for readiness checks --- src/server.js | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/server.js b/src/server.js index 95e4d7d..6e43940 100644 --- a/src/server.js +++ b/src/server.js @@ -4,6 +4,7 @@ */ const { exec, spawn } = require('child_process'); +const http = require('http'); const path = require('path'); const fs = require('fs'); const os = require('os'); @@ -145,14 +146,24 @@ class OpenCodeServer { const check = setInterval(() => { const port = this.options.port; - const { exec } = require('child_process'); - - exec(`curl -s http://${this.options.hostname}:${port}/doc`, (error, stdout) => { - if (!error && stdout) { + const healthHost = ['0.0.0.0', '::'].includes(this.options.hostname) ? '127.0.0.1' : this.options.hostname; + + const healthRequest = http.get({ + hostname: healthHost, + port, + path: '/doc', + timeout: 1000 + }, (response) => { + response.resume(); + if (response.statusCode && response.statusCode < 500) { clearInterval(check); - console.log(`āœ… OpenCode server ready at http://${this.options.hostname}:${port}`); + console.log(`āœ… OpenCode server ready at http://${healthHost}:${port}`); resolve(); - } else if (Date.now() - start > timeout) { + } + }); + healthRequest.on('timeout', () => healthRequest.destroy()); + healthRequest.on('error', () => { + if (Date.now() - start > timeout) { clearInterval(check); console.log('āš ļø Server not ready within timeout, but may still be starting...'); resolve(); From 312099f33e293a927500df2294930c42343bab6d Mon Sep 17 00:00:00 2001 From: Marceli Pawlinski Date: Mon, 5 Oct 2026 15:55:13 +0100 Subject: [PATCH 9/9] Add build and syntax CI --- .github/workflows/ci.yml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..07e2ca4 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,26 @@ +name: CI + +on: + pull_request: + push: + branches: + - main + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + - run: npm ci + - run: npm run build + - name: Check server syntax + run: | + node --check src/server.js + node --check src/server-manager.js