diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml
index af4c28f..1cf6e42 100644
--- a/.gitea/workflows/ci.yml
+++ b/.gitea/workflows/ci.yml
@@ -76,7 +76,6 @@ jobs:
node --check native-host/https-proxy.js
node --check native-host/connect-proxy.js
node --check native-host/messenger.js
- node --check native-host/dashboard-server.js
node --check native-host/host/paths.js
node --check native-host/host/logger.js
node --check native-host/host/startup.js
@@ -149,13 +148,6 @@ jobs:
echo "$OUTPUT" | grep -i "not available"
fi
- if echo "$OUTPUT" | grep -q "\[dashboard-server\] listening on"; then
- echo "PASS: dashboard server started (my.dash.board virtual host registered)"
- else
- echo "FAIL: dashboard server did not start — bundle assets may be missing"
- exit 1
- fi
-
echo "PASS: smoke test complete"
- name: Generate checksums
diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml
index 4a20042..40e1a85 100644
--- a/.gitea/workflows/release.yml
+++ b/.gitea/workflows/release.yml
@@ -71,13 +71,6 @@ jobs:
exit 1
fi
- if echo "$OUTPUT" | grep -q "\[dashboard-server\] listening on"; then
- echo "PASS: dashboard server started (my.dash.board virtual host registered)"
- else
- echo "FAIL: dashboard server did not start — bundle assets may be missing"
- exit 1
- fi
-
- name: List release artifacts
run: |
echo "=== releases/ ==="
diff --git a/extension/dashboard/pages/virtual-hosts.js b/extension/dashboard/pages/virtual-hosts.js
index e087eec..4e864f3 100644
--- a/extension/dashboard/pages/virtual-hosts.js
+++ b/extension/dashboard/pages/virtual-hosts.js
@@ -54,23 +54,20 @@ function updateConnectionsTable(state) {
const openUrl = `https://${hostname}`;
const safeHostname = hostname.replace(/"/g, '"');
const needsReconnect = v.state === 'error' || v.state === 'closed';
- const isBuiltIn = v.type === 'local';
return `
- | ${isBuiltIn ? '' : ``} |
+ |
${escapeHtml(hostname)}
- ${isBuiltIn ? 'built-in' : ''}
|
- ${isBuiltIn ? '—' : `
${truncate(hsUrl, 28)}
- `}
+
|
${escapeHtml(backend)} |
@@ -83,14 +80,14 @@ function updateConnectionsTable(state) {
Open
- ${!isBuiltIn && needsReconnect ? ` |
`;
diff --git a/native-host/dashboard-server.js b/native-host/dashboard-server.js
deleted file mode 100644
index 566607a..0000000
--- a/native-host/dashboard-server.js
+++ /dev/null
@@ -1,249 +0,0 @@
-/**
- * Local HTTP server that serves the Holesail dashboard as a static site.
- *
- * In distribution mode (running as a standalone Bare binary), dashboard files
- * are read from the bare-bundle assets embedded at build time via `require.bundle()`.
- *
- * In development mode (`bare index.mjs`), `require.bundle()` is not available,
- * so files are read from disk relative to the source directory.
- *
- * The server is registered as the backend for the `my.dash.board` virtual host
- * so the HTTPS proxy can serve the dashboard over a trusted HTTPS origin,
- * satisfying Chrome's PWA installability requirement.
- */
-
-const http1 = require('bare-http1');
-const tcp = require('bare-tcp');
-const path = require('bare-path');
-const EventEmitter = require('bare-events');
-
-const { allocateTunnelPort, releaseTunnelPort } = require('./holesail-manager/port-allocator.js');
-
-// Resolve the extension/dashboard directory for dev-mode file serving.
-// Uses the same logic as host/paths.js: in dev mode __dirname is the real
-// native-host/ directory; in distribution mode the bundle read takes priority
-// and this path is only used as a last-resort fallback.
-function _resolveDevDashboardDir() {
- // In dev mode __dirname is the real native-host/ path
- if (process.stderr) process.stderr.write('[dashboard-server] __dirname=' + __dirname + '\n');
- if (__dirname && !__dirname.startsWith('bare:')) {
- const d = path.join(__dirname, '..', 'extension', 'dashboard');
- if (process.stderr) process.stderr.write('[dashboard-server] DEV_DASHBOARD_DIR (from __dirname)=' + d + '\n');
- return d;
- }
- // Distribution mode: try executable directory as fallback
- try {
- const os = require('bare-os');
- const execPath = os.execPath();
- if (process.stderr) process.stderr.write('[dashboard-server] execPath=' + execPath + '\n');
- if (execPath && !execPath.startsWith('bare:')) {
- const d = path.join(path.dirname(execPath), 'extension', 'dashboard');
- if (process.stderr) process.stderr.write('[dashboard-server] DEV_DASHBOARD_DIR (from execPath)=' + d + '\n');
- return d;
- }
- } catch (e) {
- if (process.stderr) process.stderr.write('[dashboard-server] execPath error: ' + e.message + '\n');
- }
- if (process.stderr) process.stderr.write('[dashboard-server] DEV_DASHBOARD_DIR=null\n');
- return null;
-}
-const DEV_DASHBOARD_DIR = _resolveDevDashboardDir();
-
-const DASHBOARD_HOSTNAME = 'my.dash.board';
-
-const MIME_TYPES = {
- '.html': 'text/html; charset=utf-8',
- '.css': 'text/css; charset=utf-8',
- '.js': 'application/javascript; charset=utf-8',
- '.json': 'application/json; charset=utf-8',
- '.webmanifest': 'application/manifest+json; charset=utf-8',
- '.png': 'image/png',
- '.ico': 'image/x-icon',
- '.svg': 'image/svg+xml; charset=utf-8',
- '.woff': 'font/woff',
- '.woff2': 'font/woff2'
-};
-
-let _server = null;
-let _port = null;
-
-function _getMimeType(filePath) {
- const ext = path.extname(filePath).toLowerCase();
- return MIME_TYPES[ext] || 'application/octet-stream';
-}
-
-/**
- * Read a file by its bundle key and disk-relative path.
- * In distribution mode, reads from the bare-bundle asset embedded at build time.
- * In development mode, reads from disk relative to DEV_DASHBOARD_DIR.
- * @param {string} bundleKey - Key in the bare-bundle (e.g. '/dashboard/dashboard.html').
- * @param {string} diskRelPath - Path relative to DEV_DASHBOARD_DIR (e.g. 'dashboard.html' or '../icons/48.png').
- * @returns {Buffer|null} File contents or null if not found.
- */
-function _readFileByKey(bundleKey, diskRelPath) {
- // Distribution mode: read from bare-bundle assets
- try {
- if (typeof require.bundle === 'function') {
- const b = require.bundle();
- if (process.stderr) process.stderr.write('[dashboard-server] bundle=' + (b ? 'object' : 'null') + ' key=' + bundleKey + '\n');
- if (b) {
- const buf = b.read(bundleKey);
- if (process.stderr) process.stderr.write('[dashboard-server] bundle.read=' + (buf ? buf.length + ' bytes' : 'null') + '\n');
- if (buf !== null) return buf;
- }
- } else {
- if (process.stderr) process.stderr.write('[dashboard-server] require.bundle not a function (type=' + typeof require.bundle + ')\n');
- }
- } catch (e) {
- if (process.stderr) process.stderr.write('[dashboard-server] bundle error: ' + e.message + '\n');
- }
-
- // Disk fallback (dev mode, or distribution mode without bundle assets)
- if (DEV_DASHBOARD_DIR) {
- try {
- const fs = require('bare-fs');
- const fullPath = path.join(DEV_DASHBOARD_DIR, diskRelPath);
- if (process.stderr) process.stderr.write('[dashboard-server] disk read: ' + fullPath + '\n');
- const buf = fs.readFileSync(fullPath);
- if (process.stderr) process.stderr.write('[dashboard-server] disk read OK: ' + buf.length + ' bytes\n');
- return buf;
- } catch (e) {
- if (process.stderr) process.stderr.write('[dashboard-server] disk read error: ' + e.message + '\n');
- }
- } else {
- if (process.stderr) process.stderr.write('[dashboard-server] DEV_DASHBOARD_DIR is null, skipping disk read\n');
- }
-
- return null;
-}
-
-/**
- * Start the dashboard HTTP server.
- * @returns {Promise<{port: number}>}
- */
-function start() {
- return new Promise((resolve, reject) => {
- if (_server) {
- resolve({ port: _port });
- return;
- }
-
- _port = allocateTunnelPort();
-
- const tcpServer = tcp.createServer();
-
- // Minimal server object required by bare-http1 ServerConnection.
- // ServerConnection emits 'request' on this object, not on itself.
- const httpServer = new EventEmitter();
- httpServer.timeout = 0;
- httpServer.closing = false;
- httpServer.on('request', (req, res) => {
- let urlPath = (req.url || '/').split('?')[0];
- if (urlPath === '/') urlPath = '/dashboard.html';
-
- // Serve a dynamically generated manifest with absolute https://my.dash.board/ URLs
- // so Chrome accepts start_url, id, scope, and protocol_handlers (same-origin requirement).
- if (urlPath === '/manifest.webmanifest') {
- const manifest = {
- id: 'https://' + DASHBOARD_HOSTNAME + '/',
- name: 'Holesail Browser',
- short_name: 'Holesail',
- description: 'P2P Holesail tunnels in the browser',
- start_url: 'https://' + DASHBOARD_HOSTNAME + '/',
- scope: 'https://' + DASHBOARD_HOSTNAME + '/',
- display: 'standalone',
- display_override: ['window-controls-overlay', 'standalone'],
- background_color: '#0f1117',
- theme_color: '#0f1117',
- protocol_handlers: [
- { protocol: 'web+holesail', url: 'https://' + DASHBOARD_HOSTNAME + '/?lookup=%s' }
- ],
- screenshots: [
- { src: 'https://' + DASHBOARD_HOSTNAME + '/screenshots/wide.png', sizes: '1024x515', type: 'image/png', form_factor: 'wide', label: 'Holesail Browser Dashboard' },
- { src: 'https://' + DASHBOARD_HOSTNAME + '/screenshots/narrow.png', sizes: '390x844', type: 'image/png', form_factor: 'narrow', label: 'Holesail Browser Dashboard' }
- ],
- icons: [
- { src: 'https://' + DASHBOARD_HOSTNAME + '/icons/48.png', sizes: '48x48', type: 'image/png', purpose: 'any' },
- { src: 'https://' + DASHBOARD_HOSTNAME + '/icons/128.png', sizes: '128x128', type: 'image/png', purpose: 'any' },
- { src: 'https://' + DASHBOARD_HOSTNAME + '/icons/192.png', sizes: '192x192', type: 'image/png', purpose: 'any' },
- { src: 'https://' + DASHBOARD_HOSTNAME + '/icons/512.png', sizes: '512x512', type: 'image/png', purpose: 'any' }
- ]
- };
- const body = Buffer.from(JSON.stringify(manifest, null, 2));
- res.statusCode = 200;
- res.setHeader('Content-Type', 'application/manifest+json; charset=utf-8');
- res.setHeader('Content-Length', body.length);
- res.end(body);
- return;
- }
-
- // Strip leading slash for file lookup
- let relPath = urlPath.replace(/^\/+/, '');
-
- // Icons are stored in extension/icons/ (one level up from dashboard/).
- // Remap /icons/ → ../icons/ for the bundle key and disk path.
- let bundleKey = '/dashboard/' + relPath;
- let diskRelPath = relPath;
- if (relPath.startsWith('icons/')) {
- bundleKey = '/' + relPath; // stored as /icons/ in bundle
- diskRelPath = '../icons/' + relPath.slice('icons/'.length);
- }
-
- const buf = _readFileByKey(bundleKey, diskRelPath);
-
- if (!buf) {
- res.statusCode = 404;
- res.setHeader('Content-Type', 'text/plain');
- res.end('Not found: ' + relPath);
- return;
- }
-
- res.statusCode = 200;
- res.setHeader('Content-Type', _getMimeType(relPath));
- res.setHeader('Content-Length', buf.length);
- res.end(buf);
- });
-
- tcpServer.on('connection', (socket) => {
- // eslint-disable-next-line no-new
- new http1.ServerConnection(httpServer, socket, {});
- });
-
- tcpServer.on('error', (err) => {
- releaseTunnelPort(_port);
- _port = null;
- _server = null;
- reject(err);
- });
-
- tcpServer.listen(_port, '127.0.0.1', () => {
- _server = tcpServer;
- if (process.stderr) {
- process.stderr.write('[dashboard-server] listening on 127.0.0.1:' + _port + '\n');
- }
- resolve({ port: _port });
- });
- });
-}
-
-/**
- * Stop the dashboard HTTP server and release its port.
- */
-function stop() {
- if (_server) {
- try { _server.close(); } catch (_) {}
- _server = null;
- }
- if (_port != null) {
- releaseTunnelPort(_port);
- _port = null;
- }
-}
-
-/** @returns {string} The fixed hostname for the dashboard virtual host. */
-function getHostname() { return DASHBOARD_HOSTNAME; }
-
-/** @returns {number|null} The port the server is listening on, or null if not started. */
-function getPort() { return _port; }
-
-module.exports = { start, stop, getHostname, getPort };
diff --git a/native-host/holesail-manager/index.js b/native-host/holesail-manager/index.js
index dbc38bf..9fc49e4 100644
--- a/native-host/holesail-manager/index.js
+++ b/native-host/holesail-manager/index.js
@@ -30,7 +30,6 @@ function saveState() {
id: s.id, port: s.port, host: s.host, secure: s.secure, udp: s.udp || false, label: s.label || ''
}));
const virtualHostsList = vhostsModule.getVirtualHosts()
- .filter(v => v.type !== 'local') // never persist built-in local hosts
.map(v => ({ hostname: v.hostname, hsUrl: v.hsUrl }));
const serviceTunnelsList = svcModule.getServiceTunnels().map(t => ({
id: t.id, label: t.label, hsUrl: t.hsUrl, localPort: t.localPort
@@ -125,7 +124,6 @@ module.exports = {
getServers: serversModule.getServers,
// Virtual hosts
setVirtualHost: vhostsModule.setVirtualHost,
- setLocalVirtualHost: vhostsModule.setLocalVirtualHost,
removeVirtualHost: vhostsModule.removeVirtualHost,
getVirtualHosts: vhostsModule.getVirtualHosts,
getLocalPortForHostname: vhostsModule.getLocalPortForHostname,
diff --git a/native-host/holesail-manager/virtual-hosts.js b/native-host/holesail-manager/virtual-hosts.js
index dea109b..f2ed1c8 100644
--- a/native-host/holesail-manager/virtual-hosts.js
+++ b/native-host/holesail-manager/virtual-hosts.js
@@ -129,29 +129,6 @@ async function setVirtualHost(payload) {
}
}
-/**
- * Register a built-in local virtual host that is served by an internal HTTP
- * server rather than a Holesail P2P tunnel. The entry is marked `type: 'local'`
- * so it is never persisted to state.json and cannot be removed by the user.
- * @param {string} hostname - The hostname (e.g. `my.dash.board`).
- * @param {number} localPort - The local port the internal server is listening on.
- */
-function setLocalVirtualHost(hostname, localPort) {
- virtualHosts.set(hostname, {
- hostname,
- hsUrl: null,
- holesail: null,
- localHost: TUNNEL_HOST,
- localPort,
- state: 'ready',
- type: 'local',
- createdAt: Date.now(),
- reconnectTimer: null,
- reconnectDelay: 0
- });
- debugLog('setLocalVirtualHost: hostname=', hostname, 'localPort=', localPort);
-}
-
/**
* Remove a virtual host, close its tunnel, and release its local port.
* @param {object} payload
@@ -163,7 +140,6 @@ async function removeVirtualHost(payload) {
debugLog('removeVirtualHost: hostname=', hostname);
const entry = virtualHosts.get(hostname);
if (!entry) return { ok: false, error: 'Virtual host not found' };
- if (entry.type === 'local') return { ok: false, error: 'Built-in host cannot be removed' };
if (entry.reconnectTimer) { clearTimeout(entry.reconnectTimer); entry.reconnectTimer = null; }
if (entry.localPort) releaseTunnelPort(entry.localPort);
if (entry.holesail) { try { await entry.holesail.close(); } catch (_) {} }
@@ -229,4 +205,4 @@ async function cleanupVirtualHosts() {
virtualHosts.clear();
}
-module.exports = { init, setVirtualHost, setLocalVirtualHost, removeVirtualHost, getVirtualHosts, getLocalPortForHostname, getLocalBackend, getVirtualHostMap, cleanupVirtualHosts, RECONNECT_BASE_MS, RECONNECT_MAX_MS };
+module.exports = { init, setVirtualHost, removeVirtualHost, getVirtualHosts, getLocalPortForHostname, getLocalBackend, getVirtualHostMap, cleanupVirtualHosts, RECONNECT_BASE_MS, RECONNECT_MAX_MS };
diff --git a/native-host/host/message-router.js b/native-host/host/message-router.js
index c77bf15..c8dbef5 100644
--- a/native-host/host/message-router.js
+++ b/native-host/host/message-router.js
@@ -16,7 +16,6 @@ const certificateAuthority = require('../certificate-authority.js');
const httpsProxy = require('../https-proxy.js');
const connectProxy = require('../connect-proxy.js');
const sshManager = require('../ssh-manager.js');
-const dashboardServer = require('../dashboard-server.js');
const backupManager = require('../backup-manager.js');
backupManager.setStoragePath(STORAGE_PATH);
backupManager.setCertsPath(certificateAuthority.getCertsDir());
@@ -344,7 +343,6 @@ async function handleMessageAsync(send, msg) {
function cleanup() {
if (_scheduledBackupTimer) { clearTimeout(_scheduledBackupTimer); _scheduledBackupTimer = null; }
- dashboardServer.stop();
httpsProxy.stop(() => {});
connectProxy.stop(() => {});
sshManager.cleanup();
diff --git a/native-host/host/startup.js b/native-host/host/startup.js
index b82cc6c..9a319fe 100644
--- a/native-host/host/startup.js
+++ b/native-host/host/startup.js
@@ -5,7 +5,6 @@
*/
const { log, debugLog } = require('./logger.js');
-const dashboardServer = require('../dashboard-server.js');
const PROXY_PORT = 8443;
const CONNECT_PROXY_PORT = 8442;
@@ -113,17 +112,6 @@ function initStartup(holesailManager, certificateAuthority, httpsProxy, connectP
log('Proxies startup complete: HTTPS', savedProxyPort, 'CONNECT', connectProxy.getPort() ?? 'FAILED');
- // Start the dashboard HTTP server and register it as the my.dash.board
- // local virtual host so the HTTPS proxy can serve it over a trusted
- // HTTPS origin (satisfying Chrome's PWA installability requirement).
- try {
- const { port: dashPort } = await dashboardServer.start();
- holesailManager.setLocalVirtualHost(dashboardServer.getHostname(), dashPort);
- log('Dashboard server ready on port', dashPort, '→ https://' + dashboardServer.getHostname() + '/');
- } catch (e) {
- log('Dashboard server startup failed:', e.message);
- }
-
// Assign tunnelsRestoredPromise BEFORE resolving proxiesReadyPromise so
// that any getState call awaiting proxiesReadyPromise immediately sees the
// non-null promise rather than the one-microtask window where it is null.
diff --git a/native-host/test-chain.mjs b/native-host/test-chain.mjs
deleted file mode 100644
index 58db7a9..0000000
--- a/native-host/test-chain.mjs
+++ /dev/null
@@ -1,11 +0,0 @@
-import 'bare-process/global';
-import { createRequire } from 'bare-module';
-const require = createRequire(import.meta.url);
-
-// Simulate: startup.js (in host/) requires dashboard-server.js (in native-host/)
-const startup_require = createRequire(new URL('./host/startup.js', import.meta.url));
-const dashServer = startup_require('../dashboard-server.js');
-console.log('dashServer.getHostname():', dashServer.getHostname());
-const result = await dashServer.start();
-console.log('start result:', result);
-await dashServer.stop();
diff --git a/scripts/build-distributable.js b/scripts/build-distributable.js
index 37ee374..8df0896 100644
--- a/scripts/build-distributable.js
+++ b/scripts/build-distributable.js
@@ -36,8 +36,6 @@ const ROOT = path.join(__dirname, '..')
const NATIVE_HOST_DIR = path.join(ROOT, 'native-host')
const RELEASES_DIR = path.join(ROOT, 'releases')
const ENTRY = path.join(NATIVE_HOST_DIR, 'index.mjs')
-const EXTENSION_DASHBOARD_DIR = path.join(ROOT, 'extension', 'dashboard')
-const EXTENSION_ICONS_DIR = path.join(ROOT, 'extension', 'icons')
const ALL_HOSTS = [
'darwin-arm64',
@@ -100,23 +98,6 @@ function getPlatformModule(host) {
}
}
-/**
- * Recursively list all files under a directory.
- * @param {string} dir - Absolute path to the directory.
- * @returns {string[]} Absolute paths to all files.
- */
-function walkDir(dir) {
- const results = []
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
- const full = path.join(dir, entry.name)
- if (entry.isDirectory()) {
- results.push(...walkDir(full))
- } else {
- results.push(full)
- }
- }
- return results
-}
/**
* Patch bare-build's apple/sign.js to skip codesign when not on macOS.
@@ -266,40 +247,6 @@ module.exports = EventEmitter;
console.log(' Patched events module for node-rdpjs-2 compatibility')
}
- // ── Embed dashboard static files as bundle assets ───────────────────────────
- // The dashboard-server.js reads these via require.bundle().read('/dashboard/')
- // at runtime, so no separate file copy step is needed in the installer.
- if (fs.existsSync(EXTENSION_DASHBOARD_DIR)) {
- const dashFiles = walkDir(EXTENSION_DASHBOARD_DIR)
- let dashCount = 0
- for (const file of dashFiles) {
- const rel = path.relative(EXTENSION_DASHBOARD_DIR, file).replace(/\\/g, '/')
- const key = '/dashboard/' + rel
- bundle.write(key, fs.readFileSync(file), { asset: true })
- dashCount++
- }
- console.log(` Embedded ${dashCount} dashboard assets into bundle`)
- } else {
- console.warn(' WARNING: extension/dashboard not found — dashboard assets not embedded')
- }
-
- // ── Embed extension icons as bundle assets ───────────────────────────────────
- // Icons are served at https://my.dash.board/icons/ by dashboard-server.js.
- // They live in extension/icons/ (outside the dashboard dir) so they need a
- // separate embedding pass with keys like /icons/.
- if (fs.existsSync(EXTENSION_ICONS_DIR)) {
- const iconFiles = walkDir(EXTENSION_ICONS_DIR)
- let iconCount = 0
- for (const file of iconFiles) {
- const rel = path.relative(EXTENSION_ICONS_DIR, file).replace(/\\/g, '/')
- const key = '/icons/' + rel
- bundle.write(key, fs.readFileSync(file), { asset: true })
- iconCount++
- }
- console.log(` Embedded ${iconCount} icon assets into bundle`)
- } else {
- console.warn(' WARNING: extension/icons not found — icon assets not embedded')
- }
}
async function build(hosts, doPackage) {