feat(pwa): experimental — add my.dash.board virtual host for secure PWA install
CI / Build & Test (push) Successful in 2m51s
CI / Build & Test (push) Successful in 2m51s
Serve the dashboard over a local HTTPS virtual host (my.dash.board) so Chrome treats it as a secure origin and shows the PWA install prompt. - Add native-host/dashboard-server.js: bare-http1 static file server backed by bare-bundle assets in distribution mode, disk fallback in dev - Add setLocalVirtualHost() to virtual-hosts.js; type:local entries are protected from removal and filtered out of saveState persistence - Export setLocalVirtualHost from holesail-manager/index.js - Start dashboard server in startup.js after proxies are ready and register my.dash.board as a local virtual host - Stop dashboard server in message-router.js cleanup() - Update manifest.webmanifest: start_url, id, scope → https://my.dash.board/ - Embed all extension/dashboard/ files as bare-bundle assets in build-distributable.js patchBundle() — no installer copy step needed - Hide checkbox, hs:// URL, Reconnect, and Remove controls for built-in my.dash.board row in the virtual hosts table UI
This commit is contained in:
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"id": "/dashboard/dashboard.html",
|
"id": "https://my.dash.board/",
|
||||||
"name": "Holesail Browser",
|
"name": "Holesail Browser",
|
||||||
"short_name": "Holesail",
|
"short_name": "Holesail",
|
||||||
"description": "P2P Holesail tunnels in the browser",
|
"description": "P2P Holesail tunnels in the browser",
|
||||||
"start_url": "dashboard.html",
|
"start_url": "https://my.dash.board/",
|
||||||
"scope": ".",
|
"scope": "https://my.dash.board/",
|
||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
"display_override": ["window-controls-overlay", "standalone"],
|
"display_override": ["window-controls-overlay", "standalone"],
|
||||||
"background_color": "#0f1117",
|
"background_color": "#0f1117",
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
"protocol_handlers": [
|
"protocol_handlers": [
|
||||||
{
|
{
|
||||||
"protocol": "web+holesail",
|
"protocol": "web+holesail",
|
||||||
"url": "dashboard.html?lookup=%s"
|
"url": "https://my.dash.board/?lookup=%s"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"screenshots": [
|
"screenshots": [
|
||||||
|
|||||||
@@ -54,18 +54,23 @@ function updateConnectionsTable(state) {
|
|||||||
const openUrl = `https://${hostname}`;
|
const openUrl = `https://${hostname}`;
|
||||||
const safeHostname = hostname.replace(/"/g, '"');
|
const safeHostname = hostname.replace(/"/g, '"');
|
||||||
const needsReconnect = v.state === 'error' || v.state === 'closed';
|
const needsReconnect = v.state === 'error' || v.state === 'closed';
|
||||||
|
const isBuiltIn = v.type === 'local';
|
||||||
return `
|
return `
|
||||||
<tr>
|
<tr>
|
||||||
<td style="width:32px;"><input type="checkbox" class="vhost-row-cb" data-hostname="${safeHostname}"></td>
|
<td style="width:32px;">${isBuiltIn ? '' : `<input type="checkbox" class="vhost-row-cb" data-hostname="${safeHostname}">`}</td>
|
||||||
<td><span class="mono-chip">${escapeHtml(hostname)}</span></td>
|
|
||||||
<td>
|
<td>
|
||||||
|
<span class="mono-chip">${escapeHtml(hostname)}</span>
|
||||||
|
${isBuiltIn ? '<span style="font-size:10px;color:var(--text4);margin-left:4px;">built-in</span>' : ''}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
${isBuiltIn ? '<span style="color:var(--text4);font-size:11px;">—</span>' : `
|
||||||
<div style="display:flex;align-items:center;gap:6px;">
|
<div style="display:flex;align-items:center;gap:6px;">
|
||||||
<span class="mono" title="${escapeHtml(hsUrl)}" style="color:var(--text3);font-size:11px;">${truncate(hsUrl, 28)}</span>
|
<span class="mono" title="${escapeHtml(hsUrl)}" style="color:var(--text3);font-size:11px;">${truncate(hsUrl, 28)}</span>
|
||||||
<button class="btn-icon copy-btn" data-copy="${escapeHtml(hsUrl)}" title="Copy hs:// URL" style="flex-shrink:0;">
|
<button class="btn-icon copy-btn" data-copy="${escapeHtml(hsUrl)}" title="Copy hs:// URL" style="flex-shrink:0;">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
|
||||||
<span class="copy-tooltip">Copied!</span>
|
<span class="copy-tooltip">Copied!</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>`}
|
||||||
</td>
|
</td>
|
||||||
<td class="mono" style="font-size:11px;color:var(--text3);">${escapeHtml(backend)}</td>
|
<td class="mono" style="font-size:11px;color:var(--text3);">${escapeHtml(backend)}</td>
|
||||||
<td>
|
<td>
|
||||||
@@ -78,14 +83,14 @@ function updateConnectionsTable(state) {
|
|||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15,3 21,3 21,9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15,3 21,3 21,9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>
|
||||||
Open
|
Open
|
||||||
</a>
|
</a>
|
||||||
${needsReconnect ? `<button class="btn btn-secondary btn-sm" data-reconnect-vhost="${safeHostname}" data-hs-url="${escapeHtml(hsUrl)}" title="Reconnect tunnel">
|
${!isBuiltIn && needsReconnect ? `<button class="btn btn-secondary btn-sm" data-reconnect-vhost="${safeHostname}" data-hs-url="${escapeHtml(hsUrl)}" title="Reconnect tunnel">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="23,4 23,10 17,10"/><path d="M20.49 15a9 9 0 1 1-.07-8.13"/></svg>
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="23,4 23,10 17,10"/><path d="M20.49 15a9 9 0 1 1-.07-8.13"/></svg>
|
||||||
Reconnect
|
Reconnect
|
||||||
</button>` : ''}
|
</button>` : ''}
|
||||||
<button class="btn btn-danger btn-sm" data-remove-vhost="${safeHostname}">
|
${isBuiltIn ? '' : `<button class="btn btn-danger btn-sm" data-remove-vhost="${safeHostname}">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3,6 5,6 21,6"/><path d="M19,6l-1,14a2,2,0,0,1-2,2H8a2,2,0,0,1-2-2L5,6"/></svg>
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3,6 5,6 21,6"/><path d="M19,6l-1,14a2,2,0,0,1-2,2H8a2,2,0,0,1-2-2L5,6"/></svg>
|
||||||
Remove
|
Remove
|
||||||
</button>
|
</button>`}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
|
|||||||
@@ -0,0 +1,159 @@
|
|||||||
|
/**
|
||||||
|
* 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 { allocateTunnelPort, releaseTunnelPort } = require('./holesail-manager/port-allocator.js');
|
||||||
|
|
||||||
|
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;
|
||||||
|
let _baseDir = null; // fallback for dev mode
|
||||||
|
|
||||||
|
function _getMimeType(filePath) {
|
||||||
|
const ext = path.extname(filePath).toLowerCase();
|
||||||
|
return MIME_TYPES[ext] || 'application/octet-stream';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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').
|
||||||
|
* @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(/^[./]+/, '');
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
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 (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (_server) {
|
||||||
|
resolve({ port: _port });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_baseDir = devBasePath || null;
|
||||||
|
_port = allocateTunnelPort();
|
||||||
|
|
||||||
|
const tcpServer = tcp.createServer();
|
||||||
|
|
||||||
|
tcpServer.on('connection', (socket) => {
|
||||||
|
const conn = new http1.ServerConnection(socket);
|
||||||
|
conn.on('request', (req, res) => {
|
||||||
|
let urlPath = (req.url || '/').split('?')[0];
|
||||||
|
if (urlPath === '/') urlPath = '/dashboard.html';
|
||||||
|
|
||||||
|
// Strip leading slash for file lookup
|
||||||
|
const relPath = urlPath.replace(/^\/+/, '');
|
||||||
|
const buf = _readFile(relPath);
|
||||||
|
|
||||||
|
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);
|
||||||
|
// 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', () => {});
|
||||||
|
});
|
||||||
|
|
||||||
|
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 };
|
||||||
@@ -29,9 +29,9 @@ function saveState() {
|
|||||||
const serversList = serversModule.getServers().map(s => ({
|
const serversList = serversModule.getServers().map(s => ({
|
||||||
id: s.id, port: s.port, host: s.host, secure: s.secure, udp: s.udp || false, label: s.label || ''
|
id: s.id, port: s.port, host: s.host, secure: s.secure, udp: s.udp || false, label: s.label || ''
|
||||||
}));
|
}));
|
||||||
const virtualHostsList = vhostsModule.getVirtualHosts().map(v => ({
|
const virtualHostsList = vhostsModule.getVirtualHosts()
|
||||||
hostname: v.hostname, hsUrl: v.hsUrl
|
.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 => ({
|
const serviceTunnelsList = svcModule.getServiceTunnels().map(t => ({
|
||||||
id: t.id, label: t.label, hsUrl: t.hsUrl, localPort: t.localPort
|
id: t.id, label: t.label, hsUrl: t.hsUrl, localPort: t.localPort
|
||||||
}));
|
}));
|
||||||
@@ -125,6 +125,7 @@ module.exports = {
|
|||||||
getServers: serversModule.getServers,
|
getServers: serversModule.getServers,
|
||||||
// Virtual hosts
|
// Virtual hosts
|
||||||
setVirtualHost: vhostsModule.setVirtualHost,
|
setVirtualHost: vhostsModule.setVirtualHost,
|
||||||
|
setLocalVirtualHost: vhostsModule.setLocalVirtualHost,
|
||||||
removeVirtualHost: vhostsModule.removeVirtualHost,
|
removeVirtualHost: vhostsModule.removeVirtualHost,
|
||||||
getVirtualHosts: vhostsModule.getVirtualHosts,
|
getVirtualHosts: vhostsModule.getVirtualHosts,
|
||||||
getLocalPortForHostname: vhostsModule.getLocalPortForHostname,
|
getLocalPortForHostname: vhostsModule.getLocalPortForHostname,
|
||||||
|
|||||||
@@ -129,6 +129,29 @@ 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.
|
* Remove a virtual host, close its tunnel, and release its local port.
|
||||||
* @param {object} payload
|
* @param {object} payload
|
||||||
@@ -140,6 +163,7 @@ async function removeVirtualHost(payload) {
|
|||||||
debugLog('removeVirtualHost: hostname=', hostname);
|
debugLog('removeVirtualHost: hostname=', hostname);
|
||||||
const entry = virtualHosts.get(hostname);
|
const entry = virtualHosts.get(hostname);
|
||||||
if (!entry) return { ok: false, error: 'Virtual host not found' };
|
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.reconnectTimer) { clearTimeout(entry.reconnectTimer); entry.reconnectTimer = null; }
|
||||||
if (entry.localPort) releaseTunnelPort(entry.localPort);
|
if (entry.localPort) releaseTunnelPort(entry.localPort);
|
||||||
if (entry.holesail) { try { await entry.holesail.close(); } catch (_) {} }
|
if (entry.holesail) { try { await entry.holesail.close(); } catch (_) {} }
|
||||||
@@ -205,4 +229,4 @@ async function cleanupVirtualHosts() {
|
|||||||
virtualHosts.clear();
|
virtualHosts.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { init, setVirtualHost, removeVirtualHost, getVirtualHosts, getLocalPortForHostname, getLocalBackend, getVirtualHostMap, cleanupVirtualHosts, RECONNECT_BASE_MS, RECONNECT_MAX_MS };
|
module.exports = { init, setVirtualHost, setLocalVirtualHost, removeVirtualHost, getVirtualHosts, getLocalPortForHostname, getLocalBackend, getVirtualHostMap, cleanupVirtualHosts, RECONNECT_BASE_MS, RECONNECT_MAX_MS };
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ const certificateAuthority = require('../certificate-authority.js');
|
|||||||
const httpsProxy = require('../https-proxy.js');
|
const httpsProxy = require('../https-proxy.js');
|
||||||
const connectProxy = require('../connect-proxy.js');
|
const connectProxy = require('../connect-proxy.js');
|
||||||
const sshManager = require('../ssh-manager.js');
|
const sshManager = require('../ssh-manager.js');
|
||||||
|
const dashboardServer = require('../dashboard-server.js');
|
||||||
const backupManager = require('../backup-manager.js');
|
const backupManager = require('../backup-manager.js');
|
||||||
backupManager.setStoragePath(STORAGE_PATH);
|
backupManager.setStoragePath(STORAGE_PATH);
|
||||||
backupManager.setCertsPath(certificateAuthority.getCertsDir());
|
backupManager.setCertsPath(certificateAuthority.getCertsDir());
|
||||||
@@ -343,6 +344,7 @@ async function handleMessageAsync(send, msg) {
|
|||||||
|
|
||||||
function cleanup() {
|
function cleanup() {
|
||||||
if (_scheduledBackupTimer) { clearTimeout(_scheduledBackupTimer); _scheduledBackupTimer = null; }
|
if (_scheduledBackupTimer) { clearTimeout(_scheduledBackupTimer); _scheduledBackupTimer = null; }
|
||||||
|
dashboardServer.stop();
|
||||||
httpsProxy.stop(() => {});
|
httpsProxy.stop(() => {});
|
||||||
connectProxy.stop(() => {});
|
connectProxy.stop(() => {});
|
||||||
sshManager.cleanup();
|
sshManager.cleanup();
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const { log, debugLog } = require('./logger.js');
|
const { log, debugLog } = require('./logger.js');
|
||||||
|
const { BASE_DIR } = require('./paths.js');
|
||||||
|
const dashboardServer = require('../dashboard-server.js');
|
||||||
|
|
||||||
const PROXY_PORT = 8443;
|
const PROXY_PORT = 8443;
|
||||||
const CONNECT_PROXY_PORT = 8442;
|
const CONNECT_PROXY_PORT = 8442;
|
||||||
@@ -111,6 +113,18 @@ function initStartup(holesailManager, certificateAuthority, httpsProxy, connectP
|
|||||||
}
|
}
|
||||||
|
|
||||||
log('Proxies startup complete: HTTPS', savedProxyPort, 'CONNECT', connectProxy.getPort() ?? 'FAILED');
|
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(BASE_DIR);
|
||||||
|
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
|
// Assign tunnelsRestoredPromise BEFORE resolving proxiesReadyPromise so
|
||||||
// that any getState call awaiting proxiesReadyPromise immediately sees the
|
// that any getState call awaiting proxiesReadyPromise immediately sees the
|
||||||
// non-null promise rather than the one-microtask window where it is null.
|
// non-null promise rather than the one-microtask window where it is null.
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ const ROOT = path.join(__dirname, '..')
|
|||||||
const NATIVE_HOST_DIR = path.join(ROOT, 'native-host')
|
const NATIVE_HOST_DIR = path.join(ROOT, 'native-host')
|
||||||
const RELEASES_DIR = path.join(ROOT, 'releases')
|
const RELEASES_DIR = path.join(ROOT, 'releases')
|
||||||
const ENTRY = path.join(NATIVE_HOST_DIR, 'index.mjs')
|
const ENTRY = path.join(NATIVE_HOST_DIR, 'index.mjs')
|
||||||
|
const EXTENSION_DASHBOARD_DIR = path.join(ROOT, 'extension', 'dashboard')
|
||||||
|
|
||||||
const ALL_HOSTS = [
|
const ALL_HOSTS = [
|
||||||
'darwin-arm64',
|
'darwin-arm64',
|
||||||
@@ -98,6 +99,24 @@ 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.
|
* Patch bare-build's apple/sign.js to skip codesign when not on macOS.
|
||||||
* bare-build always calls `codesign --sign -` (ad-hoc) even when sign:false,
|
* bare-build always calls `codesign --sign -` (ad-hoc) even when sign:false,
|
||||||
@@ -245,6 +264,23 @@ module.exports = EventEmitter;
|
|||||||
bundle.write(eventsKey, Buffer.from(eventShim))
|
bundle.write(eventsKey, Buffer.from(eventShim))
|
||||||
console.log(' Patched events module for node-rdpjs-2 compatibility')
|
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/<rel>')
|
||||||
|
// 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')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function build(hosts, doPackage) {
|
async function build(hosts, doPackage) {
|
||||||
|
|||||||
Reference in New Issue
Block a user