diff --git a/extension/dashboard/manifest.webmanifest b/extension/dashboard/manifest.webmanifest index 61b0686..027b312 100644 --- a/extension/dashboard/manifest.webmanifest +++ b/extension/dashboard/manifest.webmanifest @@ -1,10 +1,10 @@ { - "id": "https://my.dash.board/", + "id": "/dashboard/dashboard.html", "name": "Holesail Browser", "short_name": "Holesail", "description": "P2P Holesail tunnels in the browser", - "start_url": "https://my.dash.board/", - "scope": "https://my.dash.board/", + "start_url": "dashboard.html", + "scope": ".", "display": "standalone", "display_override": ["window-controls-overlay", "standalone"], "background_color": "#0f1117", @@ -12,7 +12,7 @@ "protocol_handlers": [ { "protocol": "web+holesail", - "url": "https://my.dash.board/?lookup=%s" + "url": "dashboard.html?lookup=%s" } ], "screenshots": [ diff --git a/native-host/dashboard-server.js b/native-host/dashboard-server.js index 08d959d..381535f 100644 --- a/native-host/dashboard-server.js +++ b/native-host/dashboard-server.js @@ -18,6 +18,11 @@ const path = require('bare-path'); const { allocateTunnelPort, releaseTunnelPort } = require('./holesail-manager/port-allocator.js'); +// Resolve the extension/dashboard directory for dev-mode file serving. +// In dev mode, __dirname is native-host/ so the dashboard is one level up. +// In distribution mode this path is never used (files come from the bundle). +const DEV_DASHBOARD_DIR = path.join(__dirname, '..', 'extension', 'dashboard'); + const DASHBOARD_HOSTNAME = 'my.dash.board'; const MIME_TYPES = { @@ -35,7 +40,6 @@ const MIME_TYPES = { let _server = null; let _port = null; -let _baseDir = null; // fallback for dev mode function _getMimeType(filePath) { const ext = path.extname(filePath).toLowerCase(); @@ -43,51 +47,46 @@ function _getMimeType(filePath) { } /** - * Read a dashboard file. In distribution mode, reads from the bare-bundle asset - * embedded at build time. In development mode, reads from disk. - * @param {string} relPath - Relative path within the dashboard directory (e.g. 'dashboard.html'). + * 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 _readFile(relPath) { - // Normalise path separators and strip leading slashes/dots - const safe = relPath.replace(/\\/g, '/').replace(/^[./]+/, ''); - +function _readFileByKey(bundleKey, diskRelPath) { // Distribution mode: read from bare-bundle assets try { if (typeof require.bundle === 'function') { const b = require.bundle(); if (b) { - const buf = b.read('/dashboard/' + safe); + const buf = b.read(bundleKey); if (buf !== null) return buf; } } } catch (_) {} // Development mode: read from disk - if (_baseDir) { - try { - const fs = require('bare-fs'); - const fullPath = path.join(_baseDir, safe); - return fs.readFileSync(fullPath); - } catch (_) {} - } + try { + const fs = require('bare-fs'); + const fullPath = path.join(DEV_DASHBOARD_DIR, diskRelPath); + return fs.readFileSync(fullPath); + } catch (_) {} return null; } /** * Start the dashboard HTTP server. - * @param {string} [devBasePath] - Base directory for dev-mode file serving (optional). * @returns {Promise<{port: number}>} */ -function start(devBasePath) { +function start() { return new Promise((resolve, reject) => { if (_server) { resolve({ port: _port }); return; } - _baseDir = devBasePath || null; _port = allocateTunnelPort(); const tcpServer = tcp.createServer(); @@ -98,9 +97,55 @@ function start(devBasePath) { 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 - const relPath = urlPath.replace(/^\/+/, ''); - const buf = _readFile(relPath); + 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; @@ -112,8 +157,6 @@ function start(devBasePath) { res.statusCode = 200; res.setHeader('Content-Type', _getMimeType(relPath)); res.setHeader('Content-Length', buf.length); - // Allow the extension's chrome.* APIs to work when loaded in a PWA window - res.setHeader('Access-Control-Allow-Origin', '*'); res.end(buf); }); conn.on('error', () => {}); diff --git a/native-host/host/startup.js b/native-host/host/startup.js index 0352109..b82cc6c 100644 --- a/native-host/host/startup.js +++ b/native-host/host/startup.js @@ -5,7 +5,6 @@ */ const { log, debugLog } = require('./logger.js'); -const { BASE_DIR } = require('./paths.js'); const dashboardServer = require('../dashboard-server.js'); const PROXY_PORT = 8443; @@ -118,7 +117,7 @@ function initStartup(holesailManager, certificateAuthority, httpsProxy, connectP // 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(BASE_DIR); + const { port: dashPort } = await dashboardServer.start(); holesailManager.setLocalVirtualHost(dashboardServer.getHostname(), dashPort); log('Dashboard server ready on port', dashPort, '→ https://' + dashboardServer.getHostname() + '/'); } catch (e) { diff --git a/scripts/build-distributable.js b/scripts/build-distributable.js index d3cd372..37ee374 100644 --- a/scripts/build-distributable.js +++ b/scripts/build-distributable.js @@ -37,6 +37,7 @@ 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', @@ -281,6 +282,24 @@ module.exports = EventEmitter; } 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) {